From de99add26b71b9081b4fa85b87cda1ee6b2f4d3f Mon Sep 17 00:00:00 2001 From: aiordache Date: Tue, 11 Aug 2020 10:43:11 +0200 Subject: [PATCH] Use docker/api progress writer Signed-off-by: aiordache Signed-off-by: Nicolas De Loof --- ecs/cmd/commands/compose.go | 14 ++- ecs/go.mod | 8 +- ecs/go.sum | 24 ++-- ecs/pkg/amazon/backend/backend.go | 9 ++ ecs/pkg/amazon/backend/down.go | 9 +- ecs/pkg/amazon/backend/logs.go | 10 +- ecs/pkg/amazon/backend/up.go | 35 +++++- ecs/pkg/amazon/backend/wait.go | 39 ++++++- ecs/pkg/compose/api.go | 5 +- ecs/pkg/console/colors.go | 9 ++ ecs/pkg/console/progress.go | 132 ---------------------- ecs/pkg/console/progress_test.go | 65 ----------- ecs/pkg/progress/plain.go | 29 +++++ ecs/pkg/progress/spinner.go | 50 +++++++++ ecs/pkg/progress/tty.go | 177 ++++++++++++++++++++++++++++++ ecs/pkg/progress/writer.go | 112 +++++++++++++++++++ 16 files changed, 491 insertions(+), 236 deletions(-) delete mode 100644 ecs/pkg/console/progress.go delete mode 100644 ecs/pkg/console/progress_test.go create mode 100644 ecs/pkg/progress/plain.go create mode 100644 ecs/pkg/progress/spinner.go create mode 100644 ecs/pkg/progress/tty.go create mode 100644 ecs/pkg/progress/writer.go diff --git a/ecs/cmd/commands/compose.go b/ecs/cmd/commands/compose.go index d0f1a224c..c0ed60093 100644 --- a/ecs/cmd/commands/compose.go +++ b/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 diff --git a/ecs/go.mod b/ecs/go.mod index 594aa539f..b6a1f1d28 100644 --- a/ecs/go.mod +++ b/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 diff --git a/ecs/go.sum b/ecs/go.sum index d73c6e3db..168b5633e 100644 --- a/ecs/go.sum +++ b/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= diff --git a/ecs/pkg/amazon/backend/backend.go b/ecs/pkg/amazon/backend/backend.go index 4fd40c2b8..caf7e17de 100644 --- a/ecs/pkg/amazon/backend/backend.go +++ b/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) } diff --git a/ecs/pkg/amazon/backend/down.go b/ecs/pkg/amazon/backend/down.go index fe89a9f2c..0d235ad73 100644 --- a/ecs/pkg/amazon/backend/down.go +++ b/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) { diff --git a/ecs/pkg/amazon/backend/logs.go b/ecs/pkg/amazon/backend/logs.go index ef5c61e2f..fac65ba94 100644 --- a/ecs/pkg/amazon/backend/logs.go +++ b/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 } diff --git a/ecs/pkg/amazon/backend/up.go b/ecs/pkg/amazon/backend/up.go index 4bbe5bd06..38705e814 100644 --- a/ecs/pkg/amazon/backend/up.go +++ b/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) { diff --git a/ecs/pkg/amazon/backend/wait.go b/ecs/pkg/amazon/backend/wait.go index babcaaf64..82b9863b0 100644 --- a/ecs/pkg/amazon/backend/wait.go +++ b/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 } diff --git a/ecs/pkg/compose/api.go b/ecs/pkg/compose/api.go index 8e99b8ab6..77f170559 100644 --- a/ecs/pkg/compose/api.go +++ b/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) diff --git a/ecs/pkg/console/colors.go b/ecs/pkg/console/colors.go index 517afb672..672b61f5d 100644 --- a/ecs/pkg/console/colors.go +++ b/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) diff --git a/ecs/pkg/console/progress.go b/ecs/pkg/console/progress.go deleted file mode 100644 index 599ac5a7f..000000000 --- a/ecs/pkg/console/progress.go +++ /dev/null @@ -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) -} diff --git a/ecs/pkg/console/progress_test.go b/ecs/pkg/console/progress_test.go deleted file mode 100644 index 552303c2d..000000000 --- a/ecs/pkg/console/progress_test.go +++ /dev/null @@ -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 -} diff --git a/ecs/pkg/progress/plain.go b/ecs/pkg/progress/plain.go new file mode 100644 index 000000000..8e476807d --- /dev/null +++ b/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 +} diff --git a/ecs/pkg/progress/spinner.go b/ecs/pkg/progress/spinner.go new file mode 100644 index 000000000..695a56429 --- /dev/null +++ b/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 +} diff --git a/ecs/pkg/progress/tty.go b/ecs/pkg/progress/tty.go new file mode 100644 index 000000000..6877e0964 --- /dev/null +++ b/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 +} diff --git a/ecs/pkg/progress/writer.go b/ecs/pkg/progress/writer.go new file mode 100644 index 000000000..a5b8e1cdf --- /dev/null +++ b/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 +}