Sfoglia il codice sorgente

Use docker/api progress writer

Signed-off-by: aiordache <[email protected]>
Signed-off-by: Nicolas De Loof <[email protected]>
aiordache 5 anni fa
parent
commit
de99add26b

+ 11 - 3
ecs/cmd/commands/compose.go

@@ -12,6 +12,7 @@ import (
 	amazon "github.com/docker/ecs-plugin/pkg/amazon/backend"
 	"github.com/docker/ecs-plugin/pkg/amazon/cloudformation"
 	"github.com/docker/ecs-plugin/pkg/docker"
+	"github.com/docker/ecs-plugin/pkg/progress"
 	"github.com/spf13/cobra"
 )
 
@@ -79,7 +80,11 @@ func UpCommand(dockerCli command.Cli, options *composeOptions) *cobra.Command {
 			if err != nil {
 				return err
 			}
-			return backend.Up(context.Background(), opts)
+
+			return progress.Run(context.Background(), func(ctx context.Context) error {
+				backend.SetWriter(ctx)
+				return backend.Up(ctx, opts)
+			})
 		}),
 	}
 	cmd.Flags().StringVar(&opts.loadBalancerArn, "load-balancer", "", "")
@@ -124,7 +129,10 @@ func DownCommand(dockerCli command.Cli, options *composeOptions) *cobra.Command
 			if err != nil {
 				return err
 			}
-			return backend.Down(context.Background(), opts)
+			return progress.Run(context.Background(), func(ctx context.Context) error {
+				backend.SetWriter(ctx)
+				return backend.Down(ctx, opts)
+			})
 		}),
 	}
 	cmd.Flags().BoolVar(&opts.DeleteCluster, "delete-cluster", false, "Delete cluster")
@@ -139,7 +147,7 @@ func LogsCommand(dockerCli command.Cli, options *composeOptions) *cobra.Command
 			if err != nil {
 				return err
 			}
-			return backend.Logs(context.Background(), opts)
+			return backend.Logs(context.Background(), opts, os.Stdout)
 		}),
 	}
 	return cmd

+ 5 - 3
ecs/go.mod

