From dce884d5c89ff43f2134e2adc43266eeeee50208 Mon Sep 17 00:00:00 2001 From: Chris Crone Date: Mon, 2 Nov 2020 16:27:55 +0100 Subject: [PATCH 01/11] backend.local: Set environment on run Signed-off-by: Chris Crone --- local/backend.go | 1 + 1 file changed, 1 insertion(+) diff --git a/local/backend.go b/local/backend.go index ce94af92d..8ce4d4d30 100644 --- a/local/backend.go +++ b/local/backend.go @@ -145,6 +145,7 @@ func (ms *local) Run(ctx context.Context, r containers.ContainerConfig) error { containerConfig := &container.Config{ Image: r.Image, Labels: r.Labels, + Env: r.Environment, ExposedPorts: exposedPorts, } hostConfig := &container.HostConfig{ From 42f31d31293ba36d7576851482d7d688ee96a237 Mon Sep 17 00:00:00 2001 From: Chris Crone Date: Mon, 2 Nov 2020 16:28:18 +0100 Subject: [PATCH 02/11] backend.local: Improve inspect * Add restart policy * Add environment * Add labels * Add auto remove Signed-off-by: Chris Crone --- local/backend.go | 75 ++++++++++++++++++++++++++++++++++++++++--- local/backend_test.go | 70 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 140 insertions(+), 5 deletions(-) create mode 100644 local/backend_test.go diff --git a/local/backend.go b/local/backend.go index 8ce4d4d30..96c8f6749 100644 --- a/local/backend.go +++ b/local/backend.go @@ -21,7 +21,9 @@ package local import ( "bufio" "context" + "fmt" "io" + "sort" "strconv" "strings" "time" @@ -100,15 +102,78 @@ func (ms *local) Inspect(ctx context.Context, id string) (containers.Container, command = strings.Join(c.Config.Cmd, " ") } + rc := containerJSONToRuntimeConfig(&c) + hc := containerJSONToHostConfig(&c) + return containers.Container{ - ID: stringid.TruncateID(c.ID), - Status: status, - Image: c.Image, - Command: command, - Platform: c.Platform, + ID: stringid.TruncateID(c.ID), + Status: status, + Image: c.Image, + Command: command, + Platform: c.Platform, + Config: rc, + HostConfig: hc, }, nil } +func containerJSONToRuntimeConfig(m *types.ContainerJSON) *containers.RuntimeConfig { + if m.Config == nil { + return nil + } + var env map[string]string + if m.Config.Env != nil { + env = make(map[string]string) + for _, e := range m.Config.Env { + tokens := strings.Split(e, "=") + if len(tokens) != 2 { + continue + } + env[tokens[0]] = tokens[1] + } + } + + var labels []string + if m.Config.Labels != nil { + for k, v := range m.Config.Labels { + labels = append(labels, fmt.Sprintf("%s=%s", k, v)) + } + } + sort.Strings(labels) + + if env == nil && + labels == nil { + return nil + } + + return &containers.RuntimeConfig{ + Env: env, + Labels: labels, + } +} + +func containerJSONToHostConfig(m *types.ContainerJSON) *containers.HostConfig { + if m.HostConfig == nil { + return nil + } + + var restartPolicy string + switch m.HostConfig.RestartPolicy.Name { + case "always": + restartPolicy = containers.RestartPolicyAny + case "on-failure": + restartPolicy = containers.RestartPolicyOnFailure + case "no", "": + fallthrough + default: + restartPolicy = containers.RestartPolicyNone + } + + return &containers.HostConfig{ + AutoRemove: m.HostConfig.AutoRemove, + RestartPolicy: restartPolicy, + } +} + func (ms *local) List(ctx context.Context, all bool) ([]containers.Container, error) { css, err := ms.apiClient.ContainerList(ctx, types.ContainerListOptions{ All: all, diff --git a/local/backend_test.go b/local/backend_test.go new file mode 100644 index 000000000..02a1b6dfb --- /dev/null +++ b/local/backend_test.go @@ -0,0 +1,70 @@ +// +build local + +/* + Copyright 2020 Docker Compose CLI authors + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package local + +import ( + "testing" + + "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/container" + "gotest.tools/v3/assert" + + "github.com/docker/compose-cli/api/containers" +) + +func TestToRuntimeConfig(t *testing.T) { + t.Parallel() + m := &types.ContainerJSON{ + Config: &container.Config{ + Env: []string{"FOO1=BAR1", "FOO2=BAR2"}, + Labels: map[string]string{"foo1": "bar1", "foo2": "bar2"}, + }, + } + rc := containerJSONToRuntimeConfig(m) + res := &containers.RuntimeConfig{ + Env: map[string]string{"FOO1": "BAR1", "FOO2": "BAR2"}, + Labels: []string{"foo1=bar1", "foo2=bar2"}, + } + assert.DeepEqual(t, rc, res) +} + +func TestToHostConfig(t *testing.T) { + t.Parallel() + base := &types.ContainerJSONBase{ + HostConfig: &container.HostConfig{ + AutoRemove: true, + RestartPolicy: container.RestartPolicy{ + Name: "", + }, + }, + } + m := &types.ContainerJSON{ + Config: &container.Config{ + Env: []string{"FOO1=BAR1", "FOO2=BAR2"}, + Labels: map[string]string{"foo1": "bar1", "foo2": "bar2"}, + }, + ContainerJSONBase: base, + } + hc := containerJSONToHostConfig(m) + res := &containers.HostConfig{ + AutoRemove: true, + RestartPolicy: containers.RestartPolicyNone, + } + assert.DeepEqual(t, hc, res) +} From 47aa069a3d980da0ea245c7ee60d214d6a6365e9 Mon Sep 17 00:00:00 2001 From: Chris Crone Date: Tue, 3 Nov 2020 14:56:04 +0100 Subject: [PATCH 03/11] backend.local: Add CPU limits Signed-off-by: Chris Crone --- local/backend.go | 4 ++++ local/backend_test.go | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/local/backend.go b/local/backend.go index 96c8f6749..65da9405d 100644 --- a/local/backend.go +++ b/local/backend.go @@ -171,6 +171,7 @@ func containerJSONToHostConfig(m *types.ContainerJSON) *containers.HostConfig { return &containers.HostConfig{ AutoRemove: m.HostConfig.AutoRemove, RestartPolicy: restartPolicy, + CPULimit: float64(m.HostConfig.Resources.NanoCPUs) / 1e9, } } @@ -216,6 +217,9 @@ func (ms *local) Run(ctx context.Context, r containers.ContainerConfig) error { hostConfig := &container.HostConfig{ PortBindings: hostBindings, AutoRemove: r.AutoRemove, + Resources: container.Resources{ + NanoCPUs: int64(r.CPULimit * 1e9), + }, } created, err := ms.apiClient.ContainerCreate(ctx, containerConfig, hostConfig, nil, r.ID) diff --git a/local/backend_test.go b/local/backend_test.go index 02a1b6dfb..101770c59 100644 --- a/local/backend_test.go +++ b/local/backend_test.go @@ -52,6 +52,9 @@ func TestToHostConfig(t *testing.T) { RestartPolicy: container.RestartPolicy{ Name: "", }, + Resources: container.Resources{ + NanoCPUs: 750000000, + }, }, } m := &types.ContainerJSON{ @@ -65,6 +68,7 @@ func TestToHostConfig(t *testing.T) { res := &containers.HostConfig{ AutoRemove: true, RestartPolicy: containers.RestartPolicyNone, + CPULimit: 0.75, } assert.DeepEqual(t, hc, res) } From 6a9eca9bdf869d28ef23453a87474532930bf31d Mon Sep 17 00:00:00 2001 From: Chris Crone Date: Tue, 3 Nov 2020 15:06:03 +0100 Subject: [PATCH 04/11] backend.local: Add memory limits Signed-off-by: Chris Crone --- local/backend.go | 2 ++ local/backend_test.go | 2 ++ 2 files changed, 4 insertions(+) diff --git a/local/backend.go b/local/backend.go index 65da9405d..05e84a88a 100644 --- a/local/backend.go +++ b/local/backend.go @@ -172,6 +172,7 @@ func containerJSONToHostConfig(m *types.ContainerJSON) *containers.HostConfig { AutoRemove: m.HostConfig.AutoRemove, RestartPolicy: restartPolicy, CPULimit: float64(m.HostConfig.Resources.NanoCPUs) / 1e9, + MemoryLimit: uint64(m.HostConfig.Resources.Memory), } } @@ -219,6 +220,7 @@ func (ms *local) Run(ctx context.Context, r containers.ContainerConfig) error { AutoRemove: r.AutoRemove, Resources: container.Resources{ NanoCPUs: int64(r.CPULimit * 1e9), + Memory: int64(r.MemLimit), }, } diff --git a/local/backend_test.go b/local/backend_test.go index 101770c59..428f1d2e3 100644 --- a/local/backend_test.go +++ b/local/backend_test.go @@ -54,6 +54,7 @@ func TestToHostConfig(t *testing.T) { }, Resources: container.Resources{ NanoCPUs: 750000000, + Memory: 512 * 1024 * 1024, }, }, } @@ -69,6 +70,7 @@ func TestToHostConfig(t *testing.T) { AutoRemove: true, RestartPolicy: containers.RestartPolicyNone, CPULimit: 0.75, + MemoryLimit: 512 * 1024 * 1024, } assert.DeepEqual(t, hc, res) } From b8adb4b3ddda26de8c8d5142b08817df568614c3 Mon Sep 17 00:00:00 2001 From: Chris Crone Date: Tue, 3 Nov 2020 15:13:50 +0100 Subject: [PATCH 05/11] backend.local: Refactor conversion code Signed-off-by: Chris Crone --- local/backend.go | 112 +-------------- local/convert.go | 150 +++++++++++++++++++++ local/{backend_test.go => convert_test.go} | 31 ++++- 3 files changed, 181 insertions(+), 112 deletions(-) create mode 100644 local/convert.go rename local/{backend_test.go => convert_test.go} (72%) diff --git a/local/backend.go b/local/backend.go index 05e84a88a..6fb82871d 100644 --- a/local/backend.go +++ b/local/backend.go @@ -21,10 +21,7 @@ package local import ( "bufio" "context" - "fmt" "io" - "sort" - "strconv" "strings" "time" @@ -33,7 +30,6 @@ import ( "github.com/docker/docker/client" "github.com/docker/docker/pkg/stdcopy" "github.com/docker/docker/pkg/stringid" - "github.com/docker/go-connections/nat" "github.com/pkg/errors" "github.com/docker/compose-cli/api/compose" @@ -102,8 +98,8 @@ func (ms *local) Inspect(ctx context.Context, id string) (containers.Container, command = strings.Join(c.Config.Cmd, " ") } - rc := containerJSONToRuntimeConfig(&c) - hc := containerJSONToHostConfig(&c) + rc := toRuntimeConfig(&c) + hc := toHostConfig(&c) return containers.Container{ ID: stringid.TruncateID(c.ID), @@ -116,66 +112,6 @@ func (ms *local) Inspect(ctx context.Context, id string) (containers.Container, }, nil } -func containerJSONToRuntimeConfig(m *types.ContainerJSON) *containers.RuntimeConfig { - if m.Config == nil { - return nil - } - var env map[string]string - if m.Config.Env != nil { - env = make(map[string]string) - for _, e := range m.Config.Env { - tokens := strings.Split(e, "=") - if len(tokens) != 2 { - continue - } - env[tokens[0]] = tokens[1] - } - } - - var labels []string - if m.Config.Labels != nil { - for k, v := range m.Config.Labels { - labels = append(labels, fmt.Sprintf("%s=%s", k, v)) - } - } - sort.Strings(labels) - - if env == nil && - labels == nil { - return nil - } - - return &containers.RuntimeConfig{ - Env: env, - Labels: labels, - } -} - -func containerJSONToHostConfig(m *types.ContainerJSON) *containers.HostConfig { - if m.HostConfig == nil { - return nil - } - - var restartPolicy string - switch m.HostConfig.RestartPolicy.Name { - case "always": - restartPolicy = containers.RestartPolicyAny - case "on-failure": - restartPolicy = containers.RestartPolicyOnFailure - case "no", "": - fallthrough - default: - restartPolicy = containers.RestartPolicyNone - } - - return &containers.HostConfig{ - AutoRemove: m.HostConfig.AutoRemove, - RestartPolicy: restartPolicy, - CPULimit: float64(m.HostConfig.Resources.NanoCPUs) / 1e9, - MemoryLimit: uint64(m.HostConfig.Resources.Memory), - } -} - func (ms *local) List(ctx context.Context, all bool) ([]containers.Container, error) { css, err := ms.apiClient.ContainerList(ctx, types.ContainerListOptions{ All: all, @@ -352,47 +288,3 @@ func (ms *local) Delete(ctx context.Context, containerID string, request contain } return err } - -func toPorts(ports []types.Port) []containers.Port { - result := []containers.Port{} - for _, port := range ports { - result = append(result, containers.Port{ - ContainerPort: uint32(port.PrivatePort), - HostPort: uint32(port.PublicPort), - HostIP: port.IP, - Protocol: port.Type, - }) - } - - return result -} - -func fromPorts(ports []containers.Port) (map[nat.Port]struct{}, map[nat.Port][]nat.PortBinding, error) { - var ( - exposedPorts = make(map[nat.Port]struct{}, len(ports)) - bindings = make(map[nat.Port][]nat.PortBinding) - ) - - for _, port := range ports { - p, err := nat.NewPort(port.Protocol, strconv.Itoa(int(port.ContainerPort))) - if err != nil { - return nil, nil, err - } - - if _, exists := exposedPorts[p]; !exists { - exposedPorts[p] = struct{}{} - } - - portBinding := nat.PortBinding{ - HostIP: port.HostIP, - HostPort: strconv.Itoa(int(port.HostPort)), - } - bslice, exists := bindings[p] - if !exists { - bslice = []nat.PortBinding{} - } - bindings[p] = append(bslice, portBinding) - } - - return exposedPorts, bindings, nil -} diff --git a/local/convert.go b/local/convert.go new file mode 100644 index 000000000..05931a366 --- /dev/null +++ b/local/convert.go @@ -0,0 +1,150 @@ +// +build local + +/* + Copyright 2020 Docker Compose CLI authors + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package local + +import ( + "fmt" + "sort" + "strconv" + "strings" + + "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/container" + "github.com/docker/go-connections/nat" + + "github.com/docker/compose-cli/api/containers" +) + +func toRuntimeConfig(m *types.ContainerJSON) *containers.RuntimeConfig { + if m.Config == nil { + return nil + } + var env map[string]string + if m.Config.Env != nil { + env = make(map[string]string) + for _, e := range m.Config.Env { + tokens := strings.Split(e, "=") + if len(tokens) != 2 { + continue + } + env[tokens[0]] = tokens[1] + } + } + + var labels []string + if m.Config.Labels != nil { + for k, v := range m.Config.Labels { + labels = append(labels, fmt.Sprintf("%s=%s", k, v)) + } + } + sort.Strings(labels) + + if env == nil && + labels == nil { + return nil + } + + return &containers.RuntimeConfig{ + Env: env, + Labels: labels, + } +} + +func toHostConfig(m *types.ContainerJSON) *containers.HostConfig { + if m.HostConfig == nil { + return nil + } + + return &containers.HostConfig{ + AutoRemove: m.HostConfig.AutoRemove, + RestartPolicy: fromRestartPolicyName(m.HostConfig.RestartPolicy.Name), + CPULimit: float64(m.HostConfig.Resources.NanoCPUs) / 1e9, + MemoryLimit: uint64(m.HostConfig.Resources.Memory), + } +} + +func toPorts(ports []types.Port) []containers.Port { + result := []containers.Port{} + for _, port := range ports { + result = append(result, containers.Port{ + ContainerPort: uint32(port.PrivatePort), + HostPort: uint32(port.PublicPort), + HostIP: port.IP, + Protocol: port.Type, + }) + } + + return result +} + +func fromPorts(ports []containers.Port) (map[nat.Port]struct{}, map[nat.Port][]nat.PortBinding, error) { + var ( + exposedPorts = make(map[nat.Port]struct{}, len(ports)) + bindings = make(map[nat.Port][]nat.PortBinding) + ) + + for _, port := range ports { + p, err := nat.NewPort(port.Protocol, strconv.Itoa(int(port.ContainerPort))) + if err != nil { + return nil, nil, err + } + + if _, exists := exposedPorts[p]; !exists { + exposedPorts[p] = struct{}{} + } + + portBinding := nat.PortBinding{ + HostIP: port.HostIP, + HostPort: strconv.Itoa(int(port.HostPort)), + } + bslice, exists := bindings[p] + if !exists { + bslice = []nat.PortBinding{} + } + bindings[p] = append(bslice, portBinding) + } + + return exposedPorts, bindings, nil +} + +func fromRestartPolicyName(m string) string { + switch m { + case "always": + return containers.RestartPolicyAny + case "on-failure": + return containers.RestartPolicyOnFailure + case "no", "": + fallthrough + default: + return containers.RestartPolicyNone + } +} + +func toRestartPolicy(p string) container.RestartPolicy { + switch p { + case containers.RestartPolicyAny: + return container.RestartPolicy{Name: "always"} + case containers.RestartPolicyOnFailure: + return container.RestartPolicy{Name: "on-failure"} + case containers.RestartPolicyNone: + fallthrough + default: + return container.RestartPolicy{Name: "no"} + } +} diff --git a/local/backend_test.go b/local/convert_test.go similarity index 72% rename from local/backend_test.go rename to local/convert_test.go index 428f1d2e3..987b6c98a 100644 --- a/local/backend_test.go +++ b/local/convert_test.go @@ -36,7 +36,7 @@ func TestToRuntimeConfig(t *testing.T) { Labels: map[string]string{"foo1": "bar1", "foo2": "bar2"}, }, } - rc := containerJSONToRuntimeConfig(m) + rc := toRuntimeConfig(m) res := &containers.RuntimeConfig{ Env: map[string]string{"FOO1": "BAR1", "FOO2": "BAR2"}, Labels: []string{"foo1=bar1", "foo2=bar2"}, @@ -65,7 +65,7 @@ func TestToHostConfig(t *testing.T) { }, ContainerJSONBase: base, } - hc := containerJSONToHostConfig(m) + hc := toHostConfig(m) res := &containers.HostConfig{ AutoRemove: true, RestartPolicy: containers.RestartPolicyNone, @@ -74,3 +74,30 @@ func TestToHostConfig(t *testing.T) { } assert.DeepEqual(t, hc, res) } + +func TestFromRestartPolicyName(t *testing.T) { + t.Parallel() + moby := []string{"always", "on-failure", "no", ""} + ours := []string{ + containers.RestartPolicyAny, + containers.RestartPolicyOnFailure, + containers.RestartPolicyNone, + containers.RestartPolicyNone, + } + for i, p := range moby { + assert.Equal(t, fromRestartPolicyName(p), ours[i]) + } +} + +func TestToRestartPolicy(t *testing.T) { + t.Parallel() + ours := []string{containers.RestartPolicyAny, containers.RestartPolicyOnFailure, containers.RestartPolicyNone} + moby := []container.RestartPolicy{ + {Name: "always"}, + {Name: "on-failure"}, + {Name: "no"}, + } + for i, p := range ours { + assert.Equal(t, toRestartPolicy(p), moby[i]) + } +} From b2dcae685a69fc303b3d08e096e1e02b9ac4a6ea Mon Sep 17 00:00:00 2001 From: Chris Crone Date: Tue, 3 Nov 2020 15:22:16 +0100 Subject: [PATCH 06/11] backend.local: Add restart policy support to run Signed-off-by: Chris Crone --- local/backend.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/local/backend.go b/local/backend.go index 6fb82871d..18c5b6e82 100644 --- a/local/backend.go +++ b/local/backend.go @@ -152,8 +152,9 @@ func (ms *local) Run(ctx context.Context, r containers.ContainerConfig) error { ExposedPorts: exposedPorts, } hostConfig := &container.HostConfig{ - PortBindings: hostBindings, - AutoRemove: r.AutoRemove, + PortBindings: hostBindings, + AutoRemove: r.AutoRemove, + RestartPolicy: toRestartPolicy(r.RestartPolicyCondition), Resources: container.Resources{ NanoCPUs: int64(r.CPULimit * 1e9), Memory: int64(r.MemLimit), From 637dd263c952102f5bd23e6c3f803d8163259e89 Mon Sep 17 00:00:00 2001 From: Chris Crone Date: Tue, 3 Nov 2020 21:01:56 +0100 Subject: [PATCH 07/11] backend.local: Refactor container service Signed-off-by: Chris Crone --- local/backend.go | 227 +--------------------------------------- local/containers.go | 250 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 254 insertions(+), 223 deletions(-) create mode 100644 local/containers.go diff --git a/local/backend.go b/local/backend.go index 18c5b6e82..e65a8348c 100644 --- a/local/backend.go +++ b/local/backend.go @@ -19,18 +19,9 @@ package local import ( - "bufio" "context" - "io" - "strings" - "time" - "github.com/docker/docker/api/types" - "github.com/docker/docker/api/types/container" "github.com/docker/docker/client" - "github.com/docker/docker/pkg/stdcopy" - "github.com/docker/docker/pkg/stringid" - "github.com/pkg/errors" "github.com/docker/compose-cli/api/compose" "github.com/docker/compose-cli/api/containers" @@ -39,11 +30,10 @@ import ( "github.com/docker/compose-cli/api/volumes" "github.com/docker/compose-cli/backend" "github.com/docker/compose-cli/context/cloud" - "github.com/docker/compose-cli/errdefs" ) type local struct { - apiClient *client.Client + *containerService } func init() { @@ -57,12 +47,12 @@ func service(ctx context.Context) (backend.Service, error) { } return &local{ - apiClient, + containerService: &containerService{apiClient}, }, nil } -func (ms *local) ContainerService() containers.Service { - return ms +func (cs *containerService) ContainerService() containers.Service { + return cs } func (ms *local) ComposeService() compose.Service { @@ -80,212 +70,3 @@ func (ms *local) VolumeService() volumes.Service { func (ms *local) ResourceService() resources.Service { return nil } - -func (ms *local) Inspect(ctx context.Context, id string) (containers.Container, error) { - c, err := ms.apiClient.ContainerInspect(ctx, id) - if err != nil { - return containers.Container{}, err - } - - status := "" - if c.State != nil { - status = c.State.Status - } - - command := "" - if c.Config != nil && - c.Config.Cmd != nil { - command = strings.Join(c.Config.Cmd, " ") - } - - rc := toRuntimeConfig(&c) - hc := toHostConfig(&c) - - return containers.Container{ - ID: stringid.TruncateID(c.ID), - Status: status, - Image: c.Image, - Command: command, - Platform: c.Platform, - Config: rc, - HostConfig: hc, - }, nil -} - -func (ms *local) List(ctx context.Context, all bool) ([]containers.Container, error) { - css, err := ms.apiClient.ContainerList(ctx, types.ContainerListOptions{ - All: all, - }) - - if err != nil { - return []containers.Container{}, err - } - - var result []containers.Container - for _, container := range css { - result = append(result, containers.Container{ - ID: stringid.TruncateID(container.ID), - Image: container.Image, - // TODO: `Status` is a human readable string ("Up 24 minutes"), - // we need to return the `State` instead but first we need to - // define an enum on the proto side with all the possible container - // statuses. We also need to add a `Created` property on the gRPC side. - Status: container.Status, - Command: container.Command, - Ports: toPorts(container.Ports), - }) - } - - return result, nil -} - -func (ms *local) Run(ctx context.Context, r containers.ContainerConfig) error { - exposedPorts, hostBindings, err := fromPorts(r.Ports) - if err != nil { - return err - } - - containerConfig := &container.Config{ - Image: r.Image, - Labels: r.Labels, - Env: r.Environment, - ExposedPorts: exposedPorts, - } - hostConfig := &container.HostConfig{ - PortBindings: hostBindings, - AutoRemove: r.AutoRemove, - RestartPolicy: toRestartPolicy(r.RestartPolicyCondition), - Resources: container.Resources{ - NanoCPUs: int64(r.CPULimit * 1e9), - Memory: int64(r.MemLimit), - }, - } - - created, err := ms.apiClient.ContainerCreate(ctx, containerConfig, hostConfig, nil, r.ID) - - if err != nil { - if client.IsErrNotFound(err) { - io, err := ms.apiClient.ImagePull(ctx, r.Image, types.ImagePullOptions{}) - if err != nil { - return err - } - scanner := bufio.NewScanner(io) - - // Read the whole body, otherwise the pulling stops - for scanner.Scan() { - } - - if err = scanner.Err(); err != nil { - return err - } - if err = io.Close(); err != nil { - return err - } - created, err = ms.apiClient.ContainerCreate(ctx, containerConfig, hostConfig, nil, r.ID) - if err != nil { - return err - } - } else { - return err - } - } - - return ms.apiClient.ContainerStart(ctx, created.ID, types.ContainerStartOptions{}) -} - -func (ms *local) Start(ctx context.Context, containerID string) error { - return ms.apiClient.ContainerStart(ctx, containerID, types.ContainerStartOptions{}) -} - -func (ms *local) Stop(ctx context.Context, containerID string, timeout *uint32) error { - var t *time.Duration - if timeout != nil { - timeoutValue := time.Duration(*timeout) * time.Second - t = &timeoutValue - } - return ms.apiClient.ContainerStop(ctx, containerID, t) -} - -func (ms *local) Kill(ctx context.Context, containerID string, signal string) error { - return ms.apiClient.ContainerKill(ctx, containerID, signal) -} - -func (ms *local) Exec(ctx context.Context, name string, request containers.ExecRequest) error { - cec, err := ms.apiClient.ContainerExecCreate(ctx, name, types.ExecConfig{ - Cmd: []string{request.Command}, - Tty: true, - AttachStdin: true, - AttachStdout: true, - AttachStderr: true, - }) - if err != nil { - return err - } - resp, err := ms.apiClient.ContainerExecAttach(ctx, cec.ID, types.ExecStartCheck{ - Tty: true, - }) - if err != nil { - return err - } - defer resp.Close() - - readChannel := make(chan error, 10) - writeChannel := make(chan error, 10) - - go func() { - _, err := io.Copy(request.Stdout, resp.Reader) - readChannel <- err - }() - - go func() { - _, err := io.Copy(resp.Conn, request.Stdin) - writeChannel <- err - }() - - for { - select { - case err := <-readChannel: - return err - case err := <-writeChannel: - return err - } - } -} - -func (ms *local) Logs(ctx context.Context, containerName string, request containers.LogsRequest) error { - c, err := ms.apiClient.ContainerInspect(ctx, containerName) - if err != nil { - return err - } - - r, err := ms.apiClient.ContainerLogs(ctx, containerName, types.ContainerLogsOptions{ - ShowStdout: true, - ShowStderr: true, - Follow: request.Follow, - }) - - if err != nil { - return err - } - - // nolint errcheck - defer r.Close() - - if c.Config.Tty { - _, err = io.Copy(request.Writer, r) - } else { - _, err = stdcopy.StdCopy(request.Writer, request.Writer, r) - } - - return err -} - -func (ms *local) Delete(ctx context.Context, containerID string, request containers.DeleteRequest) error { - err := ms.apiClient.ContainerRemove(ctx, containerID, types.ContainerRemoveOptions{ - Force: request.Force, - }) - if client.IsErrNotFound(err) { - return errors.Wrapf(errdefs.ErrNotFound, "container %q", containerID) - } - return err -} diff --git a/local/containers.go b/local/containers.go new file mode 100644 index 000000000..89282af62 --- /dev/null +++ b/local/containers.go @@ -0,0 +1,250 @@ +// +build local + +/* + Copyright 2020 Docker Compose CLI authors + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package local + +import ( + "bufio" + "context" + "io" + "strings" + "time" + + "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/container" + "github.com/docker/docker/client" + "github.com/docker/docker/pkg/stdcopy" + "github.com/docker/docker/pkg/stringid" + "github.com/pkg/errors" + + "github.com/docker/compose-cli/api/containers" + "github.com/docker/compose-cli/errdefs" +) + +type containerService struct { + apiClient *client.Client +} + +func (cs *containerService) Inspect(ctx context.Context, id string) (containers.Container, error) { + c, err := cs.apiClient.ContainerInspect(ctx, id) + if err != nil { + return containers.Container{}, err + } + + status := "" + if c.State != nil { + status = c.State.Status + } + + command := "" + if c.Config != nil && + c.Config.Cmd != nil { + command = strings.Join(c.Config.Cmd, " ") + } + + rc := toRuntimeConfig(&c) + hc := toHostConfig(&c) + + return containers.Container{ + ID: stringid.TruncateID(c.ID), + Status: status, + Image: c.Image, + Command: command, + Platform: c.Platform, + Config: rc, + HostConfig: hc, + }, nil +} + +func (cs *containerService) List(ctx context.Context, all bool) ([]containers.Container, error) { + css, err := cs.apiClient.ContainerList(ctx, types.ContainerListOptions{ + All: all, + }) + + if err != nil { + return []containers.Container{}, err + } + + var result []containers.Container + for _, container := range css { + result = append(result, containers.Container{ + ID: stringid.TruncateID(container.ID), + Image: container.Image, + // TODO: `Status` is a human readable string ("Up 24 minutes"), + // we need to return the `State` instead but first we need to + // define an enum on the proto side with all the possible container + // statuses. We also need to add a `Created` property on the gRPC side. + Status: container.Status, + Command: container.Command, + Ports: toPorts(container.Ports), + }) + } + + return result, nil +} + +func (cs *containerService) Run(ctx context.Context, r containers.ContainerConfig) error { + exposedPorts, hostBindings, err := fromPorts(r.Ports) + if err != nil { + return err + } + + containerConfig := &container.Config{ + Image: r.Image, + Labels: r.Labels, + Env: r.Environment, + ExposedPorts: exposedPorts, + } + hostConfig := &container.HostConfig{ + PortBindings: hostBindings, + AutoRemove: r.AutoRemove, + RestartPolicy: toRestartPolicy(r.RestartPolicyCondition), + Resources: container.Resources{ + NanoCPUs: int64(r.CPULimit * 1e9), + Memory: int64(r.MemLimit), + }, + } + + created, err := cs.apiClient.ContainerCreate(ctx, containerConfig, hostConfig, nil, r.ID) + + if err != nil { + if client.IsErrNotFound(err) { + io, err := cs.apiClient.ImagePull(ctx, r.Image, types.ImagePullOptions{}) + if err != nil { + return err + } + scanner := bufio.NewScanner(io) + + // Read the whole body, otherwise the pulling stops + for scanner.Scan() { + } + + if err = scanner.Err(); err != nil { + return err + } + if err = io.Close(); err != nil { + return err + } + created, err = cs.apiClient.ContainerCreate(ctx, containerConfig, hostConfig, nil, r.ID) + if err != nil { + return err + } + } else { + return err + } + } + + return cs.apiClient.ContainerStart(ctx, created.ID, types.ContainerStartOptions{}) +} + +func (cs *containerService) Start(ctx context.Context, containerID string) error { + return cs.apiClient.ContainerStart(ctx, containerID, types.ContainerStartOptions{}) +} + +func (cs *containerService) Stop(ctx context.Context, containerID string, timeout *uint32) error { + var t *time.Duration + if timeout != nil { + timeoutValue := time.Duration(*timeout) * time.Second + t = &timeoutValue + } + return cs.apiClient.ContainerStop(ctx, containerID, t) +} + +func (cs *containerService) Kill(ctx context.Context, containerID string, signal string) error { + return cs.apiClient.ContainerKill(ctx, containerID, signal) +} + +func (cs *containerService) Exec(ctx context.Context, name string, request containers.ExecRequest) error { + cec, err := cs.apiClient.ContainerExecCreate(ctx, name, types.ExecConfig{ + Cmd: []string{request.Command}, + Tty: true, + AttachStdin: true, + AttachStdout: true, + AttachStderr: true, + }) + if err != nil { + return err + } + resp, err := cs.apiClient.ContainerExecAttach(ctx, cec.ID, types.ExecStartCheck{ + Tty: true, + }) + if err != nil { + return err + } + defer resp.Close() + + readChannel := make(chan error, 10) + writeChannel := make(chan error, 10) + + go func() { + _, err := io.Copy(request.Stdout, resp.Reader) + readChannel <- err + }() + + go func() { + _, err := io.Copy(resp.Conn, request.Stdin) + writeChannel <- err + }() + + for { + select { + case err := <-readChannel: + return err + case err := <-writeChannel: + return err + } + } +} + +func (cs *containerService) Logs(ctx context.Context, containerName string, request containers.LogsRequest) error { + c, err := cs.apiClient.ContainerInspect(ctx, containerName) + if err != nil { + return err + } + + r, err := cs.apiClient.ContainerLogs(ctx, containerName, types.ContainerLogsOptions{ + ShowStdout: true, + ShowStderr: true, + Follow: request.Follow, + }) + + if err != nil { + return err + } + + // nolint errcheck + defer r.Close() + + if c.Config.Tty { + _, err = io.Copy(request.Writer, r) + } else { + _, err = stdcopy.StdCopy(request.Writer, request.Writer, r) + } + + return err +} + +func (cs *containerService) Delete(ctx context.Context, containerID string, request containers.DeleteRequest) error { + err := cs.apiClient.ContainerRemove(ctx, containerID, types.ContainerRemoveOptions{ + Force: request.Force, + }) + if client.IsErrNotFound(err) { + return errors.Wrapf(errdefs.ErrNotFound, "container %q", containerID) + } + return err +} From 42eb0ecc3542fac55cdec2633e895d0646bfb60d Mon Sep 17 00:00:00 2001 From: Chris Crone Date: Tue, 3 Nov 2020 21:15:14 +0100 Subject: [PATCH 08/11] backend.local: Add rudimentary volume support Signed-off-by: Chris Crone --- local/backend.go | 6 ++-- local/containers.go | 13 +++++++ local/volumes.go | 84 +++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 101 insertions(+), 2 deletions(-) create mode 100644 local/volumes.go diff --git a/local/backend.go b/local/backend.go index e65a8348c..134fddb00 100644 --- a/local/backend.go +++ b/local/backend.go @@ -34,6 +34,7 @@ import ( type local struct { *containerService + *volumeService } func init() { @@ -48,6 +49,7 @@ func service(ctx context.Context) (backend.Service, error) { return &local{ containerService: &containerService{apiClient}, + volumeService: &volumeService{apiClient}, }, nil } @@ -63,8 +65,8 @@ func (ms *local) SecretsService() secrets.Service { return nil } -func (ms *local) VolumeService() volumes.Service { - return nil +func (vs *volumeService) VolumeService() volumes.Service { + return vs } func (ms *local) ResourceService() resources.Service { diff --git a/local/containers.go b/local/containers.go index 89282af62..ce83d87ef 100644 --- a/local/containers.go +++ b/local/containers.go @@ -27,6 +27,7 @@ import ( "github.com/docker/docker/api/types" "github.com/docker/docker/api/types/container" + "github.com/docker/docker/api/types/mount" "github.com/docker/docker/client" "github.com/docker/docker/pkg/stdcopy" "github.com/docker/docker/pkg/stringid" @@ -104,6 +105,17 @@ func (cs *containerService) Run(ctx context.Context, r containers.ContainerConfi return err } + var mounts []mount.Mount + for _, v := range r.Volumes { + tokens := strings.Split(v, ":") + if len(tokens) != 2 { + return errors.Wrapf(errdefs.ErrParsingFailed, "volume %q has invalid format", v) + } + src := tokens[0] + tgt := tokens[1] + mounts = append(mounts, mount.Mount{Type: "volume", Source: src, Target: tgt}) + } + containerConfig := &container.Config{ Image: r.Image, Labels: r.Labels, @@ -112,6 +124,7 @@ func (cs *containerService) Run(ctx context.Context, r containers.ContainerConfi } hostConfig := &container.HostConfig{ PortBindings: hostBindings, + Mounts: mounts, AutoRemove: r.AutoRemove, RestartPolicy: toRestartPolicy(r.RestartPolicyCondition), Resources: container.Resources{ diff --git a/local/volumes.go b/local/volumes.go new file mode 100644 index 000000000..2f56d4444 --- /dev/null +++ b/local/volumes.go @@ -0,0 +1,84 @@ +// +build local + +/* + Copyright 2020 Docker Compose CLI authors + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package local + +import ( + "context" + "fmt" + + "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/filters" + "github.com/docker/docker/api/types/volume" + "github.com/docker/docker/client" + + "github.com/docker/compose-cli/api/volumes" +) + +type volumeService struct { + apiClient *client.Client +} + +func (vs *volumeService) List(ctx context.Context) ([]volumes.Volume, error) { + l, err := vs.apiClient.VolumeList(ctx, filters.NewArgs()) + if err != nil { + return []volumes.Volume{}, err + } + + res := []volumes.Volume{} + for _, v := range l.Volumes { + res = append(res, volumes.Volume{ + ID: v.Name, + Description: description(v), + }) + } + + return res, nil +} + +func (vs *volumeService) Create(ctx context.Context, name string, options interface{}) (volumes.Volume, error) { + v, err := vs.apiClient.VolumeCreate(ctx, volume.VolumeCreateBody{ + Driver: "local", + DriverOpts: nil, + Labels: nil, + Name: name, + }) + if err != nil { + return volumes.Volume{}, err + } + return volumes.Volume{ID: name, Description: description(&v)}, nil +} + +func (vs *volumeService) Delete(ctx context.Context, volumeID string, options interface{}) error { + if err := vs.apiClient.VolumeRemove(ctx, volumeID, false); err != nil { + return err + } + return nil +} + +func (vs *volumeService) Inspect(ctx context.Context, volumeID string) (volumes.Volume, error) { + v, err := vs.apiClient.VolumeInspect(ctx, volumeID) + if err != nil { + return volumes.Volume{}, err + } + return volumes.Volume{ID: volumeID, Description: description(&v)}, nil +} + +func description(v *types.Volume) string { + return fmt.Sprintf("Created %s", v.CreatedAt) +} From 318e8c6e8b3dc24be15da0be047ccfc64a1fb404 Mon Sep 17 00:00:00 2001 From: Chris Crone Date: Tue, 3 Nov 2020 21:18:09 +0100 Subject: [PATCH 09/11] api: Fix typo Signed-off-by: Chris Crone --- api/containers/api.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/containers/api.go b/api/containers/api.go index c3770c023..876b32684 100644 --- a/api/containers/api.go +++ b/api/containers/api.go @@ -85,7 +85,7 @@ type Port struct { type ContainerConfig struct { // ID uniquely identifies the container ID string - // Image specifies the iamge reference used for a container + // Image specifies the image reference used for a container Image string // Command are the arguments passed to the container's entrypoint Command []string From db3a14694b27d8f8a8377b8522506fc96756cfb3 Mon Sep 17 00:00:00 2001 From: Chris Crone Date: Wed, 4 Nov 2020 10:43:04 +0100 Subject: [PATCH 10/11] backend.local: Add command support Signed-off-by: Chris Crone --- local/containers.go | 1 + 1 file changed, 1 insertion(+) diff --git a/local/containers.go b/local/containers.go index ce83d87ef..327a991e5 100644 --- a/local/containers.go +++ b/local/containers.go @@ -118,6 +118,7 @@ func (cs *containerService) Run(ctx context.Context, r containers.ContainerConfi containerConfig := &container.Config{ Image: r.Image, + Cmd: r.Command, Labels: r.Labels, Env: r.Environment, ExposedPorts: exposedPorts, From 74a27541e4cc9ae74103c8aa1976a8f3f1674612 Mon Sep 17 00:00:00 2001 From: Chris Crone Date: Wed, 4 Nov 2020 10:43:50 +0100 Subject: [PATCH 11/11] backend.local: Add volume e2e test Signed-off-by: Chris Crone --- local/e2e/backend_test.go | 41 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 40 insertions(+), 1 deletion(-) diff --git a/local/e2e/backend_test.go b/local/e2e/backend_test.go index ec2c4a3a0..6a7bf458e 100644 --- a/local/e2e/backend_test.go +++ b/local/e2e/backend_test.go @@ -43,12 +43,13 @@ func TestMain(m *testing.M) { os.Exit(exitCode) } -func TestLocalBackend(t *testing.T) { +func TestLocalBackendRun(t *testing.T) { c := NewParallelE2eCLI(t, binDir) c.RunDockerCmd("context", "create", "local", "test-context").Assert(t, icmd.Success) c.RunDockerCmd("context", "use", "test-context").Assert(t, icmd.Success) t.Run("run", func(t *testing.T) { + t.Parallel() res := c.RunDockerCmd("run", "-d", "nginx") containerName := strings.TrimSpace(res.Combined()) t.Cleanup(func() { @@ -59,6 +60,7 @@ func TestLocalBackend(t *testing.T) { }) t.Run("run rm", func(t *testing.T) { + t.Parallel() res := c.RunDockerCmd("run", "--rm", "-d", "nginx") containerName := strings.TrimSpace(res.Combined()) t.Cleanup(func() { @@ -87,7 +89,20 @@ func TestLocalBackend(t *testing.T) { res.Assert(t, icmd.Expected{Out: "0.0.0.0:8080->80/tcp"}) }) + t.Run("run with volume", func(t *testing.T) { + t.Parallel() + t.Cleanup(func() { + _ = c.RunDockerOrExitError("volume", "rm", "local-test") + }) + c.RunDockerCmd("volume", "create", "local-test") + c.RunDockerCmd("run", "--rm", "-d", "--volume", "local-test:/data", "alpine", "sh", "-c", `echo "testdata" > /data/test`) + // FIXME: Remove sleep when race to attach to dead container is fixed + res := c.RunDockerOrExitError("run", "--rm", "--volume", "local-test:/data", "alpine", "sh", "-c", "cat /data/test && sleep 1") + res.Assert(t, icmd.Expected{Out: "testdata"}) + }) + t.Run("inspect not found", func(t *testing.T) { + t.Parallel() res := c.RunDockerOrExitError("inspect", "nonexistentcontainer") res.Assert(t, icmd.Expected{ ExitCode: 1, @@ -95,3 +110,27 @@ func TestLocalBackend(t *testing.T) { }) }) } + +func TestLocalBackendVolumes(t *testing.T) { + c := NewParallelE2eCLI(t, binDir) + c.RunDockerCmd("context", "create", "local", "test-context").Assert(t, icmd.Success) + c.RunDockerCmd("context", "use", "test-context").Assert(t, icmd.Success) + + t.Run("volume crud", func(t *testing.T) { + t.Parallel() + name := "crud" + t.Cleanup(func() { + _ = c.RunDockerOrExitError("volume", "rm", name) + }) + res := c.RunDockerCmd("volume", "create", name) + res.Assert(t, icmd.Expected{Out: name}) + res = c.RunDockerCmd("volume", "ls") + res.Assert(t, icmd.Expected{Out: name}) + res = c.RunDockerCmd("volume", "inspect", name) + res.Assert(t, icmd.Expected{Out: fmt.Sprintf(`"ID": "%s"`, name)}) + res = c.RunDockerCmd("volume", "rm", name) + res.Assert(t, icmd.Expected{Out: name}) + res = c.RunDockerOrExitError("volume", "inspect", name) + res.Assert(t, icmd.Expected{ExitCode: 1}) + }) +}