Browse Source

Merge pull request #919 from docker/compose-in-go

Nicolas De loof 5 years ago
parent
commit
bbcf445f1f
13 changed files with 1217 additions and 148 deletions
  1. 3 40
      ecs/logs.go
  2. 1 1
      formatter/colors.go
  3. 92 0
      formatter/logs.go
  4. 13 7
      go.mod
  5. 37 80
      go.sum
  6. 12 10
      local/backend.go
  7. 598 0
      local/compose.go
  8. 18 10
      local/containers.go
  9. 241 0
      local/convergence.go
  10. 60 0
      local/dependencies.go
  11. 57 0
      local/dependencies_test.go
  12. 35 0
      local/labels.go
  13. 50 0
      local/util.go

+ 3 - 40
ecs/logs.go

@@ -17,51 +17,14 @@
 package ecs
 
 import (
-	"bytes"
 	"context"
-	"fmt"
 	"io"
-	"strconv"
-	"strings"
+
+	"github.com/docker/compose-cli/formatter"
 )
 
 func (b *ecsAPIService) Logs(ctx context.Context, project string, w io.Writer) error {
-	consumer := logConsumer{
-		colors: map[string]colorFunc{},
-		width:  0,
-		writer: w,
-	}
+	consumer := formatter.NewLogConsumer(w)
 	err := b.aws.GetLogs(ctx, project, consumer.Log)
 	return err
 }
-
-func (l *logConsumer) Log(service, container, message string) {
-	cf, ok := l.colors[service]
-	if !ok {
-		cf = <-loop
-		l.colors[service] = cf
-		l.computeWidth()
-	}
-	prefix := fmt.Sprintf("%-"+strconv.Itoa(l.width)+"s |", service)
-
-	for _, line := range strings.Split(message, "\n") {
-		buf := bytes.NewBufferString(fmt.Sprintf("%s %s\n", cf(prefix), line))
-		l.writer.Write(buf.Bytes()) // nolint:errcheck
-	}
-}
-
-func (l *logConsumer) computeWidth() {
-	width := 0
-	for n := range l.colors {
-		if len(n) > width {
-			width = len(n)
-		}
-	}
-	l.width = width + 3
-}
-
-type logConsumer struct {
-	colors map[string]colorFunc
-	width  int
-	writer io.Writer
-}

+ 1 - 1
ecs/colors.go → formatter/colors.go

@@ -14,7 +14,7 @@
    limitations under the License.
 */
 
