mirror of
https://github.com/containrrr/watchtower.git
synced 2025-12-14 06:06:38 +01:00
refactor: move container into pkg
This commit is contained in:
parent
e109a7a6ce
commit
74ce92760c
19 changed files with 7 additions and 7 deletions
290
pkg/container/client.go
Normal file
290
pkg/container/client.go
Normal file
|
|
@ -0,0 +1,290 @@
|
|||
package container
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"time"
|
||||
|
||||
t "github.com/containrrr/watchtower/pkg/types"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/docker/docker/api/types/filters"
|
||||
"github.com/docker/docker/api/types/network"
|
||||
dockerclient "github.com/docker/docker/client"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultStopSignal = "SIGTERM"
|
||||
)
|
||||
|
||||
// A Client is the interface through which watchtower interacts with the
|
||||
// Docker API.
|
||||
type Client interface {
|
||||
ListContainers(t.Filter) ([]Container, error)
|
||||
StopContainer(Container, time.Duration) error
|
||||
StartContainer(Container) error
|
||||
RenameContainer(Container, string) error
|
||||
IsContainerStale(Container) (bool, error)
|
||||
RemoveImage(Container) error
|
||||
}
|
||||
|
||||
// NewClient returns a new Client instance which can be used to interact with
|
||||
// the Docker API.
|
||||
// The client reads its configuration from the following environment variables:
|
||||
// * DOCKER_HOST the docker-engine host to send api requests to
|
||||
// * DOCKER_TLS_VERIFY whether to verify tls certificates
|
||||
// * DOCKER_API_VERSION the minimum docker api version to work with
|
||||
func NewClient(pullImages bool, includeStopped bool, removeVolumes bool) Client {
|
||||
cli, err := dockerclient.NewClientWithOpts(dockerclient.FromEnv)
|
||||
|
||||
if err != nil {
|
||||
log.Fatalf("Error instantiating Docker client: %s", err)
|
||||
}
|
||||
|
||||
return dockerClient{
|
||||
api: cli,
|
||||
pullImages: pullImages,
|
||||
removeVolumes: removeVolumes,
|
||||
includeStopped: includeStopped,
|
||||
}
|
||||
}
|
||||
|
||||
type dockerClient struct {
|
||||
api dockerclient.CommonAPIClient
|
||||
pullImages bool
|
||||
removeVolumes bool
|
||||
includeStopped bool
|
||||
}
|
||||
|
||||
func (client dockerClient) ListContainers(fn t.Filter) ([]Container, error) {
|
||||
cs := []Container{}
|
||||
bg := context.Background()
|
||||
|
||||
if client.includeStopped {
|
||||
log.Debug("Retrieving containers including stopped and exited")
|
||||
} else {
|
||||
log.Debug("Retrieving running containers")
|
||||
}
|
||||
|
||||
filter := client.createListFilter()
|
||||
containers, err := client.api.ContainerList(
|
||||
bg,
|
||||
types.ContainerListOptions{
|
||||
Filters: filter,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, runningContainer := range containers {
|
||||
containerInfo, err := client.api.ContainerInspect(bg, runningContainer.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
imageInfo, _, err := client.api.ImageInspectWithRaw(bg, containerInfo.Image)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
c := Container{containerInfo: &containerInfo, imageInfo: &imageInfo}
|
||||
|
||||
if fn(c) {
|
||||
cs = append(cs, c)
|
||||
}
|
||||
}
|
||||
|
||||
return cs, nil
|
||||
}
|
||||
|
||||
func (client dockerClient) createListFilter() filters.Args {
|
||||
filterArgs := filters.NewArgs()
|
||||
filterArgs.Add("status", "running")
|
||||
|
||||
if client.includeStopped {
|
||||
filterArgs.Add("status", "created")
|
||||
filterArgs.Add("status", "exited")
|
||||
}
|
||||
|
||||
return filterArgs
|
||||
}
|
||||
|
||||
func (client dockerClient) StopContainer(c Container, timeout time.Duration) error {
|
||||
bg := context.Background()
|
||||
signal := c.StopSignal()
|
||||
if signal == "" {
|
||||
signal = defaultStopSignal
|
||||
}
|
||||
|
||||
if c.IsRunning() {
|
||||
log.Infof("Stopping %s (%s) with %s", c.Name(), c.ID(), signal)
|
||||
if err := client.api.ContainerKill(bg, c.ID(), signal); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Wait for container to exit, but proceed anyway after the timeout elapses
|
||||
client.waitForStop(c, timeout)
|
||||
|
||||
if c.containerInfo.HostConfig.AutoRemove {
|
||||
log.Debugf("AutoRemove container %s, skipping ContainerRemove call.", c.ID())
|
||||
} else {
|
||||
log.Debugf("Removing container %s", c.ID())
|
||||
|
||||
if err := client.api.ContainerRemove(bg, c.ID(), types.ContainerRemoveOptions{Force: true, RemoveVolumes: client.removeVolumes}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Wait for container to be removed. In this case an error is a good thing
|
||||
if err := client.waitForStop(c, timeout); err == nil {
|
||||
return fmt.Errorf("Container %s (%s) could not be removed", c.Name(), c.ID())
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (client dockerClient) StartContainer(c Container) error {
|
||||
bg := context.Background()
|
||||
config := c.runtimeConfig()
|
||||
hostConfig := c.hostConfig()
|
||||
networkConfig := &network.NetworkingConfig{EndpointsConfig: c.containerInfo.NetworkSettings.Networks}
|
||||
// simpleNetworkConfig is a networkConfig with only 1 network.
|
||||
// see: https://github.com/docker/docker/issues/29265
|
||||
simpleNetworkConfig := func() *network.NetworkingConfig {
|
||||
oneEndpoint := make(map[string]*network.EndpointSettings)
|
||||
for k, v := range networkConfig.EndpointsConfig {
|
||||
oneEndpoint[k] = v
|
||||
// we only need 1
|
||||
break
|
||||
}
|
||||
return &network.NetworkingConfig{EndpointsConfig: oneEndpoint}
|
||||
}()
|
||||
|
||||
name := c.Name()
|
||||
|
||||
log.Infof("Creating %s", name)
|
||||
creation, err := client.api.ContainerCreate(bg, config, hostConfig, simpleNetworkConfig, name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !(hostConfig.NetworkMode.IsHost()) {
|
||||
|
||||
for k := range simpleNetworkConfig.EndpointsConfig {
|
||||
err = client.api.NetworkDisconnect(bg, k, creation.ID, true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
for k, v := range networkConfig.EndpointsConfig {
|
||||
err = client.api.NetworkConnect(bg, k, creation.ID, v)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return client.startContainerIfPreviouslyRunning(bg, c, creation)
|
||||
|
||||
}
|
||||
|
||||
func (client dockerClient) startContainerIfPreviouslyRunning(bg context.Context, c Container, creation container.ContainerCreateCreatedBody) error {
|
||||
name := c.Name()
|
||||
|
||||
if !c.IsRunning() {
|
||||
return nil
|
||||
}
|
||||
|
||||
log.Debugf("Starting container %s (%s)", name, creation.ID)
|
||||
err := client.api.ContainerStart(bg, creation.ID, types.ContainerStartOptions{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (client dockerClient) RenameContainer(c Container, newName string) error {
|
||||
bg := context.Background()
|
||||
log.Debugf("Renaming container %s (%s) to %s", c.Name(), c.ID(), newName)
|
||||
return client.api.ContainerRename(bg, c.ID(), newName)
|
||||
}
|
||||
|
||||
func (client dockerClient) IsContainerStale(c Container) (bool, error) {
|
||||
bg := context.Background()
|
||||
oldImageInfo := c.imageInfo
|
||||
imageName := c.ImageName()
|
||||
|
||||
if client.pullImages {
|
||||
log.Debugf("Pulling %s for %s", imageName, c.Name())
|
||||
|
||||
var opts types.ImagePullOptions // ImagePullOptions can take a RegistryAuth arg to authenticate against a private registry
|
||||
auth, err := EncodedAuth(imageName)
|
||||
log.Debugf("Got auth value: %s", auth)
|
||||
log.Debugf("Got image name: %s", imageName)
|
||||
if err != nil {
|
||||
log.Debugf("Error loading authentication credentials %s", err)
|
||||
return false, err
|
||||
} else if auth == "" {
|
||||
log.Debugf("No authentication credentials found for %s", imageName)
|
||||
opts = types.ImagePullOptions{} // empty/no auth credentials
|
||||
} else {
|
||||
opts = types.ImagePullOptions{RegistryAuth: auth, PrivilegeFunc: DefaultAuthHandler}
|
||||
}
|
||||
|
||||
response, err := client.api.ImagePull(bg, imageName, opts)
|
||||
if err != nil {
|
||||
log.Debugf("Error pulling image %s, %s", imageName, err)
|
||||
return false, err
|
||||
}
|
||||
defer response.Close()
|
||||
|
||||
// the pull request will be aborted prematurely unless the response is read
|
||||
_, err = ioutil.ReadAll(response)
|
||||
}
|
||||
|
||||
newImageInfo, _, err := client.api.ImageInspectWithRaw(bg, imageName)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
if newImageInfo.ID != oldImageInfo.ID {
|
||||
log.Infof("Found new %s image (%s)", imageName, newImageInfo.ID)
|
||||
return true, nil
|
||||
}
|
||||
|
||||
log.Debugf("No new images found for %s", c.Name())
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func (client dockerClient) RemoveImage(c Container) error {
|
||||
imageID := c.ImageID()
|
||||
log.Infof("Removing image %s", imageID)
|
||||
_, err := client.api.ImageRemove(context.Background(), imageID, types.ImageRemoveOptions{Force: true})
|
||||
return err
|
||||
}
|
||||
|
||||
func (client dockerClient) waitForStop(c Container, waitTime time.Duration) error {
|
||||
bg := context.Background()
|
||||
timeout := time.After(waitTime)
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-timeout:
|
||||
return nil
|
||||
default:
|
||||
if ci, err := client.api.ContainerInspect(bg, c.ID()); err != nil {
|
||||
return err
|
||||
} else if !ci.State.Running {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
time.Sleep(1 * time.Second)
|
||||
}
|
||||
}
|
||||
198
pkg/container/container.go
Normal file
198
pkg/container/container.go
Normal file
|
|
@ -0,0 +1,198 @@
|
|||
package container
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/containrrr/watchtower/internal/util"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/docker/docker/api/types"
|
||||
dockercontainer "github.com/docker/docker/api/types/container"
|
||||
)
|
||||
|
||||
const (
|
||||
watchtowerLabel = "com.centurylinklabs.watchtower"
|
||||
signalLabel = "com.centurylinklabs.watchtower.stop-signal"
|
||||
enableLabel = "com.centurylinklabs.watchtower.enable"
|
||||
zodiacLabel = "com.centurylinklabs.zodiac.original-image"
|
||||
)
|
||||
|
||||
// NewContainer returns a new Container instance instantiated with the
|
||||
// specified ContainerInfo and ImageInfo structs.
|
||||
func NewContainer(containerInfo *types.ContainerJSON, imageInfo *types.ImageInspect) *Container {
|
||||
return &Container{
|
||||
containerInfo: containerInfo,
|
||||
imageInfo: imageInfo,
|
||||
}
|
||||
}
|
||||
|
||||
// Container represents a running Docker container.
|
||||
type Container struct {
|
||||
Stale bool
|
||||
|
||||
containerInfo *types.ContainerJSON
|
||||
imageInfo *types.ImageInspect
|
||||
}
|
||||
|
||||
// ID returns the Docker container ID.
|
||||
func (c Container) ID() string {
|
||||
return c.containerInfo.ID
|
||||
}
|
||||
|
||||
// IsRunning returns a boolean flag indicating whether or not the current
|
||||
// container is running. The status is determined by the value of the
|
||||
// container's "State.Running" property.
|
||||
func (c Container) IsRunning() bool {
|
||||
return c.containerInfo.State.Running
|
||||
}
|
||||
|
||||
// Name returns the Docker container name.
|
||||
func (c Container) Name() string {
|
||||
return c.containerInfo.Name
|
||||
}
|
||||
|
||||
// ImageID returns the ID of the Docker image that was used to start the
|
||||
// container.
|
||||
func (c Container) ImageID() string {
|
||||
return c.imageInfo.ID
|
||||
}
|
||||
|
||||
// ImageName returns the name of the Docker image that was used to start the
|
||||
// container. If the original image was specified without a particular tag, the
|
||||
// "latest" tag is assumed.
|
||||
func (c Container) ImageName() string {
|
||||
// Compatibility w/ Zodiac deployments
|
||||
imageName, ok := c.containerInfo.Config.Labels[zodiacLabel]
|
||||
if !ok {
|
||||
imageName = c.containerInfo.Config.Image
|
||||
}
|
||||
|
||||
if !strings.Contains(imageName, ":") {
|
||||
imageName = fmt.Sprintf("%s:latest", imageName)
|
||||
}
|
||||
|
||||
return imageName
|
||||
}
|
||||
|
||||
// Enabled returns the value of the container enabled label and if the label
|
||||
// was set.
|
||||
func (c Container) Enabled() (bool, bool) {
|
||||
rawBool, ok := c.containerInfo.Config.Labels[enableLabel]
|
||||
if !ok {
|
||||
return false, false
|
||||
}
|
||||
|
||||
parsedBool, err := strconv.ParseBool(rawBool)
|
||||
if err != nil {
|
||||
return false, false
|
||||
}
|
||||
|
||||
return parsedBool, true
|
||||
}
|
||||
|
||||
// Links returns a list containing the names of all the containers to which
|
||||
// this container is linked.
|
||||
func (c Container) Links() []string {
|
||||
var links []string
|
||||
|
||||
if (c.containerInfo != nil) && (c.containerInfo.HostConfig != nil) {
|
||||
for _, link := range c.containerInfo.HostConfig.Links {
|
||||
name := strings.Split(link, ":")[0]
|
||||
links = append(links, name)
|
||||
}
|
||||
}
|
||||
|
||||
return links
|
||||
}
|
||||
|
||||
// IsWatchtower returns a boolean flag indicating whether or not the current
|
||||
// container is the watchtower container itself. The watchtower container is
|
||||
// identified by the presence of the "com.centurylinklabs.watchtower" label in
|
||||
// the container metadata.
|
||||
func (c Container) IsWatchtower() bool {
|
||||
return ContainsWatchtowerLabel(c.containerInfo.Config.Labels)
|
||||
}
|
||||
|
||||
// StopSignal returns the custom stop signal (if any) that is encoded in the
|
||||
// container's metadata. If the container has not specified a custom stop
|
||||
// signal, the empty string "" is returned.
|
||||
func (c Container) StopSignal() string {
|
||||
if val, ok := c.containerInfo.Config.Labels[signalLabel]; ok {
|
||||
return val
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
// Ideally, we'd just be able to take the ContainerConfig from the old container
|
||||
// and use it as the starting point for creating the new container; however,
|
||||
// the ContainerConfig that comes back from the Inspect call merges the default
|
||||
// configuration (the stuff specified in the metadata for the image itself)
|
||||
// with the overridden configuration (the stuff that you might specify as part
|
||||
// of the "docker run"). In order to avoid unintentionally overriding the
|
||||
// defaults in the new image we need to separate the override options from the
|
||||
// default options. To do this we have to compare the ContainerConfig for the
|
||||
// running container with the ContainerConfig from the image that container was
|
||||
// started from. This function returns a ContainerConfig which contains just
|
||||
// the options overridden at runtime.
|
||||
func (c Container) runtimeConfig() *dockercontainer.Config {
|
||||
config := c.containerInfo.Config
|
||||
imageConfig := c.imageInfo.Config
|
||||
|
||||
if config.WorkingDir == imageConfig.WorkingDir {
|
||||
config.WorkingDir = ""
|
||||
}
|
||||
|
||||
if config.User == imageConfig.User {
|
||||
config.User = ""
|
||||
}
|
||||
|
||||
if util.SliceEqual(config.Cmd, imageConfig.Cmd) {
|
||||
config.Cmd = nil
|
||||
}
|
||||
|
||||
if util.SliceEqual(config.Entrypoint, imageConfig.Entrypoint) {
|
||||
config.Entrypoint = nil
|
||||
}
|
||||
|
||||
config.Env = util.SliceSubtract(config.Env, imageConfig.Env)
|
||||
|
||||
config.Labels = util.StringMapSubtract(config.Labels, imageConfig.Labels)
|
||||
|
||||
config.Volumes = util.StructMapSubtract(config.Volumes, imageConfig.Volumes)
|
||||
|
||||
// subtract ports exposed in image from container
|
||||
for k := range config.ExposedPorts {
|
||||
if _, ok := imageConfig.ExposedPorts[k]; ok {
|
||||
delete(config.ExposedPorts, k)
|
||||
}
|
||||
}
|
||||
for p := range c.containerInfo.HostConfig.PortBindings {
|
||||
config.ExposedPorts[p] = struct{}{}
|
||||
}
|
||||
|
||||
config.Image = c.ImageName()
|
||||
return config
|
||||
}
|
||||
|
||||
// Any links in the HostConfig need to be re-written before they can be
|
||||
// re-submitted to the Docker create API.
|
||||
func (c Container) hostConfig() *dockercontainer.HostConfig {
|
||||
hostConfig := c.containerInfo.HostConfig
|
||||
|
||||
for i, link := range hostConfig.Links {
|
||||
name := link[0:strings.Index(link, ":")]
|
||||
alias := link[strings.LastIndex(link, "/"):]
|
||||
|
||||
hostConfig.Links[i] = fmt.Sprintf("%s:%s", name, alias)
|
||||
}
|
||||
|
||||
return hostConfig
|
||||
}
|
||||
|
||||
// ContainsWatchtowerLabel takes a map of labels and values and tells
|
||||
// the consumer whether it contains a valid watchtower instance label
|
||||
func ContainsWatchtowerLabel(labels map[string]string) bool {
|
||||
val, ok := labels[watchtowerLabel]
|
||||
return ok && val == "true"
|
||||
}
|
||||
205
pkg/container/container_test.go
Normal file
205
pkg/container/container_test.go
Normal file
|
|
@ -0,0 +1,205 @@
|
|||
package container
|
||||
|
||||
import (
|
||||
"github.com/containrrr/watchtower/pkg/container/mocks"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
cli "github.com/docker/docker/client"
|
||||
. "github.com/onsi/ginkgo"
|
||||
. "github.com/onsi/gomega"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestContainer(t *testing.T) {
|
||||
RegisterFailHandler(Fail)
|
||||
RunSpecs(t, "Container Suite")
|
||||
}
|
||||
|
||||
var _ = Describe("the container", func() {
|
||||
Describe("the client", func() {
|
||||
var docker *cli.Client
|
||||
var client Client
|
||||
BeforeSuite(func() {
|
||||
server := mocks.NewMockAPIServer()
|
||||
docker, _ = cli.NewClientWithOpts(
|
||||
cli.WithHost(server.URL),
|
||||
cli.WithHTTPClient(server.Client(),
|
||||
))
|
||||
client = dockerClient{
|
||||
api: docker,
|
||||
pullImages: false,
|
||||
}
|
||||
})
|
||||
It("should return a client for the api", func() {
|
||||
Expect(client).NotTo(BeNil())
|
||||
})
|
||||
When("listing containers without any filter", func() {
|
||||
It("should return all available containers", func() {
|
||||
containers, err := client.ListContainers(noFilter)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(len(containers) == 2).To(BeTrue())
|
||||
})
|
||||
})
|
||||
When("listing containers with a filter matching nothing", func() {
|
||||
It("should return an empty array", func() {
|
||||
filter := filterByNames([]string{"lollercoaster"}, noFilter)
|
||||
containers, err := client.ListContainers(filter)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(len(containers) == 0).To(BeTrue())
|
||||
})
|
||||
})
|
||||
When("listing containers with a watchtower filter", func() {
|
||||
It("should return only the watchtower container", func() {
|
||||
containers, err := client.ListContainers(WatchtowerContainersFilter)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(len(containers) == 1).To(BeTrue())
|
||||
Expect(containers[0].ImageName()).To(Equal("containrrr/watchtower:latest"))
|
||||
})
|
||||
})
|
||||
When(`listing containers with the "include stopped" option`, func() {
|
||||
It("should return both stopped and running containers", func() {
|
||||
client = dockerClient{
|
||||
api: docker,
|
||||
pullImages: false,
|
||||
includeStopped: true,
|
||||
}
|
||||
containers, err := client.ListContainers(noFilter)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(len(containers) > 0).To(BeTrue())
|
||||
})
|
||||
})
|
||||
})
|
||||
When("asked for metadata", func() {
|
||||
var c *Container
|
||||
BeforeEach(func() {
|
||||
c = mockContainerWithLabels(map[string]string{
|
||||
"com.centurylinklabs.watchtower.enable": "true",
|
||||
"com.centurylinklabs.watchtower": "true",
|
||||
})
|
||||
})
|
||||
It("should return its name on calls to .Name()", func() {
|
||||
name := c.Name()
|
||||
Expect(name).To(Equal("test-containrrr"))
|
||||
Expect(name).NotTo(Equal("wrong-name"))
|
||||
})
|
||||
It("should return its ID on calls to .ID()", func() {
|
||||
id := c.ID()
|
||||
|
||||
Expect(id).To(Equal("container_id"))
|
||||
Expect(id).NotTo(Equal("wrong-id"))
|
||||
})
|
||||
It("should return true, true if enabled on calls to .Enabled()", func() {
|
||||
enabled, exists := c.Enabled()
|
||||
|
||||
Expect(enabled).To(BeTrue())
|
||||
Expect(enabled).NotTo(BeFalse())
|
||||
Expect(exists).To(BeTrue())
|
||||
Expect(exists).NotTo(BeFalse())
|
||||
})
|
||||
It("should return false, true if present but not true on calls to .Enabled()", func() {
|
||||
c = mockContainerWithLabels(map[string]string{"com.centurylinklabs.watchtower.enable": "false"})
|
||||
enabled, exists := c.Enabled()
|
||||
|
||||
Expect(enabled).To(BeFalse())
|
||||
Expect(enabled).NotTo(BeTrue())
|
||||
Expect(exists).To(BeTrue())
|
||||
Expect(exists).NotTo(BeFalse())
|
||||
})
|
||||
It("should return false, false if not present on calls to .Enabled()", func() {
|
||||
c = mockContainerWithLabels(map[string]string{"lol": "false"})
|
||||
enabled, exists := c.Enabled()
|
||||
|
||||
Expect(enabled).To(BeFalse())
|
||||
Expect(enabled).NotTo(BeTrue())
|
||||
Expect(exists).To(BeFalse())
|
||||
Expect(exists).NotTo(BeTrue())
|
||||
})
|
||||
It("should return false, false if present but not parsable .Enabled()", func() {
|
||||
c = mockContainerWithLabels(map[string]string{"com.centurylinklabs.watchtower.enable": "falsy"})
|
||||
enabled, exists := c.Enabled()
|
||||
|
||||
Expect(enabled).To(BeFalse())
|
||||
Expect(enabled).NotTo(BeTrue())
|
||||
Expect(exists).To(BeFalse())
|
||||
Expect(exists).NotTo(BeTrue())
|
||||
})
|
||||
When("checking if its a watchtower instance", func() {
|
||||
It("should return true if the label is set to true", func() {
|
||||
isWatchtower := c.IsWatchtower()
|
||||
Expect(isWatchtower).To(BeTrue())
|
||||
})
|
||||
It("should return false if the label is present but set to false", func() {
|
||||
c = mockContainerWithLabels(map[string]string{"com.centurylinklabs.watchtower": "false"})
|
||||
isWatchtower := c.IsWatchtower()
|
||||
Expect(isWatchtower).To(BeFalse())
|
||||
})
|
||||
It("should return false if the label is not present", func() {
|
||||
c = mockContainerWithLabels(map[string]string{"funny.label": "false"})
|
||||
isWatchtower := c.IsWatchtower()
|
||||
Expect(isWatchtower).To(BeFalse())
|
||||
})
|
||||
It("should return false if there are no labels", func() {
|
||||
c = mockContainerWithLabels(map[string]string{})
|
||||
isWatchtower := c.IsWatchtower()
|
||||
Expect(isWatchtower).To(BeFalse())
|
||||
})
|
||||
})
|
||||
When("fetching the custom stop signal", func() {
|
||||
It("should return the signal if its set", func() {
|
||||
c = mockContainerWithLabels(map[string]string{
|
||||
"com.centurylinklabs.watchtower.stop-signal": "SIGKILL",
|
||||
})
|
||||
stopSignal := c.StopSignal()
|
||||
Expect(stopSignal).To(Equal("SIGKILL"))
|
||||
})
|
||||
It("should return an empty string if its not set", func() {
|
||||
c = mockContainerWithLabels(map[string]string{})
|
||||
stopSignal := c.StopSignal()
|
||||
Expect(stopSignal).To(Equal(""))
|
||||
})
|
||||
})
|
||||
When("fetching the image name", func() {
|
||||
When("the zodiac label is present", func() {
|
||||
It("should fetch the image name from it", func() {
|
||||
c = mockContainerWithLabels(map[string]string{
|
||||
"com.centurylinklabs.zodiac.original-image": "the-original-image",
|
||||
})
|
||||
imageName := c.ImageName()
|
||||
Expect(imageName).To(Equal(imageName))
|
||||
})
|
||||
})
|
||||
It("should return the image name", func() {
|
||||
name := "image-name:3"
|
||||
c = mockContainerWithImageName(name)
|
||||
imageName := c.ImageName()
|
||||
Expect(imageName).To(Equal(name))
|
||||
})
|
||||
It("should assume latest if no tag is supplied", func() {
|
||||
name := "image-name"
|
||||
c = mockContainerWithImageName(name)
|
||||
imageName := c.ImageName()
|
||||
Expect(imageName).To(Equal(name + ":latest"))
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
func mockContainerWithImageName(name string) *Container {
|
||||
container := mockContainerWithLabels(nil)
|
||||
container.containerInfo.Config.Image = name
|
||||
return container
|
||||
}
|
||||
|
||||
func mockContainerWithLabels(labels map[string]string) *Container {
|
||||
content := types.ContainerJSON{
|
||||
ContainerJSONBase: &types.ContainerJSONBase{
|
||||
ID: "container_id",
|
||||
Image: "image",
|
||||
Name: "test-containrrr",
|
||||
},
|
||||
Config: &container.Config{
|
||||
Labels: labels,
|
||||
},
|
||||
}
|
||||
return NewContainer(&content, nil)
|
||||
}
|
||||
65
pkg/container/filters.go
Normal file
65
pkg/container/filters.go
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
package container
|
||||
|
||||
import t "github.com/containrrr/watchtower/pkg/types"
|
||||
|
||||
// WatchtowerContainersFilter filters only watchtower containers
|
||||
func WatchtowerContainersFilter(c t.FilterableContainer) bool { return c.IsWatchtower() }
|
||||
|
||||
// Filter no containers and returns all
|
||||
func noFilter(t.FilterableContainer) bool { return true }
|
||||
|
||||
// Filters containers which don't have a specified name
|
||||
func filterByNames(names []string, baseFilter t.Filter) t.Filter {
|
||||
if len(names) == 0 {
|
||||
return baseFilter
|
||||
}
|
||||
|
||||
return func(c t.FilterableContainer) bool {
|
||||
for _, name := range names {
|
||||
if (name == c.Name()) || (name == c.Name()[1:]) {
|
||||
return baseFilter(c)
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// Filters out containers that don't have the 'enableLabel'
|
||||
func filterByEnableLabel(baseFilter t.Filter) t.Filter {
|
||||
return func(c t.FilterableContainer) bool {
|
||||
// If label filtering is enabled, containers should only be considered
|
||||
// if the label is specifically set.
|
||||
_, ok := c.Enabled()
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
|
||||
return baseFilter(c)
|
||||
}
|
||||
}
|
||||
|
||||
// Filters out containers that have a 'enableLabel' and is set to disable.
|
||||
func filterByDisabledLabel(baseFilter t.Filter) t.Filter {
|
||||
return func(c t.FilterableContainer) bool {
|
||||
enabledLabel, ok := c.Enabled()
|
||||
if ok && !enabledLabel {
|
||||
// If the label has been set and it demands a disable
|
||||
return false
|
||||
}
|
||||
|
||||
return baseFilter(c)
|
||||
}
|
||||
}
|
||||
|
||||
// BuildFilter creates the needed filter of containers
|
||||
func BuildFilter(names []string, enableLabel bool) t.Filter {
|
||||
filter := noFilter
|
||||
filter = filterByNames(names, filter)
|
||||
if enableLabel {
|
||||
// If label filtering is enabled, containers should only be considered
|
||||
// if the label is specifically set.
|
||||
filter = filterByEnableLabel(filter)
|
||||
}
|
||||
filter = filterByDisabledLabel(filter)
|
||||
return filter
|
||||
}
|
||||
153
pkg/container/filters_test.go
Normal file
153
pkg/container/filters_test.go
Normal file
|
|
@ -0,0 +1,153 @@
|
|||
package container
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/containrrr/watchtower/pkg/container/mocks"
|
||||
)
|
||||
|
||||
func TestWatchtowerContainersFilter(t *testing.T) {
|
||||
container := new(mocks.FilterableContainer)
|
||||
|
||||
container.On("IsWatchtower").Return(true)
|
||||
|
||||
assert.True(t, WatchtowerContainersFilter(container))
|
||||
|
||||
container.AssertExpectations(t)
|
||||
}
|
||||
|
||||
func TestNoFilter(t *testing.T) {
|
||||
container := new(mocks.FilterableContainer)
|
||||
|
||||
assert.True(t, noFilter(container))
|
||||
|
||||
container.AssertExpectations(t)
|
||||
}
|
||||
|
||||
func TestFilterByNames(t *testing.T) {
|
||||
var names []string
|
||||
|
||||
filter := filterByNames(names, nil)
|
||||
assert.Nil(t, filter)
|
||||
|
||||
names = append(names, "test")
|
||||
|
||||
filter = filterByNames(names, noFilter)
|
||||
assert.NotNil(t, filter)
|
||||
|
||||
container := new(mocks.FilterableContainer)
|
||||
container.On("Name").Return("test")
|
||||
assert.True(t, filter(container))
|
||||
container.AssertExpectations(t)
|
||||
|
||||
container = new(mocks.FilterableContainer)
|
||||
container.On("Name").Return("NoTest")
|
||||
assert.False(t, filter(container))
|
||||
container.AssertExpectations(t)
|
||||
}
|
||||
|
||||
func TestFilterByEnableLabel(t *testing.T) {
|
||||
filter := filterByEnableLabel(noFilter)
|
||||
assert.NotNil(t, filter)
|
||||
|
||||
container := new(mocks.FilterableContainer)
|
||||
container.On("Enabled").Return(true, true)
|
||||
assert.True(t, filter(container))
|
||||
container.AssertExpectations(t)
|
||||
|
||||
container = new(mocks.FilterableContainer)
|
||||
container.On("Enabled").Return(false, true)
|
||||
assert.True(t, filter(container))
|
||||
container.AssertExpectations(t)
|
||||
|
||||
container = new(mocks.FilterableContainer)
|
||||
container.On("Enabled").Return(false, false)
|
||||
assert.False(t, filter(container))
|
||||
container.AssertExpectations(t)
|
||||
}
|
||||
|
||||
func TestFilterByDisabledLabel(t *testing.T) {
|
||||
filter := filterByDisabledLabel(noFilter)
|
||||
assert.NotNil(t, filter)
|
||||
|
||||
container := new(mocks.FilterableContainer)
|
||||
container.On("Enabled").Return(true, true)
|
||||
assert.True(t, filter(container))
|
||||
container.AssertExpectations(t)
|
||||
|
||||
container = new(mocks.FilterableContainer)
|
||||
container.On("Enabled").Return(false, true)
|
||||
assert.False(t, filter(container))
|
||||
container.AssertExpectations(t)
|
||||
|
||||
container = new(mocks.FilterableContainer)
|
||||
container.On("Enabled").Return(false, false)
|
||||
assert.True(t, filter(container))
|
||||
container.AssertExpectations(t)
|
||||
}
|
||||
|
||||
func TestBuildFilter(t *testing.T) {
|
||||
var names []string
|
||||
names = append(names, "test")
|
||||
|
||||
filter := BuildFilter(names, false)
|
||||
|
||||
container := new(mocks.FilterableContainer)
|
||||
container.On("Name").Return("Invalid")
|
||||
container.On("Enabled").Return(false, false)
|
||||
assert.False(t, filter(container))
|
||||
container.AssertExpectations(t)
|
||||
|
||||
container = new(mocks.FilterableContainer)
|
||||
container.On("Name").Return("test")
|
||||
container.On("Enabled").Return(false, false)
|
||||
assert.True(t, filter(container))
|
||||
container.AssertExpectations(t)
|
||||
|
||||
container = new(mocks.FilterableContainer)
|
||||
container.On("Name").Return("Invalid")
|
||||
container.On("Enabled").Return(true, true)
|
||||
assert.False(t, filter(container))
|
||||
container.AssertExpectations(t)
|
||||
|
||||
container = new(mocks.FilterableContainer)
|
||||
container.On("Name").Return("test")
|
||||
container.On("Enabled").Return(true, true)
|
||||
assert.True(t, filter(container))
|
||||
container.AssertExpectations(t)
|
||||
|
||||
container = new(mocks.FilterableContainer)
|
||||
container.On("Enabled").Return(false, true)
|
||||
assert.False(t, filter(container))
|
||||
container.AssertExpectations(t)
|
||||
}
|
||||
|
||||
func TestBuildFilterEnableLabel(t *testing.T) {
|
||||
var names []string
|
||||
names = append(names, "test")
|
||||
|
||||
filter := BuildFilter(names, true)
|
||||
|
||||
container := new(mocks.FilterableContainer)
|
||||
container.On("Enabled").Return(false, false)
|
||||
assert.False(t, filter(container))
|
||||
container.AssertExpectations(t)
|
||||
|
||||
container = new(mocks.FilterableContainer)
|
||||
container.On("Name").Return("Invalid")
|
||||
container.On("Enabled").Twice().Return(true, true)
|
||||
assert.False(t, filter(container))
|
||||
container.AssertExpectations(t)
|
||||
|
||||
container = new(mocks.FilterableContainer)
|
||||
container.On("Name").Return("test")
|
||||
container.On("Enabled").Twice().Return(true, true)
|
||||
assert.True(t, filter(container))
|
||||
container.AssertExpectations(t)
|
||||
|
||||
container = new(mocks.FilterableContainer)
|
||||
container.On("Enabled").Return(false, true)
|
||||
assert.False(t, filter(container))
|
||||
container.AssertExpectations(t)
|
||||
}
|
||||
54
pkg/container/mocks/ApiServer.go
Normal file
54
pkg/container/mocks/ApiServer.go
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
package mocks
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/sirupsen/logrus"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// NewMockAPIServer returns a mocked docker api server that responds to some fixed requests
|
||||
// used in the test suite.
|
||||
func NewMockAPIServer() *httptest.Server {
|
||||
return httptest.NewServer(http.HandlerFunc(
|
||||
func(w http.ResponseWriter, r *http.Request) {
|
||||
logrus.Debug("Mock server has received a HTTP call on ", r.URL)
|
||||
var response = ""
|
||||
|
||||
if isRequestFor("filters=%7B%22status%22%3A%7B%22running%22%3Atrue%7D%7D&limit=0", r) {
|
||||
response = getMockJSONFromDisk("./mocks/data/containers.json")
|
||||
} else if isRequestFor("filters=%7B%22status%22%3A%7B%22created%22%3Atrue%2C%22exited%22%3Atrue%2C%22running%22%3Atrue%7D%7D&limit=0", r) {
|
||||
response = getMockJSONFromDisk("./mocks/data/containers.json")
|
||||
} else if isRequestFor("containers/json?limit=0", r) {
|
||||
response = getMockJSONFromDisk("./mocks/data/containers.json")
|
||||
} else if isRequestFor("ae8964ba86c7cd7522cf84e09781343d88e0e3543281c747d88b27e246578b65", r) {
|
||||
response = getMockJSONFromDisk("./mocks/data/container_stopped.json")
|
||||
} else if isRequestFor("b978af0b858aa8855cce46b628817d4ed58e58f2c4f66c9b9c5449134ed4c008", r) {
|
||||
response = getMockJSONFromDisk("./mocks/data/container_running.json")
|
||||
} else if isRequestFor("sha256:19d07168491a3f9e2798a9bed96544e34d57ddc4757a4ac5bb199dea896c87fd", r) {
|
||||
response = getMockJSONFromDisk("./mocks/data/image01.json")
|
||||
} else if isRequestFor("sha256:4dbc5f9c07028a985e14d1393e849ea07f68804c4293050d5a641b138db72daa", r) {
|
||||
response = getMockJSONFromDisk("./mocks/data/image02.json")
|
||||
}
|
||||
fmt.Fprintln(w, response)
|
||||
},
|
||||
))
|
||||
}
|
||||
|
||||
func isRequestFor(urlPart string, r *http.Request) bool {
|
||||
return strings.Contains(r.URL.String(), urlPart)
|
||||
}
|
||||
|
||||
func getMockJSONFromDisk(relPath string) string {
|
||||
absPath, _ := filepath.Abs(relPath)
|
||||
logrus.Error(absPath)
|
||||
buf, err := ioutil.ReadFile(absPath)
|
||||
if err != nil {
|
||||
logrus.Error(err)
|
||||
return ""
|
||||
}
|
||||
return string(buf)
|
||||
}
|
||||
57
pkg/container/mocks/FilterableContainer.go
Normal file
57
pkg/container/mocks/FilterableContainer.go
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
package mocks
|
||||
|
||||
import mock "github.com/stretchr/testify/mock"
|
||||
|
||||
// FilterableContainer is an autogenerated mock type for the FilterableContainer type
|
||||
type FilterableContainer struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
// Enabled provides a mock function with given fields:
|
||||
func (_m *FilterableContainer) Enabled() (bool, bool) {
|
||||
ret := _m.Called()
|
||||
|
||||
var r0 bool
|
||||
if rf, ok := ret.Get(0).(func() bool); ok {
|
||||
r0 = rf()
|
||||
} else {
|
||||
r0 = ret.Get(0).(bool)
|
||||
}
|
||||
|
||||
var r1 bool
|
||||
if rf, ok := ret.Get(1).(func() bool); ok {
|
||||
r1 = rf()
|
||||
} else {
|
||||
r1 = ret.Get(1).(bool)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// IsWatchtower provides a mock function with given fields:
|
||||
func (_m *FilterableContainer) IsWatchtower() bool {
|
||||
ret := _m.Called()
|
||||
|
||||
var r0 bool
|
||||
if rf, ok := ret.Get(0).(func() bool); ok {
|
||||
r0 = rf()
|
||||
} else {
|
||||
r0 = ret.Get(0).(bool)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// Name provides a mock function with given fields:
|
||||
func (_m *FilterableContainer) Name() string {
|
||||
ret := _m.Called()
|
||||
|
||||
var r0 string
|
||||
if rf, ok := ret.Get(0).(func() string); ok {
|
||||
r0 = rf()
|
||||
} else {
|
||||
r0 = ret.Get(0).(string)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
233
pkg/container/mocks/data/container_running.json
Normal file
233
pkg/container/mocks/data/container_running.json
Normal file
|
|
@ -0,0 +1,233 @@
|
|||
{
|
||||
"Id": "b978af0b858aa8855cce46b628817d4ed58e58f2c4f66c9b9c5449134ed4c008",
|
||||
"Created": "2019-04-04T20:28:32.5710901Z",
|
||||
"Path": "/portainer",
|
||||
"Args": [],
|
||||
"State": {
|
||||
"Status": "running",
|
||||
"Running": true,
|
||||
"Paused": false,
|
||||
"Restarting": false,
|
||||
"OOMKilled": false,
|
||||
"Dead": false,
|
||||
"Pid": 3854,
|
||||
"ExitCode": 0,
|
||||
"Error": "",
|
||||
"StartedAt": "2019-04-13T22:38:24.498745809Z",
|
||||
"FinishedAt": "2019-04-13T22:38:18.486292076Z"
|
||||
},
|
||||
"Image": "sha256:19d07168491a3f9e2798a9bed96544e34d57ddc4757a4ac5bb199dea896c87fd",
|
||||
"ResolvConfPath": "/var/lib/docker/containers/b978af0b858aa8855cce46b628817d4ed58e58f2c4f66c9b9c5449134ed4c008/resolv.conf",
|
||||
"HostnamePath": "/var/lib/docker/containers/b978af0b858aa8855cce46b628817d4ed58e58f2c4f66c9b9c5449134ed4c008/hostname",
|
||||
"HostsPath": "/var/lib/docker/containers/b978af0b858aa8855cce46b628817d4ed58e58f2c4f66c9b9c5449134ed4c008/hosts",
|
||||
"LogPath": "/var/lib/docker/containers/b978af0b858aa8855cce46b628817d4ed58e58f2c4f66c9b9c5449134ed4c008/b978af0b858aa8855cce46b628817d4ed58e58f2c4f66c9b9c5449134ed4c008-json.log",
|
||||
"Name": "/portainer",
|
||||
"RestartCount": 0,
|
||||
"Driver": "overlay2",
|
||||
"Platform": "linux",
|
||||
"MountLabel": "",
|
||||
"ProcessLabel": "",
|
||||
"AppArmorProfile": "",
|
||||
"ExecIDs": null,
|
||||
"HostConfig": {
|
||||
"Binds": [
|
||||
"portainer_data:/data",
|
||||
"/var/run/docker.sock:/var/run/docker.sock"
|
||||
],
|
||||
"ContainerIDFile": "",
|
||||
"LogConfig": {
|
||||
"Type": "json-file",
|
||||
"Config": {}
|
||||
},
|
||||
"NetworkMode": "default",
|
||||
"PortBindings": {
|
||||
"9000/tcp": [
|
||||
{
|
||||
"HostIp": "",
|
||||
"HostPort": "9000"
|
||||
}
|
||||
]
|
||||
},
|
||||
"RestartPolicy": {
|
||||
"Name": "always",
|
||||
"MaximumRetryCount": 0
|
||||
},
|
||||
"AutoRemove": false,
|
||||
"VolumeDriver": "",
|
||||
"VolumesFrom": null,
|
||||
"CapAdd": null,
|
||||
"CapDrop": null,
|
||||
"Dns": [],
|
||||
"DnsOptions": [],
|
||||
"DnsSearch": [],
|
||||
"ExtraHosts": null,
|
||||
"GroupAdd": null,
|
||||
"IpcMode": "shareable",
|
||||
"Cgroup": "",
|
||||
"Links": null,
|
||||
"OomScoreAdj": 0,
|
||||
"PidMode": "",
|
||||
"Privileged": false,
|
||||
"PublishAllPorts": false,
|
||||
"ReadonlyRootfs": false,
|
||||
"SecurityOpt": null,
|
||||
"UTSMode": "",
|
||||
"UsernsMode": "",
|
||||
"ShmSize": 67108864,
|
||||
"Runtime": "runc",
|
||||
"ConsoleSize": [
|
||||
0,
|
||||
0
|
||||
],
|
||||
"Isolation": "",
|
||||
"CpuShares": 0,
|
||||
"Memory": 0,
|
||||
"NanoCpus": 0,
|
||||
"CgroupParent": "",
|
||||
"BlkioWeight": 0,
|
||||
"BlkioWeightDevice": [],
|
||||
"BlkioDeviceReadBps": null,
|
||||
"BlkioDeviceWriteBps": null,
|
||||
"BlkioDeviceReadIOps": null,
|
||||
"BlkioDeviceWriteIOps": null,
|
||||
"CpuPeriod": 0,
|
||||
"CpuQuota": 0,
|
||||
"CpuRealtimePeriod": 0,
|
||||
"CpuRealtimeRuntime": 0,
|
||||
"CpusetCpus": "",
|
||||
"CpusetMems": "",
|
||||
"Devices": [],
|
||||
"DeviceCgroupRules": null,
|
||||
"DiskQuota": 0,
|
||||
"KernelMemory": 0,
|
||||
"MemoryReservation": 0,
|
||||
"MemorySwap": 0,
|
||||
"MemorySwappiness": null,
|
||||
"OomKillDisable": false,
|
||||
"PidsLimit": 0,
|
||||
"Ulimits": null,
|
||||
"CpuCount": 0,
|
||||
"CpuPercent": 0,
|
||||
"IOMaximumIOps": 0,
|
||||
"IOMaximumBandwidth": 0,
|
||||
"MaskedPaths": [
|
||||
"/proc/asound",
|
||||
"/proc/acpi",
|
||||
"/proc/kcore",
|
||||
"/proc/keys",
|
||||
"/proc/latency_stats",
|
||||
"/proc/timer_list",
|
||||
"/proc/timer_stats",
|
||||
"/proc/sched_debug",
|
||||
"/proc/scsi",
|
||||
"/sys/firmware"
|
||||
],
|
||||
"ReadonlyPaths": [
|
||||
"/proc/bus",
|
||||
"/proc/fs",
|
||||
"/proc/irq",
|
||||
"/proc/sys",
|
||||
"/proc/sysrq-trigger"
|
||||
]
|
||||
},
|
||||
"GraphDriver": {
|
||||
"Data": {
|
||||
"LowerDir": "/var/lib/docker/overlay2/99dedacb757cd8c70ccacbc4b57dd85cb34b1b6fcfd2fd1176332ce5dfa1d38c-init/diff:/var/lib/docker/overlay2/2e0c03c2476f5b4df855cb8b02a88f76d336d7e0becc3e5193906aaa760687fd/diff:/var/lib/docker/overlay2/6c3f44131f6f13c9ea1a99a1b24bf348f70ba3eef244f29202faef3a2216ac11/diff",
|
||||
"MergedDir": "/var/lib/docker/overlay2/99dedacb757cd8c70ccacbc4b57dd85cb34b1b6fcfd2fd1176332ce5dfa1d38c/merged",
|
||||
"UpperDir": "/var/lib/docker/overlay2/99dedacb757cd8c70ccacbc4b57dd85cb34b1b6fcfd2fd1176332ce5dfa1d38c/diff",
|
||||
"WorkDir": "/var/lib/docker/overlay2/99dedacb757cd8c70ccacbc4b57dd85cb34b1b6fcfd2fd1176332ce5dfa1d38c/work"
|
||||
},
|
||||
"Name": "overlay2"
|
||||
},
|
||||
"Mounts": [
|
||||
{
|
||||
"Type": "volume",
|
||||
"Name": "portainer_data",
|
||||
"Source": "/var/lib/docker/volumes/portainer_data/_data",
|
||||
"Destination": "/data",
|
||||
"Driver": "local",
|
||||
"Mode": "z",
|
||||
"RW": true,
|
||||
"Propagation": ""
|
||||
},
|
||||
{
|
||||
"Type": "bind",
|
||||
"Source": "/var/run/docker.sock",
|
||||
"Destination": "/var/run/docker.sock",
|
||||
"Mode": "",
|
||||
"RW": true,
|
||||
"Propagation": "rprivate"
|
||||
}
|
||||
],
|
||||
"Config": {
|
||||
"Hostname": "822f0f2efd78",
|
||||
"Domainname": "",
|
||||
"User": "",
|
||||
"AttachStdin": false,
|
||||
"AttachStdout": false,
|
||||
"AttachStderr": false,
|
||||
"ExposedPorts": {
|
||||
"9000/tcp": {}
|
||||
},
|
||||
"Tty": false,
|
||||
"OpenStdin": false,
|
||||
"StdinOnce": false,
|
||||
"Env": [
|
||||
"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
|
||||
],
|
||||
"Cmd": null,
|
||||
"Image": "portainer/portainer:latest",
|
||||
"Volumes": {
|
||||
"/data": {}
|
||||
},
|
||||
"WorkingDir": "/",
|
||||
"Entrypoint": [
|
||||
"/portainer"
|
||||
],
|
||||
"OnBuild": null,
|
||||
"Labels": {}
|
||||
},
|
||||
"NetworkSettings": {
|
||||
"Bridge": "",
|
||||
"SandboxID": "8819e19588be798020f2d09e36a577c39a47809e68c2769a1525880c0bcd5b11",
|
||||
"HairpinMode": false,
|
||||
"LinkLocalIPv6Address": "",
|
||||
"LinkLocalIPv6PrefixLen": 0,
|
||||
"Ports": {
|
||||
"9000/tcp": [
|
||||
{
|
||||
"HostIp": "0.0.0.0",
|
||||
"HostPort": "9000"
|
||||
}
|
||||
]
|
||||
},
|
||||
"SandboxKey": "/var/run/docker/netns/8819e19588be",
|
||||
"SecondaryIPAddresses": null,
|
||||
"SecondaryIPv6Addresses": null,
|
||||
"EndpointID": "a8bcd737f27edb4d2955f7bce0c777bb2990b792a6b335b0727387624abe0702",
|
||||
"Gateway": "172.17.0.1",
|
||||
"GlobalIPv6Address": "",
|
||||
"GlobalIPv6PrefixLen": 0,
|
||||
"IPAddress": "172.17.0.2",
|
||||
"IPPrefixLen": 16,
|
||||
"IPv6Gateway": "",
|
||||
"MacAddress": "02:42:ac:11:00:02",
|
||||
"Networks": {
|
||||
"bridge": {
|
||||
"IPAMConfig": null,
|
||||
"Links": null,
|
||||
"Aliases": null,
|
||||
"NetworkID": "9352796e0330dcf31ce3d44fae4b719304b8b3fd97b02ade3aefb8737251682b",
|
||||
"EndpointID": "a8bcd737f27edb4d2955f7bce0c777bb2990b792a6b335b0727387624abe0702",
|
||||
"Gateway": "172.17.0.1",
|
||||
"IPAddress": "172.17.0.2",
|
||||
"IPPrefixLen": 16,
|
||||
"IPv6Gateway": "",
|
||||
"GlobalIPv6Address": "",
|
||||
"GlobalIPv6PrefixLen": 0,
|
||||
"MacAddress": "02:42:ac:11:00:02",
|
||||
"DriverOpts": null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
205
pkg/container/mocks/data/container_stopped.json
Normal file
205
pkg/container/mocks/data/container_stopped.json
Normal file
|
|
@ -0,0 +1,205 @@
|
|||
{
|
||||
"Id": "ae8964ba86c7cd7522cf84e09781343d88e0e3543281c747d88b27e246578b65",
|
||||
"Created": "2019-04-10T19:51:22.245041005Z",
|
||||
"Path": "/watchtower",
|
||||
"Args": [],
|
||||
"State": {
|
||||
"Status": "exited",
|
||||
"Running": false,
|
||||
"Paused": false,
|
||||
"Restarting": false,
|
||||
"OOMKilled": false,
|
||||
"Dead": false,
|
||||
"Pid": 0,
|
||||
"ExitCode": 1,
|
||||
"Error": "",
|
||||
"StartedAt": "2019-04-10T19:51:22.918972606Z",
|
||||
"FinishedAt": "2019-04-10T19:52:14.265091583Z"
|
||||
},
|
||||
"Image": "sha256:4dbc5f9c07028a985e14d1393e849ea07f68804c4293050d5a641b138db72daa",
|
||||
"ResolvConfPath": "/var/lib/docker/containers/ae8964ba86c7cd7522cf84e09781343d88e0e3543281c747d88b27e246578b65/resolv.conf",
|
||||
"HostnamePath": "/var/lib/docker/containers/ae8964ba86c7cd7522cf84e09781343d88e0e3543281c747d88b27e246578b65/hostname",
|
||||
"HostsPath": "/var/lib/docker/containers/ae8964ba86c7cd7522cf84e09781343d88e0e3543281c747d88b27e246578b65/hosts",
|
||||
"LogPath": "/var/lib/docker/containers/ae8964ba86c7cd7522cf84e09781343d88e0e3543281c747d88b27e246578b65/ae8964ba86c7cd7522cf84e09781343d88e0e3543281c747d88b27e246578b65-json.log",
|
||||
"Name": "/watchtower-test",
|
||||
"RestartCount": 0,
|
||||
"Driver": "overlay2",
|
||||
"Platform": "linux",
|
||||
"MountLabel": "",
|
||||
"ProcessLabel": "",
|
||||
"AppArmorProfile": "",
|
||||
"ExecIDs": null,
|
||||
"HostConfig": {
|
||||
"Binds": [
|
||||
"/var/run/docker.sock:/var/run/docker.sock"
|
||||
],
|
||||
"ContainerIDFile": "",
|
||||
"LogConfig": {
|
||||
"Type": "json-file",
|
||||
"Config": {}
|
||||
},
|
||||
"NetworkMode": "default",
|
||||
"PortBindings": {},
|
||||
"RestartPolicy": {
|
||||
"Name": "no",
|
||||
"MaximumRetryCount": 0
|
||||
},
|
||||
"AutoRemove": false,
|
||||
"VolumeDriver": "",
|
||||
"VolumesFrom": null,
|
||||
"CapAdd": null,
|
||||
"CapDrop": null,
|
||||
"Dns": [],
|
||||
"DnsOptions": [],
|
||||
"DnsSearch": [],
|
||||
"ExtraHosts": null,
|
||||
"GroupAdd": null,
|
||||
"IpcMode": "shareable",
|
||||
"Cgroup": "",
|
||||
"Links": null,
|
||||
"OomScoreAdj": 0,
|
||||
"PidMode": "",
|
||||
"Privileged": false,
|
||||
"PublishAllPorts": false,
|
||||
"ReadonlyRootfs": false,
|
||||
"SecurityOpt": null,
|
||||
"UTSMode": "",
|
||||
"UsernsMode": "",
|
||||
"ShmSize": 67108864,
|
||||
"Runtime": "runc",
|
||||
"ConsoleSize": [
|
||||
0,
|
||||
0
|
||||
],
|
||||
"Isolation": "",
|
||||
"CpuShares": 0,
|
||||
"Memory": 0,
|
||||
"NanoCpus": 0,
|
||||
"CgroupParent": "",
|
||||
"BlkioWeight": 0,
|
||||
"BlkioWeightDevice": [],
|
||||
"BlkioDeviceReadBps": null,
|
||||
"BlkioDeviceWriteBps": null,
|
||||
"BlkioDeviceReadIOps": null,
|
||||
"BlkioDeviceWriteIOps": null,
|
||||
"CpuPeriod": 0,
|
||||
"CpuQuota": 0,
|
||||
"CpuRealtimePeriod": 0,
|
||||
"CpuRealtimeRuntime": 0,
|
||||
"CpusetCpus": "",
|
||||
"CpusetMems": "",
|
||||
"Devices": [],
|
||||
"DeviceCgroupRules": null,
|
||||
"DiskQuota": 0,
|
||||
"KernelMemory": 0,
|
||||
"MemoryReservation": 0,
|
||||
"MemorySwap": 0,
|
||||
"MemorySwappiness": null,
|
||||
"OomKillDisable": false,
|
||||
"PidsLimit": 0,
|
||||
"Ulimits": null,
|
||||
"CpuCount": 0,
|
||||
"CpuPercent": 0,
|
||||
"IOMaximumIOps": 0,
|
||||
"IOMaximumBandwidth": 0,
|
||||
"MaskedPaths": [
|
||||
"/proc/asound",
|
||||
"/proc/acpi",
|
||||
"/proc/kcore",
|
||||
"/proc/keys",
|
||||
"/proc/latency_stats",
|
||||
"/proc/timer_list",
|
||||
"/proc/timer_stats",
|
||||
"/proc/sched_debug",
|
||||
"/proc/scsi",
|
||||
"/sys/firmware"
|
||||
],
|
||||
"ReadonlyPaths": [
|
||||
"/proc/bus",
|
||||
"/proc/fs",
|
||||
"/proc/irq",
|
||||
"/proc/sys",
|
||||
"/proc/sysrq-trigger"
|
||||
]
|
||||
},
|
||||
"GraphDriver": {
|
||||
"Data": {
|
||||
"LowerDir": "/var/lib/docker/overlay2/9f6b91ea6e142835035d91123bbc7a05224dfa2abd4d020eac42f2ab420ccddc-init/diff:/var/lib/docker/overlay2/cdf82f50bc49177d0c17c24f3eaa29eba607b70cc6a081f77781b21c59a13eb8/diff:/var/lib/docker/overlay2/8108325ee844603c9b08d2772cf6e65dccf31dd5171f265078e5ed79a0ba3c0f/diff:/var/lib/docker/overlay2/e5e0cce6bf91b829a308424d99d7e56a33be3a11414ff5cdc48e762a1342b20f/diff",
|
||||
"MergedDir": "/var/lib/docker/overlay2/9f6b91ea6e142835035d91123bbc7a05224dfa2abd4d020eac42f2ab420ccddc/merged",
|
||||
"UpperDir": "/var/lib/docker/overlay2/9f6b91ea6e142835035d91123bbc7a05224dfa2abd4d020eac42f2ab420ccddc/diff",
|
||||
"WorkDir": "/var/lib/docker/overlay2/9f6b91ea6e142835035d91123bbc7a05224dfa2abd4d020eac42f2ab420ccddc/work"
|
||||
},
|
||||
"Name": "overlay2"
|
||||
},
|
||||
"Mounts": [
|
||||
{
|
||||
"Type": "bind",
|
||||
"Source": "/var/run/docker.sock",
|
||||
"Destination": "/var/run/docker.sock",
|
||||
"Mode": "",
|
||||
"RW": true,
|
||||
"Propagation": "rprivate"
|
||||
}
|
||||
],
|
||||
"Config": {
|
||||
"Hostname": "ae8964ba86c7",
|
||||
"Domainname": "",
|
||||
"User": "",
|
||||
"AttachStdin": false,
|
||||
"AttachStdout": true,
|
||||
"AttachStderr": true,
|
||||
"Tty": false,
|
||||
"OpenStdin": false,
|
||||
"StdinOnce": false,
|
||||
"Env": [
|
||||
"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
|
||||
],
|
||||
"Cmd": null,
|
||||
"Image": "containrrr/watchtower:latest",
|
||||
"Volumes": null,
|
||||
"WorkingDir": "",
|
||||
"Entrypoint": [
|
||||
"/watchtower"
|
||||
],
|
||||
"OnBuild": null,
|
||||
"Labels": {
|
||||
"com.centurylinklabs.watchtower": "true"
|
||||
}
|
||||
},
|
||||
"NetworkSettings": {
|
||||
"Bridge": "",
|
||||
"SandboxID": "05627d36c08ed994eebc44a2a8c9365a511756b55c500fb03fd5a14477cd4bf3",
|
||||
"HairpinMode": false,
|
||||
"LinkLocalIPv6Address": "",
|
||||
"LinkLocalIPv6PrefixLen": 0,
|
||||
"Ports": {},
|
||||
"SandboxKey": "/var/run/docker/netns/05627d36c08e",
|
||||
"SecondaryIPAddresses": null,
|
||||
"SecondaryIPv6Addresses": null,
|
||||
"EndpointID": "",
|
||||
"Gateway": "",
|
||||
"GlobalIPv6Address": "",
|
||||
"GlobalIPv6PrefixLen": 0,
|
||||
"IPAddress": "",
|
||||
"IPPrefixLen": 0,
|
||||
"IPv6Gateway": "",
|
||||
"MacAddress": "",
|
||||
"Networks": {
|
||||
"bridge": {
|
||||
"IPAMConfig": null,
|
||||
"Links": null,
|
||||
"Aliases": null,
|
||||
"NetworkID": "8fcfd56fa9203bafa98510abb08bff66ad05bef5b6e97d158cbae3397e1e065e",
|
||||
"EndpointID": "",
|
||||
"Gateway": "",
|
||||
"IPAddress": "",
|
||||
"IPPrefixLen": 0,
|
||||
"IPv6Gateway": "",
|
||||
"GlobalIPv6Address": "",
|
||||
"GlobalIPv6PrefixLen": 0,
|
||||
"MacAddress": "",
|
||||
"DriverOpts": null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
113
pkg/container/mocks/data/containers.json
Normal file
113
pkg/container/mocks/data/containers.json
Normal file
|
|
@ -0,0 +1,113 @@
|
|||
[
|
||||
{
|
||||
"Id": "ae8964ba86c7cd7522cf84e09781343d88e0e3543281c747d88b27e246578b65",
|
||||
"Names": [
|
||||
"/watchtower-test"
|
||||
],
|
||||
"Image": "containrrr/watchtower:latest",
|
||||
"ImageID": "sha256:4dbc5f9c07028a985e14d1393e849ea07f68804c4293050d5a641b138db72daa",
|
||||
"Command": "/watchtower",
|
||||
"Created": 1554925882,
|
||||
"Ports": [],
|
||||
"Labels": {
|
||||
"com.centurylinklabs.watchtower": "true"
|
||||
},
|
||||
"State": "running",
|
||||
"Status": "Exited (1) 6 days ago",
|
||||
"HostConfig": {
|
||||
"NetworkMode": "default"
|
||||
},
|
||||
"NetworkSettings": {
|
||||
"Networks": {
|
||||
"bridge": {
|
||||
"IPAMConfig": null,
|
||||
"Links": null,
|
||||
"Aliases": null,
|
||||
"NetworkID": "8fcfd56fa9203bafa98510abb08bff66ad05bef5b6e97d158cbae3397e1e065e",
|
||||
"EndpointID": "",
|
||||
"Gateway": "",
|
||||
"IPAddress": "",
|
||||
"IPPrefixLen": 0,
|
||||
"IPv6Gateway": "",
|
||||
"GlobalIPv6Address": "",
|
||||
"GlobalIPv6PrefixLen": 0,
|
||||
"MacAddress": "",
|
||||
"DriverOpts": null
|
||||
}
|
||||
}
|
||||
},
|
||||
"Mounts": [
|
||||
{
|
||||
"Type": "bind",
|
||||
"Source": "/var/run/docker.sock",
|
||||
"Destination": "/var/run/docker.sock",
|
||||
"Mode": "",
|
||||
"RW": true,
|
||||
"Propagation": "rprivate"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"Id": "b978af0b858aa8855cce46b628817d4ed58e58f2c4f66c9b9c5449134ed4c008",
|
||||
"Names": [
|
||||
"/portainer"
|
||||
],
|
||||
"Image": "portainer/portainer:latest",
|
||||
"ImageID": "sha256:19d07168491a3f9e2798a9bed96544e34d57ddc4757a4ac5bb199dea896c87fd",
|
||||
"Command": "/portainer",
|
||||
"Created": 1554409712,
|
||||
"Ports": [
|
||||
{
|
||||
"IP": "0.0.0.0",
|
||||
"PrivatePort": 9000,
|
||||
"PublicPort": 9000,
|
||||
"Type": "tcp"
|
||||
}
|
||||
],
|
||||
"Labels": {},
|
||||
"State": "running",
|
||||
"Status": "Up 3 days",
|
||||
"HostConfig": {
|
||||
"NetworkMode": "default"
|
||||
},
|
||||
"NetworkSettings": {
|
||||
"Networks": {
|
||||
"bridge": {
|
||||
"IPAMConfig": null,
|
||||
"Links": null,
|
||||
"Aliases": null,
|
||||
"NetworkID": "9352796e0330dcf31ce3d44fae4b719304b8b3fd97b02ade3aefb8737251682b",
|
||||
"EndpointID": "a8bcd737f27edb4d2955f7bce0c777bb2990b792a6b335b0727387624abe0702",
|
||||
"Gateway": "172.17.0.1",
|
||||
"IPAddress": "172.17.0.2",
|
||||
"IPPrefixLen": 16,
|
||||
"IPv6Gateway": "",
|
||||
"GlobalIPv6Address": "",
|
||||
"GlobalIPv6PrefixLen": 0,
|
||||
"MacAddress": "02:42:ac:11:00:02",
|
||||
"DriverOpts": null
|
||||
}
|
||||
}
|
||||
},
|
||||
"Mounts": [
|
||||
{
|
||||
"Type": "volume",
|
||||
"Name": "portainer_data",
|
||||
"Source": "/var/lib/docker/volumes/portainer_data/_data",
|
||||
"Destination": "/data",
|
||||
"Driver": "local",
|
||||
"Mode": "z",
|
||||
"RW": true,
|
||||
"Propagation": ""
|
||||
},
|
||||
{
|
||||
"Type": "bind",
|
||||
"Source": "/var/run/docker.sock",
|
||||
"Destination": "/var/run/docker.sock",
|
||||
"Mode": "",
|
||||
"RW": true,
|
||||
"Propagation": "rprivate"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
99
pkg/container/mocks/data/image01.json
Normal file
99
pkg/container/mocks/data/image01.json
Normal file
|
|
@ -0,0 +1,99 @@
|
|||
{
|
||||
"Id": "sha256:19d07168491a3f9e2798a9bed96544e34d57ddc4757a4ac5bb199dea896c87fd",
|
||||
"RepoTags": [
|
||||
"portainer/portainer:latest"
|
||||
],
|
||||
"RepoDigests": [
|
||||
"portainer/portainer@sha256:d6cc2c20c0af38d8d557ab994c419c799a10fe825e4aa57fea2e2e507a13747d"
|
||||
],
|
||||
"Parent": "",
|
||||
"Comment": "",
|
||||
"Created": "2019-03-05T04:41:17.612066939Z",
|
||||
"Container": "022100cf79dfee27867d5ff7aa3ff7ecc5cbd486747e808a59b6accd393d65f5",
|
||||
"ContainerConfig": {
|
||||
"Hostname": "022100cf79df",
|
||||
"Domainname": "",
|
||||
"User": "",
|
||||
"AttachStdin": false,
|
||||
"AttachStdout": false,
|
||||
"AttachStderr": false,
|
||||
"ExposedPorts": {
|
||||
"9000/tcp": {}
|
||||
},
|
||||
"Tty": false,
|
||||
"OpenStdin": false,
|
||||
"StdinOnce": false,
|
||||
"Env": [
|
||||
"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
|
||||
],
|
||||
"Cmd": [
|
||||
"/bin/sh",
|
||||
"-c",
|
||||
"#(nop) ",
|
||||
"ENTRYPOINT [\"/portainer\"]"
|
||||
],
|
||||
"Image": "sha256:9cf3ead5068a16f1bc1e18d6e730940f05fd59f60dfe1f6b3a5956196191dc77",
|
||||
"Volumes": {
|
||||
"/data": {}
|
||||
},
|
||||
"WorkingDir": "/",
|
||||
"Entrypoint": [
|
||||
"/portainer"
|
||||
],
|
||||
"OnBuild": null,
|
||||
"Labels": {}
|
||||
},
|
||||
"DockerVersion": "18.09.2",
|
||||
"Author": "",
|
||||
"Config": {
|
||||
"Hostname": "",
|
||||
"Domainname": "",
|
||||
"User": "",
|
||||
"AttachStdin": false,
|
||||
"AttachStdout": false,
|
||||
"AttachStderr": false,
|
||||
"ExposedPorts": {
|
||||
"9000/tcp": {}
|
||||
},
|
||||
"Tty": false,
|
||||
"OpenStdin": false,
|
||||
"StdinOnce": false,
|
||||
"Env": [
|
||||
"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
|
||||
],
|
||||
"Cmd": null,
|
||||
"Image": "sha256:9cf3ead5068a16f1bc1e18d6e730940f05fd59f60dfe1f6b3a5956196191dc77",
|
||||
"Volumes": {
|
||||
"/data": {}
|
||||
},
|
||||
"WorkingDir": "/",
|
||||
"Entrypoint": [
|
||||
"/portainer"
|
||||
],
|
||||
"OnBuild": null,
|
||||
"Labels": null
|
||||
},
|
||||
"Architecture": "amd64",
|
||||
"Os": "linux",
|
||||
"Size": 74089106,
|
||||
"VirtualSize": 74089106,
|
||||
"GraphDriver": {
|
||||
"Data": {
|
||||
"LowerDir": "/var/lib/docker/overlay2/6c3f44131f6f13c9ea1a99a1b24bf348f70ba3eef244f29202faef3a2216ac11/diff",
|
||||
"MergedDir": "/var/lib/docker/overlay2/2e0c03c2476f5b4df855cb8b02a88f76d336d7e0becc3e5193906aaa760687fd/merged",
|
||||
"UpperDir": "/var/lib/docker/overlay2/2e0c03c2476f5b4df855cb8b02a88f76d336d7e0becc3e5193906aaa760687fd/diff",
|
||||
"WorkDir": "/var/lib/docker/overlay2/2e0c03c2476f5b4df855cb8b02a88f76d336d7e0becc3e5193906aaa760687fd/work"
|
||||
},
|
||||
"Name": "overlay2"
|
||||
},
|
||||
"RootFS": {
|
||||
"Type": "layers",
|
||||
"Layers": [
|
||||
"sha256:dd4969f97241b9aefe2a70f560ce399ee9fa0354301c9aef841082ad52161ec5",
|
||||
"sha256:e7260fd2a5f240122129b2d421726d7a4a2bda0cc292e962b694196af8856f20"
|
||||
]
|
||||
},
|
||||
"Metadata": {
|
||||
"LastTagTime": "0001-01-01T00:00:00Z"
|
||||
}
|
||||
}
|
||||
92
pkg/container/mocks/data/image02.json
Normal file
92
pkg/container/mocks/data/image02.json
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
{
|
||||
"Id": "sha256:4dbc5f9c07028a985e14d1393e849ea07f68804c4293050d5a641b138db72daa",
|
||||
"RepoTags": [
|
||||
"containrrr/watchtower:latest"
|
||||
],
|
||||
"RepoDigests": [],
|
||||
"Parent": "sha256:2753b9621e0d76153e1725d0cea015baf0ae4d829782a463b4ea9532ec976447",
|
||||
"Comment": "",
|
||||
"Created": "2019-04-10T19:49:07.970840451Z",
|
||||
"Container": "b8387976426946f5c5191255204a66514c5e64be157f792c5bac329bb055041c",
|
||||
"ContainerConfig": {
|
||||
"Hostname": "b83879764269",
|
||||
"Domainname": "",
|
||||
"User": "",
|
||||
"AttachStdin": false,
|
||||
"AttachStdout": false,
|
||||
"AttachStderr": false,
|
||||
"Tty": false,
|
||||
"OpenStdin": false,
|
||||
"StdinOnce": false,
|
||||
"Env": [
|
||||
"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
|
||||
],
|
||||
"Cmd": [
|
||||
"/bin/sh",
|
||||
"-c",
|
||||
"#(nop) ",
|
||||
"ENTRYPOINT [\"/watchtower\"]"
|
||||
],
|
||||
"Image": "sha256:2753b9621e0d76153e1725d0cea015baf0ae4d829782a463b4ea9532ec976447",
|
||||
"Volumes": null,
|
||||
"WorkingDir": "",
|
||||
"Entrypoint": [
|
||||
"/watchtower"
|
||||
],
|
||||
"OnBuild": null,
|
||||
"Labels": {
|
||||
"com.centurylinklabs.watchtower": "true"
|
||||
}
|
||||
},
|
||||
"DockerVersion": "18.09.1",
|
||||
"Author": "",
|
||||
"Config": {
|
||||
"Hostname": "",
|
||||
"Domainname": "",
|
||||
"User": "",
|
||||
"AttachStdin": false,
|
||||
"AttachStdout": false,
|
||||
"AttachStderr": false,
|
||||
"Tty": false,
|
||||
"OpenStdin": false,
|
||||
"StdinOnce": false,
|
||||
"Env": [
|
||||
"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
|
||||
],
|
||||
"Cmd": null,
|
||||
"Image": "sha256:2753b9621e0d76153e1725d0cea015baf0ae4d829782a463b4ea9532ec976447",
|
||||
"Volumes": null,
|
||||
"WorkingDir": "",
|
||||
"Entrypoint": [
|
||||
"/watchtower"
|
||||
],
|
||||
"OnBuild": null,
|
||||
"Labels": {
|
||||
"com.centurylinklabs.watchtower": "true"
|
||||
}
|
||||
},
|
||||
"Architecture": "amd64",
|
||||
"Os": "linux",
|
||||
"Size": 13005733,
|
||||
"VirtualSize": 13005733,
|
||||
"GraphDriver": {
|
||||
"Data": {
|
||||
"LowerDir": "/var/lib/docker/overlay2/8108325ee844603c9b08d2772cf6e65dccf31dd5171f265078e5ed79a0ba3c0f/diff:/var/lib/docker/overlay2/e5e0cce6bf91b829a308424d99d7e56a33be3a11414ff5cdc48e762a1342b20f/diff",
|
||||
"MergedDir": "/var/lib/docker/overlay2/cdf82f50bc49177d0c17c24f3eaa29eba607b70cc6a081f77781b21c59a13eb8/merged",
|
||||
"UpperDir": "/var/lib/docker/overlay2/cdf82f50bc49177d0c17c24f3eaa29eba607b70cc6a081f77781b21c59a13eb8/diff",
|
||||
"WorkDir": "/var/lib/docker/overlay2/cdf82f50bc49177d0c17c24f3eaa29eba607b70cc6a081f77781b21c59a13eb8/work"
|
||||
},
|
||||
"Name": "overlay2"
|
||||
},
|
||||
"RootFS": {
|
||||
"Type": "layers",
|
||||
"Layers": [
|
||||
"sha256:1d3ad125af2c636cdd793fcf94c9d4fd2b5c4c7d63a770a01056719db13c2271",
|
||||
"sha256:06cfe8fe0892ba4a91cb93e3a25344d4a1c4771cf7297a93e3bd86a1e0fba6eb",
|
||||
"sha256:f58d451769dc30a938d8dcae22fda2acd816899f65fc6b6fa519ddf230dab447"
|
||||
]
|
||||
},
|
||||
"Metadata": {
|
||||
"LastTagTime": "2019-04-10T19:49:08.03921105Z"
|
||||
}
|
||||
}
|
||||
106
pkg/container/sort.go
Normal file
106
pkg/container/sort.go
Normal file
|
|
@ -0,0 +1,106 @@
|
|||
package container
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
// ByCreated allows a list of Container structs to be sorted by the container's
|
||||
// created date.
|
||||
type ByCreated []Container
|
||||
|
||||
func (c ByCreated) Len() int { return len(c) }
|
||||
func (c ByCreated) Swap(i, j int) { c[i], c[j] = c[j], c[i] }
|
||||
|
||||
// Less will compare two elements (identified by index) in the Container
|
||||
// list by created-date.
|
||||
func (c ByCreated) Less(i, j int) bool {
|
||||
t1, err := time.Parse(time.RFC3339Nano, c[i].containerInfo.Created)
|
||||
if err != nil {
|
||||
t1 = time.Now()
|
||||
}
|
||||
|
||||
t2, _ := time.Parse(time.RFC3339Nano, c[j].containerInfo.Created)
|
||||
if err != nil {
|
||||
t1 = time.Now()
|
||||
}
|
||||
|
||||
return t1.Before(t2)
|
||||
}
|
||||
|
||||
// SortByDependencies will sort the list of containers taking into account any
|
||||
// links between containers. Container with no outgoing links will be sorted to
|
||||
// the front of the list while containers with links will be sorted after all
|
||||
// of their dependencies. This sort order ensures that linked containers can
|
||||
// be started in the correct order.
|
||||
func SortByDependencies(containers []Container) ([]Container, error) {
|
||||
sorter := dependencySorter{}
|
||||
return sorter.Sort(containers)
|
||||
}
|
||||
|
||||
type dependencySorter struct {
|
||||
unvisited []Container
|
||||
marked map[string]bool
|
||||
sorted []Container
|
||||
}
|
||||
|
||||
func (ds *dependencySorter) Sort(containers []Container) ([]Container, error) {
|
||||
ds.unvisited = containers
|
||||
ds.marked = map[string]bool{}
|
||||
|
||||
for len(ds.unvisited) > 0 {
|
||||
if err := ds.visit(ds.unvisited[0]); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return ds.sorted, nil
|
||||
}
|
||||
|
||||
func (ds *dependencySorter) visit(c Container) error {
|
||||
|
||||
if _, ok := ds.marked[c.Name()]; ok {
|
||||
return fmt.Errorf("Circular reference to %s", c.Name())
|
||||
}
|
||||
|
||||
// Mark any visited node so that circular references can be detected
|
||||
ds.marked[c.Name()] = true
|
||||
defer delete(ds.marked, c.Name())
|
||||
|
||||
// Recursively visit links
|
||||
for _, linkName := range c.Links() {
|
||||
if linkedContainer := ds.findUnvisited(linkName); linkedContainer != nil {
|
||||
if err := ds.visit(*linkedContainer); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Move container from unvisited to sorted
|
||||
ds.removeUnvisited(c)
|
||||
ds.sorted = append(ds.sorted, c)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ds *dependencySorter) findUnvisited(name string) *Container {
|
||||
for _, c := range ds.unvisited {
|
||||
if c.Name() == name {
|
||||
return &c
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ds *dependencySorter) removeUnvisited(c Container) {
|
||||
var idx int
|
||||
for i := range ds.unvisited {
|
||||
if ds.unvisited[i].Name() == c.Name() {
|
||||
idx = i
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
ds.unvisited = append(ds.unvisited[0:idx], ds.unvisited[idx+1:]...)
|
||||
}
|
||||
102
pkg/container/trust.go
Normal file
102
pkg/container/trust.go
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
package container
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/docker/cli/cli/command"
|
||||
cliconfig "github.com/docker/cli/cli/config"
|
||||
"github.com/docker/cli/cli/config/configfile"
|
||||
"github.com/docker/cli/cli/config/credentials"
|
||||
"github.com/docker/distribution/reference"
|
||||
"github.com/docker/docker/api/types"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// EncodedAuth returns an encoded auth config for the given registry
|
||||
// loaded from environment variables or docker config
|
||||
// as available in that order
|
||||
func EncodedAuth(ref string) (string, error) {
|
||||
auth, err := EncodedEnvAuth(ref)
|
||||
if err != nil {
|
||||
auth, err = EncodedConfigAuth(ref)
|
||||
}
|
||||
return auth, err
|
||||
}
|
||||
|
||||
// EncodedEnvAuth returns an encoded auth config for the given registry
|
||||
// loaded from environment variables
|
||||
// Returns an error if authentication environment variables have not been set
|
||||
func EncodedEnvAuth(ref string) (string, error) {
|
||||
username := os.Getenv("REPO_USER")
|
||||
password := os.Getenv("REPO_PASS")
|
||||
if username != "" && password != "" {
|
||||
auth := types.AuthConfig{
|
||||
Username: username,
|
||||
Password: password,
|
||||
}
|
||||
log.Debugf("Loaded auth credentials %s for %s", auth, ref)
|
||||
return EncodeAuth(auth)
|
||||
}
|
||||
return "", errors.New("Registry auth environment variables (REPO_USER, REPO_PASS) not set")
|
||||
}
|
||||
|
||||
// EncodedConfigAuth returns an encoded auth config for the given registry
|
||||
// loaded from the docker config
|
||||
// Returns an empty string if credentials cannot be found for the referenced server
|
||||
// The docker config must be mounted on the container
|
||||
func EncodedConfigAuth(ref string) (string, error) {
|
||||
server, err := ParseServerAddress(ref)
|
||||
configDir := os.Getenv("DOCKER_CONFIG")
|
||||
if configDir == "" {
|
||||
configDir = "/"
|
||||
}
|
||||
configFile, err := cliconfig.Load(configDir)
|
||||
if err != nil {
|
||||
log.Errorf("Unable to find default config file %s", err)
|
||||
return "", err
|
||||
}
|
||||
credStore := CredentialsStore(*configFile)
|
||||
auth, err := credStore.Get(server) // returns (types.AuthConfig{}) if server not in credStore
|
||||
if auth == (types.AuthConfig{}) {
|
||||
log.Debugf("No credentials for %s in %s", server, configFile.Filename)
|
||||
return "", nil
|
||||
}
|
||||
log.Debugf("Loaded auth credentials %s from %s", auth, configFile.Filename)
|
||||
return EncodeAuth(auth)
|
||||
}
|
||||
|
||||
// ParseServerAddress extracts the server part from a container image ref
|
||||
func ParseServerAddress(ref string) (string, error) {
|
||||
|
||||
parsedRef, err := reference.Parse(ref)
|
||||
if err != nil {
|
||||
return ref, err
|
||||
}
|
||||
|
||||
parts := strings.Split(parsedRef.String(), "/")
|
||||
return parts[0], nil
|
||||
}
|
||||
|
||||
// CredentialsStore returns a new credentials store based
|
||||
// on the settings provided in the configuration file.
|
||||
func CredentialsStore(configFile configfile.ConfigFile) credentials.Store {
|
||||
if configFile.CredentialsStore != "" {
|
||||
return credentials.NewNativeStore(&configFile, configFile.CredentialsStore)
|
||||
}
|
||||
return credentials.NewFileStore(&configFile)
|
||||
}
|
||||
|
||||
// EncodeAuth Base64 encode an AuthConfig struct for transmission over HTTP
|
||||
func EncodeAuth(auth types.AuthConfig) (string, error) {
|
||||
return command.EncodeAuthToBase64(auth)
|
||||
}
|
||||
|
||||
// DefaultAuthHandler will be invoked if an AuthConfig is rejected
|
||||
// It could be used to return a new value for the "X-Registry-Auth" authentication header,
|
||||
// but there's no point trying again with the same value as used in AuthConfig
|
||||
func DefaultAuthHandler() (string, error) {
|
||||
log.Debug("Authentication request was rejected. Trying again without authentication")
|
||||
return "", nil
|
||||
}
|
||||
63
pkg/container/trust_test.go
Normal file
63
pkg/container/trust_test.go
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
package container
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
func TestEncodedEnvAuth_ShouldReturnAnErrorIfRepoEnvsAreUnset(t *testing.T) {
|
||||
os.Unsetenv("REPO_USER")
|
||||
os.Unsetenv("REPO_PASS")
|
||||
_, err := EncodedEnvAuth("")
|
||||
assert.Error(t, err)
|
||||
}
|
||||
func TestEncodedEnvAuth_ShouldReturnAuthHashIfRepoEnvsAreSet(t *testing.T) {
|
||||
expectedHash := "eyJ1c2VybmFtZSI6ImNvbnRhaW5ycnItdXNlciIsInBhc3N3b3JkIjoiY29udGFpbnJyci1wYXNzIn0="
|
||||
|
||||
os.Setenv("REPO_USER", "containrrr-user")
|
||||
os.Setenv("REPO_PASS", "containrrr-pass")
|
||||
config, _ := EncodedEnvAuth("")
|
||||
|
||||
assert.Equal(t, config, expectedHash)
|
||||
}
|
||||
|
||||
func TestEncodedConfigAuth_ShouldReturnAnErrorIfFileIsNotPresent(t *testing.T) {
|
||||
os.Setenv("DOCKER_CONFIG", "/dev/null/should-fail")
|
||||
_, err := EncodedConfigAuth("")
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
/*
|
||||
* TODO:
|
||||
* This part only confirms that it still works in the same way as it did
|
||||
* with the old version of the docker api client sdk. I'd say that
|
||||
* ParseServerAddress likely needs to be elaborated a bit to default to
|
||||
* dockerhub in case no server address was provided.
|
||||
*
|
||||
* ++ @simskij, 2019-04-04
|
||||
*/
|
||||
|
||||
func TestParseServerAddress_ShouldReturnErrorIfPassedEmptyString(t *testing.T) {
|
||||
_, err := ParseServerAddress("")
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestParseServerAddress_ShouldReturnTheRepoNameIfPassedAFullyQualifiedImageName(t *testing.T) {
|
||||
val, _ := ParseServerAddress("github.com/containrrrr/config")
|
||||
assert.Equal(t, val, "github.com")
|
||||
}
|
||||
|
||||
func TestParseServerAddress_ShouldReturnTheOrganizationPartIfPassedAnImageNameMissingServerName(t *testing.T) {
|
||||
val, _ := ParseServerAddress("containrrr/config")
|
||||
assert.Equal(t, val, "containrrr")
|
||||
}
|
||||
|
||||
func TestParseServerAddress_ShouldReturnTheServerNameIfPassedAFullyQualifiedImageName(t *testing.T) {
|
||||
val, _ := ParseServerAddress("github.com/containrrrr/config")
|
||||
assert.Equal(t, val, "github.com")
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue