From 7c17cc78250307b8d89cd0ebb13c962cc22ac037 Mon Sep 17 00:00:00 2001 From: Darren Shepherd Date: Thu, 19 Feb 2015 08:35:17 -0700 Subject: [PATCH] Better rollback/restart/upgrade container --- docker/container.go | 203 ++++++++++++++++++++++++++++++++------- docker/container_test.go | 164 +++++++++++++++++++++++++------ 2 files changed, 306 insertions(+), 61 deletions(-) diff --git a/docker/container.go b/docker/container.go index 1dd8c583..8750c6ec 100644 --- a/docker/container.go +++ b/docker/container.go @@ -7,6 +7,7 @@ import ( "errors" "fmt" "os" + "sort" "strings" log "github.com/Sirupsen/logrus" @@ -21,6 +22,7 @@ import ( const ( LABEL = "label" HASH = "io.rancher.os.hash" + ID = "io.rancher.os.id" ) type Container struct { @@ -31,10 +33,16 @@ type Container struct { Config *runconfig.Config HostConfig *runconfig.HostConfig dockerHost string - container *dockerClient.Container + Container *dockerClient.Container containerCfg *config.ContainerConfig } +type ByCreated []dockerClient.APIContainers + +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 { return c[j].Created < c[i].Created } + func getHash(containerCfg *config.ContainerConfig) (string, error) { hash := sha1.New() w := util.NewErrorWriter(hash) @@ -67,10 +75,30 @@ func (c *Container) returnErr(err error) *Container { return c } +func getByLabel(client *dockerClient.Client, key, value string) (*dockerClient.APIContainers, error) { + containers, err := client.ListContainers(dockerClient.ListContainersOptions{ + All: true, + Filters: map[string][]string{ + LABEL: []string{fmt.Sprintf("%s=%s", key, value)}, + }, + }) + + if err != nil { + return nil, err + } + + if len(containers) == 0 { + return nil, nil + } + + sort.Sort(ByCreated(containers)) + return &containers[0], nil +} + func (c *Container) Lookup() *Container { c.Parse() - if c.Err != nil || (c.container != nil && c.container.HostConfig != nil) { + if c.Err != nil || (c.Container != nil && c.Container.HostConfig != nil) { return c } @@ -98,20 +126,33 @@ func (c *Container) Lookup() *Container { return c } - c.container, c.Err = client.InspectContainer(containers[0].ID) + c.Container, c.Err = inspect(client, containers[0].ID) return c } +func inspect(client *dockerClient.Client, id string) (*dockerClient.Container, error) { + c, err := client.InspectContainer(id) + if err != nil { + return nil, err + } + + if strings.HasPrefix(c.Name, "/") { + c.Name = c.Name[1:] + } + + return c, err +} + func (c *Container) Exists() bool { c.Lookup() - return c.container != nil + return c.Container != nil } func (c *Container) Reset() *Container { c.Config = nil c.HostConfig = nil - c.container = nil + c.Container = nil c.Err = nil return c @@ -200,7 +241,7 @@ func (c *Container) Delete() *Container { } err = client.RemoveContainer(dockerClient.RemoveContainerOptions{ - ID: c.container.ID, + ID: c.Container.ID, Force: true, }) if err != nil { @@ -210,27 +251,112 @@ func (c *Container) Delete() *Container { return c } -func renameOld(client *dockerClient.Client, opts *dockerClient.CreateContainerOptions) error { +func (c *Container) renameCurrent(client *dockerClient.Client) error { + if c.Name == "" { + return nil + } + + if c.Name == c.Container.Name { + return nil + } + + err := client.RenameContainer(c.Container.ID, c.Name) + if err != nil { + return err + } + + c.Container, err = inspect(client, c.Container.ID) + return err +} + +func (c *Container) renameOld(client *dockerClient.Client, opts *dockerClient.CreateContainerOptions) error { if len(opts.Name) == 0 { return nil } - existing, err := client.InspectContainer(opts.Name) + existing, err := inspect(client, opts.Name) if _, ok := err.(dockerClient.NoSuchContainer); ok { return nil } + if err != nil { return nil } + if c.Container != nil && existing.ID == c.Container.ID { + return nil + } + if label, ok := existing.Config.Labels[HASH]; ok { - return client.RenameContainer(existing.ID, fmt.Sprintf("%s-%s", existing.Name, label)) + newName := fmt.Sprintf("%s-%s", existing.Name, label) + + if existing.State.Running { + err := client.StopContainer(existing.ID, 2) + if err != nil { + return err + } + + _, err = client.WaitContainer(existing.ID) + if err != nil { + return err + } + } + + log.Debugf("Renaming %s to %s", existing.Name, newName) + return client.RenameContainer(existing.ID, newName) } else { //TODO: do something with containers with no hash return errors.New("Existing container doesn't have a hash") } } +func (c *Container) getCreateOpts(client *dockerClient.Client) (*dockerClient.CreateContainerOptions, error) { + bytes, err := json.Marshal(c) + if err != nil { + return nil, err + } + + var opts dockerClient.CreateContainerOptions + + err = json.Unmarshal(bytes, &opts) + if err != nil { + return nil, err + } + + if opts.Config.Labels == nil { + opts.Config.Labels = make(map[string]string) + } + + hash, err := getHash(c.containerCfg) + if err != nil { + return nil, err + } + + opts.Config.Labels[HASH] = hash + opts.Config.Labels[ID] = c.containerCfg.Id + + return &opts, nil +} + +func appendVolumesFrom(client *dockerClient.Client, containerCfg *config.ContainerConfig, opts *dockerClient.CreateContainerOptions) error { + if !containerCfg.MigrateVolumes { + return nil + } + + container, err := getByLabel(client, ID, containerCfg.Id) + if err != nil || container == nil { + return err + } + + if opts.HostConfig.VolumesFrom == nil { + opts.HostConfig.VolumesFrom = []string{container.ID} + } else { + opts.HostConfig.VolumesFrom = append(opts.HostConfig.VolumesFrom, container.ID) + } + + return nil +} + func (c *Container) start(wait bool) *Container { c.Lookup() c.Stage() @@ -239,63 +365,74 @@ func (c *Container) start(wait bool) *Container { return c } - bytes, err := json.Marshal(c) - if err != nil { - return c.returnErr(err) - } - client, err := NewClient(c.dockerHost) if err != nil { return c.returnErr(err) } - var opts dockerClient.CreateContainerOptions - container := c.container + container := c.Container created := false - if !c.Exists() { - c.Err = json.Unmarshal(bytes, &opts) + opts, err := c.getCreateOpts(client) + if err != nil { + return c.returnErr(err) + } + + if c.Exists() && c.remove { + log.Debugf("Deleting container %s", c.Container.ID) + c.Delete().Reset().Lookup() + if c.Err != nil { return c } + } - if opts.Config.Labels == nil { - opts.Config.Labels = make(map[string]string) - } - - hash, err := getHash(c.containerCfg) + if !c.Exists() { + err = c.renameOld(client, opts) if err != nil { return c.returnErr(err) } - opts.Config.Labels[HASH] = hash - - err = renameOld(client, &opts) + err := appendVolumesFrom(client, c.containerCfg, opts) if err != nil { return c.returnErr(err) } - container, err = client.CreateContainer(opts) + container, err = client.CreateContainer(*opts) created = true if err != nil { return c.returnErr(err) } } - c.container = container + c.Container = container - hostConfig := container.HostConfig + hostConfig := c.Container.HostConfig if created { hostConfig = opts.HostConfig } - err = client.StartContainer(container.ID, hostConfig) - if err != nil { - return c.returnErr(err) + if !c.Container.State.Running { + if !created { + err = c.renameOld(client, opts) + if err != nil { + return c.returnErr(err) + } + } + + err = c.renameCurrent(client) + if err != nil { + return c.returnErr(err) + } + + err = client.StartContainer(c.Container.ID, hostConfig) + if err != nil { + return c.returnErr(err) + } } if !c.detach && wait { - _, c.Err = client.WaitContainer(container.ID) + _, c.Err = client.WaitContainer(c.Container.ID) return c } diff --git a/docker/container_test.go b/docker/container_test.go index b0f18f4b..71c3fcfc 100644 --- a/docker/container_test.go +++ b/docker/container_test.go @@ -1,10 +1,13 @@ package docker import ( + "strings" "testing" "github.com/rancherio/os/config" "github.com/stretchr/testify/require" + + dockerClient "github.com/fsouza/go-dockerclient" ) func TestHash(t *testing.T) { @@ -12,23 +15,23 @@ func TestHash(t *testing.T) { hash, err := getHash(&config.ContainerConfig{ Id: "id", - Cmd: []string{"1", "2", "3"}, + Cmd: "1 2 3", }) assert.NoError(err, "") hash2, err := getHash(&config.ContainerConfig{ Id: "id2", - Cmd: []string{"1", "2", "3"}, + Cmd: "1 2 3", }) assert.NoError(err, "") hash3, err := getHash(&config.ContainerConfig{ Id: "id3", - Cmd: []string{"1", "2", "3", "4"}, + Cmd: "1 2 3 4", }) assert.NoError(err, "") - assert.Equal("44096e94ed438ccda24e459412147441a376ea1c", hash, "") + assert.Equal("510b68938cba936876588b0143093a5850d4a142", hash, "") assert.NotEqual(hash, hash2, "") assert.NotEqual(hash2, hash3, "") assert.NotEqual(hash, hash3, "") @@ -38,18 +41,16 @@ func TestParse(t *testing.T) { assert := require.New(t) cfg := &config.ContainerConfig{ - Cmd: []string{ - "--name", "c1", - "-d", - "--rm", - "--privileged", - "test/image", - "arg1", - "arg2", - }, + Cmd: "--name c1 " + + "-d " + + "--rm " + + "--privileged " + + "test/image " + + "arg1 " + + "arg2 ", } - c := NewContainer(nil, cfg).Parse() + c := NewContainer("", cfg).Parse() assert.NoError(c.Err, "") assert.Equal(cfg.Id, "c1", "Id doesn't match") @@ -62,18 +63,93 @@ func TestParse(t *testing.T) { assert.True(c.HostConfig.Privileged, "Privileged doesn't match") } +func TestIdFromName(t *testing.T) { + assert := require.New(t) + + cfg := &config.ContainerConfig{ + Cmd: "--name foo -v /test busybox echo hi", + } + + assert.Equal("", cfg.Id) + NewContainer(config.DOCKER_HOST, cfg) + assert.Equal("foo", cfg.Id) +} + +func TestMigrateVolumes(t *testing.T) { + assert := require.New(t) + + c := NewContainer(config.DOCKER_HOST, &config.ContainerConfig{ + Cmd: "--name foo -v /test busybox echo hi", + }).Parse().Start().Lookup() + + assert.NoError(c.Err, "") + + test_path, ok := c.Container.Volumes["/test"] + assert.True(ok, "") + + c2 := NewContainer(config.DOCKER_HOST, &config.ContainerConfig{ + MigrateVolumes: true, + Cmd: "--name foo -v /test2 busybox echo hi", + }).Parse().Start().Lookup() + + assert.NoError(c2.Err, "") + + assert.True(c2.Container != nil) + + _, ok = c2.Container.Volumes["/test2"] + assert.True(ok, "") + assert.Equal(test_path, c2.Container.Volumes["/test"]) + + c.Delete() + c2.Delete() +} + +func TestRollback(t *testing.T) { + assert := require.New(t) + + c := NewContainer(config.DOCKER_HOST, &config.ContainerConfig{ + Cmd: "--name rollback busybox echo hi", + }).Parse().Start().Lookup() + + assert.NoError(c.Err, "") + assert.Equal("rollback", c.Container.Name) + + c2 := NewContainer(config.DOCKER_HOST, &config.ContainerConfig{ + Cmd: "--name rollback busybox echo bye", + }).Parse().Start().Lookup() + + assert.Equal("rollback", c2.Container.Name) + assert.NoError(c2.Err, "") + assert.NotEqual(c.Container.ID, c2.Container.ID) + + c3 := NewContainer(config.DOCKER_HOST, &config.ContainerConfig{ + Cmd: "--name rollback busybox echo hi", + }).Parse().Start().Lookup() + + assert.NoError(c3.Err, "") + assert.Equal(c.Container.ID, c3.Container.ID) + assert.Equal("rollback", c3.Container.Name) + + c2.Reset().Lookup() + assert.NoError(c2.Err, "") + assert.True(strings.HasPrefix(c2.Container.Name, "rollback-")) + + c.Delete() + c2.Delete() +} + func TestStart(t *testing.T) { assert := require.New(t) - c := NewContainer(nil, &config.ContainerConfig{ - Cmd: []string{"--pid=host", "--privileged", "--rm", "busybox", "echo", "hi"}, + c := NewContainer(config.DOCKER_HOST, &config.ContainerConfig{ + Cmd: "--pid=host --privileged --rm busybox echo hi", }).Parse().Start().Lookup() assert.NoError(c.Err, "") assert.True(c.HostConfig.Privileged, "") - assert.True(c.container.HostConfig.Privileged, "") - assert.Equal("host", c.container.HostConfig.PidMode, "") + assert.True(c.Container.HostConfig.Privileged, "") + assert.Equal("host", c.Container.HostConfig.PidMode, "") c.Delete() } @@ -82,26 +158,26 @@ func TestLookup(t *testing.T) { assert := require.New(t) cfg := &config.ContainerConfig{ - Cmd: []string{"--rm", "busybox", "echo", "hi"}, + Cmd: "--rm busybox echo hi", } - c := NewContainer(nil, cfg).Parse().Start() + c := NewContainer(config.DOCKER_HOST, cfg).Parse().Start() cfg2 := &config.ContainerConfig{ - Cmd: []string{"--rm", "busybox", "echo", "hi2"}, + Cmd: "--rm busybox echo hi2", } - c2 := NewContainer(nil, cfg2).Parse().Start() + c2 := NewContainer(config.DOCKER_HOST, cfg2).Parse().Start() assert.NoError(c.Err, "") assert.NoError(c2.Err, "") - c1Lookup := NewContainer(nil, cfg).Lookup() - c2Lookup := NewContainer(nil, cfg2).Lookup() + c1Lookup := NewContainer(config.DOCKER_HOST, cfg).Lookup() + c2Lookup := NewContainer(config.DOCKER_HOST, cfg2).Lookup() assert.NoError(c1Lookup.Err, "") assert.NoError(c2Lookup.Err, "") - assert.Equal(c.container.ID, c1Lookup.container.ID, "") - assert.Equal(c2.container.ID, c2Lookup.container.ID, "") + assert.Equal(c.Container.ID, c1Lookup.Container.ID, "") + assert.Equal(c2.Container.ID, c2Lookup.Container.ID, "") c.Delete() c2.Delete() @@ -110,8 +186,8 @@ func TestLookup(t *testing.T) { func TestDelete(t *testing.T) { assert := require.New(t) - c := NewContainer(nil, &config.ContainerConfig{ - Cmd: []string{"--rm", "busybox", "echo", "hi"}, + c := NewContainer(config.DOCKER_HOST, &config.ContainerConfig{ + Cmd: "--rm busybox echo hi", }).Parse() assert.False(c.Exists()) @@ -132,3 +208,35 @@ func TestDelete(t *testing.T) { assert.False(c.Exists()) assert.NoError(c.Err, "") } + +func TestDockerClientNames(t *testing.T) { + assert := require.New(t) + client, err := dockerClient.NewClient(config.DOCKER_HOST) + + assert.NoError(err, "") + + c, err := client.CreateContainer(dockerClient.CreateContainerOptions{ + Name: "foo", + Config: &dockerClient.Config{ + Image: "ubuntu", + }, + }) + + assert.NoError(err, "") + assert.Equal("foo", c.Name) + + c2, err := client.InspectContainer(c.ID) + + assert.NoError(err, "") + assert.Equal("/foo", c2.Name) + + c2, err = inspect(client, c.ID) + + assert.NoError(err, "") + assert.Equal("foo", c2.Name) + + client.RemoveContainer(dockerClient.RemoveContainerOptions{ + ID: c2.ID, + Force: true, + }) +}