-package ecs
+package formatter
 
 import (
 	"fmt"

+ 92 - 0
formatter/logs.go

@@ -0,0 +1,92 @@
+/*
+   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 formatter
+
+import (
+	"bytes"
+	"fmt"
+	"io"
+	"strconv"
+	"strings"
+)
+
+// NewLogConsumer creates a new LogConsumer
+func NewLogConsumer(w io.Writer) LogConsumer {
+	return LogConsumer{
+		colors: map[string]colorFunc{},
+		width:  0,
+		writer: w,
+	}
+}
+
+// Log formats a log message as received from service/container
+func (l *LogConsumer) Log(service, container, message string) {
+	cf, ok := l.colors[service]
+	if !ok {
+		cf = <-loop
+		l.colors[service] = cf
+		l.computeWidth()
+	}
+	prefix := fmt.Sprintf("%-"+strconv.Itoa(l.width)+"s |", service)
+
+	for _, line := range strings.Split(message, "\n") {
+		buf := bytes.NewBufferString(fmt.Sprintf("%s %s\n", cf(prefix), line))
+		l.writer.Write(buf.Bytes()) // nolint:errcheck
+	}
+}
+
+// GetWriter creates a io.Writer that will actually split by line and format by LogConsumer
+func (l *LogConsumer) GetWriter(service, container string) io.Writer {
+	return splitBuffer{
+		service:   service,
+		container: container,
+		consumer:  l,
+	}
+}
+
+func (l *LogConsumer) computeWidth() {
+	width := 0
+	for n := range l.colors {
+		if len(n) > width {
+			width = len(n)
+		}
+	}
+	l.width = width + 3
+}
+
+// LogConsumer consume logs from services and format them
+type LogConsumer struct {
+	colors map[string]colorFunc
+	width  int
+	writer io.Writer
+}
+
+type splitBuffer struct {
+	service   string
+	container string
+	consumer  *LogConsumer
+}
+
+func (s splitBuffer) Write(b []byte) (n int, err error) {
+	split := bytes.Split(b, []byte{'\n'})
+	for _, line := range split {
+		if len(line) != 0 {
+			s.consumer.Log(s.service, s.container, string(line))
+		}
+	}
+	return len(b), nil
+}

+ 13 - 7
go.mod

@@ -6,6 +6,9 @@ go 1.15
 // we need to create a new release tag for docker/distribution
 replace github.com/docker/distribution => github.com/docker/distribution v0.0.0-20200708230824-53e18a9d9bfe
 
+// unix.SYS_IOCTL has been removed from golang/sys but still in use by docker/docker until 20.x
+replace golang.org/x/sys => golang.org/x/sys v0.0.0-20200826173525-f9321e4c35a6
+
 require (
 	github.com/AlecAivazis/survey/v2 v2.1.1
 	github.com/Azure/azure-sdk-for-go v43.3.0+incompatible
@@ -17,17 +20,18 @@ require (
 	github.com/Azure/go-autorest/autorest/date v0.3.0
 	github.com/Azure/go-autorest/autorest/to v0.4.0
 	github.com/Azure/go-autorest/autorest/validation v0.2.0 // indirect
-	github.com/Microsoft/go-winio v0.4.15-0.20190919025122-fc70bd9a86b5
-	github.com/Microsoft/hcsshim v0.8.9 // indirect
+	github.com/Microsoft/go-winio v0.4.15-0.20200908182639-5b44b70ab3ab
+	github.com/Microsoft/hcsshim v0.8.10 // indirect
 	github.com/aws/aws-sdk-go v1.35.15
-	github.com/awslabs/goformation/v4 v4.15.3
+	github.com/awslabs/goformation/v4 v4.15.2
 	github.com/buger/goterm v0.0.0-20200322175922-2f3e71b85129
 	github.com/compose-spec/compose-go v0.0.0-20201116112017-777513ca88e2
-	github.com/containerd/console v1.0.1
+	github.com/containerd/console v1.0.0
 	github.com/containerd/containerd v1.3.5 // indirect
+	github.com/containerd/continuity v0.0.0-20200928162600-f2cc35102c2a // indirect
 	github.com/docker/cli v0.0.0-20200528204125-dd360c7c0de8
 	github.com/docker/distribution v0.0.0-00010101000000-000000000000 // indirect
-	github.com/docker/docker v17.12.0-ce-rc1.0.20200309214505-aa6a9891b09c+incompatible
+	github.com/docker/docker v17.12.0-ce-rc1.0.20200916142827-bd33bbf0497b+incompatible
 	github.com/docker/docker-credential-helpers v0.6.3 // indirect
 	github.com/docker/go-connections v0.4.0
 	github.com/docker/go-units v0.4.0
@@ -45,8 +49,10 @@ require (
 	github.com/joho/godotenv v1.3.0
 	github.com/labstack/echo v3.3.10+incompatible
 	github.com/labstack/gommon v0.3.0 // indirect
-	github.com/moby/term v0.0.0-20200915141129-7f0af18e79f2
+	github.com/moby/term v0.0.0-20201110203204-bea5bbe245bf
 	github.com/morikuni/aec v1.0.0
+	github.com/onsi/ginkgo v1.14.2 // indirect
+	github.com/onsi/gomega v1.10.2 // indirect
 	github.com/opencontainers/go-digest v1.0.0
 	github.com/opencontainers/runc v0.1.1 // indirect
 	github.com/pkg/errors v0.9.1
@@ -58,7 +64,7 @@ require (
 	github.com/stretchr/testify v1.6.1
 	github.com/valyala/fasttemplate v1.2.1 // indirect
 	golang.org/x/mod v0.3.0
-	golang.org/x/net v0.0.0-20201026091529-146b70c837a4
+	golang.org/x/net v0.0.0-20200822124328-c89045814202
 	golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43
 	golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9
 	google.golang.org/grpc v1.33.1

+ 37 - 80
go.sum

@@ -1,3 +1,4 @@
+bazil.org/fuse v0.0.0-20160811212531-371fbbdaa898/go.mod h1:Xbm+BRKSBEpa4q4hTSxohYNQpsxXPbPry4JJWOB3LB8=
 cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
 cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
 cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
@@ -79,10 +80,10 @@ github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBp
 github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
 github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
 github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
-github.com/Microsoft/go-winio v0.4.15-0.20190919025122-fc70bd9a86b5 h1:ygIc8M6trr62pF5DucadTWGdEB4mEyvzi0e2nbcmcyA=
-github.com/Microsoft/go-winio v0.4.15-0.20190919025122-fc70bd9a86b5/go.mod h1:tTuCMEN+UleMWgg9dVx4Hu52b1bJo+59jBh3ajtinzw=
-github.com/Microsoft/hcsshim v0.8.9 h1:VrfodqvztU8YSOvygU+DN1BGaSGxmrNfqOv5oOuX2Bk=
-github.com/Microsoft/hcsshim v0.8.9/go.mod h1:5692vkUqntj1idxauYlpoINNKeqCiG6Sg38RRsjT5y8=
+github.com/Microsoft/go-winio v0.4.15-0.20200908182639-5b44b70ab3ab h1:9pygWVFqbY9lPxM0peffumuVDyMuIMzNLyO9uFjJuQo=
+github.com/Microsoft/go-winio v0.4.15-0.20200908182639-5b44b70ab3ab/go.mod h1:tTuCMEN+UleMWgg9dVx4Hu52b1bJo+59jBh3ajtinzw=
+github.com/Microsoft/hcsshim v0.8.10 h1:k5wTrpnVU2/xv8ZuzGkbXVd3js5zJ8RnumPo5RxiIxU=
+github.com/Microsoft/hcsshim v0.8.10/go.mod h1:g5uw8EV2mAlzqe94tfNBNdr89fnbD/n3HV0OhsddkmM=
 github.com/Netflix/go-expect v0.0.0-20180615182759-c93bf25de8e8 h1:xzYJEypr/85nBpB11F9br+3HUrpgb+fcm5iADzXXYEw=
 github.com/Netflix/go-expect v0.0.0-20180615182759-c93bf25de8e8/go.mod h1:oX5x61PbNXchhh0oikYAH+4Pcfw5LKv21+Jnpr6r6Pc=
 github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
@@ -96,8 +97,8 @@ github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj
 github.com/aws/aws-sdk-go v1.15.11/go.mod h1:mFuSZ37Z9YOHbQEwBWztmVzqXrEkub65tZoCYDt7FT0=
 github.com/aws/aws-sdk-go v1.35.15 h1:JdQNM8hJe+9N9xP53S54NDmX8GCaZn8CCJ4LBHfom4U=
 github.com/aws/aws-sdk-go v1.35.15/go.mod h1:tlPOdRjfxPBpNIwqDj61rmsnA85v9jc0Ps9+muhnW+k=
-github.com/awslabs/goformation/v4 v4.15.3 h1:zVRL+gAW5Xvyhd85KNsJpKaOuArxEyUYKK5+v13Bwrc=
-github.com/awslabs/goformation/v4 v4.15.3/go.mod h1:wB5lKZf1J0MYH1Lt4B9w3opqz0uIjP7MMCAcib3QkwA=
+github.com/awslabs/goformation/v4 v4.15.2 h1:sRfSdC1FnSBhsrz5G0XZZxapEtmJSlkNpnFQJf8ylfs=
+github.com/awslabs/goformation/v4 v4.15.2/go.mod h1:GcJULxCJfloT+3pbqCluXftdEK2AD/UqpS3hkaaBntg=
 github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
 github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
 github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
@@ -120,36 +121,36 @@ github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghf
 github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
 github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
 github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
+github.com/cilium/ebpf v0.0.0-20200110133405-4032b1d8aae3/go.mod h1:MA5e5Lr8slmEg9bt0VpxxWqJlO4iwu3FBdHUzV7wQVg=
 github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
 github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
 github.com/compose-spec/compose-go v0.0.0-20201116112017-777513ca88e2 h1:m6m0rnxmq3mPapGNAWqO6FLYg6GST7tJAAZqJ0pjadk=
 github.com/compose-spec/compose-go v0.0.0-20201116112017-777513ca88e2/go.mod h1:cZJ8y+zQzrYfN5XnEu/kMeDEP0V2D9ojF7Rb4nrneUc=
-github.com/containerd/cgroups v0.0.0-20190919134610-bf292b21730f h1:tSNMc+rJDfmYntojat8lljbt1mgKNpTxUZJsSzJ9Y1s=
-github.com/containerd/cgroups v0.0.0-20190919134610-bf292b21730f/go.mod h1:OApqhQ4XNSNC13gXIwDjhOQxjWa/NxkwZXJ1EvqT0ko=
+github.com/containerd/cgroups v0.0.0-20200531161412-0dbf7f05ba59/go.mod h1:pA0z1pT8KYB3TCXK/ocprsh7MAkoW8bZVzPdih9snmM=
 github.com/containerd/console v0.0.0-20180822173158-c12b1e7919c1/go.mod h1:Tj/on1eG8kiEhd0+fhSDzsPAFESxzBBvdyEgyryXffw=
-github.com/containerd/console v1.0.1 h1:u7SFAJyRqWcG6ogaMAx3KjSTy1e3hT9QxqX7Jco7dRc=
-github.com/containerd/console v1.0.1/go.mod h1:XUsP6YE/mKtz6bxc+I8UiKKTP04qjQL4qcS3XoQ5xkw=
+github.com/containerd/console v1.0.0 h1:fU3UuQapBs+zLJu82NhR11Rif1ny2zfMMAyPJzSN5tQ=
+github.com/containerd/console v1.0.0/go.mod h1:8Pf4gM6VEbTNRIT26AyyU7hxdQU3MvAvxVI0sc00XBE=
 github.com/containerd/containerd v1.3.2/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA=
 github.com/containerd/containerd v1.3.5 h1:l0iDHQtFwcOUmOvdepI6BB67q7beT6sRp2JYsfHS08c=
 github.com/containerd/containerd v1.3.5/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA=
-github.com/containerd/continuity v0.0.0-20190426062206-aaeac12a7ffc h1:TP+534wVlf61smEIq1nwLLAjQVEK2EADoW3CX9AuT+8=
 github.com/containerd/continuity v0.0.0-20190426062206-aaeac12a7ffc/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y=
-github.com/containerd/fifo v0.0.0-20190226154929-a9fb20d87448 h1:PUD50EuOMkXVcpBIA/R95d56duJR9VxhwncsFbNnxW4=
+github.com/containerd/continuity v0.0.0-20200928162600-f2cc35102c2a h1:jEIoR0aA5GogXZ8pP3DUzE+zrhaF6/1rYZy+7KkYEWM=
+github.com/containerd/continuity v0.0.0-20200928162600-f2cc35102c2a/go.mod h1:W0qIOTD7mp2He++YVq+kgfXezRYqzP1uDuMVH1bITDY=
 github.com/containerd/fifo v0.0.0-20190226154929-a9fb20d87448/go.mod h1:ODA38xgv3Kuk8dQz2ZQXpnv/UZZUHUCL7pnLehbXgQI=
 github.com/containerd/go-runc v0.0.0-20180907222934-5a6d9f37cfa3/go.mod h1:IV7qH3hrUgRmyYrtgEeGWJfWbgcHL9CSRruz2Vqcph0=
-github.com/containerd/ttrpc v0.0.0-20190828154514-0e0f228740de h1:dlfGmNcE3jDAecLqwKPMNX6nk2qh1c1Vg1/YTzpOOF4=
 github.com/containerd/ttrpc v0.0.0-20190828154514-0e0f228740de/go.mod h1:PvCDdDGpgqzQIzDW1TphrGLssLDZp2GuS+X5DkEJB8o=
-github.com/containerd/typeurl v0.0.0-20180627222232-a93fcdb778cd h1:JNn81o/xG+8NEo3bC/vx9pbi/g2WI8mtP2/nXzu297Y=
 github.com/containerd/typeurl v0.0.0-20180627222232-a93fcdb778cd/go.mod h1:Cm3kwCdlkCfMSHURc+r6fwoGH6/F1hH3S4sg0rLFWPc=
 github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
 github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
 github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
 github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e h1:Wf6HqHfScWJN9/ZjdUKyjop4mf3Qdd+1TvvltAvM3m8=
 github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
+github.com/coreos/go-systemd/v22 v22.0.0/go.mod h1:xO0FLkIi5MaZafQlIrOotqXZ90ih+1atmu1JpKERPPk=
 github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
+github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
 github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
-github.com/creack/pty v1.1.9 h1:uDmaGzcdjhF4i/plgjmEsriH11Y0o7RKapEf/LDaM3w=
-github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
+github.com/creack/pty v1.1.11 h1:07n33Z8lZxZ2qwegKbObQohDhXDQxiMMz1NOUGYlesw=
+github.com/creack/pty v1.1.11/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@@ -165,8 +166,8 @@ github.com/docker/cli v0.0.0-20200528204125-dd360c7c0de8 h1:JRquW4uqIU+eSilDhuo9
 github.com/docker/cli v0.0.0-20200528204125-dd360c7c0de8/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
 github.com/docker/distribution v0.0.0-20200708230824-53e18a9d9bfe h1:pni13lAFm1g4cjHU6c3n4qGvvJGZQK4VvKRKMseQ42E=
 github.com/docker/distribution v0.0.0-20200708230824-53e18a9d9bfe/go.mod h1:Oqz4IonmMNc2N7GqfTL2xkhCQx0yS6nR+HrOZJnmKIk=
-github.com/docker/docker v17.12.0-ce-rc1.0.20200309214505-aa6a9891b09c+incompatible h1:G2hY8RD7jB9QaSmcb8mYEIg8QbEvVAB7se8+lXHZHfg=
-github.com/docker/docker v17.12.0-ce-rc1.0.20200309214505-aa6a9891b09c+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
+github.com/docker/docker v17.12.0-ce-rc1.0.20200916142827-bd33bbf0497b+incompatible h1:SiUATuP//KecDjpOK2tvZJgeScYAklvyjfK8JZlU6fo=
+github.com/docker/docker v17.12.0-ce-rc1.0.20200916142827-bd33bbf0497b+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
 github.com/docker/docker-credential-helpers v0.6.3 h1:zI2p9+1NQYdnG6sMU26EX4aVGlqbInSQxQXLvzJ4RPQ=
 github.com/docker/docker-credential-helpers v0.6.3/go.mod h1:WRaJzqw3CTB9bk10avuGsjVBZsD05qeibJ1/TYlvc0Y=
 github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ=
@@ -178,6 +179,7 @@ github.com/docker/go-metrics v0.0.1/go.mod h1:cG1hvH2utMXtqgqqYE9plW6lDxS3/5ayHz
 github.com/docker/go-units v0.4.0 h1:3uh0PgVws3nIA0Q+MwDC8yjEPf9zjRfZZWXZYDct3Tw=
 github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
 github.com/docker/libtrust v0.0.0-20150114040149-fa567046d9b1/go.mod h1:cyGadeNEkKy96OOhEzfZl+yxihPEzKnqJwvfuSUqbZE=
+github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
 github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
 github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
 github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
@@ -208,8 +210,7 @@ github.com/gobwas/pool v0.2.0 h1:QEmUOlnSjWtnpRGHF3SauEiOsy82Cup83Vf2LcMlnc8=
 github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
 github.com/gobwas/ws v1.0.4 h1:5eXU1CZhpQdq5kXbKb+sECH5Ia5KiO6CYzIzdlVx6Bs=
 github.com/gobwas/ws v1.0.4/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM=
-github.com/godbus/dbus v0.0.0-20190422162347-ade71ed3457e h1:BWhy2j3IXJhjCbC68FptL43tDKIq8FladmaTs3Xs7Z8=
-github.com/godbus/dbus v0.0.0-20190422162347-ade71ed3457e/go.mod h1:bBOAhwG1umN6/6ZUMtDFBMQR8jRg9O75tm9K00oMsK4=
+github.com/godbus/dbus/v5 v5.0.3/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
 github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
 github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
 github.com/gogo/protobuf v1.3.1 h1:DqDEcV5aeaTmdFBePNpYsp3FlcVH/2ISVVM9Qf8PSls=
@@ -317,6 +318,7 @@ github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpO
 github.com/iancoleman/strcase v0.1.2 h1:gnomlvw9tnV3ITTAxzKSgTF+8kFWcU/f+TgttpXGz1U=
 github.com/iancoleman/strcase v0.1.2/go.mod h1:SK73tn/9oHe+/Y0h39VT4UCxmurVJkR5NA7kMEAOgSE=
 github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
+github.com/imdario/mergo v0.3.6/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
 github.com/imdario/mergo v0.3.11 h1:3tnifQM4i+fbajXKBHXWEH+KvNHqojZ778UH75j3bGA=
 github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
 github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
@@ -343,6 +345,8 @@ github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvW
 github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00=
 github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
 github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
+github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
+github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
 github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515 h1:T+h1c/A9Gawja4Y9mFVWj2vyii2bbUNDw3kt9VxK2EY=
 github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
 github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
@@ -389,8 +393,8 @@ github.com/mitchellh/mapstructure v1.3.3 h1:SzB1nHZ2Xi+17FP0zVQBHIZqvwRN9408fJO8
 github.com/mitchellh/mapstructure v1.3.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
 github.com/mitchellh/osext v0.0.0-20151018003038-5e2d6d41470f/go.mod h1:OkQIRizQZAeMln+1tSwduZz7+Af5oFlKirV/MSYes2A=
 github.com/mjibson/esc v0.2.0/go.mod h1:9Hw9gxxfHulMF5OJKCyhYD7PzlSdhzXyaGEBRPH1OPs=
-github.com/moby/term v0.0.0-20200915141129-7f0af18e79f2 h1:SPoLlS9qUUnXcIY4pvA4CTwYjk0Is5f4UPEkeESr53k=
-github.com/moby/term v0.0.0-20200915141129-7f0af18e79f2/go.mod h1:TjQg8pa4iejrUrjiz0MCtMV38jdMNW4doKSiBrEvCQQ=
+github.com/moby/term v0.0.0-20201110203204-bea5bbe245bf h1:Un6PNx5oMK6CCwO3QTUyPiK2mtZnPrpDl5UnZ64eCkw=
+github.com/moby/term v0.0.0-20201110203204-bea5bbe245bf/go.mod h1:FBS0z0QWA44HXygs7VXDUOGoN/1TV3RuWkLO04am3wc=
 github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
 github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
 github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
@@ -403,16 +407,18 @@ github.com/nxadm/tail v1.4.4 h1:DQuhQpB1tVlglWS2hLQ5OV6B5r8aGxSrPc5Qo6uTN78=
 github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
 github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4=
 github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
+github.com/onsi/ginkgo v1.5.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
 github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
 github.com/onsi/ginkgo v1.12.1 h1:mFwc4LvZ0xpSvDZ3E+k8Yte0hLOMxXUlP+yXtJqkYfQ=
 github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
 github.com/onsi/ginkgo v1.14.2 h1:8mVmC9kjFFmA8H4pKMUhcblgifdkOIXPvbhN1T36q1M=
 github.com/onsi/ginkgo v1.14.2/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY=
+github.com/onsi/gomega v1.2.0/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA=
 github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
 github.com/onsi/gomega v1.10.1 h1:o0+MgICZLuZ7xjH7Vx6zS/zcu93/BEp1VwkIW1mEXCE=
 github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
-github.com/onsi/gomega v1.10.3 h1:gph6h/qe9GSUw1NhH1gp+qb+h8rXD8Cy60Z32Qw3ELA=
-github.com/onsi/gomega v1.10.3/go.mod h1:V9xEwhxec5O8UDM77eCW8vLymOMltsqPVYWrpDsH8xc=
+github.com/onsi/gomega v1.10.2 h1:aY/nuoWlKJud2J6U0E3NWsjlg+0GtwXxgEqthRdzlcs=
+github.com/onsi/gomega v1.10.2/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
 github.com/opencontainers/go-digest v0.0.0-20180430190053-c9281466c8b2/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s=
 github.com/opencontainers/go-digest v1.0.0-rc1/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s=
 github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
@@ -422,8 +428,7 @@ github.com/opencontainers/image-spec v1.0.1/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zM
 github.com/opencontainers/runc v0.0.0-20190115041553-12f6a991201f/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U=
 github.com/opencontainers/runc v0.1.1 h1:GlxAyO6x8rfZYN9Tt0Kti5a/cP41iuiO2yYT0IJGY8Y=
 github.com/opencontainers/runc v0.1.1/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U=
-github.com/opencontainers/runtime-spec v0.1.2-0.20190507144316-5b71a03e2700 h1:eNUVfm/RFLIi1G7flU5/ZRTHvd4kcVuzfRnL6OFlzCI=
-github.com/opencontainers/runtime-spec v0.1.2-0.20190507144316-5b71a03e2700/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0=
+github.com/opencontainers/runtime-spec v1.0.2/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0=
 github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
 github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc=
 github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
@@ -472,6 +477,7 @@ github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeV
 github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
 github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
 github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
+github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
 github.com/sirupsen/logrus v1.7.0 h1:ShrD1U9pZB12TX0cVy0DtePoCH97K8EtX+mg7ZARUtM=
 github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
 github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM=
@@ -485,11 +491,13 @@ github.com/spf13/afero v1.1.2 h1:m8/z1t7/fwjysjQRYbP0RD+bUIF/8tJwPdEZsI83ACI=
 github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
 github.com/spf13/cast v1.3.0 h1:oget//CVOEoFewqQxwr0Ej5yjygnqGkvggSE/gB35Q8=
 github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
+github.com/spf13/cobra v0.0.2-0.20171109065643-2da4a54c5cee/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
 github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
 github.com/spf13/cobra v1.1.1 h1:KfztREH0tPxJJ+geloSLaAkaPkr4ki2Er5quFV1TDo4=
 github.com/spf13/cobra v1.1.1/go.mod h1:WnodtKOvamDL/PwE2M4iKs8aMDBZ5Q5klgD3qfVJQMI=
 github.com/spf13/jwalterweatherman v1.0.0 h1:XHEdyB+EcvlqZamSM4ZOMGlc93t6AcsBEu9Gc1vn7yk=
 github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
+github.com/spf13/pflag v1.0.1-0.20171106142849-4c012f6dcd95/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
 github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
 github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
 github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
@@ -505,7 +513,7 @@ github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd
 github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
 github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
 github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
-github.com/urfave/cli v0.0.0-20171014202726-7bc6a0acffa5/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA=
+github.com/urfave/cli v1.22.2/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
 github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
 github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
 github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8=
@@ -620,10 +628,6 @@ golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81R
 golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
 golang.org/x/net v0.0.0-20200822124328-c89045814202 h1:VvcQYSHwXgi7W+TpUR6A9g6Up98WAHf3f/ulnJ62IyA=
 golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
-golang.org/x/net v0.0.0-20201006153459-a7d1128ccaa0 h1:wBouT66WTYFXdxfVdz9sVWARVd/2vfGcmI45D2gj45M=
-golang.org/x/net v0.0.0-20201006153459-a7d1128ccaa0/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
-golang.org/x/net v0.0.0-20201026091529-146b70c837a4 h1:awiuzyrRjJDb+OXi9ceHO3SDxVoN3JER57mhtqkdQBs=
-golang.org/x/net v0.0.0-20201026091529-146b70c837a4/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
 golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be h1:vEDujvNQGv4jgYKudGeI/+DAX4Jffq6hpD55MmoEvKs=
 golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
 golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@@ -644,55 +648,8 @@ golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208 h1:qwRHBd0NqMbJxfbotnDhm2By
 golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9 h1:SQFwaSi55rU7vdNs9Yr0Z324VNlrF+0wMqRXT4St8ck=
 golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
-golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
-golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
-golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
-golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
-golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
-golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
-golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
-golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
-golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20190514135907-3a4b5fb9f71f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20190530182044-ad28b68e88f1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20190801041406-cbf593c0f2f3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200803210538-64077c9b5642 h1:B6caxRw+hozq68X2MY7jEpZh/cr4/aHLv9xU8Kkadrw=
-golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200831180312-196b9ba8737a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200916030750-2334cc1a136f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f h1:+Nyd8tzPX9R7BWHguqsrbFdRx3WQ/1ib8I44HXV5yTA=
-golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200826173525-f9321e4c35a6 h1:DvY3Zkh7KabQE/kfzMvYvKirSiguP9Q/veMtkYyf0o8=
+golang.org/x/sys v0.0.0-20200826173525-f9321e4c35a6/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=

+ 12 - 10
local/backend.go

@@ -20,7 +20,6 @@ package local
 
 import (
 	"context"
-
 	"github.com/docker/docker/client"
 
 	"github.com/docker/compose-cli/api/compose"
@@ -42,7 +41,7 @@ func init() {
 }
 
 func service(ctx context.Context) (backend.Service, error) {
-	apiClient, err := client.NewClientWithOpts(client.FromEnv)
+	apiClient, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
 	if err != nil {
 		return nil, err
 	}
@@ -53,22 +52,25 @@ func service(ctx context.Context) (backend.Service, error) {
 	}, nil
 }
 
-func (cs *containerService) ContainerService() containers.Service {
-	return cs
+func (s *local) ContainerService() containers.Service {
+	return s.containerService
 }
 
-func (ms *local) ComposeService() compose.Service {
-	return nil
+func (s *local) ComposeService() compose.Service {
+	return s
 }
 
-func (ms *local) SecretsService() secrets.Service {
+func (s *local) SecretsService() secrets.Service {
 	return nil
 }
 
-func (vs *volumeService) VolumeService() volumes.Service {
-	return vs
+func (s *local) VolumeService() volumes.Service {
+	return s.volumeService
 }
 
-func (ms *local) ResourceService() resources.Service {
+func (s *local) ResourceService() resources.Service {
 	return nil
 }
+
+
+

+ 598 - 0
local/compose.go

@@ -0,0 +1,598 @@
+// +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"
+	"encoding/json"
+	"fmt"
+	"golang.org/x/sync/errgroup"
+	"io"
+	"path/filepath"
+	"strconv"
+	"strings"
+	"sync"
+
+	"github.com/compose-spec/compose-go/types"
+	"github.com/docker/compose-cli/api/compose"
+	"github.com/docker/compose-cli/api/containers"
+	"github.com/docker/compose-cli/formatter"
+	"github.com/docker/compose-cli/progress"
+	moby "github.com/docker/docker/api/types"
+	"github.com/docker/docker/api/types/container"
+	"github.com/docker/docker/api/types/filters"
+	"github.com/docker/docker/api/types/mount"
+	"github.com/docker/docker/api/types/network"
+	"github.com/docker/docker/api/types/strslice"
+	"github.com/docker/docker/errdefs"
+	"github.com/docker/docker/pkg/jsonmessage"
+	"github.com/docker/go-connections/nat"
+	"github.com/pkg/errors"
+	"github.com/sanathkr/go-yaml"
+)
+
+func (s *local) Up(ctx context.Context, project *types.Project, detach bool) error {
+	for k, network := range project.Networks {
+		if !network.External.External && network.Name != "" {
+			network.Name = fmt.Sprintf("%s_%s", project.Name, k)
+			project.Networks[k] = network
+		}
+		err := s.ensureNetwork(ctx, network)
+		if err != nil {
+			return err
+		}
+	}
+
+	for k, volume := range project.Volumes {
+		if !volume.External.External && volume.Name != "" {
+			volume.Name = fmt.Sprintf("%s_%s", project.Name, k)
+			project.Volumes[k] = volume
+		}
+		err := s.ensureVolume(ctx, volume)
+		if err != nil {
+			return err
+		}
+	}
+
+	for _, service := range project.Services {
+		err := s.applyPullPolicy(ctx, service)
+		if err != nil {
+			return err
+		}
+	}
+
+	err := inDependencyOrder(ctx, project, func(service types.ServiceConfig) error {
+		return s.ensureService(ctx, project, service)
+	})
+	return err
+}
+
+func getContainerName(c moby.Container) string {
+	// Names return container canonical name /foo  + link aliases /linked_by/foo
+	for _, name := range c.Names {
+		if strings.LastIndex(name, "/") == 0 {
+			return name[1:]
+		}
+	}
+	return c.Names[0][1:]
+}
+
+func (s *local) applyPullPolicy(ctx context.Context, service types.ServiceConfig) error {
+	w := progress.ContextWriter(ctx)
+	// TODO build vs pull should be controlled by pull policy
+	// if service.Build {}
+	if service.Image != "" {
+		_, _, err := s.containerService.apiClient.ImageInspectWithRaw(ctx, service.Image)
+		if err != nil {
+			if errdefs.IsNotFound(err) {
+				stream, err := s.containerService.apiClient.ImagePull(ctx, service.Image, moby.ImagePullOptions{})
+				if err != nil {
+					return err
+				}
+				dec := json.NewDecoder(stream)
+				for {
+					var jm jsonmessage.JSONMessage
+					if err := dec.Decode(&jm); err != nil {
+						if err == io.EOF {
+							break
+						}
+						return err
+					}
+					toProgressEvent(jm, w)
+				}
+			}
+		}
+	}
+	return nil
+}
+
+func toProgressEvent(jm jsonmessage.JSONMessage, w progress.Writer) {
+	if jm.Progress != nil {
+		if jm.Progress.Total != 0 {
+			percentage := int(float64(jm.Progress.Current)/float64(jm.Progress.Total)*100) / 2
+			numSpaces := 50 - percentage
+			w.Event(progress.Event{
+				ID:         jm.ID,
+				Text:       jm.Status,
+				Status:     0,
+				StatusText: fmt.Sprintf("[%s>%s] ", strings.Repeat("=", percentage), strings.Repeat(" ", numSpaces)),
+				Done:       jm.Status == "Pull complete",
+			})
+		} else {
+			if jm.Error != nil {
+				w.Event(progress.Event{
+					ID:         jm.ID,
+					Text:       jm.Status,
+					Status:     progress.Error,
+					StatusText: jm.Error.Message,
+					Done:       true,
+				})
+			} else if jm.Status == "Pull complete" || jm.Status == "Already exists" {
+				w.Event(progress.Event{
+					ID:     jm.ID,
+					Text:   jm.Status,
+					Status: progress.Done,
+					Done:   true,
+				})
+			} else {
+				w.Event(progress.Event{
+					ID:     jm.ID,
+					Text:   jm.Status,
+					Status: progress.Working,
+					Done:   false,
+				})
+			}
+		}
+	}
+}
+
+func (s *local) Down(ctx context.Context, projectName string) error {
+	list, err := s.containerService.apiClient.ContainerList(ctx, moby.ContainerListOptions{
+		Filters: filters.NewArgs(
+			projectFilter(projectName),
+		),
+	})
+	if err != nil {
+		return err
+	}
+
+	eg, ctx := errgroup.WithContext(ctx)
+	w := progress.ContextWriter(ctx)
+	for _, c := range list {
+		container := c
+		eg.Go(func() error {
+			w.Event(progress.Event{
+				ID:     getContainerName(container),
+				Text:   "Stopping",
+				Status: progress.Working,
+				Done:   false,
+			})
+			err := s.containerService.Stop(ctx, container.ID, nil)
+			if err != nil {
+				return err
+			}
+			w.Event(progress.Event{
+				ID:     getContainerName(container),
+				Text:   "Removing",
+				Status: progress.Working,
+				Done:   false,
+			})
+			err = s.containerService.Delete(ctx, container.ID, containers.DeleteRequest{})
+			if err != nil {
+				return err
+			}
+			w.Event(progress.Event{
+				ID:     getContainerName(container),
+				Text:   "Removed",
+				Status: progress.Done,
+				Done:   true,
+			})
+			return nil
+		})
+	}
+	return eg.Wait()
+}
+
+func (s *local) Logs(ctx context.Context, projectName string, w io.Writer) error {
+	list, err := s.containerService.apiClient.ContainerList(ctx, moby.ContainerListOptions{
+		Filters: filters.NewArgs(
+			projectFilter(projectName),
+		),
+	})
+	if err != nil {
+		return err
+	}
+	var wg sync.WaitGroup
+	consumer := formatter.NewLogConsumer(w)
+	for _, c := range list {
+		service := c.Labels[serviceLabel]
+		containerId := c.ID
+		go func() {
+			s.containerService.Logs(ctx,containerId, containers.LogsRequest{
+				Follow: true,
+				Writer: consumer.GetWriter(service, containerId),
+			})
+			wg.Done()
+		}()
+		wg.Add(1)
+	}
+	wg.Wait()
+	return nil
+}
+
+func (s *local) Ps(ctx context.Context, projectName string) ([]compose.ServiceStatus, error) {
+	list, err := s.containerService.apiClient.ContainerList(ctx, moby.ContainerListOptions{
+		Filters: filters.NewArgs(
+			projectFilter(projectName),
+		),
+	})
+	if err != nil {
+		return nil, err
+	}
+	var status []compose.ServiceStatus
+	for _, c := range list {
+		// TODO group by service
+		status = append(status, compose.ServiceStatus{
+			ID:         c.ID,
+			Name:       c.Labels[serviceLabel],
+			Replicas:   0,
+			Desired:    0,
+			Ports:      nil,
+			Publishers: nil,
+		})
+	}
+	return status, nil
+}
+
+
+func (s *local) List(ctx context.Context, projectName string) ([]compose.Stack, error) {
+	_, err := s.containerService.apiClient.ContainerList(ctx, moby.ContainerListOptions{All: true})
+	if err != nil {
+		return nil, err
+	}
+	var stacks []compose.Stack
+	// TODO rebuild stacks based on containers
+	return stacks, nil
+}
+
+func (s *local) Convert(ctx context.Context, project *types.Project, format string) ([]byte, error) {
+	switch format {
+	case "json":
+		return json.MarshalIndent(project, "", "  ")
+	case "yaml":
+		return yaml.Marshal(project)
+	default:
+		return nil, fmt.Errorf("unsupported format %q", format)
+	}
+}
+
+func getContainerCreateOptions(p *types.Project, s types.ServiceConfig, number int, inherit *moby.Container) (*container.Config, *container.HostConfig, *network.NetworkingConfig, error) {
+	hash, err := jsonHash(s)
+	if err != nil {
+		return nil, nil, nil, err
+	}
+	labels := map[string]string{
+		projectLabel:     p.Name,
+		serviceLabel:     s.Name,
+		configHashLabel: hash,
+		containerNumberLabel: strconv.Itoa(number),
+	}
+
+	var (
+		runCmd     strslice.StrSlice
+		entrypoint strslice.StrSlice
+	)
+	if len(s.Command) > 0 {
+		runCmd = strslice.StrSlice(s.Command)
+	}
+	if len(s.Entrypoint) > 0 {
+		entrypoint = strslice.StrSlice(s.Entrypoint)
+	}
+	image := s.Image
+	if s.Image == "" {
+		image = fmt.Sprintf("%s_%s", p.Name, s.Name)
+	}
+
+	var (
+		tty         = s.Tty
+		stdinOpen   = s.StdinOpen
+		attachStdin = false
+	)
+
+	containerConfig := container.Config{
+		Hostname:        s.Hostname,
+		Domainname:      s.DomainName,
+		User:            s.User,
+		ExposedPorts:    buildContainerPorts(s),
+		Tty:             tty,
+		OpenStdin:       stdinOpen,
+		StdinOnce:       true,
+		AttachStdin:     attachStdin,
+		AttachStderr:    true,
+		AttachStdout:    true,
+		Cmd:             runCmd,
+		Image:           image,
+		WorkingDir:      s.WorkingDir,
+		Entrypoint:      entrypoint,
+		NetworkDisabled: s.NetworkMode == "disabled",
+		MacAddress:      s.MacAddress,
+		Labels:          labels,
+		StopSignal:      s.StopSignal,
+		//		Env:             s.Environment, FIXME conversion
+		//		Healthcheck:     s.HealthCheck, FIXME conversion
+		// Volumes:         // FIXME unclear to me the overlap with HostConfig.Mounts
+		// StopTimeout: 	 s.StopGracePeriod FIXME conversion
+	}
+
+	mountOptions, err := buildContainerMountOptions(p, s, inherit)
+	if err != nil {
+		return nil, nil, nil, err
+	}
+
+	bindings, err := buildContainerBindingOptions(s)
+	if err != nil {
+		return nil, nil, nil, err
+	}
+
+	networkMode := getNetworkMode(p, s)
+	hostConfig := container.HostConfig{
+		Mounts:         mountOptions,
+		CapAdd:         strslice.StrSlice(s.CapAdd),
+		CapDrop:        strslice.StrSlice(s.CapDrop),
+		NetworkMode:    networkMode,
+		Init:           s.Init,
+		ReadonlyRootfs: s.ReadOnly,
+		// ShmSize: , TODO
+		Sysctls:      s.Sysctls,
+		PortBindings: bindings,
+	}
+
+	networkConfig := buildDefaultNetworkConfig(s, networkMode)
+	return &containerConfig, &hostConfig, networkConfig, nil
+}
+
+func buildContainerPorts(s types.ServiceConfig) nat.PortSet {
+	ports := nat.PortSet{}
+	for _, p := range s.Ports {
+		p := nat.Port(fmt.Sprintf("%d/%s", p.Target, p.Protocol))
+		ports[p] = struct{}{}
+	}
+	return ports
+}
+
+func buildContainerBindingOptions(s types.ServiceConfig) (nat.PortMap, error) {
+	bindings := nat.PortMap{}
+	for _, port := range s.Ports {
+		p := nat.Port(fmt.Sprintf("%d/%s", port.Target, port.Protocol))
+		bind := []nat.PortBinding{}
+		binding := nat.PortBinding{}
+		if port.Published > 0 {
+			binding.HostPort = fmt.Sprint(port.Published)
+		}
+		bind = append(bind, binding)
+		bindings[p] = bind
+	}
+	return bindings, nil
+}
+
+func buildContainerMountOptions(p *types.Project, s types.ServiceConfig, inherit *moby.Container) ([]mount.Mount, error) {
+	mounts := []mount.Mount{}
+	var inherited []string
+	if inherit != nil {
+		for _, m := range inherit.Mounts {
+			if m.Type == "tmpfs" {
+				continue
+			}
+			src := m.Source
+			if m.Type == "volume" {
+				src = m.Name
+			}
+			mounts = append(mounts, mount.Mount{
+				Type:     m.Type,
+				Source:   src,
+				Target:   m.Destination,
+				ReadOnly: !m.RW,
+			})
+			inherited = append(inherited, m.Destination)
+		}
+	}
+
+	for _, v := range s.Volumes {
+		if contains(inherited, v.Target) {
+			continue
+		}
+		source := v.Source
+		if v.Type == "bind" && !filepath.IsAbs(source) {
+			// FIXME handle ~/
+			source = filepath.Join(p.WorkingDir, source)
+		}
+
+		mounts = append(mounts, mount.Mount{
+			Type:          mount.Type(v.Type),
+			Source:        source,
+			Target:        v.Target,
+			ReadOnly:      v.ReadOnly,
+			Consistency:   mount.Consistency(v.Consistency),
+			BindOptions:   buildBindOption(v.Bind),
+			VolumeOptions: buildVolumeOptions(v.Volume),
+			TmpfsOptions:  buildTmpfsOptions(v.Tmpfs),
+		})
+	}
+	return mounts, nil
+}
+
+func buildBindOption(bind *types.ServiceVolumeBind) *mount.BindOptions {
+	if bind == nil {
+		return nil
+	}
+	return &mount.BindOptions{
+		Propagation: mount.Propagation(bind.Propagation),
+		// NonRecursive: false, FIXME missing from model ?
+	}
+}
+
+func buildVolumeOptions(vol *types.ServiceVolumeVolume) *mount.VolumeOptions {
+	if vol == nil {
+		return nil
+	}
+	return &mount.VolumeOptions{
+		NoCopy: vol.NoCopy,
+		// Labels:       , // FIXME missing from model ?
+		// DriverConfig: , // FIXME missing from model ?
+	}
+}
+
+func buildTmpfsOptions(tmpfs *types.ServiceVolumeTmpfs) *mount.TmpfsOptions {
+	if tmpfs == nil {
+		return nil
+	}
+	return &mount.TmpfsOptions{
+		SizeBytes: tmpfs.Size,
+		// Mode:      , // FIXME missing from model ?
+	}
+}
+
+func buildDefaultNetworkConfig(s types.ServiceConfig, networkMode container.NetworkMode) *network.NetworkingConfig {
+	config := map[string]*network.EndpointSettings{}
+	net := string(networkMode)
+	config[net] = &network.EndpointSettings{
+		Aliases: getAliases(s, s.Networks[net]),
+	}
+
+	return &network.NetworkingConfig{
+		EndpointsConfig: config,
+	}
+}
+
+func getAliases(s types.ServiceConfig, c *types.ServiceNetworkConfig) []string {
+	aliases := []string{s.Name}
+	if c != nil {
+		aliases = append(aliases, c.Aliases...)
+	}
+	return aliases
+}
+
+func getNetworkMode(p *types.Project, service types.ServiceConfig) container.NetworkMode {
+	mode := service.NetworkMode
+	if mode == "" {
+		if len(p.Networks) > 0 {
+			for name := range getNetworksForService(service) {
+				return container.NetworkMode(p.Networks[name].Name)
+			}
+		}
+		return container.NetworkMode("none")
+	}
+
+	/// FIXME incomplete implementation
+	if strings.HasPrefix(mode, "service:") {
+		panic("Not yet implemented")
+	}
+	if strings.HasPrefix(mode, "container:") {
+		panic("Not yet implemented")
+	}
+
+	return container.NetworkMode(mode)
+}
+
+func getNetworksForService(s types.ServiceConfig) map[string]*types.ServiceNetworkConfig {
+	if len(s.Networks) > 0 {
+		return s.Networks
+	}
+	return map[string]*types.ServiceNetworkConfig{"default": nil}
+}
+
+func (s *local) ensureNetwork(ctx context.Context, n types.NetworkConfig) error {
+	_, err := s.containerService.apiClient.NetworkInspect(ctx, n.Name, moby.NetworkInspectOptions{})
+	if err != nil {
+		if errdefs.IsNotFound(err) {
+			createOpts := moby.NetworkCreate{
+				// TODO NameSpace Labels
+				Labels:     n.Labels,
+				Driver:     n.Driver,
+				Options:    n.DriverOpts,
+				Internal:   n.Internal,
+				Attachable: n.Attachable,
+			}
+
+			if n.Ipam.Driver != "" || len(n.Ipam.Config) > 0 {
+				createOpts.IPAM = &network.IPAM{}
+			}
+
+			if n.Ipam.Driver != "" {
+				createOpts.IPAM.Driver = n.Ipam.Driver
+			}
+
+			for _, ipamConfig := range n.Ipam.Config {
+				config := network.IPAMConfig{
+					Subnet: ipamConfig.Subnet,
+				}
+				createOpts.IPAM.Config = append(createOpts.IPAM.Config, config)
+			}
+			w := progress.ContextWriter(ctx)
+			w.Event(progress.Event{
+				ID:         fmt.Sprintf("Network %q", n.Name),
+				Status:     progress.Working,
+				StatusText: "Create",
+				Done:       false,
+			})
+			if _, err := s.containerService.apiClient.NetworkCreate(context.Background(), n.Name, createOpts); err != nil {
+				return errors.Wrapf(err, "failed to create network %s", n.Name)
+			}
+			w.Event(progress.Event{
+				ID:         fmt.Sprintf("Network %q", n.Name),
+				Status:     progress.Done,
+				StatusText: "Created",
+				Done:       true,
+			})
+			return nil
+		} else {
+			return err
+		}
+	}
+	return nil
+}
+
+func (s *local) ensureVolume(ctx context.Context, volume types.VolumeConfig) error {
+	// TODO could identify volume by label vs name
+	_, err := s.volumeService.Inspect(ctx, volume.Name)
+	if err != nil {
+		if errdefs.IsNotFound(err) {
+			w := progress.ContextWriter(ctx)
+			w.Event(progress.Event{
+				ID:         fmt.Sprintf("Volume %q", volume.Name),
+				Status:     progress.Working,
+				StatusText: "Create",
+				Done:       false,
+			})
+			// TODO we miss support for driver_opts and labels
+			_, err := s.volumeService.Create(ctx, volume.Name, nil)
+			w.Event(progress.Event{
+				ID:         fmt.Sprintf("Volume %q", volume.Name),
+				Status:     progress.Done,
+				StatusText: "Created",
+				Done:       true,
+			})
+			if err != nil {
+				return err
+			}
+		}
+		return err
+	}
+	return nil
+}

+ 18 - 10
local/containers.go

@@ -28,6 +28,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/api/types/network"
 	"github.com/docker/docker/client"
 	"github.com/docker/docker/pkg/stdcopy"
 	"github.com/docker/docker/pkg/stringid"
@@ -134,13 +135,21 @@ func (cs *containerService) Run(ctx context.Context, r containers.ContainerConfi
 		},
 	}
 
-	created, err := cs.apiClient.ContainerCreate(ctx, containerConfig, hostConfig, nil, r.ID)
+	id, err := cs.create(ctx, containerConfig, hostConfig, nil, r.ID)
+	if err != nil {
+		return err
+	}
+	return cs.apiClient.ContainerStart(ctx, id, types.ContainerStartOptions{})
+}
+
+func (cs *containerService) create(ctx context.Context, containerConfig *container.Config, hostConfig *container.HostConfig, networkingConfig *network.NetworkingConfig, name string) (string, error) {
+	created, err := cs.apiClient.ContainerCreate(ctx, containerConfig, hostConfig, networkingConfig, name)
 
 	if err != nil {
 		if client.IsErrNotFound(err) {
-			io, err := cs.apiClient.ImagePull(ctx, r.Image, types.ImagePullOptions{})
+			io, err := cs.apiClient.ImagePull(ctx, containerConfig.Image, types.ImagePullOptions{})
 			if err != nil {
-				return err
+				return "", err
 			}
 			scanner := bufio.NewScanner(io)
 
@@ -149,21 +158,20 @@ func (cs *containerService) Run(ctx context.Context, r containers.ContainerConfi
 			}
 
 			if err = scanner.Err(); err != nil {
-				return err
+				return "", err
 			}
 			if err = io.Close(); err != nil {
-				return err
+				return "", err
 			}
-			created, err = cs.apiClient.ContainerCreate(ctx, containerConfig, hostConfig, nil, r.ID)
+			created, err = cs.apiClient.ContainerCreate(ctx, containerConfig, hostConfig, networkingConfig, name)
 			if err != nil {
-				return err
+				return "", err
 			}
 		} else {
-			return err
+			return "", err
 		}
 	}
-
-	return cs.apiClient.ContainerStart(ctx, created.ID, types.ContainerStartOptions{})
+	return created.ID, nil
 }
 
 func (cs *containerService) Start(ctx context.Context, containerID string) error {

+ 241 - 0
local/convergence.go

@@ -0,0 +1,241 @@
+// +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/compose-spec/compose-go/types"
+	"github.com/docker/compose-cli/api/containers"
+	"github.com/docker/compose-cli/progress"
+	moby "github.com/docker/docker/api/types"
+	"github.com/docker/docker/api/types/filters"
+	"github.com/docker/docker/api/types/network"
+	"golang.org/x/sync/errgroup"
+	"strconv"
+)
+
+func (s *local) ensureService(ctx context.Context, project *types.Project, service types.ServiceConfig) error {
+	actual, err := s.containerService.apiClient.ContainerList(ctx, moby.ContainerListOptions{
+		Filters: filters.NewArgs(
+			filters.Arg("label", fmt.Sprintf("%s=%s", projectLabel, project.Name)),
+			filters.Arg("label", fmt.Sprintf("%s=%s", serviceLabel, service.Name)),
+		),
+	})
+	if err != nil {
+		return err
+	}
+
+	scale := getScale(service)
+
+	eg, ctx := errgroup.WithContext(ctx)
+	if len(actual) < scale {
+		next, err := nextContainerNumber(actual)
+		if err != nil {
+			return err
+		}
+		missing := scale - len(actual)
+		for i := 0; i < missing; i++ {
+			number := next + i
+			name := fmt.Sprintf("%s_%s_%d", project.Name, service.Name, number)
+			eg.Go(func() error {
+				return s.createContainer(ctx, project, service, name, number)
+			})
+		}
+	}
+
+	if len(actual) > scale {
+		for i := scale; i < len(actual); i++ {
+			container := actual[i]
+			eg.Go(func() error {
+				err := s.containerService.Stop(ctx, container.ID, nil)
+				if err != nil {
+					return err
+				}
+				return s.containerService.Delete(ctx, container.ID, containers.DeleteRequest{})
+			})
+		}
+		actual = actual[:scale]
+	}
+
+	expected, err := jsonHash(service)
+	if err != nil {
+		return err
+	}
+	for _, container := range actual {
+		container := container
+		diverged := container.Labels[configHashLabel] != expected
+		if diverged {
+			eg.Go(func() error {
+				return s.recreateContainer(ctx, project, service, container)
+			})
+			continue
+		}
+
+		if container.State == "running" {
+			// already running, skip
+			continue
+		}
+
+		eg.Go(func() error {
+			return s.restartContainer(ctx, service, container)
+		})
+	}
+	return eg.Wait()
+}
+
+func nextContainerNumber(containers []moby.Container) (int, error) {
+	max := 0
+	for _, c := range containers {
+		n, err := strconv.Atoi(c.Labels[containerNumberLabel])
+		if err != nil {
+			return 0, err
+		}
+		if n > max {
+			max = n
+		}
+	}
+	return max + 1, nil
+
+}
+
+func getScale(config types.ServiceConfig) int {
+	if config.Deploy != nil && config.Deploy.Replicas != nil {
+		return int(*config.Deploy.Replicas)
+	}
+	if config.Scale != 0 {
+		return config.Scale
+	}
+	return 1
+}
+
+func (s *local) createContainer(ctx context.Context, project *types.Project, service types.ServiceConfig, name string, number int) error {
+	w := progress.ContextWriter(ctx)
+	w.Event(progress.Event{
+		ID:         fmt.Sprintf("Service %q", service.Name),
+		Status:     progress.Working,
+		StatusText: "Create",
+		Done:       false,
+	})
+	err := s.runContainer(ctx, project, service, name, number, nil)
+	if err != nil {
+		return err
+	}
+	w.Event(progress.Event{
+		ID:         fmt.Sprintf("Service %q", service.Name),
+		Status:     progress.Done,
+		StatusText: "Created",
+		Done:       true,
+	})
+	return nil
+}
+
+func (s *local) recreateContainer(ctx context.Context, project *types.Project, service types.ServiceConfig, container moby.Container) error {
+	w := progress.ContextWriter(ctx)
+	w.Event(progress.Event{
+		ID:         fmt.Sprintf("Service %q", service.Name),
+		Status:     progress.Working,
+		StatusText: "Recreate",
+		Done:       false,
+	})
+	err := s.containerService.Stop(ctx, container.ID, nil)
+	if err != nil {
+		return err
+	}
+	name := getContainerName(container)
+	tmpName := fmt.Sprintf("%s_%s", container.ID[:12], name)
+	err = s.containerService.apiClient.ContainerRename(ctx, container.ID, tmpName)
+	if err != nil {
+		return err
+	}
+	number, err := strconv.Atoi(container.Labels[containerNumberLabel])
+	if err != nil {
+		return err
+	}
+	err = s.runContainer(ctx, project, service, name, number, &container)
+	if err != nil {
+		return err
+	}
+	err = s.containerService.Delete(ctx, container.ID, containers.DeleteRequest{})
+	if err != nil {
+		return err
+	}
+	w.Event(progress.Event{
+		ID:         fmt.Sprintf("Service %q", service.Name),
+		Status:     progress.Done,
+		StatusText: "Recreated",
+		Done:       true,
+	})
+	return nil
+}
+
+func (s *local) restartContainer(ctx context.Context, service types.ServiceConfig, container moby.Container) error {
+	w := progress.ContextWriter(ctx)
+	w.Event(progress.Event{
+		ID:         fmt.Sprintf("Service %q", service.Name),
+		Status:     progress.Working,
+		StatusText: "Restart",
+		Done:       false,
+	})
+	err := s.containerService.Start(ctx, container.ID)
+	if err != nil {
+		return err
+	}
+	w.Event(progress.Event{
+		ID:         fmt.Sprintf("Service %q", service.Name),
+		Status:     progress.Done,
+		StatusText: "Restarted",
+		Done:       true,
+	})
+	return nil
+}
+
+func (s *local) runContainer(ctx context.Context, project *types.Project, service types.ServiceConfig, name string, number int, container *moby.Container) error {
+	containerConfig, hostConfig, networkingConfig, err := getContainerCreateOptions(project, service, number, container)
+	if err != nil {
+		return err
+	}
+	id, err := s.containerService.create(ctx, containerConfig, hostConfig, networkingConfig, name)
+	if err != nil {
+		return err
+	}
+	for net := range service.Networks {
+		name := fmt.Sprintf("%s_%s", project.Name, net)
+		err = s.connectContainerToNetwork(ctx, id, service.Name, name)
+		if err != nil {
+			return err
+		}
+	}
+	err = s.containerService.apiClient.ContainerStart(ctx, id, moby.ContainerStartOptions{})
+	if err != nil {
+		return err
+	}
+	return nil
+}
+
+
+func (s *local) connectContainerToNetwork(ctx context.Context, id string, service string, n string) error {
+	err := s.containerService.apiClient.NetworkConnect(ctx, n, id, &network.EndpointSettings{
+		Aliases: []string{service},
+	})
+	if err != nil {
+		return err
+	}
+	return nil
+}

+ 60 - 0
local/dependencies.go

@@ -0,0 +1,60 @@
+// +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"
+	"github.com/compose-spec/compose-go/types"
+	"golang.org/x/sync/errgroup"
+)
+
+func inDependencyOrder(ctx context.Context, project *types.Project, fn func(types.ServiceConfig) error) error {
+	eg, ctx := errgroup.WithContext(ctx)
+	var (
+		scheduled []string
+		ready []string
+	)
+	results := make(chan string)
+	for len(ready) < len(project.Services) {
+		for _, service := range project.Services {
+			if contains(scheduled, service.Name) {
+				continue
+			}
+			if containsAll(ready, service.GetDependencies()) {
+				service := service
+				scheduled = append(scheduled, service.Name)
+				eg.Go(func() error {
+					err := fn(service)
+					if err != nil {
+						close(results)
+						return err
+					}
+					results <- service.Name
+					return nil
+				})
+			}
+		}
+		result, ok := <-results
+		if !ok {
+			break
+		}
+		ready = append(ready, result)
+	}
+	return eg.Wait()
+}

+ 57 - 0
local/dependencies_test.go

@@ -0,0 +1,57 @@
+// +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"
+	"gotest.tools/v3/assert"
+	"testing"
+
+	"github.com/compose-spec/compose-go/types"
+)
+
+func TestInDependencyOrder(t *testing.T) {
+	order := make(chan string)
+	project := types.Project{
+		Services: []types.ServiceConfig{
+			{
+				Name:            "test1",
+				DependsOn: map[string]types.ServiceDependency{
+					"test2": {},
+				},
+			},
+			{
+				Name:            "test2",
+				DependsOn: map[string]types.ServiceDependency{
+					"test3": {},
+				},
+			},
+			{
+				Name:            "test3",
+			},
+		},
+	}
+	go inDependencyOrder(context.TODO(), &project, func(config types.ServiceConfig) error {
+		order <- config.Name
+		return nil
+	})
+	assert.Equal(t, <- order, "test3")
+	assert.Equal(t, <- order, "test2")
+	assert.Equal(t, <- order, "test1")
+}

+ 35 - 0
local/labels.go

@@ -0,0 +1,35 @@
+// +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"
+	"github.com/docker/docker/api/types/filters"
+)
+
+const (
+	projectLabel = "com.docker.compose.project"
+	serviceLabel = "com.docker.compose.service"
+	configHashLabel = "com.docker.compose.config-hash"
+	containerNumberLabel = "com.docker.compose.container-number"
+)
+
+func projectFilter(projectName string) filters.KeyValuePair {
+	return filters.Arg("label", fmt.Sprintf("%s=%s", projectLabel, projectName))
+}

+ 50 - 0
local/util.go

@@ -0,0 +1,50 @@
+// +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 (
+	"encoding/json"
+	"github.com/opencontainers/go-digest"
+)
+
+func jsonHash(o interface{}) (string, error) {
+	bytes, err := json.Marshal(o)
+	if err != nil {
+		return "", nil
+	}
+	return digest.SHA256.FromBytes(bytes).String(), nil
+}
+
+func contains(slice []string, item string) bool {
+	for _, v := range slice {
+		if v == item {
+			return true
+		}
+	}
+	return false
+}
+
+func containsAll(slice []string, items []string) bool {
+	for _, i := range items {
+		if !contains(slice, i) {
+			return false
+		}
+	}
+	return true
+}