commit
ce4ed7316c
@ -0,0 +1,3 @@
|
|||||||
|
# Watchtower
|
||||||
|
|
||||||
|
A process for watching your Docker containers and automatically restarting them whenever their base image is refreshed.
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
@ -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
|
||||||
|
}
|
@ -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)
|
||||||
|
}
|
@ -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)
|
||||||
|
}
|
Loading…
Reference in New Issue