Quellcode durchsuchen

Merge pull request #867 from docker/local-improvements

Nicolas De loof vor 5 Jahren
Ursprung
Commit
3afc1cd43d
7 geänderte Dateien mit 650 neuen und 262 gelöschten Zeilen
  1. 1 1
      api/containers/api.go
  2. 8 260
      local/backend.go
  3. 264 0
      local/containers.go
  4. 150 0
      local/convert.go
  5. 103 0
      local/convert_test.go
  6. 40 1
      local/e2e/backend_test.go
  7. 84 0
      local/volumes.go

+ 1 - 1
api/containers/api.go

@@ -85,7 +85,7 @@ type Port struct {
 type ContainerConfig struct {
 type ContainerConfig struct {
 	// ID uniquely identifies the container
 	// ID uniquely identifies the container
 	ID string
 	ID string
-	// Image specifies the iamge reference used for a container
+	// Image specifies the image reference used for a container
 	Image string
 	Image string
 	// Command are the arguments passed to the container's entrypoint
 	// Command are the arguments passed to the container's entrypoint
 	Command []string
 	Command []string

+ 8 - 260
local/backend.go

@@ -19,20 +19,9 @@
 package local
 package local
 
 
 import (
 import (
-	"bufio"
 	"context"
 	"context"
-	"io"
-	"strconv"
-	"strings"
-	"time"
 
 
-	"github.com/docker/docker/api/types"
-	"github.com/docker/docker/api/types/container"
 	"github.com/docker/docker/client"
 	"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"
 	"github.com/docker/compose-cli/api/compose"
 	"github.com/docker/compose-cli/api/containers"
 	"github.com/docker/compose-cli/api/containers"
@@ -41,11 +30,11 @@ import (
 	"github.com/docker/compose-cli/api/volumes"
 	"github.com/docker/compose-cli/api/volumes"
 	"github.com/docker/compose-cli/backend"
 	"github.com/docker/compose-cli/backend"
 	"github.com/docker/compose-cli/context/cloud"
 	"github.com/docker/compose-cli/context/cloud"
-	"github.com/docker/compose-cli/errdefs"
 )
 )
 
 
 type local struct {
 type local struct {
-	apiClient *client.Client
+	*containerService
+	*volumeService
 }
 }
 
 
 func init() {
 func init() {
@@ -59,12 +48,13 @@ func service(ctx context.Context) (backend.Service, error) {
 	}
 	}
 
 
 	return &local{
 	return &local{
-		apiClient,
+		containerService: &containerService{apiClient},
+		volumeService:    &volumeService{apiClient},
 	}, nil
 	}, nil
 }
 }
 
 
-func (ms *local) ContainerService() containers.Service {
-	return ms
+func (cs *containerService) ContainerService() containers.Service {
+	return cs
 }
 }
 
 
 func (ms *local) ComposeService() compose.Service {
 func (ms *local) ComposeService() compose.Service {
@@ -75,252 +65,10 @@ func (ms *local) SecretsService() secrets.Service {
 	return nil
 	return nil
 }
 }
 
 
-func (ms *local) VolumeService() volumes.Service {
-	return nil
+func (vs *volumeService) VolumeService() volumes.Service {
+	return vs
 }
 }
 
 
 func (ms *local) ResourceService() resources.Service {
 func (ms *local) ResourceService() resources.Service {
 	return nil
 	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, " ")
-	}
-
-	return containers.Container{
-		ID:       stringid.TruncateID(c.ID),
-		Status:   status,
-		Image:    c.Image,
-		Command:  command,
-		Platform: c.Platform,
-	}, 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,
-		ExposedPorts: exposedPorts,
-	}
-	hostConfig := &container.HostConfig{
-		PortBindings: hostBindings,
-		AutoRemove:   r.AutoRemove,
-	}
-
-	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
-}
-
-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
-}

+ 264 - 0
local/containers.go

@@ -0,0 +1,264 @@
+// +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/api/types/mount"
+	"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
+	}
+
+	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,
+		Cmd:          r.Command,
+		Labels:       r.Labels,
+		Env:          r.Environment,
+		ExposedPorts: exposedPorts,
+	}
+	hostConfig := &container.HostConfig{
+		PortBindings:  hostBindings,
+		Mounts:        mounts,
+		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
+}

+ 150 - 0
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"}
+	}
+}

+ 103 - 0
local/convert_test.go

@@ -0,0 +1,103 @@
+// +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 := toRuntimeConfig(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: "",
+			},
+			Resources: container.Resources{
+				NanoCPUs: 750000000,
+				Memory:   512 * 1024 * 1024,
+			},
+		},
+	}
+	m := &types.ContainerJSON{
+		Config: &container.Config{
+			Env:    []string{"FOO1=BAR1", "FOO2=BAR2"},
+			Labels: map[string]string{"foo1": "bar1", "foo2": "bar2"},
+		},
+		ContainerJSONBase: base,
+	}
+	hc := toHostConfig(m)
+	res := &containers.HostConfig{
+		AutoRemove:    true,
+		RestartPolicy: containers.RestartPolicyNone,
+		CPULimit:      0.75,
+		MemoryLimit:   512 * 1024 * 1024,
+	}
+	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])
+	}
+}

+ 40 - 1
local/e2e/backend_test.go

@@ -43,12 +43,13 @@ func TestMain(m *testing.M) {
 	os.Exit(exitCode)
 	os.Exit(exitCode)
 }
 }
 
 
-func TestLocalBackend(t *testing.T) {
+func TestLocalBackendRun(t *testing.T) {
 	c := NewParallelE2eCLI(t, binDir)
 	c := NewParallelE2eCLI(t, binDir)
 	c.RunDockerCmd("context", "create", "local", "test-context").Assert(t, icmd.Success)
 	c.RunDockerCmd("context", "create", "local", "test-context").Assert(t, icmd.Success)
 	c.RunDockerCmd("context", "use", "test-context").Assert(t, icmd.Success)
 	c.RunDockerCmd("context", "use", "test-context").Assert(t, icmd.Success)
 
 
 	t.Run("run", func(t *testing.T) {
 	t.Run("run", func(t *testing.T) {
+		t.Parallel()
 		res := c.RunDockerCmd("run", "-d", "nginx")
 		res := c.RunDockerCmd("run", "-d", "nginx")
 		containerName := strings.TrimSpace(res.Combined())
 		containerName := strings.TrimSpace(res.Combined())
 		t.Cleanup(func() {
 		t.Cleanup(func() {
@@ -59,6 +60,7 @@ func TestLocalBackend(t *testing.T) {
 	})
 	})
 
 
 	t.Run("run rm", func(t *testing.T) {
 	t.Run("run rm", func(t *testing.T) {
+		t.Parallel()
 		res := c.RunDockerCmd("run", "--rm", "-d", "nginx")
 		res := c.RunDockerCmd("run", "--rm", "-d", "nginx")
 		containerName := strings.TrimSpace(res.Combined())
 		containerName := strings.TrimSpace(res.Combined())
 		t.Cleanup(func() {
 		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"})
 		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.Run("inspect not found", func(t *testing.T) {
+		t.Parallel()
 		res := c.RunDockerOrExitError("inspect", "nonexistentcontainer")
 		res := c.RunDockerOrExitError("inspect", "nonexistentcontainer")
 		res.Assert(t, icmd.Expected{
 		res.Assert(t, icmd.Expected{
 			ExitCode: 1,
 			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})
+	})
+}

+ 84 - 0
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)
+}