refactor: move container into pkg

This commit is contained in:
Simon Aronsson 2019-07-21 20:14:28 +02:00
parent e109a7a6ce
commit 74ce92760c
19 changed files with 7 additions and 7 deletions

290
pkg/container/client.go Normal file
View 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
View 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"
}

View 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
View 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
}

View 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)
}

View 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)
}

View 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
}

View 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
}
}
}
}

View 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
}
}
}
}

View 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"
}
]
}
]

View 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"
}
}

View 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
View 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
View 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
}

View 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")
}