|
|
|
package mocks
|
|
|
|
|
|
|
|
import (
|
|
|
|
"encoding/json"
|
|
|
|
"fmt"
|
|
|
|
"github.com/onsi/ginkgo"
|
|
|
|
"net/http"
|
|
|
|
"net/url"
|
|
|
|
"os"
|
|
|
|
"path/filepath"
|
|
|
|
"strings"
|
|
|
|
|
|
|
|
t "github.com/containrrr/watchtower/pkg/types"
|
|
|
|
|
|
|
|
"github.com/docker/docker/api/types"
|
|
|
|
"github.com/docker/docker/api/types/filters"
|
|
|
|
O "github.com/onsi/gomega"
|
|
|
|
"github.com/onsi/gomega/ghttp"
|
|
|
|
)
|
|
|
|
|
|
|
|
func getMockJSONFile(relPath string) ([]byte, error) {
|
|
|
|
absPath, _ := filepath.Abs(relPath)
|
|
|
|
buf, err := os.ReadFile(absPath)
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("mock JSON file %q not found: %e", absPath, err)
|
|
|
|
}
|
|
|
|
return buf, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// RespondWithJSONFile handles a request by returning the contents of the supplied file
|
|
|
|
func RespondWithJSONFile(relPath string, statusCode int, optionalHeader ...http.Header) http.HandlerFunc {
|
|
|
|
handler, err := respondWithJSONFile(relPath, statusCode, optionalHeader...)
|
|
|
|
O.ExpectWithOffset(1, err).ShouldNot(O.HaveOccurred())
|
|
|
|
return handler
|
|
|
|
}
|
|
|
|
|
|
|
|
func respondWithJSONFile(relPath string, statusCode int, optionalHeader ...http.Header) (http.HandlerFunc, error) {
|
|
|
|
buf, err := getMockJSONFile(relPath)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
return ghttp.RespondWith(statusCode, buf, optionalHeader...), nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// GetContainerHandlers returns the handlers serving lookups for the supplied container mock files
|
|
|
|
func GetContainerHandlers(containerRefs ...*ContainerRef) []http.HandlerFunc {
|
|
|
|
handlers := make([]http.HandlerFunc, 0, len(containerRefs)*3)
|
|
|
|
for _, containerRef := range containerRefs {
|
|
|
|
handlers = append(handlers, getContainerFileHandler(containerRef))
|
|
|
|
|
|
|
|
// Also append any containers that the container references, if any
|
|
|
|
for _, ref := range containerRef.references {
|
|
|
|
handlers = append(handlers, getContainerFileHandler(ref))
|
|
|
|
}
|
|
|
|
|
|
|
|
// Also append the image request since that will be called for every container
|
|
|
|
handlers = append(handlers, getImageHandler(containerRef.image.id,
|
|
|
|
RespondWithJSONFile(containerRef.image.getFileName(), http.StatusOK),
|
|
|
|
))
|
|
|
|
}
|
|
|
|
|
|
|
|
return handlers
|
|
|
|
}
|
|
|
|
|
|
|
|
func createFilterArgs(statuses []string) filters.Args {
|
|
|
|
args := filters.NewArgs()
|
|
|
|
for _, status := range statuses {
|
|
|
|
args.Add("status", status)
|
|
|
|
}
|
|
|
|
return args
|
|
|
|
}
|
|
|
|
|
|
|
|
var defaultImage = imageRef{
|
|
|
|
// watchtower
|
|
|
|
id: t.ImageID("sha256:4dbc5f9c07028a985e14d1393e849ea07f68804c4293050d5a641b138db72daa"),
|
|
|
|
file: "default",
|
|
|
|
}
|
|
|
|
|
|
|
|
var Watchtower = ContainerRef{
|
|
|
|
name: "watchtower",
|
|
|
|
id: "3d88e0e3543281c747d88b27e246578b65ae8964ba86c7cd7522cf84e0978134",
|
|
|
|
image: &defaultImage,
|
|
|
|
}
|
|
|
|
var Stopped = ContainerRef{
|
|
|
|
name: "stopped",
|
|
|
|
id: "ae8964ba86c7cd7522cf84e09781343d88e0e3543281c747d88b27e246578b65",
|
|
|
|
image: &defaultImage,
|
|
|
|
}
|
|
|
|
var Running = ContainerRef{
|
|
|
|
name: "running",
|
|
|
|
id: "b978af0b858aa8855cce46b628817d4ed58e58f2c4f66c9b9c5449134ed4c008",
|
|
|
|
image: &imageRef{
|
|
|
|
// portainer
|
|
|
|
id: t.ImageID("sha256:19d07168491a3f9e2798a9bed96544e34d57ddc4757a4ac5bb199dea896c87fd"),
|
|
|
|
file: "running",
|
|
|
|
},
|
|
|
|
}
|
|
|
|
var Restarting = ContainerRef{
|
|
|
|
name: "restarting",
|
|
|
|
id: "ae8964ba86c7cd7522cf84e09781343d88e0e3543281c747d88b27e246578b67",
|
|
|
|
image: &defaultImage,
|
|
|
|
}
|
|
|
|
|
|
|
|
var netSupplierOK = ContainerRef{
|
|
|
|
id: "25e75393800b5c450a6841212a3b92ed28fa35414a586dec9f2c8a520d4910c2",
|
|
|
|
name: "net_supplier",
|
|
|
|
image: &imageRef{
|
|
|
|
// gluetun
|
|
|
|
id: t.ImageID("sha256:c22b543d33bfdcb9992cbef23961677133cdf09da71d782468ae2517138bad51"),
|
|
|
|
file: "net_producer",
|
|
|
|
},
|
|
|
|
}
|
|
|
|
var netSupplierNotFound = ContainerRef{
|
|
|
|
id: NetSupplierNotFoundID,
|
|
|
|
name: netSupplierOK.name,
|
|
|
|
isMissing: true,
|
|
|
|
}
|
|
|
|
|
|
|
|
// NetConsumerOK is used for testing `container` networking mode
|
|
|
|
// returns a container that consumes an existing supplier container
|
|
|
|
var NetConsumerOK = ContainerRef{
|
|
|
|
id: "1f6b79d2aff23244382026c76f4995851322bed5f9c50631620162f6f9aafbd6",
|
|
|
|
name: "net_consumer",
|
|
|
|
image: &imageRef{
|
|
|
|
id: t.ImageID("sha256:904b8cb13b932e23230836850610fa45dce9eb0650d5618c2b1487c2a4f577b8"), // nginx
|
|
|
|
file: "net_consumer",
|
|
|
|
},
|
|
|
|
references: []*ContainerRef{&netSupplierOK},
|
|
|
|
}
|
|
|
|
|
|
|
|
// NetConsumerInvalidSupplier is used for testing `container` networking mode
|
|
|
|
// returns a container that references a supplying container that does not exist
|
|
|
|
var NetConsumerInvalidSupplier = ContainerRef{
|
|
|
|
id: NetConsumerOK.id,
|
|
|
|
name: "net_consumer-missing_supplier",
|
|
|
|
image: NetConsumerOK.image,
|
|
|
|
references: []*ContainerRef{&netSupplierNotFound},
|
|
|
|
}
|
|
|
|
|
|
|
|
const NetSupplierNotFoundID = "badc1dbadc1dbadc1dbadc1dbadc1dbadc1dbadc1dbadc1dbadc1dbadc1dbadc"
|
|
|
|
const NetSupplierContainerName = "/wt-contnet-producer-1"
|
|
|
|
|
|
|
|
func getContainerFileHandler(cr *ContainerRef) http.HandlerFunc {
|
|
|
|
|
|
|
|
if cr.isMissing {
|
|
|
|
return containerNotFoundResponse(string(cr.id))
|
|
|
|
}
|
|
|
|
|
|
|
|
containerFile, err := cr.getContainerFile()
|
|
|
|
if err != nil {
|
|
|
|
ginkgo.Fail(fmt.Sprintf("Failed to get container mock file: %v", err))
|
|
|
|
}
|
|
|
|
|
|
|
|
return getContainerHandler(
|
|
|
|
string(cr.id),
|
|
|
|
RespondWithJSONFile(containerFile, http.StatusOK),
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
func getContainerHandler(containerId string, responseHandler http.HandlerFunc) http.HandlerFunc {
|
|
|
|
return ghttp.CombineHandlers(
|
|
|
|
ghttp.VerifyRequest("GET", O.HaveSuffix("/containers/%v/json", containerId)),
|
|
|
|
responseHandler,
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
// GetContainerHandler mocks the GET containers/{id}/json endpoint
|
|
|
|
func GetContainerHandler(containerID string, containerInfo *types.ContainerJSON) http.HandlerFunc {
|
|
|
|
responseHandler := containerNotFoundResponse(containerID)
|
|
|
|
if containerInfo != nil {
|
|
|
|
responseHandler = ghttp.RespondWithJSONEncoded(http.StatusOK, containerInfo)
|
|
|
|
}
|
|
|
|
return getContainerHandler(containerID, responseHandler)
|
|
|
|
}
|
|
|
|
|
|
|
|
// GetImageHandler mocks the GET images/{id}/json endpoint
|
|
|
|
func GetImageHandler(imageInfo *types.ImageInspect) http.HandlerFunc {
|
|
|
|
return getImageHandler(t.ImageID(imageInfo.ID), ghttp.RespondWithJSONEncoded(http.StatusOK, imageInfo))
|
|
|
|
}
|
|
|
|
|
|
|
|
// ListContainersHandler mocks the GET containers/json endpoint, filtering the returned containers based on statuses
|
|
|
|
func ListContainersHandler(statuses ...string) http.HandlerFunc {
|
|
|
|
filterArgs := createFilterArgs(statuses)
|
|
|
|
bytes, err := filterArgs.MarshalJSON()
|
|
|
|
O.ExpectWithOffset(1, err).ShouldNot(O.HaveOccurred())
|
|
|
|
query := url.Values{
|
|
|
|
"filters": []string{string(bytes)},
|
|
|
|
}
|
|
|
|
return ghttp.CombineHandlers(
|
|
|
|
ghttp.VerifyRequest("GET", O.HaveSuffix("containers/json"), query.Encode()),
|
|
|
|
respondWithFilteredContainers(filterArgs),
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
func respondWithFilteredContainers(filters filters.Args) http.HandlerFunc {
|
|
|
|
containersJSON, err := getMockJSONFile("./mocks/data/containers.json")
|
|
|
|
O.ExpectWithOffset(2, err).ShouldNot(O.HaveOccurred())
|
|
|
|
var filteredContainers []types.Container
|
|
|
|
var containers []types.Container
|
|
|
|
O.ExpectWithOffset(2, json.Unmarshal(containersJSON, &containers)).To(O.Succeed())
|
|
|
|
for _, v := range containers {
|
|
|
|
for _, key := range filters.Get("status") {
|
|
|
|
if v.State == key {
|
|
|
|
filteredContainers = append(filteredContainers, v)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return ghttp.RespondWithJSONEncoded(http.StatusOK, filteredContainers)
|
|
|
|
}
|
|
|
|
|
|
|
|
func getImageHandler(imageId t.ImageID, responseHandler http.HandlerFunc) http.HandlerFunc {
|
|
|
|
return ghttp.CombineHandlers(
|
|
|
|
ghttp.VerifyRequest("GET", O.HaveSuffix("/images/%s/json", imageId)),
|
|
|
|
responseHandler,
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
// KillContainerHandler mocks the POST containers/{id}/kill endpoint
|
|
|
|
func KillContainerHandler(containerID string, found FoundStatus) http.HandlerFunc {
|
|
|
|
responseHandler := noContentStatusResponse
|
|
|
|
if !found {
|
|
|
|
responseHandler = containerNotFoundResponse(containerID)
|
|
|
|
}
|
|
|
|
return ghttp.CombineHandlers(
|
|
|
|
ghttp.VerifyRequest("POST", O.HaveSuffix("containers/%s/kill", containerID)),
|
|
|
|
responseHandler,
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
// RemoveContainerHandler mocks the DELETE containers/{id} endpoint
|
|
|
|
func RemoveContainerHandler(containerID string, found FoundStatus) http.HandlerFunc {
|
|
|
|
responseHandler := noContentStatusResponse
|
|
|
|
if !found {
|
|
|
|
responseHandler = containerNotFoundResponse(containerID)
|
|
|
|
}
|
|
|
|
return ghttp.CombineHandlers(
|
|
|
|
ghttp.VerifyRequest("DELETE", O.HaveSuffix("containers/%s", containerID)),
|
|
|
|
responseHandler,
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
func containerNotFoundResponse(containerID string) http.HandlerFunc {
|
|
|
|
return ghttp.RespondWithJSONEncoded(http.StatusNotFound, struct{ message string }{message: "No such container: " + string(containerID)})
|
|
|
|
}
|
|
|
|
|
|
|
|
var noContentStatusResponse = ghttp.RespondWith(http.StatusNoContent, nil)
|
|
|
|
|
|
|
|
type FoundStatus bool
|
|
|
|
|
|
|
|
const (
|
|
|
|
Found FoundStatus = true
|
|
|
|
Missing FoundStatus = false
|
|
|
|
)
|
|
|
|
|
|
|
|
// RemoveImageHandler mocks the DELETE images/ID endpoint, simulating removal of the given imagesWithParents
|
|
|
|
func RemoveImageHandler(imagesWithParents map[string][]string) http.HandlerFunc {
|
|
|
|
return ghttp.CombineHandlers(
|
|
|
|
ghttp.VerifyRequest("DELETE", O.MatchRegexp("/images/.*")),
|
|
|
|
func(w http.ResponseWriter, r *http.Request) {
|
|
|
|
parts := strings.Split(r.URL.Path, `/`)
|
|
|
|
image := parts[len(parts)-1]
|
|
|
|
|
|
|
|
if parents, found := imagesWithParents[image]; found {
|
|
|
|
items := []types.ImageDeleteResponseItem{
|
|
|
|
{Untagged: image},
|
|
|
|
{Deleted: image},
|
|
|
|
}
|
|
|
|
for _, parent := range parents {
|
|
|
|
items = append(items, types.ImageDeleteResponseItem{Deleted: parent})
|
|
|
|
}
|
|
|
|
ghttp.RespondWithJSONEncoded(http.StatusOK, items)(w, r)
|
|
|
|
} else {
|
|
|
|
ghttp.RespondWithJSONEncoded(http.StatusNotFound, struct{ message string }{
|
|
|
|
message: "Something went wrong.",
|
|
|
|
})(w, r)
|
|
|
|
}
|
|
|
|
},
|
|
|
|
)
|
|
|
|
}
|