From e38e790374424547daf3083fb9c389573e4b9737 Mon Sep 17 00:00:00 2001 From: Darren Shepherd Date: Sat, 14 Feb 2015 09:28:46 -0700 Subject: [PATCH] Add simple API to do most container functions --- docker/container.go | 290 +++++++++++++++++++++++++++++++++++++++ docker/container_test.go | 134 ++++++++++++++++++ 2 files changed, 424 insertions(+) create mode 100644 docker/container.go create mode 100644 docker/container_test.go diff --git a/docker/container.go b/docker/container.go new file mode 100644 index 00000000..b941b5cc --- /dev/null +++ b/docker/container.go @@ -0,0 +1,290 @@ +package docker + +import ( + "crypto/sha1" + "encoding/hex" + "encoding/json" + "errors" + "fmt" + "os" + "strings" + + flag "github.com/docker/docker/pkg/mflag" + "github.com/docker/docker/runconfig" + dockerClient "github.com/fsouza/go-dockerclient" + "github.com/rancherio/os/config" + "github.com/rancherio/os/util" +) + +const ( + LABEL = "label" + HASH = "io.rancher.os.hash" +) + +type Container struct { + Err error + Name string + remove bool + detach bool + Config *runconfig.Config + HostConfig *runconfig.HostConfig + cfg *config.Config + container *dockerClient.Container + containerCfg *config.ContainerConfig +} + +func getHash(containerCfg *config.ContainerConfig) (string, error) { + hash := sha1.New() + w := util.NewErrorWriter(hash) + + w.Write([]byte(containerCfg.Id)) + w.Write([]byte(strings.Join(containerCfg.Cmd, ":"))) + + if w.Err != nil { + return "", w.Err + } + + return hex.EncodeToString(hash.Sum([]byte{})), nil +} + +func StartAndWait(cfg *config.Config, containerCfg *config.ContainerConfig) error { + container := NewContainer(cfg, containerCfg).start(true) + return container.Err +} + +func NewContainer(cfg *config.Config, containerCfg *config.ContainerConfig) *Container { + return &Container{ + cfg: cfg, + containerCfg: containerCfg, + } +} + +func (c *Container) returnErr(err error) *Container { + c.Err = err + return c +} + +func (c *Container) Lookup() *Container { + c.Parse() + + if c.Err != nil || (c.container != nil && c.container.HostConfig != nil) { + return c + } + + hash, err := getHash(c.containerCfg) + if err != nil { + return c.returnErr(err) + } + + client, err := NewClient(c.cfg) + if err != nil { + return c.returnErr(err) + } + + containers, err := client.ListContainers(dockerClient.ListContainersOptions{ + All: true, + Filters: map[string][]string{ + LABEL: []string{fmt.Sprintf("%s=%s", HASH, hash)}, + }, + }) + if err != nil { + return c.returnErr(err) + } + + if len(containers) == 0 { + return c + } + + c.container, c.Err = client.InspectContainer(containers[0].ID) + + return c +} + +func (c *Container) Exists() bool { + c.Lookup() + return c.container != nil +} + +func (c *Container) Reset() *Container { + c.Config = nil + c.HostConfig = nil + c.container = nil + c.Err = nil + + return c +} + +func (c *Container) Parse() *Container { + if c.Config != nil || c.Err != nil { + return c + } + + flags := flag.NewFlagSet("run", flag.ExitOnError) + + flRemove := flags.Bool([]string{"#rm", "-rm"}, false, "") + flDetach := flags.Bool([]string{"d", "-detach"}, false, "") + flName := flags.String([]string{"#name", "-name"}, "", "") + + c.Config, c.HostConfig, _, c.Err = runconfig.Parse(flags, c.containerCfg.Cmd) + + c.Name = *flName + c.detach = *flDetach + c.remove = *flRemove + + if len(c.containerCfg.Id) == 0 { + c.containerCfg.Id = c.Name + } + + return c +} + +func (c *Container) Start() *Container { + return c.start(false) +} + +func (c *Container) Stage() *Container { + c.Parse() + + if c.Err != nil { + return c + } + + client, err := NewClient(c.cfg) + if err != nil { + c.Err = err + return c + } + + _, err = client.InspectImage(c.Config.Image) + if err == dockerClient.ErrNoSuchImage { + c.Err = client.PullImage(dockerClient.PullImageOptions{ + Repository: c.Config.Image, + OutputStream: os.Stdout, + }, dockerClient.AuthConfiguration{}) + } else if err != nil { + c.Err = err + } + + return c +} + +func (c *Container) Delete() *Container { + c.Parse() + c.Stage() + c.Lookup() + + if c.Err != nil { + return c + } + + if !c.Exists() { + return c + } + + client, err := NewClient(c.cfg) + if err != nil { + return c.returnErr(err) + } + + err = client.RemoveContainer(dockerClient.RemoveContainerOptions{ + ID: c.container.ID, + Force: true, + }) + if err != nil { + return c.returnErr(err) + } + + return c +} + +func renameOld(client *dockerClient.Client, opts *dockerClient.CreateContainerOptions) error { + if len(opts.Name) == 0 { + return nil + } + + existing, err := client.InspectContainer(opts.Name) + if _, ok := err.(dockerClient.NoSuchContainer); ok { + return nil + } + if err != nil { + return nil + } + + if label, ok := existing.Config.Labels[HASH]; ok { + return client.RenameContainer(existing.ID, fmt.Sprintf("%s-%s", existing.Name, label)) + } else { + //TODO: do something with containers with no hash + return errors.New("Existing container doesn't have a hash") + } +} + +func (c *Container) start(wait bool) *Container { + c.Lookup() + c.Stage() + + if c.Err != nil { + return c + } + + bytes, err := json.Marshal(c) + if err != nil { + return c.returnErr(err) + } + + client, err := NewClient(c.cfg) + if err != nil { + return c.returnErr(err) + } + + var opts dockerClient.CreateContainerOptions + container := c.container + created := false + + if !c.Exists() { + c.Err = json.Unmarshal(bytes, &opts) + if c.Err != nil { + return c + } + + if opts.Config.Labels == nil { + opts.Config.Labels = make(map[string]string) + } + + hash, err := getHash(c.containerCfg) + if err != nil { + return c.returnErr(err) + } + + opts.Config.Labels[HASH] = hash + + err = renameOld(client, &opts) + if err != nil { + return c.returnErr(err) + } + + container, err = client.CreateContainer(opts) + created = true + if err != nil { + return c.returnErr(err) + } + } + + c.container = container + + hostConfig := container.HostConfig + if created { + hostConfig = opts.HostConfig + } + + err = client.StartContainer(container.ID, hostConfig) + if err != nil { + return c.returnErr(err) + } + + if !c.detach && wait { + _, c.Err = client.WaitContainer(container.ID) + return c + } + + return c +} diff --git a/docker/container_test.go b/docker/container_test.go new file mode 100644 index 00000000..b0f18f4b --- /dev/null +++ b/docker/container_test.go @@ -0,0 +1,134 @@ +package docker + +import ( + "testing" + + "github.com/rancherio/os/config" + "github.com/stretchr/testify/require" +) + +func TestHash(t *testing.T) { + assert := require.New(t) + + hash, err := getHash(&config.ContainerConfig{ + Id: "id", + Cmd: []string{"1", "2", "3"}, + }) + assert.NoError(err, "") + + hash2, err := getHash(&config.ContainerConfig{ + Id: "id2", + Cmd: []string{"1", "2", "3"}, + }) + assert.NoError(err, "") + + hash3, err := getHash(&config.ContainerConfig{ + Id: "id3", + Cmd: []string{"1", "2", "3", "4"}, + }) + assert.NoError(err, "") + + assert.Equal("44096e94ed438ccda24e459412147441a376ea1c", hash, "") + assert.NotEqual(hash, hash2, "") + assert.NotEqual(hash2, hash3, "") + assert.NotEqual(hash, hash3, "") +} + +func TestParse(t *testing.T) { + assert := require.New(t) + + cfg := &config.ContainerConfig{ + Cmd: []string{ + "--name", "c1", + "-d", + "--rm", + "--privileged", + "test/image", + "arg1", + "arg2", + }, + } + + c := NewContainer(nil, cfg).Parse() + + assert.NoError(c.Err, "") + assert.Equal(cfg.Id, "c1", "Id doesn't match") + assert.Equal(c.Name, "c1", "Name doesn't match") + assert.True(c.remove, "Remove doesn't match") + assert.True(c.detach, "Detach doesn't match") + assert.Equal(len(c.Config.Cmd), 2, "Args doesn't match") + assert.Equal(c.Config.Cmd[0], "arg1", "Arg1 doesn't match") + assert.Equal(c.Config.Cmd[1], "arg2", "Arg2 doesn't match") + assert.True(c.HostConfig.Privileged, "Privileged doesn't match") +} + +func TestStart(t *testing.T) { + assert := require.New(t) + + c := NewContainer(nil, &config.ContainerConfig{ + Cmd: []string{"--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, "") + + c.Delete() +} + +func TestLookup(t *testing.T) { + assert := require.New(t) + + cfg := &config.ContainerConfig{ + Cmd: []string{"--rm", "busybox", "echo", "hi"}, + } + c := NewContainer(nil, cfg).Parse().Start() + + cfg2 := &config.ContainerConfig{ + Cmd: []string{"--rm", "busybox", "echo", "hi2"}, + } + c2 := NewContainer(nil, cfg2).Parse().Start() + + assert.NoError(c.Err, "") + assert.NoError(c2.Err, "") + + c1Lookup := NewContainer(nil, cfg).Lookup() + c2Lookup := NewContainer(nil, 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, "") + + c.Delete() + c2.Delete() +} + +func TestDelete(t *testing.T) { + assert := require.New(t) + + c := NewContainer(nil, &config.ContainerConfig{ + Cmd: []string{"--rm", "busybox", "echo", "hi"}, + }).Parse() + + assert.False(c.Exists()) + assert.NoError(c.Err, "") + + c.Start() + assert.NoError(c.Err, "") + c.Reset() + assert.NoError(c.Err, "") + + assert.True(c.Exists()) + assert.NoError(c.Err, "") + + c.Delete() + assert.NoError(c.Err, "") + + c.Reset() + assert.False(c.Exists()) + assert.NoError(c.Err, "") +}