@@ -1,7 +1,6 @@
 module github.com/docker/ecs-plugin
 
 require (
-	github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78 // indirect
 	github.com/Microsoft/hcsshim v0.8.7 // indirect
 	github.com/Shopify/logrus-bugsnag v0.0.0-20171204204709-577dee27f20d // indirect
 	github.com/agl/ed25519 v0.0.0-20170116200512-5312a6153412 // indirect
@@ -10,11 +9,13 @@ require (
 	github.com/bitly/go-hostpool v0.1.0 // indirect
 	github.com/bitly/go-simplejson v0.5.0 // indirect
 	github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 // indirect
+	github.com/buger/goterm v0.0.0-20200322175922-2f3e71b85129
 	github.com/bugsnag/bugsnag-go v1.5.3 // indirect
 	github.com/bugsnag/panicwrap v1.2.0 // indirect
 	github.com/cenkalti/backoff v2.2.1+incompatible // indirect
 	github.com/cloudflare/cfssl v1.4.1 // indirect
 	github.com/compose-spec/compose-go v0.0.0-20200811091145-837f8f4de457
+	github.com/containerd/console v1.0.0
 	github.com/containerd/containerd v1.3.2 // indirect
 	github.com/containerd/continuity v0.0.0-20200413184840-d3ef23f19fbb // indirect
 	github.com/docker/cli v0.0.0-20200130152716-5d0cf8839492
@@ -36,16 +37,17 @@ require (
 	github.com/mattn/go-sqlite3 v2.0.3+incompatible // indirect
 	github.com/miekg/pkcs11 v1.0.3 // indirect
 	github.com/mitchellh/mapstructure v1.3.3
-	github.com/morikuni/aec v1.0.0 // indirect
+	github.com/moby/term v0.0.0-20200611042045-63b9a826fb74
+	github.com/morikuni/aec v1.0.0
 	github.com/onsi/ginkgo v1.11.0 // indirect
 	github.com/opencontainers/image-spec v1.0.1 // indirect
-	github.com/pkg/errors v0.9.1
 	github.com/sirupsen/logrus v1.6.0
 	github.com/smartystreets/goconvey v1.6.4 // indirect
 	github.com/spf13/cobra v0.0.5
 	github.com/spf13/pflag v1.0.5
 	github.com/theupdateframework/notary v0.6.1 // indirect
 	github.com/xlab/handysort v0.0.0-20150421192137-fb3537ed64a1 // indirect
+	golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208
 	golang.org/x/time v0.0.0-20191024005414-555d28b269f0 // indirect
 	google.golang.org/grpc v1.27.0 // indirect
 	gopkg.in/dancannon/gorethink.v3 v3.0.5 // indirect

+ 14 - 10
ecs/go.sum

@@ -34,6 +34,8 @@ github.com/bitly/go-simplejson v0.5.0/go.mod h1:cXHtHw4XUPsvGaxgjIAn8PhEWG9NfngE
 github.com/blang/semver v3.1.0+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk=
 github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 h1:DDGfHa7BWjL4YnC6+E63dPcxHo2sUxDIu8g3QgEJdRY=
 github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4=
+github.com/buger/goterm v0.0.0-20200322175922-2f3e71b85129 h1:gfAMKE626QEuKG3si0pdTRcr/YEbBoxY+3GOH3gWvl4=
+github.com/buger/goterm v0.0.0-20200322175922-2f3e71b85129/go.mod h1:u9UyCz2eTrSGy6fbupqJ54eY5c4IC8gREQ1053dK12U=
 github.com/bugsnag/bugsnag-go v1.5.3 h1:yeRUT3mUE13jL1tGwvoQsKdVbAsQx9AJ+fqahKveP04=
 github.com/bugsnag/bugsnag-go v1.5.3/go.mod h1:2oa8nejYd4cQ/b0hMIopN0lCRxU0bueqREvZLWFrtK8=
 github.com/bugsnag/panicwrap v1.2.0 h1:OzrKrRvXis8qEvOkfcxNcYbOd2O7xXS2nnKMEMABFQA=
@@ -54,12 +56,13 @@ github.com/cloudflare/cfssl v1.4.1 h1:vScfU2DrIUI9VPHBVeeAQ0q5A+9yshO1Gz+3QoUQiK
 github.com/cloudflare/cfssl v1.4.1/go.mod h1:KManx/OJPb5QY+y0+o/898AMcM128sF0bURvoVUSjTo=
 github.com/cloudflare/go-metrics v0.0.0-20151117154305-6a9aea36fb41/go.mod h1:eaZPlJWD+G9wseg1BuRXlHnjntPMrywMsyxf+LTOdP4=
 github.com/cloudflare/redoctober v0.0.0-20171127175943-746a508df14c/go.mod h1:6Se34jNoqrd8bTxrmJB2Bg2aoZ2CdSXonils9NsiNgo=
-github.com/compose-spec/compose-go v0.0.0-20200716130117-e87e4f7839e3 h1:+ntlMTrEcScJjlnEOP8P1IIrusJaR93Eazr66YgUueA=
-github.com/compose-spec/compose-go v0.0.0-20200716130117-e87e4f7839e3/go.mod h1:ArodJ6gsEB7iWKrbV3fSHZ08LlBvSVB0Oqg04fX86t4=
 github.com/compose-spec/compose-go v0.0.0-20200811091145-837f8f4de457 h1:8ely1LF7H02sIWz6QjgU53YBCiRpYlM9F9u1MeE1ZPk=
 github.com/compose-spec/compose-go v0.0.0-20200811091145-837f8f4de457/go.mod h1:cS0vAvM6u9yjJgKWIH2yiqYMWO7WGJb+c0Irw+RefqU=
 github.com/containerd/cgroups v0.0.0-20190919134610-bf292b21730f/go.mod h1:OApqhQ4XNSNC13gXIwDjhOQxjWa/NxkwZXJ1EvqT0ko=
+github.com/containerd/console v0.0.0-20180822173158-c12b1e7919c1 h1:uict5mhHFTzKLUCufdSLym7z/J0CbBJT59lYbP9wtbg=
 github.com/containerd/console v0.0.0-20180822173158-c12b1e7919c1/go.mod h1:Tj/on1eG8kiEhd0+fhSDzsPAFESxzBBvdyEgyryXffw=
+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.0-beta.2.0.20190828155532-0293cbd26c69/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA=
 github.com/containerd/containerd v1.3.2 h1:ForxmXkA6tPIvffbrDAcPUIB32QgXkt2XFj+F0UxetA=
 github.com/containerd/containerd v1.3.2/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA=
@@ -76,6 +79,8 @@ github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8Nz
 github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
 github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
 github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE=
+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/daaku/go.zipexe v1.0.0/go.mod h1:z8IiR6TsVLEYKwXAoE/I+8ys/sDkgTzSL0CLnGVd57E=
 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=
@@ -137,8 +142,7 @@ github.com/google/certificate-transparency-go v1.0.21/go.mod h1:QeJfpSbVSfYc7RgB
 github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
 github.com/google/go-cmp v0.3.0 h1:crn/baboCvb5fXaQ0IJ1SGTsTVrWpDsCWC8EGETZijY=
 github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
-github.com/google/go-cmp v0.5.0 h1:/QaMHBdZ26BB3SSst0Iwl10Epc+xhTquomWX0oZEB6w=
-github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 github.com/google/go-cmp v0.5.1 h1:JFrFEBb2xKufg6XkJsJr+WbKb4FQlURi5RUcBveYu9k=
 github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
@@ -157,8 +161,6 @@ github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
 github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
 github.com/imdario/mergo v0.3.6 h1:xTNEAn+kxVO7dTZGu0CegyqKZmoWFI0rF8UxjlB2d28=
 github.com/imdario/mergo v0.3.6/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
-github.com/imdario/mergo v0.3.8 h1:CGgOkSJeqMRmt0D9XLWExdT4m4F1vd3FV3VPt+0VxkQ=
-github.com/imdario/mergo v0.3.8/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
 github.com/imdario/mergo v0.3.10 h1:6q5mVkdH/vYmqngx7kZQTjJ5HRsx+ImorDIEQ+beJgc=
 github.com/imdario/mergo v0.3.10/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
 github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
@@ -229,11 +231,11 @@ github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG
 github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
 github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE=
 github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
-github.com/mitchellh/mapstructure v1.3.2 h1:mRS76wmkOn3KkKAyXDu42V+6ebnXWIztFSYGN7GeoRg=
-github.com/mitchellh/mapstructure v1.3.2/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
 github.com/mitchellh/mapstructure v1.3.3 h1:SzB1nHZ2Xi+17FP0zVQBHIZqvwRN9408fJO8h+eeNA8=
 github.com/mitchellh/mapstructure v1.3.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
 github.com/mjibson/esc v0.2.0/go.mod h1:9Hw9gxxfHulMF5OJKCyhYD7PzlSdhzXyaGEBRPH1OPs=
+github.com/moby/term v0.0.0-20200611042045-63b9a826fb74 h1:kvRIeqJNICemq2UFLx8q/Pj+1IRNZS0XPTaMFkuNsvg=
+github.com/moby/term v0.0.0-20200611042045-63b9a826fb74/go.mod h1:pJ0Ot5YGdTcMdxnPMyGCfAr6fKXe0g9cDlz16MuFEBE=
 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=
@@ -401,7 +403,9 @@ golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJ
 golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU=
 golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208 h1:qwRHBd0NqMbJxfbotnDhm2ByMI1Shq4Y6oRJo21SGJA=
 golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 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=
@@ -419,8 +423,8 @@ golang.org/x/sys v0.0.0-20190801041406-cbf593c0f2f3 h1:4y9KwBHBgBNwDbtu44R5o1fdO
 golang.org/x/sys v0.0.0-20190801041406-cbf593c0f2f3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3 h1:7TYNF4UdlohbFwpNH04CoPMp1cHUZgO1Ebq5r2hIjfo=
 golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20191022100944-742c48ecaeb7 h1:HmbHVPwrPEKPGLAcHSrMe6+hqSUlvZU0rab6x5EXfGU=
-golang.org/x/sys v0.0.0-20191022100944-742c48ecaeb7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e h1:N7DeIrjYszNmSW409R3frPPwglRwMkXSBzwVbkOjLLA=
+golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd h1:xhmwyvizuTgC2qz7ZlMluP20uW+C3Rm0FD/WLDX8884=
 golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=

+ 9 - 0
ecs/pkg/amazon/backend/backend.go

@@ -1,9 +1,12 @@
 package backend
 
 import (
+	"context"
+
 	"github.com/aws/aws-sdk-go/aws"
 	"github.com/aws/aws-sdk-go/aws/session"
 	"github.com/docker/ecs-plugin/pkg/amazon/sdk"
+	"github.com/docker/ecs-plugin/pkg/progress"
 )
 
 func NewBackend(profile string, region string) (*Backend, error) {
@@ -17,6 +20,7 @@ func NewBackend(profile string, region string) (*Backend, error) {
 	if err != nil {
 		return nil, err
 	}
+
 	return &Backend{
 		Region: region,
 		api:    sdk.NewAPI(sess),
@@ -26,4 +30,9 @@ func NewBackend(profile string, region string) (*Backend, error) {
 type Backend struct {
 	Region string
 	api    sdk.API
+	writer progress.Writer
+}
+
+func (b *Backend) SetWriter(context context.Context) {
+	b.writer = progress.ContextWriter(context)
 }

+ 1 - 8
ecs/pkg/amazon/backend/down.go

@@ -5,7 +5,6 @@ import (
 
 	"github.com/compose-spec/compose-go/cli"
 	"github.com/docker/ecs-plugin/pkg/compose"
-	"github.com/docker/ecs-plugin/pkg/console"
 )
 
 func (b *Backend) Down(ctx context.Context, options *cli.ProjectOptions) error {
@@ -18,13 +17,7 @@ func (b *Backend) Down(ctx context.Context, options *cli.ProjectOptions) error {
 	if err != nil {
 		return err
 	}
-
-	w := console.NewProgressWriter()
-	err = b.WaitStackCompletion(ctx, name, compose.StackDelete, w)
-	if err != nil {
-		return err
-	}
-	return nil
+	return b.WaitStackCompletion(ctx, name, compose.StackDelete)
 }
 
 func (b *Backend) projectName(options *cli.ProjectOptions) (string, error) {

+ 8 - 2
ecs/pkg/amazon/backend/logs.go

@@ -1,8 +1,10 @@
 package backend
 
 import (
+	"bytes"
 	"context"
 	"fmt"
+	"io"
 	"os"
 	"os/signal"
 	"strconv"
@@ -13,7 +15,7 @@ import (
 	"github.com/docker/ecs-plugin/pkg/console"
 )
 
-func (b *Backend) Logs(ctx context.Context, options *cli.ProjectOptions) error {
+func (b *Backend) Logs(ctx context.Context, options *cli.ProjectOptions, writer io.Writer) error {
 	name := options.Name
 	if name == "" {
 		project, err := cli.ProjectFromOptions(options)
@@ -26,6 +28,7 @@ func (b *Backend) Logs(ctx context.Context, options *cli.ProjectOptions) error {
 	err := b.api.GetLogs(ctx, name, &logConsumer{
 		colors: map[string]console.ColorFunc{},
 		width:  0,
+		writer: writer,
 	})
 	if err != nil {
 		return err
@@ -45,8 +48,10 @@ func (l *logConsumer) Log(service, container, message string) {
 		l.computeWidth()
 	}
 	prefix := fmt.Sprintf("%-"+strconv.Itoa(l.width)+"s |", service)
+
 	for _, line := range strings.Split(message, "\n") {
-		fmt.Printf("%s %s\n", cf(prefix), line)
+		buf := bytes.NewBufferString(fmt.Sprintf("%s %s\n", cf(prefix), line))
+		l.writer.Write(buf.Bytes())
 	}
 }
 
@@ -63,4 +68,5 @@ func (l *logConsumer) computeWidth() {
 type logConsumer struct {
 	colors map[string]console.ColorFunc
 	width  int
+	writer io.Writer
 }

+ 30 - 5
ecs/pkg/amazon/backend/up.go

@@ -5,12 +5,13 @@ import (
 	"fmt"
 	"os"
 	"os/signal"
+	"strings"
 	"syscall"
 
 	"github.com/compose-spec/compose-go/cli"
 	"github.com/compose-spec/compose-go/types"
 	"github.com/docker/ecs-plugin/pkg/compose"
-	"github.com/docker/ecs-plugin/pkg/console"
+	"github.com/docker/ecs-plugin/pkg/progress"
 )
 
 func (b *Backend) Up(ctx context.Context, options *cli.ProjectOptions) error {
@@ -82,10 +83,12 @@ func (b *Backend) Up(ctx context.Context, options *cli.ProjectOptions) error {
 		}
 	}
 
-	fmt.Println()
-	w := console.NewProgressWriter()
 	for k := range template.Resources {
-		w.ResourceEvent(k, "PENDING", "")
+		b.writer.Event(progress.Event{
+			ID:         k,
+			Status:     progress.Working,
+			StatusText: "Pending",
+		})
 	}
 
 	signalChan := make(chan os.Signal, 1)
@@ -96,7 +99,29 @@ func (b *Backend) Up(ctx context.Context, options *cli.ProjectOptions) error {
 		b.Down(ctx, options)
 	}()
 
-	return b.WaitStackCompletion(ctx, project.Name, operation, w)
+	err = b.WaitStackCompletion(ctx, project.Name, operation)
+	// update status for external resources (LB and cluster)
+	loadBalancerName := fmt.Sprintf("%.32s", fmt.Sprintf("%sLoadBalancer", strings.Title(project.Name)))
+	for k := range template.Resources {
+		switch k {
+		case "Cluster":
+			if cluster == "" {
+				continue
+			}
+		case loadBalancerName:
+			if lb == "" {
+				continue
+			}
+		default:
+			continue
+		}
+		b.writer.Event(progress.Event{
+			ID:         k,
+			Status:     progress.Done,
+			StatusText: "",
+		})
+	}
+	return err
 }
 
 func (b Backend) GetVPC(ctx context.Context, project *types.Project) (string, error) {

+ 33 - 6
ecs/pkg/amazon/backend/wait.go

@@ -8,10 +8,11 @@ import (
 	"time"
 
 	"github.com/aws/aws-sdk-go/aws"
-	"github.com/docker/ecs-plugin/pkg/console"
+	"github.com/docker/ecs-plugin/pkg/compose"
+	"github.com/docker/ecs-plugin/pkg/progress"
 )
 
-func (b *Backend) WaitStackCompletion(ctx context.Context, name string, operation int, w console.ProgressWriter) error {
+func (b *Backend) WaitStackCompletion(ctx context.Context, name string, operation int) error {
 	knownEvents := map[string]struct{}{}
 
 	// Get the unique Stack ID so we can collect events without getting some from previous deployments with same name
@@ -22,7 +23,6 @@ func (b *Backend) WaitStackCompletion(ctx context.Context, name string, operatio
 
 	ticker := time.NewTicker(1 * time.Second)
 	done := make(chan bool)
-
 	go func() {
 		b.api.WaitStackComplete(ctx, stackID, operation) //nolint:errcheck
 		ticker.Stop()
@@ -55,11 +55,38 @@ func (b *Backend) WaitStackCompletion(ctx context.Context, name string, operatio
 			resource := aws.StringValue(event.LogicalResourceId)
 			reason := aws.StringValue(event.ResourceStatusReason)
 			status := aws.StringValue(event.ResourceStatus)
-			w.ResourceEvent(resource, status, reason)
-			if stackErr == nil && strings.HasSuffix(status, "_FAILED") {
-				stackErr = fmt.Errorf(reason)
+			progressStatus := progress.Working
+
+			switch status {
+			case "CREATE_COMPLETE":
+				if operation == compose.StackCreate {
+					progressStatus = progress.Done
+
+				}
+			case "UPDATE_COMPLETE":
+				if operation == compose.StackUpdate {
+					progressStatus = progress.Done
+				}
+			case "DELETE_COMPLETE":
+				if operation == compose.StackDelete {
+					progressStatus = progress.Done
+				}
+			default:
+				if strings.HasSuffix(status, "_FAILED") {
+					progressStatus = progress.Error
+					if stackErr == nil {
+						operation = compose.StackDelete
+						stackErr = fmt.Errorf(reason)
+					}
+				}
 			}
+			b.writer.Event(progress.Event{
+				ID:         resource,
+				Status:     progressStatus,
+				StatusText: status,
+			})
 		}
 	}
+
 	return stackErr
 }

+ 3 - 2
ecs/pkg/compose/api.go

@@ -2,6 +2,7 @@ package compose
 
 import (
 	"context"
+	"io"
 
 	"github.com/awslabs/goformation/v4/cloudformation"
 	"github.com/compose-spec/compose-go/cli"
@@ -15,8 +16,8 @@ type API interface {
 	CreateContextData(ctx context.Context, params map[string]string) (contextData interface{}, description string, err error)
 
 	Convert(project *types.Project) (*cloudformation.Template, error)
-	Logs(ctx context.Context, options *cli.ProjectOptions) error
-	Ps(background context.Context, options *cli.ProjectOptions) ([]ServiceStatus, error)
+	Logs(ctx context.Context, options *cli.ProjectOptions, writer io.Writer) error
+	Ps(ctx context.Context, options *cli.ProjectOptions) ([]ServiceStatus, error)
 
 	CreateSecret(ctx context.Context, secret Secret) (string, error)
 	InspectSecret(ctx context.Context, id string) (Secret, error)

+ 9 - 0
ecs/pkg/console/colors.go

@@ -1,6 +1,7 @@
 package console
 
 import (
+	"fmt"
 	"strconv"
 )
 
@@ -24,6 +25,14 @@ var Monochrome = func(s string) string {
 	return s
 }
 
+func ansiColor(code, s string) string {
+	return fmt.Sprintf("%s%s%s", ansi(code), s, ansi("0"))
+}
+
+func ansi(code string) string {
+	return fmt.Sprintf("\033[%sm", code)
+}
+
 func makeColorFunc(code string) ColorFunc {
 	return func(s string) string {
 		return ansiColor(code, s)

+ 0 - 132
ecs/pkg/console/progress.go

@@ -1,132 +0,0 @@
-package console
-
-import (
-	"fmt"
-	"io"
-	"os"
-	"strconv"
-	"strings"
-
-	"github.com/sirupsen/logrus"
-)
-
-type resource struct {
-	name    string
-	status  string
-	details string
-}
-
-type progress struct {
-	console   console
-	resources []*resource
-}
-
-type ProgressWriter interface {
-	ResourceEvent(name string, status string, details string)
-}
-
-func NewProgressWriter() ProgressWriter {
-	return &progress{
-		console: ansiConsole{os.Stdout},
-	}
-}
-
-const (
-	blue  = "36;2"
-	red   = "31;1"
-	green = "32;1"
-)
-
-func (p *progress) ResourceEvent(name string, status string, details string) {
-	if logrus.IsLevelEnabled(logrus.DebugLevel) {
-		logrus.Debugf("> %s : %s %s\n", name, status, details)
-		return
-	}
-	p.console.MoveUp(len(p.resources))
-
-	newResource := true
-	for _, r := range p.resources {
-		if r.name == name {
-			newResource = false
-			r.status = status
-			r.details = details
-			break
-		}
-	}
-	if newResource {
-		p.resources = append(p.resources, &resource{name, status, details})
-	}
-
-	var width int
-	for _, r := range p.resources {
-		l := len(r.name)
-		if width < l {
-			width = l
-		}
-	}
-
-	for _, r := range p.resources {
-		s := r.status
-		if strings.HasSuffix(s, "_IN_PROGRESS") {
-			s = p.console.WiP(s)
-		} else if strings.HasSuffix(s, "_COMPLETE") {
-			s = p.console.OK(s)
-		} else if strings.HasSuffix(s, "_FAILED") {
-			s = p.console.KO(s)
-		}
-		p.console.ClearLine()
-		p.console.Printf("%-"+strconv.Itoa(width)+"s ... %s %s", r.name, s, r.details) // nolint:errcheck
-		p.console.MoveDown(1)
-	}
-}
-
-type console interface {
-	Printf(format string, a ...interface{})
-	MoveUp(int)
-	MoveDown(int)
-	ClearLine()
-	OK(string) string
-	KO(string) string
-	WiP(string) string
-}
-
-type ansiConsole struct {
-	out io.Writer
-}
-
-func (c ansiConsole) Printf(format string, a ...interface{}) {
-	fmt.Fprintf(c.out, format, a...) // nolint:errcheck
-	fmt.Fprintf(c.out, "\r")
-}
-
-func (c ansiConsole) MoveUp(i int) {
-	fmt.Fprintf(c.out, "\033[%dA", i) // nolint:errcheck
-}
-
-func (c ansiConsole) MoveDown(i int) {
-	fmt.Fprintf(c.out, "\033[%dB", i) // nolint:errcheck
-}
-
-func (c ansiConsole) ClearLine() {
-	fmt.Fprint(c.out, "\033[2K\r") // nolint:errcheck
-}
-
-func (c ansiConsole) OK(s string) string {
-	return ansiColor(green, s)
-}
-
-func (c ansiConsole) KO(s string) string {
-	return ansiColor(red, s)
-}
-
-func (c ansiConsole) WiP(s string) string {
-	return ansiColor(blue, s)
-}
-
-func ansiColor(code, s string) string {
-	return fmt.Sprintf("%s%s%s", ansi(code), s, ansi("0"))
-}
-
-func ansi(code string) string {
-	return fmt.Sprintf("\033[%sm", code)
-}

+ 0 - 65
ecs/pkg/console/progress_test.go

@@ -1,65 +0,0 @@
-package console
-
-import (
-	"fmt"
-	"testing"
-
-	"gotest.tools/v3/assert"
-)
-
-func TestProgressWriter(t *testing.T) {
-	c := &bufferConsole{}
-	p := progress{
-		console: c,
-	}
-	p.ResourceEvent("resource1", "CREATE_IN_PROGRESS", "")
-	assert.Equal(t, c.lines[0], "resource1 ... CREATE_IN_PROGRESS ")
-
-	p.ResourceEvent("resource2_long_name", "CREATE_IN_PROGRESS", "ok")
-	assert.Equal(t, c.lines[0], "resource1           ... CREATE_IN_PROGRESS ")
-	assert.Equal(t, c.lines[1], "resource2_long_name ... CREATE_IN_PROGRESS ok")
-
-	p.ResourceEvent("resource2_long_name", "CREATE_COMPLETE", "done")
-	assert.Equal(t, c.lines[0], "resource1           ... CREATE_IN_PROGRESS ")
-	assert.Equal(t, c.lines[1], "resource2_long_name ... CREATE_COMPLETE done")
-
-	p.ResourceEvent("resource1", "CREATE_FAILED", "oups")
-	assert.Equal(t, c.lines[0], "resource1           ... CREATE_FAILED oups")
-	assert.Equal(t, c.lines[1], "resource2_long_name ... CREATE_COMPLETE done")
-}
-
-type bufferConsole struct {
-	pos   int
-	lines []string
-}
-
-func (b *bufferConsole) Printf(format string, a ...interface{}) {
-	b.lines[b.pos] = fmt.Sprintf(format, a...)
-}
-
-func (b *bufferConsole) MoveUp(i int) {
-	b.pos -= i
-}
-
-func (b *bufferConsole) MoveDown(i int) {
-	b.pos += i
-}
-
-func (b *bufferConsole) ClearLine() {
-	if len(b.lines) <= b.pos {
-		b.lines = append(b.lines, "")
-	}
-	b.lines[b.pos] = ""
-}
-
-func (b *bufferConsole) OK(s string) string {
-	return s
-}
-
-func (b *bufferConsole) KO(s string) string {
-	return s
-}
-
-func (b *bufferConsole) WiP(s string) string {
-	return s
-}

+ 29 - 0
ecs/pkg/progress/plain.go

@@ -0,0 +1,29 @@
+package progress
+
+import (
+	"context"
+	"fmt"
+	"io"
+)
+
+type plainWriter struct {
+	out  io.Writer
+	done chan bool
+}
+
+func (p *plainWriter) Start(ctx context.Context) error {
+	select {
+	case <-ctx.Done():
+		return ctx.Err()
+	case <-p.done:
+		return nil
+	}
+}
+
+func (p *plainWriter) Event(e Event) {
+	fmt.Println(e.ID, e.Text, e.StatusText)
+}
+
+func (p *plainWriter) Stop() {
+	p.done <- true
+}

+ 50 - 0
ecs/pkg/progress/spinner.go

@@ -0,0 +1,50 @@
+package progress
+
+import (
+	"runtime"
+	"time"
+)
+
+type spinner struct {
+	time  time.Time
+	index int
+	chars []string
+	stop  bool
+	done  string
+}
+
+func newSpinner() *spinner {
+	chars := []string{
+		"⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏",
+	}
+	done := "⠿"
+
+	if runtime.GOOS == "windows" {
+		chars = []string{"-"}
+		done = "-"
+	}
+
+	return &spinner{
+		index: 0,
+		time:  time.Now(),
+		chars: chars,
+		done:  done,
+	}
+}
+
+func (s *spinner) String() string {
+	if s.stop {
+		return s.done
+	}
+
+	d := time.Since(s.time)
+	if d.Milliseconds() > 100 {
+		s.index = (s.index + 1) % len(s.chars)
+	}
+
+	return s.chars[s.index]
+}
+
+func (s *spinner) Stop() {
+	s.stop = true
+}

+ 177 - 0
ecs/pkg/progress/tty.go

@@ -0,0 +1,177 @@
+package progress
+
+import (
+	"context"
+	"fmt"
+	"io"
+	"runtime"
+	"strings"
+	"sync"
+	"time"
+
+	"github.com/buger/goterm"
+	"github.com/morikuni/aec"
+)
+
+type ttyWriter struct {
+	out      io.Writer
+	events   map[string]Event
+	eventIDs []string
+	repeated bool
+	numLines int
+	done     chan bool
+	mtx      *sync.RWMutex
+}
+
+func (w *ttyWriter) Start(ctx context.Context) error {
+	ticker := time.NewTicker(100 * time.Millisecond)
+
+	for {
+		select {
+		case <-ctx.Done():
+			w.print()
+			return ctx.Err()
+		case <-w.done:
+			w.print()
+			return nil
+		case <-ticker.C:
+			w.print()
+		}
+	}
+}
+
+func (w *ttyWriter) Stop() {
+	w.done <- true
+}
+
+func (w *ttyWriter) Event(e Event) {
+	w.mtx.Lock()
+	defer w.mtx.Unlock()
+	if !StringContains(w.eventIDs, e.ID) {
+		w.eventIDs = append(w.eventIDs, e.ID)
+	}
+	if _, ok := w.events[e.ID]; ok {
+		last := w.events[e.ID]
+		switch e.Status {
+		case Done, Error:
+			if last.Status != e.Status {
+				last.stop()
+			}
+		}
+		last.Status = e.Status
+		last.Text = e.Text
+		last.StatusText = e.StatusText
+		w.events[e.ID] = last
+	} else {
+		e.startTime = time.Now()
+		e.spinner = newSpinner()
+		w.events[e.ID] = e
+	}
+}
+
+func (w *ttyWriter) print() {
+	w.mtx.Lock()
+	defer w.mtx.Unlock()
+	if len(w.eventIDs) == 0 {
+		return
+	}
+	terminalWidth := goterm.Width()
+	b := aec.EmptyBuilder
+	for i := 0; i <= w.numLines; i++ {
+		b = b.Up(1)
+	}
+	if !w.repeated {
+		b = b.Down(1)
+	}
+	w.repeated = true
+	fmt.Fprint(w.out, b.Column(0).ANSI)
+
+	// Hide the cursor while we are printing
+	fmt.Fprint(w.out, aec.Hide)
+	defer fmt.Fprint(w.out, aec.Show)
+
+	firstLine := fmt.Sprintf("[+] Running %d/%d", numDone(w.events), w.numLines)
+	if w.numLines != 0 && numDone(w.events) == w.numLines {
+		firstLine = aec.Apply(firstLine, aec.BlueF)
+	}
+	fmt.Fprintln(w.out, firstLine)
+
+	var statusPadding int
+	for _, v := range w.eventIDs {
+		l := len(fmt.Sprintf("%s %s", w.events[v].ID, w.events[v].Text))
+		if statusPadding < l {
+			statusPadding = l
+		}
+	}
+
+	numLines := 0
+	for _, v := range w.eventIDs {
+		line := lineText(w.events[v], terminalWidth, statusPadding, runtime.GOOS != "windows")
+		// nolint: errcheck
+		fmt.Fprint(w.out, line)
+		numLines++
+	}
+
+	w.numLines = numLines
+}
+
+func lineText(event Event, terminalWidth, statusPadding int, color bool) string {
+	endTime := time.Now()
+	if event.Status != Working {
+		endTime = event.endTime
+	}
+
+	elapsed := endTime.Sub(event.startTime).Seconds()
+
+	textLen := len(fmt.Sprintf("%s %s", event.ID, event.Text))
+	padding := statusPadding - textLen
+	if padding < 0 {
+		padding = 0
+	}
+	text := fmt.Sprintf(" %s %s %s%s %s",
+		event.spinner.String(),
+		event.ID,
+		event.Text,
+		strings.Repeat(" ", padding),
+		event.StatusText,
+	)
+	timer := fmt.Sprintf("%.1fs\n", elapsed)
+	o := align(text, timer, terminalWidth)
+
+	if color {
+		color := aec.WhiteF
+		if event.Status == Done {
+			color = aec.BlueF
+		}
+		if event.Status == Error {
+			color = aec.RedF
+		}
+		return aec.Apply(o, color)
+	}
+
+	return o
+}
+
+func numDone(events map[string]Event) int {
+	i := 0
+	for _, e := range events {
+		if e.Status == Done {
+			i++
+		}
+	}
+	return i
+}
+
+func align(l, r string, w int) string {
+	return fmt.Sprintf("%-[2]*[1]s %[3]s", l, w-len(r)-1, r)
+}
+
+// StringContains check if an array contains a specific value
+func StringContains(array []string, needle string) bool {
+	for _, val := range array {
+		if val == needle {
+			return true
+		}
+	}
+	return false
+}

+ 112 - 0
ecs/pkg/progress/writer.go

@@ -0,0 +1,112 @@
+package progress
+
+import (
+	"context"
+	"os"
+	"sync"
+	"time"
+
+	"github.com/containerd/console"
+	"github.com/moby/term"
+	"golang.org/x/sync/errgroup"
+)
+
+// EventStatus indicates the status of an action
+type EventStatus int
+
+const (
+	// Working means that the current task is working
+	Working EventStatus = iota
+	// Done means that the current task is done
+	Done
+	// Error means that the current task has errored
+	Error
+)
+
+// Event reprensents a progress event
+type Event struct {
+	ID         string
+	Text       string
+	Status     EventStatus
+	StatusText string
+	Done       bool
+
+	startTime time.Time
+	endTime   time.Time
+	spinner   *spinner
+}
+
+func (e *Event) stop() {
+	e.endTime = time.Now()
+	e.spinner.Stop()
+}
+
+// Writer can write multiple progress events
+type Writer interface {
+	Start(context.Context) error
+	Stop()
+	Event(Event)
+}
+
+type writerKey struct{}
+
+// WithContextWriter adds the writer to the context
+func WithContextWriter(ctx context.Context, writer Writer) context.Context {
+	return context.WithValue(ctx, writerKey{}, writer)
+}
+
+// ContextWriter returns the writer from the context
+func ContextWriter(ctx context.Context) Writer {
+	s, _ := ctx.Value(writerKey{}).(Writer)
+	return s
+}
+
+type progressFunc func(context.Context) error
+
+// Run will run a writer and the progress function
+// in parallel
+func Run(ctx context.Context, pf progressFunc) error {
+	eg, _ := errgroup.WithContext(ctx)
+	w, err := NewWriter(os.Stderr)
+	if err != nil {
+		return err
+	}
+	eg.Go(func() error {
+		return w.Start(context.Background())
+	})
+
+	ctx = WithContextWriter(ctx, w)
+
+	eg.Go(func() error {
+		defer w.Stop()
+		return pf(ctx)
+	})
+
+	return eg.Wait()
+}
+
+// NewWriter returns a new multi-progress writer
+func NewWriter(out console.File) (Writer, error) {
+	_, isTerminal := term.GetFdInfo(out)
+
+	if isTerminal {
+		con, err := console.ConsoleFromFile(out)
+		if err != nil {
+			return nil, err
+		}
+
+		return &ttyWriter{
+			out:      con,
+			eventIDs: []string{},
+			events:   map[string]Event{},
+			repeated: false,
+			done:     make(chan bool),
+			mtx:      &sync.RWMutex{},
+		}, nil
+	}
+
+	return &plainWriter{
+		out:  out,
+		done: make(chan bool),
+	}, nil
+}