From ce4ed7316c2090221478462124011fa4886817f7 Mon Sep 17 00:00:00 2001 From: Brian DeHamer Date: Mon, 13 Jul 2015 21:41:04 +0000 Subject: [PATCH] Initial commit Signed-off-by: Brian DeHamer --- README.md | 3 ++ main.go | 58 +++++++++++++++++++++ updater/config.go | 112 +++++++++++++++++++++++++++++++++++++++++ updater/config_test.go | 38 ++++++++++++++ updater/updater.go | 81 +++++++++++++++++++++++++++++ 5 files changed, 292 insertions(+) create mode 100644 README.md create mode 100644 main.go create mode 100644 updater/config.go create mode 100644 updater/config_test.go create mode 100644 updater/updater.go diff --git a/README.md b/README.md new file mode 100644 index 0000000..a5f7ebe --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# Watchtower + +A process for watching your Docker containers and automatically restarting them whenever their base image is refreshed. diff --git a/main.go b/main.go new file mode 100644 index 0000000..4b18d0c --- /dev/null +++ b/main.go @@ -0,0 +1,58 @@ +package main + +import ( + "os" + "os/signal" + "sync" + "syscall" + "time" + + "github.com/CenturyLinkLabs/watchtower/updater" + "github.com/codegangsta/cli" +) + +var ( + wg sync.WaitGroup +) + +func main() { + app := cli.NewApp() + app.Name = "watchtower" + app.Usage = "Automatically update running Docker containers" + app.Action = start + app.Flags = []cli.Flag{ + cli.IntFlag{ + Name: "interval, i", + Value: 300, + Usage: "poll interval (in seconds)", + }, + } + + handleSignals() + app.Run(os.Args) +} + +func handleSignals() { + // Graceful shut-down on SIGINT/SIGTERM + c := make(chan os.Signal, 1) + signal.Notify(c, os.Interrupt) + signal.Notify(c, syscall.SIGTERM) + + go func() { + <-c + wg.Wait() + os.Exit(1) + }() +} + +func start(c *cli.Context) { + secs := time.Duration(c.Int("interval")) * time.Second + + for { + wg.Add(1) + updater.Run() + wg.Done() + + time.Sleep(secs) + } +} diff --git a/updater/config.go b/updater/config.go new file mode 100644 index 0000000..10e8100 --- /dev/null +++ b/updater/config.go @@ -0,0 +1,112 @@ +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 +} diff --git a/updater/config_test.go b/updater/config_test.go new file mode 100644 index 0000000..7cd6111 --- /dev/null +++ b/updater/config_test.go @@ -0,0 +1,38 @@ +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) +} diff --git a/updater/updater.go b/updater/updater.go new file mode 100644 index 0000000..27cee40 --- /dev/null +++ b/updater/updater.go @@ -0,0 +1,81 @@ +package updater + +import ( + "log" + + "github.com/samalba/dockerclient" +) + +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, "") + + 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) + } + + newImageId := newImageInfo.Id + log.Printf("Latest: %s (%s)\n", container.Image, newImageId) + + if newImageId != oldImageId { + log.Printf("Restarting %s with new image\n", name) + if err := stopContainer(oldContainerInfo); err != nil { + } + + config := GenerateContainerConfig(oldContainerInfo, oldImageInfo.Config) + + hostConfig := oldContainerInfo.HostConfig + _ = startContainer(name, config, hostConfig) + } + } + + 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 startContainer(name string, config *dockerclient.ContainerConfig, hostConfig *dockerclient.HostConfig) error { + newContainerId, err := client.CreateContainer(config, name) + if err != nil { + return err + } + + return client.StartContainer(newContainerId, hostConfig) +}