Handle container links
Ensures that linked containers are restarted if any of their dependencies are restarted -- and makes sure that everything happens in the correct order.pull/1/head
parent
ce4ed7316c
commit
c02c4b9ec1
@ -0,0 +1,116 @@
|
|||||||
|
package docker
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"github.com/samalba/dockerclient"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
pullImages bool
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
pullImages = true
|
||||||
|
}
|
||||||
|
|
||||||
|
type Client interface {
|
||||||
|
ListContainers() ([]Container, error)
|
||||||
|
RefreshImage(container *Container) error
|
||||||
|
Stop(container Container) error
|
||||||
|
Start(container Container) error
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewClient() Client {
|
||||||
|
docker, err := dockerclient.NewDockerClient("unix:///var/run/docker.sock", nil)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Error instantiating Docker client: %s\n", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return DockerClient{api: docker}
|
||||||
|
}
|
||||||
|
|
||||||
|
type DockerClient struct {
|
||||||
|
api dockerclient.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
func (client DockerClient) ListContainers() ([]Container, error) {
|
||||||
|
cs := []Container{}
|
||||||
|
|
||||||
|
runningContainers, err := client.api.ListContainers(false, false, "")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, runningContainer := range runningContainers {
|
||||||
|
containerInfo, err := client.api.InspectContainer(runningContainer.Id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
imageInfo, err := client.api.InspectImage(containerInfo.Image)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
cs = append(cs, Container{containerInfo: containerInfo, imageInfo: imageInfo})
|
||||||
|
}
|
||||||
|
|
||||||
|
return cs, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (client DockerClient) RefreshImage(c *Container) error {
|
||||||
|
containerInfo := c.containerInfo
|
||||||
|
oldImageInfo := c.imageInfo
|
||||||
|
imageName := containerInfo.Config.Image
|
||||||
|
|
||||||
|
if pullImages {
|
||||||
|
log.Printf("Pulling %s for %s\n", imageName, c.Name())
|
||||||
|
if err := client.api.PullImage(imageName, nil); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
newImageInfo, err := client.api.InspectImage(imageName)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if newImageInfo.Id != oldImageInfo.Id {
|
||||||
|
log.Printf("Found new %s image (%s)\n", imageName, newImageInfo.Id)
|
||||||
|
c.Stale = true
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (client DockerClient) Stop(c Container) error {
|
||||||
|
signal := "SIGTERM"
|
||||||
|
|
||||||
|
if sig, ok := c.containerInfo.Config.Labels["com.centurylinklabs.watchtower.stop-signal"]; ok {
|
||||||
|
signal = sig
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("Stopping: %s\n", c.Name())
|
||||||
|
|
||||||
|
if err := client.api.KillContainer(c.containerInfo.Id, signal); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return client.api.RemoveContainer(c.containerInfo.Id, true, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (client DockerClient) Start(c Container) error {
|
||||||
|
config := c.runtimeConfig()
|
||||||
|
hostConfig := c.hostConfig()
|
||||||
|
|
||||||
|
log.Printf("Starting: %s\n", c.Name())
|
||||||
|
|
||||||
|
newContainerId, err := client.api.CreateContainer(config, c.Name())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return client.api.StartContainer(newContainerId, hostConfig)
|
||||||
|
}
|
@ -0,0 +1,308 @@
|
|||||||
|
package docker
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/samalba/dockerclient"
|
||||||
|
"github.com/samalba/dockerclient/mockclient"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/mock"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestListContainers_Success(t *testing.T) {
|
||||||
|
ci := &dockerclient.ContainerInfo{Image: "abc123"}
|
||||||
|
ii := &dockerclient.ImageInfo{}
|
||||||
|
api := mockclient.NewMockClient()
|
||||||
|
api.On("ListContainers", false, false, "").Return([]dockerclient.Container{{Id: "foo"}}, nil)
|
||||||
|
api.On("InspectContainer", "foo").Return(ci, nil)
|
||||||
|
api.On("InspectImage", "abc123").Return(ii, nil)
|
||||||
|
|
||||||
|
client := DockerClient{api: api}
|
||||||
|
cs, err := client.ListContainers()
|
||||||
|
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Len(t, cs, 1)
|
||||||
|
assert.Equal(t, ci, cs[0].containerInfo)
|
||||||
|
assert.Equal(t, ii, cs[0].imageInfo)
|
||||||
|
api.AssertExpectations(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestListContainers_ListError(t *testing.T) {
|
||||||
|
api := mockclient.NewMockClient()
|
||||||
|
api.On("ListContainers", false, false, "").Return([]dockerclient.Container{}, errors.New("oops"))
|
||||||
|
|
||||||
|
client := DockerClient{api: api}
|
||||||
|
_, err := client.ListContainers()
|
||||||
|
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.EqualError(t, err, "oops")
|
||||||
|
api.AssertExpectations(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestListContainers_InspectContainerError(t *testing.T) {
|
||||||
|
api := mockclient.NewMockClient()
|
||||||
|
api.On("ListContainers", false, false, "").Return([]dockerclient.Container{{Id: "foo"}}, nil)
|
||||||
|
api.On("InspectContainer", "foo").Return(&dockerclient.ContainerInfo{}, errors.New("uh-oh"))
|
||||||
|
|
||||||
|
client := DockerClient{api: api}
|
||||||
|
_, err := client.ListContainers()
|
||||||
|
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.EqualError(t, err, "uh-oh")
|
||||||
|
api.AssertExpectations(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestListContainers_InspectImageError(t *testing.T) {
|
||||||
|
ci := &dockerclient.ContainerInfo{Image: "abc123"}
|
||||||
|
ii := &dockerclient.ImageInfo{}
|
||||||
|
api := mockclient.NewMockClient()
|
||||||
|
api.On("ListContainers", false, false, "").Return([]dockerclient.Container{{Id: "foo"}}, nil)
|
||||||
|
api.On("InspectContainer", "foo").Return(ci, nil)
|
||||||
|
api.On("InspectImage", "abc123").Return(ii, errors.New("whoops"))
|
||||||
|
|
||||||
|
client := DockerClient{api: api}
|
||||||
|
_, err := client.ListContainers()
|
||||||
|
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.EqualError(t, err, "whoops")
|
||||||
|
api.AssertExpectations(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRefreshImage_NotStaleSuccess(t *testing.T) {
|
||||||
|
c := &Container{
|
||||||
|
containerInfo: &dockerclient.ContainerInfo{
|
||||||
|
Name: "foo",
|
||||||
|
Config: &dockerclient.ContainerConfig{Image: "bar"},
|
||||||
|
},
|
||||||
|
imageInfo: &dockerclient.ImageInfo{Id: "abc123"},
|
||||||
|
}
|
||||||
|
newImageInfo := &dockerclient.ImageInfo{Id: "abc123"}
|
||||||
|
|
||||||
|
api := mockclient.NewMockClient()
|
||||||
|
api.On("PullImage", "bar", mock.Anything).Return(nil)
|
||||||
|
api.On("InspectImage", "bar").Return(newImageInfo, nil)
|
||||||
|
|
||||||
|
client := DockerClient{api: api}
|
||||||
|
err := client.RefreshImage(c)
|
||||||
|
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.False(t, c.Stale)
|
||||||
|
api.AssertExpectations(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRefreshImage_StaleSuccess(t *testing.T) {
|
||||||
|
c := &Container{
|
||||||
|
containerInfo: &dockerclient.ContainerInfo{
|
||||||
|
Name: "foo",
|
||||||
|
Config: &dockerclient.ContainerConfig{Image: "bar"},
|
||||||
|
},
|
||||||
|
imageInfo: &dockerclient.ImageInfo{Id: "abc123"},
|
||||||
|
}
|
||||||
|
newImageInfo := &dockerclient.ImageInfo{Id: "xyz789"}
|
||||||
|
|
||||||
|
api := mockclient.NewMockClient()
|
||||||
|
api.On("PullImage", "bar", mock.Anything).Return(nil)
|
||||||
|
api.On("InspectImage", "bar").Return(newImageInfo, nil)
|
||||||
|
|
||||||
|
client := DockerClient{api: api}
|
||||||
|
err := client.RefreshImage(c)
|
||||||
|
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.True(t, c.Stale)
|
||||||
|
api.AssertExpectations(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRefreshImage_PullImageError(t *testing.T) {
|
||||||
|
c := &Container{
|
||||||
|
containerInfo: &dockerclient.ContainerInfo{
|
||||||
|
Name: "foo",
|
||||||
|
Config: &dockerclient.ContainerConfig{Image: "bar"},
|
||||||
|
},
|
||||||
|
imageInfo: &dockerclient.ImageInfo{Id: "abc123"},
|
||||||
|
}
|
||||||
|
|
||||||
|
api := mockclient.NewMockClient()
|
||||||
|
api.On("PullImage", "bar", mock.Anything).Return(errors.New("oops"))
|
||||||
|
|
||||||
|
client := DockerClient{api: api}
|
||||||
|
err := client.RefreshImage(c)
|
||||||
|
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.EqualError(t, err, "oops")
|
||||||
|
api.AssertExpectations(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRefreshImage_InspectImageError(t *testing.T) {
|
||||||
|
c := &Container{
|
||||||
|
containerInfo: &dockerclient.ContainerInfo{
|
||||||
|
Name: "foo",
|
||||||
|
Config: &dockerclient.ContainerConfig{Image: "bar"},
|
||||||
|
},
|
||||||
|
imageInfo: &dockerclient.ImageInfo{Id: "abc123"},
|
||||||
|
}
|
||||||
|
newImageInfo := &dockerclient.ImageInfo{}
|
||||||
|
|
||||||
|
api := mockclient.NewMockClient()
|
||||||
|
api.On("PullImage", "bar", mock.Anything).Return(nil)
|
||||||
|
api.On("InspectImage", "bar").Return(newImageInfo, errors.New("uh-oh"))
|
||||||
|
|
||||||
|
client := DockerClient{api: api}
|
||||||
|
err := client.RefreshImage(c)
|
||||||
|
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.EqualError(t, err, "uh-oh")
|
||||||
|
api.AssertExpectations(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStop_DefaultSuccess(t *testing.T) {
|
||||||
|
c := Container{
|
||||||
|
containerInfo: &dockerclient.ContainerInfo{
|
||||||
|
Name: "foo",
|
||||||
|
Id: "abc123",
|
||||||
|
Config: &dockerclient.ContainerConfig{},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
api := mockclient.NewMockClient()
|
||||||
|
api.On("KillContainer", "abc123", "SIGTERM").Return(nil)
|
||||||
|
api.On("RemoveContainer", "abc123", true, false).Return(nil)
|
||||||
|
|
||||||
|
client := DockerClient{api: api}
|
||||||
|
err := client.Stop(c)
|
||||||
|
|
||||||
|
assert.NoError(t, err)
|
||||||
|
api.AssertExpectations(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStop_CustomSignalSuccess(t *testing.T) {
|
||||||
|
c := Container{
|
||||||
|
containerInfo: &dockerclient.ContainerInfo{
|
||||||
|
Name: "foo",
|
||||||
|
Id: "abc123",
|
||||||
|
Config: &dockerclient.ContainerConfig{
|
||||||
|
Labels: map[string]string{"com.centurylinklabs.watchtower.stop-signal": "SIGUSR1"}},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
api := mockclient.NewMockClient()
|
||||||
|
api.On("KillContainer", "abc123", "SIGUSR1").Return(nil)
|
||||||
|
api.On("RemoveContainer", "abc123", true, false).Return(nil)
|
||||||
|
|
||||||
|
client := DockerClient{api: api}
|
||||||
|
err := client.Stop(c)
|
||||||
|
|
||||||
|
assert.NoError(t, err)
|
||||||
|
api.AssertExpectations(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStop_KillContainerError(t *testing.T) {
|
||||||
|
c := Container{
|
||||||
|
containerInfo: &dockerclient.ContainerInfo{
|
||||||
|
Name: "foo",
|
||||||
|
Id: "abc123",
|
||||||
|
Config: &dockerclient.ContainerConfig{},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
api := mockclient.NewMockClient()
|
||||||
|
api.On("KillContainer", "abc123", "SIGTERM").Return(errors.New("oops"))
|
||||||
|
|
||||||
|
client := DockerClient{api: api}
|
||||||
|
err := client.Stop(c)
|
||||||
|
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.EqualError(t, err, "oops")
|
||||||
|
api.AssertExpectations(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStop_RemoveContainerError(t *testing.T) {
|
||||||
|
c := Container{
|
||||||
|
containerInfo: &dockerclient.ContainerInfo{
|
||||||
|
Name: "foo",
|
||||||
|
Id: "abc123",
|
||||||
|
Config: &dockerclient.ContainerConfig{},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
api := mockclient.NewMockClient()
|
||||||
|
api.On("KillContainer", "abc123", "SIGTERM").Return(nil)
|
||||||
|
api.On("RemoveContainer", "abc123", true, false).Return(errors.New("whoops"))
|
||||||
|
|
||||||
|
client := DockerClient{api: api}
|
||||||
|
err := client.Stop(c)
|
||||||
|
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.EqualError(t, err, "whoops")
|
||||||
|
api.AssertExpectations(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStart_Success(t *testing.T) {
|
||||||
|
c := Container{
|
||||||
|
containerInfo: &dockerclient.ContainerInfo{
|
||||||
|
Name: "foo",
|
||||||
|
Config: &dockerclient.ContainerConfig{},
|
||||||
|
HostConfig: &dockerclient.HostConfig{},
|
||||||
|
},
|
||||||
|
imageInfo: &dockerclient.ImageInfo{
|
||||||
|
Config: &dockerclient.ContainerConfig{},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
api := mockclient.NewMockClient()
|
||||||
|
api.On("CreateContainer", mock.AnythingOfType("*dockerclient.ContainerConfig"), "foo").Return("def789", nil)
|
||||||
|
api.On("StartContainer", "def789", mock.AnythingOfType("*dockerclient.HostConfig")).Return(nil)
|
||||||
|
|
||||||
|
client := DockerClient{api: api}
|
||||||
|
err := client.Start(c)
|
||||||
|
|
||||||
|
assert.NoError(t, err)
|
||||||
|
api.AssertExpectations(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStart_CreateContainerError(t *testing.T) {
|
||||||
|
c := Container{
|
||||||
|
containerInfo: &dockerclient.ContainerInfo{
|
||||||
|
Name: "foo",
|
||||||
|
Config: &dockerclient.ContainerConfig{},
|
||||||
|
HostConfig: &dockerclient.HostConfig{},
|
||||||
|
},
|
||||||
|
imageInfo: &dockerclient.ImageInfo{
|
||||||
|
Config: &dockerclient.ContainerConfig{},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
api := mockclient.NewMockClient()
|
||||||
|
api.On("CreateContainer", mock.Anything, "foo").Return("", errors.New("oops"))
|
||||||
|
|
||||||
|
client := DockerClient{api: api}
|
||||||
|
err := client.Start(c)
|
||||||
|
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.EqualError(t, err, "oops")
|
||||||
|
api.AssertExpectations(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStart_StartContainerError(t *testing.T) {
|
||||||
|
c := Container{
|
||||||
|
containerInfo: &dockerclient.ContainerInfo{
|
||||||
|
Name: "foo",
|
||||||
|
Config: &dockerclient.ContainerConfig{},
|
||||||
|
HostConfig: &dockerclient.HostConfig{},
|
||||||
|
},
|
||||||
|
imageInfo: &dockerclient.ImageInfo{
|
||||||
|
Config: &dockerclient.ContainerConfig{},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
api := mockclient.NewMockClient()
|
||||||
|
api.On("CreateContainer", mock.Anything, "foo").Return("def789", nil)
|
||||||
|
api.On("StartContainer", "def789", mock.Anything).Return(errors.New("whoops"))
|
||||||
|
|
||||||
|
client := DockerClient{api: api}
|
||||||
|
err := client.Start(c)
|
||||||
|
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.EqualError(t, err, "whoops")
|
||||||
|
api.AssertExpectations(t)
|
||||||
|
}
|
@ -0,0 +1,103 @@
|
|||||||
|
package docker
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/samalba/dockerclient"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Container struct {
|
||||||
|
Stale bool
|
||||||
|
|
||||||
|
containerInfo *dockerclient.ContainerInfo
|
||||||
|
imageInfo *dockerclient.ImageInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Container) Name() string {
|
||||||
|
return c.containerInfo.Name
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Container) Links() []string {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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() *dockerclient.ContainerConfig {
|
||||||
|
config := c.containerInfo.Config
|
||||||
|
imageConfig := c.imageInfo.Config
|
||||||
|
|
||||||
|
if config.WorkingDir == imageConfig.WorkingDir {
|
||||||
|
config.WorkingDir = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.User == imageConfig.User {
|
||||||
|
config.User = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
if sliceEqual(config.Cmd, imageConfig.Cmd) {
|
||||||
|
config.Cmd = []string{}
|
||||||
|
}
|
||||||
|
|
||||||
|
if sliceEqual(config.Entrypoint, imageConfig.Entrypoint) {
|
||||||
|
config.Entrypoint = []string{}
|
||||||
|
}
|
||||||
|
|
||||||
|
config.Env = sliceSubtract(config.Env, imageConfig.Env)
|
||||||
|
|
||||||
|
config.Labels = stringMapSubtract(config.Labels, imageConfig.Labels)
|
||||||
|
|
||||||
|
config.Volumes = structMapSubtract(config.Volumes, imageConfig.Volumes)
|
||||||
|
|
||||||
|
config.ExposedPorts = structMapSubtract(config.ExposedPorts, imageConfig.ExposedPorts)
|
||||||
|
for p, _ := range c.containerInfo.HostConfig.PortBindings {
|
||||||
|
config.ExposedPorts[p] = struct{}{}
|
||||||
|
}
|
||||||
|
|
||||||
|
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() *dockerclient.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
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewTestContainer(name string, links []string) Container {
|
||||||
|
return Container{
|
||||||
|
containerInfo: &dockerclient.ContainerInfo{
|
||||||
|
Name: name,
|
||||||
|
HostConfig: &dockerclient.HostConfig{
|
||||||
|
Links: links,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,32 @@
|
|||||||
|
package docker
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/samalba/dockerclient"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestName(t *testing.T) {
|
||||||
|
c := Container{
|
||||||
|
containerInfo: &dockerclient.ContainerInfo{Name: "foo"},
|
||||||
|
}
|
||||||
|
|
||||||
|
name := c.Name()
|
||||||
|
|
||||||
|
assert.Equal(t, "foo", name)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLinks(t *testing.T) {
|
||||||
|
c := Container{
|
||||||
|
containerInfo: &dockerclient.ContainerInfo{
|
||||||
|
HostConfig: &dockerclient.HostConfig{
|
||||||
|
Links: []string{"foo:foo", "bar:bar"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
links := c.Links()
|
||||||
|
|
||||||
|
assert.Equal(t, []string{"foo", "bar"}, links)
|
||||||
|
}
|
@ -0,0 +1,64 @@
|
|||||||
|
package docker
|
||||||
|
|
||||||
|
func sliceEqual(s1, s2 []string) bool {
|
||||||
|
if len(s1) != len(s2) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := range s1 {
|
||||||
|
if s1[i] != s2[i] {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func sliceSubtract(a1, a2 []string) []string {
|
||||||
|
a := []string{}
|
||||||
|
|
||||||
|
for _, e1 := range a1 {
|
||||||
|
found := false
|
||||||
|
|
||||||
|
for _, e2 := range a2 {
|
||||||
|
if e1 == e2 {
|
||||||
|
found = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !found {
|
||||||
|
a = append(a, e1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return a
|
||||||
|
}
|
||||||
|
|
||||||
|
func stringMapSubtract(m1, m2 map[string]string) map[string]string {
|
||||||
|
m := map[string]string{}
|
||||||
|
|
||||||
|
for k1, v1 := range m1 {
|
||||||
|
if v2, ok := m2[k1]; ok {
|
||||||
|
if v2 != v1 {
|
||||||
|
m[k1] = v1
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
m[k1] = v1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
|
||||||
|
func structMapSubtract(m1, m2 map[string]struct{}) map[string]struct{} {
|
||||||
|
m := map[string]struct{}{}
|
||||||
|
|
||||||
|
for k1, v1 := range m1 {
|
||||||
|
if _, ok := m2[k1]; !ok {
|
||||||
|
m[k1] = v1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return m
|
||||||
|
}
|
@ -1,112 +0,0 @@
|
|||||||
package updater
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/samalba/dockerclient"
|
|
||||||
)
|
|
||||||
|
|
||||||
// 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 override options.
|
|
||||||
func GenerateContainerConfig(oldContainerInfo *dockerclient.ContainerInfo, oldImageConfig *dockerclient.ContainerConfig) *dockerclient.ContainerConfig {
|
|
||||||
config := oldContainerInfo.Config
|
|
||||||
|
|
||||||
if config.WorkingDir == oldImageConfig.WorkingDir {
|
|
||||||
config.WorkingDir = ""
|
|
||||||
}
|
|
||||||
|
|
||||||
if config.User == oldImageConfig.User {
|
|
||||||
config.User = ""
|
|
||||||
}
|
|
||||||
|
|
||||||
if sliceEqual(config.Cmd, oldImageConfig.Cmd) {
|
|
||||||
config.Cmd = []string{}
|
|
||||||
}
|
|
||||||
|
|
||||||
if sliceEqual(config.Entrypoint, oldImageConfig.Entrypoint) {
|
|
||||||
config.Entrypoint = []string{}
|
|
||||||
}
|
|
||||||
|
|
||||||
config.Env = arraySubtract(config.Env, oldImageConfig.Env)
|
|
||||||
|
|
||||||
config.Labels = stringMapSubtract(config.Labels, oldImageConfig.Labels)
|
|
||||||
|
|
||||||
config.Volumes = structMapSubtract(config.Volumes, oldImageConfig.Volumes)
|
|
||||||
|
|
||||||
config.ExposedPorts = structMapSubtract(config.ExposedPorts, oldImageConfig.ExposedPorts)
|
|
||||||
for p, _ := range oldContainerInfo.HostConfig.PortBindings {
|
|
||||||
config.ExposedPorts[p] = struct{}{}
|
|
||||||
}
|
|
||||||
|
|
||||||
return config
|
|
||||||
}
|
|
||||||
|
|
||||||
func sliceEqual(s1, s2 []string) bool {
|
|
||||||
if len(s1) != len(s2) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
for i := range s1 {
|
|
||||||
if s1[i] != s2[i] {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
func stringMapSubtract(m1, m2 map[string]string) map[string]string {
|
|
||||||
m := map[string]string{}
|
|
||||||
|
|
||||||
for k1, v1 := range m1 {
|
|
||||||
if v2, ok := m2[k1]; ok {
|
|
||||||
if v2 != v1 {
|
|
||||||
m[k1] = v1
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
m[k1] = v1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return m
|
|
||||||
}
|
|
||||||
|
|
||||||
func structMapSubtract(m1, m2 map[string]struct{}) map[string]struct{} {
|
|
||||||
m := map[string]struct{}{}
|
|
||||||
|
|
||||||
for k1, v1 := range m1 {
|
|
||||||
if _, ok := m2[k1]; !ok {
|
|
||||||
m[k1] = v1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return m
|
|
||||||
}
|
|
||||||
|
|
||||||
func arraySubtract(a1, a2 []string) []string {
|
|
||||||
a := []string{}
|
|
||||||
|
|
||||||
for _, e1 := range a1 {
|
|
||||||
found := false
|
|
||||||
|
|
||||||
for _, e2 := range a2 {
|
|
||||||
if e1 == e2 {
|
|
||||||
found = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !found {
|
|
||||||
a = append(a, e1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return a
|
|
||||||
}
|
|
@ -0,0 +1,74 @@
|
|||||||
|
package updater
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/CenturyLinkLabs/watchtower/docker"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ContainerSorter struct {
|
||||||
|
unvisited []docker.Container
|
||||||
|
marked map[string]bool
|
||||||
|
sorted []docker.Container
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cs *ContainerSorter) Sort(containers []docker.Container) ([]docker.Container, error) {
|
||||||
|
cs.unvisited = containers
|
||||||
|
cs.marked = map[string]bool{}
|
||||||
|
|
||||||
|
for len(cs.unvisited) > 0 {
|
||||||
|
if err := cs.visit(cs.unvisited[0]); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return cs.sorted, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cs *ContainerSorter) visit(c docker.Container) error {
|
||||||
|
|
||||||
|
if _, ok := cs.marked[c.Name()]; ok {
|
||||||
|
return fmt.Errorf("Circular reference to %s", c.Name())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mark any visited node so that circular references can be detected
|
||||||
|
cs.marked[c.Name()] = true
|
||||||
|
defer delete(cs.marked, c.Name())
|
||||||
|
|
||||||
|
// Recursively visit links
|
||||||
|
for _, linkName := range c.Links() {
|
||||||
|
if linkedContainer := cs.findUnvisited(linkName); linkedContainer != nil {
|
||||||
|
if err := cs.visit(*linkedContainer); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Move container from unvisited to sorted
|
||||||
|
cs.removeUnvisited(c)
|
||||||
|
cs.sorted = append(cs.sorted, c)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cs *ContainerSorter) findUnvisited(name string) *docker.Container {
|
||||||
|
for _, c := range cs.unvisited {
|
||||||
|
if c.Name() == name {
|
||||||
|
return &c
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cs *ContainerSorter) removeUnvisited(c docker.Container) {
|
||||||
|
var idx int
|
||||||
|
for i := range cs.unvisited {
|
||||||
|
if cs.unvisited[i].Name() == c.Name() {
|
||||||
|
idx = i
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cs.unvisited = append(cs.unvisited[0:idx], cs.unvisited[idx+1:]...)
|
||||||
|
}
|
@ -0,0 +1,37 @@
|
|||||||
|
package updater
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/CenturyLinkLabs/watchtower/docker"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestContainerSorter_Success(t *testing.T) {
|
||||||
|
c1 := docker.NewTestContainer("1", []string{})
|
||||||
|
c2 := docker.NewTestContainer("2", []string{"1:"})
|
||||||
|
c3 := docker.NewTestContainer("3", []string{"2:"})
|
||||||
|
c4 := docker.NewTestContainer("4", []string{"3:"})
|
||||||
|
c5 := docker.NewTestContainer("5", []string{"4:"})
|
||||||
|
c6 := docker.NewTestContainer("6", []string{"5:", "3:"})
|
||||||
|
containers := []docker.Container{c6, c2, c4, c1, c3, c5}
|
||||||
|
|
||||||
|
cs := ContainerSorter{}
|
||||||
|
result, err := cs.Sort(containers)
|
||||||
|
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, []docker.Container{c1, c2, c3, c4, c5, c6}, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestContainerSorter_Error(t *testing.T) {
|
||||||
|
c1 := docker.NewTestContainer("1", []string{"3:"})
|
||||||
|
c2 := docker.NewTestContainer("2", []string{"1:"})
|
||||||
|
c3 := docker.NewTestContainer("3", []string{"2:"})
|
||||||
|
containers := []docker.Container{c1, c2, c3}
|
||||||
|
|
||||||
|
cs := ContainerSorter{}
|
||||||
|
_, err := cs.Sort(containers)
|
||||||
|
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.EqualError(t, err, "Circular reference to 1")
|
||||||
|
}
|
@ -1,81 +1,71 @@
|
|||||||
package updater
|
package updater
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"log"
|
"github.com/CenturyLinkLabs/watchtower/docker"
|
||||||
|
|
||||||
"github.com/samalba/dockerclient"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
client dockerclient.Client
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func Run() error {
|
||||||
docker, err := dockerclient.NewDockerClient("unix:///var/run/docker.sock", nil)
|
client := docker.NewClient()
|
||||||
|
containers, err := client.ListContainers()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("Error instantiating Docker client: %s\n", err)
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
client = docker
|
for i := range containers {
|
||||||
}
|
if err := client.RefreshImage(&containers[i]); err != nil {
|
||||||
|
return err
|
||||||
func Run() error {
|
}
|
||||||
containers, _ := client.ListContainers(false, false, "")
|
}
|
||||||
|
|
||||||
for _, container := range containers {
|
|
||||||
|
|
||||||
oldContainerInfo, _ := client.InspectContainer(container.Id)
|
|
||||||
name := oldContainerInfo.Name
|
|
||||||
oldImageId := oldContainerInfo.Image
|
|
||||||
log.Printf("Running: %s (%s)\n", container.Image, oldImageId)
|
|
||||||
|
|
||||||
oldImageInfo, _ := client.InspectImage(oldImageId)
|
containers, err = sortContainers(containers)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
// First check to see if a newer image has already been built
|
checkDependencies(containers)
|
||||||
newImageInfo, _ := client.InspectImage(container.Image)
|
|
||||||
|
|
||||||
if newImageInfo.Id == oldImageInfo.Id {
|
// Stop stale containers in reverse order
|
||||||
_ = client.PullImage(container.Image, nil)
|
for i := len(containers) - 1; i >= 0; i-- {
|
||||||
newImageInfo, _ = client.InspectImage(container.Image)
|
container := containers[i]
|
||||||
|
if container.Stale {
|
||||||
|
if err := client.Stop(container); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
newImageId := newImageInfo.Id
|
// Restart stale containers in sorted order
|
||||||
log.Printf("Latest: %s (%s)\n", container.Image, newImageId)
|
for _, container := range containers {
|
||||||
|
if container.Stale {
|
||||||
if newImageId != oldImageId {
|
if err := client.Start(container); err != nil {
|
||||||
log.Printf("Restarting %s with new image\n", name)
|
return err
|
||||||
if err := stopContainer(oldContainerInfo); err != nil {
|
|
||||||
}
|
}
|
||||||
|
|
||||||
config := GenerateContainerConfig(oldContainerInfo, oldImageInfo.Config)
|
|
||||||
|
|
||||||
hostConfig := oldContainerInfo.HostConfig
|
|
||||||
_ = startContainer(name, config, hostConfig)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func stopContainer(container *dockerclient.ContainerInfo) error {
|
func sortContainers(containers []docker.Container) ([]docker.Container, error) {
|
||||||
signal := "SIGTERM"
|
sorter := ContainerSorter{}
|
||||||
|
return sorter.Sort(containers)
|
||||||
|
}
|
||||||
|
|
||||||
if sig, ok := container.Config.Labels["com.centurylinklabs.watchtower.stop-signal"]; ok {
|
func checkDependencies(containers []docker.Container) {
|
||||||
signal = sig
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := client.KillContainer(container.Id, signal); err != nil {
|
for i, parent := range containers {
|
||||||
return err
|
if parent.Stale {
|
||||||
}
|
continue
|
||||||
|
}
|
||||||
return client.RemoveContainer(container.Id, true, false)
|
|
||||||
}
|
|
||||||
|
|
||||||
func startContainer(name string, config *dockerclient.ContainerConfig, hostConfig *dockerclient.HostConfig) error {
|
LinkLoop:
|
||||||
newContainerId, err := client.CreateContainer(config, name)
|
for _, linkName := range parent.Links() {
|
||||||
if err != nil {
|
for _, child := range containers {
|
||||||
return err
|
if child.Name() == linkName && child.Stale {
|
||||||
|
containers[i].Stale = true
|
||||||
|
break LinkLoop
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return client.StartContainer(newContainerId, hostConfig)
|
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,29 @@
|
|||||||
|
package updater
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/CenturyLinkLabs/watchtower/docker"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestCheckDependencies(t *testing.T) {
|
||||||
|
cs := []docker.Container{
|
||||||
|
docker.NewTestContainer("1", []string{}),
|
||||||
|
docker.NewTestContainer("2", []string{"1:"}),
|
||||||
|
docker.NewTestContainer("3", []string{"2:"}),
|
||||||
|
docker.NewTestContainer("4", []string{"3:"}),
|
||||||
|
docker.NewTestContainer("5", []string{"4:"}),
|
||||||
|
docker.NewTestContainer("6", []string{"5:"}),
|
||||||
|
}
|
||||||
|
cs[3].Stale = true
|
||||||
|
|
||||||
|
checkDependencies(cs)
|
||||||
|
|
||||||
|
assert.False(t, cs[0].Stale)
|
||||||
|
assert.False(t, cs[1].Stale)
|
||||||
|
assert.False(t, cs[2].Stale)
|
||||||
|
assert.True(t, cs[3].Stale)
|
||||||
|
assert.True(t, cs[4].Stale)
|
||||||
|
assert.True(t, cs[5].Stale)
|
||||||
|
}
|
Loading…
Reference in New Issue