From 00f2875abf77bd798278bca310c8593435fbb233 Mon Sep 17 00:00:00 2001 From: Brian DeHamer Date: Tue, 21 Jul 2015 16:04:41 +0000 Subject: [PATCH] Refactoring & renaming --- actions/check.go | 29 ++++++++ updater/updater.go => actions/update.go | 46 ++++-------- actions/update_test.go | 38 ++++++++++ {docker => container}/client.go | 16 ++-- {docker => container}/client_test.go | 2 +- {docker => container}/container.go | 23 +----- {docker => container}/container_test.go | 26 +------ container/sort.go | 98 +++++++++++++++++++++++++ container/sort_test.go | 59 +++++++++++++++ {docker => container}/util.go | 2 +- {docker => container}/util_test.go | 2 +- main.go | 6 +- updater/sorter.go | 74 ------------------- updater/sorter_test.go | 37 ---------- updater/updater_test.go | 29 -------- 15 files changed, 254 insertions(+), 233 deletions(-) create mode 100644 actions/check.go rename updater/updater.go => actions/update.go (63%) create mode 100644 actions/update_test.go rename {docker => container}/client.go (90%) rename {docker => container}/client_test.go (99%) rename {docker => container}/container.go (86%) rename {docker => container}/container_test.go (71%) create mode 100644 container/sort.go create mode 100644 container/sort_test.go rename {docker => container}/util.go (97%) rename {docker => container}/util_test.go (98%) delete mode 100644 updater/sorter.go delete mode 100644 updater/sorter_test.go delete mode 100644 updater/updater_test.go diff --git a/actions/check.go b/actions/check.go new file mode 100644 index 0000000..d57dfb0 --- /dev/null +++ b/actions/check.go @@ -0,0 +1,29 @@ +package actions + +import ( + "sort" + + "github.com/CenturyLinkLabs/watchtower/container" +) + +func watchtowerContainersFilter(c container.Container) bool { return c.IsWatchtower() } + +func CheckPrereqs() error { + client := container.NewClient() + + containers, err := client.ListContainers(watchtowerContainersFilter) + if err != nil { + return err + } + + if len(containers) > 1 { + sort.Sort(container.ByCreated(containers)) + + // Iterate over all containers execept the last one + for _, c := range containers[0 : len(containers)-1] { + client.Stop(c, 60) + } + } + + return nil +} diff --git a/updater/updater.go b/actions/update.go similarity index 63% rename from updater/updater.go rename to actions/update.go index 8031bcb..1aeb4c8 100644 --- a/updater/updater.go +++ b/actions/update.go @@ -1,37 +1,20 @@ -package updater +package actions import ( "math/rand" - "sort" - "github.com/CenturyLinkLabs/watchtower/docker" + "github.com/CenturyLinkLabs/watchtower/container" ) -var letters = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ") - -func CheckPrereqs() error { - client := docker.NewClient() - - containers, err := client.ListContainers(docker.WatchtowerContainersFilter) - if err != nil { - return err - } - - if len(containers) > 1 { - sort.Sort(docker.ByCreated(containers)) - - // Iterate over all containers execept the last one - for _, c := range containers[0 : len(containers)-1] { - client.Stop(c, 60) - } - } +var ( + letters = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ") +) - return nil -} +func allContainersFilter(container.Container) bool { return true } -func Run() error { - client := docker.NewClient() - containers, err := client.ListContainers(docker.AllContainersFilter) +func Update() error { + client := container.NewClient() + containers, err := client.ListContainers(allContainersFilter) if err != nil { return err } @@ -42,7 +25,7 @@ func Run() error { } } - containers, err = sortContainers(containers) + containers, err = container.SortByDependencies(containers) if err != nil { return err } @@ -86,12 +69,7 @@ func Run() error { return nil } -func sortContainers(containers []docker.Container) ([]docker.Container, error) { - sorter := ContainerSorter{} - return sorter.Sort(containers) -} - -func checkDependencies(containers []docker.Container) { +func checkDependencies(containers []container.Container) { for i, parent := range containers { if parent.Stale { @@ -110,10 +88,12 @@ func checkDependencies(containers []docker.Container) { } } +// Generates a random, 32-character, Docker-compatible container name. func randName() string { b := make([]rune, 32) for i := range b { b[i] = letters[rand.Intn(len(letters))] } + return string(b) } diff --git a/actions/update_test.go b/actions/update_test.go new file mode 100644 index 0000000..0d104d3 --- /dev/null +++ b/actions/update_test.go @@ -0,0 +1,38 @@ +package actions + +import ( + "regexp" + "testing" + + "github.com/CenturyLinkLabs/watchtower/container" + "github.com/stretchr/testify/assert" +) + +func TestCheckDependencies(t *testing.T) { + cs := []container.Container{ + container.NewTestContainer("1", []string{}), + container.NewTestContainer("2", []string{"1:"}), + container.NewTestContainer("3", []string{"2:"}), + container.NewTestContainer("4", []string{"3:"}), + container.NewTestContainer("5", []string{"4:"}), + container.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) +} + +func TestRandName(t *testing.T) { + validPattern := regexp.MustCompile(`^[a-zA-Z0-9][a-zA-Z0-9_.-]+$`) + + name := randName() + + assert.True(t, validPattern.MatchString(name)) +} diff --git a/docker/client.go b/container/client.go similarity index 90% rename from docker/client.go rename to container/client.go index 5de2ef0..359537c 100644 --- a/docker/client.go +++ b/container/client.go @@ -1,4 +1,4 @@ -package docker +package container import ( "fmt" @@ -9,6 +9,11 @@ import ( "github.com/samalba/dockerclient" ) +const ( + defaultStopSignal = "SIGTERM" + signalLabel = "com.centurylinklabs.watchtower.stop-signal" +) + var ( pullImages bool ) @@ -19,9 +24,6 @@ func init() { type ContainerFilter func(Container) bool -func AllContainersFilter(Container) bool { return true } -func WatchtowerContainersFilter(c Container) bool { return c.IsWatchtower() } - type Client interface { ListContainers(ContainerFilter) ([]Container, error) RefreshImage(*Container) error @@ -102,9 +104,9 @@ func (client DockerClient) RefreshImage(c *Container) error { } func (client DockerClient) Stop(c Container, timeout time.Duration) error { - signal := "SIGTERM" + signal := defaultStopSignal - if sig, ok := c.containerInfo.Config.Labels["com.centurylinklabs.watchtower.stop-signal"]; ok { + if sig, ok := c.containerInfo.Config.Labels[signalLabel]; ok { signal = sig } @@ -114,7 +116,7 @@ func (client DockerClient) Stop(c Container, timeout time.Duration) error { return err } - // Wait for container to exit, but proceed anyway after 10 seconds + // Wait for container to exit, but proceed anyway after the timeout elapses client.waitForStop(c, timeout) return client.api.RemoveContainer(c.containerInfo.Id, true, false) diff --git a/docker/client_test.go b/container/client_test.go similarity index 99% rename from docker/client_test.go rename to container/client_test.go index 324887c..18a6e50 100644 --- a/docker/client_test.go +++ b/container/client_test.go @@ -1,4 +1,4 @@ -package docker +package container import ( "errors" diff --git a/docker/container.go b/container/container.go similarity index 86% rename from docker/container.go rename to container/container.go index 1739983..0bddc86 100644 --- a/docker/container.go +++ b/container/container.go @@ -1,9 +1,8 @@ -package docker +package container import ( "fmt" "strings" - "time" "github.com/samalba/dockerclient" ) @@ -97,26 +96,6 @@ func (c Container) hostConfig() *dockerclient.HostConfig { return hostConfig } -// Sort containers by Created date -type ByCreated []Container - -func (c ByCreated) Len() int { return len(c) } -func (c ByCreated) Swap(i, j int) { c[i], c[j] = c[j], c[i] } - -func (c ByCreated) Less(i, j int) bool { - t1, err := time.Parse(time.RFC3339Nano, c[i].containerInfo.Created) - if err != nil { - t1 = time.Now() - } - - t2, _ := time.Parse(time.RFC3339Nano, c[j].containerInfo.Created) - if err != nil { - t1 = time.Now() - } - - return t1.Before(t2) -} - func NewTestContainer(name string, links []string) Container { return Container{ containerInfo: &dockerclient.ContainerInfo{ diff --git a/docker/container_test.go b/container/container_test.go similarity index 71% rename from docker/container_test.go rename to container/container_test.go index 1255e02..4d2237f 100644 --- a/docker/container_test.go +++ b/container/container_test.go @@ -1,7 +1,6 @@ -package docker +package container import ( - "sort" "testing" "github.com/samalba/dockerclient" @@ -67,26 +66,3 @@ func TestIsWatchtower_NoLabel(t *testing.T) { assert.False(t, c.IsWatchtower()) } - -func TestByCreated(t *testing.T) { - c1 := Container{ - containerInfo: &dockerclient.ContainerInfo{ - Created: "2015-07-01T12:00:01.000000000Z", - }, - } - c2 := Container{ - containerInfo: &dockerclient.ContainerInfo{ - Created: "2015-07-01T12:00:02.000000000Z", - }, - } - c3 := Container{ - containerInfo: &dockerclient.ContainerInfo{ - Created: "2015-07-01T12:00:02.000000001Z", - }, - } - cs := []Container{c3, c2, c1} - - sort.Sort(ByCreated(cs)) - - assert.Equal(t, []Container{c1, c2, c3}, cs) -} diff --git a/container/sort.go b/container/sort.go new file mode 100644 index 0000000..3465292 --- /dev/null +++ b/container/sort.go @@ -0,0 +1,98 @@ +package container + +import ( + "fmt" + "time" +) + +// Sort containers by Created date +type ByCreated []Container + +func (c ByCreated) Len() int { return len(c) } +func (c ByCreated) Swap(i, j int) { c[i], c[j] = c[j], c[i] } + +func (c ByCreated) Less(i, j int) bool { + t1, err := time.Parse(time.RFC3339Nano, c[i].containerInfo.Created) + if err != nil { + t1 = time.Now() + } + + t2, _ := time.Parse(time.RFC3339Nano, c[j].containerInfo.Created) + if err != nil { + t1 = time.Now() + } + + return t1.Before(t2) +} + +func SortByDependencies(containers []Container) ([]Container, error) { + sorter := dependencySorter{} + return sorter.Sort(containers) +} + +type dependencySorter struct { + unvisited []Container + marked map[string]bool + sorted []Container +} + +func (ds *dependencySorter) Sort(containers []Container) ([]Container, error) { + ds.unvisited = containers + ds.marked = map[string]bool{} + + for len(ds.unvisited) > 0 { + if err := ds.visit(ds.unvisited[0]); err != nil { + return nil, err + } + } + + return ds.sorted, nil +} + +func (ds *dependencySorter) visit(c Container) error { + + if _, ok := ds.marked[c.Name()]; ok { + return fmt.Errorf("Circular reference to %s", c.Name()) + } + + // Mark any visited node so that circular references can be detected + ds.marked[c.Name()] = true + defer delete(ds.marked, c.Name()) + + // Recursively visit links + for _, linkName := range c.Links() { + if linkedContainer := ds.findUnvisited(linkName); linkedContainer != nil { + if err := ds.visit(*linkedContainer); err != nil { + return err + } + } + } + + // Move container from unvisited to sorted + ds.removeUnvisited(c) + ds.sorted = append(ds.sorted, c) + + return nil +} + +func (ds *dependencySorter) findUnvisited(name string) *Container { + for _, c := range ds.unvisited { + if c.Name() == name { + return &c + } + } + + return nil +} + +func (ds *dependencySorter) removeUnvisited(c Container) { + var idx int + for i := range ds.unvisited { + if ds.unvisited[i].Name() == c.Name() { + idx = i + break + } + } + + ds.unvisited = append(ds.unvisited[0:idx], ds.unvisited[idx+1:]...) +} diff --git a/container/sort_test.go b/container/sort_test.go new file mode 100644 index 0000000..8bb1ce0 --- /dev/null +++ b/container/sort_test.go @@ -0,0 +1,59 @@ +package container + +import ( + "sort" + "testing" + + "github.com/samalba/dockerclient" + "github.com/stretchr/testify/assert" +) + +func TestByCreated(t *testing.T) { + c1 := Container{ + containerInfo: &dockerclient.ContainerInfo{ + Created: "2015-07-01T12:00:01.000000000Z", + }, + } + c2 := Container{ + containerInfo: &dockerclient.ContainerInfo{ + Created: "2015-07-01T12:00:02.000000000Z", + }, + } + c3 := Container{ + containerInfo: &dockerclient.ContainerInfo{ + Created: "2015-07-01T12:00:02.000000001Z", + }, + } + cs := []Container{c3, c2, c1} + + sort.Sort(ByCreated(cs)) + + assert.Equal(t, []Container{c1, c2, c3}, cs) +} + +func TestSortByDependencies_Success(t *testing.T) { + c1 := NewTestContainer("1", []string{}) + c2 := NewTestContainer("2", []string{"1:"}) + c3 := NewTestContainer("3", []string{"2:"}) + c4 := NewTestContainer("4", []string{"3:"}) + c5 := NewTestContainer("5", []string{"4:"}) + c6 := NewTestContainer("6", []string{"5:", "3:"}) + containers := []Container{c6, c2, c4, c1, c3, c5} + + result, err := SortByDependencies(containers) + + assert.NoError(t, err) + assert.Equal(t, []Container{c1, c2, c3, c4, c5, c6}, result) +} + +func TestSortByDependencies_Error(t *testing.T) { + c1 := NewTestContainer("1", []string{"3:"}) + c2 := NewTestContainer("2", []string{"1:"}) + c3 := NewTestContainer("3", []string{"2:"}) + containers := []Container{c1, c2, c3} + + _, err := SortByDependencies(containers) + + assert.Error(t, err) + assert.EqualError(t, err, "Circular reference to 1") +} diff --git a/docker/util.go b/container/util.go similarity index 97% rename from docker/util.go rename to container/util.go index 8dbe214..3db01d1 100644 --- a/docker/util.go +++ b/container/util.go @@ -1,4 +1,4 @@ -package docker +package container func sliceEqual(s1, s2 []string) bool { if len(s1) != len(s2) { diff --git a/docker/util_test.go b/container/util_test.go similarity index 98% rename from docker/util_test.go rename to container/util_test.go index d82a161..c4d5ca7 100644 --- a/docker/util_test.go +++ b/container/util_test.go @@ -1,4 +1,4 @@ -package docker +package container import ( "testing" diff --git a/main.go b/main.go index be74a44..e957ced 100644 --- a/main.go +++ b/main.go @@ -8,7 +8,7 @@ import ( "syscall" "time" - "github.com/CenturyLinkLabs/watchtower/updater" + "github.com/CenturyLinkLabs/watchtower/actions" "github.com/codegangsta/cli" ) @@ -48,7 +48,7 @@ func handleSignals() { } func before(c *cli.Context) error { - return updater.CheckPrereqs() + return actions.CheckPrereqs() } func start(c *cli.Context) { @@ -56,7 +56,7 @@ func start(c *cli.Context) { for { wg.Add(1) - if err := updater.Run(); err != nil { + if err := actions.Update(); err != nil { fmt.Println(err) } wg.Done() diff --git a/updater/sorter.go b/updater/sorter.go deleted file mode 100644 index 3cd099f..0000000 --- a/updater/sorter.go +++ /dev/null @@ -1,74 +0,0 @@ -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:]...) -} diff --git a/updater/sorter_test.go b/updater/sorter_test.go deleted file mode 100644 index 1314b70..0000000 --- a/updater/sorter_test.go +++ /dev/null @@ -1,37 +0,0 @@ -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") -} diff --git a/updater/updater_test.go b/updater/updater_test.go deleted file mode 100644 index b8c1f4f..0000000 --- a/updater/updater_test.go +++ /dev/null @@ -1,29 +0,0 @@ -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) -}