You've already forked watchtower
							
							
				mirror of
				https://github.com/containrrr/watchtower.git
				synced 2025-10-31 00:17:44 +02:00 
			
		
		
		
	
							
								
								
									
										3
									
								
								README.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								README.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,3 @@ | ||||
| # Watchtower | ||||
|  | ||||
| A process for watching your Docker containers and automatically restarting them whenever their base image is refreshed. | ||||
							
								
								
									
										58
									
								
								main.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										58
									
								
								main.go
									
									
									
									
									
										Normal file
									
								
							| @@ -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) | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										112
									
								
								updater/config.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										112
									
								
								updater/config.go
									
									
									
									
									
										Normal file
									
								
							| @@ -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 | ||||
| } | ||||
							
								
								
									
										38
									
								
								updater/config_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										38
									
								
								updater/config_test.go
									
									
									
									
									
										Normal file
									
								
							| @@ -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) | ||||
| } | ||||
							
								
								
									
										81
									
								
								updater/updater.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										81
									
								
								updater/updater.go
									
									
									
									
									
										Normal file
									
								
							| @@ -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) | ||||
| } | ||||
		Reference in New Issue
	
	Block a user