mirror of
https://github.com/containrrr/watchtower.git
synced 2025-09-21 21:30:48 +02:00
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.
This commit is contained in:
parent
ce4ed7316c
commit
c02c4b9ec1
12 changed files with 854 additions and 183 deletions
|
@ -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
|
||||
}
|
|
@ -1,38 +0,0 @@
|
|||
package updater
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestStringMapSubtract(t *testing.T) {
|
||||
m1 := map[string]string{"a": "a", "b": "b", "c": "sea"}
|
||||
m2 := map[string]string{"a": "a", "c": "c"}
|
||||
|
||||
result := stringMapSubtract(m1, m2)
|
||||
assert.Equal(t, map[string]string{"b": "b", "c": "sea"}, result)
|
||||
assert.Equal(t, map[string]string{"a": "a", "b": "b", "c": "sea"}, m1)
|
||||
assert.Equal(t, map[string]string{"a": "a", "c": "c"}, m2)
|
||||
}
|
||||
|
||||
func TestStructMapSubtract(t *testing.T) {
|
||||
x := struct{}{}
|
||||
m1 := map[string]struct{}{"a": x, "b": x, "c": x}
|
||||
m2 := map[string]struct{}{"a": x, "c": x}
|
||||
|
||||
result := structMapSubtract(m1, m2)
|
||||
assert.Equal(t, map[string]struct{}{"b": x}, result)
|
||||
assert.Equal(t, map[string]struct{}{"a": x, "b": x, "c": x}, m1)
|
||||
assert.Equal(t, map[string]struct{}{"a": x, "c": x}, m2)
|
||||
}
|
||||
|
||||
func TestArraySubtract(t *testing.T) {
|
||||
a1 := []string{"a", "b", "c"}
|
||||
a2 := []string{"a", "c"}
|
||||
|
||||
result := arraySubtract(a1, a2)
|
||||
assert.Equal(t, []string{"b"}, result)
|
||||
assert.Equal(t, []string{"a", "b", "c"}, a1)
|
||||
assert.Equal(t, []string{"a", "c"}, a2)
|
||||
}
|
74
updater/sorter.go
Normal file
74
updater/sorter.go
Normal file
|
@ -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:]...)
|
||||
}
|
37
updater/sorter_test.go
Normal file
37
updater/sorter_test.go
Normal file
|
@ -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
|
||||
|
||||
import (
|
||||
"log"
|
||||
|
||||
"github.com/samalba/dockerclient"
|
||||
"github.com/CenturyLinkLabs/watchtower/docker"
|
||||
)
|
||||
|
||||
var (
|
||||
client dockerclient.Client
|
||||
)
|
||||
|
||||
func init() {
|
||||
docker, err := dockerclient.NewDockerClient("unix:///var/run/docker.sock", nil)
|
||||
if err != nil {
|
||||
log.Fatalf("Error instantiating Docker client: %s\n", err)
|
||||
}
|
||||
|
||||
client = docker
|
||||
}
|
||||
|
||||
func Run() error {
|
||||
containers, _ := client.ListContainers(false, false, "")
|
||||
client := docker.NewClient()
|
||||
containers, err := client.ListContainers()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
// First check to see if a newer image has already been built
|
||||
newImageInfo, _ := client.InspectImage(container.Image)
|
||||
|
||||
if newImageInfo.Id == oldImageInfo.Id {
|
||||
_ = client.PullImage(container.Image, nil)
|
||||
newImageInfo, _ = client.InspectImage(container.Image)
|
||||
for i := range containers {
|
||||
if err := client.RefreshImage(&containers[i]); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
newImageId := newImageInfo.Id
|
||||
log.Printf("Latest: %s (%s)\n", container.Image, newImageId)
|
||||
containers, err = sortContainers(containers)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if newImageId != oldImageId {
|
||||
log.Printf("Restarting %s with new image\n", name)
|
||||
if err := stopContainer(oldContainerInfo); err != nil {
|
||||
checkDependencies(containers)
|
||||
|
||||
// Stop stale containers in reverse order
|
||||
for i := len(containers) - 1; i >= 0; i-- {
|
||||
container := containers[i]
|
||||
if container.Stale {
|
||||
if err := client.Stop(container); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
config := GenerateContainerConfig(oldContainerInfo, oldImageInfo.Config)
|
||||
|
||||
hostConfig := oldContainerInfo.HostConfig
|
||||
_ = startContainer(name, config, hostConfig)
|
||||
// Restart stale containers in sorted order
|
||||
for _, container := range containers {
|
||||
if container.Stale {
|
||||
if err := client.Start(container); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func stopContainer(container *dockerclient.ContainerInfo) error {
|
||||
signal := "SIGTERM"
|
||||
|
||||
if sig, ok := container.Config.Labels["com.centurylinklabs.watchtower.stop-signal"]; ok {
|
||||
signal = sig
|
||||
}
|
||||
|
||||
if err := client.KillContainer(container.Id, signal); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return client.RemoveContainer(container.Id, true, false)
|
||||
func sortContainers(containers []docker.Container) ([]docker.Container, error) {
|
||||
sorter := ContainerSorter{}
|
||||
return sorter.Sort(containers)
|
||||
}
|
||||
|
||||
func startContainer(name string, config *dockerclient.ContainerConfig, hostConfig *dockerclient.HostConfig) error {
|
||||
newContainerId, err := client.CreateContainer(config, name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
func checkDependencies(containers []docker.Container) {
|
||||
|
||||
return client.StartContainer(newContainerId, hostConfig)
|
||||
for i, parent := range containers {
|
||||
if parent.Stale {
|
||||
continue
|
||||
}
|
||||
|
||||
LinkLoop:
|
||||
for _, linkName := range parent.Links() {
|
||||
for _, child := range containers {
|
||||
if child.Name() == linkName && child.Stale {
|
||||
containers[i].Stale = true
|
||||
break LinkLoop
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
29
updater/updater_test.go
Normal file
29
updater/updater_test.go
Normal file
|
@ -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…
Add table
Add a link
Reference in a new issue