From 953a7a3f4c8de879947a838a7cb63e7dd5935470 Mon Sep 17 00:00:00 2001 From: Nicolas De Loof <nicolas.deloof@gmail.com> Date: Fri, 10 Apr 2020 16:09:03 +0200 Subject: [PATCH 001/198] Initial commit Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com> From 1312eec07728db069c868d59d89136136906c42a Mon Sep 17 00:00:00 2001 From: Nicolas De Loof <nicolas.deloof@gmail.com> Date: Fri, 10 Apr 2020 16:10:08 +0200 Subject: [PATCH 002/198] Project skaffloding Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com> --- ecs/LICENSE | 191 ++++++++++++++++++++++++++++++++++++++++++++++++++ ecs/README.md | 1 + ecs/go.mod | 3 + 3 files changed, 195 insertions(+) create mode 100644 ecs/LICENSE create mode 100644 ecs/README.md create mode 100644 ecs/go.mod diff --git a/ecs/LICENSE b/ecs/LICENSE new file mode 100644 index 000000000..6d8d58fb6 --- /dev/null +++ b/ecs/LICENSE @@ -0,0 +1,191 @@ + + Apache License + Version 2.0, January 2004 + https://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + Copyright 2013-2018 Docker, Inc. + + 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 + + https://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. diff --git a/ecs/README.md b/ecs/README.md new file mode 100644 index 000000000..128f6e0c1 --- /dev/null +++ b/ecs/README.md @@ -0,0 +1 @@ +# Docker CLI plugin for Amazon ECS diff --git a/ecs/go.mod b/ecs/go.mod new file mode 100644 index 000000000..960098816 --- /dev/null +++ b/ecs/go.mod @@ -0,0 +1,3 @@ +module github.com/docker/ecs-plugin + +go 1.13 From ba6c599de227c27f0efab0d49662e83958d669e4 Mon Sep 17 00:00:00 2001 From: Nicolas De Loof <nicolas.deloof@gmail.com> Date: Tue, 14 Apr 2020 08:40:52 +0200 Subject: [PATCH 003/198] This is a CLI plugin Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com> --- ecs/Makefile | 10 + ecs/cmd/main/main.go | 69 +++++++ ecs/go.mod | 47 +++++ ecs/go.sum | 400 +++++++++++++++++++++++++++++++++++++++ ecs/pkg/amazon/client.go | 7 + ecs/pkg/compose/opts.go | 16 ++ 6 files changed, 549 insertions(+) create mode 100644 ecs/Makefile create mode 100644 ecs/cmd/main/main.go create mode 100644 ecs/go.sum create mode 100644 ecs/pkg/amazon/client.go create mode 100644 ecs/pkg/compose/opts.go diff --git a/ecs/Makefile b/ecs/Makefile new file mode 100644 index 000000000..d8cc4a504 --- /dev/null +++ b/ecs/Makefile @@ -0,0 +1,10 @@ +build: + go build -v -o dist/docker-ecs cmd/main/main.go + +test: ## Run tests + go test ./... -v + +dev: build + ln -f -s "${PWD}/dist/docker-ecs" "${HOME}/.docker/cli-plugins/docker-ecs" + +.PHONY: build test dev \ No newline at end of file diff --git a/ecs/cmd/main/main.go b/ecs/cmd/main/main.go new file mode 100644 index 000000000..e248b0c70 --- /dev/null +++ b/ecs/cmd/main/main.go @@ -0,0 +1,69 @@ +package main + +import ( + "fmt" + "github.com/docker/cli/cli-plugins/manager" + "github.com/docker/cli/cli-plugins/plugin" + "github.com/docker/cli/cli/command" + "github.com/docker/ecs-plugin/pkg/compose" + "github.com/spf13/cobra" +) + +const version = "0.0.1" + +func main() { + plugin.Run(func(dockerCli command.Cli) *cobra.Command { + cmd := NewRootCmd("ecs", dockerCli) + return cmd + }, manager.Metadata{ + SchemaVersion: "0.1.0", + Vendor: "Docker Inc.", + Version: version, + Experimental: true, + }) +} + +type clusterOptions struct { + region string + cluster string +} + +// NewRootCmd returns the base root command. +func NewRootCmd(name string, dockerCli command.Cli) *cobra.Command { + var opts clusterOptions + + cmd := &cobra.Command{ + Short: "Docker ECS", + Long: `run multi-container applications on Amazon ECS.`, + Use: name, + Annotations: map[string]string{"experimentalCLI": "true"}, + } + cmd.AddCommand( + VersionCommand(), + ComposeCommand(&opts), + ) + cmd.Flags().StringVarP(&opts.cluster, "cluster", "c", "default", "ECS cluster") + cmd.Flags().StringVarP(&opts.region, "region", "r", "", "AWS region") + + return cmd +} + +func VersionCommand() *cobra.Command { + return &cobra.Command{ + Use: "version", + Short: "Show version.", + RunE: func(cmd *cobra.Command, args []string) error { + fmt.Printf("Docker ECS plugin %s\n", version) + return nil + }, + } +} + +func ComposeCommand(clusteropts *clusterOptions) *cobra.Command { + cmd := &cobra.Command{ + Use: "compose", + } + opts := &compose.ProjectOptions{} + opts.AddFlags(cmd.Flags()) + return cmd +} diff --git a/ecs/go.mod b/ecs/go.mod index 960098816..573b02b4b 100644 --- a/ecs/go.mod +++ b/ecs/go.mod @@ -1,3 +1,50 @@ 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 + github.com/aws/aws-sdk-go v1.28.9 + 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/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-20200409090215-53c0040c9127 + 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 + github.com/docker/distribution v2.7.1+incompatible // indirect + github.com/docker/docker v1.4.2-0.20200128034134-2ebaeef943cc // indirect + github.com/docker/docker-credential-helpers v0.6.3 // indirect + github.com/docker/go v1.5.1-1 // indirect + github.com/docker/go-metrics v0.0.1 // indirect + github.com/docker/libtrust v0.0.0-20160708172513-aabc10ec26b7 // indirect + github.com/go-sql-driver/mysql v1.5.0 // indirect + github.com/gofrs/uuid v3.2.0+incompatible // indirect + github.com/gogo/protobuf v1.3.1 // indirect + github.com/gorilla/mux v1.7.3 // indirect + github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed // indirect + github.com/jinzhu/gorm v1.9.12 // indirect + github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0 // indirect + github.com/lib/pq v1.3.0 // indirect + github.com/mattn/go-sqlite3 v2.0.3+incompatible // indirect + github.com/miekg/pkcs11 v1.0.3 // indirect + github.com/morikuni/aec v1.0.0 // indirect + github.com/opencontainers/image-spec v1.0.1 // indirect + github.com/spf13/cobra v0.0.5 + github.com/spf13/pflag v1.0.3 + github.com/theupdateframework/notary v0.6.1 // indirect + github.com/xlab/handysort v0.0.0-20150421192137-fb3537ed64a1 // indirect + 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 + gopkg.in/fatih/pool.v2 v2.0.0 // indirect + gopkg.in/gorethink/gorethink.v3 v3.0.5 // indirect + vbom.ml/util v0.0.0-20180919145318-efcd4e0f9787 // indirect +) + go 1.13 diff --git a/ecs/go.sum b/ecs/go.sum new file mode 100644 index 000000000..3bc9036db --- /dev/null +++ b/ecs/go.sum @@ -0,0 +1,400 @@ +bazil.org/fuse v0.0.0-20160811212531-371fbbdaa898/go.mod h1:Xbm+BRKSBEpa4q4hTSxohYNQpsxXPbPry4JJWOB3LB8= +bitbucket.org/liamstask/goose v0.0.0-20150115234039-8488cc47d90c/go.mod h1:hSVuE3qU7grINVSwrmzHfpg9k87ALBk+XaualNyUzI4= +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78 h1:w+iIsaOQNcT7OZ575w+acHgRric5iCyQh+xv+KJ4HB8= +github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8= +github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/GeertJohan/go.incremental v1.0.0/go.mod h1:6fAjUhbVuX1KcMD3c8TEgVUqmo4seqhv0i0kdATSkM0= +github.com/GeertJohan/go.rice v1.0.0/go.mod h1:eH6gbSOAUv07dQuZVnBmoDP8mgsM1rtixis4Tib9if0= +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.7 h1:ptnOoufxGSzauVTsdE+wMYnCWA301PdoN4xg5oRdZpg= +github.com/Microsoft/hcsshim v0.8.7/go.mod h1:OHd7sQqRFrYd3RmSgbgji+ctCwkbq2wbEYNSzOYtcBQ= +github.com/Shopify/logrus-bugsnag v0.0.0-20171204204709-577dee27f20d h1:UrqY+r/OJnIp5u0s1SbQ8dVfLCZJsnvazdBP5hS4iRs= +github.com/Shopify/logrus-bugsnag v0.0.0-20171204204709-577dee27f20d/go.mod h1:HI8ITrYtUY+O+ZhtlqUnD8+KwNPOyugEhfP9fdUIaEQ= +github.com/agl/ed25519 v0.0.0-20170116200512-5312a6153412 h1:w1UutsfOrms1J05zt7ISrnJIXKzwaspym5BTKGx93EI= +github.com/agl/ed25519 v0.0.0-20170116200512-5312a6153412/go.mod h1:WPjqKcmVOxf0XSf3YxCJs6N6AOSrOx3obionmG7T0y0= +github.com/akavel/rsrc v0.8.0/go.mod h1:uLoCtb9J+EyAqh+26kdrTgmzRBFPGOolLWKpdxkKq+c= +github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= +github.com/aws/aws-sdk-go v1.28.9 h1:grIuBQc+p3dTRXerh5+2OxSuWFi0iXuxbFdTSg0jaW0= +github.com/aws/aws-sdk-go v1.28.9/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= +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= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/bitly/go-hostpool v0.1.0 h1:XKmsF6k5el6xHG3WPJ8U0Ku/ye7njX7W81Ng7O2ioR0= +github.com/bitly/go-hostpool v0.1.0/go.mod h1:4gOCgp6+NZnVqlKyZ/iBZFTAJKembaVENUpMkpg42fw= +github.com/bitly/go-simplejson v0.5.0 h1:6IH+V8/tVMab511d5bn4M7EwGXZf9Hj6i2xSwkNEM+Y= +github.com/bitly/go-simplejson v0.5.0/go.mod h1:cXHtHw4XUPsvGaxgjIAn8PhEWG9NfngEKAMDJEczWVA= +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/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= +github.com/bugsnag/panicwrap v1.2.0/go.mod h1:D/8v3kj0zr8ZAKg1AQ6crr+5VwKN5eIywRkfhyM/+dE= +github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEet03Pg2g16Swow4= +github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/certifi/gocertifi v0.0.0-20180118203423-deb3ae2ef261/go.mod h1:GJKEexRPVJrBSOjoqN5VNOIKJ5Q3RViH6eu3puDRwx4= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cloudflare/backoff v0.0.0-20161212185259-647f3cdfc87a/go.mod h1:rzgs2ZOiguV6/NpiDgADjRLPNyZlApIWxKpkT+X8SdY= +github.com/cloudflare/cfssl v1.4.1 h1:vScfU2DrIUI9VPHBVeeAQ0q5A+9yshO1Gz+3QoUQiKw= +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-20200131085702-0b38cc2d8e6b h1:VK0c2Hfrg9FHjvJpWfGwiHPP2UeU0QZ6/5/dN0ehbSQ= +github.com/compose-spec/compose-go v0.0.0-20200131085702-0b38cc2d8e6b/go.mod h1:KoJjdV81vERSyYVuQD63nryyt8ZTlqTWe8JuJIMhRo4= +github.com/compose-spec/compose-go v0.0.0-20200409090215-53c0040c9127 h1:mAsQN3s19glh3KBOQjiRYBhqaX1SdzNqhB3/cuqgSbE= +github.com/compose-spec/compose-go v0.0.0-20200409090215-53c0040c9127/go.mod h1:1PUpzRF1O/65VOqXZuwpCuYY7pJxbIq1jbAvAf62FGM= +github.com/containerd/cgroups v0.0.0-20190919134610-bf292b21730f/go.mod h1:OApqhQ4XNSNC13gXIwDjhOQxjWa/NxkwZXJ1EvqT0ko= +github.com/containerd/console v0.0.0-20180822173158-c12b1e7919c1/go.mod h1:Tj/on1eG8kiEhd0+fhSDzsPAFESxzBBvdyEgyryXffw= +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= +github.com/containerd/continuity v0.0.0-20190426062206-aaeac12a7ffc/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y= +github.com/containerd/continuity v0.0.0-20200413184840-d3ef23f19fbb h1:nXPkFq8X1a9ycY3GYQpFNxHh3j2JgY7zDZfq2EXMIzk= +github.com/containerd/continuity v0.0.0-20200413184840-d3ef23f19fbb/go.mod h1:Dq467ZllaHgAtVp4p1xUQWBrFXR9s/wyoTpG8zOJGkY= +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/go.mod h1:PvCDdDGpgqzQIzDW1TphrGLssLDZp2GuS+X5DkEJB8o= +github.com/containerd/typeurl v0.0.0-20180627222232-a93fcdb778cd/go.mod h1:Cm3kwCdlkCfMSHURc+r6fwoGH6/F1hH3S4sg0rLFWPc= +github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= +github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk= +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/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= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/denisenkom/go-mssqldb v0.0.0-20191124224453-732737034ffd h1:83Wprp6ROGeiHFAP8WJdI2RoxALQYgdllERc3N5N2DM= +github.com/denisenkom/go-mssqldb v0.0.0-20191124224453-732737034ffd/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU= +github.com/docker/cli v0.0.0-20200130152716-5d0cf8839492 h1:FwssHbCDJD025h+BchanCwE1Q8fyMgqDr2mOQAWOLGw= +github.com/docker/cli v0.0.0-20200130152716-5d0cf8839492/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= +github.com/docker/distribution v2.7.1+incompatible h1:a5mlkVzth6W5A4fOsS3D2EO5BUmsJpcB+cRlLU7cSug= +github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= +github.com/docker/docker v1.4.2-0.20200128034134-2ebaeef943cc h1:2xtQXEoAs2hjCs4Ez4/KT2mDaYrXwcUi7TCkfyT+n2k= +github.com/docker/docker v1.4.2-0.20200128034134-2ebaeef943cc/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 v1.5.1-1 h1:hr4w35acWBPhGBXlzPoHpmZ/ygPjnmFVxGxxGnMyP7k= +github.com/docker/go v1.5.1-1/go.mod h1:CADgU4DSXK5QUlFslkQu2yW2TKzFZcXq/leZfM0UH5Q= +github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ= +github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= +github.com/docker/go-metrics v0.0.1 h1:AgB/0SvBxihN0X8OR4SjsblXkbMvalQ8cjmtKQ2rQV8= +github.com/docker/go-metrics v0.0.1/go.mod h1:cG1hvH2utMXtqgqqYE9plW6lDxS3/5ayHzueweSI3Vw= +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-20160708172513-aabc10ec26b7 h1:UhxFibDNY/bfvqU5CAUmr9zpesgbU6SWc8/B4mflAE4= +github.com/docker/libtrust v0.0.0-20160708172513-aabc10ec26b7/go.mod h1:cyGadeNEkKy96OOhEzfZl+yxihPEzKnqJwvfuSUqbZE= +github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5 h1:Yzb9+7DPaBjB8zlTR87/ElzFsnQfuHnVUVqpZZIcV5Y= +github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5/go.mod h1:a2zkGnVExMxdzMo3M0Hi/3sEU+cWnZpSni0O6/Yb/P0= +github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/getsentry/raven-go v0.0.0-20180121060056-563b81fc02b7/go.mod h1:KungGk8q33+aIAZUIVWZDr2OfAEBsO49PX4NzFV5kcQ= +github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= +github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= +github.com/go-sql-driver/mysql v1.3.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= +github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= +github.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs= +github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= +github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/godbus/dbus v0.0.0-20190422162347-ade71ed3457e/go.mod h1:bBOAhwG1umN6/6ZUMtDFBMQR8jRg9O75tm9K00oMsK4= +github.com/gofrs/uuid v3.2.0+incompatible h1:y12jRkkFxsd7GpqdSZ+/KCs/fJbqpEXSGd4+jfEaewE= +github.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= +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= +github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= +github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe h1:lXe2qZdvpiX5WZkZR4hgp4KJVfY3nMkvmwbVkpv1rVY= +github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/google/certificate-transparency-go v1.0.21 h1:Yf1aXowfZ2nuboBsg7iYGLmwsOARdV86pfH3g95wXmE= +github.com/google/certificate-transparency-go v1.0.21/go.mod h1:QeJfpSbVSfYc7RgB3gJFj9cbuQMMchQxrWXz8Ruopmg= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/gorilla/mux v1.7.3 h1:gnP5JzjVOuiZD07fKKToCAOjS0yOpj/qPETTXCCS6hw= +github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= +github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed h1:5upAirOpQc1Q53c0bnx2ufif5kANL7bfZWcc6VJWJd8= +github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed/go.mod h1:tMWxXQ9wFIaZeTI9F+hmhFiGpFmhOHzyShyFUhRm0H4= +github.com/hashicorp/errwrap v0.0.0-20141028054710-7554cd9344ce/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-multierror v0.0.0-20161216184304-ed905158d874/go.mod h1:JMRHfdO9jKNzS/+BTlxCjKNQHg/jZAft8U7LloJvN7I= +github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/imdario/mergo v0.3.8/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= +github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= +github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= +github.com/jinzhu/gorm v1.9.12 h1:Drgk1clyWT9t9ERbzHza6Mj/8FY/CqMyVzOiHviMo6Q= +github.com/jinzhu/gorm v1.9.12/go.mod h1:vhTjlKSJUTWNtcbQtrMBFCxy7eXTzeCAzfL5fBZT/Qs= +github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= +github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= +github.com/jinzhu/now v1.0.1 h1:HjfetcXq097iXP0uoPCdnM4Efp5/9MsM0/M+XOTeR3M= +github.com/jinzhu/now v1.0.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= +github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af h1:pmfjZENx5imkbgOkpRUYLnmbU7UEFbjtDA2hxJ1ichM= +github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= +github.com/jmhodges/clock v0.0.0-20160418191101-880ee4c33548/go.mod h1:hGT6jSUVzF6no3QaDSMLGLEHtHSBSefs+MgcDWnmhmo= +github.com/jmoiron/sqlx v0.0.0-20180124204410-05cef0741ade/go.mod h1:IiEW3SEiiErVyFdH8NTuWjSifiEQKUoyK3LNqr2kCHU= +github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= +github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0 h1:iQTw/8FWTuc7uiaSepXwyf3o52HaUYcV+Tu66S3F5GA= +github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0/go.mod h1:1NbS8ALrpOvjt0rHPNLyCIeMtbizbir8U//inJ+zuB8= +github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= +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/kisielk/sqlstruct v0.0.0-20150923205031-648daed35d49/go.mod h1:yyMNCyc/Ib3bDTKd379tNMpB/7/H5TjM2Y9QJ5THLbE= +github.com/kisom/goutils v1.1.0/go.mod h1:+UBTfd78habUYWFbNWTJNG+jNG/i/lGURakr4A/yNRw= +github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +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= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kylelemons/go-gypsy v0.0.0-20160905020020-08cad365cd28/go.mod h1:T/T7jsxVqf9k/zYOqbgNAsANsjxTd1Yq3htjDhQ1H0c= +github.com/lib/pq v0.0.0-20180201184707-88edab080323/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/lib/pq v1.1.1/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/lib/pq v1.3.0 h1:/qkRGz8zljWiDcFvgpwUpwIAPu3r07TDvs3Rws+o/pU= +github.com/lib/pq v1.3.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/magiconair/properties v1.8.0 h1:LLgXmsheXeRoUOBOjtwPQCWIYqM/LU1ayDtDePerRcY= +github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= +github.com/mattn/go-shellwords v1.0.9/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y= +github.com/mattn/go-shellwords v1.0.10/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y= +github.com/mattn/go-sqlite3 v1.10.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= +github.com/mattn/go-sqlite3 v2.0.1+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= +github.com/mattn/go-sqlite3 v2.0.3+incompatible h1:gXHsfypPkaMZrKbD5209QV9jbUTJKjyR5WD3HYQSd+U= +github.com/mattn/go-sqlite3 v2.0.3+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= +github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= +github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/miekg/pkcs11 v1.0.3 h1:iMwmD7I5225wv84WxIG/bmxz9AXjWvTWIbM/TYHvWtw= +github.com/miekg/pkcs11 v1.0.3/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs= +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.2.2/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +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= +github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= +github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= +github.com/mreiferson/go-httpclient v0.0.0-20160630210159-31f0106b4474/go.mod h1:OQA4XLvDbMgS8P0CevmM4m9Q3Jq4phKUzcocxuGJ5m8= +github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/nkovacs/streamquote v0.0.0-20170412213628-49af9bddb229/go.mod h1:0aYXnNPJ8l7uZxf45rWW1a/uME32OF0rhiYGNQ2oF2E= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.10.1/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk= +github.com/opencontainers/go-digest v0.0.0-20180430190053-c9281466c8b2/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s= +github.com/opencontainers/go-digest v1.0.0-rc1 h1:WzifXhOVOEOuFYOJAW6aQqW0TooG2iki3E3Ii+WN7gQ= +github.com/opencontainers/go-digest v1.0.0-rc1/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s= +github.com/opencontainers/image-spec v1.0.1 h1:JMemWkRwHx4Zj+fVxWoMCFm/8sYGGrUVojFA6h/TRcI= +github.com/opencontainers/image-spec v1.0.1/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= +github.com/opencontainers/runc v0.0.0-20190115041553-12f6a991201f/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U= +github.com/opencontainers/runtime-spec v0.1.2-0.20190507144316-5b71a03e2700/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= +github.com/opencontainers/runtime-tools v0.0.0-20181011054405-1d69bd0f9c39/go.mod h1:r3f7wjNzSs2extwzU3Y+6pKfobzPh+kKFJ3ofN+3nfs= +github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc= +github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= +github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.8.1-0.20171018195549-f15c970de5b7/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= +github.com/prometheus/client_golang v1.1.0 h1:BQ53HtBmfOitExawJ6LokA4x8ov/z0SYYb0+HxJfRI8= +github.com/prometheus/client_golang v1.1.0/go.mod h1:I1FGZT9+L76gKKOs5djB6ezCbFQP1xR9D75/vuwEF3g= +github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4 h1:gQz4mCbXsO+nc9n1hCxHcGA3Zx3Eo+UHZoInFGUIXNM= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/common v0.6.0 h1:kRhiuYSXR3+uv2IbVbZhUxK5zVD/2pp3Gd2PpvPkpEo= +github.com/prometheus/common v0.6.0/go.mod h1:eBmuwkDJBwy6iBfxCBob6t6dR6ENT/y+J+Zk0j9GMYc= +github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/procfs v0.0.3 h1:CTwfnzjQ+8dS6MhHHu4YswVAD99sL2wjPqP+VkURmKE= +github.com/prometheus/procfs v0.0.3/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ= +github.com/prometheus/procfs v0.0.5 h1:3+auTFlqw+ZaQYJARz6ArODtkaIwtvBTx3N2NehQlL8= +github.com/prometheus/procfs v0.0.5/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ= +github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= +github.com/sirupsen/logrus v1.0.4-0.20170822132746-89742aefa4b2/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc= +github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/sirupsen/logrus v1.3.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 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4= +github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= +github.com/sirupsen/logrus v1.5.0 h1:1N5EYkVAPEywqZRJd7cwnRtCb6xJx7NH3T3WUTF980Q= +github.com/sirupsen/logrus v1.5.0/go.mod h1:+F7Ogzej0PZc/94MaYx/nvG9jOFMD2osvC3s+Squfpo= +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.5 h1:f0B+LkLX6DtmRH1isoNA9VTtNUK9K8xYd28JNNfOv/s= +github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU= +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 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg= +github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/viper v1.3.2 h1:VUFqw5KcqRf7i70GOzW7N+Q7+gxVBkSSqiXB12+JQ4M= +github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/syndtr/gocapability v0.0.0-20170704070218-db04d3cc01c8/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww= +github.com/theupdateframework/notary v0.6.1 h1:7wshjstgS9x9F5LuB1L5mBI2xNMObWqjz+cjWoom6l0= +github.com/theupdateframework/notary v0.6.1/go.mod h1:MOfgIfmox8s7/7fduvB2xyPPMJCrjRLRizA8OFwpnKY= +github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= +github.com/urfave/cli v0.0.0-20171014202726-7bc6a0acffa5/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= +github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= +github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8= +github.com/weppos/publicsuffix-go v0.4.0/go.mod h1:z3LCPQ38eedDQSwmsSRW4Y7t2L8Ln16JPQ02lHAdn5k= +github.com/weppos/publicsuffix-go v0.5.0 h1:rutRtjBJViU/YjcI5d80t4JAVvDltS6bciJg2K1HrLU= +github.com/weppos/publicsuffix-go v0.5.0/go.mod h1:z3LCPQ38eedDQSwmsSRW4Y7t2L8Ln16JPQ02lHAdn5k= +github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= +github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= +github.com/xeipuuv/gojsonschema v0.0.0-20180618132009-1d523034197f/go.mod h1:5yf86TLmAcydyeJq5YvxkGPE2fm/u4myDekKRoLuqhs= +github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= +github.com/xlab/handysort v0.0.0-20150421192137-fb3537ed64a1 h1:j2hhcujLRHAg872RWAV5yaUrEjHEObwDv3aImCaNLek= +github.com/xlab/handysort v0.0.0-20150421192137-fb3537ed64a1/go.mod h1:QcJo0QPSfTONNIgpN5RA8prR7fF8nkF6cTWTcNerRO8= +github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= +github.com/ziutek/mymysql v1.5.4/go.mod h1:LMSpPZ6DbqWFxNCHW77HeMg9I646SAhApZ/wKdgO/C0= +github.com/zmap/rc2 v0.0.0-20131011165748-24b9757f5521/go.mod h1:3YZ9o3WnatTIZhuOtot4IcUfzoKVjUHqu6WALIyI0nE= +github.com/zmap/zcertificate v0.0.0-20180516150559-0e3d58b1bac4/go.mod h1:5iU54tB79AMBcySS0R2XIyZBAVmeHranShAFELYx7is= +github.com/zmap/zcrypto v0.0.0-20190729165852-9051775e6a2e h1:mvOa4+/DXStR4ZXOks/UsjeFdn5O5JpLUtzqk9U8xXw= +github.com/zmap/zcrypto v0.0.0-20190729165852-9051775e6a2e/go.mod h1:w7kd3qXHh8FNaczNjslXqvFQiv5mMWRXlL9klTUAHc8= +github.com/zmap/zlint v0.0.0-20190806154020-fd021b4cfbeb h1:vxqkjztXSaPVDc8FQCdHTaejm2x747f6yPbnu1h2xkg= +github.com/zmap/zlint v0.0.0-20190806154020-fd021b4cfbeb/go.mod h1:29UiAJNsiVdvTBFCJW8e3q6dcDbOoPkhMgttOSCIMMY= +go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= +golang.org/x/crypto v0.0.0-20171113213409-9f005a07e0d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90PveolxSbWFaJdECFbxSq0Mqo2M= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191205180655-e7c4368fe9dd h1:GGJVjV8waZKRHrgwvtH66z9ZGVurTD1MT0n1Bb+q4aM= +golang.org/x/crypto v0.0.0-20191205180655-e7c4368fe9dd/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a h1:oWX7TPOiFAMXLq8o0ikBYfCJVlRHBcsciT5bXOrH628= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09 h1:KaQtG+aDELoNmXYas3TVkGNYRuq8JQ1aa7LJt8EXVyo= +golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190613194153-d28f0bde5980 h1:dfGZHvZk057jK2MCeWus/TowKpJ8y4AmooUzdBSR9GU= +golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +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/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= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/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-20181205085412-a5c9d58dba9a/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-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190422165155-953cdadca894 h1:Cz4ceDQGXuKRnVBDTS23GTn/pU5OE2C0WrNTOYK1Uuc= +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-20190514135907-3a4b5fb9f71f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190801041406-cbf593c0f2f3 h1:4y9KwBHBgBNwDbtu44R5o1fdOCQUEXhbk/P4A9WmJq0= +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/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/time v0.0.0-20191024005414-555d28b269f0 h1:/5xXl8Y5W96D+TtHSlonuFqGHIWVuyCkGJLwGh9JJFs= +golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190624222133-a101b041ded4/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55 h1:gSJIx1SDwno+2ElGhA4+qG2zF97qiUzTM+rQ0klBOcE= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.27.0 h1:rRYRFMVgRv6E0D70Skyfsr28tDXIuuPZyWGMPdMcnXg= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +gopkg.in/airbrake/gobrake.v2 v2.0.9/go.mod h1:/h5ZAUhDkGaJfjzjKLSjv6zCL6O0LLBxU4K+aSYdM/U= +gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/dancannon/gorethink.v3 v3.0.5 h1:/g7PWP7zUS6vSNmHSDbjCHQh1Rqn8Jy6zSMQxAsBSMQ= +gopkg.in/dancannon/gorethink.v3 v3.0.5/go.mod h1:GXsi1e3N2OcKhcP6nsYABTiUejbWMFO4GY5a4pEaeEc= +gopkg.in/fatih/pool.v2 v2.0.0 h1:xIFeWtxifuQJGk/IEPKsTduEKcKvPmhoiVDGpC40nKg= +gopkg.in/fatih/pool.v2 v2.0.0/go.mod h1:8xVGeu1/2jr2wm5V9SPuMht2H5AEmf5aFMGSQixtjTY= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2/go.mod h1:Xk6kEKp8OKb+X14hQBKWaSkCsqBpgog8nAV2xsGOxlo= +gopkg.in/gorethink/gorethink.v3 v3.0.5 h1:e2Uc/Xe+hpcVQFsj6MuHlYog3r0JYpnTzwDj/y2O4MU= +gopkg.in/gorethink/gorethink.v3 v3.0.5/go.mod h1:+3yIIHJUGMBK+wyPH+iN5TP+88ikFDfZdqTlK3Y9q8I= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo= +gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= +gotest.tools/v3 v3.0.0 h1:d+tVGRu6X0ZBQ+kyAR8JKi6AXhTP2gmQaoIYaGFz634= +gotest.tools/v3 v3.0.0/go.mod h1:TUP+/YtXl/dp++T+SZ5v2zUmLVBHmptSb/ajDLCJ+3c= +gotest.tools/v3 v3.0.2/go.mod h1:3SzNCllyD9/Y+b5r9JIKQ474KzkZyqLqEfYqMsX94Bk= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +k8s.io/kubernetes v1.13.0/go.mod h1:ocZa8+6APFNC2tX1DZASIbocyYT5jHzqFVsY5aoB7Jk= +vbom.ml/util v0.0.0-20180919145318-efcd4e0f9787 h1:O69FD9pJA4WUZlEwYatBEEkRWKQ5cKodWpdKTrCS/iQ= +vbom.ml/util v0.0.0-20180919145318-efcd4e0f9787/go.mod h1:so/NYdZXCz+E3ZpW0uAoCj6uzU2+8OWDFv/HxUSs7kI= diff --git a/ecs/pkg/amazon/client.go b/ecs/pkg/amazon/client.go new file mode 100644 index 000000000..ea788b0b6 --- /dev/null +++ b/ecs/pkg/amazon/client.go @@ -0,0 +1,7 @@ +package amazon + +import "github.com/aws/aws-sdk-go/aws/session" + +type Client struct { + sess *session.Session +} diff --git a/ecs/pkg/compose/opts.go b/ecs/pkg/compose/opts.go new file mode 100644 index 000000000..6755abc2b --- /dev/null +++ b/ecs/pkg/compose/opts.go @@ -0,0 +1,16 @@ +package compose + +import ( + _ "github.com/compose-spec/compose-go/types" + "github.com/spf13/pflag" +) + +type ProjectOptions struct { + ConfigPaths []string + name string +} + +func (o *ProjectOptions) AddFlags(flags *pflag.FlagSet) { + flags.StringArrayVarP(&o.ConfigPaths, "file", "f", nil, "Specify an alternate compose file") + flags.StringVarP(&o.name, "project-name", "n", "", "Specify an alternate project name (default: directory name)") +} From 40bf8c2dae64bb2dad78e241188e70f7df738d3a Mon Sep 17 00:00:00 2001 From: Nicolas De Loof <nicolas.deloof@gmail.com> Date: Tue, 14 Apr 2020 11:42:04 +0200 Subject: [PATCH 004/198] Load a compose file and pass Project to cobra command close #2 Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com> --- ecs/go.mod | 2 + ecs/go.sum | 7 + ecs/pkg/compose/opts.go | 16 +- ecs/pkg/compose/project.go | 165 ++++++++++++++++++ ecs/pkg/compose/project_test.go | 45 +++++ .../simple/compose-with-overrides.yaml | 4 + ecs/pkg/compose/testdata/simple/compose.yaml | 4 + 7 files changed, 242 insertions(+), 1 deletion(-) create mode 100644 ecs/pkg/compose/project.go create mode 100644 ecs/pkg/compose/project_test.go create mode 100644 ecs/pkg/compose/testdata/simple/compose-with-overrides.yaml create mode 100644 ecs/pkg/compose/testdata/simple/compose.yaml diff --git a/ecs/go.mod b/ecs/go.mod index 573b02b4b..1189b1d57 100644 --- a/ecs/go.mod +++ b/ecs/go.mod @@ -35,6 +35,7 @@ require ( github.com/miekg/pkcs11 v1.0.3 // indirect github.com/morikuni/aec v1.0.0 // indirect github.com/opencontainers/image-spec v1.0.1 // indirect + github.com/sirupsen/logrus v1.5.0 github.com/spf13/cobra v0.0.5 github.com/spf13/pflag v1.0.3 github.com/theupdateframework/notary v0.6.1 // indirect @@ -44,6 +45,7 @@ require ( gopkg.in/dancannon/gorethink.v3 v3.0.5 // indirect gopkg.in/fatih/pool.v2 v2.0.0 // indirect gopkg.in/gorethink/gorethink.v3 v3.0.5 // indirect + gotest.tools/v3 v3.0.2 vbom.ml/util v0.0.0-20180919145318-efcd4e0f9787 // indirect ) diff --git a/ecs/go.sum b/ecs/go.sum index 3bc9036db..f2d87a8ca 100644 --- a/ecs/go.sum +++ b/ecs/go.sum @@ -140,6 +140,7 @@ github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/imdario/mergo v0.3.8 h1:CGgOkSJeqMRmt0D9XLWExdT4m4F1vd3FV3VPt+0VxkQ= github.com/imdario/mergo v0.3.8/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= @@ -180,6 +181,7 @@ github.com/lib/pq v1.3.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/magiconair/properties v1.8.0 h1:LLgXmsheXeRoUOBOjtwPQCWIYqM/LU1ayDtDePerRcY= github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/mattn/go-shellwords v1.0.9/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y= +github.com/mattn/go-shellwords v1.0.10 h1:Y7Xqm8piKOO3v10Thp7Z36h4FYFjt5xB//6XvOrs2Gw= github.com/mattn/go-shellwords v1.0.10/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y= github.com/mattn/go-sqlite3 v1.10.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= github.com/mattn/go-sqlite3 v2.0.1+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= @@ -192,6 +194,7 @@ github.com/miekg/pkcs11 v1.0.3/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WT 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.2.2 h1:dxe5oCinTXiTIcfgmZecdCzPmAJKd46KsCWc35r0TV4= github.com/mitchellh/mapstructure v1.2.2/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= 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= @@ -280,9 +283,12 @@ github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPU github.com/weppos/publicsuffix-go v0.4.0/go.mod h1:z3LCPQ38eedDQSwmsSRW4Y7t2L8Ln16JPQ02lHAdn5k= github.com/weppos/publicsuffix-go v0.5.0 h1:rutRtjBJViU/YjcI5d80t4JAVvDltS6bciJg2K1HrLU= github.com/weppos/publicsuffix-go v0.5.0/go.mod h1:z3LCPQ38eedDQSwmsSRW4Y7t2L8Ln16JPQ02lHAdn5k= +github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f h1:J9EGpcZtP0E/raorCMxlFGSTBrsSlaDGf3jU/qvAE2c= github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= +github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0= github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= github.com/xeipuuv/gojsonschema v0.0.0-20180618132009-1d523034197f/go.mod h1:5yf86TLmAcydyeJq5YvxkGPE2fm/u4myDekKRoLuqhs= +github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74= github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= github.com/xlab/handysort v0.0.0-20150421192137-fb3537ed64a1 h1:j2hhcujLRHAg872RWAV5yaUrEjHEObwDv3aImCaNLek= github.com/xlab/handysort v0.0.0-20150421192137-fb3537ed64a1/go.mod h1:QcJo0QPSfTONNIgpN5RA8prR7fF8nkF6cTWTcNerRO8= @@ -392,6 +398,7 @@ gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo= gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= gotest.tools/v3 v3.0.0 h1:d+tVGRu6X0ZBQ+kyAR8JKi6AXhTP2gmQaoIYaGFz634= gotest.tools/v3 v3.0.0/go.mod h1:TUP+/YtXl/dp++T+SZ5v2zUmLVBHmptSb/ajDLCJ+3c= +gotest.tools/v3 v3.0.2 h1:kG1BFyqVHuQoVQiR1bWGnfz/fmHvvuiSPIV7rvl360E= gotest.tools/v3 v3.0.2/go.mod h1:3SzNCllyD9/Y+b5r9JIKQ474KzkZyqLqEfYqMsX94Bk= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/ecs/pkg/compose/opts.go b/ecs/pkg/compose/opts.go index 6755abc2b..9429fb01a 100644 --- a/ecs/pkg/compose/opts.go +++ b/ecs/pkg/compose/opts.go @@ -1,7 +1,7 @@ package compose import ( - _ "github.com/compose-spec/compose-go/types" + "github.com/spf13/cobra" "github.com/spf13/pflag" ) @@ -14,3 +14,17 @@ func (o *ProjectOptions) AddFlags(flags *pflag.FlagSet) { flags.StringArrayVarP(&o.ConfigPaths, "file", "f", nil, "Specify an alternate compose file") flags.StringVarP(&o.name, "project-name", "n", "", "Specify an alternate project name (default: directory name)") } + + +type ProjectFunc func(project *Project, args []string) error + +// WithProject wrap a ProjectFunc into a cobra command +func WithProject(options *ProjectOptions, f ProjectFunc) func(cmd *cobra.Command, args []string) error { + return func(cmd *cobra.Command, args []string) error { + project, err := projectFromOptions(options) + if err != nil { + return err + } + return f(project, args) + } +} diff --git a/ecs/pkg/compose/project.go b/ecs/pkg/compose/project.go new file mode 100644 index 000000000..c2c6c007a --- /dev/null +++ b/ecs/pkg/compose/project.go @@ -0,0 +1,165 @@ +package compose + +import ( + "fmt" + "github.com/compose-spec/compose-go/loader" + "github.com/compose-spec/compose-go/types" + "github.com/sirupsen/logrus" + "io/ioutil" + "os" + "path/filepath" + "regexp" + "strings" +) + +type Project struct { + types.Config + projectDir string + Name string `yaml:"-" json:"-"` +} + +func NewProject(config types.ConfigDetails, name string) (*Project, error) { + model, err := loader.Load(config) + if err != nil { + return nil, err + } + + p := Project{ + Config: *model, + projectDir: config.WorkingDir, + Name: name, + } + return &p, nil +} + + +// projectFromOptions load a compose project based on command line options +func projectFromOptions(options *ProjectOptions) (*Project, error) { + configPath, err := getConfigPathFromOptions(options) + if err != nil { + return nil, err + } + + name := options.name + if name == "" { + name = os.Getenv("COMPOSE_PROJECT_NAME") + } + + workingDir := filepath.Dir(configPath[0]) + + if name == "" { + r := regexp.MustCompile(`[^a-z0-9\\-_]+`) + name = r.ReplaceAllString(strings.ToLower(filepath.Base(workingDir)), "") + } + + configs, err := parseConfigs(configPath) + if err != nil { + return nil, err + } + + return NewProject(types.ConfigDetails{ + WorkingDir: workingDir, + ConfigFiles: configs, + Environment: environment(), + }, name) +} + +func getConfigPathFromOptions(options *ProjectOptions) ([]string, error) { + paths := []string{} + pwd, err := os.Getwd() + if err != nil { + return nil, err + } + + if len(options.ConfigPaths) != 0 { + for _, f := range options.ConfigPaths { + if f == "-" { + paths = append(paths, f) + continue + } + if !filepath.IsAbs(f) { + f = filepath.Join(pwd, f) + } + if _, err := os.Stat(f); err != nil { + return nil, err + } + paths = append(paths, f) + } + return paths, nil + } + + sep := os.Getenv("COMPOSE_FILE_SEPARATOR") + if sep == "" { + sep = string(os.PathListSeparator) + } + f := os.Getenv("COMPOSE_FILE") + if f != "" { + return strings.Split(f, sep), nil + } + + for { + candidates := []string{} + for _, n := range SupportedFilenames { + f := filepath.Join(pwd, n) + if _, err := os.Stat(f); err == nil { + candidates = append(candidates, f) + } + } + if len(candidates) > 0 { + winner := candidates[0] + if len(candidates) > 1 { + logrus.Warnf("Found multiple config files with supported names: %s", strings.Join(candidates, ", ")) + logrus.Warnf("Using %s\n", winner) + } + return []string{winner}, nil + } + parent := filepath.Dir(pwd) + if parent == pwd { + return nil, fmt.Errorf("Can't find a suitable configuration file in this directory or any parent. Are you in the right directory?") + } + pwd = parent + } +} + +var SupportedFilenames = []string{"compose.yaml", "compose.yml", "docker-compose.yml", "docker-compose.yaml"} + +func parseConfigs(configPaths []string) ([]types.ConfigFile, error) { + files := []types.ConfigFile{} + for _, f := range configPaths { + var ( + b []byte + err error + ) + if f == "-" { + b, err = ioutil.ReadAll(os.Stdin) + } else { + if _, err := os.Stat(f); err != nil { + return nil, err + } + b, err = ioutil.ReadFile(f) + } + if err != nil { + return nil, err + } + config, err := loader.ParseYAML(b) + if err != nil { + return nil, err + } + files = append(files, types.ConfigFile{Filename: f, Config: config}) + } + return files, nil +} + +func environment() map[string]string { + return getAsEqualsMap(os.Environ()) +} + +// getAsEqualsMap split key=value formatted strings into a key : value map +func getAsEqualsMap(em []string) map[string]string { + m := make(map[string]string) + for _, v := range em { + kv := strings.SplitN(v, "=", 2) + m[kv[0]] = kv[1] + } + return m +} diff --git a/ecs/pkg/compose/project_test.go b/ecs/pkg/compose/project_test.go new file mode 100644 index 000000000..d5f771404 --- /dev/null +++ b/ecs/pkg/compose/project_test.go @@ -0,0 +1,45 @@ +package compose + +import ( + "gotest.tools/v3/assert" + "os" + "testing" +) + +func Test_project_name(t *testing.T) { + p, err := projectFromOptions(&ProjectOptions{ + name: "my_project", + ConfigPaths: []string{"testdata/simple/compose.yaml"}, + }) + assert.NilError(t, err) + assert.Equal(t, p.Name, "my_project") + + p, err = projectFromOptions(&ProjectOptions{ + name: "", + ConfigPaths: []string{"testdata/simple/compose.yaml"}, + }) + assert.NilError(t, err) + assert.Equal(t, p.Name, "simple") + + os.Setenv("COMPOSE_PROJECT_NAME", "my_project_from_env") + p, err = projectFromOptions(&ProjectOptions{ + name: "", + ConfigPaths: []string{"testdata/simple/compose.yaml"}, + }) + assert.NilError(t, err) + assert.Equal(t, p.Name, "my_project_from_env") +} + +func Test_project_from_set_of_files(t *testing.T) { + p, err := projectFromOptions(&ProjectOptions{ + name: "my_project", + ConfigPaths: []string{ + "testdata/simple/compose.yaml", + "testdata/simple/compose-with-overrides.yaml", + }, + }) + assert.NilError(t, err) + service, err := p.GetService("simple") + assert.NilError(t, err) + assert.Equal(t, service.Image, "haproxy") +} diff --git a/ecs/pkg/compose/testdata/simple/compose-with-overrides.yaml b/ecs/pkg/compose/testdata/simple/compose-with-overrides.yaml new file mode 100644 index 000000000..3dc8a0b6f --- /dev/null +++ b/ecs/pkg/compose/testdata/simple/compose-with-overrides.yaml @@ -0,0 +1,4 @@ +version: "3" +services: + simple: + image: haproxy diff --git a/ecs/pkg/compose/testdata/simple/compose.yaml b/ecs/pkg/compose/testdata/simple/compose.yaml new file mode 100644 index 000000000..4b3f9af21 --- /dev/null +++ b/ecs/pkg/compose/testdata/simple/compose.yaml @@ -0,0 +1,4 @@ +version: "3" +services: + simple: + image: nginx \ No newline at end of file From 91daf0dcc0de009fbc9386b5a665a906519cb20a Mon Sep 17 00:00:00 2001 From: Nicolas De Loof <nicolas.deloof@gmail.com> Date: Tue, 14 Apr 2020 17:44:00 +0200 Subject: [PATCH 005/198] Skeletton for "compose up" command Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com> --- ecs/cmd/main/main.go | 21 +++++++++++++++++++++ ecs/pkg/amazon/client.go | 34 ++++++++++++++++++++++++++++++++-- ecs/pkg/amazon/compose.go | 11 +++++++++++ ecs/pkg/compose/api.go | 5 +++++ 4 files changed, 69 insertions(+), 2 deletions(-) create mode 100644 ecs/pkg/amazon/compose.go create mode 100644 ecs/pkg/compose/api.go diff --git a/ecs/cmd/main/main.go b/ecs/cmd/main/main.go index e248b0c70..83d130218 100644 --- a/ecs/cmd/main/main.go +++ b/ecs/cmd/main/main.go @@ -5,6 +5,7 @@ import ( "github.com/docker/cli/cli-plugins/manager" "github.com/docker/cli/cli-plugins/plugin" "github.com/docker/cli/cli/command" + "github.com/docker/ecs-plugin/pkg/amazon" "github.com/docker/ecs-plugin/pkg/compose" "github.com/spf13/cobra" ) @@ -24,6 +25,7 @@ func main() { } type clusterOptions struct { + profile string region string cluster string } @@ -42,6 +44,7 @@ func NewRootCmd(name string, dockerCli command.Cli) *cobra.Command { VersionCommand(), ComposeCommand(&opts), ) + cmd.Flags().StringVarP(&opts.profile, "profile", "p", "default", "AWS Profile") cmd.Flags().StringVarP(&opts.cluster, "cluster", "c", "default", "ECS cluster") cmd.Flags().StringVarP(&opts.region, "region", "r", "", "AWS region") @@ -65,5 +68,23 @@ func ComposeCommand(clusteropts *clusterOptions) *cobra.Command { } opts := &compose.ProjectOptions{} opts.AddFlags(cmd.Flags()) + + cmd.AddCommand( + UpCommand(clusteropts, opts), + ) return cmd } + +func UpCommand(clusteropts *clusterOptions, opts *compose.ProjectOptions) *cobra.Command { + cmd := &cobra.Command{ + Use: "up", + RunE: compose.WithProject(opts, func(project *compose.Project, args []string) error { + client, err := amazon.NewClient(clusteropts.profile, clusteropts.cluster, clusteropts.region) + if err != nil { + return err + } + return client.ComposeUp(project) + }), + } + return cmd +} \ No newline at end of file diff --git a/ecs/pkg/amazon/client.go b/ecs/pkg/amazon/client.go index ea788b0b6..e1f5d2e9b 100644 --- a/ecs/pkg/amazon/client.go +++ b/ecs/pkg/amazon/client.go @@ -1,7 +1,37 @@ package amazon -import "github.com/aws/aws-sdk-go/aws/session" +import ( + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/session" + "github.com/docker/ecs-plugin/pkg/compose" +) -type Client struct { + +const ( + ProjectTag = "com.docker.compose.project" +) + +func NewClient(profile string, cluster string, region string) (compose.API, error) { + sess, err := session.NewSessionWithOptions(session.Options{ + Profile: profile, + Config: aws.Config{ + Region: aws.String(region), + }, + }) + if err != nil { + return nil, err + } + return &client{ + Cluster: cluster, + Region: region, + sess: sess, + }, nil +} + +type client struct { + Cluster string + Region string sess *session.Session } + +var _ compose.API = &client{} diff --git a/ecs/pkg/amazon/compose.go b/ecs/pkg/amazon/compose.go new file mode 100644 index 000000000..6a1073792 --- /dev/null +++ b/ecs/pkg/amazon/compose.go @@ -0,0 +1,11 @@ +package amazon + +import ( + "fmt" + "github.com/docker/ecs-plugin/pkg/compose" +) + +func (c *client) ComposeUp(project *compose.Project) error { + fmt.Println("TODO Up") + return nil +} diff --git a/ecs/pkg/compose/api.go b/ecs/pkg/compose/api.go new file mode 100644 index 000000000..14e1ca050 --- /dev/null +++ b/ecs/pkg/compose/api.go @@ -0,0 +1,5 @@ +package compose + +type API interface { + ComposeUp(project *Project) error +} \ No newline at end of file From 4542e05ddfe0182c648e45d56b3e15e0bc63a94d Mon Sep 17 00:00:00 2001 From: Nicolas De Loof <nicolas.deloof@gmail.com> Date: Tue, 14 Apr 2020 18:03:33 +0200 Subject: [PATCH 006/198] API calls to register services matching compose.yaml Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com> --- ecs/pkg/amazon/client.go | 12 ++++++ ecs/pkg/amazon/compose.go | 72 ++++++++++++++++++++++++++++++- ecs/pkg/amazon/ecs.go | 21 +++++++++ ecs/pkg/amazon/logs.go | 26 +++++++++++ ecs/pkg/amazon/network.go | 90 +++++++++++++++++++++++++++++++++++++++ ecs/pkg/amazon/roles.go | 48 +++++++++++++++++++++ 6 files changed, 267 insertions(+), 2 deletions(-) create mode 100644 ecs/pkg/amazon/ecs.go create mode 100644 ecs/pkg/amazon/logs.go create mode 100644 ecs/pkg/amazon/network.go create mode 100644 ecs/pkg/amazon/roles.go diff --git a/ecs/pkg/amazon/client.go b/ecs/pkg/amazon/client.go index e1f5d2e9b..8e98a55f0 100644 --- a/ecs/pkg/amazon/client.go +++ b/ecs/pkg/amazon/client.go @@ -3,6 +3,10 @@ package amazon import ( "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws/session" + "github.com/aws/aws-sdk-go/service/cloudwatchlogs" + "github.com/aws/aws-sdk-go/service/ec2" + "github.com/aws/aws-sdk-go/service/ecs" + "github.com/aws/aws-sdk-go/service/iam" "github.com/docker/ecs-plugin/pkg/compose" ) @@ -25,6 +29,10 @@ func NewClient(profile string, cluster string, region string) (compose.API, erro Cluster: cluster, Region: region, sess: sess, + ECS: ecs.New(sess), + EC2: ec2.New(sess), + CW: cloudwatchlogs.New(sess), + IAM: iam.New(sess), }, nil } @@ -32,6 +40,10 @@ type client struct { Cluster string Region string sess *session.Session + ECS *ecs.ECS + EC2 *ec2.EC2 + CW *cloudwatchlogs.CloudWatchLogs + IAM *iam.IAM } var _ compose.API = &client{} diff --git a/ecs/pkg/amazon/compose.go b/ecs/pkg/amazon/compose.go index 6a1073792..202143f7e 100644 --- a/ecs/pkg/amazon/compose.go +++ b/ecs/pkg/amazon/compose.go @@ -1,11 +1,79 @@ package amazon import ( - "fmt" + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/ecs" + "github.com/compose-spec/compose-go/types" "github.com/docker/ecs-plugin/pkg/compose" ) func (c *client) ComposeUp(project *compose.Project) error { - fmt.Println("TODO Up") + vpc, err := c.GetDefaultVPC() + if err != nil { + return err + } + subnets, err := c.GetSubNets(vpc) + if err != nil { + return err + } + + securityGroup, err := c.CreateSecurityGroup(project, vpc) + if err != nil { + return err + } + + logGroup, err := c.GetOrCreateLogGroup(project.Name) + if err != nil { + return err + } + + for _, service := range project.Services { + err = c.CreateService(service, securityGroup, subnets, logGroup) + if err != nil { + return err + } + } return nil } + +func (c *client) CreateService(service types.ServiceConfig, securityGroup *string, subnets []*string, logGroup *string) error { + task, err := ConvertToTaskDefinition(service) + if err != nil { + return err + } + + role, err := c.GetEcsTaskExecutionRole(service) + if err != nil { + return err + } + + task.ExecutionRoleArn = role + + for _, def := range task.ContainerDefinitions { + def.LogConfiguration.Options["awslogs-group"] = logGroup + def.LogConfiguration.Options["awslogs-stream-prefix"] = aws.String(service.Name) + def.LogConfiguration.Options["awslogs-region"] = aws.String(c.Region) + } + + arn, err := c.RegisterTaskDefinition(task) + if err != nil { + return err + } + + _, err = c.ECS.CreateService(&ecs.CreateServiceInput{ + Cluster: aws.String(c.Cluster), + DesiredCount: aws.Int64(1), // FIXME get from deploy options + LaunchType: aws.String(ecs.LaunchTypeFargate), //FIXME use service.Isolation tro select EC2 vs Fargate + NetworkConfiguration: &ecs.NetworkConfiguration{ + AwsvpcConfiguration: &ecs.AwsVpcConfiguration{ + AssignPublicIp: aws.String(ecs.AssignPublicIpEnabled), + SecurityGroups: []*string{securityGroup}, + Subnets: subnets, + }, + }, + ServiceName: aws.String(service.Name), + SchedulingStrategy: aws.String(ecs.SchedulingStrategyReplica), + TaskDefinition: arn, + }) + return err +} diff --git a/ecs/pkg/amazon/ecs.go b/ecs/pkg/amazon/ecs.go new file mode 100644 index 000000000..c405b1356 --- /dev/null +++ b/ecs/pkg/amazon/ecs.go @@ -0,0 +1,21 @@ +package amazon + +import ( + "github.com/aws/aws-sdk-go/service/ecs" + "github.com/compose-spec/compose-go/types" + "github.com/sirupsen/logrus" +) + +func ConvertToTaskDefinition(service types.ServiceConfig) (*ecs.RegisterTaskDefinitionInput, error) { + panic("Please implement me") +} + + +func (c client) RegisterTaskDefinition(task *ecs.RegisterTaskDefinitionInput) (*string, error) { + logrus.Debug("Register Task Definition") + def, err := c.ECS.RegisterTaskDefinition(task) + if err != nil { + return nil, err + } + return def.TaskDefinition.TaskDefinitionArn, err +} diff --git a/ecs/pkg/amazon/logs.go b/ecs/pkg/amazon/logs.go new file mode 100644 index 000000000..7553ba1b7 --- /dev/null +++ b/ecs/pkg/amazon/logs.go @@ -0,0 +1,26 @@ +package amazon + +import ( + "fmt" + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/cloudwatchlogs" + "github.com/sirupsen/logrus" +) + +// GetOrCreateLogGroup retrieve a pre-existing log group for project or create one +func (c client) GetOrCreateLogGroup(project string) (*string, error) { + logrus.Debug("Create Log Group") + logGroup := fmt.Sprintf("/ecs/%s", project) + _, err := c.CW.CreateLogGroup(&cloudwatchlogs.CreateLogGroupInput{ + LogGroupName: aws.String(logGroup), + Tags: map[string]*string{ + ProjectTag: aws.String(project), + }, + }) + if err != nil { + if _, ok := err.(*cloudwatchlogs.ResourceAlreadyExistsException); !ok { + return nil, err + } + } + return &logGroup, nil +} diff --git a/ecs/pkg/amazon/network.go b/ecs/pkg/amazon/network.go new file mode 100644 index 000000000..d994cce1f --- /dev/null +++ b/ecs/pkg/amazon/network.go @@ -0,0 +1,90 @@ +package amazon + +import ( + "fmt" + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/ec2" + "github.com/docker/ecs-plugin/pkg/compose" + "github.com/sirupsen/logrus" +) + +// GetDefaultVPC retrieve the default VPC for AWS account +func (c client) GetDefaultVPC() (*string, error) { + logrus.Debug("Retrieve default VPC") + vpcs, err := c.EC2.DescribeVpcs(&ec2.DescribeVpcsInput{ + Filters: []*ec2.Filter{ + { + Name: aws.String("isDefault"), + Values: []*string{aws.String("true")}, + }, + }, + }) + if err != nil { + return nil, err + } + if len(vpcs.Vpcs) == 0 { + return nil, fmt.Errorf("account has not default VPC") + } + return vpcs.Vpcs[0].VpcId, nil +} + + +// GetSubNets retrieve default subnets for a VPC +func (c client) GetSubNets(vpc *string) ([]*string, error) { + logrus.Debug("Retrieve SubNets") + subnets, err := c.EC2.DescribeSubnets(&ec2.DescribeSubnetsInput{ + DryRun: nil, + Filters: []*ec2.Filter{ + { + Name: aws.String("vpc-id"), + Values: []*string{vpc}, + }, + { + Name: aws.String("default-for-az"), + Values: []*string{aws.String("true")}, + }, + }, + }) + if err != nil { + return nil, err + } + + ids := []*string{} + for _, subnet := range subnets.Subnets { + ids = append(ids, subnet.SubnetId) + } + return ids, nil +} + +// CreateSecurityGroup create a security group for the project +func (c client) CreateSecurityGroup(project *compose.Project, vpc *string) (*string, error) { + logrus.Debug("Create Security Group") + name := fmt.Sprintf("%s Security Group", project) + securityGroup, err := c.EC2.CreateSecurityGroup(&ec2.CreateSecurityGroupInput{ + Description: aws.String(name), + GroupName: aws.String(name), + VpcId: vpc, + }) + if err != nil { + return nil, err + } + + _, err = c.EC2.CreateTags(&ec2.CreateTagsInput{ + Resources: []*string{securityGroup.GroupId}, + Tags: []*ec2.Tag{ + { + Key: aws.String("Name"), + Value: aws.String(name), + }, + { + Key: aws.String(ProjectTag), + Value: aws.String(project.Name), + }, + }, + }) + if err != nil { + return nil, err + } + + return securityGroup.GroupId, nil +} \ No newline at end of file diff --git a/ecs/pkg/amazon/roles.go b/ecs/pkg/amazon/roles.go new file mode 100644 index 000000000..3e8c5303a --- /dev/null +++ b/ecs/pkg/amazon/roles.go @@ -0,0 +1,48 @@ +package amazon + +import ( + "fmt" + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/iam" + "github.com/compose-spec/compose-go/types" + "github.com/sirupsen/logrus" +) + +const ECSTaskExecutionPolicy = "arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy" + +var defaultTaskExecutionRole *string + +// GetEcsTaskExecutionRole retrieve the role ARN to apply for task execution +func (c client) GetEcsTaskExecutionRole(spec types.ServiceConfig) (*string, error) { + if arn, ok := spec.Extras["x-ecs-TaskExecutionRole"]; ok { + s := arn.(string) + return &s, nil + } + if defaultTaskExecutionRole != nil { + return defaultTaskExecutionRole, nil + } + + logrus.Debug("Retrieve Task Execution Role") + entities, err := c.IAM.ListEntitiesForPolicy(&iam.ListEntitiesForPolicyInput{ + EntityFilter: aws.String("Role"), + PolicyArn: aws.String(ECSTaskExecutionPolicy), + }) + if err != nil { + return nil, err + } + if len(entities.PolicyRoles) == 0 { + return nil, fmt.Errorf("no Role is attached to AmazonECSTaskExecutionRole Policy, please provide an explicit task execution role") + } + if len(entities.PolicyRoles) > 1 { + return nil, fmt.Errorf("multiple Roles are attached to AmazonECSTaskExecutionRole Policy, please provide an explicit task execution role") + } + + role, err := c.IAM.GetRole(&iam.GetRoleInput{ + RoleName: entities.PolicyRoles[0].RoleName, + }) + if err != nil { + return nil, err + } + defaultTaskExecutionRole = role.Role.Arn + return role.Role.Arn, nil +} From fc7266f3f78824c184ec9fb6d097645e788fae1f Mon Sep 17 00:00:00 2001 From: Nicolas De Loof <nicolas.deloof@gmail.com> Date: Wed, 15 Apr 2020 10:38:19 +0200 Subject: [PATCH 007/198] Convert compose service into TaskDefinition (code imported from prototype) Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com> --- ecs/pkg/amazon/compose.go | 29 ++- ecs/pkg/amazon/ecs.go | 5 - ecs/pkg/amazon/logs.go | 7 +- ecs/pkg/amazon/network.go | 25 ++- ecs/pkg/convert/convert.go | 358 +++++++++++++++++++++++++++++++++++++ 5 files changed, 406 insertions(+), 18 deletions(-) create mode 100644 ecs/pkg/convert/convert.go diff --git a/ecs/pkg/amazon/compose.go b/ecs/pkg/amazon/compose.go index 202143f7e..c8b4352b9 100644 --- a/ecs/pkg/amazon/compose.go +++ b/ecs/pkg/amazon/compose.go @@ -5,6 +5,8 @@ import ( "github.com/aws/aws-sdk-go/service/ecs" "github.com/compose-spec/compose-go/types" "github.com/docker/ecs-plugin/pkg/compose" + "github.com/docker/ecs-plugin/pkg/convert" + "github.com/sirupsen/logrus" ) func (c *client) ComposeUp(project *compose.Project) error { @@ -22,13 +24,13 @@ func (c *client) ComposeUp(project *compose.Project) error { return err } - logGroup, err := c.GetOrCreateLogGroup(project.Name) + logGroup, err := c.GetOrCreateLogGroup(project) if err != nil { return err } for _, service := range project.Services { - err = c.CreateService(service, securityGroup, subnets, logGroup) + _, err = c.CreateService(project, service, securityGroup, subnets, logGroup) if err != nil { return err } @@ -36,15 +38,15 @@ func (c *client) ComposeUp(project *compose.Project) error { return nil } -func (c *client) CreateService(service types.ServiceConfig, securityGroup *string, subnets []*string, logGroup *string) error { - task, err := ConvertToTaskDefinition(service) +func (c *client) CreateService(project *compose.Project, service types.ServiceConfig, securityGroup *string, subnets []*string, logGroup *string) (*string, error) { + task, err := convert.Convert(project, service) if err != nil { - return err + return nil, err } role, err := c.GetEcsTaskExecutionRole(service) if err != nil { - return err + return nil, err } task.ExecutionRoleArn = role @@ -57,10 +59,11 @@ func (c *client) CreateService(service types.ServiceConfig, securityGroup *strin arn, err := c.RegisterTaskDefinition(task) if err != nil { - return err + return nil, err } - _, err = c.ECS.CreateService(&ecs.CreateServiceInput{ + logrus.Debug("Create Service") + created, err := c.ECS.CreateService(&ecs.CreateServiceInput{ Cluster: aws.String(c.Cluster), DesiredCount: aws.Int64(1), // FIXME get from deploy options LaunchType: aws.String(ecs.LaunchTypeFargate), //FIXME use service.Isolation tro select EC2 vs Fargate @@ -75,5 +78,13 @@ func (c *client) CreateService(service types.ServiceConfig, securityGroup *strin SchedulingStrategy: aws.String(ecs.SchedulingStrategyReplica), TaskDefinition: arn, }) - return err + + for _, port := range service.Ports { + err = c.ExposePort(securityGroup, port) + if err != nil { + return nil, err + } + } + + return created.Service.ServiceArn, err } diff --git a/ecs/pkg/amazon/ecs.go b/ecs/pkg/amazon/ecs.go index c405b1356..5366a7a07 100644 --- a/ecs/pkg/amazon/ecs.go +++ b/ecs/pkg/amazon/ecs.go @@ -2,14 +2,9 @@ package amazon import ( "github.com/aws/aws-sdk-go/service/ecs" - "github.com/compose-spec/compose-go/types" "github.com/sirupsen/logrus" ) -func ConvertToTaskDefinition(service types.ServiceConfig) (*ecs.RegisterTaskDefinitionInput, error) { - panic("Please implement me") -} - func (c client) RegisterTaskDefinition(task *ecs.RegisterTaskDefinitionInput) (*string, error) { logrus.Debug("Register Task Definition") diff --git a/ecs/pkg/amazon/logs.go b/ecs/pkg/amazon/logs.go index 7553ba1b7..b42bc8b08 100644 --- a/ecs/pkg/amazon/logs.go +++ b/ecs/pkg/amazon/logs.go @@ -4,17 +4,18 @@ import ( "fmt" "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/cloudwatchlogs" + "github.com/docker/ecs-plugin/pkg/compose" "github.com/sirupsen/logrus" ) // GetOrCreateLogGroup retrieve a pre-existing log group for project or create one -func (c client) GetOrCreateLogGroup(project string) (*string, error) { +func (c client) GetOrCreateLogGroup(project *compose.Project) (*string, error) { logrus.Debug("Create Log Group") - logGroup := fmt.Sprintf("/ecs/%s", project) + logGroup := fmt.Sprintf("/ecs/%s", project.Name) _, err := c.CW.CreateLogGroup(&cloudwatchlogs.CreateLogGroupInput{ LogGroupName: aws.String(logGroup), Tags: map[string]*string{ - ProjectTag: aws.String(project), + ProjectTag: aws.String(project.Name), }, }) if err != nil { diff --git a/ecs/pkg/amazon/network.go b/ecs/pkg/amazon/network.go index d994cce1f..a876b4bc3 100644 --- a/ecs/pkg/amazon/network.go +++ b/ecs/pkg/amazon/network.go @@ -4,8 +4,10 @@ import ( "fmt" "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/ec2" + "github.com/compose-spec/compose-go/types" "github.com/docker/ecs-plugin/pkg/compose" "github.com/sirupsen/logrus" + "strings" ) // GetDefaultVPC retrieve the default VPC for AWS account @@ -59,7 +61,7 @@ func (c client) GetSubNets(vpc *string) ([]*string, error) { // CreateSecurityGroup create a security group for the project func (c client) CreateSecurityGroup(project *compose.Project, vpc *string) (*string, error) { logrus.Debug("Create Security Group") - name := fmt.Sprintf("%s Security Group", project) + name := fmt.Sprintf("%s Security Group", project.Name) securityGroup, err := c.EC2.CreateSecurityGroup(&ec2.CreateSecurityGroupInput{ Description: aws.String(name), GroupName: aws.String(name), @@ -87,4 +89,25 @@ func (c client) CreateSecurityGroup(project *compose.Project, vpc *string) (*str } return securityGroup.GroupId, nil +} + + +func (c *client) ExposePort(securityGroup *string, port types.ServicePortConfig) error { + logrus.Debugf("Authorize ingress port %d/%s\n", port.Published, port.Protocol) + _, err := c.EC2.AuthorizeSecurityGroupIngress(&ec2.AuthorizeSecurityGroupIngressInput{ + GroupId: securityGroup, + IpPermissions: []*ec2.IpPermission{ + { + IpProtocol: aws.String(strings.ToUpper(port.Protocol)), + IpRanges: []*ec2.IpRange{ + { + CidrIp: aws.String("0.0.0.0/0"), + }, + }, + FromPort: aws.Int64(int64(port.Target)), + ToPort: aws.Int64(int64(port.Target)), + }, + }, + }) + return err } \ No newline at end of file diff --git a/ecs/pkg/convert/convert.go b/ecs/pkg/convert/convert.go new file mode 100644 index 000000000..502f5089e --- /dev/null +++ b/ecs/pkg/convert/convert.go @@ -0,0 +1,358 @@ +package convert + +import ( + "github.com/docker/ecs-plugin/pkg/compose" + "time" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/ecs" + "github.com/compose-spec/compose-go/types" + "github.com/docker/cli/opts" +) + +func Convert(project *compose.Project, service types.ServiceConfig) (*ecs.RegisterTaskDefinitionInput, error) { + _, err := toCPULimits(service) + if err != nil { + return nil, err + } + + foo := int64(256) + logDriver := "awslogs" // FIXME could be set by service.Logging, especially to enable use of firelens + return &ecs.RegisterTaskDefinitionInput{ + ContainerDefinitions: []*ecs.ContainerDefinition{ + // Here we can declare sidecars and init-containers using https://docs.aws.amazon.com/AmazonECS/latest/developerguide/task_definition_parameters.html#container_definition_dependson + { + Command: toStringPtrSlice(service.Command), + Cpu: &foo, + DependsOn: nil, + DisableNetworking: toBoolPtr(service.NetworkMode == "none"), + DnsSearchDomains: toStringPtrSlice(service.DNSSearch), + DnsServers: toStringPtrSlice(service.DNS), + DockerLabels: nil, + DockerSecurityOptions: toStringPtrSlice(service.SecurityOpt), + EntryPoint: toStringPtrSlice(service.Entrypoint), + Environment: toKeyValuePairPtr(service.Environment), + Essential: toBoolPtr(true), + ExtraHosts: toHostEntryPtr(service.ExtraHosts), + FirelensConfiguration: nil, + HealthCheck: toHealthCheck(service.HealthCheck), + Hostname: toStringPtr(service.Hostname), + Image: toStringPtr(service.Image), + Interactive: nil, + Links: nil, + LinuxParameters: toLinuxParameters(service), + LogConfiguration: &ecs.LogConfiguration{ + LogDriver: &logDriver, + Options: map[string]*string{}, + SecretOptions: nil, + }, + Memory: toMemoryLimits(service.Deploy), + MemoryReservation: toMemoryReservation(service.Deploy), + MountPoints: nil, + Name: toStringPtr(service.Name), + PortMappings: toPortMappings(service.Ports), + Privileged: toBoolPtr(service.Privileged), + PseudoTerminal: toBoolPtr(service.Tty), + ReadonlyRootFilesystem: toBoolPtr(service.ReadOnly), + RepositoryCredentials: nil, + ResourceRequirements: nil, + Secrets: nil, + StartTimeout: nil, + StopTimeout: durationToInt64Ptr(service.StopGracePeriod), + SystemControls: nil, + Ulimits: toUlimits(service.Ulimits), + User: toStringPtr(service.User), + VolumesFrom: nil, + WorkingDirectory: toStringPtr(service.WorkingDir), + }, + }, + Cpu: toCPU(service), + ExecutionRoleArn: nil, + Family: toStringPtr(project.Name), + IpcMode: toStringPtr(service.Ipc), + Memory: toMemory(service), + NetworkMode: toStringPtr("awsvpc"), // FIXME could be set by service.NetworkMode, Fargate only supports network mode ‘awsvpc’. + PidMode: toStringPtr(service.Pid), + PlacementConstraints: toPlacementConstraints(service.Deploy), + ProxyConfiguration: nil, + RequiresCompatibilities: toRequiresCompatibilities(ecs.LaunchTypeFargate), + Tags: nil, + Volumes: []*ecs.Volume{ + { + /* ONLY supported when using EC2 launch type + DockerVolumeConfiguration: { + Autoprovision: nil, + Driver: nil, + DriverOpts: nil, + Labels: nil, + Scope: nil, + }, */ + /* Beta and ONLY supported when using EC2 launch type + EfsVolumeConfiguration: { + FileSystemId: nil, + RootDirectory: nil, + }, */ + /* Bind mount host volume + Host: { + SourcePath: + }, */ + Name: aws.String("MyVolume"), + }, + }, + }, nil + +} + +func toCPU(service types.ServiceConfig) *string { + // FIXME based on service's memory/cpu requirements, select the adequate Fargate CPU + v := "256" + return &v +} + +func toMemory(service types.ServiceConfig) *string { + // FIXME based on service's memory/cpu requirements, select the adequate Fargate CPU + v := "512" + return &v +} + +func toCPULimits(service types.ServiceConfig) (*int64, error) { + if service.Deploy == nil { + return nil, nil + } + res := service.Deploy.Resources.Limits + if res == nil { + return nil, nil + } + if res.NanoCPUs == "" { + return nil, nil + } + v, err := opts.ParseCPUs(res.NanoCPUs) + if err != nil { + return nil, err + } + return &v, nil +} + +func toRequiresCompatibilities(isolation string) []*string { + if isolation == "" { + return nil + } + return []*string{&isolation} +} + +func hasMemoryOrMemoryReservation(service types.ServiceConfig) bool { + if service.Deploy == nil { + return false + } + if service.Deploy.Resources.Reservations != nil { + return true + } + if service.Deploy.Resources.Limits != nil { + return true + } + return false +} + +func toPlacementConstraints(deploy *types.DeployConfig) []*ecs.TaskDefinitionPlacementConstraint { + if deploy == nil || deploy.Placement.Constraints == nil || len(deploy.Placement.Constraints) == 0 { + return nil + } + pl := []*ecs.TaskDefinitionPlacementConstraint{} + for _, c := range deploy.Placement.Constraints { + pl = append(pl, &ecs.TaskDefinitionPlacementConstraint{ + Expression: toStringPtr(c), + Type: nil, + }) + } + return pl +} + +func toPortMappings(ports []types.ServicePortConfig) []*ecs.PortMapping { + if len(ports) == 0 { + return nil + } + m := []*ecs.PortMapping{} + for _, p := range ports { + m = append(m, &ecs.PortMapping{ + ContainerPort: uint32Toint64Ptr(p.Target), + HostPort: uint32Toint64Ptr(p.Published), + Protocol: toStringPtr(p.Protocol), + }) + } + return m +} + +func toUlimits(ulimits map[string]*types.UlimitsConfig) []*ecs.Ulimit { + if len(ulimits) == 0 { + return nil + } + u := []*ecs.Ulimit{} + for k, v := range ulimits { + u = append(u, &ecs.Ulimit{ + Name: toStringPtr(k), + SoftLimit: intToInt64Ptr(v.Soft), + HardLimit: intToInt64Ptr(v.Hard), + }) + } + return u +} + +func uint32Toint64Ptr(i uint32) *int64 { + v := int64(i) + return &v +} + +func intToInt64Ptr(i int) *int64 { + v := int64(i) + return &v +} + +const Mb = 1024 * 1024 + +func toMemoryLimits(deploy *types.DeployConfig) *int64 { + if deploy == nil { + return nil + } + res := deploy.Resources.Limits + if res == nil { + return nil + } + v := int64(res.MemoryBytes) / Mb + return &v +} + +func toMemoryReservation(deploy *types.DeployConfig) *int64 { + if deploy == nil { + return nil + } + res := deploy.Resources.Reservations + if res == nil { + return nil + } + v := int64(res.MemoryBytes) / Mb + return &v +} + +func toLinuxParameters(service types.ServiceConfig) *ecs.LinuxParameters { + return &ecs.LinuxParameters{ + Capabilities: toKernelCapabilities(service.CapAdd, service.CapDrop), + Devices: nil, + InitProcessEnabled: service.Init, + MaxSwap: nil, + // FIXME SharedMemorySize: service.ShmSize, + Swappiness: nil, + Tmpfs: toTmpfs(service.Tmpfs), + } +} + +func toTmpfs(tmpfs types.StringList) []*ecs.Tmpfs { + if tmpfs == nil || len(tmpfs) == 0 { + return nil + } + o := []*ecs.Tmpfs{} + for _, t := range tmpfs { + path := t + o = append(o, &ecs.Tmpfs{ + ContainerPath: &path, + MountOptions: nil, + Size: nil, + }) + } + return o +} + +func toKernelCapabilities(add []string, drop []string) *ecs.KernelCapabilities { + if len(add) == 0 && len(drop) == 0 { + return nil + } + return &ecs.KernelCapabilities{ + Add: toStringPtrSlice(add), + Drop: toStringPtrSlice(drop), + } + +} + +func toHealthCheck(check *types.HealthCheckConfig) *ecs.HealthCheck { + if check == nil { + return nil + } + return &ecs.HealthCheck{ + Command: toStringPtrSlice(check.Test), + Interval: durationToInt64Ptr(check.Interval), + Retries: uint64ToInt64Ptr(check.Retries), + StartPeriod: durationToInt64Ptr(check.StartPeriod), + Timeout: durationToInt64Ptr(check.Timeout), + } +} + +func uint64ToInt64Ptr(i *uint64) *int64 { + if i == nil { + return nil + } + v := int64(*i) + return &v +} + +func durationToInt64Ptr(interval *types.Duration) *int64 { + if interval == nil { + return nil + } + v := int64(time.Duration(*interval).Seconds()) + return &v +} + +func toHostEntryPtr(hosts types.HostsList) []*ecs.HostEntry { + if hosts == nil || len(hosts) == 0 { + return nil + } + e := []*ecs.HostEntry{} + for _, h := range hosts { + host := h + e = append(e, &ecs.HostEntry{ + Hostname: &host, + }) + } + return e +} + +func toKeyValuePairPtr(environment types.MappingWithEquals) []*ecs.KeyValuePair { + if environment == nil || len(environment) == 0 { + return nil + } + pairs := []*ecs.KeyValuePair{} + for k, v := range environment { + name := k + value := v + pairs = append(pairs, &ecs.KeyValuePair{ + Name: &name, + Value: value, + }) + } + return pairs +} + +func toStringPtr(s string) *string { + if s == "" { + return nil + } + return &s +} + +func toStringPtrSlice(s []string) []*string { + if len(s) == 0 { + return nil + } + v := []*string{} + for _, x := range s { + value := x + v = append(v, &value) + } + return v +} + +func toBoolPtr(b bool) *bool { + if !b { + return nil + } + return &b +} \ No newline at end of file From 7763de47eb85d32853408488a42d4aa14ed1b89d Mon Sep 17 00:00:00 2001 From: Nicolas De Loof <nicolas.deloof@gmail.com> Date: Thu, 16 Apr 2020 10:07:28 +0200 Subject: [PATCH 008/198] Introduce "down" command Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com> --- ecs/cmd/main/main.go | 15 +++++++ ecs/pkg/amazon/down.go | 60 ++++++++++++++++++++++++++++ ecs/pkg/amazon/{compose.go => up.go} | 0 ecs/pkg/compose/api.go | 1 + 4 files changed, 76 insertions(+) create mode 100644 ecs/pkg/amazon/down.go rename ecs/pkg/amazon/{compose.go => up.go} (100%) diff --git a/ecs/cmd/main/main.go b/ecs/cmd/main/main.go index 83d130218..daf11ac07 100644 --- a/ecs/cmd/main/main.go +++ b/ecs/cmd/main/main.go @@ -71,6 +71,7 @@ func ComposeCommand(clusteropts *clusterOptions) *cobra.Command { cmd.AddCommand( UpCommand(clusteropts, opts), + DownCommand(clusteropts, opts), ) return cmd } @@ -87,4 +88,18 @@ func UpCommand(clusteropts *clusterOptions, opts *compose.ProjectOptions) *cobra }), } return cmd +} + +func DownCommand(clusteropts *clusterOptions, opts *compose.ProjectOptions) *cobra.Command { + cmd := &cobra.Command{ + Use: "down", + RunE: compose.WithProject(opts, func(project *compose.Project, args []string) error { + client, err := amazon.NewClient(clusteropts.profile, clusteropts.cluster, clusteropts.region) + if err != nil { + return err + } + return client.ComposeDown(project) + }), + } + return cmd } \ No newline at end of file diff --git a/ecs/pkg/amazon/down.go b/ecs/pkg/amazon/down.go new file mode 100644 index 000000000..611d84f4a --- /dev/null +++ b/ecs/pkg/amazon/down.go @@ -0,0 +1,60 @@ +package amazon + +import ( + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/ec2" + "github.com/aws/aws-sdk-go/service/ecs" + "github.com/docker/ecs-plugin/pkg/compose" + "github.com/sirupsen/logrus" +) + +func (c *client) ComposeDown(project *compose.Project) error { + services := []*string{} + // FIXME we should be able to retrieve services by tags, so we don't need the initial compose file to run "down" + for _, service := range project.Services { + logrus.Debugf("Deleting service %q\n", service.Name) + out, err := c.ECS.DeleteService(&ecs.DeleteServiceInput{ + // Force to true so that we don't have to scale down to 0 + // before deleting + Force: aws.Bool(true), + Cluster: aws.String(c.Cluster), + Service: aws.String(service.Name), + }) + if err != nil { + return err + } + + logrus.Debugf("Service deleted %q\n", *out.Service.ServiceName) + services = append(services, out.Service.ServiceName) + } + logrus.Info("All services stopped") + + err := c.ECS.WaitUntilServicesInactive(&ecs.DescribeServicesInput{ + Services: services, + }) + if err != nil { + return err + } + + logrus.Debug("Deleting security groups") + groups, err := c.EC2.DescribeSecurityGroups(&ec2.DescribeSecurityGroupsInput{ + Filters: []*ec2.Filter{ + { + Name: aws.String("tag:" + ProjectTag), + Values: aws.StringSlice([]string{project.Name}), + }, + }, + }) + if err != nil { + return err + } + for _, g := range groups.SecurityGroups { + _, err = c.EC2.DeleteSecurityGroup(&ec2.DeleteSecurityGroupInput{ + GroupId: g.GroupId, + }) + if err != nil { + return err + } + } + return nil +} diff --git a/ecs/pkg/amazon/compose.go b/ecs/pkg/amazon/up.go similarity index 100% rename from ecs/pkg/amazon/compose.go rename to ecs/pkg/amazon/up.go diff --git a/ecs/pkg/compose/api.go b/ecs/pkg/compose/api.go index 14e1ca050..cd6eb7878 100644 --- a/ecs/pkg/compose/api.go +++ b/ecs/pkg/compose/api.go @@ -2,4 +2,5 @@ package compose type API interface { ComposeUp(project *Project) error + ComposeDown(project *Project) error } \ No newline at end of file From 17f3ff9db1f8b54ea93e7c3482dfddec81d48c9d Mon Sep 17 00:00:00 2001 From: Nicolas De Loof <nicolas.deloof@gmail.com> Date: Thu, 16 Apr 2020 11:16:50 +0200 Subject: [PATCH 009/198] Convert services into TaskDefinition before creating resources close #6 Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com> --- ecs/pkg/amazon/roles.go | 2 +- ecs/pkg/amazon/up.go | 27 +++++++++++++++++++-------- 2 files changed, 20 insertions(+), 9 deletions(-) diff --git a/ecs/pkg/amazon/roles.go b/ecs/pkg/amazon/roles.go index 3e8c5303a..bfa5026a2 100644 --- a/ecs/pkg/amazon/roles.go +++ b/ecs/pkg/amazon/roles.go @@ -13,7 +13,7 @@ const ECSTaskExecutionPolicy = "arn:aws:iam::aws:policy/service-role/AmazonECSTa var defaultTaskExecutionRole *string // GetEcsTaskExecutionRole retrieve the role ARN to apply for task execution -func (c client) GetEcsTaskExecutionRole(spec types.ServiceConfig) (*string, error) { +func (c client) GetEcsTaskExecutionRole(spec *types.ServiceConfig) (*string, error) { if arn, ok := spec.Extras["x-ecs-TaskExecutionRole"]; ok { s := arn.(string) return &s, nil diff --git a/ecs/pkg/amazon/up.go b/ecs/pkg/amazon/up.go index c8b4352b9..e54aae14f 100644 --- a/ecs/pkg/amazon/up.go +++ b/ecs/pkg/amazon/up.go @@ -10,6 +10,22 @@ import ( ) func (c *client) ComposeUp(project *compose.Project) error { + type mapping struct { + service *types.ServiceConfig + task *ecs.RegisterTaskDefinitionInput + } + mappings := []mapping{} + for _, service := range project.Services { + task, err := convert.Convert(project, service) + if err != nil { + return err + } + mappings = append(mappings, mapping{ + service: &service, + task: task, + }) + } + vpc, err := c.GetDefaultVPC() if err != nil { return err @@ -29,8 +45,8 @@ func (c *client) ComposeUp(project *compose.Project) error { return err } - for _, service := range project.Services { - _, err = c.CreateService(project, service, securityGroup, subnets, logGroup) + for _, mapping := range mappings { + _, err = c.CreateService(project, mapping.service, mapping.task, securityGroup, subnets, logGroup) if err != nil { return err } @@ -38,12 +54,7 @@ func (c *client) ComposeUp(project *compose.Project) error { return nil } -func (c *client) CreateService(project *compose.Project, service types.ServiceConfig, securityGroup *string, subnets []*string, logGroup *string) (*string, error) { - task, err := convert.Convert(project, service) - if err != nil { - return nil, err - } - +func (c *client) CreateService(project *compose.Project, service *types.ServiceConfig, task *ecs.RegisterTaskDefinitionInput, securityGroup *string, subnets []*string, logGroup *string) (*string, error) { role, err := c.GetEcsTaskExecutionRole(service) if err != nil { return nil, err From a44ee2a4ed71c7abe24a1b701ec1cb28a9d49452 Mon Sep 17 00:00:00 2001 From: Nicolas De Loof <nicolas.deloof@gmail.com> Date: Thu, 16 Apr 2020 15:15:39 +0200 Subject: [PATCH 010/198] Expose services using a LoadBalancer Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com> --- ecs/cmd/main/main.go | 4 +- ecs/pkg/amazon/client.go | 3 + ecs/pkg/amazon/down.go | 14 ++-- ecs/pkg/amazon/loadBalancer.go | 133 +++++++++++++++++++++++++++++++++ ecs/pkg/amazon/up.go | 30 +++++++- 5 files changed, 175 insertions(+), 9 deletions(-) create mode 100644 ecs/pkg/amazon/loadBalancer.go diff --git a/ecs/cmd/main/main.go b/ecs/cmd/main/main.go index daf11ac07..75b6c2b7d 100644 --- a/ecs/cmd/main/main.go +++ b/ecs/cmd/main/main.go @@ -76,10 +76,10 @@ func ComposeCommand(clusteropts *clusterOptions) *cobra.Command { return cmd } -func UpCommand(clusteropts *clusterOptions, opts *compose.ProjectOptions) *cobra.Command { +func UpCommand(clusteropts *clusterOptions, projectOpts *compose.ProjectOptions) *cobra.Command { cmd := &cobra.Command{ Use: "up", - RunE: compose.WithProject(opts, func(project *compose.Project, args []string) error { + RunE: compose.WithProject(projectOpts, func(project *compose.Project, args []string) error { client, err := amazon.NewClient(clusteropts.profile, clusteropts.cluster, clusteropts.region) if err != nil { return err diff --git a/ecs/pkg/amazon/client.go b/ecs/pkg/amazon/client.go index 8e98a55f0..247536a98 100644 --- a/ecs/pkg/amazon/client.go +++ b/ecs/pkg/amazon/client.go @@ -6,6 +6,7 @@ import ( "github.com/aws/aws-sdk-go/service/cloudwatchlogs" "github.com/aws/aws-sdk-go/service/ec2" "github.com/aws/aws-sdk-go/service/ecs" + "github.com/aws/aws-sdk-go/service/elbv2" "github.com/aws/aws-sdk-go/service/iam" "github.com/docker/ecs-plugin/pkg/compose" ) @@ -31,6 +32,7 @@ func NewClient(profile string, cluster string, region string) (compose.API, erro sess: sess, ECS: ecs.New(sess), EC2: ec2.New(sess), + ELB: elbv2.New(sess), CW: cloudwatchlogs.New(sess), IAM: iam.New(sess), }, nil @@ -42,6 +44,7 @@ type client struct { sess *session.Session ECS *ecs.ECS EC2 *ec2.EC2 + ELB *elbv2.ELBV2 CW *cloudwatchlogs.CloudWatchLogs IAM *iam.IAM } diff --git a/ecs/pkg/amazon/down.go b/ecs/pkg/amazon/down.go index 611d84f4a..e68c31329 100644 --- a/ecs/pkg/amazon/down.go +++ b/ecs/pkg/amazon/down.go @@ -9,6 +9,11 @@ import ( ) func (c *client) ComposeDown(project *compose.Project) error { + err := c.DeleteLoadBalancer(project) + if err != nil { + return err + } + services := []*string{} // FIXME we should be able to retrieve services by tags, so we don't need the initial compose file to run "down" for _, service := range project.Services { @@ -23,18 +28,17 @@ func (c *client) ComposeDown(project *compose.Project) error { if err != nil { return err } - - logrus.Debugf("Service deleted %q\n", *out.Service.ServiceName) - services = append(services, out.Service.ServiceName) + services = append(services, out.Service.ServiceArn) } - logrus.Info("All services stopped") - err := c.ECS.WaitUntilServicesInactive(&ecs.DescribeServicesInput{ + logrus.Info("Stopping services...") + err = c.ECS.WaitUntilServicesInactive(&ecs.DescribeServicesInput{ Services: services, }) if err != nil { return err } + logrus.Info("All services stopped") logrus.Debug("Deleting security groups") groups, err := c.EC2.DescribeSecurityGroups(&ec2.DescribeSecurityGroupsInput{ diff --git a/ecs/pkg/amazon/loadBalancer.go b/ecs/pkg/amazon/loadBalancer.go new file mode 100644 index 000000000..6edc79556 --- /dev/null +++ b/ecs/pkg/amazon/loadBalancer.go @@ -0,0 +1,133 @@ +package amazon + +import ( + "fmt" + "github.com/docker/ecs-plugin/pkg/compose" + "github.com/sirupsen/logrus" + "strings" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/elbv2" + "github.com/compose-spec/compose-go/types" +) + +func (c client) CreateLoadBalancer(project *compose.Project, subnets []*string) (*string, error) { + logrus.Debug("Create Load Balancer") + alb, err := c.ELB.CreateLoadBalancer(&elbv2.CreateLoadBalancerInput{ + IpAddressType: nil, + Name: aws.String(fmt.Sprintf("%s-LoadBalancer", project.Name)), + Subnets: subnets, + Type: aws.String(elbv2.LoadBalancerTypeEnumNetwork), + Tags: []*elbv2.Tag{ + { + Key: aws.String("com.docker.compose.project"), + Value: aws.String(project.Name), + }, + }, + }) + if err != nil { + return nil, err + } + return alb.LoadBalancers[0].LoadBalancerArn, nil +} + +func (c client) DeleteLoadBalancer(project *compose.Project) error { + logrus.Debug("Delete Load Balancer") + // FIXME We can tag LoadBalancer but not search by tag ? + loadBalancer, err := c.ELB.DescribeLoadBalancers(&elbv2.DescribeLoadBalancersInput{ + Names: aws.StringSlice([]string{fmt.Sprintf("%s-LoadBalancer", project.Name)}), + }) + if err != nil { + return err + } + arn := loadBalancer.LoadBalancers[0].LoadBalancerArn + + err = c.DeleteListeners(arn) + if err != nil { + return err + } + + err = c.DeleteTargetGroups(arn) + if err != nil { + return err + } + + _, err = c.ELB.DeleteLoadBalancer(&elbv2.DeleteLoadBalancerInput{LoadBalancerArn: arn}) + return err +} + +func (c client) CreateTargetGroup(name string, vpc *string, port types.ServicePortConfig) (*string, error) { + logrus.Debugf("Create Target Group %d/%s\n", port.Target, port.Protocol) + group, err := c.ELB.CreateTargetGroup(&elbv2.CreateTargetGroupInput{ + Name: aws.String(name), + Port: aws.Int64(int64(port.Target)), + Protocol: aws.String(strings.ToUpper(port.Protocol)), + TargetType: aws.String("ip"), + VpcId: vpc, + }) + if err != nil { + return nil, err + } + arn := group.TargetGroups[0].TargetGroupArn + return arn, nil +} + +func (c client) DeleteTargetGroups(loadBalancer *string) error { + groups, err := c.ELB.DescribeTargetGroups(&elbv2.DescribeTargetGroupsInput{ + LoadBalancerArn: loadBalancer, + }) + if err != nil { + return err + } + for _, group := range groups.TargetGroups { + logrus.Debugf("Delete Target Group %s\n", *group.TargetGroupArn) + _, err := c.ELB.DeleteTargetGroup(&elbv2.DeleteTargetGroupInput{ + TargetGroupArn: group.TargetGroupArn, + }) + if err != nil { + return err + } + } + return nil +} + +func (c client) CreateListener(port types.ServicePortConfig, arn *string, target *string) error { + logrus.Debugf("Create Listener %d\n", port.Published) + _, err := c.ELB.CreateListener(&elbv2.CreateListenerInput{ + DefaultActions: []*elbv2.Action{ + { + ForwardConfig: &elbv2.ForwardActionConfig{ + TargetGroups: []*elbv2.TargetGroupTuple{ + { + TargetGroupArn: target, + }, + }, + }, + Type: aws.String(elbv2.ActionTypeEnumForward), + }, + }, + LoadBalancerArn: arn, + Port: aws.Int64(int64(port.Published)), + Protocol: aws.String(strings.ToUpper(port.Protocol)), + }) + return err +} + +func (c client) DeleteListeners(loadBalancer *string) error { + listeners, err := c.ELB.DescribeListeners(&elbv2.DescribeListenersInput{ + LoadBalancerArn: loadBalancer, + }) + if err != nil { + return err + } + for _, listener := range listeners.Listeners { + logrus.Debugf("Delete Listener %s\n", *listener.ListenerArn) + _, err := c.ELB.DeleteListener(&elbv2.DeleteListenerInput{ + ListenerArn: listener.ListenerArn, + }) + if err != nil { + return err + } + } + return nil +} \ No newline at end of file diff --git a/ecs/pkg/amazon/up.go b/ecs/pkg/amazon/up.go index e54aae14f..d0b84b2a6 100644 --- a/ecs/pkg/amazon/up.go +++ b/ecs/pkg/amazon/up.go @@ -1,6 +1,7 @@ package amazon import ( + "fmt" "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/ecs" "github.com/compose-spec/compose-go/types" @@ -40,13 +41,37 @@ func (c *client) ComposeUp(project *compose.Project) error { return err } + loadBalancer, err := c.CreateLoadBalancer(project, subnets) + if err != nil { + return err + } + logGroup, err := c.GetOrCreateLogGroup(project) if err != nil { return err } for _, mapping := range mappings { - _, err = c.CreateService(project, mapping.service, mapping.task, securityGroup, subnets, logGroup) + ingress := []*ecs.LoadBalancer{} + for _, port := range mapping.service.Ports { + name := fmt.Sprintf("%s-%s-%d-%s", project.Name, mapping.service.Name, port.Target, port.Protocol) + targetgroup, err := c.CreateTargetGroup(name, vpc, port) + if err != nil { + return err + } + ingress = append(ingress, &ecs.LoadBalancer{ + ContainerName: aws.String(mapping.service.Name), + ContainerPort: aws.Int64(int64(port.Target)), + TargetGroupArn: targetgroup, + }) + + err = c.CreateListener(port, loadBalancer, targetgroup) + if err != nil { + return err + } + } + + _, err = c.CreateService(project, mapping.service, mapping.task, securityGroup, subnets, logGroup, ingress) if err != nil { return err } @@ -54,7 +79,7 @@ func (c *client) ComposeUp(project *compose.Project) error { return nil } -func (c *client) CreateService(project *compose.Project, service *types.ServiceConfig, task *ecs.RegisterTaskDefinitionInput, securityGroup *string, subnets []*string, logGroup *string) (*string, error) { +func (c *client) CreateService(project *compose.Project, service *types.ServiceConfig, task *ecs.RegisterTaskDefinitionInput, securityGroup *string, subnets []*string, logGroup *string, ingress []*ecs.LoadBalancer) (*string, error) { role, err := c.GetEcsTaskExecutionRole(service) if err != nil { return nil, err @@ -88,6 +113,7 @@ func (c *client) CreateService(project *compose.Project, service *types.ServiceC ServiceName: aws.String(service.Name), SchedulingStrategy: aws.String(ecs.SchedulingStrategyReplica), TaskDefinition: arn, + LoadBalancers: ingress, }) for _, port := range service.Ports { From dd48cc4599a5319f9de63f0dacb42ba526005fb7 Mon Sep 17 00:00:00 2001 From: Nicolas De Loof <nicolas.deloof@gmail.com> Date: Thu, 16 Apr 2020 16:18:06 +0200 Subject: [PATCH 011/198] Introduct option to re-use LoadBalancer Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com> --- ecs/cmd/main/main.go | 28 ++++++++++++++++++++++++---- ecs/pkg/amazon/down.go | 4 ++-- ecs/pkg/amazon/loadBalancer.go | 6 ++++-- ecs/pkg/amazon/up.go | 12 +++++++----- ecs/pkg/compose/api.go | 4 ++-- 5 files changed, 39 insertions(+), 15 deletions(-) diff --git a/ecs/cmd/main/main.go b/ecs/cmd/main/main.go index 75b6c2b7d..183404768 100644 --- a/ecs/cmd/main/main.go +++ b/ecs/cmd/main/main.go @@ -76,7 +76,19 @@ func ComposeCommand(clusteropts *clusterOptions) *cobra.Command { return cmd } +type upOptions struct { + loadBalancerArn string +} + +func (o upOptions) LoadBalancerArn() *string { + if o.loadBalancerArn == "" { + return nil + } + return &o.loadBalancerArn +} + func UpCommand(clusteropts *clusterOptions, projectOpts *compose.ProjectOptions) *cobra.Command { + opts := upOptions{} cmd := &cobra.Command{ Use: "up", RunE: compose.WithProject(projectOpts, func(project *compose.Project, args []string) error { @@ -84,22 +96,30 @@ func UpCommand(clusteropts *clusterOptions, projectOpts *compose.ProjectOptions) if err != nil { return err } - return client.ComposeUp(project) + return client.ComposeUp(project, opts.LoadBalancerArn()) }), } + cmd.Flags().StringVar(&opts.loadBalancerArn, "load-balancer", "", "") return cmd } -func DownCommand(clusteropts *clusterOptions, opts *compose.ProjectOptions) *cobra.Command { +type downOptions struct { + KeepLoadBalancer bool +} + + +func DownCommand(clusteropts *clusterOptions, projectOpts *compose.ProjectOptions) *cobra.Command { + opts := downOptions{} cmd := &cobra.Command{ Use: "down", - RunE: compose.WithProject(opts, func(project *compose.Project, args []string) error { + RunE: compose.WithProject(projectOpts, func(project *compose.Project, args []string) error { client, err := amazon.NewClient(clusteropts.profile, clusteropts.cluster, clusteropts.region) if err != nil { return err } - return client.ComposeDown(project) + return client.ComposeDown(project, opts.KeepLoadBalancer) }), } + cmd.Flags().BoolVar(&opts.KeepLoadBalancer, "keep-load-balancer", false, "Keep Load Balancer for further use") return cmd } \ No newline at end of file diff --git a/ecs/pkg/amazon/down.go b/ecs/pkg/amazon/down.go index e68c31329..dbd5b6842 100644 --- a/ecs/pkg/amazon/down.go +++ b/ecs/pkg/amazon/down.go @@ -8,8 +8,8 @@ import ( "github.com/sirupsen/logrus" ) -func (c *client) ComposeDown(project *compose.Project) error { - err := c.DeleteLoadBalancer(project) +func (c *client) ComposeDown(project *compose.Project, keepLoadBalancer bool) error { + err := c.DeleteLoadBalancer(project, keepLoadBalancer) if err != nil { return err } diff --git a/ecs/pkg/amazon/loadBalancer.go b/ecs/pkg/amazon/loadBalancer.go index 6edc79556..6972406e4 100644 --- a/ecs/pkg/amazon/loadBalancer.go +++ b/ecs/pkg/amazon/loadBalancer.go @@ -31,7 +31,7 @@ func (c client) CreateLoadBalancer(project *compose.Project, subnets []*string) return alb.LoadBalancers[0].LoadBalancerArn, nil } -func (c client) DeleteLoadBalancer(project *compose.Project) error { +func (c client) DeleteLoadBalancer(project *compose.Project, keepLoadBalancer bool) error { logrus.Debug("Delete Load Balancer") // FIXME We can tag LoadBalancer but not search by tag ? loadBalancer, err := c.ELB.DescribeLoadBalancers(&elbv2.DescribeLoadBalancersInput{ @@ -52,7 +52,9 @@ func (c client) DeleteLoadBalancer(project *compose.Project) error { return err } - _, err = c.ELB.DeleteLoadBalancer(&elbv2.DeleteLoadBalancerInput{LoadBalancerArn: arn}) + if !keepLoadBalancer { + _, err = c.ELB.DeleteLoadBalancer(&elbv2.DeleteLoadBalancerInput{LoadBalancerArn: arn}) + } return err } diff --git a/ecs/pkg/amazon/up.go b/ecs/pkg/amazon/up.go index d0b84b2a6..0dbc35c92 100644 --- a/ecs/pkg/amazon/up.go +++ b/ecs/pkg/amazon/up.go @@ -10,7 +10,7 @@ import ( "github.com/sirupsen/logrus" ) -func (c *client) ComposeUp(project *compose.Project) error { +func (c *client) ComposeUp(project *compose.Project, loadBalancerArn *string) error { type mapping struct { service *types.ServiceConfig task *ecs.RegisterTaskDefinitionInput @@ -41,9 +41,11 @@ func (c *client) ComposeUp(project *compose.Project) error { return err } - loadBalancer, err := c.CreateLoadBalancer(project, subnets) - if err != nil { - return err + if loadBalancerArn == nil { + loadBalancerArn, err = c.CreateLoadBalancer(project, subnets) + if err != nil { + return err + } } logGroup, err := c.GetOrCreateLogGroup(project) @@ -65,7 +67,7 @@ func (c *client) ComposeUp(project *compose.Project) error { TargetGroupArn: targetgroup, }) - err = c.CreateListener(port, loadBalancer, targetgroup) + err = c.CreateListener(port, loadBalancerArn, targetgroup) if err != nil { return err } diff --git a/ecs/pkg/compose/api.go b/ecs/pkg/compose/api.go index cd6eb7878..ceebacb89 100644 --- a/ecs/pkg/compose/api.go +++ b/ecs/pkg/compose/api.go @@ -1,6 +1,6 @@ package compose type API interface { - ComposeUp(project *Project) error - ComposeDown(project *Project) error + ComposeUp(project *Project, loadBalancerArn *string) error + ComposeDown(project *Project, keepLoadBalancer bool) error } \ No newline at end of file From 4e72d1892a555360f06e8e78211a7751b2befa4c Mon Sep 17 00:00:00 2001 From: Nicolas De Loof <nicolas.deloof@gmail.com> Date: Mon, 20 Apr 2020 13:47:38 +0200 Subject: [PATCH 012/198] Prefer AWS API interface over actual implementation This will help introduce mock-based tests Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com> --- ecs/pkg/amazon/client.go | 19 ++++++++++++++----- ecs/pkg/amazon/cloudformation.go | 1 + 2 files changed, 15 insertions(+), 5 deletions(-) create mode 100644 ecs/pkg/amazon/cloudformation.go diff --git a/ecs/pkg/amazon/client.go b/ecs/pkg/amazon/client.go index 247536a98..3dceb4ded 100644 --- a/ecs/pkg/amazon/client.go +++ b/ecs/pkg/amazon/client.go @@ -3,11 +3,18 @@ package amazon import ( "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws/session" + "github.com/aws/aws-sdk-go/service/cloudformation" + "github.com/aws/aws-sdk-go/service/cloudformation/cloudformationiface" "github.com/aws/aws-sdk-go/service/cloudwatchlogs" + "github.com/aws/aws-sdk-go/service/cloudwatchlogs/cloudwatchlogsiface" "github.com/aws/aws-sdk-go/service/ec2" + "github.com/aws/aws-sdk-go/service/ec2/ec2iface" "github.com/aws/aws-sdk-go/service/ecs" + "github.com/aws/aws-sdk-go/service/ecs/ecsiface" "github.com/aws/aws-sdk-go/service/elbv2" + "github.com/aws/aws-sdk-go/service/elbv2/elbv2iface" "github.com/aws/aws-sdk-go/service/iam" + "github.com/aws/aws-sdk-go/service/iam/iamiface" "github.com/docker/ecs-plugin/pkg/compose" ) @@ -35,6 +42,7 @@ func NewClient(profile string, cluster string, region string) (compose.API, erro ELB: elbv2.New(sess), CW: cloudwatchlogs.New(sess), IAM: iam.New(sess), + CF: cloudformation.New(sess), }, nil } @@ -42,11 +50,12 @@ type client struct { Cluster string Region string sess *session.Session - ECS *ecs.ECS - EC2 *ec2.EC2 - ELB *elbv2.ELBV2 - CW *cloudwatchlogs.CloudWatchLogs - IAM *iam.IAM + ECS ecsiface.ECSAPI + EC2 ec2iface.EC2API + ELB elbv2iface.ELBV2API + CW cloudwatchlogsiface.CloudWatchLogsAPI + IAM iamiface.IAMAPI + CF cloudformationiface.CloudFormationAPI } var _ compose.API = &client{} diff --git a/ecs/pkg/amazon/cloudformation.go b/ecs/pkg/amazon/cloudformation.go new file mode 100644 index 000000000..1f74174fe --- /dev/null +++ b/ecs/pkg/amazon/cloudformation.go @@ -0,0 +1 @@ +package amazon From b70f01d2f4d172ec537be41427c7f33c692d5005 Mon Sep 17 00:00:00 2001 From: Nicolas De Loof <nicolas.deloof@gmail.com> Date: Tue, 21 Apr 2020 11:38:52 +0200 Subject: [PATCH 013/198] Adopt CloudFormation to create ECS app from compose.yaml Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com> --- ecs/cmd/main/main.go | 31 +++- ecs/go.mod | 1 + ecs/go.sum | 26 ++- ecs/pkg/amazon/client.go | 33 ++-- ecs/pkg/amazon/cloudformation.go | 80 +++++++++ ecs/pkg/amazon/down.go | 54 +----- ecs/pkg/amazon/ecs.go | 1 - ecs/pkg/amazon/loadBalancer.go | 5 +- ecs/pkg/amazon/logs.go | 1 + ecs/pkg/amazon/network.go | 13 +- ecs/pkg/amazon/roles.go | 3 +- ecs/pkg/amazon/up.go | 132 ++++----------- ecs/pkg/compose/api.go | 5 +- ecs/pkg/compose/opts.go | 1 - ecs/pkg/compose/project.go | 8 +- ecs/pkg/compose/project_test.go | 3 +- ecs/pkg/convert/convert.go | 278 +++++++++++++------------------ 17 files changed, 314 insertions(+), 361 deletions(-) diff --git a/ecs/cmd/main/main.go b/ecs/cmd/main/main.go index 183404768..cde9be3bd 100644 --- a/ecs/cmd/main/main.go +++ b/ecs/cmd/main/main.go @@ -2,6 +2,7 @@ package main import ( "fmt" + "github.com/docker/cli/cli-plugins/manager" "github.com/docker/cli/cli-plugins/plugin" "github.com/docker/cli/cli/command" @@ -70,6 +71,7 @@ func ComposeCommand(clusteropts *clusterOptions) *cobra.Command { opts.AddFlags(cmd.Flags()) cmd.AddCommand( + ConvertCommand(clusteropts, opts), UpCommand(clusteropts, opts), DownCommand(clusteropts, opts), ) @@ -87,6 +89,32 @@ func (o upOptions) LoadBalancerArn() *string { return &o.loadBalancerArn } +func ConvertCommand(clusteropts *clusterOptions, projectOpts *compose.ProjectOptions) *cobra.Command { + opts := upOptions{} + cmd := &cobra.Command{ + Use: "convert", + RunE: compose.WithProject(projectOpts, func(project *compose.Project, args []string) error { + client, err := amazon.NewClient(clusteropts.profile, clusteropts.cluster, clusteropts.region) + if err != nil { + return err + } + template, err := client.Convert(project, opts.LoadBalancerArn()) + if err != nil { + return err + } + + j, err := template.JSON() + if err != nil { + fmt.Printf("Failed to generate JSON: %s\n", err) + } else { + fmt.Printf("%s\n", string(j)) + } + return nil + }), + } + return cmd +} + func UpCommand(clusteropts *clusterOptions, projectOpts *compose.ProjectOptions) *cobra.Command { opts := upOptions{} cmd := &cobra.Command{ @@ -107,7 +135,6 @@ type downOptions struct { KeepLoadBalancer bool } - func DownCommand(clusteropts *clusterOptions, projectOpts *compose.ProjectOptions) *cobra.Command { opts := downOptions{} cmd := &cobra.Command{ @@ -122,4 +149,4 @@ func DownCommand(clusteropts *clusterOptions, projectOpts *compose.ProjectOption } cmd.Flags().BoolVar(&opts.KeepLoadBalancer, "keep-load-balancer", false, "Keep Load Balancer for further use") return cmd -} \ No newline at end of file +} diff --git a/ecs/go.mod b/ecs/go.mod index 1189b1d57..a8f444c52 100644 --- a/ecs/go.mod +++ b/ecs/go.mod @@ -6,6 +6,7 @@ require ( github.com/Shopify/logrus-bugsnag v0.0.0-20171204204709-577dee27f20d // indirect github.com/agl/ed25519 v0.0.0-20170116200512-5312a6153412 // indirect github.com/aws/aws-sdk-go v1.28.9 + github.com/awslabs/goformation/v4 v4.8.0 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 diff --git a/ecs/go.sum b/ecs/go.sum index f2d87a8ca..dc5a0393a 100644 --- a/ecs/go.sum +++ b/ecs/go.sum @@ -21,6 +21,8 @@ github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRF github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= github.com/aws/aws-sdk-go v1.28.9 h1:grIuBQc+p3dTRXerh5+2OxSuWFi0iXuxbFdTSg0jaW0= github.com/aws/aws-sdk-go v1.28.9/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= +github.com/awslabs/goformation/v4 v4.8.0 h1:UiUhyokRy3suEqBXTnipvY8klqY3Eyl4GCH17brraEc= +github.com/awslabs/goformation/v4 v4.8.0/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= @@ -46,8 +48,6 @@ 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-20200131085702-0b38cc2d8e6b h1:VK0c2Hfrg9FHjvJpWfGwiHPP2UeU0QZ6/5/dN0ehbSQ= -github.com/compose-spec/compose-go v0.0.0-20200131085702-0b38cc2d8e6b/go.mod h1:KoJjdV81vERSyYVuQD63nryyt8ZTlqTWe8JuJIMhRo4= github.com/compose-spec/compose-go v0.0.0-20200409090215-53c0040c9127 h1:mAsQN3s19glh3KBOQjiRYBhqaX1SdzNqhB3/cuqgSbE= github.com/compose-spec/compose-go v0.0.0-20200409090215-53c0040c9127/go.mod h1:1PUpzRF1O/65VOqXZuwpCuYY7pJxbIq1jbAvAf62FGM= github.com/containerd/cgroups v0.0.0-20190919134610-bf292b21730f/go.mod h1:OApqhQ4XNSNC13gXIwDjhOQxjWa/NxkwZXJ1EvqT0ko= @@ -139,7 +139,9 @@ github.com/hashicorp/go-multierror v0.0.0-20161216184304-ed905158d874/go.mod h1: github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +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/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/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= @@ -180,7 +182,6 @@ github.com/lib/pq v1.3.0 h1:/qkRGz8zljWiDcFvgpwUpwIAPu3r07TDvs3Rws+o/pU= github.com/lib/pq v1.3.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/magiconair/properties v1.8.0 h1:LLgXmsheXeRoUOBOjtwPQCWIYqM/LU1ayDtDePerRcY= github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= -github.com/mattn/go-shellwords v1.0.9/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y= github.com/mattn/go-shellwords v1.0.10 h1:Y7Xqm8piKOO3v10Thp7Z36h4FYFjt5xB//6XvOrs2Gw= github.com/mattn/go-shellwords v1.0.10/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y= github.com/mattn/go-sqlite3 v1.10.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= @@ -205,8 +206,12 @@ github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7P github.com/mreiferson/go-httpclient v0.0.0-20160630210159-31f0106b4474/go.mod h1:OQA4XLvDbMgS8P0CevmM4m9Q3Jq4phKUzcocxuGJ5m8= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/nkovacs/streamquote v0.0.0-20170412213628-49af9bddb229/go.mod h1:0aYXnNPJ8l7uZxf45rWW1a/uME32OF0rhiYGNQ2oF2E= +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.10.1 h1:q/mM8GF/n0shIN8SaAZ0V+jnLPzen6WIVZdiwrRlMlo= github.com/onsi/ginkgo v1.10.1/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/gomega v1.2.0/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= +github.com/onsi/gomega v1.7.0 h1:XPnZz8VVBHjVsy1vzJmRwIcSwiUO+JFfrv/xGiigmME= github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk= github.com/opencontainers/go-digest v0.0.0-20180430190053-c9281466c8b2/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s= @@ -244,12 +249,14 @@ github.com/prometheus/procfs v0.0.3/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDa github.com/prometheus/procfs v0.0.5 h1:3+auTFlqw+ZaQYJARz6ArODtkaIwtvBTx3N2NehQlL8= github.com/prometheus/procfs v0.0.5/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ= github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= +github.com/sanathkr/go-yaml v0.0.0-20170819195128-ed9d249f429b h1:jUK33OXuZP/l6babJtnLo1qsGvq6G9so9KMflGAm4YA= +github.com/sanathkr/go-yaml v0.0.0-20170819195128-ed9d249f429b/go.mod h1:8458kAagoME2+LN5//WxE71ysZ3B7r22fdgb7qVmXSY= +github.com/sanathkr/yaml v0.0.0-20170819201035-0056894fa522 h1:fOCp11H0yuyAt2wqlbJtbyPzSgaxHTv8uN1pMpkG1t8= +github.com/sanathkr/yaml v0.0.0-20170819201035-0056894fa522/go.mod h1:tQTYKOQgxoH3v6dEmdHiz4JG+nbxWwM5fgPQUpSZqVQ= github.com/sirupsen/logrus v1.0.4-0.20170822132746-89742aefa4b2/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.3.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 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4= -github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/sirupsen/logrus v1.5.0 h1:1N5EYkVAPEywqZRJd7cwnRtCb6xJx7NH3T3WUTF980Q= github.com/sirupsen/logrus v1.5.0/go.mod h1:+F7Ogzej0PZc/94MaYx/nvG9jOFMD2osvC3s+Squfpo= github.com/spf13/afero v1.1.2 h1:m8/z1t7/fwjysjQRYbP0RD+bUIF/8tJwPdEZsI83ACI= @@ -285,9 +292,12 @@ github.com/weppos/publicsuffix-go v0.5.0 h1:rutRtjBJViU/YjcI5d80t4JAVvDltS6bciJg github.com/weppos/publicsuffix-go v0.5.0/go.mod h1:z3LCPQ38eedDQSwmsSRW4Y7t2L8Ln16JPQ02lHAdn5k= github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f h1:J9EGpcZtP0E/raorCMxlFGSTBrsSlaDGf3jU/qvAE2c= github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= +github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo= +github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0= github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= github.com/xeipuuv/gojsonschema v0.0.0-20180618132009-1d523034197f/go.mod h1:5yf86TLmAcydyeJq5YvxkGPE2fm/u4myDekKRoLuqhs= +github.com/xeipuuv/gojsonschema v0.0.0-20181112162635-ac52e6811b56/go.mod h1:5yf86TLmAcydyeJq5YvxkGPE2fm/u4myDekKRoLuqhs= github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74= github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= github.com/xlab/handysort v0.0.0-20150421192137-fb3537ed64a1 h1:j2hhcujLRHAg872RWAV5yaUrEjHEObwDv3aImCaNLek= @@ -327,6 +337,8 @@ golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09 h1:KaQtG+aDELoNmXYas3TVkGNYR golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190613194153-d28f0bde5980 h1:dfGZHvZk057jK2MCeWus/TowKpJ8y4AmooUzdBSR9GU= golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b h1:0mm1VjtFUOIlE1SbDlwjYaDxZVDP2S5ou6y0gSgXHu8= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -385,10 +397,12 @@ gopkg.in/dancannon/gorethink.v3 v3.0.5 h1:/g7PWP7zUS6vSNmHSDbjCHQh1Rqn8Jy6zSMQxA gopkg.in/dancannon/gorethink.v3 v3.0.5/go.mod h1:GXsi1e3N2OcKhcP6nsYABTiUejbWMFO4GY5a4pEaeEc= gopkg.in/fatih/pool.v2 v2.0.0 h1:xIFeWtxifuQJGk/IEPKsTduEKcKvPmhoiVDGpC40nKg= gopkg.in/fatih/pool.v2 v2.0.0/go.mod h1:8xVGeu1/2jr2wm5V9SPuMht2H5AEmf5aFMGSQixtjTY= +gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2/go.mod h1:Xk6kEKp8OKb+X14hQBKWaSkCsqBpgog8nAV2xsGOxlo= gopkg.in/gorethink/gorethink.v3 v3.0.5 h1:e2Uc/Xe+hpcVQFsj6MuHlYog3r0JYpnTzwDj/y2O4MU= gopkg.in/gorethink/gorethink.v3 v3.0.5/go.mod h1:+3yIIHJUGMBK+wyPH+iN5TP+88ikFDfZdqTlK3Y9q8I= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= @@ -396,8 +410,6 @@ gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo= gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= -gotest.tools/v3 v3.0.0 h1:d+tVGRu6X0ZBQ+kyAR8JKi6AXhTP2gmQaoIYaGFz634= -gotest.tools/v3 v3.0.0/go.mod h1:TUP+/YtXl/dp++T+SZ5v2zUmLVBHmptSb/ajDLCJ+3c= gotest.tools/v3 v3.0.2 h1:kG1BFyqVHuQoVQiR1bWGnfz/fmHvvuiSPIV7rvl360E= gotest.tools/v3 v3.0.2/go.mod h1:3SzNCllyD9/Y+b5r9JIKQ474KzkZyqLqEfYqMsX94Bk= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/ecs/pkg/amazon/client.go b/ecs/pkg/amazon/client.go index 3dceb4ded..ff202fc23 100644 --- a/ecs/pkg/amazon/client.go +++ b/ecs/pkg/amazon/client.go @@ -18,7 +18,6 @@ import ( "github.com/docker/ecs-plugin/pkg/compose" ) - const ( ProjectTag = "com.docker.compose.project" ) @@ -35,27 +34,27 @@ func NewClient(profile string, cluster string, region string) (compose.API, erro } return &client{ Cluster: cluster, - Region: region, - sess: sess, - ECS: ecs.New(sess), - EC2: ec2.New(sess), - ELB: elbv2.New(sess), - CW: cloudwatchlogs.New(sess), - IAM: iam.New(sess), - CF: cloudformation.New(sess), + Region: region, + sess: sess, + ECS: ecs.New(sess), + EC2: ec2.New(sess), + ELB: elbv2.New(sess), + CW: cloudwatchlogs.New(sess), + IAM: iam.New(sess), + CF: cloudformation.New(sess), }, nil } type client struct { Cluster string - Region string - sess *session.Session - ECS ecsiface.ECSAPI - EC2 ec2iface.EC2API - ELB elbv2iface.ELBV2API - CW cloudwatchlogsiface.CloudWatchLogsAPI - IAM iamiface.IAMAPI - CF cloudformationiface.CloudFormationAPI + Region string + sess *session.Session + ECS ecsiface.ECSAPI + EC2 ec2iface.EC2API + ELB elbv2iface.ELBV2API + CW cloudwatchlogsiface.CloudWatchLogsAPI + IAM iamiface.IAMAPI + CF cloudformationiface.CloudFormationAPI } var _ compose.API = &client{} diff --git a/ecs/pkg/amazon/cloudformation.go b/ecs/pkg/amazon/cloudformation.go index 1f74174fe..0b3b0bc41 100644 --- a/ecs/pkg/amazon/cloudformation.go +++ b/ecs/pkg/amazon/cloudformation.go @@ -1 +1,81 @@ package amazon + +import ( + "fmt" + "strings" + + ecsapi "github.com/aws/aws-sdk-go/service/ecs" + "github.com/awslabs/goformation/v4/cloudformation" + "github.com/awslabs/goformation/v4/cloudformation/ec2" + "github.com/awslabs/goformation/v4/cloudformation/ecs" + "github.com/docker/ecs-plugin/pkg/compose" + "github.com/docker/ecs-plugin/pkg/convert" +) + +func (c client) Convert(project *compose.Project, loadBalancerArn *string) (*cloudformation.Template, error) { + template := cloudformation.NewTemplate() + + vpc, err := c.GetDefaultVPC() + if err != nil { + return nil, err + } + + subnets, err := c.GetSubNets(vpc) + if err != nil { + return nil, err + } + + var ingresses = []ec2.SecurityGroup_Ingress{} + for _, service := range project.Services { + for _, port := range service.Ports { + ingresses = append(ingresses, ec2.SecurityGroup_Ingress{ + CidrIp: "0.0.0.0/0", + Description: fmt.Sprintf("%d/%s", port.Target, port.Protocol), + FromPort: int(port.Target), + IpProtocol: strings.ToUpper(port.Protocol), + ToPort: int(port.Target), + }) + } + } + + securityGroup := fmt.Sprintf("%s Security Group", project.Name) + template.Resources["SecurityGroup"] = &ec2.SecurityGroup{ + GroupDescription: securityGroup, + GroupName: securityGroup, + SecurityGroupIngress: ingresses, + VpcId: *vpc, + } + + for _, service := range project.Services { + definition, err := convert.Convert(project, service) + if err != nil { + return nil, err + } + + role, err := c.GetEcsTaskExecutionRole(service) + if err != nil { + return nil, err + } + definition.TaskRoleArn = *role + + taskDefinition := fmt.Sprintf("%sTaskDefinition", service.Name) + template.Resources[taskDefinition] = definition + + template.Resources[service.Name] = &ecs.Service{ + Cluster: c.Cluster, + DesiredCount: 1, + LaunchType: ecsapi.LaunchTypeFargate, + NetworkConfiguration: &ecs.Service_NetworkConfiguration{ + AwsvpcConfiguration: &ecs.Service_AwsVpcConfiguration{ + AssignPublicIp: ecsapi.AssignPublicIpEnabled, + SecurityGroups: []string{cloudformation.Ref("SecurityGroup")}, + Subnets: subnets, + }, + }, + SchedulingStrategy: ecsapi.SchedulingStrategyReplica, + ServiceName: service.Name, + TaskDefinition: cloudformation.Ref(taskDefinition), + } + } + return template, nil +} diff --git a/ecs/pkg/amazon/down.go b/ecs/pkg/amazon/down.go index dbd5b6842..ac2648325 100644 --- a/ecs/pkg/amazon/down.go +++ b/ecs/pkg/amazon/down.go @@ -1,64 +1,18 @@ package amazon import ( - "github.com/aws/aws-sdk-go/aws" - "github.com/aws/aws-sdk-go/service/ec2" - "github.com/aws/aws-sdk-go/service/ecs" + "github.com/aws/aws-sdk-go/service/cloudformation" "github.com/docker/ecs-plugin/pkg/compose" - "github.com/sirupsen/logrus" ) func (c *client) ComposeDown(project *compose.Project, keepLoadBalancer bool) error { - err := c.DeleteLoadBalancer(project, keepLoadBalancer) - if err != nil { - return err - } - - services := []*string{} - // FIXME we should be able to retrieve services by tags, so we don't need the initial compose file to run "down" - for _, service := range project.Services { - logrus.Debugf("Deleting service %q\n", service.Name) - out, err := c.ECS.DeleteService(&ecs.DeleteServiceInput{ - // Force to true so that we don't have to scale down to 0 - // before deleting - Force: aws.Bool(true), - Cluster: aws.String(c.Cluster), - Service: aws.String(service.Name), - }) - if err != nil { - return err - } - services = append(services, out.Service.ServiceArn) - } - - logrus.Info("Stopping services...") - err = c.ECS.WaitUntilServicesInactive(&ecs.DescribeServicesInput{ - Services: services, + _, err := c.CF.DeleteStack(&cloudformation.DeleteStackInput{ + StackName: &project.Name, }) if err != nil { return err } - logrus.Info("All services stopped") - logrus.Debug("Deleting security groups") - groups, err := c.EC2.DescribeSecurityGroups(&ec2.DescribeSecurityGroupsInput{ - Filters: []*ec2.Filter{ - { - Name: aws.String("tag:" + ProjectTag), - Values: aws.StringSlice([]string{project.Name}), - }, - }, - }) - if err != nil { - return err - } - for _, g := range groups.SecurityGroups { - _, err = c.EC2.DeleteSecurityGroup(&ec2.DeleteSecurityGroupInput{ - GroupId: g.GroupId, - }) - if err != nil { - return err - } - } + // TODO monitor progress return nil } diff --git a/ecs/pkg/amazon/ecs.go b/ecs/pkg/amazon/ecs.go index 5366a7a07..423980f3d 100644 --- a/ecs/pkg/amazon/ecs.go +++ b/ecs/pkg/amazon/ecs.go @@ -5,7 +5,6 @@ import ( "github.com/sirupsen/logrus" ) - func (c client) RegisterTaskDefinition(task *ecs.RegisterTaskDefinitionInput) (*string, error) { logrus.Debug("Register Task Definition") def, err := c.ECS.RegisterTaskDefinition(task) diff --git a/ecs/pkg/amazon/loadBalancer.go b/ecs/pkg/amazon/loadBalancer.go index 6972406e4..c8f96cd9b 100644 --- a/ecs/pkg/amazon/loadBalancer.go +++ b/ecs/pkg/amazon/loadBalancer.go @@ -2,9 +2,10 @@ package amazon import ( "fmt" + "strings" + "github.com/docker/ecs-plugin/pkg/compose" "github.com/sirupsen/logrus" - "strings" "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/elbv2" @@ -132,4 +133,4 @@ func (c client) DeleteListeners(loadBalancer *string) error { } } return nil -} \ No newline at end of file +} diff --git a/ecs/pkg/amazon/logs.go b/ecs/pkg/amazon/logs.go index b42bc8b08..0c06671c2 100644 --- a/ecs/pkg/amazon/logs.go +++ b/ecs/pkg/amazon/logs.go @@ -2,6 +2,7 @@ package amazon import ( "fmt" + "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/cloudwatchlogs" "github.com/docker/ecs-plugin/pkg/compose" diff --git a/ecs/pkg/amazon/network.go b/ecs/pkg/amazon/network.go index a876b4bc3..26b412758 100644 --- a/ecs/pkg/amazon/network.go +++ b/ecs/pkg/amazon/network.go @@ -2,12 +2,13 @@ package amazon import ( "fmt" + "strings" + "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/ec2" "github.com/compose-spec/compose-go/types" "github.com/docker/ecs-plugin/pkg/compose" "github.com/sirupsen/logrus" - "strings" ) // GetDefaultVPC retrieve the default VPC for AWS account @@ -30,9 +31,8 @@ func (c client) GetDefaultVPC() (*string, error) { return vpcs.Vpcs[0].VpcId, nil } - // GetSubNets retrieve default subnets for a VPC -func (c client) GetSubNets(vpc *string) ([]*string, error) { +func (c client) GetSubNets(vpc *string) ([]string, error) { logrus.Debug("Retrieve SubNets") subnets, err := c.EC2.DescribeSubnets(&ec2.DescribeSubnetsInput{ DryRun: nil, @@ -51,9 +51,9 @@ func (c client) GetSubNets(vpc *string) ([]*string, error) { return nil, err } - ids := []*string{} + ids := []string{} for _, subnet := range subnets.Subnets { - ids = append(ids, subnet.SubnetId) + ids = append(ids, *subnet.SubnetId) } return ids, nil } @@ -91,7 +91,6 @@ func (c client) CreateSecurityGroup(project *compose.Project, vpc *string) (*str return securityGroup.GroupId, nil } - func (c *client) ExposePort(securityGroup *string, port types.ServicePortConfig) error { logrus.Debugf("Authorize ingress port %d/%s\n", port.Published, port.Protocol) _, err := c.EC2.AuthorizeSecurityGroupIngress(&ec2.AuthorizeSecurityGroupIngressInput{ @@ -110,4 +109,4 @@ func (c *client) ExposePort(securityGroup *string, port types.ServicePortConfig) }, }) return err -} \ No newline at end of file +} diff --git a/ecs/pkg/amazon/roles.go b/ecs/pkg/amazon/roles.go index bfa5026a2..7e4594af4 100644 --- a/ecs/pkg/amazon/roles.go +++ b/ecs/pkg/amazon/roles.go @@ -2,6 +2,7 @@ package amazon import ( "fmt" + "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/iam" "github.com/compose-spec/compose-go/types" @@ -13,7 +14,7 @@ const ECSTaskExecutionPolicy = "arn:aws:iam::aws:policy/service-role/AmazonECSTa var defaultTaskExecutionRole *string // GetEcsTaskExecutionRole retrieve the role ARN to apply for task execution -func (c client) GetEcsTaskExecutionRole(spec *types.ServiceConfig) (*string, error) { +func (c client) GetEcsTaskExecutionRole(spec types.ServiceConfig) (*string, error) { if arn, ok := spec.Extras["x-ecs-TaskExecutionRole"]; ok { s := arn.(string) return &s, nil diff --git a/ecs/pkg/amazon/up.go b/ecs/pkg/amazon/up.go index 0dbc35c92..5f9b8750d 100644 --- a/ecs/pkg/amazon/up.go +++ b/ecs/pkg/amazon/up.go @@ -2,128 +2,54 @@ package amazon import ( "fmt" + "os" + "github.com/aws/aws-sdk-go/aws" - "github.com/aws/aws-sdk-go/service/ecs" - "github.com/compose-spec/compose-go/types" + "github.com/aws/aws-sdk-go/service/cloudformation" "github.com/docker/ecs-plugin/pkg/compose" - "github.com/docker/ecs-plugin/pkg/convert" - "github.com/sirupsen/logrus" ) func (c *client) ComposeUp(project *compose.Project, loadBalancerArn *string) error { - type mapping struct { - service *types.ServiceConfig - task *ecs.RegisterTaskDefinitionInput - } - mappings := []mapping{} - for _, service := range project.Services { - task, err := convert.Convert(project, service) - if err != nil { - return err - } - mappings = append(mappings, mapping{ - service: &service, - task: task, - }) - } - - vpc, err := c.GetDefaultVPC() - if err != nil { - return err - } - subnets, err := c.GetSubNets(vpc) + template, err := c.Convert(project, loadBalancerArn) if err != nil { return err } - securityGroup, err := c.CreateSecurityGroup(project, vpc) + json, err := template.JSON() if err != nil { return err } - if loadBalancerArn == nil { - loadBalancerArn, err = c.CreateLoadBalancer(project, subnets) - if err != nil { - return err - } - } - - logGroup, err := c.GetOrCreateLogGroup(project) + _, err = c.CF.ValidateTemplate(&cloudformation.ValidateTemplateInput{ + TemplateBody: aws.String(string(json)), + }) if err != nil { return err } - for _, mapping := range mappings { - ingress := []*ecs.LoadBalancer{} - for _, port := range mapping.service.Ports { - name := fmt.Sprintf("%s-%s-%d-%s", project.Name, mapping.service.Name, port.Target, port.Protocol) - targetgroup, err := c.CreateTargetGroup(name, vpc, port) - if err != nil { - return err - } - ingress = append(ingress, &ecs.LoadBalancer{ - ContainerName: aws.String(mapping.service.Name), - ContainerPort: aws.Int64(int64(port.Target)), - TargetGroupArn: targetgroup, - }) + _, err = c.CF.CreateStack(&cloudformation.CreateStackInput{ + OnFailure: aws.String("DELETE"), + StackName: aws.String(project.Name), + TemplateBody: aws.String(string(json)), + TimeoutInMinutes: aws.Int64(10), + }) + if err != nil { + return err + } - err = c.CreateListener(port, loadBalancerArn, targetgroup) - if err != nil { - return err - } - } - - _, err = c.CreateService(project, mapping.service, mapping.task, securityGroup, subnets, logGroup, ingress) - if err != nil { - return err + events, err := c.CF.DescribeStackEvents(&cloudformation.DescribeStackEventsInput{ + StackName: aws.String(project.Name), + }) + if err != nil { + return err + } + for _, event := range events.StackEvents { + fmt.Printf("%s %s\n", *event.LogicalResourceId, *event.ResourceStatus) + if *event.ResourceStatus == "CREATE_FAILED" { + fmt.Fprintln(os.Stderr, event.ResourceStatusReason) } } + + // TODO monitor progress return nil } - -func (c *client) CreateService(project *compose.Project, service *types.ServiceConfig, task *ecs.RegisterTaskDefinitionInput, securityGroup *string, subnets []*string, logGroup *string, ingress []*ecs.LoadBalancer) (*string, error) { - role, err := c.GetEcsTaskExecutionRole(service) - if err != nil { - return nil, err - } - - task.ExecutionRoleArn = role - - for _, def := range task.ContainerDefinitions { - def.LogConfiguration.Options["awslogs-group"] = logGroup - def.LogConfiguration.Options["awslogs-stream-prefix"] = aws.String(service.Name) - def.LogConfiguration.Options["awslogs-region"] = aws.String(c.Region) - } - - arn, err := c.RegisterTaskDefinition(task) - if err != nil { - return nil, err - } - - logrus.Debug("Create Service") - created, err := c.ECS.CreateService(&ecs.CreateServiceInput{ - Cluster: aws.String(c.Cluster), - DesiredCount: aws.Int64(1), // FIXME get from deploy options - LaunchType: aws.String(ecs.LaunchTypeFargate), //FIXME use service.Isolation tro select EC2 vs Fargate - NetworkConfiguration: &ecs.NetworkConfiguration{ - AwsvpcConfiguration: &ecs.AwsVpcConfiguration{ - AssignPublicIp: aws.String(ecs.AssignPublicIpEnabled), - SecurityGroups: []*string{securityGroup}, - Subnets: subnets, - }, - }, - ServiceName: aws.String(service.Name), - SchedulingStrategy: aws.String(ecs.SchedulingStrategyReplica), - TaskDefinition: arn, - LoadBalancers: ingress, - }) - - for _, port := range service.Ports { - err = c.ExposePort(securityGroup, port) - if err != nil { - return nil, err - } - } - - return created.Service.ServiceArn, err -} diff --git a/ecs/pkg/compose/api.go b/ecs/pkg/compose/api.go index ceebacb89..50f62ccf7 100644 --- a/ecs/pkg/compose/api.go +++ b/ecs/pkg/compose/api.go @@ -1,6 +1,9 @@ package compose +import "github.com/awslabs/goformation/v4/cloudformation" + type API interface { + Convert(project *Project, loadBalancerArn *string) (*cloudformation.Template, error) ComposeUp(project *Project, loadBalancerArn *string) error ComposeDown(project *Project, keepLoadBalancer bool) error -} \ No newline at end of file +} diff --git a/ecs/pkg/compose/opts.go b/ecs/pkg/compose/opts.go index 9429fb01a..390ccaa69 100644 --- a/ecs/pkg/compose/opts.go +++ b/ecs/pkg/compose/opts.go @@ -15,7 +15,6 @@ func (o *ProjectOptions) AddFlags(flags *pflag.FlagSet) { flags.StringVarP(&o.name, "project-name", "n", "", "Specify an alternate project name (default: directory name)") } - type ProjectFunc func(project *Project, args []string) error // WithProject wrap a ProjectFunc into a cobra command diff --git a/ecs/pkg/compose/project.go b/ecs/pkg/compose/project.go index c2c6c007a..6f2ef98e9 100644 --- a/ecs/pkg/compose/project.go +++ b/ecs/pkg/compose/project.go @@ -2,14 +2,15 @@ package compose import ( "fmt" - "github.com/compose-spec/compose-go/loader" - "github.com/compose-spec/compose-go/types" - "github.com/sirupsen/logrus" "io/ioutil" "os" "path/filepath" "regexp" "strings" + + "github.com/compose-spec/compose-go/loader" + "github.com/compose-spec/compose-go/types" + "github.com/sirupsen/logrus" ) type Project struct { @@ -32,7 +33,6 @@ func NewProject(config types.ConfigDetails, name string) (*Project, error) { return &p, nil } - // projectFromOptions load a compose project based on command line options func projectFromOptions(options *ProjectOptions) (*Project, error) { configPath, err := getConfigPathFromOptions(options) diff --git a/ecs/pkg/compose/project_test.go b/ecs/pkg/compose/project_test.go index d5f771404..0b44fb33d 100644 --- a/ecs/pkg/compose/project_test.go +++ b/ecs/pkg/compose/project_test.go @@ -1,9 +1,10 @@ package compose import ( - "gotest.tools/v3/assert" "os" "testing" + + "gotest.tools/v3/assert" ) func Test_project_name(t *testing.T) { diff --git a/ecs/pkg/convert/convert.go b/ecs/pkg/convert/convert.go index 502f5089e..33e95c73c 100644 --- a/ecs/pkg/convert/convert.go +++ b/ecs/pkg/convert/convert.go @@ -1,118 +1,87 @@ package convert import ( - "github.com/docker/ecs-plugin/pkg/compose" + "strings" "time" - "github.com/aws/aws-sdk-go/aws" - "github.com/aws/aws-sdk-go/service/ecs" + ecsapi "github.com/aws/aws-sdk-go/service/ecs" + "github.com/awslabs/goformation/v4/cloudformation/ecs" "github.com/compose-spec/compose-go/types" "github.com/docker/cli/opts" + "github.com/docker/ecs-plugin/pkg/compose" ) -func Convert(project *compose.Project, service types.ServiceConfig) (*ecs.RegisterTaskDefinitionInput, error) { +func Convert(project *compose.Project, service types.ServiceConfig) (*ecs.TaskDefinition, error) { _, err := toCPULimits(service) if err != nil { return nil, err } - foo := int64(256) - logDriver := "awslogs" // FIXME could be set by service.Logging, especially to enable use of firelens - return &ecs.RegisterTaskDefinitionInput{ - ContainerDefinitions: []*ecs.ContainerDefinition{ + return &ecs.TaskDefinition{ + ContainerDefinitions: []ecs.TaskDefinition_ContainerDefinition{ // Here we can declare sidecars and init-containers using https://docs.aws.amazon.com/AmazonECS/latest/developerguide/task_definition_parameters.html#container_definition_dependson { - Command: toStringPtrSlice(service.Command), - Cpu: &foo, - DependsOn: nil, - DisableNetworking: toBoolPtr(service.NetworkMode == "none"), - DnsSearchDomains: toStringPtrSlice(service.DNSSearch), - DnsServers: toStringPtrSlice(service.DNS), - DockerLabels: nil, - DockerSecurityOptions: toStringPtrSlice(service.SecurityOpt), - EntryPoint: toStringPtrSlice(service.Entrypoint), - Environment: toKeyValuePairPtr(service.Environment), - Essential: toBoolPtr(true), - ExtraHosts: toHostEntryPtr(service.ExtraHosts), - FirelensConfiguration: nil, - HealthCheck: toHealthCheck(service.HealthCheck), - Hostname: toStringPtr(service.Hostname), - Image: toStringPtr(service.Image), - Interactive: nil, - Links: nil, - LinuxParameters: toLinuxParameters(service), - LogConfiguration: &ecs.LogConfiguration{ - LogDriver: &logDriver, - Options: map[string]*string{}, - SecretOptions: nil, - }, + Command: service.Command, + Cpu: 256, + DisableNetworking: service.NetworkMode == "none", + DnsSearchDomains: service.DNSSearch, + DnsServers: service.DNS, + DockerLabels: nil, + DockerSecurityOptions: service.SecurityOpt, + EntryPoint: service.Entrypoint, + Environment: toKeyValuePair(service.Environment), + Essential: true, + ExtraHosts: toHostEntryPtr(service.ExtraHosts), + FirelensConfiguration: nil, + HealthCheck: toHealthCheck(service.HealthCheck), + Hostname: service.Hostname, + Image: service.Image, + Interactive: false, + Links: nil, + LinuxParameters: toLinuxParameters(service), Memory: toMemoryLimits(service.Deploy), MemoryReservation: toMemoryReservation(service.Deploy), MountPoints: nil, - Name: toStringPtr(service.Name), + Name: service.Name, PortMappings: toPortMappings(service.Ports), - Privileged: toBoolPtr(service.Privileged), - PseudoTerminal: toBoolPtr(service.Tty), - ReadonlyRootFilesystem: toBoolPtr(service.ReadOnly), + Privileged: service.Privileged, + PseudoTerminal: service.Tty, + ReadonlyRootFilesystem: service.ReadOnly, RepositoryCredentials: nil, ResourceRequirements: nil, Secrets: nil, - StartTimeout: nil, - StopTimeout: durationToInt64Ptr(service.StopGracePeriod), + StartTimeout: 0, + StopTimeout: durationToInt(service.StopGracePeriod), SystemControls: nil, Ulimits: toUlimits(service.Ulimits), - User: toStringPtr(service.User), + User: service.User, VolumesFrom: nil, - WorkingDirectory: toStringPtr(service.WorkingDir), + WorkingDirectory: service.WorkingDir, }, }, Cpu: toCPU(service), - ExecutionRoleArn: nil, - Family: toStringPtr(project.Name), - IpcMode: toStringPtr(service.Ipc), + Family: project.Name, + IpcMode: service.Ipc, Memory: toMemory(service), - NetworkMode: toStringPtr("awsvpc"), // FIXME could be set by service.NetworkMode, Fargate only supports network mode ‘awsvpc’. - PidMode: toStringPtr(service.Pid), + NetworkMode: ecsapi.NetworkModeAwsvpc, // FIXME could be set by service.NetworkMode, Fargate only supports network mode ‘awsvpc’. + PidMode: service.Pid, PlacementConstraints: toPlacementConstraints(service.Deploy), ProxyConfiguration: nil, - RequiresCompatibilities: toRequiresCompatibilities(ecs.LaunchTypeFargate), + RequiresCompatibilities: []string{ecsapi.LaunchTypeFargate}, Tags: nil, - Volumes: []*ecs.Volume{ - { - /* ONLY supported when using EC2 launch type - DockerVolumeConfiguration: { - Autoprovision: nil, - Driver: nil, - DriverOpts: nil, - Labels: nil, - Scope: nil, - }, */ - /* Beta and ONLY supported when using EC2 launch type - EfsVolumeConfiguration: { - FileSystemId: nil, - RootDirectory: nil, - }, */ - /* Bind mount host volume - Host: { - SourcePath: - }, */ - Name: aws.String("MyVolume"), - }, - }, + Volumes: []ecs.TaskDefinition_Volume{}, }, nil } -func toCPU(service types.ServiceConfig) *string { +func toCPU(service types.ServiceConfig) string { // FIXME based on service's memory/cpu requirements, select the adequate Fargate CPU - v := "256" - return &v + return "256" } -func toMemory(service types.ServiceConfig) *string { +func toMemory(service types.ServiceConfig) string { // FIXME based on service's memory/cpu requirements, select the adequate Fargate CPU - v := "512" - return &v + return "512" } func toCPULimits(service types.ServiceConfig) (*int64, error) { @@ -153,45 +122,45 @@ func hasMemoryOrMemoryReservation(service types.ServiceConfig) bool { return false } -func toPlacementConstraints(deploy *types.DeployConfig) []*ecs.TaskDefinitionPlacementConstraint { +func toPlacementConstraints(deploy *types.DeployConfig) []ecs.TaskDefinition_TaskDefinitionPlacementConstraint { if deploy == nil || deploy.Placement.Constraints == nil || len(deploy.Placement.Constraints) == 0 { return nil } - pl := []*ecs.TaskDefinitionPlacementConstraint{} + pl := []ecs.TaskDefinition_TaskDefinitionPlacementConstraint{} for _, c := range deploy.Placement.Constraints { - pl = append(pl, &ecs.TaskDefinitionPlacementConstraint{ - Expression: toStringPtr(c), - Type: nil, + pl = append(pl, ecs.TaskDefinition_TaskDefinitionPlacementConstraint{ + Expression: c, + Type: "", }) } return pl } -func toPortMappings(ports []types.ServicePortConfig) []*ecs.PortMapping { +func toPortMappings(ports []types.ServicePortConfig) []ecs.TaskDefinition_PortMapping { if len(ports) == 0 { return nil } - m := []*ecs.PortMapping{} + m := []ecs.TaskDefinition_PortMapping{} for _, p := range ports { - m = append(m, &ecs.PortMapping{ - ContainerPort: uint32Toint64Ptr(p.Target), - HostPort: uint32Toint64Ptr(p.Published), - Protocol: toStringPtr(p.Protocol), + m = append(m, ecs.TaskDefinition_PortMapping{ + ContainerPort: int(p.Target), + HostPort: int(p.Published), + Protocol: p.Protocol, }) } return m } -func toUlimits(ulimits map[string]*types.UlimitsConfig) []*ecs.Ulimit { +func toUlimits(ulimits map[string]*types.UlimitsConfig) []ecs.TaskDefinition_Ulimit { if len(ulimits) == 0 { return nil } - u := []*ecs.Ulimit{} + u := []ecs.TaskDefinition_Ulimit{} for k, v := range ulimits { - u = append(u, &ecs.Ulimit{ - Name: toStringPtr(k), - SoftLimit: intToInt64Ptr(v.Soft), - HardLimit: intToInt64Ptr(v.Hard), + u = append(u, ecs.TaskDefinition_Ulimit{ + Name: k, + SoftLimit: v.Soft, + HardLimit: v.Hard, }) } return u @@ -209,79 +178,82 @@ func intToInt64Ptr(i int) *int64 { const Mb = 1024 * 1024 -func toMemoryLimits(deploy *types.DeployConfig) *int64 { +func toMemoryLimits(deploy *types.DeployConfig) int { if deploy == nil { - return nil + return 0 } res := deploy.Resources.Limits if res == nil { - return nil + return 0 } - v := int64(res.MemoryBytes) / Mb - return &v + v := int(res.MemoryBytes) / Mb + return v } -func toMemoryReservation(deploy *types.DeployConfig) *int64 { +func toMemoryReservation(deploy *types.DeployConfig) int { if deploy == nil { - return nil + return 0 } res := deploy.Resources.Reservations if res == nil { - return nil + return 0 } - v := int64(res.MemoryBytes) / Mb - return &v + v := int(res.MemoryBytes) / Mb + return v } -func toLinuxParameters(service types.ServiceConfig) *ecs.LinuxParameters { - return &ecs.LinuxParameters{ +func toLinuxParameters(service types.ServiceConfig) *ecs.TaskDefinition_LinuxParameters { + return &ecs.TaskDefinition_LinuxParameters{ Capabilities: toKernelCapabilities(service.CapAdd, service.CapDrop), Devices: nil, - InitProcessEnabled: service.Init, - MaxSwap: nil, + InitProcessEnabled: service.Init != nil && *service.Init, + MaxSwap: 0, // FIXME SharedMemorySize: service.ShmSize, - Swappiness: nil, + Swappiness: 0, Tmpfs: toTmpfs(service.Tmpfs), } } -func toTmpfs(tmpfs types.StringList) []*ecs.Tmpfs { +func toTmpfs(tmpfs types.StringList) []ecs.TaskDefinition_Tmpfs { if tmpfs == nil || len(tmpfs) == 0 { return nil } - o := []*ecs.Tmpfs{} - for _, t := range tmpfs { - path := t - o = append(o, &ecs.Tmpfs{ - ContainerPath: &path, + o := []ecs.TaskDefinition_Tmpfs{} + for _, path := range tmpfs { + o = append(o, ecs.TaskDefinition_Tmpfs{ + ContainerPath: path, MountOptions: nil, - Size: nil, + Size: 0, }) } return o } -func toKernelCapabilities(add []string, drop []string) *ecs.KernelCapabilities { +func toKernelCapabilities(add []string, drop []string) *ecs.TaskDefinition_KernelCapabilities { if len(add) == 0 && len(drop) == 0 { return nil } - return &ecs.KernelCapabilities{ - Add: toStringPtrSlice(add), - Drop: toStringPtrSlice(drop), + return &ecs.TaskDefinition_KernelCapabilities{ + Add: add, + Drop: drop, } } -func toHealthCheck(check *types.HealthCheckConfig) *ecs.HealthCheck { +func toHealthCheck(check *types.HealthCheckConfig) *ecs.TaskDefinition_HealthCheck { if check == nil { return nil } - return &ecs.HealthCheck{ - Command: toStringPtrSlice(check.Test), - Interval: durationToInt64Ptr(check.Interval), - Retries: uint64ToInt64Ptr(check.Retries), - StartPeriod: durationToInt64Ptr(check.StartPeriod), - Timeout: durationToInt64Ptr(check.Timeout), + retries := 0 + if check.Retries != nil { + retries = int(*check.Retries) + } + return &ecs.TaskDefinition_HealthCheck{ + Command: check.Test, + Interval: durationToInt(check.Interval), + Retries: retries, + StartPeriod: durationToInt(check.StartPeriod), + Timeout: durationToInt(check.Timeout), } } @@ -293,66 +265,44 @@ func uint64ToInt64Ptr(i *uint64) *int64 { return &v } -func durationToInt64Ptr(interval *types.Duration) *int64 { +func durationToInt(interval *types.Duration) int { if interval == nil { - return nil + return 0 } - v := int64(time.Duration(*interval).Seconds()) - return &v + v := int(time.Duration(*interval).Seconds()) + return v } -func toHostEntryPtr(hosts types.HostsList) []*ecs.HostEntry { +func toHostEntryPtr(hosts types.HostsList) []ecs.TaskDefinition_HostEntry { if hosts == nil || len(hosts) == 0 { return nil } - e := []*ecs.HostEntry{} + e := []ecs.TaskDefinition_HostEntry{} for _, h := range hosts { - host := h - e = append(e, &ecs.HostEntry{ - Hostname: &host, + parts := strings.SplitN(h, ":", 2) // FIXME this should be handled by compose-go + e = append(e, ecs.TaskDefinition_HostEntry{ + Hostname: parts[0], + IpAddress: parts[1], }) } return e } -func toKeyValuePairPtr(environment types.MappingWithEquals) []*ecs.KeyValuePair { +func toKeyValuePair(environment types.MappingWithEquals) []ecs.TaskDefinition_KeyValuePair { if environment == nil || len(environment) == 0 { return nil } - pairs := []*ecs.KeyValuePair{} + pairs := []ecs.TaskDefinition_KeyValuePair{} for k, v := range environment { name := k - value := v - pairs = append(pairs, &ecs.KeyValuePair{ - Name: &name, + var value string + if v != nil { + value = *v + } + pairs = append(pairs, ecs.TaskDefinition_KeyValuePair{ + Name: name, Value: value, }) } return pairs } - -func toStringPtr(s string) *string { - if s == "" { - return nil - } - return &s -} - -func toStringPtrSlice(s []string) []*string { - if len(s) == 0 { - return nil - } - v := []*string{} - for _, x := range s { - value := x - v = append(v, &value) - } - return v -} - -func toBoolPtr(b bool) *bool { - if !b { - return nil - } - return &b -} \ No newline at end of file From 0972776e6d469113570c84a2fa1f4c4c35919710 Mon Sep 17 00:00:00 2001 From: Nicolas De Loof <nicolas.deloof@gmail.com> Date: Tue, 21 Apr 2020 14:48:31 +0200 Subject: [PATCH 014/198] Ingress description to include service being exposed Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com> --- ecs/pkg/amazon/cloudformation.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ecs/pkg/amazon/cloudformation.go b/ecs/pkg/amazon/cloudformation.go index 0b3b0bc41..1865aa820 100644 --- a/ecs/pkg/amazon/cloudformation.go +++ b/ecs/pkg/amazon/cloudformation.go @@ -30,7 +30,7 @@ func (c client) Convert(project *compose.Project, loadBalancerArn *string) (*clo for _, port := range service.Ports { ingresses = append(ingresses, ec2.SecurityGroup_Ingress{ CidrIp: "0.0.0.0/0", - Description: fmt.Sprintf("%d/%s", port.Target, port.Protocol), + Description: fmt.Sprintf("%s:%d/%s", service.Name, port.Target, port.Protocol), FromPort: int(port.Target), IpProtocol: strings.ToUpper(port.Protocol), ToPort: int(port.Target), From 30fd37b6cabb96b99e5a2754342494e7f7ab25cf Mon Sep 17 00:00:00 2001 From: aiordache <anca.iordache@docker.com> Date: Wed, 22 Apr 2020 15:06:01 +0200 Subject: [PATCH 015/198] ecs cluster create Signed-off-by: aiordache <anca.iordache@docker.com> --- ecs/pkg/amazon/down.go | 2 +- ecs/pkg/amazon/ecs.go | 37 +++++++++++++++++++++++++++++++++++++ ecs/pkg/amazon/up.go | 7 +++++++ 3 files changed, 45 insertions(+), 1 deletion(-) diff --git a/ecs/pkg/amazon/down.go b/ecs/pkg/amazon/down.go index ac2648325..44f493515 100644 --- a/ecs/pkg/amazon/down.go +++ b/ecs/pkg/amazon/down.go @@ -12,7 +12,7 @@ func (c *client) ComposeDown(project *compose.Project, keepLoadBalancer bool) er if err != nil { return err } - + c.DeleteCluster() // TODO monitor progress return nil } diff --git a/ecs/pkg/amazon/ecs.go b/ecs/pkg/amazon/ecs.go index 423980f3d..232d1d60f 100644 --- a/ecs/pkg/amazon/ecs.go +++ b/ecs/pkg/amazon/ecs.go @@ -1,6 +1,9 @@ package amazon import ( + "errors" + "strings" + "github.com/aws/aws-sdk-go/service/ecs" "github.com/sirupsen/logrus" ) @@ -13,3 +16,37 @@ func (c client) RegisterTaskDefinition(task *ecs.RegisterTaskDefinitionInput) (* } return def.TaskDefinition.TaskDefinitionArn, err } + +func (c client) CreateCluster() (*string, error) { + logrus.Debug("Create cluster ", c.Cluster) + response, err := c.ECS.CreateCluster(&ecs.CreateClusterInput{ClusterName: &c.Cluster}) + if err != nil { + return nil, err + } + return response.Cluster.Status, nil +} + +func (c client) DeleteCluster() error { + logrus.Debug("Delete cluster ", c.Cluster) + response, err := c.ECS.DeleteCluster(&ecs.DeleteClusterInput{Cluster: &c.Cluster}) + if err != nil { + return err + } + if *response.Cluster.Status == "INACTIVE" { + return nil + } + return errors.New("Failed to delete cluster, status: " + *response.Cluster.Status) +} + +func (c client) ClusterExists() (bool, error) { + logrus.Debug("Check if cluster was already created: ", c.Cluster) + clusters, err := c.ECS.ListClusters(nil) + if err != nil { + return false, err + } + found := false + for _, arn := range clusters.ClusterArns { + found = found || strings.HasSuffix(*arn, "/"+c.Cluster) + } + return found, nil +} diff --git a/ecs/pkg/amazon/up.go b/ecs/pkg/amazon/up.go index 5f9b8750d..baa117933 100644 --- a/ecs/pkg/amazon/up.go +++ b/ecs/pkg/amazon/up.go @@ -10,6 +10,13 @@ import ( ) func (c *client) ComposeUp(project *compose.Project, loadBalancerArn *string) error { + ok, err := c.ClusterExists() + if err != nil { + return err + } + if !ok { + c.CreateCluster() + } template, err := c.Convert(project, loadBalancerArn) if err != nil { return err From 87f053d7104515848d706be5951dc8a17edfcfb5 Mon Sep 17 00:00:00 2001 From: Nicolas De Loof <nicolas.deloof@gmail.com> Date: Wed, 22 Apr 2020 15:37:39 +0200 Subject: [PATCH 016/198] Detect stack already exists This will later be used to switch to ChangeSet logic Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com> --- ecs/pkg/amazon/cloudformation.go | 1 - ecs/pkg/amazon/up.go | 10 ++++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/ecs/pkg/amazon/cloudformation.go b/ecs/pkg/amazon/cloudformation.go index 1865aa820..dd304ab48 100644 --- a/ecs/pkg/amazon/cloudformation.go +++ b/ecs/pkg/amazon/cloudformation.go @@ -14,7 +14,6 @@ import ( func (c client) Convert(project *compose.Project, loadBalancerArn *string) (*cloudformation.Template, error) { template := cloudformation.NewTemplate() - vpc, err := c.GetDefaultVPC() if err != nil { return nil, err diff --git a/ecs/pkg/amazon/up.go b/ecs/pkg/amazon/up.go index 5f9b8750d..efb78f2bd 100644 --- a/ecs/pkg/amazon/up.go +++ b/ecs/pkg/amazon/up.go @@ -10,6 +10,16 @@ import ( ) func (c *client) ComposeUp(project *compose.Project, loadBalancerArn *string) error { + stacks, err := c.CF.DescribeStacks(&cloudformation.DescribeStacksInput{ + StackName: aws.String(project.Name), + }) + if err != nil { + return err + } + if len(stacks.Stacks) > 0 { + return fmt.Errorf("we do not (yet) support updating an existing CloudFormation stack") + } + template, err := c.Convert(project, loadBalancerArn) if err != nil { return err From 5110cb6b851e109012b243a31f012a07653e5470 Mon Sep 17 00:00:00 2001 From: Nicolas De Loof <nicolas.deloof@gmail.com> Date: Wed, 22 Apr 2020 15:39:02 +0200 Subject: [PATCH 017/198] Basic architecture documentation Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com> --- ecs/README.md | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/ecs/README.md b/ecs/README.md index 128f6e0c1..9ec4ab694 100644 --- a/ecs/README.md +++ b/ecs/README.md @@ -1 +1,31 @@ # Docker CLI plugin for Amazon ECS + +## Architecture + +ECS plugin is a [Docker CLI plugin](https://docs.docker.com/engine/extend/cli_plugins/) +root command `ecs` require aws profile to get API credentials from `~/.aws/credentials` +as well as AWS region - those will later be stored in a docker context + +A `compose.yaml` is parsed and converted into a [CloudFormation](https://aws.amazon.com/cloudformation/) +template, which will create all resources in dependent order and cleanup on +`down` command or deployment failure. + +``` + +-----------------------------+ + | compose.yaml file | + +-----------------------------+ +- Load + +-----------------------------+ + | compose-go Model | + +-----------------------------+ +- Convert + +-----------------------------+ + | CloudFormation Template | + +-----------------------------+ +- Apply + +---------+ +------------+ + | AWS API | or | stack file | + +---------+ +------------+ +``` + +(if this sounds familiar, see [Kompose](https://github.com/kubernetes/kompose/blob/master/docs/architecture.md)) \ No newline at end of file From 55f2908c16dd503e1a72a750ccb9268adea18f11 Mon Sep 17 00:00:00 2001 From: aiordache <anca.iordache@docker.com> Date: Wed, 22 Apr 2020 16:42:52 +0200 Subject: [PATCH 018/198] wait for stack removal on cluster delete Signed-off-by: aiordache <anca.iordache@docker.com> --- ecs/go.mod | 4 +++- ecs/go.sum | 48 ++++++++++++++++++++++++++++++++++++++++++ ecs/pkg/amazon/down.go | 14 ++++++++++-- 3 files changed, 63 insertions(+), 3 deletions(-) diff --git a/ecs/go.mod b/ecs/go.mod index a8f444c52..88254c27c 100644 --- a/ecs/go.mod +++ b/ecs/go.mod @@ -38,7 +38,7 @@ require ( github.com/opencontainers/image-spec v1.0.1 // indirect github.com/sirupsen/logrus v1.5.0 github.com/spf13/cobra v0.0.5 - github.com/spf13/pflag v1.0.3 + 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/time v0.0.0-20191024005414-555d28b269f0 // indirect @@ -46,7 +46,9 @@ require ( gopkg.in/dancannon/gorethink.v3 v3.0.5 // indirect gopkg.in/fatih/pool.v2 v2.0.0 // indirect gopkg.in/gorethink/gorethink.v3 v3.0.5 // indirect + gotest.tools v2.2.0+incompatible gotest.tools/v3 v3.0.2 + k8s.io/apimachinery v0.18.2 vbom.ml/util v0.0.0-20180919145318-efcd4e0f9787 // indirect ) diff --git a/ecs/go.sum b/ecs/go.sum index dc5a0393a..31441e0de 100644 --- a/ecs/go.sum +++ b/ecs/go.sum @@ -11,6 +11,9 @@ github.com/Microsoft/go-winio v0.4.15-0.20190919025122-fc70bd9a86b5 h1:ygIc8M6tr github.com/Microsoft/go-winio v0.4.15-0.20190919025122-fc70bd9a86b5/go.mod h1:tTuCMEN+UleMWgg9dVx4Hu52b1bJo+59jBh3ajtinzw= github.com/Microsoft/hcsshim v0.8.7 h1:ptnOoufxGSzauVTsdE+wMYnCWA301PdoN4xg5oRdZpg= github.com/Microsoft/hcsshim v0.8.7/go.mod h1:OHd7sQqRFrYd3RmSgbgji+ctCwkbq2wbEYNSzOYtcBQ= +github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ= +github.com/PuerkitoBio/purell v1.0.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= +github.com/PuerkitoBio/urlesc v0.0.0-20160726150825-5bd2802263f2/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= github.com/Shopify/logrus-bugsnag v0.0.0-20171204204709-577dee27f20d h1:UrqY+r/OJnIp5u0s1SbQ8dVfLCZJsnvazdBP5hS4iRs= github.com/Shopify/logrus-bugsnag v0.0.0-20171204204709-577dee27f20d/go.mod h1:HI8ITrYtUY+O+ZhtlqUnD8+KwNPOyugEhfP9fdUIaEQ= github.com/agl/ed25519 v0.0.0-20170116200512-5312a6153412 h1:w1UutsfOrms1J05zt7ISrnJIXKzwaspym5BTKGx93EI= @@ -91,17 +94,27 @@ 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-20160708172513-aabc10ec26b7 h1:UhxFibDNY/bfvqU5CAUmr9zpesgbU6SWc8/B4mflAE4= github.com/docker/libtrust v0.0.0-20160708172513-aabc10ec26b7/go.mod h1:cyGadeNEkKy96OOhEzfZl+yxihPEzKnqJwvfuSUqbZE= +github.com/docker/spdystream v0.0.0-20160310174837-449fdfce4d96/go.mod h1:Qh8CwZgvJUkLughtfhJv5dyTYa91l1fOUCrgjqmcifM= github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= +github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc= +github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5 h1:Yzb9+7DPaBjB8zlTR87/ElzFsnQfuHnVUVqpZZIcV5Y= github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5/go.mod h1:a2zkGnVExMxdzMo3M0Hi/3sEU+cWnZpSni0O6/Yb/P0= +github.com/evanphx/json-patch v4.2.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/getsentry/raven-go v0.0.0-20180121060056-563b81fc02b7/go.mod h1:KungGk8q33+aIAZUIVWZDr2OfAEBsO49PX4NzFV5kcQ= +github.com/ghodss/yaml v0.0.0-20150909031657-73d445a93680/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= +github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas= +github.com/go-openapi/jsonpointer v0.0.0-20160704185906-46af16f9f7b1/go.mod h1:+35s3my2LFTysnkMfxsJBAMHj/DoqoB9knIWoYG/Vk0= +github.com/go-openapi/jsonreference v0.0.0-20160704190145-13c6e3589ad9/go.mod h1:W3Z9FmVs9qj+KR4zFKmDPGiLdk1D9Rlm7cyMvf57TTg= +github.com/go-openapi/spec v0.0.0-20160808142527-6aced65f8501/go.mod h1:J8+jY1nAiCcj+friV/PDoE1/3eeccG9LYBs0tYvLOWc= +github.com/go-openapi/swag v0.0.0-20160704191624-1d0bd113de87/go.mod h1:DXUve3Dpr1UfpPtxFw+EFuQ41HhCWZfha5jSVRG7C7I= github.com/go-sql-driver/mysql v1.3.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= github.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs= @@ -118,7 +131,9 @@ github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe h1:lXe2qZdvpiX5WZ github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/protobuf v0.0.0-20161109072736-4bd1920723d7/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs= @@ -130,6 +145,10 @@ github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMyw github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/gnostic v0.0.0-20170729233727-0c5108395e2d/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY= +github.com/googleapis/gnostic v0.1.0/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY= github.com/gorilla/mux v1.7.3 h1:gnP5JzjVOuiZD07fKKToCAOjS0yOpj/qPETTXCCS6hw= github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed h1:5upAirOpQc1Q53c0bnx2ufif5kANL7bfZWcc6VJWJd8= @@ -159,6 +178,7 @@ github.com/jmhodges/clock v0.0.0-20160418191101-880ee4c33548/go.mod h1:hGT6jSUVz github.com/jmoiron/sqlx v0.0.0-20180124204410-05cef0741ade/go.mod h1:IiEW3SEiiErVyFdH8NTuWjSifiEQKUoyK3LNqr2kCHU= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.8/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0 h1:iQTw/8FWTuc7uiaSepXwyf3o52HaUYcV+Tu66S3F5GA= github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0/go.mod h1:1NbS8ALrpOvjt0rHPNLyCIeMtbizbir8U//inJ+zuB8= @@ -182,6 +202,7 @@ github.com/lib/pq v1.3.0 h1:/qkRGz8zljWiDcFvgpwUpwIAPu3r07TDvs3Rws+o/pU= github.com/lib/pq v1.3.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/magiconair/properties v1.8.0 h1:LLgXmsheXeRoUOBOjtwPQCWIYqM/LU1ayDtDePerRcY= github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= +github.com/mailru/easyjson v0.0.0-20160728113105-d5b7844b561a/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mattn/go-shellwords v1.0.10 h1:Y7Xqm8piKOO3v10Thp7Z36h4FYFjt5xB//6XvOrs2Gw= github.com/mattn/go-shellwords v1.0.10/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y= github.com/mattn/go-sqlite3 v1.10.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= @@ -204,12 +225,17 @@ github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3Rllmb github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= github.com/mreiferson/go-httpclient v0.0.0-20160630210159-31f0106b4474/go.mod h1:OQA4XLvDbMgS8P0CevmM4m9Q3Jq4phKUzcocxuGJ5m8= +github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= github.com/nkovacs/streamquote v0.0.0-20170412213628-49af9bddb229/go.mod h1:0aYXnNPJ8l7uZxf45rWW1a/uME32OF0rhiYGNQ2oF2E= +github.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 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.10.1 h1:q/mM8GF/n0shIN8SaAZ0V+jnLPzen6WIVZdiwrRlMlo= github.com/onsi/ginkgo v1.10.1/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.11.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= github.com/onsi/gomega v1.2.0/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= github.com/onsi/gomega v1.7.0 h1:XPnZz8VVBHjVsy1vzJmRwIcSwiUO+JFfrv/xGiigmME= github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= @@ -268,9 +294,12 @@ github.com/spf13/cobra v0.0.5 h1:f0B+LkLX6DtmRH1isoNA9VTtNUK9K8xYd28JNNfOv/s= github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU= 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 v0.0.0-20170130214245-9ff6c6923cff/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.1-0.20171106142849-4c012f6dcd95/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg= 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= github.com/spf13/viper v1.3.2 h1:VUFqw5KcqRf7i70GOzW7N+Q7+gxVBkSSqiXB12+JQ4M= github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= @@ -325,6 +354,7 @@ golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTk golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/net v0.0.0-20170114055629-f2499483f923/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -337,6 +367,7 @@ golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09 h1:KaQtG+aDELoNmXYas3TVkGNYR golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190613194153-d28f0bde5980 h1:dfGZHvZk057jK2MCeWus/TowKpJ8y4AmooUzdBSR9GU= golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191004110552-13f9640d40b9/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b h1:0mm1VjtFUOIlE1SbDlwjYaDxZVDP2S5ou6y0gSgXHu8= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= @@ -345,6 +376,7 @@ golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJ 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/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20170830134202-bb24a47a89ea/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= @@ -360,6 +392,9 @@ 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/text v0.0.0-20160726164857-2910a502d2bf/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= @@ -368,6 +403,7 @@ golang.org/x/time v0.0.0-20191024005414-555d28b269f0 h1:/5xXl8Y5W96D+TtHSlonuFqG golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20181011042414-1f849cf54d09/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= @@ -402,6 +438,7 @@ gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMy gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2/go.mod h1:Xk6kEKp8OKb+X14hQBKWaSkCsqBpgog8nAV2xsGOxlo= gopkg.in/gorethink/gorethink.v3 v3.0.5 h1:e2Uc/Xe+hpcVQFsj6MuHlYog3r0JYpnTzwDj/y2O4MU= gopkg.in/gorethink/gorethink.v3 v3.0.5/go.mod h1:+3yIIHJUGMBK+wyPH+iN5TP+88ikFDfZdqTlK3Y9q8I= +gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= @@ -414,6 +451,17 @@ gotest.tools/v3 v3.0.2 h1:kG1BFyqVHuQoVQiR1bWGnfz/fmHvvuiSPIV7rvl360E= gotest.tools/v3 v3.0.2/go.mod h1:3SzNCllyD9/Y+b5r9JIKQ474KzkZyqLqEfYqMsX94Bk= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +k8s.io/apimachinery v0.18.2 h1:44CmtbmkzVDAhCpRVSiP2R5PPrC2RtlIv/MoB8xpdRA= +k8s.io/apimachinery v0.18.2/go.mod h1:9SnR/e11v5IbyPCGbvJViimtJ0SwHG4nfZFjU77ftcA= +k8s.io/gengo v0.0.0-20190128074634-0689ccc1d7d6/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0= +k8s.io/klog v0.0.0-20181102134211-b9b56d5dfc92/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk= +k8s.io/klog v1.0.0 h1:Pt+yjF5aB1xDSVbau4VsWe+dQNzA0qv1LlXdC2dF6Q8= +k8s.io/klog v1.0.0/go.mod h1:4Bi6QPql/J/LkTDqv7R/cd3hPo4k2DG6Ptcz060Ez5I= +k8s.io/kube-openapi v0.0.0-20200121204235-bf4fb3bd569c/go.mod h1:GRQhZsXIAJ1xR0C9bd8UpWHZ5plfAS9fzPjJuQ6JL3E= k8s.io/kubernetes v1.13.0/go.mod h1:ocZa8+6APFNC2tX1DZASIbocyYT5jHzqFVsY5aoB7Jk= +sigs.k8s.io/structured-merge-diff/v3 v3.0.0-20200116222232-67a7b8c61874/go.mod h1:PlARxl6Hbt/+BC80dRLi1qAmnMqwqDg62YvvVkZjemw= +sigs.k8s.io/structured-merge-diff/v3 v3.0.0/go.mod h1:PlARxl6Hbt/+BC80dRLi1qAmnMqwqDg62YvvVkZjemw= +sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o= +sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc= vbom.ml/util v0.0.0-20180919145318-efcd4e0f9787 h1:O69FD9pJA4WUZlEwYatBEEkRWKQ5cKodWpdKTrCS/iQ= vbom.ml/util v0.0.0-20180919145318-efcd4e0f9787/go.mod h1:so/NYdZXCz+E3ZpW0uAoCj6uzU2+8OWDFv/HxUSs7kI= diff --git a/ecs/pkg/amazon/down.go b/ecs/pkg/amazon/down.go index 44f493515..4976aef14 100644 --- a/ecs/pkg/amazon/down.go +++ b/ecs/pkg/amazon/down.go @@ -1,7 +1,10 @@ package amazon import ( + "fmt" + "github.com/aws/aws-sdk-go/service/cloudformation" + cf "github.com/aws/aws-sdk-go/service/cloudformation" "github.com/docker/ecs-plugin/pkg/compose" ) @@ -12,7 +15,14 @@ func (c *client) ComposeDown(project *compose.Project, keepLoadBalancer bool) er if err != nil { return err } - c.DeleteCluster() - // TODO monitor progress + fmt.Printf("Delete stack ") + if err = c.CF.WaitUntilStackDeleteComplete(&cf.DescribeStacksInput{StackName: &project.Name}); err != nil { + return err + } + fmt.Printf("... done.\nDelete cluster %s", c.Cluster) + if err = c.DeleteCluster(); err != nil { + return err + } + fmt.Printf("... done. \n") return nil } From 3c9905c4747560dd4dd4e7b4d4f34eade8c6e007 Mon Sep 17 00:00:00 2001 From: aiordache <anca.iordache@docker.com> Date: Thu, 23 Apr 2020 09:45:50 +0200 Subject: [PATCH 019/198] tidy up go mod Signed-off-by: aiordache <anca.iordache@docker.com> --- ecs/go.mod | 4 ++-- ecs/go.sum | 44 +------------------------------------------- 2 files changed, 3 insertions(+), 45 deletions(-) diff --git a/ecs/go.mod b/ecs/go.mod index 88254c27c..0bc80e999 100644 --- a/ecs/go.mod +++ b/ecs/go.mod @@ -35,20 +35,20 @@ require ( github.com/mattn/go-sqlite3 v2.0.3+incompatible // indirect github.com/miekg/pkcs11 v1.0.3 // indirect github.com/morikuni/aec v1.0.0 // indirect + github.com/onsi/ginkgo v1.11.0 // indirect github.com/opencontainers/image-spec v1.0.1 // indirect github.com/sirupsen/logrus v1.5.0 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/sys v0.0.0-20191022100944-742c48ecaeb7 // indirect 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 gopkg.in/fatih/pool.v2 v2.0.0 // indirect gopkg.in/gorethink/gorethink.v3 v3.0.5 // indirect - gotest.tools v2.2.0+incompatible gotest.tools/v3 v3.0.2 - k8s.io/apimachinery v0.18.2 vbom.ml/util v0.0.0-20180919145318-efcd4e0f9787 // indirect ) diff --git a/ecs/go.sum b/ecs/go.sum index 31441e0de..ee07c6080 100644 --- a/ecs/go.sum +++ b/ecs/go.sum @@ -11,9 +11,6 @@ github.com/Microsoft/go-winio v0.4.15-0.20190919025122-fc70bd9a86b5 h1:ygIc8M6tr github.com/Microsoft/go-winio v0.4.15-0.20190919025122-fc70bd9a86b5/go.mod h1:tTuCMEN+UleMWgg9dVx4Hu52b1bJo+59jBh3ajtinzw= github.com/Microsoft/hcsshim v0.8.7 h1:ptnOoufxGSzauVTsdE+wMYnCWA301PdoN4xg5oRdZpg= github.com/Microsoft/hcsshim v0.8.7/go.mod h1:OHd7sQqRFrYd3RmSgbgji+ctCwkbq2wbEYNSzOYtcBQ= -github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ= -github.com/PuerkitoBio/purell v1.0.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= -github.com/PuerkitoBio/urlesc v0.0.0-20160726150825-5bd2802263f2/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= github.com/Shopify/logrus-bugsnag v0.0.0-20171204204709-577dee27f20d h1:UrqY+r/OJnIp5u0s1SbQ8dVfLCZJsnvazdBP5hS4iRs= github.com/Shopify/logrus-bugsnag v0.0.0-20171204204709-577dee27f20d/go.mod h1:HI8ITrYtUY+O+ZhtlqUnD8+KwNPOyugEhfP9fdUIaEQ= github.com/agl/ed25519 v0.0.0-20170116200512-5312a6153412 h1:w1UutsfOrms1J05zt7ISrnJIXKzwaspym5BTKGx93EI= @@ -94,27 +91,17 @@ 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-20160708172513-aabc10ec26b7 h1:UhxFibDNY/bfvqU5CAUmr9zpesgbU6SWc8/B4mflAE4= github.com/docker/libtrust v0.0.0-20160708172513-aabc10ec26b7/go.mod h1:cyGadeNEkKy96OOhEzfZl+yxihPEzKnqJwvfuSUqbZE= -github.com/docker/spdystream v0.0.0-20160310174837-449fdfce4d96/go.mod h1:Qh8CwZgvJUkLughtfhJv5dyTYa91l1fOUCrgjqmcifM= github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= -github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc= -github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5 h1:Yzb9+7DPaBjB8zlTR87/ElzFsnQfuHnVUVqpZZIcV5Y= github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5/go.mod h1:a2zkGnVExMxdzMo3M0Hi/3sEU+cWnZpSni0O6/Yb/P0= -github.com/evanphx/json-patch v4.2.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/getsentry/raven-go v0.0.0-20180121060056-563b81fc02b7/go.mod h1:KungGk8q33+aIAZUIVWZDr2OfAEBsO49PX4NzFV5kcQ= -github.com/ghodss/yaml v0.0.0-20150909031657-73d445a93680/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= -github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas= -github.com/go-openapi/jsonpointer v0.0.0-20160704185906-46af16f9f7b1/go.mod h1:+35s3my2LFTysnkMfxsJBAMHj/DoqoB9knIWoYG/Vk0= -github.com/go-openapi/jsonreference v0.0.0-20160704190145-13c6e3589ad9/go.mod h1:W3Z9FmVs9qj+KR4zFKmDPGiLdk1D9Rlm7cyMvf57TTg= -github.com/go-openapi/spec v0.0.0-20160808142527-6aced65f8501/go.mod h1:J8+jY1nAiCcj+friV/PDoE1/3eeccG9LYBs0tYvLOWc= -github.com/go-openapi/swag v0.0.0-20160704191624-1d0bd113de87/go.mod h1:DXUve3Dpr1UfpPtxFw+EFuQ41HhCWZfha5jSVRG7C7I= github.com/go-sql-driver/mysql v1.3.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= github.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs= @@ -131,9 +118,7 @@ github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe h1:lXe2qZdvpiX5WZ github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= -github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= -github.com/golang/protobuf v0.0.0-20161109072736-4bd1920723d7/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs= @@ -145,10 +130,6 @@ github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMyw github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/googleapis/gnostic v0.0.0-20170729233727-0c5108395e2d/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY= -github.com/googleapis/gnostic v0.1.0/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY= github.com/gorilla/mux v1.7.3 h1:gnP5JzjVOuiZD07fKKToCAOjS0yOpj/qPETTXCCS6hw= github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed h1:5upAirOpQc1Q53c0bnx2ufif5kANL7bfZWcc6VJWJd8= @@ -178,7 +159,6 @@ github.com/jmhodges/clock v0.0.0-20160418191101-880ee4c33548/go.mod h1:hGT6jSUVz github.com/jmoiron/sqlx v0.0.0-20180124204410-05cef0741ade/go.mod h1:IiEW3SEiiErVyFdH8NTuWjSifiEQKUoyK3LNqr2kCHU= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= -github.com/json-iterator/go v1.1.8/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0 h1:iQTw/8FWTuc7uiaSepXwyf3o52HaUYcV+Tu66S3F5GA= github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0/go.mod h1:1NbS8ALrpOvjt0rHPNLyCIeMtbizbir8U//inJ+zuB8= @@ -202,7 +182,6 @@ github.com/lib/pq v1.3.0 h1:/qkRGz8zljWiDcFvgpwUpwIAPu3r07TDvs3Rws+o/pU= github.com/lib/pq v1.3.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/magiconair/properties v1.8.0 h1:LLgXmsheXeRoUOBOjtwPQCWIYqM/LU1ayDtDePerRcY= github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= -github.com/mailru/easyjson v0.0.0-20160728113105-d5b7844b561a/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mattn/go-shellwords v1.0.10 h1:Y7Xqm8piKOO3v10Thp7Z36h4FYFjt5xB//6XvOrs2Gw= github.com/mattn/go-shellwords v1.0.10/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y= github.com/mattn/go-sqlite3 v1.10.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= @@ -225,17 +204,14 @@ github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3Rllmb github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= github.com/mreiferson/go-httpclient v0.0.0-20160630210159-31f0106b4474/go.mod h1:OQA4XLvDbMgS8P0CevmM4m9Q3Jq4phKUzcocxuGJ5m8= -github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= -github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= github.com/nkovacs/streamquote v0.0.0-20170412213628-49af9bddb229/go.mod h1:0aYXnNPJ8l7uZxf45rWW1a/uME32OF0rhiYGNQ2oF2E= -github.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 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.10.1 h1:q/mM8GF/n0shIN8SaAZ0V+jnLPzen6WIVZdiwrRlMlo= github.com/onsi/ginkgo v1.10.1/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.11.0 h1:JAKSXpt1YjtLA7YpPiqO9ss6sNXEsPfSGdwN0UHqzrw= github.com/onsi/ginkgo v1.11.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= -github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= github.com/onsi/gomega v1.2.0/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= github.com/onsi/gomega v1.7.0 h1:XPnZz8VVBHjVsy1vzJmRwIcSwiUO+JFfrv/xGiigmME= github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= @@ -294,7 +270,6 @@ github.com/spf13/cobra v0.0.5 h1:f0B+LkLX6DtmRH1isoNA9VTtNUK9K8xYd28JNNfOv/s= github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU= 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 v0.0.0-20170130214245-9ff6c6923cff/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.1-0.20171106142849-4c012f6dcd95/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg= github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= @@ -354,7 +329,6 @@ golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTk golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/net v0.0.0-20170114055629-f2499483f923/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -367,7 +341,6 @@ golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09 h1:KaQtG+aDELoNmXYas3TVkGNYR golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190613194153-d28f0bde5980 h1:dfGZHvZk057jK2MCeWus/TowKpJ8y4AmooUzdBSR9GU= golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20191004110552-13f9640d40b9/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b h1:0mm1VjtFUOIlE1SbDlwjYaDxZVDP2S5ou6y0gSgXHu8= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= @@ -376,7 +349,6 @@ golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJ 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/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sys v0.0.0-20170830134202-bb24a47a89ea/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= @@ -394,7 +366,6 @@ golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3 h1:7TYNF4UdlohbFwpNH04CoPMp1 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/text v0.0.0-20160726164857-2910a502d2bf/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= @@ -403,7 +374,6 @@ golang.org/x/time v0.0.0-20191024005414-555d28b269f0 h1:/5xXl8Y5W96D+TtHSlonuFqG golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20181011042414-1f849cf54d09/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= @@ -438,7 +408,6 @@ gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMy gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2/go.mod h1:Xk6kEKp8OKb+X14hQBKWaSkCsqBpgog8nAV2xsGOxlo= gopkg.in/gorethink/gorethink.v3 v3.0.5 h1:e2Uc/Xe+hpcVQFsj6MuHlYog3r0JYpnTzwDj/y2O4MU= gopkg.in/gorethink/gorethink.v3 v3.0.5/go.mod h1:+3yIIHJUGMBK+wyPH+iN5TP+88ikFDfZdqTlK3Y9q8I= -gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= @@ -451,17 +420,6 @@ gotest.tools/v3 v3.0.2 h1:kG1BFyqVHuQoVQiR1bWGnfz/fmHvvuiSPIV7rvl360E= gotest.tools/v3 v3.0.2/go.mod h1:3SzNCllyD9/Y+b5r9JIKQ474KzkZyqLqEfYqMsX94Bk= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -k8s.io/apimachinery v0.18.2 h1:44CmtbmkzVDAhCpRVSiP2R5PPrC2RtlIv/MoB8xpdRA= -k8s.io/apimachinery v0.18.2/go.mod h1:9SnR/e11v5IbyPCGbvJViimtJ0SwHG4nfZFjU77ftcA= -k8s.io/gengo v0.0.0-20190128074634-0689ccc1d7d6/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0= -k8s.io/klog v0.0.0-20181102134211-b9b56d5dfc92/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk= -k8s.io/klog v1.0.0 h1:Pt+yjF5aB1xDSVbau4VsWe+dQNzA0qv1LlXdC2dF6Q8= -k8s.io/klog v1.0.0/go.mod h1:4Bi6QPql/J/LkTDqv7R/cd3hPo4k2DG6Ptcz060Ez5I= -k8s.io/kube-openapi v0.0.0-20200121204235-bf4fb3bd569c/go.mod h1:GRQhZsXIAJ1xR0C9bd8UpWHZ5plfAS9fzPjJuQ6JL3E= k8s.io/kubernetes v1.13.0/go.mod h1:ocZa8+6APFNC2tX1DZASIbocyYT5jHzqFVsY5aoB7Jk= -sigs.k8s.io/structured-merge-diff/v3 v3.0.0-20200116222232-67a7b8c61874/go.mod h1:PlARxl6Hbt/+BC80dRLi1qAmnMqwqDg62YvvVkZjemw= -sigs.k8s.io/structured-merge-diff/v3 v3.0.0/go.mod h1:PlARxl6Hbt/+BC80dRLi1qAmnMqwqDg62YvvVkZjemw= -sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o= -sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc= vbom.ml/util v0.0.0-20180919145318-efcd4e0f9787 h1:O69FD9pJA4WUZlEwYatBEEkRWKQ5cKodWpdKTrCS/iQ= vbom.ml/util v0.0.0-20180919145318-efcd4e0f9787/go.mod h1:so/NYdZXCz+E3ZpW0uAoCj6uzU2+8OWDFv/HxUSs7kI= From 48096eeed8b2e0fd25266f5d77a9565073c87e8e Mon Sep 17 00:00:00 2001 From: Nicolas De Loof <nicolas.deloof@gmail.com> Date: Thu, 23 Apr 2020 11:19:59 +0200 Subject: [PATCH 020/198] DescribeStacks fail with error if stack does not exists Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com> --- ecs/pkg/amazon/up.go | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/ecs/pkg/amazon/up.go b/ecs/pkg/amazon/up.go index efb78f2bd..4cd521158 100644 --- a/ecs/pkg/amazon/up.go +++ b/ecs/pkg/amazon/up.go @@ -10,13 +10,11 @@ import ( ) func (c *client) ComposeUp(project *compose.Project, loadBalancerArn *string) error { - stacks, err := c.CF.DescribeStacks(&cloudformation.DescribeStacksInput{ + _, err := c.CF.DescribeStacks(&cloudformation.DescribeStacksInput{ StackName: aws.String(project.Name), }) - if err != nil { - return err - } - if len(stacks.Stacks) > 0 { + if err == nil { + // FIXME no ErrNotFound err type here return fmt.Errorf("we do not (yet) support updating an existing CloudFormation stack") } From 3d7e062215924e6e19a2af5927aab14ede2a9fbd Mon Sep 17 00:00:00 2001 From: aiordache <anca.iordache@docker.com> Date: Thu, 23 Apr 2020 14:50:18 +0200 Subject: [PATCH 021/198] add delete-cluster flag on down cmd Signed-off-by: aiordache <anca.iordache@docker.com> --- ecs/cmd/main/main.go | 4 +++- ecs/pkg/amazon/down.go | 10 ++++++++-- ecs/pkg/compose/api.go | 2 +- 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/ecs/cmd/main/main.go b/ecs/cmd/main/main.go index cde9be3bd..ab365bd40 100644 --- a/ecs/cmd/main/main.go +++ b/ecs/cmd/main/main.go @@ -133,6 +133,7 @@ func UpCommand(clusteropts *clusterOptions, projectOpts *compose.ProjectOptions) type downOptions struct { KeepLoadBalancer bool + DeleteCluster bool } func DownCommand(clusteropts *clusterOptions, projectOpts *compose.ProjectOptions) *cobra.Command { @@ -144,9 +145,10 @@ func DownCommand(clusteropts *clusterOptions, projectOpts *compose.ProjectOption if err != nil { return err } - return client.ComposeDown(project, opts.KeepLoadBalancer) + return client.ComposeDown(project, opts.KeepLoadBalancer, opts.DeleteCluster) }), } cmd.Flags().BoolVar(&opts.KeepLoadBalancer, "keep-load-balancer", false, "Keep Load Balancer for further use") + cmd.Flags().BoolVar(&opts.DeleteCluster, "delete-cluster", false, "Delete cluster") return cmd } diff --git a/ecs/pkg/amazon/down.go b/ecs/pkg/amazon/down.go index 4976aef14..a7b44892e 100644 --- a/ecs/pkg/amazon/down.go +++ b/ecs/pkg/amazon/down.go @@ -8,7 +8,7 @@ import ( "github.com/docker/ecs-plugin/pkg/compose" ) -func (c *client) ComposeDown(project *compose.Project, keepLoadBalancer bool) error { +func (c *client) ComposeDown(project *compose.Project, keepLoadBalancer, deleteCluster bool) error { _, err := c.CF.DeleteStack(&cloudformation.DeleteStackInput{ StackName: &project.Name, }) @@ -19,7 +19,13 @@ func (c *client) ComposeDown(project *compose.Project, keepLoadBalancer bool) er if err = c.CF.WaitUntilStackDeleteComplete(&cf.DescribeStacksInput{StackName: &project.Name}); err != nil { return err } - fmt.Printf("... done.\nDelete cluster %s", c.Cluster) + fmt.Printf("... done.\n") + + if !deleteCluster { + return nil + } + + fmt.Printf("Delete cluster %s", c.Cluster) if err = c.DeleteCluster(); err != nil { return err } diff --git a/ecs/pkg/compose/api.go b/ecs/pkg/compose/api.go index 50f62ccf7..4218de767 100644 --- a/ecs/pkg/compose/api.go +++ b/ecs/pkg/compose/api.go @@ -5,5 +5,5 @@ import "github.com/awslabs/goformation/v4/cloudformation" type API interface { Convert(project *Project, loadBalancerArn *string) (*cloudformation.Template, error) ComposeUp(project *Project, loadBalancerArn *string) error - ComposeDown(project *Project, keepLoadBalancer bool) error + ComposeDown(project *Project, keepLoadBalancer, deleteCluster bool) error } From ea6d35a9274a2ee24ca698f2956ba89775825661 Mon Sep 17 00:00:00 2001 From: Nicolas De Loof <nicolas.deloof@gmail.com> Date: Thu, 23 Apr 2020 16:19:47 +0200 Subject: [PATCH 022/198] Fix minor issue after merge conflit resolution Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com> --- ecs/pkg/amazon/up.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ecs/pkg/amazon/up.go b/ecs/pkg/amazon/up.go index 8b6c6da66..2dbfd3062 100644 --- a/ecs/pkg/amazon/up.go +++ b/ecs/pkg/amazon/up.go @@ -17,7 +17,7 @@ func (c *client) ComposeUp(project *compose.Project, loadBalancerArn *string) er if !ok { c.CreateCluster() } - _, err := c.CF.DescribeStacks(&cloudformation.DescribeStacksInput{ + _, err = c.CF.DescribeStacks(&cloudformation.DescribeStacksInput{ StackName: aws.String(project.Name), }) if err == nil { From f8bf0078aa165460ceac3181c397486fbd92f15f Mon Sep 17 00:00:00 2001 From: Nicolas De Loof <nicolas.deloof@gmail.com> Date: Fri, 24 Apr 2020 10:13:38 +0200 Subject: [PATCH 023/198] Use DescribeCluster as ListCluster is a Paginated API Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com> --- ecs/pkg/amazon/ecs.go | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/ecs/pkg/amazon/ecs.go b/ecs/pkg/amazon/ecs.go index 232d1d60f..03ca92f4d 100644 --- a/ecs/pkg/amazon/ecs.go +++ b/ecs/pkg/amazon/ecs.go @@ -2,8 +2,7 @@ package amazon import ( "errors" - "strings" - + "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/ecs" "github.com/sirupsen/logrus" ) @@ -40,13 +39,11 @@ func (c client) DeleteCluster() error { func (c client) ClusterExists() (bool, error) { logrus.Debug("Check if cluster was already created: ", c.Cluster) - clusters, err := c.ECS.ListClusters(nil) + clusters, err := c.ECS.DescribeClusters(&ecs.DescribeClustersInput{ + Clusters: []*string{aws.String(c.Cluster)}, + }) if err != nil { return false, err } - found := false - for _, arn := range clusters.ClusterArns { - found = found || strings.HasSuffix(*arn, "/"+c.Cluster) - } - return found, nil + return len(clusters.Clusters) > 0, nil } From d612a4ab894c43178713fa1ef6a75ec10091e11e Mon Sep 17 00:00:00 2001 From: aiordache <anca.iordache@docker.com> Date: Fri, 24 Apr 2020 17:25:29 +0200 Subject: [PATCH 024/198] Project name parameter as alternative to compose file on down Signed-off-by: aiordache <anca.iordache@docker.com> --- ecs/cmd/main/main.go | 20 +++++++++++++++++--- ecs/pkg/amazon/down.go | 7 +++---- ecs/pkg/compose/api.go | 2 +- ecs/pkg/compose/opts.go | 2 +- ecs/pkg/compose/project.go | 2 +- 5 files changed, 23 insertions(+), 10 deletions(-) diff --git a/ecs/cmd/main/main.go b/ecs/cmd/main/main.go index ab365bd40..7b3d1d6aa 100644 --- a/ecs/cmd/main/main.go +++ b/ecs/cmd/main/main.go @@ -140,13 +140,27 @@ func DownCommand(clusteropts *clusterOptions, projectOpts *compose.ProjectOption opts := downOptions{} cmd := &cobra.Command{ Use: "down", - RunE: compose.WithProject(projectOpts, func(project *compose.Project, args []string) error { + RunE: func(cmd *cobra.Command, args []string) error { client, err := amazon.NewClient(clusteropts.profile, clusteropts.cluster, clusteropts.region) if err != nil { return err } - return client.ComposeDown(project, opts.KeepLoadBalancer, opts.DeleteCluster) - }), + if len(args) == 0 { + project, err := compose.ProjectFromOptions(projectOpts) + if err != nil { + return err + } + return client.ComposeDown(&project.Name, opts.KeepLoadBalancer, opts.DeleteCluster) + } + // project names passed as parameters + for _, name := range args { + err := client.ComposeDown(&name, opts.KeepLoadBalancer, opts.DeleteCluster) + if err != nil { + return err + } + } + return nil + }, } cmd.Flags().BoolVar(&opts.KeepLoadBalancer, "keep-load-balancer", false, "Keep Load Balancer for further use") cmd.Flags().BoolVar(&opts.DeleteCluster, "delete-cluster", false, "Delete cluster") diff --git a/ecs/pkg/amazon/down.go b/ecs/pkg/amazon/down.go index a7b44892e..4dfdff4a3 100644 --- a/ecs/pkg/amazon/down.go +++ b/ecs/pkg/amazon/down.go @@ -5,18 +5,17 @@ import ( "github.com/aws/aws-sdk-go/service/cloudformation" cf "github.com/aws/aws-sdk-go/service/cloudformation" - "github.com/docker/ecs-plugin/pkg/compose" ) -func (c *client) ComposeDown(project *compose.Project, keepLoadBalancer, deleteCluster bool) error { +func (c *client) ComposeDown(projectName *string, keepLoadBalancer, deleteCluster bool) error { _, err := c.CF.DeleteStack(&cloudformation.DeleteStackInput{ - StackName: &project.Name, + StackName: projectName, }) if err != nil { return err } fmt.Printf("Delete stack ") - if err = c.CF.WaitUntilStackDeleteComplete(&cf.DescribeStacksInput{StackName: &project.Name}); err != nil { + if err = c.CF.WaitUntilStackDeleteComplete(&cf.DescribeStacksInput{StackName: projectName}); err != nil { return err } fmt.Printf("... done.\n") diff --git a/ecs/pkg/compose/api.go b/ecs/pkg/compose/api.go index 4218de767..b30750824 100644 --- a/ecs/pkg/compose/api.go +++ b/ecs/pkg/compose/api.go @@ -5,5 +5,5 @@ import "github.com/awslabs/goformation/v4/cloudformation" type API interface { Convert(project *Project, loadBalancerArn *string) (*cloudformation.Template, error) ComposeUp(project *Project, loadBalancerArn *string) error - ComposeDown(project *Project, keepLoadBalancer, deleteCluster bool) error + ComposeDown(projectName *string, keepLoadBalancer, deleteCluster bool) error } diff --git a/ecs/pkg/compose/opts.go b/ecs/pkg/compose/opts.go index 390ccaa69..9e9bdeee0 100644 --- a/ecs/pkg/compose/opts.go +++ b/ecs/pkg/compose/opts.go @@ -20,7 +20,7 @@ type ProjectFunc func(project *Project, args []string) error // WithProject wrap a ProjectFunc into a cobra command func WithProject(options *ProjectOptions, f ProjectFunc) func(cmd *cobra.Command, args []string) error { return func(cmd *cobra.Command, args []string) error { - project, err := projectFromOptions(options) + project, err := ProjectFromOptions(options) if err != nil { return err } diff --git a/ecs/pkg/compose/project.go b/ecs/pkg/compose/project.go index 6f2ef98e9..e17e22527 100644 --- a/ecs/pkg/compose/project.go +++ b/ecs/pkg/compose/project.go @@ -34,7 +34,7 @@ func NewProject(config types.ConfigDetails, name string) (*Project, error) { } // projectFromOptions load a compose project based on command line options -func projectFromOptions(options *ProjectOptions) (*Project, error) { +func ProjectFromOptions(options *ProjectOptions) (*Project, error) { configPath, err := getConfigPathFromOptions(options) if err != nil { return nil, err From 52440a473201d5e8391f301b1161f2f80265a14b Mon Sep 17 00:00:00 2001 From: Nicolas De Loof <nicolas.deloof@gmail.com> Date: Tue, 28 Apr 2020 10:47:03 +0200 Subject: [PATCH 025/198] Setup Github Action for CI close #1 Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com> --- ecs/Makefile | 5 ++++- ecs/golangci.yaml | 12 ++++++++++++ ecs/pkg/amazon/ecs.go | 1 + 3 files changed, 17 insertions(+), 1 deletion(-) create mode 100644 ecs/golangci.yaml diff --git a/ecs/Makefile b/ecs/Makefile index d8cc4a504..6ff9437d7 100644 --- a/ecs/Makefile +++ b/ecs/Makefile @@ -7,4 +7,7 @@ test: ## Run tests dev: build ln -f -s "${PWD}/dist/docker-ecs" "${HOME}/.docker/cli-plugins/docker-ecs" -.PHONY: build test dev \ No newline at end of file +lint: ## Verify Go files + golangci-lint run --config ./golangci.yaml ./... + +.PHONY: clean build test dev lint diff --git a/ecs/golangci.yaml b/ecs/golangci.yaml new file mode 100644 index 000000000..396293367 --- /dev/null +++ b/ecs/golangci.yaml @@ -0,0 +1,12 @@ +run: + deadline: 2m + +linters: + disable-all: true + enable: + - gofmt + - goimports + - golint + - gosimple + - ineffassign + - misspell \ No newline at end of file diff --git a/ecs/pkg/amazon/ecs.go b/ecs/pkg/amazon/ecs.go index 03ca92f4d..a1ee2f928 100644 --- a/ecs/pkg/amazon/ecs.go +++ b/ecs/pkg/amazon/ecs.go @@ -2,6 +2,7 @@ package amazon import ( "errors" + "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/ecs" "github.com/sirupsen/logrus" From 8c0fee5abf2237273ad9ba0b727ad545fcc69cbd Mon Sep 17 00:00:00 2001 From: Nicolas De Loof <nicolas.deloof@gmail.com> Date: Fri, 24 Apr 2020 14:19:14 +0200 Subject: [PATCH 026/198] Define amazon.API as a simplified and currated interface over AWS SDK This makes code simpler to read and easier to mock within tests Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com> --- ecs/Makefile | 3 + ecs/pkg/amazon/api.go | 22 ++++ ecs/pkg/amazon/client.go | 28 +---- ecs/pkg/amazon/cloudformation.go | 42 ++++++- ecs/pkg/amazon/down.go | 14 +-- ecs/pkg/amazon/ecs.go | 50 --------- ecs/pkg/amazon/loadBalancer.go | 136 ---------------------- ecs/pkg/amazon/logs.go | 28 ----- ecs/pkg/amazon/network.go | 112 ------------------ ecs/pkg/amazon/roles.go | 49 -------- ecs/pkg/amazon/sdk.go | 187 +++++++++++++++++++++++++++++++ ecs/pkg/amazon/up.go | 47 +------- 12 files changed, 261 insertions(+), 457 deletions(-) create mode 100644 ecs/pkg/amazon/api.go delete mode 100644 ecs/pkg/amazon/ecs.go delete mode 100644 ecs/pkg/amazon/loadBalancer.go delete mode 100644 ecs/pkg/amazon/logs.go delete mode 100644 ecs/pkg/amazon/network.go delete mode 100644 ecs/pkg/amazon/roles.go create mode 100644 ecs/pkg/amazon/sdk.go diff --git a/ecs/Makefile b/ecs/Makefile index 6ff9437d7..5d3c5883d 100644 --- a/ecs/Makefile +++ b/ecs/Makefile @@ -1,3 +1,6 @@ +clean: + rm -rf dist/ + build: go build -v -o dist/docker-ecs cmd/main/main.go diff --git a/ecs/pkg/amazon/api.go b/ecs/pkg/amazon/api.go new file mode 100644 index 000000000..954c1b452 --- /dev/null +++ b/ecs/pkg/amazon/api.go @@ -0,0 +1,22 @@ +package amazon + +import ( + "github.com/awslabs/goformation/v4/cloudformation" +) + +type API interface { + ClusterExists(name string) (bool, error) + CreateCluster(name string) (string, error) + DeleteCluster(name string) error + + GetDefaultVPC() (string, error) + GetSubNets(vpcId string) ([]string, error) + + ListRolesForPolicy(policy string) ([]string, error) + GetRoleArn(name string) (string, error) + + StackExists(name string) (bool, error) + CreateStack(name string, template *cloudformation.Template) error + DescribeStackEvents(stack string) error + DeleteStack(name string) error +} diff --git a/ecs/pkg/amazon/client.go b/ecs/pkg/amazon/client.go index ff202fc23..18ffc9855 100644 --- a/ecs/pkg/amazon/client.go +++ b/ecs/pkg/amazon/client.go @@ -3,18 +3,6 @@ package amazon import ( "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws/session" - "github.com/aws/aws-sdk-go/service/cloudformation" - "github.com/aws/aws-sdk-go/service/cloudformation/cloudformationiface" - "github.com/aws/aws-sdk-go/service/cloudwatchlogs" - "github.com/aws/aws-sdk-go/service/cloudwatchlogs/cloudwatchlogsiface" - "github.com/aws/aws-sdk-go/service/ec2" - "github.com/aws/aws-sdk-go/service/ec2/ec2iface" - "github.com/aws/aws-sdk-go/service/ecs" - "github.com/aws/aws-sdk-go/service/ecs/ecsiface" - "github.com/aws/aws-sdk-go/service/elbv2" - "github.com/aws/aws-sdk-go/service/elbv2/elbv2iface" - "github.com/aws/aws-sdk-go/service/iam" - "github.com/aws/aws-sdk-go/service/iam/iamiface" "github.com/docker/ecs-plugin/pkg/compose" ) @@ -35,26 +23,14 @@ func NewClient(profile string, cluster string, region string) (compose.API, erro return &client{ Cluster: cluster, Region: region, - sess: sess, - ECS: ecs.New(sess), - EC2: ec2.New(sess), - ELB: elbv2.New(sess), - CW: cloudwatchlogs.New(sess), - IAM: iam.New(sess), - CF: cloudformation.New(sess), + api: NewAPI(sess), }, nil } type client struct { Cluster string Region string - sess *session.Session - ECS ecsiface.ECSAPI - EC2 ec2iface.EC2API - ELB elbv2iface.ELBV2API - CW cloudwatchlogsiface.CloudWatchLogsAPI - IAM iamiface.IAMAPI - CF cloudformationiface.CloudFormationAPI + api API } var _ compose.API = &client{} diff --git a/ecs/pkg/amazon/cloudformation.go b/ecs/pkg/amazon/cloudformation.go index dd304ab48..7458e7863 100644 --- a/ecs/pkg/amazon/cloudformation.go +++ b/ecs/pkg/amazon/cloudformation.go @@ -2,6 +2,8 @@ package amazon import ( "fmt" + "github.com/compose-spec/compose-go/types" + "github.com/sirupsen/logrus" "strings" ecsapi "github.com/aws/aws-sdk-go/service/ecs" @@ -14,12 +16,12 @@ import ( func (c client) Convert(project *compose.Project, loadBalancerArn *string) (*cloudformation.Template, error) { template := cloudformation.NewTemplate() - vpc, err := c.GetDefaultVPC() + vpc, err := c.api.GetDefaultVPC() if err != nil { return nil, err } - subnets, err := c.GetSubNets(vpc) + subnets, err := c.api.GetSubNets(vpc) if err != nil { return nil, err } @@ -42,7 +44,7 @@ func (c client) Convert(project *compose.Project, loadBalancerArn *string) (*clo GroupDescription: securityGroup, GroupName: securityGroup, SecurityGroupIngress: ingresses, - VpcId: *vpc, + VpcId: vpc, } for _, service := range project.Services { @@ -55,7 +57,7 @@ func (c client) Convert(project *compose.Project, loadBalancerArn *string) (*clo if err != nil { return nil, err } - definition.TaskRoleArn = *role + definition.TaskRoleArn = role taskDefinition := fmt.Sprintf("%sTaskDefinition", service.Name) template.Resources[taskDefinition] = definition @@ -78,3 +80,35 @@ func (c client) Convert(project *compose.Project, loadBalancerArn *string) (*clo } return template, nil } + +const ECSTaskExecutionPolicy = "arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy" +var defaultTaskExecutionRole string + +// GetEcsTaskExecutionRole retrieve the role ARN to apply for task execution +func (c client) GetEcsTaskExecutionRole(spec types.ServiceConfig) (string, error) { + if arn, ok := spec.Extras["x-ecs-TaskExecutionRole"]; ok { + return arn.(string), nil + } + if defaultTaskExecutionRole != "" { + return defaultTaskExecutionRole, nil + } + + logrus.Debug("Retrieve Task Execution Role") + entities, err := c.api.ListRolesForPolicy(ECSTaskExecutionPolicy) + if err != nil { + return "", err + } + if len(entities) == 0 { + return "", fmt.Errorf("no Role is attached to AmazonECSTaskExecutionRole Policy, please provide an explicit task execution role") + } + if len(entities) > 1 { + return "", fmt.Errorf("multiple Roles are attached to AmazonECSTaskExecutionRole Policy, please provide an explicit task execution role") + } + + arn, err := c.api.GetRoleArn(entities[0]) + if err != nil { + return "", err + } + defaultTaskExecutionRole = arn + return arn, nil +} \ No newline at end of file diff --git a/ecs/pkg/amazon/down.go b/ecs/pkg/amazon/down.go index 4dfdff4a3..49583864c 100644 --- a/ecs/pkg/amazon/down.go +++ b/ecs/pkg/amazon/down.go @@ -2,30 +2,22 @@ package amazon import ( "fmt" - - "github.com/aws/aws-sdk-go/service/cloudformation" - cf "github.com/aws/aws-sdk-go/service/cloudformation" ) func (c *client) ComposeDown(projectName *string, keepLoadBalancer, deleteCluster bool) error { - _, err := c.CF.DeleteStack(&cloudformation.DeleteStackInput{ - StackName: projectName, - }) + err := c.api.DeleteStack(projectName) if err != nil { return err } fmt.Printf("Delete stack ") - if err = c.CF.WaitUntilStackDeleteComplete(&cf.DescribeStacksInput{StackName: projectName}); err != nil { - return err - } - fmt.Printf("... done.\n") + if !deleteCluster { return nil } fmt.Printf("Delete cluster %s", c.Cluster) - if err = c.DeleteCluster(); err != nil { + if err = c.api.DeleteCluster(c.Cluster); err != nil { return err } fmt.Printf("... done. \n") diff --git a/ecs/pkg/amazon/ecs.go b/ecs/pkg/amazon/ecs.go deleted file mode 100644 index a1ee2f928..000000000 --- a/ecs/pkg/amazon/ecs.go +++ /dev/null @@ -1,50 +0,0 @@ -package amazon - -import ( - "errors" - - "github.com/aws/aws-sdk-go/aws" - "github.com/aws/aws-sdk-go/service/ecs" - "github.com/sirupsen/logrus" -) - -func (c client) RegisterTaskDefinition(task *ecs.RegisterTaskDefinitionInput) (*string, error) { - logrus.Debug("Register Task Definition") - def, err := c.ECS.RegisterTaskDefinition(task) - if err != nil { - return nil, err - } - return def.TaskDefinition.TaskDefinitionArn, err -} - -func (c client) CreateCluster() (*string, error) { - logrus.Debug("Create cluster ", c.Cluster) - response, err := c.ECS.CreateCluster(&ecs.CreateClusterInput{ClusterName: &c.Cluster}) - if err != nil { - return nil, err - } - return response.Cluster.Status, nil -} - -func (c client) DeleteCluster() error { - logrus.Debug("Delete cluster ", c.Cluster) - response, err := c.ECS.DeleteCluster(&ecs.DeleteClusterInput{Cluster: &c.Cluster}) - if err != nil { - return err - } - if *response.Cluster.Status == "INACTIVE" { - return nil - } - return errors.New("Failed to delete cluster, status: " + *response.Cluster.Status) -} - -func (c client) ClusterExists() (bool, error) { - logrus.Debug("Check if cluster was already created: ", c.Cluster) - clusters, err := c.ECS.DescribeClusters(&ecs.DescribeClustersInput{ - Clusters: []*string{aws.String(c.Cluster)}, - }) - if err != nil { - return false, err - } - return len(clusters.Clusters) > 0, nil -} diff --git a/ecs/pkg/amazon/loadBalancer.go b/ecs/pkg/amazon/loadBalancer.go deleted file mode 100644 index c8f96cd9b..000000000 --- a/ecs/pkg/amazon/loadBalancer.go +++ /dev/null @@ -1,136 +0,0 @@ -package amazon - -import ( - "fmt" - "strings" - - "github.com/docker/ecs-plugin/pkg/compose" - "github.com/sirupsen/logrus" - - "github.com/aws/aws-sdk-go/aws" - "github.com/aws/aws-sdk-go/service/elbv2" - "github.com/compose-spec/compose-go/types" -) - -func (c client) CreateLoadBalancer(project *compose.Project, subnets []*string) (*string, error) { - logrus.Debug("Create Load Balancer") - alb, err := c.ELB.CreateLoadBalancer(&elbv2.CreateLoadBalancerInput{ - IpAddressType: nil, - Name: aws.String(fmt.Sprintf("%s-LoadBalancer", project.Name)), - Subnets: subnets, - Type: aws.String(elbv2.LoadBalancerTypeEnumNetwork), - Tags: []*elbv2.Tag{ - { - Key: aws.String("com.docker.compose.project"), - Value: aws.String(project.Name), - }, - }, - }) - if err != nil { - return nil, err - } - return alb.LoadBalancers[0].LoadBalancerArn, nil -} - -func (c client) DeleteLoadBalancer(project *compose.Project, keepLoadBalancer bool) error { - logrus.Debug("Delete Load Balancer") - // FIXME We can tag LoadBalancer but not search by tag ? - loadBalancer, err := c.ELB.DescribeLoadBalancers(&elbv2.DescribeLoadBalancersInput{ - Names: aws.StringSlice([]string{fmt.Sprintf("%s-LoadBalancer", project.Name)}), - }) - if err != nil { - return err - } - arn := loadBalancer.LoadBalancers[0].LoadBalancerArn - - err = c.DeleteListeners(arn) - if err != nil { - return err - } - - err = c.DeleteTargetGroups(arn) - if err != nil { - return err - } - - if !keepLoadBalancer { - _, err = c.ELB.DeleteLoadBalancer(&elbv2.DeleteLoadBalancerInput{LoadBalancerArn: arn}) - } - return err -} - -func (c client) CreateTargetGroup(name string, vpc *string, port types.ServicePortConfig) (*string, error) { - logrus.Debugf("Create Target Group %d/%s\n", port.Target, port.Protocol) - group, err := c.ELB.CreateTargetGroup(&elbv2.CreateTargetGroupInput{ - Name: aws.String(name), - Port: aws.Int64(int64(port.Target)), - Protocol: aws.String(strings.ToUpper(port.Protocol)), - TargetType: aws.String("ip"), - VpcId: vpc, - }) - if err != nil { - return nil, err - } - arn := group.TargetGroups[0].TargetGroupArn - return arn, nil -} - -func (c client) DeleteTargetGroups(loadBalancer *string) error { - groups, err := c.ELB.DescribeTargetGroups(&elbv2.DescribeTargetGroupsInput{ - LoadBalancerArn: loadBalancer, - }) - if err != nil { - return err - } - for _, group := range groups.TargetGroups { - logrus.Debugf("Delete Target Group %s\n", *group.TargetGroupArn) - _, err := c.ELB.DeleteTargetGroup(&elbv2.DeleteTargetGroupInput{ - TargetGroupArn: group.TargetGroupArn, - }) - if err != nil { - return err - } - } - return nil -} - -func (c client) CreateListener(port types.ServicePortConfig, arn *string, target *string) error { - logrus.Debugf("Create Listener %d\n", port.Published) - _, err := c.ELB.CreateListener(&elbv2.CreateListenerInput{ - DefaultActions: []*elbv2.Action{ - { - ForwardConfig: &elbv2.ForwardActionConfig{ - TargetGroups: []*elbv2.TargetGroupTuple{ - { - TargetGroupArn: target, - }, - }, - }, - Type: aws.String(elbv2.ActionTypeEnumForward), - }, - }, - LoadBalancerArn: arn, - Port: aws.Int64(int64(port.Published)), - Protocol: aws.String(strings.ToUpper(port.Protocol)), - }) - return err -} - -func (c client) DeleteListeners(loadBalancer *string) error { - listeners, err := c.ELB.DescribeListeners(&elbv2.DescribeListenersInput{ - LoadBalancerArn: loadBalancer, - }) - if err != nil { - return err - } - for _, listener := range listeners.Listeners { - logrus.Debugf("Delete Listener %s\n", *listener.ListenerArn) - _, err := c.ELB.DeleteListener(&elbv2.DeleteListenerInput{ - ListenerArn: listener.ListenerArn, - }) - if err != nil { - return err - } - } - return nil -} diff --git a/ecs/pkg/amazon/logs.go b/ecs/pkg/amazon/logs.go deleted file mode 100644 index 0c06671c2..000000000 --- a/ecs/pkg/amazon/logs.go +++ /dev/null @@ -1,28 +0,0 @@ -package amazon - -import ( - "fmt" - - "github.com/aws/aws-sdk-go/aws" - "github.com/aws/aws-sdk-go/service/cloudwatchlogs" - "github.com/docker/ecs-plugin/pkg/compose" - "github.com/sirupsen/logrus" -) - -// GetOrCreateLogGroup retrieve a pre-existing log group for project or create one -func (c client) GetOrCreateLogGroup(project *compose.Project) (*string, error) { - logrus.Debug("Create Log Group") - logGroup := fmt.Sprintf("/ecs/%s", project.Name) - _, err := c.CW.CreateLogGroup(&cloudwatchlogs.CreateLogGroupInput{ - LogGroupName: aws.String(logGroup), - Tags: map[string]*string{ - ProjectTag: aws.String(project.Name), - }, - }) - if err != nil { - if _, ok := err.(*cloudwatchlogs.ResourceAlreadyExistsException); !ok { - return nil, err - } - } - return &logGroup, nil -} diff --git a/ecs/pkg/amazon/network.go b/ecs/pkg/amazon/network.go deleted file mode 100644 index 26b412758..000000000 --- a/ecs/pkg/amazon/network.go +++ /dev/null @@ -1,112 +0,0 @@ -package amazon - -import ( - "fmt" - "strings" - - "github.com/aws/aws-sdk-go/aws" - "github.com/aws/aws-sdk-go/service/ec2" - "github.com/compose-spec/compose-go/types" - "github.com/docker/ecs-plugin/pkg/compose" - "github.com/sirupsen/logrus" -) - -// GetDefaultVPC retrieve the default VPC for AWS account -func (c client) GetDefaultVPC() (*string, error) { - logrus.Debug("Retrieve default VPC") - vpcs, err := c.EC2.DescribeVpcs(&ec2.DescribeVpcsInput{ - Filters: []*ec2.Filter{ - { - Name: aws.String("isDefault"), - Values: []*string{aws.String("true")}, - }, - }, - }) - if err != nil { - return nil, err - } - if len(vpcs.Vpcs) == 0 { - return nil, fmt.Errorf("account has not default VPC") - } - return vpcs.Vpcs[0].VpcId, nil -} - -// GetSubNets retrieve default subnets for a VPC -func (c client) GetSubNets(vpc *string) ([]string, error) { - logrus.Debug("Retrieve SubNets") - subnets, err := c.EC2.DescribeSubnets(&ec2.DescribeSubnetsInput{ - DryRun: nil, - Filters: []*ec2.Filter{ - { - Name: aws.String("vpc-id"), - Values: []*string{vpc}, - }, - { - Name: aws.String("default-for-az"), - Values: []*string{aws.String("true")}, - }, - }, - }) - if err != nil { - return nil, err - } - - ids := []string{} - for _, subnet := range subnets.Subnets { - ids = append(ids, *subnet.SubnetId) - } - return ids, nil -} - -// CreateSecurityGroup create a security group for the project -func (c client) CreateSecurityGroup(project *compose.Project, vpc *string) (*string, error) { - logrus.Debug("Create Security Group") - name := fmt.Sprintf("%s Security Group", project.Name) - securityGroup, err := c.EC2.CreateSecurityGroup(&ec2.CreateSecurityGroupInput{ - Description: aws.String(name), - GroupName: aws.String(name), - VpcId: vpc, - }) - if err != nil { - return nil, err - } - - _, err = c.EC2.CreateTags(&ec2.CreateTagsInput{ - Resources: []*string{securityGroup.GroupId}, - Tags: []*ec2.Tag{ - { - Key: aws.String("Name"), - Value: aws.String(name), - }, - { - Key: aws.String(ProjectTag), - Value: aws.String(project.Name), - }, - }, - }) - if err != nil { - return nil, err - } - - return securityGroup.GroupId, nil -} - -func (c *client) ExposePort(securityGroup *string, port types.ServicePortConfig) error { - logrus.Debugf("Authorize ingress port %d/%s\n", port.Published, port.Protocol) - _, err := c.EC2.AuthorizeSecurityGroupIngress(&ec2.AuthorizeSecurityGroupIngressInput{ - GroupId: securityGroup, - IpPermissions: []*ec2.IpPermission{ - { - IpProtocol: aws.String(strings.ToUpper(port.Protocol)), - IpRanges: []*ec2.IpRange{ - { - CidrIp: aws.String("0.0.0.0/0"), - }, - }, - FromPort: aws.Int64(int64(port.Target)), - ToPort: aws.Int64(int64(port.Target)), - }, - }, - }) - return err -} diff --git a/ecs/pkg/amazon/roles.go b/ecs/pkg/amazon/roles.go deleted file mode 100644 index 7e4594af4..000000000 --- a/ecs/pkg/amazon/roles.go +++ /dev/null @@ -1,49 +0,0 @@ -package amazon - -import ( - "fmt" - - "github.com/aws/aws-sdk-go/aws" - "github.com/aws/aws-sdk-go/service/iam" - "github.com/compose-spec/compose-go/types" - "github.com/sirupsen/logrus" -) - -const ECSTaskExecutionPolicy = "arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy" - -var defaultTaskExecutionRole *string - -// GetEcsTaskExecutionRole retrieve the role ARN to apply for task execution -func (c client) GetEcsTaskExecutionRole(spec types.ServiceConfig) (*string, error) { - if arn, ok := spec.Extras["x-ecs-TaskExecutionRole"]; ok { - s := arn.(string) - return &s, nil - } - if defaultTaskExecutionRole != nil { - return defaultTaskExecutionRole, nil - } - - logrus.Debug("Retrieve Task Execution Role") - entities, err := c.IAM.ListEntitiesForPolicy(&iam.ListEntitiesForPolicyInput{ - EntityFilter: aws.String("Role"), - PolicyArn: aws.String(ECSTaskExecutionPolicy), - }) - if err != nil { - return nil, err - } - if len(entities.PolicyRoles) == 0 { - return nil, fmt.Errorf("no Role is attached to AmazonECSTaskExecutionRole Policy, please provide an explicit task execution role") - } - if len(entities.PolicyRoles) > 1 { - return nil, fmt.Errorf("multiple Roles are attached to AmazonECSTaskExecutionRole Policy, please provide an explicit task execution role") - } - - role, err := c.IAM.GetRole(&iam.GetRoleInput{ - RoleName: entities.PolicyRoles[0].RoleName, - }) - if err != nil { - return nil, err - } - defaultTaskExecutionRole = role.Role.Arn - return role.Role.Arn, nil -} diff --git a/ecs/pkg/amazon/sdk.go b/ecs/pkg/amazon/sdk.go new file mode 100644 index 000000000..228507c17 --- /dev/null +++ b/ecs/pkg/amazon/sdk.go @@ -0,0 +1,187 @@ +package amazon + +import ( + "fmt" + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/session" + "github.com/aws/aws-sdk-go/service/cloudformation" + "github.com/aws/aws-sdk-go/service/cloudformation/cloudformationiface" + "github.com/aws/aws-sdk-go/service/cloudwatchlogs" + "github.com/aws/aws-sdk-go/service/cloudwatchlogs/cloudwatchlogsiface" + "github.com/aws/aws-sdk-go/service/ec2" + "github.com/aws/aws-sdk-go/service/ec2/ec2iface" + "github.com/aws/aws-sdk-go/service/ecs" + "github.com/aws/aws-sdk-go/service/ecs/ecsiface" + "github.com/aws/aws-sdk-go/service/elbv2" + "github.com/aws/aws-sdk-go/service/elbv2/elbv2iface" + "github.com/aws/aws-sdk-go/service/iam" + "github.com/aws/aws-sdk-go/service/iam/iamiface" + cf "github.com/awslabs/goformation/v4/cloudformation" + "github.com/sirupsen/logrus" +) + +type sdk struct { + sess *session.Session + ECS ecsiface.ECSAPI + EC2 ec2iface.EC2API + ELB elbv2iface.ELBV2API + CW cloudwatchlogsiface.CloudWatchLogsAPI + IAM iamiface.IAMAPI + CF cloudformationiface.CloudFormationAPI +} + +func NewAPI(sess *session.Session) API { + return sdk{ + ECS: ecs.New(sess), + EC2: ec2.New(sess), + ELB: elbv2.New(sess), + CW: cloudwatchlogs.New(sess), + IAM: iam.New(sess), + CF: cloudformation.New(sess), + } +} + +func (s sdk) ClusterExists(name string) (bool, error) { + logrus.Debug("Check if cluster was already created: ", name) + clusters, err := s.ECS.DescribeClusters(&ecs.DescribeClustersInput{ + Clusters: []*string{aws.String(name)}, + }) + if err != nil { + return false, err + } + return len(clusters.Clusters) > 0, nil +} + +func (s sdk) CreateCluster(name string) (string, error) { + logrus.Debug("Create cluster ", name) + response, err := s.ECS.CreateCluster(&ecs.CreateClusterInput{ClusterName: aws.String(name)}) + if err != nil { + return "", err + } + return *response.Cluster.Status, nil +} + +func (s sdk) DeleteCluster(name string) error { + logrus.Debug("Delete cluster ", name) + response, err := s.ECS.DeleteCluster(&ecs.DeleteClusterInput{Cluster: aws.String(name)}) + if err != nil { + return err + } + if *response.Cluster.Status == "INACTIVE" { + return nil + } + return fmt.Errorf("Failed to delete cluster, status: %s" + *response.Cluster.Status) +} + +func (s sdk) GetDefaultVPC() (string, error) { + logrus.Debug("Retrieve default VPC") + vpcs, err := s.EC2.DescribeVpcs(&ec2.DescribeVpcsInput{ + Filters: []*ec2.Filter{ + { + Name: aws.String("isDefault"), + Values: []*string{aws.String("true")}, + }, + }, + }) + if err != nil { + return "", err + } + if len(vpcs.Vpcs) == 0 { + return "", fmt.Errorf("account has not default VPC") + } + return *vpcs.Vpcs[0].VpcId, nil +} + +func (s sdk) GetSubNets(vpc string) ([]string, error) { + logrus.Debug("Retrieve SubNets") + subnets, err := s.EC2.DescribeSubnets(&ec2.DescribeSubnetsInput{ + DryRun: nil, + Filters: []*ec2.Filter{ + { + Name: aws.String("vpc-id"), + Values: []*string{ aws.String(vpc)}, + }, + { + Name: aws.String("default-for-az"), + Values: []*string{aws.String("true")}, + }, + }, + }) + if err != nil { + return nil, err + } + + ids := []string{} + for _, subnet := range subnets.Subnets { + ids = append(ids, *subnet.SubnetId) + } + return ids, nil +} + +func (s sdk) ListRolesForPolicy(policy string) ([]string, error) { + entities, err := s.IAM.ListEntitiesForPolicy(&iam.ListEntitiesForPolicyInput{ + EntityFilter: aws.String("Role"), + PolicyArn: aws.String(policy), + }) + if err != nil { + return nil, err + } + roles := []string{} + for _, e := range entities.PolicyRoles { + roles = append(roles, *e.RoleName) + } + return roles, nil +} + +func (s sdk) GetRoleArn(name string) (string, error) { + role, err := s.IAM.GetRole(&iam.GetRoleInput{ + RoleName: aws.String(name), + }) + if err != nil { + return "", err + } + return *role.Role.Arn, nil +} + +func (s sdk) StackExists(name string) (bool, error) { + stacks, err := s.CF.DescribeStacks(&cloudformation.DescribeStacksInput{ + StackName: aws.String(name), + }) + if err != nil { + // FIXME doesn't work as expected + return false, nil + } + return len(stacks.Stacks) > 0, nil +} + +func (s sdk) CreateStack(name string, template *cf.Template) error { + logrus.Debug("Create CloudFormation stack") + json, err := template.JSON() + if err != nil { + return err + } + + _, err = s.CF.CreateStack(&cloudformation.CreateStackInput{ + OnFailure: aws.String("DELETE"), + StackName: aws.String(name), + TemplateBody: aws.String(string(json)), + TimeoutInMinutes: aws.Int64(10), + }) + return err +} + +func (s sdk) DescribeStackEvents(name string) error { + // Fixme implement Paginator on Events and return as a chan(events) + _, err := s.CF.DescribeStackEvents(&cloudformation.DescribeStackEventsInput{ + StackName: aws.String(name), + }) + return err +} + +func (s sdk) DeleteStack(name string) error { + logrus.Debug("Delete CloudFormation stack") + _, err := s.CF.DeleteStack(&cloudformation.DeleteStackInput{ + StackName: aws.String(name), + }) + return err +} diff --git a/ecs/pkg/amazon/up.go b/ecs/pkg/amazon/up.go index 2dbfd3062..4488cf670 100644 --- a/ecs/pkg/amazon/up.go +++ b/ecs/pkg/amazon/up.go @@ -2,26 +2,19 @@ package amazon import ( "fmt" - "os" - - "github.com/aws/aws-sdk-go/aws" - "github.com/aws/aws-sdk-go/service/cloudformation" "github.com/docker/ecs-plugin/pkg/compose" ) func (c *client) ComposeUp(project *compose.Project, loadBalancerArn *string) error { - ok, err := c.ClusterExists() + ok, err := c.api.ClusterExists(c.Cluster) if err != nil { return err } if !ok { - c.CreateCluster() + c.api.CreateCluster(c.Cluster) } - _, err = c.CF.DescribeStacks(&cloudformation.DescribeStacksInput{ - StackName: aws.String(project.Name), - }) - if err == nil { - // FIXME no ErrNotFound err type here + update, err := c.api.StackExists(project.Name) + if update { return fmt.Errorf("we do not (yet) support updating an existing CloudFormation stack") } @@ -30,40 +23,12 @@ func (c *client) ComposeUp(project *compose.Project, loadBalancerArn *string) er return err } - json, err := template.JSON() + err = c.api.CreateStack(project.Name, template) if err != nil { return err } - _, err = c.CF.ValidateTemplate(&cloudformation.ValidateTemplateInput{ - TemplateBody: aws.String(string(json)), - }) - if err != nil { - return err - } - - _, err = c.CF.CreateStack(&cloudformation.CreateStackInput{ - OnFailure: aws.String("DELETE"), - StackName: aws.String(project.Name), - TemplateBody: aws.String(string(json)), - TimeoutInMinutes: aws.Int64(10), - }) - if err != nil { - return err - } - - events, err := c.CF.DescribeStackEvents(&cloudformation.DescribeStackEventsInput{ - StackName: aws.String(project.Name), - }) - if err != nil { - return err - } - for _, event := range events.StackEvents { - fmt.Printf("%s %s\n", *event.LogicalResourceId, *event.ResourceStatus) - if *event.ResourceStatus == "CREATE_FAILED" { - fmt.Fprintln(os.Stderr, event.ResourceStatusReason) - } - } + err = c.api.DescribeStackEvents(project.Name) // TODO monitor progress return nil From 52c6177ff7c91ece52b870373fccfb7bd6c50ad7 Mon Sep 17 00:00:00 2001 From: Nicolas De Loof <nicolas.deloof@gmail.com> Date: Mon, 27 Apr 2020 09:42:44 +0200 Subject: [PATCH 027/198] API mock and a test case relying on it Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com> --- ecs/go.mod | 1 + ecs/go.sum | 6 ++ ecs/pkg/amazon/down_test.go | 45 +++++++++ ecs/pkg/amazon/mock/api.go | 195 ++++++++++++++++++++++++++++++++++++ 4 files changed, 247 insertions(+) create mode 100644 ecs/pkg/amazon/down_test.go create mode 100644 ecs/pkg/amazon/mock/api.go diff --git a/ecs/go.mod b/ecs/go.mod index 0bc80e999..0ddf6c857 100644 --- a/ecs/go.mod +++ b/ecs/go.mod @@ -27,6 +27,7 @@ require ( github.com/go-sql-driver/mysql v1.5.0 // indirect github.com/gofrs/uuid v3.2.0+incompatible // indirect github.com/gogo/protobuf v1.3.1 // indirect + github.com/golang/mock v1.4.3 github.com/gorilla/mux v1.7.3 // indirect github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed // indirect github.com/jinzhu/gorm v1.9.12 // indirect diff --git a/ecs/go.sum b/ecs/go.sum index ee07c6080..5cd52e764 100644 --- a/ecs/go.sum +++ b/ecs/go.sum @@ -119,6 +119,8 @@ github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2V github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.4.3 h1:GV+pQPG/EUUbkh47niozDcADz6go/dUwhVzdUQHIVRw= +github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs= @@ -366,6 +368,7 @@ golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3 h1:7TYNF4UdlohbFwpNH04CoPMp1 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/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= @@ -378,6 +381,7 @@ golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGm golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190624222133-a101b041ded4/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= @@ -421,5 +425,7 @@ gotest.tools/v3 v3.0.2/go.mod h1:3SzNCllyD9/Y+b5r9JIKQ474KzkZyqLqEfYqMsX94Bk= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= k8s.io/kubernetes v1.13.0/go.mod h1:ocZa8+6APFNC2tX1DZASIbocyYT5jHzqFVsY5aoB7Jk= +rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= +rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= vbom.ml/util v0.0.0-20180919145318-efcd4e0f9787 h1:O69FD9pJA4WUZlEwYatBEEkRWKQ5cKodWpdKTrCS/iQ= vbom.ml/util v0.0.0-20180919145318-efcd4e0f9787/go.mod h1:so/NYdZXCz+E3ZpW0uAoCj6uzU2+8OWDFv/HxUSs7kI= diff --git a/ecs/pkg/amazon/down_test.go b/ecs/pkg/amazon/down_test.go new file mode 100644 index 000000000..3245ecef5 --- /dev/null +++ b/ecs/pkg/amazon/down_test.go @@ -0,0 +1,45 @@ +package amazon + +import ( + "github.com/docker/ecs-plugin/pkg/amazon/mock" + "github.com/docker/ecs-plugin/pkg/compose" + "github.com/golang/mock/gomock" + "testing" +) + +func Test_down_dont_delete_cluster(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + m := mock.NewMockAPI(ctrl) + c := &client{ + Cluster: "test_cluster", + Region: "region", + api: m, + } + + recorder := m.EXPECT() + recorder.DeleteStack("test_project").Return(nil).Times(1) + + c.ComposeDown(&compose.Project{ + Name: "test_project", + }, false, false) +} + +func Test_down_delete_cluster(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + m := mock.NewMockAPI(ctrl) + c := &client{ + Cluster: "test_cluster", + Region: "region", + api: m, + } + + recorder := m.EXPECT() + recorder.DeleteStack("test_project").Return(nil).Times(1) + recorder.DeleteCluster("test_cluster").Return(nil).Times(1) + + c.ComposeDown(&compose.Project{ + Name: "test_project", + }, false, true) +} \ No newline at end of file diff --git a/ecs/pkg/amazon/mock/api.go b/ecs/pkg/amazon/mock/api.go new file mode 100644 index 000000000..8adaca5f4 --- /dev/null +++ b/ecs/pkg/amazon/mock/api.go @@ -0,0 +1,195 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: ./pkg/amazon/api.go + +// Package mock is a generated GoMock package. +package mock + +import ( + cloudformation "github.com/awslabs/goformation/v4/cloudformation" + gomock "github.com/golang/mock/gomock" + reflect "reflect" +) + +// MockAPI is a mock of API interface +type MockAPI struct { + ctrl *gomock.Controller + recorder *MockAPIMockRecorder +} + +// MockAPIMockRecorder is the mock recorder for MockAPI +type MockAPIMockRecorder struct { + mock *MockAPI +} + +// NewMockAPI creates a new mock instance +func NewMockAPI(ctrl *gomock.Controller) *MockAPI { + mock := &MockAPI{ctrl: ctrl} + mock.recorder = &MockAPIMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use +func (m *MockAPI) EXPECT() *MockAPIMockRecorder { + return m.recorder +} + +// ClusterExists mocks base method +func (m *MockAPI) ClusterExists(name string) (bool, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ClusterExists", name) + ret0, _ := ret[0].(bool) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ClusterExists indicates an expected call of ClusterExists +func (mr *MockAPIMockRecorder) ClusterExists(name interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ClusterExists", reflect.TypeOf((*MockAPI)(nil).ClusterExists), name) +} + +// CreateCluster mocks base method +func (m *MockAPI) CreateCluster(name string) (string, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "CreateCluster", name) + ret0, _ := ret[0].(string) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// CreateCluster indicates an expected call of CreateCluster +func (mr *MockAPIMockRecorder) CreateCluster(name interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateCluster", reflect.TypeOf((*MockAPI)(nil).CreateCluster), name) +} + +// DeleteCluster mocks base method +func (m *MockAPI) DeleteCluster(name string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DeleteCluster", name) + ret0, _ := ret[0].(error) + return ret0 +} + +// DeleteCluster indicates an expected call of DeleteCluster +func (mr *MockAPIMockRecorder) DeleteCluster(name interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteCluster", reflect.TypeOf((*MockAPI)(nil).DeleteCluster), name) +} + +// GetDefaultVPC mocks base method +func (m *MockAPI) GetDefaultVPC() (string, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetDefaultVPC") + ret0, _ := ret[0].(string) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetDefaultVPC indicates an expected call of GetDefaultVPC +func (mr *MockAPIMockRecorder) GetDefaultVPC() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetDefaultVPC", reflect.TypeOf((*MockAPI)(nil).GetDefaultVPC)) +} + +// GetSubNets mocks base method +func (m *MockAPI) GetSubNets(vpcId string) ([]string, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetSubNets", vpcId) + ret0, _ := ret[0].([]string) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetSubNets indicates an expected call of GetSubNets +func (mr *MockAPIMockRecorder) GetSubNets(vpcId interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSubNets", reflect.TypeOf((*MockAPI)(nil).GetSubNets), vpcId) +} + +// ListRolesForPolicy mocks base method +func (m *MockAPI) ListRolesForPolicy(policy string) ([]string, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ListRolesForPolicy", policy) + ret0, _ := ret[0].([]string) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ListRolesForPolicy indicates an expected call of ListRolesForPolicy +func (mr *MockAPIMockRecorder) ListRolesForPolicy(policy interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListRolesForPolicy", reflect.TypeOf((*MockAPI)(nil).ListRolesForPolicy), policy) +} + +// GetRoleArn mocks base method +func (m *MockAPI) GetRoleArn(name string) (string, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetRoleArn", name) + ret0, _ := ret[0].(string) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetRoleArn indicates an expected call of GetRoleArn +func (mr *MockAPIMockRecorder) GetRoleArn(name interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetRoleArn", reflect.TypeOf((*MockAPI)(nil).GetRoleArn), name) +} + +// StackExists mocks base method +func (m *MockAPI) StackExists(name string) (bool, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "StackExists", name) + ret0, _ := ret[0].(bool) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// StackExists indicates an expected call of StackExists +func (mr *MockAPIMockRecorder) StackExists(name interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "StackExists", reflect.TypeOf((*MockAPI)(nil).StackExists), name) +} + +// CreateStack mocks base method +func (m *MockAPI) CreateStack(name string, template *cloudformation.Template) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "CreateStack", name, template) + ret0, _ := ret[0].(error) + return ret0 +} + +// CreateStack indicates an expected call of CreateStack +func (mr *MockAPIMockRecorder) CreateStack(name, template interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateStack", reflect.TypeOf((*MockAPI)(nil).CreateStack), name, template) +} + +// DescribeStackEvents mocks base method +func (m *MockAPI) DescribeStackEvents(stack string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DescribeStackEvents", stack) + ret0, _ := ret[0].(error) + return ret0 +} + +// DescribeStackEvents indicates an expected call of DescribeStackEvents +func (mr *MockAPIMockRecorder) DescribeStackEvents(stack interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DescribeStackEvents", reflect.TypeOf((*MockAPI)(nil).DescribeStackEvents), stack) +} + +// DeleteStack mocks base method +func (m *MockAPI) DeleteStack(name string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DeleteStack", name) + ret0, _ := ret[0].(error) + return ret0 +} + +// DeleteStack indicates an expected call of DeleteStack +func (mr *MockAPIMockRecorder) DeleteStack(name interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteStack", reflect.TypeOf((*MockAPI)(nil).DeleteStack), name) +} From 3d8d982d4a547b7413c1833f551467b66cdd80f8 Mon Sep 17 00:00:00 2001 From: Nicolas De Loof <nicolas.deloof@gmail.com> Date: Mon, 27 Apr 2020 09:46:13 +0200 Subject: [PATCH 028/198] format Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com> --- ecs/pkg/amazon/client.go | 2 +- ecs/pkg/amazon/cloudformation.go | 3 ++- ecs/pkg/amazon/down_test.go | 2 +- ecs/pkg/amazon/sdk.go | 28 ++++++++++++++-------------- 4 files changed, 18 insertions(+), 17 deletions(-) diff --git a/ecs/pkg/amazon/client.go b/ecs/pkg/amazon/client.go index 18ffc9855..2f6fd4331 100644 --- a/ecs/pkg/amazon/client.go +++ b/ecs/pkg/amazon/client.go @@ -30,7 +30,7 @@ func NewClient(profile string, cluster string, region string) (compose.API, erro type client struct { Cluster string Region string - api API + api API } var _ compose.API = &client{} diff --git a/ecs/pkg/amazon/cloudformation.go b/ecs/pkg/amazon/cloudformation.go index 7458e7863..91c8bca7c 100644 --- a/ecs/pkg/amazon/cloudformation.go +++ b/ecs/pkg/amazon/cloudformation.go @@ -82,6 +82,7 @@ func (c client) Convert(project *compose.Project, loadBalancerArn *string) (*clo } const ECSTaskExecutionPolicy = "arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy" + var defaultTaskExecutionRole string // GetEcsTaskExecutionRole retrieve the role ARN to apply for task execution @@ -111,4 +112,4 @@ func (c client) GetEcsTaskExecutionRole(spec types.ServiceConfig) (string, error } defaultTaskExecutionRole = arn return arn, nil -} \ No newline at end of file +} diff --git a/ecs/pkg/amazon/down_test.go b/ecs/pkg/amazon/down_test.go index 3245ecef5..3738e78da 100644 --- a/ecs/pkg/amazon/down_test.go +++ b/ecs/pkg/amazon/down_test.go @@ -42,4 +42,4 @@ func Test_down_delete_cluster(t *testing.T) { c.ComposeDown(&compose.Project{ Name: "test_project", }, false, true) -} \ No newline at end of file +} diff --git a/ecs/pkg/amazon/sdk.go b/ecs/pkg/amazon/sdk.go index 228507c17..fdea7c6f3 100644 --- a/ecs/pkg/amazon/sdk.go +++ b/ecs/pkg/amazon/sdk.go @@ -21,23 +21,23 @@ import ( ) type sdk struct { - sess *session.Session - ECS ecsiface.ECSAPI - EC2 ec2iface.EC2API - ELB elbv2iface.ELBV2API - CW cloudwatchlogsiface.CloudWatchLogsAPI - IAM iamiface.IAMAPI - CF cloudformationiface.CloudFormationAPI + sess *session.Session + ECS ecsiface.ECSAPI + EC2 ec2iface.EC2API + ELB elbv2iface.ELBV2API + CW cloudwatchlogsiface.CloudWatchLogsAPI + IAM iamiface.IAMAPI + CF cloudformationiface.CloudFormationAPI } func NewAPI(sess *session.Session) API { return sdk{ - ECS: ecs.New(sess), - EC2: ec2.New(sess), - ELB: elbv2.New(sess), - CW: cloudwatchlogs.New(sess), - IAM: iam.New(sess), - CF: cloudformation.New(sess), + ECS: ecs.New(sess), + EC2: ec2.New(sess), + ELB: elbv2.New(sess), + CW: cloudwatchlogs.New(sess), + IAM: iam.New(sess), + CF: cloudformation.New(sess), } } @@ -99,7 +99,7 @@ func (s sdk) GetSubNets(vpc string) ([]string, error) { Filters: []*ec2.Filter{ { Name: aws.String("vpc-id"), - Values: []*string{ aws.String(vpc)}, + Values: []*string{aws.String(vpc)}, }, { Name: aws.String("default-for-az"), From 4138dcfb5a1a0fbafc4bc824261265a026a96423 Mon Sep 17 00:00:00 2001 From: Nicolas De Loof <nicolas.deloof@gmail.com> Date: Mon, 27 Apr 2020 10:04:07 +0200 Subject: [PATCH 029/198] Split API interface by required SDK func per command Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com> --- ecs/cmd/main/main.go | 4 ++-- ecs/pkg/amazon/api.go | 21 +++------------------ ecs/pkg/amazon/cloudformation.go | 12 ++++++++++-- ecs/pkg/amazon/down.go | 6 +++++- ecs/pkg/amazon/down_test.go | 7 ++++--- ecs/pkg/amazon/up.go | 14 ++++++++++++-- 6 files changed, 36 insertions(+), 28 deletions(-) diff --git a/ecs/cmd/main/main.go b/ecs/cmd/main/main.go index 7b3d1d6aa..ad67ece0f 100644 --- a/ecs/cmd/main/main.go +++ b/ecs/cmd/main/main.go @@ -98,7 +98,7 @@ func ConvertCommand(clusteropts *clusterOptions, projectOpts *compose.ProjectOpt if err != nil { return err } - template, err := client.Convert(project, opts.LoadBalancerArn()) + template, err := client.Convert(project) if err != nil { return err } @@ -124,7 +124,7 @@ func UpCommand(clusteropts *clusterOptions, projectOpts *compose.ProjectOptions) if err != nil { return err } - return client.ComposeUp(project, opts.LoadBalancerArn()) + return client.ComposeUp(project) }), } cmd.Flags().StringVar(&opts.loadBalancerArn, "load-balancer", "", "") diff --git a/ecs/pkg/amazon/api.go b/ecs/pkg/amazon/api.go index 954c1b452..543014046 100644 --- a/ecs/pkg/amazon/api.go +++ b/ecs/pkg/amazon/api.go @@ -1,22 +1,7 @@ package amazon -import ( - "github.com/awslabs/goformation/v4/cloudformation" -) - type API interface { - ClusterExists(name string) (bool, error) - CreateCluster(name string) (string, error) - DeleteCluster(name string) error - - GetDefaultVPC() (string, error) - GetSubNets(vpcId string) ([]string, error) - - ListRolesForPolicy(policy string) ([]string, error) - GetRoleArn(name string) (string, error) - - StackExists(name string) (bool, error) - CreateStack(name string, template *cloudformation.Template) error - DescribeStackEvents(stack string) error - DeleteStack(name string) error + downAPI + upAPI + convertAPI } diff --git a/ecs/pkg/amazon/cloudformation.go b/ecs/pkg/amazon/cloudformation.go index 91c8bca7c..5f91d76e7 100644 --- a/ecs/pkg/amazon/cloudformation.go +++ b/ecs/pkg/amazon/cloudformation.go @@ -2,9 +2,10 @@ package amazon import ( "fmt" + "strings" + "github.com/compose-spec/compose-go/types" "github.com/sirupsen/logrus" - "strings" ecsapi "github.com/aws/aws-sdk-go/service/ecs" "github.com/awslabs/goformation/v4/cloudformation" @@ -14,7 +15,7 @@ import ( "github.com/docker/ecs-plugin/pkg/convert" ) -func (c client) Convert(project *compose.Project, loadBalancerArn *string) (*cloudformation.Template, error) { +func (c client) Convert(project *compose.Project) (*cloudformation.Template, error) { template := cloudformation.NewTemplate() vpc, err := c.api.GetDefaultVPC() if err != nil { @@ -113,3 +114,10 @@ func (c client) GetEcsTaskExecutionRole(spec types.ServiceConfig) (string, error defaultTaskExecutionRole = arn return arn, nil } + +type convertAPI interface { + GetDefaultVPC() (string, error) + GetSubNets(vpcId string) ([]string, error) + ListRolesForPolicy(policy string) ([]string, error) + GetRoleArn(name string) (string, error) +} diff --git a/ecs/pkg/amazon/down.go b/ecs/pkg/amazon/down.go index 49583864c..185572315 100644 --- a/ecs/pkg/amazon/down.go +++ b/ecs/pkg/amazon/down.go @@ -11,7 +11,6 @@ func (c *client) ComposeDown(projectName *string, keepLoadBalancer, deleteCluste } fmt.Printf("Delete stack ") - if !deleteCluster { return nil } @@ -23,3 +22,8 @@ func (c *client) ComposeDown(projectName *string, keepLoadBalancer, deleteCluste fmt.Printf("... done. \n") return nil } + +type downAPI interface { + DeleteStack(name string) error + DeleteCluster(name string) error +} diff --git a/ecs/pkg/amazon/down_test.go b/ecs/pkg/amazon/down_test.go index 3738e78da..161bdfe19 100644 --- a/ecs/pkg/amazon/down_test.go +++ b/ecs/pkg/amazon/down_test.go @@ -1,10 +1,11 @@ package amazon import ( + "testing" + "github.com/docker/ecs-plugin/pkg/amazon/mock" "github.com/docker/ecs-plugin/pkg/compose" "github.com/golang/mock/gomock" - "testing" ) func Test_down_dont_delete_cluster(t *testing.T) { @@ -22,7 +23,7 @@ func Test_down_dont_delete_cluster(t *testing.T) { c.ComposeDown(&compose.Project{ Name: "test_project", - }, false, false) + }, false) } func Test_down_delete_cluster(t *testing.T) { @@ -41,5 +42,5 @@ func Test_down_delete_cluster(t *testing.T) { c.ComposeDown(&compose.Project{ Name: "test_project", - }, false, true) + }, true) } diff --git a/ecs/pkg/amazon/up.go b/ecs/pkg/amazon/up.go index 4488cf670..493c8cbaa 100644 --- a/ecs/pkg/amazon/up.go +++ b/ecs/pkg/amazon/up.go @@ -2,10 +2,12 @@ package amazon import ( "fmt" + + "github.com/awslabs/goformation/v4/cloudformation" "github.com/docker/ecs-plugin/pkg/compose" ) -func (c *client) ComposeUp(project *compose.Project, loadBalancerArn *string) error { +func (c *client) ComposeUp(project *compose.Project) error { ok, err := c.api.ClusterExists(c.Cluster) if err != nil { return err @@ -18,7 +20,7 @@ func (c *client) ComposeUp(project *compose.Project, loadBalancerArn *string) er return fmt.Errorf("we do not (yet) support updating an existing CloudFormation stack") } - template, err := c.Convert(project, loadBalancerArn) + template, err := c.Convert(project) if err != nil { return err } @@ -33,3 +35,11 @@ func (c *client) ComposeUp(project *compose.Project, loadBalancerArn *string) er // TODO monitor progress return nil } + +type upAPI interface { + ClusterExists(name string) (bool, error) + CreateCluster(name string) (string, error) + StackExists(name string) (bool, error) + CreateStack(name string, template *cloudformation.Template) error + DescribeStackEvents(stack string) error +} From 096c800c1b3a3389d3eeebaeba748cee0dca864f Mon Sep 17 00:00:00 2001 From: Nicolas De Loof <nicolas.deloof@gmail.com> Date: Mon, 27 Apr 2020 11:00:08 +0200 Subject: [PATCH 030/198] use go:generate to automatically run mockgen on build Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com> --- ecs/cmd/main/main.go | 1 - ecs/pkg/amazon/api.go | 2 + ecs/pkg/amazon/mock/api.go | 186 ++++++++++++++++++------------------- 3 files changed, 95 insertions(+), 94 deletions(-) diff --git a/ecs/cmd/main/main.go b/ecs/cmd/main/main.go index ad67ece0f..70468704e 100644 --- a/ecs/cmd/main/main.go +++ b/ecs/cmd/main/main.go @@ -90,7 +90,6 @@ func (o upOptions) LoadBalancerArn() *string { } func ConvertCommand(clusteropts *clusterOptions, projectOpts *compose.ProjectOptions) *cobra.Command { - opts := upOptions{} cmd := &cobra.Command{ Use: "convert", RunE: compose.WithProject(projectOpts, func(project *compose.Project, args []string) error { diff --git a/ecs/pkg/amazon/api.go b/ecs/pkg/amazon/api.go index 543014046..ff61174d0 100644 --- a/ecs/pkg/amazon/api.go +++ b/ecs/pkg/amazon/api.go @@ -1,5 +1,7 @@ package amazon +//go:generate mockgen -destination=./mock/api.go -package=mock . API + type API interface { downAPI upAPI diff --git a/ecs/pkg/amazon/mock/api.go b/ecs/pkg/amazon/mock/api.go index 8adaca5f4..ded7e8c8c 100644 --- a/ecs/pkg/amazon/mock/api.go +++ b/ecs/pkg/amazon/mock/api.go @@ -1,5 +1,5 @@ // Code generated by MockGen. DO NOT EDIT. -// Source: ./pkg/amazon/api.go +// Source: github.com/docker/ecs-plugin/pkg/amazon (interfaces: API) // Package mock is a generated GoMock package. package mock @@ -34,47 +34,89 @@ func (m *MockAPI) EXPECT() *MockAPIMockRecorder { } // ClusterExists mocks base method -func (m *MockAPI) ClusterExists(name string) (bool, error) { +func (m *MockAPI) ClusterExists(arg0 string) (bool, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "ClusterExists", name) + ret := m.ctrl.Call(m, "ClusterExists", arg0) ret0, _ := ret[0].(bool) ret1, _ := ret[1].(error) return ret0, ret1 } // ClusterExists indicates an expected call of ClusterExists -func (mr *MockAPIMockRecorder) ClusterExists(name interface{}) *gomock.Call { +func (mr *MockAPIMockRecorder) ClusterExists(arg0 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ClusterExists", reflect.TypeOf((*MockAPI)(nil).ClusterExists), name) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ClusterExists", reflect.TypeOf((*MockAPI)(nil).ClusterExists), arg0) } // CreateCluster mocks base method -func (m *MockAPI) CreateCluster(name string) (string, error) { +func (m *MockAPI) CreateCluster(arg0 string) (string, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "CreateCluster", name) + ret := m.ctrl.Call(m, "CreateCluster", arg0) ret0, _ := ret[0].(string) ret1, _ := ret[1].(error) return ret0, ret1 } // CreateCluster indicates an expected call of CreateCluster -func (mr *MockAPIMockRecorder) CreateCluster(name interface{}) *gomock.Call { +func (mr *MockAPIMockRecorder) CreateCluster(arg0 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateCluster", reflect.TypeOf((*MockAPI)(nil).CreateCluster), name) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateCluster", reflect.TypeOf((*MockAPI)(nil).CreateCluster), arg0) +} + +// CreateStack mocks base method +func (m *MockAPI) CreateStack(arg0 string, arg1 *cloudformation.Template) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "CreateStack", arg0, arg1) + ret0, _ := ret[0].(error) + return ret0 +} + +// CreateStack indicates an expected call of CreateStack +func (mr *MockAPIMockRecorder) CreateStack(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateStack", reflect.TypeOf((*MockAPI)(nil).CreateStack), arg0, arg1) } // DeleteCluster mocks base method -func (m *MockAPI) DeleteCluster(name string) error { +func (m *MockAPI) DeleteCluster(arg0 string) error { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "DeleteCluster", name) + ret := m.ctrl.Call(m, "DeleteCluster", arg0) ret0, _ := ret[0].(error) return ret0 } // DeleteCluster indicates an expected call of DeleteCluster -func (mr *MockAPIMockRecorder) DeleteCluster(name interface{}) *gomock.Call { +func (mr *MockAPIMockRecorder) DeleteCluster(arg0 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteCluster", reflect.TypeOf((*MockAPI)(nil).DeleteCluster), name) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteCluster", reflect.TypeOf((*MockAPI)(nil).DeleteCluster), arg0) +} + +// DeleteStack mocks base method +func (m *MockAPI) DeleteStack(arg0 string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DeleteStack", arg0) + ret0, _ := ret[0].(error) + return ret0 +} + +// DeleteStack indicates an expected call of DeleteStack +func (mr *MockAPIMockRecorder) DeleteStack(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteStack", reflect.TypeOf((*MockAPI)(nil).DeleteStack), arg0) +} + +// DescribeStackEvents mocks base method +func (m *MockAPI) DescribeStackEvents(arg0 string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DescribeStackEvents", arg0) + ret0, _ := ret[0].(error) + return ret0 +} + +// DescribeStackEvents indicates an expected call of DescribeStackEvents +func (mr *MockAPIMockRecorder) DescribeStackEvents(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DescribeStackEvents", reflect.TypeOf((*MockAPI)(nil).DescribeStackEvents), arg0) } // GetDefaultVPC mocks base method @@ -92,104 +134,62 @@ func (mr *MockAPIMockRecorder) GetDefaultVPC() *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetDefaultVPC", reflect.TypeOf((*MockAPI)(nil).GetDefaultVPC)) } -// GetSubNets mocks base method -func (m *MockAPI) GetSubNets(vpcId string) ([]string, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetSubNets", vpcId) - ret0, _ := ret[0].([]string) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// GetSubNets indicates an expected call of GetSubNets -func (mr *MockAPIMockRecorder) GetSubNets(vpcId interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSubNets", reflect.TypeOf((*MockAPI)(nil).GetSubNets), vpcId) -} - -// ListRolesForPolicy mocks base method -func (m *MockAPI) ListRolesForPolicy(policy string) ([]string, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "ListRolesForPolicy", policy) - ret0, _ := ret[0].([]string) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// ListRolesForPolicy indicates an expected call of ListRolesForPolicy -func (mr *MockAPIMockRecorder) ListRolesForPolicy(policy interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListRolesForPolicy", reflect.TypeOf((*MockAPI)(nil).ListRolesForPolicy), policy) -} - // GetRoleArn mocks base method -func (m *MockAPI) GetRoleArn(name string) (string, error) { +func (m *MockAPI) GetRoleArn(arg0 string) (string, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetRoleArn", name) + ret := m.ctrl.Call(m, "GetRoleArn", arg0) ret0, _ := ret[0].(string) ret1, _ := ret[1].(error) return ret0, ret1 } // GetRoleArn indicates an expected call of GetRoleArn -func (mr *MockAPIMockRecorder) GetRoleArn(name interface{}) *gomock.Call { +func (mr *MockAPIMockRecorder) GetRoleArn(arg0 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetRoleArn", reflect.TypeOf((*MockAPI)(nil).GetRoleArn), name) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetRoleArn", reflect.TypeOf((*MockAPI)(nil).GetRoleArn), arg0) +} + +// GetSubNets mocks base method +func (m *MockAPI) GetSubNets(arg0 string) ([]string, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetSubNets", arg0) + ret0, _ := ret[0].([]string) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetSubNets indicates an expected call of GetSubNets +func (mr *MockAPIMockRecorder) GetSubNets(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSubNets", reflect.TypeOf((*MockAPI)(nil).GetSubNets), arg0) +} + +// ListRolesForPolicy mocks base method +func (m *MockAPI) ListRolesForPolicy(arg0 string) ([]string, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ListRolesForPolicy", arg0) + ret0, _ := ret[0].([]string) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ListRolesForPolicy indicates an expected call of ListRolesForPolicy +func (mr *MockAPIMockRecorder) ListRolesForPolicy(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListRolesForPolicy", reflect.TypeOf((*MockAPI)(nil).ListRolesForPolicy), arg0) } // StackExists mocks base method -func (m *MockAPI) StackExists(name string) (bool, error) { +func (m *MockAPI) StackExists(arg0 string) (bool, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "StackExists", name) + ret := m.ctrl.Call(m, "StackExists", arg0) ret0, _ := ret[0].(bool) ret1, _ := ret[1].(error) return ret0, ret1 } // StackExists indicates an expected call of StackExists -func (mr *MockAPIMockRecorder) StackExists(name interface{}) *gomock.Call { +func (mr *MockAPIMockRecorder) StackExists(arg0 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "StackExists", reflect.TypeOf((*MockAPI)(nil).StackExists), name) -} - -// CreateStack mocks base method -func (m *MockAPI) CreateStack(name string, template *cloudformation.Template) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "CreateStack", name, template) - ret0, _ := ret[0].(error) - return ret0 -} - -// CreateStack indicates an expected call of CreateStack -func (mr *MockAPIMockRecorder) CreateStack(name, template interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateStack", reflect.TypeOf((*MockAPI)(nil).CreateStack), name, template) -} - -// DescribeStackEvents mocks base method -func (m *MockAPI) DescribeStackEvents(stack string) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "DescribeStackEvents", stack) - ret0, _ := ret[0].(error) - return ret0 -} - -// DescribeStackEvents indicates an expected call of DescribeStackEvents -func (mr *MockAPIMockRecorder) DescribeStackEvents(stack interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DescribeStackEvents", reflect.TypeOf((*MockAPI)(nil).DescribeStackEvents), stack) -} - -// DeleteStack mocks base method -func (m *MockAPI) DeleteStack(name string) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "DeleteStack", name) - ret0, _ := ret[0].(error) - return ret0 -} - -// DeleteStack indicates an expected call of DeleteStack -func (mr *MockAPIMockRecorder) DeleteStack(name interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteStack", reflect.TypeOf((*MockAPI)(nil).DeleteStack), name) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "StackExists", reflect.TypeOf((*MockAPI)(nil).StackExists), arg0) } From 541bda3af8e4e8eb74e5ecb9b7d0032583492b09 Mon Sep 17 00:00:00 2001 From: Nicolas De Loof <nicolas.deloof@gmail.com> Date: Tue, 28 Apr 2020 10:22:29 +0200 Subject: [PATCH 031/198] Remove ALB related options to be defined on phase 2 Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com> --- ecs/cmd/main/main.go | 8 +++----- ecs/pkg/amazon/down.go | 2 +- ecs/pkg/compose/api.go | 6 +++--- 3 files changed, 7 insertions(+), 9 deletions(-) diff --git a/ecs/cmd/main/main.go b/ecs/cmd/main/main.go index 70468704e..85802552f 100644 --- a/ecs/cmd/main/main.go +++ b/ecs/cmd/main/main.go @@ -131,8 +131,7 @@ func UpCommand(clusteropts *clusterOptions, projectOpts *compose.ProjectOptions) } type downOptions struct { - KeepLoadBalancer bool - DeleteCluster bool + DeleteCluster bool } func DownCommand(clusteropts *clusterOptions, projectOpts *compose.ProjectOptions) *cobra.Command { @@ -149,11 +148,11 @@ func DownCommand(clusteropts *clusterOptions, projectOpts *compose.ProjectOption if err != nil { return err } - return client.ComposeDown(&project.Name, opts.KeepLoadBalancer, opts.DeleteCluster) + return client.ComposeDown(project.Name, opts.DeleteCluster) } // project names passed as parameters for _, name := range args { - err := client.ComposeDown(&name, opts.KeepLoadBalancer, opts.DeleteCluster) + err := client.ComposeDown(name, opts.DeleteCluster) if err != nil { return err } @@ -161,7 +160,6 @@ func DownCommand(clusteropts *clusterOptions, projectOpts *compose.ProjectOption return nil }, } - cmd.Flags().BoolVar(&opts.KeepLoadBalancer, "keep-load-balancer", false, "Keep Load Balancer for further use") cmd.Flags().BoolVar(&opts.DeleteCluster, "delete-cluster", false, "Delete cluster") return cmd } diff --git a/ecs/pkg/amazon/down.go b/ecs/pkg/amazon/down.go index 185572315..5c52e9286 100644 --- a/ecs/pkg/amazon/down.go +++ b/ecs/pkg/amazon/down.go @@ -4,7 +4,7 @@ import ( "fmt" ) -func (c *client) ComposeDown(projectName *string, keepLoadBalancer, deleteCluster bool) error { +func (c *client) ComposeDown(projectName string, deleteCluster bool) error { err := c.api.DeleteStack(projectName) if err != nil { return err diff --git a/ecs/pkg/compose/api.go b/ecs/pkg/compose/api.go index b30750824..d2e2b1a2d 100644 --- a/ecs/pkg/compose/api.go +++ b/ecs/pkg/compose/api.go @@ -3,7 +3,7 @@ package compose import "github.com/awslabs/goformation/v4/cloudformation" type API interface { - Convert(project *Project, loadBalancerArn *string) (*cloudformation.Template, error) - ComposeUp(project *Project, loadBalancerArn *string) error - ComposeDown(projectName *string, keepLoadBalancer, deleteCluster bool) error + Convert(project *Project) (*cloudformation.Template, error) + ComposeUp(project *Project) error + ComposeDown(projectName string, deleteCluster bool) error } From 30029fa701df1351e9d68a0e26516b4f07cc756a Mon Sep 17 00:00:00 2001 From: Nicolas De Loof <nicolas.deloof@gmail.com> Date: Tue, 28 Apr 2020 10:37:35 +0200 Subject: [PATCH 032/198] ComposeDown only require stack name Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com> g Sur la branche api Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com> --- ecs/pkg/amazon/down_test.go | 9 ++------- ecs/pkg/amazon/sdk.go | 1 + ecs/pkg/compose/project_test.go | 8 ++++---- 3 files changed, 7 insertions(+), 11 deletions(-) diff --git a/ecs/pkg/amazon/down_test.go b/ecs/pkg/amazon/down_test.go index 161bdfe19..7e10c9416 100644 --- a/ecs/pkg/amazon/down_test.go +++ b/ecs/pkg/amazon/down_test.go @@ -4,7 +4,6 @@ import ( "testing" "github.com/docker/ecs-plugin/pkg/amazon/mock" - "github.com/docker/ecs-plugin/pkg/compose" "github.com/golang/mock/gomock" ) @@ -21,9 +20,7 @@ func Test_down_dont_delete_cluster(t *testing.T) { recorder := m.EXPECT() recorder.DeleteStack("test_project").Return(nil).Times(1) - c.ComposeDown(&compose.Project{ - Name: "test_project", - }, false) + c.ComposeDown("test_project", false) } func Test_down_delete_cluster(t *testing.T) { @@ -40,7 +37,5 @@ func Test_down_delete_cluster(t *testing.T) { recorder.DeleteStack("test_project").Return(nil).Times(1) recorder.DeleteCluster("test_cluster").Return(nil).Times(1) - c.ComposeDown(&compose.Project{ - Name: "test_project", - }, true) + c.ComposeDown("test_project", true) } diff --git a/ecs/pkg/amazon/sdk.go b/ecs/pkg/amazon/sdk.go index fdea7c6f3..75e3cc22e 100644 --- a/ecs/pkg/amazon/sdk.go +++ b/ecs/pkg/amazon/sdk.go @@ -2,6 +2,7 @@ package amazon import ( "fmt" + "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws/session" "github.com/aws/aws-sdk-go/service/cloudformation" diff --git a/ecs/pkg/compose/project_test.go b/ecs/pkg/compose/project_test.go index 0b44fb33d..2906c9d21 100644 --- a/ecs/pkg/compose/project_test.go +++ b/ecs/pkg/compose/project_test.go @@ -8,14 +8,14 @@ import ( ) func Test_project_name(t *testing.T) { - p, err := projectFromOptions(&ProjectOptions{ + p, err := ProjectFromOptions(&ProjectOptions{ name: "my_project", ConfigPaths: []string{"testdata/simple/compose.yaml"}, }) assert.NilError(t, err) assert.Equal(t, p.Name, "my_project") - p, err = projectFromOptions(&ProjectOptions{ + p, err = ProjectFromOptions(&ProjectOptions{ name: "", ConfigPaths: []string{"testdata/simple/compose.yaml"}, }) @@ -23,7 +23,7 @@ func Test_project_name(t *testing.T) { assert.Equal(t, p.Name, "simple") os.Setenv("COMPOSE_PROJECT_NAME", "my_project_from_env") - p, err = projectFromOptions(&ProjectOptions{ + p, err = ProjectFromOptions(&ProjectOptions{ name: "", ConfigPaths: []string{"testdata/simple/compose.yaml"}, }) @@ -32,7 +32,7 @@ func Test_project_name(t *testing.T) { } func Test_project_from_set_of_files(t *testing.T) { - p, err := projectFromOptions(&ProjectOptions{ + p, err := ProjectFromOptions(&ProjectOptions{ name: "my_project", ConfigPaths: []string{ "testdata/simple/compose.yaml", From 4642bfa1728888575cccab854804a3592391059b Mon Sep 17 00:00:00 2001 From: Nicolas De Loof <nicolas.deloof@gmail.com> Date: Tue, 28 Apr 2020 11:05:00 +0200 Subject: [PATCH 033/198] Fix linter warnings Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com> --- ecs/pkg/amazon/cloudformation.go | 2 +- ecs/pkg/amazon/down_test.go | 4 ++-- ecs/pkg/amazon/sdk.go | 4 ++-- ecs/pkg/amazon/up.go | 6 ++++++ 4 files changed, 11 insertions(+), 5 deletions(-) diff --git a/ecs/pkg/amazon/cloudformation.go b/ecs/pkg/amazon/cloudformation.go index 5f91d76e7..22b0c1d79 100644 --- a/ecs/pkg/amazon/cloudformation.go +++ b/ecs/pkg/amazon/cloudformation.go @@ -117,7 +117,7 @@ func (c client) GetEcsTaskExecutionRole(spec types.ServiceConfig) (string, error type convertAPI interface { GetDefaultVPC() (string, error) - GetSubNets(vpcId string) ([]string, error) + GetSubNets(vpcID string) ([]string, error) ListRolesForPolicy(policy string) ([]string, error) GetRoleArn(name string) (string, error) } diff --git a/ecs/pkg/amazon/down_test.go b/ecs/pkg/amazon/down_test.go index 7e10c9416..c0ec4d0ed 100644 --- a/ecs/pkg/amazon/down_test.go +++ b/ecs/pkg/amazon/down_test.go @@ -7,7 +7,7 @@ import ( "github.com/golang/mock/gomock" ) -func Test_down_dont_delete_cluster(t *testing.T) { +func TestDownDontDeleteCluster(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() m := mock.NewMockAPI(ctrl) @@ -23,7 +23,7 @@ func Test_down_dont_delete_cluster(t *testing.T) { c.ComposeDown("test_project", false) } -func Test_down_delete_cluster(t *testing.T) { +func TestDownDeleteCluster(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() m := mock.NewMockAPI(ctrl) diff --git a/ecs/pkg/amazon/sdk.go b/ecs/pkg/amazon/sdk.go index 75e3cc22e..86b65e348 100644 --- a/ecs/pkg/amazon/sdk.go +++ b/ecs/pkg/amazon/sdk.go @@ -93,14 +93,14 @@ func (s sdk) GetDefaultVPC() (string, error) { return *vpcs.Vpcs[0].VpcId, nil } -func (s sdk) GetSubNets(vpc string) ([]string, error) { +func (s sdk) GetSubNets(vpcID string) ([]string, error) { logrus.Debug("Retrieve SubNets") subnets, err := s.EC2.DescribeSubnets(&ec2.DescribeSubnetsInput{ DryRun: nil, Filters: []*ec2.Filter{ { Name: aws.String("vpc-id"), - Values: []*string{aws.String(vpc)}, + Values: []*string{aws.String(vpcID)}, }, { Name: aws.String("default-for-az"), diff --git a/ecs/pkg/amazon/up.go b/ecs/pkg/amazon/up.go index 493c8cbaa..d89385bc4 100644 --- a/ecs/pkg/amazon/up.go +++ b/ecs/pkg/amazon/up.go @@ -16,6 +16,9 @@ func (c *client) ComposeUp(project *compose.Project) error { c.api.CreateCluster(c.Cluster) } update, err := c.api.StackExists(project.Name) + if err != nil { + return err + } if update { return fmt.Errorf("we do not (yet) support updating an existing CloudFormation stack") } @@ -31,6 +34,9 @@ func (c *client) ComposeUp(project *compose.Project) error { } err = c.api.DescribeStackEvents(project.Name) + if err != nil { + return err + } // TODO monitor progress return nil From b6be4a0ac3626eaaafeaba546f3d40834ae522b5 Mon Sep 17 00:00:00 2001 From: Nicolas De Loof <nicolas.deloof@gmail.com> Date: Tue, 28 Apr 2020 11:31:41 +0200 Subject: [PATCH 034/198] Use `WithContext` SDK APIs so we can implement cancelation Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com> --- ecs/cmd/main/main.go | 9 ++-- ecs/pkg/amazon/cloudformation.go | 23 +++++---- ecs/pkg/amazon/down.go | 11 ++-- ecs/pkg/amazon/down_test.go | 14 ++--- ecs/pkg/amazon/mock/api.go | 89 ++++++++++++++++---------------- ecs/pkg/amazon/sdk.go | 45 ++++++++-------- ecs/pkg/amazon/up.go | 25 ++++----- ecs/pkg/compose/api.go | 12 +++-- 8 files changed, 120 insertions(+), 108 deletions(-) diff --git a/ecs/cmd/main/main.go b/ecs/cmd/main/main.go index 85802552f..99f556c7c 100644 --- a/ecs/cmd/main/main.go +++ b/ecs/cmd/main/main.go @@ -1,6 +1,7 @@ package main import ( + "context" "fmt" "github.com/docker/cli/cli-plugins/manager" @@ -97,7 +98,7 @@ func ConvertCommand(clusteropts *clusterOptions, projectOpts *compose.ProjectOpt if err != nil { return err } - template, err := client.Convert(project) + template, err := client.Convert(context.Background(), project) if err != nil { return err } @@ -123,7 +124,7 @@ func UpCommand(clusteropts *clusterOptions, projectOpts *compose.ProjectOptions) if err != nil { return err } - return client.ComposeUp(project) + return client.ComposeUp(context.Background(), project) }), } cmd.Flags().StringVar(&opts.loadBalancerArn, "load-balancer", "", "") @@ -148,11 +149,11 @@ func DownCommand(clusteropts *clusterOptions, projectOpts *compose.ProjectOption if err != nil { return err } - return client.ComposeDown(project.Name, opts.DeleteCluster) + return client.ComposeDown(context.Background(), project.Name, opts.DeleteCluster) } // project names passed as parameters for _, name := range args { - err := client.ComposeDown(name, opts.DeleteCluster) + err := client.ComposeDown(context.Background(), name, opts.DeleteCluster) if err != nil { return err } diff --git a/ecs/pkg/amazon/cloudformation.go b/ecs/pkg/amazon/cloudformation.go index 22b0c1d79..44a952fec 100644 --- a/ecs/pkg/amazon/cloudformation.go +++ b/ecs/pkg/amazon/cloudformation.go @@ -1,6 +1,7 @@ package amazon import ( + "context" "fmt" "strings" @@ -15,14 +16,14 @@ import ( "github.com/docker/ecs-plugin/pkg/convert" ) -func (c client) Convert(project *compose.Project) (*cloudformation.Template, error) { +func (c client) Convert(ctx context.Context, project *compose.Project) (*cloudformation.Template, error) { template := cloudformation.NewTemplate() - vpc, err := c.api.GetDefaultVPC() + vpc, err := c.api.GetDefaultVPC(ctx) if err != nil { return nil, err } - subnets, err := c.api.GetSubNets(vpc) + subnets, err := c.api.GetSubNets(ctx, vpc) if err != nil { return nil, err } @@ -54,7 +55,7 @@ func (c client) Convert(project *compose.Project) (*cloudformation.Template, err return nil, err } - role, err := c.GetEcsTaskExecutionRole(service) + role, err := c.GetEcsTaskExecutionRole(ctx, service) if err != nil { return nil, err } @@ -87,7 +88,7 @@ const ECSTaskExecutionPolicy = "arn:aws:iam::aws:policy/service-role/AmazonECSTa var defaultTaskExecutionRole string // GetEcsTaskExecutionRole retrieve the role ARN to apply for task execution -func (c client) GetEcsTaskExecutionRole(spec types.ServiceConfig) (string, error) { +func (c client) GetEcsTaskExecutionRole(ctx context.Context, spec types.ServiceConfig) (string, error) { if arn, ok := spec.Extras["x-ecs-TaskExecutionRole"]; ok { return arn.(string), nil } @@ -96,7 +97,7 @@ func (c client) GetEcsTaskExecutionRole(spec types.ServiceConfig) (string, error } logrus.Debug("Retrieve Task Execution Role") - entities, err := c.api.ListRolesForPolicy(ECSTaskExecutionPolicy) + entities, err := c.api.ListRolesForPolicy(ctx, ECSTaskExecutionPolicy) if err != nil { return "", err } @@ -107,7 +108,7 @@ func (c client) GetEcsTaskExecutionRole(spec types.ServiceConfig) (string, error return "", fmt.Errorf("multiple Roles are attached to AmazonECSTaskExecutionRole Policy, please provide an explicit task execution role") } - arn, err := c.api.GetRoleArn(entities[0]) + arn, err := c.api.GetRoleArn(ctx, entities[0]) if err != nil { return "", err } @@ -116,8 +117,8 @@ func (c client) GetEcsTaskExecutionRole(spec types.ServiceConfig) (string, error } type convertAPI interface { - GetDefaultVPC() (string, error) - GetSubNets(vpcID string) ([]string, error) - ListRolesForPolicy(policy string) ([]string, error) - GetRoleArn(name string) (string, error) + GetDefaultVPC(ctx context.Context) (string, error) + GetSubNets(ctx context.Context, vpcID string) ([]string, error) + ListRolesForPolicy(ctx context.Context, policy string) ([]string, error) + GetRoleArn(ctx context.Context, name string) (string, error) } diff --git a/ecs/pkg/amazon/down.go b/ecs/pkg/amazon/down.go index 5c52e9286..ab708d21f 100644 --- a/ecs/pkg/amazon/down.go +++ b/ecs/pkg/amazon/down.go @@ -1,11 +1,12 @@ package amazon import ( + "context" "fmt" ) -func (c *client) ComposeDown(projectName string, deleteCluster bool) error { - err := c.api.DeleteStack(projectName) +func (c *client) ComposeDown(ctx context.Context, projectName string, deleteCluster bool) error { + err := c.api.DeleteStack(ctx, projectName) if err != nil { return err } @@ -16,7 +17,7 @@ func (c *client) ComposeDown(projectName string, deleteCluster bool) error { } fmt.Printf("Delete cluster %s", c.Cluster) - if err = c.api.DeleteCluster(c.Cluster); err != nil { + if err = c.api.DeleteCluster(ctx, c.Cluster); err != nil { return err } fmt.Printf("... done. \n") @@ -24,6 +25,6 @@ func (c *client) ComposeDown(projectName string, deleteCluster bool) error { } type downAPI interface { - DeleteStack(name string) error - DeleteCluster(name string) error + DeleteStack(ctx context.Context, name string) error + DeleteCluster(ctx context.Context, name string) error } diff --git a/ecs/pkg/amazon/down_test.go b/ecs/pkg/amazon/down_test.go index c0ec4d0ed..3bfc1e40d 100644 --- a/ecs/pkg/amazon/down_test.go +++ b/ecs/pkg/amazon/down_test.go @@ -1,6 +1,7 @@ package amazon import ( + "context" "testing" "github.com/docker/ecs-plugin/pkg/amazon/mock" @@ -16,11 +17,11 @@ func TestDownDontDeleteCluster(t *testing.T) { Region: "region", api: m, } - + ctx := context.TODO() recorder := m.EXPECT() - recorder.DeleteStack("test_project").Return(nil).Times(1) + recorder.DeleteStack(ctx, "test_project").Return(nil).Times(1) - c.ComposeDown("test_project", false) + c.ComposeDown(ctx, "test_project", false) } func TestDownDeleteCluster(t *testing.T) { @@ -33,9 +34,10 @@ func TestDownDeleteCluster(t *testing.T) { api: m, } + ctx := context.TODO() recorder := m.EXPECT() - recorder.DeleteStack("test_project").Return(nil).Times(1) - recorder.DeleteCluster("test_cluster").Return(nil).Times(1) + recorder.DeleteStack(ctx, "test_project").Return(nil).Times(1) + recorder.DeleteCluster(ctx, "test_cluster").Return(nil).Times(1) - c.ComposeDown("test_project", true) + c.ComposeDown(ctx, "test_project", true) } diff --git a/ecs/pkg/amazon/mock/api.go b/ecs/pkg/amazon/mock/api.go index ded7e8c8c..59f661775 100644 --- a/ecs/pkg/amazon/mock/api.go +++ b/ecs/pkg/amazon/mock/api.go @@ -5,6 +5,7 @@ package mock import ( + context "context" cloudformation "github.com/awslabs/goformation/v4/cloudformation" gomock "github.com/golang/mock/gomock" reflect "reflect" @@ -34,162 +35,162 @@ func (m *MockAPI) EXPECT() *MockAPIMockRecorder { } // ClusterExists mocks base method -func (m *MockAPI) ClusterExists(arg0 string) (bool, error) { +func (m *MockAPI) ClusterExists(arg0 context.Context, arg1 string) (bool, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "ClusterExists", arg0) + ret := m.ctrl.Call(m, "ClusterExists", arg0, arg1) ret0, _ := ret[0].(bool) ret1, _ := ret[1].(error) return ret0, ret1 } // ClusterExists indicates an expected call of ClusterExists -func (mr *MockAPIMockRecorder) ClusterExists(arg0 interface{}) *gomock.Call { +func (mr *MockAPIMockRecorder) ClusterExists(arg0, arg1 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ClusterExists", reflect.TypeOf((*MockAPI)(nil).ClusterExists), arg0) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ClusterExists", reflect.TypeOf((*MockAPI)(nil).ClusterExists), arg0, arg1) } // CreateCluster mocks base method -func (m *MockAPI) CreateCluster(arg0 string) (string, error) { +func (m *MockAPI) CreateCluster(arg0 context.Context, arg1 string) (string, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "CreateCluster", arg0) + ret := m.ctrl.Call(m, "CreateCluster", arg0, arg1) ret0, _ := ret[0].(string) ret1, _ := ret[1].(error) return ret0, ret1 } // CreateCluster indicates an expected call of CreateCluster -func (mr *MockAPIMockRecorder) CreateCluster(arg0 interface{}) *gomock.Call { +func (mr *MockAPIMockRecorder) CreateCluster(arg0, arg1 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateCluster", reflect.TypeOf((*MockAPI)(nil).CreateCluster), arg0) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateCluster", reflect.TypeOf((*MockAPI)(nil).CreateCluster), arg0, arg1) } // CreateStack mocks base method -func (m *MockAPI) CreateStack(arg0 string, arg1 *cloudformation.Template) error { +func (m *MockAPI) CreateStack(arg0 context.Context, arg1 string, arg2 *cloudformation.Template) error { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "CreateStack", arg0, arg1) + ret := m.ctrl.Call(m, "CreateStack", arg0, arg1, arg2) ret0, _ := ret[0].(error) return ret0 } // CreateStack indicates an expected call of CreateStack -func (mr *MockAPIMockRecorder) CreateStack(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockAPIMockRecorder) CreateStack(arg0, arg1, arg2 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateStack", reflect.TypeOf((*MockAPI)(nil).CreateStack), arg0, arg1) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateStack", reflect.TypeOf((*MockAPI)(nil).CreateStack), arg0, arg1, arg2) } // DeleteCluster mocks base method -func (m *MockAPI) DeleteCluster(arg0 string) error { +func (m *MockAPI) DeleteCluster(arg0 context.Context, arg1 string) error { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "DeleteCluster", arg0) + ret := m.ctrl.Call(m, "DeleteCluster", arg0, arg1) ret0, _ := ret[0].(error) return ret0 } // DeleteCluster indicates an expected call of DeleteCluster -func (mr *MockAPIMockRecorder) DeleteCluster(arg0 interface{}) *gomock.Call { +func (mr *MockAPIMockRecorder) DeleteCluster(arg0, arg1 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteCluster", reflect.TypeOf((*MockAPI)(nil).DeleteCluster), arg0) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteCluster", reflect.TypeOf((*MockAPI)(nil).DeleteCluster), arg0, arg1) } // DeleteStack mocks base method -func (m *MockAPI) DeleteStack(arg0 string) error { +func (m *MockAPI) DeleteStack(arg0 context.Context, arg1 string) error { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "DeleteStack", arg0) + ret := m.ctrl.Call(m, "DeleteStack", arg0, arg1) ret0, _ := ret[0].(error) return ret0 } // DeleteStack indicates an expected call of DeleteStack -func (mr *MockAPIMockRecorder) DeleteStack(arg0 interface{}) *gomock.Call { +func (mr *MockAPIMockRecorder) DeleteStack(arg0, arg1 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteStack", reflect.TypeOf((*MockAPI)(nil).DeleteStack), arg0) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteStack", reflect.TypeOf((*MockAPI)(nil).DeleteStack), arg0, arg1) } // DescribeStackEvents mocks base method -func (m *MockAPI) DescribeStackEvents(arg0 string) error { +func (m *MockAPI) DescribeStackEvents(arg0 context.Context, arg1 string) error { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "DescribeStackEvents", arg0) + ret := m.ctrl.Call(m, "DescribeStackEvents", arg0, arg1) ret0, _ := ret[0].(error) return ret0 } // DescribeStackEvents indicates an expected call of DescribeStackEvents -func (mr *MockAPIMockRecorder) DescribeStackEvents(arg0 interface{}) *gomock.Call { +func (mr *MockAPIMockRecorder) DescribeStackEvents(arg0, arg1 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DescribeStackEvents", reflect.TypeOf((*MockAPI)(nil).DescribeStackEvents), arg0) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DescribeStackEvents", reflect.TypeOf((*MockAPI)(nil).DescribeStackEvents), arg0, arg1) } // GetDefaultVPC mocks base method -func (m *MockAPI) GetDefaultVPC() (string, error) { +func (m *MockAPI) GetDefaultVPC(arg0 context.Context) (string, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetDefaultVPC") + ret := m.ctrl.Call(m, "GetDefaultVPC", arg0) ret0, _ := ret[0].(string) ret1, _ := ret[1].(error) return ret0, ret1 } // GetDefaultVPC indicates an expected call of GetDefaultVPC -func (mr *MockAPIMockRecorder) GetDefaultVPC() *gomock.Call { +func (mr *MockAPIMockRecorder) GetDefaultVPC(arg0 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetDefaultVPC", reflect.TypeOf((*MockAPI)(nil).GetDefaultVPC)) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetDefaultVPC", reflect.TypeOf((*MockAPI)(nil).GetDefaultVPC), arg0) } // GetRoleArn mocks base method -func (m *MockAPI) GetRoleArn(arg0 string) (string, error) { +func (m *MockAPI) GetRoleArn(arg0 context.Context, arg1 string) (string, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetRoleArn", arg0) + ret := m.ctrl.Call(m, "GetRoleArn", arg0, arg1) ret0, _ := ret[0].(string) ret1, _ := ret[1].(error) return ret0, ret1 } // GetRoleArn indicates an expected call of GetRoleArn -func (mr *MockAPIMockRecorder) GetRoleArn(arg0 interface{}) *gomock.Call { +func (mr *MockAPIMockRecorder) GetRoleArn(arg0, arg1 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetRoleArn", reflect.TypeOf((*MockAPI)(nil).GetRoleArn), arg0) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetRoleArn", reflect.TypeOf((*MockAPI)(nil).GetRoleArn), arg0, arg1) } // GetSubNets mocks base method -func (m *MockAPI) GetSubNets(arg0 string) ([]string, error) { +func (m *MockAPI) GetSubNets(arg0 context.Context, arg1 string) ([]string, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetSubNets", arg0) + ret := m.ctrl.Call(m, "GetSubNets", arg0, arg1) ret0, _ := ret[0].([]string) ret1, _ := ret[1].(error) return ret0, ret1 } // GetSubNets indicates an expected call of GetSubNets -func (mr *MockAPIMockRecorder) GetSubNets(arg0 interface{}) *gomock.Call { +func (mr *MockAPIMockRecorder) GetSubNets(arg0, arg1 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSubNets", reflect.TypeOf((*MockAPI)(nil).GetSubNets), arg0) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSubNets", reflect.TypeOf((*MockAPI)(nil).GetSubNets), arg0, arg1) } // ListRolesForPolicy mocks base method -func (m *MockAPI) ListRolesForPolicy(arg0 string) ([]string, error) { +func (m *MockAPI) ListRolesForPolicy(arg0 context.Context, arg1 string) ([]string, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "ListRolesForPolicy", arg0) + ret := m.ctrl.Call(m, "ListRolesForPolicy", arg0, arg1) ret0, _ := ret[0].([]string) ret1, _ := ret[1].(error) return ret0, ret1 } // ListRolesForPolicy indicates an expected call of ListRolesForPolicy -func (mr *MockAPIMockRecorder) ListRolesForPolicy(arg0 interface{}) *gomock.Call { +func (mr *MockAPIMockRecorder) ListRolesForPolicy(arg0, arg1 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListRolesForPolicy", reflect.TypeOf((*MockAPI)(nil).ListRolesForPolicy), arg0) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListRolesForPolicy", reflect.TypeOf((*MockAPI)(nil).ListRolesForPolicy), arg0, arg1) } // StackExists mocks base method -func (m *MockAPI) StackExists(arg0 string) (bool, error) { +func (m *MockAPI) StackExists(arg0 context.Context, arg1 string) (bool, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "StackExists", arg0) + ret := m.ctrl.Call(m, "StackExists", arg0, arg1) ret0, _ := ret[0].(bool) ret1, _ := ret[1].(error) return ret0, ret1 } // StackExists indicates an expected call of StackExists -func (mr *MockAPIMockRecorder) StackExists(arg0 interface{}) *gomock.Call { +func (mr *MockAPIMockRecorder) StackExists(arg0, arg1 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "StackExists", reflect.TypeOf((*MockAPI)(nil).StackExists), arg0) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "StackExists", reflect.TypeOf((*MockAPI)(nil).StackExists), arg0, arg1) } diff --git a/ecs/pkg/amazon/sdk.go b/ecs/pkg/amazon/sdk.go index 86b65e348..e3460c0db 100644 --- a/ecs/pkg/amazon/sdk.go +++ b/ecs/pkg/amazon/sdk.go @@ -1,6 +1,7 @@ package amazon import ( + "context" "fmt" "github.com/aws/aws-sdk-go/aws" @@ -42,9 +43,9 @@ func NewAPI(sess *session.Session) API { } } -func (s sdk) ClusterExists(name string) (bool, error) { +func (s sdk) ClusterExists(ctx context.Context, name string) (bool, error) { logrus.Debug("Check if cluster was already created: ", name) - clusters, err := s.ECS.DescribeClusters(&ecs.DescribeClustersInput{ + clusters, err := s.ECS.DescribeClustersWithContext(aws.Context(ctx), &ecs.DescribeClustersInput{ Clusters: []*string{aws.String(name)}, }) if err != nil { @@ -53,18 +54,18 @@ func (s sdk) ClusterExists(name string) (bool, error) { return len(clusters.Clusters) > 0, nil } -func (s sdk) CreateCluster(name string) (string, error) { +func (s sdk) CreateCluster(ctx context.Context, name string) (string, error) { logrus.Debug("Create cluster ", name) - response, err := s.ECS.CreateCluster(&ecs.CreateClusterInput{ClusterName: aws.String(name)}) + response, err := s.ECS.CreateClusterWithContext(aws.Context(ctx), &ecs.CreateClusterInput{ClusterName: aws.String(name)}) if err != nil { return "", err } return *response.Cluster.Status, nil } -func (s sdk) DeleteCluster(name string) error { +func (s sdk) DeleteCluster(ctx context.Context, name string) error { logrus.Debug("Delete cluster ", name) - response, err := s.ECS.DeleteCluster(&ecs.DeleteClusterInput{Cluster: aws.String(name)}) + response, err := s.ECS.DeleteClusterWithContext(aws.Context(ctx), &ecs.DeleteClusterInput{Cluster: aws.String(name)}) if err != nil { return err } @@ -74,9 +75,9 @@ func (s sdk) DeleteCluster(name string) error { return fmt.Errorf("Failed to delete cluster, status: %s" + *response.Cluster.Status) } -func (s sdk) GetDefaultVPC() (string, error) { +func (s sdk) GetDefaultVPC(ctx context.Context) (string, error) { logrus.Debug("Retrieve default VPC") - vpcs, err := s.EC2.DescribeVpcs(&ec2.DescribeVpcsInput{ + vpcs, err := s.EC2.DescribeVpcsWithContext(aws.Context(ctx), &ec2.DescribeVpcsInput{ Filters: []*ec2.Filter{ { Name: aws.String("isDefault"), @@ -93,9 +94,9 @@ func (s sdk) GetDefaultVPC() (string, error) { return *vpcs.Vpcs[0].VpcId, nil } -func (s sdk) GetSubNets(vpcID string) ([]string, error) { +func (s sdk) GetSubNets(ctx context.Context, vpcID string) ([]string, error) { logrus.Debug("Retrieve SubNets") - subnets, err := s.EC2.DescribeSubnets(&ec2.DescribeSubnetsInput{ + subnets, err := s.EC2.DescribeSubnetsWithContext(aws.Context(ctx), &ec2.DescribeSubnetsInput{ DryRun: nil, Filters: []*ec2.Filter{ { @@ -119,8 +120,8 @@ func (s sdk) GetSubNets(vpcID string) ([]string, error) { return ids, nil } -func (s sdk) ListRolesForPolicy(policy string) ([]string, error) { - entities, err := s.IAM.ListEntitiesForPolicy(&iam.ListEntitiesForPolicyInput{ +func (s sdk) ListRolesForPolicy(ctx context.Context, policy string) ([]string, error) { + entities, err := s.IAM.ListEntitiesForPolicyWithContext(aws.Context(ctx), &iam.ListEntitiesForPolicyInput{ EntityFilter: aws.String("Role"), PolicyArn: aws.String(policy), }) @@ -134,8 +135,8 @@ func (s sdk) ListRolesForPolicy(policy string) ([]string, error) { return roles, nil } -func (s sdk) GetRoleArn(name string) (string, error) { - role, err := s.IAM.GetRole(&iam.GetRoleInput{ +func (s sdk) GetRoleArn(ctx context.Context, name string) (string, error) { + role, err := s.IAM.GetRoleWithContext(aws.Context(ctx), &iam.GetRoleInput{ RoleName: aws.String(name), }) if err != nil { @@ -144,8 +145,8 @@ func (s sdk) GetRoleArn(name string) (string, error) { return *role.Role.Arn, nil } -func (s sdk) StackExists(name string) (bool, error) { - stacks, err := s.CF.DescribeStacks(&cloudformation.DescribeStacksInput{ +func (s sdk) StackExists(ctx context.Context, name string) (bool, error) { + stacks, err := s.CF.DescribeStacksWithContext(aws.Context(ctx), &cloudformation.DescribeStacksInput{ StackName: aws.String(name), }) if err != nil { @@ -155,14 +156,14 @@ func (s sdk) StackExists(name string) (bool, error) { return len(stacks.Stacks) > 0, nil } -func (s sdk) CreateStack(name string, template *cf.Template) error { +func (s sdk) CreateStack(ctx context.Context, name string, template *cf.Template) error { logrus.Debug("Create CloudFormation stack") json, err := template.JSON() if err != nil { return err } - _, err = s.CF.CreateStack(&cloudformation.CreateStackInput{ + _, err = s.CF.CreateStackWithContext(aws.Context(ctx), &cloudformation.CreateStackInput{ OnFailure: aws.String("DELETE"), StackName: aws.String(name), TemplateBody: aws.String(string(json)), @@ -171,17 +172,17 @@ func (s sdk) CreateStack(name string, template *cf.Template) error { return err } -func (s sdk) DescribeStackEvents(name string) error { +func (s sdk) DescribeStackEvents(ctx context.Context, name string) error { // Fixme implement Paginator on Events and return as a chan(events) - _, err := s.CF.DescribeStackEvents(&cloudformation.DescribeStackEventsInput{ + _, err := s.CF.DescribeStackEventsWithContext(aws.Context(ctx), &cloudformation.DescribeStackEventsInput{ StackName: aws.String(name), }) return err } -func (s sdk) DeleteStack(name string) error { +func (s sdk) DeleteStack(ctx context.Context, name string) error { logrus.Debug("Delete CloudFormation stack") - _, err := s.CF.DeleteStack(&cloudformation.DeleteStackInput{ + _, err := s.CF.DeleteStackWithContext(aws.Context(ctx), &cloudformation.DeleteStackInput{ StackName: aws.String(name), }) return err diff --git a/ecs/pkg/amazon/up.go b/ecs/pkg/amazon/up.go index d89385bc4..41b4f5cba 100644 --- a/ecs/pkg/amazon/up.go +++ b/ecs/pkg/amazon/up.go @@ -1,21 +1,22 @@ package amazon import ( + "context" "fmt" "github.com/awslabs/goformation/v4/cloudformation" "github.com/docker/ecs-plugin/pkg/compose" ) -func (c *client) ComposeUp(project *compose.Project) error { - ok, err := c.api.ClusterExists(c.Cluster) +func (c *client) ComposeUp(ctx context.Context, project *compose.Project) error { + ok, err := c.api.ClusterExists(ctx, c.Cluster) if err != nil { return err } if !ok { - c.api.CreateCluster(c.Cluster) + c.api.CreateCluster(ctx, c.Cluster) } - update, err := c.api.StackExists(project.Name) + update, err := c.api.StackExists(ctx, project.Name) if err != nil { return err } @@ -23,17 +24,17 @@ func (c *client) ComposeUp(project *compose.Project) error { return fmt.Errorf("we do not (yet) support updating an existing CloudFormation stack") } - template, err := c.Convert(project) + template, err := c.Convert(ctx, project) if err != nil { return err } - err = c.api.CreateStack(project.Name, template) + err = c.api.CreateStack(ctx, project.Name, template) if err != nil { return err } - err = c.api.DescribeStackEvents(project.Name) + err = c.api.DescribeStackEvents(ctx, project.Name) if err != nil { return err } @@ -43,9 +44,9 @@ func (c *client) ComposeUp(project *compose.Project) error { } type upAPI interface { - ClusterExists(name string) (bool, error) - CreateCluster(name string) (string, error) - StackExists(name string) (bool, error) - CreateStack(name string, template *cloudformation.Template) error - DescribeStackEvents(stack string) error + ClusterExists(ctx context.Context, name string) (bool, error) + CreateCluster(ctx context.Context, name string) (string, error) + StackExists(ctx context.Context, name string) (bool, error) + CreateStack(ctx context.Context, name string, template *cloudformation.Template) error + DescribeStackEvents(ctx context.Context, stack string) error } diff --git a/ecs/pkg/compose/api.go b/ecs/pkg/compose/api.go index d2e2b1a2d..f5e45bc68 100644 --- a/ecs/pkg/compose/api.go +++ b/ecs/pkg/compose/api.go @@ -1,9 +1,13 @@ package compose -import "github.com/awslabs/goformation/v4/cloudformation" +import ( + "context" + + "github.com/awslabs/goformation/v4/cloudformation" +) type API interface { - Convert(project *Project) (*cloudformation.Template, error) - ComposeUp(project *Project) error - ComposeDown(projectName string, deleteCluster bool) error + Convert(ctx context.Context, project *Project) (*cloudformation.Template, error) + ComposeUp(ctx context.Context, project *Project) error + ComposeDown(ctx context.Context, projectName string, deleteCluster bool) error } From 39a59ae55fe78ad2e639ddd854ad8edec945aff5 Mon Sep 17 00:00:00 2001 From: aiordache <anca.iordache@docker.com> Date: Thu, 23 Apr 2020 17:18:51 +0200 Subject: [PATCH 035/198] Deploy with user-defined vpc id Signed-off-by: aiordache <anca.iordache@docker.com> Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com> --- ecs/cmd/main/main.go | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/ecs/cmd/main/main.go b/ecs/cmd/main/main.go index 99f556c7c..9e7fbb94a 100644 --- a/ecs/cmd/main/main.go +++ b/ecs/cmd/main/main.go @@ -81,6 +81,7 @@ func ComposeCommand(clusteropts *clusterOptions) *cobra.Command { type upOptions struct { loadBalancerArn string + vpcID string } func (o upOptions) LoadBalancerArn() *string { @@ -89,6 +90,12 @@ func (o upOptions) LoadBalancerArn() *string { } return &o.loadBalancerArn } +func (o upOptions) GetVPC() *string { + if o.vpcID == "" { + return nil + } + return &o.vpcID +} func ConvertCommand(clusteropts *clusterOptions, projectOpts *compose.ProjectOptions) *cobra.Command { cmd := &cobra.Command{ @@ -128,6 +135,7 @@ func UpCommand(clusteropts *clusterOptions, projectOpts *compose.ProjectOptions) }), } cmd.Flags().StringVar(&opts.loadBalancerArn, "load-balancer", "", "") + cmd.Flags().StringVar(&opts.vpcID, "vpc-id", "", "") return cmd } From 95c88acfb48f48b561aaecf227185d17a7127e07 Mon Sep 17 00:00:00 2001 From: aiordache <anca.iordache@docker.com> Date: Mon, 27 Apr 2020 19:14:32 +0200 Subject: [PATCH 036/198] Set existing vpc as default external network in the compose file Signed-off-by: aiordache <anca.iordache@docker.com> Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com> --- ecs/cmd/main/main.go | 8 -------- 1 file changed, 8 deletions(-) diff --git a/ecs/cmd/main/main.go b/ecs/cmd/main/main.go index 9e7fbb94a..99f556c7c 100644 --- a/ecs/cmd/main/main.go +++ b/ecs/cmd/main/main.go @@ -81,7 +81,6 @@ func ComposeCommand(clusteropts *clusterOptions) *cobra.Command { type upOptions struct { loadBalancerArn string - vpcID string } func (o upOptions) LoadBalancerArn() *string { @@ -90,12 +89,6 @@ func (o upOptions) LoadBalancerArn() *string { } return &o.loadBalancerArn } -func (o upOptions) GetVPC() *string { - if o.vpcID == "" { - return nil - } - return &o.vpcID -} func ConvertCommand(clusteropts *clusterOptions, projectOpts *compose.ProjectOptions) *cobra.Command { cmd := &cobra.Command{ @@ -135,7 +128,6 @@ func UpCommand(clusteropts *clusterOptions, projectOpts *compose.ProjectOptions) }), } cmd.Flags().StringVar(&opts.loadBalancerArn, "load-balancer", "", "") - cmd.Flags().StringVar(&opts.vpcID, "vpc-id", "", "") return cmd } From cec44fbb7b07db4ba9fda046e7193239673996e9 Mon Sep 17 00:00:00 2001 From: aiordache <anca.iordache@docker.com> Date: Tue, 28 Apr 2020 14:33:40 +0200 Subject: [PATCH 037/198] move to sdk Signed-off-by: aiordache <anca.iordache@docker.com> Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com> --- ecs/pkg/amazon/cloudformation.go | 29 ++++++++++++++++++++++++++++- ecs/pkg/amazon/sdk.go | 6 ++++++ 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/ecs/pkg/amazon/cloudformation.go b/ecs/pkg/amazon/cloudformation.go index 44a952fec..32afb3466 100644 --- a/ecs/pkg/amazon/cloudformation.go +++ b/ecs/pkg/amazon/cloudformation.go @@ -1,7 +1,11 @@ package amazon import ( +<<<<<<< HEAD "context" +======= + "errors" +>>>>>>> a0701b8... move to sdk "fmt" "strings" @@ -18,7 +22,7 @@ import ( func (c client) Convert(ctx context.Context, project *compose.Project) (*cloudformation.Template, error) { template := cloudformation.NewTemplate() - vpc, err := c.api.GetDefaultVPC(ctx) + vpc, err := c.GetVPC(ctx, project) if err != nil { return nil, err } @@ -83,6 +87,28 @@ func (c client) Convert(ctx context.Context, project *compose.Project) (*cloudfo return template, nil } +func (c client) GetVPC(project *compose.Project) (string, error) { + //check compose file for the default external network + if net, ok := project.Networks["default"]; ok { + if net.External.External { + vpc := net.Name + ok, err := c.api.VpcExists(vpc) + if err != nil { + return "", err + } + if !ok { + return "", errors.New("Vpc does not exist: " + vpc) + } + return vpc, nil + } + } + defaultVPC, err := c.api.GetDefaultVPC() + if err != nil { + return "", err + } + return defaultVPC, nil +} + const ECSTaskExecutionPolicy = "arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy" var defaultTaskExecutionRole string @@ -118,6 +144,7 @@ func (c client) GetEcsTaskExecutionRole(ctx context.Context, spec types.ServiceC type convertAPI interface { GetDefaultVPC(ctx context.Context) (string, error) + VpcExists(ctx context.Context, vpcID string) (bool, error) GetSubNets(ctx context.Context, vpcID string) ([]string, error) ListRolesForPolicy(ctx context.Context, policy string) ([]string, error) GetRoleArn(ctx context.Context, name string) (string, error) diff --git a/ecs/pkg/amazon/sdk.go b/ecs/pkg/amazon/sdk.go index e3460c0db..b02e0df8c 100644 --- a/ecs/pkg/amazon/sdk.go +++ b/ecs/pkg/amazon/sdk.go @@ -75,6 +75,12 @@ func (s sdk) DeleteCluster(ctx context.Context, name string) error { return fmt.Errorf("Failed to delete cluster, status: %s" + *response.Cluster.Status) } +func (s sdk) VpcExists(ctx context.Context, vpcID string) (bool, error) { + logrus.Debug("Check if VPC exists: ", vpcID) + _, err := s.EC2.DescribeVpcsWithContext(aws.Context(ctx), &ec2.DescribeVpcsInput{VpcIds: []*string{&vpcID}}) + return err == nil, err +} + func (s sdk) GetDefaultVPC(ctx context.Context) (string, error) { logrus.Debug("Retrieve default VPC") vpcs, err := s.EC2.DescribeVpcsWithContext(aws.Context(ctx), &ec2.DescribeVpcsInput{ From 4a6fec63d21b6f98cbfcb298fc7861a99e88ec65 Mon Sep 17 00:00:00 2001 From: aiordache <anca.iordache@docker.com> Date: Tue, 28 Apr 2020 15:11:12 +0200 Subject: [PATCH 038/198] yet another rebase Signed-off-by: aiordache <anca.iordache@docker.com> Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com> --- ecs/pkg/amazon/cloudformation.go | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/ecs/pkg/amazon/cloudformation.go b/ecs/pkg/amazon/cloudformation.go index 32afb3466..e5c4cd71b 100644 --- a/ecs/pkg/amazon/cloudformation.go +++ b/ecs/pkg/amazon/cloudformation.go @@ -1,11 +1,8 @@ package amazon import ( -<<<<<<< HEAD "context" -======= "errors" ->>>>>>> a0701b8... move to sdk "fmt" "strings" @@ -87,12 +84,12 @@ func (c client) Convert(ctx context.Context, project *compose.Project) (*cloudfo return template, nil } -func (c client) GetVPC(project *compose.Project) (string, error) { +func (c client) GetVPC(ctx context.Context, project *compose.Project) (string, error) { //check compose file for the default external network if net, ok := project.Networks["default"]; ok { if net.External.External { vpc := net.Name - ok, err := c.api.VpcExists(vpc) + ok, err := c.api.VpcExists(ctx, vpc) if err != nil { return "", err } @@ -102,7 +99,7 @@ func (c client) GetVPC(project *compose.Project) (string, error) { return vpc, nil } } - defaultVPC, err := c.api.GetDefaultVPC() + defaultVPC, err := c.api.GetDefaultVPC(ctx) if err != nil { return "", err } From de365f41e91019b9d0c8b179721c5870ca52b12b Mon Sep 17 00:00:00 2001 From: Nicolas De Loof <nicolas.deloof@gmail.com> Date: Wed, 29 Apr 2020 15:13:25 +0200 Subject: [PATCH 039/198] Fix test by regenerating mock Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com> --- ecs/pkg/amazon/mock/api.go | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/ecs/pkg/amazon/mock/api.go b/ecs/pkg/amazon/mock/api.go index 59f661775..fd9b01c05 100644 --- a/ecs/pkg/amazon/mock/api.go +++ b/ecs/pkg/amazon/mock/api.go @@ -194,3 +194,18 @@ func (mr *MockAPIMockRecorder) StackExists(arg0, arg1 interface{}) *gomock.Call mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "StackExists", reflect.TypeOf((*MockAPI)(nil).StackExists), arg0, arg1) } + +// VpcExists mocks base method +func (m *MockAPI) VpcExists(arg0 context.Context, arg1 string) (bool, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "VpcExists", arg0, arg1) + ret0, _ := ret[0].(bool) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// VpcExists indicates an expected call of VpcExists +func (mr *MockAPIMockRecorder) VpcExists(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "VpcExists", reflect.TypeOf((*MockAPI)(nil).VpcExists), arg0, arg1) +} From 2ad9504d158586c18c69adb0d3bfd1e4a7ca3e4d Mon Sep 17 00:00:00 2001 From: aiordache <anca.iordache@docker.com> Date: Wed, 29 Apr 2020 16:54:24 +0200 Subject: [PATCH 040/198] add secret interface Signed-off-by: aiordache <anca.iordache@docker.com> Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com> --- ecs/cmd/main/main.go | 95 +++++++++++++++++++++++++++++++++++++++ ecs/pkg/amazon/api.go | 1 + ecs/pkg/amazon/sdk.go | 24 ++++++++++ ecs/pkg/amazon/secrets.go | 28 ++++++++++++ ecs/pkg/compose/api.go | 5 +++ 5 files changed, 153 insertions(+) create mode 100644 ecs/pkg/amazon/secrets.go diff --git a/ecs/cmd/main/main.go b/ecs/cmd/main/main.go index 99f556c7c..9326f3eee 100644 --- a/ecs/cmd/main/main.go +++ b/ecs/cmd/main/main.go @@ -2,6 +2,7 @@ package main import ( "context" + "errors" "fmt" "github.com/docker/cli/cli-plugins/manager" @@ -45,6 +46,7 @@ func NewRootCmd(name string, dockerCli command.Cli) *cobra.Command { cmd.AddCommand( VersionCommand(), ComposeCommand(&opts), + SecretCommand(&opts), ) cmd.Flags().StringVarP(&opts.profile, "profile", "p", "default", "AWS Profile") cmd.Flags().StringVarP(&opts.cluster, "cluster", "c", "default", "ECS cluster") @@ -164,3 +166,96 @@ func DownCommand(clusteropts *clusterOptions, projectOpts *compose.ProjectOption cmd.Flags().BoolVar(&opts.DeleteCluster, "delete-cluster", false, "Delete cluster") return cmd } + +func SecretCommand(clusteropts *clusterOptions) *cobra.Command { + cmd := &cobra.Command{ + Use: "secret", + } + opts := &compose.ProjectOptions{} + opts.AddFlags(cmd.Flags()) + + cmd.AddCommand( + CreateSecret(clusteropts), + InspectSecret(clusteropts), + ListSecrets(clusteropts), + DeleteSecret(clusteropts), + ) + return cmd +} + +type createSecretOptions struct { + Label string +} + +func CreateSecret(clusteropts *clusterOptions) *cobra.Command { + //opts := createSecretOptions{} + cmd := &cobra.Command{ + Use: "create [NAME]", + RunE: func(cmd *cobra.Command, args []string) error { + client, err := amazon.NewClient(clusteropts.profile, clusteropts.cluster, clusteropts.region) + if err != nil { + return err + } + if len(args) == 0 { + return errors.New("Missing mandatory parameter: [NAME]") + } + name := args[0] + content := "blabla" + id, err := client.CreateSecret(context.Background(), name, content) + fmt.Println(id) + return err + }, + } + //cmd.Flags().BoolVar(&opts.Label, "label", false, "Secret label") + return cmd +} + +func InspectSecret(clusteropts *clusterOptions) *cobra.Command { + cmd := &cobra.Command{ + Use: "inspect [NAME]", + RunE: func(cmd *cobra.Command, args []string) error { + client, err := amazon.NewClient(clusteropts.profile, clusteropts.cluster, clusteropts.region) + if err != nil { + return err + } + if len(args) == 0 { + return errors.New("Missing mandatory parameter: [NAME]") + } + name := args[0] + return client.InspectSecret(context.Background(), name) + }, + } + return cmd +} + +func ListSecrets(clusteropts *clusterOptions) *cobra.Command { + cmd := &cobra.Command{ + Use: "list", + Aliases: []string{"ls"}, + RunE: func(cmd *cobra.Command, args []string) error { + client, err := amazon.NewClient(clusteropts.profile, clusteropts.cluster, clusteropts.region) + if err != nil { + return err + } + return client.ListSecrets(context.Background()) + }, + } + return cmd +} + +func DeleteSecret(clusteropts *clusterOptions) *cobra.Command { + cmd := &cobra.Command{ + Use: "delete [NAME]", + RunE: func(cmd *cobra.Command, args []string) error { + client, err := amazon.NewClient(clusteropts.profile, clusteropts.cluster, clusteropts.region) + if err != nil { + return err + } + if len(args) == 0 { + return errors.New("Missing mandatory parameter: [NAME]") + } + return client.DeleteSecret(context.Background(), args[0]) + }, + } + return cmd +} diff --git a/ecs/pkg/amazon/api.go b/ecs/pkg/amazon/api.go index ff61174d0..b4914d68a 100644 --- a/ecs/pkg/amazon/api.go +++ b/ecs/pkg/amazon/api.go @@ -6,4 +6,5 @@ type API interface { downAPI upAPI convertAPI + secretsAPI } diff --git a/ecs/pkg/amazon/sdk.go b/ecs/pkg/amazon/sdk.go index b02e0df8c..2aa017e9f 100644 --- a/ecs/pkg/amazon/sdk.go +++ b/ecs/pkg/amazon/sdk.go @@ -18,6 +18,8 @@ import ( "github.com/aws/aws-sdk-go/service/elbv2/elbv2iface" "github.com/aws/aws-sdk-go/service/iam" "github.com/aws/aws-sdk-go/service/iam/iamiface" + "github.com/aws/aws-sdk-go/service/secretsmanager" + "github.com/aws/aws-sdk-go/service/secretsmanager/secretsmanageriface" cf "github.com/awslabs/goformation/v4/cloudformation" "github.com/sirupsen/logrus" ) @@ -30,6 +32,7 @@ type sdk struct { CW cloudwatchlogsiface.CloudWatchLogsAPI IAM iamiface.IAMAPI CF cloudformationiface.CloudFormationAPI + SM secretsmanageriface.SecretsManagerAPI } func NewAPI(sess *session.Session) API { @@ -40,6 +43,7 @@ func NewAPI(sess *session.Session) API { CW: cloudwatchlogs.New(sess), IAM: iam.New(sess), CF: cloudformation.New(sess), + SM: secretsmanager.New(sess), } } @@ -193,3 +197,23 @@ func (s sdk) DeleteStack(ctx context.Context, name string) error { }) return err } + +func (s sdk) CreateSecret(ctx context.Context, name string, content string) (string, error) { + logrus.Debug("Create secret " + name) + return "test", nil +} + +func (s sdk) InspectSecret(ctx context.Context, name string) error { + fmt.Printf("... done. \n") + return nil +} + +func (s sdk) ListSecrets(ctx context.Context) error { + fmt.Printf("... done. \n") + return nil +} + +func (s sdk) DeleteSecret(ctx context.Context, name string) error { + fmt.Printf("... done. \n") + return nil +} diff --git a/ecs/pkg/amazon/secrets.go b/ecs/pkg/amazon/secrets.go new file mode 100644 index 000000000..daabe79bc --- /dev/null +++ b/ecs/pkg/amazon/secrets.go @@ -0,0 +1,28 @@ +package amazon + +import ( + "context" +) + +type secretsAPI interface { + CreateSecret(ctx context.Context, name string, content string) (string, error) + InspectSecret(ctx context.Context, name string) error + ListSecrets(ctx context.Context) error + DeleteSecret(ctx context.Context, name string) error +} + +func (c client) CreateSecret(ctx context.Context, name string, content string) (string, error) { + return c.api.CreateSecret(ctx, name, content) +} + +func (c client) InspectSecret(ctx context.Context, name string) error { + return c.api.InspectSecret(ctx, name) +} + +func (c client) ListSecrets(ctx context.Context) error { + return c.api.ListSecrets(ctx) +} + +func (c client) DeleteSecret(ctx context.Context, name string) error { + return c.api.DeleteSecret(ctx, name) +} diff --git a/ecs/pkg/compose/api.go b/ecs/pkg/compose/api.go index f5e45bc68..32095102c 100644 --- a/ecs/pkg/compose/api.go +++ b/ecs/pkg/compose/api.go @@ -10,4 +10,9 @@ type API interface { Convert(ctx context.Context, project *Project) (*cloudformation.Template, error) ComposeUp(ctx context.Context, project *Project) error ComposeDown(ctx context.Context, projectName string, deleteCluster bool) error + + CreateSecret(ctx context.Context, name string, content string) (string, error) + InspectSecret(ctx context.Context, name string) error + ListSecrets(ctx context.Context) error + DeleteSecret(ctx context.Context, name string) error } From 41aaf802e39fbb3a5495093a77eeddb6f7606ebb Mon Sep 17 00:00:00 2001 From: aiordache <anca.iordache@docker.com> Date: Wed, 29 Apr 2020 20:36:00 +0200 Subject: [PATCH 041/198] implement secret management Signed-off-by: aiordache <anca.iordache@docker.com> Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com> --- ecs/Makefile | 2 +- ecs/cmd/commands/compose.go | 117 ++++++++++++++++ ecs/cmd/commands/secret.go | 147 ++++++++++++++++++++ ecs/cmd/main.go | 58 ++++++++ ecs/cmd/main/main.go | 261 ------------------------------------ ecs/pkg/amazon/sdk.go | 70 ++++++++-- ecs/pkg/amazon/secrets.go | 18 +-- ecs/pkg/compose/api.go | 9 +- ecs/pkg/docker/secret.go | 20 +++ 9 files changed, 417 insertions(+), 285 deletions(-) create mode 100644 ecs/cmd/commands/compose.go create mode 100644 ecs/cmd/commands/secret.go create mode 100644 ecs/cmd/main.go delete mode 100644 ecs/cmd/main/main.go create mode 100644 ecs/pkg/docker/secret.go diff --git a/ecs/Makefile b/ecs/Makefile index 5d3c5883d..e07af815e 100644 --- a/ecs/Makefile +++ b/ecs/Makefile @@ -2,7 +2,7 @@ clean: rm -rf dist/ build: - go build -v -o dist/docker-ecs cmd/main/main.go + go build -v -o dist/docker-ecs cmd/main.go test: ## Run tests go test ./... -v diff --git a/ecs/cmd/commands/compose.go b/ecs/cmd/commands/compose.go new file mode 100644 index 000000000..972477b3a --- /dev/null +++ b/ecs/cmd/commands/compose.go @@ -0,0 +1,117 @@ +package commands + +import ( + "context" + "fmt" + + "github.com/docker/ecs-plugin/pkg/amazon" + "github.com/docker/ecs-plugin/pkg/compose" + "github.com/spf13/cobra" +) + +type ClusterOptions struct { + Profile string + Region string + Cluster string +} + +func ComposeCommand(clusteropts *ClusterOptions) *cobra.Command { + cmd := &cobra.Command{ + Use: "compose", + } + opts := &compose.ProjectOptions{} + opts.AddFlags(cmd.Flags()) + + cmd.AddCommand( + ConvertCommand(clusteropts, opts), + UpCommand(clusteropts, opts), + DownCommand(clusteropts, opts), + ) + return cmd +} + +type upOptions struct { + loadBalancerArn string +} + +func (o upOptions) LoadBalancerArn() *string { + if o.loadBalancerArn == "" { + return nil + } + return &o.loadBalancerArn +} + +func ConvertCommand(clusteropts *ClusterOptions, projectOpts *compose.ProjectOptions) *cobra.Command { + cmd := &cobra.Command{ + Use: "convert", + RunE: compose.WithProject(projectOpts, func(project *compose.Project, args []string) error { + client, err := amazon.NewClient(clusteropts.Profile, clusteropts.Cluster, clusteropts.Region) + if err != nil { + return err + } + template, err := client.Convert(context.Background(), project) + if err != nil { + return err + } + + j, err := template.JSON() + if err != nil { + fmt.Printf("Failed to generate JSON: %s\n", err) + } else { + fmt.Printf("%s\n", string(j)) + } + return nil + }), + } + return cmd +} + +func UpCommand(clusteropts *ClusterOptions, projectOpts *compose.ProjectOptions) *cobra.Command { + opts := upOptions{} + cmd := &cobra.Command{ + Use: "up", + RunE: compose.WithProject(projectOpts, func(project *compose.Project, args []string) error { + client, err := amazon.NewClient(clusteropts.Profile, clusteropts.Cluster, clusteropts.Region) + if err != nil { + return err + } + return client.ComposeUp(context.Background(), project) + }), + } + cmd.Flags().StringVar(&opts.loadBalancerArn, "load-balancer", "", "") + return cmd +} + +type downOptions struct { + DeleteCluster bool +} + +func DownCommand(clusteropts *ClusterOptions, projectOpts *compose.ProjectOptions) *cobra.Command { + opts := downOptions{} + cmd := &cobra.Command{ + Use: "down", + RunE: func(cmd *cobra.Command, args []string) error { + client, err := amazon.NewClient(clusteropts.Profile, clusteropts.Cluster, clusteropts.Region) + if err != nil { + return err + } + if len(args) == 0 { + project, err := compose.ProjectFromOptions(projectOpts) + if err != nil { + return err + } + return client.ComposeDown(context.Background(), project.Name, opts.DeleteCluster) + } + // project names passed as parameters + for _, name := range args { + err := client.ComposeDown(context.Background(), name, opts.DeleteCluster) + if err != nil { + return err + } + } + return nil + }, + } + cmd.Flags().BoolVar(&opts.DeleteCluster, "delete-cluster", false, "Delete cluster") + return cmd +} diff --git a/ecs/cmd/commands/secret.go b/ecs/cmd/commands/secret.go new file mode 100644 index 000000000..0c228c54b --- /dev/null +++ b/ecs/cmd/commands/secret.go @@ -0,0 +1,147 @@ +package commands + +import ( + "context" + "errors" + "fmt" + "io" + "os" + "strings" + "text/tabwriter" + + "github.com/docker/ecs-plugin/pkg/amazon" + "github.com/docker/ecs-plugin/pkg/docker" + "github.com/spf13/cobra" +) + +type createSecretOptions struct { + Label string +} + +type deleteSecretOptions struct { + recover bool +} + +func SecretCommand(clusteropts *ClusterOptions) *cobra.Command { + cmd := &cobra.Command{ + Use: "secret", + Short: "Manages secrets", + } + + cmd.AddCommand( + CreateSecret(clusteropts), + InspectSecret(clusteropts), + ListSecrets(clusteropts), + DeleteSecret(clusteropts), + ) + return cmd +} + +func CreateSecret(clusteropts *ClusterOptions) *cobra.Command { + //opts := createSecretOptions{} + cmd := &cobra.Command{ + Use: "create NAME SECRET", + Short: "Creates a secret.", + RunE: func(cmd *cobra.Command, args []string) error { + client, err := amazon.NewClient(clusteropts.Profile, clusteropts.Cluster, clusteropts.Region) + if err != nil { + return err + } + if len(args) == 0 { + return errors.New("Missing mandatory parameter: NAME") + } + name := args[0] + secret := args[1] + id, err := client.CreateSecret(context.Background(), name, secret) + fmt.Println(id) + return err + }, + } + return cmd +} + +func InspectSecret(clusteropts *ClusterOptions) *cobra.Command { + cmd := &cobra.Command{ + Use: "inspect ID", + Short: "Displays secret details", + RunE: func(cmd *cobra.Command, args []string) error { + client, err := amazon.NewClient(clusteropts.Profile, clusteropts.Cluster, clusteropts.Region) + if err != nil { + return err + } + if len(args) == 0 { + return errors.New("Missing mandatory parameter: ID") + } + id := args[0] + secret, err := client.InspectSecret(context.Background(), id) + if err != nil { + return err + } + out, err := secret.ToJSON() + if err != nil { + return err + } + fmt.Println(out) + return nil + }, + } + return cmd +} + +func ListSecrets(clusteropts *ClusterOptions) *cobra.Command { + cmd := &cobra.Command{ + Use: "list", + Aliases: []string{"ls"}, + Short: "List secrets stored for the existing account.", + RunE: func(cmd *cobra.Command, args []string) error { + client, err := amazon.NewClient(clusteropts.Profile, clusteropts.Cluster, clusteropts.Region) + if err != nil { + return err + } + secrets, err := client.ListSecrets(context.Background()) + if err != nil { + return err + } + + printList(os.Stdout, secrets) + return nil + }, + } + return cmd +} + +func DeleteSecret(clusteropts *ClusterOptions) *cobra.Command { + opts := deleteSecretOptions{} + cmd := &cobra.Command{ + Use: "delete NAME", + Aliases: []string{"rm", "remove"}, + Short: "Removes a secret.", + RunE: func(cmd *cobra.Command, args []string) error { + client, err := amazon.NewClient(clusteropts.Profile, clusteropts.Cluster, clusteropts.Region) + if err != nil { + return err + } + if len(args) == 0 { + return errors.New("Missing mandatory parameter: [NAME]") + } + return client.DeleteSecret(context.Background(), args[0], opts.recover) + }, + } + cmd.Flags().BoolVar(&opts.recover, "recover", false, "Enable recovery.") + return cmd +} + +func printList(out io.Writer, secrets []docker.Secret) { + printSection(out, len(secrets), func(w io.Writer) { + for _, secret := range secrets { + fmt.Fprintf(w, "%s\t%s\t%s\n", secret.ID, secret.Name, secret.Description) + } + }, "ID", "NAME", "DESCRIPTION") +} + +func printSection(out io.Writer, len int, printer func(io.Writer), headers ...string) { + w := tabwriter.NewWriter(out, 20, 1, 3, ' ', 0) + fmt.Fprintln(w, strings.Join(headers, "\t")) + printer(w) + w.Flush() +} diff --git a/ecs/cmd/main.go b/ecs/cmd/main.go new file mode 100644 index 000000000..72d072a09 --- /dev/null +++ b/ecs/cmd/main.go @@ -0,0 +1,58 @@ +package main + +import ( + "fmt" + + "github.com/docker/cli/cli-plugins/manager" + "github.com/docker/cli/cli-plugins/plugin" + "github.com/docker/cli/cli/command" + commands "github.com/docker/ecs-plugin/cmd/commands" + "github.com/spf13/cobra" +) + +const version = "0.0.1" + +func main() { + plugin.Run(func(dockerCli command.Cli) *cobra.Command { + cmd := NewRootCmd("ecs", dockerCli) + return cmd + }, manager.Metadata{ + SchemaVersion: "0.1.0", + Vendor: "Docker Inc.", + Version: version, + Experimental: true, + }) +} + +// NewRootCmd returns the base root command. +func NewRootCmd(name string, dockerCli command.Cli) *cobra.Command { + var opts commands.ClusterOptions + + cmd := &cobra.Command{ + Short: "Docker ECS", + Long: `run multi-container applications on Amazon ECS.`, + Use: name, + Annotations: map[string]string{"experimentalCLI": "true"}, + } + cmd.AddCommand( + VersionCommand(), + commands.ComposeCommand(&opts), + commands.SecretCommand(&opts), + ) + cmd.Flags().StringVarP(&opts.Profile, "profile", "p", "default", "AWS Profile") + cmd.Flags().StringVarP(&opts.Cluster, "cluster", "c", "default", "ECS cluster") + cmd.Flags().StringVarP(&opts.Region, "region", "r", "", "AWS region") + + return cmd +} + +func VersionCommand() *cobra.Command { + return &cobra.Command{ + Use: "version", + Short: "Show version.", + RunE: func(cmd *cobra.Command, args []string) error { + fmt.Printf("Docker ECS plugin %s\n", version) + return nil + }, + } +} diff --git a/ecs/cmd/main/main.go b/ecs/cmd/main/main.go deleted file mode 100644 index 9326f3eee..000000000 --- a/ecs/cmd/main/main.go +++ /dev/null @@ -1,261 +0,0 @@ -package main - -import ( - "context" - "errors" - "fmt" - - "github.com/docker/cli/cli-plugins/manager" - "github.com/docker/cli/cli-plugins/plugin" - "github.com/docker/cli/cli/command" - "github.com/docker/ecs-plugin/pkg/amazon" - "github.com/docker/ecs-plugin/pkg/compose" - "github.com/spf13/cobra" -) - -const version = "0.0.1" - -func main() { - plugin.Run(func(dockerCli command.Cli) *cobra.Command { - cmd := NewRootCmd("ecs", dockerCli) - return cmd - }, manager.Metadata{ - SchemaVersion: "0.1.0", - Vendor: "Docker Inc.", - Version: version, - Experimental: true, - }) -} - -type clusterOptions struct { - profile string - region string - cluster string -} - -// NewRootCmd returns the base root command. -func NewRootCmd(name string, dockerCli command.Cli) *cobra.Command { - var opts clusterOptions - - cmd := &cobra.Command{ - Short: "Docker ECS", - Long: `run multi-container applications on Amazon ECS.`, - Use: name, - Annotations: map[string]string{"experimentalCLI": "true"}, - } - cmd.AddCommand( - VersionCommand(), - ComposeCommand(&opts), - SecretCommand(&opts), - ) - cmd.Flags().StringVarP(&opts.profile, "profile", "p", "default", "AWS Profile") - cmd.Flags().StringVarP(&opts.cluster, "cluster", "c", "default", "ECS cluster") - cmd.Flags().StringVarP(&opts.region, "region", "r", "", "AWS region") - - return cmd -} - -func VersionCommand() *cobra.Command { - return &cobra.Command{ - Use: "version", - Short: "Show version.", - RunE: func(cmd *cobra.Command, args []string) error { - fmt.Printf("Docker ECS plugin %s\n", version) - return nil - }, - } -} - -func ComposeCommand(clusteropts *clusterOptions) *cobra.Command { - cmd := &cobra.Command{ - Use: "compose", - } - opts := &compose.ProjectOptions{} - opts.AddFlags(cmd.Flags()) - - cmd.AddCommand( - ConvertCommand(clusteropts, opts), - UpCommand(clusteropts, opts), - DownCommand(clusteropts, opts), - ) - return cmd -} - -type upOptions struct { - loadBalancerArn string -} - -func (o upOptions) LoadBalancerArn() *string { - if o.loadBalancerArn == "" { - return nil - } - return &o.loadBalancerArn -} - -func ConvertCommand(clusteropts *clusterOptions, projectOpts *compose.ProjectOptions) *cobra.Command { - cmd := &cobra.Command{ - Use: "convert", - RunE: compose.WithProject(projectOpts, func(project *compose.Project, args []string) error { - client, err := amazon.NewClient(clusteropts.profile, clusteropts.cluster, clusteropts.region) - if err != nil { - return err - } - template, err := client.Convert(context.Background(), project) - if err != nil { - return err - } - - j, err := template.JSON() - if err != nil { - fmt.Printf("Failed to generate JSON: %s\n", err) - } else { - fmt.Printf("%s\n", string(j)) - } - return nil - }), - } - return cmd -} - -func UpCommand(clusteropts *clusterOptions, projectOpts *compose.ProjectOptions) *cobra.Command { - opts := upOptions{} - cmd := &cobra.Command{ - Use: "up", - RunE: compose.WithProject(projectOpts, func(project *compose.Project, args []string) error { - client, err := amazon.NewClient(clusteropts.profile, clusteropts.cluster, clusteropts.region) - if err != nil { - return err - } - return client.ComposeUp(context.Background(), project) - }), - } - cmd.Flags().StringVar(&opts.loadBalancerArn, "load-balancer", "", "") - return cmd -} - -type downOptions struct { - DeleteCluster bool -} - -func DownCommand(clusteropts *clusterOptions, projectOpts *compose.ProjectOptions) *cobra.Command { - opts := downOptions{} - cmd := &cobra.Command{ - Use: "down", - RunE: func(cmd *cobra.Command, args []string) error { - client, err := amazon.NewClient(clusteropts.profile, clusteropts.cluster, clusteropts.region) - if err != nil { - return err - } - if len(args) == 0 { - project, err := compose.ProjectFromOptions(projectOpts) - if err != nil { - return err - } - return client.ComposeDown(context.Background(), project.Name, opts.DeleteCluster) - } - // project names passed as parameters - for _, name := range args { - err := client.ComposeDown(context.Background(), name, opts.DeleteCluster) - if err != nil { - return err - } - } - return nil - }, - } - cmd.Flags().BoolVar(&opts.DeleteCluster, "delete-cluster", false, "Delete cluster") - return cmd -} - -func SecretCommand(clusteropts *clusterOptions) *cobra.Command { - cmd := &cobra.Command{ - Use: "secret", - } - opts := &compose.ProjectOptions{} - opts.AddFlags(cmd.Flags()) - - cmd.AddCommand( - CreateSecret(clusteropts), - InspectSecret(clusteropts), - ListSecrets(clusteropts), - DeleteSecret(clusteropts), - ) - return cmd -} - -type createSecretOptions struct { - Label string -} - -func CreateSecret(clusteropts *clusterOptions) *cobra.Command { - //opts := createSecretOptions{} - cmd := &cobra.Command{ - Use: "create [NAME]", - RunE: func(cmd *cobra.Command, args []string) error { - client, err := amazon.NewClient(clusteropts.profile, clusteropts.cluster, clusteropts.region) - if err != nil { - return err - } - if len(args) == 0 { - return errors.New("Missing mandatory parameter: [NAME]") - } - name := args[0] - content := "blabla" - id, err := client.CreateSecret(context.Background(), name, content) - fmt.Println(id) - return err - }, - } - //cmd.Flags().BoolVar(&opts.Label, "label", false, "Secret label") - return cmd -} - -func InspectSecret(clusteropts *clusterOptions) *cobra.Command { - cmd := &cobra.Command{ - Use: "inspect [NAME]", - RunE: func(cmd *cobra.Command, args []string) error { - client, err := amazon.NewClient(clusteropts.profile, clusteropts.cluster, clusteropts.region) - if err != nil { - return err - } - if len(args) == 0 { - return errors.New("Missing mandatory parameter: [NAME]") - } - name := args[0] - return client.InspectSecret(context.Background(), name) - }, - } - return cmd -} - -func ListSecrets(clusteropts *clusterOptions) *cobra.Command { - cmd := &cobra.Command{ - Use: "list", - Aliases: []string{"ls"}, - RunE: func(cmd *cobra.Command, args []string) error { - client, err := amazon.NewClient(clusteropts.profile, clusteropts.cluster, clusteropts.region) - if err != nil { - return err - } - return client.ListSecrets(context.Background()) - }, - } - return cmd -} - -func DeleteSecret(clusteropts *clusterOptions) *cobra.Command { - cmd := &cobra.Command{ - Use: "delete [NAME]", - RunE: func(cmd *cobra.Command, args []string) error { - client, err := amazon.NewClient(clusteropts.profile, clusteropts.cluster, clusteropts.region) - if err != nil { - return err - } - if len(args) == 0 { - return errors.New("Missing mandatory parameter: [NAME]") - } - return client.DeleteSecret(context.Background(), args[0]) - }, - } - return cmd -} diff --git a/ecs/pkg/amazon/sdk.go b/ecs/pkg/amazon/sdk.go index 2aa017e9f..8f8c34033 100644 --- a/ecs/pkg/amazon/sdk.go +++ b/ecs/pkg/amazon/sdk.go @@ -22,6 +22,8 @@ import ( "github.com/aws/aws-sdk-go/service/secretsmanager/secretsmanageriface" cf "github.com/awslabs/goformation/v4/cloudformation" "github.com/sirupsen/logrus" + + "github.com/docker/ecs-plugin/pkg/docker" ) type sdk struct { @@ -198,22 +200,68 @@ func (s sdk) DeleteStack(ctx context.Context, name string) error { return err } -func (s sdk) CreateSecret(ctx context.Context, name string, content string) (string, error) { +func (s sdk) CreateSecret(ctx context.Context, name string, secret string) (string, error) { logrus.Debug("Create secret " + name) - return "test", nil + response, err := s.SM.CreateSecret(&secretsmanager.CreateSecretInput{Name: &name, SecretString: &secret}) + if err != nil { + return "", err + } + return *response.ARN, nil } -func (s sdk) InspectSecret(ctx context.Context, name string) error { - fmt.Printf("... done. \n") - return nil +func (s sdk) InspectSecret(ctx context.Context, id string) (docker.Secret, error) { + logrus.Debug("Inspect secret " + id) + response, err := s.SM.DescribeSecret(&secretsmanager.DescribeSecretInput{SecretId: &id}) + if err != nil { + return docker.Secret{}, err + } + labels := map[string]string{} + for _, tag := range response.Tags { + labels[*tag.Key] = *tag.Value + } + secret := docker.Secret{ + ID: *response.ARN, + Name: *response.Name, + Labels: labels, + } + if response.Description != nil { + secret.Description = *response.Description + } + return secret, nil } -func (s sdk) ListSecrets(ctx context.Context) error { - fmt.Printf("... done. \n") - return nil +func (s sdk) ListSecrets(ctx context.Context) ([]docker.Secret, error) { + + logrus.Debug("List secrets ...") + response, err := s.SM.ListSecrets(&secretsmanager.ListSecretsInput{}) + if err != nil { + return []docker.Secret{}, err + } + var secrets []docker.Secret + + for _, sec := range response.SecretList { + + labels := map[string]string{} + for _, tag := range sec.Tags { + labels[*tag.Key] = *tag.Value + } + description := "" + if sec.Description != nil { + description = *sec.Description + } + secrets = append(secrets, docker.Secret{ + ID: *sec.ARN, + Name: *sec.Name, + Labels: labels, + Description: description, + }) + } + return secrets, nil } -func (s sdk) DeleteSecret(ctx context.Context, name string) error { - fmt.Printf("... done. \n") - return nil +func (s sdk) DeleteSecret(ctx context.Context, id string, recover bool) error { + logrus.Debug("List secrets ...") + force := !recover + _, err := s.SM.DeleteSecret(&secretsmanager.DeleteSecretInput{SecretId: &id, ForceDeleteWithoutRecovery: &force}) + return err } diff --git a/ecs/pkg/amazon/secrets.go b/ecs/pkg/amazon/secrets.go index daabe79bc..649705f02 100644 --- a/ecs/pkg/amazon/secrets.go +++ b/ecs/pkg/amazon/secrets.go @@ -2,27 +2,29 @@ package amazon import ( "context" + + "github.com/docker/ecs-plugin/pkg/docker" ) type secretsAPI interface { CreateSecret(ctx context.Context, name string, content string) (string, error) - InspectSecret(ctx context.Context, name string) error - ListSecrets(ctx context.Context) error - DeleteSecret(ctx context.Context, name string) error + InspectSecret(ctx context.Context, id string) (docker.Secret, error) + ListSecrets(ctx context.Context) ([]docker.Secret, error) + DeleteSecret(ctx context.Context, id string, recover bool) error } func (c client) CreateSecret(ctx context.Context, name string, content string) (string, error) { return c.api.CreateSecret(ctx, name, content) } -func (c client) InspectSecret(ctx context.Context, name string) error { - return c.api.InspectSecret(ctx, name) +func (c client) InspectSecret(ctx context.Context, id string) (docker.Secret, error) { + return c.api.InspectSecret(ctx, id) } -func (c client) ListSecrets(ctx context.Context) error { +func (c client) ListSecrets(ctx context.Context) ([]docker.Secret, error) { return c.api.ListSecrets(ctx) } -func (c client) DeleteSecret(ctx context.Context, name string) error { - return c.api.DeleteSecret(ctx, name) +func (c client) DeleteSecret(ctx context.Context, id string, recover bool) error { + return c.api.DeleteSecret(ctx, id, recover) } diff --git a/ecs/pkg/compose/api.go b/ecs/pkg/compose/api.go index 32095102c..e23651b63 100644 --- a/ecs/pkg/compose/api.go +++ b/ecs/pkg/compose/api.go @@ -4,6 +4,7 @@ import ( "context" "github.com/awslabs/goformation/v4/cloudformation" + "github.com/docker/ecs-plugin/pkg/docker" ) type API interface { @@ -11,8 +12,8 @@ type API interface { ComposeUp(ctx context.Context, project *Project) error ComposeDown(ctx context.Context, projectName string, deleteCluster bool) error - CreateSecret(ctx context.Context, name string, content string) (string, error) - InspectSecret(ctx context.Context, name string) error - ListSecrets(ctx context.Context) error - DeleteSecret(ctx context.Context, name string) error + CreateSecret(ctx context.Context, name string, secret string) (string, error) + InspectSecret(ctx context.Context, id string) (docker.Secret, error) + ListSecrets(ctx context.Context) ([]docker.Secret, error) + DeleteSecret(ctx context.Context, id string, recover bool) error } diff --git a/ecs/pkg/docker/secret.go b/ecs/pkg/docker/secret.go new file mode 100644 index 000000000..0efae5d67 --- /dev/null +++ b/ecs/pkg/docker/secret.go @@ -0,0 +1,20 @@ +package docker + +import ( + "encoding/json" +) + +type Secret struct { + ID string `json:"ID"` + Name string `json:"Name"` + Labels map[string]string `json:"Labels"` + Description string `json:"Description"` +} + +func (s Secret) ToJSON() (string, error) { + b, err := json.MarshalIndent(&s, "", "\t") + if err != nil { + return "", err + } + return string(b), nil +} From 678f4018f077ea6e274c499995b268365fdd2c21 Mon Sep 17 00:00:00 2001 From: Nicolas De Loof <nicolas.deloof@gmail.com> Date: Tue, 28 Apr 2020 16:48:17 +0200 Subject: [PATCH 042/198] Collect events while waiting for stack to complete Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com> --- ecs/pkg/amazon/mock/api.go | 26 ++++++++++++++++----- ecs/pkg/amazon/sdk.go | 46 +++++++++++++++++++++++++++++++++----- ecs/pkg/amazon/up.go | 26 +++++++++++++++++++-- 3 files changed, 86 insertions(+), 12 deletions(-) diff --git a/ecs/pkg/amazon/mock/api.go b/ecs/pkg/amazon/mock/api.go index fd9b01c05..571f0bf24 100644 --- a/ecs/pkg/amazon/mock/api.go +++ b/ecs/pkg/amazon/mock/api.go @@ -6,7 +6,8 @@ package mock import ( context "context" - cloudformation "github.com/awslabs/goformation/v4/cloudformation" + cloudformation "github.com/aws/aws-sdk-go/service/cloudformation" + cloudformation0 "github.com/awslabs/goformation/v4/cloudformation" gomock "github.com/golang/mock/gomock" reflect "reflect" ) @@ -65,7 +66,7 @@ func (mr *MockAPIMockRecorder) CreateCluster(arg0, arg1 interface{}) *gomock.Cal } // CreateStack mocks base method -func (m *MockAPI) CreateStack(arg0 context.Context, arg1 string, arg2 *cloudformation.Template) error { +func (m *MockAPI) CreateStack(arg0 context.Context, arg1 string, arg2 *cloudformation0.Template) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "CreateStack", arg0, arg1, arg2) ret0, _ := ret[0].(error) @@ -107,11 +108,12 @@ func (mr *MockAPIMockRecorder) DeleteStack(arg0, arg1 interface{}) *gomock.Call } // DescribeStackEvents mocks base method -func (m *MockAPI) DescribeStackEvents(arg0 context.Context, arg1 string) error { +func (m *MockAPI) DescribeStackEvents(arg0 context.Context, arg1 string) ([]*cloudformation.StackEvent, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "DescribeStackEvents", arg0, arg1) - ret0, _ := ret[0].(error) - return ret0 + ret0, _ := ret[0].([]*cloudformation.StackEvent) + ret1, _ := ret[1].(error) + return ret0, ret1 } // DescribeStackEvents indicates an expected call of DescribeStackEvents @@ -209,3 +211,17 @@ func (mr *MockAPIMockRecorder) VpcExists(arg0, arg1 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "VpcExists", reflect.TypeOf((*MockAPI)(nil).VpcExists), arg0, arg1) } + +// WaitStackComplete mocks base method +func (m *MockAPI) WaitStackComplete(arg0 context.Context, arg1 string, arg2 func() error) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "WaitStackComplete", arg0, arg1, arg2) + ret0, _ := ret[0].(error) + return ret0 +} + +// WaitStackComplete indicates an expected call of WaitStackComplete +func (mr *MockAPIMockRecorder) WaitStackComplete(arg0, arg1, arg2 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "WaitStackComplete", reflect.TypeOf((*MockAPI)(nil).WaitStackComplete), arg0, arg1, arg2) +} diff --git a/ecs/pkg/amazon/sdk.go b/ecs/pkg/amazon/sdk.go index 8f8c34033..95897511b 100644 --- a/ecs/pkg/amazon/sdk.go +++ b/ecs/pkg/amazon/sdk.go @@ -3,6 +3,8 @@ package amazon import ( "context" "fmt" + "strings" + "time" "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws/session" @@ -183,13 +185,47 @@ func (s sdk) CreateStack(ctx context.Context, name string, template *cf.Template }) return err } +func (s sdk) WaitStackComplete(ctx context.Context, name string, fn func() error) error { + for i := 0; i < 120; i++ { + stacks, err := s.CF.DescribeStacks(&cloudformation.DescribeStacksInput{ + StackName: aws.String(name), + }) + if err != nil { + return err + } -func (s sdk) DescribeStackEvents(ctx context.Context, name string) error { + err = fn() + if err != nil { + return err + } + + status := *stacks.Stacks[0].StackStatus + if strings.HasSuffix(status, "_COMPLETE") || strings.HasSuffix(status, "_FAILED") { + return nil + } + time.Sleep(1 * time.Second) + } + return fmt.Errorf("120s timeout waiting for CloudFormation stack %s to complete", name) +} + +func (s sdk) DescribeStackEvents(ctx context.Context, name string) ([]*cloudformation.StackEvent, error) { // Fixme implement Paginator on Events and return as a chan(events) - _, err := s.CF.DescribeStackEventsWithContext(aws.Context(ctx), &cloudformation.DescribeStackEventsInput{ - StackName: aws.String(name), - }) - return err + events := []*cloudformation.StackEvent{} + var nextToken *string + for { + resp, err := s.CF.DescribeStackEventsWithContext(aws.Context(ctx), &cloudformation.DescribeStackEventsInput{ + StackName: aws.String(name), + NextToken: nextToken, + }) + if err != nil { + return nil, err + } + events = append(events, resp.StackEvents...) + if resp.NextToken == nil { + return events, nil + } + nextToken = resp.NextToken + } } func (s sdk) DeleteStack(ctx context.Context, name string) error { diff --git a/ecs/pkg/amazon/up.go b/ecs/pkg/amazon/up.go index 41b4f5cba..5b964b6eb 100644 --- a/ecs/pkg/amazon/up.go +++ b/ecs/pkg/amazon/up.go @@ -4,6 +4,8 @@ import ( "context" "fmt" + cf "github.com/aws/aws-sdk-go/service/cloudformation" + "github.com/awslabs/goformation/v4/cloudformation" "github.com/docker/ecs-plugin/pkg/compose" ) @@ -34,7 +36,26 @@ func (c *client) ComposeUp(ctx context.Context, project *compose.Project) error return err } - err = c.api.DescribeStackEvents(ctx, project.Name) + known := map[string]struct{}{} + err = c.api.WaitStackComplete(ctx, project.Name, func() error { + events, err := c.api.DescribeStackEvents(ctx, project.Name) + if err != nil { + return err + } + for _, event := range events { + if _, ok := known[*event.EventId]; ok { + continue + } + known[*event.EventId] = struct{}{} + + description := "-" + if event.ResourceStatusReason != nil { + description = *event.ResourceStatusReason + } + fmt.Printf("%s %q %s %s\n", *event.ResourceType, *event.LogicalResourceId, *event.ResourceStatus, description) + } + return nil + }) if err != nil { return err } @@ -48,5 +69,6 @@ type upAPI interface { CreateCluster(ctx context.Context, name string) (string, error) StackExists(ctx context.Context, name string) (bool, error) CreateStack(ctx context.Context, name string, template *cloudformation.Template) error - DescribeStackEvents(ctx context.Context, stack string) error + WaitStackComplete(ctx context.Context, name string, fn func() error) error + DescribeStackEvents(ctx context.Context, stack string) ([]*cf.StackEvent, error) } From 5d61fc119a5618b4bf5619b8d3ffd58929bf67f7 Mon Sep 17 00:00:00 2001 From: Nicolas De Loof <nicolas.deloof@gmail.com> Date: Wed, 29 Apr 2020 15:08:26 +0200 Subject: [PATCH 043/198] Format stack events as a set of resources with progress status This mimic how docker-compose report containers creation Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com> --- ecs/pkg/amazon/up.go | 17 ++-- ecs/pkg/console/progress.go | 134 +++++++++++++++++++++++++++++++ ecs/pkg/console/progress_test.go | 65 +++++++++++++++ 3 files changed, 210 insertions(+), 6 deletions(-) create mode 100644 ecs/pkg/console/progress.go create mode 100644 ecs/pkg/console/progress_test.go diff --git a/ecs/pkg/amazon/up.go b/ecs/pkg/amazon/up.go index 5b964b6eb..c7a504098 100644 --- a/ecs/pkg/amazon/up.go +++ b/ecs/pkg/amazon/up.go @@ -3,11 +3,13 @@ package amazon import ( "context" "fmt" + "sort" + "github.com/aws/aws-sdk-go/aws" cf "github.com/aws/aws-sdk-go/service/cloudformation" - "github.com/awslabs/goformation/v4/cloudformation" "github.com/docker/ecs-plugin/pkg/compose" + "github.com/docker/ecs-plugin/pkg/console" ) func (c *client) ComposeUp(ctx context.Context, project *compose.Project) error { @@ -36,23 +38,26 @@ func (c *client) ComposeUp(ctx context.Context, project *compose.Project) error return err } + w := console.NewProgressWriter() known := map[string]struct{}{} err = c.api.WaitStackComplete(ctx, project.Name, func() error { events, err := c.api.DescribeStackEvents(ctx, project.Name) if err != nil { return err } + + sort.Slice(events, func(i, j int) bool { + return events[i].Timestamp.Before(*events[j].Timestamp) + }) + for _, event := range events { if _, ok := known[*event.EventId]; ok { continue } known[*event.EventId] = struct{}{} - description := "-" - if event.ResourceStatusReason != nil { - description = *event.ResourceStatusReason - } - fmt.Printf("%s %q %s %s\n", *event.ResourceType, *event.LogicalResourceId, *event.ResourceStatus, description) + resource := fmt.Sprintf("%s %q", aws.StringValue(event.ResourceType), aws.StringValue(event.LogicalResourceId)) + w.ResourceEvent(resource, aws.StringValue(event.ResourceStatus), aws.StringValue(event.ResourceStatusReason)) } return nil }) diff --git a/ecs/pkg/console/progress.go b/ecs/pkg/console/progress.go new file mode 100644 index 000000000..8ada4df8a --- /dev/null +++ b/ecs/pkg/console/progress.go @@ -0,0 +1,134 @@ +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 +} + +func NewProgressWriter() *progress { + return &progress{ + console: ansiConsole{os.Stdout}, + } +} + +const ( + cyan = "36;1" + 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) { + if i == 0 { + return + } + fmt.Fprintf(c.out, "\033[%dA", i) // nolint:errcheck +} + +func (c ansiConsole) MoveDown(i int) { + if i == 0 { + return + } + 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(cyan, 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 new file mode 100644 index 000000000..552303c2d --- /dev/null +++ b/ecs/pkg/console/progress_test.go @@ -0,0 +1,65 @@ +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 +} From 814259ae33be3daf17a008194892519185e6b69a Mon Sep 17 00:00:00 2001 From: Nicolas De Loof <nicolas.deloof@gmail.com> Date: Wed, 29 Apr 2020 15:26:11 +0200 Subject: [PATCH 044/198] Also wait for deletion events Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com> --- ecs/pkg/amazon/down.go | 5 ++++ ecs/pkg/amazon/down_test.go | 2 ++ ecs/pkg/amazon/up.go | 37 ++--------------------------- ecs/pkg/amazon/wait.go | 46 +++++++++++++++++++++++++++++++++++++ ecs/pkg/console/progress.go | 6 ++++- 5 files changed, 60 insertions(+), 36 deletions(-) create mode 100644 ecs/pkg/amazon/wait.go diff --git a/ecs/pkg/amazon/down.go b/ecs/pkg/amazon/down.go index ab708d21f..616d6659d 100644 --- a/ecs/pkg/amazon/down.go +++ b/ecs/pkg/amazon/down.go @@ -12,6 +12,11 @@ func (c *client) ComposeDown(ctx context.Context, projectName string, deleteClus } fmt.Printf("Delete stack ") + err = c.WaitStackCompletion(ctx, projectName) + if err != nil { + return err + } + if !deleteCluster { return nil } diff --git a/ecs/pkg/amazon/down_test.go b/ecs/pkg/amazon/down_test.go index 3bfc1e40d..49e5fe2ac 100644 --- a/ecs/pkg/amazon/down_test.go +++ b/ecs/pkg/amazon/down_test.go @@ -20,6 +20,7 @@ func TestDownDontDeleteCluster(t *testing.T) { ctx := context.TODO() recorder := m.EXPECT() recorder.DeleteStack(ctx, "test_project").Return(nil).Times(1) + recorder.WaitStackComplete(ctx, "test_project", gomock.Any()).Return(nil).Times(1) c.ComposeDown(ctx, "test_project", false) } @@ -37,6 +38,7 @@ func TestDownDeleteCluster(t *testing.T) { ctx := context.TODO() recorder := m.EXPECT() recorder.DeleteStack(ctx, "test_project").Return(nil).Times(1) + recorder.WaitStackComplete(ctx, "test_project", gomock.Any()).Return(nil).Times(1) recorder.DeleteCluster(ctx, "test_cluster").Return(nil).Times(1) c.ComposeDown(ctx, "test_project", true) diff --git a/ecs/pkg/amazon/up.go b/ecs/pkg/amazon/up.go index c7a504098..0ee37e5fb 100644 --- a/ecs/pkg/amazon/up.go +++ b/ecs/pkg/amazon/up.go @@ -3,13 +3,9 @@ package amazon import ( "context" "fmt" - "sort" - "github.com/aws/aws-sdk-go/aws" - cf "github.com/aws/aws-sdk-go/service/cloudformation" "github.com/awslabs/goformation/v4/cloudformation" "github.com/docker/ecs-plugin/pkg/compose" - "github.com/docker/ecs-plugin/pkg/console" ) func (c *client) ComposeUp(ctx context.Context, project *compose.Project) error { @@ -38,42 +34,13 @@ func (c *client) ComposeUp(ctx context.Context, project *compose.Project) error return err } - w := console.NewProgressWriter() - known := map[string]struct{}{} - err = c.api.WaitStackComplete(ctx, project.Name, func() error { - events, err := c.api.DescribeStackEvents(ctx, project.Name) - if err != nil { - return err - } - - sort.Slice(events, func(i, j int) bool { - return events[i].Timestamp.Before(*events[j].Timestamp) - }) - - for _, event := range events { - if _, ok := known[*event.EventId]; ok { - continue - } - known[*event.EventId] = struct{}{} - - resource := fmt.Sprintf("%s %q", aws.StringValue(event.ResourceType), aws.StringValue(event.LogicalResourceId)) - w.ResourceEvent(resource, aws.StringValue(event.ResourceStatus), aws.StringValue(event.ResourceStatusReason)) - } - return nil - }) - if err != nil { - return err - } - - // TODO monitor progress - return nil + return c.WaitStackCompletion(ctx, project.Name) } type upAPI interface { + waitAPI ClusterExists(ctx context.Context, name string) (bool, error) CreateCluster(ctx context.Context, name string) (string, error) StackExists(ctx context.Context, name string) (bool, error) CreateStack(ctx context.Context, name string, template *cloudformation.Template) error - WaitStackComplete(ctx context.Context, name string, fn func() error) error - DescribeStackEvents(ctx context.Context, stack string) ([]*cf.StackEvent, error) } diff --git a/ecs/pkg/amazon/wait.go b/ecs/pkg/amazon/wait.go new file mode 100644 index 000000000..f3a25c897 --- /dev/null +++ b/ecs/pkg/amazon/wait.go @@ -0,0 +1,46 @@ +package amazon + +import ( + "context" + "fmt" + "sort" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/cloudformation" + "github.com/docker/ecs-plugin/pkg/console" +) + +func (c *client) WaitStackCompletion(ctx context.Context, name string) error { + w := console.NewProgressWriter() + known := map[string]struct{}{} + err := c.api.WaitStackComplete(ctx, name, func() error { + events, err := c.api.DescribeStackEvents(ctx, name) + if err != nil { + return err + } + + sort.Slice(events, func(i, j int) bool { + return events[i].Timestamp.Before(*events[j].Timestamp) + }) + + for _, event := range events { + if _, ok := known[*event.EventId]; ok { + continue + } + known[*event.EventId] = struct{}{} + + resource := fmt.Sprintf("%s %q", aws.StringValue(event.ResourceType), aws.StringValue(event.LogicalResourceId)) + w.ResourceEvent(resource, aws.StringValue(event.ResourceStatus), aws.StringValue(event.ResourceStatusReason)) + } + return nil + }) + if err != nil { + return err + } + return nil +} + +type waitAPI interface { + WaitStackComplete(ctx context.Context, name string, fn func() error) error + DescribeStackEvents(ctx context.Context, stack string) ([]*cloudformation.StackEvent, error) +} diff --git a/ecs/pkg/console/progress.go b/ecs/pkg/console/progress.go index 8ada4df8a..157ee8131 100644 --- a/ecs/pkg/console/progress.go +++ b/ecs/pkg/console/progress.go @@ -21,7 +21,11 @@ type progress struct { resources []*resource } -func NewProgressWriter() *progress { +type ProgressWriter interface { + ResourceEvent(name string, status string, details string) +} + +func NewProgressWriter() ProgressWriter { return &progress{ console: ansiConsole{os.Stdout}, } From f69ada632a46d3ff1f8160cc9c81359239f26e7b Mon Sep 17 00:00:00 2001 From: Nicolas De Loof <nicolas.deloof@gmail.com> Date: Wed, 29 Apr 2020 15:35:34 +0200 Subject: [PATCH 045/198] use faint for "in progress" to distingish completed/failed Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com> --- ecs/pkg/console/progress.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ecs/pkg/console/progress.go b/ecs/pkg/console/progress.go index 157ee8131..6943dd8ad 100644 --- a/ecs/pkg/console/progress.go +++ b/ecs/pkg/console/progress.go @@ -32,7 +32,7 @@ func NewProgressWriter() ProgressWriter { } const ( - cyan = "36;1" + blue = "36;2" red = "31;1" green = "32;1" ) @@ -126,7 +126,7 @@ func (c ansiConsole) KO(s string) string { } func (c ansiConsole) WiP(s string) string { - return ansiColor(cyan, s) + return ansiColor(blue, s) } func ansiColor(code, s string) string { From a8e963a304ab932a97fa71a3058356a94d97c995 Mon Sep 17 00:00:00 2001 From: Nicolas De Loof <nicolas.deloof@gmail.com> Date: Thu, 30 Apr 2020 15:42:34 +0200 Subject: [PATCH 046/198] Query stack events by stack ID (not name) This prevent a race condition on `down` as stack is deleted and we still ask for stack events as we didn't recieved the DELETE_COMPLETE one Use WaitUntilStack* to detect stack operation completion Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com> --- ecs/pkg/amazon/down.go | 3 +- ecs/pkg/amazon/mock/api.go | 17 ++++++++- ecs/pkg/amazon/sdk.go | 71 ++++++++++++++++++------------------- ecs/pkg/amazon/up.go | 2 +- ecs/pkg/amazon/wait.go | 53 ++++++++++++++++++++------- ecs/pkg/console/progress.go | 6 ---- ecs/pkg/convert/convert.go | 1 - 7 files changed, 93 insertions(+), 60 deletions(-) diff --git a/ecs/pkg/amazon/down.go b/ecs/pkg/amazon/down.go index 616d6659d..d9bee735c 100644 --- a/ecs/pkg/amazon/down.go +++ b/ecs/pkg/amazon/down.go @@ -10,9 +10,8 @@ func (c *client) ComposeDown(ctx context.Context, projectName string, deleteClus if err != nil { return err } - fmt.Printf("Delete stack ") - err = c.WaitStackCompletion(ctx, projectName) + err = c.WaitStackCompletion(ctx, projectName, StackDelete) if err != nil { return err } diff --git a/ecs/pkg/amazon/mock/api.go b/ecs/pkg/amazon/mock/api.go index 571f0bf24..81b0829db 100644 --- a/ecs/pkg/amazon/mock/api.go +++ b/ecs/pkg/amazon/mock/api.go @@ -152,6 +152,21 @@ func (mr *MockAPIMockRecorder) GetRoleArn(arg0, arg1 interface{}) *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetRoleArn", reflect.TypeOf((*MockAPI)(nil).GetRoleArn), arg0, arg1) } +// GetStackID mocks base method +func (m *MockAPI) GetStackID(arg0 context.Context, arg1 string) (string, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetStackID", arg0, arg1) + ret0, _ := ret[0].(string) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetStackID indicates an expected call of GetStackID +func (mr *MockAPIMockRecorder) GetStackID(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetStackID", reflect.TypeOf((*MockAPI)(nil).GetStackID), arg0, arg1) +} + // GetSubNets mocks base method func (m *MockAPI) GetSubNets(arg0 context.Context, arg1 string) ([]string, error) { m.ctrl.T.Helper() @@ -213,7 +228,7 @@ func (mr *MockAPIMockRecorder) VpcExists(arg0, arg1 interface{}) *gomock.Call { } // WaitStackComplete mocks base method -func (m *MockAPI) WaitStackComplete(arg0 context.Context, arg1 string, arg2 func() error) error { +func (m *MockAPI) WaitStackComplete(arg0 context.Context, arg1 string, arg2 int) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "WaitStackComplete", arg0, arg1, arg2) ret0, _ := ret[0].(error) diff --git a/ecs/pkg/amazon/sdk.go b/ecs/pkg/amazon/sdk.go index 95897511b..29ca7f7bd 100644 --- a/ecs/pkg/amazon/sdk.go +++ b/ecs/pkg/amazon/sdk.go @@ -3,8 +3,6 @@ package amazon import ( "context" "fmt" - "strings" - "time" "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws/session" @@ -53,7 +51,7 @@ func NewAPI(sess *session.Session) API { func (s sdk) ClusterExists(ctx context.Context, name string) (bool, error) { logrus.Debug("Check if cluster was already created: ", name) - clusters, err := s.ECS.DescribeClustersWithContext(aws.Context(ctx), &ecs.DescribeClustersInput{ + clusters, err := s.ECS.DescribeClustersWithContext(ctx, &ecs.DescribeClustersInput{ Clusters: []*string{aws.String(name)}, }) if err != nil { @@ -64,7 +62,7 @@ func (s sdk) ClusterExists(ctx context.Context, name string) (bool, error) { func (s sdk) CreateCluster(ctx context.Context, name string) (string, error) { logrus.Debug("Create cluster ", name) - response, err := s.ECS.CreateClusterWithContext(aws.Context(ctx), &ecs.CreateClusterInput{ClusterName: aws.String(name)}) + response, err := s.ECS.CreateClusterWithContext(ctx, &ecs.CreateClusterInput{ClusterName: aws.String(name)}) if err != nil { return "", err } @@ -73,7 +71,7 @@ func (s sdk) CreateCluster(ctx context.Context, name string) (string, error) { func (s sdk) DeleteCluster(ctx context.Context, name string) error { logrus.Debug("Delete cluster ", name) - response, err := s.ECS.DeleteClusterWithContext(aws.Context(ctx), &ecs.DeleteClusterInput{Cluster: aws.String(name)}) + response, err := s.ECS.DeleteClusterWithContext(ctx, &ecs.DeleteClusterInput{Cluster: aws.String(name)}) if err != nil { return err } @@ -85,13 +83,13 @@ func (s sdk) DeleteCluster(ctx context.Context, name string) error { func (s sdk) VpcExists(ctx context.Context, vpcID string) (bool, error) { logrus.Debug("Check if VPC exists: ", vpcID) - _, err := s.EC2.DescribeVpcsWithContext(aws.Context(ctx), &ec2.DescribeVpcsInput{VpcIds: []*string{&vpcID}}) + _, err := s.EC2.DescribeVpcsWithContext(ctx, &ec2.DescribeVpcsInput{VpcIds: []*string{&vpcID}}) return err == nil, err } func (s sdk) GetDefaultVPC(ctx context.Context) (string, error) { logrus.Debug("Retrieve default VPC") - vpcs, err := s.EC2.DescribeVpcsWithContext(aws.Context(ctx), &ec2.DescribeVpcsInput{ + vpcs, err := s.EC2.DescribeVpcsWithContext(ctx, &ec2.DescribeVpcsInput{ Filters: []*ec2.Filter{ { Name: aws.String("isDefault"), @@ -110,7 +108,7 @@ func (s sdk) GetDefaultVPC(ctx context.Context) (string, error) { func (s sdk) GetSubNets(ctx context.Context, vpcID string) ([]string, error) { logrus.Debug("Retrieve SubNets") - subnets, err := s.EC2.DescribeSubnetsWithContext(aws.Context(ctx), &ec2.DescribeSubnetsInput{ + subnets, err := s.EC2.DescribeSubnetsWithContext(ctx, &ec2.DescribeSubnetsInput{ DryRun: nil, Filters: []*ec2.Filter{ { @@ -135,7 +133,7 @@ func (s sdk) GetSubNets(ctx context.Context, vpcID string) ([]string, error) { } func (s sdk) ListRolesForPolicy(ctx context.Context, policy string) ([]string, error) { - entities, err := s.IAM.ListEntitiesForPolicyWithContext(aws.Context(ctx), &iam.ListEntitiesForPolicyInput{ + entities, err := s.IAM.ListEntitiesForPolicyWithContext(ctx, &iam.ListEntitiesForPolicyInput{ EntityFilter: aws.String("Role"), PolicyArn: aws.String(policy), }) @@ -150,7 +148,7 @@ func (s sdk) ListRolesForPolicy(ctx context.Context, policy string) ([]string, e } func (s sdk) GetRoleArn(ctx context.Context, name string) (string, error) { - role, err := s.IAM.GetRoleWithContext(aws.Context(ctx), &iam.GetRoleInput{ + role, err := s.IAM.GetRoleWithContext(ctx, &iam.GetRoleInput{ RoleName: aws.String(name), }) if err != nil { @@ -160,7 +158,7 @@ func (s sdk) GetRoleArn(ctx context.Context, name string) (string, error) { } func (s sdk) StackExists(ctx context.Context, name string) (bool, error) { - stacks, err := s.CF.DescribeStacksWithContext(aws.Context(ctx), &cloudformation.DescribeStacksInput{ + stacks, err := s.CF.DescribeStacksWithContext(ctx, &cloudformation.DescribeStacksInput{ StackName: aws.String(name), }) if err != nil { @@ -177,7 +175,7 @@ func (s sdk) CreateStack(ctx context.Context, name string, template *cf.Template return err } - _, err = s.CF.CreateStackWithContext(aws.Context(ctx), &cloudformation.CreateStackInput{ + _, err = s.CF.CreateStackWithContext(ctx, &cloudformation.CreateStackInput{ OnFailure: aws.String("DELETE"), StackName: aws.String(name), TemplateBody: aws.String(string(json)), @@ -185,36 +183,37 @@ func (s sdk) CreateStack(ctx context.Context, name string, template *cf.Template }) return err } -func (s sdk) WaitStackComplete(ctx context.Context, name string, fn func() error) error { - for i := 0; i < 120; i++ { - stacks, err := s.CF.DescribeStacks(&cloudformation.DescribeStacksInput{ - StackName: aws.String(name), - }) - if err != nil { - return err - } - - err = fn() - if err != nil { - return err - } - - status := *stacks.Stacks[0].StackStatus - if strings.HasSuffix(status, "_COMPLETE") || strings.HasSuffix(status, "_FAILED") { - return nil - } - time.Sleep(1 * time.Second) +func (s sdk) WaitStackComplete(ctx context.Context, name string, operation int) error { + input := &cloudformation.DescribeStacksInput{ + StackName: aws.String(name), + } + switch operation { + case StackCreate: + return s.CF.WaitUntilStackCreateCompleteWithContext(ctx, input) + case StackDelete: + return s.CF.WaitUntilStackDeleteCompleteWithContext(ctx, input) + default: + return fmt.Errorf("internal error: unexpected stack operation %d", operation) } - return fmt.Errorf("120s timeout waiting for CloudFormation stack %s to complete", name) } -func (s sdk) DescribeStackEvents(ctx context.Context, name string) ([]*cloudformation.StackEvent, error) { +func (s sdk) GetStackID(ctx context.Context, name string) (string, error) { + stacks, err := s.CF.DescribeStacksWithContext(ctx, &cloudformation.DescribeStacksInput{ + StackName: aws.String(name), + }) + if err != nil { + return "", err + } + return *stacks.Stacks[0].StackId, nil +} + +func (s sdk) DescribeStackEvents(ctx context.Context, stackID string) ([]*cloudformation.StackEvent, error) { // Fixme implement Paginator on Events and return as a chan(events) events := []*cloudformation.StackEvent{} var nextToken *string for { - resp, err := s.CF.DescribeStackEventsWithContext(aws.Context(ctx), &cloudformation.DescribeStackEventsInput{ - StackName: aws.String(name), + resp, err := s.CF.DescribeStackEventsWithContext(ctx, &cloudformation.DescribeStackEventsInput{ + StackName: aws.String(stackID), NextToken: nextToken, }) if err != nil { @@ -230,7 +229,7 @@ func (s sdk) DescribeStackEvents(ctx context.Context, name string) ([]*cloudform func (s sdk) DeleteStack(ctx context.Context, name string) error { logrus.Debug("Delete CloudFormation stack") - _, err := s.CF.DeleteStackWithContext(aws.Context(ctx), &cloudformation.DeleteStackInput{ + _, err := s.CF.DeleteStackWithContext(ctx, &cloudformation.DeleteStackInput{ StackName: aws.String(name), }) return err diff --git a/ecs/pkg/amazon/up.go b/ecs/pkg/amazon/up.go index 0ee37e5fb..459f95d52 100644 --- a/ecs/pkg/amazon/up.go +++ b/ecs/pkg/amazon/up.go @@ -34,7 +34,7 @@ func (c *client) ComposeUp(ctx context.Context, project *compose.Project) error return err } - return c.WaitStackCompletion(ctx, project.Name) + return c.WaitStackCompletion(ctx, project.Name, StackCreate) } type upAPI interface { diff --git a/ecs/pkg/amazon/wait.go b/ecs/pkg/amazon/wait.go index f3a25c897..095fcc6b1 100644 --- a/ecs/pkg/amazon/wait.go +++ b/ecs/pkg/amazon/wait.go @@ -4,17 +4,42 @@ import ( "context" "fmt" "sort" + "time" "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/cloudformation" "github.com/docker/ecs-plugin/pkg/console" ) -func (c *client) WaitStackCompletion(ctx context.Context, name string) error { +func (c *client) WaitStackCompletion(ctx context.Context, name string, operation int) error { w := console.NewProgressWriter() - known := map[string]struct{}{} - err := c.api.WaitStackComplete(ctx, name, func() error { - events, err := c.api.DescribeStackEvents(ctx, name) + knownEvents := map[string]struct{}{} + + // Get the unique Stack ID so we can collect events without getting some from previous deployments with same name + stackID, err := c.api.GetStackID(ctx, name) + if err != nil { + return err + } + + ticker := time.NewTicker(1 * time.Second) + done := make(chan error) + + go func() { + err := c.api.WaitStackComplete(ctx, name, operation) + ticker.Stop() + done <- err + }() + + var completed bool + var waitErr error + for !completed { + select { + case err := <-done: + completed = true + waitErr = err + case <-ticker.C: + } + events, err := c.api.DescribeStackEvents(ctx, stackID) if err != nil { return err } @@ -24,23 +49,25 @@ func (c *client) WaitStackCompletion(ctx context.Context, name string) error { }) for _, event := range events { - if _, ok := known[*event.EventId]; ok { + if _, ok := knownEvents[*event.EventId]; ok { continue } - known[*event.EventId] = struct{}{} + knownEvents[*event.EventId] = struct{}{} resource := fmt.Sprintf("%s %q", aws.StringValue(event.ResourceType), aws.StringValue(event.LogicalResourceId)) w.ResourceEvent(resource, aws.StringValue(event.ResourceStatus), aws.StringValue(event.ResourceStatusReason)) } - return nil - }) - if err != nil { - return err } - return nil + return waitErr } type waitAPI interface { - WaitStackComplete(ctx context.Context, name string, fn func() error) error - DescribeStackEvents(ctx context.Context, stack string) ([]*cloudformation.StackEvent, error) + GetStackID(ctx context.Context, name string) (string, error) + WaitStackComplete(ctx context.Context, name string, operation int) error + DescribeStackEvents(ctx context.Context, stackID string) ([]*cloudformation.StackEvent, error) } + +const ( + StackCreate = iota + StackDelete +) diff --git a/ecs/pkg/console/progress.go b/ecs/pkg/console/progress.go index 6943dd8ad..599ac5a7f 100644 --- a/ecs/pkg/console/progress.go +++ b/ecs/pkg/console/progress.go @@ -100,16 +100,10 @@ func (c ansiConsole) Printf(format string, a ...interface{}) { } func (c ansiConsole) MoveUp(i int) { - if i == 0 { - return - } fmt.Fprintf(c.out, "\033[%dA", i) // nolint:errcheck } func (c ansiConsole) MoveDown(i int) { - if i == 0 { - return - } fmt.Fprintf(c.out, "\033[%dB", i) // nolint:errcheck } diff --git a/ecs/pkg/convert/convert.go b/ecs/pkg/convert/convert.go index 33e95c73c..c7a8cab87 100644 --- a/ecs/pkg/convert/convert.go +++ b/ecs/pkg/convert/convert.go @@ -71,7 +71,6 @@ func Convert(project *compose.Project, service types.ServiceConfig) (*ecs.TaskDe Tags: nil, Volumes: []ecs.TaskDefinition_Volume{}, }, nil - } func toCPU(service types.ServiceConfig) string { From 99b4ed0bfdf320018c1a8d19163e027f5b7ac459 Mon Sep 17 00:00:00 2001 From: Nicolas De Loof <nicolas.deloof@gmail.com> Date: Thu, 30 Apr 2020 16:14:36 +0200 Subject: [PATCH 047/198] Capture first failure to report root error to user on command completion Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com> --- ecs/pkg/amazon/down_test.go | 14 +++++++++----- ecs/pkg/amazon/wait.go | 21 +++++++++++++-------- 2 files changed, 22 insertions(+), 13 deletions(-) diff --git a/ecs/pkg/amazon/down_test.go b/ecs/pkg/amazon/down_test.go index 49e5fe2ac..bf98a6b9c 100644 --- a/ecs/pkg/amazon/down_test.go +++ b/ecs/pkg/amazon/down_test.go @@ -19,8 +19,10 @@ func TestDownDontDeleteCluster(t *testing.T) { } ctx := context.TODO() recorder := m.EXPECT() - recorder.DeleteStack(ctx, "test_project").Return(nil).Times(1) - recorder.WaitStackComplete(ctx, "test_project", gomock.Any()).Return(nil).Times(1) + recorder.DeleteStack(ctx, "test_project").Return(nil) + recorder.GetStackID(ctx, "test_project").Return("stack-123", nil) + recorder.WaitStackComplete(ctx, "stack-123", StackDelete).Return(nil) + recorder.DescribeStackEvents(ctx, "stack-123").Return(nil, nil) c.ComposeDown(ctx, "test_project", false) } @@ -37,9 +39,11 @@ func TestDownDeleteCluster(t *testing.T) { ctx := context.TODO() recorder := m.EXPECT() - recorder.DeleteStack(ctx, "test_project").Return(nil).Times(1) - recorder.WaitStackComplete(ctx, "test_project", gomock.Any()).Return(nil).Times(1) - recorder.DeleteCluster(ctx, "test_cluster").Return(nil).Times(1) + recorder.DeleteStack(ctx, "test_project").Return(nil) + recorder.GetStackID(ctx, "test_project").Return("stack-123", nil) + recorder.WaitStackComplete(ctx, "stack-123", StackDelete).Return(nil) + recorder.DescribeStackEvents(ctx, "stack-123").Return(nil, nil) + recorder.DeleteCluster(ctx, "test_cluster").Return(nil) c.ComposeDown(ctx, "test_project", true) } diff --git a/ecs/pkg/amazon/wait.go b/ecs/pkg/amazon/wait.go index 095fcc6b1..58ae93d77 100644 --- a/ecs/pkg/amazon/wait.go +++ b/ecs/pkg/amazon/wait.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "sort" + "strings" "time" "github.com/aws/aws-sdk-go/aws" @@ -22,21 +23,20 @@ func (c *client) WaitStackCompletion(ctx context.Context, name string, operation } ticker := time.NewTicker(1 * time.Second) - done := make(chan error) + done := make(chan bool) go func() { - err := c.api.WaitStackComplete(ctx, name, operation) + c.api.WaitStackComplete(ctx, stackID, operation) //nolint:errcheck ticker.Stop() - done <- err + done <- true }() var completed bool - var waitErr error + var stackErr error for !completed { select { - case err := <-done: + case <-done: completed = true - waitErr = err case <-ticker.C: } events, err := c.api.DescribeStackEvents(ctx, stackID) @@ -55,10 +55,15 @@ func (c *client) WaitStackCompletion(ctx context.Context, name string, operation knownEvents[*event.EventId] = struct{}{} resource := fmt.Sprintf("%s %q", aws.StringValue(event.ResourceType), aws.StringValue(event.LogicalResourceId)) - w.ResourceEvent(resource, aws.StringValue(event.ResourceStatus), aws.StringValue(event.ResourceStatusReason)) + 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) + } } } - return waitErr + return stackErr } type waitAPI interface { From 3e30f2cd1a3991d09c49f76b8ba5a0656cd3480f Mon Sep 17 00:00:00 2001 From: Nicolas De Loof <nicolas.deloof@gmail.com> Date: Mon, 4 May 2020 15:09:08 +0200 Subject: [PATCH 048/198] Create CloudWatch LogGroup and IAM TaskExecutionRole As part of the CloudFormation template, create a LogGroup and configure task with awslogs log-driver. Also create a dedicated IAM Role, with AmazonECSTaskExecutionRolePolicy. This one will later be fine-tuned to grant access to secrets/config and other AWS resources according to custom extensions close #42 Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com> --- ecs/pkg/amazon/cloudformation.go | 26 +++++++++++++----- ecs/pkg/amazon/iam.go | 31 ++++++++++++++++++++++ ecs/pkg/amazon/sdk.go | 3 +++ ecs/pkg/convert/convert.go | 45 +++++++++++++++++++------------- 4 files changed, 80 insertions(+), 25 deletions(-) create mode 100644 ecs/pkg/amazon/iam.go diff --git a/ecs/pkg/amazon/cloudformation.go b/ecs/pkg/amazon/cloudformation.go index e5c4cd71b..40bf8eddd 100644 --- a/ecs/pkg/amazon/cloudformation.go +++ b/ecs/pkg/amazon/cloudformation.go @@ -6,15 +6,17 @@ import ( "fmt" "strings" - "github.com/compose-spec/compose-go/types" - "github.com/sirupsen/logrus" + "github.com/awslabs/goformation/v4/cloudformation/logs" ecsapi "github.com/aws/aws-sdk-go/service/ecs" "github.com/awslabs/goformation/v4/cloudformation" "github.com/awslabs/goformation/v4/cloudformation/ec2" "github.com/awslabs/goformation/v4/cloudformation/ecs" + "github.com/awslabs/goformation/v4/cloudformation/iam" + "github.com/compose-spec/compose-go/types" "github.com/docker/ecs-plugin/pkg/compose" "github.com/docker/ecs-plugin/pkg/convert" + "github.com/sirupsen/logrus" ) func (c client) Convert(ctx context.Context, project *compose.Project) (*cloudformation.Template, error) { @@ -50,22 +52,32 @@ func (c client) Convert(ctx context.Context, project *compose.Project) (*cloudfo VpcId: vpc, } + logGroup := fmt.Sprintf("/docker-compose/%s", project.Name) + template.Resources["LogGroup"] = &logs.LogGroup{ + LogGroupName: logGroup, + } + for _, service := range project.Services { definition, err := convert.Convert(project, service) if err != nil { return nil, err } - role, err := c.GetEcsTaskExecutionRole(ctx, service) - if err != nil { - return nil, err + taskExecutionRole := fmt.Sprintf("%sTaskExecutionRole", service.Name) + template.Resources[taskExecutionRole] = &iam.Role{ + AssumeRolePolicyDocument: assumeRolePolicyDocument, + // Here we can grant access to secrets/configs using a Policy { Allow,ssm:GetParameters,secret|config ARN} + ManagedPolicyArns: []string{ + ECSTaskExecutionPolicy, + }, } - definition.TaskRoleArn = role + definition.ExecutionRoleArn = cloudformation.Ref(taskExecutionRole) + // FIXME definition.TaskRoleArn = ? taskDefinition := fmt.Sprintf("%sTaskDefinition", service.Name) template.Resources[taskDefinition] = definition - template.Resources[service.Name] = &ecs.Service{ + template.Resources[fmt.Sprintf("%sService", service.Name)] = &ecs.Service{ Cluster: c.Cluster, DesiredCount: 1, LaunchType: ecsapi.LaunchTypeFargate, diff --git a/ecs/pkg/amazon/iam.go b/ecs/pkg/amazon/iam.go new file mode 100644 index 000000000..38c4d8339 --- /dev/null +++ b/ecs/pkg/amazon/iam.go @@ -0,0 +1,31 @@ +package amazon + +var assumeRolePolicyDocument = PolicyDocument{ + Version: "2012-10-17", // https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements_version.html + Statement: []PolicyStatement{ + { + Effect: "Allow", + Principal: PolicyPrincipal{ + Service: "ecs-tasks.amazonaws.com", + }, + Action: []string{"sts:AssumeRole"}, + }, + }, +} + +// could alternatively depend on https://github.com/kubernetes-sigs/cluster-api-provider-aws/blob/master/pkg/cloud/services/iam/types.go#L52 +type PolicyDocument struct { + Version string `json:",omitempty"` + Statement []PolicyStatement `json:",omitempty"` +} + +type PolicyStatement struct { + Effect string `json:",omitempty"` + Action []string `json:",omitempty"` + Principal PolicyPrincipal `json:",omitempty"` + Resource []string `json:",omitempty"` +} + +type PolicyPrincipal struct { + Service string `json:",omitempty"` +} diff --git a/ecs/pkg/amazon/sdk.go b/ecs/pkg/amazon/sdk.go index 29ca7f7bd..89d3ec319 100644 --- a/ecs/pkg/amazon/sdk.go +++ b/ecs/pkg/amazon/sdk.go @@ -180,6 +180,9 @@ func (s sdk) CreateStack(ctx context.Context, name string, template *cf.Template StackName: aws.String(name), TemplateBody: aws.String(string(json)), TimeoutInMinutes: aws.Int64(10), + Capabilities: []*string{ + aws.String(cloudformation.CapabilityCapabilityIam), + }, }) return err } diff --git a/ecs/pkg/convert/convert.go b/ecs/pkg/convert/convert.go index c7a8cab87..8b70a6488 100644 --- a/ecs/pkg/convert/convert.go +++ b/ecs/pkg/convert/convert.go @@ -5,6 +5,7 @@ import ( "time" ecsapi "github.com/aws/aws-sdk-go/service/ecs" + "github.com/awslabs/goformation/v4/cloudformation" "github.com/awslabs/goformation/v4/cloudformation/ecs" "github.com/compose-spec/compose-go/types" "github.com/docker/cli/opts" @@ -21,24 +22,32 @@ func Convert(project *compose.Project, service types.ServiceConfig) (*ecs.TaskDe ContainerDefinitions: []ecs.TaskDefinition_ContainerDefinition{ // Here we can declare sidecars and init-containers using https://docs.aws.amazon.com/AmazonECS/latest/developerguide/task_definition_parameters.html#container_definition_dependson { - Command: service.Command, - Cpu: 256, - DisableNetworking: service.NetworkMode == "none", - DnsSearchDomains: service.DNSSearch, - DnsServers: service.DNS, - DockerLabels: nil, - DockerSecurityOptions: service.SecurityOpt, - EntryPoint: service.Entrypoint, - Environment: toKeyValuePair(service.Environment), - Essential: true, - ExtraHosts: toHostEntryPtr(service.ExtraHosts), - FirelensConfiguration: nil, - HealthCheck: toHealthCheck(service.HealthCheck), - Hostname: service.Hostname, - Image: service.Image, - Interactive: false, - Links: nil, - LinuxParameters: toLinuxParameters(service), + Command: service.Command, + Cpu: 256, + DisableNetworking: service.NetworkMode == "none", + DnsSearchDomains: service.DNSSearch, + DnsServers: service.DNS, + DockerLabels: nil, + DockerSecurityOptions: service.SecurityOpt, + EntryPoint: service.Entrypoint, + Environment: toKeyValuePair(service.Environment), + Essential: true, + ExtraHosts: toHostEntryPtr(service.ExtraHosts), + FirelensConfiguration: nil, + HealthCheck: toHealthCheck(service.HealthCheck), + Hostname: service.Hostname, + Image: service.Image, + Interactive: false, + Links: nil, + LinuxParameters: toLinuxParameters(service), + LogConfiguration: &ecs.TaskDefinition_LogConfiguration{ + LogDriver: ecsapi.LogDriverAwslogs, + Options: map[string]string{ + "awslogs-region": cloudformation.Ref("AWS::Region"), + "awslogs-group": cloudformation.Ref("LogGroup"), + "awslogs-stream-prefix": service.Name, + }, + }, Memory: toMemoryLimits(service.Deploy), MemoryReservation: toMemoryReservation(service.Deploy), MountPoints: nil, From 2544307f55c5b0f3a0da8479f5836eaef0a49b3b Mon Sep 17 00:00:00 2001 From: Nicolas De Loof <nicolas.deloof@gmail.com> Date: Mon, 4 May 2020 15:15:22 +0200 Subject: [PATCH 049/198] drop GetEcsTaskExecutionRole which is not in used anymore We need to define a way for compose-user to declare additional Policies to be added to TaskExecutionRole Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com> --- ecs/pkg/amazon/cloudformation.go | 36 ------------------- ecs/pkg/amazon/iam.go | 2 ++ ecs/pkg/amazon/mock/api.go | 59 ++++++++++++++++++++++++++++---- ecs/pkg/amazon/sdk.go | 15 -------- 4 files changed, 54 insertions(+), 58 deletions(-) diff --git a/ecs/pkg/amazon/cloudformation.go b/ecs/pkg/amazon/cloudformation.go index 40bf8eddd..c805e099c 100644 --- a/ecs/pkg/amazon/cloudformation.go +++ b/ecs/pkg/amazon/cloudformation.go @@ -13,10 +13,8 @@ import ( "github.com/awslabs/goformation/v4/cloudformation/ec2" "github.com/awslabs/goformation/v4/cloudformation/ecs" "github.com/awslabs/goformation/v4/cloudformation/iam" - "github.com/compose-spec/compose-go/types" "github.com/docker/ecs-plugin/pkg/compose" "github.com/docker/ecs-plugin/pkg/convert" - "github.com/sirupsen/logrus" ) func (c client) Convert(ctx context.Context, project *compose.Project) (*cloudformation.Template, error) { @@ -118,43 +116,9 @@ func (c client) GetVPC(ctx context.Context, project *compose.Project) (string, e return defaultVPC, nil } -const ECSTaskExecutionPolicy = "arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy" - -var defaultTaskExecutionRole string - -// GetEcsTaskExecutionRole retrieve the role ARN to apply for task execution -func (c client) GetEcsTaskExecutionRole(ctx context.Context, spec types.ServiceConfig) (string, error) { - if arn, ok := spec.Extras["x-ecs-TaskExecutionRole"]; ok { - return arn.(string), nil - } - if defaultTaskExecutionRole != "" { - return defaultTaskExecutionRole, nil - } - - logrus.Debug("Retrieve Task Execution Role") - entities, err := c.api.ListRolesForPolicy(ctx, ECSTaskExecutionPolicy) - if err != nil { - return "", err - } - if len(entities) == 0 { - return "", fmt.Errorf("no Role is attached to AmazonECSTaskExecutionRole Policy, please provide an explicit task execution role") - } - if len(entities) > 1 { - return "", fmt.Errorf("multiple Roles are attached to AmazonECSTaskExecutionRole Policy, please provide an explicit task execution role") - } - - arn, err := c.api.GetRoleArn(ctx, entities[0]) - if err != nil { - return "", err - } - defaultTaskExecutionRole = arn - return arn, nil -} - type convertAPI interface { GetDefaultVPC(ctx context.Context) (string, error) VpcExists(ctx context.Context, vpcID string) (bool, error) GetSubNets(ctx context.Context, vpcID string) ([]string, error) - ListRolesForPolicy(ctx context.Context, policy string) ([]string, error) GetRoleArn(ctx context.Context, name string) (string, error) } diff --git a/ecs/pkg/amazon/iam.go b/ecs/pkg/amazon/iam.go index 38c4d8339..c07e34fec 100644 --- a/ecs/pkg/amazon/iam.go +++ b/ecs/pkg/amazon/iam.go @@ -1,5 +1,7 @@ package amazon +const ECSTaskExecutionPolicy = "arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy" + var assumeRolePolicyDocument = PolicyDocument{ Version: "2012-10-17", // https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements_version.html Statement: []PolicyStatement{ diff --git a/ecs/pkg/amazon/mock/api.go b/ecs/pkg/amazon/mock/api.go index 81b0829db..4a1163315 100644 --- a/ecs/pkg/amazon/mock/api.go +++ b/ecs/pkg/amazon/mock/api.go @@ -8,6 +8,7 @@ import ( context "context" cloudformation "github.com/aws/aws-sdk-go/service/cloudformation" cloudformation0 "github.com/awslabs/goformation/v4/cloudformation" + docker "github.com/docker/ecs-plugin/pkg/docker" gomock "github.com/golang/mock/gomock" reflect "reflect" ) @@ -65,6 +66,21 @@ func (mr *MockAPIMockRecorder) CreateCluster(arg0, arg1 interface{}) *gomock.Cal return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateCluster", reflect.TypeOf((*MockAPI)(nil).CreateCluster), arg0, arg1) } +// CreateSecret mocks base method +func (m *MockAPI) CreateSecret(arg0 context.Context, arg1, arg2 string) (string, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "CreateSecret", arg0, arg1, arg2) + ret0, _ := ret[0].(string) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// CreateSecret indicates an expected call of CreateSecret +func (mr *MockAPIMockRecorder) CreateSecret(arg0, arg1, arg2 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateSecret", reflect.TypeOf((*MockAPI)(nil).CreateSecret), arg0, arg1, arg2) +} + // CreateStack mocks base method func (m *MockAPI) CreateStack(arg0 context.Context, arg1 string, arg2 *cloudformation0.Template) error { m.ctrl.T.Helper() @@ -93,6 +109,20 @@ func (mr *MockAPIMockRecorder) DeleteCluster(arg0, arg1 interface{}) *gomock.Cal return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteCluster", reflect.TypeOf((*MockAPI)(nil).DeleteCluster), arg0, arg1) } +// DeleteSecret mocks base method +func (m *MockAPI) DeleteSecret(arg0 context.Context, arg1 string, arg2 bool) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DeleteSecret", arg0, arg1, arg2) + ret0, _ := ret[0].(error) + return ret0 +} + +// DeleteSecret indicates an expected call of DeleteSecret +func (mr *MockAPIMockRecorder) DeleteSecret(arg0, arg1, arg2 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteSecret", reflect.TypeOf((*MockAPI)(nil).DeleteSecret), arg0, arg1, arg2) +} + // DeleteStack mocks base method func (m *MockAPI) DeleteStack(arg0 context.Context, arg1 string) error { m.ctrl.T.Helper() @@ -182,19 +212,34 @@ func (mr *MockAPIMockRecorder) GetSubNets(arg0, arg1 interface{}) *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSubNets", reflect.TypeOf((*MockAPI)(nil).GetSubNets), arg0, arg1) } -// ListRolesForPolicy mocks base method -func (m *MockAPI) ListRolesForPolicy(arg0 context.Context, arg1 string) ([]string, error) { +// InspectSecret mocks base method +func (m *MockAPI) InspectSecret(arg0 context.Context, arg1 string) (docker.Secret, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "ListRolesForPolicy", arg0, arg1) - ret0, _ := ret[0].([]string) + ret := m.ctrl.Call(m, "InspectSecret", arg0, arg1) + ret0, _ := ret[0].(docker.Secret) ret1, _ := ret[1].(error) return ret0, ret1 } -// ListRolesForPolicy indicates an expected call of ListRolesForPolicy -func (mr *MockAPIMockRecorder) ListRolesForPolicy(arg0, arg1 interface{}) *gomock.Call { +// InspectSecret indicates an expected call of InspectSecret +func (mr *MockAPIMockRecorder) InspectSecret(arg0, arg1 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListRolesForPolicy", reflect.TypeOf((*MockAPI)(nil).ListRolesForPolicy), arg0, arg1) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InspectSecret", reflect.TypeOf((*MockAPI)(nil).InspectSecret), arg0, arg1) +} + +// ListSecrets mocks base method +func (m *MockAPI) ListSecrets(arg0 context.Context) ([]docker.Secret, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ListSecrets", arg0) + ret0, _ := ret[0].([]docker.Secret) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ListSecrets indicates an expected call of ListSecrets +func (mr *MockAPIMockRecorder) ListSecrets(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListSecrets", reflect.TypeOf((*MockAPI)(nil).ListSecrets), arg0) } // StackExists mocks base method diff --git a/ecs/pkg/amazon/sdk.go b/ecs/pkg/amazon/sdk.go index 89d3ec319..74cb04484 100644 --- a/ecs/pkg/amazon/sdk.go +++ b/ecs/pkg/amazon/sdk.go @@ -132,21 +132,6 @@ func (s sdk) GetSubNets(ctx context.Context, vpcID string) ([]string, error) { return ids, nil } -func (s sdk) ListRolesForPolicy(ctx context.Context, policy string) ([]string, error) { - entities, err := s.IAM.ListEntitiesForPolicyWithContext(ctx, &iam.ListEntitiesForPolicyInput{ - EntityFilter: aws.String("Role"), - PolicyArn: aws.String(policy), - }) - if err != nil { - return nil, err - } - roles := []string{} - for _, e := range entities.PolicyRoles { - roles = append(roles, *e.RoleName) - } - return roles, nil -} - func (s sdk) GetRoleArn(ctx context.Context, name string) (string, error) { role, err := s.IAM.GetRoleWithContext(ctx, &iam.GetRoleInput{ RoleName: aws.String(name), From 9a6fe86a86bab70f2f051236230cef6662c045c1 Mon Sep 17 00:00:00 2001 From: Nicolas De Loof <nicolas.deloof@gmail.com> Date: Tue, 5 May 2020 13:50:57 +0200 Subject: [PATCH 050/198] Introduce "Validate" phase to check/make app ECS-compliant Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com> --- ecs/README.md | 36 ++++++++++++++++---------- ecs/pkg/amazon/cloudformation.go | 3 +-- ecs/pkg/{convert => amazon}/convert.go | 20 +------------- ecs/pkg/amazon/up.go | 5 ++++ ecs/pkg/amazon/validate.go | 20 ++++++++++++++ 5 files changed, 50 insertions(+), 34 deletions(-) rename ecs/pkg/{convert => amazon}/convert.go (97%) create mode 100644 ecs/pkg/amazon/validate.go diff --git a/ecs/README.md b/ecs/README.md index 9ec4ab694..f15fd25f5 100644 --- a/ecs/README.md +++ b/ecs/README.md @@ -11,21 +11,31 @@ template, which will create all resources in dependent order and cleanup on `down` command or deployment failure. ``` - +-----------------------------+ - | compose.yaml file | - +-----------------------------+ + +--------------------------------------+ + | compose.yaml file | + +--------------------------------------+ - Load - +-----------------------------+ - | compose-go Model | - +-----------------------------+ + +--------------------------------------+ + | compose Model | + +--------------------------------------+ +- Validate + +--------------------------------------+ + | compose Model suitable for ECS | + +--------------------------------------+ - Convert - +-----------------------------+ - | CloudFormation Template | - +-----------------------------+ + +--------------------------------------+ + | CloudFormation Template | + +--------------------------------------+ - Apply - +---------+ +------------+ - | AWS API | or | stack file | - +---------+ +------------+ + +--------------+ +----------------+ + | AWS API | or | stack file | + +--------------+ +----------------+ ``` -(if this sounds familiar, see [Kompose](https://github.com/kubernetes/kompose/blob/master/docs/architecture.md)) \ No newline at end of file +* _Load_ phase relies on [compose-go](https://github.com/compose-spec/compose-go). Any generic code we write for this +purpose should be proposed upstream. +* _Validate_ phase is responsible to inject sane ECS defaults into the compose-go model, and validate the `compose.yaml` +file do not include unsupported features. +* _Convert_ produces a CloudFormation template to define all resources required to implement the application model on AWS. +* _Apply_ phase do apply the CloudFormation template, either by exporting to a stack file or to deploy on AWS. + diff --git a/ecs/pkg/amazon/cloudformation.go b/ecs/pkg/amazon/cloudformation.go index c805e099c..76aa80887 100644 --- a/ecs/pkg/amazon/cloudformation.go +++ b/ecs/pkg/amazon/cloudformation.go @@ -14,7 +14,6 @@ import ( "github.com/awslabs/goformation/v4/cloudformation/ecs" "github.com/awslabs/goformation/v4/cloudformation/iam" "github.com/docker/ecs-plugin/pkg/compose" - "github.com/docker/ecs-plugin/pkg/convert" ) func (c client) Convert(ctx context.Context, project *compose.Project) (*cloudformation.Template, error) { @@ -56,7 +55,7 @@ func (c client) Convert(ctx context.Context, project *compose.Project) (*cloudfo } for _, service := range project.Services { - definition, err := convert.Convert(project, service) + definition, err := Convert(project, service) if err != nil { return nil, err } diff --git a/ecs/pkg/convert/convert.go b/ecs/pkg/amazon/convert.go similarity index 97% rename from ecs/pkg/convert/convert.go rename to ecs/pkg/amazon/convert.go index 8b70a6488..a0bd9e423 100644 --- a/ecs/pkg/convert/convert.go +++ b/ecs/pkg/amazon/convert.go @@ -1,4 +1,4 @@ -package convert +package amazon import ( "strings" @@ -174,16 +174,6 @@ func toUlimits(ulimits map[string]*types.UlimitsConfig) []ecs.TaskDefinition_Uli return u } -func uint32Toint64Ptr(i uint32) *int64 { - v := int64(i) - return &v -} - -func intToInt64Ptr(i int) *int64 { - v := int64(i) - return &v -} - const Mb = 1024 * 1024 func toMemoryLimits(deploy *types.DeployConfig) int { @@ -265,14 +255,6 @@ func toHealthCheck(check *types.HealthCheckConfig) *ecs.TaskDefinition_HealthChe } } -func uint64ToInt64Ptr(i *uint64) *int64 { - if i == nil { - return nil - } - v := int64(*i) - return &v -} - func durationToInt(interval *types.Duration) int { if interval == nil { return 0 diff --git a/ecs/pkg/amazon/up.go b/ecs/pkg/amazon/up.go index 459f95d52..21adf9733 100644 --- a/ecs/pkg/amazon/up.go +++ b/ecs/pkg/amazon/up.go @@ -24,6 +24,11 @@ func (c *client) ComposeUp(ctx context.Context, project *compose.Project) error return fmt.Errorf("we do not (yet) support updating an existing CloudFormation stack") } + err = c.Validate(project) + if err != nil { + return err + } + template, err := c.Convert(ctx, project) if err != nil { return err diff --git a/ecs/pkg/amazon/validate.go b/ecs/pkg/amazon/validate.go new file mode 100644 index 000000000..5e61a273d --- /dev/null +++ b/ecs/pkg/amazon/validate.go @@ -0,0 +1,20 @@ +package amazon + +import ( + "github.com/compose-spec/compose-go/types" + "github.com/docker/ecs-plugin/pkg/compose" +) + +// Validate check the compose model do not use unsupported features and inject sane defaults for ECS deployment +func (c *client) Validate(project *compose.Project) error { + if len(project.Networks) == 0 { + // Compose application model implies a default network if none is explicitly set. + // FIXME move this to compose-go + project.Networks["default"] = types.NetworkConfig{ + Name: "default", + } + } + + // Here we can check for incompatible attributes, inject sane defaults, etc + return nil +} From 0eab586106e53d35677fd47d30c389b93200e4e7 Mon Sep 17 00:00:00 2001 From: Nicolas De Loof <nicolas.deloof@gmail.com> Date: Tue, 5 May 2020 00:58:20 +0200 Subject: [PATCH 051/198] Create CloudMap private namespace and register services Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com> --- ecs/Makefile | 2 +- ecs/cmd/{ => main}/main.go | 0 ecs/pkg/amazon/cloudformation.go | 21 +++++++++++++++++++++ 3 files changed, 22 insertions(+), 1 deletion(-) rename ecs/cmd/{ => main}/main.go (100%) diff --git a/ecs/Makefile b/ecs/Makefile index e07af815e..5d3c5883d 100644 --- a/ecs/Makefile +++ b/ecs/Makefile @@ -2,7 +2,7 @@ clean: rm -rf dist/ build: - go build -v -o dist/docker-ecs cmd/main.go + go build -v -o dist/docker-ecs cmd/main/main.go test: ## Run tests go test ./... -v diff --git a/ecs/cmd/main.go b/ecs/cmd/main/main.go similarity index 100% rename from ecs/cmd/main.go rename to ecs/cmd/main/main.go diff --git a/ecs/pkg/amazon/cloudformation.go b/ecs/pkg/amazon/cloudformation.go index 76aa80887..7664d1a9c 100644 --- a/ecs/pkg/amazon/cloudformation.go +++ b/ecs/pkg/amazon/cloudformation.go @@ -13,6 +13,7 @@ import ( "github.com/awslabs/goformation/v4/cloudformation/ec2" "github.com/awslabs/goformation/v4/cloudformation/ecs" "github.com/awslabs/goformation/v4/cloudformation/iam" + cloudmap "github.com/awslabs/goformation/v4/cloudformation/servicediscovery" "github.com/docker/ecs-plugin/pkg/compose" ) @@ -54,6 +55,13 @@ func (c client) Convert(ctx context.Context, project *compose.Project) (*cloudfo LogGroupName: logGroup, } + // Private DNS namespace will allow DNS name for the services to be <service>.<project>.local + template.Resources["CloudMap"] = &cloudmap.PrivateDnsNamespace{ + Description: fmt.Sprintf("Service Map for Docker Compose project %s", project.Name), + Name: fmt.Sprintf("%s.local", project.Name), + Vpc: vpc, + } + for _, service := range project.Services { definition, err := Convert(project, service) if err != nil { @@ -89,6 +97,19 @@ func (c client) Convert(ctx context.Context, project *compose.Project) (*cloudfo ServiceName: service.Name, TaskDefinition: cloudformation.Ref(taskDefinition), } + + var healthCheck *cloudmap.Service_HealthCheckConfig + if service.HealthCheck != nil && !service.HealthCheck.Disable { + // FIXME ECS only support HTTP(s) health checks, while Docker only support CMD + } + + serviceRegistration := fmt.Sprintf("%sServiceRegistration", service.Name) + template.Resources[serviceRegistration] = &cloudmap.Service{ + Description: fmt.Sprintf("%q registration in Service Map", service.Name), + HealthCheckConfig: healthCheck, + Name: serviceRegistration, + NamespaceId: cloudformation.Ref("CloudMap"), + } } return template, nil } From 09871400efde3da8613a9b00a3efd9cbeed05c7a Mon Sep 17 00:00:00 2001 From: Nicolas De Loof <nicolas.deloof@gmail.com> Date: Tue, 5 May 2020 12:06:32 +0200 Subject: [PATCH 052/198] Register services within Cloud Map close #35 Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com> --- ecs/pkg/amazon/cloudformation.go | 46 +++++++++++++++++++++----------- 1 file changed, 30 insertions(+), 16 deletions(-) diff --git a/ecs/pkg/amazon/cloudformation.go b/ecs/pkg/amazon/cloudformation.go index 7664d1a9c..4a9fcda9c 100644 --- a/ecs/pkg/amazon/cloudformation.go +++ b/ecs/pkg/amazon/cloudformation.go @@ -6,13 +6,13 @@ import ( "fmt" "strings" - "github.com/awslabs/goformation/v4/cloudformation/logs" - ecsapi "github.com/aws/aws-sdk-go/service/ecs" + cloudmapapi "github.com/aws/aws-sdk-go/service/servicediscovery" "github.com/awslabs/goformation/v4/cloudformation" "github.com/awslabs/goformation/v4/cloudformation/ec2" "github.com/awslabs/goformation/v4/cloudformation/ecs" "github.com/awslabs/goformation/v4/cloudformation/iam" + "github.com/awslabs/goformation/v4/cloudformation/logs" cloudmap "github.com/awslabs/goformation/v4/cloudformation/servicediscovery" "github.com/docker/ecs-plugin/pkg/compose" ) @@ -82,6 +82,28 @@ func (c client) Convert(ctx context.Context, project *compose.Project) (*cloudfo taskDefinition := fmt.Sprintf("%sTaskDefinition", service.Name) template.Resources[taskDefinition] = definition + var healthCheck *cloudmap.Service_HealthCheckConfig + if service.HealthCheck != nil && !service.HealthCheck.Disable { + // FIXME ECS only support HTTP(s) health checks, while Docker only support CMD + } + + serviceRegistration := fmt.Sprintf("%sServiceDiscoveryEntry", service.Name) + template.Resources[serviceRegistration] = &cloudmap.Service{ + Description: fmt.Sprintf("%q service discovery entry in Cloud Map", service.Name), + HealthCheckConfig: healthCheck, + Name: service.Name, + NamespaceId: cloudformation.Ref("CloudMap"), + DnsConfig: &cloudmap.Service_DnsConfig{ + DnsRecords: []cloudmap.Service_DnsRecord{ + { + TTL: 300, + Type: cloudmapapi.RecordTypeA, + }, + }, + RoutingPolicy: cloudmapapi.RoutingPolicyMultivalue, + }, + } + template.Resources[fmt.Sprintf("%sService", service.Name)] = &ecs.Service{ Cluster: c.Cluster, DesiredCount: 1, @@ -95,20 +117,12 @@ func (c client) Convert(ctx context.Context, project *compose.Project) (*cloudfo }, SchedulingStrategy: ecsapi.SchedulingStrategyReplica, ServiceName: service.Name, - TaskDefinition: cloudformation.Ref(taskDefinition), - } - - var healthCheck *cloudmap.Service_HealthCheckConfig - if service.HealthCheck != nil && !service.HealthCheck.Disable { - // FIXME ECS only support HTTP(s) health checks, while Docker only support CMD - } - - serviceRegistration := fmt.Sprintf("%sServiceRegistration", service.Name) - template.Resources[serviceRegistration] = &cloudmap.Service{ - Description: fmt.Sprintf("%q registration in Service Map", service.Name), - HealthCheckConfig: healthCheck, - Name: serviceRegistration, - NamespaceId: cloudformation.Ref("CloudMap"), + ServiceRegistries: []ecs.Service_ServiceRegistry{ + { + RegistryArn: cloudformation.GetAtt(serviceRegistration, "Arn"), + }, + }, + TaskDefinition: cloudformation.Ref(taskDefinition), } } return template, nil From aa8587095f4b4e23d494b4b3f7997e0528eb8e64 Mon Sep 17 00:00:00 2001 From: Guillaume Lours <guillaume.lours@docker.com> Date: Mon, 4 May 2020 18:35:23 +0200 Subject: [PATCH 053/198] Add setup command to define a docker context for ecs-plugin Signed-off-by: Guillaume Lours <guillaume.lours@docker.com> Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com> --- ecs/cmd/commands/compose.go | 15 +++----- ecs/cmd/commands/secret.go | 10 +++--- ecs/cmd/commands/setup.go | 33 ++++++++++++++++++ ecs/cmd/main/main.go | 21 ++++++++---- ecs/go.mod | 1 + ecs/pkg/docker/contextStore.go | 63 ++++++++++++++++++++++++++++++++++ 6 files changed, 121 insertions(+), 22 deletions(-) create mode 100644 ecs/cmd/commands/setup.go create mode 100644 ecs/pkg/docker/contextStore.go diff --git a/ecs/cmd/commands/compose.go b/ecs/cmd/commands/compose.go index 972477b3a..459a48813 100644 --- a/ecs/cmd/commands/compose.go +++ b/ecs/cmd/commands/compose.go @@ -6,16 +6,11 @@ import ( "github.com/docker/ecs-plugin/pkg/amazon" "github.com/docker/ecs-plugin/pkg/compose" + "github.com/docker/ecs-plugin/pkg/docker" "github.com/spf13/cobra" ) -type ClusterOptions struct { - Profile string - Region string - Cluster string -} - -func ComposeCommand(clusteropts *ClusterOptions) *cobra.Command { +func ComposeCommand(clusteropts *docker.AwsContext) *cobra.Command { cmd := &cobra.Command{ Use: "compose", } @@ -41,7 +36,7 @@ func (o upOptions) LoadBalancerArn() *string { return &o.loadBalancerArn } -func ConvertCommand(clusteropts *ClusterOptions, projectOpts *compose.ProjectOptions) *cobra.Command { +func ConvertCommand(clusteropts *docker.AwsContext, projectOpts *compose.ProjectOptions) *cobra.Command { cmd := &cobra.Command{ Use: "convert", RunE: compose.WithProject(projectOpts, func(project *compose.Project, args []string) error { @@ -66,7 +61,7 @@ func ConvertCommand(clusteropts *ClusterOptions, projectOpts *compose.ProjectOpt return cmd } -func UpCommand(clusteropts *ClusterOptions, projectOpts *compose.ProjectOptions) *cobra.Command { +func UpCommand(clusteropts *docker.AwsContext, projectOpts *compose.ProjectOptions) *cobra.Command { opts := upOptions{} cmd := &cobra.Command{ Use: "up", @@ -86,7 +81,7 @@ type downOptions struct { DeleteCluster bool } -func DownCommand(clusteropts *ClusterOptions, projectOpts *compose.ProjectOptions) *cobra.Command { +func DownCommand(clusteropts *docker.AwsContext, projectOpts *compose.ProjectOptions) *cobra.Command { opts := downOptions{} cmd := &cobra.Command{ Use: "down", diff --git a/ecs/cmd/commands/secret.go b/ecs/cmd/commands/secret.go index 0c228c54b..b46eeeb3d 100644 --- a/ecs/cmd/commands/secret.go +++ b/ecs/cmd/commands/secret.go @@ -22,7 +22,7 @@ type deleteSecretOptions struct { recover bool } -func SecretCommand(clusteropts *ClusterOptions) *cobra.Command { +func SecretCommand(clusteropts *docker.AwsContext) *cobra.Command { cmd := &cobra.Command{ Use: "secret", Short: "Manages secrets", @@ -37,7 +37,7 @@ func SecretCommand(clusteropts *ClusterOptions) *cobra.Command { return cmd } -func CreateSecret(clusteropts *ClusterOptions) *cobra.Command { +func CreateSecret(clusteropts *docker.AwsContext) *cobra.Command { //opts := createSecretOptions{} cmd := &cobra.Command{ Use: "create NAME SECRET", @@ -60,7 +60,7 @@ func CreateSecret(clusteropts *ClusterOptions) *cobra.Command { return cmd } -func InspectSecret(clusteropts *ClusterOptions) *cobra.Command { +func InspectSecret(clusteropts *docker.AwsContext) *cobra.Command { cmd := &cobra.Command{ Use: "inspect ID", Short: "Displays secret details", @@ -88,7 +88,7 @@ func InspectSecret(clusteropts *ClusterOptions) *cobra.Command { return cmd } -func ListSecrets(clusteropts *ClusterOptions) *cobra.Command { +func ListSecrets(clusteropts *docker.AwsContext) *cobra.Command { cmd := &cobra.Command{ Use: "list", Aliases: []string{"ls"}, @@ -110,7 +110,7 @@ func ListSecrets(clusteropts *ClusterOptions) *cobra.Command { return cmd } -func DeleteSecret(clusteropts *ClusterOptions) *cobra.Command { +func DeleteSecret(clusteropts *docker.AwsContext) *cobra.Command { opts := deleteSecretOptions{} cmd := &cobra.Command{ Use: "delete NAME", diff --git a/ecs/cmd/commands/setup.go b/ecs/cmd/commands/setup.go new file mode 100644 index 000000000..a927605d1 --- /dev/null +++ b/ecs/cmd/commands/setup.go @@ -0,0 +1,33 @@ +package commands + +import ( + "github.com/docker/cli/cli-plugins/plugin" + contextStore "github.com/docker/ecs-plugin/pkg/docker" + "github.com/spf13/cobra" +) + +func SetupCommand() *cobra.Command { + var opts contextStore.AwsContext + var name string + cmd := &cobra.Command{ + Use: "setup", + Short: "", + PersistentPreRunE: func(cmd *cobra.Command, args []string) error { + //Override the root command PersistentPreRun + //We just need to initialize the top parent command + return plugin.PersistentPreRunE(cmd, args) + }, + RunE: func(cmd *cobra.Command, args []string) error { + return contextStore.NewContext(name, &opts) + }, + } + cmd.Flags().StringVarP(&name, "name", "n", "aws", "Context Name") + cmd.Flags().StringVarP(&opts.Profile, "profile", "p", "", "AWS Profile") + cmd.Flags().StringVarP(&opts.Cluster, "cluster", "c", "", "ECS cluster") + cmd.Flags().StringVarP(&opts.Region, "region", "r", "", "AWS region") + + cmd.MarkFlagRequired("profile") + cmd.MarkFlagRequired("cluster") + cmd.MarkFlagRequired("region") + return cmd +} diff --git a/ecs/cmd/main/main.go b/ecs/cmd/main/main.go index 72d072a09..be8d21829 100644 --- a/ecs/cmd/main/main.go +++ b/ecs/cmd/main/main.go @@ -7,6 +7,7 @@ import ( "github.com/docker/cli/cli-plugins/plugin" "github.com/docker/cli/cli/command" commands "github.com/docker/ecs-plugin/cmd/commands" + "github.com/docker/ecs-plugin/pkg/docker" "github.com/spf13/cobra" ) @@ -26,23 +27,29 @@ func main() { // NewRootCmd returns the base root command. func NewRootCmd(name string, dockerCli command.Cli) *cobra.Command { - var opts commands.ClusterOptions + var opts *docker.AwsContext cmd := &cobra.Command{ Short: "Docker ECS", Long: `run multi-container applications on Amazon ECS.`, Use: name, Annotations: map[string]string{"experimentalCLI": "true"}, + PersistentPreRunE: func(cmd *cobra.Command, args []string) error { + err := plugin.PersistentPreRunE(cmd, args) + if err != nil { + return err + } + contextName := dockerCli.CurrentContext() + opts, err = docker.CheckAwsContextExists(contextName) + return err + }, } cmd.AddCommand( VersionCommand(), - commands.ComposeCommand(&opts), - commands.SecretCommand(&opts), + commands.ComposeCommand(opts), + commands.SecretCommand(opts), + commands.SetupCommand(), ) - cmd.Flags().StringVarP(&opts.Profile, "profile", "p", "default", "AWS Profile") - cmd.Flags().StringVarP(&opts.Cluster, "cluster", "c", "default", "ECS cluster") - cmd.Flags().StringVarP(&opts.Region, "region", "r", "", "AWS region") - return cmd } diff --git a/ecs/go.mod b/ecs/go.mod index 0ddf6c857..564890334 100644 --- a/ecs/go.mod +++ b/ecs/go.mod @@ -35,6 +35,7 @@ require ( github.com/lib/pq v1.3.0 // indirect github.com/mattn/go-sqlite3 v2.0.3+incompatible // indirect github.com/miekg/pkcs11 v1.0.3 // indirect + github.com/mitchellh/mapstructure v1.2.2 github.com/morikuni/aec v1.0.0 // indirect github.com/onsi/ginkgo v1.11.0 // indirect github.com/opencontainers/image-spec v1.0.1 // indirect diff --git a/ecs/pkg/docker/contextStore.go b/ecs/pkg/docker/contextStore.go new file mode 100644 index 000000000..2d40b9dd0 --- /dev/null +++ b/ecs/pkg/docker/contextStore.go @@ -0,0 +1,63 @@ +package docker + +import ( + "fmt" + + cliconfig "github.com/docker/cli/cli/config" + "github.com/docker/cli/cli/context/store" + "github.com/mitchellh/mapstructure" +) + +const contextType = "aws" + +type TypeContext struct { + Type string +} + +func getter() interface{} { + return &TypeContext{} +} + +type AwsContext struct { + Profile string + Cluster string + Region string +} + +func NewContext(name string, awsContext *AwsContext) error { + contextStore := initContextStore() + endpoints := map[string]interface{}{ + "aws": awsContext, + "docker": awsContext, + } + + metadata := store.Metadata{ + Name: name, + Endpoints: endpoints, + Metadata: TypeContext{Type: contextType}, + } + return contextStore.CreateOrUpdate(metadata) +} + +func initContextStore() store.Store { + config := store.NewConfig(getter) + return store.New(cliconfig.ContextStoreDir(), config) +} + +func CheckAwsContextExists(contextName string) (*AwsContext, error) { + contextStore := initContextStore() + metadata, err := contextStore.GetMetadata(contextName) + if err != nil { + return nil, err + } + endpoint := metadata.Endpoints["aws"] + awsContext := AwsContext{} + err = mapstructure.Decode(endpoint, &awsContext) + if err != nil { + return nil, err + } + if awsContext == (AwsContext{}) { + return nil, fmt.Errorf("can't use \"%s\" with ECS command because it is not an AWS context", contextName) + } + return &awsContext, nil +} From 6febf6874885c0b67c3aa0249c13f795b13535dd Mon Sep 17 00:00:00 2001 From: Guillaume Lours <guillaume.lours@docker.com> Date: Tue, 5 May 2020 23:33:54 +0200 Subject: [PATCH 054/198] Add e2e tests for plugin behavior and commands Signed-off-by: Guillaume Lours <guillaume.lours@docker.com> Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com> --- ecs/Makefile | 2 +- ecs/cmd/main/main.go | 6 + ecs/go.mod | 1 + ecs/pkg/docker/contextStore.go | 19 +++- ecs/tests/command_test.go | 18 +++ ecs/tests/main_test.go | 127 ++++++++++++++++++++++ ecs/tests/plugin_test.go | 33 ++++++ ecs/tests/setup_command_test.go | 34 ++++++ ecs/tests/testdata/context-inspect.golden | 16 +++ ecs/tests/testdata/plugin-usage.golden | 14 +++ 10 files changed, 264 insertions(+), 6 deletions(-) create mode 100644 ecs/tests/command_test.go create mode 100644 ecs/tests/main_test.go create mode 100644 ecs/tests/plugin_test.go create mode 100644 ecs/tests/setup_command_test.go create mode 100644 ecs/tests/testdata/context-inspect.golden create mode 100644 ecs/tests/testdata/plugin-usage.golden diff --git a/ecs/Makefile b/ecs/Makefile index 5d3c5883d..126d5bc56 100644 --- a/ecs/Makefile +++ b/ecs/Makefile @@ -4,7 +4,7 @@ clean: build: go build -v -o dist/docker-ecs cmd/main/main.go -test: ## Run tests +test: build ## Run tests go test ./... -v dev: build diff --git a/ecs/cmd/main/main.go b/ecs/cmd/main/main.go index be8d21829..812ccfeea 100644 --- a/ecs/cmd/main/main.go +++ b/ecs/cmd/main/main.go @@ -43,6 +43,12 @@ func NewRootCmd(name string, dockerCli command.Cli) *cobra.Command { opts, err = docker.CheckAwsContextExists(contextName) return err }, + RunE: func(cmd *cobra.Command, args []string) error { + if len(args) != 0 { + return fmt.Errorf("%q is not a docker ecs command\nSee 'docker ecs --help'", args[0]) + } + return nil + }, } cmd.AddCommand( VersionCommand(), diff --git a/ecs/go.mod b/ecs/go.mod index 564890334..74a6a89f1 100644 --- a/ecs/go.mod +++ b/ecs/go.mod @@ -50,6 +50,7 @@ require ( gopkg.in/dancannon/gorethink.v3 v3.0.5 // indirect gopkg.in/fatih/pool.v2 v2.0.0 // indirect gopkg.in/gorethink/gorethink.v3 v3.0.5 // indirect + gotest.tools v2.2.0+incompatible gotest.tools/v3 v3.0.2 vbom.ml/util v0.0.0-20180919145318-efcd4e0f9787 // indirect ) diff --git a/ecs/pkg/docker/contextStore.go b/ecs/pkg/docker/contextStore.go index 2d40b9dd0..31e4bc4e8 100644 --- a/ecs/pkg/docker/contextStore.go +++ b/ecs/pkg/docker/contextStore.go @@ -3,6 +3,7 @@ package docker import ( "fmt" + "github.com/docker/cli/cli/command" cliconfig "github.com/docker/cli/cli/config" "github.com/docker/cli/cli/context/store" "github.com/mitchellh/mapstructure" @@ -25,7 +26,12 @@ type AwsContext struct { } func NewContext(name string, awsContext *AwsContext) error { - contextStore := initContextStore() + _, err := NewContextWithStore(name, awsContext, cliconfig.ContextStoreDir()) + return err +} + +func NewContextWithStore(name string, awsContext *AwsContext, contextDirectory string) (store.Store, error) { + contextStore := initContextStore(contextDirectory) endpoints := map[string]interface{}{ "aws": awsContext, "docker": awsContext, @@ -36,16 +42,19 @@ func NewContext(name string, awsContext *AwsContext) error { Endpoints: endpoints, Metadata: TypeContext{Type: contextType}, } - return contextStore.CreateOrUpdate(metadata) + return contextStore, contextStore.CreateOrUpdate(metadata) } -func initContextStore() store.Store { +func initContextStore(contextDir string) store.Store { config := store.NewConfig(getter) - return store.New(cliconfig.ContextStoreDir(), config) + return store.New(contextDir, config) } func CheckAwsContextExists(contextName string) (*AwsContext, error) { - contextStore := initContextStore() + if contextName == command.DefaultContextName { + return nil, fmt.Errorf("can't use \"%s\" with ECS command because it is not an AWS context", contextName) + } + contextStore := initContextStore(cliconfig.ContextStoreDir()) metadata, err := contextStore.GetMetadata(contextName) if err != nil { return nil, err diff --git a/ecs/tests/command_test.go b/ecs/tests/command_test.go new file mode 100644 index 000000000..a027fbaef --- /dev/null +++ b/ecs/tests/command_test.go @@ -0,0 +1,18 @@ +package tests + +import ( + "testing" + + "gotest.tools/v3/icmd" +) + +func TestExitErrorCode(t *testing.T) { + cmd, cleanup := dockerCli.createTestCmd() + defer cleanup() + + cmd.Command = dockerCli.Command("ecs", "unknown_command") + icmd.RunCmd(cmd).Assert(t, icmd.Expected{ + ExitCode: 1, + Err: "\"unknown_command\" is not a docker ecs command\nSee 'docker ecs --help'", + }) +} diff --git a/ecs/tests/main_test.go b/ecs/tests/main_test.go new file mode 100644 index 000000000..a093c6c9b --- /dev/null +++ b/ecs/tests/main_test.go @@ -0,0 +1,127 @@ +package tests + +import ( + "encoding/json" + "flag" + "fmt" + "io/ioutil" + "os" + "path/filepath" + "runtime" + "testing" + + dockerConfigFile "github.com/docker/cli/cli/config/configfile" + "github.com/docker/ecs-plugin/pkg/docker" + "gotest.tools/v3/icmd" +) + +var ( + e2ePath = flag.String("e2e-path", ".", "Set path to the e2e directory") + dockerCliPath = os.Getenv("DOCKERCLI_BINARY") + dockerCli dockerCliCommand + testContextName = "testAwsContextToBeRemoved" +) + +type dockerCliCommand struct { + path string + cliPluginDir string +} + +type ConfigFileOperator func(configFile *dockerConfigFile.ConfigFile) + +func (d dockerCliCommand) createTestCmd(ops ...ConfigFileOperator) (icmd.Cmd, func()) { + configDir, err := ioutil.TempDir("", "config") + if err != nil { + panic(err) + } + configFilePath := filepath.Join(configDir, "config.json") + config := dockerConfigFile.ConfigFile{ + CLIPluginsExtraDirs: []string{ + d.cliPluginDir, + }, + Filename: configFilePath, + } + for _, op := range ops { + op(&config) + } + configFile, err := os.Create(configFilePath) + if err != nil { + panic(err) + } + defer configFile.Close() + err = json.NewEncoder(configFile).Encode(config) + if err != nil { + panic(err) + } + + awsContext := docker.AwsContext{ + Profile: "TestProfile", + Cluster: "TestCluster", + Region: "TestRegion", + } + testStore, err := docker.NewContextWithStore(testContextName, &awsContext, filepath.Join(configDir, "contexts")) + if err != nil { + panic(err) + } + cleanup := func() { + fmt.Println("cleanup") + testStore.Remove(testContextName) + os.RemoveAll(configDir) + } + env := append(os.Environ(), + "DOCKER_CONFIG="+configDir, + "DOCKER_CLI_EXPERIMENTAL=enabled") // TODO: Remove this once docker ecs plugin is no more experimental + return icmd.Cmd{Env: env}, cleanup +} + +func (d dockerCliCommand) Command(args ...string) []string { + return append([]string{d.path, "--context", testContextName}, args...) +} + +func TestMain(m *testing.M) { + flag.Parse() + if err := os.Chdir(*e2ePath); err != nil { + panic(err) + } + cwd, err := os.Getwd() + if err != nil { + panic(err) + } + dockerEcs := os.Getenv("DOCKERECS_BINARY") + if dockerEcs == "" { + dockerEcs = filepath.Join(cwd, "../dist/docker-ecs") + } + dockerEcs, err = filepath.Abs(dockerEcs) + if err != nil { + panic(err) + } + if dockerCliPath == "" { + dockerCliPath = "docker" + } else { + dockerCliPath, err = filepath.Abs(dockerCliPath) + if err != nil { + panic(err) + } + } + // Prepare docker cli to call the docker-ecs plugin binary: + // - Create a symbolic link with the dockerEcs binary to the plugin directory + cliPluginDir, err := ioutil.TempDir("", "configContent") + if err != nil { + panic(err) + } + defer os.RemoveAll(cliPluginDir) + createDockerECSSymLink(dockerEcs, cliPluginDir) + + dockerCli = dockerCliCommand{path: dockerCliPath, cliPluginDir: cliPluginDir} + os.Exit(m.Run()) +} + +func createDockerECSSymLink(dockerEcs, configDir string) { + dockerEcsExecName := "docker-ecs" + if runtime.GOOS == "windows" { + dockerEcsExecName += ".exe" + } + if err := os.Symlink(dockerEcs, filepath.Join(configDir, dockerEcsExecName)); err != nil { + panic(err) + } +} diff --git a/ecs/tests/plugin_test.go b/ecs/tests/plugin_test.go new file mode 100644 index 000000000..f519c252a --- /dev/null +++ b/ecs/tests/plugin_test.go @@ -0,0 +1,33 @@ +package tests + +import ( + "regexp" + "testing" + + "gotest.tools/assert" + "gotest.tools/v3/golden" + "gotest.tools/v3/icmd" +) + +func TestInvokePluginFromCLI(t *testing.T) { + cmd, cleanup := dockerCli.createTestCmd() + defer cleanup() + // docker --help should list app as a top command + cmd.Command = dockerCli.Command("--help") + icmd.RunCmd(cmd).Assert(t, icmd.Expected{ + Out: "ecs* Docker ECS (Docker Inc.,", + }) + + // docker app --help prints docker-app help + cmd.Command = dockerCli.Command("ecs", "--help") + usage := icmd.RunCmd(cmd).Assert(t, icmd.Success).Combined() + + goldenFile := "plugin-usage.golden" + golden.Assert(t, usage, goldenFile) + + // docker info should print app version and short description + cmd.Command = dockerCli.Command("info") + re := regexp.MustCompile(`ecs: Docker ECS \(Docker Inc\., .*\)`) + output := icmd.RunCmd(cmd).Assert(t, icmd.Success).Combined() + assert.Assert(t, re.MatchString(output)) +} diff --git a/ecs/tests/setup_command_test.go b/ecs/tests/setup_command_test.go new file mode 100644 index 000000000..308d3e7ea --- /dev/null +++ b/ecs/tests/setup_command_test.go @@ -0,0 +1,34 @@ +package tests + +import ( + "strings" + "testing" + + "gotest.tools/assert" + "gotest.tools/v3/golden" + "gotest.tools/v3/icmd" +) + +func TestSetupMandatoryArguments(t *testing.T) { + cmd, cleanup := dockerCli.createTestCmd() + defer cleanup() + + cmd.Command = dockerCli.Command("ecs", "setup") + icmd.RunCmd(cmd).Assert(t, icmd.Expected{ + ExitCode: 1, + Err: "required flag(s) \"cluster\", \"profile\", \"region\" not set", + }) +} +func TestDefaultAwsContextName(t *testing.T) { + cmd, cleanup := dockerCli.createTestCmd() + defer cleanup() + + cmd.Command = dockerCli.Command("ecs", "setup", "--cluster", "clusterName", "--profile", "profileName", + "--region", "regionName") + icmd.RunCmd(cmd).Assert(t, icmd.Success) + + cmd.Command = dockerCli.Command("context", "inspect", "aws") + output := icmd.RunCmd(cmd).Assert(t, icmd.Success).Combined() + expected := golden.Get(t, "context-inspect.golden") + assert.Assert(t, strings.HasPrefix(output, string(expected))) +} diff --git a/ecs/tests/testdata/context-inspect.golden b/ecs/tests/testdata/context-inspect.golden new file mode 100644 index 000000000..ff61b55fc --- /dev/null +++ b/ecs/tests/testdata/context-inspect.golden @@ -0,0 +1,16 @@ +[ + { + "Name": "aws", + "Metadata": {}, + "Endpoints": { + "aws": { + "Cluster": "clusterName", + "Profile": "profileName", + "Region": "regionName" + }, + "docker": { + "SkipTLSVerify": false + } + }, + "TLSMaterial": {}, + "Storage": \ No newline at end of file diff --git a/ecs/tests/testdata/plugin-usage.golden b/ecs/tests/testdata/plugin-usage.golden new file mode 100644 index 000000000..8114b6f43 --- /dev/null +++ b/ecs/tests/testdata/plugin-usage.golden @@ -0,0 +1,14 @@ + +Usage: docker ecs COMMAND + +run multi-container applications on Amazon ECS. + +Management Commands: + compose + secret Manages secrets + +Commands: + setup + version Show version. + +Run 'docker ecs COMMAND --help' for more information on a command. From 1889d04d831dd94ce929eb610409fbcebed26010 Mon Sep 17 00:00:00 2001 From: Nicolas De Loof <nicolas.deloof@gmail.com> Date: Tue, 5 May 2020 14:45:34 +0200 Subject: [PATCH 055/198] Implement "network" using SecurityGroups Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com> --- ecs/pkg/amazon/client.go | 1 + ecs/pkg/amazon/cloudformation.go | 74 +++++++++++++++++++++++--------- ecs/pkg/amazon/validate.go | 9 ++++ 3 files changed, 63 insertions(+), 21 deletions(-) diff --git a/ecs/pkg/amazon/client.go b/ecs/pkg/amazon/client.go index 2f6fd4331..53b40af65 100644 --- a/ecs/pkg/amazon/client.go +++ b/ecs/pkg/amazon/client.go @@ -8,6 +8,7 @@ import ( const ( ProjectTag = "com.docker.compose.project" + NetworkTag = "com.docker.compose.network" ) func NewClient(profile string, cluster string, region string) (compose.API, error) { diff --git a/ecs/pkg/amazon/cloudformation.go b/ecs/pkg/amazon/cloudformation.go index 4a9fcda9c..45740b48b 100644 --- a/ecs/pkg/amazon/cloudformation.go +++ b/ecs/pkg/amazon/cloudformation.go @@ -6,14 +6,16 @@ import ( "fmt" "strings" - ecsapi "github.com/aws/aws-sdk-go/service/ecs" cloudmapapi "github.com/aws/aws-sdk-go/service/servicediscovery" + + ecsapi "github.com/aws/aws-sdk-go/service/ecs" "github.com/awslabs/goformation/v4/cloudformation" "github.com/awslabs/goformation/v4/cloudformation/ec2" "github.com/awslabs/goformation/v4/cloudformation/ecs" "github.com/awslabs/goformation/v4/cloudformation/iam" "github.com/awslabs/goformation/v4/cloudformation/logs" cloudmap "github.com/awslabs/goformation/v4/cloudformation/servicediscovery" + "github.com/awslabs/goformation/v4/cloudformation/tags" "github.com/docker/ecs-plugin/pkg/compose" ) @@ -29,25 +31,9 @@ func (c client) Convert(ctx context.Context, project *compose.Project) (*cloudfo return nil, err } - var ingresses = []ec2.SecurityGroup_Ingress{} - for _, service := range project.Services { - for _, port := range service.Ports { - ingresses = append(ingresses, ec2.SecurityGroup_Ingress{ - CidrIp: "0.0.0.0/0", - Description: fmt.Sprintf("%s:%d/%s", service.Name, port.Target, port.Protocol), - FromPort: int(port.Target), - IpProtocol: strings.ToUpper(port.Protocol), - ToPort: int(port.Target), - }) - } - } - - securityGroup := fmt.Sprintf("%s Security Group", project.Name) - template.Resources["SecurityGroup"] = &ec2.SecurityGroup{ - GroupDescription: securityGroup, - GroupName: securityGroup, - SecurityGroupIngress: ingresses, - VpcId: vpc, + for net := range project.Networks { + name, resource := convertNetwork(project, net, vpc) + template.Resources[name] = resource } logGroup := fmt.Sprintf("/docker-compose/%s", project.Name) @@ -104,6 +90,12 @@ func (c client) Convert(ctx context.Context, project *compose.Project) (*cloudfo }, } + serviceSecurityGroups := []string{} + for net := range service.Networks { + logicalName := networkResourceName(project, net) + serviceSecurityGroups = append(serviceSecurityGroups, cloudformation.Ref(logicalName)) + } + template.Resources[fmt.Sprintf("%sService", service.Name)] = &ecs.Service{ Cluster: c.Cluster, DesiredCount: 1, @@ -111,7 +103,7 @@ func (c client) Convert(ctx context.Context, project *compose.Project) (*cloudfo NetworkConfiguration: &ecs.Service_NetworkConfiguration{ AwsvpcConfiguration: &ecs.Service_AwsVpcConfiguration{ AssignPublicIp: ecsapi.AssignPublicIpEnabled, - SecurityGroups: []string{cloudformation.Ref("SecurityGroup")}, + SecurityGroups: serviceSecurityGroups, Subnets: subnets, }, }, @@ -128,6 +120,46 @@ func (c client) Convert(ctx context.Context, project *compose.Project) (*cloudfo return template, nil } +func convertNetwork(project *compose.Project, net string, vpc string) (string, cloudformation.Resource) { + var ingresses []ec2.SecurityGroup_Ingress + for _, service := range project.Services { + if _, ok := service.Networks[net]; ok { + for _, port := range service.Ports { + ingresses = append(ingresses, ec2.SecurityGroup_Ingress{ + CidrIp: "0.0.0.0/0", + Description: fmt.Sprintf("%s:%d/%s", service.Name, port.Target, port.Protocol), + FromPort: int(port.Target), + IpProtocol: strings.ToUpper(port.Protocol), + ToPort: int(port.Target), + }) + } + } + } + + securityGroup := networkResourceName(project, net) + resource := &ec2.SecurityGroup{ + GroupDescription: fmt.Sprintf("%s %s Security Group", project.Name, net), + GroupName: securityGroup, + SecurityGroupIngress: ingresses, + VpcId: vpc, + Tags: []tags.Tag{ + { + Key: ProjectTag, + Value: project.Name, + }, + { + Key: NetworkTag, + Value: net, + }, + }, + } + return securityGroup, resource +} + +func networkResourceName(project *compose.Project, network string) string { + return fmt.Sprintf("%s%sNetwork", project.Name, strings.Title(network)) +} + func (c client) GetVPC(ctx context.Context, project *compose.Project) (string, error) { //check compose file for the default external network if net, ok := project.Networks["default"]; ok { diff --git a/ecs/pkg/amazon/validate.go b/ecs/pkg/amazon/validate.go index 5e61a273d..551dc7bc7 100644 --- a/ecs/pkg/amazon/validate.go +++ b/ecs/pkg/amazon/validate.go @@ -15,6 +15,15 @@ func (c *client) Validate(project *compose.Project) error { } } + for i, service := range project.Services { + if len(service.Networks) == 0 { + // Service without explicit network attachment are implicitly exposed on default network + // FIXME move this to compose-go + service.Networks = map[string]*types.ServiceNetworkConfig{"default": nil} + project.Services[i] = service + } + } + // Here we can check for incompatible attributes, inject sane defaults, etc return nil } From 3e785e2cb055fa226e31d8c530e6b19232bbe942 Mon Sep 17 00:00:00 2001 From: Guillaume Lours <guillaume.lours@docker.com> Date: Wed, 6 May 2020 12:28:33 +0200 Subject: [PATCH 056/198] Fix initialization issue of aws context with PreRun function Signed-off-by: Guillaume Lours <guillaume.lours@docker.com> Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com> --- ecs/cmd/commands/compose.go | 27 +++++++++++++++++--------- ecs/cmd/commands/secret.go | 35 +++++++++++++++++----------------- ecs/cmd/main/main.go | 17 +++-------------- ecs/pkg/docker/contextStore.go | 20 ++++++++++++++++++- 4 files changed, 58 insertions(+), 41 deletions(-) diff --git a/ecs/cmd/commands/compose.go b/ecs/cmd/commands/compose.go index 459a48813..06fc80a80 100644 --- a/ecs/cmd/commands/compose.go +++ b/ecs/cmd/commands/compose.go @@ -4,13 +4,14 @@ import ( "context" "fmt" + "github.com/docker/cli/cli/command" "github.com/docker/ecs-plugin/pkg/amazon" "github.com/docker/ecs-plugin/pkg/compose" "github.com/docker/ecs-plugin/pkg/docker" "github.com/spf13/cobra" ) -func ComposeCommand(clusteropts *docker.AwsContext) *cobra.Command { +func ComposeCommand(dockerCli command.Cli) *cobra.Command { cmd := &cobra.Command{ Use: "compose", } @@ -18,9 +19,9 @@ func ComposeCommand(clusteropts *docker.AwsContext) *cobra.Command { opts.AddFlags(cmd.Flags()) cmd.AddCommand( - ConvertCommand(clusteropts, opts), - UpCommand(clusteropts, opts), - DownCommand(clusteropts, opts), + ConvertCommand(dockerCli, opts), + UpCommand(dockerCli, opts), + DownCommand(dockerCli, opts), ) return cmd } @@ -36,10 +37,14 @@ func (o upOptions) LoadBalancerArn() *string { return &o.loadBalancerArn } -func ConvertCommand(clusteropts *docker.AwsContext, projectOpts *compose.ProjectOptions) *cobra.Command { +func ConvertCommand(dockerCli command.Cli, projectOpts *compose.ProjectOptions) *cobra.Command { cmd := &cobra.Command{ Use: "convert", RunE: compose.WithProject(projectOpts, func(project *compose.Project, args []string) error { + clusteropts, err := docker.GetAwsContext(dockerCli) + if err != nil { + return err + } client, err := amazon.NewClient(clusteropts.Profile, clusteropts.Cluster, clusteropts.Region) if err != nil { return err @@ -61,11 +66,15 @@ func ConvertCommand(clusteropts *docker.AwsContext, projectOpts *compose.Project return cmd } -func UpCommand(clusteropts *docker.AwsContext, projectOpts *compose.ProjectOptions) *cobra.Command { +func UpCommand(dockerCli command.Cli, projectOpts *compose.ProjectOptions) *cobra.Command { opts := upOptions{} cmd := &cobra.Command{ Use: "up", RunE: compose.WithProject(projectOpts, func(project *compose.Project, args []string) error { + clusteropts, err := docker.GetAwsContext(dockerCli) + if err != nil { + return err + } client, err := amazon.NewClient(clusteropts.Profile, clusteropts.Cluster, clusteropts.Region) if err != nil { return err @@ -81,11 +90,11 @@ type downOptions struct { DeleteCluster bool } -func DownCommand(clusteropts *docker.AwsContext, projectOpts *compose.ProjectOptions) *cobra.Command { +func DownCommand(dockerCli command.Cli, projectOpts *compose.ProjectOptions) *cobra.Command { opts := downOptions{} cmd := &cobra.Command{ Use: "down", - RunE: func(cmd *cobra.Command, args []string) error { + RunE: docker.WithAwsContext(dockerCli, func(clusteropts docker.AwsContext, args []string) error { client, err := amazon.NewClient(clusteropts.Profile, clusteropts.Cluster, clusteropts.Region) if err != nil { return err @@ -105,7 +114,7 @@ func DownCommand(clusteropts *docker.AwsContext, projectOpts *compose.ProjectOpt } } return nil - }, + }), } cmd.Flags().BoolVar(&opts.DeleteCluster, "delete-cluster", false, "Delete cluster") return cmd diff --git a/ecs/cmd/commands/secret.go b/ecs/cmd/commands/secret.go index b46eeeb3d..8488e6cb1 100644 --- a/ecs/cmd/commands/secret.go +++ b/ecs/cmd/commands/secret.go @@ -9,6 +9,7 @@ import ( "strings" "text/tabwriter" + "github.com/docker/cli/cli/command" "github.com/docker/ecs-plugin/pkg/amazon" "github.com/docker/ecs-plugin/pkg/docker" "github.com/spf13/cobra" @@ -22,27 +23,27 @@ type deleteSecretOptions struct { recover bool } -func SecretCommand(clusteropts *docker.AwsContext) *cobra.Command { +func SecretCommand(dockerCli command.Cli) *cobra.Command { cmd := &cobra.Command{ Use: "secret", Short: "Manages secrets", } cmd.AddCommand( - CreateSecret(clusteropts), - InspectSecret(clusteropts), - ListSecrets(clusteropts), - DeleteSecret(clusteropts), + CreateSecret(dockerCli), + InspectSecret(dockerCli), + ListSecrets(dockerCli), + DeleteSecret(dockerCli), ) return cmd } -func CreateSecret(clusteropts *docker.AwsContext) *cobra.Command { +func CreateSecret(dockerCli command.Cli) *cobra.Command { //opts := createSecretOptions{} cmd := &cobra.Command{ Use: "create NAME SECRET", Short: "Creates a secret.", - RunE: func(cmd *cobra.Command, args []string) error { + RunE: docker.WithAwsContext(dockerCli, func(clusteropts docker.AwsContext, args []string) error { client, err := amazon.NewClient(clusteropts.Profile, clusteropts.Cluster, clusteropts.Region) if err != nil { return err @@ -55,16 +56,16 @@ func CreateSecret(clusteropts *docker.AwsContext) *cobra.Command { id, err := client.CreateSecret(context.Background(), name, secret) fmt.Println(id) return err - }, + }), } return cmd } -func InspectSecret(clusteropts *docker.AwsContext) *cobra.Command { +func InspectSecret(dockerCli command.Cli) *cobra.Command { cmd := &cobra.Command{ Use: "inspect ID", Short: "Displays secret details", - RunE: func(cmd *cobra.Command, args []string) error { + RunE: docker.WithAwsContext(dockerCli, func(clusteropts docker.AwsContext, args []string) error { client, err := amazon.NewClient(clusteropts.Profile, clusteropts.Cluster, clusteropts.Region) if err != nil { return err @@ -83,17 +84,17 @@ func InspectSecret(clusteropts *docker.AwsContext) *cobra.Command { } fmt.Println(out) return nil - }, + }), } return cmd } -func ListSecrets(clusteropts *docker.AwsContext) *cobra.Command { +func ListSecrets(dockerCli command.Cli) *cobra.Command { cmd := &cobra.Command{ Use: "list", Aliases: []string{"ls"}, Short: "List secrets stored for the existing account.", - RunE: func(cmd *cobra.Command, args []string) error { + RunE: docker.WithAwsContext(dockerCli, func(clusteropts docker.AwsContext, args []string) error { client, err := amazon.NewClient(clusteropts.Profile, clusteropts.Cluster, clusteropts.Region) if err != nil { return err @@ -105,18 +106,18 @@ func ListSecrets(clusteropts *docker.AwsContext) *cobra.Command { printList(os.Stdout, secrets) return nil - }, + }), } return cmd } -func DeleteSecret(clusteropts *docker.AwsContext) *cobra.Command { +func DeleteSecret(dockerCli command.Cli) *cobra.Command { opts := deleteSecretOptions{} cmd := &cobra.Command{ Use: "delete NAME", Aliases: []string{"rm", "remove"}, Short: "Removes a secret.", - RunE: func(cmd *cobra.Command, args []string) error { + RunE: docker.WithAwsContext(dockerCli, func(clusteropts docker.AwsContext, args []string) error { client, err := amazon.NewClient(clusteropts.Profile, clusteropts.Cluster, clusteropts.Region) if err != nil { return err @@ -125,7 +126,7 @@ func DeleteSecret(clusteropts *docker.AwsContext) *cobra.Command { return errors.New("Missing mandatory parameter: [NAME]") } return client.DeleteSecret(context.Background(), args[0], opts.recover) - }, + }), } cmd.Flags().BoolVar(&opts.recover, "recover", false, "Enable recovery.") return cmd diff --git a/ecs/cmd/main/main.go b/ecs/cmd/main/main.go index 812ccfeea..9af648292 100644 --- a/ecs/cmd/main/main.go +++ b/ecs/cmd/main/main.go @@ -7,7 +7,6 @@ import ( "github.com/docker/cli/cli-plugins/plugin" "github.com/docker/cli/cli/command" commands "github.com/docker/ecs-plugin/cmd/commands" - "github.com/docker/ecs-plugin/pkg/docker" "github.com/spf13/cobra" ) @@ -27,33 +26,23 @@ func main() { // NewRootCmd returns the base root command. func NewRootCmd(name string, dockerCli command.Cli) *cobra.Command { - var opts *docker.AwsContext - cmd := &cobra.Command{ Short: "Docker ECS", Long: `run multi-container applications on Amazon ECS.`, Use: name, Annotations: map[string]string{"experimentalCLI": "true"}, - PersistentPreRunE: func(cmd *cobra.Command, args []string) error { - err := plugin.PersistentPreRunE(cmd, args) - if err != nil { - return err - } - contextName := dockerCli.CurrentContext() - opts, err = docker.CheckAwsContextExists(contextName) - return err - }, RunE: func(cmd *cobra.Command, args []string) error { if len(args) != 0 { return fmt.Errorf("%q is not a docker ecs command\nSee 'docker ecs --help'", args[0]) } + cmd.Help() return nil }, } cmd.AddCommand( VersionCommand(), - commands.ComposeCommand(opts), - commands.SecretCommand(opts), + commands.ComposeCommand(dockerCli), + commands.SecretCommand(dockerCli), commands.SetupCommand(), ) return cmd diff --git a/ecs/pkg/docker/contextStore.go b/ecs/pkg/docker/contextStore.go index 31e4bc4e8..b038eb924 100644 --- a/ecs/pkg/docker/contextStore.go +++ b/ecs/pkg/docker/contextStore.go @@ -7,6 +7,7 @@ import ( cliconfig "github.com/docker/cli/cli/config" "github.com/docker/cli/cli/context/store" "github.com/mitchellh/mapstructure" + "github.com/spf13/cobra" ) const contextType = "aws" @@ -50,7 +51,7 @@ func initContextStore(contextDir string) store.Store { return store.New(contextDir, config) } -func CheckAwsContextExists(contextName string) (*AwsContext, error) { +func checkAwsContextExists(contextName string) (*AwsContext, error) { if contextName == command.DefaultContextName { return nil, fmt.Errorf("can't use \"%s\" with ECS command because it is not an AWS context", contextName) } @@ -70,3 +71,20 @@ func CheckAwsContextExists(contextName string) (*AwsContext, error) { } return &awsContext, nil } + +type ContextFunc func(ctx AwsContext, args []string) error + +func WithAwsContext(dockerCli command.Cli, f ContextFunc) func(cmd *cobra.Command, args []string) error { + return func(cmd *cobra.Command, args []string) error { + ctx, err := GetAwsContext(dockerCli) + if err != nil { + return err + } + return f(*ctx, args) + } +} + +func GetAwsContext(dockerCli command.Cli) (*AwsContext, error) { + contextName := dockerCli.CurrentContext() + return checkAwsContextExists(contextName) +} From f95bd4fdbf4859ba56ffa097eb3a581f640e6b9c Mon Sep 17 00:00:00 2001 From: aiordache <anca.iordache@docker.com> Date: Wed, 6 May 2020 18:36:22 +0200 Subject: [PATCH 057/198] mapping cpu and memory to fargate values Signed-off-by: aiordache <anca.iordache@docker.com> Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com> --- ecs/pkg/amazon/convert.go | 102 +++++++++++++++----------------------- 1 file changed, 40 insertions(+), 62 deletions(-) diff --git a/ecs/pkg/amazon/convert.go b/ecs/pkg/amazon/convert.go index a0bd9e423..6021b8826 100644 --- a/ecs/pkg/amazon/convert.go +++ b/ecs/pkg/amazon/convert.go @@ -1,6 +1,8 @@ package amazon import ( + "errors" + "strconv" "strings" "time" @@ -13,7 +15,7 @@ import ( ) func Convert(project *compose.Project, service types.ServiceConfig) (*ecs.TaskDefinition, error) { - _, err := toCPULimits(service) + cpu, mem, err := toLimits(service) if err != nil { return nil, err } @@ -23,7 +25,6 @@ func Convert(project *compose.Project, service types.ServiceConfig) (*ecs.TaskDe // Here we can declare sidecars and init-containers using https://docs.aws.amazon.com/AmazonECS/latest/developerguide/task_definition_parameters.html#container_definition_dependson { Command: service.Command, - Cpu: 256, DisableNetworking: service.NetworkMode == "none", DnsSearchDomains: service.DNSSearch, DnsServers: service.DNS, @@ -48,8 +49,6 @@ func Convert(project *compose.Project, service types.ServiceConfig) (*ecs.TaskDe "awslogs-stream-prefix": service.Name, }, }, - Memory: toMemoryLimits(service.Deploy), - MemoryReservation: toMemoryReservation(service.Deploy), MountPoints: nil, Name: service.Name, PortMappings: toPortMappings(service.Ports), @@ -68,10 +67,10 @@ func Convert(project *compose.Project, service types.ServiceConfig) (*ecs.TaskDe WorkingDirectory: service.WorkingDir, }, }, - Cpu: toCPU(service), + Cpu: cpu, Family: project.Name, IpcMode: service.Ipc, - Memory: toMemory(service), + Memory: mem, NetworkMode: ecsapi.NetworkModeAwsvpc, // FIXME could be set by service.NetworkMode, Fargate only supports network mode ‘awsvpc’. PidMode: service.Pid, PlacementConstraints: toPlacementConstraints(service.Deploy), @@ -82,32 +81,48 @@ func Convert(project *compose.Project, service types.ServiceConfig) (*ecs.TaskDe }, nil } -func toCPU(service types.ServiceConfig) string { - // FIXME based on service's memory/cpu requirements, select the adequate Fargate CPU - return "256" -} +func toLimits(service types.ServiceConfig) (string, string, error) { + // All possible cpu/mem values for Fargate + cpuToMem := map[int64][]types.UnitBytes{ + 256: {512, 1024, 2048}, + 512: {1024, 2048, 3072, 4096}, + 1024: {2048, 3072, 4096, 5120, 6144, 7168, 8192}, + 2048: {4096, 5120, 6144, 7168, 8192, 9216, 10240, 11264, 12288, 13312, 14336, 15360, 16384}, + 4096: {8192, 9216, 10240, 11264, 12288, 13312, 14336, 15360, 16384, 17408, 18432, 19456, 20480, 21504, 22528, 23552, 24576, 25600, 26624, 27648, 28672, 29696, 30720}, + } + cpuLimit := "256" + memLimit := "512" -func toMemory(service types.ServiceConfig) string { - // FIXME based on service's memory/cpu requirements, select the adequate Fargate CPU - return "512" -} - -func toCPULimits(service types.ServiceConfig) (*int64, error) { if service.Deploy == nil { - return nil, nil + return cpuLimit, memLimit, nil } - res := service.Deploy.Resources.Limits - if res == nil { - return nil, nil + + limits := service.Deploy.Resources.Limits + if limits == nil { + return cpuLimit, memLimit, nil } - if res.NanoCPUs == "" { - return nil, nil + + if limits.NanoCPUs == "" { + return cpuLimit, memLimit, nil } - v, err := opts.ParseCPUs(res.NanoCPUs) + + v, err := opts.ParseCPUs(limits.NanoCPUs) if err != nil { - return nil, err + return "", "", err } - return &v, nil + + for cpu, mem := range cpuToMem { + if v <= cpu*1024*1024 { + for _, m := range mem { + if limits.MemoryBytes <= m*1024*1024 { + cpuLimit = strconv.FormatInt(cpu, 10) + memLimit = strconv.FormatInt(int64(m), 10) + return cpuLimit, memLimit, nil + } + } + } + } + return "", "", errors.New("unable to find cpu/mem for the required resources") } func toRequiresCompatibilities(isolation string) []*string { @@ -117,19 +132,6 @@ func toRequiresCompatibilities(isolation string) []*string { return []*string{&isolation} } -func hasMemoryOrMemoryReservation(service types.ServiceConfig) bool { - if service.Deploy == nil { - return false - } - if service.Deploy.Resources.Reservations != nil { - return true - } - if service.Deploy.Resources.Limits != nil { - return true - } - return false -} - func toPlacementConstraints(deploy *types.DeployConfig) []ecs.TaskDefinition_TaskDefinitionPlacementConstraint { if deploy == nil || deploy.Placement.Constraints == nil || len(deploy.Placement.Constraints) == 0 { return nil @@ -176,30 +178,6 @@ func toUlimits(ulimits map[string]*types.UlimitsConfig) []ecs.TaskDefinition_Uli const Mb = 1024 * 1024 -func toMemoryLimits(deploy *types.DeployConfig) int { - if deploy == nil { - return 0 - } - res := deploy.Resources.Limits - if res == nil { - return 0 - } - v := int(res.MemoryBytes) / Mb - return v -} - -func toMemoryReservation(deploy *types.DeployConfig) int { - if deploy == nil { - return 0 - } - res := deploy.Resources.Reservations - if res == nil { - return 0 - } - v := int(res.MemoryBytes) / Mb - return v -} - func toLinuxParameters(service types.ServiceConfig) *ecs.TaskDefinition_LinuxParameters { return &ecs.TaskDefinition_LinuxParameters{ Capabilities: toKernelCapabilities(service.CapAdd, service.CapDrop), From 57d7474f7d3dc97b41755a4497d5fde4168f7923 Mon Sep 17 00:00:00 2001 From: aiordache <anca.iordache@docker.com> Date: Thu, 30 Apr 2020 17:31:25 +0200 Subject: [PATCH 058/198] set secrets in cloudformation template Signed-off-by: aiordache <anca.iordache@docker.com> Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com> --- ecs/pkg/amazon/convert.go | 42 ++++++++++++++++++++++++++++++++++++--- 1 file changed, 39 insertions(+), 3 deletions(-) diff --git a/ecs/pkg/amazon/convert.go b/ecs/pkg/amazon/convert.go index 6021b8826..4d75072c1 100644 --- a/ecs/pkg/amazon/convert.go +++ b/ecs/pkg/amazon/convert.go @@ -19,7 +19,14 @@ func Convert(project *compose.Project, service types.ServiceConfig) (*ecs.TaskDe if err != nil { return nil, err } - + credential, err := getRepoCredentials(service) + if err != nil { + return nil, err + } + secrets, err := getSecrets(service) + if err != nil { + return nil, err + } return &ecs.TaskDefinition{ ContainerDefinitions: []ecs.TaskDefinition_ContainerDefinition{ // Here we can declare sidecars and init-containers using https://docs.aws.amazon.com/AmazonECS/latest/developerguide/task_definition_parameters.html#container_definition_dependson @@ -55,9 +62,9 @@ func Convert(project *compose.Project, service types.ServiceConfig) (*ecs.TaskDe Privileged: service.Privileged, PseudoTerminal: service.Tty, ReadonlyRootFilesystem: service.ReadOnly, - RepositoryCredentials: nil, + RepositoryCredentials: credential, ResourceRequirements: nil, - Secrets: nil, + Secrets: secrets, StartTimeout: 0, StopTimeout: durationToInt(service.StopGracePeriod), SystemControls: nil, @@ -274,3 +281,32 @@ func toKeyValuePair(environment types.MappingWithEquals) []ecs.TaskDefinition_Ke } return pairs } + +func getRepoCredentials(service types.ServiceConfig) (*ecs.TaskDefinition_RepositoryCredentials, error) { + // extract registry and namespace string from image name + fields := strings.Split(service.Image, "/") + regPath := "" + for i, field := range fields { + if i < len(fields)-1 { + regPath = regPath + field + } + } + if regPath == "" || len(service.Secrets) == 0 { + return nil, nil + } + for _, secret := range service.Secrets { + if secret.Source == regPath { + return &ecs.TaskDefinition_RepositoryCredentials{CredentialsParameter: secret.Target}, nil + } + } + return nil, nil +} + +func getSecrets(service types.ServiceConfig) ([]ecs.TaskDefinition_Secret, error) { + secrets := []ecs.TaskDefinition_Secret{} + + for _, secret := range service.Secrets { + secrets = append(secrets, ecs.TaskDefinition_Secret{Name: secret.Target}) + } + return secrets, nil +} From d09c8c7236749a1bcb6af056d0aed15ed7d49f02 Mon Sep 17 00:00:00 2001 From: aiordache <anca.iordache@docker.com> Date: Tue, 5 May 2020 18:55:03 +0200 Subject: [PATCH 059/198] add private images support Signed-off-by: aiordache <anca.iordache@docker.com> Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com> --- ecs/cmd/commands/secret.go | 17 +++++++---- ecs/pkg/amazon/cloudformation.go | 48 ++++++++++++++++++++++++++++---- ecs/pkg/amazon/convert.go | 33 ++++++++++++---------- ecs/pkg/amazon/mock/api.go | 11 ++++---- ecs/pkg/amazon/sdk.go | 15 ++++++++-- ecs/pkg/amazon/secrets.go | 6 ++-- ecs/pkg/compose/api.go | 2 +- ecs/pkg/docker/secret.go | 23 +++++++++++++++ 8 files changed, 119 insertions(+), 36 deletions(-) diff --git a/ecs/cmd/commands/secret.go b/ecs/cmd/commands/secret.go index 8488e6cb1..f964f1eae 100644 --- a/ecs/cmd/commands/secret.go +++ b/ecs/cmd/commands/secret.go @@ -16,7 +16,10 @@ import ( ) type createSecretOptions struct { - Label string + Label string + Username string + Password string + Description string } type deleteSecretOptions struct { @@ -39,9 +42,9 @@ func SecretCommand(dockerCli command.Cli) *cobra.Command { } func CreateSecret(dockerCli command.Cli) *cobra.Command { - //opts := createSecretOptions{} + opts := createSecretOptions{} cmd := &cobra.Command{ - Use: "create NAME SECRET", + Use: "create NAME", Short: "Creates a secret.", RunE: docker.WithAwsContext(dockerCli, func(clusteropts docker.AwsContext, args []string) error { client, err := amazon.NewClient(clusteropts.Profile, clusteropts.Cluster, clusteropts.Region) @@ -52,12 +55,16 @@ func CreateSecret(dockerCli command.Cli) *cobra.Command { return errors.New("Missing mandatory parameter: NAME") } name := args[0] - secret := args[1] - id, err := client.CreateSecret(context.Background(), name, secret) + + secret := docker.NewSecret(name, opts.Username, opts.Password, opts.Description) + id, err := client.CreateSecret(context.Background(), secret) fmt.Println(id) return err }), } + cmd.Flags().StringVarP(&opts.Username, "username", "u", "", "username") + cmd.Flags().StringVarP(&opts.Password, "password", "p", "", "password") + cmd.Flags().StringVarP(&opts.Description, "description", "d", "", "Secret description") return cmd } diff --git a/ecs/pkg/amazon/cloudformation.go b/ecs/pkg/amazon/cloudformation.go index 45740b48b..fa7f2f8ac 100644 --- a/ecs/pkg/amazon/cloudformation.go +++ b/ecs/pkg/amazon/cloudformation.go @@ -55,17 +55,28 @@ func (c client) Convert(ctx context.Context, project *compose.Project) (*cloudfo } taskExecutionRole := fmt.Sprintf("%sTaskExecutionRole", service.Name) + policy, err := c.getPolicy(ctx, definition) + if err != nil { + return nil, err + } + rolePolicies := []iam.Role_Policy{} + if policy != nil { + rolePolicies = append(rolePolicies, iam.Role_Policy{ + PolicyDocument: policy, + PolicyName: taskExecutionRole, + }) + + } + definition.ExecutionRoleArn = cloudformation.Ref(taskExecutionRole) + + taskDefinition := fmt.Sprintf("%sTaskDefinition", service.Name) template.Resources[taskExecutionRole] = &iam.Role{ AssumeRolePolicyDocument: assumeRolePolicyDocument, - // Here we can grant access to secrets/configs using a Policy { Allow,ssm:GetParameters,secret|config ARN} + Policies: rolePolicies, ManagedPolicyArns: []string{ ECSTaskExecutionPolicy, }, } - definition.ExecutionRoleArn = cloudformation.Ref(taskExecutionRole) - // FIXME definition.TaskRoleArn = ? - - taskDefinition := fmt.Sprintf("%sTaskDefinition", service.Name) template.Resources[taskDefinition] = definition var healthCheck *cloudmap.Service_HealthCheckConfig @@ -182,6 +193,33 @@ func (c client) GetVPC(ctx context.Context, project *compose.Project) (string, e return defaultVPC, nil } +func (c client) getPolicy(ctx context.Context, taskDef *ecs.TaskDefinition) (*PolicyDocument, error) { + + arns := []string{} + for _, container := range taskDef.ContainerDefinitions { + if container.RepositoryCredentials != nil { + arns = append(arns, container.RepositoryCredentials.CredentialsParameter) + } + if len(container.Secrets) > 0 { + for _, s := range container.Secrets { + arns = append(arns, s.ValueFrom) + } + } + + } + if len(arns) > 0 { + return &PolicyDocument{ + Statement: []PolicyStatement{ + { + Effect: "Allow", + Action: []string{"secretsmanager:GetSecretValue", "ssm:GetParameters", "kms:Decrypt"}, + Resource: arns, + }}, + }, nil + } + return nil, nil +} + type convertAPI interface { GetDefaultVPC(ctx context.Context) (string, error) VpcExists(ctx context.Context, vpcID string) (bool, error) diff --git a/ecs/pkg/amazon/convert.go b/ecs/pkg/amazon/convert.go index 4d75072c1..50c722ffd 100644 --- a/ecs/pkg/amazon/convert.go +++ b/ecs/pkg/amazon/convert.go @@ -44,7 +44,7 @@ func Convert(project *compose.Project, service types.ServiceConfig) (*ecs.TaskDe FirelensConfiguration: nil, HealthCheck: toHealthCheck(service.HealthCheck), Hostname: service.Hostname, - Image: service.Image, + Image: getImage(service.Image), Interactive: false, Links: nil, LinuxParameters: toLinuxParameters(service), @@ -282,22 +282,27 @@ func toKeyValuePair(environment types.MappingWithEquals) []ecs.TaskDefinition_Ke return pairs } +func getImage(image string) string { + switch f := strings.Split(image, "/"); len(f) { + case 1: + return "docker.io/library/" + image + case 2: + return "docker.io/" + image + default: + return image + } +} + func getRepoCredentials(service types.ServiceConfig) (*ecs.TaskDefinition_RepositoryCredentials, error) { // extract registry and namespace string from image name - fields := strings.Split(service.Image, "/") - regPath := "" - for i, field := range fields { - if i < len(fields)-1 { - regPath = regPath + field + credential := "" + for key, value := range service.Extras { + if strings.HasPrefix(key, "x-aws-pull_credentials") { + credential = value.(string) } } - if regPath == "" || len(service.Secrets) == 0 { - return nil, nil - } - for _, secret := range service.Secrets { - if secret.Source == regPath { - return &ecs.TaskDefinition_RepositoryCredentials{CredentialsParameter: secret.Target}, nil - } + if credential != "" { + return &ecs.TaskDefinition_RepositoryCredentials{CredentialsParameter: credential}, nil } return nil, nil } @@ -306,7 +311,7 @@ func getSecrets(service types.ServiceConfig) ([]ecs.TaskDefinition_Secret, error secrets := []ecs.TaskDefinition_Secret{} for _, secret := range service.Secrets { - secrets = append(secrets, ecs.TaskDefinition_Secret{Name: secret.Target}) + secrets = append(secrets, ecs.TaskDefinition_Secret{Name: secret.Target, ValueFrom: secret.Source}) } return secrets, nil } diff --git a/ecs/pkg/amazon/mock/api.go b/ecs/pkg/amazon/mock/api.go index 4a1163315..7eba94054 100644 --- a/ecs/pkg/amazon/mock/api.go +++ b/ecs/pkg/amazon/mock/api.go @@ -6,11 +6,12 @@ package mock import ( context "context" + reflect "reflect" + cloudformation "github.com/aws/aws-sdk-go/service/cloudformation" cloudformation0 "github.com/awslabs/goformation/v4/cloudformation" docker "github.com/docker/ecs-plugin/pkg/docker" gomock "github.com/golang/mock/gomock" - reflect "reflect" ) // MockAPI is a mock of API interface @@ -67,18 +68,18 @@ func (mr *MockAPIMockRecorder) CreateCluster(arg0, arg1 interface{}) *gomock.Cal } // CreateSecret mocks base method -func (m *MockAPI) CreateSecret(arg0 context.Context, arg1, arg2 string) (string, error) { +func (m *MockAPI) CreateSecret(arg0 context.Context, arg1 docker.Secret) (string, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "CreateSecret", arg0, arg1, arg2) + ret := m.ctrl.Call(m, "CreateSecret", arg0, arg1) ret0, _ := ret[0].(string) ret1, _ := ret[1].(error) return ret0, ret1 } // CreateSecret indicates an expected call of CreateSecret -func (mr *MockAPIMockRecorder) CreateSecret(arg0, arg1, arg2 interface{}) *gomock.Call { +func (mr *MockAPIMockRecorder) CreateSecret(arg0, arg1 docker.Secret) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateSecret", reflect.TypeOf((*MockAPI)(nil).CreateSecret), arg0, arg1, arg2) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateSecret", reflect.TypeOf((*MockAPI)(nil).CreateSecret), arg0, arg1) } // CreateStack mocks base method diff --git a/ecs/pkg/amazon/sdk.go b/ecs/pkg/amazon/sdk.go index 74cb04484..4bd9eea85 100644 --- a/ecs/pkg/amazon/sdk.go +++ b/ecs/pkg/amazon/sdk.go @@ -223,9 +223,18 @@ func (s sdk) DeleteStack(ctx context.Context, name string) error { return err } -func (s sdk) CreateSecret(ctx context.Context, name string, secret string) (string, error) { - logrus.Debug("Create secret " + name) - response, err := s.SM.CreateSecret(&secretsmanager.CreateSecretInput{Name: &name, SecretString: &secret}) +func (s sdk) CreateSecret(ctx context.Context, secret docker.Secret) (string, error) { + logrus.Debug("Create secret " + secret.Name) + secretStr, err := secret.GetCredString() + if err != nil { + return "", err + } + + response, err := s.SM.CreateSecret(&secretsmanager.CreateSecretInput{ + Name: &secret.Name, + SecretString: &secretStr, + Description: &secret.Description, + }) if err != nil { return "", err } diff --git a/ecs/pkg/amazon/secrets.go b/ecs/pkg/amazon/secrets.go index 649705f02..96a2a476d 100644 --- a/ecs/pkg/amazon/secrets.go +++ b/ecs/pkg/amazon/secrets.go @@ -7,14 +7,14 @@ import ( ) type secretsAPI interface { - CreateSecret(ctx context.Context, name string, content string) (string, error) + CreateSecret(ctx context.Context, secret docker.Secret) (string, error) InspectSecret(ctx context.Context, id string) (docker.Secret, error) ListSecrets(ctx context.Context) ([]docker.Secret, error) DeleteSecret(ctx context.Context, id string, recover bool) error } -func (c client) CreateSecret(ctx context.Context, name string, content string) (string, error) { - return c.api.CreateSecret(ctx, name, content) +func (c client) CreateSecret(ctx context.Context, secret docker.Secret) (string, error) { + return c.api.CreateSecret(ctx, secret) } func (c client) InspectSecret(ctx context.Context, id string) (docker.Secret, error) { diff --git a/ecs/pkg/compose/api.go b/ecs/pkg/compose/api.go index e23651b63..6fd8409a5 100644 --- a/ecs/pkg/compose/api.go +++ b/ecs/pkg/compose/api.go @@ -12,7 +12,7 @@ type API interface { ComposeUp(ctx context.Context, project *Project) error ComposeDown(ctx context.Context, projectName string, deleteCluster bool) error - CreateSecret(ctx context.Context, name string, secret string) (string, error) + CreateSecret(ctx context.Context, secret docker.Secret) (string, error) InspectSecret(ctx context.Context, id string) (docker.Secret, error) ListSecrets(ctx context.Context) ([]docker.Secret, error) DeleteSecret(ctx context.Context, id string, recover bool) error diff --git a/ecs/pkg/docker/secret.go b/ecs/pkg/docker/secret.go index 0efae5d67..613c62638 100644 --- a/ecs/pkg/docker/secret.go +++ b/ecs/pkg/docker/secret.go @@ -9,6 +9,17 @@ type Secret struct { Name string `json:"Name"` Labels map[string]string `json:"Labels"` Description string `json:"Description"` + username string + password string +} + +func NewSecret(name, username, password, description string) Secret { + return Secret{ + Name: name, + username: username, + password: password, + Description: description, + } } func (s Secret) ToJSON() (string, error) { @@ -18,3 +29,15 @@ func (s Secret) ToJSON() (string, error) { } return string(b), nil } + +func (s Secret) GetCredString() (string, error) { + creds := map[string]string{ + "username": s.username, + "password": s.password, + } + b, err := json.Marshal(&creds) + if err != nil { + return "", err + } + return string(b), nil +} From 3a678fd7dc462f6516683a225409e61cef22f9e8 Mon Sep 17 00:00:00 2001 From: aiordache <anca.iordache@docker.com> Date: Wed, 6 May 2020 15:15:46 +0200 Subject: [PATCH 060/198] cleanup Signed-off-by: aiordache <anca.iordache@docker.com> Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com> --- ecs/pkg/amazon/cloudformation.go | 2 +- ecs/pkg/amazon/convert.go | 28 ++++++---------------------- 2 files changed, 7 insertions(+), 23 deletions(-) diff --git a/ecs/pkg/amazon/cloudformation.go b/ecs/pkg/amazon/cloudformation.go index fa7f2f8ac..da041b853 100644 --- a/ecs/pkg/amazon/cloudformation.go +++ b/ecs/pkg/amazon/cloudformation.go @@ -63,7 +63,7 @@ func (c client) Convert(ctx context.Context, project *compose.Project) (*cloudfo if policy != nil { rolePolicies = append(rolePolicies, iam.Role_Policy{ PolicyDocument: policy, - PolicyName: taskExecutionRole, + PolicyName: fmt.Sprintf("%sGrantAccessToSecrets", service.Name), }) } diff --git a/ecs/pkg/amazon/convert.go b/ecs/pkg/amazon/convert.go index 50c722ffd..1ccb0b020 100644 --- a/ecs/pkg/amazon/convert.go +++ b/ecs/pkg/amazon/convert.go @@ -19,14 +19,8 @@ func Convert(project *compose.Project, service types.ServiceConfig) (*ecs.TaskDe if err != nil { return nil, err } - credential, err := getRepoCredentials(service) - if err != nil { - return nil, err - } - secrets, err := getSecrets(service) - if err != nil { - return nil, err - } + credential := getRepoCredentials(service) + return &ecs.TaskDefinition{ ContainerDefinitions: []ecs.TaskDefinition_ContainerDefinition{ // Here we can declare sidecars and init-containers using https://docs.aws.amazon.com/AmazonECS/latest/developerguide/task_definition_parameters.html#container_definition_dependson @@ -64,7 +58,6 @@ func Convert(project *compose.Project, service types.ServiceConfig) (*ecs.TaskDe ReadonlyRootFilesystem: service.ReadOnly, RepositoryCredentials: credential, ResourceRequirements: nil, - Secrets: secrets, StartTimeout: 0, StopTimeout: durationToInt(service.StopGracePeriod), SystemControls: nil, @@ -293,25 +286,16 @@ func getImage(image string) string { } } -func getRepoCredentials(service types.ServiceConfig) (*ecs.TaskDefinition_RepositoryCredentials, error) { +func getRepoCredentials(service types.ServiceConfig) *ecs.TaskDefinition_RepositoryCredentials { // extract registry and namespace string from image name credential := "" for key, value := range service.Extras { - if strings.HasPrefix(key, "x-aws-pull_credentials") { + if key == "x-aws-pull_credentials" { credential = value.(string) } } if credential != "" { - return &ecs.TaskDefinition_RepositoryCredentials{CredentialsParameter: credential}, nil + return &ecs.TaskDefinition_RepositoryCredentials{CredentialsParameter: credential} } - return nil, nil -} - -func getSecrets(service types.ServiceConfig) ([]ecs.TaskDefinition_Secret, error) { - secrets := []ecs.TaskDefinition_Secret{} - - for _, secret := range service.Secrets { - secrets = append(secrets, ecs.TaskDefinition_Secret{Name: secret.Target, ValueFrom: secret.Source}) - } - return secrets, nil + return nil } From 895dc249b455f0ffa3b7524a2d01c9bec2591aaa Mon Sep 17 00:00:00 2001 From: Guillaume Lours <guillaume.lours@docker.com> Date: Fri, 8 May 2020 11:01:52 +0200 Subject: [PATCH 061/198] Manage aws credentials within setup command Signed-off-by: Guillaume Lours <guillaume.lours@docker.com> Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com> --- ecs/cmd/commands/setup.go | 45 +++++++++++++++++++++++++++++++++++++++ ecs/go.mod | 1 + ecs/go.sum | 2 ++ 3 files changed, 48 insertions(+) diff --git a/ecs/cmd/commands/setup.go b/ecs/cmd/commands/setup.go index a927605d1..9beec37ca 100644 --- a/ecs/cmd/commands/setup.go +++ b/ecs/cmd/commands/setup.go @@ -1,14 +1,23 @@ package commands import ( + "fmt" + "os" + + "github.com/aws/aws-sdk-go/aws/awserr" + "github.com/aws/aws-sdk-go/aws/credentials" "github.com/docker/cli/cli-plugins/plugin" contextStore "github.com/docker/ecs-plugin/pkg/docker" "github.com/spf13/cobra" + "gopkg.in/ini.v1" ) func SetupCommand() *cobra.Command { var opts contextStore.AwsContext var name string + var accessKeyID string + var secretAccessKey string + cmd := &cobra.Command{ Use: "setup", Short: "", @@ -18,6 +27,11 @@ func SetupCommand() *cobra.Command { return plugin.PersistentPreRunE(cmd, args) }, RunE: func(cmd *cobra.Command, args []string) error { + if accessKeyID != "" && secretAccessKey != "" { + if err := saveCredentials(opts.Profile, accessKeyID, secretAccessKey); err != nil { + return err + } + } return contextStore.NewContext(name, &opts) }, } @@ -25,9 +39,40 @@ func SetupCommand() *cobra.Command { cmd.Flags().StringVarP(&opts.Profile, "profile", "p", "", "AWS Profile") cmd.Flags().StringVarP(&opts.Cluster, "cluster", "c", "", "ECS cluster") cmd.Flags().StringVarP(&opts.Region, "region", "r", "", "AWS region") + cmd.Flags().StringVarP(&accessKeyID, "aws-key-id", "k", "", "AWS Access Key ID") + cmd.Flags().StringVarP(&secretAccessKey, "aws-secret-key", "s", "", "AWS Secret Access Key") cmd.MarkFlagRequired("profile") cmd.MarkFlagRequired("cluster") cmd.MarkFlagRequired("region") return cmd } + +func saveCredentials(profile string, accessKeyID string, secretAccessKey string) error { + p := credentials.SharedCredentialsProvider{Profile: profile} + _, err := p.Retrieve() + if err == nil { + fmt.Println("credentials already exists!") + return nil + } + if err.(awserr.Error).Code() == "SharedCredsLoad" { + os.Create(p.Filename) + } + + credIni, err := ini.Load(p.Filename) + if err != nil { + return err + } + section := credIni.Section(profile) + section.Key("aws_access_key_id").SetValue(accessKeyID) + section.Key("aws_secret_access_key").SetValue(secretAccessKey) + + credFile, err := os.OpenFile(p.Filename, os.O_WRONLY, 0600) + if err != nil { + return err + } + if _, err = credIni.WriteTo(credFile); err != nil { + return err + } + return credFile.Close() +} diff --git a/ecs/go.mod b/ecs/go.mod index 74a6a89f1..20f071d6f 100644 --- a/ecs/go.mod +++ b/ecs/go.mod @@ -50,6 +50,7 @@ require ( gopkg.in/dancannon/gorethink.v3 v3.0.5 // indirect gopkg.in/fatih/pool.v2 v2.0.0 // indirect gopkg.in/gorethink/gorethink.v3 v3.0.5 // indirect + gopkg.in/ini.v1 v1.55.0 gotest.tools v2.2.0+incompatible gotest.tools/v3 v3.0.2 vbom.ml/util v0.0.0-20180919145318-efcd4e0f9787 // indirect diff --git a/ecs/go.sum b/ecs/go.sum index 5cd52e764..5463d5c0d 100644 --- a/ecs/go.sum +++ b/ecs/go.sum @@ -412,6 +412,8 @@ gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMy gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2/go.mod h1:Xk6kEKp8OKb+X14hQBKWaSkCsqBpgog8nAV2xsGOxlo= gopkg.in/gorethink/gorethink.v3 v3.0.5 h1:e2Uc/Xe+hpcVQFsj6MuHlYog3r0JYpnTzwDj/y2O4MU= gopkg.in/gorethink/gorethink.v3 v3.0.5/go.mod h1:+3yIIHJUGMBK+wyPH+iN5TP+88ikFDfZdqTlK3Y9q8I= +gopkg.in/ini.v1 v1.55.0 h1:E8yzL5unfpW3M6fz/eB7Cb5MQAYSZ7GKo4Qth+N2sgQ= +gopkg.in/ini.v1 v1.55.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= From 51e04a4702651bcf4869623f3da2f612cd2fdf73 Mon Sep 17 00:00:00 2001 From: Guillaume Lours <guillaume.lours@docker.com> Date: Sun, 10 May 2020 23:31:10 +0200 Subject: [PATCH 062/198] Add interactive context setup Signed-off-by: Guillaume Lours <guillaume.lours@docker.com> Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com> --- ecs/cmd/commands/setup.go | 241 ++++++++++++++++-- ecs/go.mod | 1 + ecs/go.sum | 15 ++ ecs/tests/setup_command_test.go | 7 +- .../testdata/setup-required-flags.golden | 13 + 5 files changed, 250 insertions(+), 27 deletions(-) create mode 100644 ecs/tests/testdata/setup-required-flags.golden diff --git a/ecs/cmd/commands/setup.go b/ecs/cmd/commands/setup.go index 9beec37ca..7b93284f3 100644 --- a/ecs/cmd/commands/setup.go +++ b/ecs/cmd/commands/setup.go @@ -3,20 +3,46 @@ package commands import ( "fmt" "os" + "reflect" + "strings" "github.com/aws/aws-sdk-go/aws/awserr" "github.com/aws/aws-sdk-go/aws/credentials" + "github.com/aws/aws-sdk-go/aws/defaults" "github.com/docker/cli/cli-plugins/plugin" contextStore "github.com/docker/ecs-plugin/pkg/docker" + "github.com/manifoldco/promptui" "github.com/spf13/cobra" "gopkg.in/ini.v1" ) +const enterLabelPrefix = "Enter " + +type setupOptions struct { + name string + context contextStore.AwsContext + accessKeyID string + secretAccessKey string +} + +func (s setupOptions) unsetRequiredArgs() []string { + unset := []string{} + if s.context.Profile == "" { + unset = append(unset, "profile") + } + if s.context.Cluster == "" { + unset = append(unset, "cluster") + } + + if s.context.Region == "" { + unset = append(unset, "region") + } + return unset +} + func SetupCommand() *cobra.Command { - var opts contextStore.AwsContext - var name string - var accessKeyID string - var secretAccessKey string + var opts setupOptions + var interactive bool cmd := &cobra.Command{ Use: "setup", @@ -27,27 +53,63 @@ func SetupCommand() *cobra.Command { return plugin.PersistentPreRunE(cmd, args) }, RunE: func(cmd *cobra.Command, args []string) error { - if accessKeyID != "" && secretAccessKey != "" { - if err := saveCredentials(opts.Profile, accessKeyID, secretAccessKey); err != nil { + if interactive { + if err := interactiveCli(&opts); err != nil { + return err + } + } else { + if requiredFlag := opts.unsetRequiredArgs(); len(requiredFlag) > 0 { + fmt.Printf("required flag(s) %q not set", requiredFlag) + cmd.Help() + os.Exit(1) + } + } + if opts.accessKeyID != "" && opts.secretAccessKey != "" { + if err := saveCredentials(opts.context.Profile, opts.accessKeyID, opts.secretAccessKey); err != nil { return err } } - return contextStore.NewContext(name, &opts) + return contextStore.NewContext(opts.name, &opts.context) }, } - cmd.Flags().StringVarP(&name, "name", "n", "aws", "Context Name") - cmd.Flags().StringVarP(&opts.Profile, "profile", "p", "", "AWS Profile") - cmd.Flags().StringVarP(&opts.Cluster, "cluster", "c", "", "ECS cluster") - cmd.Flags().StringVarP(&opts.Region, "region", "r", "", "AWS region") - cmd.Flags().StringVarP(&accessKeyID, "aws-key-id", "k", "", "AWS Access Key ID") - cmd.Flags().StringVarP(&secretAccessKey, "aws-secret-key", "s", "", "AWS Secret Access Key") + cmd.Flags().StringVarP(&opts.name, "name", "n", "aws", "Context Name") + cmd.Flags().StringVarP(&opts.context.Profile, "profile", "p", "", "AWS Profile") + cmd.Flags().StringVarP(&opts.context.Cluster, "cluster", "c", "", "ECS cluster") + cmd.Flags().StringVarP(&opts.context.Region, "region", "r", "", "AWS region") + cmd.Flags().StringVarP(&opts.accessKeyID, "aws-key-id", "k", "", "AWS Access Key ID") + cmd.Flags().StringVarP(&opts.secretAccessKey, "aws-secret-key", "s", "", "AWS Secret Access Key") + cmd.Flags().BoolVarP(&interactive, "interactive", "", false, "Interactively setup Context and Credentials") - cmd.MarkFlagRequired("profile") - cmd.MarkFlagRequired("cluster") - cmd.MarkFlagRequired("region") return cmd } +func interactiveCli(opts *setupOptions) error { + var section ini.Section + + if err := setContextName(opts); err != nil { + return err + } + + section, err := setProfile(opts, section) + if err != nil { + return err + } + + if err := setCluster(opts, err); err != nil { + return err + } + + if err := setRegion(opts, section); err != nil { + return err + } + + if err := setCredentials(opts); err != nil { + return err + } + + return nil +} + func saveCredentials(profile string, accessKeyID string, secretAccessKey string) error { p := credentials.SharedCredentialsProvider{Profile: profile} _, err := p.Retrieve() @@ -55,7 +117,8 @@ func saveCredentials(profile string, accessKeyID string, secretAccessKey string) fmt.Println("credentials already exists!") return nil } - if err.(awserr.Error).Code() == "SharedCredsLoad" { + + if err.(awserr.Error).Code() == "SharedCredsLoad" && err.(awserr.Error).Message() == "failed to load shared credentials file" { os.Create(p.Filename) } @@ -63,16 +126,146 @@ func saveCredentials(profile string, accessKeyID string, secretAccessKey string) if err != nil { return err } - section := credIni.Section(profile) - section.Key("aws_access_key_id").SetValue(accessKeyID) - section.Key("aws_secret_access_key").SetValue(secretAccessKey) - - credFile, err := os.OpenFile(p.Filename, os.O_WRONLY, 0600) + section, err := credIni.NewSection(profile) if err != nil { return err } - if _, err = credIni.WriteTo(credFile); err != nil { + section.NewKey("aws_access_key_id", accessKeyID) + section.NewKey("aws_secret_access_key", secretAccessKey) + return credIni.SaveTo(p.Filename) +} + +func awsProfiles(filename string) (map[string]ini.Section, error) { + profiles := map[string]ini.Section{"new profile": {}} + if filename == "" { + filename = defaults.SharedConfigFilename() + } + credIni, err := ini.Load(filename) + if err != nil { + return nil, err + } + if err != nil { + return nil, err + } + for _, section := range credIni.Sections() { + if strings.HasPrefix(section.Name(), "profile") { + profiles[section.Name()[len("profile "):]] = *section + } + } + return profiles, nil +} + +func setContextName(opts *setupOptions) error { + if opts.name == "aws" { + result, err := promptString(opts.name, "context name", enterLabelPrefix, 2) + if err != nil { + return err + } + opts.name = result + } + return nil +} + +func setProfile(opts *setupOptions, section ini.Section) (ini.Section, error) { + profilesList, err := awsProfiles("") + if err != nil { + return ini.Section{}, err + } + section, ok := profilesList[opts.context.Profile] + if !ok { + prompt := promptui.Select{ + Label: "Select AWS Profile", + Items: reflect.ValueOf(profilesList).MapKeys(), + } + _, result, err := prompt.Run() + if result == "new profile" { + result, err := promptString(opts.context.Profile, "profile name", enterLabelPrefix, 2) + if err != nil { + return ini.Section{}, err + } + opts.context.Profile = result + } else { + section = profilesList[result] + opts.context.Profile = result + } + if err != nil { + return ini.Section{}, err + } + } + return section, nil +} + +func setRegion(opts *setupOptions, section ini.Section) error { + defaultRegion := opts.context.Region + if defaultRegion == "" && section.Name() != "" { + region, err := section.GetKey("region") + if err == nil { + defaultRegion = region.Value() + } + } + result, err := promptString(defaultRegion, "region", enterLabelPrefix, 2) + if err != nil { return err } - return credFile.Close() + opts.context.Region = result + return nil +} + +func setCluster(opts *setupOptions, err error) error { + result, err := promptString(opts.context.Cluster, "cluster name", enterLabelPrefix, 2) + if err != nil { + return err + } + opts.context.Cluster = result + return nil +} + +func setCredentials(opts *setupOptions) error { + prompt := promptui.Prompt{ + Label: "Enter credentials", + IsConfirm: true, + } + _, err := prompt.Run() + if err == nil { + result, err := promptString(opts.accessKeyID, "AWS Access Key ID", enterLabelPrefix, 3) + if err != nil { + return err + } + opts.accessKeyID = result + + prompt = promptui.Prompt{ + Label: "Enter AWS Secret Access Key", + Validate: validateMinLen("AWS Secret Access Key", 3), + Mask: '*', + Default: opts.secretAccessKey, + } + result, err = prompt.Run() + if err != nil { + return err + } + opts.secretAccessKey = result + } + return nil +} + +func promptString(defaultValue string, label string, labelPrefix string, minLength int) (string, error) { + prompt := promptui.Prompt{ + Label: labelPrefix + label, + Validate: validateMinLen(label, minLength), + Default: defaultValue, + } + result, err := prompt.Run() + if err != nil { + return "", err + } + return result, nil +} + +func validateMinLen(label string, minLength int) func(input string) error { + return func(input string) error { + if len(input) < minLength { + return fmt.Errorf("%s must have more than %d characters", label, minLength) + } + return nil + } } diff --git a/ecs/go.mod b/ecs/go.mod index 20f071d6f..7d04a3de6 100644 --- a/ecs/go.mod +++ b/ecs/go.mod @@ -33,6 +33,7 @@ require ( github.com/jinzhu/gorm v1.9.12 // indirect github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0 // indirect github.com/lib/pq v1.3.0 // indirect + github.com/manifoldco/promptui v0.7.0 github.com/mattn/go-sqlite3 v2.0.3+incompatible // indirect github.com/miekg/pkcs11 v1.0.3 // indirect github.com/mitchellh/mapstructure v1.2.2 diff --git a/ecs/go.sum b/ecs/go.sum index 5463d5c0d..2151851e9 100644 --- a/ecs/go.sum +++ b/ecs/go.sum @@ -42,6 +42,10 @@ github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEe github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/certifi/gocertifi v0.0.0-20180118203423-deb3ae2ef261/go.mod h1:GJKEexRPVJrBSOjoqN5VNOIKJ5Q3RViH6eu3puDRwx4= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e h1:fY5BOSpyZCqRo5OhCuC+XN+r/bBCmeuuJtjz+bCNIf8= +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/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cloudflare/backoff v0.0.0-20161212185259-647f3cdfc87a/go.mod h1:rzgs2ZOiguV6/NpiDgADjRLPNyZlApIWxKpkT+X8SdY= github.com/cloudflare/cfssl v1.4.1 h1:vScfU2DrIUI9VPHBVeeAQ0q5A+9yshO1Gz+3QoUQiKw= @@ -161,6 +165,8 @@ github.com/jmhodges/clock v0.0.0-20160418191101-880ee4c33548/go.mod h1:hGT6jSUVz github.com/jmoiron/sqlx v0.0.0-20180124204410-05cef0741ade/go.mod h1:IiEW3SEiiErVyFdH8NTuWjSifiEQKUoyK3LNqr2kCHU= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/juju/ansiterm v0.0.0-20180109212912-720a0952cc2a h1:FaWFmfWdAUKbSCtOU2QjDaorUexogfaMgbipgYATUMU= +github.com/juju/ansiterm v0.0.0-20180109212912-720a0952cc2a/go.mod h1:UJSiEoRfvx3hP73CvoARgeLjaIOjybY9vj8PUPPFGeU= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0 h1:iQTw/8FWTuc7uiaSepXwyf3o52HaUYcV+Tu66S3F5GA= github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0/go.mod h1:1NbS8ALrpOvjt0rHPNLyCIeMtbizbir8U//inJ+zuB8= @@ -182,8 +188,16 @@ github.com/lib/pq v0.0.0-20180201184707-88edab080323/go.mod h1:5WUZQaWbwv1U+lTRe github.com/lib/pq v1.1.1/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.3.0 h1:/qkRGz8zljWiDcFvgpwUpwIAPu3r07TDvs3Rws+o/pU= github.com/lib/pq v1.3.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/lunixbochs/vtclean v0.0.0-20180621232353-2d01aacdc34a h1:weJVJJRzAJBFRlAiJQROKQs8oC9vOxvm4rZmBBk0ONw= +github.com/lunixbochs/vtclean v0.0.0-20180621232353-2d01aacdc34a/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI= github.com/magiconair/properties v1.8.0 h1:LLgXmsheXeRoUOBOjtwPQCWIYqM/LU1ayDtDePerRcY= github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= +github.com/manifoldco/promptui v0.7.0 h1:3l11YT8tm9MnwGFQ4kETwkzpAwY2Jt9lCrumCUW4+z4= +github.com/manifoldco/promptui v0.7.0/go.mod h1:n4zTdgP0vr0S3w7/O/g98U+e0gwLScEXGwov2nIKuGQ= +github.com/mattn/go-colorable v0.0.9 h1:UVL0vNpWh04HeJXV0KLcaT7r06gOH2l4OW6ddYRUIY4= +github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= +github.com/mattn/go-isatty v0.0.4 h1:bnP0vzxcAdeI1zdubAl5PjU6zsERjGZb7raWodagDYs= +github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= github.com/mattn/go-shellwords v1.0.10 h1:Y7Xqm8piKOO3v10Thp7Z36h4FYFjt5xB//6XvOrs2Gw= github.com/mattn/go-shellwords v1.0.10/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y= github.com/mattn/go-sqlite3 v1.10.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= @@ -355,6 +369,7 @@ golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5h 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-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/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-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= diff --git a/ecs/tests/setup_command_test.go b/ecs/tests/setup_command_test.go index 308d3e7ea..104ba2294 100644 --- a/ecs/tests/setup_command_test.go +++ b/ecs/tests/setup_command_test.go @@ -14,10 +14,11 @@ func TestSetupMandatoryArguments(t *testing.T) { defer cleanup() cmd.Command = dockerCli.Command("ecs", "setup") - icmd.RunCmd(cmd).Assert(t, icmd.Expected{ + usage := icmd.RunCmd(cmd).Assert(t, icmd.Expected{ ExitCode: 1, - Err: "required flag(s) \"cluster\", \"profile\", \"region\" not set", - }) + }).Combined() + goldenFile := "setup-required-flags.golden" + golden.Assert(t, usage, goldenFile) } func TestDefaultAwsContextName(t *testing.T) { cmd, cleanup := dockerCli.createTestCmd() diff --git a/ecs/tests/testdata/setup-required-flags.golden b/ecs/tests/testdata/setup-required-flags.golden new file mode 100644 index 000000000..8666ef14f --- /dev/null +++ b/ecs/tests/testdata/setup-required-flags.golden @@ -0,0 +1,13 @@ +required flag(s) ["profile" "cluster" "region"] not set +Usage: docker ecs setup + + + +Options: + -k, --aws-key-id string AWS Access Key ID + -s, --aws-secret-key string AWS Secret Access Key + -c, --cluster string ECS cluster + --interactive Interactively setup Context and Credentials + -n, --name string Context Name (default "aws") + -p, --profile string AWS Profile + -r, --region string AWS region From 0864513bfeb9aa8a2bf2ee8e2172ca939c837b3e Mon Sep 17 00:00:00 2001 From: Guillaume Lours <guillaume.lours@docker.com> Date: Mon, 11 May 2020 16:20:50 +0200 Subject: [PATCH 063/198] Switch automatically to interactive mode if one of the required flag is not set Signed-off-by: Guillaume Lours <guillaume.lours@docker.com> Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com> --- ecs/cmd/commands/setup.go | 10 +--------- ecs/tests/setup_command_test.go | 11 ----------- ecs/tests/testdata/setup-required-flags.golden | 13 ------------- 3 files changed, 1 insertion(+), 33 deletions(-) delete mode 100644 ecs/tests/testdata/setup-required-flags.golden diff --git a/ecs/cmd/commands/setup.go b/ecs/cmd/commands/setup.go index 7b93284f3..a4d55e612 100644 --- a/ecs/cmd/commands/setup.go +++ b/ecs/cmd/commands/setup.go @@ -42,7 +42,6 @@ func (s setupOptions) unsetRequiredArgs() []string { func SetupCommand() *cobra.Command { var opts setupOptions - var interactive bool cmd := &cobra.Command{ Use: "setup", @@ -53,16 +52,10 @@ func SetupCommand() *cobra.Command { return plugin.PersistentPreRunE(cmd, args) }, RunE: func(cmd *cobra.Command, args []string) error { - if interactive { + if requiredFlag := opts.unsetRequiredArgs(); len(requiredFlag) > 0 { if err := interactiveCli(&opts); err != nil { return err } - } else { - if requiredFlag := opts.unsetRequiredArgs(); len(requiredFlag) > 0 { - fmt.Printf("required flag(s) %q not set", requiredFlag) - cmd.Help() - os.Exit(1) - } } if opts.accessKeyID != "" && opts.secretAccessKey != "" { if err := saveCredentials(opts.context.Profile, opts.accessKeyID, opts.secretAccessKey); err != nil { @@ -78,7 +71,6 @@ func SetupCommand() *cobra.Command { cmd.Flags().StringVarP(&opts.context.Region, "region", "r", "", "AWS region") cmd.Flags().StringVarP(&opts.accessKeyID, "aws-key-id", "k", "", "AWS Access Key ID") cmd.Flags().StringVarP(&opts.secretAccessKey, "aws-secret-key", "s", "", "AWS Secret Access Key") - cmd.Flags().BoolVarP(&interactive, "interactive", "", false, "Interactively setup Context and Credentials") return cmd } diff --git a/ecs/tests/setup_command_test.go b/ecs/tests/setup_command_test.go index 104ba2294..a0cc9e46d 100644 --- a/ecs/tests/setup_command_test.go +++ b/ecs/tests/setup_command_test.go @@ -9,17 +9,6 @@ import ( "gotest.tools/v3/icmd" ) -func TestSetupMandatoryArguments(t *testing.T) { - cmd, cleanup := dockerCli.createTestCmd() - defer cleanup() - - cmd.Command = dockerCli.Command("ecs", "setup") - usage := icmd.RunCmd(cmd).Assert(t, icmd.Expected{ - ExitCode: 1, - }).Combined() - goldenFile := "setup-required-flags.golden" - golden.Assert(t, usage, goldenFile) -} func TestDefaultAwsContextName(t *testing.T) { cmd, cleanup := dockerCli.createTestCmd() defer cleanup() diff --git a/ecs/tests/testdata/setup-required-flags.golden b/ecs/tests/testdata/setup-required-flags.golden deleted file mode 100644 index 8666ef14f..000000000 --- a/ecs/tests/testdata/setup-required-flags.golden +++ /dev/null @@ -1,13 +0,0 @@ -required flag(s) ["profile" "cluster" "region"] not set -Usage: docker ecs setup - - - -Options: - -k, --aws-key-id string AWS Access Key ID - -s, --aws-secret-key string AWS Secret Access Key - -c, --cluster string ECS cluster - --interactive Interactively setup Context and Credentials - -n, --name string Context Name (default "aws") - -p, --profile string AWS Profile - -r, --region string AWS region From 4ab37f8229988a349b5090980827345b0169f7ff Mon Sep 17 00:00:00 2001 From: Nicolas De Loof <nicolas.deloof@gmail.com> Date: Tue, 12 May 2020 09:41:29 +0200 Subject: [PATCH 064/198] Implement Hostname-only service discovery using LOCALDOMAIN Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com> --- ecs/README.md | 24 ++++++++++++++++++++++++ ecs/go.mod | 3 +-- ecs/go.sum | 7 +++++++ ecs/pkg/amazon/convert.go | 22 ++++++++++++++-------- 4 files changed, 46 insertions(+), 10 deletions(-) diff --git a/ecs/README.md b/ecs/README.md index f15fd25f5..afa354d3e 100644 --- a/ecs/README.md +++ b/ecs/README.md @@ -39,3 +39,27 @@ file do not include unsupported features. * _Convert_ produces a CloudFormation template to define all resources required to implement the application model on AWS. * _Apply_ phase do apply the CloudFormation template, either by exporting to a stack file or to deploy on AWS. +## Application model + +### Services + +Compose services are mapped to ECS services. Compose specification has no support for multi-container services, nor +does it support sidecars. When an ECS feature requires a sidecar, we introduce custom Compose extension (`x-aws-*`) +to actually expose ECS feature as a service-level feature, not plumbing details. + +### Networking + +We map the "network" abstraction from Compose model to AWS SecurityGroups. The whole application is created within a +single VPC, SecurityGroups are created per networks, including the implicit `default` one. Services are attached +according to the networks declared in Compose model. Doing so, services attached to a common security group can +communicate together, while services from distinct SecurityGroups can't. We just can't set service aliasses per network. + +A CloudMap private namespace is created for application as `{project}.local`. Services get registered so that we +get service discovery and DNS round-robin (equivalent for Compose's `endpoint_mode: dnsrr`). Hostname-only service +discovery is enabled by running application containers with `LOCALDOMAIN={project}.local` +(see [resolv.conf(5)](http://man7.org/linux/man-pages/man5/resolv.conf.5.html)). This works out-of-the-box for +debian-based Docker images. Alpine images have to include a tiny entrypoint script to replicate this feature: +```shell script +if [ $LOCALDOMAIN ]; then echo "search ${LOCALDOMAIN}" >> /etc/resolv.conf; fi +``` + diff --git a/ecs/go.mod b/ecs/go.mod index 7d04a3de6..7eedd06bb 100644 --- a/ecs/go.mod +++ b/ecs/go.mod @@ -5,7 +5,7 @@ require ( 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 - github.com/aws/aws-sdk-go v1.28.9 + github.com/aws/aws-sdk-go v1.30.22 github.com/awslabs/goformation/v4 v4.8.0 github.com/bitly/go-hostpool v0.1.0 // indirect github.com/bitly/go-simplejson v0.5.0 // indirect @@ -24,7 +24,6 @@ require ( github.com/docker/go v1.5.1-1 // indirect github.com/docker/go-metrics v0.0.1 // indirect github.com/docker/libtrust v0.0.0-20160708172513-aabc10ec26b7 // indirect - github.com/go-sql-driver/mysql v1.5.0 // indirect github.com/gofrs/uuid v3.2.0+incompatible // indirect github.com/gogo/protobuf v1.3.1 // indirect github.com/golang/mock v1.4.3 diff --git a/ecs/go.sum b/ecs/go.sum index 2151851e9..1e4fd8fd4 100644 --- a/ecs/go.sum +++ b/ecs/go.sum @@ -21,6 +21,9 @@ github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRF github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= github.com/aws/aws-sdk-go v1.28.9 h1:grIuBQc+p3dTRXerh5+2OxSuWFi0iXuxbFdTSg0jaW0= github.com/aws/aws-sdk-go v1.28.9/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= +github.com/aws/aws-sdk-go v1.30.2/go.mod h1:5zCpMtNQVjRREroY7sYe8lOMRSxkhG6MZveU8YkpAk0= +github.com/aws/aws-sdk-go v1.30.22 h1:wImJ8jQrplgmxaTeUY7FrJFn4te/VtWq+mmmJ1TnWAg= +github.com/aws/aws-sdk-go v1.30.22/go.mod h1:5zCpMtNQVjRREroY7sYe8lOMRSxkhG6MZveU8YkpAk0= github.com/awslabs/goformation/v4 v4.8.0 h1:UiUhyokRy3suEqBXTnipvY8klqY3Eyl4GCH17brraEc= github.com/awslabs/goformation/v4 v4.8.0/go.mod h1:GcJULxCJfloT+3pbqCluXftdEK2AD/UqpS3hkaaBntg= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= @@ -161,6 +164,8 @@ github.com/jinzhu/now v1.0.1 h1:HjfetcXq097iXP0uoPCdnM4Efp5/9MsM0/M+XOTeR3M= github.com/jinzhu/now v1.0.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af h1:pmfjZENx5imkbgOkpRUYLnmbU7UEFbjtDA2hxJ1ichM= github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= +github.com/jmespath/go-jmespath v0.3.0 h1:OS12ieG61fsCg5+qLJ+SsW9NicxNkg3b25OyT2yCeUc= +github.com/jmespath/go-jmespath v0.3.0/go.mod h1:9QtRXoHjLGCJ5IBSaohpXITPlowMeeYCZ7fLUTSywik= github.com/jmhodges/clock v0.0.0-20160418191101-880ee4c33548/go.mod h1:hGT6jSUVzF6no3QaDSMLGLEHtHSBSefs+MgcDWnmhmo= github.com/jmoiron/sqlx v0.0.0-20180124204410-05cef0741ade/go.mod h1:IiEW3SEiiErVyFdH8NTuWjSifiEQKUoyK3LNqr2kCHU= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= @@ -300,6 +305,7 @@ github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0 github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/syndtr/gocapability v0.0.0-20170704070218-db04d3cc01c8/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww= github.com/theupdateframework/notary v0.6.1 h1:7wshjstgS9x9F5LuB1L5mBI2xNMObWqjz+cjWoom6l0= github.com/theupdateframework/notary v0.6.1/go.mod h1:MOfgIfmox8s7/7fduvB2xyPPMJCrjRLRizA8OFwpnKY= @@ -357,6 +363,7 @@ golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09 h1:KaQtG+aDELoNmXYas3TVkGNYR golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190613194153-d28f0bde5980 h1:dfGZHvZk057jK2MCeWus/TowKpJ8y4AmooUzdBSR9GU= golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b h1:0mm1VjtFUOIlE1SbDlwjYaDxZVDP2S5ou6y0gSgXHu8= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= diff --git a/ecs/pkg/amazon/convert.go b/ecs/pkg/amazon/convert.go index 1ccb0b020..13e90690a 100644 --- a/ecs/pkg/amazon/convert.go +++ b/ecs/pkg/amazon/convert.go @@ -1,11 +1,12 @@ package amazon import ( - "errors" + "fmt" "strconv" "strings" "time" + "github.com/aws/aws-sdk-go/aws" ecsapi "github.com/aws/aws-sdk-go/service/ecs" "github.com/awslabs/goformation/v4/cloudformation" "github.com/awslabs/goformation/v4/cloudformation/ecs" @@ -21,9 +22,17 @@ func Convert(project *compose.Project, service types.ServiceConfig) (*ecs.TaskDe } credential := getRepoCredentials(service) + // override resolve.conf search directive to also search <project>.local + // TODO remove once ECS support hostname-only service discovery + service.Environment["LOCALDOMAIN"] = aws.String( + cloudformation.Join("", []string{ + cloudformation.Ref("AWS::Region"), + ".compute.internal", + fmt.Sprintf(" %s.local", project.Name), + })) + return &ecs.TaskDefinition{ ContainerDefinitions: []ecs.TaskDefinition_ContainerDefinition{ - // Here we can declare sidecars and init-containers using https://docs.aws.amazon.com/AmazonECS/latest/developerguide/task_definition_parameters.html#container_definition_dependson { Command: service.Command, DisableNetworking: service.NetworkMode == "none", @@ -50,7 +59,6 @@ func Convert(project *compose.Project, service types.ServiceConfig) (*ecs.TaskDe "awslogs-stream-prefix": service.Name, }, }, - MountPoints: nil, Name: service.Name, PortMappings: toPortMappings(service.Ports), Privileged: service.Privileged, @@ -68,7 +76,7 @@ func Convert(project *compose.Project, service types.ServiceConfig) (*ecs.TaskDe }, }, Cpu: cpu, - Family: project.Name, + Family: fmt.Sprintf("%s-%s", project.Name, service.Name), IpcMode: service.Ipc, Memory: mem, NetworkMode: ecsapi.NetworkModeAwsvpc, // FIXME could be set by service.NetworkMode, Fargate only supports network mode ‘awsvpc’. @@ -77,7 +85,6 @@ func Convert(project *compose.Project, service types.ServiceConfig) (*ecs.TaskDe ProxyConfiguration: nil, RequiresCompatibilities: []string{ecsapi.LaunchTypeFargate}, Tags: nil, - Volumes: []ecs.TaskDefinition_Volume{}, }, nil } @@ -122,7 +129,7 @@ func toLimits(service types.ServiceConfig) (string, string, error) { } } } - return "", "", errors.New("unable to find cpu/mem for the required resources") + return "", "", fmt.Errorf("unable to find cpu/mem for the required resources") } func toRequiresCompatibilities(isolation string) []*string { @@ -198,8 +205,7 @@ func toTmpfs(tmpfs types.StringList) []ecs.TaskDefinition_Tmpfs { for _, path := range tmpfs { o = append(o, ecs.TaskDefinition_Tmpfs{ ContainerPath: path, - MountOptions: nil, - Size: 0, + Size: 100, // size is required on ECS, unlimited by the compose spec }) } return o From 69a7ef076367cd97007a0245f3bbafcec09fe6e8 Mon Sep 17 00:00:00 2001 From: Guillaume Lours <guillaume.lours@docker.com> Date: Mon, 11 May 2020 17:19:51 +0200 Subject: [PATCH 065/198] Make cluster optional in context setup Signed-off-by: Guillaume Lours <guillaume.lours@docker.com> Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com> --- ecs/cmd/commands/setup.go | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/ecs/cmd/commands/setup.go b/ecs/cmd/commands/setup.go index a4d55e612..dd6c40f61 100644 --- a/ecs/cmd/commands/setup.go +++ b/ecs/cmd/commands/setup.go @@ -30,10 +30,6 @@ func (s setupOptions) unsetRequiredArgs() []string { if s.context.Profile == "" { unset = append(unset, "profile") } - if s.context.Cluster == "" { - unset = append(unset, "cluster") - } - if s.context.Region == "" { unset = append(unset, "region") } @@ -204,7 +200,7 @@ func setRegion(opts *setupOptions, section ini.Section) error { } func setCluster(opts *setupOptions, err error) error { - result, err := promptString(opts.context.Cluster, "cluster name", enterLabelPrefix, 2) + result, err := promptString(opts.context.Cluster, "cluster name", enterLabelPrefix, 0) if err != nil { return err } From 1fdac494f30258e0795d1a10531e4f633712b96f Mon Sep 17 00:00:00 2001 From: Nicolas De Loof <nicolas.deloof@gmail.com> Date: Tue, 12 May 2020 15:22:17 +0200 Subject: [PATCH 066/198] Create CloudFormation template with parameters so we don't need AWS API to resolve IDs and can run conversion offline Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com> --- ecs/cmd/commands/compose.go | 2 +- ecs/go.sum | 1 + ecs/pkg/amazon/api.go | 1 - ecs/pkg/amazon/cloudformation.go | 71 ++++++++++++-------------------- ecs/pkg/amazon/iam.go | 8 +++- ecs/pkg/amazon/mock/api.go | 28 +++---------- ecs/pkg/amazon/sdk.go | 12 +++++- ecs/pkg/amazon/up.go | 48 +++++++++++++++++++-- ecs/pkg/compose/api.go | 2 +- 9 files changed, 99 insertions(+), 74 deletions(-) diff --git a/ecs/cmd/commands/compose.go b/ecs/cmd/commands/compose.go index 06fc80a80..18a5bf810 100644 --- a/ecs/cmd/commands/compose.go +++ b/ecs/cmd/commands/compose.go @@ -49,7 +49,7 @@ func ConvertCommand(dockerCli command.Cli, projectOpts *compose.ProjectOptions) if err != nil { return err } - template, err := client.Convert(context.Background(), project) + template, err := client.Convert(project) if err != nil { return err } diff --git a/ecs/go.sum b/ecs/go.sum index 1e4fd8fd4..5ca9a4bfc 100644 --- a/ecs/go.sum +++ b/ecs/go.sum @@ -305,6 +305,7 @@ github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0 github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/syndtr/gocapability v0.0.0-20170704070218-db04d3cc01c8/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww= github.com/theupdateframework/notary v0.6.1 h1:7wshjstgS9x9F5LuB1L5mBI2xNMObWqjz+cjWoom6l0= diff --git a/ecs/pkg/amazon/api.go b/ecs/pkg/amazon/api.go index b4914d68a..493061850 100644 --- a/ecs/pkg/amazon/api.go +++ b/ecs/pkg/amazon/api.go @@ -5,6 +5,5 @@ package amazon type API interface { downAPI upAPI - convertAPI secretsAPI } diff --git a/ecs/pkg/amazon/cloudformation.go b/ecs/pkg/amazon/cloudformation.go index da041b853..075037c08 100644 --- a/ecs/pkg/amazon/cloudformation.go +++ b/ecs/pkg/amazon/cloudformation.go @@ -1,8 +1,6 @@ package amazon import ( - "context" - "errors" "fmt" "strings" @@ -19,20 +17,31 @@ import ( "github.com/docker/ecs-plugin/pkg/compose" ) -func (c client) Convert(ctx context.Context, project *compose.Project) (*cloudformation.Template, error) { +func (c client) Convert(project *compose.Project) (*cloudformation.Template, error) { template := cloudformation.NewTemplate() - vpc, err := c.GetVPC(ctx, project) - if err != nil { - return nil, err + template.Parameters["VPCId"] = cloudformation.Parameter{ + Type: "AWS::EC2::VPC::Id", + Description: "ID of the VPC", } - subnets, err := c.api.GetSubNets(ctx, vpc) - if err != nil { - return nil, err + /* + FIXME can't set subnets: Ref("SubnetIds") see https://github.com/awslabs/goformation/issues/282 + template.Parameters["SubnetIds"] = cloudformation.Parameter{ + Type: "List<AWS::EC2::Subnet::Id>", + Description: "The list of SubnetIds, for at least two Availability Zones in the region in your VPC", + } + */ + template.Parameters["Subnet1Id"] = cloudformation.Parameter{ + Type: "AWS::EC2::Subnet::Id", + Description: "SubnetId,for Availability Zone 1 in the region in your VPC", + } + template.Parameters["Subnet2Id"] = cloudformation.Parameter{ + Type: "AWS::EC2::Subnet::Id", + Description: "SubnetId,for Availability Zone 1 in the region in your VPC", } for net := range project.Networks { - name, resource := convertNetwork(project, net, vpc) + name, resource := convertNetwork(project, net, cloudformation.Ref("VPCId")) template.Resources[name] = resource } @@ -45,7 +54,7 @@ func (c client) Convert(ctx context.Context, project *compose.Project) (*cloudfo template.Resources["CloudMap"] = &cloudmap.PrivateDnsNamespace{ Description: fmt.Sprintf("Service Map for Docker Compose project %s", project.Name), Name: fmt.Sprintf("%s.local", project.Name), - Vpc: vpc, + Vpc: cloudformation.Ref("VPCId"), } for _, service := range project.Services { @@ -55,7 +64,7 @@ func (c client) Convert(ctx context.Context, project *compose.Project) (*cloudfo } taskExecutionRole := fmt.Sprintf("%sTaskExecutionRole", service.Name) - policy, err := c.getPolicy(ctx, definition) + policy, err := c.getPolicy(definition) if err != nil { return nil, err } @@ -115,7 +124,10 @@ func (c client) Convert(ctx context.Context, project *compose.Project) (*cloudfo AwsvpcConfiguration: &ecs.Service_AwsVpcConfiguration{ AssignPublicIp: ecsapi.AssignPublicIpEnabled, SecurityGroups: serviceSecurityGroups, - Subnets: subnets, + Subnets: []string{ + cloudformation.Ref("Subnet1Id"), + cloudformation.Ref("Subnet2Id"), + }, }, }, SchedulingStrategy: ecsapi.SchedulingStrategyReplica, @@ -171,29 +183,7 @@ func networkResourceName(project *compose.Project, network string) string { return fmt.Sprintf("%s%sNetwork", project.Name, strings.Title(network)) } -func (c client) GetVPC(ctx context.Context, project *compose.Project) (string, error) { - //check compose file for the default external network - if net, ok := project.Networks["default"]; ok { - if net.External.External { - vpc := net.Name - ok, err := c.api.VpcExists(ctx, vpc) - if err != nil { - return "", err - } - if !ok { - return "", errors.New("Vpc does not exist: " + vpc) - } - return vpc, nil - } - } - defaultVPC, err := c.api.GetDefaultVPC(ctx) - if err != nil { - return "", err - } - return defaultVPC, nil -} - -func (c client) getPolicy(ctx context.Context, taskDef *ecs.TaskDefinition) (*PolicyDocument, error) { +func (c client) getPolicy(taskDef *ecs.TaskDefinition) (*PolicyDocument, error) { arns := []string{} for _, container := range taskDef.ContainerDefinitions { @@ -212,17 +202,10 @@ func (c client) getPolicy(ctx context.Context, taskDef *ecs.TaskDefinition) (*Po Statement: []PolicyStatement{ { Effect: "Allow", - Action: []string{"secretsmanager:GetSecretValue", "ssm:GetParameters", "kms:Decrypt"}, + Action: []string{ActionGetSecretValue, ActionGetParameters, ActionDecrypt}, Resource: arns, }}, }, nil } return nil, nil } - -type convertAPI interface { - GetDefaultVPC(ctx context.Context) (string, error) - VpcExists(ctx context.Context, vpcID string) (bool, error) - GetSubNets(ctx context.Context, vpcID string) ([]string, error) - GetRoleArn(ctx context.Context, name string) (string, error) -} diff --git a/ecs/pkg/amazon/iam.go b/ecs/pkg/amazon/iam.go index c07e34fec..663577306 100644 --- a/ecs/pkg/amazon/iam.go +++ b/ecs/pkg/amazon/iam.go @@ -1,6 +1,12 @@ package amazon -const ECSTaskExecutionPolicy = "arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy" +const ( + ECSTaskExecutionPolicy = "arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy" + + ActionGetSecretValue = "secretsmanager:GetSecretValue" + ActionGetParameters = "ssm:GetParameters" + ActionDecrypt = "kms:Decrypt" +) var assumeRolePolicyDocument = PolicyDocument{ Version: "2012-10-17", // https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements_version.html diff --git a/ecs/pkg/amazon/mock/api.go b/ecs/pkg/amazon/mock/api.go index 7eba94054..1210a8d35 100644 --- a/ecs/pkg/amazon/mock/api.go +++ b/ecs/pkg/amazon/mock/api.go @@ -6,12 +6,11 @@ package mock import ( context "context" - reflect "reflect" - cloudformation "github.com/aws/aws-sdk-go/service/cloudformation" cloudformation0 "github.com/awslabs/goformation/v4/cloudformation" docker "github.com/docker/ecs-plugin/pkg/docker" gomock "github.com/golang/mock/gomock" + reflect "reflect" ) // MockAPI is a mock of API interface @@ -77,23 +76,23 @@ func (m *MockAPI) CreateSecret(arg0 context.Context, arg1 docker.Secret) (string } // CreateSecret indicates an expected call of CreateSecret -func (mr *MockAPIMockRecorder) CreateSecret(arg0, arg1 docker.Secret) *gomock.Call { +func (mr *MockAPIMockRecorder) CreateSecret(arg0, arg1 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateSecret", reflect.TypeOf((*MockAPI)(nil).CreateSecret), arg0, arg1) } // CreateStack mocks base method -func (m *MockAPI) CreateStack(arg0 context.Context, arg1 string, arg2 *cloudformation0.Template) error { +func (m *MockAPI) CreateStack(arg0 context.Context, arg1 string, arg2 *cloudformation0.Template, arg3 map[string]string) error { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "CreateStack", arg0, arg1, arg2) + ret := m.ctrl.Call(m, "CreateStack", arg0, arg1, arg2, arg3) ret0, _ := ret[0].(error) return ret0 } // CreateStack indicates an expected call of CreateStack -func (mr *MockAPIMockRecorder) CreateStack(arg0, arg1, arg2 interface{}) *gomock.Call { +func (mr *MockAPIMockRecorder) CreateStack(arg0, arg1, arg2, arg3 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateStack", reflect.TypeOf((*MockAPI)(nil).CreateStack), arg0, arg1, arg2) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateStack", reflect.TypeOf((*MockAPI)(nil).CreateStack), arg0, arg1, arg2, arg3) } // DeleteCluster mocks base method @@ -168,21 +167,6 @@ func (mr *MockAPIMockRecorder) GetDefaultVPC(arg0 interface{}) *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetDefaultVPC", reflect.TypeOf((*MockAPI)(nil).GetDefaultVPC), arg0) } -// GetRoleArn mocks base method -func (m *MockAPI) GetRoleArn(arg0 context.Context, arg1 string) (string, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetRoleArn", arg0, arg1) - ret0, _ := ret[0].(string) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// GetRoleArn indicates an expected call of GetRoleArn -func (mr *MockAPIMockRecorder) GetRoleArn(arg0, arg1 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetRoleArn", reflect.TypeOf((*MockAPI)(nil).GetRoleArn), arg0, arg1) -} - // GetStackID mocks base method func (m *MockAPI) GetStackID(arg0 context.Context, arg1 string) (string, error) { m.ctrl.T.Helper() diff --git a/ecs/pkg/amazon/sdk.go b/ecs/pkg/amazon/sdk.go index 4bd9eea85..f0fc076f5 100644 --- a/ecs/pkg/amazon/sdk.go +++ b/ecs/pkg/amazon/sdk.go @@ -153,17 +153,27 @@ func (s sdk) StackExists(ctx context.Context, name string) (bool, error) { return len(stacks.Stacks) > 0, nil } -func (s sdk) CreateStack(ctx context.Context, name string, template *cf.Template) error { +func (s sdk) CreateStack(ctx context.Context, name string, template *cf.Template, parameters map[string]string) error { logrus.Debug("Create CloudFormation stack") json, err := template.JSON() if err != nil { return err } + param := []*cloudformation.Parameter{} + for name, value := range parameters { + param = append(param, &cloudformation.Parameter{ + ParameterKey: aws.String(name), + ParameterValue: aws.String(value), + UsePreviousValue: aws.Bool(true), + }) + } + _, err = s.CF.CreateStackWithContext(ctx, &cloudformation.CreateStackInput{ OnFailure: aws.String("DELETE"), StackName: aws.String(name), TemplateBody: aws.String(string(json)), + Parameters: param, TimeoutInMinutes: aws.Int64(10), Capabilities: []*string{ aws.String(cloudformation.CapabilityCapabilityIam), diff --git a/ecs/pkg/amazon/up.go b/ecs/pkg/amazon/up.go index 21adf9733..e1a0f1fd6 100644 --- a/ecs/pkg/amazon/up.go +++ b/ecs/pkg/amazon/up.go @@ -29,12 +29,28 @@ func (c *client) ComposeUp(ctx context.Context, project *compose.Project) error return err } - template, err := c.Convert(ctx, project) + template, err := c.Convert(project) if err != nil { return err } - err = c.api.CreateStack(ctx, project.Name, template) + vpc, err := c.GetVPC(ctx, project) + if err != nil { + return err + } + + subNets, err := c.api.GetSubNets(ctx, vpc) + if err != nil { + return err + } + + parameters := map[string]string{ + "VPCId": vpc, + "Subnet1Id": subNets[0], + "Subnet2Id": subNets[1], + } + + err = c.api.CreateStack(ctx, project.Name, template, parameters) if err != nil { return err } @@ -42,10 +58,36 @@ func (c *client) ComposeUp(ctx context.Context, project *compose.Project) error return c.WaitStackCompletion(ctx, project.Name, StackCreate) } +func (c client) GetVPC(ctx context.Context, project *compose.Project) (string, error) { + //check compose file for the default external network + if net, ok := project.Networks["default"]; ok { + if net.External.External { + vpc := net.Name + ok, err := c.api.VpcExists(ctx, vpc) + if err != nil { + return "", err + } + if !ok { + return "", fmt.Errorf("VPC does not exist: %s", vpc) + } + return vpc, nil + } + } + defaultVPC, err := c.api.GetDefaultVPC(ctx) + if err != nil { + return "", err + } + return defaultVPC, nil +} + type upAPI interface { waitAPI + GetDefaultVPC(ctx context.Context) (string, error) + VpcExists(ctx context.Context, vpcID string) (bool, error) + GetSubNets(ctx context.Context, vpcID string) ([]string, error) + ClusterExists(ctx context.Context, name string) (bool, error) CreateCluster(ctx context.Context, name string) (string, error) StackExists(ctx context.Context, name string) (bool, error) - CreateStack(ctx context.Context, name string, template *cloudformation.Template) error + CreateStack(ctx context.Context, name string, template *cloudformation.Template, parameters map[string]string) error } diff --git a/ecs/pkg/compose/api.go b/ecs/pkg/compose/api.go index 6fd8409a5..70de9aa58 100644 --- a/ecs/pkg/compose/api.go +++ b/ecs/pkg/compose/api.go @@ -8,7 +8,7 @@ import ( ) type API interface { - Convert(ctx context.Context, project *Project) (*cloudformation.Template, error) + Convert(project *Project) (*cloudformation.Template, error) ComposeUp(ctx context.Context, project *Project) error ComposeDown(ctx context.Context, projectName string, deleteCluster bool) error From 43d3d94c43e21166b4577a8c8b84d0a18ddff2a3 Mon Sep 17 00:00:00 2001 From: Nicolas De Loof <nicolas.deloof@gmail.com> Date: Wed, 13 May 2020 10:34:23 +0200 Subject: [PATCH 067/198] Create cluster by compose up close #53 Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com> --- ecs/pkg/amazon/cloudformation.go | 45 ++++++++++++++++++++++++++------ ecs/pkg/amazon/up.go | 23 +++++++++------- 2 files changed, 50 insertions(+), 18 deletions(-) diff --git a/ecs/pkg/amazon/cloudformation.go b/ecs/pkg/amazon/cloudformation.go index 075037c08..d14b5fafa 100644 --- a/ecs/pkg/amazon/cloudformation.go +++ b/ecs/pkg/amazon/cloudformation.go @@ -17,9 +17,23 @@ import ( "github.com/docker/ecs-plugin/pkg/compose" ) +const ( + ParameterClusterName = "ParameterClusterName" + ParameterVPCId = "ParameterVPCId" + ParameterSubnet1Id = "ParameterSubnet1Id" + ParameterSubnet2Id = "ParameterSubnet2Id" +) + +// Convert a compose project into a CloudFormation template func (c client) Convert(project *compose.Project) (*cloudformation.Template, error) { template := cloudformation.NewTemplate() - template.Parameters["VPCId"] = cloudformation.Parameter{ + + template.Parameters[ParameterClusterName] = cloudformation.Parameter{ + Type: "String", + Description: "Name of the ECS cluster to deploy to (optional)", + } + + template.Parameters[ParameterVPCId] = cloudformation.Parameter{ Type: "AWS::EC2::VPC::Id", Description: "ID of the VPC", } @@ -31,17 +45,32 @@ func (c client) Convert(project *compose.Project) (*cloudformation.Template, err Description: "The list of SubnetIds, for at least two Availability Zones in the region in your VPC", } */ - template.Parameters["Subnet1Id"] = cloudformation.Parameter{ + template.Parameters[ParameterSubnet1Id] = cloudformation.Parameter{ Type: "AWS::EC2::Subnet::Id", Description: "SubnetId,for Availability Zone 1 in the region in your VPC", } - template.Parameters["Subnet2Id"] = cloudformation.Parameter{ + template.Parameters[ParameterSubnet2Id] = cloudformation.Parameter{ Type: "AWS::EC2::Subnet::Id", Description: "SubnetId,for Availability Zone 1 in the region in your VPC", } + // Create Cluster is `ParameterClusterName` parameter is not set + template.Conditions["CreateCluster"] = cloudformation.Equals("", cloudformation.Ref(ParameterClusterName)) + + template.Resources["Cluster"] = &ecs.Cluster{ + ClusterName: project.Name, + Tags: []tags.Tag{ + { + Key: ProjectTag, + Value: project.Name, + }, + }, + AWSCloudFormationCondition: "CreateCluster", + } + cluster := cloudformation.If("CreateCluster", cloudformation.Ref("Cluster"), cloudformation.Ref(ParameterClusterName)) + for net := range project.Networks { - name, resource := convertNetwork(project, net, cloudformation.Ref("VPCId")) + name, resource := convertNetwork(project, net, cloudformation.Ref(ParameterVPCId)) template.Resources[name] = resource } @@ -54,7 +83,7 @@ func (c client) Convert(project *compose.Project) (*cloudformation.Template, err template.Resources["CloudMap"] = &cloudmap.PrivateDnsNamespace{ Description: fmt.Sprintf("Service Map for Docker Compose project %s", project.Name), Name: fmt.Sprintf("%s.local", project.Name), - Vpc: cloudformation.Ref("VPCId"), + Vpc: cloudformation.Ref(ParameterVPCId), } for _, service := range project.Services { @@ -117,7 +146,7 @@ func (c client) Convert(project *compose.Project) (*cloudformation.Template, err } template.Resources[fmt.Sprintf("%sService", service.Name)] = &ecs.Service{ - Cluster: c.Cluster, + Cluster: cluster, DesiredCount: 1, LaunchType: ecsapi.LaunchTypeFargate, NetworkConfiguration: &ecs.Service_NetworkConfiguration{ @@ -125,8 +154,8 @@ func (c client) Convert(project *compose.Project) (*cloudformation.Template, err AssignPublicIp: ecsapi.AssignPublicIpEnabled, SecurityGroups: serviceSecurityGroups, Subnets: []string{ - cloudformation.Ref("Subnet1Id"), - cloudformation.Ref("Subnet2Id"), + cloudformation.Ref(ParameterSubnet1Id), + cloudformation.Ref(ParameterSubnet2Id), }, }, }, diff --git a/ecs/pkg/amazon/up.go b/ecs/pkg/amazon/up.go index e1a0f1fd6..2ad74ba8d 100644 --- a/ecs/pkg/amazon/up.go +++ b/ecs/pkg/amazon/up.go @@ -9,13 +9,16 @@ import ( ) func (c *client) ComposeUp(ctx context.Context, project *compose.Project) error { - ok, err := c.api.ClusterExists(ctx, c.Cluster) - if err != nil { - return err - } - if !ok { - c.api.CreateCluster(ctx, c.Cluster) + if c.Cluster != "" { + ok, err := c.api.ClusterExists(ctx, c.Cluster) + if err != nil { + return err + } + if !ok { + return fmt.Errorf("configured cluster %q does not exist", c.Cluster) + } } + update, err := c.api.StackExists(ctx, project.Name) if err != nil { return err @@ -45,9 +48,10 @@ func (c *client) ComposeUp(ctx context.Context, project *compose.Project) error } parameters := map[string]string{ - "VPCId": vpc, - "Subnet1Id": subNets[0], - "Subnet2Id": subNets[1], + ParameterClusterName: c.Cluster, + ParameterVPCId: vpc, + ParameterSubnet1Id: subNets[0], + ParameterSubnet2Id: subNets[1], } err = c.api.CreateStack(ctx, project.Name, template, parameters) @@ -87,7 +91,6 @@ type upAPI interface { GetSubNets(ctx context.Context, vpcID string) ([]string, error) ClusterExists(ctx context.Context, name string) (bool, error) - CreateCluster(ctx context.Context, name string) (string, error) StackExists(ctx context.Context, name string) (bool, error) CreateStack(ctx context.Context, name string, template *cloudformation.Template, parameters map[string]string) error } From 9dbff1eb7272acd8497b7864123186b278fea4ca Mon Sep 17 00:00:00 2001 From: aiordache <anca.iordache@docker.com> Date: Wed, 13 May 2020 12:14:13 +0200 Subject: [PATCH 068/198] add logs command Signed-off-by: aiordache <anca.iordache@docker.com> Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com> --- ecs/cmd/commands/compose.go | 26 ++++++++++++++++++++++++++ ecs/pkg/amazon/api.go | 1 + ecs/pkg/amazon/logs.go | 13 +++++++++++++ ecs/pkg/amazon/sdk.go | 32 ++++++++++++++++++++++++++++++++ ecs/pkg/compose/api.go | 1 + 5 files changed, 73 insertions(+) create mode 100644 ecs/pkg/amazon/logs.go diff --git a/ecs/cmd/commands/compose.go b/ecs/cmd/commands/compose.go index 18a5bf810..f7a07226d 100644 --- a/ecs/cmd/commands/compose.go +++ b/ecs/cmd/commands/compose.go @@ -22,6 +22,7 @@ func ComposeCommand(dockerCli command.Cli) *cobra.Command { ConvertCommand(dockerCli, opts), UpCommand(dockerCli, opts), DownCommand(dockerCli, opts), + LogsCommand(dockerCli, opts), ) return cmd } @@ -119,3 +120,28 @@ func DownCommand(dockerCli command.Cli, projectOpts *compose.ProjectOptions) *co cmd.Flags().BoolVar(&opts.DeleteCluster, "delete-cluster", false, "Delete cluster") return cmd } + +func LogsCommand(dockerCli command.Cli, projectOpts *compose.ProjectOptions) *cobra.Command { + cmd := &cobra.Command{ + Use: "logs [PROJECT NAME]", + RunE: docker.WithAwsContext(dockerCli, func(clusteropts docker.AwsContext, args []string) error { + client, err := amazon.NewClient(clusteropts.Profile, clusteropts.Cluster, clusteropts.Region) + if err != nil { + return err + } + var name string + + if len(args) == 0 { + project, err := compose.ProjectFromOptions(projectOpts) + if err != nil { + return err + } + name = project.Name + } else { + name = args[0] + } + return client.ComposeLogs(context.Background(), name) + }), + } + return cmd +} diff --git a/ecs/pkg/amazon/api.go b/ecs/pkg/amazon/api.go index 493061850..4fa6ddc44 100644 --- a/ecs/pkg/amazon/api.go +++ b/ecs/pkg/amazon/api.go @@ -5,5 +5,6 @@ package amazon type API interface { downAPI upAPI + logsAPI secretsAPI } diff --git a/ecs/pkg/amazon/logs.go b/ecs/pkg/amazon/logs.go new file mode 100644 index 000000000..b515e25d3 --- /dev/null +++ b/ecs/pkg/amazon/logs.go @@ -0,0 +1,13 @@ +package amazon + +import ( + "context" +) + +func (c *client) ComposeLogs(ctx context.Context, projectName string) error { + return c.api.GetLogs(ctx, projectName) +} + +type logsAPI interface { + GetLogs(ctx context.Context, name string) error +} diff --git a/ecs/pkg/amazon/sdk.go b/ecs/pkg/amazon/sdk.go index f0fc076f5..62736d92b 100644 --- a/ecs/pkg/amazon/sdk.go +++ b/ecs/pkg/amazon/sdk.go @@ -3,6 +3,7 @@ package amazon import ( "context" "fmt" + "time" "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws/session" @@ -307,3 +308,34 @@ func (s sdk) DeleteSecret(ctx context.Context, id string, recover bool) error { _, err := s.SM.DeleteSecret(&secretsmanager.DeleteSecretInput{SecretId: &id, ForceDeleteWithoutRecovery: &force}) return err } + +func (s sdk) GetLogs(ctx context.Context, name string) error { + logGroup := fmt.Sprintf("/docker-compose/%s", name) + var startTime int64 + for { + var hasMore = true + var token *string + token = nil + for hasMore { + events, err := s.CW.FilterLogEvents(&cloudwatchlogs.FilterLogEventsInput{ + LogGroupName: aws.String(logGroup), + NextToken: token, + StartTime: aws.Int64(startTime), + }) + if err != nil { + return err + } + if events.NextToken == nil { + hasMore = false + } else { + token = events.NextToken + } + + for _, event := range events.Events { + fmt.Println(*event.Message) + startTime = *event.IngestionTime + } + } + time.Sleep(500 * time.Millisecond) + } +} diff --git a/ecs/pkg/compose/api.go b/ecs/pkg/compose/api.go index 70de9aa58..b39afe02d 100644 --- a/ecs/pkg/compose/api.go +++ b/ecs/pkg/compose/api.go @@ -11,6 +11,7 @@ type API interface { Convert(project *Project) (*cloudformation.Template, error) ComposeUp(ctx context.Context, project *Project) error ComposeDown(ctx context.Context, projectName string, deleteCluster bool) error + ComposeLogs(ctx context.Context, projectName string) error CreateSecret(ctx context.Context, secret docker.Secret) (string, error) InspectSecret(ctx context.Context, id string) (docker.Secret, error) From 0492dacfee9f2c47efd984707fa7c68da2afee9c Mon Sep 17 00:00:00 2001 From: aiordache <anca.iordache@docker.com> Date: Wed, 13 May 2020 12:28:23 +0200 Subject: [PATCH 069/198] remove redundant var init Signed-off-by: aiordache <anca.iordache@docker.com> Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com> --- ecs/pkg/amazon/sdk.go | 1 - 1 file changed, 1 deletion(-) diff --git a/ecs/pkg/amazon/sdk.go b/ecs/pkg/amazon/sdk.go index 62736d92b..b29751309 100644 --- a/ecs/pkg/amazon/sdk.go +++ b/ecs/pkg/amazon/sdk.go @@ -315,7 +315,6 @@ func (s sdk) GetLogs(ctx context.Context, name string) error { for { var hasMore = true var token *string - token = nil for hasMore { events, err := s.CW.FilterLogEvents(&cloudwatchlogs.FilterLogEventsInput{ LogGroupName: aws.String(logGroup), From 3e5b118f26359e1be6e7316c04de61da10c29323 Mon Sep 17 00:00:00 2001 From: aiordache <anca.iordache@docker.com> Date: Wed, 13 May 2020 14:32:17 +0200 Subject: [PATCH 070/198] add GetLogs to MockAPI Signed-off-by: aiordache <anca.iordache@docker.com> Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com> --- ecs/pkg/amazon/mock/api.go | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/ecs/pkg/amazon/mock/api.go b/ecs/pkg/amazon/mock/api.go index 1210a8d35..c592162b8 100644 --- a/ecs/pkg/amazon/mock/api.go +++ b/ecs/pkg/amazon/mock/api.go @@ -6,11 +6,12 @@ package mock import ( context "context" + reflect "reflect" + cloudformation "github.com/aws/aws-sdk-go/service/cloudformation" cloudformation0 "github.com/awslabs/goformation/v4/cloudformation" docker "github.com/docker/ecs-plugin/pkg/docker" gomock "github.com/golang/mock/gomock" - reflect "reflect" ) // MockAPI is a mock of API interface @@ -270,3 +271,17 @@ func (mr *MockAPIMockRecorder) WaitStackComplete(arg0, arg1, arg2 interface{}) * mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "WaitStackComplete", reflect.TypeOf((*MockAPI)(nil).WaitStackComplete), arg0, arg1, arg2) } + +// GetLogs mocks base method +func (m *MockAPI) GetLogs(arg0 context.Context, arg1 string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetLogs", arg0, arg1) + ret0, _ := ret[0].(error) + return ret0 +} + +// GetLogs mocks base method +func (mr *MockAPIMockRecorder) GetLogs(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetLogs", reflect.TypeOf((*MockAPI)(nil).GetLogs), arg0, arg1) +} From 4bbe3f15896231f65d2b7e1b77b0aeaf998fbee1 Mon Sep 17 00:00:00 2001 From: Guillaume Lours <guillaume.lours@docker.com> Date: Wed, 13 May 2020 15:32:24 +0200 Subject: [PATCH 071/198] Add first compose to cloudformation conversion tests Signed-off-by: Guillaume Lours <guillaume.lours@docker.com> Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com> --- ecs/tests/compose_command_test.go | 49 ++++++ .../simple-single-service-with-overrides.yaml | 4 + .../testdata/input/simple-single-service.yaml | 4 + .../simple-cloudformation-conversion.golden | 164 ++++++++++++++++++ ...formation-with-overrides-conversion.golden | 164 ++++++++++++++++++ 5 files changed, 385 insertions(+) create mode 100644 ecs/tests/compose_command_test.go create mode 100644 ecs/tests/testdata/input/simple-single-service-with-overrides.yaml create mode 100644 ecs/tests/testdata/input/simple-single-service.yaml create mode 100644 ecs/tests/testdata/simple/simple-cloudformation-conversion.golden create mode 100644 ecs/tests/testdata/simple/simple-cloudformation-with-overrides-conversion.golden diff --git a/ecs/tests/compose_command_test.go b/ecs/tests/compose_command_test.go new file mode 100644 index 000000000..0bdb94bc3 --- /dev/null +++ b/ecs/tests/compose_command_test.go @@ -0,0 +1,49 @@ +package tests + +import ( + "testing" + + "gotest.tools/v3/fs" + "gotest.tools/v3/golden" + "gotest.tools/v3/icmd" +) + +const ( + composeFileName = "compose.yaml" +) + +func TestSimpleConvert(t *testing.T) { + cmd, cleanup := dockerCli.createTestCmd() + defer cleanup() + + composeYAML := golden.Get(t, "input/simple-single-service.yaml") + tmpDir := fs.NewDir(t, t.Name(), + fs.WithFile(composeFileName, "", fs.WithBytes(composeYAML)), + ) + defer tmpDir.Remove() + + cmd.Command = dockerCli.Command("ecs", "compose", "--file="+tmpDir.Join(composeFileName), "--project-name", t.Name(), "convert") + result := icmd.RunCmd(cmd).Assert(t, icmd.Success).Combined() + + expected := "simple/simple-cloudformation-conversion.golden" + golden.Assert(t, result, expected) +} + +func TestSimpleWithOverrides(t *testing.T) { + cmd, cleanup := dockerCli.createTestCmd() + defer cleanup() + + composeYAML := golden.Get(t, "input/simple-single-service.yaml") + overriddenComposeYAML := golden.Get(t, "input/simple-single-service-with-overrides.yaml") + tmpDir := fs.NewDir(t, t.Name(), + fs.WithFile(composeFileName, "", fs.WithBytes(composeYAML)), + fs.WithFile("overriddenService.yaml", "", fs.WithBytes(overriddenComposeYAML)), + ) + defer tmpDir.Remove() + cmd.Command = dockerCli.Command("ecs", "compose", "--file="+tmpDir.Join(composeFileName), "--file", + tmpDir.Join("overriddenService.yaml"), "--project-name", t.Name(), "convert") + result := icmd.RunCmd(cmd).Assert(t, icmd.Success).Combined() + + expected := "simple/simple-cloudformation-with-overrides-conversion.golden" + golden.Assert(t, result, expected) +} diff --git a/ecs/tests/testdata/input/simple-single-service-with-overrides.yaml b/ecs/tests/testdata/input/simple-single-service-with-overrides.yaml new file mode 100644 index 000000000..3dc8a0b6f --- /dev/null +++ b/ecs/tests/testdata/input/simple-single-service-with-overrides.yaml @@ -0,0 +1,4 @@ +version: "3" +services: + simple: + image: haproxy diff --git a/ecs/tests/testdata/input/simple-single-service.yaml b/ecs/tests/testdata/input/simple-single-service.yaml new file mode 100644 index 000000000..4b3f9af21 --- /dev/null +++ b/ecs/tests/testdata/input/simple-single-service.yaml @@ -0,0 +1,4 @@ +version: "3" +services: + simple: + image: nginx \ No newline at end of file diff --git a/ecs/tests/testdata/simple/simple-cloudformation-conversion.golden b/ecs/tests/testdata/simple/simple-cloudformation-conversion.golden new file mode 100644 index 000000000..51a528b3a --- /dev/null +++ b/ecs/tests/testdata/simple/simple-cloudformation-conversion.golden @@ -0,0 +1,164 @@ +{ + "AWSTemplateFormatVersion": "2010-09-09", + "Parameters": { + "Subnet1Id": { + "Description": "SubnetId,for Availability Zone 1 in the region in your VPC", + "Type": "AWS::EC2::Subnet::Id" + }, + "Subnet2Id": { + "Description": "SubnetId,for Availability Zone 1 in the region in your VPC", + "Type": "AWS::EC2::Subnet::Id" + }, + "VPCId": { + "Description": "ID of the VPC", + "Type": "AWS::EC2::VPC::Id" + } + }, + "Resources": { + "CloudMap": { + "Properties": { + "Description": "Service Map for Docker Compose project TestSimpleConvert", + "Name": "TestSimpleConvert.local", + "Vpc": { + "Ref": "VPCId" + } + }, + "Type": "AWS::ServiceDiscovery::PrivateDnsNamespace" + }, + "LogGroup": { + "Properties": { + "LogGroupName": "/docker-compose/TestSimpleConvert" + }, + "Type": "AWS::Logs::LogGroup" + }, + "simpleService": { + "Properties": { + "Cluster": "TestCluster", + "DesiredCount": 1, + "LaunchType": "FARGATE", + "NetworkConfiguration": { + "AwsvpcConfiguration": { + "AssignPublicIp": "ENABLED", + "Subnets": [ + { + "Ref": "Subnet1Id" + }, + { + "Ref": "Subnet2Id" + } + ] + } + }, + "SchedulingStrategy": "REPLICA", + "ServiceName": "simple", + "ServiceRegistries": [ + { + "RegistryArn": { + "Fn::GetAtt": [ + "simpleServiceDiscoveryEntry", + "Arn" + ] + } + } + ], + "TaskDefinition": { + "Ref": "simpleTaskDefinition" + } + }, + "Type": "AWS::ECS::Service" + }, + "simpleServiceDiscoveryEntry": { + "Properties": { + "Description": "\"simple\" service discovery entry in Cloud Map", + "DnsConfig": { + "DnsRecords": [ + { + "TTL": 300, + "Type": "A" + } + ], + "RoutingPolicy": "MULTIVALUE" + }, + "Name": "simple", + "NamespaceId": { + "Ref": "CloudMap" + } + }, + "Type": "AWS::ServiceDiscovery::Service" + }, + "simpleTaskDefinition": { + "Properties": { + "ContainerDefinitions": [ + { + "Environment": [ + { + "Name": "LOCALDOMAIN", + "Value": { + "Fn::Join": [ + "", + [ + { + "Ref": "AWS::Region" + }, + ".compute.internal", + " TestSimpleConvert.local" + ] + ] + } + } + ], + "Essential": true, + "Image": "docker.io/library/nginx", + "LinuxParameters": {}, + "LogConfiguration": { + "LogDriver": "awslogs", + "Options": { + "awslogs-group": { + "Ref": "LogGroup" + }, + "awslogs-region": { + "Ref": "AWS::Region" + }, + "awslogs-stream-prefix": "simple" + } + }, + "Name": "simple" + } + ], + "Cpu": "256", + "ExecutionRoleArn": { + "Ref": "simpleTaskExecutionRole" + }, + "Family": "TestSimpleConvert-simple", + "Memory": "512", + "NetworkMode": "awsvpc", + "RequiresCompatibilities": [ + "FARGATE" + ] + }, + "Type": "AWS::ECS::TaskDefinition" + }, + "simpleTaskExecutionRole": { + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": [ + "sts:AssumeRole" + ], + "Effect": "Allow", + "Principal": { + "Service": "ecs-tasks.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "ManagedPolicyArns": [ + "arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy" + ] + }, + "Type": "AWS::IAM::Role" + } + } +} diff --git a/ecs/tests/testdata/simple/simple-cloudformation-with-overrides-conversion.golden b/ecs/tests/testdata/simple/simple-cloudformation-with-overrides-conversion.golden new file mode 100644 index 000000000..2e8f5a4ad --- /dev/null +++ b/ecs/tests/testdata/simple/simple-cloudformation-with-overrides-conversion.golden @@ -0,0 +1,164 @@ +{ + "AWSTemplateFormatVersion": "2010-09-09", + "Parameters": { + "Subnet1Id": { + "Description": "SubnetId,for Availability Zone 1 in the region in your VPC", + "Type": "AWS::EC2::Subnet::Id" + }, + "Subnet2Id": { + "Description": "SubnetId,for Availability Zone 1 in the region in your VPC", + "Type": "AWS::EC2::Subnet::Id" + }, + "VPCId": { + "Description": "ID of the VPC", + "Type": "AWS::EC2::VPC::Id" + } + }, + "Resources": { + "CloudMap": { + "Properties": { + "Description": "Service Map for Docker Compose project TestSimpleWithOverrides", + "Name": "TestSimpleWithOverrides.local", + "Vpc": { + "Ref": "VPCId" + } + }, + "Type": "AWS::ServiceDiscovery::PrivateDnsNamespace" + }, + "LogGroup": { + "Properties": { + "LogGroupName": "/docker-compose/TestSimpleWithOverrides" + }, + "Type": "AWS::Logs::LogGroup" + }, + "simpleService": { + "Properties": { + "Cluster": "TestCluster", + "DesiredCount": 1, + "LaunchType": "FARGATE", + "NetworkConfiguration": { + "AwsvpcConfiguration": { + "AssignPublicIp": "ENABLED", + "Subnets": [ + { + "Ref": "Subnet1Id" + }, + { + "Ref": "Subnet2Id" + } + ] + } + }, + "SchedulingStrategy": "REPLICA", + "ServiceName": "simple", + "ServiceRegistries": [ + { + "RegistryArn": { + "Fn::GetAtt": [ + "simpleServiceDiscoveryEntry", + "Arn" + ] + } + } + ], + "TaskDefinition": { + "Ref": "simpleTaskDefinition" + } + }, + "Type": "AWS::ECS::Service" + }, + "simpleServiceDiscoveryEntry": { + "Properties": { + "Description": "\"simple\" service discovery entry in Cloud Map", + "DnsConfig": { + "DnsRecords": [ + { + "TTL": 300, + "Type": "A" + } + ], + "RoutingPolicy": "MULTIVALUE" + }, + "Name": "simple", + "NamespaceId": { + "Ref": "CloudMap" + } + }, + "Type": "AWS::ServiceDiscovery::Service" + }, + "simpleTaskDefinition": { + "Properties": { + "ContainerDefinitions": [ + { + "Environment": [ + { + "Name": "LOCALDOMAIN", + "Value": { + "Fn::Join": [ + "", + [ + { + "Ref": "AWS::Region" + }, + ".compute.internal", + " TestSimpleWithOverrides.local" + ] + ] + } + } + ], + "Essential": true, + "Image": "docker.io/library/haproxy", + "LinuxParameters": {}, + "LogConfiguration": { + "LogDriver": "awslogs", + "Options": { + "awslogs-group": { + "Ref": "LogGroup" + }, + "awslogs-region": { + "Ref": "AWS::Region" + }, + "awslogs-stream-prefix": "simple" + } + }, + "Name": "simple" + } + ], + "Cpu": "256", + "ExecutionRoleArn": { + "Ref": "simpleTaskExecutionRole" + }, + "Family": "TestSimpleWithOverrides-simple", + "Memory": "512", + "NetworkMode": "awsvpc", + "RequiresCompatibilities": [ + "FARGATE" + ] + }, + "Type": "AWS::ECS::TaskDefinition" + }, + "simpleTaskExecutionRole": { + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": [ + "sts:AssumeRole" + ], + "Effect": "Allow", + "Principal": { + "Service": "ecs-tasks.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "ManagedPolicyArns": [ + "arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy" + ] + }, + "Type": "AWS::IAM::Role" + } + } +} From 07a57469dbf9fa8b929554203782d0b7ac889b4f Mon Sep 17 00:00:00 2001 From: Guillaume Lours <guillaume.lours@docker.com> Date: Wed, 13 May 2020 18:02:52 +0200 Subject: [PATCH 072/198] Add unit tests version of migration tests instead of e2e one Signed-off-by: Guillaume Lours <guillaume.lours@docker.com> Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com> --- ecs/go.sum | 1 + ecs/pkg/amazon/cloudformation_test.go | 49 +++++++++++++++++++ .../simple-single-service-with-overrides.yaml | 0 .../testdata/input/simple-single-service.yaml | 0 .../simple-cloudformation-conversion.golden | 0 ...formation-with-overrides-conversion.golden | 0 ecs/pkg/compose/opts.go | 4 +- ecs/pkg/compose/project.go | 2 +- ecs/pkg/compose/project_test.go | 8 +-- ecs/tests/compose_command_test.go | 49 ------------------- 10 files changed, 57 insertions(+), 56 deletions(-) create mode 100644 ecs/pkg/amazon/cloudformation_test.go rename ecs/{tests => pkg/amazon}/testdata/input/simple-single-service-with-overrides.yaml (100%) rename ecs/{tests => pkg/amazon}/testdata/input/simple-single-service.yaml (100%) rename ecs/{tests => pkg/amazon}/testdata/simple/simple-cloudformation-conversion.golden (100%) rename ecs/{tests => pkg/amazon}/testdata/simple/simple-cloudformation-with-overrides-conversion.golden (100%) delete mode 100644 ecs/tests/compose_command_test.go diff --git a/ecs/go.sum b/ecs/go.sum index 5ca9a4bfc..9088eaf22 100644 --- a/ecs/go.sum +++ b/ecs/go.sum @@ -213,6 +213,7 @@ github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0j github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/miekg/pkcs11 v1.0.3 h1:iMwmD7I5225wv84WxIG/bmxz9AXjWvTWIbM/TYHvWtw= github.com/miekg/pkcs11 v1.0.3/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs= +github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= 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= diff --git a/ecs/pkg/amazon/cloudformation_test.go b/ecs/pkg/amazon/cloudformation_test.go new file mode 100644 index 000000000..e45073385 --- /dev/null +++ b/ecs/pkg/amazon/cloudformation_test.go @@ -0,0 +1,49 @@ +package amazon + +import ( + "fmt" + "testing" + + "github.com/docker/ecs-plugin/pkg/compose" + "gotest.tools/v3/golden" +) + +func TestSimpleConvert(t *testing.T) { + options := compose.ProjectOptions{ + Name: t.Name(), + ConfigPaths: []string{"testdata/input/simple-single-service.yaml"}, + } + result := convertResultAsString(t, options, "TestCluster") + expected := "simple/simple-cloudformation-conversion.golden" + golden.Assert(t, result, expected) +} + +func TestSimpleWithOverrides(t *testing.T) { + options := compose.ProjectOptions{ + Name: t.Name(), + ConfigPaths: []string{"testdata/input/simple-single-service.yaml", "testdata/input/simple-single-service-with-overrides.yaml"}, + } + result := convertResultAsString(t, options, "TestCluster") + expected := "simple/simple-cloudformation-with-overrides-conversion.golden" + golden.Assert(t, result, expected) +} + +func convertResultAsString(t *testing.T, options compose.ProjectOptions, clusterName string) string { + project, err := compose.ProjectFromOptions(&options) + if err != nil { + t.Error(err) + } + client, err := NewClient("", clusterName, "") + if err != nil { + t.Error(err) + } + result, err := client.Convert(project) + if err != nil { + t.Error(err) + } + resultAsJSON, err := result.JSON() + if err != nil { + t.Error(err) + } + return fmt.Sprintf("%s\n", string(resultAsJSON)) +} diff --git a/ecs/tests/testdata/input/simple-single-service-with-overrides.yaml b/ecs/pkg/amazon/testdata/input/simple-single-service-with-overrides.yaml similarity index 100% rename from ecs/tests/testdata/input/simple-single-service-with-overrides.yaml rename to ecs/pkg/amazon/testdata/input/simple-single-service-with-overrides.yaml diff --git a/ecs/tests/testdata/input/simple-single-service.yaml b/ecs/pkg/amazon/testdata/input/simple-single-service.yaml similarity index 100% rename from ecs/tests/testdata/input/simple-single-service.yaml rename to ecs/pkg/amazon/testdata/input/simple-single-service.yaml diff --git a/ecs/tests/testdata/simple/simple-cloudformation-conversion.golden b/ecs/pkg/amazon/testdata/simple/simple-cloudformation-conversion.golden similarity index 100% rename from ecs/tests/testdata/simple/simple-cloudformation-conversion.golden rename to ecs/pkg/amazon/testdata/simple/simple-cloudformation-conversion.golden diff --git a/ecs/tests/testdata/simple/simple-cloudformation-with-overrides-conversion.golden b/ecs/pkg/amazon/testdata/simple/simple-cloudformation-with-overrides-conversion.golden similarity index 100% rename from ecs/tests/testdata/simple/simple-cloudformation-with-overrides-conversion.golden rename to ecs/pkg/amazon/testdata/simple/simple-cloudformation-with-overrides-conversion.golden diff --git a/ecs/pkg/compose/opts.go b/ecs/pkg/compose/opts.go index 9e9bdeee0..d2fef0c9d 100644 --- a/ecs/pkg/compose/opts.go +++ b/ecs/pkg/compose/opts.go @@ -7,12 +7,12 @@ import ( type ProjectOptions struct { ConfigPaths []string - name string + Name string } func (o *ProjectOptions) AddFlags(flags *pflag.FlagSet) { flags.StringArrayVarP(&o.ConfigPaths, "file", "f", nil, "Specify an alternate compose file") - flags.StringVarP(&o.name, "project-name", "n", "", "Specify an alternate project name (default: directory name)") + flags.StringVarP(&o.Name, "project-name", "n", "", "Specify an alternate project name (default: directory name)") } type ProjectFunc func(project *Project, args []string) error diff --git a/ecs/pkg/compose/project.go b/ecs/pkg/compose/project.go index e17e22527..5c244b2c4 100644 --- a/ecs/pkg/compose/project.go +++ b/ecs/pkg/compose/project.go @@ -40,7 +40,7 @@ func ProjectFromOptions(options *ProjectOptions) (*Project, error) { return nil, err } - name := options.name + name := options.Name if name == "" { name = os.Getenv("COMPOSE_PROJECT_NAME") } diff --git a/ecs/pkg/compose/project_test.go b/ecs/pkg/compose/project_test.go index 2906c9d21..733f34f03 100644 --- a/ecs/pkg/compose/project_test.go +++ b/ecs/pkg/compose/project_test.go @@ -9,14 +9,14 @@ import ( func Test_project_name(t *testing.T) { p, err := ProjectFromOptions(&ProjectOptions{ - name: "my_project", + Name: "my_project", ConfigPaths: []string{"testdata/simple/compose.yaml"}, }) assert.NilError(t, err) assert.Equal(t, p.Name, "my_project") p, err = ProjectFromOptions(&ProjectOptions{ - name: "", + Name: "", ConfigPaths: []string{"testdata/simple/compose.yaml"}, }) assert.NilError(t, err) @@ -24,7 +24,7 @@ func Test_project_name(t *testing.T) { os.Setenv("COMPOSE_PROJECT_NAME", "my_project_from_env") p, err = ProjectFromOptions(&ProjectOptions{ - name: "", + Name: "", ConfigPaths: []string{"testdata/simple/compose.yaml"}, }) assert.NilError(t, err) @@ -33,7 +33,7 @@ func Test_project_name(t *testing.T) { func Test_project_from_set_of_files(t *testing.T) { p, err := ProjectFromOptions(&ProjectOptions{ - name: "my_project", + Name: "my_project", ConfigPaths: []string{ "testdata/simple/compose.yaml", "testdata/simple/compose-with-overrides.yaml", diff --git a/ecs/tests/compose_command_test.go b/ecs/tests/compose_command_test.go deleted file mode 100644 index 0bdb94bc3..000000000 --- a/ecs/tests/compose_command_test.go +++ /dev/null @@ -1,49 +0,0 @@ -package tests - -import ( - "testing" - - "gotest.tools/v3/fs" - "gotest.tools/v3/golden" - "gotest.tools/v3/icmd" -) - -const ( - composeFileName = "compose.yaml" -) - -func TestSimpleConvert(t *testing.T) { - cmd, cleanup := dockerCli.createTestCmd() - defer cleanup() - - composeYAML := golden.Get(t, "input/simple-single-service.yaml") - tmpDir := fs.NewDir(t, t.Name(), - fs.WithFile(composeFileName, "", fs.WithBytes(composeYAML)), - ) - defer tmpDir.Remove() - - cmd.Command = dockerCli.Command("ecs", "compose", "--file="+tmpDir.Join(composeFileName), "--project-name", t.Name(), "convert") - result := icmd.RunCmd(cmd).Assert(t, icmd.Success).Combined() - - expected := "simple/simple-cloudformation-conversion.golden" - golden.Assert(t, result, expected) -} - -func TestSimpleWithOverrides(t *testing.T) { - cmd, cleanup := dockerCli.createTestCmd() - defer cleanup() - - composeYAML := golden.Get(t, "input/simple-single-service.yaml") - overriddenComposeYAML := golden.Get(t, "input/simple-single-service-with-overrides.yaml") - tmpDir := fs.NewDir(t, t.Name(), - fs.WithFile(composeFileName, "", fs.WithBytes(composeYAML)), - fs.WithFile("overriddenService.yaml", "", fs.WithBytes(overriddenComposeYAML)), - ) - defer tmpDir.Remove() - cmd.Command = dockerCli.Command("ecs", "compose", "--file="+tmpDir.Join(composeFileName), "--file", - tmpDir.Join("overriddenService.yaml"), "--project-name", t.Name(), "convert") - result := icmd.RunCmd(cmd).Assert(t, icmd.Success).Combined() - - expected := "simple/simple-cloudformation-with-overrides-conversion.golden" - golden.Assert(t, result, expected) -} From 8cd4a6fe9b257f5658e92b1dd2fc040b75b46d9c Mon Sep 17 00:00:00 2001 From: Nicolas De Loof <nicolas.deloof@gmail.com> Date: Wed, 13 May 2020 22:09:20 +0200 Subject: [PATCH 073/198] Fix golden files after rebase Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com> --- .../simple-cloudformation-conversion.golden | 51 ++++++++++++++++--- ...formation-with-overrides-conversion.golden | 51 ++++++++++++++++--- 2 files changed, 88 insertions(+), 14 deletions(-) diff --git a/ecs/pkg/amazon/testdata/simple/simple-cloudformation-conversion.golden b/ecs/pkg/amazon/testdata/simple/simple-cloudformation-conversion.golden index 51a528b3a..d0e632d9c 100644 --- a/ecs/pkg/amazon/testdata/simple/simple-cloudformation-conversion.golden +++ b/ecs/pkg/amazon/testdata/simple/simple-cloudformation-conversion.golden @@ -1,15 +1,29 @@ { "AWSTemplateFormatVersion": "2010-09-09", + "Conditions": { + "CreateCluster": { + "Fn::Equals": [ + "", + { + "Ref": "ParameterClusterName" + } + ] + } + }, "Parameters": { - "Subnet1Id": { + "ParameterClusterName": { + "Description": "Name of the ECS cluster to deploy to (optional)", + "Type": "String" + }, + "ParameterSubnet1Id": { "Description": "SubnetId,for Availability Zone 1 in the region in your VPC", "Type": "AWS::EC2::Subnet::Id" }, - "Subnet2Id": { + "ParameterSubnet2Id": { "Description": "SubnetId,for Availability Zone 1 in the region in your VPC", "Type": "AWS::EC2::Subnet::Id" }, - "VPCId": { + "ParameterVPCId": { "Description": "ID of the VPC", "Type": "AWS::EC2::VPC::Id" } @@ -20,11 +34,24 @@ "Description": "Service Map for Docker Compose project TestSimpleConvert", "Name": "TestSimpleConvert.local", "Vpc": { - "Ref": "VPCId" + "Ref": "ParameterVPCId" } }, "Type": "AWS::ServiceDiscovery::PrivateDnsNamespace" }, + "Cluster": { + "Condition": "CreateCluster", + "Properties": { + "ClusterName": "TestSimpleConvert", + "Tags": [ + { + "Key": "com.docker.compose.project", + "Value": "TestSimpleConvert" + } + ] + }, + "Type": "AWS::ECS::Cluster" + }, "LogGroup": { "Properties": { "LogGroupName": "/docker-compose/TestSimpleConvert" @@ -33,7 +60,17 @@ }, "simpleService": { "Properties": { - "Cluster": "TestCluster", + "Cluster": { + "Fn::If": [ + "CreateCluster", + { + "Ref": "Cluster" + }, + { + "Ref": "ParameterClusterName" + } + ] + }, "DesiredCount": 1, "LaunchType": "FARGATE", "NetworkConfiguration": { @@ -41,10 +78,10 @@ "AssignPublicIp": "ENABLED", "Subnets": [ { - "Ref": "Subnet1Id" + "Ref": "ParameterSubnet1Id" }, { - "Ref": "Subnet2Id" + "Ref": "ParameterSubnet2Id" } ] } diff --git a/ecs/pkg/amazon/testdata/simple/simple-cloudformation-with-overrides-conversion.golden b/ecs/pkg/amazon/testdata/simple/simple-cloudformation-with-overrides-conversion.golden index 2e8f5a4ad..5e641af54 100644 --- a/ecs/pkg/amazon/testdata/simple/simple-cloudformation-with-overrides-conversion.golden +++ b/ecs/pkg/amazon/testdata/simple/simple-cloudformation-with-overrides-conversion.golden @@ -1,15 +1,29 @@ { "AWSTemplateFormatVersion": "2010-09-09", + "Conditions": { + "CreateCluster": { + "Fn::Equals": [ + "", + { + "Ref": "ParameterClusterName" + } + ] + } + }, "Parameters": { - "Subnet1Id": { + "ParameterClusterName": { + "Description": "Name of the ECS cluster to deploy to (optional)", + "Type": "String" + }, + "ParameterSubnet1Id": { "Description": "SubnetId,for Availability Zone 1 in the region in your VPC", "Type": "AWS::EC2::Subnet::Id" }, - "Subnet2Id": { + "ParameterSubnet2Id": { "Description": "SubnetId,for Availability Zone 1 in the region in your VPC", "Type": "AWS::EC2::Subnet::Id" }, - "VPCId": { + "ParameterVPCId": { "Description": "ID of the VPC", "Type": "AWS::EC2::VPC::Id" } @@ -20,11 +34,24 @@ "Description": "Service Map for Docker Compose project TestSimpleWithOverrides", "Name": "TestSimpleWithOverrides.local", "Vpc": { - "Ref": "VPCId" + "Ref": "ParameterVPCId" } }, "Type": "AWS::ServiceDiscovery::PrivateDnsNamespace" }, + "Cluster": { + "Condition": "CreateCluster", + "Properties": { + "ClusterName": "TestSimpleWithOverrides", + "Tags": [ + { + "Key": "com.docker.compose.project", + "Value": "TestSimpleWithOverrides" + } + ] + }, + "Type": "AWS::ECS::Cluster" + }, "LogGroup": { "Properties": { "LogGroupName": "/docker-compose/TestSimpleWithOverrides" @@ -33,7 +60,17 @@ }, "simpleService": { "Properties": { - "Cluster": "TestCluster", + "Cluster": { + "Fn::If": [ + "CreateCluster", + { + "Ref": "Cluster" + }, + { + "Ref": "ParameterClusterName" + } + ] + }, "DesiredCount": 1, "LaunchType": "FARGATE", "NetworkConfiguration": { @@ -41,10 +78,10 @@ "AssignPublicIp": "ENABLED", "Subnets": [ { - "Ref": "Subnet1Id" + "Ref": "ParameterSubnet1Id" }, { - "Ref": "Subnet2Id" + "Ref": "ParameterSubnet2Id" } ] } From ae4dc2e0db7ff27b52ce0c21e36948f7d2b2e2d9 Mon Sep 17 00:00:00 2001 From: Nicolas De Loof <nicolas.deloof@gmail.com> Date: Mon, 18 May 2020 10:53:07 +0200 Subject: [PATCH 074/198] Reject compose file with unsupported features Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com> --- ecs/pkg/amazon/cloudformation.go | 5 +++ ecs/pkg/amazon/cloudformation_test.go | 44 +++++++++---------- .../amazon/testdata/invalid_network_mode.yaml | 5 +++ ecs/pkg/amazon/up.go | 5 --- ecs/pkg/amazon/validate.go | 8 +++- ecs/pkg/amazon/validate_test.go | 13 ++++++ 6 files changed, 50 insertions(+), 30 deletions(-) create mode 100644 ecs/pkg/amazon/testdata/invalid_network_mode.yaml create mode 100644 ecs/pkg/amazon/validate_test.go diff --git a/ecs/pkg/amazon/cloudformation.go b/ecs/pkg/amazon/cloudformation.go index d14b5fafa..93ed2e095 100644 --- a/ecs/pkg/amazon/cloudformation.go +++ b/ecs/pkg/amazon/cloudformation.go @@ -26,6 +26,11 @@ const ( // Convert a compose project into a CloudFormation template func (c client) Convert(project *compose.Project) (*cloudformation.Template, error) { + err := Validate(project) + if err != nil { + return nil, err + } + template := cloudformation.NewTemplate() template.Parameters[ParameterClusterName] = cloudformation.Parameter{ diff --git a/ecs/pkg/amazon/cloudformation_test.go b/ecs/pkg/amazon/cloudformation_test.go index e45073385..456efbd01 100644 --- a/ecs/pkg/amazon/cloudformation_test.go +++ b/ecs/pkg/amazon/cloudformation_test.go @@ -4,46 +4,42 @@ import ( "fmt" "testing" + "gotest.tools/assert" + "github.com/docker/ecs-plugin/pkg/compose" "gotest.tools/v3/golden" ) func TestSimpleConvert(t *testing.T) { - options := compose.ProjectOptions{ - Name: t.Name(), - ConfigPaths: []string{"testdata/input/simple-single-service.yaml"}, - } - result := convertResultAsString(t, options, "TestCluster") + project := load(t, "testdata/input/simple-single-service.yaml") + result := convertResultAsString(t, project, "TestCluster") expected := "simple/simple-cloudformation-conversion.golden" golden.Assert(t, result, expected) } func TestSimpleWithOverrides(t *testing.T) { - options := compose.ProjectOptions{ - Name: t.Name(), - ConfigPaths: []string{"testdata/input/simple-single-service.yaml", "testdata/input/simple-single-service-with-overrides.yaml"}, - } - result := convertResultAsString(t, options, "TestCluster") + project := load(t, "testdata/input/simple-single-service.yaml", "testdata/input/simple-single-service-with-overrides.yaml") + result := convertResultAsString(t, project, "TestCluster") expected := "simple/simple-cloudformation-with-overrides-conversion.golden" golden.Assert(t, result, expected) } -func convertResultAsString(t *testing.T, options compose.ProjectOptions, clusterName string) string { - project, err := compose.ProjectFromOptions(&options) - if err != nil { - t.Error(err) - } +func convertResultAsString(t *testing.T, project *compose.Project, clusterName string) string { client, err := NewClient("", clusterName, "") - if err != nil { - t.Error(err) - } + assert.NilError(t, err) result, err := client.Convert(project) - if err != nil { - t.Error(err) - } + assert.NilError(t, err) resultAsJSON, err := result.JSON() - if err != nil { - t.Error(err) - } + assert.NilError(t, err) return fmt.Sprintf("%s\n", string(resultAsJSON)) } + +func load(t *testing.T, paths ...string) *compose.Project { + options := compose.ProjectOptions{ + Name: t.Name(), + ConfigPaths: paths, + } + project, err := compose.ProjectFromOptions(&options) + assert.NilError(t, err) + return project +} diff --git a/ecs/pkg/amazon/testdata/invalid_network_mode.yaml b/ecs/pkg/amazon/testdata/invalid_network_mode.yaml new file mode 100644 index 000000000..ce8ed8ad5 --- /dev/null +++ b/ecs/pkg/amazon/testdata/invalid_network_mode.yaml @@ -0,0 +1,5 @@ +version: "3" +services: + simple: + image: nginx + network_mode: bridge \ No newline at end of file diff --git a/ecs/pkg/amazon/up.go b/ecs/pkg/amazon/up.go index 2ad74ba8d..e3297249d 100644 --- a/ecs/pkg/amazon/up.go +++ b/ecs/pkg/amazon/up.go @@ -27,11 +27,6 @@ func (c *client) ComposeUp(ctx context.Context, project *compose.Project) error return fmt.Errorf("we do not (yet) support updating an existing CloudFormation stack") } - err = c.Validate(project) - if err != nil { - return err - } - template, err := c.Convert(project) if err != nil { return err diff --git a/ecs/pkg/amazon/validate.go b/ecs/pkg/amazon/validate.go index 551dc7bc7..4e7918fc0 100644 --- a/ecs/pkg/amazon/validate.go +++ b/ecs/pkg/amazon/validate.go @@ -1,12 +1,14 @@ package amazon import ( + "fmt" + "github.com/compose-spec/compose-go/types" "github.com/docker/ecs-plugin/pkg/compose" ) // Validate check the compose model do not use unsupported features and inject sane defaults for ECS deployment -func (c *client) Validate(project *compose.Project) error { +func Validate(project *compose.Project) error { if len(project.Networks) == 0 { // Compose application model implies a default network if none is explicitly set. // FIXME move this to compose-go @@ -22,6 +24,10 @@ func (c *client) Validate(project *compose.Project) error { service.Networks = map[string]*types.ServiceNetworkConfig{"default": nil} project.Services[i] = service } + + if service.NetworkMode != "" && service.NetworkMode != "awsvpc" { + return fmt.Errorf("ECS do not support NetworkMode %q", service.NetworkMode) + } } // Here we can check for incompatible attributes, inject sane defaults, etc diff --git a/ecs/pkg/amazon/validate_test.go b/ecs/pkg/amazon/validate_test.go new file mode 100644 index 000000000..e0cdd6507 --- /dev/null +++ b/ecs/pkg/amazon/validate_test.go @@ -0,0 +1,13 @@ +package amazon + +import ( + "testing" + + "gotest.tools/assert" +) + +func TestInvalidNetworkMode(t *testing.T) { + project := load(t, "testdata/invalid_network_mode.yaml") + err := Validate(project) + assert.Error(t, err, "ECS do not support NetworkMode \"bridge\"") +} From 6798ad12451823ef8cc5113203249e108800f069 Mon Sep 17 00:00:00 2001 From: Guillaume Lours <guillaume.lours@docker.com> Date: Mon, 18 May 2020 11:36:42 +0200 Subject: [PATCH 075/198] Add security group declaration in cloudformation conversion tests Signed-off-by: Guillaume Lours <guillaume.lours@docker.com> Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com> --- .../simple-cloudformation-conversion.golden | 25 +++++++++++++++++++ ...formation-with-overrides-conversion.golden | 25 +++++++++++++++++++ 2 files changed, 50 insertions(+) diff --git a/ecs/pkg/amazon/testdata/simple/simple-cloudformation-conversion.golden b/ecs/pkg/amazon/testdata/simple/simple-cloudformation-conversion.golden index d0e632d9c..0910aaf34 100644 --- a/ecs/pkg/amazon/testdata/simple/simple-cloudformation-conversion.golden +++ b/ecs/pkg/amazon/testdata/simple/simple-cloudformation-conversion.golden @@ -58,6 +58,26 @@ }, "Type": "AWS::Logs::LogGroup" }, + "TestSimpleConvertDefaultNetwork": { + "Properties": { + "GroupDescription": "TestSimpleConvert default Security Group", + "GroupName": "TestSimpleConvertDefaultNetwork", + "Tags": [ + { + "Key": "com.docker.compose.project", + "Value": "TestSimpleConvert" + }, + { + "Key": "com.docker.compose.network", + "Value": "default" + } + ], + "VpcId": { + "Ref": "ParameterVPCId" + } + }, + "Type": "AWS::EC2::SecurityGroup" + }, "simpleService": { "Properties": { "Cluster": { @@ -76,6 +96,11 @@ "NetworkConfiguration": { "AwsvpcConfiguration": { "AssignPublicIp": "ENABLED", + "SecurityGroups": [ + { + "Ref": "TestSimpleConvertDefaultNetwork" + } + ], "Subnets": [ { "Ref": "ParameterSubnet1Id" diff --git a/ecs/pkg/amazon/testdata/simple/simple-cloudformation-with-overrides-conversion.golden b/ecs/pkg/amazon/testdata/simple/simple-cloudformation-with-overrides-conversion.golden index 5e641af54..7c4134382 100644 --- a/ecs/pkg/amazon/testdata/simple/simple-cloudformation-with-overrides-conversion.golden +++ b/ecs/pkg/amazon/testdata/simple/simple-cloudformation-with-overrides-conversion.golden @@ -58,6 +58,26 @@ }, "Type": "AWS::Logs::LogGroup" }, + "TestSimpleWithOverridesDefaultNetwork": { + "Properties": { + "GroupDescription": "TestSimpleWithOverrides default Security Group", + "GroupName": "TestSimpleWithOverridesDefaultNetwork", + "Tags": [ + { + "Key": "com.docker.compose.project", + "Value": "TestSimpleWithOverrides" + }, + { + "Key": "com.docker.compose.network", + "Value": "default" + } + ], + "VpcId": { + "Ref": "ParameterVPCId" + } + }, + "Type": "AWS::EC2::SecurityGroup" + }, "simpleService": { "Properties": { "Cluster": { @@ -76,6 +96,11 @@ "NetworkConfiguration": { "AwsvpcConfiguration": { "AssignPublicIp": "ENABLED", + "SecurityGroups": [ + { + "Ref": "TestSimpleWithOverridesDefaultNetwork" + } + ], "Subnets": [ { "Ref": "ParameterSubnet1Id" From a5a925173c9c12364d8bf23fd716500dd930db21 Mon Sep 17 00:00:00 2001 From: Nicolas De Loof <nicolas.deloof@gmail.com> Date: Mon, 18 May 2020 16:01:00 +0200 Subject: [PATCH 076/198] SDK methods to query service tasks and retrieve public IP Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com> --- ecs/pkg/amazon/sdk.go | 52 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/ecs/pkg/amazon/sdk.go b/ecs/pkg/amazon/sdk.go index b29751309..2df26efd6 100644 --- a/ecs/pkg/amazon/sdk.go +++ b/ecs/pkg/amazon/sdk.go @@ -338,3 +338,55 @@ func (s sdk) GetLogs(ctx context.Context, name string) error { time.Sleep(500 * time.Millisecond) } } + +func (s sdk) GetTasks(ctx context.Context, cluster string, name string) ([]string, error) { + tasks, err := s.ECS.ListTasksWithContext(ctx, &ecs.ListTasksInput{ + Cluster: aws.String(cluster), + ServiceName: aws.String(name), + }) + if err != nil { + return nil, err + } + arns := []string{} + for _, arn := range tasks.TaskArns { + arns = append(arns, *arn) + } + return arns, nil +} + +func (s sdk) GetNetworkInterfaces(ctx context.Context, cluster string, arns ...string) ([]string, error) { + tasks, err := s.ECS.DescribeTasksWithContext(ctx, &ecs.DescribeTasksInput{ + Cluster: aws.String(cluster), + Tasks: aws.StringSlice(arns), + }) + if err != nil { + return nil, err + } + interfaces := []string{} + for _, task := range tasks.Tasks { + for _, attachement := range task.Attachments { + if *attachement.Type == "ElasticNetworkInterface" { + for _, pair := range attachement.Details { + if *pair.Name == "networkInterfaceId" { + interfaces = append(interfaces, *pair.Value) + } + } + } + } + } + return interfaces, nil +} + +func (s sdk) GetPublicIPs(ctx context.Context, interfaces ...string) ([]string, error) { + desc, err := s.EC2.DescribeNetworkInterfaces(&ec2.DescribeNetworkInterfacesInput{ + NetworkInterfaceIds: aws.StringSlice(interfaces), + }) + if err != nil { + return nil, err + } + publicIPs := []string{} + for _, interf := range desc.NetworkInterfaces { + publicIPs = append(publicIPs, *interf.Association.PublicIp) + } + return publicIPs, nil +} From 08bd18231dd30f7564fcf5dbb205b97ea0a283db Mon Sep 17 00:00:00 2001 From: Nicolas De Loof <nicolas.deloof@gmail.com> Date: Tue, 19 May 2020 15:27:11 +0200 Subject: [PATCH 077/198] Introduce `Normalize` and `Check` in compose model lifecycle Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com> --- ecs/pkg/amazon/check.go | 42 ++++ .../{validate_test.go => check_test.go} | 4 +- ecs/pkg/amazon/cloudformation.go | 8 +- ecs/pkg/amazon/compatibility.go | 179 ++++++++++++++++++ ecs/pkg/amazon/convert.go | 27 ++- ecs/pkg/amazon/validate.go | 35 ---- ecs/pkg/compose/normalize.go | 53 ++++++ ecs/pkg/compose/project.go | 5 + 8 files changed, 311 insertions(+), 42 deletions(-) create mode 100644 ecs/pkg/amazon/check.go rename ecs/pkg/amazon/{validate_test.go => check_test.go} (63%) create mode 100644 ecs/pkg/amazon/compatibility.go delete mode 100644 ecs/pkg/amazon/validate.go create mode 100644 ecs/pkg/compose/normalize.go diff --git a/ecs/pkg/amazon/check.go b/ecs/pkg/amazon/check.go new file mode 100644 index 000000000..d249afe4a --- /dev/null +++ b/ecs/pkg/amazon/check.go @@ -0,0 +1,42 @@ +package amazon + +import ( + "github.com/compose-spec/compose-go/types" + "github.com/docker/ecs-plugin/pkg/compose" +) + +type Warning string +type Warnings []string + +type CompatibilityChecker interface { + CheckService(service *types.ServiceConfig) + CheckCapAdd(service *types.ServiceConfig) + CheckDependsOn(service *types.ServiceConfig) + CheckDNS(service *types.ServiceConfig) + CheckDNSOpts(service *types.ServiceConfig) + CheckDNSSearch(service *types.ServiceConfig) + CheckDomainName(service *types.ServiceConfig) + CheckExtraHosts(service *types.ServiceConfig) + CheckHostname(service *types.ServiceConfig) + CheckIpc(service *types.ServiceConfig) + CheckLabels(service *types.ServiceConfig) + CheckLinks(service *types.ServiceConfig) + CheckLogging(service *types.ServiceConfig) + CheckMacAddress(service *types.ServiceConfig) + CheckNetworkMode(service *types.ServiceConfig) + CheckPid(service *types.ServiceConfig) + CheckSysctls(service *types.ServiceConfig) + CheckTmpfs(service *types.ServiceConfig) + CheckUserNSMode(service *types.ServiceConfig) + Errors() []error +} + +// Check the compose model do not use unsupported features and inject sane defaults for ECS deployment +func Check(project *compose.Project) []error { + c := FargateCompatibilityChecker{} + for i, service := range project.Services { + c.CheckService(&service) + project.Services[i] = service + } + return c.errors +} diff --git a/ecs/pkg/amazon/validate_test.go b/ecs/pkg/amazon/check_test.go similarity index 63% rename from ecs/pkg/amazon/validate_test.go rename to ecs/pkg/amazon/check_test.go index e0cdd6507..3f1859548 100644 --- a/ecs/pkg/amazon/validate_test.go +++ b/ecs/pkg/amazon/check_test.go @@ -8,6 +8,6 @@ import ( func TestInvalidNetworkMode(t *testing.T) { project := load(t, "testdata/invalid_network_mode.yaml") - err := Validate(project) - assert.Error(t, err, "ECS do not support NetworkMode \"bridge\"") + err := Check(project) + assert.Error(t, err[0], "'network_mode' \"bridge\" is not supported") } diff --git a/ecs/pkg/amazon/cloudformation.go b/ecs/pkg/amazon/cloudformation.go index 93ed2e095..2eb6f1df8 100644 --- a/ecs/pkg/amazon/cloudformation.go +++ b/ecs/pkg/amazon/cloudformation.go @@ -4,6 +4,8 @@ import ( "fmt" "strings" + "github.com/sirupsen/logrus" + cloudmapapi "github.com/aws/aws-sdk-go/service/servicediscovery" ecsapi "github.com/aws/aws-sdk-go/service/ecs" @@ -26,9 +28,9 @@ const ( // Convert a compose project into a CloudFormation template func (c client) Convert(project *compose.Project) (*cloudformation.Template, error) { - err := Validate(project) - if err != nil { - return nil, err + warnings := Check(project) + for _, w := range warnings { + logrus.Warn(w) } template := cloudformation.NewTemplate() diff --git a/ecs/pkg/amazon/compatibility.go b/ecs/pkg/amazon/compatibility.go new file mode 100644 index 000000000..6049a4d4e --- /dev/null +++ b/ecs/pkg/amazon/compatibility.go @@ -0,0 +1,179 @@ +package amazon + +import ( + "fmt" + + "github.com/aws/aws-sdk-go/service/ecs" + "github.com/compose-spec/compose-go/types" +) + +type FargateCompatibilityChecker struct { + errors []error +} + +func (c *FargateCompatibilityChecker) error(message string, args ...interface{}) { + c.errors = append(c.errors, fmt.Errorf(message, args...)) +} + +func (c *FargateCompatibilityChecker) Errors() []error { + return c.errors +} + +func (c *FargateCompatibilityChecker) CheckService(service *types.ServiceConfig) { + c.CheckCapAdd(service) + c.CheckDependsOn(service) + c.CheckDNS(service) + c.CheckDNSOpts(service) + c.CheckDNSSearch(service) + c.CheckDomainName(service) + c.CheckExtraHosts(service) + c.CheckHostname(service) + c.CheckIpc(service) + c.CheckLabels(service) + c.CheckLinks(service) + c.CheckLogging(service) + c.CheckMacAddress(service) + c.CheckNetworkMode(service) + c.CheckPid(service) + c.CheckSysctls(service) + c.CheckTmpfs(service) + c.CheckUserNSMode(service) +} + +func (c *FargateCompatibilityChecker) CheckNetworkMode(service *types.ServiceConfig) { + if service.NetworkMode != "" && service.NetworkMode != ecs.NetworkModeAwsvpc { + c.error("'network_mode' %q is not supported", service.NetworkMode) + } + service.NetworkMode = ecs.NetworkModeAwsvpc +} + +func (c *FargateCompatibilityChecker) CheckDependsOn(service *types.ServiceConfig) { + if len(service.DependsOn) != 0 { + c.error("'depends_on' is not supported") + service.DependsOn = nil + } +} + +func (c *FargateCompatibilityChecker) CheckLinks(service *types.ServiceConfig) { + if len(service.Links) != 0 { + c.error("'links' is not supported") + service.Links = nil + } +} + +func (c *FargateCompatibilityChecker) CheckLogging(service *types.ServiceConfig) { + c.CheckLoggingDriver(service) +} + +func (c *FargateCompatibilityChecker) CheckLoggingDriver(service *types.ServiceConfig) { + if service.LogDriver != "" && service.LogDriver != ecs.LogDriverAwslogs { + c.error("'log_driver' %q is not supported", service.LogDriver) + service.LogDriver = ecs.LogDriverAwslogs + } +} + +func (c *FargateCompatibilityChecker) CheckPid(service *types.ServiceConfig) { + if service.Pid != "" { + c.error("'pid' is not supported") + service.Pid = "" + } +} + +func (c *FargateCompatibilityChecker) CheckUserNSMode(service *types.ServiceConfig) { + if service.UserNSMode != "" { + c.error("'userns_mode' is not supported") + service.UserNSMode = "" + } +} + +func (c *FargateCompatibilityChecker) CheckIpc(service *types.ServiceConfig) { + if service.Ipc != "" { + c.error("'ipc' is not supported") + service.Ipc = "" + } +} + +func (c *FargateCompatibilityChecker) CheckMacAddress(service *types.ServiceConfig) { + if service.MacAddress != "" { + c.error("'mac_address' is not supported") + service.MacAddress = "" + } +} + +func (c *FargateCompatibilityChecker) CheckHostname(service *types.ServiceConfig) { + if service.Hostname != "" { + c.error("'hostname' is not supported") + service.Hostname = "" + } +} + +func (c *FargateCompatibilityChecker) CheckDomainName(service *types.ServiceConfig) { + if service.DomainName != "" { + c.error("'domainname' is not supported") + service.DomainName = "" + } +} + +func (c *FargateCompatibilityChecker) CheckDNSSearch(service *types.ServiceConfig) { + if len(service.DNSSearch) > 0 { + c.error("'dns_search' is not supported") + service.DNSSearch = nil + } +} + +func (c *FargateCompatibilityChecker) CheckDNS(service *types.ServiceConfig) { + if len(service.DNS) > 0 { + c.error("'dns' is not supported") + service.DNS = nil + } +} + +func (c *FargateCompatibilityChecker) CheckDNSOpts(service *types.ServiceConfig) { + if len(service.DNSOpts) > 0 { + c.error("'dns_opt' is not supported") + service.DNSOpts = nil + } +} + +func (c *FargateCompatibilityChecker) CheckExtraHosts(service *types.ServiceConfig) { + if len(service.ExtraHosts) > 0 { + c.error("'extra_hosts' is not supported") + service.ExtraHosts = nil + } +} + +func (c *FargateCompatibilityChecker) CheckCapAdd(service *types.ServiceConfig) { + for i, v := range service.CapAdd { + if v != "SYS_PTRACE" { + c.error("'cap_add' %s is not supported", v) + l := len(service.CapAdd) + service.CapAdd[i] = service.CapAdd[l-1] + service.CapAdd = service.CapAdd[:l-1] + } + } +} + +func (c *FargateCompatibilityChecker) CheckTmpfs(service *types.ServiceConfig) { + if len(service.Tmpfs) > 0 { + c.error("'tmpfs' is not supported") + service.Tmpfs = nil + } +} + +func (c *FargateCompatibilityChecker) CheckSysctls(service *types.ServiceConfig) { + if len(service.Sysctls) > 0 { + c.error("'sysctls' is not supported") + service.Sysctls = nil + } +} + +func (c *FargateCompatibilityChecker) CheckLabels(service *types.ServiceConfig) { + for k, v := range service.Labels { + if v == "" { + c.error("'labels' with an empty value is not supported") + delete(service.Labels, k) + } + } +} + +var _ CompatibilityChecker = &FargateCompatibilityChecker{} diff --git a/ecs/pkg/amazon/convert.go b/ecs/pkg/amazon/convert.go index 13e90690a..7c8724c47 100644 --- a/ecs/pkg/amazon/convert.go +++ b/ecs/pkg/amazon/convert.go @@ -10,6 +10,7 @@ import ( ecsapi "github.com/aws/aws-sdk-go/service/ecs" "github.com/awslabs/goformation/v4/cloudformation" "github.com/awslabs/goformation/v4/cloudformation/ecs" + "github.com/awslabs/goformation/v4/cloudformation/tags" "github.com/compose-spec/compose-go/types" "github.com/docker/cli/opts" "github.com/docker/ecs-plugin/pkg/compose" @@ -68,7 +69,7 @@ func Convert(project *compose.Project, service types.ServiceConfig) (*ecs.TaskDe ResourceRequirements: nil, StartTimeout: 0, StopTimeout: durationToInt(service.StopGracePeriod), - SystemControls: nil, + SystemControls: toSystemControls(service.Sysctls), Ulimits: toUlimits(service.Ulimits), User: service.User, VolumesFrom: nil, @@ -84,10 +85,32 @@ func Convert(project *compose.Project, service types.ServiceConfig) (*ecs.TaskDe PlacementConstraints: toPlacementConstraints(service.Deploy), ProxyConfiguration: nil, RequiresCompatibilities: []string{ecsapi.LaunchTypeFargate}, - Tags: nil, + Tags: toTags(service.Labels), }, nil } +func toTags(labels types.Labels) []tags.Tag { + t := []tags.Tag{} + for n, v := range labels { + t = append(t, tags.Tag{ + Key: n, + Value: v, + }) + } + return t +} + +func toSystemControls(sysctls types.Mapping) []ecs.TaskDefinition_SystemControl { + sys := []ecs.TaskDefinition_SystemControl{} + for k, v := range sysctls { + sys = append(sys, ecs.TaskDefinition_SystemControl{ + Namespace: k, + Value: v, + }) + } + return sys +} + func toLimits(service types.ServiceConfig) (string, string, error) { // All possible cpu/mem values for Fargate cpuToMem := map[int64][]types.UnitBytes{ diff --git a/ecs/pkg/amazon/validate.go b/ecs/pkg/amazon/validate.go deleted file mode 100644 index 4e7918fc0..000000000 --- a/ecs/pkg/amazon/validate.go +++ /dev/null @@ -1,35 +0,0 @@ -package amazon - -import ( - "fmt" - - "github.com/compose-spec/compose-go/types" - "github.com/docker/ecs-plugin/pkg/compose" -) - -// Validate check the compose model do not use unsupported features and inject sane defaults for ECS deployment -func Validate(project *compose.Project) error { - if len(project.Networks) == 0 { - // Compose application model implies a default network if none is explicitly set. - // FIXME move this to compose-go - project.Networks["default"] = types.NetworkConfig{ - Name: "default", - } - } - - for i, service := range project.Services { - if len(service.Networks) == 0 { - // Service without explicit network attachment are implicitly exposed on default network - // FIXME move this to compose-go - service.Networks = map[string]*types.ServiceNetworkConfig{"default": nil} - project.Services[i] = service - } - - if service.NetworkMode != "" && service.NetworkMode != "awsvpc" { - return fmt.Errorf("ECS do not support NetworkMode %q", service.NetworkMode) - } - } - - // Here we can check for incompatible attributes, inject sane defaults, etc - return nil -} diff --git a/ecs/pkg/compose/normalize.go b/ecs/pkg/compose/normalize.go new file mode 100644 index 000000000..0061a1f1f --- /dev/null +++ b/ecs/pkg/compose/normalize.go @@ -0,0 +1,53 @@ +package compose + +import ( + "fmt" + + "github.com/compose-spec/compose-go/types" + "github.com/sirupsen/logrus" +) + +// Normalize a compose-go model to move deprecated attributes to canonical position, and introduce implicit defaults +// FIXME move this to compose-go +func Normalize(model *types.Config) error { + if len(model.Networks) == 0 { + // Compose application model implies a default network if none is explicitly set. + model.Networks["default"] = types.NetworkConfig{ + Name: "default", + } + } + + for i, s := range model.Services { + if len(s.Networks) == 0 { + // Service without explicit network attachment are implicitly exposed on default network + s.Networks = map[string]*types.ServiceNetworkConfig{"default": nil} + } + + if s.LogDriver != "" { + logrus.Warn("`log_driver` is deprecated. Use the `logging` attribute") + if s.Logging == nil { + s.Logging = &types.LoggingConfig{} + } + if s.Logging.Driver == "" { + s.Logging.Driver = s.LogDriver + } else { + return fmt.Errorf("can't use both 'log_driver' (deprecated) and 'logging.driver'") + } + } + if len(s.LogOpt) != 0 { + logrus.Warn("`log_opts` is deprecated. Use the `logging` attribute") + if s.Logging == nil { + s.Logging = &types.LoggingConfig{} + } + for k, v := range s.LogOpt { + if _, ok := s.Logging.Options[k]; !ok { + s.Logging.Options[k] = v + } else { + return fmt.Errorf("can't use both 'log_opt' (deprecated) and 'logging.options'") + } + } + } + model.Services[i] = s + } + return nil +} diff --git a/ecs/pkg/compose/project.go b/ecs/pkg/compose/project.go index 5c244b2c4..a90bba071 100644 --- a/ecs/pkg/compose/project.go +++ b/ecs/pkg/compose/project.go @@ -25,6 +25,11 @@ func NewProject(config types.ConfigDetails, name string) (*Project, error) { return nil, err } + err = Normalize(model) + if err != nil { + return nil, err + } + p := Project{ Config: *model, projectDir: config.WorkingDir, From e9fe3b28643f595c9d9fe027d8947f12d164e1cb Mon Sep 17 00:00:00 2001 From: Guillaume Lours <guillaume.lours@docker.com> Date: Tue, 19 May 2020 11:49:58 +0200 Subject: [PATCH 078/198] Add e2e test deploying a compose application to an ECS cluster Signed-off-by: Guillaume Lours <guillaume.lours@docker.com> Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com> --- ecs/Makefile | 5 +- ecs/pkg/amazon/api.go | 5 + ecs/pkg/amazon/mock/api.go | 101 ++++++++++++------ ecs/tests/command_test.go | 2 +- ecs/tests/e2e_deploy_services_test.go | 65 +++++++++++ ecs/tests/main_test.go | 9 +- ecs/tests/plugin_test.go | 2 +- ecs/tests/setup_command_test.go | 2 +- .../testdata/input/simple-single-service.yaml | 6 ++ 9 files changed, 157 insertions(+), 40 deletions(-) create mode 100644 ecs/tests/e2e_deploy_services_test.go create mode 100644 ecs/tests/testdata/input/simple-single-service.yaml diff --git a/ecs/Makefile b/ecs/Makefile index 126d5bc56..1033acd2e 100644 --- a/ecs/Makefile +++ b/ecs/Makefile @@ -7,10 +7,13 @@ build: test: build ## Run tests go test ./... -v +e2e: build ## Run tests + go test ./... -v -tags=e2e + dev: build ln -f -s "${PWD}/dist/docker-ecs" "${HOME}/.docker/cli-plugins/docker-ecs" lint: ## Verify Go files golangci-lint run --config ./golangci.yaml ./... -.PHONY: clean build test dev lint +.PHONY: clean build test dev lint e2e diff --git a/ecs/pkg/amazon/api.go b/ecs/pkg/amazon/api.go index 4fa6ddc44..d32e7b7f7 100644 --- a/ecs/pkg/amazon/api.go +++ b/ecs/pkg/amazon/api.go @@ -1,5 +1,7 @@ package amazon +import "context" + //go:generate mockgen -destination=./mock/api.go -package=mock . API type API interface { @@ -7,4 +9,7 @@ type API interface { upAPI logsAPI secretsAPI + GetTasks(ctx context.Context, cluster string, name string) ([]string, error) + GetNetworkInterfaces(ctx context.Context, cluster string, arns ...string) ([]string, error) + GetPublicIPs(ctx context.Context, interfaces ...string) ([]string, error) } diff --git a/ecs/pkg/amazon/mock/api.go b/ecs/pkg/amazon/mock/api.go index c592162b8..d834ec628 100644 --- a/ecs/pkg/amazon/mock/api.go +++ b/ecs/pkg/amazon/mock/api.go @@ -6,12 +6,11 @@ package mock import ( context "context" - reflect "reflect" - cloudformation "github.com/aws/aws-sdk-go/service/cloudformation" cloudformation0 "github.com/awslabs/goformation/v4/cloudformation" docker "github.com/docker/ecs-plugin/pkg/docker" gomock "github.com/golang/mock/gomock" + reflect "reflect" ) // MockAPI is a mock of API interface @@ -52,21 +51,6 @@ func (mr *MockAPIMockRecorder) ClusterExists(arg0, arg1 interface{}) *gomock.Cal return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ClusterExists", reflect.TypeOf((*MockAPI)(nil).ClusterExists), arg0, arg1) } -// CreateCluster mocks base method -func (m *MockAPI) CreateCluster(arg0 context.Context, arg1 string) (string, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "CreateCluster", arg0, arg1) - ret0, _ := ret[0].(string) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// CreateCluster indicates an expected call of CreateCluster -func (mr *MockAPIMockRecorder) CreateCluster(arg0, arg1 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateCluster", reflect.TypeOf((*MockAPI)(nil).CreateCluster), arg0, arg1) -} - // CreateSecret mocks base method func (m *MockAPI) CreateSecret(arg0 context.Context, arg1 docker.Secret) (string, error) { m.ctrl.T.Helper() @@ -168,6 +152,60 @@ func (mr *MockAPIMockRecorder) GetDefaultVPC(arg0 interface{}) *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetDefaultVPC", reflect.TypeOf((*MockAPI)(nil).GetDefaultVPC), arg0) } +// GetLogs mocks base method +func (m *MockAPI) GetLogs(arg0 context.Context, arg1 string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetLogs", arg0, arg1) + ret0, _ := ret[0].(error) + return ret0 +} + +// GetLogs indicates an expected call of GetLogs +func (mr *MockAPIMockRecorder) GetLogs(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetLogs", reflect.TypeOf((*MockAPI)(nil).GetLogs), arg0, arg1) +} + +// GetNetworkInterfaces mocks base method +func (m *MockAPI) GetNetworkInterfaces(arg0 context.Context, arg1 string, arg2 ...string) ([]string, error) { + m.ctrl.T.Helper() + varargs := []interface{}{arg0, arg1} + for _, a := range arg2 { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "GetNetworkInterfaces", varargs...) + ret0, _ := ret[0].([]string) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetNetworkInterfaces indicates an expected call of GetNetworkInterfaces +func (mr *MockAPIMockRecorder) GetNetworkInterfaces(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{arg0, arg1}, arg2...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetNetworkInterfaces", reflect.TypeOf((*MockAPI)(nil).GetNetworkInterfaces), varargs...) +} + +// GetPublicIPs mocks base method +func (m *MockAPI) GetPublicIPs(arg0 context.Context, arg1 ...string) ([]string, error) { + m.ctrl.T.Helper() + varargs := []interface{}{arg0} + for _, a := range arg1 { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "GetPublicIPs", varargs...) + ret0, _ := ret[0].([]string) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetPublicIPs indicates an expected call of GetPublicIPs +func (mr *MockAPIMockRecorder) GetPublicIPs(arg0 interface{}, arg1 ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{arg0}, arg1...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetPublicIPs", reflect.TypeOf((*MockAPI)(nil).GetPublicIPs), varargs...) +} + // GetStackID mocks base method func (m *MockAPI) GetStackID(arg0 context.Context, arg1 string) (string, error) { m.ctrl.T.Helper() @@ -198,6 +236,21 @@ func (mr *MockAPIMockRecorder) GetSubNets(arg0, arg1 interface{}) *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSubNets", reflect.TypeOf((*MockAPI)(nil).GetSubNets), arg0, arg1) } +// GetTasks mocks base method +func (m *MockAPI) GetTasks(arg0 context.Context, arg1, arg2 string) ([]string, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetTasks", arg0, arg1, arg2) + ret0, _ := ret[0].([]string) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetTasks indicates an expected call of GetTasks +func (mr *MockAPIMockRecorder) GetTasks(arg0, arg1, arg2 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetTasks", reflect.TypeOf((*MockAPI)(nil).GetTasks), arg0, arg1, arg2) +} + // InspectSecret mocks base method func (m *MockAPI) InspectSecret(arg0 context.Context, arg1 string) (docker.Secret, error) { m.ctrl.T.Helper() @@ -271,17 +324,3 @@ func (mr *MockAPIMockRecorder) WaitStackComplete(arg0, arg1, arg2 interface{}) * mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "WaitStackComplete", reflect.TypeOf((*MockAPI)(nil).WaitStackComplete), arg0, arg1, arg2) } - -// GetLogs mocks base method -func (m *MockAPI) GetLogs(arg0 context.Context, arg1 string) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetLogs", arg0, arg1) - ret0, _ := ret[0].(error) - return ret0 -} - -// GetLogs mocks base method -func (mr *MockAPIMockRecorder) GetLogs(arg0, arg1 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetLogs", reflect.TypeOf((*MockAPI)(nil).GetLogs), arg0, arg1) -} diff --git a/ecs/tests/command_test.go b/ecs/tests/command_test.go index a027fbaef..3ca7433d4 100644 --- a/ecs/tests/command_test.go +++ b/ecs/tests/command_test.go @@ -7,7 +7,7 @@ import ( ) func TestExitErrorCode(t *testing.T) { - cmd, cleanup := dockerCli.createTestCmd() + cmd, cleanup, _ := dockerCli.createTestCmd() defer cleanup() cmd.Command = dockerCli.Command("ecs", "unknown_command") diff --git a/ecs/tests/e2e_deploy_services_test.go b/ecs/tests/e2e_deploy_services_test.go new file mode 100644 index 000000000..80d77f31e --- /dev/null +++ b/ecs/tests/e2e_deploy_services_test.go @@ -0,0 +1,65 @@ +// +build e2e + +package tests + +import ( + "context" + "testing" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/session" + "github.com/docker/ecs-plugin/pkg/amazon" + "github.com/docker/ecs-plugin/pkg/docker" + "gotest.tools/assert" + "gotest.tools/v3/fs" + "gotest.tools/v3/golden" + "gotest.tools/v3/icmd" +) + +const ( + composeFileName = "compose.yaml" +) + +func TestE2eDeployServices(t *testing.T) { + cmd, cleanup, awsContext := dockerCli.createTestCmd() + defer cleanup() + + composeUpSimpleService(t, cmd, awsContext) +} + +func composeUpSimpleService(t *testing.T, cmd icmd.Cmd, awsContext docker.AwsContext) { + bgContext := context.Background() + composeYAML := golden.Get(t, "input/simple-single-service.yaml") + tmpDir := fs.NewDir(t, t.Name(), + fs.WithFile(composeFileName, "", fs.WithBytes(composeYAML)), + ) + // We can't use the file added in the tmp directory because it will drop if an assertion fails + defer composeDown(t, cmd, golden.Path("input/simple-single-service.yaml")) + defer tmpDir.Remove() + + cmd.Command = dockerCli.Command("ecs", "compose", "--file="+tmpDir.Join(composeFileName), "--project-name", t.Name(), "up") + icmd.RunCmd(cmd).Assert(t, icmd.Success) + + session, err := session.NewSessionWithOptions(session.Options{ + Profile: awsContext.Profile, + Config: aws.Config{ + Region: aws.String(awsContext.Region), + }, + }) + assert.NilError(t, err) + sdk := amazon.NewAPI(session) + arns, err := sdk.GetTasks(bgContext, t.Name(), "simple") + assert.NilError(t, err) + networkInterfaces, err := sdk.GetNetworkInterfaces(bgContext, t.Name(), arns...) + publicIps, err := sdk.GetPublicIPs(context.Background(), networkInterfaces...) + assert.NilError(t, err) + for _, ip := range publicIps { + icmd.RunCommand("curl", "-I", "http://"+ip).Assert(t, icmd.Success) + } + +} + +func composeDown(t *testing.T, cmd icmd.Cmd, composeFile string) { + cmd.Command = dockerCli.Command("ecs", "compose", "--file="+composeFile, "--project-name", t.Name(), "down") + icmd.RunCmd(cmd).Assert(t, icmd.Success) +} diff --git a/ecs/tests/main_test.go b/ecs/tests/main_test.go index a093c6c9b..b8a758144 100644 --- a/ecs/tests/main_test.go +++ b/ecs/tests/main_test.go @@ -29,7 +29,7 @@ type dockerCliCommand struct { type ConfigFileOperator func(configFile *dockerConfigFile.ConfigFile) -func (d dockerCliCommand) createTestCmd(ops ...ConfigFileOperator) (icmd.Cmd, func()) { +func (d dockerCliCommand) createTestCmd(ops ...ConfigFileOperator) (icmd.Cmd, func(), docker.AwsContext) { configDir, err := ioutil.TempDir("", "config") if err != nil { panic(err) @@ -55,9 +55,8 @@ func (d dockerCliCommand) createTestCmd(ops ...ConfigFileOperator) (icmd.Cmd, fu } awsContext := docker.AwsContext{ - Profile: "TestProfile", - Cluster: "TestCluster", - Region: "TestRegion", + Profile: "sandbox.devtools.developer", + Region: "eu-west-3", } testStore, err := docker.NewContextWithStore(testContextName, &awsContext, filepath.Join(configDir, "contexts")) if err != nil { @@ -71,7 +70,7 @@ func (d dockerCliCommand) createTestCmd(ops ...ConfigFileOperator) (icmd.Cmd, fu env := append(os.Environ(), "DOCKER_CONFIG="+configDir, "DOCKER_CLI_EXPERIMENTAL=enabled") // TODO: Remove this once docker ecs plugin is no more experimental - return icmd.Cmd{Env: env}, cleanup + return icmd.Cmd{Env: env}, cleanup, awsContext } func (d dockerCliCommand) Command(args ...string) []string { diff --git a/ecs/tests/plugin_test.go b/ecs/tests/plugin_test.go index f519c252a..48d7e2f54 100644 --- a/ecs/tests/plugin_test.go +++ b/ecs/tests/plugin_test.go @@ -10,7 +10,7 @@ import ( ) func TestInvokePluginFromCLI(t *testing.T) { - cmd, cleanup := dockerCli.createTestCmd() + cmd, cleanup, _ := dockerCli.createTestCmd() defer cleanup() // docker --help should list app as a top command cmd.Command = dockerCli.Command("--help") diff --git a/ecs/tests/setup_command_test.go b/ecs/tests/setup_command_test.go index a0cc9e46d..d36393f48 100644 --- a/ecs/tests/setup_command_test.go +++ b/ecs/tests/setup_command_test.go @@ -10,7 +10,7 @@ import ( ) func TestDefaultAwsContextName(t *testing.T) { - cmd, cleanup := dockerCli.createTestCmd() + cmd, cleanup, _ := dockerCli.createTestCmd() defer cleanup() cmd.Command = dockerCli.Command("ecs", "setup", "--cluster", "clusterName", "--profile", "profileName", diff --git a/ecs/tests/testdata/input/simple-single-service.yaml b/ecs/tests/testdata/input/simple-single-service.yaml new file mode 100644 index 000000000..95f30b1d5 --- /dev/null +++ b/ecs/tests/testdata/input/simple-single-service.yaml @@ -0,0 +1,6 @@ +version: "3" +services: + simple: + image: nginx + ports: + - 80:80 \ No newline at end of file From 3283bceac6cae74a02561438d83f8c934d074a2d Mon Sep 17 00:00:00 2001 From: Nicolas De Loof <nicolas.deloof@gmail.com> Date: Mon, 25 May 2020 16:49:58 +0200 Subject: [PATCH 079/198] Support pull from ECR close #58 Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com> --- ecs/pkg/amazon/cloudformation.go | 1 + ecs/pkg/amazon/iam.go | 1 + .../testdata/simple/simple-cloudformation-conversion.golden | 3 ++- .../simple-cloudformation-with-overrides-conversion.golden | 3 ++- 4 files changed, 6 insertions(+), 2 deletions(-) diff --git a/ecs/pkg/amazon/cloudformation.go b/ecs/pkg/amazon/cloudformation.go index 2eb6f1df8..eec6b854a 100644 --- a/ecs/pkg/amazon/cloudformation.go +++ b/ecs/pkg/amazon/cloudformation.go @@ -120,6 +120,7 @@ func (c client) Convert(project *compose.Project) (*cloudformation.Template, err Policies: rolePolicies, ManagedPolicyArns: []string{ ECSTaskExecutionPolicy, + ECRReadOnlyPolicy, }, } template.Resources[taskDefinition] = definition diff --git a/ecs/pkg/amazon/iam.go b/ecs/pkg/amazon/iam.go index 663577306..affcaaaef 100644 --- a/ecs/pkg/amazon/iam.go +++ b/ecs/pkg/amazon/iam.go @@ -2,6 +2,7 @@ package amazon const ( ECSTaskExecutionPolicy = "arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy" + ECRReadOnlyPolicy = "arn:aws:iam::aws:policy/AmazonEC2ContainerRegistryReadOnly" ActionGetSecretValue = "secretsmanager:GetSecretValue" ActionGetParameters = "ssm:GetParameters" diff --git a/ecs/pkg/amazon/testdata/simple/simple-cloudformation-conversion.golden b/ecs/pkg/amazon/testdata/simple/simple-cloudformation-conversion.golden index 0910aaf34..0050bca06 100644 --- a/ecs/pkg/amazon/testdata/simple/simple-cloudformation-conversion.golden +++ b/ecs/pkg/amazon/testdata/simple/simple-cloudformation-conversion.golden @@ -217,7 +217,8 @@ "Version": "2012-10-17" }, "ManagedPolicyArns": [ - "arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy" + "arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy", + "arn:aws:iam::aws:policy/AmazonEC2ContainerRegistryReadOnly" ] }, "Type": "AWS::IAM::Role" diff --git a/ecs/pkg/amazon/testdata/simple/simple-cloudformation-with-overrides-conversion.golden b/ecs/pkg/amazon/testdata/simple/simple-cloudformation-with-overrides-conversion.golden index 7c4134382..328d627a4 100644 --- a/ecs/pkg/amazon/testdata/simple/simple-cloudformation-with-overrides-conversion.golden +++ b/ecs/pkg/amazon/testdata/simple/simple-cloudformation-with-overrides-conversion.golden @@ -217,7 +217,8 @@ "Version": "2012-10-17" }, "ManagedPolicyArns": [ - "arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy" + "arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy", + "arn:aws:iam::aws:policy/AmazonEC2ContainerRegistryReadOnly" ] }, "Type": "AWS::IAM::Role" From a798c95963d67883e3f0aa0c070dae24a910fac5 Mon Sep 17 00:00:00 2001 From: Nicolas De Loof <nicolas.deloof@gmail.com> Date: Wed, 27 May 2020 14:34:17 +0200 Subject: [PATCH 080/198] Register services with a known port with SRV record see https://github.com/docker/docker_aws/issues/15#issuecomment-634357859 Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com> --- ecs/pkg/amazon/cloudformation.go | 33 ++++++++++++------- .../simple-cloudformation-conversion.golden | 2 +- ...formation-with-overrides-conversion.golden | 2 +- 3 files changed, 23 insertions(+), 14 deletions(-) diff --git a/ecs/pkg/amazon/cloudformation.go b/ecs/pkg/amazon/cloudformation.go index eec6b854a..48960592f 100644 --- a/ecs/pkg/amazon/cloudformation.go +++ b/ecs/pkg/amazon/cloudformation.go @@ -131,18 +131,31 @@ func (c client) Convert(project *compose.Project) (*cloudformation.Template, err } serviceRegistration := fmt.Sprintf("%sServiceDiscoveryEntry", service.Name) + records := []cloudmap.Service_DnsRecord{ + { + TTL: 60, + Type: cloudmapapi.RecordTypeA, + }, + } + serviceRegistry := ecs.Service_ServiceRegistry{ + RegistryArn: cloudformation.GetAtt(serviceRegistration, "Arn"), + } + + if len(service.Ports) > 0 { + records = append(records, cloudmap.Service_DnsRecord{ + TTL: 60, + Type: cloudmapapi.RecordTypeSrv, + }) + serviceRegistry.Port = int(service.Ports[0].Target) + } + template.Resources[serviceRegistration] = &cloudmap.Service{ Description: fmt.Sprintf("%q service discovery entry in Cloud Map", service.Name), HealthCheckConfig: healthCheck, Name: service.Name, NamespaceId: cloudformation.Ref("CloudMap"), DnsConfig: &cloudmap.Service_DnsConfig{ - DnsRecords: []cloudmap.Service_DnsRecord{ - { - TTL: 300, - Type: cloudmapapi.RecordTypeA, - }, - }, + DnsRecords: records, RoutingPolicy: cloudmapapi.RoutingPolicyMultivalue, }, } @@ -169,12 +182,8 @@ func (c client) Convert(project *compose.Project) (*cloudformation.Template, err }, SchedulingStrategy: ecsapi.SchedulingStrategyReplica, ServiceName: service.Name, - ServiceRegistries: []ecs.Service_ServiceRegistry{ - { - RegistryArn: cloudformation.GetAtt(serviceRegistration, "Arn"), - }, - }, - TaskDefinition: cloudformation.Ref(taskDefinition), + ServiceRegistries: []ecs.Service_ServiceRegistry{serviceRegistry}, + TaskDefinition: cloudformation.Ref(taskDefinition), } } return template, nil diff --git a/ecs/pkg/amazon/testdata/simple/simple-cloudformation-conversion.golden b/ecs/pkg/amazon/testdata/simple/simple-cloudformation-conversion.golden index 0050bca06..868dcdd04 100644 --- a/ecs/pkg/amazon/testdata/simple/simple-cloudformation-conversion.golden +++ b/ecs/pkg/amazon/testdata/simple/simple-cloudformation-conversion.golden @@ -135,7 +135,7 @@ "DnsConfig": { "DnsRecords": [ { - "TTL": 300, + "TTL": 60, "Type": "A" } ], diff --git a/ecs/pkg/amazon/testdata/simple/simple-cloudformation-with-overrides-conversion.golden b/ecs/pkg/amazon/testdata/simple/simple-cloudformation-with-overrides-conversion.golden index 328d627a4..fe41a67ff 100644 --- a/ecs/pkg/amazon/testdata/simple/simple-cloudformation-with-overrides-conversion.golden +++ b/ecs/pkg/amazon/testdata/simple/simple-cloudformation-with-overrides-conversion.golden @@ -135,7 +135,7 @@ "DnsConfig": { "DnsRecords": [ { - "TTL": 300, + "TTL": 60, "Type": "A" } ], From 3bc5fc129ebc713a0aaafe03e5d87c56a8a404c9 Mon Sep 17 00:00:00 2001 From: Chad Metcalf <metcalfc@gmail.com> Date: Tue, 26 May 2020 12:01:43 -0700 Subject: [PATCH 081/198] Create the plugin directory if it doesn't exist. Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com> --- ecs/Makefile | 1 + 1 file changed, 1 insertion(+) diff --git a/ecs/Makefile b/ecs/Makefile index 1033acd2e..0fd6b65ae 100644 --- a/ecs/Makefile +++ b/ecs/Makefile @@ -11,6 +11,7 @@ e2e: build ## Run tests go test ./... -v -tags=e2e dev: build + @mkdir -p ~/.docker/cli-plugins/ ln -f -s "${PWD}/dist/docker-ecs" "${HOME}/.docker/cli-plugins/docker-ecs" lint: ## Verify Go files From 01e2b0c989ff6c3f798e162cd2f13f0affa8f469 Mon Sep 17 00:00:00 2001 From: Nicolas De Loof <nicolas.deloof@gmail.com> Date: Wed, 27 May 2020 16:50:04 +0200 Subject: [PATCH 082/198] Present service logs with colored service prefix This reproduce docker-compose behaviour to report logs with prefix also moves log formating out from sdk.go Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com> --- ecs/pkg/amazon/api.go | 2 +- ecs/pkg/amazon/{mock/api.go => api_mock.go} | 12 ++-- ecs/pkg/amazon/down_test.go | 5 +- ecs/pkg/amazon/logs.go | 54 +++++++++++++++++- ecs/pkg/amazon/sdk.go | 6 +- ecs/pkg/console/colors.go | 62 +++++++++++++++++++++ 6 files changed, 127 insertions(+), 14 deletions(-) rename ecs/pkg/amazon/{mock/api.go => api_mock.go} (97%) create mode 100644 ecs/pkg/console/colors.go diff --git a/ecs/pkg/amazon/api.go b/ecs/pkg/amazon/api.go index d32e7b7f7..fecc5d80b 100644 --- a/ecs/pkg/amazon/api.go +++ b/ecs/pkg/amazon/api.go @@ -2,7 +2,7 @@ package amazon import "context" -//go:generate mockgen -destination=./mock/api.go -package=mock . API +//go:generate mockgen -destination=./api_mock.go -self_package "github.com/docker/ecs-plugin/pkg/amazon" -package=amazon . API type API interface { downAPI diff --git a/ecs/pkg/amazon/mock/api.go b/ecs/pkg/amazon/api_mock.go similarity index 97% rename from ecs/pkg/amazon/mock/api.go rename to ecs/pkg/amazon/api_mock.go index d834ec628..06ef4fc07 100644 --- a/ecs/pkg/amazon/mock/api.go +++ b/ecs/pkg/amazon/api_mock.go @@ -1,8 +1,8 @@ // Code generated by MockGen. DO NOT EDIT. // Source: github.com/docker/ecs-plugin/pkg/amazon (interfaces: API) -// Package mock is a generated GoMock package. -package mock +// Package amazon is a generated GoMock package. +package amazon import ( context "context" @@ -153,17 +153,17 @@ func (mr *MockAPIMockRecorder) GetDefaultVPC(arg0 interface{}) *gomock.Call { } // GetLogs mocks base method -func (m *MockAPI) GetLogs(arg0 context.Context, arg1 string) error { +func (m *MockAPI) GetLogs(arg0 context.Context, arg1 string, arg2 LogConsumer) error { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetLogs", arg0, arg1) + ret := m.ctrl.Call(m, "GetLogs", arg0, arg1, arg2) ret0, _ := ret[0].(error) return ret0 } // GetLogs indicates an expected call of GetLogs -func (mr *MockAPIMockRecorder) GetLogs(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockAPIMockRecorder) GetLogs(arg0, arg1, arg2 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetLogs", reflect.TypeOf((*MockAPI)(nil).GetLogs), arg0, arg1) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetLogs", reflect.TypeOf((*MockAPI)(nil).GetLogs), arg0, arg1, arg2) } // GetNetworkInterfaces mocks base method diff --git a/ecs/pkg/amazon/down_test.go b/ecs/pkg/amazon/down_test.go index bf98a6b9c..642faf759 100644 --- a/ecs/pkg/amazon/down_test.go +++ b/ecs/pkg/amazon/down_test.go @@ -4,14 +4,13 @@ import ( "context" "testing" - "github.com/docker/ecs-plugin/pkg/amazon/mock" "github.com/golang/mock/gomock" ) func TestDownDontDeleteCluster(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() - m := mock.NewMockAPI(ctrl) + m := NewMockAPI(ctrl) c := &client{ Cluster: "test_cluster", Region: "region", @@ -30,7 +29,7 @@ func TestDownDontDeleteCluster(t *testing.T) { func TestDownDeleteCluster(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() - m := mock.NewMockAPI(ctrl) + m := NewMockAPI(ctrl) c := &client{ Cluster: "test_cluster", Region: "region", diff --git a/ecs/pkg/amazon/logs.go b/ecs/pkg/amazon/logs.go index b515e25d3..683ecc260 100644 --- a/ecs/pkg/amazon/logs.go +++ b/ecs/pkg/amazon/logs.go @@ -2,12 +2,62 @@ package amazon import ( "context" + "fmt" + "os" + "os/signal" + "strconv" + "strings" + + "github.com/docker/ecs-plugin/pkg/console" ) func (c *client) ComposeLogs(ctx context.Context, projectName string) error { - return c.api.GetLogs(ctx, projectName) + err := c.api.GetLogs(ctx, projectName, &logConsumer{ + colors: map[string]console.ColorFunc{}, + width: 0, + }) + if err != nil { + return err + } + + signalChan := make(chan os.Signal, 1) + signal.Notify(signalChan, os.Interrupt) + <-signalChan + return nil +} + +type logConsumer struct { + colors map[string]console.ColorFunc + width int +} + +func (l *logConsumer) Log(service, container, message string) { + cf, ok := l.colors[service] + if !ok { + cf = <-console.Rainbow + l.colors[service] = cf + 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) + } +} + +func (l *logConsumer) computeWidth() { + width := 0 + for n := range l.colors { + if len(n) > width { + width = len(n) + } + } + l.width = width + 3 +} + +type LogConsumer interface { + Log(service, container, message string) } type logsAPI interface { - GetLogs(ctx context.Context, name string) error + GetLogs(ctx context.Context, name string, consumer LogConsumer) error } diff --git a/ecs/pkg/amazon/sdk.go b/ecs/pkg/amazon/sdk.go index 2df26efd6..42b00c8e8 100644 --- a/ecs/pkg/amazon/sdk.go +++ b/ecs/pkg/amazon/sdk.go @@ -3,6 +3,7 @@ package amazon import ( "context" "fmt" + "strings" "time" "github.com/aws/aws-sdk-go/aws" @@ -309,7 +310,7 @@ func (s sdk) DeleteSecret(ctx context.Context, id string, recover bool) error { return err } -func (s sdk) GetLogs(ctx context.Context, name string) error { +func (s sdk) GetLogs(ctx context.Context, name string, consumer LogConsumer) error { logGroup := fmt.Sprintf("/docker-compose/%s", name) var startTime int64 for { @@ -331,7 +332,8 @@ func (s sdk) GetLogs(ctx context.Context, name string) error { } for _, event := range events.Events { - fmt.Println(*event.Message) + p := strings.Split(*event.LogStreamName, "/") + consumer.Log(p[1], p[2], *event.Message) startTime = *event.IngestionTime } } diff --git a/ecs/pkg/console/colors.go b/ecs/pkg/console/colors.go new file mode 100644 index 000000000..517afb672 --- /dev/null +++ b/ecs/pkg/console/colors.go @@ -0,0 +1,62 @@ +package console + +import ( + "strconv" +) + +var NAMES = []string{ + "grey", + "red", + "green", + "yellow", + "blue", + "magenta", + "cyan", + "white", +} + +var COLORS map[string]ColorFunc + +// ColorFunc use ANSI codes to render colored text on console +type ColorFunc func(s string) string + +var Monochrome = func(s string) string { + return s +} + +func makeColorFunc(code string) ColorFunc { + return func(s string) string { + return ansiColor(code, s) + } +} + +var Rainbow = make(chan ColorFunc) + +func init() { + COLORS = map[string]ColorFunc{} + for i, name := range NAMES { + COLORS[name] = makeColorFunc(strconv.Itoa(30 + i)) + COLORS["intense_"+name] = makeColorFunc(strconv.Itoa(30+i) + ";1") + } + + go func() { + i := 0 + rainbow := []ColorFunc{ + COLORS["cyan"], + COLORS["yellow"], + COLORS["green"], + COLORS["magenta"], + COLORS["blue"], + COLORS["intense_cyan"], + COLORS["intense_yellow"], + COLORS["intense_green"], + COLORS["intense_magenta"], + COLORS["intense_blue"], + } + + for { + Rainbow <- rainbow[i] + i = (i + 1) % len(rainbow) + } + }() +} From 257f8296794709f33a32ac01c8359f0721c16fa4 Mon Sep 17 00:00:00 2001 From: Nicolas De Loof <nicolas.deloof@gmail.com> Date: Wed, 27 May 2020 17:00:25 +0200 Subject: [PATCH 083/198] Create service with project and service tags Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com> --- ecs/pkg/amazon/client.go | 1 + ecs/pkg/amazon/cloudformation.go | 12 +++++++++++- .../simple/simple-cloudformation-conversion.golden | 10 ++++++++++ ...e-cloudformation-with-overrides-conversion.golden | 10 ++++++++++ 4 files changed, 32 insertions(+), 1 deletion(-) diff --git a/ecs/pkg/amazon/client.go b/ecs/pkg/amazon/client.go index 53b40af65..839c5118e 100644 --- a/ecs/pkg/amazon/client.go +++ b/ecs/pkg/amazon/client.go @@ -9,6 +9,7 @@ import ( const ( ProjectTag = "com.docker.compose.project" NetworkTag = "com.docker.compose.network" + ServiceTag = "com.docker.compose.service" ) func NewClient(profile string, cluster string, region string) (compose.API, error) { diff --git a/ecs/pkg/amazon/cloudformation.go b/ecs/pkg/amazon/cloudformation.go index 48960592f..e838f14aa 100644 --- a/ecs/pkg/amazon/cloudformation.go +++ b/ecs/pkg/amazon/cloudformation.go @@ -183,7 +183,17 @@ func (c client) Convert(project *compose.Project) (*cloudformation.Template, err SchedulingStrategy: ecsapi.SchedulingStrategyReplica, ServiceName: service.Name, ServiceRegistries: []ecs.Service_ServiceRegistry{serviceRegistry}, - TaskDefinition: cloudformation.Ref(taskDefinition), + Tags: []tags.Tag{ + { + Key: ProjectTag, + Value: project.Name, + }, + { + Key: ServiceTag, + Value: service.Name, + }, + }, + TaskDefinition: cloudformation.Ref(taskDefinition), } } return template, nil diff --git a/ecs/pkg/amazon/testdata/simple/simple-cloudformation-conversion.golden b/ecs/pkg/amazon/testdata/simple/simple-cloudformation-conversion.golden index 868dcdd04..bbe34758d 100644 --- a/ecs/pkg/amazon/testdata/simple/simple-cloudformation-conversion.golden +++ b/ecs/pkg/amazon/testdata/simple/simple-cloudformation-conversion.golden @@ -123,6 +123,16 @@ } } ], + "Tags": [ + { + "Key": "com.docker.compose.project", + "Value": "TestSimpleConvert" + }, + { + "Key": "com.docker.compose.service", + "Value": "simple" + } + ], "TaskDefinition": { "Ref": "simpleTaskDefinition" } diff --git a/ecs/pkg/amazon/testdata/simple/simple-cloudformation-with-overrides-conversion.golden b/ecs/pkg/amazon/testdata/simple/simple-cloudformation-with-overrides-conversion.golden index fe41a67ff..d9e57b00a 100644 --- a/ecs/pkg/amazon/testdata/simple/simple-cloudformation-with-overrides-conversion.golden +++ b/ecs/pkg/amazon/testdata/simple/simple-cloudformation-with-overrides-conversion.golden @@ -123,6 +123,16 @@ } } ], + "Tags": [ + { + "Key": "com.docker.compose.project", + "Value": "TestSimpleWithOverrides" + }, + { + "Key": "com.docker.compose.service", + "Value": "simple" + } + ], "TaskDefinition": { "Ref": "simpleTaskDefinition" } From 564c369c3e617c34a30b2bff7c0b7e0a8e2d49c1 Mon Sep 17 00:00:00 2001 From: Nicolas De Loof <nicolas.deloof@gmail.com> Date: Wed, 27 May 2020 16:58:58 +0200 Subject: [PATCH 084/198] Compute resource names to avoid unsupported characters Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com> --- ecs/pkg/amazon/cloudformation.go | 17 ++++-- ecs/pkg/amazon/convert.go | 2 +- .../simple-cloudformation-conversion.golden | 56 +++++++++---------- ...formation-with-overrides-conversion.golden | 56 +++++++++---------- 4 files changed, 68 insertions(+), 63 deletions(-) diff --git a/ecs/pkg/amazon/cloudformation.go b/ecs/pkg/amazon/cloudformation.go index e838f14aa..9df247500 100644 --- a/ecs/pkg/amazon/cloudformation.go +++ b/ecs/pkg/amazon/cloudformation.go @@ -2,6 +2,7 @@ package amazon import ( "fmt" + "regexp" "strings" "github.com/sirupsen/logrus" @@ -99,7 +100,7 @@ func (c client) Convert(project *compose.Project) (*cloudformation.Template, err return nil, err } - taskExecutionRole := fmt.Sprintf("%sTaskExecutionRole", service.Name) + taskExecutionRole := fmt.Sprintf("%sTaskExecutionRole", normalizeResourceName(service.Name)) policy, err := c.getPolicy(definition) if err != nil { return nil, err @@ -114,7 +115,7 @@ func (c client) Convert(project *compose.Project) (*cloudformation.Template, err } definition.ExecutionRoleArn = cloudformation.Ref(taskExecutionRole) - taskDefinition := fmt.Sprintf("%sTaskDefinition", service.Name) + taskDefinition := fmt.Sprintf("%sTaskDefinition", normalizeResourceName(service.Name)) template.Resources[taskExecutionRole] = &iam.Role{ AssumeRolePolicyDocument: assumeRolePolicyDocument, Policies: rolePolicies, @@ -130,7 +131,7 @@ func (c client) Convert(project *compose.Project) (*cloudformation.Template, err // FIXME ECS only support HTTP(s) health checks, while Docker only support CMD } - serviceRegistration := fmt.Sprintf("%sServiceDiscoveryEntry", service.Name) + serviceRegistration := fmt.Sprintf("%sServiceDiscoveryEntry", normalizeResourceName(service.Name)) records := []cloudmap.Service_DnsRecord{ { TTL: 60, @@ -166,7 +167,7 @@ func (c client) Convert(project *compose.Project) (*cloudformation.Template, err serviceSecurityGroups = append(serviceSecurityGroups, cloudformation.Ref(logicalName)) } - template.Resources[fmt.Sprintf("%sService", service.Name)] = &ecs.Service{ + template.Resources[fmt.Sprintf("%sService", normalizeResourceName(service.Name))] = &ecs.Service{ Cluster: cluster, DesiredCount: 1, LaunchType: ecsapi.LaunchTypeFargate, @@ -193,7 +194,7 @@ func (c client) Convert(project *compose.Project) (*cloudformation.Template, err Value: service.Name, }, }, - TaskDefinition: cloudformation.Ref(taskDefinition), + TaskDefinition: cloudformation.Ref(normalizeResourceName(taskDefinition)), } } return template, nil @@ -236,7 +237,11 @@ func convertNetwork(project *compose.Project, net string, vpc string) (string, c } func networkResourceName(project *compose.Project, network string) string { - return fmt.Sprintf("%s%sNetwork", project.Name, strings.Title(network)) + return fmt.Sprintf("%s%sNetwork", normalizeResourceName(project.Name), normalizeResourceName(network)) +} + +func normalizeResourceName(s string) string { + return strings.Title(regexp.MustCompile("[^a-zA-Z0-9]+").ReplaceAllString(s, "")) } func (c client) getPolicy(taskDef *ecs.TaskDefinition) (*PolicyDocument, error) { diff --git a/ecs/pkg/amazon/convert.go b/ecs/pkg/amazon/convert.go index 7c8724c47..93d546a6d 100644 --- a/ecs/pkg/amazon/convert.go +++ b/ecs/pkg/amazon/convert.go @@ -57,7 +57,7 @@ func Convert(project *compose.Project, service types.ServiceConfig) (*ecs.TaskDe Options: map[string]string{ "awslogs-region": cloudformation.Ref("AWS::Region"), "awslogs-group": cloudformation.Ref("LogGroup"), - "awslogs-stream-prefix": service.Name, + "awslogs-stream-prefix": project.Name, }, }, Name: service.Name, diff --git a/ecs/pkg/amazon/testdata/simple/simple-cloudformation-conversion.golden b/ecs/pkg/amazon/testdata/simple/simple-cloudformation-conversion.golden index bbe34758d..d78914e65 100644 --- a/ecs/pkg/amazon/testdata/simple/simple-cloudformation-conversion.golden +++ b/ecs/pkg/amazon/testdata/simple/simple-cloudformation-conversion.golden @@ -58,27 +58,7 @@ }, "Type": "AWS::Logs::LogGroup" }, - "TestSimpleConvertDefaultNetwork": { - "Properties": { - "GroupDescription": "TestSimpleConvert default Security Group", - "GroupName": "TestSimpleConvertDefaultNetwork", - "Tags": [ - { - "Key": "com.docker.compose.project", - "Value": "TestSimpleConvert" - }, - { - "Key": "com.docker.compose.network", - "Value": "default" - } - ], - "VpcId": { - "Ref": "ParameterVPCId" - } - }, - "Type": "AWS::EC2::SecurityGroup" - }, - "simpleService": { + "SimpleService": { "Properties": { "Cluster": { "Fn::If": [ @@ -117,7 +97,7 @@ { "RegistryArn": { "Fn::GetAtt": [ - "simpleServiceDiscoveryEntry", + "SimpleServiceDiscoveryEntry", "Arn" ] } @@ -134,12 +114,12 @@ } ], "TaskDefinition": { - "Ref": "simpleTaskDefinition" + "Ref": "SimpleTaskDefinition" } }, "Type": "AWS::ECS::Service" }, - "simpleServiceDiscoveryEntry": { + "SimpleServiceDiscoveryEntry": { "Properties": { "Description": "\"simple\" service discovery entry in Cloud Map", "DnsConfig": { @@ -158,7 +138,7 @@ }, "Type": "AWS::ServiceDiscovery::Service" }, - "simpleTaskDefinition": { + "SimpleTaskDefinition": { "Properties": { "ContainerDefinitions": [ { @@ -191,7 +171,7 @@ "awslogs-region": { "Ref": "AWS::Region" }, - "awslogs-stream-prefix": "simple" + "awslogs-stream-prefix": "TestSimpleConvert" } }, "Name": "simple" @@ -199,7 +179,7 @@ ], "Cpu": "256", "ExecutionRoleArn": { - "Ref": "simpleTaskExecutionRole" + "Ref": "SimpleTaskExecutionRole" }, "Family": "TestSimpleConvert-simple", "Memory": "512", @@ -210,7 +190,7 @@ }, "Type": "AWS::ECS::TaskDefinition" }, - "simpleTaskExecutionRole": { + "SimpleTaskExecutionRole": { "Properties": { "AssumeRolePolicyDocument": { "Statement": [ @@ -232,6 +212,26 @@ ] }, "Type": "AWS::IAM::Role" + }, + "TestSimpleConvertDefaultNetwork": { + "Properties": { + "GroupDescription": "TestSimpleConvert default Security Group", + "GroupName": "TestSimpleConvertDefaultNetwork", + "Tags": [ + { + "Key": "com.docker.compose.project", + "Value": "TestSimpleConvert" + }, + { + "Key": "com.docker.compose.network", + "Value": "default" + } + ], + "VpcId": { + "Ref": "ParameterVPCId" + } + }, + "Type": "AWS::EC2::SecurityGroup" } } } diff --git a/ecs/pkg/amazon/testdata/simple/simple-cloudformation-with-overrides-conversion.golden b/ecs/pkg/amazon/testdata/simple/simple-cloudformation-with-overrides-conversion.golden index d9e57b00a..1eaeed178 100644 --- a/ecs/pkg/amazon/testdata/simple/simple-cloudformation-with-overrides-conversion.golden +++ b/ecs/pkg/amazon/testdata/simple/simple-cloudformation-with-overrides-conversion.golden @@ -58,27 +58,7 @@ }, "Type": "AWS::Logs::LogGroup" }, - "TestSimpleWithOverridesDefaultNetwork": { - "Properties": { - "GroupDescription": "TestSimpleWithOverrides default Security Group", - "GroupName": "TestSimpleWithOverridesDefaultNetwork", - "Tags": [ - { - "Key": "com.docker.compose.project", - "Value": "TestSimpleWithOverrides" - }, - { - "Key": "com.docker.compose.network", - "Value": "default" - } - ], - "VpcId": { - "Ref": "ParameterVPCId" - } - }, - "Type": "AWS::EC2::SecurityGroup" - }, - "simpleService": { + "SimpleService": { "Properties": { "Cluster": { "Fn::If": [ @@ -117,7 +97,7 @@ { "RegistryArn": { "Fn::GetAtt": [ - "simpleServiceDiscoveryEntry", + "SimpleServiceDiscoveryEntry", "Arn" ] } @@ -134,12 +114,12 @@ } ], "TaskDefinition": { - "Ref": "simpleTaskDefinition" + "Ref": "SimpleTaskDefinition" } }, "Type": "AWS::ECS::Service" }, - "simpleServiceDiscoveryEntry": { + "SimpleServiceDiscoveryEntry": { "Properties": { "Description": "\"simple\" service discovery entry in Cloud Map", "DnsConfig": { @@ -158,7 +138,7 @@ }, "Type": "AWS::ServiceDiscovery::Service" }, - "simpleTaskDefinition": { + "SimpleTaskDefinition": { "Properties": { "ContainerDefinitions": [ { @@ -191,7 +171,7 @@ "awslogs-region": { "Ref": "AWS::Region" }, - "awslogs-stream-prefix": "simple" + "awslogs-stream-prefix": "TestSimpleWithOverrides" } }, "Name": "simple" @@ -199,7 +179,7 @@ ], "Cpu": "256", "ExecutionRoleArn": { - "Ref": "simpleTaskExecutionRole" + "Ref": "SimpleTaskExecutionRole" }, "Family": "TestSimpleWithOverrides-simple", "Memory": "512", @@ -210,7 +190,7 @@ }, "Type": "AWS::ECS::TaskDefinition" }, - "simpleTaskExecutionRole": { + "SimpleTaskExecutionRole": { "Properties": { "AssumeRolePolicyDocument": { "Statement": [ @@ -232,6 +212,26 @@ ] }, "Type": "AWS::IAM::Role" + }, + "TestSimpleWithOverridesDefaultNetwork": { + "Properties": { + "GroupDescription": "TestSimpleWithOverrides default Security Group", + "GroupName": "TestSimpleWithOverridesDefaultNetwork", + "Tags": [ + { + "Key": "com.docker.compose.project", + "Value": "TestSimpleWithOverrides" + }, + { + "Key": "com.docker.compose.network", + "Value": "default" + } + ], + "VpcId": { + "Ref": "ParameterVPCId" + } + }, + "Type": "AWS::EC2::SecurityGroup" } } } From da299f59e2a941dc7e2c903111fc114407eb01b8 Mon Sep 17 00:00:00 2001 From: Nicolas De Loof <nicolas.deloof@gmail.com> Date: Wed, 27 May 2020 14:38:50 +0200 Subject: [PATCH 085/198] introduce 'ps' command Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com> --- ecs/cmd/commands/compose.go | 21 ++++++++++++++ ecs/pkg/amazon/api.go | 8 ++---- ecs/pkg/amazon/list.go | 55 +++++++++++++++++++++++++++++++++++++ ecs/pkg/amazon/sdk.go | 4 ++- ecs/pkg/compose/api.go | 1 + 5 files changed, 82 insertions(+), 7 deletions(-) create mode 100644 ecs/pkg/amazon/list.go diff --git a/ecs/cmd/commands/compose.go b/ecs/cmd/commands/compose.go index f7a07226d..0b1e70e65 100644 --- a/ecs/cmd/commands/compose.go +++ b/ecs/cmd/commands/compose.go @@ -23,6 +23,7 @@ func ComposeCommand(dockerCli command.Cli) *cobra.Command { UpCommand(dockerCli, opts), DownCommand(dockerCli, opts), LogsCommand(dockerCli, opts), + PsCommand(dockerCli, opts), ) return cmd } @@ -87,6 +88,26 @@ func UpCommand(dockerCli command.Cli, projectOpts *compose.ProjectOptions) *cobr return cmd } +func PsCommand(dockerCli command.Cli, projectOpts *compose.ProjectOptions) *cobra.Command { + opts := upOptions{} + cmd := &cobra.Command{ + Use: "ps", + RunE: compose.WithProject(projectOpts, func(project *compose.Project, args []string) error { + clusteropts, err := docker.GetAwsContext(dockerCli) + if err != nil { + return err + } + client, err := amazon.NewClient(clusteropts.Profile, clusteropts.Cluster, clusteropts.Region) + if err != nil { + return err + } + return client.ComposePs(context.Background(), project) + }), + } + cmd.Flags().StringVar(&opts.loadBalancerArn, "load-balancer", "", "") + return cmd +} + type downOptions struct { DeleteCluster bool } diff --git a/ecs/pkg/amazon/api.go b/ecs/pkg/amazon/api.go index fecc5d80b..9fd599a1b 100644 --- a/ecs/pkg/amazon/api.go +++ b/ecs/pkg/amazon/api.go @@ -1,15 +1,11 @@ package amazon -import "context" - -//go:generate mockgen -destination=./api_mock.go -self_package "github.com/docker/ecs-plugin/pkg/amazon" -package=amazon . API +//go:generate mockgen -destination=./mock/api.go -package=mock . API type API interface { downAPI upAPI logsAPI secretsAPI - GetTasks(ctx context.Context, cluster string, name string) ([]string, error) - GetNetworkInterfaces(ctx context.Context, cluster string, arns ...string) ([]string, error) - GetPublicIPs(ctx context.Context, interfaces ...string) ([]string, error) + psAPI } diff --git a/ecs/pkg/amazon/list.go b/ecs/pkg/amazon/list.go new file mode 100644 index 000000000..d401e0d69 --- /dev/null +++ b/ecs/pkg/amazon/list.go @@ -0,0 +1,55 @@ +package amazon + +import ( + "context" + "fmt" + "os" + "strings" + "text/tabwriter" + + "github.com/docker/ecs-plugin/pkg/compose" +) + +func (c *client) ComposePs(ctx context.Context, project *compose.Project) error { + cluster := c.Cluster + if cluster == "" { + cluster = project.Name + } + w := tabwriter.NewWriter(os.Stdout, 20, 2, 3, ' ', 0) + fmt.Fprintf(w, "Name\tState\tPorts\n") + for _, s := range project.Services { + tasks, err := c.api.GetTasks(ctx, cluster, s.Name) + if err != nil { + return err + } + if len(tasks) == 0 { + continue + } + // TODO get more data from DescribeTask, including tasks status + networkInterfaces, err := c.api.GetNetworkInterfaces(ctx, cluster, tasks...) + if err != nil { + return err + } + if len(networkInterfaces) == 0 { + fmt.Fprintf(w, "%s\t%s\t\n", s.Name, "Provisioning") + continue + } + publicIps, err := c.api.GetPublicIPs(ctx, networkInterfaces...) + if err != nil { + return err + } + ports := []string{} + for _, p := range s.Ports { + ports = append(ports, fmt.Sprintf("%s:%d->%d/%s", strings.Join(publicIps, ","), p.Published, p.Target, p.Protocol)) + } + fmt.Fprintf(w, "%s\t%s\t%s\n", s.Name, "Up", strings.Join(ports, ", ")) + } + w.Flush() + return nil +} + +type psAPI interface { + GetTasks(ctx context.Context, cluster string, name string) ([]string, error) + GetNetworkInterfaces(ctx context.Context, cluster string, arns ...string) ([]string, error) + GetPublicIPs(ctx context.Context, interfaces ...string) ([]string, error) +} diff --git a/ecs/pkg/amazon/sdk.go b/ecs/pkg/amazon/sdk.go index 42b00c8e8..3f9b9e6b6 100644 --- a/ecs/pkg/amazon/sdk.go +++ b/ecs/pkg/amazon/sdk.go @@ -388,7 +388,9 @@ func (s sdk) GetPublicIPs(ctx context.Context, interfaces ...string) ([]string, } publicIPs := []string{} for _, interf := range desc.NetworkInterfaces { - publicIPs = append(publicIPs, *interf.Association.PublicIp) + if interf.Association != nil { + publicIPs = append(publicIPs, *interf.Association.PublicIp) + } } return publicIPs, nil } diff --git a/ecs/pkg/compose/api.go b/ecs/pkg/compose/api.go index b39afe02d..1049a993c 100644 --- a/ecs/pkg/compose/api.go +++ b/ecs/pkg/compose/api.go @@ -17,4 +17,5 @@ type API interface { InspectSecret(ctx context.Context, id string) (docker.Secret, error) ListSecrets(ctx context.Context) ([]docker.Secret, error) DeleteSecret(ctx context.Context, id string, recover bool) error + ComposePs(background context.Context, project *Project) error } From be1c65d44189bb15ecb7ea0111d4a1739125ed1e Mon Sep 17 00:00:00 2001 From: Nicolas De Loof <nicolas.deloof@gmail.com> Date: Thu, 28 May 2020 09:24:21 +0200 Subject: [PATCH 086/198] Get more from DescribeTask Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com> --- ecs/pkg/amazon/api.go | 4 +- ecs/pkg/amazon/api_mock.go | 74 +++++++++---------- ecs/pkg/amazon/convert.go | 2 +- ecs/pkg/amazon/list.go | 66 ++++++++++------- ecs/pkg/amazon/sdk.go | 26 ++++--- .../simple-cloudformation-conversion.golden | 2 +- ...formation-with-overrides-conversion.golden | 2 +- ecs/tests/e2e_deploy_services_test.go | 6 +- 8 files changed, 100 insertions(+), 82 deletions(-) diff --git a/ecs/pkg/amazon/api.go b/ecs/pkg/amazon/api.go index 9fd599a1b..5b058584a 100644 --- a/ecs/pkg/amazon/api.go +++ b/ecs/pkg/amazon/api.go @@ -1,11 +1,11 @@ package amazon -//go:generate mockgen -destination=./mock/api.go -package=mock . API +//go:generate mockgen -destination=./api_mock.go -self_package "github.com/docker/ecs-plugin/pkg/amazon" -package=amazon . API type API interface { downAPI upAPI logsAPI secretsAPI - psAPI + listAPI } diff --git a/ecs/pkg/amazon/api_mock.go b/ecs/pkg/amazon/api_mock.go index 06ef4fc07..40e9f6872 100644 --- a/ecs/pkg/amazon/api_mock.go +++ b/ecs/pkg/amazon/api_mock.go @@ -137,6 +137,26 @@ func (mr *MockAPIMockRecorder) DescribeStackEvents(arg0, arg1 interface{}) *gomo return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DescribeStackEvents", reflect.TypeOf((*MockAPI)(nil).DescribeStackEvents), arg0, arg1) } +// DescribeTasks mocks base method +func (m *MockAPI) DescribeTasks(arg0 context.Context, arg1 string, arg2 ...string) ([]TaskStatus, error) { + m.ctrl.T.Helper() + varargs := []interface{}{arg0, arg1} + for _, a := range arg2 { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "DescribeTasks", varargs...) + ret0, _ := ret[0].([]TaskStatus) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// DescribeTasks indicates an expected call of DescribeTasks +func (mr *MockAPIMockRecorder) DescribeTasks(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{arg0, arg1}, arg2...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DescribeTasks", reflect.TypeOf((*MockAPI)(nil).DescribeTasks), varargs...) +} + // GetDefaultVPC mocks base method func (m *MockAPI) GetDefaultVPC(arg0 context.Context) (string, error) { m.ctrl.T.Helper() @@ -166,35 +186,15 @@ func (mr *MockAPIMockRecorder) GetLogs(arg0, arg1, arg2 interface{}) *gomock.Cal return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetLogs", reflect.TypeOf((*MockAPI)(nil).GetLogs), arg0, arg1, arg2) } -// GetNetworkInterfaces mocks base method -func (m *MockAPI) GetNetworkInterfaces(arg0 context.Context, arg1 string, arg2 ...string) ([]string, error) { - m.ctrl.T.Helper() - varargs := []interface{}{arg0, arg1} - for _, a := range arg2 { - varargs = append(varargs, a) - } - ret := m.ctrl.Call(m, "GetNetworkInterfaces", varargs...) - ret0, _ := ret[0].([]string) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// GetNetworkInterfaces indicates an expected call of GetNetworkInterfaces -func (mr *MockAPIMockRecorder) GetNetworkInterfaces(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - varargs := append([]interface{}{arg0, arg1}, arg2...) - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetNetworkInterfaces", reflect.TypeOf((*MockAPI)(nil).GetNetworkInterfaces), varargs...) -} - // GetPublicIPs mocks base method -func (m *MockAPI) GetPublicIPs(arg0 context.Context, arg1 ...string) ([]string, error) { +func (m *MockAPI) GetPublicIPs(arg0 context.Context, arg1 ...string) (map[string]string, error) { m.ctrl.T.Helper() varargs := []interface{}{arg0} for _, a := range arg1 { varargs = append(varargs, a) } ret := m.ctrl.Call(m, "GetPublicIPs", varargs...) - ret0, _ := ret[0].([]string) + ret0, _ := ret[0].(map[string]string) ret1, _ := ret[1].(error) return ret0, ret1 } @@ -236,21 +236,6 @@ func (mr *MockAPIMockRecorder) GetSubNets(arg0, arg1 interface{}) *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSubNets", reflect.TypeOf((*MockAPI)(nil).GetSubNets), arg0, arg1) } -// GetTasks mocks base method -func (m *MockAPI) GetTasks(arg0 context.Context, arg1, arg2 string) ([]string, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetTasks", arg0, arg1, arg2) - ret0, _ := ret[0].([]string) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// GetTasks indicates an expected call of GetTasks -func (mr *MockAPIMockRecorder) GetTasks(arg0, arg1, arg2 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetTasks", reflect.TypeOf((*MockAPI)(nil).GetTasks), arg0, arg1, arg2) -} - // InspectSecret mocks base method func (m *MockAPI) InspectSecret(arg0 context.Context, arg1 string) (docker.Secret, error) { m.ctrl.T.Helper() @@ -281,6 +266,21 @@ func (mr *MockAPIMockRecorder) ListSecrets(arg0 interface{}) *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListSecrets", reflect.TypeOf((*MockAPI)(nil).ListSecrets), arg0) } +// ListTasks mocks base method +func (m *MockAPI) ListTasks(arg0 context.Context, arg1, arg2 string) ([]string, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ListTasks", arg0, arg1, arg2) + ret0, _ := ret[0].([]string) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ListTasks indicates an expected call of ListTasks +func (mr *MockAPIMockRecorder) ListTasks(arg0, arg1, arg2 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListTasks", reflect.TypeOf((*MockAPI)(nil).ListTasks), arg0, arg1, arg2) +} + // StackExists mocks base method func (m *MockAPI) StackExists(arg0 context.Context, arg1 string) (bool, error) { m.ctrl.T.Helper() diff --git a/ecs/pkg/amazon/convert.go b/ecs/pkg/amazon/convert.go index 93d546a6d..807f23ca5 100644 --- a/ecs/pkg/amazon/convert.go +++ b/ecs/pkg/amazon/convert.go @@ -77,7 +77,7 @@ func Convert(project *compose.Project, service types.ServiceConfig) (*ecs.TaskDe }, }, Cpu: cpu, - Family: fmt.Sprintf("%s-%s", project.Name, service.Name), + Family: project.Name, IpcMode: service.Ipc, Memory: mem, NetworkMode: ecsapi.NetworkModeAwsvpc, // FIXME could be set by service.NetworkMode, Fargate only supports network mode ‘awsvpc’. diff --git a/ecs/pkg/amazon/list.go b/ecs/pkg/amazon/list.go index d401e0d69..2515fffe9 100644 --- a/ecs/pkg/amazon/list.go +++ b/ecs/pkg/amazon/list.go @@ -17,39 +17,51 @@ func (c *client) ComposePs(ctx context.Context, project *compose.Project) error } w := tabwriter.NewWriter(os.Stdout, 20, 2, 3, ' ', 0) fmt.Fprintf(w, "Name\tState\tPorts\n") - for _, s := range project.Services { - tasks, err := c.api.GetTasks(ctx, cluster, s.Name) - if err != nil { - return err - } - if len(tasks) == 0 { - continue - } - // TODO get more data from DescribeTask, including tasks status - networkInterfaces, err := c.api.GetNetworkInterfaces(ctx, cluster, tasks...) - if err != nil { - return err - } - if len(networkInterfaces) == 0 { - fmt.Fprintf(w, "%s\t%s\t\n", s.Name, "Provisioning") - continue - } - publicIps, err := c.api.GetPublicIPs(ctx, networkInterfaces...) - if err != nil { - return err + arns, err := c.api.ListTasks(ctx, cluster, project.Name) + if err != nil { + return err + } + + tasks, err := c.api.DescribeTasks(ctx, cluster, arns...) + if err != nil { + return err + } + + networkInterfaces := []string{} + for _, t := range tasks { + if t.NetworkInterface != "" { + networkInterfaces = append(networkInterfaces, t.NetworkInterface) } + } + publicIps, err := c.api.GetPublicIPs(ctx, networkInterfaces...) + if err != nil { + return err + } + + for _, t := range tasks { ports := []string{} - for _, p := range s.Ports { - ports = append(ports, fmt.Sprintf("%s:%d->%d/%s", strings.Join(publicIps, ","), p.Published, p.Target, p.Protocol)) + s, err := project.GetService(t.Service) + if err != nil { + return err } - fmt.Fprintf(w, "%s\t%s\t%s\n", s.Name, "Up", strings.Join(ports, ", ")) + for _, p := range s.Ports { + ports = append(ports, fmt.Sprintf("%s:%d->%d/%s", publicIps[t.NetworkInterface], p.Published, p.Target, p.Protocol)) + } + fmt.Fprintf(w, "%s\t%s\t%s\n", s.Name, t.State, strings.Join(ports, ", ")) } w.Flush() return nil } -type psAPI interface { - GetTasks(ctx context.Context, cluster string, name string) ([]string, error) - GetNetworkInterfaces(ctx context.Context, cluster string, arns ...string) ([]string, error) - GetPublicIPs(ctx context.Context, interfaces ...string) ([]string, error) +type TaskStatus struct { + State string + Service string + NetworkInterface string + PublicIP string +} + +type listAPI interface { + ListTasks(ctx context.Context, cluster string, name string) ([]string, error) + DescribeTasks(ctx context.Context, cluster string, arns ...string) ([]TaskStatus, error) + GetPublicIPs(ctx context.Context, interfaces ...string) (map[string]string, error) } diff --git a/ecs/pkg/amazon/sdk.go b/ecs/pkg/amazon/sdk.go index 3f9b9e6b6..1580901fc 100644 --- a/ecs/pkg/amazon/sdk.go +++ b/ecs/pkg/amazon/sdk.go @@ -341,10 +341,10 @@ func (s sdk) GetLogs(ctx context.Context, name string, consumer LogConsumer) err } } -func (s sdk) GetTasks(ctx context.Context, cluster string, name string) ([]string, error) { +func (s sdk) ListTasks(ctx context.Context, cluster string, name string) ([]string, error) { tasks, err := s.ECS.ListTasksWithContext(ctx, &ecs.ListTasksInput{ - Cluster: aws.String(cluster), - ServiceName: aws.String(name), + Cluster: aws.String(cluster), + Family: aws.String(name), }) if err != nil { return nil, err @@ -356,7 +356,7 @@ func (s sdk) GetTasks(ctx context.Context, cluster string, name string) ([]strin return arns, nil } -func (s sdk) GetNetworkInterfaces(ctx context.Context, cluster string, arns ...string) ([]string, error) { +func (s sdk) DescribeTasks(ctx context.Context, cluster string, arns ...string) ([]TaskStatus, error) { tasks, err := s.ECS.DescribeTasksWithContext(ctx, &ecs.DescribeTasksInput{ Cluster: aws.String(cluster), Tasks: aws.StringSlice(arns), @@ -364,32 +364,38 @@ func (s sdk) GetNetworkInterfaces(ctx context.Context, cluster string, arns ...s if err != nil { return nil, err } - interfaces := []string{} + result := []TaskStatus{} for _, task := range tasks.Tasks { + var networkInterface string for _, attachement := range task.Attachments { if *attachement.Type == "ElasticNetworkInterface" { for _, pair := range attachement.Details { if *pair.Name == "networkInterfaceId" { - interfaces = append(interfaces, *pair.Value) + networkInterface = *pair.Value } } } } + result = append(result, TaskStatus{ + State: *task.LastStatus, + Service: strings.Replace(*task.Group, "service:", "", 1), + NetworkInterface: networkInterface, + }) } - return interfaces, nil + return result, nil } -func (s sdk) GetPublicIPs(ctx context.Context, interfaces ...string) ([]string, error) { +func (s sdk) GetPublicIPs(ctx context.Context, interfaces ...string) (map[string]string, error) { desc, err := s.EC2.DescribeNetworkInterfaces(&ec2.DescribeNetworkInterfacesInput{ NetworkInterfaceIds: aws.StringSlice(interfaces), }) if err != nil { return nil, err } - publicIPs := []string{} + publicIPs := map[string]string{} for _, interf := range desc.NetworkInterfaces { if interf.Association != nil { - publicIPs = append(publicIPs, *interf.Association.PublicIp) + publicIPs[*interf.NetworkInterfaceId] = *interf.Association.PublicIp } } return publicIPs, nil diff --git a/ecs/pkg/amazon/testdata/simple/simple-cloudformation-conversion.golden b/ecs/pkg/amazon/testdata/simple/simple-cloudformation-conversion.golden index d78914e65..677c3483e 100644 --- a/ecs/pkg/amazon/testdata/simple/simple-cloudformation-conversion.golden +++ b/ecs/pkg/amazon/testdata/simple/simple-cloudformation-conversion.golden @@ -181,7 +181,7 @@ "ExecutionRoleArn": { "Ref": "SimpleTaskExecutionRole" }, - "Family": "TestSimpleConvert-simple", + "Family": "TestSimpleConvert", "Memory": "512", "NetworkMode": "awsvpc", "RequiresCompatibilities": [ diff --git a/ecs/pkg/amazon/testdata/simple/simple-cloudformation-with-overrides-conversion.golden b/ecs/pkg/amazon/testdata/simple/simple-cloudformation-with-overrides-conversion.golden index 1eaeed178..b81f63512 100644 --- a/ecs/pkg/amazon/testdata/simple/simple-cloudformation-with-overrides-conversion.golden +++ b/ecs/pkg/amazon/testdata/simple/simple-cloudformation-with-overrides-conversion.golden @@ -181,7 +181,7 @@ "ExecutionRoleArn": { "Ref": "SimpleTaskExecutionRole" }, - "Family": "TestSimpleWithOverrides-simple", + "Family": "TestSimpleWithOverrides", "Memory": "512", "NetworkMode": "awsvpc", "RequiresCompatibilities": [ diff --git a/ecs/tests/e2e_deploy_services_test.go b/ecs/tests/e2e_deploy_services_test.go index 80d77f31e..c7002a19f 100644 --- a/ecs/tests/e2e_deploy_services_test.go +++ b/ecs/tests/e2e_deploy_services_test.go @@ -48,10 +48,10 @@ func composeUpSimpleService(t *testing.T, cmd icmd.Cmd, awsContext docker.AwsCon }) assert.NilError(t, err) sdk := amazon.NewAPI(session) - arns, err := sdk.GetTasks(bgContext, t.Name(), "simple") + arns, err := sdk.ListTasks(bgContext, t.Name(), t.Name()) assert.NilError(t, err) - networkInterfaces, err := sdk.GetNetworkInterfaces(bgContext, t.Name(), arns...) - publicIps, err := sdk.GetPublicIPs(context.Background(), networkInterfaces...) + tasks, err := sdk.DescribeTasks(bgContext, t.Name(), arns...) + publicIps, err := sdk.GetPublicIPs(context.Background(), tasks[0].NetworkInterface) assert.NilError(t, err) for _, ip := range publicIps { icmd.RunCommand("curl", "-I", "http://"+ip).Assert(t, icmd.Success) From 6c57fb9693565523853912f397e6d416eb268f7b Mon Sep 17 00:00:00 2001 From: Nicolas De Loof <nicolas.deloof@gmail.com> Date: Thu, 28 May 2020 11:09:36 +0200 Subject: [PATCH 087/198] support deploy.replicas Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com> --- ecs/pkg/amazon/cloudformation.go | 6 +++++- ecs/pkg/amazon/convert.go | 6 +----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/ecs/pkg/amazon/cloudformation.go b/ecs/pkg/amazon/cloudformation.go index 9df247500..ebd948aff 100644 --- a/ecs/pkg/amazon/cloudformation.go +++ b/ecs/pkg/amazon/cloudformation.go @@ -167,9 +167,13 @@ func (c client) Convert(project *compose.Project) (*cloudformation.Template, err serviceSecurityGroups = append(serviceSecurityGroups, cloudformation.Ref(logicalName)) } + desiredCount := 1 + if service.Deploy != nil && service.Deploy.Replicas != nil { + desiredCount = int(*service.Deploy.Replicas) + } template.Resources[fmt.Sprintf("%sService", normalizeResourceName(service.Name))] = &ecs.Service{ Cluster: cluster, - DesiredCount: 1, + DesiredCount: desiredCount, LaunchType: ecsapi.LaunchTypeFargate, NetworkConfiguration: &ecs.Service_NetworkConfiguration{ AwsvpcConfiguration: &ecs.Service_AwsVpcConfiguration{ diff --git a/ecs/pkg/amazon/convert.go b/ecs/pkg/amazon/convert.go index 807f23ca5..5d74df29e 100644 --- a/ecs/pkg/amazon/convert.go +++ b/ecs/pkg/amazon/convert.go @@ -317,14 +317,10 @@ func getImage(image string) string { func getRepoCredentials(service types.ServiceConfig) *ecs.TaskDefinition_RepositoryCredentials { // extract registry and namespace string from image name - credential := "" for key, value := range service.Extras { if key == "x-aws-pull_credentials" { - credential = value.(string) + return &ecs.TaskDefinition_RepositoryCredentials{CredentialsParameter: value.(string)} } } - if credential != "" { - return &ecs.TaskDefinition_RepositoryCredentials{CredentialsParameter: credential} - } return nil } From 5783b6355603c3305ea269b887296b167b75a817 Mon Sep 17 00:00:00 2001 From: Nicolas De Loof <nicolas.deloof@gmail.com> Date: Thu, 28 May 2020 16:45:53 +0200 Subject: [PATCH 088/198] Service can freely communicate within a network Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com> --- ecs/README.md | 8 ++- ecs/pkg/amazon/cloudformation.go | 53 ++++++++++++------- .../simple-cloudformation-conversion.golden | 13 +++++ ...formation-with-overrides-conversion.golden | 13 +++++ ecs/pkg/amazon/up.go | 1 + ecs/pkg/compose/normalize.go | 29 ++++++++++ 6 files changed, 93 insertions(+), 24 deletions(-) diff --git a/ecs/README.md b/ecs/README.md index afa354d3e..c0cb834b5 100644 --- a/ecs/README.md +++ b/ecs/README.md @@ -55,11 +55,9 @@ according to the networks declared in Compose model. Doing so, services attached communicate together, while services from distinct SecurityGroups can't. We just can't set service aliasses per network. A CloudMap private namespace is created for application as `{project}.local`. Services get registered so that we -get service discovery and DNS round-robin (equivalent for Compose's `endpoint_mode: dnsrr`). Hostname-only service -discovery is enabled by running application containers with `LOCALDOMAIN={project}.local` -(see [resolv.conf(5)](http://man7.org/linux/man-pages/man5/resolv.conf.5.html)). This works out-of-the-box for -debian-based Docker images. Alpine images have to include a tiny entrypoint script to replicate this feature: +get service discovery and DNS round-robin (equivalent for Compose's `endpoint_mode: dnsrr`). Docker images SHOULD +include a tiny entrypoint script to replicate this feature: ```shell script -if [ $LOCALDOMAIN ]; then echo "search ${LOCALDOMAIN}" >> /etc/resolv.conf; fi +if [ ! -z LOCALDOMAIN ]; then echo "search ${LOCALDOMAIN}" >> /etc/resolv.conf; fi ``` diff --git a/ecs/pkg/amazon/cloudformation.go b/ecs/pkg/amazon/cloudformation.go index ebd948aff..a9642a6a1 100644 --- a/ecs/pkg/amazon/cloudformation.go +++ b/ecs/pkg/amazon/cloudformation.go @@ -5,6 +5,8 @@ import ( "regexp" "strings" + "github.com/compose-spec/compose-go/types" + "github.com/sirupsen/logrus" cloudmapapi "github.com/aws/aws-sdk-go/service/servicediscovery" @@ -77,9 +79,10 @@ func (c client) Convert(project *compose.Project) (*cloudformation.Template, err } cluster := cloudformation.If("CreateCluster", cloudformation.Ref("Cluster"), cloudformation.Ref(ParameterClusterName)) - for net := range project.Networks { - name, resource := convertNetwork(project, net, cloudformation.Ref(ParameterVPCId)) - template.Resources[name] = resource + for _, net := range project.Networks { + for k, v := range convertNetwork(project, net, cloudformation.Ref(ParameterVPCId)) { + template.Resources[k] = v + } } logGroup := fmt.Sprintf("/docker-compose/%s", project.Name) @@ -204,25 +207,28 @@ func (c client) Convert(project *compose.Project) (*cloudformation.Template, err return template, nil } -func convertNetwork(project *compose.Project, net string, vpc string) (string, cloudformation.Resource) { +func convertNetwork(project *compose.Project, net types.NetworkConfig, vpc string) map[string]cloudformation.Resource { + resources := map[string]cloudformation.Resource{} var ingresses []ec2.SecurityGroup_Ingress - for _, service := range project.Services { - if _, ok := service.Networks[net]; ok { - for _, port := range service.Ports { - ingresses = append(ingresses, ec2.SecurityGroup_Ingress{ - CidrIp: "0.0.0.0/0", - Description: fmt.Sprintf("%s:%d/%s", service.Name, port.Target, port.Protocol), - FromPort: int(port.Target), - IpProtocol: strings.ToUpper(port.Protocol), - ToPort: int(port.Target), - }) + if !net.Internal { + for _, service := range project.Services { + if _, ok := service.Networks[net.Name]; ok { + for _, port := range service.Ports { + ingresses = append(ingresses, ec2.SecurityGroup_Ingress{ + CidrIp: "0.0.0.0/0", + Description: fmt.Sprintf("%s:%d/%s", service.Name, port.Target, port.Protocol), + FromPort: int(port.Target), + IpProtocol: strings.ToUpper(port.Protocol), + ToPort: int(port.Target), + }) + } } } } - securityGroup := networkResourceName(project, net) - resource := &ec2.SecurityGroup{ - GroupDescription: fmt.Sprintf("%s %s Security Group", project.Name, net), + securityGroup := networkResourceName(project, net.Name) + resources[securityGroup] = &ec2.SecurityGroup{ + GroupDescription: fmt.Sprintf("%s %s Security Group", project.Name, net.Name), GroupName: securityGroup, SecurityGroupIngress: ingresses, VpcId: vpc, @@ -233,11 +239,20 @@ func convertNetwork(project *compose.Project, net string, vpc string) (string, c }, { Key: NetworkTag, - Value: net, + Value: net.Name, }, }, } - return securityGroup, resource + + ingress := securityGroup + "Ingress" + resources[ingress] = &ec2.SecurityGroupIngress{ + Description: fmt.Sprintf("Allow communication within network %s", net.Name), + IpProtocol: "-1", // all protocols + GroupId: cloudformation.Ref(securityGroup), + SourceSecurityGroupId: cloudformation.Ref(securityGroup), + } + + return resources } func networkResourceName(project *compose.Project, network string) string { diff --git a/ecs/pkg/amazon/testdata/simple/simple-cloudformation-conversion.golden b/ecs/pkg/amazon/testdata/simple/simple-cloudformation-conversion.golden index 677c3483e..613aba14b 100644 --- a/ecs/pkg/amazon/testdata/simple/simple-cloudformation-conversion.golden +++ b/ecs/pkg/amazon/testdata/simple/simple-cloudformation-conversion.golden @@ -232,6 +232,19 @@ } }, "Type": "AWS::EC2::SecurityGroup" + }, + "TestSimpleConvertDefaultNetworkIngress": { + "Properties": { + "Description": "Allow communication within network default", + "GroupId": { + "Ref": "TestSimpleConvertDefaultNetwork" + }, + "IpProtocol": "-1", + "SourceSecurityGroupId": { + "Ref": "TestSimpleConvertDefaultNetwork" + } + }, + "Type": "AWS::EC2::SecurityGroupIngress" } } } diff --git a/ecs/pkg/amazon/testdata/simple/simple-cloudformation-with-overrides-conversion.golden b/ecs/pkg/amazon/testdata/simple/simple-cloudformation-with-overrides-conversion.golden index b81f63512..0e506fce8 100644 --- a/ecs/pkg/amazon/testdata/simple/simple-cloudformation-with-overrides-conversion.golden +++ b/ecs/pkg/amazon/testdata/simple/simple-cloudformation-with-overrides-conversion.golden @@ -232,6 +232,19 @@ } }, "Type": "AWS::EC2::SecurityGroup" + }, + "TestSimpleWithOverridesDefaultNetworkIngress": { + "Properties": { + "Description": "Allow communication within network default", + "GroupId": { + "Ref": "TestSimpleWithOverridesDefaultNetwork" + }, + "IpProtocol": "-1", + "SourceSecurityGroupId": { + "Ref": "TestSimpleWithOverridesDefaultNetwork" + } + }, + "Type": "AWS::EC2::SecurityGroupIngress" } } } diff --git a/ecs/pkg/amazon/up.go b/ecs/pkg/amazon/up.go index e3297249d..23517a118 100644 --- a/ecs/pkg/amazon/up.go +++ b/ecs/pkg/amazon/up.go @@ -54,6 +54,7 @@ func (c *client) ComposeUp(ctx context.Context, project *compose.Project) error return err } + fmt.Println() return c.WaitStackCompletion(ctx, project.Name, StackCreate) } diff --git a/ecs/pkg/compose/normalize.go b/ecs/pkg/compose/normalize.go index 0061a1f1f..861d146f3 100644 --- a/ecs/pkg/compose/normalize.go +++ b/ecs/pkg/compose/normalize.go @@ -49,5 +49,34 @@ func Normalize(model *types.Config) error { } model.Services[i] = s } + + for i, n := range model.Networks { + if n.Name == "" { + n.Name = i + model.Networks[i] = n + } + } + + for i, v := range model.Volumes { + if v.Name == "" { + v.Name = i + model.Volumes[i] = v + } + } + + for i, c := range model.Configs { + if c.Name == "" { + c.Name = i + model.Configs[i] = c + } + } + + for i, s := range model.Secrets { + if s.Name == "" { + s.Name = i + model.Secrets[i] = s + } + } + return nil } From 5080a83242928414098b99e249fae7994ca15b32 Mon Sep 17 00:00:00 2001 From: Nicolas De Loof <nicolas.deloof@gmail.com> Date: Fri, 29 May 2020 08:55:44 +0200 Subject: [PATCH 089/198] prevent "Tasks cannot be empty" error Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com> --- ecs/pkg/amazon/list.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/ecs/pkg/amazon/list.go b/ecs/pkg/amazon/list.go index 2515fffe9..0a5f4c176 100644 --- a/ecs/pkg/amazon/list.go +++ b/ecs/pkg/amazon/list.go @@ -17,10 +17,15 @@ func (c *client) ComposePs(ctx context.Context, project *compose.Project) error } w := tabwriter.NewWriter(os.Stdout, 20, 2, 3, ' ', 0) fmt.Fprintf(w, "Name\tState\tPorts\n") + defer w.Flush() + arns, err := c.api.ListTasks(ctx, cluster, project.Name) if err != nil { return err } + if len(arns) == 0 { + return nil + } tasks, err := c.api.DescribeTasks(ctx, cluster, arns...) if err != nil { @@ -49,7 +54,6 @@ func (c *client) ComposePs(ctx context.Context, project *compose.Project) error } fmt.Fprintf(w, "%s\t%s\t%s\n", s.Name, t.State, strings.Join(ports, ", ")) } - w.Flush() return nil } From ff882903023e152b31ccceb9f89b812d52d30c78 Mon Sep 17 00:00:00 2001 From: Nicolas De Loof <nicolas.deloof@gmail.com> Date: Fri, 29 May 2020 15:13:16 +0200 Subject: [PATCH 090/198] Make `ps` order predictable so one can run `watch docker ecs compose ps` Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com> --- ecs/pkg/amazon/list.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/ecs/pkg/amazon/list.go b/ecs/pkg/amazon/list.go index 0a5f4c176..4b16f98c8 100644 --- a/ecs/pkg/amazon/list.go +++ b/ecs/pkg/amazon/list.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "os" + "sort" "strings" "text/tabwriter" @@ -43,6 +44,10 @@ func (c *client) ComposePs(ctx context.Context, project *compose.Project) error return err } + sort.Slice(tasks, func(i, j int) bool { + return strings.Compare(tasks[i].Service, tasks[j].Service) < 0 + }) + for _, t := range tasks { ports := []string{} s, err := project.GetService(t.Service) From 7d4222a725f592dfffac6cce79faf699a97f6fd6 Mon Sep 17 00:00:00 2001 From: Nicolas De Loof <nicolas.deloof@gmail.com> Date: Tue, 2 Jun 2020 13:54:33 +0200 Subject: [PATCH 091/198] Implement depends_on using CloudFormation Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com> --- ecs/pkg/amazon/check.go | 1 - ecs/pkg/amazon/cloudformation.go | 18 ++++++++++++++---- ecs/pkg/amazon/compatibility.go | 8 -------- 3 files changed, 14 insertions(+), 13 deletions(-) diff --git a/ecs/pkg/amazon/check.go b/ecs/pkg/amazon/check.go index d249afe4a..169794c76 100644 --- a/ecs/pkg/amazon/check.go +++ b/ecs/pkg/amazon/check.go @@ -11,7 +11,6 @@ type Warnings []string type CompatibilityChecker interface { CheckService(service *types.ServiceConfig) CheckCapAdd(service *types.ServiceConfig) - CheckDependsOn(service *types.ServiceConfig) CheckDNS(service *types.ServiceConfig) CheckDNSOpts(service *types.ServiceConfig) CheckDNSSearch(service *types.ServiceConfig) diff --git a/ecs/pkg/amazon/cloudformation.go b/ecs/pkg/amazon/cloudformation.go index a9642a6a1..87d9f0600 100644 --- a/ecs/pkg/amazon/cloudformation.go +++ b/ecs/pkg/amazon/cloudformation.go @@ -174,10 +174,16 @@ func (c client) Convert(project *compose.Project) (*cloudformation.Template, err if service.Deploy != nil && service.Deploy.Replicas != nil { desiredCount = int(*service.Deploy.Replicas) } - template.Resources[fmt.Sprintf("%sService", normalizeResourceName(service.Name))] = &ecs.Service{ - Cluster: cluster, - DesiredCount: desiredCount, - LaunchType: ecsapi.LaunchTypeFargate, + + dependsOn := []string{} + for _, dependency := range service.DependsOn { + dependsOn = append(dependsOn, serviceResourceName(dependency)) + } + template.Resources[serviceResourceName(service.Name)] = &ecs.Service{ + AWSCloudFormationDependsOn: dependsOn, + Cluster: cluster, + DesiredCount: desiredCount, + LaunchType: ecsapi.LaunchTypeFargate, NetworkConfiguration: &ecs.Service_NetworkConfiguration{ AwsvpcConfiguration: &ecs.Service_AwsVpcConfiguration{ AssignPublicIp: ecsapi.AssignPublicIpEnabled, @@ -259,6 +265,10 @@ func networkResourceName(project *compose.Project, network string) string { return fmt.Sprintf("%s%sNetwork", normalizeResourceName(project.Name), normalizeResourceName(network)) } +func serviceResourceName(dependency string) string { + return fmt.Sprintf("%sService", normalizeResourceName(dependency)) +} + func normalizeResourceName(s string) string { return strings.Title(regexp.MustCompile("[^a-zA-Z0-9]+").ReplaceAllString(s, "")) } diff --git a/ecs/pkg/amazon/compatibility.go b/ecs/pkg/amazon/compatibility.go index 6049a4d4e..1a7f136a4 100644 --- a/ecs/pkg/amazon/compatibility.go +++ b/ecs/pkg/amazon/compatibility.go @@ -21,7 +21,6 @@ func (c *FargateCompatibilityChecker) Errors() []error { func (c *FargateCompatibilityChecker) CheckService(service *types.ServiceConfig) { c.CheckCapAdd(service) - c.CheckDependsOn(service) c.CheckDNS(service) c.CheckDNSOpts(service) c.CheckDNSSearch(service) @@ -47,13 +46,6 @@ func (c *FargateCompatibilityChecker) CheckNetworkMode(service *types.ServiceCon service.NetworkMode = ecs.NetworkModeAwsvpc } -func (c *FargateCompatibilityChecker) CheckDependsOn(service *types.ServiceConfig) { - if len(service.DependsOn) != 0 { - c.error("'depends_on' is not supported") - service.DependsOn = nil - } -} - func (c *FargateCompatibilityChecker) CheckLinks(service *types.ServiceConfig) { if len(service.Links) != 0 { c.error("'links' is not supported") From 1bf4bc9d4614efa819d9fcc832d8c86a9844a57b Mon Sep 17 00:00:00 2001 From: Nicolas De Loof <nicolas.deloof@gmail.com> Date: Tue, 2 Jun 2020 14:17:48 +0200 Subject: [PATCH 092/198] Use distinct family per service definition Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com> --- ecs/pkg/amazon/convert.go | 2 +- ecs/pkg/amazon/list.go | 10 +++++++--- ecs/pkg/amazon/sdk.go | 6 +++--- .../simple/simple-cloudformation-conversion.golden | 2 +- ...ple-cloudformation-with-overrides-conversion.golden | 2 +- 5 files changed, 13 insertions(+), 9 deletions(-) diff --git a/ecs/pkg/amazon/convert.go b/ecs/pkg/amazon/convert.go index 5d74df29e..8f43d62f9 100644 --- a/ecs/pkg/amazon/convert.go +++ b/ecs/pkg/amazon/convert.go @@ -77,7 +77,7 @@ func Convert(project *compose.Project, service types.ServiceConfig) (*ecs.TaskDe }, }, Cpu: cpu, - Family: project.Name, + Family: fmt.Sprintf("%s-%s", project.Name, service.Name), IpcMode: service.Ipc, Memory: mem, NetworkMode: ecsapi.NetworkModeAwsvpc, // FIXME could be set by service.NetworkMode, Fargate only supports network mode ‘awsvpc’. diff --git a/ecs/pkg/amazon/list.go b/ecs/pkg/amazon/list.go index 4b16f98c8..98903f402 100644 --- a/ecs/pkg/amazon/list.go +++ b/ecs/pkg/amazon/list.go @@ -20,9 +20,13 @@ func (c *client) ComposePs(ctx context.Context, project *compose.Project) error fmt.Fprintf(w, "Name\tState\tPorts\n") defer w.Flush() - arns, err := c.api.ListTasks(ctx, cluster, project.Name) - if err != nil { - return err + arns := []string{} + for _, service := range project.Services { + tasks, err := c.api.ListTasks(ctx, cluster, service.Name) + if err != nil { + return err + } + arns = append(arns, tasks...) } if len(arns) == 0 { return nil diff --git a/ecs/pkg/amazon/sdk.go b/ecs/pkg/amazon/sdk.go index 1580901fc..6bc85381b 100644 --- a/ecs/pkg/amazon/sdk.go +++ b/ecs/pkg/amazon/sdk.go @@ -341,10 +341,10 @@ func (s sdk) GetLogs(ctx context.Context, name string, consumer LogConsumer) err } } -func (s sdk) ListTasks(ctx context.Context, cluster string, name string) ([]string, error) { +func (s sdk) ListTasks(ctx context.Context, cluster string, service string) ([]string, error) { tasks, err := s.ECS.ListTasksWithContext(ctx, &ecs.ListTasksInput{ - Cluster: aws.String(cluster), - Family: aws.String(name), + Cluster: aws.String(cluster), + ServiceName: aws.String(service), }) if err != nil { return nil, err diff --git a/ecs/pkg/amazon/testdata/simple/simple-cloudformation-conversion.golden b/ecs/pkg/amazon/testdata/simple/simple-cloudformation-conversion.golden index 613aba14b..c1233d0a3 100644 --- a/ecs/pkg/amazon/testdata/simple/simple-cloudformation-conversion.golden +++ b/ecs/pkg/amazon/testdata/simple/simple-cloudformation-conversion.golden @@ -181,7 +181,7 @@ "ExecutionRoleArn": { "Ref": "SimpleTaskExecutionRole" }, - "Family": "TestSimpleConvert", + "Family": "TestSimpleConvert-simple", "Memory": "512", "NetworkMode": "awsvpc", "RequiresCompatibilities": [ diff --git a/ecs/pkg/amazon/testdata/simple/simple-cloudformation-with-overrides-conversion.golden b/ecs/pkg/amazon/testdata/simple/simple-cloudformation-with-overrides-conversion.golden index 0e506fce8..ca03c1176 100644 --- a/ecs/pkg/amazon/testdata/simple/simple-cloudformation-with-overrides-conversion.golden +++ b/ecs/pkg/amazon/testdata/simple/simple-cloudformation-with-overrides-conversion.golden @@ -181,7 +181,7 @@ "ExecutionRoleArn": { "Ref": "SimpleTaskExecutionRole" }, - "Family": "TestSimpleWithOverrides", + "Family": "TestSimpleWithOverrides-simple", "Memory": "512", "NetworkMode": "awsvpc", "RequiresCompatibilities": [ From b702065075b07f3ec7e52dc0604badae829e0628 Mon Sep 17 00:00:00 2001 From: Nicolas De Loof <nicolas.deloof@gmail.com> Date: Wed, 3 Jun 2020 14:31:19 +0200 Subject: [PATCH 093/198] custom extension to select existing VPC and SecurityGroups Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com> --- ecs/pkg/amazon/cloudformation.go | 26 ++++++++++--------- ecs/pkg/amazon/convert.go | 2 +- .../simple-cloudformation-conversion.golden | 4 +-- ...formation-with-overrides-conversion.golden | 4 +-- ecs/pkg/amazon/up.go | 21 +++++++-------- ecs/pkg/amazon/x.go | 7 +++++ 6 files changed, 35 insertions(+), 29 deletions(-) create mode 100644 ecs/pkg/amazon/x.go diff --git a/ecs/pkg/amazon/cloudformation.go b/ecs/pkg/amazon/cloudformation.go index 87d9f0600..84d0b767d 100644 --- a/ecs/pkg/amazon/cloudformation.go +++ b/ecs/pkg/amazon/cloudformation.go @@ -57,11 +57,11 @@ func (c client) Convert(project *compose.Project) (*cloudformation.Template, err */ template.Parameters[ParameterSubnet1Id] = cloudformation.Parameter{ Type: "AWS::EC2::Subnet::Id", - Description: "SubnetId,for Availability Zone 1 in the region in your VPC", + Description: "SubnetId, for Availability Zone 1 in the region in your VPC", } template.Parameters[ParameterSubnet2Id] = cloudformation.Parameter{ Type: "AWS::EC2::Subnet::Id", - Description: "SubnetId,for Availability Zone 1 in the region in your VPC", + Description: "SubnetId, for Availability Zone 2 in the region in your VPC", } // Create Cluster is `ParameterClusterName` parameter is not set @@ -79,10 +79,9 @@ func (c client) Convert(project *compose.Project) (*cloudformation.Template, err } cluster := cloudformation.If("CreateCluster", cloudformation.Ref("Cluster"), cloudformation.Ref(ParameterClusterName)) + networks := map[string]string{} for _, net := range project.Networks { - for k, v := range convertNetwork(project, net, cloudformation.Ref(ParameterVPCId)) { - template.Resources[k] = v - } + networks[net.Name] = convertNetwork(project, net, cloudformation.Ref(ParameterVPCId), template) } logGroup := fmt.Sprintf("/docker-compose/%s", project.Name) @@ -166,8 +165,7 @@ func (c client) Convert(project *compose.Project) (*cloudformation.Template, err serviceSecurityGroups := []string{} for net := range service.Networks { - logicalName := networkResourceName(project, net) - serviceSecurityGroups = append(serviceSecurityGroups, cloudformation.Ref(logicalName)) + serviceSecurityGroups = append(serviceSecurityGroups, networks[net]) } desiredCount := 1 @@ -213,8 +211,12 @@ func (c client) Convert(project *compose.Project) (*cloudformation.Template, err return template, nil } -func convertNetwork(project *compose.Project, net types.NetworkConfig, vpc string) map[string]cloudformation.Resource { - resources := map[string]cloudformation.Resource{} +func convertNetwork(project *compose.Project, net types.NetworkConfig, vpc string, template *cloudformation.Template) string { + if sg, ok := net.Extras[ExtensionSecurityGroup]; ok { + logrus.Debugf("Security Group for network %q set by user to %q", net.Name, sg) + return sg.(string) + } + var ingresses []ec2.SecurityGroup_Ingress if !net.Internal { for _, service := range project.Services { @@ -233,7 +235,7 @@ func convertNetwork(project *compose.Project, net types.NetworkConfig, vpc strin } securityGroup := networkResourceName(project, net.Name) - resources[securityGroup] = &ec2.SecurityGroup{ + template.Resources[securityGroup] = &ec2.SecurityGroup{ GroupDescription: fmt.Sprintf("%s %s Security Group", project.Name, net.Name), GroupName: securityGroup, SecurityGroupIngress: ingresses, @@ -251,14 +253,14 @@ func convertNetwork(project *compose.Project, net types.NetworkConfig, vpc strin } ingress := securityGroup + "Ingress" - resources[ingress] = &ec2.SecurityGroupIngress{ + template.Resources[ingress] = &ec2.SecurityGroupIngress{ Description: fmt.Sprintf("Allow communication within network %s", net.Name), IpProtocol: "-1", // all protocols GroupId: cloudformation.Ref(securityGroup), SourceSecurityGroupId: cloudformation.Ref(securityGroup), } - return resources + return cloudformation.Ref(securityGroup) } func networkResourceName(project *compose.Project, network string) string { diff --git a/ecs/pkg/amazon/convert.go b/ecs/pkg/amazon/convert.go index 8f43d62f9..82047ddb3 100644 --- a/ecs/pkg/amazon/convert.go +++ b/ecs/pkg/amazon/convert.go @@ -318,7 +318,7 @@ func getImage(image string) string { func getRepoCredentials(service types.ServiceConfig) *ecs.TaskDefinition_RepositoryCredentials { // extract registry and namespace string from image name for key, value := range service.Extras { - if key == "x-aws-pull_credentials" { + if key == ExtensionPullCredentials { return &ecs.TaskDefinition_RepositoryCredentials{CredentialsParameter: value.(string)} } } diff --git a/ecs/pkg/amazon/testdata/simple/simple-cloudformation-conversion.golden b/ecs/pkg/amazon/testdata/simple/simple-cloudformation-conversion.golden index c1233d0a3..8a95637a6 100644 --- a/ecs/pkg/amazon/testdata/simple/simple-cloudformation-conversion.golden +++ b/ecs/pkg/amazon/testdata/simple/simple-cloudformation-conversion.golden @@ -16,11 +16,11 @@ "Type": "String" }, "ParameterSubnet1Id": { - "Description": "SubnetId,for Availability Zone 1 in the region in your VPC", + "Description": "SubnetId, for Availability Zone 1 in the region in your VPC", "Type": "AWS::EC2::Subnet::Id" }, "ParameterSubnet2Id": { - "Description": "SubnetId,for Availability Zone 1 in the region in your VPC", + "Description": "SubnetId, for Availability Zone 2 in the region in your VPC", "Type": "AWS::EC2::Subnet::Id" }, "ParameterVPCId": { diff --git a/ecs/pkg/amazon/testdata/simple/simple-cloudformation-with-overrides-conversion.golden b/ecs/pkg/amazon/testdata/simple/simple-cloudformation-with-overrides-conversion.golden index ca03c1176..a5551ddcb 100644 --- a/ecs/pkg/amazon/testdata/simple/simple-cloudformation-with-overrides-conversion.golden +++ b/ecs/pkg/amazon/testdata/simple/simple-cloudformation-with-overrides-conversion.golden @@ -16,11 +16,11 @@ "Type": "String" }, "ParameterSubnet1Id": { - "Description": "SubnetId,for Availability Zone 1 in the region in your VPC", + "Description": "SubnetId, for Availability Zone 1 in the region in your VPC", "Type": "AWS::EC2::Subnet::Id" }, "ParameterSubnet2Id": { - "Description": "SubnetId,for Availability Zone 1 in the region in your VPC", + "Description": "SubnetId, for Availability Zone 2 in the region in your VPC", "Type": "AWS::EC2::Subnet::Id" }, "ParameterVPCId": { diff --git a/ecs/pkg/amazon/up.go b/ecs/pkg/amazon/up.go index 23517a118..21500c381 100644 --- a/ecs/pkg/amazon/up.go +++ b/ecs/pkg/amazon/up.go @@ -59,18 +59,15 @@ func (c *client) ComposeUp(ctx context.Context, project *compose.Project) error } func (c client) GetVPC(ctx context.Context, project *compose.Project) (string, error) { - //check compose file for the default external network - if net, ok := project.Networks["default"]; ok { - if net.External.External { - vpc := net.Name - ok, err := c.api.VpcExists(ctx, vpc) - if err != nil { - return "", err - } - if !ok { - return "", fmt.Errorf("VPC does not exist: %s", vpc) - } - return vpc, nil + //check compose file for custom VPC selected + if vpc, ok := project.Extras[ExtensionVPC]; ok { + vpcID := vpc.(string) + ok, err := c.api.VpcExists(ctx, vpcID) + if err != nil { + return "", err + } + if !ok { + return "", fmt.Errorf("VPC does not exist: %s", vpc) } } defaultVPC, err := c.api.GetDefaultVPC(ctx) diff --git a/ecs/pkg/amazon/x.go b/ecs/pkg/amazon/x.go new file mode 100644 index 000000000..0b022bac2 --- /dev/null +++ b/ecs/pkg/amazon/x.go @@ -0,0 +1,7 @@ +package amazon + +const ( + ExtensionSecurityGroup = "x-aws-securitygroup" + ExtensionVPC = "x-aws-vpc" + ExtensionPullCredentials = "x-aws-pull_credentials" +) From fc9b10fc9118c0f283de3938fdcae0f2c82e350e Mon Sep 17 00:00:00 2001 From: aiordache <anca.iordache@docker.com> Date: Wed, 3 Jun 2020 11:11:57 +0200 Subject: [PATCH 094/198] add load balancer Signed-off-by: aiordache <anca.iordache@docker.com> Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com> --- ecs/pkg/amazon/cloudformation.go | 119 ++++++++++++++++++++++++++++--- 1 file changed, 111 insertions(+), 8 deletions(-) diff --git a/ecs/pkg/amazon/cloudformation.go b/ecs/pkg/amazon/cloudformation.go index 84d0b767d..400862f5f 100644 --- a/ecs/pkg/amazon/cloudformation.go +++ b/ecs/pkg/amazon/cloudformation.go @@ -9,12 +9,14 @@ import ( "github.com/sirupsen/logrus" + "github.com/aws/aws-sdk-go/service/elbv2" cloudmapapi "github.com/aws/aws-sdk-go/service/servicediscovery" ecsapi "github.com/aws/aws-sdk-go/service/ecs" "github.com/awslabs/goformation/v4/cloudformation" "github.com/awslabs/goformation/v4/cloudformation/ec2" "github.com/awslabs/goformation/v4/cloudformation/ecs" + "github.com/awslabs/goformation/v4/cloudformation/elasticloadbalancingv2" "github.com/awslabs/goformation/v4/cloudformation/iam" "github.com/awslabs/goformation/v4/cloudformation/logs" cloudmap "github.com/awslabs/goformation/v4/cloudformation/servicediscovery" @@ -144,13 +146,7 @@ func (c client) Convert(project *compose.Project) (*cloudformation.Template, err RegistryArn: cloudformation.GetAtt(serviceRegistration, "Arn"), } - if len(service.Ports) > 0 { - records = append(records, cloudmap.Service_DnsRecord{ - TTL: 60, - Type: cloudmapapi.RecordTypeSrv, - }) - serviceRegistry.Port = int(service.Ports[0].Target) - } + loadBalancers := []ecs.Service_LoadBalancer{} template.Resources[serviceRegistration] = &cloudmap.Service{ Description: fmt.Sprintf("%q service discovery entry in Cloud Map", service.Name), @@ -168,12 +164,118 @@ func (c client) Convert(project *compose.Project) (*cloudformation.Template, err serviceSecurityGroups = append(serviceSecurityGroups, networks[net]) } + dependsOn := []string{} + if len(service.Ports) > 0 { + records = append(records, cloudmap.Service_DnsRecord{ + TTL: 60, + Type: cloudmapapi.RecordTypeSrv, + }) + //serviceRegistry.Port = int(service.Ports[0].Target) + // add targetgroup for each published port + for _, port := range service.Ports { + targetGroupName := fmt.Sprintf( + "%s%s%sTargetGroup", + normalizeResourceName(service.Name), + strings.ToUpper(port.Protocol), + string(port.Published), + ) + listenerName := fmt.Sprintf( + "%s%s%sListener", + normalizeResourceName(service.Name), + strings.ToUpper(port.Protocol), + string(port.Published), + ) + loadBalancerName := fmt.Sprintf( + "%s%s%sLoadBalancer", + normalizeResourceName(service.Name), + strings.ToUpper(port.Protocol), + string(port.Published), + ) + dependsOn = append(dependsOn, listenerName) + lbType := "network" + lbSecGroups := []string{} + protocolType := strings.ToUpper(port.Protocol) + targetType := elbv2.TargetTypeEnumInstance + if port.Published == 80 || port.Published == 443 { + lbType = "application" + lbSecGroups = serviceSecurityGroups + protocolType = "HTTPS" + targetType = elbv2.TargetTypeEnumIp + if port.Published == 80 { + protocolType = "HTTP" + } + } + + template.Resources[targetGroupName] = &elasticloadbalancingv2.TargetGroup{ + Name: targetGroupName, + Port: int(port.Target), + Protocol: protocolType, + Tags: []tags.Tag{ + { + Key: ProjectTag, + Value: project.Name, + }, + { + Key: ServiceTag, + Value: service.Name, + }, + }, + VpcId: cloudformation.Ref(ParameterVPCId), + TargetType: targetType, + } + + template.Resources[loadBalancerName] = &elasticloadbalancingv2.LoadBalancer{ + Name: loadBalancerName, + Scheme: "internet-facing", + SecurityGroups: lbSecGroups, + Subnets: []string{ + cloudformation.Ref(ParameterSubnet1Id), + cloudformation.Ref(ParameterSubnet2Id), + }, + Tags: []tags.Tag{ + { + Key: ProjectTag, + Value: project.Name, + }, + { + Key: ServiceTag, + Value: service.Name, + }, + }, + Type: lbType, + } + + template.Resources[listenerName] = &elasticloadbalancingv2.Listener{ + DefaultActions: []elasticloadbalancingv2.Listener_Action{ + { + ForwardConfig: &elasticloadbalancingv2.Listener_ForwardConfig{ + TargetGroups: []elasticloadbalancingv2.Listener_TargetGroupTuple{ + { + TargetGroupArn: cloudformation.Ref(targetGroupName), + }, + }, + }, + Type: elbv2.ActionTypeEnumForward, + }, + }, + LoadBalancerArn: cloudformation.Ref(loadBalancerName), + Protocol: protocolType, + Port: int(port.Published), + } + + loadBalancers = append(loadBalancers, ecs.Service_LoadBalancer{ + ContainerName: service.Name, + ContainerPort: int(port.Published), + TargetGroupArn: cloudformation.Ref(targetGroupName), + }) + } + } + desiredCount := 1 if service.Deploy != nil && service.Deploy.Replicas != nil { desiredCount = int(*service.Deploy.Replicas) } - dependsOn := []string{} for _, dependency := range service.DependsOn { dependsOn = append(dependsOn, serviceResourceName(dependency)) } @@ -182,6 +284,7 @@ func (c client) Convert(project *compose.Project) (*cloudformation.Template, err Cluster: cluster, DesiredCount: desiredCount, LaunchType: ecsapi.LaunchTypeFargate, + LoadBalancers: loadBalancers, NetworkConfiguration: &ecs.Service_NetworkConfiguration{ AwsvpcConfiguration: &ecs.Service_AwsVpcConfiguration{ AssignPublicIp: ecsapi.AssignPublicIpEnabled, From ae3101fe1237e50955c7efa2e6a531494eeac2a5 Mon Sep 17 00:00:00 2001 From: aiordache <anca.iordache@docker.com> Date: Wed, 3 Jun 2020 13:06:21 +0200 Subject: [PATCH 095/198] create unique load balancer per app and cleanup Signed-off-by: aiordache <anca.iordache@docker.com> Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com> --- ecs/pkg/amazon/cloudformation.go | 118 ++++++++++++++----------------- 1 file changed, 55 insertions(+), 63 deletions(-) diff --git a/ecs/pkg/amazon/cloudformation.go b/ecs/pkg/amazon/cloudformation.go index 400862f5f..4c57a7c74 100644 --- a/ecs/pkg/amazon/cloudformation.go +++ b/ecs/pkg/amazon/cloudformation.go @@ -136,17 +136,15 @@ func (c client) Convert(project *compose.Project) (*cloudformation.Template, err } serviceRegistration := fmt.Sprintf("%sServiceDiscoveryEntry", normalizeResourceName(service.Name)) - records := []cloudmap.Service_DnsRecord{ - { - TTL: 60, - Type: cloudmapapi.RecordTypeA, - }, - } serviceRegistry := ecs.Service_ServiceRegistry{ RegistryArn: cloudformation.GetAtt(serviceRegistration, "Arn"), } - loadBalancers := []ecs.Service_LoadBalancer{} + serviceSecurityGroups := []string{} + for net := range service.Networks { + logicalName := networkResourceName(project, net) + serviceSecurityGroups = append(serviceSecurityGroups, cloudformation.Ref(logicalName)) + } template.Resources[serviceRegistration] = &cloudmap.Service{ Description: fmt.Sprintf("%q service discovery entry in Cloud Map", service.Name), @@ -154,7 +152,12 @@ func (c client) Convert(project *compose.Project) (*cloudformation.Template, err Name: service.Name, NamespaceId: cloudformation.Ref("CloudMap"), DnsConfig: &cloudmap.Service_DnsConfig{ - DnsRecords: records, + DnsRecords: []cloudmap.Service_DnsRecord{ + { + TTL: 60, + Type: cloudmapapi.RecordTypeA, + }, + }, RoutingPolicy: cloudmapapi.RoutingPolicyMultivalue, }, } @@ -165,47 +168,55 @@ func (c client) Convert(project *compose.Project) (*cloudformation.Template, err } dependsOn := []string{} + loadBalancers := []ecs.Service_LoadBalancer{} if len(service.Ports) > 0 { - records = append(records, cloudmap.Service_DnsRecord{ - TTL: 60, - Type: cloudmapapi.RecordTypeSrv, - }) - //serviceRegistry.Port = int(service.Ports[0].Target) - // add targetgroup for each published port for _, port := range service.Ports { - targetGroupName := fmt.Sprintf( - "%s%s%sTargetGroup", - normalizeResourceName(service.Name), - strings.ToUpper(port.Protocol), - string(port.Published), - ) - listenerName := fmt.Sprintf( - "%s%s%sListener", - normalizeResourceName(service.Name), - strings.ToUpper(port.Protocol), - string(port.Published), - ) - loadBalancerName := fmt.Sprintf( - "%s%s%sLoadBalancer", - normalizeResourceName(service.Name), - strings.ToUpper(port.Protocol), - string(port.Published), - ) - dependsOn = append(dependsOn, listenerName) - lbType := "network" - lbSecGroups := []string{} + loadBalancerType := "network" + protocolType := strings.ToUpper(port.Protocol) targetType := elbv2.TargetTypeEnumInstance + loadBalancerSecGroups := []string{} + if port.Published == 80 || port.Published == 443 { - lbType = "application" - lbSecGroups = serviceSecurityGroups + loadBalancerType = "application" + loadBalancerSecGroups = serviceSecurityGroups protocolType = "HTTPS" targetType = elbv2.TargetTypeEnumIp if port.Published == 80 { protocolType = "HTTP" } } + loadBalancerName := fmt.Sprintf( + "%s%sLB", + strings.Title(project.Name), + strings.ToUpper(loadBalancerType[0:1]), + ) + // create load baalncer if it doesn't exist + if _, ok := template.Resources[loadBalancerName]; !ok { + template.Resources[loadBalancerName] = &elasticloadbalancingv2.LoadBalancer{ + Name: loadBalancerName, + Scheme: "internet-facing", + SecurityGroups: loadBalancerSecGroups, + Subnets: []string{ + cloudformation.Ref(ParameterSubnet1Id), + cloudformation.Ref(ParameterSubnet2Id), + }, + Tags: []tags.Tag{ + { + Key: ProjectTag, + Value: project.Name, + }, + }, + Type: loadBalancerType, + } + } + targetGroupName := fmt.Sprintf( + "%s%s%sTargetGroup", + normalizeResourceName(service.Name), + strings.ToUpper(port.Protocol), + string(port.Published), + ) template.Resources[targetGroupName] = &elasticloadbalancingv2.TargetGroup{ Name: targetGroupName, Port: int(port.Target), @@ -215,36 +226,17 @@ func (c client) Convert(project *compose.Project) (*cloudformation.Template, err Key: ProjectTag, Value: project.Name, }, - { - Key: ServiceTag, - Value: service.Name, - }, }, VpcId: cloudformation.Ref(ParameterVPCId), TargetType: targetType, } - - template.Resources[loadBalancerName] = &elasticloadbalancingv2.LoadBalancer{ - Name: loadBalancerName, - Scheme: "internet-facing", - SecurityGroups: lbSecGroups, - Subnets: []string{ - cloudformation.Ref(ParameterSubnet1Id), - cloudformation.Ref(ParameterSubnet2Id), - }, - Tags: []tags.Tag{ - { - Key: ProjectTag, - Value: project.Name, - }, - { - Key: ServiceTag, - Value: service.Name, - }, - }, - Type: lbType, - } - + listenerName := fmt.Sprintf( + "%s%s%sListener", + normalizeResourceName(service.Name), + strings.ToUpper(port.Protocol), + string(port.Published), + ) + dependsOn = append(dependsOn, listenerName) template.Resources[listenerName] = &elasticloadbalancingv2.Listener{ DefaultActions: []elasticloadbalancingv2.Listener_Action{ { From 92173eaf352f8c3eb2ad6d064048a2af54dfbed6 Mon Sep 17 00:00:00 2001 From: aiordache <anca.iordache@docker.com> Date: Wed, 3 Jun 2020 16:56:13 +0200 Subject: [PATCH 096/198] add SO link for issue if listener is not in service dependencies Signed-off-by: aiordache <anca.iordache@docker.com> Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com> --- ecs/pkg/amazon/cloudformation.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ecs/pkg/amazon/cloudformation.go b/ecs/pkg/amazon/cloudformation.go index 4c57a7c74..a2df3f8eb 100644 --- a/ecs/pkg/amazon/cloudformation.go +++ b/ecs/pkg/amazon/cloudformation.go @@ -236,6 +236,8 @@ func (c client) Convert(project *compose.Project) (*cloudformation.Template, err strings.ToUpper(port.Protocol), string(port.Published), ) + //add listener to dependsOn + //https://stackoverflow.com/questions/53971873/the-target-group-does-not-have-an-associated-load-balancer dependsOn = append(dependsOn, listenerName) template.Resources[listenerName] = &elasticloadbalancingv2.Listener{ DefaultActions: []elasticloadbalancingv2.Listener_Action{ From e7f77ca3ef7e58583dfa2d378b439434f9caa3d2 Mon Sep 17 00:00:00 2001 From: aiordache <anca.iordache@docker.com> Date: Wed, 3 Jun 2020 17:40:39 +0200 Subject: [PATCH 097/198] add all service security groups to LB Signed-off-by: aiordache <anca.iordache@docker.com> Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com> --- ecs/pkg/amazon/cloudformation.go | 58 ++++++++++++++++++++------------ 1 file changed, 36 insertions(+), 22 deletions(-) diff --git a/ecs/pkg/amazon/cloudformation.go b/ecs/pkg/amazon/cloudformation.go index a2df3f8eb..d66bda509 100644 --- a/ecs/pkg/amazon/cloudformation.go +++ b/ecs/pkg/amazon/cloudformation.go @@ -97,6 +97,8 @@ func (c client) Convert(project *compose.Project) (*cloudformation.Template, err Name: fmt.Sprintf("%s.local", project.Name), Vpc: cloudformation.Ref(ParameterVPCId), } + //map LB type to security groups list + loadBalancers := map[string][]string{} for _, service := range project.Services { definition, err := Convert(project, service) @@ -168,7 +170,7 @@ func (c client) Convert(project *compose.Project) (*cloudformation.Template, err } dependsOn := []string{} - loadBalancers := []ecs.Service_LoadBalancer{} + serviceLB := []ecs.Service_LoadBalancer{} if len(service.Ports) > 0 { for _, port := range service.Ports { loadBalancerType := "network" @@ -191,26 +193,12 @@ func (c client) Convert(project *compose.Project) (*cloudformation.Template, err strings.Title(project.Name), strings.ToUpper(loadBalancerType[0:1]), ) - // create load baalncer if it doesn't exist - if _, ok := template.Resources[loadBalancerName]; !ok { - - template.Resources[loadBalancerName] = &elasticloadbalancingv2.LoadBalancer{ - Name: loadBalancerName, - Scheme: "internet-facing", - SecurityGroups: loadBalancerSecGroups, - Subnets: []string{ - cloudformation.Ref(ParameterSubnet1Id), - cloudformation.Ref(ParameterSubnet2Id), - }, - Tags: []tags.Tag{ - { - Key: ProjectTag, - Value: project.Name, - }, - }, - Type: loadBalancerType, - } + // create load balancer if it doesn't exist + if _, ok := loadBalancers[loadBalancerType]; !ok { + loadBalancers[loadBalancerType] = []string{} } + loadBalancers[loadBalancerType] = append(loadBalancers[loadBalancerType], loadBalancerSecGroups...) + targetGroupName := fmt.Sprintf( "%s%s%sTargetGroup", normalizeResourceName(service.Name), @@ -257,7 +245,7 @@ func (c client) Convert(project *compose.Project) (*cloudformation.Template, err Port: int(port.Published), } - loadBalancers = append(loadBalancers, ecs.Service_LoadBalancer{ + serviceLB = append(serviceLB, ecs.Service_LoadBalancer{ ContainerName: service.Name, ContainerPort: int(port.Published), TargetGroupArn: cloudformation.Ref(targetGroupName), @@ -278,7 +266,7 @@ func (c client) Convert(project *compose.Project) (*cloudformation.Template, err Cluster: cluster, DesiredCount: desiredCount, LaunchType: ecsapi.LaunchTypeFargate, - LoadBalancers: loadBalancers, + LoadBalancers: serviceLB, NetworkConfiguration: &ecs.Service_NetworkConfiguration{ AwsvpcConfiguration: &ecs.Service_AwsVpcConfiguration{ AssignPublicIp: ecsapi.AssignPublicIpEnabled, @@ -305,6 +293,32 @@ func (c client) Convert(project *compose.Project) (*cloudformation.Template, err TaskDefinition: cloudformation.Ref(normalizeResourceName(taskDefinition)), } } + + // create LBs + for lbType, lbSecGroups := range loadBalancers { + loadBalancerName := fmt.Sprintf( + "%s%sLB", + strings.Title(project.Name), + strings.ToUpper(lbType[0:1]), + ) + + template.Resources[loadBalancerName] = &elasticloadbalancingv2.LoadBalancer{ + Name: loadBalancerName, + Scheme: "internet-facing", + SecurityGroups: lbSecGroups, + Subnets: []string{ + cloudformation.Ref(ParameterSubnet1Id), + cloudformation.Ref(ParameterSubnet2Id), + }, + Tags: []tags.Tag{ + { + Key: ProjectTag, + Value: project.Name, + }, + }, + Type: lbType, + } + } return template, nil } From 335806a17961eab352d6a33267b80b718c6e70a1 Mon Sep 17 00:00:00 2001 From: aiordache <anca.iordache@docker.com> Date: Thu, 4 Jun 2020 10:26:15 +0200 Subject: [PATCH 098/198] create only one global load balancer - error out if exports port require different types Signed-off-by: aiordache <anca.iordache@docker.com> Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com> --- ecs/pkg/amazon/cloudformation.go | 72 ++++++++++++++++++-------------- 1 file changed, 40 insertions(+), 32 deletions(-) diff --git a/ecs/pkg/amazon/cloudformation.go b/ecs/pkg/amazon/cloudformation.go index d66bda509..4da59d534 100644 --- a/ecs/pkg/amazon/cloudformation.go +++ b/ecs/pkg/amazon/cloudformation.go @@ -97,8 +97,8 @@ func (c client) Convert(project *compose.Project) (*cloudformation.Template, err Name: fmt.Sprintf("%s.local", project.Name), Vpc: cloudformation.Ref(ParameterVPCId), } - //map LB type to security groups list - loadBalancers := map[string][]string{} + + var loadBalancer *elasticloadbalancingv2.LoadBalancer for _, service := range project.Services { definition, err := Convert(project, service) @@ -193,11 +193,33 @@ func (c client) Convert(project *compose.Project) (*cloudformation.Template, err strings.Title(project.Name), strings.ToUpper(loadBalancerType[0:1]), ) - // create load balancer if it doesn't exist - if _, ok := loadBalancers[loadBalancerType]; !ok { - loadBalancers[loadBalancerType] = []string{} + // create load balancer if it doesn't exist -- global load balancer + if loadBalancer == nil { + + loadBalancer = &elasticloadbalancingv2.LoadBalancer{ + Name: loadBalancerName, + Scheme: "internet-facing", + SecurityGroups: loadBalancerSecGroups, + Subnets: []string{ + cloudformation.Ref(ParameterSubnet1Id), + cloudformation.Ref(ParameterSubnet2Id), + }, + Tags: []tags.Tag{ + { + Key: ProjectTag, + Value: project.Name, + }, + }, + Type: loadBalancerType, + } + template.Resources[loadBalancerName] = loadBalancer } - loadBalancers[loadBalancerType] = append(loadBalancers[loadBalancerType], loadBalancerSecGroups...) + if loadBalancer.Type != loadBalancerType { + return nil, fmt.Errorf( + "exposed ports require different types of load balancers, only one type is permitted") + } + loadBalancer.SecurityGroups = append(loadBalancer.SecurityGroups, loadBalancerSecGroups...) + loadBalancer.SecurityGroups = uniqueLBSecurityGroups(loadBalancer.SecurityGroups) targetGroupName := fmt.Sprintf( "%s%s%sTargetGroup", @@ -293,32 +315,6 @@ func (c client) Convert(project *compose.Project) (*cloudformation.Template, err TaskDefinition: cloudformation.Ref(normalizeResourceName(taskDefinition)), } } - - // create LBs - for lbType, lbSecGroups := range loadBalancers { - loadBalancerName := fmt.Sprintf( - "%s%sLB", - strings.Title(project.Name), - strings.ToUpper(lbType[0:1]), - ) - - template.Resources[loadBalancerName] = &elasticloadbalancingv2.LoadBalancer{ - Name: loadBalancerName, - Scheme: "internet-facing", - SecurityGroups: lbSecGroups, - Subnets: []string{ - cloudformation.Ref(ParameterSubnet1Id), - cloudformation.Ref(ParameterSubnet2Id), - }, - Tags: []tags.Tag{ - { - Key: ProjectTag, - Value: project.Name, - }, - }, - Type: lbType, - } - } return template, nil } @@ -412,3 +408,15 @@ func (c client) getPolicy(taskDef *ecs.TaskDefinition) (*PolicyDocument, error) } return nil, nil } + +func uniqueLBSecurityGroups(groups []string) []string { + keys := make(map[string]bool) + unique := []string{} + for _, k := range groups { + if _, val := keys[k]; !val { + keys[k] = true + unique = append(unique, k) + } + } + return unique +} From eddaa70a9efb9aa3befc951eb84041b66fc6bd74 Mon Sep 17 00:00:00 2001 From: aiordache <anca.iordache@docker.com> Date: Thu, 4 Jun 2020 11:17:35 +0200 Subject: [PATCH 099/198] create NLB load balancer only Signed-off-by: aiordache <anca.iordache@docker.com> Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com> --- ecs/pkg/amazon/cloudformation.go | 89 +++++++++----------------------- 1 file changed, 23 insertions(+), 66 deletions(-) diff --git a/ecs/pkg/amazon/cloudformation.go b/ecs/pkg/amazon/cloudformation.go index 4da59d534..98970753c 100644 --- a/ecs/pkg/amazon/cloudformation.go +++ b/ecs/pkg/amazon/cloudformation.go @@ -98,7 +98,28 @@ func (c client) Convert(project *compose.Project) (*cloudformation.Template, err Vpc: cloudformation.Ref(ParameterVPCId), } - var loadBalancer *elasticloadbalancingv2.LoadBalancer + loadBalancerType := "network" + loadBalancerName := fmt.Sprintf( + "%s%sLB", + strings.Title(project.Name), + strings.ToUpper(loadBalancerType[0:1]), + ) + loadBalancer := &elasticloadbalancingv2.LoadBalancer{ + Name: loadBalancerName, + Scheme: "internet-facing", + Subnets: []string{ + cloudformation.Ref(ParameterSubnet1Id), + cloudformation.Ref(ParameterSubnet2Id), + }, + Tags: []tags.Tag{ + { + Key: ProjectTag, + Value: project.Name, + }, + }, + Type: loadBalancerType, + } + template.Resources[loadBalancerName] = loadBalancer for _, service := range project.Services { definition, err := Convert(project, service) @@ -142,12 +163,6 @@ func (c client) Convert(project *compose.Project) (*cloudformation.Template, err RegistryArn: cloudformation.GetAtt(serviceRegistration, "Arn"), } - serviceSecurityGroups := []string{} - for net := range service.Networks { - logicalName := networkResourceName(project, net) - serviceSecurityGroups = append(serviceSecurityGroups, cloudformation.Ref(logicalName)) - } - template.Resources[serviceRegistration] = &cloudmap.Service{ Description: fmt.Sprintf("%q service discovery entry in Cloud Map", service.Name), HealthCheckConfig: healthCheck, @@ -173,54 +188,8 @@ func (c client) Convert(project *compose.Project) (*cloudformation.Template, err serviceLB := []ecs.Service_LoadBalancer{} if len(service.Ports) > 0 { for _, port := range service.Ports { - loadBalancerType := "network" protocolType := strings.ToUpper(port.Protocol) - targetType := elbv2.TargetTypeEnumInstance - loadBalancerSecGroups := []string{} - - if port.Published == 80 || port.Published == 443 { - loadBalancerType = "application" - loadBalancerSecGroups = serviceSecurityGroups - protocolType = "HTTPS" - targetType = elbv2.TargetTypeEnumIp - if port.Published == 80 { - protocolType = "HTTP" - } - } - loadBalancerName := fmt.Sprintf( - "%s%sLB", - strings.Title(project.Name), - strings.ToUpper(loadBalancerType[0:1]), - ) - // create load balancer if it doesn't exist -- global load balancer - if loadBalancer == nil { - - loadBalancer = &elasticloadbalancingv2.LoadBalancer{ - Name: loadBalancerName, - Scheme: "internet-facing", - SecurityGroups: loadBalancerSecGroups, - Subnets: []string{ - cloudformation.Ref(ParameterSubnet1Id), - cloudformation.Ref(ParameterSubnet2Id), - }, - Tags: []tags.Tag{ - { - Key: ProjectTag, - Value: project.Name, - }, - }, - Type: loadBalancerType, - } - template.Resources[loadBalancerName] = loadBalancer - } - if loadBalancer.Type != loadBalancerType { - return nil, fmt.Errorf( - "exposed ports require different types of load balancers, only one type is permitted") - } - loadBalancer.SecurityGroups = append(loadBalancer.SecurityGroups, loadBalancerSecGroups...) - loadBalancer.SecurityGroups = uniqueLBSecurityGroups(loadBalancer.SecurityGroups) - targetGroupName := fmt.Sprintf( "%s%s%sTargetGroup", normalizeResourceName(service.Name), @@ -238,7 +207,7 @@ func (c client) Convert(project *compose.Project) (*cloudformation.Template, err }, }, VpcId: cloudformation.Ref(ParameterVPCId), - TargetType: targetType, + TargetType: elbv2.TargetTypeEnumIp, } listenerName := fmt.Sprintf( "%s%s%sListener", @@ -408,15 +377,3 @@ func (c client) getPolicy(taskDef *ecs.TaskDefinition) (*PolicyDocument, error) } return nil, nil } - -func uniqueLBSecurityGroups(groups []string) []string { - keys := make(map[string]bool) - unique := []string{} - for _, k := range groups { - if _, val := keys[k]; !val { - keys[k] = true - unique = append(unique, k) - } - } - return unique -} From f71109be9e24d1231887ac4af7827fbd60ca4bb3 Mon Sep 17 00:00:00 2001 From: aiordache <anca.iordache@docker.com> Date: Thu, 4 Jun 2020 11:39:47 +0200 Subject: [PATCH 100/198] update testdata Signed-off-by: aiordache <anca.iordache@docker.com> Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com> --- .../simple-cloudformation-conversion.golden | 22 +++++++++++++++++++ ...formation-with-overrides-conversion.golden | 22 +++++++++++++++++++ 2 files changed, 44 insertions(+) diff --git a/ecs/pkg/amazon/testdata/simple/simple-cloudformation-conversion.golden b/ecs/pkg/amazon/testdata/simple/simple-cloudformation-conversion.golden index 8a95637a6..258fa960d 100644 --- a/ecs/pkg/amazon/testdata/simple/simple-cloudformation-conversion.golden +++ b/ecs/pkg/amazon/testdata/simple/simple-cloudformation-conversion.golden @@ -245,6 +245,28 @@ } }, "Type": "AWS::EC2::SecurityGroupIngress" + }, + "TestSimpleConvertNLB": { + "Properties": { + "Name": "TestSimpleConvertNLB", + "Scheme": "internet-facing", + "Subnets": [ + { + "Ref": "ParameterSubnet1Id" + }, + { + "Ref": "ParameterSubnet2Id" + } + ], + "Tags": [ + { + "Key": "com.docker.compose.project", + "Value": "TestSimpleConvert" + } + ], + "Type": "network" + }, + "Type": "AWS::ElasticLoadBalancingV2::LoadBalancer" } } } diff --git a/ecs/pkg/amazon/testdata/simple/simple-cloudformation-with-overrides-conversion.golden b/ecs/pkg/amazon/testdata/simple/simple-cloudformation-with-overrides-conversion.golden index a5551ddcb..991f65b6b 100644 --- a/ecs/pkg/amazon/testdata/simple/simple-cloudformation-with-overrides-conversion.golden +++ b/ecs/pkg/amazon/testdata/simple/simple-cloudformation-with-overrides-conversion.golden @@ -245,6 +245,28 @@ } }, "Type": "AWS::EC2::SecurityGroupIngress" + }, + "TestSimpleWithOverridesNLB": { + "Properties": { + "Name": "TestSimpleWithOverridesNLB", + "Scheme": "internet-facing", + "Subnets": [ + { + "Ref": "ParameterSubnet1Id" + }, + { + "Ref": "ParameterSubnet2Id" + } + ], + "Tags": [ + { + "Key": "com.docker.compose.project", + "Value": "TestSimpleWithOverrides" + } + ], + "Type": "network" + }, + "Type": "AWS::ElasticLoadBalancingV2::LoadBalancer" } } } From 37177e6d7a63c6dc69e326674f5c0ef5793691b5 Mon Sep 17 00:00:00 2001 From: Nicolas De Loof <nicolas.deloof@gmail.com> Date: Thu, 4 Jun 2020 16:10:34 +0200 Subject: [PATCH 101/198] Split long `Convert` func into smaller, focussed sub-func Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com> --- ecs/pkg/amazon/cloudformation.go | 284 +++++++++++++++++-------------- 1 file changed, 159 insertions(+), 125 deletions(-) diff --git a/ecs/pkg/amazon/cloudformation.go b/ecs/pkg/amazon/cloudformation.go index 98970753c..0e6966bb1 100644 --- a/ecs/pkg/amazon/cloudformation.go +++ b/ecs/pkg/amazon/cloudformation.go @@ -69,17 +69,7 @@ func (c client) Convert(project *compose.Project) (*cloudformation.Template, err // Create Cluster is `ParameterClusterName` parameter is not set template.Conditions["CreateCluster"] = cloudformation.Equals("", cloudformation.Ref(ParameterClusterName)) - template.Resources["Cluster"] = &ecs.Cluster{ - ClusterName: project.Name, - Tags: []tags.Tag{ - { - Key: ProjectTag, - Value: project.Name, - }, - }, - AWSCloudFormationCondition: "CreateCluster", - } - cluster := cloudformation.If("CreateCluster", cloudformation.Ref("Cluster"), cloudformation.Ref(ParameterClusterName)) + cluster := c.createCluster(project, template) networks := map[string]string{} for _, net := range project.Networks { @@ -92,34 +82,8 @@ func (c client) Convert(project *compose.Project) (*cloudformation.Template, err } // Private DNS namespace will allow DNS name for the services to be <service>.<project>.local - template.Resources["CloudMap"] = &cloudmap.PrivateDnsNamespace{ - Description: fmt.Sprintf("Service Map for Docker Compose project %s", project.Name), - Name: fmt.Sprintf("%s.local", project.Name), - Vpc: cloudformation.Ref(ParameterVPCId), - } - - loadBalancerType := "network" - loadBalancerName := fmt.Sprintf( - "%s%sLB", - strings.Title(project.Name), - strings.ToUpper(loadBalancerType[0:1]), - ) - loadBalancer := &elasticloadbalancingv2.LoadBalancer{ - Name: loadBalancerName, - Scheme: "internet-facing", - Subnets: []string{ - cloudformation.Ref(ParameterSubnet1Id), - cloudformation.Ref(ParameterSubnet2Id), - }, - Tags: []tags.Tag{ - { - Key: ProjectTag, - Value: project.Name, - }, - }, - Type: loadBalancerType, - } - template.Resources[loadBalancerName] = loadBalancer + c.createCloudMap(project, template) + loadBalancer := c.createLoadBalancer(project, template) for _, service := range project.Services { definition, err := Convert(project, service) @@ -127,30 +91,13 @@ func (c client) Convert(project *compose.Project) (*cloudformation.Template, err return nil, err } - taskExecutionRole := fmt.Sprintf("%sTaskExecutionRole", normalizeResourceName(service.Name)) - policy, err := c.getPolicy(definition) + taskExecutionRole, err := c.createTaskExecutionRole(service, err, definition, template) if err != nil { - return nil, err - } - rolePolicies := []iam.Role_Policy{} - if policy != nil { - rolePolicies = append(rolePolicies, iam.Role_Policy{ - PolicyDocument: policy, - PolicyName: fmt.Sprintf("%sGrantAccessToSecrets", service.Name), - }) - + return template, err } definition.ExecutionRoleArn = cloudformation.Ref(taskExecutionRole) taskDefinition := fmt.Sprintf("%sTaskDefinition", normalizeResourceName(service.Name)) - template.Resources[taskExecutionRole] = &iam.Role{ - AssumeRolePolicyDocument: assumeRolePolicyDocument, - Policies: rolePolicies, - ManagedPolicyArns: []string{ - ECSTaskExecutionPolicy, - ECRReadOnlyPolicy, - }, - } template.Resources[taskDefinition] = definition var healthCheck *cloudmap.Service_HealthCheckConfig @@ -158,26 +105,7 @@ func (c client) Convert(project *compose.Project) (*cloudformation.Template, err // FIXME ECS only support HTTP(s) health checks, while Docker only support CMD } - serviceRegistration := fmt.Sprintf("%sServiceDiscoveryEntry", normalizeResourceName(service.Name)) - serviceRegistry := ecs.Service_ServiceRegistry{ - RegistryArn: cloudformation.GetAtt(serviceRegistration, "Arn"), - } - - template.Resources[serviceRegistration] = &cloudmap.Service{ - Description: fmt.Sprintf("%q service discovery entry in Cloud Map", service.Name), - HealthCheckConfig: healthCheck, - Name: service.Name, - NamespaceId: cloudformation.Ref("CloudMap"), - DnsConfig: &cloudmap.Service_DnsConfig{ - DnsRecords: []cloudmap.Service_DnsRecord{ - { - TTL: 60, - Type: cloudmapapi.RecordTypeA, - }, - }, - RoutingPolicy: cloudmapapi.RoutingPolicyMultivalue, - }, - } + serviceRegistry := c.createServiceRegistry(service, template, healthCheck) serviceSecurityGroups := []string{} for net := range service.Networks { @@ -188,54 +116,10 @@ func (c client) Convert(project *compose.Project) (*cloudformation.Template, err serviceLB := []ecs.Service_LoadBalancer{} if len(service.Ports) > 0 { for _, port := range service.Ports { - - protocolType := strings.ToUpper(port.Protocol) - targetGroupName := fmt.Sprintf( - "%s%s%sTargetGroup", - normalizeResourceName(service.Name), - strings.ToUpper(port.Protocol), - string(port.Published), - ) - template.Resources[targetGroupName] = &elasticloadbalancingv2.TargetGroup{ - Name: targetGroupName, - Port: int(port.Target), - Protocol: protocolType, - Tags: []tags.Tag{ - { - Key: ProjectTag, - Value: project.Name, - }, - }, - VpcId: cloudformation.Ref(ParameterVPCId), - TargetType: elbv2.TargetTypeEnumIp, - } - listenerName := fmt.Sprintf( - "%s%s%sListener", - normalizeResourceName(service.Name), - strings.ToUpper(port.Protocol), - string(port.Published), - ) - //add listener to dependsOn - //https://stackoverflow.com/questions/53971873/the-target-group-does-not-have-an-associated-load-balancer + protocol := strings.ToUpper(port.Protocol) + targetGroupName := c.createTargetGroup(project, service, port, template, protocol) + listenerName := c.createListener(service, port, template, targetGroupName, loadBalancer, protocol) dependsOn = append(dependsOn, listenerName) - template.Resources[listenerName] = &elasticloadbalancingv2.Listener{ - DefaultActions: []elasticloadbalancingv2.Listener_Action{ - { - ForwardConfig: &elasticloadbalancingv2.Listener_ForwardConfig{ - TargetGroups: []elasticloadbalancingv2.Listener_TargetGroupTuple{ - { - TargetGroupArn: cloudformation.Ref(targetGroupName), - }, - }, - }, - Type: elbv2.ActionTypeEnumForward, - }, - }, - LoadBalancerArn: cloudformation.Ref(loadBalancerName), - Protocol: protocolType, - Port: int(port.Published), - } - serviceLB = append(serviceLB, ecs.Service_LoadBalancer{ ContainerName: service.Name, ContainerPort: int(port.Published), @@ -287,6 +171,156 @@ func (c client) Convert(project *compose.Project) (*cloudformation.Template, err return template, nil } +func (c client) createLoadBalancer(project *compose.Project, template *cloudformation.Template) string { + loadBalancerType := "network" + loadBalancerName := fmt.Sprintf( + "%s%sLB", + strings.Title(project.Name), + strings.ToUpper(loadBalancerType[0:1]), + ) + loadBalancer := &elasticloadbalancingv2.LoadBalancer{ + Name: loadBalancerName, + Scheme: "internet-facing", + Subnets: []string{ + cloudformation.Ref(ParameterSubnet1Id), + cloudformation.Ref(ParameterSubnet2Id), + }, + Tags: []tags.Tag{ + { + Key: ProjectTag, + Value: project.Name, + }, + }, + Type: loadBalancerType, + } + template.Resources[loadBalancerName] = loadBalancer + return loadBalancerName +} + +func (c client) createListener(service types.ServiceConfig, port types.ServicePortConfig, template *cloudformation.Template, targetGroupName string, loadBalancerName string, protocol string) string { + listenerName := fmt.Sprintf( + "%s%s%sListener", + normalizeResourceName(service.Name), + strings.ToUpper(port.Protocol), + string(port.Published), + ) + //add listener to dependsOn + //https://stackoverflow.com/questions/53971873/the-target-group-does-not-have-an-associated-load-balancer + template.Resources[listenerName] = &elasticloadbalancingv2.Listener{ + DefaultActions: []elasticloadbalancingv2.Listener_Action{ + { + ForwardConfig: &elasticloadbalancingv2.Listener_ForwardConfig{ + TargetGroups: []elasticloadbalancingv2.Listener_TargetGroupTuple{ + { + TargetGroupArn: cloudformation.Ref(targetGroupName), + }, + }, + }, + Type: elbv2.ActionTypeEnumForward, + }, + }, + LoadBalancerArn: cloudformation.Ref(loadBalancerName), + Protocol: protocol, + Port: int(port.Published), + } + return listenerName +} + +func (c client) createTargetGroup(project *compose.Project, service types.ServiceConfig, port types.ServicePortConfig, template *cloudformation.Template, protocol string) string { + targetGroupName := fmt.Sprintf( + "%s%s%sTargetGroup", + normalizeResourceName(service.Name), + strings.ToUpper(port.Protocol), + string(port.Published), + ) + template.Resources[targetGroupName] = &elasticloadbalancingv2.TargetGroup{ + Name: targetGroupName, + Port: int(port.Target), + Protocol: protocol, + Tags: []tags.Tag{ + { + Key: ProjectTag, + Value: project.Name, + }, + }, + VpcId: cloudformation.Ref(ParameterVPCId), + TargetType: elbv2.TargetTypeEnumIp, + } + return targetGroupName +} + +func (c client) createServiceRegistry(service types.ServiceConfig, template *cloudformation.Template, healthCheck *cloudmap.Service_HealthCheckConfig) ecs.Service_ServiceRegistry { + serviceRegistration := fmt.Sprintf("%sServiceDiscoveryEntry", normalizeResourceName(service.Name)) + serviceRegistry := ecs.Service_ServiceRegistry{ + RegistryArn: cloudformation.GetAtt(serviceRegistration, "Arn"), + } + + template.Resources[serviceRegistration] = &cloudmap.Service{ + Description: fmt.Sprintf("%q service discovery entry in Cloud Map", service.Name), + HealthCheckConfig: healthCheck, + Name: service.Name, + NamespaceId: cloudformation.Ref("CloudMap"), + DnsConfig: &cloudmap.Service_DnsConfig{ + DnsRecords: []cloudmap.Service_DnsRecord{ + { + TTL: 60, + Type: cloudmapapi.RecordTypeA, + }, + }, + RoutingPolicy: cloudmapapi.RoutingPolicyMultivalue, + }, + } + return serviceRegistry +} + +func (c client) createTaskExecutionRole(service types.ServiceConfig, err error, definition *ecs.TaskDefinition, template *cloudformation.Template) (string, error) { + taskExecutionRole := fmt.Sprintf("%sTaskExecutionRole", normalizeResourceName(service.Name)) + policy, err := c.getPolicy(definition) + if err != nil { + return taskExecutionRole, err + } + rolePolicies := []iam.Role_Policy{} + if policy != nil { + rolePolicies = append(rolePolicies, iam.Role_Policy{ + PolicyDocument: policy, + PolicyName: fmt.Sprintf("%sGrantAccessToSecrets", service.Name), + }) + + } + template.Resources[taskExecutionRole] = &iam.Role{ + AssumeRolePolicyDocument: assumeRolePolicyDocument, + Policies: rolePolicies, + ManagedPolicyArns: []string{ + ECSTaskExecutionPolicy, + ECRReadOnlyPolicy, + }, + } + return taskExecutionRole, nil +} + +func (c client) createCluster(project *compose.Project, template *cloudformation.Template) string { + template.Resources["Cluster"] = &ecs.Cluster{ + ClusterName: project.Name, + Tags: []tags.Tag{ + { + Key: ProjectTag, + Value: project.Name, + }, + }, + AWSCloudFormationCondition: "CreateCluster", + } + cluster := cloudformation.If("CreateCluster", cloudformation.Ref("Cluster"), cloudformation.Ref(ParameterClusterName)) + return cluster +} + +func (c client) createCloudMap(project *compose.Project, template *cloudformation.Template) { + template.Resources["CloudMap"] = &cloudmap.PrivateDnsNamespace{ + Description: fmt.Sprintf("Service Map for Docker Compose project %s", project.Name), + Name: fmt.Sprintf("%s.local", project.Name), + Vpc: cloudformation.Ref(ParameterVPCId), + } +} + func convertNetwork(project *compose.Project, net types.NetworkConfig, vpc string, template *cloudformation.Template) string { if sg, ok := net.Extras[ExtensionSecurityGroup]; ok { logrus.Debugf("Security Group for network %q set by user to %q", net.Name, sg) From fbb5bdac6ee132ce6565a7eefd77894f65d3a6c5 Mon Sep 17 00:00:00 2001 From: Nicolas De Loof <nicolas.deloof@gmail.com> Date: Thu, 4 Jun 2020 16:20:21 +0200 Subject: [PATCH 102/198] Fix resource naming Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com> --- ecs/pkg/amazon/cloudformation.go | 22 ++++++++-------------- ecs/pkg/compose/normalize.go | 7 +++++++ 2 files changed, 15 insertions(+), 14 deletions(-) diff --git a/ecs/pkg/amazon/cloudformation.go b/ecs/pkg/amazon/cloudformation.go index 0e6966bb1..35d2e123e 100644 --- a/ecs/pkg/amazon/cloudformation.go +++ b/ecs/pkg/amazon/cloudformation.go @@ -172,15 +172,10 @@ func (c client) Convert(project *compose.Project) (*cloudformation.Template, err } func (c client) createLoadBalancer(project *compose.Project, template *cloudformation.Template) string { - loadBalancerType := "network" - loadBalancerName := fmt.Sprintf( - "%s%sLB", - strings.Title(project.Name), - strings.ToUpper(loadBalancerType[0:1]), - ) - loadBalancer := &elasticloadbalancingv2.LoadBalancer{ + loadBalancerName := fmt.Sprintf("%sLoadBalancer", strings.Title(project.Name)) + template.Resources[loadBalancerName] = &elasticloadbalancingv2.LoadBalancer{ Name: loadBalancerName, - Scheme: "internet-facing", + Scheme: elbv2.LoadBalancerSchemeEnumInternetFacing, Subnets: []string{ cloudformation.Ref(ParameterSubnet1Id), cloudformation.Ref(ParameterSubnet2Id), @@ -191,18 +186,17 @@ func (c client) createLoadBalancer(project *compose.Project, template *cloudform Value: project.Name, }, }, - Type: loadBalancerType, + Type: elbv2.LoadBalancerTypeEnumNetwork, } - template.Resources[loadBalancerName] = loadBalancer return loadBalancerName } func (c client) createListener(service types.ServiceConfig, port types.ServicePortConfig, template *cloudformation.Template, targetGroupName string, loadBalancerName string, protocol string) string { listenerName := fmt.Sprintf( - "%s%s%sListener", + "%s%s%dListener", normalizeResourceName(service.Name), strings.ToUpper(port.Protocol), - string(port.Published), + port.Published, ) //add listener to dependsOn //https://stackoverflow.com/questions/53971873/the-target-group-does-not-have-an-associated-load-balancer @@ -228,10 +222,10 @@ func (c client) createListener(service types.ServiceConfig, port types.ServicePo func (c client) createTargetGroup(project *compose.Project, service types.ServiceConfig, port types.ServicePortConfig, template *cloudformation.Template, protocol string) string { targetGroupName := fmt.Sprintf( - "%s%s%sTargetGroup", + "%s%s%dTargetGroup", normalizeResourceName(service.Name), strings.ToUpper(port.Protocol), - string(port.Published), + port.Published, ) template.Resources[targetGroupName] = &elasticloadbalancingv2.TargetGroup{ Name: targetGroupName, diff --git a/ecs/pkg/compose/normalize.go b/ecs/pkg/compose/normalize.go index 861d146f3..3e4809d1e 100644 --- a/ecs/pkg/compose/normalize.go +++ b/ecs/pkg/compose/normalize.go @@ -23,6 +23,13 @@ func Normalize(model *types.Config) error { s.Networks = map[string]*types.ServiceNetworkConfig{"default": nil} } + for i, p := range s.Ports { + if p.Published == 0 { + p.Published = p.Target + s.Ports[i] = p + } + } + if s.LogDriver != "" { logrus.Warn("`log_driver` is deprecated. Use the `logging` attribute") if s.Logging == nil { From 3194cc9b1617224e3d486cda02a27d2682c59951 Mon Sep 17 00:00:00 2001 From: aiordache <anca.iordache@docker.com> Date: Thu, 4 Jun 2020 16:28:11 +0200 Subject: [PATCH 103/198] allow user defined LB Signed-off-by: aiordache <anca.iordache@docker.com> Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com> --- ecs/pkg/amazon/cloudformation.go | 41 +++++++++++++++++++++++--------- ecs/pkg/amazon/sdk.go | 22 +++++++++++++++++ ecs/pkg/amazon/up.go | 33 +++++++++++++++++++++---- ecs/pkg/amazon/x.go | 1 + 4 files changed, 82 insertions(+), 15 deletions(-) diff --git a/ecs/pkg/amazon/cloudformation.go b/ecs/pkg/amazon/cloudformation.go index 35d2e123e..68f6b3443 100644 --- a/ecs/pkg/amazon/cloudformation.go +++ b/ecs/pkg/amazon/cloudformation.go @@ -25,10 +25,11 @@ import ( ) const ( - ParameterClusterName = "ParameterClusterName" - ParameterVPCId = "ParameterVPCId" - ParameterSubnet1Id = "ParameterSubnet1Id" - ParameterSubnet2Id = "ParameterSubnet2Id" + ParameterClusterName = "ParameterClusterName" + ParameterVPCId = "ParameterVPCId" + ParameterSubnet1Id = "ParameterSubnet1Id" + ParameterSubnet2Id = "ParameterSubnet2Id" + ParameterLoadBalancerARN = "ParameterLoadBalancerARN" ) // Convert a compose project into a CloudFormation template @@ -66,6 +67,11 @@ func (c client) Convert(project *compose.Project) (*cloudformation.Template, err Description: "SubnetId, for Availability Zone 2 in the region in your VPC", } + template.Parameters[ParameterLoadBalancerARN] = cloudformation.Parameter{ + Type: "String", + Description: "Name of the LoadBalancer to connect to (optional)", + } + // Create Cluster is `ParameterClusterName` parameter is not set template.Conditions["CreateCluster"] = cloudformation.Equals("", cloudformation.Ref(ParameterClusterName)) @@ -172,10 +178,19 @@ func (c client) Convert(project *compose.Project) (*cloudformation.Template, err } func (c client) createLoadBalancer(project *compose.Project, template *cloudformation.Template) string { - loadBalancerName := fmt.Sprintf("%sLoadBalancer", strings.Title(project.Name)) - template.Resources[loadBalancerName] = &elasticloadbalancingv2.LoadBalancer{ + + loadBalancerType := "network" + loadBalancerName := fmt.Sprintf( + "%s%sLB", + strings.Title(project.Name), + strings.ToUpper(loadBalancerType[0:1]), + ) + // Create LoadBalancer if `ParameterLoadBalancerName` is not set + template.Conditions["CreateLoadBalancer"] = cloudformation.Equals("", cloudformation.Ref(ParameterLoadBalancerARN)) + + loadBalancer := &elasticloadbalancingv2.LoadBalancer{ Name: loadBalancerName, - Scheme: elbv2.LoadBalancerSchemeEnumInternetFacing, + Scheme: "internet-facing", Subnets: []string{ cloudformation.Ref(ParameterSubnet1Id), cloudformation.Ref(ParameterSubnet2Id), @@ -186,12 +201,16 @@ func (c client) createLoadBalancer(project *compose.Project, template *cloudform Value: project.Name, }, }, - Type: elbv2.LoadBalancerTypeEnumNetwork, + Type: loadBalancerType, + AWSCloudFormationCondition: "CreateLoadBalancer", } - return loadBalancerName + template.Resources[loadBalancerName] = loadBalancer + loadBalancerRef := cloudformation.If("CreateLoadBalancer", cloudformation.Ref(loadBalancerName), cloudformation.Ref(ParameterLoadBalancerARN)) + + return loadBalancerRef } -func (c client) createListener(service types.ServiceConfig, port types.ServicePortConfig, template *cloudformation.Template, targetGroupName string, loadBalancerName string, protocol string) string { +func (c client) createListener(service types.ServiceConfig, port types.ServicePortConfig, template *cloudformation.Template, targetGroupName string, loadBalancerARN string, protocol string) string { listenerName := fmt.Sprintf( "%s%s%dListener", normalizeResourceName(service.Name), @@ -213,7 +232,7 @@ func (c client) createListener(service types.ServiceConfig, port types.ServicePo Type: elbv2.ActionTypeEnumForward, }, }, - LoadBalancerArn: cloudformation.Ref(loadBalancerName), + LoadBalancerArn: loadBalancerARN, Protocol: protocol, Port: int(port.Published), } diff --git a/ecs/pkg/amazon/sdk.go b/ecs/pkg/amazon/sdk.go index 6bc85381b..e074fb11d 100644 --- a/ecs/pkg/amazon/sdk.go +++ b/ecs/pkg/amazon/sdk.go @@ -400,3 +400,25 @@ func (s sdk) GetPublicIPs(ctx context.Context, interfaces ...string) (map[string } return publicIPs, nil } + +func (s sdk) LoadBalancerExists(ctx context.Context, name string) (bool, error) { + logrus.Debug("Check if cluster was already created: ", name) + lbs, err := s.ELB.DescribeLoadBalancersWithContext(ctx, &elbv2.DescribeLoadBalancersInput{ + Names: []*string{aws.String(name)}, + }) + if err != nil { + return false, err + } + return len(lbs.LoadBalancers) > 0, nil +} + +func (s sdk) GetLoadBalancerARN(ctx context.Context, name string) (string, error) { + logrus.Debug("Check if cluster was already created: ", name) + lbs, err := s.ELB.DescribeLoadBalancersWithContext(ctx, &elbv2.DescribeLoadBalancersInput{ + Names: []*string{aws.String(name)}, + }) + if err != nil { + return "", err + } + return *lbs.LoadBalancers[0].LoadBalancerArn, nil +} diff --git a/ecs/pkg/amazon/up.go b/ecs/pkg/amazon/up.go index 21500c381..3c9477247 100644 --- a/ecs/pkg/amazon/up.go +++ b/ecs/pkg/amazon/up.go @@ -42,11 +42,17 @@ func (c *client) ComposeUp(ctx context.Context, project *compose.Project) error return err } + lb, err := c.GetLoadBalancer(ctx, project) + if err != nil { + return err + } + parameters := map[string]string{ - ParameterClusterName: c.Cluster, - ParameterVPCId: vpc, - ParameterSubnet1Id: subNets[0], - ParameterSubnet2Id: subNets[1], + ParameterClusterName: c.Cluster, + ParameterVPCId: vpc, + ParameterSubnet1Id: subNets[0], + ParameterSubnet2Id: subNets[1], + ParameterLoadBalancerARN: lb, } err = c.api.CreateStack(ctx, project.Name, template, parameters) @@ -77,6 +83,22 @@ func (c client) GetVPC(ctx context.Context, project *compose.Project) (string, e return defaultVPC, nil } +func (c client) GetLoadBalancer(ctx context.Context, project *compose.Project) (string, error) { + //check compose file for custom VPC selected + if lb, ok := project.Extras[ExtensionLB]; ok { + lbName := lb.(string) + ok, err := c.api.LoadBalancerExists(ctx, lbName) + if err != nil { + return "", err + } + if !ok { + return "", fmt.Errorf("Load Balancer does not exist: %s", lb) + } + return c.api.GetLoadBalancerARN(ctx, lbName) + } + return "", nil +} + type upAPI interface { waitAPI GetDefaultVPC(ctx context.Context) (string, error) @@ -86,4 +108,7 @@ type upAPI interface { ClusterExists(ctx context.Context, name string) (bool, error) StackExists(ctx context.Context, name string) (bool, error) CreateStack(ctx context.Context, name string, template *cloudformation.Template, parameters map[string]string) error + + LoadBalancerExists(ctx context.Context, name string) (bool, error) + GetLoadBalancerARN(ctx context.Context, name string) (string, error) } diff --git a/ecs/pkg/amazon/x.go b/ecs/pkg/amazon/x.go index 0b022bac2..b16c3d253 100644 --- a/ecs/pkg/amazon/x.go +++ b/ecs/pkg/amazon/x.go @@ -4,4 +4,5 @@ const ( ExtensionSecurityGroup = "x-aws-securitygroup" ExtensionVPC = "x-aws-vpc" ExtensionPullCredentials = "x-aws-pull_credentials" + ExtensionLB = "x-aws-lb" ) From 2ea694a1c5b7331e8c3820cae9ad9c876ca5e5a6 Mon Sep 17 00:00:00 2001 From: aiordache <anca.iordache@docker.com> Date: Thu, 4 Jun 2020 17:09:41 +0200 Subject: [PATCH 104/198] update test data Signed-off-by: aiordache <anca.iordache@docker.com> Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com> --- ecs/pkg/amazon/api_mock.go | 27 ++++++++++++++++++- .../simple-cloudformation-conversion.golden | 13 +++++++++ ...formation-with-overrides-conversion.golden | 13 +++++++++ 3 files changed, 52 insertions(+), 1 deletion(-) diff --git a/ecs/pkg/amazon/api_mock.go b/ecs/pkg/amazon/api_mock.go index 40e9f6872..cbf2b6d17 100644 --- a/ecs/pkg/amazon/api_mock.go +++ b/ecs/pkg/amazon/api_mock.go @@ -6,11 +6,12 @@ package amazon import ( context "context" + reflect "reflect" + cloudformation "github.com/aws/aws-sdk-go/service/cloudformation" cloudformation0 "github.com/awslabs/goformation/v4/cloudformation" docker "github.com/docker/ecs-plugin/pkg/docker" gomock "github.com/golang/mock/gomock" - reflect "reflect" ) // MockAPI is a mock of API interface @@ -324,3 +325,27 @@ func (mr *MockAPIMockRecorder) WaitStackComplete(arg0, arg1, arg2 interface{}) * mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "WaitStackComplete", reflect.TypeOf((*MockAPI)(nil).WaitStackComplete), arg0, arg1, arg2) } + +// LoadBalancerExists mocks base method +func (m *MockAPI) LoadBalancerExists(arg0 context.Context, arg1 string) (bool, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "LoadBalancerExists", arg0, arg1) + ret0, _ := ret[0].(bool) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// LoadBalancerExists indicates an expected call of VpcExists +func (mr *MockAPIMockRecorder) LoadBalancerExists(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LoadBalancerExists", reflect.TypeOf((*MockAPI)(nil).LoadBalancerExists), arg0, arg1) +} + +// GetLoadBalancerARN mocks base method +func (m *MockAPI) GetLoadBalancerARN(arg0 context.Context, arg1 string) (string, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetLoadBalancerARN", arg0) + ret0, _ := ret[0].(string) + ret1, _ := ret[1].(error) + return ret0, ret1 +} diff --git a/ecs/pkg/amazon/testdata/simple/simple-cloudformation-conversion.golden b/ecs/pkg/amazon/testdata/simple/simple-cloudformation-conversion.golden index 258fa960d..15006580c 100644 --- a/ecs/pkg/amazon/testdata/simple/simple-cloudformation-conversion.golden +++ b/ecs/pkg/amazon/testdata/simple/simple-cloudformation-conversion.golden @@ -8,6 +8,14 @@ "Ref": "ParameterClusterName" } ] + }, + "CreateLoadBalancer": { + "Fn::Equals": [ + "", + { + "Ref": "ParameterLoadBalancerARN" + } + ] } }, "Parameters": { @@ -15,6 +23,10 @@ "Description": "Name of the ECS cluster to deploy to (optional)", "Type": "String" }, + "ParameterLoadBalancerARN": { + "Description": "Name of the LoadBalancer to connect to (optional)", + "Type": "String" + }, "ParameterSubnet1Id": { "Description": "SubnetId, for Availability Zone 1 in the region in your VPC", "Type": "AWS::EC2::Subnet::Id" @@ -247,6 +259,7 @@ "Type": "AWS::EC2::SecurityGroupIngress" }, "TestSimpleConvertNLB": { + "Condition": "CreateLoadBalancer", "Properties": { "Name": "TestSimpleConvertNLB", "Scheme": "internet-facing", diff --git a/ecs/pkg/amazon/testdata/simple/simple-cloudformation-with-overrides-conversion.golden b/ecs/pkg/amazon/testdata/simple/simple-cloudformation-with-overrides-conversion.golden index 991f65b6b..0324c2892 100644 --- a/ecs/pkg/amazon/testdata/simple/simple-cloudformation-with-overrides-conversion.golden +++ b/ecs/pkg/amazon/testdata/simple/simple-cloudformation-with-overrides-conversion.golden @@ -8,6 +8,14 @@ "Ref": "ParameterClusterName" } ] + }, + "CreateLoadBalancer": { + "Fn::Equals": [ + "", + { + "Ref": "ParameterLoadBalancerARN" + } + ] } }, "Parameters": { @@ -15,6 +23,10 @@ "Description": "Name of the ECS cluster to deploy to (optional)", "Type": "String" }, + "ParameterLoadBalancerARN": { + "Description": "Name of the LoadBalancer to connect to (optional)", + "Type": "String" + }, "ParameterSubnet1Id": { "Description": "SubnetId, for Availability Zone 1 in the region in your VPC", "Type": "AWS::EC2::Subnet::Id" @@ -247,6 +259,7 @@ "Type": "AWS::EC2::SecurityGroupIngress" }, "TestSimpleWithOverridesNLB": { + "Condition": "CreateLoadBalancer", "Properties": { "Name": "TestSimpleWithOverridesNLB", "Scheme": "internet-facing", From 02cc644c5af2e56846ccc5ccc67395a019f4128b Mon Sep 17 00:00:00 2001 From: aiordache <anca.iordache@docker.com> Date: Thu, 4 Jun 2020 18:04:16 +0200 Subject: [PATCH 105/198] fix test data Signed-off-by: aiordache <anca.iordache@docker.com> Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com> --- ecs/pkg/amazon/cloudformation.go | 17 +++++------------ .../simple-cloudformation-conversion.golden | 4 ++-- ...udformation-with-overrides-conversion.golden | 4 ++-- 3 files changed, 9 insertions(+), 16 deletions(-) diff --git a/ecs/pkg/amazon/cloudformation.go b/ecs/pkg/amazon/cloudformation.go index 68f6b3443..24b5786da 100644 --- a/ecs/pkg/amazon/cloudformation.go +++ b/ecs/pkg/amazon/cloudformation.go @@ -89,7 +89,7 @@ func (c client) Convert(project *compose.Project) (*cloudformation.Template, err // Private DNS namespace will allow DNS name for the services to be <service>.<project>.local c.createCloudMap(project, template) - loadBalancer := c.createLoadBalancer(project, template) + loadBalancer := c.createLoadBalancer(project, template, "network") for _, service := range project.Services { definition, err := Convert(project, service) @@ -177,20 +177,14 @@ func (c client) Convert(project *compose.Project) (*cloudformation.Template, err return template, nil } -func (c client) createLoadBalancer(project *compose.Project, template *cloudformation.Template) string { - - loadBalancerType := "network" - loadBalancerName := fmt.Sprintf( - "%s%sLB", - strings.Title(project.Name), - strings.ToUpper(loadBalancerType[0:1]), - ) +func (c client) createLoadBalancer(project *compose.Project, template *cloudformation.Template, loadBalancerType string) string { + loadBalancerName := fmt.Sprintf("%sLoadBalancer", strings.Title(project.Name)) // Create LoadBalancer if `ParameterLoadBalancerName` is not set template.Conditions["CreateLoadBalancer"] = cloudformation.Equals("", cloudformation.Ref(ParameterLoadBalancerARN)) - loadBalancer := &elasticloadbalancingv2.LoadBalancer{ + template.Resources[loadBalancerName] = &elasticloadbalancingv2.LoadBalancer{ Name: loadBalancerName, - Scheme: "internet-facing", + Scheme: elbv2.LoadBalancerSchemeEnumInternetFacing, Subnets: []string{ cloudformation.Ref(ParameterSubnet1Id), cloudformation.Ref(ParameterSubnet2Id), @@ -204,7 +198,6 @@ func (c client) createLoadBalancer(project *compose.Project, template *cloudform Type: loadBalancerType, AWSCloudFormationCondition: "CreateLoadBalancer", } - template.Resources[loadBalancerName] = loadBalancer loadBalancerRef := cloudformation.If("CreateLoadBalancer", cloudformation.Ref(loadBalancerName), cloudformation.Ref(ParameterLoadBalancerARN)) return loadBalancerRef diff --git a/ecs/pkg/amazon/testdata/simple/simple-cloudformation-conversion.golden b/ecs/pkg/amazon/testdata/simple/simple-cloudformation-conversion.golden index 15006580c..15222e571 100644 --- a/ecs/pkg/amazon/testdata/simple/simple-cloudformation-conversion.golden +++ b/ecs/pkg/amazon/testdata/simple/simple-cloudformation-conversion.golden @@ -258,10 +258,10 @@ }, "Type": "AWS::EC2::SecurityGroupIngress" }, - "TestSimpleConvertNLB": { + "TestSimpleConvertLoadBalancer": { "Condition": "CreateLoadBalancer", "Properties": { - "Name": "TestSimpleConvertNLB", + "Name": "TestSimpleConvertLoadBalancer", "Scheme": "internet-facing", "Subnets": [ { diff --git a/ecs/pkg/amazon/testdata/simple/simple-cloudformation-with-overrides-conversion.golden b/ecs/pkg/amazon/testdata/simple/simple-cloudformation-with-overrides-conversion.golden index 0324c2892..5cc9cb98c 100644 --- a/ecs/pkg/amazon/testdata/simple/simple-cloudformation-with-overrides-conversion.golden +++ b/ecs/pkg/amazon/testdata/simple/simple-cloudformation-with-overrides-conversion.golden @@ -258,10 +258,10 @@ }, "Type": "AWS::EC2::SecurityGroupIngress" }, - "TestSimpleWithOverridesNLB": { + "TestSimpleWithOverridesLoadBalancer": { "Condition": "CreateLoadBalancer", "Properties": { - "Name": "TestSimpleWithOverridesNLB", + "Name": "TestSimpleWithOverridesLoadBalancer", "Scheme": "internet-facing", "Subnets": [ { From 7337c7520fad4f6ecf752b60567aca9870c3ca9c Mon Sep 17 00:00:00 2001 From: aiordache <anca.iordache@docker.com> Date: Thu, 4 Jun 2020 18:23:31 +0200 Subject: [PATCH 106/198] rename LB field in the compose file Signed-off-by: aiordache <anca.iordache@docker.com> Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com> --- ecs/pkg/amazon/x.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ecs/pkg/amazon/x.go b/ecs/pkg/amazon/x.go index b16c3d253..315c50c4c 100644 --- a/ecs/pkg/amazon/x.go +++ b/ecs/pkg/amazon/x.go @@ -4,5 +4,5 @@ const ( ExtensionSecurityGroup = "x-aws-securitygroup" ExtensionVPC = "x-aws-vpc" ExtensionPullCredentials = "x-aws-pull_credentials" - ExtensionLB = "x-aws-lb" + ExtensionLB = "x-aws-loadbalancer" ) From dad36e09f9b1f04a7ad5106cdf8bbee65cc48c93 Mon Sep 17 00:00:00 2001 From: aiordache <anca.iordache@docker.com> Date: Fri, 5 Jun 2020 10:32:30 +0200 Subject: [PATCH 107/198] set ALB and security groups for http(s) protocol Signed-off-by: aiordache <anca.iordache@docker.com> Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com> --- ecs/pkg/amazon/cloudformation.go | 54 +++++++++++++++++-- .../simple-cloudformation-conversion.golden | 2 +- ...formation-with-overrides-conversion.golden | 2 +- 3 files changed, 52 insertions(+), 6 deletions(-) diff --git a/ecs/pkg/amazon/cloudformation.go b/ecs/pkg/amazon/cloudformation.go index 24b5786da..8c3e5e410 100644 --- a/ecs/pkg/amazon/cloudformation.go +++ b/ecs/pkg/amazon/cloudformation.go @@ -89,7 +89,9 @@ func (c client) Convert(project *compose.Project) (*cloudformation.Template, err // Private DNS namespace will allow DNS name for the services to be <service>.<project>.local c.createCloudMap(project, template) - loadBalancer := c.createLoadBalancer(project, template, "network") + + loadBalancerType, albSecurityGroups := c.getLoadBalancerType(project, networks) + loadBalancer := c.createLoadBalancer(project, template, loadBalancerType, albSecurityGroups) for _, service := range project.Services { definition, err := Convert(project, service) @@ -123,6 +125,12 @@ func (c client) Convert(project *compose.Project) (*cloudformation.Template, err if len(service.Ports) > 0 { for _, port := range service.Ports { protocol := strings.ToUpper(port.Protocol) + if loadBalancerType == elbv2.LoadBalancerTypeEnumApplication { + protocol = elbv2.ProtocolEnumHttps + if port.Published == 80 { + protocol = elbv2.ProtocolEnumHttp + } + } targetGroupName := c.createTargetGroup(project, service, port, template, protocol) listenerName := c.createListener(service, port, template, targetGroupName, loadBalancer, protocol) dependsOn = append(dependsOn, listenerName) @@ -177,14 +185,40 @@ func (c client) Convert(project *compose.Project) (*cloudformation.Template, err return template, nil } -func (c client) createLoadBalancer(project *compose.Project, template *cloudformation.Template, loadBalancerType string) string { +func (c client) getLoadBalancerType(project *compose.Project, networks map[string]string) (string, []string) { + // check what type of load balancer to create, we asssume by default application type + loadBalancerType := elbv2.LoadBalancerTypeEnumApplication + albSecurityGroups := []string{} + + for _, service := range project.Services { + if len(service.Ports) == 0 { + continue + } + for _, port := range service.Ports { + if port.Published != 80 && port.Published != 443 { + return elbv2.LoadBalancerTypeEnumNetwork, []string{} + } + } + + serviceSecurityGroups := []string{} + for net := range service.Networks { + serviceSecurityGroups = append(serviceSecurityGroups, networks[net]) + } + albSecurityGroups = append(albSecurityGroups, serviceSecurityGroups...) + albSecurityGroups = uniqueStrings(albSecurityGroups) + } + return loadBalancerType, albSecurityGroups +} + +func (c client) createLoadBalancer(project *compose.Project, template *cloudformation.Template, loadBalancerType string, securityGroups []string) string { loadBalancerName := fmt.Sprintf("%sLoadBalancer", strings.Title(project.Name)) // Create LoadBalancer if `ParameterLoadBalancerName` is not set template.Conditions["CreateLoadBalancer"] = cloudformation.Equals("", cloudformation.Ref(ParameterLoadBalancerARN)) template.Resources[loadBalancerName] = &elasticloadbalancingv2.LoadBalancer{ - Name: loadBalancerName, - Scheme: elbv2.LoadBalancerSchemeEnumInternetFacing, + Name: loadBalancerName, + Scheme: elbv2.LoadBalancerSchemeEnumInternetFacing, + SecurityGroups: securityGroups, Subnets: []string{ cloudformation.Ref(ParameterSubnet1Id), cloudformation.Ref(ParameterSubnet2Id), @@ -417,3 +451,15 @@ func (c client) getPolicy(taskDef *ecs.TaskDefinition) (*PolicyDocument, error) } return nil, nil } + +func uniqueStrings(items []string) []string { + keys := make(map[string]bool) + unique := []string{} + for _, item := range items { + if _, val := keys[item]; !val { + keys[item] = true + unique = append(unique, item) + } + } + return unique +} diff --git a/ecs/pkg/amazon/testdata/simple/simple-cloudformation-conversion.golden b/ecs/pkg/amazon/testdata/simple/simple-cloudformation-conversion.golden index 15222e571..3b9e55c47 100644 --- a/ecs/pkg/amazon/testdata/simple/simple-cloudformation-conversion.golden +++ b/ecs/pkg/amazon/testdata/simple/simple-cloudformation-conversion.golden @@ -277,7 +277,7 @@ "Value": "TestSimpleConvert" } ], - "Type": "network" + "Type": "application" }, "Type": "AWS::ElasticLoadBalancingV2::LoadBalancer" } diff --git a/ecs/pkg/amazon/testdata/simple/simple-cloudformation-with-overrides-conversion.golden b/ecs/pkg/amazon/testdata/simple/simple-cloudformation-with-overrides-conversion.golden index 5cc9cb98c..80c9b2aa7 100644 --- a/ecs/pkg/amazon/testdata/simple/simple-cloudformation-with-overrides-conversion.golden +++ b/ecs/pkg/amazon/testdata/simple/simple-cloudformation-with-overrides-conversion.golden @@ -277,7 +277,7 @@ "Value": "TestSimpleWithOverrides" } ], - "Type": "network" + "Type": "application" }, "Type": "AWS::ElasticLoadBalancingV2::LoadBalancer" } From c04950cdac7755a1d1f3dd7bb348586f81de08e6 Mon Sep 17 00:00:00 2001 From: aiordache <anca.iordache@docker.com> Date: Fri, 5 Jun 2020 11:34:02 +0200 Subject: [PATCH 108/198] improve lb security groups parsing Signed-off-by: aiordache <anca.iordache@docker.com> Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com> --- ecs/pkg/amazon/cloudformation.go | 24 +++++++++--------------- 1 file changed, 9 insertions(+), 15 deletions(-) diff --git a/ecs/pkg/amazon/cloudformation.go b/ecs/pkg/amazon/cloudformation.go index 8c3e5e410..955a8c2b2 100644 --- a/ecs/pkg/amazon/cloudformation.go +++ b/ecs/pkg/amazon/cloudformation.go @@ -186,28 +186,22 @@ func (c client) Convert(project *compose.Project) (*cloudformation.Template, err } func (c client) getLoadBalancerType(project *compose.Project, networks map[string]string) (string, []string) { - // check what type of load balancer to create, we asssume by default application type - loadBalancerType := elbv2.LoadBalancerTypeEnumApplication - albSecurityGroups := []string{} - for _, service := range project.Services { - if len(service.Ports) == 0 { - continue - } for _, port := range service.Ports { if port.Published != 80 && port.Published != 443 { return elbv2.LoadBalancerTypeEnumNetwork, []string{} } } - - serviceSecurityGroups := []string{} - for net := range service.Networks { - serviceSecurityGroups = append(serviceSecurityGroups, networks[net]) - } - albSecurityGroups = append(albSecurityGroups, serviceSecurityGroups...) - albSecurityGroups = uniqueStrings(albSecurityGroups) } - return loadBalancerType, albSecurityGroups + + albSecurityGroups := []string{} + for _, network := range project.Networks { + if !network.Internal { + albSecurityGroups = append(albSecurityGroups, networks[network.Name]) + } + } + albSecurityGroups = uniqueStrings(albSecurityGroups) + return elbv2.LoadBalancerTypeEnumApplication, albSecurityGroups } func (c client) createLoadBalancer(project *compose.Project, template *cloudformation.Template, loadBalancerType string, securityGroups []string) string { From 45dc8eda80dd3e773228b9c20eb13b31338a791e Mon Sep 17 00:00:00 2001 From: aiordache <anca.iordache@docker.com> Date: Fri, 5 Jun 2020 11:38:27 +0200 Subject: [PATCH 109/198] update test data Signed-off-by: aiordache <anca.iordache@docker.com> Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com> --- .../testdata/simple/simple-cloudformation-conversion.golden | 5 +++++ .../simple-cloudformation-with-overrides-conversion.golden | 5 +++++ 2 files changed, 10 insertions(+) diff --git a/ecs/pkg/amazon/testdata/simple/simple-cloudformation-conversion.golden b/ecs/pkg/amazon/testdata/simple/simple-cloudformation-conversion.golden index 3b9e55c47..7c7af6781 100644 --- a/ecs/pkg/amazon/testdata/simple/simple-cloudformation-conversion.golden +++ b/ecs/pkg/amazon/testdata/simple/simple-cloudformation-conversion.golden @@ -263,6 +263,11 @@ "Properties": { "Name": "TestSimpleConvertLoadBalancer", "Scheme": "internet-facing", + "SecurityGroups": [ + { + "Ref": "TestSimpleConvertDefaultNetwork" + } + ], "Subnets": [ { "Ref": "ParameterSubnet1Id" diff --git a/ecs/pkg/amazon/testdata/simple/simple-cloudformation-with-overrides-conversion.golden b/ecs/pkg/amazon/testdata/simple/simple-cloudformation-with-overrides-conversion.golden index 80c9b2aa7..e9535e908 100644 --- a/ecs/pkg/amazon/testdata/simple/simple-cloudformation-with-overrides-conversion.golden +++ b/ecs/pkg/amazon/testdata/simple/simple-cloudformation-with-overrides-conversion.golden @@ -263,6 +263,11 @@ "Properties": { "Name": "TestSimpleWithOverridesLoadBalancer", "Scheme": "internet-facing", + "SecurityGroups": [ + { + "Ref": "TestSimpleWithOverridesDefaultNetwork" + } + ], "Subnets": [ { "Ref": "ParameterSubnet1Id" From c0f1a8bf18bab3dda418728c2fc4b345889a852a Mon Sep 17 00:00:00 2001 From: aiordache <anca.iordache@docker.com> Date: Fri, 5 Jun 2020 15:14:50 +0200 Subject: [PATCH 110/198] create different methods to get lb type and security groups Signed-off-by: aiordache <anca.iordache@docker.com> Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com> --- ecs/pkg/amazon/cloudformation.go | 44 ++++++++++++++++++-------------- 1 file changed, 25 insertions(+), 19 deletions(-) diff --git a/ecs/pkg/amazon/cloudformation.go b/ecs/pkg/amazon/cloudformation.go index 955a8c2b2..c95034919 100644 --- a/ecs/pkg/amazon/cloudformation.go +++ b/ecs/pkg/amazon/cloudformation.go @@ -90,8 +90,7 @@ func (c client) Convert(project *compose.Project) (*cloudformation.Template, err // Private DNS namespace will allow DNS name for the services to be <service>.<project>.local c.createCloudMap(project, template) - loadBalancerType, albSecurityGroups := c.getLoadBalancerType(project, networks) - loadBalancer := c.createLoadBalancer(project, template, loadBalancerType, albSecurityGroups) + loadBalancerARN := c.createLoadBalancer(project, template) for _, service := range project.Services { definition, err := Convert(project, service) @@ -125,14 +124,14 @@ func (c client) Convert(project *compose.Project) (*cloudformation.Template, err if len(service.Ports) > 0 { for _, port := range service.Ports { protocol := strings.ToUpper(port.Protocol) - if loadBalancerType == elbv2.LoadBalancerTypeEnumApplication { + if c.getLoadBalancerType(project) == elbv2.LoadBalancerTypeEnumApplication { protocol = elbv2.ProtocolEnumHttps if port.Published == 80 { protocol = elbv2.ProtocolEnumHttp } } targetGroupName := c.createTargetGroup(project, service, port, template, protocol) - listenerName := c.createListener(service, port, template, targetGroupName, loadBalancer, protocol) + listenerName := c.createListener(service, port, template, targetGroupName, loadBalancerARN, protocol) dependsOn = append(dependsOn, listenerName) serviceLB = append(serviceLB, ecs.Service_LoadBalancer{ ContainerName: service.Name, @@ -185,30 +184,39 @@ func (c client) Convert(project *compose.Project) (*cloudformation.Template, err return template, nil } -func (c client) getLoadBalancerType(project *compose.Project, networks map[string]string) (string, []string) { +func (c client) getLoadBalancerType(project *compose.Project) string { for _, service := range project.Services { for _, port := range service.Ports { if port.Published != 80 && port.Published != 443 { - return elbv2.LoadBalancerTypeEnumNetwork, []string{} + return elbv2.LoadBalancerTypeEnumNetwork } } } - - albSecurityGroups := []string{} - for _, network := range project.Networks { - if !network.Internal { - albSecurityGroups = append(albSecurityGroups, networks[network.Name]) - } - } - albSecurityGroups = uniqueStrings(albSecurityGroups) - return elbv2.LoadBalancerTypeEnumApplication, albSecurityGroups + return elbv2.LoadBalancerTypeEnumApplication } -func (c client) createLoadBalancer(project *compose.Project, template *cloudformation.Template, loadBalancerType string, securityGroups []string) string { +func (c client) getLoadBalancerSecurityGroups(project *compose.Project, template *cloudformation.Template) []string { + securityGroups := []string{} + for _, network := range project.Networks { + if !network.Internal { + net := convertNetwork(project, network, cloudformation.Ref(ParameterVPCId), template) + securityGroups = append(securityGroups, net) + } + } + return uniqueStrings(securityGroups) +} + +func (c client) createLoadBalancer(project *compose.Project, template *cloudformation.Template) string { loadBalancerName := fmt.Sprintf("%sLoadBalancer", strings.Title(project.Name)) // Create LoadBalancer if `ParameterLoadBalancerName` is not set template.Conditions["CreateLoadBalancer"] = cloudformation.Equals("", cloudformation.Ref(ParameterLoadBalancerARN)) + loadBalancerType := c.getLoadBalancerType(project) + securityGroups := []string{} + if loadBalancerType == elbv2.LoadBalancerTypeEnumApplication { + securityGroups = c.getLoadBalancerSecurityGroups(project, template) + } + template.Resources[loadBalancerName] = &elasticloadbalancingv2.LoadBalancer{ Name: loadBalancerName, Scheme: elbv2.LoadBalancerSchemeEnumInternetFacing, @@ -226,9 +234,7 @@ func (c client) createLoadBalancer(project *compose.Project, template *cloudform Type: loadBalancerType, AWSCloudFormationCondition: "CreateLoadBalancer", } - loadBalancerRef := cloudformation.If("CreateLoadBalancer", cloudformation.Ref(loadBalancerName), cloudformation.Ref(ParameterLoadBalancerARN)) - - return loadBalancerRef + return cloudformation.If("CreateLoadBalancer", cloudformation.Ref(loadBalancerName), cloudformation.Ref(ParameterLoadBalancerARN)) } func (c client) createListener(service types.ServiceConfig, port types.ServicePortConfig, template *cloudformation.Template, targetGroupName string, loadBalancerARN string, protocol string) string { From e88b11bc2693d6491176c34d662ff14a1828f1c3 Mon Sep 17 00:00:00 2001 From: Nicolas De Loof <nicolas.deloof@gmail.com> Date: Mon, 8 Jun 2020 09:16:58 +0200 Subject: [PATCH 111/198] Introduce test to check CloudFormation conversion Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com> --- ecs/pkg/amazon/cloudformation_test.go | 46 +++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/ecs/pkg/amazon/cloudformation_test.go b/ecs/pkg/amazon/cloudformation_test.go index 456efbd01..84b7c9bcc 100644 --- a/ecs/pkg/amazon/cloudformation_test.go +++ b/ecs/pkg/amazon/cloudformation_test.go @@ -4,6 +4,12 @@ import ( "fmt" "testing" + "github.com/awslabs/goformation/v4/cloudformation/ec2" + + "github.com/awslabs/goformation/v4/cloudformation" + "github.com/compose-spec/compose-go/loader" + "github.com/compose-spec/compose-go/types" + "gotest.tools/assert" "github.com/docker/ecs-plugin/pkg/compose" @@ -24,6 +30,27 @@ func TestSimpleWithOverrides(t *testing.T) { golden.Assert(t, result, expected) } +func TestMapNetworksToSecurityGroups(t *testing.T) { + template := convertYaml(t, ` +version: "3" +services: + test: + image: hello_world +networks: + front-tier: + name: public + back-tier: + internal: true +`) + assert.Check(t, template.Resources["TestPublicNetwork"] != nil) + assert.Check(t, template.Resources["TestBacktierNetwork"] != nil) + assert.Check(t, template.Resources["TestBacktierNetworkIngress"] != nil) + ingress := template.Resources["TestPublicNetworkIngress"].(*ec2.SecurityGroupIngress) + assert.Check(t, ingress != nil) + assert.Check(t, ingress.SourceSecurityGroupId == cloudformation.Ref("TestPublicNetwork")) + +} + func convertResultAsString(t *testing.T, project *compose.Project, clusterName string) string { client, err := NewClient("", clusterName, "") assert.NilError(t, err) @@ -43,3 +70,22 @@ func load(t *testing.T, paths ...string) *compose.Project { assert.NilError(t, err) return project } + +func convertYaml(t *testing.T, yaml string) *cloudformation.Template { + dict, err := loader.ParseYAML([]byte(yaml)) + assert.NilError(t, err) + model, err := loader.Load(types.ConfigDetails{ + ConfigFiles: []types.ConfigFile{ + {Config: dict}, + }, + }) + assert.NilError(t, err) + err = compose.Normalize(model) + assert.NilError(t, err) + template, err := client{}.Convert(&compose.Project{ + Config: *model, + Name: "test", + }) + assert.NilError(t, err) + return template +} From 1d11e847fb234912c7b609cecccbf22036aa17d6 Mon Sep 17 00:00:00 2001 From: Nicolas De Loof <nicolas.deloof@gmail.com> Date: Mon, 8 Jun 2020 11:28:15 +0200 Subject: [PATCH 112/198] Test we create the expected policy document for pull_credentials Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com> --- ecs/pkg/amazon/cloudformation.go | 1 - ecs/pkg/amazon/cloudformation_test.go | 28 ++++++++++++++++++++++----- 2 files changed, 23 insertions(+), 6 deletions(-) diff --git a/ecs/pkg/amazon/cloudformation.go b/ecs/pkg/amazon/cloudformation.go index c95034919..6afa06c3d 100644 --- a/ecs/pkg/amazon/cloudformation.go +++ b/ecs/pkg/amazon/cloudformation.go @@ -426,7 +426,6 @@ func normalizeResourceName(s string) string { } func (c client) getPolicy(taskDef *ecs.TaskDefinition) (*PolicyDocument, error) { - arns := []string{} for _, container := range taskDef.ContainerDefinitions { if container.RepositoryCredentials != nil { diff --git a/ecs/pkg/amazon/cloudformation_test.go b/ecs/pkg/amazon/cloudformation_test.go index 84b7c9bcc..e23079d5b 100644 --- a/ecs/pkg/amazon/cloudformation_test.go +++ b/ecs/pkg/amazon/cloudformation_test.go @@ -4,15 +4,13 @@ import ( "fmt" "testing" - "github.com/awslabs/goformation/v4/cloudformation/ec2" - "github.com/awslabs/goformation/v4/cloudformation" + "github.com/awslabs/goformation/v4/cloudformation/ec2" + "github.com/awslabs/goformation/v4/cloudformation/iam" "github.com/compose-spec/compose-go/loader" "github.com/compose-spec/compose-go/types" - - "gotest.tools/assert" - "github.com/docker/ecs-plugin/pkg/compose" + "gotest.tools/assert" "gotest.tools/v3/golden" ) @@ -30,6 +28,26 @@ func TestSimpleWithOverrides(t *testing.T) { golden.Assert(t, result, expected) } +func TestRolePolicy(t *testing.T) { + template := convertYaml(t, ` +version: "3" +services: + foo: + image: hello_world + x-aws-pull_credentials: "secret" +`) + role := template.Resources["FooTaskExecutionRole"].(*iam.Role) + assert.Check(t, role != nil) + assert.Check(t, role.ManagedPolicyArns[0] == ECSTaskExecutionPolicy) + assert.Check(t, role.ManagedPolicyArns[1] == ECRReadOnlyPolicy) + // We expect an extra policy has been created for x-aws-pull_credentials + assert.Check(t, len(role.Policies) == 1) + policy := role.Policies[0].PolicyDocument.(*PolicyDocument) + expected := []string{"secretsmanager:GetSecretValue", "ssm:GetParameters", "kms:Decrypt"} + assert.DeepEqual(t, expected, policy.Statement[0].Action) + assert.DeepEqual(t, []string{"secret"}, policy.Statement[0].Resource) +} + func TestMapNetworksToSecurityGroups(t *testing.T) { template := convertYaml(t, ` version: "3" From 2c190f11f71696701dd98cd2b9e9f146069e87d9 Mon Sep 17 00:00:00 2001 From: aiordache <anca.iordache@docker.com> Date: Mon, 8 Jun 2020 15:34:33 +0200 Subject: [PATCH 113/198] LB Type tests Signed-off-by: aiordache <anca.iordache@docker.com> Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com> --- ecs/pkg/amazon/cloudformation_test.go | 34 +++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/ecs/pkg/amazon/cloudformation_test.go b/ecs/pkg/amazon/cloudformation_test.go index e23079d5b..0c6ce62e1 100644 --- a/ecs/pkg/amazon/cloudformation_test.go +++ b/ecs/pkg/amazon/cloudformation_test.go @@ -4,9 +4,13 @@ import ( "fmt" "testing" + "github.com/aws/aws-sdk-go/service/elbv2" "github.com/awslabs/goformation/v4/cloudformation" "github.com/awslabs/goformation/v4/cloudformation/ec2" "github.com/awslabs/goformation/v4/cloudformation/iam" + + "github.com/awslabs/goformation/v4/cloudformation" + "github.com/awslabs/goformation/v4/cloudformation/elasticloadbalancingv2" "github.com/compose-spec/compose-go/loader" "github.com/compose-spec/compose-go/types" "github.com/docker/ecs-plugin/pkg/compose" @@ -69,6 +73,36 @@ networks: } +func TestLoadBalancerTypeApplication(t *testing.T) { + template := convertYaml(t, ` +version: "3" +services: + test: + image: nginx + ports: + - 80:80 +`) + lb := template.Resources["TestLoadBalancer"].(*elasticloadbalancingv2.LoadBalancer) + assert.Check(t, lb != nil) + assert.Check(t, lb.Type == elbv2.LoadBalancerTypeEnumApplication) + assert.Check(t, len(lb.SecurityGroups) > 0) +} + +func TestLoadBalancerTypeNetwork(t *testing.T) { + template := convertYaml(t, ` +version: "3" +services: + test: + image: nginx + ports: + - 80:80 + - 88:88 +`) + lb := template.Resources["TestLoadBalancer"].(*elasticloadbalancingv2.LoadBalancer) + assert.Check(t, lb != nil) + assert.Check(t, lb.Type == elbv2.LoadBalancerTypeEnumNetwork) +} + func convertResultAsString(t *testing.T, project *compose.Project, clusterName string) string { client, err := NewClient("", clusterName, "") assert.NilError(t, err) From d597e55f229cf02a55c45026bf2229e41fbea374 Mon Sep 17 00:00:00 2001 From: aiordache <anca.iordache@docker.com> Date: Mon, 8 Jun 2020 19:00:30 +0200 Subject: [PATCH 114/198] fix rebase Signed-off-by: aiordache <anca.iordache@docker.com> Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com> --- ecs/pkg/amazon/cloudformation_test.go | 1 - 1 file changed, 1 deletion(-) diff --git a/ecs/pkg/amazon/cloudformation_test.go b/ecs/pkg/amazon/cloudformation_test.go index 0c6ce62e1..84e12e761 100644 --- a/ecs/pkg/amazon/cloudformation_test.go +++ b/ecs/pkg/amazon/cloudformation_test.go @@ -9,7 +9,6 @@ import ( "github.com/awslabs/goformation/v4/cloudformation/ec2" "github.com/awslabs/goformation/v4/cloudformation/iam" - "github.com/awslabs/goformation/v4/cloudformation" "github.com/awslabs/goformation/v4/cloudformation/elasticloadbalancingv2" "github.com/compose-spec/compose-go/loader" "github.com/compose-spec/compose-go/types" From 5f628cd0e5ad8fe4cc460eee909ca8e471397670 Mon Sep 17 00:00:00 2001 From: Ulysses Souza <ulyssessouza@gmail.com> Date: Mon, 8 Jun 2020 17:41:09 +0200 Subject: [PATCH 115/198] Refactor build process to build in containers Signed-off-by: Ulysses Souza <ulyssessouza@gmail.com> Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com> --- ecs/Dockerfile | 32 ++++++++++++++++++++++++++++++++ ecs/Makefile | 37 ++++++++++++++++++++++++++++++------- ecs/builder.Makefile | 39 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 101 insertions(+), 7 deletions(-) create mode 100644 ecs/Dockerfile create mode 100644 ecs/builder.Makefile diff --git a/ecs/Dockerfile b/ecs/Dockerfile new file mode 100644 index 000000000..db374fb8b --- /dev/null +++ b/ecs/Dockerfile @@ -0,0 +1,32 @@ +# syntax = docker/dockerfile:experimental +ARG GO_VERSION=1.14.2 + +FROM golang:${GO_VERSION} AS base +ARG TARGET_OS=unknown +ARG TARGET_ARCH=unknown +ARG PWD=/ecs-plugin +ENV GO111MODULE=on + +WORKDIR ${PWD} +ADD go.* ${PWD} +RUN go mod download +ADD . ${PWD} + +FROM base AS make-plugin +RUN --mount=type=cache,target=/root/.cache/go-build \ + GOOS=${TARGET_OS} \ + GOARCH=${TARGET_ARCH} \ + make -f builder.Makefile build + +FROM base AS make-cross +RUN --mount=type=cache,target=/root/.cache/go-build \ + make -f builder.Makefile cross + +FROM scratch AS build +COPY --from=make-plugin /ecs-plugin/dist/* . + +FROM scratch AS cross +COPY --from=make-cross /ecs-plugin/dist/* . + +FROM base as test +RUN make -f builder.Makefile test diff --git a/ecs/Makefile b/ecs/Makefile index 0fd6b65ae..c5fe28758 100644 --- a/ecs/Makefile +++ b/ecs/Makefile @@ -1,11 +1,27 @@ -clean: - rm -rf dist/ +GOOS ?= $(shell go env GOOS) +GOARCH ?= $(shell go env GOARCH) +PWD = $(shell pwd) -build: - go build -v -o dist/docker-ecs cmd/main/main.go +export DOCKER_BUILDKIT=1 + +.DEFAULT_GOAL := build + +build: ## Build for the current + @docker build . \ + --output type=local,dest=./dist \ + --build-arg TARGET_OS=${GOOS} \ + --build-arg TARGET_ARCH=${GOARCH} \ + --target build + +cross: ## Cross build for linux, macos and windows + @docker build . \ + --output type=local,dest=./dist \ + --target cross test: build ## Run tests - go test ./... -v + @docker build . \ + --output type=local,dest=./dist \ + --target test e2e: build ## Run tests go test ./... -v -tags=e2e @@ -15,6 +31,13 @@ dev: build ln -f -s "${PWD}/dist/docker-ecs" "${HOME}/.docker/cli-plugins/docker-ecs" lint: ## Verify Go files - golangci-lint run --config ./golangci.yaml ./... + @docker run --rm -t \ + -v $(PWD):/app \ + -w /app \ + golangci/golangci-lint:v1.27-alpine \ + golangci-lint run --timeout 10m0s --config ./golangci.yaml ./... -.PHONY: clean build test dev lint e2e +clean: + rm -rf dist/ + +.PHONY: clean build test dev lint e2e cross diff --git a/ecs/builder.Makefile b/ecs/builder.Makefile new file mode 100644 index 000000000..0fd8e30b6 --- /dev/null +++ b/ecs/builder.Makefile @@ -0,0 +1,39 @@ +GOOS ?= $(shell go env GOOS) +GOARCH ?= $(shell go env GOARCH) + +PROTOS=$(shell find . -name \*.proto) + +EXTENSION := +ifeq ($(GOOS),windows) + EXTENSION := .exe +endif + +STATIC_FLAGS= CGO_ENABLED=0 +LDFLAGS := "-s -w" +GO_BUILD = $(STATIC_FLAGS) go build -trimpath -ldflags=$(LDFLAGS) + +BINARY=dist/docker +BINARY_WITH_EXTENSION=$(BINARY)$(EXTENSION) + +export DOCKER_BUILDKIT=1 + +all: build + +clean: + rm -rf dist/ + +build: + $(GO_BUILD) -v -o $(BINARY_WITH_EXTENSION) cmd/main/main.go + +cross: + @GOOS=linux GOARCH=amd64 $(GO_BUILD) -v -o $(BINARY)-linux-amd64 cmd/main/main.go + @GOOS=darwin GOARCH=amd64 $(GO_BUILD) -v -o $(BINARY)-darwin-amd64 cmd/main/main.go + @GOOS=windows GOARCH=amd64 $(GO_BUILD) -v -o $(BINARY)-windows-amd64.exe cmd/main/main.go + +test: build ## Run tests + @go test ./... -v + +lint: ## Verify Go files + golangci-lint run --timeout 10m0s --config ./golangci.yaml ./... + +.PHONY: clean build test dev lint e2e From f192904d4210e66db1f694fab6789e90eefdea4a Mon Sep 17 00:00:00 2001 From: Nicolas De Loof <nicolas.deloof@gmail.com> Date: Tue, 9 Jun 2020 15:21:34 +0200 Subject: [PATCH 116/198] fix Makefile to produce docker-ecs binary Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com> --- ecs/builder.Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ecs/builder.Makefile b/ecs/builder.Makefile index 0fd8e30b6..81145e029 100644 --- a/ecs/builder.Makefile +++ b/ecs/builder.Makefile @@ -12,7 +12,7 @@ STATIC_FLAGS= CGO_ENABLED=0 LDFLAGS := "-s -w" GO_BUILD = $(STATIC_FLAGS) go build -trimpath -ldflags=$(LDFLAGS) -BINARY=dist/docker +BINARY=dist/docker-ecs BINARY_WITH_EXTENSION=$(BINARY)$(EXTENSION) export DOCKER_BUILDKIT=1 From 2ab64ea10ef534fa623e74b9f7edd1223ac46512 Mon Sep 17 00:00:00 2001 From: Christopher Crone <christopher.crone@docker.com> Date: Tue, 9 Jun 2020 13:51:46 +0200 Subject: [PATCH 117/198] docs: Add Linux install instructions Signed-off-by: Christopher Crone <christopher.crone@docker.com> Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com> --- ecs/docs/get-started-linux.md | 84 +++++++++++++++++++++++++++++++++++ 1 file changed, 84 insertions(+) create mode 100644 ecs/docs/get-started-linux.md diff --git a/ecs/docs/get-started-linux.md b/ecs/docs/get-started-linux.md new file mode 100644 index 000000000..2b1a129bf --- /dev/null +++ b/ecs/docs/get-started-linux.md @@ -0,0 +1,84 @@ +Getting Started with Docker AWS ECS Plugin Beta on Linux +-------------------------------------------------------- + +The beta release of [AWS ECS](https://aws.amazon.com/ecs/) support for the +Docker CLI is shipped as a CLI plugin. Later releases will be included as part +of the Docker CLI. + +This plugin is included as part of Docker Desktop on Windows and macOS but on +Linux it needs to be installed manually. + +## Prerequisites + +* [Docker 19.03 or later](https://docs.docker.com/get-docker/) + +## Step by step install + +### Download + +You can download the Docker ECS plugin from this repository using the following +command: + +<!-- FIXME(chris-crone): get real download link --> +```console +$ curl -L http://xxx | tar xzf - +``` + +You will then need to make it executable: + +```console +$ chmod +x docker-ecs +``` + +### Plugin install + +In order for the Docker CLI to use the downloaded plugin, you will need to move +it to the right place: + +```console +$ mkdir -p /usr/local/lib/docker/cli-plugins + +$ mv docker-ecs /usr/local/lib/docker/cli-plugins/ +``` + +You can put the CLI plugin into any of the following directories: + +* `/usr/local/lib/docker/cli-plugins` +* `/usr/local/libexec/docker/cli-plugins` +* `/usr/lib/docker/cli-plugins` +* `/usr/libexec/docker/cli-plugins` + +Finally you need to enable the experimental features on the CLI. This can be +done by setting the environment variable `DOCKER_CLI_EXPERIMENTAL=enabled` or by +setting `experimental` to `"enabled"` in your Docker config found at +`~/.docker/config.json`: + +```console +$ export DOCKER_CLI_EXPERIMENTAL=enabled + +$ DOCKER_CLI_EXPERIMENTAL=enabled docker help + +$ cat ~/.docker/config.json +{ + "experimental" : "enabled", + "auths" : { + "https://index.docker.io/v1/" : { + + } + } +} +``` + +To verify the CLI plugin installation, you can check that it appears in the CLI +help output or by outputting the plugin version: + +```console +$ docker help | grep ecs + ecs* Docker ECS (Docker Inc., 0.0.1) + +$ docker ecs version +Docker ECS plugin 0.0.1 +``` + +<!-- FIXME(chris-crone): Link to ECS docs --> +You are now ready to [start deploying to ECS](http://xxx) From 4be356245048c6b7d13fd4eaed5a79dacffe2e71 Mon Sep 17 00:00:00 2001 From: Nicolas De Loof <nicolas.deloof@gmail.com> Date: Wed, 10 Jun 2020 10:51:52 +0200 Subject: [PATCH 118/198] Revert "Refactor build process to build in containers" This reverts commit adab0d1bdf7bf2cc242128aae7f5044bd5182ea1. Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com> --- ecs/Dockerfile | 32 -------------------------------- ecs/Makefile | 37 +++++++------------------------------ ecs/builder.Makefile | 39 --------------------------------------- 3 files changed, 7 insertions(+), 101 deletions(-) delete mode 100644 ecs/Dockerfile delete mode 100644 ecs/builder.Makefile diff --git a/ecs/Dockerfile b/ecs/Dockerfile deleted file mode 100644 index db374fb8b..000000000 --- a/ecs/Dockerfile +++ /dev/null @@ -1,32 +0,0 @@ -# syntax = docker/dockerfile:experimental -ARG GO_VERSION=1.14.2 - -FROM golang:${GO_VERSION} AS base -ARG TARGET_OS=unknown -ARG TARGET_ARCH=unknown -ARG PWD=/ecs-plugin -ENV GO111MODULE=on - -WORKDIR ${PWD} -ADD go.* ${PWD} -RUN go mod download -ADD . ${PWD} - -FROM base AS make-plugin -RUN --mount=type=cache,target=/root/.cache/go-build \ - GOOS=${TARGET_OS} \ - GOARCH=${TARGET_ARCH} \ - make -f builder.Makefile build - -FROM base AS make-cross -RUN --mount=type=cache,target=/root/.cache/go-build \ - make -f builder.Makefile cross - -FROM scratch AS build -COPY --from=make-plugin /ecs-plugin/dist/* . - -FROM scratch AS cross -COPY --from=make-cross /ecs-plugin/dist/* . - -FROM base as test -RUN make -f builder.Makefile test diff --git a/ecs/Makefile b/ecs/Makefile index c5fe28758..0fd6b65ae 100644 --- a/ecs/Makefile +++ b/ecs/Makefile @@ -1,27 +1,11 @@ -GOOS ?= $(shell go env GOOS) -GOARCH ?= $(shell go env GOARCH) -PWD = $(shell pwd) +clean: + rm -rf dist/ -export DOCKER_BUILDKIT=1 - -.DEFAULT_GOAL := build - -build: ## Build for the current - @docker build . \ - --output type=local,dest=./dist \ - --build-arg TARGET_OS=${GOOS} \ - --build-arg TARGET_ARCH=${GOARCH} \ - --target build - -cross: ## Cross build for linux, macos and windows - @docker build . \ - --output type=local,dest=./dist \ - --target cross +build: + go build -v -o dist/docker-ecs cmd/main/main.go test: build ## Run tests - @docker build . \ - --output type=local,dest=./dist \ - --target test + go test ./... -v e2e: build ## Run tests go test ./... -v -tags=e2e @@ -31,13 +15,6 @@ dev: build ln -f -s "${PWD}/dist/docker-ecs" "${HOME}/.docker/cli-plugins/docker-ecs" lint: ## Verify Go files - @docker run --rm -t \ - -v $(PWD):/app \ - -w /app \ - golangci/golangci-lint:v1.27-alpine \ - golangci-lint run --timeout 10m0s --config ./golangci.yaml ./... + golangci-lint run --config ./golangci.yaml ./... -clean: - rm -rf dist/ - -.PHONY: clean build test dev lint e2e cross +.PHONY: clean build test dev lint e2e diff --git a/ecs/builder.Makefile b/ecs/builder.Makefile deleted file mode 100644 index 81145e029..000000000 --- a/ecs/builder.Makefile +++ /dev/null @@ -1,39 +0,0 @@ -GOOS ?= $(shell go env GOOS) -GOARCH ?= $(shell go env GOARCH) - -PROTOS=$(shell find . -name \*.proto) - -EXTENSION := -ifeq ($(GOOS),windows) - EXTENSION := .exe -endif - -STATIC_FLAGS= CGO_ENABLED=0 -LDFLAGS := "-s -w" -GO_BUILD = $(STATIC_FLAGS) go build -trimpath -ldflags=$(LDFLAGS) - -BINARY=dist/docker-ecs -BINARY_WITH_EXTENSION=$(BINARY)$(EXTENSION) - -export DOCKER_BUILDKIT=1 - -all: build - -clean: - rm -rf dist/ - -build: - $(GO_BUILD) -v -o $(BINARY_WITH_EXTENSION) cmd/main/main.go - -cross: - @GOOS=linux GOARCH=amd64 $(GO_BUILD) -v -o $(BINARY)-linux-amd64 cmd/main/main.go - @GOOS=darwin GOARCH=amd64 $(GO_BUILD) -v -o $(BINARY)-darwin-amd64 cmd/main/main.go - @GOOS=windows GOARCH=amd64 $(GO_BUILD) -v -o $(BINARY)-windows-amd64.exe cmd/main/main.go - -test: build ## Run tests - @go test ./... -v - -lint: ## Verify Go files - golangci-lint run --timeout 10m0s --config ./golangci.yaml ./... - -.PHONY: clean build test dev lint e2e From 1a09dc51ea16b85713b70c6aa04c792de6b8cf29 Mon Sep 17 00:00:00 2001 From: Ulysses Souza <ulyssessouza@gmail.com> Date: Wed, 10 Jun 2020 15:07:25 +0200 Subject: [PATCH 119/198] Refactor build process to build in containers This is a re-apply from a previous commit Signed-off-by: Ulysses Souza <ulyssessouza@gmail.com> Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com> --- ecs/Dockerfile | 32 ++++++++++++++++++++++++++++++++ ecs/Makefile | 37 ++++++++++++++++++++++++++++++------- ecs/builder.Makefile | 39 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 101 insertions(+), 7 deletions(-) create mode 100644 ecs/Dockerfile create mode 100644 ecs/builder.Makefile diff --git a/ecs/Dockerfile b/ecs/Dockerfile new file mode 100644 index 000000000..db374fb8b --- /dev/null +++ b/ecs/Dockerfile @@ -0,0 +1,32 @@ +# syntax = docker/dockerfile:experimental +ARG GO_VERSION=1.14.2 + +FROM golang:${GO_VERSION} AS base +ARG TARGET_OS=unknown +ARG TARGET_ARCH=unknown +ARG PWD=/ecs-plugin +ENV GO111MODULE=on + +WORKDIR ${PWD} +ADD go.* ${PWD} +RUN go mod download +ADD . ${PWD} + +FROM base AS make-plugin +RUN --mount=type=cache,target=/root/.cache/go-build \ + GOOS=${TARGET_OS} \ + GOARCH=${TARGET_ARCH} \ + make -f builder.Makefile build + +FROM base AS make-cross +RUN --mount=type=cache,target=/root/.cache/go-build \ + make -f builder.Makefile cross + +FROM scratch AS build +COPY --from=make-plugin /ecs-plugin/dist/* . + +FROM scratch AS cross +COPY --from=make-cross /ecs-plugin/dist/* . + +FROM base as test +RUN make -f builder.Makefile test diff --git a/ecs/Makefile b/ecs/Makefile index 0fd6b65ae..c5fe28758 100644 --- a/ecs/Makefile +++ b/ecs/Makefile @@ -1,11 +1,27 @@ -clean: - rm -rf dist/ +GOOS ?= $(shell go env GOOS) +GOARCH ?= $(shell go env GOARCH) +PWD = $(shell pwd) -build: - go build -v -o dist/docker-ecs cmd/main/main.go +export DOCKER_BUILDKIT=1 + +.DEFAULT_GOAL := build + +build: ## Build for the current + @docker build . \ + --output type=local,dest=./dist \ + --build-arg TARGET_OS=${GOOS} \ + --build-arg TARGET_ARCH=${GOARCH} \ + --target build + +cross: ## Cross build for linux, macos and windows + @docker build . \ + --output type=local,dest=./dist \ + --target cross test: build ## Run tests - go test ./... -v + @docker build . \ + --output type=local,dest=./dist \ + --target test e2e: build ## Run tests go test ./... -v -tags=e2e @@ -15,6 +31,13 @@ dev: build ln -f -s "${PWD}/dist/docker-ecs" "${HOME}/.docker/cli-plugins/docker-ecs" lint: ## Verify Go files - golangci-lint run --config ./golangci.yaml ./... + @docker run --rm -t \ + -v $(PWD):/app \ + -w /app \ + golangci/golangci-lint:v1.27-alpine \ + golangci-lint run --timeout 10m0s --config ./golangci.yaml ./... -.PHONY: clean build test dev lint e2e +clean: + rm -rf dist/ + +.PHONY: clean build test dev lint e2e cross diff --git a/ecs/builder.Makefile b/ecs/builder.Makefile new file mode 100644 index 000000000..0fd8e30b6 --- /dev/null +++ b/ecs/builder.Makefile @@ -0,0 +1,39 @@ +GOOS ?= $(shell go env GOOS) +GOARCH ?= $(shell go env GOARCH) + +PROTOS=$(shell find . -name \*.proto) + +EXTENSION := +ifeq ($(GOOS),windows) + EXTENSION := .exe +endif + +STATIC_FLAGS= CGO_ENABLED=0 +LDFLAGS := "-s -w" +GO_BUILD = $(STATIC_FLAGS) go build -trimpath -ldflags=$(LDFLAGS) + +BINARY=dist/docker +BINARY_WITH_EXTENSION=$(BINARY)$(EXTENSION) + +export DOCKER_BUILDKIT=1 + +all: build + +clean: + rm -rf dist/ + +build: + $(GO_BUILD) -v -o $(BINARY_WITH_EXTENSION) cmd/main/main.go + +cross: + @GOOS=linux GOARCH=amd64 $(GO_BUILD) -v -o $(BINARY)-linux-amd64 cmd/main/main.go + @GOOS=darwin GOARCH=amd64 $(GO_BUILD) -v -o $(BINARY)-darwin-amd64 cmd/main/main.go + @GOOS=windows GOARCH=amd64 $(GO_BUILD) -v -o $(BINARY)-windows-amd64.exe cmd/main/main.go + +test: build ## Run tests + @go test ./... -v + +lint: ## Verify Go files + golangci-lint run --timeout 10m0s --config ./golangci.yaml ./... + +.PHONY: clean build test dev lint e2e From a0500799d04ab52c311d894343312a40d5451eee Mon Sep 17 00:00:00 2001 From: Ulysses Souza <ulyssessouza@gmail.com> Date: Wed, 10 Jun 2020 15:16:11 +0200 Subject: [PATCH 120/198] Fix and optimize build process Kudos @chris-crone! Signed-off-by: Ulysses Souza <ulyssessouza@gmail.com> Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com> --- ecs/Dockerfile | 49 ++++++++++++++++++++++++++++------------ ecs/Makefile | 22 ++++++------------ ecs/builder.Makefile | 22 ++++++++---------- ecs/go.mod | 3 ++- ecs/go.sum | 16 +++++++++---- ecs/tests/plugin_test.go | 33 --------------------------- 6 files changed, 64 insertions(+), 81 deletions(-) delete mode 100644 ecs/tests/plugin_test.go diff --git a/ecs/Dockerfile b/ecs/Dockerfile index db374fb8b..8d18ddfed 100644 --- a/ecs/Dockerfile +++ b/ecs/Dockerfile @@ -1,32 +1,51 @@ # syntax = docker/dockerfile:experimental -ARG GO_VERSION=1.14.2 +ARG GO_VERSION=1.14.4-alpine +ARG ALPINE_PKG_DOCKER_VERSION=19.03.11-r0 +ARG GOLANGCI_LINT_VERSION=v1.27.0-alpine -FROM golang:${GO_VERSION} AS base -ARG TARGET_OS=unknown -ARG TARGET_ARCH=unknown -ARG PWD=/ecs-plugin +FROM --platform=${BUILDPLATFORM} golang:${GO_VERSION} AS base +WORKDIR /ecs-plugin ENV GO111MODULE=on - -WORKDIR ${PWD} -ADD go.* ${PWD} -RUN go mod download -ADD . ${PWD} +ARG ALPINE_PKG_DOCKER_VERSION +RUN apk add --no-cache \ + docker=${ALPINE_PKG_DOCKER_VERSION} \ + make +COPY go.* . +RUN --mount=type=cache,target=/go/pkg/mod \ + go mod download +COPY . . FROM base AS make-plugin +ARG TARGETOS +ARG TARGETARCH RUN --mount=type=cache,target=/root/.cache/go-build \ - GOOS=${TARGET_OS} \ - GOARCH=${TARGET_ARCH} \ + --mount=type=cache,target=/go/pkg/mod \ + GOOS=${TARGETOS} \ + GOARCH=${TARGETARCH} \ make -f builder.Makefile build FROM base AS make-cross RUN --mount=type=cache,target=/root/.cache/go-build \ + --mount=type=cache,target=/go/pkg/mod \ make -f builder.Makefile cross FROM scratch AS build -COPY --from=make-plugin /ecs-plugin/dist/* . +COPY --from=make-plugin /ecs-plugin/dist/docker-ecs . FROM scratch AS cross COPY --from=make-cross /ecs-plugin/dist/* . -FROM base as test -RUN make -f builder.Makefile test +FROM make-plugin AS test +RUN --mount=type=cache,target=/root/.cache/go-build \ + --mount=type=cache,target=/go/pkg/mod \ + make -f builder.Makefile test + +FROM golangci/golangci-lint:${GOLANGCI_LINT_VERSION} AS lint-base + +FROM base AS lint +COPY --from=lint-base /usr/bin/golangci-lint /usr/bin/golangci-lint +RUN --mount=target=. \ + --mount=type=cache,target=/root/.cache/go-build \ + --mount=type=cache,target=/go/pkg/mod \ + --mount=type=cache,target=/root/.cache/golangci-lint \ + make -f builder.Makefile lint diff --git a/ecs/Makefile b/ecs/Makefile index c5fe28758..22dd36049 100644 --- a/ecs/Makefile +++ b/ecs/Makefile @@ -1,6 +1,5 @@ -GOOS ?= $(shell go env GOOS) -GOARCH ?= $(shell go env GOARCH) -PWD = $(shell pwd) +PLATFORM?=local +PWD=$(shell pwd) export DOCKER_BUILDKIT=1 @@ -8,20 +7,17 @@ export DOCKER_BUILDKIT=1 build: ## Build for the current @docker build . \ - --output type=local,dest=./dist \ - --build-arg TARGET_OS=${GOOS} \ - --build-arg TARGET_ARCH=${GOARCH} \ + --output ./dist \ + --platform ${PLATFORM} \ --target build cross: ## Cross build for linux, macos and windows @docker build . \ - --output type=local,dest=./dist \ + --output ./dist \ --target cross test: build ## Run tests - @docker build . \ - --output type=local,dest=./dist \ - --target test + @docker build . --target test e2e: build ## Run tests go test ./... -v -tags=e2e @@ -31,11 +27,7 @@ dev: build ln -f -s "${PWD}/dist/docker-ecs" "${HOME}/.docker/cli-plugins/docker-ecs" lint: ## Verify Go files - @docker run --rm -t \ - -v $(PWD):/app \ - -w /app \ - golangci/golangci-lint:v1.27-alpine \ - golangci-lint run --timeout 10m0s --config ./golangci.yaml ./... + @docker build . --target lint clean: rm -rf dist/ diff --git a/ecs/builder.Makefile b/ecs/builder.Makefile index 0fd8e30b6..8b6920baa 100644 --- a/ecs/builder.Makefile +++ b/ecs/builder.Makefile @@ -1,18 +1,16 @@ -GOOS ?= $(shell go env GOOS) -GOARCH ?= $(shell go env GOARCH) - -PROTOS=$(shell find . -name \*.proto) +GOOS?=$(shell go env GOOS) +GOARCH?=$(shell go env GOARCH) EXTENSION := ifeq ($(GOOS),windows) EXTENSION := .exe endif -STATIC_FLAGS= CGO_ENABLED=0 -LDFLAGS := "-s -w" -GO_BUILD = $(STATIC_FLAGS) go build -trimpath -ldflags=$(LDFLAGS) +STATIC_FLAGS=CGO_ENABLED=0 +LDFLAGS:="-s -w" +GO_BUILD=$(STATIC_FLAGS) go build -trimpath -ldflags=$(LDFLAGS) -BINARY=dist/docker +BINARY=dist/docker-ecs BINARY_WITH_EXTENSION=$(BINARY)$(EXTENSION) export DOCKER_BUILDKIT=1 @@ -30,10 +28,10 @@ cross: @GOOS=darwin GOARCH=amd64 $(GO_BUILD) -v -o $(BINARY)-darwin-amd64 cmd/main/main.go @GOOS=windows GOARCH=amd64 $(GO_BUILD) -v -o $(BINARY)-windows-amd64.exe cmd/main/main.go -test: build ## Run tests - @go test ./... -v +test: ## Run tests + @$(STATIC_FLAGS) go test -cover $(shell go list ./... | grep -vE 'e2e') lint: ## Verify Go files - golangci-lint run --timeout 10m0s --config ./golangci.yaml ./... + $(STATIC_FLAGS) golangci-lint run --timeout 10m0s --config ./golangci.yaml ./... -.PHONY: clean build test dev lint e2e +.PHONY: all clean build cross test dev lint diff --git a/ecs/go.mod b/ecs/go.mod index 7eedd06bb..7dd8d7fe2 100644 --- a/ecs/go.mod +++ b/ecs/go.mod @@ -40,6 +40,7 @@ require ( github.com/onsi/ginkgo v1.11.0 // indirect github.com/opencontainers/image-spec v1.0.1 // indirect github.com/sirupsen/logrus v1.5.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 @@ -56,4 +57,4 @@ require ( vbom.ml/util v0.0.0-20180919145318-efcd4e0f9787 // indirect ) -go 1.13 +go 1.14 diff --git a/ecs/go.sum b/ecs/go.sum index 9088eaf22..f0303a304 100644 --- a/ecs/go.sum +++ b/ecs/go.sum @@ -19,9 +19,6 @@ github.com/akavel/rsrc v0.8.0/go.mod h1:uLoCtb9J+EyAqh+26kdrTgmzRBFPGOolLWKpdxkK github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= -github.com/aws/aws-sdk-go v1.28.9 h1:grIuBQc+p3dTRXerh5+2OxSuWFi0iXuxbFdTSg0jaW0= -github.com/aws/aws-sdk-go v1.28.9/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= -github.com/aws/aws-sdk-go v1.30.2/go.mod h1:5zCpMtNQVjRREroY7sYe8lOMRSxkhG6MZveU8YkpAk0= github.com/aws/aws-sdk-go v1.30.22 h1:wImJ8jQrplgmxaTeUY7FrJFn4te/VtWq+mmmJ1TnWAg= github.com/aws/aws-sdk-go v1.30.22/go.mod h1:5zCpMtNQVjRREroY7sYe8lOMRSxkhG6MZveU8YkpAk0= github.com/awslabs/goformation/v4 v4.8.0 h1:UiUhyokRy3suEqBXTnipvY8klqY3Eyl4GCH17brraEc= @@ -45,9 +42,11 @@ github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEe github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/certifi/gocertifi v0.0.0-20180118203423-deb3ae2ef261/go.mod h1:GJKEexRPVJrBSOjoqN5VNOIKJ5Q3RViH6eu3puDRwx4= +github.com/chzyer/logex v1.1.10 h1:Swpa1K6QvQznwJRcfTfQJmTE72DqScAa40E+fbHEXEE= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e h1:fY5BOSpyZCqRo5OhCuC+XN+r/bBCmeuuJtjz+bCNIf8= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 h1:q763qf9huN11kDQavWsoZXJNW3xEE4JJyHa5Q25/sd8= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cloudflare/backoff v0.0.0-20161212185259-647f3cdfc87a/go.mod h1:rzgs2ZOiguV6/NpiDgADjRLPNyZlApIWxKpkT+X8SdY= @@ -139,6 +138,8 @@ github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMyw github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= +github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gorilla/mux v1.7.3 h1:gnP5JzjVOuiZD07fKKToCAOjS0yOpj/qPETTXCCS6hw= github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed h1:5upAirOpQc1Q53c0bnx2ufif5kANL7bfZWcc6VJWJd8= @@ -162,14 +163,14 @@ github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= github.com/jinzhu/now v1.0.1 h1:HjfetcXq097iXP0uoPCdnM4Efp5/9MsM0/M+XOTeR3M= github.com/jinzhu/now v1.0.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= -github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af h1:pmfjZENx5imkbgOkpRUYLnmbU7UEFbjtDA2hxJ1ichM= -github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= github.com/jmespath/go-jmespath v0.3.0 h1:OS12ieG61fsCg5+qLJ+SsW9NicxNkg3b25OyT2yCeUc= github.com/jmespath/go-jmespath v0.3.0/go.mod h1:9QtRXoHjLGCJ5IBSaohpXITPlowMeeYCZ7fLUTSywik= github.com/jmhodges/clock v0.0.0-20160418191101-880ee4c33548/go.mod h1:hGT6jSUVzF6no3QaDSMLGLEHtHSBSefs+MgcDWnmhmo= github.com/jmoiron/sqlx v0.0.0-20180124204410-05cef0741ade/go.mod h1:IiEW3SEiiErVyFdH8NTuWjSifiEQKUoyK3LNqr2kCHU= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= +github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/juju/ansiterm v0.0.0-20180109212912-720a0952cc2a h1:FaWFmfWdAUKbSCtOU2QjDaorUexogfaMgbipgYATUMU= github.com/juju/ansiterm v0.0.0-20180109212912-720a0952cc2a/go.mod h1:UJSiEoRfvx3hP73CvoARgeLjaIOjybY9vj8PUPPFGeU= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= @@ -283,6 +284,10 @@ github.com/sirupsen/logrus v1.3.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPx github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= github.com/sirupsen/logrus v1.5.0 h1:1N5EYkVAPEywqZRJd7cwnRtCb6xJx7NH3T3WUTF980Q= github.com/sirupsen/logrus v1.5.0/go.mod h1:+F7Ogzej0PZc/94MaYx/nvG9jOFMD2osvC3s+Squfpo= +github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM= +github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= +github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s= +github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= 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= @@ -405,6 +410,7 @@ golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGm golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190624222133-a101b041ded4/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= diff --git a/ecs/tests/plugin_test.go b/ecs/tests/plugin_test.go deleted file mode 100644 index 48d7e2f54..000000000 --- a/ecs/tests/plugin_test.go +++ /dev/null @@ -1,33 +0,0 @@ -package tests - -import ( - "regexp" - "testing" - - "gotest.tools/assert" - "gotest.tools/v3/golden" - "gotest.tools/v3/icmd" -) - -func TestInvokePluginFromCLI(t *testing.T) { - cmd, cleanup, _ := dockerCli.createTestCmd() - defer cleanup() - // docker --help should list app as a top command - cmd.Command = dockerCli.Command("--help") - icmd.RunCmd(cmd).Assert(t, icmd.Expected{ - Out: "ecs* Docker ECS (Docker Inc.,", - }) - - // docker app --help prints docker-app help - cmd.Command = dockerCli.Command("ecs", "--help") - usage := icmd.RunCmd(cmd).Assert(t, icmd.Success).Combined() - - goldenFile := "plugin-usage.golden" - golden.Assert(t, usage, goldenFile) - - // docker info should print app version and short description - cmd.Command = dockerCli.Command("info") - re := regexp.MustCompile(`ecs: Docker ECS \(Docker Inc\., .*\)`) - output := icmd.RunCmd(cmd).Assert(t, icmd.Success).Combined() - assert.Assert(t, re.MatchString(output)) -} From d95798747153d76ff0cc1d4436948d515a43ebca Mon Sep 17 00:00:00 2001 From: Nicolas De Loof <nicolas.deloof@gmail.com> Date: Wed, 10 Jun 2020 10:42:48 +0200 Subject: [PATCH 121/198] Unit tests for cobra commands Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com> --- ecs/cmd/commands/root.go | 32 +++++++++++++++++ ecs/cmd/commands/root_test.go | 13 +++++++ ecs/cmd/commands/setup.go | 6 ---- ecs/cmd/commands/setup_test.go | 32 +++++++++++++++++ ecs/cmd/commands/testdata/context.golden | 1 + ecs/cmd/commands/version.go | 20 +++++++++++ ecs/cmd/commands/version_test.go | 18 ++++++++++ ecs/cmd/main/main.go | 44 ++--------------------- ecs/pkg/amazon/check_test.go | 2 +- ecs/pkg/amazon/cloudformation_test.go | 2 +- ecs/tests/command_test.go | 18 ---------- ecs/tests/e2e_deploy_services_test.go | 2 +- ecs/tests/setup_command_test.go | 24 ------------- ecs/tests/testdata/context-inspect.golden | 16 --------- 14 files changed, 122 insertions(+), 108 deletions(-) create mode 100644 ecs/cmd/commands/root.go create mode 100644 ecs/cmd/commands/root_test.go create mode 100644 ecs/cmd/commands/setup_test.go create mode 100644 ecs/cmd/commands/testdata/context.golden create mode 100644 ecs/cmd/commands/version.go create mode 100644 ecs/cmd/commands/version_test.go delete mode 100644 ecs/tests/command_test.go delete mode 100644 ecs/tests/setup_command_test.go delete mode 100644 ecs/tests/testdata/context-inspect.golden diff --git a/ecs/cmd/commands/root.go b/ecs/cmd/commands/root.go new file mode 100644 index 000000000..a9c229218 --- /dev/null +++ b/ecs/cmd/commands/root.go @@ -0,0 +1,32 @@ +package commands + +import ( + "fmt" + + "github.com/docker/cli/cli/command" + "github.com/spf13/cobra" +) + +// NewRootCmd returns the base root command. +func NewRootCmd(dockerCli command.Cli) *cobra.Command { + cmd := &cobra.Command{ + Short: "Docker ECS", + Long: `run multi-container applications on Amazon ECS.`, + Use: "ecs", + Annotations: map[string]string{"experimentalCLI": "true"}, + RunE: func(cmd *cobra.Command, args []string) error { + if len(args) != 0 { + return fmt.Errorf("%q is not a docker ecs command\nSee 'docker ecs --help'", args[0]) + } + cmd.Help() + return nil + }, + } + cmd.AddCommand( + VersionCommand(), + ComposeCommand(dockerCli), + SecretCommand(dockerCli), + SetupCommand(), + ) + return cmd +} diff --git a/ecs/cmd/commands/root_test.go b/ecs/cmd/commands/root_test.go new file mode 100644 index 000000000..80f2a72d5 --- /dev/null +++ b/ecs/cmd/commands/root_test.go @@ -0,0 +1,13 @@ +package commands + +import ( + "testing" + + "gotest.tools/v3/assert" +) + +func TestUnknownCommand(t *testing.T) { + root := NewRootCmd(nil) + _, _, err := root.Find([]string{"unknown_command"}) + assert.Error(t, err, "unknown command \"unknown_command\" for \"ecs\"") +} diff --git a/ecs/cmd/commands/setup.go b/ecs/cmd/commands/setup.go index dd6c40f61..6d52664f9 100644 --- a/ecs/cmd/commands/setup.go +++ b/ecs/cmd/commands/setup.go @@ -9,7 +9,6 @@ import ( "github.com/aws/aws-sdk-go/aws/awserr" "github.com/aws/aws-sdk-go/aws/credentials" "github.com/aws/aws-sdk-go/aws/defaults" - "github.com/docker/cli/cli-plugins/plugin" contextStore "github.com/docker/ecs-plugin/pkg/docker" "github.com/manifoldco/promptui" "github.com/spf13/cobra" @@ -42,11 +41,6 @@ func SetupCommand() *cobra.Command { cmd := &cobra.Command{ Use: "setup", Short: "", - PersistentPreRunE: func(cmd *cobra.Command, args []string) error { - //Override the root command PersistentPreRun - //We just need to initialize the top parent command - return plugin.PersistentPreRunE(cmd, args) - }, RunE: func(cmd *cobra.Command, args []string) error { if requiredFlag := opts.unsetRequiredArgs(); len(requiredFlag) > 0 { if err := interactiveCli(&opts); err != nil { diff --git a/ecs/cmd/commands/setup_test.go b/ecs/cmd/commands/setup_test.go new file mode 100644 index 000000000..97b7add38 --- /dev/null +++ b/ecs/cmd/commands/setup_test.go @@ -0,0 +1,32 @@ +package commands + +import ( + "io/ioutil" + "path/filepath" + "testing" + + "github.com/docker/cli/cli/config" + "gotest.tools/v3/assert" + "gotest.tools/v3/fs" + "gotest.tools/v3/golden" +) + +func TestDefaultAwsContextName(t *testing.T) { + dir := fs.NewDir(t, "setup") + defer dir.Remove() + cmd := NewRootCmd(nil) + dockerConfig := config.Dir() + config.SetDir(dir.Path()) + defer config.SetDir(dockerConfig) + + cmd.SetArgs([]string{"setup", "--cluster", "clusterName", "--profile", "profileName", "--region", "regionName"}) + err := cmd.Execute() + assert.NilError(t, err) + + files, err := filepath.Glob(dir.Join("contexts", "meta", "*", "meta.json")) + assert.NilError(t, err) + assert.Equal(t, len(files), 1) + b, err := ioutil.ReadFile(files[0]) + assert.NilError(t, err) + golden.Assert(t, string(b), "context.golden") +} diff --git a/ecs/cmd/commands/testdata/context.golden b/ecs/cmd/commands/testdata/context.golden new file mode 100644 index 000000000..891cb9cf8 --- /dev/null +++ b/ecs/cmd/commands/testdata/context.golden @@ -0,0 +1 @@ +{"Name":"aws","Metadata":{"Type":"aws"},"Endpoints":{"aws":{"Profile":"profileName","Cluster":"clusterName","Region":"regionName"},"docker":{"Profile":"profileName","Cluster":"clusterName","Region":"regionName"}}} \ No newline at end of file diff --git a/ecs/cmd/commands/version.go b/ecs/cmd/commands/version.go new file mode 100644 index 000000000..d3b44687c --- /dev/null +++ b/ecs/cmd/commands/version.go @@ -0,0 +1,20 @@ +package commands + +import ( + "fmt" + + "github.com/spf13/cobra" +) + +const Version = "0.0.1" + +func VersionCommand() *cobra.Command { + return &cobra.Command{ + Use: "version", + Short: "Show version.", + RunE: func(cmd *cobra.Command, args []string) error { + fmt.Fprintf(cmd.OutOrStdout(), "Docker ECS plugin %s\n", Version) + return nil + }, + } +} diff --git a/ecs/cmd/commands/version_test.go b/ecs/cmd/commands/version_test.go new file mode 100644 index 000000000..4c0ed7e69 --- /dev/null +++ b/ecs/cmd/commands/version_test.go @@ -0,0 +1,18 @@ +package commands + +import ( + "bytes" + "strings" + "testing" + + "gotest.tools/v3/assert" +) + +func TestVersion(t *testing.T) { + root := NewRootCmd(nil) + var out bytes.Buffer + root.SetOut(&out) + root.SetArgs([]string{"version"}) + root.Execute() + assert.Check(t, strings.Contains(out.String(), Version)) +} diff --git a/ecs/cmd/main/main.go b/ecs/cmd/main/main.go index 9af648292..5dd1f4dd1 100644 --- a/ecs/cmd/main/main.go +++ b/ecs/cmd/main/main.go @@ -1,60 +1,22 @@ package main import ( - "fmt" + "github.com/docker/ecs-plugin/cmd/commands" "github.com/docker/cli/cli-plugins/manager" "github.com/docker/cli/cli-plugins/plugin" "github.com/docker/cli/cli/command" - commands "github.com/docker/ecs-plugin/cmd/commands" "github.com/spf13/cobra" ) -const version = "0.0.1" - func main() { plugin.Run(func(dockerCli command.Cli) *cobra.Command { - cmd := NewRootCmd("ecs", dockerCli) + cmd := commands.NewRootCmd(dockerCli) return cmd }, manager.Metadata{ SchemaVersion: "0.1.0", Vendor: "Docker Inc.", - Version: version, + Version: commands.Version, Experimental: true, }) } - -// NewRootCmd returns the base root command. -func NewRootCmd(name string, dockerCli command.Cli) *cobra.Command { - cmd := &cobra.Command{ - Short: "Docker ECS", - Long: `run multi-container applications on Amazon ECS.`, - Use: name, - Annotations: map[string]string{"experimentalCLI": "true"}, - RunE: func(cmd *cobra.Command, args []string) error { - if len(args) != 0 { - return fmt.Errorf("%q is not a docker ecs command\nSee 'docker ecs --help'", args[0]) - } - cmd.Help() - return nil - }, - } - cmd.AddCommand( - VersionCommand(), - commands.ComposeCommand(dockerCli), - commands.SecretCommand(dockerCli), - commands.SetupCommand(), - ) - return cmd -} - -func VersionCommand() *cobra.Command { - return &cobra.Command{ - Use: "version", - Short: "Show version.", - RunE: func(cmd *cobra.Command, args []string) error { - fmt.Printf("Docker ECS plugin %s\n", version) - return nil - }, - } -} diff --git a/ecs/pkg/amazon/check_test.go b/ecs/pkg/amazon/check_test.go index 3f1859548..3038eee86 100644 --- a/ecs/pkg/amazon/check_test.go +++ b/ecs/pkg/amazon/check_test.go @@ -3,7 +3,7 @@ package amazon import ( "testing" - "gotest.tools/assert" + "gotest.tools/v3/assert" ) func TestInvalidNetworkMode(t *testing.T) { diff --git a/ecs/pkg/amazon/cloudformation_test.go b/ecs/pkg/amazon/cloudformation_test.go index 84e12e761..4b525d8fc 100644 --- a/ecs/pkg/amazon/cloudformation_test.go +++ b/ecs/pkg/amazon/cloudformation_test.go @@ -13,7 +13,7 @@ import ( "github.com/compose-spec/compose-go/loader" "github.com/compose-spec/compose-go/types" "github.com/docker/ecs-plugin/pkg/compose" - "gotest.tools/assert" + "gotest.tools/v3/assert" "gotest.tools/v3/golden" ) diff --git a/ecs/tests/command_test.go b/ecs/tests/command_test.go deleted file mode 100644 index 3ca7433d4..000000000 --- a/ecs/tests/command_test.go +++ /dev/null @@ -1,18 +0,0 @@ -package tests - -import ( - "testing" - - "gotest.tools/v3/icmd" -) - -func TestExitErrorCode(t *testing.T) { - cmd, cleanup, _ := dockerCli.createTestCmd() - defer cleanup() - - cmd.Command = dockerCli.Command("ecs", "unknown_command") - icmd.RunCmd(cmd).Assert(t, icmd.Expected{ - ExitCode: 1, - Err: "\"unknown_command\" is not a docker ecs command\nSee 'docker ecs --help'", - }) -} diff --git a/ecs/tests/e2e_deploy_services_test.go b/ecs/tests/e2e_deploy_services_test.go index c7002a19f..c8a2242ce 100644 --- a/ecs/tests/e2e_deploy_services_test.go +++ b/ecs/tests/e2e_deploy_services_test.go @@ -10,7 +10,7 @@ import ( "github.com/aws/aws-sdk-go/aws/session" "github.com/docker/ecs-plugin/pkg/amazon" "github.com/docker/ecs-plugin/pkg/docker" - "gotest.tools/assert" + "gotest.tools/v3/assert" "gotest.tools/v3/fs" "gotest.tools/v3/golden" "gotest.tools/v3/icmd" diff --git a/ecs/tests/setup_command_test.go b/ecs/tests/setup_command_test.go deleted file mode 100644 index d36393f48..000000000 --- a/ecs/tests/setup_command_test.go +++ /dev/null @@ -1,24 +0,0 @@ -package tests - -import ( - "strings" - "testing" - - "gotest.tools/assert" - "gotest.tools/v3/golden" - "gotest.tools/v3/icmd" -) - -func TestDefaultAwsContextName(t *testing.T) { - cmd, cleanup, _ := dockerCli.createTestCmd() - defer cleanup() - - cmd.Command = dockerCli.Command("ecs", "setup", "--cluster", "clusterName", "--profile", "profileName", - "--region", "regionName") - icmd.RunCmd(cmd).Assert(t, icmd.Success) - - cmd.Command = dockerCli.Command("context", "inspect", "aws") - output := icmd.RunCmd(cmd).Assert(t, icmd.Success).Combined() - expected := golden.Get(t, "context-inspect.golden") - assert.Assert(t, strings.HasPrefix(output, string(expected))) -} diff --git a/ecs/tests/testdata/context-inspect.golden b/ecs/tests/testdata/context-inspect.golden deleted file mode 100644 index ff61b55fc..000000000 --- a/ecs/tests/testdata/context-inspect.golden +++ /dev/null @@ -1,16 +0,0 @@ -[ - { - "Name": "aws", - "Metadata": {}, - "Endpoints": { - "aws": { - "Cluster": "clusterName", - "Profile": "profileName", - "Region": "regionName" - }, - "docker": { - "SkipTLSVerify": false - } - }, - "TLSMaterial": {}, - "Storage": \ No newline at end of file From 0f1a362664edfc927f6d3b811fecff4175a47ca7 Mon Sep 17 00:00:00 2001 From: Nicolas De Loof <nicolas.deloof@gmail.com> Date: Mon, 15 Jun 2020 09:27:12 +0200 Subject: [PATCH 122/198] Set FailureThreshold Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com> --- ecs/pkg/amazon/cloudformation.go | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/ecs/pkg/amazon/cloudformation.go b/ecs/pkg/amazon/cloudformation.go index 6afa06c3d..51eba2c41 100644 --- a/ecs/pkg/amazon/cloudformation.go +++ b/ecs/pkg/amazon/cloudformation.go @@ -298,8 +298,11 @@ func (c client) createServiceRegistry(service types.ServiceConfig, template *clo template.Resources[serviceRegistration] = &cloudmap.Service{ Description: fmt.Sprintf("%q service discovery entry in Cloud Map", service.Name), HealthCheckConfig: healthCheck, - Name: service.Name, - NamespaceId: cloudformation.Ref("CloudMap"), + HealthCheckCustomConfig: &cloudmap.Service_HealthCheckCustomConfig{ + FailureThreshold: 1, + }, + Name: service.Name, + NamespaceId: cloudformation.Ref("CloudMap"), DnsConfig: &cloudmap.Service_DnsConfig{ DnsRecords: []cloudmap.Service_DnsRecord{ { From d36b9b104e0576e79ebb787d4cadf9e6a6e7d74f Mon Sep 17 00:00:00 2001 From: Nicolas De Loof <nicolas.deloof@gmail.com> Date: Mon, 15 Jun 2020 09:41:01 +0200 Subject: [PATCH 123/198] Fix broken master Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com> --- .../testdata/simple/simple-cloudformation-conversion.golden | 3 +++ .../simple-cloudformation-with-overrides-conversion.golden | 3 +++ 2 files changed, 6 insertions(+) diff --git a/ecs/pkg/amazon/testdata/simple/simple-cloudformation-conversion.golden b/ecs/pkg/amazon/testdata/simple/simple-cloudformation-conversion.golden index 7c7af6781..0003b38e5 100644 --- a/ecs/pkg/amazon/testdata/simple/simple-cloudformation-conversion.golden +++ b/ecs/pkg/amazon/testdata/simple/simple-cloudformation-conversion.golden @@ -143,6 +143,9 @@ ], "RoutingPolicy": "MULTIVALUE" }, + "HealthCheckCustomConfig": { + "FailureThreshold": 1 + }, "Name": "simple", "NamespaceId": { "Ref": "CloudMap" diff --git a/ecs/pkg/amazon/testdata/simple/simple-cloudformation-with-overrides-conversion.golden b/ecs/pkg/amazon/testdata/simple/simple-cloudformation-with-overrides-conversion.golden index e9535e908..07c47765b 100644 --- a/ecs/pkg/amazon/testdata/simple/simple-cloudformation-with-overrides-conversion.golden +++ b/ecs/pkg/amazon/testdata/simple/simple-cloudformation-with-overrides-conversion.golden @@ -143,6 +143,9 @@ ], "RoutingPolicy": "MULTIVALUE" }, + "HealthCheckCustomConfig": { + "FailureThreshold": 1 + }, "Name": "simple", "NamespaceId": { "Ref": "CloudMap" From bb98dae0829a3cc0804d24cb5c5785503a5bbd6c Mon Sep 17 00:00:00 2001 From: aiordache <anca.iordache@docker.com> Date: Thu, 11 Jun 2020 19:22:12 +0200 Subject: [PATCH 124/198] code restructure Signed-off-by: aiordache <anca.iordache@docker.com> Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com> --- ecs/cmd/commands/compose.go | 36 ++++-- ecs/cmd/commands/secret.go | 23 ++-- ecs/pkg/amazon/amazon.go | 8 ++ ecs/pkg/amazon/api.go | 11 -- .../amazon/{client.go => backend/backend.go} | 16 ++- .../amazon/{ => backend}/cloudformation.go | 56 +++++---- .../{ => backend}/cloudformation_test.go | 7 +- ecs/pkg/amazon/backend/down.go | 31 +++++ ecs/pkg/amazon/{ => backend}/down_test.go | 16 +-- ecs/pkg/amazon/{ => backend}/iam.go | 2 +- ecs/pkg/amazon/backend/list.go | 63 ++++++++++ ecs/pkg/amazon/{ => backend}/logs.go | 20 +-- ecs/pkg/amazon/backend/secrets.go | 23 ++++ .../simple-single-service-with-overrides.yaml | 0 .../testdata/input/simple-single-service.yaml | 0 .../testdata/invalid_network_mode.yaml | 0 .../simple-cloudformation-conversion.golden | 0 ...formation-with-overrides-conversion.golden | 0 ecs/pkg/amazon/backend/up.go | 100 +++++++++++++++ ecs/pkg/amazon/{ => backend}/wait.go | 22 +--- ecs/pkg/amazon/check_test.go | 13 -- ecs/pkg/amazon/{ => compatibility}/check.go | 2 +- ecs/pkg/amazon/compatibility/check_test.go | 23 ++++ .../{ => compatibility}/compatibility.go | 2 +- ecs/pkg/amazon/down.go | 34 ------ ecs/pkg/amazon/list.go | 80 ------------ ecs/pkg/amazon/sdk/api.go | 61 ++++++++++ ecs/pkg/amazon/{ => sdk}/api_mock.go | 20 +-- ecs/pkg/amazon/{ => sdk}/convert.go | 5 +- ecs/pkg/amazon/{ => sdk}/sdk.go | 33 ++--- ecs/pkg/amazon/secrets.go | 30 ----- .../secret.go => amazon/types/types.go} | 22 +++- ecs/pkg/amazon/{ => types}/x.go | 2 +- ecs/pkg/amazon/up.go | 114 ------------------ ecs/pkg/compose/api.go | 10 +- 35 files changed, 464 insertions(+), 421 deletions(-) create mode 100644 ecs/pkg/amazon/amazon.go delete mode 100644 ecs/pkg/amazon/api.go rename ecs/pkg/amazon/{client.go => backend/backend.go} (66%) rename ecs/pkg/amazon/{ => backend}/cloudformation.go (85%) rename ecs/pkg/amazon/{ => backend}/cloudformation_test.go (97%) create mode 100644 ecs/pkg/amazon/backend/down.go rename ecs/pkg/amazon/{ => backend}/down_test.go (73%) rename ecs/pkg/amazon/{ => backend}/iam.go (98%) create mode 100644 ecs/pkg/amazon/backend/list.go rename ecs/pkg/amazon/{ => backend}/logs.go (73%) create mode 100644 ecs/pkg/amazon/backend/secrets.go rename ecs/pkg/amazon/{ => backend}/testdata/input/simple-single-service-with-overrides.yaml (100%) rename ecs/pkg/amazon/{ => backend}/testdata/input/simple-single-service.yaml (100%) rename ecs/pkg/amazon/{ => backend}/testdata/invalid_network_mode.yaml (100%) rename ecs/pkg/amazon/{ => backend}/testdata/simple/simple-cloudformation-conversion.golden (100%) rename ecs/pkg/amazon/{ => backend}/testdata/simple/simple-cloudformation-with-overrides-conversion.golden (100%) create mode 100644 ecs/pkg/amazon/backend/up.go rename ecs/pkg/amazon/{ => backend}/wait.go (67%) delete mode 100644 ecs/pkg/amazon/check_test.go rename ecs/pkg/amazon/{ => compatibility}/check.go (98%) create mode 100644 ecs/pkg/amazon/compatibility/check_test.go rename ecs/pkg/amazon/{ => compatibility}/compatibility.go (99%) delete mode 100644 ecs/pkg/amazon/down.go delete mode 100644 ecs/pkg/amazon/list.go create mode 100644 ecs/pkg/amazon/sdk/api.go rename ecs/pkg/amazon/{ => sdk}/api_mock.go (96%) rename ecs/pkg/amazon/{ => sdk}/convert.go (98%) rename ecs/pkg/amazon/{ => sdk}/sdk.go (94%) delete mode 100644 ecs/pkg/amazon/secrets.go rename ecs/pkg/{docker/secret.go => amazon/types/types.go} (71%) rename ecs/pkg/amazon/{ => types}/x.go (93%) delete mode 100644 ecs/pkg/amazon/up.go diff --git a/ecs/cmd/commands/compose.go b/ecs/cmd/commands/compose.go index 0b1e70e65..55eb7a148 100644 --- a/ecs/cmd/commands/compose.go +++ b/ecs/cmd/commands/compose.go @@ -3,9 +3,12 @@ package commands import ( "context" "fmt" + "io" + "os" + "strings" "github.com/docker/cli/cli/command" - "github.com/docker/ecs-plugin/pkg/amazon" + amazon "github.com/docker/ecs-plugin/pkg/amazon/backend" "github.com/docker/ecs-plugin/pkg/compose" "github.com/docker/ecs-plugin/pkg/docker" "github.com/spf13/cobra" @@ -47,11 +50,11 @@ func ConvertCommand(dockerCli command.Cli, projectOpts *compose.ProjectOptions) if err != nil { return err } - client, err := amazon.NewClient(clusteropts.Profile, clusteropts.Cluster, clusteropts.Region) + backend, err := amazon.NewBackend(clusteropts.Profile, clusteropts.Cluster, clusteropts.Region) if err != nil { return err } - template, err := client.Convert(project) + template, err := backend.Convert(project) if err != nil { return err } @@ -77,11 +80,11 @@ func UpCommand(dockerCli command.Cli, projectOpts *compose.ProjectOptions) *cobr if err != nil { return err } - client, err := amazon.NewClient(clusteropts.Profile, clusteropts.Cluster, clusteropts.Region) + backend, err := amazon.NewBackend(clusteropts.Profile, clusteropts.Cluster, clusteropts.Region) if err != nil { return err } - return client.ComposeUp(context.Background(), project) + return backend.ComposeUp(context.Background(), project) }), } cmd.Flags().StringVar(&opts.loadBalancerArn, "load-balancer", "", "") @@ -97,11 +100,20 @@ func PsCommand(dockerCli command.Cli, projectOpts *compose.ProjectOptions) *cobr if err != nil { return err } - client, err := amazon.NewClient(clusteropts.Profile, clusteropts.Cluster, clusteropts.Region) + backend, err := amazon.NewBackend(clusteropts.Profile, clusteropts.Cluster, clusteropts.Region) if err != nil { return err } - return client.ComposePs(context.Background(), project) + tasks, err := backend.ComposePs(context.Background(), project) + if err != nil { + return err + } + printSection(os.Stdout, len(tasks), func(w io.Writer) { + for _, task := range tasks { + fmt.Fprintf(w, "%s\t%s\t%s\n", task.Name, task.State, strings.Join(task.Ports, " ")) + } + }, "NAME", "STATE", "PORTS") + return nil }), } cmd.Flags().StringVar(&opts.loadBalancerArn, "load-balancer", "", "") @@ -117,7 +129,7 @@ func DownCommand(dockerCli command.Cli, projectOpts *compose.ProjectOptions) *co cmd := &cobra.Command{ Use: "down", RunE: docker.WithAwsContext(dockerCli, func(clusteropts docker.AwsContext, args []string) error { - client, err := amazon.NewClient(clusteropts.Profile, clusteropts.Cluster, clusteropts.Region) + backend, err := amazon.NewBackend(clusteropts.Profile, clusteropts.Cluster, clusteropts.Region) if err != nil { return err } @@ -126,11 +138,11 @@ func DownCommand(dockerCli command.Cli, projectOpts *compose.ProjectOptions) *co if err != nil { return err } - return client.ComposeDown(context.Background(), project.Name, opts.DeleteCluster) + return backend.ComposeDown(context.Background(), project.Name, opts.DeleteCluster) } // project names passed as parameters for _, name := range args { - err := client.ComposeDown(context.Background(), name, opts.DeleteCluster) + err := backend.ComposeDown(context.Background(), name, opts.DeleteCluster) if err != nil { return err } @@ -146,7 +158,7 @@ func LogsCommand(dockerCli command.Cli, projectOpts *compose.ProjectOptions) *co cmd := &cobra.Command{ Use: "logs [PROJECT NAME]", RunE: docker.WithAwsContext(dockerCli, func(clusteropts docker.AwsContext, args []string) error { - client, err := amazon.NewClient(clusteropts.Profile, clusteropts.Cluster, clusteropts.Region) + backend, err := amazon.NewBackend(clusteropts.Profile, clusteropts.Cluster, clusteropts.Region) if err != nil { return err } @@ -161,7 +173,7 @@ func LogsCommand(dockerCli command.Cli, projectOpts *compose.ProjectOptions) *co } else { name = args[0] } - return client.ComposeLogs(context.Background(), name) + return backend.ComposeLogs(context.Background(), name) }), } return cmd diff --git a/ecs/cmd/commands/secret.go b/ecs/cmd/commands/secret.go index f964f1eae..a426740df 100644 --- a/ecs/cmd/commands/secret.go +++ b/ecs/cmd/commands/secret.go @@ -10,7 +10,8 @@ import ( "text/tabwriter" "github.com/docker/cli/cli/command" - "github.com/docker/ecs-plugin/pkg/amazon" + amazon "github.com/docker/ecs-plugin/pkg/amazon/backend" + "github.com/docker/ecs-plugin/pkg/amazon/types" "github.com/docker/ecs-plugin/pkg/docker" "github.com/spf13/cobra" ) @@ -47,7 +48,7 @@ func CreateSecret(dockerCli command.Cli) *cobra.Command { Use: "create NAME", Short: "Creates a secret.", RunE: docker.WithAwsContext(dockerCli, func(clusteropts docker.AwsContext, args []string) error { - client, err := amazon.NewClient(clusteropts.Profile, clusteropts.Cluster, clusteropts.Region) + backend, err := amazon.NewBackend(clusteropts.Profile, clusteropts.Cluster, clusteropts.Region) if err != nil { return err } @@ -56,8 +57,8 @@ func CreateSecret(dockerCli command.Cli) *cobra.Command { } name := args[0] - secret := docker.NewSecret(name, opts.Username, opts.Password, opts.Description) - id, err := client.CreateSecret(context.Background(), secret) + secret := types.NewSecret(name, opts.Username, opts.Password, opts.Description) + id, err := backend.CreateSecret(context.Background(), secret) fmt.Println(id) return err }), @@ -73,7 +74,7 @@ func InspectSecret(dockerCli command.Cli) *cobra.Command { Use: "inspect ID", Short: "Displays secret details", RunE: docker.WithAwsContext(dockerCli, func(clusteropts docker.AwsContext, args []string) error { - client, err := amazon.NewClient(clusteropts.Profile, clusteropts.Cluster, clusteropts.Region) + backend, err := amazon.NewBackend(clusteropts.Profile, clusteropts.Cluster, clusteropts.Region) if err != nil { return err } @@ -81,7 +82,7 @@ func InspectSecret(dockerCli command.Cli) *cobra.Command { return errors.New("Missing mandatory parameter: ID") } id := args[0] - secret, err := client.InspectSecret(context.Background(), id) + secret, err := backend.InspectSecret(context.Background(), id) if err != nil { return err } @@ -102,11 +103,11 @@ func ListSecrets(dockerCli command.Cli) *cobra.Command { Aliases: []string{"ls"}, Short: "List secrets stored for the existing account.", RunE: docker.WithAwsContext(dockerCli, func(clusteropts docker.AwsContext, args []string) error { - client, err := amazon.NewClient(clusteropts.Profile, clusteropts.Cluster, clusteropts.Region) + backend, err := amazon.NewBackend(clusteropts.Profile, clusteropts.Cluster, clusteropts.Region) if err != nil { return err } - secrets, err := client.ListSecrets(context.Background()) + secrets, err := backend.ListSecrets(context.Background()) if err != nil { return err } @@ -125,21 +126,21 @@ func DeleteSecret(dockerCli command.Cli) *cobra.Command { Aliases: []string{"rm", "remove"}, Short: "Removes a secret.", RunE: docker.WithAwsContext(dockerCli, func(clusteropts docker.AwsContext, args []string) error { - client, err := amazon.NewClient(clusteropts.Profile, clusteropts.Cluster, clusteropts.Region) + backend, err := amazon.NewBackend(clusteropts.Profile, clusteropts.Cluster, clusteropts.Region) if err != nil { return err } if len(args) == 0 { return errors.New("Missing mandatory parameter: [NAME]") } - return client.DeleteSecret(context.Background(), args[0], opts.recover) + return backend.DeleteSecret(context.Background(), args[0], opts.recover) }), } cmd.Flags().BoolVar(&opts.recover, "recover", false, "Enable recovery.") return cmd } -func printList(out io.Writer, secrets []docker.Secret) { +func printList(out io.Writer, secrets []types.Secret) { printSection(out, len(secrets), func(w io.Writer) { for _, secret := range secrets { fmt.Fprintf(w, "%s\t%s\t%s\n", secret.ID, secret.Name, secret.Description) diff --git a/ecs/pkg/amazon/amazon.go b/ecs/pkg/amazon/amazon.go new file mode 100644 index 000000000..f6c588d11 --- /dev/null +++ b/ecs/pkg/amazon/amazon.go @@ -0,0 +1,8 @@ +package amazon + +import ( + "github.com/docker/ecs-plugin/pkg/amazon/backend" + "github.com/docker/ecs-plugin/pkg/compose" +) + +var _ compose.API = &backend.Backend{} diff --git a/ecs/pkg/amazon/api.go b/ecs/pkg/amazon/api.go deleted file mode 100644 index 5b058584a..000000000 --- a/ecs/pkg/amazon/api.go +++ /dev/null @@ -1,11 +0,0 @@ -package amazon - -//go:generate mockgen -destination=./api_mock.go -self_package "github.com/docker/ecs-plugin/pkg/amazon" -package=amazon . API - -type API interface { - downAPI - upAPI - logsAPI - secretsAPI - listAPI -} diff --git a/ecs/pkg/amazon/client.go b/ecs/pkg/amazon/backend/backend.go similarity index 66% rename from ecs/pkg/amazon/client.go rename to ecs/pkg/amazon/backend/backend.go index 839c5118e..0d2f07f98 100644 --- a/ecs/pkg/amazon/client.go +++ b/ecs/pkg/amazon/backend/backend.go @@ -1,9 +1,9 @@ -package amazon +package backend import ( "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws/session" - "github.com/docker/ecs-plugin/pkg/compose" + "github.com/docker/ecs-plugin/pkg/amazon/sdk" ) const ( @@ -12,7 +12,7 @@ const ( ServiceTag = "com.docker.compose.service" ) -func NewClient(profile string, cluster string, region string) (compose.API, error) { +func NewBackend(profile string, cluster string, region string) (*Backend, error) { sess, err := session.NewSessionWithOptions(session.Options{ Profile: profile, Config: aws.Config{ @@ -22,17 +22,15 @@ func NewClient(profile string, cluster string, region string) (compose.API, erro if err != nil { return nil, err } - return &client{ + return &Backend{ Cluster: cluster, Region: region, - api: NewAPI(sess), + api: sdk.NewAPI(sess), }, nil } -type client struct { +type Backend struct { Cluster string Region string - api API + api sdk.API } - -var _ compose.API = &client{} diff --git a/ecs/pkg/amazon/cloudformation.go b/ecs/pkg/amazon/backend/cloudformation.go similarity index 85% rename from ecs/pkg/amazon/cloudformation.go rename to ecs/pkg/amazon/backend/cloudformation.go index 51eba2c41..115a67d64 100644 --- a/ecs/pkg/amazon/cloudformation.go +++ b/ecs/pkg/amazon/backend/cloudformation.go @@ -1,4 +1,4 @@ -package amazon +package backend import ( "fmt" @@ -21,6 +21,9 @@ import ( "github.com/awslabs/goformation/v4/cloudformation/logs" cloudmap "github.com/awslabs/goformation/v4/cloudformation/servicediscovery" "github.com/awslabs/goformation/v4/cloudformation/tags" + "github.com/docker/ecs-plugin/pkg/amazon/compatibility" + sdk "github.com/docker/ecs-plugin/pkg/amazon/sdk" + btypes "github.com/docker/ecs-plugin/pkg/amazon/types" "github.com/docker/ecs-plugin/pkg/compose" ) @@ -33,8 +36,8 @@ const ( ) // Convert a compose project into a CloudFormation template -func (c client) Convert(project *compose.Project) (*cloudformation.Template, error) { - warnings := Check(project) +func (b Backend) Convert(project *compose.Project) (*cloudformation.Template, error) { + warnings := compatibility.Check(project) for _, w := range warnings { logrus.Warn(w) } @@ -75,7 +78,7 @@ func (c client) Convert(project *compose.Project) (*cloudformation.Template, err // Create Cluster is `ParameterClusterName` parameter is not set template.Conditions["CreateCluster"] = cloudformation.Equals("", cloudformation.Ref(ParameterClusterName)) - cluster := c.createCluster(project, template) + cluster := createCluster(project, template) networks := map[string]string{} for _, net := range project.Networks { @@ -88,17 +91,18 @@ func (c client) Convert(project *compose.Project) (*cloudformation.Template, err } // Private DNS namespace will allow DNS name for the services to be <service>.<project>.local - c.createCloudMap(project, template) + createCloudMap(project, template) - loadBalancerARN := c.createLoadBalancer(project, template) + loadBalancerARN := createLoadBalancer(project, template) for _, service := range project.Services { - definition, err := Convert(project, service) + + definition, err := sdk.Convert(project, service) if err != nil { return nil, err } - taskExecutionRole, err := c.createTaskExecutionRole(service, err, definition, template) + taskExecutionRole, err := createTaskExecutionRole(service, err, definition, template) if err != nil { return template, err } @@ -112,7 +116,7 @@ func (c client) Convert(project *compose.Project) (*cloudformation.Template, err // FIXME ECS only support HTTP(s) health checks, while Docker only support CMD } - serviceRegistry := c.createServiceRegistry(service, template, healthCheck) + serviceRegistry := createServiceRegistry(service, template, healthCheck) serviceSecurityGroups := []string{} for net := range service.Networks { @@ -124,14 +128,14 @@ func (c client) Convert(project *compose.Project) (*cloudformation.Template, err if len(service.Ports) > 0 { for _, port := range service.Ports { protocol := strings.ToUpper(port.Protocol) - if c.getLoadBalancerType(project) == elbv2.LoadBalancerTypeEnumApplication { + if getLoadBalancerType(project) == elbv2.LoadBalancerTypeEnumApplication { protocol = elbv2.ProtocolEnumHttps if port.Published == 80 { protocol = elbv2.ProtocolEnumHttp } } - targetGroupName := c.createTargetGroup(project, service, port, template, protocol) - listenerName := c.createListener(service, port, template, targetGroupName, loadBalancerARN, protocol) + targetGroupName := createTargetGroup(project, service, port, template, protocol) + listenerName := createListener(service, port, template, targetGroupName, loadBalancerARN, protocol) dependsOn = append(dependsOn, listenerName) serviceLB = append(serviceLB, ecs.Service_LoadBalancer{ ContainerName: service.Name, @@ -184,7 +188,7 @@ func (c client) Convert(project *compose.Project) (*cloudformation.Template, err return template, nil } -func (c client) getLoadBalancerType(project *compose.Project) string { +func getLoadBalancerType(project *compose.Project) string { for _, service := range project.Services { for _, port := range service.Ports { if port.Published != 80 && port.Published != 443 { @@ -195,7 +199,7 @@ func (c client) getLoadBalancerType(project *compose.Project) string { return elbv2.LoadBalancerTypeEnumApplication } -func (c client) getLoadBalancerSecurityGroups(project *compose.Project, template *cloudformation.Template) []string { +func getLoadBalancerSecurityGroups(project *compose.Project, template *cloudformation.Template) []string { securityGroups := []string{} for _, network := range project.Networks { if !network.Internal { @@ -206,15 +210,15 @@ func (c client) getLoadBalancerSecurityGroups(project *compose.Project, template return uniqueStrings(securityGroups) } -func (c client) createLoadBalancer(project *compose.Project, template *cloudformation.Template) string { +func createLoadBalancer(project *compose.Project, template *cloudformation.Template) string { loadBalancerName := fmt.Sprintf("%sLoadBalancer", strings.Title(project.Name)) // Create LoadBalancer if `ParameterLoadBalancerName` is not set template.Conditions["CreateLoadBalancer"] = cloudformation.Equals("", cloudformation.Ref(ParameterLoadBalancerARN)) - loadBalancerType := c.getLoadBalancerType(project) + loadBalancerType := getLoadBalancerType(project) securityGroups := []string{} if loadBalancerType == elbv2.LoadBalancerTypeEnumApplication { - securityGroups = c.getLoadBalancerSecurityGroups(project, template) + securityGroups = getLoadBalancerSecurityGroups(project, template) } template.Resources[loadBalancerName] = &elasticloadbalancingv2.LoadBalancer{ @@ -237,7 +241,7 @@ func (c client) createLoadBalancer(project *compose.Project, template *cloudform return cloudformation.If("CreateLoadBalancer", cloudformation.Ref(loadBalancerName), cloudformation.Ref(ParameterLoadBalancerARN)) } -func (c client) createListener(service types.ServiceConfig, port types.ServicePortConfig, template *cloudformation.Template, targetGroupName string, loadBalancerARN string, protocol string) string { +func createListener(service types.ServiceConfig, port types.ServicePortConfig, template *cloudformation.Template, targetGroupName string, loadBalancerARN string, protocol string) string { listenerName := fmt.Sprintf( "%s%s%dListener", normalizeResourceName(service.Name), @@ -266,7 +270,7 @@ func (c client) createListener(service types.ServiceConfig, port types.ServicePo return listenerName } -func (c client) createTargetGroup(project *compose.Project, service types.ServiceConfig, port types.ServicePortConfig, template *cloudformation.Template, protocol string) string { +func createTargetGroup(project *compose.Project, service types.ServiceConfig, port types.ServicePortConfig, template *cloudformation.Template, protocol string) string { targetGroupName := fmt.Sprintf( "%s%s%dTargetGroup", normalizeResourceName(service.Name), @@ -289,7 +293,7 @@ func (c client) createTargetGroup(project *compose.Project, service types.Servic return targetGroupName } -func (c client) createServiceRegistry(service types.ServiceConfig, template *cloudformation.Template, healthCheck *cloudmap.Service_HealthCheckConfig) ecs.Service_ServiceRegistry { +func createServiceRegistry(service types.ServiceConfig, template *cloudformation.Template, healthCheck *cloudmap.Service_HealthCheckConfig) ecs.Service_ServiceRegistry { serviceRegistration := fmt.Sprintf("%sServiceDiscoveryEntry", normalizeResourceName(service.Name)) serviceRegistry := ecs.Service_ServiceRegistry{ RegistryArn: cloudformation.GetAtt(serviceRegistration, "Arn"), @@ -316,9 +320,9 @@ func (c client) createServiceRegistry(service types.ServiceConfig, template *clo return serviceRegistry } -func (c client) createTaskExecutionRole(service types.ServiceConfig, err error, definition *ecs.TaskDefinition, template *cloudformation.Template) (string, error) { +func createTaskExecutionRole(service types.ServiceConfig, err error, definition *ecs.TaskDefinition, template *cloudformation.Template) (string, error) { taskExecutionRole := fmt.Sprintf("%sTaskExecutionRole", normalizeResourceName(service.Name)) - policy, err := c.getPolicy(definition) + policy, err := getPolicy(definition) if err != nil { return taskExecutionRole, err } @@ -341,7 +345,7 @@ func (c client) createTaskExecutionRole(service types.ServiceConfig, err error, return taskExecutionRole, nil } -func (c client) createCluster(project *compose.Project, template *cloudformation.Template) string { +func createCluster(project *compose.Project, template *cloudformation.Template) string { template.Resources["Cluster"] = &ecs.Cluster{ ClusterName: project.Name, Tags: []tags.Tag{ @@ -356,7 +360,7 @@ func (c client) createCluster(project *compose.Project, template *cloudformation return cluster } -func (c client) createCloudMap(project *compose.Project, template *cloudformation.Template) { +func createCloudMap(project *compose.Project, template *cloudformation.Template) { template.Resources["CloudMap"] = &cloudmap.PrivateDnsNamespace{ Description: fmt.Sprintf("Service Map for Docker Compose project %s", project.Name), Name: fmt.Sprintf("%s.local", project.Name), @@ -365,7 +369,7 @@ func (c client) createCloudMap(project *compose.Project, template *cloudformatio } func convertNetwork(project *compose.Project, net types.NetworkConfig, vpc string, template *cloudformation.Template) string { - if sg, ok := net.Extras[ExtensionSecurityGroup]; ok { + if sg, ok := net.Extras[btypes.ExtensionSecurityGroup]; ok { logrus.Debugf("Security Group for network %q set by user to %q", net.Name, sg) return sg.(string) } @@ -428,7 +432,7 @@ func normalizeResourceName(s string) string { return strings.Title(regexp.MustCompile("[^a-zA-Z0-9]+").ReplaceAllString(s, "")) } -func (c client) getPolicy(taskDef *ecs.TaskDefinition) (*PolicyDocument, error) { +func getPolicy(taskDef *ecs.TaskDefinition) (*PolicyDocument, error) { arns := []string{} for _, container := range taskDef.ContainerDefinitions { if container.RepositoryCredentials != nil { diff --git a/ecs/pkg/amazon/cloudformation_test.go b/ecs/pkg/amazon/backend/cloudformation_test.go similarity index 97% rename from ecs/pkg/amazon/cloudformation_test.go rename to ecs/pkg/amazon/backend/cloudformation_test.go index 4b525d8fc..122714781 100644 --- a/ecs/pkg/amazon/cloudformation_test.go +++ b/ecs/pkg/amazon/backend/cloudformation_test.go @@ -1,4 +1,4 @@ -package amazon +package backend import ( "fmt" @@ -13,6 +13,7 @@ import ( "github.com/compose-spec/compose-go/loader" "github.com/compose-spec/compose-go/types" "github.com/docker/ecs-plugin/pkg/compose" + "gotest.tools/v3/assert" "gotest.tools/v3/golden" ) @@ -103,7 +104,7 @@ services: } func convertResultAsString(t *testing.T, project *compose.Project, clusterName string) string { - client, err := NewClient("", clusterName, "") + client, err := NewBackend("", clusterName, "") assert.NilError(t, err) result, err := client.Convert(project) assert.NilError(t, err) @@ -133,7 +134,7 @@ func convertYaml(t *testing.T, yaml string) *cloudformation.Template { assert.NilError(t, err) err = compose.Normalize(model) assert.NilError(t, err) - template, err := client{}.Convert(&compose.Project{ + template, err := Backend{}.Convert(&compose.Project{ Config: *model, Name: "test", }) diff --git a/ecs/pkg/amazon/backend/down.go b/ecs/pkg/amazon/backend/down.go new file mode 100644 index 000000000..8b978e796 --- /dev/null +++ b/ecs/pkg/amazon/backend/down.go @@ -0,0 +1,31 @@ +package backend + +import ( + "context" + "fmt" + + "github.com/docker/ecs-plugin/pkg/amazon/types" +) + +func (b *Backend) ComposeDown(ctx context.Context, projectName string, deleteCluster bool) error { + err := b.api.DeleteStack(ctx, projectName) + if err != nil { + return err + } + + err = b.WaitStackCompletion(ctx, projectName, types.StackDelete) + if err != nil { + return err + } + + if !deleteCluster { + return nil + } + + fmt.Printf("Delete cluster %s", b.Cluster) + if err = b.api.DeleteCluster(ctx, b.Cluster); err != nil { + return err + } + fmt.Printf("... done. \n") + return nil +} diff --git a/ecs/pkg/amazon/down_test.go b/ecs/pkg/amazon/backend/down_test.go similarity index 73% rename from ecs/pkg/amazon/down_test.go rename to ecs/pkg/amazon/backend/down_test.go index 642faf759..d9abf70ca 100644 --- a/ecs/pkg/amazon/down_test.go +++ b/ecs/pkg/amazon/backend/down_test.go @@ -1,17 +1,19 @@ -package amazon +package backend import ( "context" "testing" + "github.com/docker/ecs-plugin/pkg/amazon/sdk" + btypes "github.com/docker/ecs-plugin/pkg/amazon/types" "github.com/golang/mock/gomock" ) func TestDownDontDeleteCluster(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() - m := NewMockAPI(ctrl) - c := &client{ + m := sdk.NewMockAPI(ctrl) + c := &Backend{ Cluster: "test_cluster", Region: "region", api: m, @@ -20,7 +22,7 @@ func TestDownDontDeleteCluster(t *testing.T) { recorder := m.EXPECT() recorder.DeleteStack(ctx, "test_project").Return(nil) recorder.GetStackID(ctx, "test_project").Return("stack-123", nil) - recorder.WaitStackComplete(ctx, "stack-123", StackDelete).Return(nil) + recorder.WaitStackComplete(ctx, "stack-123", btypes.StackDelete).Return(nil) recorder.DescribeStackEvents(ctx, "stack-123").Return(nil, nil) c.ComposeDown(ctx, "test_project", false) @@ -29,8 +31,8 @@ func TestDownDontDeleteCluster(t *testing.T) { func TestDownDeleteCluster(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() - m := NewMockAPI(ctrl) - c := &client{ + m := sdk.NewMockAPI(ctrl) + c := &Backend{ Cluster: "test_cluster", Region: "region", api: m, @@ -40,7 +42,7 @@ func TestDownDeleteCluster(t *testing.T) { recorder := m.EXPECT() recorder.DeleteStack(ctx, "test_project").Return(nil) recorder.GetStackID(ctx, "test_project").Return("stack-123", nil) - recorder.WaitStackComplete(ctx, "stack-123", StackDelete).Return(nil) + recorder.WaitStackComplete(ctx, "stack-123", btypes.StackDelete).Return(nil) recorder.DescribeStackEvents(ctx, "stack-123").Return(nil, nil) recorder.DeleteCluster(ctx, "test_cluster").Return(nil) diff --git a/ecs/pkg/amazon/iam.go b/ecs/pkg/amazon/backend/iam.go similarity index 98% rename from ecs/pkg/amazon/iam.go rename to ecs/pkg/amazon/backend/iam.go index affcaaaef..81a4fdb0f 100644 --- a/ecs/pkg/amazon/iam.go +++ b/ecs/pkg/amazon/backend/iam.go @@ -1,4 +1,4 @@ -package amazon +package backend const ( ECSTaskExecutionPolicy = "arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy" diff --git a/ecs/pkg/amazon/backend/list.go b/ecs/pkg/amazon/backend/list.go new file mode 100644 index 000000000..055362aa8 --- /dev/null +++ b/ecs/pkg/amazon/backend/list.go @@ -0,0 +1,63 @@ +package backend + +import ( + "context" + "fmt" + "sort" + "strings" + + "github.com/docker/ecs-plugin/pkg/amazon/types" + "github.com/docker/ecs-plugin/pkg/compose" +) + +func (b *Backend) ComposePs(ctx context.Context, project *compose.Project) ([]types.TaskStatus, error) { + cluster := b.Cluster + if cluster == "" { + cluster = project.Name + } + arns := []string{} + for _, service := range project.Services { + tasks, err := b.api.ListTasks(ctx, cluster, service.Name) + if err != nil { + return []types.TaskStatus{}, err + } + arns = append(arns, tasks...) + } + if len(arns) == 0 { + return []types.TaskStatus{}, nil + } + + tasks, err := b.api.DescribeTasks(ctx, cluster, arns...) + if err != nil { + return []types.TaskStatus{}, err + } + + networkInterfaces := []string{} + for _, t := range tasks { + if t.NetworkInterface != "" { + networkInterfaces = append(networkInterfaces, t.NetworkInterface) + } + } + publicIps, err := b.api.GetPublicIPs(ctx, networkInterfaces...) + if err != nil { + return []types.TaskStatus{}, err + } + + sort.Slice(tasks, func(i, j int) bool { + return strings.Compare(tasks[i].Service, tasks[j].Service) < 0 + }) + + for i, t := range tasks { + ports := []string{} + s, err := project.GetService(t.Service) + if err != nil { + return []types.TaskStatus{}, err + } + for _, p := range s.Ports { + ports = append(ports, fmt.Sprintf("%s:%d->%d/%s", publicIps[t.NetworkInterface], p.Published, p.Target, p.Protocol)) + } + tasks[i].Name = s.Name + tasks[i].Ports = ports + } + return tasks, nil +} diff --git a/ecs/pkg/amazon/logs.go b/ecs/pkg/amazon/backend/logs.go similarity index 73% rename from ecs/pkg/amazon/logs.go rename to ecs/pkg/amazon/backend/logs.go index 683ecc260..179635290 100644 --- a/ecs/pkg/amazon/logs.go +++ b/ecs/pkg/amazon/backend/logs.go @@ -1,4 +1,4 @@ -package amazon +package backend import ( "context" @@ -11,8 +11,8 @@ import ( "github.com/docker/ecs-plugin/pkg/console" ) -func (c *client) ComposeLogs(ctx context.Context, projectName string) error { - err := c.api.GetLogs(ctx, projectName, &logConsumer{ +func (b *Backend) ComposeLogs(ctx context.Context, projectName string) error { + err := b.api.GetLogs(ctx, projectName, &logConsumer{ colors: map[string]console.ColorFunc{}, width: 0, }) @@ -26,11 +26,6 @@ func (c *client) ComposeLogs(ctx context.Context, projectName string) error { return nil } -type logConsumer struct { - colors map[string]console.ColorFunc - width int -} - func (l *logConsumer) Log(service, container, message string) { cf, ok := l.colors[service] if !ok { @@ -54,10 +49,7 @@ func (l *logConsumer) computeWidth() { l.width = width + 3 } -type LogConsumer interface { - Log(service, container, message string) -} - -type logsAPI interface { - GetLogs(ctx context.Context, name string, consumer LogConsumer) error +type logConsumer struct { + colors map[string]console.ColorFunc + width int } diff --git a/ecs/pkg/amazon/backend/secrets.go b/ecs/pkg/amazon/backend/secrets.go new file mode 100644 index 000000000..f2ae7c678 --- /dev/null +++ b/ecs/pkg/amazon/backend/secrets.go @@ -0,0 +1,23 @@ +package backend + +import ( + "context" + + "github.com/docker/ecs-plugin/pkg/amazon/types" +) + +func (b Backend) CreateSecret(ctx context.Context, secret types.Secret) (string, error) { + return b.api.CreateSecret(ctx, secret) +} + +func (b Backend) InspectSecret(ctx context.Context, id string) (types.Secret, error) { + return b.api.InspectSecret(ctx, id) +} + +func (b Backend) ListSecrets(ctx context.Context) ([]types.Secret, error) { + return b.api.ListSecrets(ctx) +} + +func (b Backend) DeleteSecret(ctx context.Context, id string, recover bool) error { + return b.api.DeleteSecret(ctx, id, recover) +} diff --git a/ecs/pkg/amazon/testdata/input/simple-single-service-with-overrides.yaml b/ecs/pkg/amazon/backend/testdata/input/simple-single-service-with-overrides.yaml similarity index 100% rename from ecs/pkg/amazon/testdata/input/simple-single-service-with-overrides.yaml rename to ecs/pkg/amazon/backend/testdata/input/simple-single-service-with-overrides.yaml diff --git a/ecs/pkg/amazon/testdata/input/simple-single-service.yaml b/ecs/pkg/amazon/backend/testdata/input/simple-single-service.yaml similarity index 100% rename from ecs/pkg/amazon/testdata/input/simple-single-service.yaml rename to ecs/pkg/amazon/backend/testdata/input/simple-single-service.yaml diff --git a/ecs/pkg/amazon/testdata/invalid_network_mode.yaml b/ecs/pkg/amazon/backend/testdata/invalid_network_mode.yaml similarity index 100% rename from ecs/pkg/amazon/testdata/invalid_network_mode.yaml rename to ecs/pkg/amazon/backend/testdata/invalid_network_mode.yaml diff --git a/ecs/pkg/amazon/testdata/simple/simple-cloudformation-conversion.golden b/ecs/pkg/amazon/backend/testdata/simple/simple-cloudformation-conversion.golden similarity index 100% rename from ecs/pkg/amazon/testdata/simple/simple-cloudformation-conversion.golden rename to ecs/pkg/amazon/backend/testdata/simple/simple-cloudformation-conversion.golden diff --git a/ecs/pkg/amazon/testdata/simple/simple-cloudformation-with-overrides-conversion.golden b/ecs/pkg/amazon/backend/testdata/simple/simple-cloudformation-with-overrides-conversion.golden similarity index 100% rename from ecs/pkg/amazon/testdata/simple/simple-cloudformation-with-overrides-conversion.golden rename to ecs/pkg/amazon/backend/testdata/simple/simple-cloudformation-with-overrides-conversion.golden diff --git a/ecs/pkg/amazon/backend/up.go b/ecs/pkg/amazon/backend/up.go new file mode 100644 index 000000000..8a7a895fe --- /dev/null +++ b/ecs/pkg/amazon/backend/up.go @@ -0,0 +1,100 @@ +package backend + +import ( + "context" + "fmt" + + "github.com/docker/ecs-plugin/pkg/amazon/types" + "github.com/docker/ecs-plugin/pkg/compose" +) + +func (b *Backend) ComposeUp(ctx context.Context, project *compose.Project) error { + if b.Cluster != "" { + ok, err := b.api.ClusterExists(ctx, b.Cluster) + if err != nil { + return err + } + if !ok { + return fmt.Errorf("configured cluster %q does not exist", b.Cluster) + } + } + + update, err := b.api.StackExists(ctx, project.Name) + if err != nil { + return err + } + if update { + return fmt.Errorf("we do not (yet) support updating an existing CloudFormation stack") + } + + template, err := b.Convert(project) + if err != nil { + return err + } + + vpc, err := b.GetVPC(ctx, project) + if err != nil { + return err + } + + subNets, err := b.api.GetSubNets(ctx, vpc) + if err != nil { + return err + } + + lb, err := b.GetLoadBalancer(ctx, project) + if err != nil { + return err + } + + parameters := map[string]string{ + ParameterClusterName: b.Cluster, + ParameterVPCId: vpc, + ParameterSubnet1Id: subNets[0], + ParameterSubnet2Id: subNets[1], + ParameterLoadBalancerARN: lb, + } + + err = b.api.CreateStack(ctx, project.Name, template, parameters) + if err != nil { + return err + } + + fmt.Println() + return b.WaitStackCompletion(ctx, project.Name, types.StackCreate) +} + +func (b Backend) GetVPC(ctx context.Context, project *compose.Project) (string, error) { + //check compose file for custom VPC selected + if vpc, ok := project.Extras[types.ExtensionVPC]; ok { + vpcID := vpc.(string) + ok, err := b.api.VpcExists(ctx, vpcID) + if err != nil { + return "", err + } + if !ok { + return "", fmt.Errorf("VPC does not exist: %s", vpc) + } + } + defaultVPC, err := b.api.GetDefaultVPC(ctx) + if err != nil { + return "", err + } + return defaultVPC, nil +} + +func (b Backend) GetLoadBalancer(ctx context.Context, project *compose.Project) (string, error) { + //check compose file for custom VPC selected + if lb, ok := project.Extras[types.ExtensionLB]; ok { + lbName := lb.(string) + ok, err := b.api.LoadBalancerExists(ctx, lbName) + if err != nil { + return "", err + } + if !ok { + return "", fmt.Errorf("Load Balancer does not exist: %s", lb) + } + return b.api.GetLoadBalancerARN(ctx, lbName) + } + return "", nil +} diff --git a/ecs/pkg/amazon/wait.go b/ecs/pkg/amazon/backend/wait.go similarity index 67% rename from ecs/pkg/amazon/wait.go rename to ecs/pkg/amazon/backend/wait.go index 58ae93d77..77ad844af 100644 --- a/ecs/pkg/amazon/wait.go +++ b/ecs/pkg/amazon/backend/wait.go @@ -1,4 +1,4 @@ -package amazon +package backend import ( "context" @@ -8,16 +8,15 @@ import ( "time" "github.com/aws/aws-sdk-go/aws" - "github.com/aws/aws-sdk-go/service/cloudformation" "github.com/docker/ecs-plugin/pkg/console" ) -func (c *client) WaitStackCompletion(ctx context.Context, name string, operation int) error { +func (b *Backend) WaitStackCompletion(ctx context.Context, name string, operation int) error { w := console.NewProgressWriter() knownEvents := map[string]struct{}{} // Get the unique Stack ID so we can collect events without getting some from previous deployments with same name - stackID, err := c.api.GetStackID(ctx, name) + stackID, err := b.api.GetStackID(ctx, name) if err != nil { return err } @@ -26,7 +25,7 @@ func (c *client) WaitStackCompletion(ctx context.Context, name string, operation done := make(chan bool) go func() { - c.api.WaitStackComplete(ctx, stackID, operation) //nolint:errcheck + b.api.WaitStackComplete(ctx, stackID, operation) //nolint:errcheck ticker.Stop() done <- true }() @@ -39,7 +38,7 @@ func (c *client) WaitStackCompletion(ctx context.Context, name string, operation completed = true case <-ticker.C: } - events, err := c.api.DescribeStackEvents(ctx, stackID) + events, err := b.api.DescribeStackEvents(ctx, stackID) if err != nil { return err } @@ -65,14 +64,3 @@ func (c *client) WaitStackCompletion(ctx context.Context, name string, operation } return stackErr } - -type waitAPI interface { - GetStackID(ctx context.Context, name string) (string, error) - WaitStackComplete(ctx context.Context, name string, operation int) error - DescribeStackEvents(ctx context.Context, stackID string) ([]*cloudformation.StackEvent, error) -} - -const ( - StackCreate = iota - StackDelete -) diff --git a/ecs/pkg/amazon/check_test.go b/ecs/pkg/amazon/check_test.go deleted file mode 100644 index 3038eee86..000000000 --- a/ecs/pkg/amazon/check_test.go +++ /dev/null @@ -1,13 +0,0 @@ -package amazon - -import ( - "testing" - - "gotest.tools/v3/assert" -) - -func TestInvalidNetworkMode(t *testing.T) { - project := load(t, "testdata/invalid_network_mode.yaml") - err := Check(project) - assert.Error(t, err[0], "'network_mode' \"bridge\" is not supported") -} diff --git a/ecs/pkg/amazon/check.go b/ecs/pkg/amazon/compatibility/check.go similarity index 98% rename from ecs/pkg/amazon/check.go rename to ecs/pkg/amazon/compatibility/check.go index 169794c76..58c4d306d 100644 --- a/ecs/pkg/amazon/check.go +++ b/ecs/pkg/amazon/compatibility/check.go @@ -1,4 +1,4 @@ -package amazon +package compatibility import ( "github.com/compose-spec/compose-go/types" diff --git a/ecs/pkg/amazon/compatibility/check_test.go b/ecs/pkg/amazon/compatibility/check_test.go new file mode 100644 index 000000000..f65898537 --- /dev/null +++ b/ecs/pkg/amazon/compatibility/check_test.go @@ -0,0 +1,23 @@ +package compatibility + +import ( + "testing" + + "github.com/docker/ecs-plugin/pkg/compose" + "gotest.tools/v3/assert" +) + +func load(t *testing.T, paths ...string) *compose.Project { + options := compose.ProjectOptions{ + Name: t.Name(), + ConfigPaths: paths, + } + project, err := compose.ProjectFromOptions(&options) + assert.NilError(t, err) + return project +} +func TestInvalidNetworkMode(t *testing.T) { + project := load(t, "../backend/testdata/invalid_network_mode.yaml") + err := Check(project) + assert.Error(t, err[0], "'network_mode' \"bridge\" is not supported") +} diff --git a/ecs/pkg/amazon/compatibility.go b/ecs/pkg/amazon/compatibility/compatibility.go similarity index 99% rename from ecs/pkg/amazon/compatibility.go rename to ecs/pkg/amazon/compatibility/compatibility.go index 1a7f136a4..a0fff1421 100644 --- a/ecs/pkg/amazon/compatibility.go +++ b/ecs/pkg/amazon/compatibility/compatibility.go @@ -1,4 +1,4 @@ -package amazon +package compatibility import ( "fmt" diff --git a/ecs/pkg/amazon/down.go b/ecs/pkg/amazon/down.go deleted file mode 100644 index d9bee735c..000000000 --- a/ecs/pkg/amazon/down.go +++ /dev/null @@ -1,34 +0,0 @@ -package amazon - -import ( - "context" - "fmt" -) - -func (c *client) ComposeDown(ctx context.Context, projectName string, deleteCluster bool) error { - err := c.api.DeleteStack(ctx, projectName) - if err != nil { - return err - } - - err = c.WaitStackCompletion(ctx, projectName, StackDelete) - if err != nil { - return err - } - - if !deleteCluster { - return nil - } - - fmt.Printf("Delete cluster %s", c.Cluster) - if err = c.api.DeleteCluster(ctx, c.Cluster); err != nil { - return err - } - fmt.Printf("... done. \n") - return nil -} - -type downAPI interface { - DeleteStack(ctx context.Context, name string) error - DeleteCluster(ctx context.Context, name string) error -} diff --git a/ecs/pkg/amazon/list.go b/ecs/pkg/amazon/list.go deleted file mode 100644 index 98903f402..000000000 --- a/ecs/pkg/amazon/list.go +++ /dev/null @@ -1,80 +0,0 @@ -package amazon - -import ( - "context" - "fmt" - "os" - "sort" - "strings" - "text/tabwriter" - - "github.com/docker/ecs-plugin/pkg/compose" -) - -func (c *client) ComposePs(ctx context.Context, project *compose.Project) error { - cluster := c.Cluster - if cluster == "" { - cluster = project.Name - } - w := tabwriter.NewWriter(os.Stdout, 20, 2, 3, ' ', 0) - fmt.Fprintf(w, "Name\tState\tPorts\n") - defer w.Flush() - - arns := []string{} - for _, service := range project.Services { - tasks, err := c.api.ListTasks(ctx, cluster, service.Name) - if err != nil { - return err - } - arns = append(arns, tasks...) - } - if len(arns) == 0 { - return nil - } - - tasks, err := c.api.DescribeTasks(ctx, cluster, arns...) - if err != nil { - return err - } - - networkInterfaces := []string{} - for _, t := range tasks { - if t.NetworkInterface != "" { - networkInterfaces = append(networkInterfaces, t.NetworkInterface) - } - } - publicIps, err := c.api.GetPublicIPs(ctx, networkInterfaces...) - if err != nil { - return err - } - - sort.Slice(tasks, func(i, j int) bool { - return strings.Compare(tasks[i].Service, tasks[j].Service) < 0 - }) - - for _, t := range tasks { - ports := []string{} - s, err := project.GetService(t.Service) - if err != nil { - return err - } - for _, p := range s.Ports { - ports = append(ports, fmt.Sprintf("%s:%d->%d/%s", publicIps[t.NetworkInterface], p.Published, p.Target, p.Protocol)) - } - fmt.Fprintf(w, "%s\t%s\t%s\n", s.Name, t.State, strings.Join(ports, ", ")) - } - return nil -} - -type TaskStatus struct { - State string - Service string - NetworkInterface string - PublicIP string -} - -type listAPI interface { - ListTasks(ctx context.Context, cluster string, name string) ([]string, error) - DescribeTasks(ctx context.Context, cluster string, arns ...string) ([]TaskStatus, error) - GetPublicIPs(ctx context.Context, interfaces ...string) (map[string]string, error) -} diff --git a/ecs/pkg/amazon/sdk/api.go b/ecs/pkg/amazon/sdk/api.go new file mode 100644 index 000000000..137a43397 --- /dev/null +++ b/ecs/pkg/amazon/sdk/api.go @@ -0,0 +1,61 @@ +package sdk + +import ( + "context" + + cf "github.com/aws/aws-sdk-go/service/cloudformation" + "github.com/awslabs/goformation/v4/cloudformation" + "github.com/docker/ecs-plugin/pkg/amazon/types" +) + +//go:generate mockgen -destination=./api_mock.go -self_package "github.com/docker/ecs-plugin/pkg/amazon" -package=amazon . API + +type API interface { + downAPI + upAPI + logsAPI + secretsAPI + listAPI +} + +type upAPI interface { + waitAPI + GetDefaultVPC(ctx context.Context) (string, error) + VpcExists(ctx context.Context, vpcID string) (bool, error) + GetSubNets(ctx context.Context, vpcID string) ([]string, error) + + ClusterExists(ctx context.Context, name string) (bool, error) + StackExists(ctx context.Context, name string) (bool, error) + CreateStack(ctx context.Context, name string, template *cloudformation.Template, parameters map[string]string) error + + LoadBalancerExists(ctx context.Context, name string) (bool, error) + GetLoadBalancerARN(ctx context.Context, name string) (string, error) +} + +type downAPI interface { + DeleteStack(ctx context.Context, name string) error + DeleteCluster(ctx context.Context, name string) error +} + +type logsAPI interface { + GetLogs(ctx context.Context, name string, consumer types.LogConsumer) error +} + +type secretsAPI interface { + CreateSecret(ctx context.Context, secret types.Secret) (string, error) + InspectSecret(ctx context.Context, id string) (types.Secret, error) + ListSecrets(ctx context.Context) ([]types.Secret, error) + DeleteSecret(ctx context.Context, id string, recover bool) error +} + +type listAPI interface { + ListTasks(ctx context.Context, cluster string, name string) ([]string, error) + DescribeTasks(ctx context.Context, cluster string, arns ...string) ([]types.TaskStatus, error) + GetPublicIPs(ctx context.Context, interfaces ...string) (map[string]string, error) +} + +type waitAPI interface { + GetStackID(ctx context.Context, name string) (string, error) + WaitStackComplete(ctx context.Context, name string, operation int) error + DescribeStackEvents(ctx context.Context, stackID string) ([]*cf.StackEvent, error) +} diff --git a/ecs/pkg/amazon/api_mock.go b/ecs/pkg/amazon/sdk/api_mock.go similarity index 96% rename from ecs/pkg/amazon/api_mock.go rename to ecs/pkg/amazon/sdk/api_mock.go index cbf2b6d17..07ce79e74 100644 --- a/ecs/pkg/amazon/api_mock.go +++ b/ecs/pkg/amazon/sdk/api_mock.go @@ -2,7 +2,7 @@ // Source: github.com/docker/ecs-plugin/pkg/amazon (interfaces: API) // Package amazon is a generated GoMock package. -package amazon +package sdk import ( context "context" @@ -10,7 +10,7 @@ import ( cloudformation "github.com/aws/aws-sdk-go/service/cloudformation" cloudformation0 "github.com/awslabs/goformation/v4/cloudformation" - docker "github.com/docker/ecs-plugin/pkg/docker" + btypes "github.com/docker/ecs-plugin/pkg/amazon/types" gomock "github.com/golang/mock/gomock" ) @@ -53,7 +53,7 @@ func (mr *MockAPIMockRecorder) ClusterExists(arg0, arg1 interface{}) *gomock.Cal } // CreateSecret mocks base method -func (m *MockAPI) CreateSecret(arg0 context.Context, arg1 docker.Secret) (string, error) { +func (m *MockAPI) CreateSecret(arg0 context.Context, arg1 btypes.Secret) (string, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "CreateSecret", arg0, arg1) ret0, _ := ret[0].(string) @@ -139,14 +139,14 @@ func (mr *MockAPIMockRecorder) DescribeStackEvents(arg0, arg1 interface{}) *gomo } // DescribeTasks mocks base method -func (m *MockAPI) DescribeTasks(arg0 context.Context, arg1 string, arg2 ...string) ([]TaskStatus, error) { +func (m *MockAPI) DescribeTasks(arg0 context.Context, arg1 string, arg2 ...string) ([]btypes.TaskStatus, error) { m.ctrl.T.Helper() varargs := []interface{}{arg0, arg1} for _, a := range arg2 { varargs = append(varargs, a) } ret := m.ctrl.Call(m, "DescribeTasks", varargs...) - ret0, _ := ret[0].([]TaskStatus) + ret0, _ := ret[0].([]btypes.TaskStatus) ret1, _ := ret[1].(error) return ret0, ret1 } @@ -174,7 +174,7 @@ func (mr *MockAPIMockRecorder) GetDefaultVPC(arg0 interface{}) *gomock.Call { } // GetLogs mocks base method -func (m *MockAPI) GetLogs(arg0 context.Context, arg1 string, arg2 LogConsumer) error { +func (m *MockAPI) GetLogs(arg0 context.Context, arg1 string, arg2 btypes.LogConsumer) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "GetLogs", arg0, arg1, arg2) ret0, _ := ret[0].(error) @@ -238,10 +238,10 @@ func (mr *MockAPIMockRecorder) GetSubNets(arg0, arg1 interface{}) *gomock.Call { } // InspectSecret mocks base method -func (m *MockAPI) InspectSecret(arg0 context.Context, arg1 string) (docker.Secret, error) { +func (m *MockAPI) InspectSecret(arg0 context.Context, arg1 string) (btypes.Secret, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "InspectSecret", arg0, arg1) - ret0, _ := ret[0].(docker.Secret) + ret0, _ := ret[0].(btypes.Secret) ret1, _ := ret[1].(error) return ret0, ret1 } @@ -253,10 +253,10 @@ func (mr *MockAPIMockRecorder) InspectSecret(arg0, arg1 interface{}) *gomock.Cal } // ListSecrets mocks base method -func (m *MockAPI) ListSecrets(arg0 context.Context) ([]docker.Secret, error) { +func (m *MockAPI) ListSecrets(arg0 context.Context) ([]btypes.Secret, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "ListSecrets", arg0) - ret0, _ := ret[0].([]docker.Secret) + ret0, _ := ret[0].([]btypes.Secret) ret1, _ := ret[1].(error) return ret0, ret1 } diff --git a/ecs/pkg/amazon/convert.go b/ecs/pkg/amazon/sdk/convert.go similarity index 98% rename from ecs/pkg/amazon/convert.go rename to ecs/pkg/amazon/sdk/convert.go index 82047ddb3..8874aa83e 100644 --- a/ecs/pkg/amazon/convert.go +++ b/ecs/pkg/amazon/sdk/convert.go @@ -1,4 +1,4 @@ -package amazon +package sdk import ( "fmt" @@ -13,6 +13,7 @@ import ( "github.com/awslabs/goformation/v4/cloudformation/tags" "github.com/compose-spec/compose-go/types" "github.com/docker/cli/opts" + t "github.com/docker/ecs-plugin/pkg/amazon/types" "github.com/docker/ecs-plugin/pkg/compose" ) @@ -318,7 +319,7 @@ func getImage(image string) string { func getRepoCredentials(service types.ServiceConfig) *ecs.TaskDefinition_RepositoryCredentials { // extract registry and namespace string from image name for key, value := range service.Extras { - if key == ExtensionPullCredentials { + if key == t.ExtensionPullCredentials { return &ecs.TaskDefinition_RepositoryCredentials{CredentialsParameter: value.(string)} } } diff --git a/ecs/pkg/amazon/sdk.go b/ecs/pkg/amazon/sdk/sdk.go similarity index 94% rename from ecs/pkg/amazon/sdk.go rename to ecs/pkg/amazon/sdk/sdk.go index e074fb11d..1e1cb8496 100644 --- a/ecs/pkg/amazon/sdk.go +++ b/ecs/pkg/amazon/sdk/sdk.go @@ -1,4 +1,4 @@ -package amazon +package sdk import ( "context" @@ -25,7 +25,8 @@ import ( cf "github.com/awslabs/goformation/v4/cloudformation" "github.com/sirupsen/logrus" - "github.com/docker/ecs-plugin/pkg/docker" + "github.com/docker/ecs-plugin/pkg/amazon/types" + t "github.com/docker/ecs-plugin/pkg/amazon/types" ) type sdk struct { @@ -188,9 +189,9 @@ func (s sdk) WaitStackComplete(ctx context.Context, name string, operation int) StackName: aws.String(name), } switch operation { - case StackCreate: + case t.StackCreate: return s.CF.WaitUntilStackCreateCompleteWithContext(ctx, input) - case StackDelete: + case t.StackDelete: return s.CF.WaitUntilStackDeleteCompleteWithContext(ctx, input) default: return fmt.Errorf("internal error: unexpected stack operation %d", operation) @@ -235,7 +236,7 @@ func (s sdk) DeleteStack(ctx context.Context, name string) error { return err } -func (s sdk) CreateSecret(ctx context.Context, secret docker.Secret) (string, error) { +func (s sdk) CreateSecret(ctx context.Context, secret t.Secret) (string, error) { logrus.Debug("Create secret " + secret.Name) secretStr, err := secret.GetCredString() if err != nil { @@ -253,17 +254,17 @@ func (s sdk) CreateSecret(ctx context.Context, secret docker.Secret) (string, er return *response.ARN, nil } -func (s sdk) InspectSecret(ctx context.Context, id string) (docker.Secret, error) { +func (s sdk) InspectSecret(ctx context.Context, id string) (t.Secret, error) { logrus.Debug("Inspect secret " + id) response, err := s.SM.DescribeSecret(&secretsmanager.DescribeSecretInput{SecretId: &id}) if err != nil { - return docker.Secret{}, err + return t.Secret{}, err } labels := map[string]string{} for _, tag := range response.Tags { labels[*tag.Key] = *tag.Value } - secret := docker.Secret{ + secret := t.Secret{ ID: *response.ARN, Name: *response.Name, Labels: labels, @@ -274,14 +275,14 @@ func (s sdk) InspectSecret(ctx context.Context, id string) (docker.Secret, error return secret, nil } -func (s sdk) ListSecrets(ctx context.Context) ([]docker.Secret, error) { +func (s sdk) ListSecrets(ctx context.Context) ([]t.Secret, error) { logrus.Debug("List secrets ...") response, err := s.SM.ListSecrets(&secretsmanager.ListSecretsInput{}) if err != nil { - return []docker.Secret{}, err + return []t.Secret{}, err } - var secrets []docker.Secret + var secrets []t.Secret for _, sec := range response.SecretList { @@ -293,7 +294,7 @@ func (s sdk) ListSecrets(ctx context.Context) ([]docker.Secret, error) { if sec.Description != nil { description = *sec.Description } - secrets = append(secrets, docker.Secret{ + secrets = append(secrets, t.Secret{ ID: *sec.ARN, Name: *sec.Name, Labels: labels, @@ -310,7 +311,7 @@ func (s sdk) DeleteSecret(ctx context.Context, id string, recover bool) error { return err } -func (s sdk) GetLogs(ctx context.Context, name string, consumer LogConsumer) error { +func (s sdk) GetLogs(ctx context.Context, name string, consumer types.LogConsumer) error { logGroup := fmt.Sprintf("/docker-compose/%s", name) var startTime int64 for { @@ -356,7 +357,7 @@ func (s sdk) ListTasks(ctx context.Context, cluster string, service string) ([]s return arns, nil } -func (s sdk) DescribeTasks(ctx context.Context, cluster string, arns ...string) ([]TaskStatus, error) { +func (s sdk) DescribeTasks(ctx context.Context, cluster string, arns ...string) ([]t.TaskStatus, error) { tasks, err := s.ECS.DescribeTasksWithContext(ctx, &ecs.DescribeTasksInput{ Cluster: aws.String(cluster), Tasks: aws.StringSlice(arns), @@ -364,7 +365,7 @@ func (s sdk) DescribeTasks(ctx context.Context, cluster string, arns ...string) if err != nil { return nil, err } - result := []TaskStatus{} + result := []t.TaskStatus{} for _, task := range tasks.Tasks { var networkInterface string for _, attachement := range task.Attachments { @@ -376,7 +377,7 @@ func (s sdk) DescribeTasks(ctx context.Context, cluster string, arns ...string) } } } - result = append(result, TaskStatus{ + result = append(result, t.TaskStatus{ State: *task.LastStatus, Service: strings.Replace(*task.Group, "service:", "", 1), NetworkInterface: networkInterface, diff --git a/ecs/pkg/amazon/secrets.go b/ecs/pkg/amazon/secrets.go deleted file mode 100644 index 96a2a476d..000000000 --- a/ecs/pkg/amazon/secrets.go +++ /dev/null @@ -1,30 +0,0 @@ -package amazon - -import ( - "context" - - "github.com/docker/ecs-plugin/pkg/docker" -) - -type secretsAPI interface { - CreateSecret(ctx context.Context, secret docker.Secret) (string, error) - InspectSecret(ctx context.Context, id string) (docker.Secret, error) - ListSecrets(ctx context.Context) ([]docker.Secret, error) - DeleteSecret(ctx context.Context, id string, recover bool) error -} - -func (c client) CreateSecret(ctx context.Context, secret docker.Secret) (string, error) { - return c.api.CreateSecret(ctx, secret) -} - -func (c client) InspectSecret(ctx context.Context, id string) (docker.Secret, error) { - return c.api.InspectSecret(ctx, id) -} - -func (c client) ListSecrets(ctx context.Context) ([]docker.Secret, error) { - return c.api.ListSecrets(ctx) -} - -func (c client) DeleteSecret(ctx context.Context, id string, recover bool) error { - return c.api.DeleteSecret(ctx, id, recover) -} diff --git a/ecs/pkg/docker/secret.go b/ecs/pkg/amazon/types/types.go similarity index 71% rename from ecs/pkg/docker/secret.go rename to ecs/pkg/amazon/types/types.go index 613c62638..f6815955d 100644 --- a/ecs/pkg/docker/secret.go +++ b/ecs/pkg/amazon/types/types.go @@ -1,9 +1,25 @@ -package docker +package types -import ( - "encoding/json" +import "encoding/json" + +type TaskStatus struct { + Name string + State string + Service string + NetworkInterface string + PublicIP string + Ports []string +} + +const ( + StackCreate = iota + StackDelete ) +type LogConsumer interface { + Log(service, container, message string) +} + type Secret struct { ID string `json:"ID"` Name string `json:"Name"` diff --git a/ecs/pkg/amazon/x.go b/ecs/pkg/amazon/types/x.go similarity index 93% rename from ecs/pkg/amazon/x.go rename to ecs/pkg/amazon/types/x.go index 315c50c4c..d8b8e0112 100644 --- a/ecs/pkg/amazon/x.go +++ b/ecs/pkg/amazon/types/x.go @@ -1,4 +1,4 @@ -package amazon +package types const ( ExtensionSecurityGroup = "x-aws-securitygroup" diff --git a/ecs/pkg/amazon/up.go b/ecs/pkg/amazon/up.go deleted file mode 100644 index 3c9477247..000000000 --- a/ecs/pkg/amazon/up.go +++ /dev/null @@ -1,114 +0,0 @@ -package amazon - -import ( - "context" - "fmt" - - "github.com/awslabs/goformation/v4/cloudformation" - "github.com/docker/ecs-plugin/pkg/compose" -) - -func (c *client) ComposeUp(ctx context.Context, project *compose.Project) error { - if c.Cluster != "" { - ok, err := c.api.ClusterExists(ctx, c.Cluster) - if err != nil { - return err - } - if !ok { - return fmt.Errorf("configured cluster %q does not exist", c.Cluster) - } - } - - update, err := c.api.StackExists(ctx, project.Name) - if err != nil { - return err - } - if update { - return fmt.Errorf("we do not (yet) support updating an existing CloudFormation stack") - } - - template, err := c.Convert(project) - if err != nil { - return err - } - - vpc, err := c.GetVPC(ctx, project) - if err != nil { - return err - } - - subNets, err := c.api.GetSubNets(ctx, vpc) - if err != nil { - return err - } - - lb, err := c.GetLoadBalancer(ctx, project) - if err != nil { - return err - } - - parameters := map[string]string{ - ParameterClusterName: c.Cluster, - ParameterVPCId: vpc, - ParameterSubnet1Id: subNets[0], - ParameterSubnet2Id: subNets[1], - ParameterLoadBalancerARN: lb, - } - - err = c.api.CreateStack(ctx, project.Name, template, parameters) - if err != nil { - return err - } - - fmt.Println() - return c.WaitStackCompletion(ctx, project.Name, StackCreate) -} - -func (c client) GetVPC(ctx context.Context, project *compose.Project) (string, error) { - //check compose file for custom VPC selected - if vpc, ok := project.Extras[ExtensionVPC]; ok { - vpcID := vpc.(string) - ok, err := c.api.VpcExists(ctx, vpcID) - if err != nil { - return "", err - } - if !ok { - return "", fmt.Errorf("VPC does not exist: %s", vpc) - } - } - defaultVPC, err := c.api.GetDefaultVPC(ctx) - if err != nil { - return "", err - } - return defaultVPC, nil -} - -func (c client) GetLoadBalancer(ctx context.Context, project *compose.Project) (string, error) { - //check compose file for custom VPC selected - if lb, ok := project.Extras[ExtensionLB]; ok { - lbName := lb.(string) - ok, err := c.api.LoadBalancerExists(ctx, lbName) - if err != nil { - return "", err - } - if !ok { - return "", fmt.Errorf("Load Balancer does not exist: %s", lb) - } - return c.api.GetLoadBalancerARN(ctx, lbName) - } - return "", nil -} - -type upAPI interface { - waitAPI - GetDefaultVPC(ctx context.Context) (string, error) - VpcExists(ctx context.Context, vpcID string) (bool, error) - GetSubNets(ctx context.Context, vpcID string) ([]string, error) - - ClusterExists(ctx context.Context, name string) (bool, error) - StackExists(ctx context.Context, name string) (bool, error) - CreateStack(ctx context.Context, name string, template *cloudformation.Template, parameters map[string]string) error - - LoadBalancerExists(ctx context.Context, name string) (bool, error) - GetLoadBalancerARN(ctx context.Context, name string) (string, error) -} diff --git a/ecs/pkg/compose/api.go b/ecs/pkg/compose/api.go index 1049a993c..016d49478 100644 --- a/ecs/pkg/compose/api.go +++ b/ecs/pkg/compose/api.go @@ -4,7 +4,7 @@ import ( "context" "github.com/awslabs/goformation/v4/cloudformation" - "github.com/docker/ecs-plugin/pkg/docker" + "github.com/docker/ecs-plugin/pkg/amazon/types" ) type API interface { @@ -13,9 +13,9 @@ type API interface { ComposeDown(ctx context.Context, projectName string, deleteCluster bool) error ComposeLogs(ctx context.Context, projectName string) error - CreateSecret(ctx context.Context, secret docker.Secret) (string, error) - InspectSecret(ctx context.Context, id string) (docker.Secret, error) - ListSecrets(ctx context.Context) ([]docker.Secret, error) + CreateSecret(ctx context.Context, secret types.Secret) (string, error) + InspectSecret(ctx context.Context, id string) (types.Secret, error) + ListSecrets(ctx context.Context) ([]types.Secret, error) DeleteSecret(ctx context.Context, id string, recover bool) error - ComposePs(background context.Context, project *Project) error + ComposePs(background context.Context, project *Project) ([]types.TaskStatus, error) } From 9e8ddb63cc10427c7b553acd5e538047e2d2da0f Mon Sep 17 00:00:00 2001 From: aiordache <anca.iordache@docker.com> Date: Fri, 12 Jun 2020 15:00:27 +0200 Subject: [PATCH 125/198] fix lint issue - renamed CompatibilityChecker to Checker Signed-off-by: aiordache <anca.iordache@docker.com> Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com> --- ecs/pkg/amazon/compatibility/check.go | 2 +- ecs/pkg/amazon/compatibility/compatibility.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/ecs/pkg/amazon/compatibility/check.go b/ecs/pkg/amazon/compatibility/check.go index 58c4d306d..8e46becba 100644 --- a/ecs/pkg/amazon/compatibility/check.go +++ b/ecs/pkg/amazon/compatibility/check.go @@ -8,7 +8,7 @@ import ( type Warning string type Warnings []string -type CompatibilityChecker interface { +type Checker interface { CheckService(service *types.ServiceConfig) CheckCapAdd(service *types.ServiceConfig) CheckDNS(service *types.ServiceConfig) diff --git a/ecs/pkg/amazon/compatibility/compatibility.go b/ecs/pkg/amazon/compatibility/compatibility.go index a0fff1421..c7d67c26d 100644 --- a/ecs/pkg/amazon/compatibility/compatibility.go +++ b/ecs/pkg/amazon/compatibility/compatibility.go @@ -168,4 +168,4 @@ func (c *FargateCompatibilityChecker) CheckLabels(service *types.ServiceConfig) } } -var _ CompatibilityChecker = &FargateCompatibilityChecker{} +var _ Checker = &FargateCompatibilityChecker{} From 1bb95134f0c0a846be4201d533fd9c8995cf7783 Mon Sep 17 00:00:00 2001 From: aiordache <anca.iordache@docker.com> Date: Mon, 15 Jun 2020 10:57:51 +0200 Subject: [PATCH 126/198] match docker/api signature for up and down methods Signed-off-by: aiordache <anca.iordache@docker.com> Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com> --- ecs/cmd/commands/compose.go | 28 +++++----------------------- ecs/pkg/amazon/backend/down.go | 17 ++++++----------- ecs/pkg/amazon/backend/down_test.go | 29 ++++++----------------------- ecs/pkg/amazon/backend/list.go | 2 +- ecs/pkg/amazon/backend/logs.go | 2 +- ecs/pkg/amazon/backend/up.go | 7 ++++++- ecs/pkg/compose/api.go | 9 +++++---- 7 files changed, 30 insertions(+), 64 deletions(-) diff --git a/ecs/cmd/commands/compose.go b/ecs/cmd/commands/compose.go index 55eb7a148..fc5802e0b 100644 --- a/ecs/cmd/commands/compose.go +++ b/ecs/cmd/commands/compose.go @@ -75,16 +75,12 @@ func UpCommand(dockerCli command.Cli, projectOpts *compose.ProjectOptions) *cobr opts := upOptions{} cmd := &cobra.Command{ Use: "up", - RunE: compose.WithProject(projectOpts, func(project *compose.Project, args []string) error { - clusteropts, err := docker.GetAwsContext(dockerCli) - if err != nil { - return err - } + RunE: docker.WithAwsContext(dockerCli, func(clusteropts docker.AwsContext, args []string) error { backend, err := amazon.NewBackend(clusteropts.Profile, clusteropts.Cluster, clusteropts.Region) if err != nil { return err } - return backend.ComposeUp(context.Background(), project) + return backend.Up(context.Background(), *projectOpts) }), } cmd.Flags().StringVar(&opts.loadBalancerArn, "load-balancer", "", "") @@ -104,7 +100,7 @@ func PsCommand(dockerCli command.Cli, projectOpts *compose.ProjectOptions) *cobr if err != nil { return err } - tasks, err := backend.ComposePs(context.Background(), project) + tasks, err := backend.Ps(context.Background(), project) if err != nil { return err } @@ -133,21 +129,7 @@ func DownCommand(dockerCli command.Cli, projectOpts *compose.ProjectOptions) *co if err != nil { return err } - if len(args) == 0 { - project, err := compose.ProjectFromOptions(projectOpts) - if err != nil { - return err - } - return backend.ComposeDown(context.Background(), project.Name, opts.DeleteCluster) - } - // project names passed as parameters - for _, name := range args { - err := backend.ComposeDown(context.Background(), name, opts.DeleteCluster) - if err != nil { - return err - } - } - return nil + return backend.Down(context.Background(), *projectOpts) }), } cmd.Flags().BoolVar(&opts.DeleteCluster, "delete-cluster", false, "Delete cluster") @@ -173,7 +155,7 @@ func LogsCommand(dockerCli command.Cli, projectOpts *compose.ProjectOptions) *co } else { name = args[0] } - return backend.ComposeLogs(context.Background(), name) + return backend.Logs(context.Background(), name) }), } return cmd diff --git a/ecs/pkg/amazon/backend/down.go b/ecs/pkg/amazon/backend/down.go index 8b978e796..d07c4a069 100644 --- a/ecs/pkg/amazon/backend/down.go +++ b/ecs/pkg/amazon/backend/down.go @@ -2,30 +2,25 @@ package backend import ( "context" - "fmt" "github.com/docker/ecs-plugin/pkg/amazon/types" + "github.com/docker/ecs-plugin/pkg/compose" ) -func (b *Backend) ComposeDown(ctx context.Context, projectName string, deleteCluster bool) error { - err := b.api.DeleteStack(ctx, projectName) +func (b *Backend) Down(ctx context.Context, options compose.ProjectOptions) error { + project, err := compose.ProjectFromOptions(&options) if err != nil { return err } - err = b.WaitStackCompletion(ctx, projectName, types.StackDelete) + err = b.api.DeleteStack(ctx, project.Name) if err != nil { return err } - if !deleteCluster { - return nil - } - - fmt.Printf("Delete cluster %s", b.Cluster) - if err = b.api.DeleteCluster(ctx, b.Cluster); err != nil { + err = b.WaitStackCompletion(ctx, project.Name, types.StackDelete) + if err != nil { return err } - fmt.Printf("... done. \n") return nil } diff --git a/ecs/pkg/amazon/backend/down_test.go b/ecs/pkg/amazon/backend/down_test.go index d9abf70ca..7d156e246 100644 --- a/ecs/pkg/amazon/backend/down_test.go +++ b/ecs/pkg/amazon/backend/down_test.go @@ -6,10 +6,11 @@ import ( "github.com/docker/ecs-plugin/pkg/amazon/sdk" btypes "github.com/docker/ecs-plugin/pkg/amazon/types" + "github.com/docker/ecs-plugin/pkg/compose" "github.com/golang/mock/gomock" ) -func TestDownDontDeleteCluster(t *testing.T) { +func TestDown(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() m := sdk.NewMockAPI(ctrl) @@ -25,26 +26,8 @@ func TestDownDontDeleteCluster(t *testing.T) { recorder.WaitStackComplete(ctx, "stack-123", btypes.StackDelete).Return(nil) recorder.DescribeStackEvents(ctx, "stack-123").Return(nil, nil) - c.ComposeDown(ctx, "test_project", false) -} - -func TestDownDeleteCluster(t *testing.T) { - ctrl := gomock.NewController(t) - defer ctrl.Finish() - m := sdk.NewMockAPI(ctrl) - c := &Backend{ - Cluster: "test_cluster", - Region: "region", - api: m, - } - - ctx := context.TODO() - recorder := m.EXPECT() - recorder.DeleteStack(ctx, "test_project").Return(nil) - recorder.GetStackID(ctx, "test_project").Return("stack-123", nil) - recorder.WaitStackComplete(ctx, "stack-123", btypes.StackDelete).Return(nil) - recorder.DescribeStackEvents(ctx, "stack-123").Return(nil, nil) - recorder.DeleteCluster(ctx, "test_cluster").Return(nil) - - c.ComposeDown(ctx, "test_project", true) + c.Down(ctx, compose.ProjectOptions{ + ConfigPaths: []string{}, + Name: "test_project", + }) } diff --git a/ecs/pkg/amazon/backend/list.go b/ecs/pkg/amazon/backend/list.go index 055362aa8..b90905945 100644 --- a/ecs/pkg/amazon/backend/list.go +++ b/ecs/pkg/amazon/backend/list.go @@ -10,7 +10,7 @@ import ( "github.com/docker/ecs-plugin/pkg/compose" ) -func (b *Backend) ComposePs(ctx context.Context, project *compose.Project) ([]types.TaskStatus, error) { +func (b *Backend) Ps(ctx context.Context, project *compose.Project) ([]types.TaskStatus, error) { cluster := b.Cluster if cluster == "" { cluster = project.Name diff --git a/ecs/pkg/amazon/backend/logs.go b/ecs/pkg/amazon/backend/logs.go index 179635290..a17158857 100644 --- a/ecs/pkg/amazon/backend/logs.go +++ b/ecs/pkg/amazon/backend/logs.go @@ -11,7 +11,7 @@ import ( "github.com/docker/ecs-plugin/pkg/console" ) -func (b *Backend) ComposeLogs(ctx context.Context, projectName string) error { +func (b *Backend) Logs(ctx context.Context, projectName string) error { err := b.api.GetLogs(ctx, projectName, &logConsumer{ colors: map[string]console.ColorFunc{}, width: 0, diff --git a/ecs/pkg/amazon/backend/up.go b/ecs/pkg/amazon/backend/up.go index 8a7a895fe..8c78748a4 100644 --- a/ecs/pkg/amazon/backend/up.go +++ b/ecs/pkg/amazon/backend/up.go @@ -8,7 +8,12 @@ import ( "github.com/docker/ecs-plugin/pkg/compose" ) -func (b *Backend) ComposeUp(ctx context.Context, project *compose.Project) error { +func (b *Backend) Up(ctx context.Context, options compose.ProjectOptions) error { + project, err := compose.ProjectFromOptions(&options) + if err != nil { + return err + } + if b.Cluster != "" { ok, err := b.api.ClusterExists(ctx, b.Cluster) if err != nil { diff --git a/ecs/pkg/compose/api.go b/ecs/pkg/compose/api.go index 016d49478..6d84ccec4 100644 --- a/ecs/pkg/compose/api.go +++ b/ecs/pkg/compose/api.go @@ -8,14 +8,15 @@ import ( ) type API interface { + Up(ctx context.Context, options ProjectOptions) error + Down(ctx context.Context, options ProjectOptions) error + Convert(project *Project) (*cloudformation.Template, error) - ComposeUp(ctx context.Context, project *Project) error - ComposeDown(ctx context.Context, projectName string, deleteCluster bool) error - ComposeLogs(ctx context.Context, projectName string) error + Logs(ctx context.Context, projectName string) error + Ps(background context.Context, project *Project) ([]types.TaskStatus, error) CreateSecret(ctx context.Context, secret types.Secret) (string, error) InspectSecret(ctx context.Context, id string) (types.Secret, error) ListSecrets(ctx context.Context) ([]types.Secret, error) DeleteSecret(ctx context.Context, id string, recover bool) error - ComposePs(background context.Context, project *Project) ([]types.TaskStatus, error) } From dcf84f24998a756a1bd6a4b2930b8da76f314d77 Mon Sep 17 00:00:00 2001 From: Nicolas De Loof <nicolas.deloof@gmail.com> Date: Mon, 15 Jun 2020 11:25:52 +0200 Subject: [PATCH 127/198] Fix broken build on master Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com> --- ecs/pkg/amazon/backend/down.go | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/ecs/pkg/amazon/backend/down.go b/ecs/pkg/amazon/backend/down.go index d07c4a069..adbf9af32 100644 --- a/ecs/pkg/amazon/backend/down.go +++ b/ecs/pkg/amazon/backend/down.go @@ -8,17 +8,21 @@ import ( ) func (b *Backend) Down(ctx context.Context, options compose.ProjectOptions) error { - project, err := compose.ProjectFromOptions(&options) + name := options.Name + if name == "" { + project, err := compose.ProjectFromOptions(&options) + if err != nil { + return err + } + name = project.Name + } + + err := b.api.DeleteStack(ctx, name) if err != nil { return err } - err = b.api.DeleteStack(ctx, project.Name) - if err != nil { - return err - } - - err = b.WaitStackCompletion(ctx, project.Name, types.StackDelete) + err = b.WaitStackCompletion(ctx, name, types.StackDelete) if err != nil { return err } From c5895fe09af30eb1336877211c7833b7ebc18cbd Mon Sep 17 00:00:00 2001 From: Nicolas De Loof <nicolas.deloof@gmail.com> Date: Wed, 17 Jun 2020 14:48:42 +0200 Subject: [PATCH 128/198] Use `Project` from compose-go Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com> --- ecs/cmd/commands/compose.go | 23 +-- ecs/{pkg/compose => cmd/commands}/opts.go | 17 +- ecs/cmd/commands/secret.go | 6 +- ecs/go.mod | 7 +- ecs/go.sum | 29 ++- ecs/pkg/amazon/backend/cloudformation.go | 65 ++++--- ecs/pkg/amazon/backend/cloudformation_test.go | 27 ++- ecs/pkg/amazon/backend/down.go | 8 +- ecs/pkg/amazon/backend/down_test.go | 6 +- ecs/pkg/amazon/backend/list.go | 20 +- ecs/pkg/amazon/backend/secrets.go | 8 +- ecs/pkg/amazon/backend/up.go | 17 +- ecs/pkg/amazon/compatibility/check.go | 41 ----- ecs/pkg/amazon/compatibility/check_test.go | 23 --- ecs/pkg/amazon/compatibility/compatibility.go | 171 ------------------ ecs/pkg/amazon/sdk/api.go | 12 +- ecs/pkg/amazon/sdk/api_mock.go | 19 +- ecs/pkg/amazon/sdk/convert.go | 7 +- ecs/pkg/amazon/sdk/sdk.go | 32 ++-- ecs/pkg/compose/api.go | 17 +- ecs/pkg/compose/normalize.go | 89 --------- ecs/pkg/compose/project.go | 170 ----------------- ecs/pkg/compose/project_test.go | 46 ----- ecs/pkg/{amazon/types => compose}/types.go | 2 +- ecs/pkg/{amazon/types => compose}/x.go | 2 +- 25 files changed, 178 insertions(+), 686 deletions(-) rename ecs/{pkg/compose => cmd/commands}/opts.go (53%) delete mode 100644 ecs/pkg/amazon/compatibility/check.go delete mode 100644 ecs/pkg/amazon/compatibility/check_test.go delete mode 100644 ecs/pkg/amazon/compatibility/compatibility.go delete mode 100644 ecs/pkg/compose/normalize.go delete mode 100644 ecs/pkg/compose/project.go delete mode 100644 ecs/pkg/compose/project_test.go rename ecs/pkg/{amazon/types => compose}/types.go (98%) rename ecs/pkg/{amazon/types => compose}/x.go (92%) diff --git a/ecs/cmd/commands/compose.go b/ecs/cmd/commands/compose.go index fc5802e0b..b3489baee 100644 --- a/ecs/cmd/commands/compose.go +++ b/ecs/cmd/commands/compose.go @@ -7,9 +7,10 @@ import ( "os" "strings" + "github.com/compose-spec/compose-go/cli" + "github.com/compose-spec/compose-go/types" "github.com/docker/cli/cli/command" amazon "github.com/docker/ecs-plugin/pkg/amazon/backend" - "github.com/docker/ecs-plugin/pkg/compose" "github.com/docker/ecs-plugin/pkg/docker" "github.com/spf13/cobra" ) @@ -18,8 +19,8 @@ func ComposeCommand(dockerCli command.Cli) *cobra.Command { cmd := &cobra.Command{ Use: "compose", } - opts := &compose.ProjectOptions{} - opts.AddFlags(cmd.Flags()) + opts := &cli.ProjectOptions{} + AddFlags(opts, cmd.Flags()) cmd.AddCommand( ConvertCommand(dockerCli, opts), @@ -42,10 +43,10 @@ func (o upOptions) LoadBalancerArn() *string { return &o.loadBalancerArn } -func ConvertCommand(dockerCli command.Cli, projectOpts *compose.ProjectOptions) *cobra.Command { +func ConvertCommand(dockerCli command.Cli, projectOpts *cli.ProjectOptions) *cobra.Command { cmd := &cobra.Command{ Use: "convert", - RunE: compose.WithProject(projectOpts, func(project *compose.Project, args []string) error { + RunE: WithProject(projectOpts, func(project *types.Project, args []string) error { clusteropts, err := docker.GetAwsContext(dockerCli) if err != nil { return err @@ -71,7 +72,7 @@ func ConvertCommand(dockerCli command.Cli, projectOpts *compose.ProjectOptions) return cmd } -func UpCommand(dockerCli command.Cli, projectOpts *compose.ProjectOptions) *cobra.Command { +func UpCommand(dockerCli command.Cli, projectOpts *cli.ProjectOptions) *cobra.Command { opts := upOptions{} cmd := &cobra.Command{ Use: "up", @@ -87,11 +88,11 @@ func UpCommand(dockerCli command.Cli, projectOpts *compose.ProjectOptions) *cobr return cmd } -func PsCommand(dockerCli command.Cli, projectOpts *compose.ProjectOptions) *cobra.Command { +func PsCommand(dockerCli command.Cli, projectOpts *cli.ProjectOptions) *cobra.Command { opts := upOptions{} cmd := &cobra.Command{ Use: "ps", - RunE: compose.WithProject(projectOpts, func(project *compose.Project, args []string) error { + RunE: WithProject(projectOpts, func(project *types.Project, args []string) error { clusteropts, err := docker.GetAwsContext(dockerCli) if err != nil { return err @@ -120,7 +121,7 @@ type downOptions struct { DeleteCluster bool } -func DownCommand(dockerCli command.Cli, projectOpts *compose.ProjectOptions) *cobra.Command { +func DownCommand(dockerCli command.Cli, projectOpts *cli.ProjectOptions) *cobra.Command { opts := downOptions{} cmd := &cobra.Command{ Use: "down", @@ -136,7 +137,7 @@ func DownCommand(dockerCli command.Cli, projectOpts *compose.ProjectOptions) *co return cmd } -func LogsCommand(dockerCli command.Cli, projectOpts *compose.ProjectOptions) *cobra.Command { +func LogsCommand(dockerCli command.Cli, projectOpts *cli.ProjectOptions) *cobra.Command { cmd := &cobra.Command{ Use: "logs [PROJECT NAME]", RunE: docker.WithAwsContext(dockerCli, func(clusteropts docker.AwsContext, args []string) error { @@ -147,7 +148,7 @@ func LogsCommand(dockerCli command.Cli, projectOpts *compose.ProjectOptions) *co var name string if len(args) == 0 { - project, err := compose.ProjectFromOptions(projectOpts) + project, err := cli.ProjectFromOptions(projectOpts) if err != nil { return err } diff --git a/ecs/pkg/compose/opts.go b/ecs/cmd/commands/opts.go similarity index 53% rename from ecs/pkg/compose/opts.go rename to ecs/cmd/commands/opts.go index d2fef0c9d..7d0856c86 100644 --- a/ecs/pkg/compose/opts.go +++ b/ecs/cmd/commands/opts.go @@ -1,26 +1,23 @@ -package compose +package commands import ( + "github.com/compose-spec/compose-go/cli" + "github.com/compose-spec/compose-go/types" "github.com/spf13/cobra" "github.com/spf13/pflag" ) -type ProjectOptions struct { - ConfigPaths []string - Name string -} - -func (o *ProjectOptions) AddFlags(flags *pflag.FlagSet) { +func AddFlags(o *cli.ProjectOptions, flags *pflag.FlagSet) { flags.StringArrayVarP(&o.ConfigPaths, "file", "f", nil, "Specify an alternate compose file") flags.StringVarP(&o.Name, "project-name", "n", "", "Specify an alternate project name (default: directory name)") } -type ProjectFunc func(project *Project, args []string) error +type ProjectFunc func(project *types.Project, args []string) error // WithProject wrap a ProjectFunc into a cobra command -func WithProject(options *ProjectOptions, f ProjectFunc) func(cmd *cobra.Command, args []string) error { +func WithProject(options *cli.ProjectOptions, f ProjectFunc) func(cmd *cobra.Command, args []string) error { return func(cmd *cobra.Command, args []string) error { - project, err := ProjectFromOptions(options) + project, err := cli.ProjectFromOptions(options) if err != nil { return err } diff --git a/ecs/cmd/commands/secret.go b/ecs/cmd/commands/secret.go index a426740df..b6a32c783 100644 --- a/ecs/cmd/commands/secret.go +++ b/ecs/cmd/commands/secret.go @@ -11,7 +11,7 @@ import ( "github.com/docker/cli/cli/command" amazon "github.com/docker/ecs-plugin/pkg/amazon/backend" - "github.com/docker/ecs-plugin/pkg/amazon/types" + "github.com/docker/ecs-plugin/pkg/compose" "github.com/docker/ecs-plugin/pkg/docker" "github.com/spf13/cobra" ) @@ -57,7 +57,7 @@ func CreateSecret(dockerCli command.Cli) *cobra.Command { } name := args[0] - secret := types.NewSecret(name, opts.Username, opts.Password, opts.Description) + secret := compose.NewSecret(name, opts.Username, opts.Password, opts.Description) id, err := backend.CreateSecret(context.Background(), secret) fmt.Println(id) return err @@ -140,7 +140,7 @@ func DeleteSecret(dockerCli command.Cli) *cobra.Command { return cmd } -func printList(out io.Writer, secrets []types.Secret) { +func printList(out io.Writer, secrets []compose.Secret) { printSection(out, len(secrets), func(w io.Writer) { for _, secret := range secrets { fmt.Fprintf(w, "%s\t%s\t%s\n", secret.ID, secret.Name, secret.Description) diff --git a/ecs/go.mod b/ecs/go.mod index 7dd8d7fe2..438cf8b0c 100644 --- a/ecs/go.mod +++ b/ecs/go.mod @@ -14,7 +14,7 @@ require ( 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-20200409090215-53c0040c9127 + github.com/compose-spec/compose-go v0.0.0-20200622094647-0bb9a6c7d89a 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 @@ -35,11 +35,11 @@ require ( github.com/manifoldco/promptui v0.7.0 github.com/mattn/go-sqlite3 v2.0.3+incompatible // indirect github.com/miekg/pkcs11 v1.0.3 // indirect - github.com/mitchellh/mapstructure v1.2.2 + github.com/mitchellh/mapstructure v1.3.2 github.com/morikuni/aec v1.0.0 // indirect github.com/onsi/ginkgo v1.11.0 // indirect github.com/opencontainers/image-spec v1.0.1 // indirect - github.com/sirupsen/logrus v1.5.0 + 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 @@ -52,7 +52,6 @@ require ( gopkg.in/fatih/pool.v2 v2.0.0 // indirect gopkg.in/gorethink/gorethink.v3 v3.0.5 // indirect gopkg.in/ini.v1 v1.55.0 - gotest.tools v2.2.0+incompatible gotest.tools/v3 v3.0.2 vbom.ml/util v0.0.0-20180919145318-efcd4e0f9787 // indirect ) diff --git a/ecs/go.sum b/ecs/go.sum index f0303a304..9edabc47c 100644 --- a/ecs/go.sum +++ b/ecs/go.sum @@ -54,8 +54,12 @@ 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-20200409090215-53c0040c9127 h1:mAsQN3s19glh3KBOQjiRYBhqaX1SdzNqhB3/cuqgSbE= -github.com/compose-spec/compose-go v0.0.0-20200409090215-53c0040c9127/go.mod h1:1PUpzRF1O/65VOqXZuwpCuYY7pJxbIq1jbAvAf62FGM= +github.com/compose-spec/compose-go v0.0.0-20200616184722-5b8dc203fd7f h1:XE6hHZdPjxN8uGaRlvdCB8YwXbz1PXnQ0CboNygdL2o= +github.com/compose-spec/compose-go v0.0.0-20200616184722-5b8dc203fd7f/go.mod h1:d3Vb4tH01Pr4YKD3RvfwguRcezDBUYJTVYgpCSRYSVg= +github.com/compose-spec/compose-go v0.0.0-20200617133919-fca3bb55c5cc h1:jZfF+HzxW+c8Em308MvcK7j5+ZqIAWqFjN1RZnVFzck= +github.com/compose-spec/compose-go v0.0.0-20200617133919-fca3bb55c5cc/go.mod h1:d3Vb4tH01Pr4YKD3RvfwguRcezDBUYJTVYgpCSRYSVg= +github.com/compose-spec/compose-go v0.0.0-20200622094647-0bb9a6c7d89a h1:FmEuebUePUA0Kd/NSiCmdPG/n6eKdZdBtIbfejVtRS8= +github.com/compose-spec/compose-go v0.0.0-20200622094647-0bb9a6c7d89a/go.mod h1:ih9anT8po+49hrb+1j3ldIJ/YRAaBH52ErlQLTKE2Yo= github.com/containerd/cgroups v0.0.0-20190919134610-bf292b21730f/go.mod h1:OApqhQ4XNSNC13gXIwDjhOQxjWa/NxkwZXJ1EvqT0ko= github.com/containerd/console v0.0.0-20180822173158-c12b1e7919c1/go.mod h1:Tj/on1eG8kiEhd0+fhSDzsPAFESxzBBvdyEgyryXffw= github.com/containerd/containerd v1.3.0-beta.2.0.20190828155532-0293cbd26c69/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= @@ -134,9 +138,12 @@ github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5y github.com/google/certificate-transparency-go v1.0.21 h1:Yf1aXowfZ2nuboBsg7iYGLmwsOARdV86pfH3g95wXmE= github.com/google/certificate-transparency-go v1.0.21/go.mod h1:QeJfpSbVSfYc7RgB3gJFj9cbuQMMchQxrWXz8Ruopmg= 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.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4= -github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.4.1 h1:/exdXoGamhu5ONeUJH0deniYLWYvQwW66yvlfiiKTu0= +github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +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/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= @@ -151,6 +158,7 @@ github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= 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= @@ -183,6 +191,8 @@ github.com/kisielk/sqlstruct v0.0.0-20150923205031-648daed35d49/go.mod h1:yyMNCy github.com/kisom/goutils v1.1.0/go.mod h1:+UBTfd78habUYWFbNWTJNG+jNG/i/lGURakr4A/yNRw= github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk= 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.3 h1:CE8S1cTafDpPvMhIxNJKvHsGVBgn1xWYf1NbHQhywc8= +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/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= @@ -218,8 +228,8 @@ 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.2.2 h1:dxe5oCinTXiTIcfgmZecdCzPmAJKd46KsCWc35r0TV4= -github.com/mitchellh/mapstructure v1.2.2/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +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/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= @@ -282,8 +292,8 @@ github.com/sirupsen/logrus v1.0.4-0.20170822132746-89742aefa4b2/go.mod h1:pMByvH github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.3.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= -github.com/sirupsen/logrus v1.5.0 h1:1N5EYkVAPEywqZRJd7cwnRtCb6xJx7NH3T3WUTF980Q= -github.com/sirupsen/logrus v1.5.0/go.mod h1:+F7Ogzej0PZc/94MaYx/nvG9jOFMD2osvC3s+Squfpo= +github.com/sirupsen/logrus v1.6.0 h1:UBcNElsrwanuuMsnGSlYmtmgbb23qDR5dG+6X6Oo89I= +github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s= @@ -330,6 +340,7 @@ github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2 github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0= github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= github.com/xeipuuv/gojsonschema v0.0.0-20180618132009-1d523034197f/go.mod h1:5yf86TLmAcydyeJq5YvxkGPE2fm/u4myDekKRoLuqhs= +github.com/xeipuuv/gojsonschema v0.0.0-20181112162635-ac52e6811b56 h1:yhqBHs09SmmUoNOHc9jgK4a60T3XFRtPAkYxVnqgY50= github.com/xeipuuv/gojsonschema v0.0.0-20181112162635-ac52e6811b56/go.mod h1:5yf86TLmAcydyeJq5YvxkGPE2fm/u4myDekKRoLuqhs= github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74= github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= @@ -450,6 +461,8 @@ gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo= gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= gotest.tools/v3 v3.0.2 h1:kG1BFyqVHuQoVQiR1bWGnfz/fmHvvuiSPIV7rvl360E= diff --git a/ecs/pkg/amazon/backend/cloudformation.go b/ecs/pkg/amazon/backend/cloudformation.go index 115a67d64..5cdbcdc29 100644 --- a/ecs/pkg/amazon/backend/cloudformation.go +++ b/ecs/pkg/amazon/backend/cloudformation.go @@ -5,14 +5,9 @@ import ( "regexp" "strings" - "github.com/compose-spec/compose-go/types" - - "github.com/sirupsen/logrus" - + ecsapi "github.com/aws/aws-sdk-go/service/ecs" "github.com/aws/aws-sdk-go/service/elbv2" cloudmapapi "github.com/aws/aws-sdk-go/service/servicediscovery" - - ecsapi "github.com/aws/aws-sdk-go/service/ecs" "github.com/awslabs/goformation/v4/cloudformation" "github.com/awslabs/goformation/v4/cloudformation/ec2" "github.com/awslabs/goformation/v4/cloudformation/ecs" @@ -21,10 +16,11 @@ import ( "github.com/awslabs/goformation/v4/cloudformation/logs" cloudmap "github.com/awslabs/goformation/v4/cloudformation/servicediscovery" "github.com/awslabs/goformation/v4/cloudformation/tags" - "github.com/docker/ecs-plugin/pkg/amazon/compatibility" + "github.com/compose-spec/compose-go/compatibility" + "github.com/compose-spec/compose-go/types" sdk "github.com/docker/ecs-plugin/pkg/amazon/sdk" - btypes "github.com/docker/ecs-plugin/pkg/amazon/types" "github.com/docker/ecs-plugin/pkg/compose" + "github.com/sirupsen/logrus" ) const ( @@ -35,11 +31,38 @@ const ( ParameterLoadBalancerARN = "ParameterLoadBalancerARN" ) +type FargateCompatibilityChecker struct { + *compatibility.AllowList +} + // Convert a compose project into a CloudFormation template -func (b Backend) Convert(project *compose.Project) (*cloudformation.Template, error) { - warnings := compatibility.Check(project) - for _, w := range warnings { - logrus.Warn(w) +func (b Backend) Convert(project *types.Project) (*cloudformation.Template, error) { + var checker compatibility.Checker = FargateCompatibilityChecker{ + &compatibility.AllowList{ + Supported: []string{ + "services.command", + "services.container_name", + "services.depends_on", + "services.entrypoint", + "services.environment", + "services.healthcheck", + "services.healthcheck.interval", + "services.healthcheck.start_period", + "services.healthcheck.test", + "services.healthcheck.timeout", + "services.networks", + "services.ports", + "services.ports.mode", + "services.ports.target", + "services.ports.protocol", + "services.user", + "services.working_dir", + }, + }, + } + compatibility.Check(project, checker) + for _, err := range checker.Errors() { + logrus.Warn(err.Error()) } template := cloudformation.NewTemplate() @@ -188,7 +211,7 @@ func (b Backend) Convert(project *compose.Project) (*cloudformation.Template, er return template, nil } -func getLoadBalancerType(project *compose.Project) string { +func getLoadBalancerType(project *types.Project) string { for _, service := range project.Services { for _, port := range service.Ports { if port.Published != 80 && port.Published != 443 { @@ -199,7 +222,7 @@ func getLoadBalancerType(project *compose.Project) string { return elbv2.LoadBalancerTypeEnumApplication } -func getLoadBalancerSecurityGroups(project *compose.Project, template *cloudformation.Template) []string { +func getLoadBalancerSecurityGroups(project *types.Project, template *cloudformation.Template) []string { securityGroups := []string{} for _, network := range project.Networks { if !network.Internal { @@ -210,7 +233,7 @@ func getLoadBalancerSecurityGroups(project *compose.Project, template *cloudform return uniqueStrings(securityGroups) } -func createLoadBalancer(project *compose.Project, template *cloudformation.Template) string { +func createLoadBalancer(project *types.Project, template *cloudformation.Template) string { loadBalancerName := fmt.Sprintf("%sLoadBalancer", strings.Title(project.Name)) // Create LoadBalancer if `ParameterLoadBalancerName` is not set template.Conditions["CreateLoadBalancer"] = cloudformation.Equals("", cloudformation.Ref(ParameterLoadBalancerARN)) @@ -270,7 +293,7 @@ func createListener(service types.ServiceConfig, port types.ServicePortConfig, t return listenerName } -func createTargetGroup(project *compose.Project, service types.ServiceConfig, port types.ServicePortConfig, template *cloudformation.Template, protocol string) string { +func createTargetGroup(project *types.Project, service types.ServiceConfig, port types.ServicePortConfig, template *cloudformation.Template, protocol string) string { targetGroupName := fmt.Sprintf( "%s%s%dTargetGroup", normalizeResourceName(service.Name), @@ -345,7 +368,7 @@ func createTaskExecutionRole(service types.ServiceConfig, err error, definition return taskExecutionRole, nil } -func createCluster(project *compose.Project, template *cloudformation.Template) string { +func createCluster(project *types.Project, template *cloudformation.Template) string { template.Resources["Cluster"] = &ecs.Cluster{ ClusterName: project.Name, Tags: []tags.Tag{ @@ -360,7 +383,7 @@ func createCluster(project *compose.Project, template *cloudformation.Template) return cluster } -func createCloudMap(project *compose.Project, template *cloudformation.Template) { +func createCloudMap(project *types.Project, template *cloudformation.Template) { template.Resources["CloudMap"] = &cloudmap.PrivateDnsNamespace{ Description: fmt.Sprintf("Service Map for Docker Compose project %s", project.Name), Name: fmt.Sprintf("%s.local", project.Name), @@ -368,8 +391,8 @@ func createCloudMap(project *compose.Project, template *cloudformation.Template) } } -func convertNetwork(project *compose.Project, net types.NetworkConfig, vpc string, template *cloudformation.Template) string { - if sg, ok := net.Extras[btypes.ExtensionSecurityGroup]; ok { +func convertNetwork(project *types.Project, net types.NetworkConfig, vpc string, template *cloudformation.Template) string { + if sg, ok := net.Extensions[compose.ExtensionSecurityGroup]; ok { logrus.Debugf("Security Group for network %q set by user to %q", net.Name, sg) return sg.(string) } @@ -420,7 +443,7 @@ func convertNetwork(project *compose.Project, net types.NetworkConfig, vpc strin return cloudformation.Ref(securityGroup) } -func networkResourceName(project *compose.Project, network string) string { +func networkResourceName(project *types.Project, network string) string { return fmt.Sprintf("%s%sNetwork", normalizeResourceName(project.Name), normalizeResourceName(network)) } diff --git a/ecs/pkg/amazon/backend/cloudformation_test.go b/ecs/pkg/amazon/backend/cloudformation_test.go index 122714781..001931b0a 100644 --- a/ecs/pkg/amazon/backend/cloudformation_test.go +++ b/ecs/pkg/amazon/backend/cloudformation_test.go @@ -7,13 +7,11 @@ import ( "github.com/aws/aws-sdk-go/service/elbv2" "github.com/awslabs/goformation/v4/cloudformation" "github.com/awslabs/goformation/v4/cloudformation/ec2" - "github.com/awslabs/goformation/v4/cloudformation/iam" - "github.com/awslabs/goformation/v4/cloudformation/elasticloadbalancingv2" + "github.com/awslabs/goformation/v4/cloudformation/iam" + "github.com/compose-spec/compose-go/cli" "github.com/compose-spec/compose-go/loader" "github.com/compose-spec/compose-go/types" - "github.com/docker/ecs-plugin/pkg/compose" - "gotest.tools/v3/assert" "gotest.tools/v3/golden" ) @@ -58,6 +56,10 @@ version: "3" services: test: image: hello_world + networks: + - front-tier + - back-tier + networks: front-tier: name: public @@ -103,7 +105,7 @@ services: assert.Check(t, lb.Type == elbv2.LoadBalancerTypeEnumNetwork) } -func convertResultAsString(t *testing.T, project *compose.Project, clusterName string) string { +func convertResultAsString(t *testing.T, project *types.Project, clusterName string) string { client, err := NewBackend("", clusterName, "") assert.NilError(t, err) result, err := client.Convert(project) @@ -113,12 +115,12 @@ func convertResultAsString(t *testing.T, project *compose.Project, clusterName s return fmt.Sprintf("%s\n", string(resultAsJSON)) } -func load(t *testing.T, paths ...string) *compose.Project { - options := compose.ProjectOptions{ +func load(t *testing.T, paths ...string) *types.Project { + options := cli.ProjectOptions{ Name: t.Name(), ConfigPaths: paths, } - project, err := compose.ProjectFromOptions(&options) + project, err := cli.ProjectFromOptions(&options) assert.NilError(t, err) return project } @@ -130,14 +132,11 @@ func convertYaml(t *testing.T, yaml string) *cloudformation.Template { ConfigFiles: []types.ConfigFile{ {Config: dict}, }, + }, func(options *loader.Options) { + options.Name = "Test" }) assert.NilError(t, err) - err = compose.Normalize(model) - assert.NilError(t, err) - template, err := Backend{}.Convert(&compose.Project{ - Config: *model, - Name: "test", - }) + template, err := Backend{}.Convert(model) assert.NilError(t, err) return template } diff --git a/ecs/pkg/amazon/backend/down.go b/ecs/pkg/amazon/backend/down.go index adbf9af32..18b4cbf9a 100644 --- a/ecs/pkg/amazon/backend/down.go +++ b/ecs/pkg/amazon/backend/down.go @@ -3,14 +3,14 @@ package backend import ( "context" - "github.com/docker/ecs-plugin/pkg/amazon/types" + "github.com/compose-spec/compose-go/cli" "github.com/docker/ecs-plugin/pkg/compose" ) -func (b *Backend) Down(ctx context.Context, options compose.ProjectOptions) error { +func (b *Backend) Down(ctx context.Context, options cli.ProjectOptions) error { name := options.Name if name == "" { - project, err := compose.ProjectFromOptions(&options) + project, err := cli.ProjectFromOptions(&options) if err != nil { return err } @@ -22,7 +22,7 @@ func (b *Backend) Down(ctx context.Context, options compose.ProjectOptions) erro return err } - err = b.WaitStackCompletion(ctx, name, types.StackDelete) + err = b.WaitStackCompletion(ctx, name, compose.StackDelete) if err != nil { return err } diff --git a/ecs/pkg/amazon/backend/down_test.go b/ecs/pkg/amazon/backend/down_test.go index 7d156e246..7439f626c 100644 --- a/ecs/pkg/amazon/backend/down_test.go +++ b/ecs/pkg/amazon/backend/down_test.go @@ -4,8 +4,8 @@ import ( "context" "testing" + "github.com/compose-spec/compose-go/cli" "github.com/docker/ecs-plugin/pkg/amazon/sdk" - btypes "github.com/docker/ecs-plugin/pkg/amazon/types" "github.com/docker/ecs-plugin/pkg/compose" "github.com/golang/mock/gomock" ) @@ -23,10 +23,10 @@ func TestDown(t *testing.T) { recorder := m.EXPECT() recorder.DeleteStack(ctx, "test_project").Return(nil) recorder.GetStackID(ctx, "test_project").Return("stack-123", nil) - recorder.WaitStackComplete(ctx, "stack-123", btypes.StackDelete).Return(nil) + recorder.WaitStackComplete(ctx, "stack-123", compose.StackDelete).Return(nil) recorder.DescribeStackEvents(ctx, "stack-123").Return(nil, nil) - c.Down(ctx, compose.ProjectOptions{ + c.Down(ctx, cli.ProjectOptions{ ConfigPaths: []string{}, Name: "test_project", }) diff --git a/ecs/pkg/amazon/backend/list.go b/ecs/pkg/amazon/backend/list.go index b90905945..4385eae55 100644 --- a/ecs/pkg/amazon/backend/list.go +++ b/ecs/pkg/amazon/backend/list.go @@ -6,11 +6,11 @@ import ( "sort" "strings" - "github.com/docker/ecs-plugin/pkg/amazon/types" + "github.com/compose-spec/compose-go/types" "github.com/docker/ecs-plugin/pkg/compose" ) -func (b *Backend) Ps(ctx context.Context, project *compose.Project) ([]types.TaskStatus, error) { +func (b *Backend) Ps(ctx context.Context, project *types.Project) ([]compose.TaskStatus, error) { cluster := b.Cluster if cluster == "" { cluster = project.Name @@ -19,17 +19,17 @@ func (b *Backend) Ps(ctx context.Context, project *compose.Project) ([]types.Tas for _, service := range project.Services { tasks, err := b.api.ListTasks(ctx, cluster, service.Name) if err != nil { - return []types.TaskStatus{}, err + return []compose.TaskStatus{}, err } arns = append(arns, tasks...) } if len(arns) == 0 { - return []types.TaskStatus{}, nil + return []compose.TaskStatus{}, nil } tasks, err := b.api.DescribeTasks(ctx, cluster, arns...) if err != nil { - return []types.TaskStatus{}, err + return []compose.TaskStatus{}, err } networkInterfaces := []string{} @@ -40,21 +40,21 @@ func (b *Backend) Ps(ctx context.Context, project *compose.Project) ([]types.Tas } publicIps, err := b.api.GetPublicIPs(ctx, networkInterfaces...) if err != nil { - return []types.TaskStatus{}, err + return []compose.TaskStatus{}, err } sort.Slice(tasks, func(i, j int) bool { return strings.Compare(tasks[i].Service, tasks[j].Service) < 0 }) - for i, t := range tasks { + for i, task := range tasks { ports := []string{} - s, err := project.GetService(t.Service) + s, err := project.GetService(task.Service) if err != nil { - return []types.TaskStatus{}, err + return []compose.TaskStatus{}, err } for _, p := range s.Ports { - ports = append(ports, fmt.Sprintf("%s:%d->%d/%s", publicIps[t.NetworkInterface], p.Published, p.Target, p.Protocol)) + ports = append(ports, fmt.Sprintf("%s:%d->%d/%s", publicIps[task.NetworkInterface], p.Published, p.Target, p.Protocol)) } tasks[i].Name = s.Name tasks[i].Ports = ports diff --git a/ecs/pkg/amazon/backend/secrets.go b/ecs/pkg/amazon/backend/secrets.go index f2ae7c678..6c86e95d8 100644 --- a/ecs/pkg/amazon/backend/secrets.go +++ b/ecs/pkg/amazon/backend/secrets.go @@ -3,18 +3,18 @@ package backend import ( "context" - "github.com/docker/ecs-plugin/pkg/amazon/types" + "github.com/docker/ecs-plugin/pkg/compose" ) -func (b Backend) CreateSecret(ctx context.Context, secret types.Secret) (string, error) { +func (b Backend) CreateSecret(ctx context.Context, secret compose.Secret) (string, error) { return b.api.CreateSecret(ctx, secret) } -func (b Backend) InspectSecret(ctx context.Context, id string) (types.Secret, error) { +func (b Backend) InspectSecret(ctx context.Context, id string) (compose.Secret, error) { return b.api.InspectSecret(ctx, id) } -func (b Backend) ListSecrets(ctx context.Context) ([]types.Secret, error) { +func (b Backend) ListSecrets(ctx context.Context) ([]compose.Secret, error) { return b.api.ListSecrets(ctx) } diff --git a/ecs/pkg/amazon/backend/up.go b/ecs/pkg/amazon/backend/up.go index 8c78748a4..24c75d5c6 100644 --- a/ecs/pkg/amazon/backend/up.go +++ b/ecs/pkg/amazon/backend/up.go @@ -4,12 +4,13 @@ import ( "context" "fmt" - "github.com/docker/ecs-plugin/pkg/amazon/types" + "github.com/compose-spec/compose-go/cli" + "github.com/compose-spec/compose-go/types" "github.com/docker/ecs-plugin/pkg/compose" ) -func (b *Backend) Up(ctx context.Context, options compose.ProjectOptions) error { - project, err := compose.ProjectFromOptions(&options) +func (b *Backend) Up(ctx context.Context, options cli.ProjectOptions) error { + project, err := cli.ProjectFromOptions(&options) if err != nil { return err } @@ -66,12 +67,12 @@ func (b *Backend) Up(ctx context.Context, options compose.ProjectOptions) error } fmt.Println() - return b.WaitStackCompletion(ctx, project.Name, types.StackCreate) + return b.WaitStackCompletion(ctx, project.Name, compose.StackCreate) } -func (b Backend) GetVPC(ctx context.Context, project *compose.Project) (string, error) { +func (b Backend) GetVPC(ctx context.Context, project *types.Project) (string, error) { //check compose file for custom VPC selected - if vpc, ok := project.Extras[types.ExtensionVPC]; ok { + if vpc, ok := project.Extensions[compose.ExtensionVPC]; ok { vpcID := vpc.(string) ok, err := b.api.VpcExists(ctx, vpcID) if err != nil { @@ -88,9 +89,9 @@ func (b Backend) GetVPC(ctx context.Context, project *compose.Project) (string, return defaultVPC, nil } -func (b Backend) GetLoadBalancer(ctx context.Context, project *compose.Project) (string, error) { +func (b Backend) GetLoadBalancer(ctx context.Context, project *types.Project) (string, error) { //check compose file for custom VPC selected - if lb, ok := project.Extras[types.ExtensionLB]; ok { + if lb, ok := project.Extensions[compose.ExtensionLB]; ok { lbName := lb.(string) ok, err := b.api.LoadBalancerExists(ctx, lbName) if err != nil { diff --git a/ecs/pkg/amazon/compatibility/check.go b/ecs/pkg/amazon/compatibility/check.go deleted file mode 100644 index 8e46becba..000000000 --- a/ecs/pkg/amazon/compatibility/check.go +++ /dev/null @@ -1,41 +0,0 @@ -package compatibility - -import ( - "github.com/compose-spec/compose-go/types" - "github.com/docker/ecs-plugin/pkg/compose" -) - -type Warning string -type Warnings []string - -type Checker interface { - CheckService(service *types.ServiceConfig) - CheckCapAdd(service *types.ServiceConfig) - CheckDNS(service *types.ServiceConfig) - CheckDNSOpts(service *types.ServiceConfig) - CheckDNSSearch(service *types.ServiceConfig) - CheckDomainName(service *types.ServiceConfig) - CheckExtraHosts(service *types.ServiceConfig) - CheckHostname(service *types.ServiceConfig) - CheckIpc(service *types.ServiceConfig) - CheckLabels(service *types.ServiceConfig) - CheckLinks(service *types.ServiceConfig) - CheckLogging(service *types.ServiceConfig) - CheckMacAddress(service *types.ServiceConfig) - CheckNetworkMode(service *types.ServiceConfig) - CheckPid(service *types.ServiceConfig) - CheckSysctls(service *types.ServiceConfig) - CheckTmpfs(service *types.ServiceConfig) - CheckUserNSMode(service *types.ServiceConfig) - Errors() []error -} - -// Check the compose model do not use unsupported features and inject sane defaults for ECS deployment -func Check(project *compose.Project) []error { - c := FargateCompatibilityChecker{} - for i, service := range project.Services { - c.CheckService(&service) - project.Services[i] = service - } - return c.errors -} diff --git a/ecs/pkg/amazon/compatibility/check_test.go b/ecs/pkg/amazon/compatibility/check_test.go deleted file mode 100644 index f65898537..000000000 --- a/ecs/pkg/amazon/compatibility/check_test.go +++ /dev/null @@ -1,23 +0,0 @@ -package compatibility - -import ( - "testing" - - "github.com/docker/ecs-plugin/pkg/compose" - "gotest.tools/v3/assert" -) - -func load(t *testing.T, paths ...string) *compose.Project { - options := compose.ProjectOptions{ - Name: t.Name(), - ConfigPaths: paths, - } - project, err := compose.ProjectFromOptions(&options) - assert.NilError(t, err) - return project -} -func TestInvalidNetworkMode(t *testing.T) { - project := load(t, "../backend/testdata/invalid_network_mode.yaml") - err := Check(project) - assert.Error(t, err[0], "'network_mode' \"bridge\" is not supported") -} diff --git a/ecs/pkg/amazon/compatibility/compatibility.go b/ecs/pkg/amazon/compatibility/compatibility.go deleted file mode 100644 index c7d67c26d..000000000 --- a/ecs/pkg/amazon/compatibility/compatibility.go +++ /dev/null @@ -1,171 +0,0 @@ -package compatibility - -import ( - "fmt" - - "github.com/aws/aws-sdk-go/service/ecs" - "github.com/compose-spec/compose-go/types" -) - -type FargateCompatibilityChecker struct { - errors []error -} - -func (c *FargateCompatibilityChecker) error(message string, args ...interface{}) { - c.errors = append(c.errors, fmt.Errorf(message, args...)) -} - -func (c *FargateCompatibilityChecker) Errors() []error { - return c.errors -} - -func (c *FargateCompatibilityChecker) CheckService(service *types.ServiceConfig) { - c.CheckCapAdd(service) - c.CheckDNS(service) - c.CheckDNSOpts(service) - c.CheckDNSSearch(service) - c.CheckDomainName(service) - c.CheckExtraHosts(service) - c.CheckHostname(service) - c.CheckIpc(service) - c.CheckLabels(service) - c.CheckLinks(service) - c.CheckLogging(service) - c.CheckMacAddress(service) - c.CheckNetworkMode(service) - c.CheckPid(service) - c.CheckSysctls(service) - c.CheckTmpfs(service) - c.CheckUserNSMode(service) -} - -func (c *FargateCompatibilityChecker) CheckNetworkMode(service *types.ServiceConfig) { - if service.NetworkMode != "" && service.NetworkMode != ecs.NetworkModeAwsvpc { - c.error("'network_mode' %q is not supported", service.NetworkMode) - } - service.NetworkMode = ecs.NetworkModeAwsvpc -} - -func (c *FargateCompatibilityChecker) CheckLinks(service *types.ServiceConfig) { - if len(service.Links) != 0 { - c.error("'links' is not supported") - service.Links = nil - } -} - -func (c *FargateCompatibilityChecker) CheckLogging(service *types.ServiceConfig) { - c.CheckLoggingDriver(service) -} - -func (c *FargateCompatibilityChecker) CheckLoggingDriver(service *types.ServiceConfig) { - if service.LogDriver != "" && service.LogDriver != ecs.LogDriverAwslogs { - c.error("'log_driver' %q is not supported", service.LogDriver) - service.LogDriver = ecs.LogDriverAwslogs - } -} - -func (c *FargateCompatibilityChecker) CheckPid(service *types.ServiceConfig) { - if service.Pid != "" { - c.error("'pid' is not supported") - service.Pid = "" - } -} - -func (c *FargateCompatibilityChecker) CheckUserNSMode(service *types.ServiceConfig) { - if service.UserNSMode != "" { - c.error("'userns_mode' is not supported") - service.UserNSMode = "" - } -} - -func (c *FargateCompatibilityChecker) CheckIpc(service *types.ServiceConfig) { - if service.Ipc != "" { - c.error("'ipc' is not supported") - service.Ipc = "" - } -} - -func (c *FargateCompatibilityChecker) CheckMacAddress(service *types.ServiceConfig) { - if service.MacAddress != "" { - c.error("'mac_address' is not supported") - service.MacAddress = "" - } -} - -func (c *FargateCompatibilityChecker) CheckHostname(service *types.ServiceConfig) { - if service.Hostname != "" { - c.error("'hostname' is not supported") - service.Hostname = "" - } -} - -func (c *FargateCompatibilityChecker) CheckDomainName(service *types.ServiceConfig) { - if service.DomainName != "" { - c.error("'domainname' is not supported") - service.DomainName = "" - } -} - -func (c *FargateCompatibilityChecker) CheckDNSSearch(service *types.ServiceConfig) { - if len(service.DNSSearch) > 0 { - c.error("'dns_search' is not supported") - service.DNSSearch = nil - } -} - -func (c *FargateCompatibilityChecker) CheckDNS(service *types.ServiceConfig) { - if len(service.DNS) > 0 { - c.error("'dns' is not supported") - service.DNS = nil - } -} - -func (c *FargateCompatibilityChecker) CheckDNSOpts(service *types.ServiceConfig) { - if len(service.DNSOpts) > 0 { - c.error("'dns_opt' is not supported") - service.DNSOpts = nil - } -} - -func (c *FargateCompatibilityChecker) CheckExtraHosts(service *types.ServiceConfig) { - if len(service.ExtraHosts) > 0 { - c.error("'extra_hosts' is not supported") - service.ExtraHosts = nil - } -} - -func (c *FargateCompatibilityChecker) CheckCapAdd(service *types.ServiceConfig) { - for i, v := range service.CapAdd { - if v != "SYS_PTRACE" { - c.error("'cap_add' %s is not supported", v) - l := len(service.CapAdd) - service.CapAdd[i] = service.CapAdd[l-1] - service.CapAdd = service.CapAdd[:l-1] - } - } -} - -func (c *FargateCompatibilityChecker) CheckTmpfs(service *types.ServiceConfig) { - if len(service.Tmpfs) > 0 { - c.error("'tmpfs' is not supported") - service.Tmpfs = nil - } -} - -func (c *FargateCompatibilityChecker) CheckSysctls(service *types.ServiceConfig) { - if len(service.Sysctls) > 0 { - c.error("'sysctls' is not supported") - service.Sysctls = nil - } -} - -func (c *FargateCompatibilityChecker) CheckLabels(service *types.ServiceConfig) { - for k, v := range service.Labels { - if v == "" { - c.error("'labels' with an empty value is not supported") - delete(service.Labels, k) - } - } -} - -var _ Checker = &FargateCompatibilityChecker{} diff --git a/ecs/pkg/amazon/sdk/api.go b/ecs/pkg/amazon/sdk/api.go index 137a43397..1e3e61ad3 100644 --- a/ecs/pkg/amazon/sdk/api.go +++ b/ecs/pkg/amazon/sdk/api.go @@ -5,7 +5,7 @@ import ( cf "github.com/aws/aws-sdk-go/service/cloudformation" "github.com/awslabs/goformation/v4/cloudformation" - "github.com/docker/ecs-plugin/pkg/amazon/types" + "github.com/docker/ecs-plugin/pkg/compose" ) //go:generate mockgen -destination=./api_mock.go -self_package "github.com/docker/ecs-plugin/pkg/amazon" -package=amazon . API @@ -38,19 +38,19 @@ type downAPI interface { } type logsAPI interface { - GetLogs(ctx context.Context, name string, consumer types.LogConsumer) error + GetLogs(ctx context.Context, name string, consumer compose.LogConsumer) error } type secretsAPI interface { - CreateSecret(ctx context.Context, secret types.Secret) (string, error) - InspectSecret(ctx context.Context, id string) (types.Secret, error) - ListSecrets(ctx context.Context) ([]types.Secret, error) + CreateSecret(ctx context.Context, secret compose.Secret) (string, error) + InspectSecret(ctx context.Context, id string) (compose.Secret, error) + ListSecrets(ctx context.Context) ([]compose.Secret, error) DeleteSecret(ctx context.Context, id string, recover bool) error } type listAPI interface { ListTasks(ctx context.Context, cluster string, name string) ([]string, error) - DescribeTasks(ctx context.Context, cluster string, arns ...string) ([]types.TaskStatus, error) + DescribeTasks(ctx context.Context, cluster string, arns ...string) ([]compose.TaskStatus, error) GetPublicIPs(ctx context.Context, interfaces ...string) (map[string]string, error) } diff --git a/ecs/pkg/amazon/sdk/api_mock.go b/ecs/pkg/amazon/sdk/api_mock.go index 07ce79e74..0fe42fb2f 100644 --- a/ecs/pkg/amazon/sdk/api_mock.go +++ b/ecs/pkg/amazon/sdk/api_mock.go @@ -8,9 +8,10 @@ import ( context "context" reflect "reflect" + "github.com/docker/ecs-plugin/pkg/compose" + cloudformation "github.com/aws/aws-sdk-go/service/cloudformation" cloudformation0 "github.com/awslabs/goformation/v4/cloudformation" - btypes "github.com/docker/ecs-plugin/pkg/amazon/types" gomock "github.com/golang/mock/gomock" ) @@ -53,7 +54,7 @@ func (mr *MockAPIMockRecorder) ClusterExists(arg0, arg1 interface{}) *gomock.Cal } // CreateSecret mocks base method -func (m *MockAPI) CreateSecret(arg0 context.Context, arg1 btypes.Secret) (string, error) { +func (m *MockAPI) CreateSecret(arg0 context.Context, arg1 compose.Secret) (string, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "CreateSecret", arg0, arg1) ret0, _ := ret[0].(string) @@ -139,14 +140,14 @@ func (mr *MockAPIMockRecorder) DescribeStackEvents(arg0, arg1 interface{}) *gomo } // DescribeTasks mocks base method -func (m *MockAPI) DescribeTasks(arg0 context.Context, arg1 string, arg2 ...string) ([]btypes.TaskStatus, error) { +func (m *MockAPI) DescribeTasks(arg0 context.Context, arg1 string, arg2 ...string) ([]compose.TaskStatus, error) { m.ctrl.T.Helper() varargs := []interface{}{arg0, arg1} for _, a := range arg2 { varargs = append(varargs, a) } ret := m.ctrl.Call(m, "DescribeTasks", varargs...) - ret0, _ := ret[0].([]btypes.TaskStatus) + ret0, _ := ret[0].([]compose.TaskStatus) ret1, _ := ret[1].(error) return ret0, ret1 } @@ -174,7 +175,7 @@ func (mr *MockAPIMockRecorder) GetDefaultVPC(arg0 interface{}) *gomock.Call { } // GetLogs mocks base method -func (m *MockAPI) GetLogs(arg0 context.Context, arg1 string, arg2 btypes.LogConsumer) error { +func (m *MockAPI) GetLogs(arg0 context.Context, arg1 string, arg2 compose.LogConsumer) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "GetLogs", arg0, arg1, arg2) ret0, _ := ret[0].(error) @@ -238,10 +239,10 @@ func (mr *MockAPIMockRecorder) GetSubNets(arg0, arg1 interface{}) *gomock.Call { } // InspectSecret mocks base method -func (m *MockAPI) InspectSecret(arg0 context.Context, arg1 string) (btypes.Secret, error) { +func (m *MockAPI) InspectSecret(arg0 context.Context, arg1 string) (compose.Secret, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "InspectSecret", arg0, arg1) - ret0, _ := ret[0].(btypes.Secret) + ret0, _ := ret[0].(compose.Secret) ret1, _ := ret[1].(error) return ret0, ret1 } @@ -253,10 +254,10 @@ func (mr *MockAPIMockRecorder) InspectSecret(arg0, arg1 interface{}) *gomock.Cal } // ListSecrets mocks base method -func (m *MockAPI) ListSecrets(arg0 context.Context) ([]btypes.Secret, error) { +func (m *MockAPI) ListSecrets(arg0 context.Context) ([]compose.Secret, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "ListSecrets", arg0) - ret0, _ := ret[0].([]btypes.Secret) + ret0, _ := ret[0].([]compose.Secret) ret1, _ := ret[1].(error) return ret0, ret1 } diff --git a/ecs/pkg/amazon/sdk/convert.go b/ecs/pkg/amazon/sdk/convert.go index 8874aa83e..3e2b861f8 100644 --- a/ecs/pkg/amazon/sdk/convert.go +++ b/ecs/pkg/amazon/sdk/convert.go @@ -13,11 +13,10 @@ import ( "github.com/awslabs/goformation/v4/cloudformation/tags" "github.com/compose-spec/compose-go/types" "github.com/docker/cli/opts" - t "github.com/docker/ecs-plugin/pkg/amazon/types" "github.com/docker/ecs-plugin/pkg/compose" ) -func Convert(project *compose.Project, service types.ServiceConfig) (*ecs.TaskDefinition, error) { +func Convert(project *types.Project, service types.ServiceConfig) (*ecs.TaskDefinition, error) { cpu, mem, err := toLimits(service) if err != nil { return nil, err @@ -318,8 +317,8 @@ func getImage(image string) string { func getRepoCredentials(service types.ServiceConfig) *ecs.TaskDefinition_RepositoryCredentials { // extract registry and namespace string from image name - for key, value := range service.Extras { - if key == t.ExtensionPullCredentials { + for key, value := range service.Extensions { + if key == compose.ExtensionPullCredentials { return &ecs.TaskDefinition_RepositoryCredentials{CredentialsParameter: value.(string)} } } diff --git a/ecs/pkg/amazon/sdk/sdk.go b/ecs/pkg/amazon/sdk/sdk.go index 1e1cb8496..3f0ed9297 100644 --- a/ecs/pkg/amazon/sdk/sdk.go +++ b/ecs/pkg/amazon/sdk/sdk.go @@ -23,10 +23,8 @@ import ( "github.com/aws/aws-sdk-go/service/secretsmanager" "github.com/aws/aws-sdk-go/service/secretsmanager/secretsmanageriface" cf "github.com/awslabs/goformation/v4/cloudformation" + "github.com/docker/ecs-plugin/pkg/compose" "github.com/sirupsen/logrus" - - "github.com/docker/ecs-plugin/pkg/amazon/types" - t "github.com/docker/ecs-plugin/pkg/amazon/types" ) type sdk struct { @@ -189,9 +187,9 @@ func (s sdk) WaitStackComplete(ctx context.Context, name string, operation int) StackName: aws.String(name), } switch operation { - case t.StackCreate: + case compose.StackCreate: return s.CF.WaitUntilStackCreateCompleteWithContext(ctx, input) - case t.StackDelete: + case compose.StackDelete: return s.CF.WaitUntilStackDeleteCompleteWithContext(ctx, input) default: return fmt.Errorf("internal error: unexpected stack operation %d", operation) @@ -236,7 +234,7 @@ func (s sdk) DeleteStack(ctx context.Context, name string) error { return err } -func (s sdk) CreateSecret(ctx context.Context, secret t.Secret) (string, error) { +func (s sdk) CreateSecret(ctx context.Context, secret compose.Secret) (string, error) { logrus.Debug("Create secret " + secret.Name) secretStr, err := secret.GetCredString() if err != nil { @@ -254,17 +252,17 @@ func (s sdk) CreateSecret(ctx context.Context, secret t.Secret) (string, error) return *response.ARN, nil } -func (s sdk) InspectSecret(ctx context.Context, id string) (t.Secret, error) { +func (s sdk) InspectSecret(ctx context.Context, id string) (compose.Secret, error) { logrus.Debug("Inspect secret " + id) response, err := s.SM.DescribeSecret(&secretsmanager.DescribeSecretInput{SecretId: &id}) if err != nil { - return t.Secret{}, err + return compose.Secret{}, err } labels := map[string]string{} for _, tag := range response.Tags { labels[*tag.Key] = *tag.Value } - secret := t.Secret{ + secret := compose.Secret{ ID: *response.ARN, Name: *response.Name, Labels: labels, @@ -275,14 +273,14 @@ func (s sdk) InspectSecret(ctx context.Context, id string) (t.Secret, error) { return secret, nil } -func (s sdk) ListSecrets(ctx context.Context) ([]t.Secret, error) { +func (s sdk) ListSecrets(ctx context.Context) ([]compose.Secret, error) { logrus.Debug("List secrets ...") response, err := s.SM.ListSecrets(&secretsmanager.ListSecretsInput{}) if err != nil { - return []t.Secret{}, err + return []compose.Secret{}, err } - var secrets []t.Secret + var secrets []compose.Secret for _, sec := range response.SecretList { @@ -294,7 +292,7 @@ func (s sdk) ListSecrets(ctx context.Context) ([]t.Secret, error) { if sec.Description != nil { description = *sec.Description } - secrets = append(secrets, t.Secret{ + secrets = append(secrets, compose.Secret{ ID: *sec.ARN, Name: *sec.Name, Labels: labels, @@ -311,7 +309,7 @@ func (s sdk) DeleteSecret(ctx context.Context, id string, recover bool) error { return err } -func (s sdk) GetLogs(ctx context.Context, name string, consumer types.LogConsumer) error { +func (s sdk) GetLogs(ctx context.Context, name string, consumer compose.LogConsumer) error { logGroup := fmt.Sprintf("/docker-compose/%s", name) var startTime int64 for { @@ -357,7 +355,7 @@ func (s sdk) ListTasks(ctx context.Context, cluster string, service string) ([]s return arns, nil } -func (s sdk) DescribeTasks(ctx context.Context, cluster string, arns ...string) ([]t.TaskStatus, error) { +func (s sdk) DescribeTasks(ctx context.Context, cluster string, arns ...string) ([]compose.TaskStatus, error) { tasks, err := s.ECS.DescribeTasksWithContext(ctx, &ecs.DescribeTasksInput{ Cluster: aws.String(cluster), Tasks: aws.StringSlice(arns), @@ -365,7 +363,7 @@ func (s sdk) DescribeTasks(ctx context.Context, cluster string, arns ...string) if err != nil { return nil, err } - result := []t.TaskStatus{} + result := []compose.TaskStatus{} for _, task := range tasks.Tasks { var networkInterface string for _, attachement := range task.Attachments { @@ -377,7 +375,7 @@ func (s sdk) DescribeTasks(ctx context.Context, cluster string, arns ...string) } } } - result = append(result, t.TaskStatus{ + result = append(result, compose.TaskStatus{ State: *task.LastStatus, Service: strings.Replace(*task.Group, "service:", "", 1), NetworkInterface: networkInterface, diff --git a/ecs/pkg/compose/api.go b/ecs/pkg/compose/api.go index 6d84ccec4..0d4877a8b 100644 --- a/ecs/pkg/compose/api.go +++ b/ecs/pkg/compose/api.go @@ -4,19 +4,20 @@ import ( "context" "github.com/awslabs/goformation/v4/cloudformation" - "github.com/docker/ecs-plugin/pkg/amazon/types" + "github.com/compose-spec/compose-go/cli" + "github.com/compose-spec/compose-go/types" ) type API interface { - Up(ctx context.Context, options ProjectOptions) error - Down(ctx context.Context, options ProjectOptions) error + Up(ctx context.Context, options cli.ProjectOptions) error + Down(ctx context.Context, options cli.ProjectOptions) error - Convert(project *Project) (*cloudformation.Template, error) + Convert(project *types.Project) (*cloudformation.Template, error) Logs(ctx context.Context, projectName string) error - Ps(background context.Context, project *Project) ([]types.TaskStatus, error) + Ps(background context.Context, project *types.Project) ([]TaskStatus, error) - CreateSecret(ctx context.Context, secret types.Secret) (string, error) - InspectSecret(ctx context.Context, id string) (types.Secret, error) - ListSecrets(ctx context.Context) ([]types.Secret, error) + CreateSecret(ctx context.Context, secret Secret) (string, error) + InspectSecret(ctx context.Context, id string) (Secret, error) + ListSecrets(ctx context.Context) ([]Secret, error) DeleteSecret(ctx context.Context, id string, recover bool) error } diff --git a/ecs/pkg/compose/normalize.go b/ecs/pkg/compose/normalize.go deleted file mode 100644 index 3e4809d1e..000000000 --- a/ecs/pkg/compose/normalize.go +++ /dev/null @@ -1,89 +0,0 @@ -package compose - -import ( - "fmt" - - "github.com/compose-spec/compose-go/types" - "github.com/sirupsen/logrus" -) - -// Normalize a compose-go model to move deprecated attributes to canonical position, and introduce implicit defaults -// FIXME move this to compose-go -func Normalize(model *types.Config) error { - if len(model.Networks) == 0 { - // Compose application model implies a default network if none is explicitly set. - model.Networks["default"] = types.NetworkConfig{ - Name: "default", - } - } - - for i, s := range model.Services { - if len(s.Networks) == 0 { - // Service without explicit network attachment are implicitly exposed on default network - s.Networks = map[string]*types.ServiceNetworkConfig{"default": nil} - } - - for i, p := range s.Ports { - if p.Published == 0 { - p.Published = p.Target - s.Ports[i] = p - } - } - - if s.LogDriver != "" { - logrus.Warn("`log_driver` is deprecated. Use the `logging` attribute") - if s.Logging == nil { - s.Logging = &types.LoggingConfig{} - } - if s.Logging.Driver == "" { - s.Logging.Driver = s.LogDriver - } else { - return fmt.Errorf("can't use both 'log_driver' (deprecated) and 'logging.driver'") - } - } - if len(s.LogOpt) != 0 { - logrus.Warn("`log_opts` is deprecated. Use the `logging` attribute") - if s.Logging == nil { - s.Logging = &types.LoggingConfig{} - } - for k, v := range s.LogOpt { - if _, ok := s.Logging.Options[k]; !ok { - s.Logging.Options[k] = v - } else { - return fmt.Errorf("can't use both 'log_opt' (deprecated) and 'logging.options'") - } - } - } - model.Services[i] = s - } - - for i, n := range model.Networks { - if n.Name == "" { - n.Name = i - model.Networks[i] = n - } - } - - for i, v := range model.Volumes { - if v.Name == "" { - v.Name = i - model.Volumes[i] = v - } - } - - for i, c := range model.Configs { - if c.Name == "" { - c.Name = i - model.Configs[i] = c - } - } - - for i, s := range model.Secrets { - if s.Name == "" { - s.Name = i - model.Secrets[i] = s - } - } - - return nil -} diff --git a/ecs/pkg/compose/project.go b/ecs/pkg/compose/project.go deleted file mode 100644 index a90bba071..000000000 --- a/ecs/pkg/compose/project.go +++ /dev/null @@ -1,170 +0,0 @@ -package compose - -import ( - "fmt" - "io/ioutil" - "os" - "path/filepath" - "regexp" - "strings" - - "github.com/compose-spec/compose-go/loader" - "github.com/compose-spec/compose-go/types" - "github.com/sirupsen/logrus" -) - -type Project struct { - types.Config - projectDir string - Name string `yaml:"-" json:"-"` -} - -func NewProject(config types.ConfigDetails, name string) (*Project, error) { - model, err := loader.Load(config) - if err != nil { - return nil, err - } - - err = Normalize(model) - if err != nil { - return nil, err - } - - p := Project{ - Config: *model, - projectDir: config.WorkingDir, - Name: name, - } - return &p, nil -} - -// projectFromOptions load a compose project based on command line options -func ProjectFromOptions(options *ProjectOptions) (*Project, error) { - configPath, err := getConfigPathFromOptions(options) - if err != nil { - return nil, err - } - - name := options.Name - if name == "" { - name = os.Getenv("COMPOSE_PROJECT_NAME") - } - - workingDir := filepath.Dir(configPath[0]) - - if name == "" { - r := regexp.MustCompile(`[^a-z0-9\\-_]+`) - name = r.ReplaceAllString(strings.ToLower(filepath.Base(workingDir)), "") - } - - configs, err := parseConfigs(configPath) - if err != nil { - return nil, err - } - - return NewProject(types.ConfigDetails{ - WorkingDir: workingDir, - ConfigFiles: configs, - Environment: environment(), - }, name) -} - -func getConfigPathFromOptions(options *ProjectOptions) ([]string, error) { - paths := []string{} - pwd, err := os.Getwd() - if err != nil { - return nil, err - } - - if len(options.ConfigPaths) != 0 { - for _, f := range options.ConfigPaths { - if f == "-" { - paths = append(paths, f) - continue - } - if !filepath.IsAbs(f) { - f = filepath.Join(pwd, f) - } - if _, err := os.Stat(f); err != nil { - return nil, err - } - paths = append(paths, f) - } - return paths, nil - } - - sep := os.Getenv("COMPOSE_FILE_SEPARATOR") - if sep == "" { - sep = string(os.PathListSeparator) - } - f := os.Getenv("COMPOSE_FILE") - if f != "" { - return strings.Split(f, sep), nil - } - - for { - candidates := []string{} - for _, n := range SupportedFilenames { - f := filepath.Join(pwd, n) - if _, err := os.Stat(f); err == nil { - candidates = append(candidates, f) - } - } - if len(candidates) > 0 { - winner := candidates[0] - if len(candidates) > 1 { - logrus.Warnf("Found multiple config files with supported names: %s", strings.Join(candidates, ", ")) - logrus.Warnf("Using %s\n", winner) - } - return []string{winner}, nil - } - parent := filepath.Dir(pwd) - if parent == pwd { - return nil, fmt.Errorf("Can't find a suitable configuration file in this directory or any parent. Are you in the right directory?") - } - pwd = parent - } -} - -var SupportedFilenames = []string{"compose.yaml", "compose.yml", "docker-compose.yml", "docker-compose.yaml"} - -func parseConfigs(configPaths []string) ([]types.ConfigFile, error) { - files := []types.ConfigFile{} - for _, f := range configPaths { - var ( - b []byte - err error - ) - if f == "-" { - b, err = ioutil.ReadAll(os.Stdin) - } else { - if _, err := os.Stat(f); err != nil { - return nil, err - } - b, err = ioutil.ReadFile(f) - } - if err != nil { - return nil, err - } - config, err := loader.ParseYAML(b) - if err != nil { - return nil, err - } - files = append(files, types.ConfigFile{Filename: f, Config: config}) - } - return files, nil -} - -func environment() map[string]string { - return getAsEqualsMap(os.Environ()) -} - -// getAsEqualsMap split key=value formatted strings into a key : value map -func getAsEqualsMap(em []string) map[string]string { - m := make(map[string]string) - for _, v := range em { - kv := strings.SplitN(v, "=", 2) - m[kv[0]] = kv[1] - } - return m -} diff --git a/ecs/pkg/compose/project_test.go b/ecs/pkg/compose/project_test.go deleted file mode 100644 index 733f34f03..000000000 --- a/ecs/pkg/compose/project_test.go +++ /dev/null @@ -1,46 +0,0 @@ -package compose - -import ( - "os" - "testing" - - "gotest.tools/v3/assert" -) - -func Test_project_name(t *testing.T) { - p, err := ProjectFromOptions(&ProjectOptions{ - Name: "my_project", - ConfigPaths: []string{"testdata/simple/compose.yaml"}, - }) - assert.NilError(t, err) - assert.Equal(t, p.Name, "my_project") - - p, err = ProjectFromOptions(&ProjectOptions{ - Name: "", - ConfigPaths: []string{"testdata/simple/compose.yaml"}, - }) - assert.NilError(t, err) - assert.Equal(t, p.Name, "simple") - - os.Setenv("COMPOSE_PROJECT_NAME", "my_project_from_env") - p, err = ProjectFromOptions(&ProjectOptions{ - Name: "", - ConfigPaths: []string{"testdata/simple/compose.yaml"}, - }) - assert.NilError(t, err) - assert.Equal(t, p.Name, "my_project_from_env") -} - -func Test_project_from_set_of_files(t *testing.T) { - p, err := ProjectFromOptions(&ProjectOptions{ - Name: "my_project", - ConfigPaths: []string{ - "testdata/simple/compose.yaml", - "testdata/simple/compose-with-overrides.yaml", - }, - }) - assert.NilError(t, err) - service, err := p.GetService("simple") - assert.NilError(t, err) - assert.Equal(t, service.Image, "haproxy") -} diff --git a/ecs/pkg/amazon/types/types.go b/ecs/pkg/compose/types.go similarity index 98% rename from ecs/pkg/amazon/types/types.go rename to ecs/pkg/compose/types.go index f6815955d..133263658 100644 --- a/ecs/pkg/amazon/types/types.go +++ b/ecs/pkg/compose/types.go @@ -1,4 +1,4 @@ -package types +package compose import "encoding/json" diff --git a/ecs/pkg/amazon/types/x.go b/ecs/pkg/compose/x.go similarity index 92% rename from ecs/pkg/amazon/types/x.go rename to ecs/pkg/compose/x.go index d8b8e0112..7a2a5bd0b 100644 --- a/ecs/pkg/amazon/types/x.go +++ b/ecs/pkg/compose/x.go @@ -1,4 +1,4 @@ -package types +package compose const ( ExtensionSecurityGroup = "x-aws-securitygroup" From 874be0873d425a3ab7c3ec687835338767e57d0c Mon Sep 17 00:00:00 2001 From: Nicolas De Loof <nicolas.deloof@gmail.com> Date: Mon, 15 Jun 2020 15:52:47 +0200 Subject: [PATCH 129/198] generate code inside Docker container Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com> --- ecs/Dockerfile | 2 ++ ecs/builder.Makefile | 5 ++++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/ecs/Dockerfile b/ecs/Dockerfile index 8d18ddfed..dead5829d 100644 --- a/ecs/Dockerfile +++ b/ecs/Dockerfile @@ -18,6 +18,8 @@ COPY . . FROM base AS make-plugin ARG TARGETOS ARG TARGETARCH +RUN apk add build-base +RUN GO111MODULE=on go get github.com/golang/mock/mockgen@latest RUN --mount=type=cache,target=/root/.cache/go-build \ --mount=type=cache,target=/go/pkg/mod \ GOOS=${TARGETOS} \ diff --git a/ecs/builder.Makefile b/ecs/builder.Makefile index 8b6920baa..18bd2da24 100644 --- a/ecs/builder.Makefile +++ b/ecs/builder.Makefile @@ -20,7 +20,10 @@ all: build clean: rm -rf dist/ -build: +generate: pkg/amazon/sdk/api_mock.go + go generate ./... + +build: generate $(GO_BUILD) -v -o $(BINARY_WITH_EXTENSION) cmd/main/main.go cross: From ed262a0461d6e9b897b0f75b6336ee7ba89dcf4a Mon Sep 17 00:00:00 2001 From: Nicolas De Loof <nicolas.deloof@gmail.com> Date: Tue, 23 Jun 2020 10:27:27 +0200 Subject: [PATCH 130/198] Generate mock inside a container Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com> --- ecs/pkg/amazon/sdk/api.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ecs/pkg/amazon/sdk/api.go b/ecs/pkg/amazon/sdk/api.go index 1e3e61ad3..feaa1eb77 100644 --- a/ecs/pkg/amazon/sdk/api.go +++ b/ecs/pkg/amazon/sdk/api.go @@ -8,7 +8,7 @@ import ( "github.com/docker/ecs-plugin/pkg/compose" ) -//go:generate mockgen -destination=./api_mock.go -self_package "github.com/docker/ecs-plugin/pkg/amazon" -package=amazon . API +//go:generate mockgen -destination=./api_mock.go -self_package "github.com/docker/ecs-plugin/pkg/amazon/sdk" -package=sdk . API type API interface { downAPI From 5e1f40b752937fb1c9b9e5787eca56fcc35c0600 Mon Sep 17 00:00:00 2001 From: Nicolas De Loof <nicolas.deloof@gmail.com> Date: Mon, 29 Jun 2020 09:12:43 +0200 Subject: [PATCH 131/198] Document required AWS permissions Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com> --- ecs/docs/requirements.md | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 ecs/docs/requirements.md diff --git a/ecs/docs/requirements.md b/ecs/docs/requirements.md new file mode 100644 index 000000000..d142de4f9 --- /dev/null +++ b/ecs/docs/requirements.md @@ -0,0 +1,31 @@ +## Requirements + +This plugin relies on AWS API credentials, using the same configuration files as +the AWS command line. + +Such credentials can be configured by the `docker ecs setup` command, either by +selecting an existing AWS CLI profile from existing config files, or by creating +one passing an AWS access key ID and secret access key. + +## Permissions + +AWS accounts (or IAM roles) used with the ECS plugin require following permissions: + +- ec2:DescribeSubnets +- ec2:DescribeVpcs +- iam:CreateServiceLinkedRole +- iam:AttachRolePolicy +- cloudformation:* +- ecs:* +- logs:* +- servicediscovery:* +- elasticloadbalancing:* + + +## Okta support + +For those relying on [aws-okta](https://github.com/segmentio/aws-okta) to access a managed AWS account +(as we do at Docker), you can populate your aws config files with temporary access tokens using: +```shell script +aws-okta write-to-credentials <profile> ~/.aws/credentials +``` From cb74f7924e5bf89e67f55229c04fe67b2137643e Mon Sep 17 00:00:00 2001 From: Nicolas De Loof <nicolas.deloof@gmail.com> Date: Mon, 15 Jun 2020 17:48:56 +0200 Subject: [PATCH 132/198] Don't define service resource name if we do, CloudFormation can't update resource and changeset fail with "CloudFormation cannot update a stack when a custom-named resource requires replacing" Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com> --- ecs/pkg/amazon/backend/cloudformation.go | 1 - ecs/pkg/amazon/sdk/sdk.go | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/ecs/pkg/amazon/backend/cloudformation.go b/ecs/pkg/amazon/backend/cloudformation.go index 5cdbcdc29..c27f65317 100644 --- a/ecs/pkg/amazon/backend/cloudformation.go +++ b/ecs/pkg/amazon/backend/cloudformation.go @@ -193,7 +193,6 @@ func (b Backend) Convert(project *types.Project) (*cloudformation.Template, erro }, }, SchedulingStrategy: ecsapi.SchedulingStrategyReplica, - ServiceName: service.Name, ServiceRegistries: []ecs.Service_ServiceRegistry{serviceRegistry}, Tags: []tags.Tag{ { diff --git a/ecs/pkg/amazon/sdk/sdk.go b/ecs/pkg/amazon/sdk/sdk.go index 3f0ed9297..4d8828697 100644 --- a/ecs/pkg/amazon/sdk/sdk.go +++ b/ecs/pkg/amazon/sdk/sdk.go @@ -182,6 +182,7 @@ func (s sdk) CreateStack(ctx context.Context, name string, template *cf.Template }) return err } + func (s sdk) WaitStackComplete(ctx context.Context, name string, operation int) error { input := &cloudformation.DescribeStacksInput{ StackName: aws.String(name), From a1eba59a467578b807b23d7a274252f86203dc65 Mon Sep 17 00:00:00 2001 From: Nicolas De Loof <nicolas.deloof@gmail.com> Date: Wed, 17 Jun 2020 10:20:12 +0200 Subject: [PATCH 133/198] `ps` do list services, not containers Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com> --- ecs/cmd/commands/compose.go | 10 +- ecs/pkg/amazon/backend/cloudformation.go | 3 +- ecs/pkg/amazon/{sdk => backend}/convert.go | 2 +- ecs/pkg/amazon/backend/list.go | 52 ++------ .../simple-cloudformation-conversion.golden | 1 - ...formation-with-overrides-conversion.golden | 1 - ecs/pkg/amazon/sdk/api.go | 4 +- ecs/pkg/amazon/sdk/api_mock.go | 118 ++++++------------ ecs/pkg/amazon/sdk/sdk.go | 51 +++----- ecs/pkg/compose/api.go | 2 +- ecs/pkg/compose/types.go | 13 +- ecs/tests/e2e_deploy_services_test.go | 11 +- 12 files changed, 91 insertions(+), 177 deletions(-) rename ecs/pkg/amazon/{sdk => backend}/convert.go (99%) diff --git a/ecs/cmd/commands/compose.go b/ecs/cmd/commands/compose.go index b3489baee..ed488a891 100644 --- a/ecs/cmd/commands/compose.go +++ b/ecs/cmd/commands/compose.go @@ -101,15 +101,15 @@ func PsCommand(dockerCli command.Cli, projectOpts *cli.ProjectOptions) *cobra.Co if err != nil { return err } - tasks, err := backend.Ps(context.Background(), project) + status, err := backend.Ps(context.Background(), project) if err != nil { return err } - printSection(os.Stdout, len(tasks), func(w io.Writer) { - for _, task := range tasks { - fmt.Fprintf(w, "%s\t%s\t%s\n", task.Name, task.State, strings.Join(task.Ports, " ")) + printSection(os.Stdout, len(status), func(w io.Writer) { + for _, service := range status { + fmt.Fprintf(w, "%s\t%s\t%d/%d\t%s\n", service.ID, service.Name, service.Replicas, service.Desired, strings.Join(service.Ports, " ")) } - }, "NAME", "STATE", "PORTS") + }, "ID", "NAME", "REPLICAS", "PORTS") return nil }), } diff --git a/ecs/pkg/amazon/backend/cloudformation.go b/ecs/pkg/amazon/backend/cloudformation.go index c27f65317..f4111240b 100644 --- a/ecs/pkg/amazon/backend/cloudformation.go +++ b/ecs/pkg/amazon/backend/cloudformation.go @@ -18,7 +18,6 @@ import ( "github.com/awslabs/goformation/v4/cloudformation/tags" "github.com/compose-spec/compose-go/compatibility" "github.com/compose-spec/compose-go/types" - sdk "github.com/docker/ecs-plugin/pkg/amazon/sdk" "github.com/docker/ecs-plugin/pkg/compose" "github.com/sirupsen/logrus" ) @@ -120,7 +119,7 @@ func (b Backend) Convert(project *types.Project) (*cloudformation.Template, erro for _, service := range project.Services { - definition, err := sdk.Convert(project, service) + definition, err := Convert(project, service) if err != nil { return nil, err } diff --git a/ecs/pkg/amazon/sdk/convert.go b/ecs/pkg/amazon/backend/convert.go similarity index 99% rename from ecs/pkg/amazon/sdk/convert.go rename to ecs/pkg/amazon/backend/convert.go index 3e2b861f8..0eace3a69 100644 --- a/ecs/pkg/amazon/sdk/convert.go +++ b/ecs/pkg/amazon/backend/convert.go @@ -1,4 +1,4 @@ -package sdk +package backend import ( "fmt" diff --git a/ecs/pkg/amazon/backend/list.go b/ecs/pkg/amazon/backend/list.go index 4385eae55..c13d2f903 100644 --- a/ecs/pkg/amazon/backend/list.go +++ b/ecs/pkg/amazon/backend/list.go @@ -3,61 +3,29 @@ package backend import ( "context" "fmt" - "sort" - "strings" "github.com/compose-spec/compose-go/types" "github.com/docker/ecs-plugin/pkg/compose" ) -func (b *Backend) Ps(ctx context.Context, project *types.Project) ([]compose.TaskStatus, error) { +func (b *Backend) Ps(ctx context.Context, project *types.Project) ([]compose.ServiceStatus, error) { cluster := b.Cluster if cluster == "" { cluster = project.Name } - arns := []string{} + + status := []compose.ServiceStatus{} for _, service := range project.Services { - tasks, err := b.api.ListTasks(ctx, cluster, service.Name) + desc, err := b.api.DescribeService(ctx, cluster, service.Name) if err != nil { - return []compose.TaskStatus{}, err + return nil, err } - arns = append(arns, tasks...) - } - if len(arns) == 0 { - return []compose.TaskStatus{}, nil - } - - tasks, err := b.api.DescribeTasks(ctx, cluster, arns...) - if err != nil { - return []compose.TaskStatus{}, err - } - - networkInterfaces := []string{} - for _, t := range tasks { - if t.NetworkInterface != "" { - networkInterfaces = append(networkInterfaces, t.NetworkInterface) - } - } - publicIps, err := b.api.GetPublicIPs(ctx, networkInterfaces...) - if err != nil { - return []compose.TaskStatus{}, err - } - - sort.Slice(tasks, func(i, j int) bool { - return strings.Compare(tasks[i].Service, tasks[j].Service) < 0 - }) - - for i, task := range tasks { ports := []string{} - s, err := project.GetService(task.Service) - if err != nil { - return []compose.TaskStatus{}, err + for _, p := range service.Ports { + ports = append(ports, fmt.Sprintf("*:%d->%d/%s", p.Published, p.Target, p.Protocol)) } - for _, p := range s.Ports { - ports = append(ports, fmt.Sprintf("%s:%d->%d/%s", publicIps[task.NetworkInterface], p.Published, p.Target, p.Protocol)) - } - tasks[i].Name = s.Name - tasks[i].Ports = ports + desc.Ports = ports + status = append(status, desc) } - return tasks, nil + return status, nil } diff --git a/ecs/pkg/amazon/backend/testdata/simple/simple-cloudformation-conversion.golden b/ecs/pkg/amazon/backend/testdata/simple/simple-cloudformation-conversion.golden index 0003b38e5..b9fd6a045 100644 --- a/ecs/pkg/amazon/backend/testdata/simple/simple-cloudformation-conversion.golden +++ b/ecs/pkg/amazon/backend/testdata/simple/simple-cloudformation-conversion.golden @@ -104,7 +104,6 @@ } }, "SchedulingStrategy": "REPLICA", - "ServiceName": "simple", "ServiceRegistries": [ { "RegistryArn": { diff --git a/ecs/pkg/amazon/backend/testdata/simple/simple-cloudformation-with-overrides-conversion.golden b/ecs/pkg/amazon/backend/testdata/simple/simple-cloudformation-with-overrides-conversion.golden index 07c47765b..0f93cbd54 100644 --- a/ecs/pkg/amazon/backend/testdata/simple/simple-cloudformation-with-overrides-conversion.golden +++ b/ecs/pkg/amazon/backend/testdata/simple/simple-cloudformation-with-overrides-conversion.golden @@ -104,7 +104,6 @@ } }, "SchedulingStrategy": "REPLICA", - "ServiceName": "simple", "ServiceRegistries": [ { "RegistryArn": { diff --git a/ecs/pkg/amazon/sdk/api.go b/ecs/pkg/amazon/sdk/api.go index feaa1eb77..1ad68afa8 100644 --- a/ecs/pkg/amazon/sdk/api.go +++ b/ecs/pkg/amazon/sdk/api.go @@ -49,9 +49,7 @@ type secretsAPI interface { } type listAPI interface { - ListTasks(ctx context.Context, cluster string, name string) ([]string, error) - DescribeTasks(ctx context.Context, cluster string, arns ...string) ([]compose.TaskStatus, error) - GetPublicIPs(ctx context.Context, interfaces ...string) (map[string]string, error) + DescribeService(ctx context.Context, cluster string, name string) (compose.ServiceStatus, error) } type waitAPI interface { diff --git a/ecs/pkg/amazon/sdk/api_mock.go b/ecs/pkg/amazon/sdk/api_mock.go index 0fe42fb2f..dda73b715 100644 --- a/ecs/pkg/amazon/sdk/api_mock.go +++ b/ecs/pkg/amazon/sdk/api_mock.go @@ -1,18 +1,16 @@ // Code generated by MockGen. DO NOT EDIT. -// Source: github.com/docker/ecs-plugin/pkg/amazon (interfaces: API) +// Source: github.com/docker/ecs-plugin/pkg/amazon/sdk (interfaces: API) -// Package amazon is a generated GoMock package. +// Package sdk is a generated GoMock package. package sdk import ( context "context" - reflect "reflect" - - "github.com/docker/ecs-plugin/pkg/compose" - cloudformation "github.com/aws/aws-sdk-go/service/cloudformation" cloudformation0 "github.com/awslabs/goformation/v4/cloudformation" + compose "github.com/docker/ecs-plugin/pkg/compose" gomock "github.com/golang/mock/gomock" + reflect "reflect" ) // MockAPI is a mock of API interface @@ -124,6 +122,21 @@ func (mr *MockAPIMockRecorder) DeleteStack(arg0, arg1 interface{}) *gomock.Call return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteStack", reflect.TypeOf((*MockAPI)(nil).DeleteStack), arg0, arg1) } +// DescribeService mocks base method +func (m *MockAPI) DescribeService(arg0 context.Context, arg1, arg2 string) (compose.ServiceStatus, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DescribeService", arg0, arg1, arg2) + ret0, _ := ret[0].(compose.ServiceStatus) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// DescribeService indicates an expected call of DescribeService +func (mr *MockAPIMockRecorder) DescribeService(arg0, arg1, arg2 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DescribeService", reflect.TypeOf((*MockAPI)(nil).DescribeService), arg0, arg1, arg2) +} + // DescribeStackEvents mocks base method func (m *MockAPI) DescribeStackEvents(arg0 context.Context, arg1 string) ([]*cloudformation.StackEvent, error) { m.ctrl.T.Helper() @@ -139,26 +152,6 @@ func (mr *MockAPIMockRecorder) DescribeStackEvents(arg0, arg1 interface{}) *gomo return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DescribeStackEvents", reflect.TypeOf((*MockAPI)(nil).DescribeStackEvents), arg0, arg1) } -// DescribeTasks mocks base method -func (m *MockAPI) DescribeTasks(arg0 context.Context, arg1 string, arg2 ...string) ([]compose.TaskStatus, error) { - m.ctrl.T.Helper() - varargs := []interface{}{arg0, arg1} - for _, a := range arg2 { - varargs = append(varargs, a) - } - ret := m.ctrl.Call(m, "DescribeTasks", varargs...) - ret0, _ := ret[0].([]compose.TaskStatus) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// DescribeTasks indicates an expected call of DescribeTasks -func (mr *MockAPIMockRecorder) DescribeTasks(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - varargs := append([]interface{}{arg0, arg1}, arg2...) - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DescribeTasks", reflect.TypeOf((*MockAPI)(nil).DescribeTasks), varargs...) -} - // GetDefaultVPC mocks base method func (m *MockAPI) GetDefaultVPC(arg0 context.Context) (string, error) { m.ctrl.T.Helper() @@ -174,6 +167,21 @@ func (mr *MockAPIMockRecorder) GetDefaultVPC(arg0 interface{}) *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetDefaultVPC", reflect.TypeOf((*MockAPI)(nil).GetDefaultVPC), arg0) } +// GetLoadBalancerARN mocks base method +func (m *MockAPI) GetLoadBalancerARN(arg0 context.Context, arg1 string) (string, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetLoadBalancerARN", arg0, arg1) + ret0, _ := ret[0].(string) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetLoadBalancerARN indicates an expected call of GetLoadBalancerARN +func (mr *MockAPIMockRecorder) GetLoadBalancerARN(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetLoadBalancerARN", reflect.TypeOf((*MockAPI)(nil).GetLoadBalancerARN), arg0, arg1) +} + // GetLogs mocks base method func (m *MockAPI) GetLogs(arg0 context.Context, arg1 string, arg2 compose.LogConsumer) error { m.ctrl.T.Helper() @@ -188,26 +196,6 @@ func (mr *MockAPIMockRecorder) GetLogs(arg0, arg1, arg2 interface{}) *gomock.Cal return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetLogs", reflect.TypeOf((*MockAPI)(nil).GetLogs), arg0, arg1, arg2) } -// GetPublicIPs mocks base method -func (m *MockAPI) GetPublicIPs(arg0 context.Context, arg1 ...string) (map[string]string, error) { - m.ctrl.T.Helper() - varargs := []interface{}{arg0} - for _, a := range arg1 { - varargs = append(varargs, a) - } - ret := m.ctrl.Call(m, "GetPublicIPs", varargs...) - ret0, _ := ret[0].(map[string]string) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// GetPublicIPs indicates an expected call of GetPublicIPs -func (mr *MockAPIMockRecorder) GetPublicIPs(arg0 interface{}, arg1 ...interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - varargs := append([]interface{}{arg0}, arg1...) - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetPublicIPs", reflect.TypeOf((*MockAPI)(nil).GetPublicIPs), varargs...) -} - // GetStackID mocks base method func (m *MockAPI) GetStackID(arg0 context.Context, arg1 string) (string, error) { m.ctrl.T.Helper() @@ -268,19 +256,19 @@ func (mr *MockAPIMockRecorder) ListSecrets(arg0 interface{}) *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListSecrets", reflect.TypeOf((*MockAPI)(nil).ListSecrets), arg0) } -// ListTasks mocks base method -func (m *MockAPI) ListTasks(arg0 context.Context, arg1, arg2 string) ([]string, error) { +// LoadBalancerExists mocks base method +func (m *MockAPI) LoadBalancerExists(arg0 context.Context, arg1 string) (bool, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "ListTasks", arg0, arg1, arg2) - ret0, _ := ret[0].([]string) + ret := m.ctrl.Call(m, "LoadBalancerExists", arg0, arg1) + ret0, _ := ret[0].(bool) ret1, _ := ret[1].(error) return ret0, ret1 } -// ListTasks indicates an expected call of ListTasks -func (mr *MockAPIMockRecorder) ListTasks(arg0, arg1, arg2 interface{}) *gomock.Call { +// LoadBalancerExists indicates an expected call of LoadBalancerExists +func (mr *MockAPIMockRecorder) LoadBalancerExists(arg0, arg1 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListTasks", reflect.TypeOf((*MockAPI)(nil).ListTasks), arg0, arg1, arg2) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LoadBalancerExists", reflect.TypeOf((*MockAPI)(nil).LoadBalancerExists), arg0, arg1) } // StackExists mocks base method @@ -326,27 +314,3 @@ func (mr *MockAPIMockRecorder) WaitStackComplete(arg0, arg1, arg2 interface{}) * mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "WaitStackComplete", reflect.TypeOf((*MockAPI)(nil).WaitStackComplete), arg0, arg1, arg2) } - -// LoadBalancerExists mocks base method -func (m *MockAPI) LoadBalancerExists(arg0 context.Context, arg1 string) (bool, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "LoadBalancerExists", arg0, arg1) - ret0, _ := ret[0].(bool) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// LoadBalancerExists indicates an expected call of VpcExists -func (mr *MockAPIMockRecorder) LoadBalancerExists(arg0, arg1 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LoadBalancerExists", reflect.TypeOf((*MockAPI)(nil).LoadBalancerExists), arg0, arg1) -} - -// GetLoadBalancerARN mocks base method -func (m *MockAPI) GetLoadBalancerARN(arg0 context.Context, arg1 string) (string, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetLoadBalancerARN", arg0) - ret0, _ := ret[0].(string) - ret1, _ := ret[1].(error) - return ret0, ret1 -} diff --git a/ecs/pkg/amazon/sdk/sdk.go b/ecs/pkg/amazon/sdk/sdk.go index 4d8828697..3cc2c0fa0 100644 --- a/ecs/pkg/amazon/sdk/sdk.go +++ b/ecs/pkg/amazon/sdk/sdk.go @@ -341,10 +341,26 @@ func (s sdk) GetLogs(ctx context.Context, name string, consumer compose.LogConsu } } -func (s sdk) ListTasks(ctx context.Context, cluster string, service string) ([]string, error) { +func (s sdk) DescribeService(ctx context.Context, cluster string, name string) (compose.ServiceStatus, error) { + services, err := s.ECS.DescribeServicesWithContext(ctx, &ecs.DescribeServicesInput{ + Cluster: aws.String(cluster), + Services: aws.StringSlice([]string{name}), + }) + if err != nil { + return compose.ServiceStatus{}, err + } + return compose.ServiceStatus{ + ID: *services.Services[0].ServiceName, + Name: name, + Replicas: int(*services.Services[0].RunningCount), + Desired: int(*services.Services[0].DesiredCount), + }, nil +} + +func (s sdk) ListTasks(ctx context.Context, cluster string, family string) ([]string, error) { tasks, err := s.ECS.ListTasksWithContext(ctx, &ecs.ListTasksInput{ - Cluster: aws.String(cluster), - ServiceName: aws.String(service), + Cluster: aws.String(cluster), + Family: aws.String(family), }) if err != nil { return nil, err @@ -356,35 +372,6 @@ func (s sdk) ListTasks(ctx context.Context, cluster string, service string) ([]s return arns, nil } -func (s sdk) DescribeTasks(ctx context.Context, cluster string, arns ...string) ([]compose.TaskStatus, error) { - tasks, err := s.ECS.DescribeTasksWithContext(ctx, &ecs.DescribeTasksInput{ - Cluster: aws.String(cluster), - Tasks: aws.StringSlice(arns), - }) - if err != nil { - return nil, err - } - result := []compose.TaskStatus{} - for _, task := range tasks.Tasks { - var networkInterface string - for _, attachement := range task.Attachments { - if *attachement.Type == "ElasticNetworkInterface" { - for _, pair := range attachement.Details { - if *pair.Name == "networkInterfaceId" { - networkInterface = *pair.Value - } - } - } - } - result = append(result, compose.TaskStatus{ - State: *task.LastStatus, - Service: strings.Replace(*task.Group, "service:", "", 1), - NetworkInterface: networkInterface, - }) - } - return result, nil -} - func (s sdk) GetPublicIPs(ctx context.Context, interfaces ...string) (map[string]string, error) { desc, err := s.EC2.DescribeNetworkInterfaces(&ec2.DescribeNetworkInterfacesInput{ NetworkInterfaceIds: aws.StringSlice(interfaces), diff --git a/ecs/pkg/compose/api.go b/ecs/pkg/compose/api.go index 0d4877a8b..64e7e5c87 100644 --- a/ecs/pkg/compose/api.go +++ b/ecs/pkg/compose/api.go @@ -14,7 +14,7 @@ type API interface { Convert(project *types.Project) (*cloudformation.Template, error) Logs(ctx context.Context, projectName string) error - Ps(background context.Context, project *types.Project) ([]TaskStatus, error) + Ps(background context.Context, project *types.Project) ([]ServiceStatus, error) CreateSecret(ctx context.Context, secret Secret) (string, error) InspectSecret(ctx context.Context, id string) (Secret, error) diff --git a/ecs/pkg/compose/types.go b/ecs/pkg/compose/types.go index 133263658..ae56e65ea 100644 --- a/ecs/pkg/compose/types.go +++ b/ecs/pkg/compose/types.go @@ -2,13 +2,12 @@ package compose import "encoding/json" -type TaskStatus struct { - Name string - State string - Service string - NetworkInterface string - PublicIP string - Ports []string +type ServiceStatus struct { + ID string + Name string + Replicas int + Desired int + Ports []string } const ( diff --git a/ecs/tests/e2e_deploy_services_test.go b/ecs/tests/e2e_deploy_services_test.go index c8a2242ce..e55fc93be 100644 --- a/ecs/tests/e2e_deploy_services_test.go +++ b/ecs/tests/e2e_deploy_services_test.go @@ -6,9 +6,10 @@ import ( "context" "testing" + "github.com/docker/ecs-plugin/pkg/amazon/sdk" + "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws/session" - "github.com/docker/ecs-plugin/pkg/amazon" "github.com/docker/ecs-plugin/pkg/docker" "gotest.tools/v3/assert" "gotest.tools/v3/fs" @@ -47,11 +48,11 @@ func composeUpSimpleService(t *testing.T, cmd icmd.Cmd, awsContext docker.AwsCon }, }) assert.NilError(t, err) - sdk := amazon.NewAPI(session) - arns, err := sdk.ListTasks(bgContext, t.Name(), t.Name()) + api := sdk.NewAPI(session) + arns, err := api.ListTasks(bgContext, t.Name(), t.Name()) assert.NilError(t, err) - tasks, err := sdk.DescribeTasks(bgContext, t.Name(), arns...) - publicIps, err := sdk.GetPublicIPs(context.Background(), tasks[0].NetworkInterface) + tasks, err := api.DescribeTasks(bgContext, t.Name(), arns...) + publicIps, err := api.GetPublicIPs(context.Background(), tasks[0].NetworkInterface) assert.NilError(t, err) for _, ip := range publicIps { icmd.RunCommand("curl", "-I", "http://"+ip).Assert(t, icmd.Success) From 934e7ab9ea2d31df33aa2859ef369703b5ffef9d Mon Sep 17 00:00:00 2001 From: Nicolas De Loof <nicolas.deloof@gmail.com> Date: Thu, 25 Jun 2020 08:14:54 +0200 Subject: [PATCH 134/198] don't set service `Name` so they can be updated by CloudFormation Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com> --- ecs/Dockerfile | 4 +- ecs/go.mod | 2 +- ecs/go.sum | 4 ++ ecs/pkg/amazon/backend/backend.go | 6 --- ecs/pkg/amazon/backend/cloudformation.go | 35 +++++++++++------ ecs/pkg/amazon/backend/down.go | 4 +- ecs/pkg/amazon/backend/list.go | 16 +++++--- ecs/pkg/amazon/backend/up.go | 7 +++- ecs/pkg/amazon/backend/wait.go | 5 +-- ecs/pkg/amazon/sdk/api.go | 2 +- ecs/pkg/amazon/sdk/api_mock.go | 14 +++---- ecs/pkg/amazon/sdk/sdk.go | 50 ++++++++++++++++++------ ecs/pkg/compose/tags.go | 7 ++++ 13 files changed, 103 insertions(+), 53 deletions(-) create mode 100644 ecs/pkg/compose/tags.go diff --git a/ecs/Dockerfile b/ecs/Dockerfile index dead5829d..2d2213bd2 100644 --- a/ecs/Dockerfile +++ b/ecs/Dockerfile @@ -9,7 +9,8 @@ ENV GO111MODULE=on ARG ALPINE_PKG_DOCKER_VERSION RUN apk add --no-cache \ docker=${ALPINE_PKG_DOCKER_VERSION} \ - make + make \ + build-base COPY go.* . RUN --mount=type=cache,target=/go/pkg/mod \ go mod download @@ -18,7 +19,6 @@ COPY . . FROM base AS make-plugin ARG TARGETOS ARG TARGETARCH -RUN apk add build-base RUN GO111MODULE=on go get github.com/golang/mock/mockgen@latest RUN --mount=type=cache,target=/root/.cache/go-build \ --mount=type=cache,target=/go/pkg/mod \ diff --git a/ecs/go.mod b/ecs/go.mod index 438cf8b0c..16f9c9e3a 100644 --- a/ecs/go.mod +++ b/ecs/go.mod @@ -14,7 +14,7 @@ require ( 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-20200622094647-0bb9a6c7d89a + github.com/compose-spec/compose-go v0.0.0-20200624120600-614475470cd8 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 diff --git a/ecs/go.sum b/ecs/go.sum index 9edabc47c..5e211f537 100644 --- a/ecs/go.sum +++ b/ecs/go.sum @@ -60,6 +60,10 @@ github.com/compose-spec/compose-go v0.0.0-20200617133919-fca3bb55c5cc h1:jZfF+Hz github.com/compose-spec/compose-go v0.0.0-20200617133919-fca3bb55c5cc/go.mod h1:d3Vb4tH01Pr4YKD3RvfwguRcezDBUYJTVYgpCSRYSVg= github.com/compose-spec/compose-go v0.0.0-20200622094647-0bb9a6c7d89a h1:FmEuebUePUA0Kd/NSiCmdPG/n6eKdZdBtIbfejVtRS8= github.com/compose-spec/compose-go v0.0.0-20200622094647-0bb9a6c7d89a/go.mod h1:ih9anT8po+49hrb+1j3ldIJ/YRAaBH52ErlQLTKE2Yo= +github.com/compose-spec/compose-go v0.0.0-20200624090650-5d46d553c1e6 h1:9rsA2PlPOv50IOnzSiTqCWrWr3u2q7shPr76Y5hlxF0= +github.com/compose-spec/compose-go v0.0.0-20200624090650-5d46d553c1e6/go.mod h1:ih9anT8po+49hrb+1j3ldIJ/YRAaBH52ErlQLTKE2Yo= +github.com/compose-spec/compose-go v0.0.0-20200624120600-614475470cd8 h1:sVvKsoXizFOuJNc8dM91IeET2/zDNFj3hwHgk437iJ8= +github.com/compose-spec/compose-go v0.0.0-20200624120600-614475470cd8/go.mod h1:ih9anT8po+49hrb+1j3ldIJ/YRAaBH52ErlQLTKE2Yo= github.com/containerd/cgroups v0.0.0-20190919134610-bf292b21730f/go.mod h1:OApqhQ4XNSNC13gXIwDjhOQxjWa/NxkwZXJ1EvqT0ko= github.com/containerd/console v0.0.0-20180822173158-c12b1e7919c1/go.mod h1:Tj/on1eG8kiEhd0+fhSDzsPAFESxzBBvdyEgyryXffw= github.com/containerd/containerd v1.3.0-beta.2.0.20190828155532-0293cbd26c69/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= diff --git a/ecs/pkg/amazon/backend/backend.go b/ecs/pkg/amazon/backend/backend.go index 0d2f07f98..07764df35 100644 --- a/ecs/pkg/amazon/backend/backend.go +++ b/ecs/pkg/amazon/backend/backend.go @@ -6,12 +6,6 @@ import ( "github.com/docker/ecs-plugin/pkg/amazon/sdk" ) -const ( - ProjectTag = "com.docker.compose.project" - NetworkTag = "com.docker.compose.network" - ServiceTag = "com.docker.compose.service" -) - func NewBackend(profile string, cluster string, region string) (*Backend, error) { sess, err := session.NewSessionWithOptions(session.Options{ Profile: profile, diff --git a/ecs/pkg/amazon/backend/cloudformation.go b/ecs/pkg/amazon/backend/cloudformation.go index f4111240b..98b9b00bb 100644 --- a/ecs/pkg/amazon/backend/cloudformation.go +++ b/ecs/pkg/amazon/backend/cloudformation.go @@ -31,13 +31,22 @@ const ( ) type FargateCompatibilityChecker struct { - *compatibility.AllowList + compatibility.AllowList +} + +func (c *FargateCompatibilityChecker) CheckPortsPublished(p *types.ServicePortConfig) { + if p.Published == 0 { + p.Published = p.Target + } + if p.Published != p.Target { + c.Error("published port can't be set to a distinct value than container port") + } } // Convert a compose project into a CloudFormation template func (b Backend) Convert(project *types.Project) (*cloudformation.Template, error) { - var checker compatibility.Checker = FargateCompatibilityChecker{ - &compatibility.AllowList{ + var checker compatibility.Checker = &FargateCompatibilityChecker{ + compatibility.AllowList{ Supported: []string{ "services.command", "services.container_name", @@ -161,7 +170,7 @@ func (b Backend) Convert(project *types.Project) (*cloudformation.Template, erro dependsOn = append(dependsOn, listenerName) serviceLB = append(serviceLB, ecs.Service_LoadBalancer{ ContainerName: service.Name, - ContainerPort: int(port.Published), + ContainerPort: int(port.Target), TargetGroupArn: cloudformation.Ref(targetGroupName), }) } @@ -195,11 +204,11 @@ func (b Backend) Convert(project *types.Project) (*cloudformation.Template, erro ServiceRegistries: []ecs.Service_ServiceRegistry{serviceRegistry}, Tags: []tags.Tag{ { - Key: ProjectTag, + Key: compose.ProjectTag, Value: project.Name, }, { - Key: ServiceTag, + Key: compose.ServiceTag, Value: service.Name, }, }, @@ -252,7 +261,7 @@ func createLoadBalancer(project *types.Project, template *cloudformation.Templat }, Tags: []tags.Tag{ { - Key: ProjectTag, + Key: compose.ProjectTag, Value: project.Name, }, }, @@ -267,7 +276,7 @@ func createListener(service types.ServiceConfig, port types.ServicePortConfig, t "%s%s%dListener", normalizeResourceName(service.Name), strings.ToUpper(port.Protocol), - port.Published, + port.Target, ) //add listener to dependsOn //https://stackoverflow.com/questions/53971873/the-target-group-does-not-have-an-associated-load-balancer @@ -286,7 +295,7 @@ func createListener(service types.ServiceConfig, port types.ServicePortConfig, t }, LoadBalancerArn: loadBalancerARN, Protocol: protocol, - Port: int(port.Published), + Port: int(port.Target), } return listenerName } @@ -304,7 +313,7 @@ func createTargetGroup(project *types.Project, service types.ServiceConfig, port Protocol: protocol, Tags: []tags.Tag{ { - Key: ProjectTag, + Key: compose.ProjectTag, Value: project.Name, }, }, @@ -371,7 +380,7 @@ func createCluster(project *types.Project, template *cloudformation.Template) st ClusterName: project.Name, Tags: []tags.Tag{ { - Key: ProjectTag, + Key: compose.ProjectTag, Value: project.Name, }, }, @@ -420,11 +429,11 @@ func convertNetwork(project *types.Project, net types.NetworkConfig, vpc string, VpcId: vpc, Tags: []tags.Tag{ { - Key: ProjectTag, + Key: compose.ProjectTag, Value: project.Name, }, { - Key: NetworkTag, + Key: compose.NetworkTag, Value: net.Name, }, }, diff --git a/ecs/pkg/amazon/backend/down.go b/ecs/pkg/amazon/backend/down.go index 18b4cbf9a..fcfbc9acf 100644 --- a/ecs/pkg/amazon/backend/down.go +++ b/ecs/pkg/amazon/backend/down.go @@ -5,6 +5,7 @@ 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 { @@ -22,7 +23,8 @@ func (b *Backend) Down(ctx context.Context, options cli.ProjectOptions) error { return err } - err = b.WaitStackCompletion(ctx, name, compose.StackDelete) + w := console.NewProgressWriter() + err = b.WaitStackCompletion(ctx, name, compose.StackDelete, w) if err != nil { return err } diff --git a/ecs/pkg/amazon/backend/list.go b/ecs/pkg/amazon/backend/list.go index c13d2f903..1193d1fcb 100644 --- a/ecs/pkg/amazon/backend/list.go +++ b/ecs/pkg/amazon/backend/list.go @@ -14,18 +14,22 @@ func (b *Backend) Ps(ctx context.Context, project *types.Project) ([]compose.Ser cluster = project.Name } - status := []compose.ServiceStatus{} - for _, service := range project.Services { - desc, err := b.api.DescribeService(ctx, cluster, service.Name) + status, err := b.api.DescribeServices(ctx, cluster, project.Name) + if err != nil { + return nil, err + } + + for i, state := range status { + s, err := project.GetService(state.Name) if err != nil { return nil, err } ports := []string{} - for _, p := range service.Ports { + for _, p := range s.Ports { ports = append(ports, fmt.Sprintf("*:%d->%d/%s", p.Published, p.Target, p.Protocol)) } - desc.Ports = ports - status = append(status, desc) + state.Ports = ports + status[i] = state } return status, nil } diff --git a/ecs/pkg/amazon/backend/up.go b/ecs/pkg/amazon/backend/up.go index 24c75d5c6..fae330e62 100644 --- a/ecs/pkg/amazon/backend/up.go +++ b/ecs/pkg/amazon/backend/up.go @@ -7,6 +7,7 @@ import ( "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" ) func (b *Backend) Up(ctx context.Context, options cli.ProjectOptions) error { @@ -67,7 +68,11 @@ func (b *Backend) Up(ctx context.Context, options cli.ProjectOptions) error { } fmt.Println() - return b.WaitStackCompletion(ctx, project.Name, compose.StackCreate) + w := console.NewProgressWriter() + for k := range template.Resources { + w.ResourceEvent(k, "PENDING", "") + } + return b.WaitStackCompletion(ctx, project.Name, compose.StackCreate, w) } 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 77ad844af..babcaaf64 100644 --- a/ecs/pkg/amazon/backend/wait.go +++ b/ecs/pkg/amazon/backend/wait.go @@ -11,8 +11,7 @@ import ( "github.com/docker/ecs-plugin/pkg/console" ) -func (b *Backend) WaitStackCompletion(ctx context.Context, name string, operation int) error { - w := console.NewProgressWriter() +func (b *Backend) WaitStackCompletion(ctx context.Context, name string, operation int, w console.ProgressWriter) error { knownEvents := map[string]struct{}{} // Get the unique Stack ID so we can collect events without getting some from previous deployments with same name @@ -53,7 +52,7 @@ func (b *Backend) WaitStackCompletion(ctx context.Context, name string, operatio } knownEvents[*event.EventId] = struct{}{} - resource := fmt.Sprintf("%s %q", aws.StringValue(event.ResourceType), aws.StringValue(event.LogicalResourceId)) + resource := aws.StringValue(event.LogicalResourceId) reason := aws.StringValue(event.ResourceStatusReason) status := aws.StringValue(event.ResourceStatus) w.ResourceEvent(resource, status, reason) diff --git a/ecs/pkg/amazon/sdk/api.go b/ecs/pkg/amazon/sdk/api.go index 1ad68afa8..39e27d37d 100644 --- a/ecs/pkg/amazon/sdk/api.go +++ b/ecs/pkg/amazon/sdk/api.go @@ -49,7 +49,7 @@ type secretsAPI interface { } type listAPI interface { - DescribeService(ctx context.Context, cluster string, name string) (compose.ServiceStatus, error) + DescribeServices(ctx context.Context, cluster string, project string) ([]compose.ServiceStatus, error) } type waitAPI interface { diff --git a/ecs/pkg/amazon/sdk/api_mock.go b/ecs/pkg/amazon/sdk/api_mock.go index dda73b715..22294ff0c 100644 --- a/ecs/pkg/amazon/sdk/api_mock.go +++ b/ecs/pkg/amazon/sdk/api_mock.go @@ -122,19 +122,19 @@ func (mr *MockAPIMockRecorder) DeleteStack(arg0, arg1 interface{}) *gomock.Call return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteStack", reflect.TypeOf((*MockAPI)(nil).DeleteStack), arg0, arg1) } -// DescribeService mocks base method -func (m *MockAPI) DescribeService(arg0 context.Context, arg1, arg2 string) (compose.ServiceStatus, error) { +// DescribeServices mocks base method +func (m *MockAPI) DescribeServices(arg0 context.Context, arg1, arg2 string) ([]compose.ServiceStatus, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "DescribeService", arg0, arg1, arg2) - ret0, _ := ret[0].(compose.ServiceStatus) + ret := m.ctrl.Call(m, "DescribeServices", arg0, arg1, arg2) + ret0, _ := ret[0].([]compose.ServiceStatus) ret1, _ := ret[1].(error) return ret0, ret1 } -// DescribeService indicates an expected call of DescribeService -func (mr *MockAPIMockRecorder) DescribeService(arg0, arg1, arg2 interface{}) *gomock.Call { +// DescribeServices indicates an expected call of DescribeServices +func (mr *MockAPIMockRecorder) DescribeServices(arg0, arg1, arg2 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DescribeService", reflect.TypeOf((*MockAPI)(nil).DescribeService), arg0, arg1, arg2) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DescribeServices", reflect.TypeOf((*MockAPI)(nil).DescribeServices), arg0, arg1, arg2) } // DescribeStackEvents mocks base method diff --git a/ecs/pkg/amazon/sdk/sdk.go b/ecs/pkg/amazon/sdk/sdk.go index 3cc2c0fa0..1710c314b 100644 --- a/ecs/pkg/amazon/sdk/sdk.go +++ b/ecs/pkg/amazon/sdk/sdk.go @@ -175,7 +175,7 @@ func (s sdk) CreateStack(ctx context.Context, name string, template *cf.Template StackName: aws.String(name), TemplateBody: aws.String(string(json)), Parameters: param, - TimeoutInMinutes: aws.Int64(10), + TimeoutInMinutes: aws.Int64(15), Capabilities: []*string{ aws.String(cloudformation.CapabilityCapabilityIam), }, @@ -341,20 +341,46 @@ func (s sdk) GetLogs(ctx context.Context, name string, consumer compose.LogConsu } } -func (s sdk) DescribeService(ctx context.Context, cluster string, name string) (compose.ServiceStatus, error) { - services, err := s.ECS.DescribeServicesWithContext(ctx, &ecs.DescribeServicesInput{ - Cluster: aws.String(cluster), - Services: aws.StringSlice([]string{name}), +func (s sdk) DescribeServices(ctx context.Context, cluster string, project string) ([]compose.ServiceStatus, error) { + // TODO handle pagination + list, err := s.ECS.ListServicesWithContext(ctx, &ecs.ListServicesInput{ + Cluster: aws.String(cluster), }) if err != nil { - return compose.ServiceStatus{}, err + return nil, err } - return compose.ServiceStatus{ - ID: *services.Services[0].ServiceName, - Name: name, - Replicas: int(*services.Services[0].RunningCount), - Desired: int(*services.Services[0].DesiredCount), - }, nil + + services, err := s.ECS.DescribeServicesWithContext(ctx, &ecs.DescribeServicesInput{ + Cluster: aws.String(cluster), + Services: list.ServiceArns, + }) + if err != nil { + return nil, err + } + status := []compose.ServiceStatus{} + for _, service := range services.Services { + var name string + var stack string + for _, t := range service.Tags { + switch *t.Key { + case compose.ProjectTag: + stack = *t.Value + case compose.ServiceTag: + name = *t.Value + } + } + if stack != project { + continue + } + status = append(status, compose.ServiceStatus{ + ID: *service.ServiceName, + Name: name, + Replicas: int(*services.Services[0].RunningCount), + Desired: int(*services.Services[0].DesiredCount), + }) + } + + return status, nil } func (s sdk) ListTasks(ctx context.Context, cluster string, family string) ([]string, error) { diff --git a/ecs/pkg/compose/tags.go b/ecs/pkg/compose/tags.go new file mode 100644 index 000000000..43236d45f --- /dev/null +++ b/ecs/pkg/compose/tags.go @@ -0,0 +1,7 @@ +package compose + +const ( + ProjectTag = "com.docker.compose.project" + NetworkTag = "com.docker.compose.network" + ServiceTag = "com.docker.compose.service" +) From d2911c1ea9c463603e3ea82ea5dbcf3ef62a6835 Mon Sep 17 00:00:00 2001 From: Nicolas De Loof <nicolas.deloof@gmail.com> Date: Mon, 29 Jun 2020 10:44:40 +0200 Subject: [PATCH 135/198] includes:"TAGS" is required for DescribeServices Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com> --- ecs/pkg/amazon/sdk/sdk.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/ecs/pkg/amazon/sdk/sdk.go b/ecs/pkg/amazon/sdk/sdk.go index 1710c314b..10ad4b5b0 100644 --- a/ecs/pkg/amazon/sdk/sdk.go +++ b/ecs/pkg/amazon/sdk/sdk.go @@ -353,6 +353,7 @@ func (s sdk) DescribeServices(ctx context.Context, cluster string, project strin services, err := s.ECS.DescribeServicesWithContext(ctx, &ecs.DescribeServicesInput{ Cluster: aws.String(cluster), Services: list.ServiceArns, + Include: aws.StringSlice([]string{"TAGS"}), }) if err != nil { return nil, err @@ -375,8 +376,8 @@ func (s sdk) DescribeServices(ctx context.Context, cluster string, project strin status = append(status, compose.ServiceStatus{ ID: *service.ServiceName, Name: name, - Replicas: int(*services.Services[0].RunningCount), - Desired: int(*services.Services[0].DesiredCount), + Replicas: int(*service.RunningCount), + Desired: int(*service.DesiredCount), }) } From e2c903c85fde8821f20259cd7978675564bd562d Mon Sep 17 00:00:00 2001 From: Nicolas De Loof <nicolas.deloof@gmail.com> Date: Tue, 30 Jun 2020 11:37:03 +0200 Subject: [PATCH 136/198] Set version by most recent Tag Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com> --- ecs/Dockerfile | 7 ++++++- ecs/Makefile | 12 +++++++++++- ecs/builder.Makefile | 4 +++- ecs/cmd/commands/version.go | 9 +++++++-- ecs/tests/e2e_deploy_services_test.go | 3 +-- ecs/tests/version_test.go | 18 ++++++++++++++++++ 6 files changed, 46 insertions(+), 7 deletions(-) create mode 100644 ecs/tests/version_test.go diff --git a/ecs/Dockerfile b/ecs/Dockerfile index 2d2213bd2..9d2b0751e 100644 --- a/ecs/Dockerfile +++ b/ecs/Dockerfile @@ -14,12 +14,14 @@ RUN apk add --no-cache \ COPY go.* . RUN --mount=type=cache,target=/go/pkg/mod \ go mod download -COPY . . FROM base AS make-plugin ARG TARGETOS ARG TARGETARCH RUN GO111MODULE=on go get github.com/golang/mock/mockgen@latest +ARG COMMIT +ARG TAG +COPY . . RUN --mount=type=cache,target=/root/.cache/go-build \ --mount=type=cache,target=/go/pkg/mod \ GOOS=${TARGETOS} \ @@ -27,6 +29,9 @@ RUN --mount=type=cache,target=/root/.cache/go-build \ make -f builder.Makefile build FROM base AS make-cross +ARG COMMIT +ARG TAG +COPY . . RUN --mount=type=cache,target=/root/.cache/go-build \ --mount=type=cache,target=/go/pkg/mod \ make -f builder.Makefile cross diff --git a/ecs/Makefile b/ecs/Makefile index 22dd36049..cf9c48cc2 100644 --- a/ecs/Makefile +++ b/ecs/Makefile @@ -3,21 +3,31 @@ PWD=$(shell pwd) export DOCKER_BUILDKIT=1 +COMMIT := $(shell git rev-parse --short HEAD) +TAG := $(shell git describe --tags --dirty --match "v*") + .DEFAULT_GOAL := build build: ## Build for the current @docker build . \ --output ./dist \ --platform ${PLATFORM} \ + --build-arg COMMIT=${COMMIT} \ + --build-arg TAG=${TAG} \ --target build cross: ## Cross build for linux, macos and windows @docker build . \ --output ./dist \ + --build-arg COMMIT=${COMMIT} \ + --build-arg TAG=${TAG} \ --target cross test: build ## Run tests - @docker build . --target test + @docker build . \ + --build-arg COMMIT=${COMMIT} \ + --build-arg TAG=${TAG} \ + --target test e2e: build ## Run tests go test ./... -v -tags=e2e diff --git a/ecs/builder.Makefile b/ecs/builder.Makefile index 18bd2da24..4bdf27894 100644 --- a/ecs/builder.Makefile +++ b/ecs/builder.Makefile @@ -7,7 +7,9 @@ ifeq ($(GOOS),windows) endif STATIC_FLAGS=CGO_ENABLED=0 -LDFLAGS:="-s -w" +LDFLAGS := "-s -w \ + -X github.com/docker/ecs-plugin/cmd/commands.GitCommit=$(COMMIT) \ + -X github.com/docker/ecs-plugin/cmd/commands.Version=$(TAG)" GO_BUILD=$(STATIC_FLAGS) go build -trimpath -ldflags=$(LDFLAGS) BINARY=dist/docker-ecs diff --git a/ecs/cmd/commands/version.go b/ecs/cmd/commands/version.go index d3b44687c..58e7ccab3 100644 --- a/ecs/cmd/commands/version.go +++ b/ecs/cmd/commands/version.go @@ -6,14 +6,19 @@ import ( "github.com/spf13/cobra" ) -const Version = "0.0.1" +var ( + // Version is the git tag that this was built from. + Version = "unknown" + // GitCommit is the commit that this was built from. + GitCommit = "unknown" +) func VersionCommand() *cobra.Command { return &cobra.Command{ Use: "version", Short: "Show version.", RunE: func(cmd *cobra.Command, args []string) error { - fmt.Fprintf(cmd.OutOrStdout(), "Docker ECS plugin %s\n", Version) + fmt.Fprintf(cmd.OutOrStdout(), "Docker ECS plugin %s (%s)\n", Version, GitCommit) return nil }, } diff --git a/ecs/tests/e2e_deploy_services_test.go b/ecs/tests/e2e_deploy_services_test.go index e55fc93be..32b6dab50 100644 --- a/ecs/tests/e2e_deploy_services_test.go +++ b/ecs/tests/e2e_deploy_services_test.go @@ -6,10 +6,9 @@ import ( "context" "testing" - "github.com/docker/ecs-plugin/pkg/amazon/sdk" - "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/docker" "gotest.tools/v3/assert" "gotest.tools/v3/fs" diff --git a/ecs/tests/version_test.go b/ecs/tests/version_test.go new file mode 100644 index 000000000..ad4d6455f --- /dev/null +++ b/ecs/tests/version_test.go @@ -0,0 +1,18 @@ +package tests + +import ( + "strings" + "testing" + + "gotest.tools/v3/assert" + "gotest.tools/v3/icmd" +) + +func TestVersionIsSet(t *testing.T) { + cmd, cleanup, _ := dockerCli.createTestCmd() + defer cleanup() + + cmd.Command = dockerCli.Command("ecs", "version") + out := icmd.RunCmd(cmd).Assert(t, icmd.Success).Stdout() + assert.Check(t, !strings.Contains(out, "unknown")) +} From 2bc1b710f2b11fe5b584815a638be92b87e708ab Mon Sep 17 00:00:00 2001 From: Nicolas De Loof <nicolas.deloof@gmail.com> Date: Tue, 30 Jun 2020 15:52:04 +0200 Subject: [PATCH 137/198] Testcase to check resources get tagged Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com> --- ecs/pkg/amazon/backend/cloudformation_test.go | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/ecs/pkg/amazon/backend/cloudformation_test.go b/ecs/pkg/amazon/backend/cloudformation_test.go index 001931b0a..fec167203 100644 --- a/ecs/pkg/amazon/backend/cloudformation_test.go +++ b/ecs/pkg/amazon/backend/cloudformation_test.go @@ -2,8 +2,11 @@ package backend import ( "fmt" + "reflect" "testing" + "github.com/docker/ecs-plugin/pkg/compose" + "github.com/aws/aws-sdk-go/service/elbv2" "github.com/awslabs/goformation/v4/cloudformation" "github.com/awslabs/goformation/v4/cloudformation/ec2" @@ -105,6 +108,31 @@ services: assert.Check(t, lb.Type == elbv2.LoadBalancerTypeEnumNetwork) } +func TestResourcesHaveProjectTagSet(t *testing.T) { + template := convertYaml(t, ` +version: "3" +services: + test: + image: nginx + ports: + - 80:80 + - 88:88 +`) + for _, r := range template.Resources { + tags := reflect.Indirect(reflect.ValueOf(r)).FieldByName("Tags") + if !tags.IsValid() { + continue + } + for i := 0; i < tags.Len(); i++ { + k := tags.Index(i).FieldByName("Key").String() + v := tags.Index(i).FieldByName("Value").String() + if k == compose.ProjectTag { + assert.Equal(t, v, "Test") + } + } + } +} + func convertResultAsString(t *testing.T, project *types.Project, clusterName string) string { client, err := NewBackend("", clusterName, "") assert.NilError(t, err) From c0c31de0c8fd72b6b7bd69cbcbb9d3051d4ce2d7 Mon Sep 17 00:00:00 2001 From: Nicolas De Loof <nicolas.deloof@gmail.com> Date: Tue, 30 Jun 2020 17:15:52 +0200 Subject: [PATCH 138/198] testcase to check service mapping Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com> --- ecs/pkg/amazon/backend/cloudformation.go | 15 +++++++ ecs/pkg/amazon/backend/cloudformation_test.go | 44 ++++++++++++++++++- 2 files changed, 57 insertions(+), 2 deletions(-) diff --git a/ecs/pkg/amazon/backend/cloudformation.go b/ecs/pkg/amazon/backend/cloudformation.go index 98b9b00bb..3df5b8ab1 100644 --- a/ecs/pkg/amazon/backend/cloudformation.go +++ b/ecs/pkg/amazon/backend/cloudformation.go @@ -43,6 +43,19 @@ func (c *FargateCompatibilityChecker) CheckPortsPublished(p *types.ServicePortCo } } +func (c *FargateCompatibilityChecker) CheckCapAdd(service *types.ServiceConfig) { + add := []string{} + for _, cap := range service.CapAdd { + switch cap { + case "SYS_PTRACE": + add = append(add, cap) + default: + c.Error("service.cap_add = %s", cap) + } + } + service.CapAdd = add +} + // Convert a compose project into a CloudFormation template func (b Backend) Convert(project *types.Project) (*cloudformation.Template, error) { var checker compatibility.Checker = &FargateCompatibilityChecker{ @@ -50,9 +63,11 @@ func (b Backend) Convert(project *types.Project) (*cloudformation.Template, erro Supported: []string{ "services.command", "services.container_name", + "services.cap_drop", "services.depends_on", "services.entrypoint", "services.environment", + "services.init", "services.healthcheck", "services.healthcheck.interval", "services.healthcheck.start_period", diff --git a/ecs/pkg/amazon/backend/cloudformation_test.go b/ecs/pkg/amazon/backend/cloudformation_test.go index fec167203..2fcd15420 100644 --- a/ecs/pkg/amazon/backend/cloudformation_test.go +++ b/ecs/pkg/amazon/backend/cloudformation_test.go @@ -5,16 +5,16 @@ import ( "reflect" "testing" - "github.com/docker/ecs-plugin/pkg/compose" - "github.com/aws/aws-sdk-go/service/elbv2" "github.com/awslabs/goformation/v4/cloudformation" "github.com/awslabs/goformation/v4/cloudformation/ec2" + "github.com/awslabs/goformation/v4/cloudformation/ecs" "github.com/awslabs/goformation/v4/cloudformation/elasticloadbalancingv2" "github.com/awslabs/goformation/v4/cloudformation/iam" "github.com/compose-spec/compose-go/cli" "github.com/compose-spec/compose-go/loader" "github.com/compose-spec/compose-go/types" + "github.com/docker/ecs-plugin/pkg/compose" "gotest.tools/v3/assert" "gotest.tools/v3/golden" ) @@ -108,6 +108,46 @@ services: assert.Check(t, lb.Type == elbv2.LoadBalancerTypeEnumNetwork) } +func TestServiceMapping(t *testing.T) { + template := convertYaml(t, ` +version: "3" +services: + test: + image: "image" + command: "command" + entrypoint: "entrypoint" + environment: + - "FOO=BAR" + cap_add: + - SYS_PTRACE + cap_drop: + - SYSLOG + init: true + user: "user" + working_dir: "working_dir" +`) + def := template.Resources["TestTaskDefinition"].(*ecs.TaskDefinition) + container := def.ContainerDefinitions[0] + assert.Equal(t, container.Image, "docker.io/library/image") + assert.Equal(t, container.Command[0], "command") + assert.Equal(t, container.EntryPoint[0], "entrypoint") + assert.Equal(t, get(container.Environment, "FOO"), "BAR") + assert.Check(t, container.LinuxParameters.InitProcessEnabled) + assert.Equal(t, container.LinuxParameters.Capabilities.Add[0], "SYS_PTRACE") + assert.Equal(t, container.LinuxParameters.Capabilities.Drop[0], "SYSLOG") + assert.Equal(t, container.User, "user") + assert.Equal(t, container.WorkingDirectory, "working_dir") +} + +func get(l []ecs.TaskDefinition_KeyValuePair, name string) string { + for _, e := range l { + if e.Name == name { + return e.Value + } + } + return "" +} + func TestResourcesHaveProjectTagSet(t *testing.T) { template := convertYaml(t, ` version: "3" From 5c53138a34fcd75f825c3b38c0b243245c2ea619 Mon Sep 17 00:00:00 2001 From: Nicolas De Loof <nicolas.deloof@gmail.com> Date: Wed, 1 Jul 2020 16:21:15 +0200 Subject: [PATCH 139/198] Drop use of mockgen made obsolete as we use CloudFormation Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com> --- ecs/Dockerfile | 1 - ecs/builder.Makefile | 5 +- ecs/go.mod | 1 - ecs/go.sum | 16 -- ecs/pkg/amazon/backend/down_test.go | 33 --- ecs/pkg/amazon/sdk/api.go | 36 +--- ecs/pkg/amazon/sdk/api_mock.go | 316 ---------------------------- ecs/pkg/amazon/sdk/sdk.go | 12 -- 8 files changed, 7 insertions(+), 413 deletions(-) delete mode 100644 ecs/pkg/amazon/backend/down_test.go delete mode 100644 ecs/pkg/amazon/sdk/api_mock.go diff --git a/ecs/Dockerfile b/ecs/Dockerfile index 9d2b0751e..185a080e5 100644 --- a/ecs/Dockerfile +++ b/ecs/Dockerfile @@ -18,7 +18,6 @@ RUN --mount=type=cache,target=/go/pkg/mod \ FROM base AS make-plugin ARG TARGETOS ARG TARGETARCH -RUN GO111MODULE=on go get github.com/golang/mock/mockgen@latest ARG COMMIT ARG TAG COPY . . diff --git a/ecs/builder.Makefile b/ecs/builder.Makefile index 4bdf27894..a39549917 100644 --- a/ecs/builder.Makefile +++ b/ecs/builder.Makefile @@ -22,10 +22,7 @@ all: build clean: rm -rf dist/ -generate: pkg/amazon/sdk/api_mock.go - go generate ./... - -build: generate +build: $(GO_BUILD) -v -o $(BINARY_WITH_EXTENSION) cmd/main/main.go cross: diff --git a/ecs/go.mod b/ecs/go.mod index 16f9c9e3a..163b56fc3 100644 --- a/ecs/go.mod +++ b/ecs/go.mod @@ -26,7 +26,6 @@ require ( github.com/docker/libtrust v0.0.0-20160708172513-aabc10ec26b7 // indirect github.com/gofrs/uuid v3.2.0+incompatible // indirect github.com/gogo/protobuf v1.3.1 // indirect - github.com/golang/mock v1.4.3 github.com/gorilla/mux v1.7.3 // indirect github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed // indirect github.com/jinzhu/gorm v1.9.12 // indirect diff --git a/ecs/go.sum b/ecs/go.sum index 5e211f537..27588bfef 100644 --- a/ecs/go.sum +++ b/ecs/go.sum @@ -54,14 +54,6 @@ 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-20200616184722-5b8dc203fd7f h1:XE6hHZdPjxN8uGaRlvdCB8YwXbz1PXnQ0CboNygdL2o= -github.com/compose-spec/compose-go v0.0.0-20200616184722-5b8dc203fd7f/go.mod h1:d3Vb4tH01Pr4YKD3RvfwguRcezDBUYJTVYgpCSRYSVg= -github.com/compose-spec/compose-go v0.0.0-20200617133919-fca3bb55c5cc h1:jZfF+HzxW+c8Em308MvcK7j5+ZqIAWqFjN1RZnVFzck= -github.com/compose-spec/compose-go v0.0.0-20200617133919-fca3bb55c5cc/go.mod h1:d3Vb4tH01Pr4YKD3RvfwguRcezDBUYJTVYgpCSRYSVg= -github.com/compose-spec/compose-go v0.0.0-20200622094647-0bb9a6c7d89a h1:FmEuebUePUA0Kd/NSiCmdPG/n6eKdZdBtIbfejVtRS8= -github.com/compose-spec/compose-go v0.0.0-20200622094647-0bb9a6c7d89a/go.mod h1:ih9anT8po+49hrb+1j3ldIJ/YRAaBH52ErlQLTKE2Yo= -github.com/compose-spec/compose-go v0.0.0-20200624090650-5d46d553c1e6 h1:9rsA2PlPOv50IOnzSiTqCWrWr3u2q7shPr76Y5hlxF0= -github.com/compose-spec/compose-go v0.0.0-20200624090650-5d46d553c1e6/go.mod h1:ih9anT8po+49hrb+1j3ldIJ/YRAaBH52ErlQLTKE2Yo= github.com/compose-spec/compose-go v0.0.0-20200624120600-614475470cd8 h1:sVvKsoXizFOuJNc8dM91IeET2/zDNFj3hwHgk437iJ8= github.com/compose-spec/compose-go v0.0.0-20200624120600-614475470cd8/go.mod h1:ih9anT8po+49hrb+1j3ldIJ/YRAaBH52ErlQLTKE2Yo= github.com/containerd/cgroups v0.0.0-20190919134610-bf292b21730f/go.mod h1:OApqhQ4XNSNC13gXIwDjhOQxjWa/NxkwZXJ1EvqT0ko= @@ -133,8 +125,6 @@ github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2V github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= -github.com/golang/mock v1.4.3 h1:GV+pQPG/EUUbkh47niozDcADz6go/dUwhVzdUQHIVRw= -github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs= @@ -144,8 +134,6 @@ 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.4.1 h1:/exdXoGamhu5ONeUJH0deniYLWYvQwW66yvlfiiKTu0= -github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 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/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= @@ -412,7 +400,6 @@ golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3 h1:7TYNF4UdlohbFwpNH04CoPMp1 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/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= @@ -426,7 +413,6 @@ golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGm golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190624222133-a101b041ded4/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= @@ -474,7 +460,5 @@ gotest.tools/v3 v3.0.2/go.mod h1:3SzNCllyD9/Y+b5r9JIKQ474KzkZyqLqEfYqMsX94Bk= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= k8s.io/kubernetes v1.13.0/go.mod h1:ocZa8+6APFNC2tX1DZASIbocyYT5jHzqFVsY5aoB7Jk= -rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= -rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= vbom.ml/util v0.0.0-20180919145318-efcd4e0f9787 h1:O69FD9pJA4WUZlEwYatBEEkRWKQ5cKodWpdKTrCS/iQ= vbom.ml/util v0.0.0-20180919145318-efcd4e0f9787/go.mod h1:so/NYdZXCz+E3ZpW0uAoCj6uzU2+8OWDFv/HxUSs7kI= diff --git a/ecs/pkg/amazon/backend/down_test.go b/ecs/pkg/amazon/backend/down_test.go deleted file mode 100644 index 7439f626c..000000000 --- a/ecs/pkg/amazon/backend/down_test.go +++ /dev/null @@ -1,33 +0,0 @@ -package backend - -import ( - "context" - "testing" - - "github.com/compose-spec/compose-go/cli" - "github.com/docker/ecs-plugin/pkg/amazon/sdk" - "github.com/docker/ecs-plugin/pkg/compose" - "github.com/golang/mock/gomock" -) - -func TestDown(t *testing.T) { - ctrl := gomock.NewController(t) - defer ctrl.Finish() - m := sdk.NewMockAPI(ctrl) - c := &Backend{ - Cluster: "test_cluster", - Region: "region", - api: m, - } - ctx := context.TODO() - recorder := m.EXPECT() - recorder.DeleteStack(ctx, "test_project").Return(nil) - recorder.GetStackID(ctx, "test_project").Return("stack-123", nil) - recorder.WaitStackComplete(ctx, "stack-123", compose.StackDelete).Return(nil) - recorder.DescribeStackEvents(ctx, "stack-123").Return(nil, nil) - - c.Down(ctx, cli.ProjectOptions{ - ConfigPaths: []string{}, - Name: "test_project", - }) -} diff --git a/ecs/pkg/amazon/sdk/api.go b/ecs/pkg/amazon/sdk/api.go index 39e27d37d..38782f5d6 100644 --- a/ecs/pkg/amazon/sdk/api.go +++ b/ecs/pkg/amazon/sdk/api.go @@ -8,52 +8,28 @@ import ( "github.com/docker/ecs-plugin/pkg/compose" ) -//go:generate mockgen -destination=./api_mock.go -self_package "github.com/docker/ecs-plugin/pkg/amazon/sdk" -package=sdk . API - type API interface { - downAPI - upAPI - logsAPI - secretsAPI - listAPI -} - -type upAPI interface { - waitAPI GetDefaultVPC(ctx context.Context) (string, error) VpcExists(ctx context.Context, vpcID string) (bool, error) GetSubNets(ctx context.Context, vpcID string) ([]string, error) - ClusterExists(ctx context.Context, name string) (bool, error) StackExists(ctx context.Context, name string) (bool, error) CreateStack(ctx context.Context, name string, template *cloudformation.Template, parameters map[string]string) error + DeleteStack(ctx context.Context, name string) error + DescribeServices(ctx context.Context, cluster string, project string) ([]compose.ServiceStatus, error) + GetStackID(ctx context.Context, name string) (string, error) + WaitStackComplete(ctx context.Context, name string, operation int) error + DescribeStackEvents(ctx context.Context, stackID string) ([]*cf.StackEvent, error) LoadBalancerExists(ctx context.Context, name string) (bool, error) GetLoadBalancerARN(ctx context.Context, name string) (string, error) -} -type downAPI interface { - DeleteStack(ctx context.Context, name string) error - DeleteCluster(ctx context.Context, name string) error -} + ClusterExists(ctx context.Context, name string) (bool, error) -type logsAPI interface { GetLogs(ctx context.Context, name string, consumer compose.LogConsumer) error -} -type secretsAPI interface { CreateSecret(ctx context.Context, secret compose.Secret) (string, error) InspectSecret(ctx context.Context, id string) (compose.Secret, error) ListSecrets(ctx context.Context) ([]compose.Secret, error) DeleteSecret(ctx context.Context, id string, recover bool) error } - -type listAPI interface { - DescribeServices(ctx context.Context, cluster string, project string) ([]compose.ServiceStatus, error) -} - -type waitAPI interface { - GetStackID(ctx context.Context, name string) (string, error) - WaitStackComplete(ctx context.Context, name string, operation int) error - DescribeStackEvents(ctx context.Context, stackID string) ([]*cf.StackEvent, error) -} diff --git a/ecs/pkg/amazon/sdk/api_mock.go b/ecs/pkg/amazon/sdk/api_mock.go deleted file mode 100644 index 22294ff0c..000000000 --- a/ecs/pkg/amazon/sdk/api_mock.go +++ /dev/null @@ -1,316 +0,0 @@ -// Code generated by MockGen. DO NOT EDIT. -// Source: github.com/docker/ecs-plugin/pkg/amazon/sdk (interfaces: API) - -// Package sdk is a generated GoMock package. -package sdk - -import ( - context "context" - cloudformation "github.com/aws/aws-sdk-go/service/cloudformation" - cloudformation0 "github.com/awslabs/goformation/v4/cloudformation" - compose "github.com/docker/ecs-plugin/pkg/compose" - gomock "github.com/golang/mock/gomock" - reflect "reflect" -) - -// MockAPI is a mock of API interface -type MockAPI struct { - ctrl *gomock.Controller - recorder *MockAPIMockRecorder -} - -// MockAPIMockRecorder is the mock recorder for MockAPI -type MockAPIMockRecorder struct { - mock *MockAPI -} - -// NewMockAPI creates a new mock instance -func NewMockAPI(ctrl *gomock.Controller) *MockAPI { - mock := &MockAPI{ctrl: ctrl} - mock.recorder = &MockAPIMockRecorder{mock} - return mock -} - -// EXPECT returns an object that allows the caller to indicate expected use -func (m *MockAPI) EXPECT() *MockAPIMockRecorder { - return m.recorder -} - -// ClusterExists mocks base method -func (m *MockAPI) ClusterExists(arg0 context.Context, arg1 string) (bool, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "ClusterExists", arg0, arg1) - ret0, _ := ret[0].(bool) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// ClusterExists indicates an expected call of ClusterExists -func (mr *MockAPIMockRecorder) ClusterExists(arg0, arg1 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ClusterExists", reflect.TypeOf((*MockAPI)(nil).ClusterExists), arg0, arg1) -} - -// CreateSecret mocks base method -func (m *MockAPI) CreateSecret(arg0 context.Context, arg1 compose.Secret) (string, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "CreateSecret", arg0, arg1) - ret0, _ := ret[0].(string) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// CreateSecret indicates an expected call of CreateSecret -func (mr *MockAPIMockRecorder) CreateSecret(arg0, arg1 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateSecret", reflect.TypeOf((*MockAPI)(nil).CreateSecret), arg0, arg1) -} - -// CreateStack mocks base method -func (m *MockAPI) CreateStack(arg0 context.Context, arg1 string, arg2 *cloudformation0.Template, arg3 map[string]string) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "CreateStack", arg0, arg1, arg2, arg3) - ret0, _ := ret[0].(error) - return ret0 -} - -// CreateStack indicates an expected call of CreateStack -func (mr *MockAPIMockRecorder) CreateStack(arg0, arg1, arg2, arg3 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateStack", reflect.TypeOf((*MockAPI)(nil).CreateStack), arg0, arg1, arg2, arg3) -} - -// DeleteCluster mocks base method -func (m *MockAPI) DeleteCluster(arg0 context.Context, arg1 string) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "DeleteCluster", arg0, arg1) - ret0, _ := ret[0].(error) - return ret0 -} - -// DeleteCluster indicates an expected call of DeleteCluster -func (mr *MockAPIMockRecorder) DeleteCluster(arg0, arg1 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteCluster", reflect.TypeOf((*MockAPI)(nil).DeleteCluster), arg0, arg1) -} - -// DeleteSecret mocks base method -func (m *MockAPI) DeleteSecret(arg0 context.Context, arg1 string, arg2 bool) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "DeleteSecret", arg0, arg1, arg2) - ret0, _ := ret[0].(error) - return ret0 -} - -// DeleteSecret indicates an expected call of DeleteSecret -func (mr *MockAPIMockRecorder) DeleteSecret(arg0, arg1, arg2 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteSecret", reflect.TypeOf((*MockAPI)(nil).DeleteSecret), arg0, arg1, arg2) -} - -// DeleteStack mocks base method -func (m *MockAPI) DeleteStack(arg0 context.Context, arg1 string) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "DeleteStack", arg0, arg1) - ret0, _ := ret[0].(error) - return ret0 -} - -// DeleteStack indicates an expected call of DeleteStack -func (mr *MockAPIMockRecorder) DeleteStack(arg0, arg1 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteStack", reflect.TypeOf((*MockAPI)(nil).DeleteStack), arg0, arg1) -} - -// DescribeServices mocks base method -func (m *MockAPI) DescribeServices(arg0 context.Context, arg1, arg2 string) ([]compose.ServiceStatus, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "DescribeServices", arg0, arg1, arg2) - ret0, _ := ret[0].([]compose.ServiceStatus) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// DescribeServices indicates an expected call of DescribeServices -func (mr *MockAPIMockRecorder) DescribeServices(arg0, arg1, arg2 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DescribeServices", reflect.TypeOf((*MockAPI)(nil).DescribeServices), arg0, arg1, arg2) -} - -// DescribeStackEvents mocks base method -func (m *MockAPI) DescribeStackEvents(arg0 context.Context, arg1 string) ([]*cloudformation.StackEvent, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "DescribeStackEvents", arg0, arg1) - ret0, _ := ret[0].([]*cloudformation.StackEvent) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// DescribeStackEvents indicates an expected call of DescribeStackEvents -func (mr *MockAPIMockRecorder) DescribeStackEvents(arg0, arg1 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DescribeStackEvents", reflect.TypeOf((*MockAPI)(nil).DescribeStackEvents), arg0, arg1) -} - -// GetDefaultVPC mocks base method -func (m *MockAPI) GetDefaultVPC(arg0 context.Context) (string, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetDefaultVPC", arg0) - ret0, _ := ret[0].(string) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// GetDefaultVPC indicates an expected call of GetDefaultVPC -func (mr *MockAPIMockRecorder) GetDefaultVPC(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetDefaultVPC", reflect.TypeOf((*MockAPI)(nil).GetDefaultVPC), arg0) -} - -// GetLoadBalancerARN mocks base method -func (m *MockAPI) GetLoadBalancerARN(arg0 context.Context, arg1 string) (string, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetLoadBalancerARN", arg0, arg1) - ret0, _ := ret[0].(string) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// GetLoadBalancerARN indicates an expected call of GetLoadBalancerARN -func (mr *MockAPIMockRecorder) GetLoadBalancerARN(arg0, arg1 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetLoadBalancerARN", reflect.TypeOf((*MockAPI)(nil).GetLoadBalancerARN), arg0, arg1) -} - -// GetLogs mocks base method -func (m *MockAPI) GetLogs(arg0 context.Context, arg1 string, arg2 compose.LogConsumer) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetLogs", arg0, arg1, arg2) - ret0, _ := ret[0].(error) - return ret0 -} - -// GetLogs indicates an expected call of GetLogs -func (mr *MockAPIMockRecorder) GetLogs(arg0, arg1, arg2 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetLogs", reflect.TypeOf((*MockAPI)(nil).GetLogs), arg0, arg1, arg2) -} - -// GetStackID mocks base method -func (m *MockAPI) GetStackID(arg0 context.Context, arg1 string) (string, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetStackID", arg0, arg1) - ret0, _ := ret[0].(string) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// GetStackID indicates an expected call of GetStackID -func (mr *MockAPIMockRecorder) GetStackID(arg0, arg1 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetStackID", reflect.TypeOf((*MockAPI)(nil).GetStackID), arg0, arg1) -} - -// GetSubNets mocks base method -func (m *MockAPI) GetSubNets(arg0 context.Context, arg1 string) ([]string, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetSubNets", arg0, arg1) - ret0, _ := ret[0].([]string) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// GetSubNets indicates an expected call of GetSubNets -func (mr *MockAPIMockRecorder) GetSubNets(arg0, arg1 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSubNets", reflect.TypeOf((*MockAPI)(nil).GetSubNets), arg0, arg1) -} - -// InspectSecret mocks base method -func (m *MockAPI) InspectSecret(arg0 context.Context, arg1 string) (compose.Secret, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "InspectSecret", arg0, arg1) - ret0, _ := ret[0].(compose.Secret) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// InspectSecret indicates an expected call of InspectSecret -func (mr *MockAPIMockRecorder) InspectSecret(arg0, arg1 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InspectSecret", reflect.TypeOf((*MockAPI)(nil).InspectSecret), arg0, arg1) -} - -// ListSecrets mocks base method -func (m *MockAPI) ListSecrets(arg0 context.Context) ([]compose.Secret, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "ListSecrets", arg0) - ret0, _ := ret[0].([]compose.Secret) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// ListSecrets indicates an expected call of ListSecrets -func (mr *MockAPIMockRecorder) ListSecrets(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListSecrets", reflect.TypeOf((*MockAPI)(nil).ListSecrets), arg0) -} - -// LoadBalancerExists mocks base method -func (m *MockAPI) LoadBalancerExists(arg0 context.Context, arg1 string) (bool, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "LoadBalancerExists", arg0, arg1) - ret0, _ := ret[0].(bool) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// LoadBalancerExists indicates an expected call of LoadBalancerExists -func (mr *MockAPIMockRecorder) LoadBalancerExists(arg0, arg1 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LoadBalancerExists", reflect.TypeOf((*MockAPI)(nil).LoadBalancerExists), arg0, arg1) -} - -// StackExists mocks base method -func (m *MockAPI) StackExists(arg0 context.Context, arg1 string) (bool, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "StackExists", arg0, arg1) - ret0, _ := ret[0].(bool) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// StackExists indicates an expected call of StackExists -func (mr *MockAPIMockRecorder) StackExists(arg0, arg1 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "StackExists", reflect.TypeOf((*MockAPI)(nil).StackExists), arg0, arg1) -} - -// VpcExists mocks base method -func (m *MockAPI) VpcExists(arg0 context.Context, arg1 string) (bool, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "VpcExists", arg0, arg1) - ret0, _ := ret[0].(bool) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// VpcExists indicates an expected call of VpcExists -func (mr *MockAPIMockRecorder) VpcExists(arg0, arg1 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "VpcExists", reflect.TypeOf((*MockAPI)(nil).VpcExists), arg0, arg1) -} - -// WaitStackComplete mocks base method -func (m *MockAPI) WaitStackComplete(arg0 context.Context, arg1 string, arg2 int) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "WaitStackComplete", arg0, arg1, arg2) - ret0, _ := ret[0].(error) - return ret0 -} - -// WaitStackComplete indicates an expected call of WaitStackComplete -func (mr *MockAPIMockRecorder) WaitStackComplete(arg0, arg1, arg2 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "WaitStackComplete", reflect.TypeOf((*MockAPI)(nil).WaitStackComplete), arg0, arg1, arg2) -} diff --git a/ecs/pkg/amazon/sdk/sdk.go b/ecs/pkg/amazon/sdk/sdk.go index 10ad4b5b0..7359862c2 100644 --- a/ecs/pkg/amazon/sdk/sdk.go +++ b/ecs/pkg/amazon/sdk/sdk.go @@ -70,18 +70,6 @@ func (s sdk) CreateCluster(ctx context.Context, name string) (string, error) { return *response.Cluster.Status, nil } -func (s sdk) DeleteCluster(ctx context.Context, name string) error { - logrus.Debug("Delete cluster ", name) - response, err := s.ECS.DeleteClusterWithContext(ctx, &ecs.DeleteClusterInput{Cluster: aws.String(name)}) - if err != nil { - return err - } - if *response.Cluster.Status == "INACTIVE" { - return nil - } - return fmt.Errorf("Failed to delete cluster, status: %s" + *response.Cluster.Status) -} - func (s sdk) VpcExists(ctx context.Context, vpcID string) (bool, error) { logrus.Debug("Check if VPC exists: ", vpcID) _, err := s.EC2.DescribeVpcsWithContext(ctx, &ec2.DescribeVpcsInput{VpcIds: []*string{&vpcID}}) From 2917251f5fdfad551ccb469e31fe4fb41d2d9c25 Mon Sep 17 00:00:00 2001 From: Nicolas De Loof <nicolas.deloof@gmail.com> Date: Thu, 2 Jul 2020 16:51:55 +0200 Subject: [PATCH 140/198] clarify project status Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com> --- ecs/README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/ecs/README.md b/ecs/README.md index c0cb834b5..a4e0caf53 100644 --- a/ecs/README.md +++ b/ecs/README.md @@ -1,5 +1,9 @@ # Docker CLI plugin for Amazon ECS +## Status + +:exclamation: The Docker ECS plugin is still in Beta. It's design and UX will evolve until 1.0 Final release. + ## Architecture ECS plugin is a [Docker CLI plugin](https://docs.docker.com/engine/extend/cli_plugins/) From 324443deb618edfb298fcbab0a74dbd7b83b4333 Mon Sep 17 00:00:00 2001 From: Nicolas De Loof <nicolas.deloof@gmail.com> Date: Thu, 2 Jul 2020 11:43:20 +0200 Subject: [PATCH 141/198] Customize SDK requests to AWS API with user-agent Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com> --- ecs/builder.Makefile | 4 ++-- ecs/cmd/commands/version.go | 11 +++-------- ecs/cmd/commands/version_test.go | 4 +++- ecs/cmd/main/main.go | 3 ++- ecs/internal/version.go | 8 ++++++++ ecs/pkg/amazon/sdk/sdk.go | 7 +++++++ 6 files changed, 25 insertions(+), 12 deletions(-) create mode 100644 ecs/internal/version.go diff --git a/ecs/builder.Makefile b/ecs/builder.Makefile index a39549917..ae52173d5 100644 --- a/ecs/builder.Makefile +++ b/ecs/builder.Makefile @@ -8,8 +8,8 @@ endif STATIC_FLAGS=CGO_ENABLED=0 LDFLAGS := "-s -w \ - -X github.com/docker/ecs-plugin/cmd/commands.GitCommit=$(COMMIT) \ - -X github.com/docker/ecs-plugin/cmd/commands.Version=$(TAG)" + -X github.com/docker/ecs-plugin/internal.GitCommit=$(COMMIT) \ + -X github.com/docker/ecs-plugin/internal.Version=$(TAG)" GO_BUILD=$(STATIC_FLAGS) go build -trimpath -ldflags=$(LDFLAGS) BINARY=dist/docker-ecs diff --git a/ecs/cmd/commands/version.go b/ecs/cmd/commands/version.go index 58e7ccab3..ce0253898 100644 --- a/ecs/cmd/commands/version.go +++ b/ecs/cmd/commands/version.go @@ -3,14 +3,9 @@ package commands import ( "fmt" - "github.com/spf13/cobra" -) + "github.com/docker/ecs-plugin/internal" -var ( - // Version is the git tag that this was built from. - Version = "unknown" - // GitCommit is the commit that this was built from. - GitCommit = "unknown" + "github.com/spf13/cobra" ) func VersionCommand() *cobra.Command { @@ -18,7 +13,7 @@ func VersionCommand() *cobra.Command { Use: "version", Short: "Show version.", RunE: func(cmd *cobra.Command, args []string) error { - fmt.Fprintf(cmd.OutOrStdout(), "Docker ECS plugin %s (%s)\n", Version, GitCommit) + fmt.Fprintf(cmd.OutOrStdout(), "Docker ECS plugin %s (%s)\n", internal.Version, internal.GitCommit) return nil }, } diff --git a/ecs/cmd/commands/version_test.go b/ecs/cmd/commands/version_test.go index 4c0ed7e69..43a9ab814 100644 --- a/ecs/cmd/commands/version_test.go +++ b/ecs/cmd/commands/version_test.go @@ -5,6 +5,8 @@ import ( "strings" "testing" + "github.com/docker/ecs-plugin/internal" + "gotest.tools/v3/assert" ) @@ -14,5 +16,5 @@ func TestVersion(t *testing.T) { root.SetOut(&out) root.SetArgs([]string{"version"}) root.Execute() - assert.Check(t, strings.Contains(out.String(), Version)) + assert.Check(t, strings.Contains(out.String(), internal.Version)) } diff --git a/ecs/cmd/main/main.go b/ecs/cmd/main/main.go index 5dd1f4dd1..f4140394a 100644 --- a/ecs/cmd/main/main.go +++ b/ecs/cmd/main/main.go @@ -2,6 +2,7 @@ package main import ( "github.com/docker/ecs-plugin/cmd/commands" + "github.com/docker/ecs-plugin/internal" "github.com/docker/cli/cli-plugins/manager" "github.com/docker/cli/cli-plugins/plugin" @@ -16,7 +17,7 @@ func main() { }, manager.Metadata{ SchemaVersion: "0.1.0", Vendor: "Docker Inc.", - Version: commands.Version, + Version: internal.Version, Experimental: true, }) } diff --git a/ecs/internal/version.go b/ecs/internal/version.go new file mode 100644 index 000000000..b0fc1f6a6 --- /dev/null +++ b/ecs/internal/version.go @@ -0,0 +1,8 @@ +package internal + +var ( + // Version is the git tag that this was built from. + Version = "unknown" + // GitCommit is the commit that this was built from. + GitCommit = "unknown" +) diff --git a/ecs/pkg/amazon/sdk/sdk.go b/ecs/pkg/amazon/sdk/sdk.go index 7359862c2..af38523b7 100644 --- a/ecs/pkg/amazon/sdk/sdk.go +++ b/ecs/pkg/amazon/sdk/sdk.go @@ -6,6 +6,10 @@ import ( "strings" "time" + "github.com/docker/ecs-plugin/internal" + + "github.com/aws/aws-sdk-go/aws/request" + "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws/session" "github.com/aws/aws-sdk-go/service/cloudformation" @@ -39,6 +43,9 @@ type sdk struct { } func NewAPI(sess *session.Session) API { + sess.Handlers.Build.PushBack(func(r *request.Request) { + request.AddToUserAgent(r, fmt.Sprintf("Docker CLI %s", internal.Version)) + }) return sdk{ ECS: ecs.New(sess), EC2: ec2.New(sess), From f892ee1004fc4cdd9a1d78663b33be36d34a8e18 Mon Sep 17 00:00:00 2001 From: Nicolas De Loof <nicolas.deloof@gmail.com> Date: Fri, 3 Jul 2020 11:07:08 +0200 Subject: [PATCH 142/198] `ps` shows LoadBalancer URL Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com> --- ecs/pkg/amazon/backend/list.go | 28 ++++++++++++++-- ecs/pkg/amazon/backend/up.go | 1 - ecs/pkg/amazon/sdk/api.go | 8 +++-- ecs/pkg/amazon/sdk/sdk.go | 58 +++++++++++++++++++--------------- ecs/pkg/compose/types.go | 7 ++++ 5 files changed, 71 insertions(+), 31 deletions(-) diff --git a/ecs/pkg/amazon/backend/list.go b/ecs/pkg/amazon/backend/list.go index 1193d1fcb..00b4adee3 100644 --- a/ecs/pkg/amazon/backend/list.go +++ b/ecs/pkg/amazon/backend/list.go @@ -14,7 +14,31 @@ func (b *Backend) Ps(ctx context.Context, project *types.Project) ([]compose.Ser cluster = project.Name } - status, err := b.api.DescribeServices(ctx, cluster, project.Name) + resources, err := b.api.ListStackResources(ctx, project.Name) + if err != nil { + return nil, err + } + + var loadBalancer string + if lb, ok := project.Extensions[compose.ExtensionLB]; ok { + loadBalancer = lb.(string) + } + servicesARN := []string{} + for _, r := range resources { + switch r.Type { + case "AWS::ECS::Service": + servicesARN = append(servicesARN, r.ARN) + case "AWS::ElasticLoadBalancingV2::LoadBalancer": + loadBalancer = r.ARN + } + } + + status, err := b.api.DescribeServices(ctx, cluster, servicesARN) + if err != nil { + return nil, err + } + + url, err := b.api.GetLoadBalancerURL(ctx, loadBalancer) if err != nil { return nil, err } @@ -26,7 +50,7 @@ func (b *Backend) Ps(ctx context.Context, project *types.Project) ([]compose.Ser } ports := []string{} for _, p := range s.Ports { - ports = append(ports, fmt.Sprintf("*:%d->%d/%s", p.Published, p.Target, p.Protocol)) + ports = append(ports, fmt.Sprintf("%s:%d->%d/%s", url, p.Published, p.Target, p.Protocol)) } state.Ports = ports status[i] = state diff --git a/ecs/pkg/amazon/backend/up.go b/ecs/pkg/amazon/backend/up.go index fae330e62..104fef915 100644 --- a/ecs/pkg/amazon/backend/up.go +++ b/ecs/pkg/amazon/backend/up.go @@ -105,7 +105,6 @@ func (b Backend) GetLoadBalancer(ctx context.Context, project *types.Project) (s if !ok { return "", fmt.Errorf("Load Balancer does not exist: %s", lb) } - return b.api.GetLoadBalancerARN(ctx, lbName) } return "", nil } diff --git a/ecs/pkg/amazon/sdk/api.go b/ecs/pkg/amazon/sdk/api.go index 38782f5d6..057823c53 100644 --- a/ecs/pkg/amazon/sdk/api.go +++ b/ecs/pkg/amazon/sdk/api.go @@ -16,13 +16,15 @@ type API interface { StackExists(ctx context.Context, name string) (bool, error) CreateStack(ctx context.Context, name string, template *cloudformation.Template, parameters map[string]string) error DeleteStack(ctx context.Context, name string) error - DescribeServices(ctx context.Context, cluster string, project string) ([]compose.ServiceStatus, error) + ListStackResources(ctx context.Context, name string) ([]compose.StackResource, error) GetStackID(ctx context.Context, name string) (string, error) WaitStackComplete(ctx context.Context, name string, operation int) error DescribeStackEvents(ctx context.Context, stackID string) ([]*cf.StackEvent, error) - LoadBalancerExists(ctx context.Context, name string) (bool, error) - GetLoadBalancerARN(ctx context.Context, name string) (string, error) + DescribeServices(ctx context.Context, cluster string, arns []string) ([]compose.ServiceStatus, error) + + LoadBalancerExists(ctx context.Context, arn string) (bool, error) + GetLoadBalancerURL(ctx context.Context, arn string) (string, error) ClusterExists(ctx context.Context, name string) (bool, error) diff --git a/ecs/pkg/amazon/sdk/sdk.go b/ecs/pkg/amazon/sdk/sdk.go index af38523b7..5874a8009 100644 --- a/ecs/pkg/amazon/sdk/sdk.go +++ b/ecs/pkg/amazon/sdk/sdk.go @@ -222,6 +222,27 @@ func (s sdk) DescribeStackEvents(ctx context.Context, stackID string) ([]*cloudf } } +func (s sdk) ListStackResources(ctx context.Context, name string) ([]compose.StackResource, error) { + // FIXME handle pagination + res, err := s.CF.ListStackResourcesWithContext(ctx, &cloudformation.ListStackResourcesInput{ + StackName: aws.String(name), + }) + if err != nil { + return nil, err + } + + resources := []compose.StackResource{} + for _, r := range res.StackResourceSummaries { + resources = append(resources, compose.StackResource{ + LogicalID: *r.LogicalResourceId, + Type: *r.ResourceType, + ARN: *r.PhysicalResourceId, + Status: *r.ResourceStatus, + }) + } + return resources, nil +} + func (s sdk) DeleteStack(ctx context.Context, name string) error { logrus.Debug("Delete CloudFormation stack") _, err := s.CF.DeleteStackWithContext(ctx, &cloudformation.DeleteStackInput{ @@ -270,7 +291,6 @@ func (s sdk) InspectSecret(ctx context.Context, id string) (compose.Secret, erro } func (s sdk) ListSecrets(ctx context.Context) ([]compose.Secret, error) { - logrus.Debug("List secrets ...") response, err := s.SM.ListSecrets(&secretsmanager.ListSecretsInput{}) if err != nil { @@ -336,18 +356,10 @@ func (s sdk) GetLogs(ctx context.Context, name string, consumer compose.LogConsu } } -func (s sdk) DescribeServices(ctx context.Context, cluster string, project string) ([]compose.ServiceStatus, error) { - // TODO handle pagination - list, err := s.ECS.ListServicesWithContext(ctx, &ecs.ListServicesInput{ - Cluster: aws.String(cluster), - }) - if err != nil { - return nil, err - } - +func (s sdk) DescribeServices(ctx context.Context, cluster string, arns []string) ([]compose.ServiceStatus, error) { services, err := s.ECS.DescribeServicesWithContext(ctx, &ecs.DescribeServicesInput{ Cluster: aws.String(cluster), - Services: list.ServiceArns, + Services: aws.StringSlice(arns), Include: aws.StringSlice([]string{"TAGS"}), }) if err != nil { @@ -356,17 +368,13 @@ func (s sdk) DescribeServices(ctx context.Context, cluster string, project strin status := []compose.ServiceStatus{} for _, service := range services.Services { var name string - var stack string for _, t := range service.Tags { - switch *t.Key { - case compose.ProjectTag: - stack = *t.Value - case compose.ServiceTag: + if *t.Key == compose.ServiceTag { name = *t.Value } } - if stack != project { - continue + if name == "" { + return nil, fmt.Errorf("service %s doesn't have a %s tag", *service.ServiceArn, compose.ServiceTag) } status = append(status, compose.ServiceStatus{ ID: *service.ServiceName, @@ -410,10 +418,10 @@ func (s sdk) GetPublicIPs(ctx context.Context, interfaces ...string) (map[string return publicIPs, nil } -func (s sdk) LoadBalancerExists(ctx context.Context, name string) (bool, error) { - logrus.Debug("Check if cluster was already created: ", name) +func (s sdk) LoadBalancerExists(ctx context.Context, arn string) (bool, error) { + logrus.Debug("Check if LoadBalancer exists: ", arn) lbs, err := s.ELB.DescribeLoadBalancersWithContext(ctx, &elbv2.DescribeLoadBalancersInput{ - Names: []*string{aws.String(name)}, + LoadBalancerArns: []*string{aws.String(arn)}, }) if err != nil { return false, err @@ -421,13 +429,13 @@ func (s sdk) LoadBalancerExists(ctx context.Context, name string) (bool, error) return len(lbs.LoadBalancers) > 0, nil } -func (s sdk) GetLoadBalancerARN(ctx context.Context, name string) (string, error) { - logrus.Debug("Check if cluster was already created: ", name) +func (s sdk) GetLoadBalancerURL(ctx context.Context, arn string) (string, error) { + logrus.Debug("Retrieve load balancer URL: ", arn) lbs, err := s.ELB.DescribeLoadBalancersWithContext(ctx, &elbv2.DescribeLoadBalancersInput{ - Names: []*string{aws.String(name)}, + LoadBalancerArns: []*string{aws.String(arn)}, }) if err != nil { return "", err } - return *lbs.LoadBalancers[0].LoadBalancerArn, nil + return *lbs.LoadBalancers[0].DNSName, nil } diff --git a/ecs/pkg/compose/types.go b/ecs/pkg/compose/types.go index ae56e65ea..370bfc4b5 100644 --- a/ecs/pkg/compose/types.go +++ b/ecs/pkg/compose/types.go @@ -2,6 +2,13 @@ package compose import "encoding/json" +type StackResource struct { + LogicalID string + Type string + ARN string + Status string +} + type ServiceStatus struct { ID string Name string From 4700fed83660354cc63c804c4399d23c0eef2456 Mon Sep 17 00:00:00 2001 From: Nicolas De Loof <nicolas.deloof@gmail.com> Date: Mon, 6 Jul 2020 14:40:41 +0200 Subject: [PATCH 143/198] Unwrapp API errors to get user-friendly error message Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com> --- ecs/cmd/commands/compose.go | 60 +++++++--------------------------- ecs/cmd/commands/opts.go | 15 --------- ecs/cmd/commands/secret.go | 24 +++----------- ecs/pkg/amazon/backend/list.go | 9 +++-- ecs/pkg/amazon/backend/logs.go | 15 +++++++-- ecs/pkg/amazon/sdk/sdk.go | 4 ++- ecs/pkg/compose/api.go | 4 +-- ecs/pkg/docker/contextStore.go | 14 ++++++-- 8 files changed, 53 insertions(+), 92 deletions(-) diff --git a/ecs/cmd/commands/compose.go b/ecs/cmd/commands/compose.go index ed488a891..9f5a38418 100644 --- a/ecs/cmd/commands/compose.go +++ b/ecs/cmd/commands/compose.go @@ -8,7 +8,6 @@ import ( "strings" "github.com/compose-spec/compose-go/cli" - "github.com/compose-spec/compose-go/types" "github.com/docker/cli/cli/command" amazon "github.com/docker/ecs-plugin/pkg/amazon/backend" "github.com/docker/ecs-plugin/pkg/docker" @@ -43,15 +42,11 @@ func (o upOptions) LoadBalancerArn() *string { return &o.loadBalancerArn } -func ConvertCommand(dockerCli command.Cli, projectOpts *cli.ProjectOptions) *cobra.Command { +func ConvertCommand(dockerCli command.Cli, options *cli.ProjectOptions) *cobra.Command { cmd := &cobra.Command{ Use: "convert", - RunE: WithProject(projectOpts, func(project *types.Project, args []string) error { - clusteropts, err := docker.GetAwsContext(dockerCli) - if err != nil { - return err - } - backend, err := amazon.NewBackend(clusteropts.Profile, clusteropts.Cluster, clusteropts.Region) + RunE: docker.WithAwsContext(dockerCli, func(ctx docker.AwsContext, backend *amazon.Backend, args []string) error { + project, err := cli.ProjectFromOptions(options) if err != nil { return err } @@ -72,36 +67,24 @@ func ConvertCommand(dockerCli command.Cli, projectOpts *cli.ProjectOptions) *cob return cmd } -func UpCommand(dockerCli command.Cli, projectOpts *cli.ProjectOptions) *cobra.Command { +func UpCommand(dockerCli command.Cli, options *cli.ProjectOptions) *cobra.Command { opts := upOptions{} cmd := &cobra.Command{ Use: "up", - RunE: docker.WithAwsContext(dockerCli, func(clusteropts docker.AwsContext, args []string) error { - backend, err := amazon.NewBackend(clusteropts.Profile, clusteropts.Cluster, clusteropts.Region) - if err != nil { - return err - } - return backend.Up(context.Background(), *projectOpts) + RunE: docker.WithAwsContext(dockerCli, func(clusteropts docker.AwsContext, backend *amazon.Backend, args []string) error { + return backend.Up(context.Background(), *options) }), } cmd.Flags().StringVar(&opts.loadBalancerArn, "load-balancer", "", "") return cmd } -func PsCommand(dockerCli command.Cli, projectOpts *cli.ProjectOptions) *cobra.Command { +func PsCommand(dockerCli command.Cli, options *cli.ProjectOptions) *cobra.Command { opts := upOptions{} cmd := &cobra.Command{ Use: "ps", - RunE: WithProject(projectOpts, func(project *types.Project, args []string) error { - clusteropts, err := docker.GetAwsContext(dockerCli) - if err != nil { - return err - } - backend, err := amazon.NewBackend(clusteropts.Profile, clusteropts.Cluster, clusteropts.Region) - if err != nil { - return err - } - status, err := backend.Ps(context.Background(), project) + RunE: docker.WithAwsContext(dockerCli, func(ctx docker.AwsContext, backend *amazon.Backend, args []string) error { + status, err := backend.Ps(context.Background(), *options) if err != nil { return err } @@ -125,11 +108,7 @@ func DownCommand(dockerCli command.Cli, projectOpts *cli.ProjectOptions) *cobra. opts := downOptions{} cmd := &cobra.Command{ Use: "down", - RunE: docker.WithAwsContext(dockerCli, func(clusteropts docker.AwsContext, args []string) error { - backend, err := amazon.NewBackend(clusteropts.Profile, clusteropts.Cluster, clusteropts.Region) - if err != nil { - return err - } + RunE: docker.WithAwsContext(dockerCli, func(ctx docker.AwsContext, backend *amazon.Backend, args []string) error { return backend.Down(context.Background(), *projectOpts) }), } @@ -140,23 +119,8 @@ func DownCommand(dockerCli command.Cli, projectOpts *cli.ProjectOptions) *cobra. func LogsCommand(dockerCli command.Cli, projectOpts *cli.ProjectOptions) *cobra.Command { cmd := &cobra.Command{ Use: "logs [PROJECT NAME]", - RunE: docker.WithAwsContext(dockerCli, func(clusteropts docker.AwsContext, args []string) error { - backend, err := amazon.NewBackend(clusteropts.Profile, clusteropts.Cluster, clusteropts.Region) - if err != nil { - return err - } - var name string - - if len(args) == 0 { - project, err := cli.ProjectFromOptions(projectOpts) - if err != nil { - return err - } - name = project.Name - } else { - name = args[0] - } - return backend.Logs(context.Background(), name) + RunE: docker.WithAwsContext(dockerCli, func(clusteropts docker.AwsContext, backend *amazon.Backend, args []string) error { + return backend.Logs(context.Background(), *projectOpts) }), } return cmd diff --git a/ecs/cmd/commands/opts.go b/ecs/cmd/commands/opts.go index 7d0856c86..bb63ec383 100644 --- a/ecs/cmd/commands/opts.go +++ b/ecs/cmd/commands/opts.go @@ -2,8 +2,6 @@ package commands import ( "github.com/compose-spec/compose-go/cli" - "github.com/compose-spec/compose-go/types" - "github.com/spf13/cobra" "github.com/spf13/pflag" ) @@ -11,16 +9,3 @@ func AddFlags(o *cli.ProjectOptions, flags *pflag.FlagSet) { flags.StringArrayVarP(&o.ConfigPaths, "file", "f", nil, "Specify an alternate compose file") flags.StringVarP(&o.Name, "project-name", "n", "", "Specify an alternate project name (default: directory name)") } - -type ProjectFunc func(project *types.Project, args []string) error - -// WithProject wrap a ProjectFunc into a cobra command -func WithProject(options *cli.ProjectOptions, f ProjectFunc) func(cmd *cobra.Command, args []string) error { - return func(cmd *cobra.Command, args []string) error { - project, err := cli.ProjectFromOptions(options) - if err != nil { - return err - } - return f(project, args) - } -} diff --git a/ecs/cmd/commands/secret.go b/ecs/cmd/commands/secret.go index b6a32c783..5bbefaa9b 100644 --- a/ecs/cmd/commands/secret.go +++ b/ecs/cmd/commands/secret.go @@ -47,11 +47,7 @@ func CreateSecret(dockerCli command.Cli) *cobra.Command { cmd := &cobra.Command{ Use: "create NAME", Short: "Creates a secret.", - RunE: docker.WithAwsContext(dockerCli, func(clusteropts docker.AwsContext, args []string) error { - backend, err := amazon.NewBackend(clusteropts.Profile, clusteropts.Cluster, clusteropts.Region) - if err != nil { - return err - } + RunE: docker.WithAwsContext(dockerCli, func(clusteropts docker.AwsContext, backend *amazon.Backend, args []string) error { if len(args) == 0 { return errors.New("Missing mandatory parameter: NAME") } @@ -73,11 +69,7 @@ func InspectSecret(dockerCli command.Cli) *cobra.Command { cmd := &cobra.Command{ Use: "inspect ID", Short: "Displays secret details", - RunE: docker.WithAwsContext(dockerCli, func(clusteropts docker.AwsContext, args []string) error { - backend, err := amazon.NewBackend(clusteropts.Profile, clusteropts.Cluster, clusteropts.Region) - if err != nil { - return err - } + RunE: docker.WithAwsContext(dockerCli, func(clusteropts docker.AwsContext, backend *amazon.Backend, args []string) error { if len(args) == 0 { return errors.New("Missing mandatory parameter: ID") } @@ -102,11 +94,7 @@ func ListSecrets(dockerCli command.Cli) *cobra.Command { Use: "list", Aliases: []string{"ls"}, Short: "List secrets stored for the existing account.", - RunE: docker.WithAwsContext(dockerCli, func(clusteropts docker.AwsContext, args []string) error { - backend, err := amazon.NewBackend(clusteropts.Profile, clusteropts.Cluster, clusteropts.Region) - if err != nil { - return err - } + RunE: docker.WithAwsContext(dockerCli, func(clusteropts docker.AwsContext, backend *amazon.Backend, args []string) error { secrets, err := backend.ListSecrets(context.Background()) if err != nil { return err @@ -125,11 +113,7 @@ func DeleteSecret(dockerCli command.Cli) *cobra.Command { Use: "delete NAME", Aliases: []string{"rm", "remove"}, Short: "Removes a secret.", - RunE: docker.WithAwsContext(dockerCli, func(clusteropts docker.AwsContext, args []string) error { - backend, err := amazon.NewBackend(clusteropts.Profile, clusteropts.Cluster, clusteropts.Region) - if err != nil { - return err - } + RunE: docker.WithAwsContext(dockerCli, func(clusteropts docker.AwsContext, backend *amazon.Backend, args []string) error { if len(args) == 0 { return errors.New("Missing mandatory parameter: [NAME]") } diff --git a/ecs/pkg/amazon/backend/list.go b/ecs/pkg/amazon/backend/list.go index 00b4adee3..232b2c816 100644 --- a/ecs/pkg/amazon/backend/list.go +++ b/ecs/pkg/amazon/backend/list.go @@ -4,11 +4,16 @@ import ( "context" "fmt" - "github.com/compose-spec/compose-go/types" + "github.com/compose-spec/compose-go/cli" "github.com/docker/ecs-plugin/pkg/compose" ) -func (b *Backend) Ps(ctx context.Context, project *types.Project) ([]compose.ServiceStatus, error) { +func (b *Backend) Ps(ctx context.Context, options cli.ProjectOptions) ([]compose.ServiceStatus, error) { + project, err := cli.ProjectFromOptions(&options) + if err != nil { + return nil, err + } + cluster := b.Cluster if cluster == "" { cluster = project.Name diff --git a/ecs/pkg/amazon/backend/logs.go b/ecs/pkg/amazon/backend/logs.go index a17158857..00b8b2d4f 100644 --- a/ecs/pkg/amazon/backend/logs.go +++ b/ecs/pkg/amazon/backend/logs.go @@ -8,11 +8,22 @@ import ( "strconv" "strings" + "github.com/compose-spec/compose-go/cli" + "github.com/docker/ecs-plugin/pkg/console" ) -func (b *Backend) Logs(ctx context.Context, projectName string) error { - err := b.api.GetLogs(ctx, projectName, &logConsumer{ +func (b *Backend) Logs(ctx context.Context, options cli.ProjectOptions) error { + name := options.Name + if name == "" { + project, err := cli.ProjectFromOptions(&options) + if err != nil { + return err + } + name = project.Name + } + + err := b.api.GetLogs(ctx, name, &logConsumer{ colors: map[string]console.ColorFunc{}, width: 0, }) diff --git a/ecs/pkg/amazon/sdk/sdk.go b/ecs/pkg/amazon/sdk/sdk.go index 5874a8009..3365b07f8 100644 --- a/ecs/pkg/amazon/sdk/sdk.go +++ b/ecs/pkg/amazon/sdk/sdk.go @@ -143,7 +143,9 @@ func (s sdk) StackExists(ctx context.Context, name string) (bool, error) { StackName: aws.String(name), }) if err != nil { - // FIXME doesn't work as expected + if strings.HasPrefix(err.Error(), fmt.Sprintf("ValidationError: Stack with id %s does not exist", name)) { + return false, nil + } return false, nil } return len(stacks.Stacks) > 0, nil diff --git a/ecs/pkg/compose/api.go b/ecs/pkg/compose/api.go index 64e7e5c87..5f8278c65 100644 --- a/ecs/pkg/compose/api.go +++ b/ecs/pkg/compose/api.go @@ -13,8 +13,8 @@ type API interface { Down(ctx context.Context, options cli.ProjectOptions) error Convert(project *types.Project) (*cloudformation.Template, error) - Logs(ctx context.Context, projectName string) error - Ps(background context.Context, project *types.Project) ([]ServiceStatus, error) + Logs(ctx context.Context, projectName cli.ProjectOptions) error + Ps(background 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/docker/contextStore.go b/ecs/pkg/docker/contextStore.go index b038eb924..83c055f68 100644 --- a/ecs/pkg/docker/contextStore.go +++ b/ecs/pkg/docker/contextStore.go @@ -3,9 +3,11 @@ package docker import ( "fmt" + "github.com/aws/aws-sdk-go/aws/awserr" "github.com/docker/cli/cli/command" cliconfig "github.com/docker/cli/cli/config" "github.com/docker/cli/cli/context/store" + amazon "github.com/docker/ecs-plugin/pkg/amazon/backend" "github.com/mitchellh/mapstructure" "github.com/spf13/cobra" ) @@ -72,7 +74,7 @@ func checkAwsContextExists(contextName string) (*AwsContext, error) { return &awsContext, nil } -type ContextFunc func(ctx AwsContext, args []string) error +type ContextFunc func(ctx AwsContext, backend *amazon.Backend, args []string) error func WithAwsContext(dockerCli command.Cli, f ContextFunc) func(cmd *cobra.Command, args []string) error { return func(cmd *cobra.Command, args []string) error { @@ -80,7 +82,15 @@ func WithAwsContext(dockerCli command.Cli, f ContextFunc) func(cmd *cobra.Comman if err != nil { return err } - return f(*ctx, args) + backend, err := amazon.NewBackend(ctx.Profile, ctx.Cluster, ctx.Region) + if err != nil { + return err + } + err = f(*ctx, backend, args) + if e, ok := err.(awserr.Error); ok { + return fmt.Errorf(e.Message()) + } + return err } } From 242216cab1ffc2ea4b14bf32550f5936e8ef8b1b Mon Sep 17 00:00:00 2001 From: Nicolas De Loof <nicolas.deloof@gmail.com> Date: Tue, 7 Jul 2020 11:50:42 +0200 Subject: [PATCH 144/198] Reject compose file not setting service image Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com> --- ecs/pkg/amazon/backend/cloudformation.go | 1 + ecs/pkg/amazon/backend/convert.go | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/ecs/pkg/amazon/backend/cloudformation.go b/ecs/pkg/amazon/backend/cloudformation.go index 3df5b8ab1..5a096237e 100644 --- a/ecs/pkg/amazon/backend/cloudformation.go +++ b/ecs/pkg/amazon/backend/cloudformation.go @@ -40,6 +40,7 @@ func (c *FargateCompatibilityChecker) CheckPortsPublished(p *types.ServicePortCo } if p.Published != p.Target { c.Error("published port can't be set to a distinct value than container port") + p.Published = p.Target } } diff --git a/ecs/pkg/amazon/backend/convert.go b/ecs/pkg/amazon/backend/convert.go index 0eace3a69..196ff9ac6 100644 --- a/ecs/pkg/amazon/backend/convert.go +++ b/ecs/pkg/amazon/backend/convert.go @@ -17,6 +17,10 @@ import ( ) func Convert(project *types.Project, service types.ServiceConfig) (*ecs.TaskDefinition, error) { + if service.Image == "" { + return nil, fmt.Errorf("service %s doesn't define a Docker image to run", service.Name) + } + cpu, mem, err := toLimits(service) if err != nil { return nil, err From 98ec6c173b5e53d8109dabaabb68e649c9f479fb Mon Sep 17 00:00:00 2001 From: Nicolas De Loof <nicolas.deloof@gmail.com> Date: Tue, 7 Jul 2020 14:50:52 +0200 Subject: [PATCH 145/198] Reject compose file that uses incompatible features Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com> --- ecs/go.mod | 2 +- ecs/go.sum | 4 ++++ ecs/pkg/amazon/backend/cloudformation.go | 15 +++++++++++---- 3 files changed, 16 insertions(+), 5 deletions(-) diff --git a/ecs/go.mod b/ecs/go.mod index 163b56fc3..cdfa6c315 100644 --- a/ecs/go.mod +++ b/ecs/go.mod @@ -14,7 +14,7 @@ require ( 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-20200624120600-614475470cd8 + github.com/compose-spec/compose-go v0.0.0-20200707124823-710ff8e60ad9 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 diff --git a/ecs/go.sum b/ecs/go.sum index 27588bfef..f03fc1475 100644 --- a/ecs/go.sum +++ b/ecs/go.sum @@ -56,6 +56,8 @@ github.com/cloudflare/go-metrics v0.0.0-20151117154305-6a9aea36fb41/go.mod h1:ea github.com/cloudflare/redoctober v0.0.0-20171127175943-746a508df14c/go.mod h1:6Se34jNoqrd8bTxrmJB2Bg2aoZ2CdSXonils9NsiNgo= github.com/compose-spec/compose-go v0.0.0-20200624120600-614475470cd8 h1:sVvKsoXizFOuJNc8dM91IeET2/zDNFj3hwHgk437iJ8= github.com/compose-spec/compose-go v0.0.0-20200624120600-614475470cd8/go.mod h1:ih9anT8po+49hrb+1j3ldIJ/YRAaBH52ErlQLTKE2Yo= +github.com/compose-spec/compose-go v0.0.0-20200707124823-710ff8e60ad9 h1:WkFqc6UpRqxROso9KC+ceaTiXx/VWpeO1x+NV0d4d+o= +github.com/compose-spec/compose-go v0.0.0-20200707124823-710ff8e60ad9/go.mod h1:ArodJ6gsEB7iWKrbV3fSHZ08LlBvSVB0Oqg04fX86t4= github.com/containerd/cgroups v0.0.0-20190919134610-bf292b21730f/go.mod h1:OApqhQ4XNSNC13gXIwDjhOQxjWa/NxkwZXJ1EvqT0ko= github.com/containerd/console v0.0.0-20180822173158-c12b1e7919c1/go.mod h1:Tj/on1eG8kiEhd0+fhSDzsPAFESxzBBvdyEgyryXffw= github.com/containerd/containerd v1.3.0-beta.2.0.20190828155532-0293cbd26c69/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= @@ -167,6 +169,8 @@ github.com/jmespath/go-jmespath v0.3.0 h1:OS12ieG61fsCg5+qLJ+SsW9NicxNkg3b25OyT2 github.com/jmespath/go-jmespath v0.3.0/go.mod h1:9QtRXoHjLGCJ5IBSaohpXITPlowMeeYCZ7fLUTSywik= github.com/jmhodges/clock v0.0.0-20160418191101-880ee4c33548/go.mod h1:hGT6jSUVzF6no3QaDSMLGLEHtHSBSefs+MgcDWnmhmo= github.com/jmoiron/sqlx v0.0.0-20180124204410-05cef0741ade/go.mod h1:IiEW3SEiiErVyFdH8NTuWjSifiEQKUoyK3LNqr2kCHU= +github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc= +github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= diff --git a/ecs/pkg/amazon/backend/cloudformation.go b/ecs/pkg/amazon/backend/cloudformation.go index 5a096237e..4e0f3099c 100644 --- a/ecs/pkg/amazon/backend/cloudformation.go +++ b/ecs/pkg/amazon/backend/cloudformation.go @@ -17,6 +17,7 @@ import ( cloudmap "github.com/awslabs/goformation/v4/cloudformation/servicediscovery" "github.com/awslabs/goformation/v4/cloudformation/tags" "github.com/compose-spec/compose-go/compatibility" + "github.com/compose-spec/compose-go/errdefs" "github.com/compose-spec/compose-go/types" "github.com/docker/ecs-plugin/pkg/compose" "github.com/sirupsen/logrus" @@ -39,8 +40,7 @@ func (c *FargateCompatibilityChecker) CheckPortsPublished(p *types.ServicePortCo p.Published = p.Target } if p.Published != p.Target { - c.Error("published port can't be set to a distinct value than container port") - p.Published = p.Target + c.Incompatible("published port can't be set to a distinct value than container port") } } @@ -51,7 +51,7 @@ func (c *FargateCompatibilityChecker) CheckCapAdd(service *types.ServiceConfig) case "SYS_PTRACE": add = append(add, cap) default: - c.Error("service.cap_add = %s", cap) + c.Incompatible("ECS doesn't allow to add capability %s", cap) } } service.CapAdd = add @@ -86,7 +86,14 @@ func (b Backend) Convert(project *types.Project) (*cloudformation.Template, erro } compatibility.Check(project, checker) for _, err := range checker.Errors() { - logrus.Warn(err.Error()) + if errdefs.IsIncompatibleError(err) { + logrus.Error(err.Error()) + } else { + logrus.Warn(err.Error()) + } + } + if !compatibility.IsCompatible(checker) { + return nil, fmt.Errorf("compose file is incompatible with Amazon ECS") } template := cloudformation.NewTemplate() From 6664447d291eb09eb6812203c15847bd3bf9bca9 Mon Sep 17 00:00:00 2001 From: Nicolas De Loof <nicolas.deloof@gmail.com> Date: Wed, 8 Jul 2020 12:00:25 +0200 Subject: [PATCH 146/198] Fix setup command breaks if .aws/config does not exists Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com> --- ecs/cmd/commands/setup.go | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/ecs/cmd/commands/setup.go b/ecs/cmd/commands/setup.go index 6d52664f9..be876d666 100644 --- a/ecs/cmd/commands/setup.go +++ b/ecs/cmd/commands/setup.go @@ -3,6 +3,7 @@ package commands import ( "fmt" "os" + "path/filepath" "reflect" "strings" @@ -101,10 +102,17 @@ func saveCredentials(profile string, accessKeyID string, secretAccessKey string) } if err.(awserr.Error).Code() == "SharedCredsLoad" && err.(awserr.Error).Message() == "failed to load shared credentials file" { - os.Create(p.Filename) + err = os.MkdirAll(filepath.Dir(p.Filename), 0700) + if err != nil { + return err + } + _, err = os.Create(p.Filename) + if err != nil { + return err + } } - credIni, err := ini.Load(p.Filename) + credIni, err := ini.LooseLoad(p.Filename) if err != nil { return err } @@ -122,7 +130,7 @@ func awsProfiles(filename string) (map[string]ini.Section, error) { if filename == "" { filename = defaults.SharedConfigFilename() } - credIni, err := ini.Load(filename) + credIni, err := ini.LooseLoad(filename) if err != nil { return nil, err } From ec58975524ff95e69800eba50cf4e68e228205e3 Mon Sep 17 00:00:00 2001 From: Nicolas De Loof <nicolas.deloof@gmail.com> Date: Wed, 8 Jul 2020 16:07:13 +0200 Subject: [PATCH 147/198] Don't prepent docker.io to image URI. Let the container runtime apply default registry Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com> --- ecs/pkg/amazon/backend/cloudformation_test.go | 2 +- ecs/pkg/amazon/backend/convert.go | 13 +------------ .../simple/simple-cloudformation-conversion.golden | 2 +- ...-cloudformation-with-overrides-conversion.golden | 2 +- 4 files changed, 4 insertions(+), 15 deletions(-) diff --git a/ecs/pkg/amazon/backend/cloudformation_test.go b/ecs/pkg/amazon/backend/cloudformation_test.go index 2fcd15420..518c7c77a 100644 --- a/ecs/pkg/amazon/backend/cloudformation_test.go +++ b/ecs/pkg/amazon/backend/cloudformation_test.go @@ -128,7 +128,7 @@ services: `) def := template.Resources["TestTaskDefinition"].(*ecs.TaskDefinition) container := def.ContainerDefinitions[0] - assert.Equal(t, container.Image, "docker.io/library/image") + assert.Equal(t, container.Image, "image") assert.Equal(t, container.Command[0], "command") assert.Equal(t, container.EntryPoint[0], "entrypoint") assert.Equal(t, get(container.Environment, "FOO"), "BAR") diff --git a/ecs/pkg/amazon/backend/convert.go b/ecs/pkg/amazon/backend/convert.go index 196ff9ac6..ee5aa95b1 100644 --- a/ecs/pkg/amazon/backend/convert.go +++ b/ecs/pkg/amazon/backend/convert.go @@ -52,7 +52,7 @@ func Convert(project *types.Project, service types.ServiceConfig) (*ecs.TaskDefi FirelensConfiguration: nil, HealthCheck: toHealthCheck(service.HealthCheck), Hostname: service.Hostname, - Image: getImage(service.Image), + Image: service.Image, Interactive: false, Links: nil, LinuxParameters: toLinuxParameters(service), @@ -308,17 +308,6 @@ func toKeyValuePair(environment types.MappingWithEquals) []ecs.TaskDefinition_Ke return pairs } -func getImage(image string) string { - switch f := strings.Split(image, "/"); len(f) { - case 1: - return "docker.io/library/" + image - case 2: - return "docker.io/" + image - default: - return image - } -} - func getRepoCredentials(service types.ServiceConfig) *ecs.TaskDefinition_RepositoryCredentials { // extract registry and namespace string from image name for key, value := range service.Extensions { diff --git a/ecs/pkg/amazon/backend/testdata/simple/simple-cloudformation-conversion.golden b/ecs/pkg/amazon/backend/testdata/simple/simple-cloudformation-conversion.golden index b9fd6a045..0256a5868 100644 --- a/ecs/pkg/amazon/backend/testdata/simple/simple-cloudformation-conversion.golden +++ b/ecs/pkg/amazon/backend/testdata/simple/simple-cloudformation-conversion.golden @@ -174,7 +174,7 @@ } ], "Essential": true, - "Image": "docker.io/library/nginx", + "Image": "nginx", "LinuxParameters": {}, "LogConfiguration": { "LogDriver": "awslogs", diff --git a/ecs/pkg/amazon/backend/testdata/simple/simple-cloudformation-with-overrides-conversion.golden b/ecs/pkg/amazon/backend/testdata/simple/simple-cloudformation-with-overrides-conversion.golden index 0f93cbd54..b2d1ffad8 100644 --- a/ecs/pkg/amazon/backend/testdata/simple/simple-cloudformation-with-overrides-conversion.golden +++ b/ecs/pkg/amazon/backend/testdata/simple/simple-cloudformation-with-overrides-conversion.golden @@ -174,7 +174,7 @@ } ], "Essential": true, - "Image": "docker.io/library/haproxy", + "Image": "haproxy", "LinuxParameters": {}, "LogConfiguration": { "LogDriver": "awslogs", From 6f916ab9cedd35d2cf59f71a0ac6e45b63ae8346 Mon Sep 17 00:00:00 2001 From: Nicolas De Loof <nicolas.deloof@gmail.com> Date: Tue, 7 Jul 2020 10:43:15 +0200 Subject: [PATCH 148/198] Update docs with download and docs links Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com> --- ecs/docs/get-started-linux.md | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/ecs/docs/get-started-linux.md b/ecs/docs/get-started-linux.md index 2b1a129bf..1b24c84f7 100644 --- a/ecs/docs/get-started-linux.md +++ b/ecs/docs/get-started-linux.md @@ -19,15 +19,14 @@ Linux it needs to be installed manually. You can download the Docker ECS plugin from this repository using the following command: -<!-- FIXME(chris-crone): get real download link --> ```console -$ curl -L http://xxx | tar xzf - +$ curl -L https://github.com/docker/ecs-plugin/releases/latest/download/docker-ecs-linux-amd64 ``` You will then need to make it executable: ```console -$ chmod +x docker-ecs +$ chmod +x docker-ecs-linux-amd64 ``` ### Plugin install @@ -38,7 +37,7 @@ it to the right place: ```console $ mkdir -p /usr/local/lib/docker/cli-plugins -$ mv docker-ecs /usr/local/lib/docker/cli-plugins/ +$ mv docker-ecs-linux-amd64 /usr/local/lib/docker/cli-plugins/docker-ecs ``` You can put the CLI plugin into any of the following directories: @@ -80,5 +79,4 @@ $ docker ecs version Docker ECS plugin 0.0.1 ``` -<!-- FIXME(chris-crone): Link to ECS docs --> -You are now ready to [start deploying to ECS](http://xxx) +You are now ready to [start deploying to ECS](https://docs.docker.com/engine/context/ecs-integration/) From 2586fa35d42a94becd6660232b0cc8cd81721ffc Mon Sep 17 00:00:00 2001 From: Chad Metcalf <metcalfc@gmail.com> Date: Tue, 7 Jul 2020 22:20:27 -0700 Subject: [PATCH 149/198] Adding the demo from AWS C3. Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com> --- ecs/demo/Makefile | 25 ++++++ ecs/demo/README.md | 107 ++++++++++++++++++++++++ ecs/demo/app/Dockerfile | 7 ++ ecs/demo/app/app.py | 19 +++++ ecs/demo/app/requirements.txt | 2 + ecs/demo/app/scripts/entrypoint.sh | 4 + ecs/demo/app/templates/index.html | 125 +++++++++++++++++++++++++++++ ecs/demo/docker-compose.yml | 12 +++ 8 files changed, 301 insertions(+) create mode 100644 ecs/demo/Makefile create mode 100644 ecs/demo/README.md create mode 100644 ecs/demo/app/Dockerfile create mode 100644 ecs/demo/app/app.py create mode 100644 ecs/demo/app/requirements.txt create mode 100755 ecs/demo/app/scripts/entrypoint.sh create mode 100644 ecs/demo/app/templates/index.html create mode 100644 ecs/demo/docker-compose.yml diff --git a/ecs/demo/Makefile b/ecs/demo/Makefile new file mode 100644 index 000000000..deba2b83f --- /dev/null +++ b/ecs/demo/Makefile @@ -0,0 +1,25 @@ +REPO_NAMESPACE ?= ${USER} +FRONTEND_IMG = ${REPO_NAMESPACE}/timestamper +REGISTRY_ID ?= PUT_ECR_REGISTRY_ID_HERE +DOCKER_PUSH_REPOSITORY=dkr.ecr.us-west-2.amazonaws.com + +all: build-image + +create-ecr: + aws ecr create-repository --repository-name ${FRONTEND_IMG} + +build-image: + docker build -t $(REGISTRY_ID).$(DOCKER_PUSH_REPOSITORY)/$(FRONTEND_IMG) ./app + docker build -t $(FRONTEND_IMG) ./app + +push-image-ecr: + aws ecr get-login-password --region us-west-2 | docker login -u AWS --password-stdin $(REGISTRY_ID).$(DOCKER_PUSH_REPOSITORY) + docker push $(REGISTRY_ID).$(DOCKER_PUSH_REPOSITORY)/$(FRONTEND_IMG) + +push-image-hub: + docker push $(FRONTEND_IMG) + +clean: + @docker context use default + @docker context rm aws || true + @docker-compose rm -f || true diff --git a/ecs/demo/README.md b/ecs/demo/README.md new file mode 100644 index 000000000..803235204 --- /dev/null +++ b/ecs/demo/README.md @@ -0,0 +1,107 @@ +## Compose sample application + +### Python/Flask application + +``` ++--------------------+ +------------------+ +| | | | +| Python Flask | timestamps | Redis | +| Application |------------->| | +| | | | ++--------------------+ +------------------+ +``` + +### Things you'll need to do. + +There are a number of places you'll need to fill in information. You can find them with: + +``` +grep -r '<<<' ./* +./docker-compose.yml: x-aws-pull_credentials: <<<your arn for your secret you can get with docker ecs secret list>>> +./docker-compose.yml: image: <<<yourhubname>>>/timestamper +## Walk through +``` + +### Setup pull credentials for private Docker Hub repositories + +You should use a Personal Access Token (PAT) vs your default password. If you have 2FA enabled on your Hub account you will have to create a PAT. You can read more about managing access tokens here: https://docs.docker.com/docker-hub/access-tokens/ + +``` + docker ecs secret create -d MyKey -u myhubusername -p myhubpat +``` + +### Create an AWS Docker context and list available contexts + +``` +docker ecs setup +Enter context name: aws +✔ sandbox.devtools.developer +Enter cluster name: +Enter region: us-west-2 +✗ Enter credentials: + +docker context ls +NAME DESCRIPTION DOCKER ENDPOINT KUBERNETES ENDPOINT ORCHESTRATOR +aws +default * Current DOCKER_HOST based configuration unix:///var/run/docker.sock swarm +``` + +### Test locally + +``` +docker context use default +docker-compose up +open http://localhost:5000 +``` + +### Push images to hub for ecs (ecs cannot see your local image cache) + +``` +docker-compose push +``` + +### Switch to ECS context and launch the app + +``` +docker context use aws +docker ecs compose up +``` + +### Check out the CLI + +``` +docker ecs compose ps +docker ecs compose logs +``` + +### Check out the aws console + +- cloud formation +- cloud watch +- security groups +- Load balancers (ELB for this example / ALB if your app only uses 80/443) + +### Checkout cloudformation + +``` +docker ecs compose convert +``` + +### Stop the meters + +``` +docker ecs compose down + +``` + +## Using Amazon ECR instead of Docker Hub + +[Makefile](Makefile) has an example setup for creating an ECR repository and pushing to it. You'll need to have the AWS CLI installed and your AWS credentials available. + +``` +make create-ecr +REGISTRY_ID=<from the create above> make build-image +REGISTRY_ID=<from the create above> make push-image-ecr +``` + +If you want to use this often, you'll likely want to replace `PUT_ECR_REGISTRY_ID_HERE` with the value from above. diff --git a/ecs/demo/app/Dockerfile b/ecs/demo/app/Dockerfile new file mode 100644 index 000000000..64469d281 --- /dev/null +++ b/ecs/demo/app/Dockerfile @@ -0,0 +1,7 @@ +FROM python:3.7-alpine +WORKDIR /app +COPY requirements.txt /app +RUN pip3 install -r requirements.txt +COPY . /app +ENTRYPOINT ["/app/scripts/entrypoint.sh"] +CMD ["python3", "app.py"] diff --git a/ecs/demo/app/app.py b/ecs/demo/app/app.py new file mode 100644 index 000000000..e54b19a28 --- /dev/null +++ b/ecs/demo/app/app.py @@ -0,0 +1,19 @@ + +from flask import Flask +from flask import render_template +from redis import StrictRedis +from datetime import datetime + +app = Flask(__name__) +redis = StrictRedis(host='backend', port=6379) + + +@app.route('/') +def home(): + redis.lpush('times', datetime.now().strftime('%Y-%m-%dT%H:%M:%S%z')) + return render_template('index.html', title='Home', + times=redis.lrange('times', 0, -1)) + + +if __name__ == '__main__': + app.run(host='0.0.0.0', debug=True) diff --git a/ecs/demo/app/requirements.txt b/ecs/demo/app/requirements.txt new file mode 100644 index 000000000..1a5dc97b1 --- /dev/null +++ b/ecs/demo/app/requirements.txt @@ -0,0 +1,2 @@ +flask +redis diff --git a/ecs/demo/app/scripts/entrypoint.sh b/ecs/demo/app/scripts/entrypoint.sh new file mode 100755 index 000000000..198660244 --- /dev/null +++ b/ecs/demo/app/scripts/entrypoint.sh @@ -0,0 +1,4 @@ +#! /bin/sh + +if [ "${LOCALDOMAIN}" != "" ]; then echo "search ${LOCALDOMAIN}" >> /etc/resolv.conf; fi +exec "$@" diff --git a/ecs/demo/app/templates/index.html b/ecs/demo/app/templates/index.html new file mode 100644 index 000000000..91efdaefb --- /dev/null +++ b/ecs/demo/app/templates/index.html @@ -0,0 +1,125 @@ +<!DOCTYPE html> +<html lang="en"> + <head> + <!-- Required meta tags --> + <meta charset="utf-8" /> + <meta + name="viewport" + content="width=device-width, initial-scale=1, shrink-to-fit=yes" + /> + + <!-- Bootstrap CSS --> + <link + rel="stylesheet" + href="https://stackpath.bootstrapcdn.com/bootstrap/4.2.1/css/bootstrap.min.css" + integrity="sha384-GJzZqFGwb1QTTN6wy59ffF1BuGJpLSa9DkKMp0DgiMDm4iYMj70gZWKYbI706tWS" + crossorigin="anonymous" + /> + + <title>Hello, Docker!</title> + </head> + <body> + <div class="container"> + <nav class="navbar sticky-top navbar-expand-lg navbar-dark bg-dark"> + <a class="navbar-brand" href="/">Demo</a> + <button + class="navbar-toggler" + type="button" + data-toggle="collapse" + data-target="#navbarNav" + aria-controls="navbarNav" + aria-expanded="false" + aria-label="Toggle navigation" + > + <span class="navbar-toggler-icon"></span> + </button> + <div class="collapse navbar-collapse" id="navbarNav"> + <ul class="navbar-nav"> + <li class="nav-item dropdown"> + <a class="nav-link dropdown-toggle" href="#" id="navbarDropdownMenuLink" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"> + Compose Spec + </a> + <div class="dropdown-menu" aria-labelledby="navbarDropdownMenuLink"> + <a class="dropdown-item" href="https://compose-spec.io/">Overview</a> + <a class="dropdown-item" href="https://github.com/compose-spec/compose-spec/blob/master/spec.md">Specification</a> + <a class="dropdown-item" href="https://github.com/docker/awesome-compose">Awesome Compose - Compose Examples</a> + </div> + </li> + + <li class="nav-item dropdown"> + <a class="nav-link dropdown-toggle" href="#" id="navbarDropdownMenuLink" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"> + Get Docker Desktop + </a> + <div class="dropdown-menu" aria-labelledby="navbarDropdownMenuLink"> + <a class="dropdown-item" href="https://download.docker.com/mac/stable/Docker.dmg?utm_source=demo&utm_medium=web%20referral&utm_campaign=awsc32020&utm_budget=">Mac</a> + <a class="dropdown-item" href="https://download.docker.com/win/stable/Docker%20Desktop%20Installer.exe?utm_source=demo&utm_medium=web%20referral&utm_campaign=awsc32020&utm_budget=">Windows</a> + </div> + </li> + <li class="nav-item"> + <a class="nav-link" href="https://hub.docker.com/signup?utm_source=demo&utm_medium=web%20referral&utm_campaign=awsc32020&utm_budget=" + >Sign up for Docker Hub</a + > + </li> + <li class="nav-item"> + <a class="nav-link" href="https://docs.aws.amazon.com/AmazonECS/latest/developerguide/Welcome.html" + >Amazon ECS Docs</a + > + </li> + + + </ul> + </div> + </nav> + + <br /> + <h1>Hello, Docker Folks!</h1> + <br /> + + <div class="container-sm"> + <div class="text-center"> + <!-- Button trigger modal --> + <a class="btn btn-primary" href="/"> + Timestamp! + </a> + </div> + + <br /> + + <div class="table-responsive"> + <table class="table table-striped table-hover table-sm"> + <thead> + <tr> + <th scope="col">Timestamp</th> + </tr> + </thead> + <tbody> + {% for t in times %} + <tr> + <td>{{ t.decode('utf-8') }}</td> + </tr> + {% endfor %} + </tbody> + </table> + </div> + + </div> + + <!-- Optional JavaScript --> + <!-- jQuery first, then Popper.js, then Bootstrap JS --> + <script + src="https://code.jquery.com/jquery-3.3.1.slim.min.js" + integrity="sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo" + crossorigin="anonymous" + ></script> + <script + src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.6/umd/popper.min.js" + integrity="sha384-wHAiFfRlMFy6i5SRaxvfOCifBUQy1xHdJ/yoi7FRNXMRBu5WHdZYu1hA6ZOblgut" + crossorigin="anonymous" + ></script> + <script + src="https://stackpath.bootstrapcdn.com/bootstrap/4.2.1/js/bootstrap.min.js" + integrity="sha384-B0UglyR+jN6CkvvICOB2joaf5I4l3gm9GU6Hc1og6Ls7i6U/mkkaduKaBhlAXv9k" + crossorigin="anonymous" + ></script> + </body> +</html> diff --git a/ecs/demo/docker-compose.yml b/ecs/demo/docker-compose.yml new file mode 100644 index 000000000..31152b2a7 --- /dev/null +++ b/ecs/demo/docker-compose.yml @@ -0,0 +1,12 @@ +version: "3.8" +services: + frontend: + build: app + x-aws-pull_credentials: <<<your arn for your secret you can get with docker ecs secret list>>> + image: <<<yourhubname>>>/timestamper + ports: + - "5000:5000" + depends_on: + - backend + backend: + image: redis:alpine From 52a64845c7714cc68c233ac19c74b92498b0bf9f Mon Sep 17 00:00:00 2001 From: Christopher Crone <christopher.crone@docker.com> Date: Thu, 9 Jul 2020 11:51:07 +0200 Subject: [PATCH 150/198] example: Add details and format Signed-off-by: Christopher Crone <christopher.crone@docker.com> Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com> --- ecs/demo/README.md | 107 ----------- ecs/{demo => example}/Makefile | 0 ecs/example/README.md | 178 ++++++++++++++++++ ecs/{demo => example}/app/Dockerfile | 0 ecs/{demo => example}/app/app.py | 0 ecs/{demo => example}/app/requirements.txt | 0 .../app/scripts/entrypoint.sh | 0 .../app/templates/index.html | 0 ecs/{demo => example}/docker-compose.yml | 2 +- 9 files changed, 179 insertions(+), 108 deletions(-) delete mode 100644 ecs/demo/README.md rename ecs/{demo => example}/Makefile (100%) create mode 100644 ecs/example/README.md rename ecs/{demo => example}/app/Dockerfile (100%) rename ecs/{demo => example}/app/app.py (100%) rename ecs/{demo => example}/app/requirements.txt (100%) rename ecs/{demo => example}/app/scripts/entrypoint.sh (100%) rename ecs/{demo => example}/app/templates/index.html (100%) rename ecs/{demo => example}/docker-compose.yml (81%) diff --git a/ecs/demo/README.md b/ecs/demo/README.md deleted file mode 100644 index 803235204..000000000 --- a/ecs/demo/README.md +++ /dev/null @@ -1,107 +0,0 @@ -## Compose sample application - -### Python/Flask application - -``` -+--------------------+ +------------------+ -| | | | -| Python Flask | timestamps | Redis | -| Application |------------->| | -| | | | -+--------------------+ +------------------+ -``` - -### Things you'll need to do. - -There are a number of places you'll need to fill in information. You can find them with: - -``` -grep -r '<<<' ./* -./docker-compose.yml: x-aws-pull_credentials: <<<your arn for your secret you can get with docker ecs secret list>>> -./docker-compose.yml: image: <<<yourhubname>>>/timestamper -## Walk through -``` - -### Setup pull credentials for private Docker Hub repositories - -You should use a Personal Access Token (PAT) vs your default password. If you have 2FA enabled on your Hub account you will have to create a PAT. You can read more about managing access tokens here: https://docs.docker.com/docker-hub/access-tokens/ - -``` - docker ecs secret create -d MyKey -u myhubusername -p myhubpat -``` - -### Create an AWS Docker context and list available contexts - -``` -docker ecs setup -Enter context name: aws -✔ sandbox.devtools.developer -Enter cluster name: -Enter region: us-west-2 -✗ Enter credentials: - -docker context ls -NAME DESCRIPTION DOCKER ENDPOINT KUBERNETES ENDPOINT ORCHESTRATOR -aws -default * Current DOCKER_HOST based configuration unix:///var/run/docker.sock swarm -``` - -### Test locally - -``` -docker context use default -docker-compose up -open http://localhost:5000 -``` - -### Push images to hub for ecs (ecs cannot see your local image cache) - -``` -docker-compose push -``` - -### Switch to ECS context and launch the app - -``` -docker context use aws -docker ecs compose up -``` - -### Check out the CLI - -``` -docker ecs compose ps -docker ecs compose logs -``` - -### Check out the aws console - -- cloud formation -- cloud watch -- security groups -- Load balancers (ELB for this example / ALB if your app only uses 80/443) - -### Checkout cloudformation - -``` -docker ecs compose convert -``` - -### Stop the meters - -``` -docker ecs compose down - -``` - -## Using Amazon ECR instead of Docker Hub - -[Makefile](Makefile) has an example setup for creating an ECR repository and pushing to it. You'll need to have the AWS CLI installed and your AWS credentials available. - -``` -make create-ecr -REGISTRY_ID=<from the create above> make build-image -REGISTRY_ID=<from the create above> make push-image-ecr -``` - -If you want to use this often, you'll likely want to replace `PUT_ECR_REGISTRY_ID_HERE` with the value from above. diff --git a/ecs/demo/Makefile b/ecs/example/Makefile similarity index 100% rename from ecs/demo/Makefile rename to ecs/example/Makefile diff --git a/ecs/example/README.md b/ecs/example/README.md new file mode 100644 index 000000000..96e3e5261 --- /dev/null +++ b/ecs/example/README.md @@ -0,0 +1,178 @@ +## Compose sample application + +This sample application was demoed as part of the AWS Cloud Containers +Conference on 2020-07-09. It has been tested on Linux and macOS. + +Note that `$` is used to denote commands in blocks where the command and its +output are included. + +### Python/Flask application + +``` ++--------------------+ +------------------+ +| | | | +| Python Flask | timestamps | Redis | +| Application |------------->| | +| | | | ++--------------------+ +------------------+ +``` + +### Things you'll need to do + +There are a number of places you'll need to fill in information. +You can find them with: + +```console +$ grep -r '<<<' ./* +./docker-compose.yml: x-aws-pull_credentials: <<<your arn for your secret you can get with docker ecs secret list>>> +./docker-compose.yml: image: <<<your docker hub user name>>>/timestamper +## Walk through +``` + +### Setup pull credentials for private Docker Hub repositories + +You should use a Personal Access Token (PAT) rather than your account password. +If you have 2FA enabled on your Hub account you will need to create a PAT. +You can read more about managing access tokens here: +https://docs.docker.com/docker-hub/access-tokens/ + +```console +docker ecs secret create -d MyKey -u myhubusername -p myhubpat +``` + +### Create an AWS Docker context and list available contexts + +To initialize the Docker ECS integration, you will need to run the `setup` +command. This will create a Docker context that works with AWS ECS. + +```console +$ docker ecs setup +Enter context name: aws +✔ sandbox.devtools.developer +Enter cluster name: +Enter region: us-west-2 +✗ Enter credentials: +``` + +You can verify that the context was created by listing your Docker contexts: + +```console +$ docker context ls +NAME DESCRIPTION DOCKER ENDPOINT KUBERNETES ENDPOINT ORCHESTRATOR +aws +default * Current DOCKER_HOST based configuration unix:///var/run/docker.sock swarm +``` + +### Test locally + +The first step is to test your application works locally. To do this, you will +need to switch to using the default local context so that you are targeting your +local machine. + +```console +docker context use default +``` + +You can then run the application using `docker-compose`: + +```console +docker-compose up +``` + +Once the application has started, you can navigate to http://localhost:5000 +using your Web browser using the following command: + +```console +open http://localhost:5000 +``` + +### Push images to Docker Hub for ECS (ECS cannot see your local image cache) + +In order to run your application in the cloud, you will need your container +images to be in a registry. You can push them from your local machine using: + +```console +docker-compose push +``` + +You can verify that this command pushed to the Docker Hub by +[logging in](https://hub.docker.com) and looking for the `timestamper` +repository under your user name. + +### Switch to ECS context and launch the app + +Now that you've tested the application works locally and that you've pushed the +container images to the Docker Hub, you can switch to using the `aws` context +you created earlier. + +```console +docker context use aws +``` + +Running the application on ECS is then as simple as doing a `compose up`: + +```console +docker ecs compose up +``` + +### Check out the CLI + +Once the application is running in ECS, you can list the running containers with +the `ps` command. Note that you will need to run this from the directory where +you Compose file is. + +```console +docker ecs compose ps +``` + +You can also read the application logs using `compose logs`: + +```console +docker ecs compose logs +``` + +### Check out the AWS console + +You can see all the AWS components created for your running application in the +AWS console. There you will find: + +- CloudFormation being used to manage all the infrastructure +- CloudWatch for logs +- Security Groups for network policies +- Load balancers (ELB for this example / ALB if your app only uses 80/443) + +### Checkout CloudFormation + +The ECS Docker CLI integration has the ability to output the CloudFormation +template used to create the application in the `compose convert` command. You +can see this by running: + +```console +docker ecs compose convert +``` + +### Stop the meters + +To shut down your application, you simply need to run: + +```console +docker ecs compose down +``` + +## Using Amazon ECR instead of Docker Hub + +If you'd like to use AWS ECR instead of Docker Hub, the [Makefile](Makefile) has +an example setup for creating an ECR repository and pushing to it. +You'll need to have the AWS CLI installed and your AWS credentials available. + +```console +make create-ecr +REGISTRY_ID=<from the create above> make build-image +REGISTRY_ID=<from the create above> make push-image-ecr +``` + +Note that you will need to change the name of the image in the +[Compose file](docker-compose.yml). + +If you want to use this often, you'll likely want to replace +`PUT_ECR_REGISTRY_ID_HERE` with the value from above. diff --git a/ecs/demo/app/Dockerfile b/ecs/example/app/Dockerfile similarity index 100% rename from ecs/demo/app/Dockerfile rename to ecs/example/app/Dockerfile diff --git a/ecs/demo/app/app.py b/ecs/example/app/app.py similarity index 100% rename from ecs/demo/app/app.py rename to ecs/example/app/app.py diff --git a/ecs/demo/app/requirements.txt b/ecs/example/app/requirements.txt similarity index 100% rename from ecs/demo/app/requirements.txt rename to ecs/example/app/requirements.txt diff --git a/ecs/demo/app/scripts/entrypoint.sh b/ecs/example/app/scripts/entrypoint.sh similarity index 100% rename from ecs/demo/app/scripts/entrypoint.sh rename to ecs/example/app/scripts/entrypoint.sh diff --git a/ecs/demo/app/templates/index.html b/ecs/example/app/templates/index.html similarity index 100% rename from ecs/demo/app/templates/index.html rename to ecs/example/app/templates/index.html diff --git a/ecs/demo/docker-compose.yml b/ecs/example/docker-compose.yml similarity index 81% rename from ecs/demo/docker-compose.yml rename to ecs/example/docker-compose.yml index 31152b2a7..9d08d45d7 100644 --- a/ecs/demo/docker-compose.yml +++ b/ecs/example/docker-compose.yml @@ -3,7 +3,7 @@ services: frontend: build: app x-aws-pull_credentials: <<<your arn for your secret you can get with docker ecs secret list>>> - image: <<<yourhubname>>>/timestamper + image: <<<your docker hub user name>>>/timestamper ports: - "5000:5000" depends_on: From 164e0d750d9098f5c0eeadcc2677444b937f7f04 Mon Sep 17 00:00:00 2001 From: Christopher Crone <christopher.crone@docker.com> Date: Thu, 9 Jul 2020 11:54:03 +0200 Subject: [PATCH 151/198] readme: Add link to example Signed-off-by: Christopher Crone <christopher.crone@docker.com> Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com> --- ecs/README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/ecs/README.md b/ecs/README.md index a4e0caf53..bf721a3aa 100644 --- a/ecs/README.md +++ b/ecs/README.md @@ -4,6 +4,10 @@ :exclamation: The Docker ECS plugin is still in Beta. It's design and UX will evolve until 1.0 Final release. +## Example + +You can find an application for testing this in [example](./example). + ## Architecture ECS plugin is a [Docker CLI plugin](https://docs.docker.com/engine/extend/cli_plugins/) From bdb8ad0b95234c5f8827286355d824578eb09f71 Mon Sep 17 00:00:00 2001 From: Christopher Crone <christopher.crone@docker.com> Date: Thu, 9 Jul 2020 12:25:34 +0200 Subject: [PATCH 152/198] readme: Tidy and add docs link Signed-off-by: Christopher Crone <christopher.crone@docker.com> Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com> --- ecs/README.md | 86 +++++++++++++++++++++++++++++++++------------------ 1 file changed, 56 insertions(+), 30 deletions(-) diff --git a/ecs/README.md b/ecs/README.md index bf721a3aa..d948263b6 100644 --- a/ecs/README.md +++ b/ecs/README.md @@ -2,21 +2,34 @@ ## Status -:exclamation: The Docker ECS plugin is still in Beta. It's design and UX will evolve until 1.0 Final release. +:exclamation: The Docker ECS plugin is still in Beta. +Its design and UX will evolve until 1.0 Final release. -## Example +## Example and documentation You can find an application for testing this in [example](./example). +You can find more documentation about using the Docker ECS integration +[here](https://docs.docker.com/engine/context/ecs-integration/). + ## Architecture -ECS plugin is a [Docker CLI plugin](https://docs.docker.com/engine/extend/cli_plugins/) -root command `ecs` require aws profile to get API credentials from `~/.aws/credentials` -as well as AWS region - those will later be stored in a docker context +The Docker ECS integration is a +[Docker CLI plugin](https://docs.docker.com/engine/extend/cli_plugins/) +with the root command of `ecs`. +It requires an AWS profile to select the AWS API credentials from +`~/.aws/credentials` as well as an AWS region. You can read more about CLI AWS +credential management +[here](https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-files.html). +Once setup, the AWS profile and region are stored in a Docker context. -A `compose.yaml` is parsed and converted into a [CloudFormation](https://aws.amazon.com/cloudformation/) -template, which will create all resources in dependent order and cleanup on -`down` command or deployment failure. +A `compose.yaml` file is parsed and converted into a +[CloudFormation](https://aws.amazon.com/cloudformation/) template, +which is then used to create all application resources in dependent order. +Resources are cleaned up with the `down` command or in the case of a deployment +failure. + +The architecture of the ECS integration is shown below: ``` +--------------------------------------+ @@ -24,48 +37,61 @@ template, which will create all resources in dependent order and cleanup on +--------------------------------------+ - Load +--------------------------------------+ - | compose Model | + | Compose Model | +--------------------------------------+ - Validate +--------------------------------------+ - | compose Model suitable for ECS | + | Compose Model suitable for ECS | +--------------------------------------+ - Convert +--------------------------------------+ | CloudFormation Template | +--------------------------------------+ - Apply - +--------------+ +----------------+ + +--------------+ +----------------+ | AWS API | or | stack file | +--------------+ +----------------+ ``` -* _Load_ phase relies on [compose-go](https://github.com/compose-spec/compose-go). Any generic code we write for this -purpose should be proposed upstream. -* _Validate_ phase is responsible to inject sane ECS defaults into the compose-go model, and validate the `compose.yaml` -file do not include unsupported features. -* _Convert_ produces a CloudFormation template to define all resources required to implement the application model on AWS. -* _Apply_ phase do apply the CloudFormation template, either by exporting to a stack file or to deploy on AWS. +* The _Load_ phase relies on + [compose-go](https://github.com/compose-spec/compose-go). + Any generic code we write for this purpose should be proposed upstream. +* The _Validate_ phase is responsible for injecting sane ECS defaults into the + compose-go model, and validating the `compose.yaml` file does not include + unsupported features. +* The _Convert_ phase produces a CloudFormation template to define all + application resources required to implement the application model on AWS. +* The _Apply_ phase does the actual apply of the CloudFormation template, + either by exporting to a stack file or to deploy on AWS. ## Application model ### Services -Compose services are mapped to ECS services. Compose specification has no support for multi-container services, nor -does it support sidecars. When an ECS feature requires a sidecar, we introduce custom Compose extension (`x-aws-*`) -to actually expose ECS feature as a service-level feature, not plumbing details. +Compose services are mapped to ECS services. The Compose specification does not +have support for multi-container services (like Kubernetes pods) or sidecars. +When an ECS feature requires a sidecar, we use a custom Compose extension +(`x-aws-*`) to expose the ECS features as a service-level feature, +and keep the plumbing details from the user. ### Networking -We map the "network" abstraction from Compose model to AWS SecurityGroups. The whole application is created within a -single VPC, SecurityGroups are created per networks, including the implicit `default` one. Services are attached -according to the networks declared in Compose model. Doing so, services attached to a common security group can -communicate together, while services from distinct SecurityGroups can't. We just can't set service aliasses per network. +We map the "network" abstraction from the Compose model to AWS security groups. +The whole application is created within a single VPC, +security groups are created per Compose network, including the implicit +`default` one. +Services are attached according to the networks declared in Compose model. +This approach means that services attached to a common security group can +communicate together, while services from distinct security groups cannot. +This matches the intent of the Compose network model with the limitation that we +cannot set service aliases per network. + +A [CloudMap](https://aws.amazon.com/cloud-map/) private namespace is created for +each application as `{project}.local`. Services get registered so that we +have service discovery and DNS round-robin +(equivalent to Compose's `endpoint_mode: dnsrr`). Docker images SHOULD include +an entrypoint script to replicate this feature: -A CloudMap private namespace is created for application as `{project}.local`. Services get registered so that we -get service discovery and DNS round-robin (equivalent for Compose's `endpoint_mode: dnsrr`). Docker images SHOULD -include a tiny entrypoint script to replicate this feature: ```shell script -if [ ! -z LOCALDOMAIN ]; then echo "search ${LOCALDOMAIN}" >> /etc/resolv.conf; fi -``` - +if [ ! -z LOCALDOMAIN ]; then echo "search ${LOCALDOMAIN}" >> /etc/resolv.conf; fi +``` From 16e7bbd69726b9971afafaa21f65b7f350307dc4 Mon Sep 17 00:00:00 2001 From: Nicolas De Loof <nicolas.deloof@gmail.com> Date: Thu, 9 Jul 2020 10:49:21 +0200 Subject: [PATCH 153/198] Use compatibilityChecker to detect missing service image as a blocker Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com> --- ecs/go.mod | 2 +- ecs/go.sum | 2 + ecs/pkg/amazon/backend/cloudformation.go | 48 +------------------ ecs/pkg/amazon/backend/compatibility.go | 61 ++++++++++++++++++++++++ ecs/pkg/amazon/backend/convert.go | 4 -- 5 files changed, 65 insertions(+), 52 deletions(-) create mode 100644 ecs/pkg/amazon/backend/compatibility.go diff --git a/ecs/go.mod b/ecs/go.mod index cdfa6c315..332a46886 100644 --- a/ecs/go.mod +++ b/ecs/go.mod @@ -14,7 +14,7 @@ require ( 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-20200707124823-710ff8e60ad9 + github.com/compose-spec/compose-go v0.0.0-20200709084333-492a50989a5a 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 diff --git a/ecs/go.sum b/ecs/go.sum index f03fc1475..9fa33dd97 100644 --- a/ecs/go.sum +++ b/ecs/go.sum @@ -58,6 +58,8 @@ github.com/compose-spec/compose-go v0.0.0-20200624120600-614475470cd8 h1:sVvKsoX github.com/compose-spec/compose-go v0.0.0-20200624120600-614475470cd8/go.mod h1:ih9anT8po+49hrb+1j3ldIJ/YRAaBH52ErlQLTKE2Yo= github.com/compose-spec/compose-go v0.0.0-20200707124823-710ff8e60ad9 h1:WkFqc6UpRqxROso9KC+ceaTiXx/VWpeO1x+NV0d4d+o= github.com/compose-spec/compose-go v0.0.0-20200707124823-710ff8e60ad9/go.mod h1:ArodJ6gsEB7iWKrbV3fSHZ08LlBvSVB0Oqg04fX86t4= +github.com/compose-spec/compose-go v0.0.0-20200709084333-492a50989a5a h1:pIiSz5jML7rQ1aupg/KHlTqCxhyXvIgeDMf4kDTzIg8= +github.com/compose-spec/compose-go v0.0.0-20200709084333-492a50989a5a/go.mod h1:ArodJ6gsEB7iWKrbV3fSHZ08LlBvSVB0Oqg04fX86t4= github.com/containerd/cgroups v0.0.0-20190919134610-bf292b21730f/go.mod h1:OApqhQ4XNSNC13gXIwDjhOQxjWa/NxkwZXJ1EvqT0ko= github.com/containerd/console v0.0.0-20180822173158-c12b1e7919c1/go.mod h1:Tj/on1eG8kiEhd0+fhSDzsPAFESxzBBvdyEgyryXffw= github.com/containerd/containerd v1.3.0-beta.2.0.20190828155532-0293cbd26c69/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= diff --git a/ecs/pkg/amazon/backend/cloudformation.go b/ecs/pkg/amazon/backend/cloudformation.go index 4e0f3099c..bf8ecb3f8 100644 --- a/ecs/pkg/amazon/backend/cloudformation.go +++ b/ecs/pkg/amazon/backend/cloudformation.go @@ -31,57 +31,11 @@ const ( ParameterLoadBalancerARN = "ParameterLoadBalancerARN" ) -type FargateCompatibilityChecker struct { - compatibility.AllowList -} - -func (c *FargateCompatibilityChecker) CheckPortsPublished(p *types.ServicePortConfig) { - if p.Published == 0 { - p.Published = p.Target - } - if p.Published != p.Target { - c.Incompatible("published port can't be set to a distinct value than container port") - } -} - -func (c *FargateCompatibilityChecker) CheckCapAdd(service *types.ServiceConfig) { - add := []string{} - for _, cap := range service.CapAdd { - switch cap { - case "SYS_PTRACE": - add = append(add, cap) - default: - c.Incompatible("ECS doesn't allow to add capability %s", cap) - } - } - service.CapAdd = add -} - // Convert a compose project into a CloudFormation template func (b Backend) Convert(project *types.Project) (*cloudformation.Template, error) { var checker compatibility.Checker = &FargateCompatibilityChecker{ compatibility.AllowList{ - Supported: []string{ - "services.command", - "services.container_name", - "services.cap_drop", - "services.depends_on", - "services.entrypoint", - "services.environment", - "services.init", - "services.healthcheck", - "services.healthcheck.interval", - "services.healthcheck.start_period", - "services.healthcheck.test", - "services.healthcheck.timeout", - "services.networks", - "services.ports", - "services.ports.mode", - "services.ports.target", - "services.ports.protocol", - "services.user", - "services.working_dir", - }, + Supported: compatibleComposeAttributes, }, } compatibility.Check(project, checker) diff --git a/ecs/pkg/amazon/backend/compatibility.go b/ecs/pkg/amazon/backend/compatibility.go new file mode 100644 index 000000000..bc74bd87c --- /dev/null +++ b/ecs/pkg/amazon/backend/compatibility.go @@ -0,0 +1,61 @@ +package backend + +import ( + "github.com/compose-spec/compose-go/compatibility" + "github.com/compose-spec/compose-go/types" +) + +type FargateCompatibilityChecker struct { + compatibility.AllowList +} + +var compatibleComposeAttributes = []string{ + "services.command", + "services.container_name", + "services.cap_drop", + "services.depends_on", + "services.entrypoint", + "services.environment", + "service.image", + "services.init", + "services.healthcheck", + "services.healthcheck.interval", + "services.healthcheck.start_period", + "services.healthcheck.test", + "services.healthcheck.timeout", + "services.networks", + "services.ports", + "services.ports.mode", + "services.ports.target", + "services.ports.protocol", + "services.user", + "services.working_dir", +} + +func (c *FargateCompatibilityChecker) CheckImage(service *types.ServiceConfig) { + if service.Image == "" { + c.Incompatible("service %s doesn't define a Docker image to run", service.Name) + } +} + +func (c *FargateCompatibilityChecker) CheckPortsPublished(p *types.ServicePortConfig) { + if p.Published == 0 { + p.Published = p.Target + } + if p.Published != p.Target { + c.Incompatible("published port can't be set to a distinct value than container port") + } +} + +func (c *FargateCompatibilityChecker) CheckCapAdd(service *types.ServiceConfig) { + add := []string{} + for _, cap := range service.CapAdd { + switch cap { + case "SYS_PTRACE": + add = append(add, cap) + default: + c.Incompatible("ECS doesn't allow to add capability %s", cap) + } + } + service.CapAdd = add +} diff --git a/ecs/pkg/amazon/backend/convert.go b/ecs/pkg/amazon/backend/convert.go index ee5aa95b1..6fdc0abc3 100644 --- a/ecs/pkg/amazon/backend/convert.go +++ b/ecs/pkg/amazon/backend/convert.go @@ -17,10 +17,6 @@ import ( ) func Convert(project *types.Project, service types.ServiceConfig) (*ecs.TaskDefinition, error) { - if service.Image == "" { - return nil, fmt.Errorf("service %s doesn't define a Docker image to run", service.Name) - } - cpu, mem, err := toLimits(service) if err != nil { return nil, err From bcd9cda41c66def61f907c98ae05ec30a7f78c10 Mon Sep 17 00:00:00 2001 From: Christopher Crone <christopher.crone@docker.com> Date: Thu, 9 Jul 2020 18:01:12 +0200 Subject: [PATCH 154/198] docs: Fix download command Signed-off-by: Christopher Crone <christopher.crone@docker.com> Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com> --- ecs/docs/get-started-linux.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ecs/docs/get-started-linux.md b/ecs/docs/get-started-linux.md index 1b24c84f7..afada8906 100644 --- a/ecs/docs/get-started-linux.md +++ b/ecs/docs/get-started-linux.md @@ -20,7 +20,7 @@ You can download the Docker ECS plugin from this repository using the following command: ```console -$ curl -L https://github.com/docker/ecs-plugin/releases/latest/download/docker-ecs-linux-amd64 +$ curl -LO https://github.com/docker/ecs-plugin/releases/latest/download/docker-ecs-linux-amd64 ``` You will then need to make it executable: From 0b5779b52d50a5c47d9802668fa4f84b64e72d52 Mon Sep 17 00:00:00 2001 From: Christopher Crone <christopher.crone@docker.com> Date: Thu, 9 Jul 2020 18:09:47 +0200 Subject: [PATCH 155/198] readme: Add link to blog, install instructions Signed-off-by: Christopher Crone <christopher.crone@docker.com> Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com> --- ecs/README.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/ecs/README.md b/ecs/README.md index d948263b6..874e243e5 100644 --- a/ecs/README.md +++ b/ecs/README.md @@ -1,10 +1,21 @@ # Docker CLI plugin for Amazon ECS +This was announced at AWS Cloud Containers Conference 2020, read the +[blog post](https://www.docker.com/blog/https-docker-com-blog-from-docker-straight-to-aws/). + ## Status :exclamation: The Docker ECS plugin is still in Beta. Its design and UX will evolve until 1.0 Final release. +## Get started + +If you're using macOS or Windows you just need to install +[Docker Desktop Edge](https://www.docker.com/products/docker-desktop) and you +will have the ECS integration installed. + +You can find Linux install instructions [here](./docs/get-started-linux.md). + ## Example and documentation You can find an application for testing this in [example](./example). From 55d560badba92ae0d03a38f7dab18f4a823151ff Mon Sep 17 00:00:00 2001 From: Christopher Crone <christopher.crone@docker.com> Date: Thu, 9 Jul 2020 18:27:41 +0200 Subject: [PATCH 156/198] readme: Blog link fix Signed-off-by: Christopher Crone <christopher.crone@docker.com> Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com> --- ecs/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ecs/README.md b/ecs/README.md index 874e243e5..66e419c68 100644 --- a/ecs/README.md +++ b/ecs/README.md @@ -1,7 +1,7 @@ # Docker CLI plugin for Amazon ECS This was announced at AWS Cloud Containers Conference 2020, read the -[blog post](https://www.docker.com/blog/https-docker-com-blog-from-docker-straight-to-aws/). +[blog post](https://www.docker.com/blog/from-docker-straight-to-aws/). ## Status From a0a785f19e60bc410c2adb2a4c341c514e9e4877 Mon Sep 17 00:00:00 2001 From: Nicolas De Loof <nicolas.deloof@gmail.com> Date: Fri, 10 Jul 2020 13:54:59 +0200 Subject: [PATCH 157/198] Clarify example/README about required secret NAME Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com> --- ecs/example/README.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/ecs/example/README.md b/ecs/example/README.md index 96e3e5261..e8dbbd738 100644 --- a/ecs/example/README.md +++ b/ecs/example/README.md @@ -36,8 +36,11 @@ If you have 2FA enabled on your Hub account you will need to create a PAT. You can read more about managing access tokens here: https://docs.docker.com/docker-hub/access-tokens/ + +You can then create `DockerHubToken` secret on [AWS Secret Manager](https://aws.amazon.com/secrets-manager/) using following command + ```console -docker ecs secret create -d MyKey -u myhubusername -p myhubpat +docker ecs secret create -d MyKey -u myhubusername -p myhubpat DockerHubToken ``` ### Create an AWS Docker context and list available contexts From 1783716a6aa8b7c20eba394cde6a8af4c9246682 Mon Sep 17 00:00:00 2001 From: Nicolas De Loof <nicolas.deloof@gmail.com> Date: Fri, 10 Jul 2020 09:35:32 +0200 Subject: [PATCH 158/198] claim support for deploy.replicas Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com> --- ecs/go.mod | 2 +- ecs/go.sum | 2 ++ ecs/pkg/amazon/backend/cloudformation_test.go | 14 ++++++++++++++ ecs/pkg/amazon/backend/compatibility.go | 2 ++ 4 files changed, 19 insertions(+), 1 deletion(-) diff --git a/ecs/go.mod b/ecs/go.mod index 332a46886..25611f037 100644 --- a/ecs/go.mod +++ b/ecs/go.mod @@ -14,7 +14,7 @@ require ( 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-20200709084333-492a50989a5a + github.com/compose-spec/compose-go v0.0.0-20200710075715-6fcc35384ee1 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 diff --git a/ecs/go.sum b/ecs/go.sum index 9fa33dd97..996e1bb7b 100644 --- a/ecs/go.sum +++ b/ecs/go.sum @@ -60,6 +60,8 @@ github.com/compose-spec/compose-go v0.0.0-20200707124823-710ff8e60ad9 h1:WkFqc6U github.com/compose-spec/compose-go v0.0.0-20200707124823-710ff8e60ad9/go.mod h1:ArodJ6gsEB7iWKrbV3fSHZ08LlBvSVB0Oqg04fX86t4= github.com/compose-spec/compose-go v0.0.0-20200709084333-492a50989a5a h1:pIiSz5jML7rQ1aupg/KHlTqCxhyXvIgeDMf4kDTzIg8= github.com/compose-spec/compose-go v0.0.0-20200709084333-492a50989a5a/go.mod h1:ArodJ6gsEB7iWKrbV3fSHZ08LlBvSVB0Oqg04fX86t4= +github.com/compose-spec/compose-go v0.0.0-20200710075715-6fcc35384ee1 h1:F+YIkKDMHdgZBacawhFY1P9RAIgO+6uv2te6hjsjzF0= +github.com/compose-spec/compose-go v0.0.0-20200710075715-6fcc35384ee1/go.mod h1:ArodJ6gsEB7iWKrbV3fSHZ08LlBvSVB0Oqg04fX86t4= github.com/containerd/cgroups v0.0.0-20190919134610-bf292b21730f/go.mod h1:OApqhQ4XNSNC13gXIwDjhOQxjWa/NxkwZXJ1EvqT0ko= github.com/containerd/console v0.0.0-20180822173158-c12b1e7919c1/go.mod h1:Tj/on1eG8kiEhd0+fhSDzsPAFESxzBBvdyEgyryXffw= github.com/containerd/containerd v1.3.0-beta.2.0.20190828155532-0293cbd26c69/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= diff --git a/ecs/pkg/amazon/backend/cloudformation_test.go b/ecs/pkg/amazon/backend/cloudformation_test.go index 518c7c77a..8a03e0f58 100644 --- a/ecs/pkg/amazon/backend/cloudformation_test.go +++ b/ecs/pkg/amazon/backend/cloudformation_test.go @@ -93,6 +93,20 @@ services: assert.Check(t, len(lb.SecurityGroups) > 0) } +func TestServiceReplicas(t *testing.T) { + template := convertYaml(t, ` +version: "3" +services: + test: + image: nginx + deploy: + replicas: 10 +`) + s := template.Resources["TestService"].(*ecs.Service) + assert.Check(t, s != nil) + assert.Check(t, s.DesiredCount == 10) +} + func TestLoadBalancerTypeNetwork(t *testing.T) { template := convertYaml(t, ` version: "3" diff --git a/ecs/pkg/amazon/backend/compatibility.go b/ecs/pkg/amazon/backend/compatibility.go index bc74bd87c..85a00128a 100644 --- a/ecs/pkg/amazon/backend/compatibility.go +++ b/ecs/pkg/amazon/backend/compatibility.go @@ -14,6 +14,8 @@ var compatibleComposeAttributes = []string{ "services.container_name", "services.cap_drop", "services.depends_on", + "services.deploy", + "services.deploy.replicas", "services.entrypoint", "services.environment", "service.image", From 794ea3cc24092793aff7d16eb6ced6e2000cce6c Mon Sep 17 00:00:00 2001 From: Nicolas De Loof <nicolas.deloof@gmail.com> Date: Fri, 10 Jul 2020 09:22:38 +0200 Subject: [PATCH 159/198] Check context created by `context` command Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com> --- ecs/cmd/commands/setup.go | 11 ++++++++ ecs/cmd/commands/setup_test.go | 32 ------------------------ ecs/cmd/commands/testdata/context.golden | 1 - ecs/pkg/amazon/backend/context.go | 24 ++++++++++++++++++ ecs/pkg/amazon/sdk/api.go | 2 ++ ecs/pkg/amazon/sdk/sdk.go | 28 +++++++++++++++------ ecs/pkg/compose/api.go | 2 ++ 7 files changed, 60 insertions(+), 40 deletions(-) delete mode 100644 ecs/cmd/commands/setup_test.go delete mode 100644 ecs/cmd/commands/testdata/context.golden create mode 100644 ecs/pkg/amazon/backend/context.go diff --git a/ecs/cmd/commands/setup.go b/ecs/cmd/commands/setup.go index be876d666..33cd31291 100644 --- a/ecs/cmd/commands/setup.go +++ b/ecs/cmd/commands/setup.go @@ -1,6 +1,7 @@ package commands import ( + "context" "fmt" "os" "path/filepath" @@ -10,6 +11,7 @@ import ( "github.com/aws/aws-sdk-go/aws/awserr" "github.com/aws/aws-sdk-go/aws/credentials" "github.com/aws/aws-sdk-go/aws/defaults" + amazon "github.com/docker/ecs-plugin/pkg/amazon/backend" contextStore "github.com/docker/ecs-plugin/pkg/docker" "github.com/manifoldco/promptui" "github.com/spf13/cobra" @@ -53,6 +55,14 @@ func SetupCommand() *cobra.Command { return err } } + backend, err := amazon.NewBackend(opts.context.Profile, opts.context.Cluster, opts.context.Region) + if err != nil { + return err + } + _, _, err = backend.CreateContextData(context.Background(), nil) + if err != nil { + return err + } return contextStore.NewContext(opts.name, &opts.context) }, } @@ -206,6 +216,7 @@ func setCluster(opts *setupOptions, err error) error { if err != nil { return err } + opts.context.Cluster = result return nil } diff --git a/ecs/cmd/commands/setup_test.go b/ecs/cmd/commands/setup_test.go deleted file mode 100644 index 97b7add38..000000000 --- a/ecs/cmd/commands/setup_test.go +++ /dev/null @@ -1,32 +0,0 @@ -package commands - -import ( - "io/ioutil" - "path/filepath" - "testing" - - "github.com/docker/cli/cli/config" - "gotest.tools/v3/assert" - "gotest.tools/v3/fs" - "gotest.tools/v3/golden" -) - -func TestDefaultAwsContextName(t *testing.T) { - dir := fs.NewDir(t, "setup") - defer dir.Remove() - cmd := NewRootCmd(nil) - dockerConfig := config.Dir() - config.SetDir(dir.Path()) - defer config.SetDir(dockerConfig) - - cmd.SetArgs([]string{"setup", "--cluster", "clusterName", "--profile", "profileName", "--region", "regionName"}) - err := cmd.Execute() - assert.NilError(t, err) - - files, err := filepath.Glob(dir.Join("contexts", "meta", "*", "meta.json")) - assert.NilError(t, err) - assert.Equal(t, len(files), 1) - b, err := ioutil.ReadFile(files[0]) - assert.NilError(t, err) - golden.Assert(t, string(b), "context.golden") -} diff --git a/ecs/cmd/commands/testdata/context.golden b/ecs/cmd/commands/testdata/context.golden deleted file mode 100644 index 891cb9cf8..000000000 --- a/ecs/cmd/commands/testdata/context.golden +++ /dev/null @@ -1 +0,0 @@ -{"Name":"aws","Metadata":{"Type":"aws"},"Endpoints":{"aws":{"Profile":"profileName","Cluster":"clusterName","Region":"regionName"},"docker":{"Profile":"profileName","Cluster":"clusterName","Region":"regionName"}}} \ No newline at end of file diff --git a/ecs/pkg/amazon/backend/context.go b/ecs/pkg/amazon/backend/context.go new file mode 100644 index 000000000..bc785d91a --- /dev/null +++ b/ecs/pkg/amazon/backend/context.go @@ -0,0 +1,24 @@ +package backend + +import ( + "context" + "fmt" +) + +func (b *Backend) CreateContextData(ctx context.Context, params map[string]string) (contextData interface{}, description string, err error) { + err = b.api.CheckRequirements(ctx) + if err != nil { + return "", "", err + } + + if b.Cluster != "" { + exists, err := b.api.ClusterExists(ctx, b.Cluster) + if err != nil { + return "", "", err + } + if !exists { + return "", "", fmt.Errorf("cluster %s does not exists", b.Cluster) + } + } + return "", "", nil +} diff --git a/ecs/pkg/amazon/sdk/api.go b/ecs/pkg/amazon/sdk/api.go index 057823c53..5de5d5af2 100644 --- a/ecs/pkg/amazon/sdk/api.go +++ b/ecs/pkg/amazon/sdk/api.go @@ -9,6 +9,8 @@ import ( ) type API interface { + CheckRequirements(ctx context.Context) error + GetDefaultVPC(ctx context.Context) (string, error) VpcExists(ctx context.Context, vpcID string) (bool, error) GetSubNets(ctx context.Context, vpcID string) ([]string, error) diff --git a/ecs/pkg/amazon/sdk/sdk.go b/ecs/pkg/amazon/sdk/sdk.go index 3365b07f8..fd0e649fa 100644 --- a/ecs/pkg/amazon/sdk/sdk.go +++ b/ecs/pkg/amazon/sdk/sdk.go @@ -2,15 +2,13 @@ package sdk import ( "context" + "errors" "fmt" "strings" "time" - "github.com/docker/ecs-plugin/internal" - - "github.com/aws/aws-sdk-go/aws/request" - "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/request" "github.com/aws/aws-sdk-go/aws/session" "github.com/aws/aws-sdk-go/service/cloudformation" "github.com/aws/aws-sdk-go/service/cloudformation/cloudformationiface" @@ -27,6 +25,7 @@ import ( "github.com/aws/aws-sdk-go/service/secretsmanager" "github.com/aws/aws-sdk-go/service/secretsmanager/secretsmanageriface" cf "github.com/awslabs/goformation/v4/cloudformation" + "github.com/docker/ecs-plugin/internal" "github.com/docker/ecs-plugin/pkg/compose" "github.com/sirupsen/logrus" ) @@ -57,8 +56,23 @@ func NewAPI(sess *session.Session) API { } } +func (s sdk) CheckRequirements(ctx context.Context) error { + settings, err := s.ECS.ListAccountSettingsWithContext(ctx, &ecs.ListAccountSettingsInput{ + EffectiveSettings: aws.Bool(true), + Name: aws.String("serviceLongArnFormat"), + }) + if err != nil { + return err + } + serviceLongArnFormat := settings.Settings[0].Value + if *serviceLongArnFormat != "enabled" { + return errors.New("this tool requires the \"new ARN resource ID format\"") + } + return nil +} + func (s sdk) ClusterExists(ctx context.Context, name string) (bool, error) { - logrus.Debug("Check if cluster was already created: ", name) + logrus.Debug("CheckRequirements if cluster was already created: ", name) clusters, err := s.ECS.DescribeClustersWithContext(ctx, &ecs.DescribeClustersInput{ Clusters: []*string{aws.String(name)}, }) @@ -78,7 +92,7 @@ func (s sdk) CreateCluster(ctx context.Context, name string) (string, error) { } func (s sdk) VpcExists(ctx context.Context, vpcID string) (bool, error) { - logrus.Debug("Check if VPC exists: ", vpcID) + logrus.Debug("CheckRequirements if VPC exists: ", vpcID) _, err := s.EC2.DescribeVpcsWithContext(ctx, &ec2.DescribeVpcsInput{VpcIds: []*string{&vpcID}}) return err == nil, err } @@ -421,7 +435,7 @@ func (s sdk) GetPublicIPs(ctx context.Context, interfaces ...string) (map[string } func (s sdk) LoadBalancerExists(ctx context.Context, arn string) (bool, error) { - logrus.Debug("Check if LoadBalancer exists: ", arn) + logrus.Debug("CheckRequirements if LoadBalancer exists: ", arn) lbs, err := s.ELB.DescribeLoadBalancersWithContext(ctx, &elbv2.DescribeLoadBalancersInput{ LoadBalancerArns: []*string{aws.String(arn)}, }) diff --git a/ecs/pkg/compose/api.go b/ecs/pkg/compose/api.go index 5f8278c65..66364703e 100644 --- a/ecs/pkg/compose/api.go +++ b/ecs/pkg/compose/api.go @@ -12,6 +12,8 @@ type API interface { Up(ctx context.Context, options cli.ProjectOptions) error Down(ctx context.Context, options cli.ProjectOptions) error + 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, projectName cli.ProjectOptions) error Ps(background context.Context, options cli.ProjectOptions) ([]ServiceStatus, error) From b7d0b704e5ebd4f7a0872f60f8e0a51cdae0d2ab Mon Sep 17 00:00:00 2001 From: Chad Metcalf <metcalfc@gmail.com> Date: Mon, 13 Jul 2020 23:08:36 -0700 Subject: [PATCH 160/198] Change default context name to 'ecs'. The ACI backend uses 'aci' as the default context name. The ECS backend uses 'aws'. There may be other AWS or Azure backends so lets name them for what they are. Addresses issue #154. Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com> --- ecs/cmd/commands/setup.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ecs/cmd/commands/setup.go b/ecs/cmd/commands/setup.go index 33cd31291..c6ac612ce 100644 --- a/ecs/cmd/commands/setup.go +++ b/ecs/cmd/commands/setup.go @@ -66,7 +66,7 @@ func SetupCommand() *cobra.Command { return contextStore.NewContext(opts.name, &opts.context) }, } - cmd.Flags().StringVarP(&opts.name, "name", "n", "aws", "Context Name") + cmd.Flags().StringVarP(&opts.name, "name", "n", "ecs", "Context Name") cmd.Flags().StringVarP(&opts.context.Profile, "profile", "p", "", "AWS Profile") cmd.Flags().StringVarP(&opts.context.Cluster, "cluster", "c", "", "ECS cluster") cmd.Flags().StringVarP(&opts.context.Region, "region", "r", "", "AWS region") From 8ab544a7703dbc447623a3dd06d3c92616ec76da Mon Sep 17 00:00:00 2001 From: Nicolas De Loof <nicolas.deloof@gmail.com> Date: Wed, 15 Jul 2020 09:52:49 +0200 Subject: [PATCH 161/198] Use env variables from os for interpolation Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com> --- ecs/cmd/commands/compose.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ecs/cmd/commands/compose.go b/ecs/cmd/commands/compose.go index 9f5a38418..551b5370a 100644 --- a/ecs/cmd/commands/compose.go +++ b/ecs/cmd/commands/compose.go @@ -46,7 +46,8 @@ func ConvertCommand(dockerCli command.Cli, options *cli.ProjectOptions) *cobra.C cmd := &cobra.Command{ Use: "convert", RunE: docker.WithAwsContext(dockerCli, func(ctx docker.AwsContext, backend *amazon.Backend, args []string) error { - project, err := cli.ProjectFromOptions(options) + opts := options.WithOsEnv() + project, err := cli.ProjectFromOptions(&opts) if err != nil { return err } From b2a90197000c298c0dc7a1a2f3bac009795ef329 Mon Sep 17 00:00:00 2001 From: Chad Metcalf <metcalfc@gmail.com> Date: Tue, 14 Jul 2020 22:50:40 -0700 Subject: [PATCH 162/198] LoadBalancer names cannot be longer then 32 characters.. Longer names will fail with: AmazonElasticLoadBalancingV2; Status Code: 400; Error Code: ValidationError; Fairly straight forward approach, truncate at 32 characters. Considering that we append "LoadBalancer" to every name and it is 12 characters the name should stay useful. We can decide later if "LB" is better then a truncated "LoadBalan". The golden test data needed to be updated because its names were also over 32 characters. Fixes #160. Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com> --- ecs/pkg/amazon/backend/cloudformation.go | 4 +++- ecs/pkg/amazon/backend/cloudformation_test.go | 21 ++++++++++--------- ...formation-with-overrides-conversion.golden | 4 ++-- 3 files changed, 16 insertions(+), 13 deletions(-) diff --git a/ecs/pkg/amazon/backend/cloudformation.go b/ecs/pkg/amazon/backend/cloudformation.go index bf8ecb3f8..3cd72360f 100644 --- a/ecs/pkg/amazon/backend/cloudformation.go +++ b/ecs/pkg/amazon/backend/cloudformation.go @@ -218,7 +218,9 @@ func getLoadBalancerSecurityGroups(project *types.Project, template *cloudformat } func createLoadBalancer(project *types.Project, template *cloudformation.Template) string { - loadBalancerName := fmt.Sprintf("%sLoadBalancer", strings.Title(project.Name)) + + // load balancer names are limited to 32 characters total + loadBalancerName := fmt.Sprintf("%.32s", fmt.Sprintf("%sLoadBalancer", strings.Title(project.Name))) // Create LoadBalancer if `ParameterLoadBalancerName` is not set template.Conditions["CreateLoadBalancer"] = cloudformation.Equals("", cloudformation.Ref(ParameterLoadBalancerARN)) diff --git a/ecs/pkg/amazon/backend/cloudformation_test.go b/ecs/pkg/amazon/backend/cloudformation_test.go index 8a03e0f58..d362a93fc 100644 --- a/ecs/pkg/amazon/backend/cloudformation_test.go +++ b/ecs/pkg/amazon/backend/cloudformation_test.go @@ -34,7 +34,7 @@ func TestSimpleWithOverrides(t *testing.T) { } func TestRolePolicy(t *testing.T) { - template := convertYaml(t, ` + template := convertYaml(t, "test", ` version: "3" services: foo: @@ -54,14 +54,14 @@ services: } func TestMapNetworksToSecurityGroups(t *testing.T) { - template := convertYaml(t, ` + template := convertYaml(t, "test", ` version: "3" services: test: image: hello_world networks: - - front-tier - - back-tier + - front-tier + - back-tier networks: front-tier: @@ -79,7 +79,7 @@ networks: } func TestLoadBalancerTypeApplication(t *testing.T) { - template := convertYaml(t, ` + template := convertYaml(t, "test123456789009876543211234567890", ` version: "3" services: test: @@ -89,12 +89,13 @@ services: `) lb := template.Resources["TestLoadBalancer"].(*elasticloadbalancingv2.LoadBalancer) assert.Check(t, lb != nil) + assert.Check(t, len(lb.Name) <= 32) assert.Check(t, lb.Type == elbv2.LoadBalancerTypeEnumApplication) assert.Check(t, len(lb.SecurityGroups) > 0) } func TestServiceReplicas(t *testing.T) { - template := convertYaml(t, ` + template := convertYaml(t, "test", ` version: "3" services: test: @@ -108,7 +109,7 @@ services: } func TestLoadBalancerTypeNetwork(t *testing.T) { - template := convertYaml(t, ` + template := convertYaml(t, "test", ` version: "3" services: test: @@ -123,7 +124,7 @@ services: } func TestServiceMapping(t *testing.T) { - template := convertYaml(t, ` + template := convertYaml(t, "test", ` version: "3" services: test: @@ -163,7 +164,7 @@ func get(l []ecs.TaskDefinition_KeyValuePair, name string) string { } func TestResourcesHaveProjectTagSet(t *testing.T) { - template := convertYaml(t, ` + template := convertYaml(t, "test", ` version: "3" services: test: @@ -207,7 +208,7 @@ func load(t *testing.T, paths ...string) *types.Project { return project } -func convertYaml(t *testing.T, yaml string) *cloudformation.Template { +func convertYaml(t *testing.T, name string, yaml string) *cloudformation.Template { dict, err := loader.ParseYAML([]byte(yaml)) assert.NilError(t, err) model, err := loader.Load(types.ConfigDetails{ diff --git a/ecs/pkg/amazon/backend/testdata/simple/simple-cloudformation-with-overrides-conversion.golden b/ecs/pkg/amazon/backend/testdata/simple/simple-cloudformation-with-overrides-conversion.golden index b2d1ffad8..0bc56cf99 100644 --- a/ecs/pkg/amazon/backend/testdata/simple/simple-cloudformation-with-overrides-conversion.golden +++ b/ecs/pkg/amazon/backend/testdata/simple/simple-cloudformation-with-overrides-conversion.golden @@ -260,10 +260,10 @@ }, "Type": "AWS::EC2::SecurityGroupIngress" }, - "TestSimpleWithOverridesLoadBalancer": { + "TestSimpleWithOverridesLoadBalan": { "Condition": "CreateLoadBalancer", "Properties": { - "Name": "TestSimpleWithOverridesLoadBalancer", + "Name": "TestSimpleWithOverridesLoadBalan", "Scheme": "internet-facing", "SecurityGroups": [ { From dbbd24d270857e618c395ee494746be8a4953344 Mon Sep 17 00:00:00 2001 From: Nicolas De Loof <nicolas.deloof@gmail.com> Date: Wed, 15 Jul 2020 18:02:57 +0200 Subject: [PATCH 163/198] Don't create a LoadBalancer if compose app has no port exposed Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com> --- ecs/pkg/amazon/backend/cloudformation.go | 27 +- ecs/pkg/amazon/backend/cloudformation_test.go | 23 +- .../simple-single-service-with-overrides.yaml | 4 - .../testdata/input/simple-single-service.yaml | 4 +- .../simple-cloudformation-conversion.golden | 80 ++++- ...formation-with-overrides-conversion.golden | 292 ------------------ 6 files changed, 117 insertions(+), 313 deletions(-) delete mode 100644 ecs/pkg/amazon/backend/testdata/input/simple-single-service-with-overrides.yaml delete mode 100644 ecs/pkg/amazon/backend/testdata/simple/simple-cloudformation-with-overrides-conversion.golden diff --git a/ecs/pkg/amazon/backend/cloudformation.go b/ecs/pkg/amazon/backend/cloudformation.go index 3cd72360f..c07f7766d 100644 --- a/ecs/pkg/amazon/backend/cloudformation.go +++ b/ecs/pkg/amazon/backend/cloudformation.go @@ -142,14 +142,16 @@ func (b Backend) Convert(project *types.Project) (*cloudformation.Template, erro protocol = elbv2.ProtocolEnumHttp } } - targetGroupName := createTargetGroup(project, service, port, template, protocol) - listenerName := createListener(service, port, template, targetGroupName, loadBalancerARN, protocol) - dependsOn = append(dependsOn, listenerName) - serviceLB = append(serviceLB, ecs.Service_LoadBalancer{ - ContainerName: service.Name, - ContainerPort: int(port.Target), - TargetGroupArn: cloudformation.Ref(targetGroupName), - }) + if loadBalancerARN != "" { + targetGroupName := createTargetGroup(project, service, port, template, protocol) + listenerName := createListener(service, port, template, targetGroupName, loadBalancerARN, protocol) + dependsOn = append(dependsOn, listenerName) + serviceLB = append(serviceLB, ecs.Service_LoadBalancer{ + ContainerName: service.Name, + ContainerPort: int(port.Target), + TargetGroupArn: cloudformation.Ref(targetGroupName), + }) + } } } @@ -218,6 +220,15 @@ func getLoadBalancerSecurityGroups(project *types.Project, template *cloudformat } func createLoadBalancer(project *types.Project, template *cloudformation.Template) string { + ports := 0 + for _, service := range project.Services { + ports += len(service.Ports) + } + if ports == 0 { + // Project do not expose any port (batch jobs?) + // So no need to create a LoadBalancer + return "" + } // load balancer names are limited to 32 characters total loadBalancerName := fmt.Sprintf("%.32s", fmt.Sprintf("%sLoadBalancer", strings.Title(project.Name))) diff --git a/ecs/pkg/amazon/backend/cloudformation_test.go b/ecs/pkg/amazon/backend/cloudformation_test.go index d362a93fc..032b9cd58 100644 --- a/ecs/pkg/amazon/backend/cloudformation_test.go +++ b/ecs/pkg/amazon/backend/cloudformation_test.go @@ -26,13 +26,6 @@ func TestSimpleConvert(t *testing.T) { golden.Assert(t, result, expected) } -func TestSimpleWithOverrides(t *testing.T) { - project := load(t, "testdata/input/simple-single-service.yaml", "testdata/input/simple-single-service-with-overrides.yaml") - result := convertResultAsString(t, project, "TestCluster") - expected := "simple/simple-cloudformation-with-overrides-conversion.golden" - golden.Assert(t, result, expected) -} - func TestRolePolicy(t *testing.T) { template := convertYaml(t, "test", ` version: "3" @@ -94,6 +87,22 @@ services: assert.Check(t, len(lb.SecurityGroups) > 0) } +func TestNoLoadBalancerIfNoPortExposed(t *testing.T) { + template := convertYaml(t, "test", ` +version: "3" +services: + test: + image: nginx + foo: + image: bar +`) + for _, r := range template.Resources { + assert.Check(t, r.AWSCloudFormationType() != "AWS::ElasticLoadBalancingV2::TargetGroup") + assert.Check(t, r.AWSCloudFormationType() != "AWS::ElasticLoadBalancingV2::Listener") + assert.Check(t, r.AWSCloudFormationType() != "AWS::ElasticLoadBalancingV2::LoadBalancer") + } +} + func TestServiceReplicas(t *testing.T) { template := convertYaml(t, "test", ` version: "3" diff --git a/ecs/pkg/amazon/backend/testdata/input/simple-single-service-with-overrides.yaml b/ecs/pkg/amazon/backend/testdata/input/simple-single-service-with-overrides.yaml deleted file mode 100644 index 3dc8a0b6f..000000000 --- a/ecs/pkg/amazon/backend/testdata/input/simple-single-service-with-overrides.yaml +++ /dev/null @@ -1,4 +0,0 @@ -version: "3" -services: - simple: - image: haproxy diff --git a/ecs/pkg/amazon/backend/testdata/input/simple-single-service.yaml b/ecs/pkg/amazon/backend/testdata/input/simple-single-service.yaml index 4b3f9af21..448f21108 100644 --- a/ecs/pkg/amazon/backend/testdata/input/simple-single-service.yaml +++ b/ecs/pkg/amazon/backend/testdata/input/simple-single-service.yaml @@ -1,4 +1,6 @@ version: "3" services: simple: - image: nginx \ No newline at end of file + image: nginx + ports: + - "80:80" \ No newline at end of file diff --git a/ecs/pkg/amazon/backend/testdata/simple/simple-cloudformation-conversion.golden b/ecs/pkg/amazon/backend/testdata/simple/simple-cloudformation-conversion.golden index 0256a5868..7e8859050 100644 --- a/ecs/pkg/amazon/backend/testdata/simple/simple-cloudformation-conversion.golden +++ b/ecs/pkg/amazon/backend/testdata/simple/simple-cloudformation-conversion.golden @@ -71,6 +71,9 @@ "Type": "AWS::Logs::LogGroup" }, "SimpleService": { + "DependsOn": [ + "SimpleTCP80Listener" + ], "Properties": { "Cluster": { "Fn::If": [ @@ -85,6 +88,15 @@ }, "DesiredCount": 1, "LaunchType": "FARGATE", + "LoadBalancers": [ + { + "ContainerName": "simple", + "ContainerPort": 80, + "TargetGroupArn": { + "Ref": "SimpleTCP80TargetGroup" + } + } + ], "NetworkConfiguration": { "AwsvpcConfiguration": { "AssignPublicIp": "ENABLED", @@ -152,6 +164,56 @@ }, "Type": "AWS::ServiceDiscovery::Service" }, + "SimpleTCP80Listener": { + "Properties": { + "DefaultActions": [ + { + "ForwardConfig": { + "TargetGroups": [ + { + "TargetGroupArn": { + "Ref": "SimpleTCP80TargetGroup" + } + } + ] + }, + "Type": "forward" + } + ], + "LoadBalancerArn": { + "Fn::If": [ + "CreateLoadBalancer", + { + "Ref": "TestSimpleConvertLoadBalancer" + }, + { + "Ref": "ParameterLoadBalancerARN" + } + ] + }, + "Port": 80, + "Protocol": "HTTP" + }, + "Type": "AWS::ElasticLoadBalancingV2::Listener" + }, + "SimpleTCP80TargetGroup": { + "Properties": { + "Name": "SimpleTCP80TargetGroup", + "Port": 80, + "Protocol": "HTTP", + "Tags": [ + { + "Key": "com.docker.compose.project", + "Value": "TestSimpleConvert" + } + ], + "TargetType": "ip", + "VpcId": { + "Ref": "ParameterVPCId" + } + }, + "Type": "AWS::ElasticLoadBalancingV2::TargetGroup" + }, "SimpleTaskDefinition": { "Properties": { "ContainerDefinitions": [ @@ -188,7 +250,14 @@ "awslogs-stream-prefix": "TestSimpleConvert" } }, - "Name": "simple" + "Name": "simple", + "PortMappings": [ + { + "ContainerPort": 80, + "HostPort": 80, + "Protocol": "tcp" + } + ] } ], "Cpu": "256", @@ -231,6 +300,15 @@ "Properties": { "GroupDescription": "TestSimpleConvert default Security Group", "GroupName": "TestSimpleConvertDefaultNetwork", + "SecurityGroupIngress": [ + { + "CidrIp": "0.0.0.0/0", + "Description": "simple:80/tcp", + "FromPort": 80, + "IpProtocol": "TCP", + "ToPort": 80 + } + ], "Tags": [ { "Key": "com.docker.compose.project", diff --git a/ecs/pkg/amazon/backend/testdata/simple/simple-cloudformation-with-overrides-conversion.golden b/ecs/pkg/amazon/backend/testdata/simple/simple-cloudformation-with-overrides-conversion.golden deleted file mode 100644 index 0bc56cf99..000000000 --- a/ecs/pkg/amazon/backend/testdata/simple/simple-cloudformation-with-overrides-conversion.golden +++ /dev/null @@ -1,292 +0,0 @@ -{ - "AWSTemplateFormatVersion": "2010-09-09", - "Conditions": { - "CreateCluster": { - "Fn::Equals": [ - "", - { - "Ref": "ParameterClusterName" - } - ] - }, - "CreateLoadBalancer": { - "Fn::Equals": [ - "", - { - "Ref": "ParameterLoadBalancerARN" - } - ] - } - }, - "Parameters": { - "ParameterClusterName": { - "Description": "Name of the ECS cluster to deploy to (optional)", - "Type": "String" - }, - "ParameterLoadBalancerARN": { - "Description": "Name of the LoadBalancer to connect to (optional)", - "Type": "String" - }, - "ParameterSubnet1Id": { - "Description": "SubnetId, for Availability Zone 1 in the region in your VPC", - "Type": "AWS::EC2::Subnet::Id" - }, - "ParameterSubnet2Id": { - "Description": "SubnetId, for Availability Zone 2 in the region in your VPC", - "Type": "AWS::EC2::Subnet::Id" - }, - "ParameterVPCId": { - "Description": "ID of the VPC", - "Type": "AWS::EC2::VPC::Id" - } - }, - "Resources": { - "CloudMap": { - "Properties": { - "Description": "Service Map for Docker Compose project TestSimpleWithOverrides", - "Name": "TestSimpleWithOverrides.local", - "Vpc": { - "Ref": "ParameterVPCId" - } - }, - "Type": "AWS::ServiceDiscovery::PrivateDnsNamespace" - }, - "Cluster": { - "Condition": "CreateCluster", - "Properties": { - "ClusterName": "TestSimpleWithOverrides", - "Tags": [ - { - "Key": "com.docker.compose.project", - "Value": "TestSimpleWithOverrides" - } - ] - }, - "Type": "AWS::ECS::Cluster" - }, - "LogGroup": { - "Properties": { - "LogGroupName": "/docker-compose/TestSimpleWithOverrides" - }, - "Type": "AWS::Logs::LogGroup" - }, - "SimpleService": { - "Properties": { - "Cluster": { - "Fn::If": [ - "CreateCluster", - { - "Ref": "Cluster" - }, - { - "Ref": "ParameterClusterName" - } - ] - }, - "DesiredCount": 1, - "LaunchType": "FARGATE", - "NetworkConfiguration": { - "AwsvpcConfiguration": { - "AssignPublicIp": "ENABLED", - "SecurityGroups": [ - { - "Ref": "TestSimpleWithOverridesDefaultNetwork" - } - ], - "Subnets": [ - { - "Ref": "ParameterSubnet1Id" - }, - { - "Ref": "ParameterSubnet2Id" - } - ] - } - }, - "SchedulingStrategy": "REPLICA", - "ServiceRegistries": [ - { - "RegistryArn": { - "Fn::GetAtt": [ - "SimpleServiceDiscoveryEntry", - "Arn" - ] - } - } - ], - "Tags": [ - { - "Key": "com.docker.compose.project", - "Value": "TestSimpleWithOverrides" - }, - { - "Key": "com.docker.compose.service", - "Value": "simple" - } - ], - "TaskDefinition": { - "Ref": "SimpleTaskDefinition" - } - }, - "Type": "AWS::ECS::Service" - }, - "SimpleServiceDiscoveryEntry": { - "Properties": { - "Description": "\"simple\" service discovery entry in Cloud Map", - "DnsConfig": { - "DnsRecords": [ - { - "TTL": 60, - "Type": "A" - } - ], - "RoutingPolicy": "MULTIVALUE" - }, - "HealthCheckCustomConfig": { - "FailureThreshold": 1 - }, - "Name": "simple", - "NamespaceId": { - "Ref": "CloudMap" - } - }, - "Type": "AWS::ServiceDiscovery::Service" - }, - "SimpleTaskDefinition": { - "Properties": { - "ContainerDefinitions": [ - { - "Environment": [ - { - "Name": "LOCALDOMAIN", - "Value": { - "Fn::Join": [ - "", - [ - { - "Ref": "AWS::Region" - }, - ".compute.internal", - " TestSimpleWithOverrides.local" - ] - ] - } - } - ], - "Essential": true, - "Image": "haproxy", - "LinuxParameters": {}, - "LogConfiguration": { - "LogDriver": "awslogs", - "Options": { - "awslogs-group": { - "Ref": "LogGroup" - }, - "awslogs-region": { - "Ref": "AWS::Region" - }, - "awslogs-stream-prefix": "TestSimpleWithOverrides" - } - }, - "Name": "simple" - } - ], - "Cpu": "256", - "ExecutionRoleArn": { - "Ref": "SimpleTaskExecutionRole" - }, - "Family": "TestSimpleWithOverrides-simple", - "Memory": "512", - "NetworkMode": "awsvpc", - "RequiresCompatibilities": [ - "FARGATE" - ] - }, - "Type": "AWS::ECS::TaskDefinition" - }, - "SimpleTaskExecutionRole": { - "Properties": { - "AssumeRolePolicyDocument": { - "Statement": [ - { - "Action": [ - "sts:AssumeRole" - ], - "Effect": "Allow", - "Principal": { - "Service": "ecs-tasks.amazonaws.com" - } - } - ], - "Version": "2012-10-17" - }, - "ManagedPolicyArns": [ - "arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy", - "arn:aws:iam::aws:policy/AmazonEC2ContainerRegistryReadOnly" - ] - }, - "Type": "AWS::IAM::Role" - }, - "TestSimpleWithOverridesDefaultNetwork": { - "Properties": { - "GroupDescription": "TestSimpleWithOverrides default Security Group", - "GroupName": "TestSimpleWithOverridesDefaultNetwork", - "Tags": [ - { - "Key": "com.docker.compose.project", - "Value": "TestSimpleWithOverrides" - }, - { - "Key": "com.docker.compose.network", - "Value": "default" - } - ], - "VpcId": { - "Ref": "ParameterVPCId" - } - }, - "Type": "AWS::EC2::SecurityGroup" - }, - "TestSimpleWithOverridesDefaultNetworkIngress": { - "Properties": { - "Description": "Allow communication within network default", - "GroupId": { - "Ref": "TestSimpleWithOverridesDefaultNetwork" - }, - "IpProtocol": "-1", - "SourceSecurityGroupId": { - "Ref": "TestSimpleWithOverridesDefaultNetwork" - } - }, - "Type": "AWS::EC2::SecurityGroupIngress" - }, - "TestSimpleWithOverridesLoadBalan": { - "Condition": "CreateLoadBalancer", - "Properties": { - "Name": "TestSimpleWithOverridesLoadBalan", - "Scheme": "internet-facing", - "SecurityGroups": [ - { - "Ref": "TestSimpleWithOverridesDefaultNetwork" - } - ], - "Subnets": [ - { - "Ref": "ParameterSubnet1Id" - }, - { - "Ref": "ParameterSubnet2Id" - } - ], - "Tags": [ - { - "Key": "com.docker.compose.project", - "Value": "TestSimpleWithOverrides" - } - ], - "Type": "application" - }, - "Type": "AWS::ElasticLoadBalancingV2::LoadBalancer" - } - } -} From 1b53dbf84fed4dc71087fbadcfd5e34d3152aaaa Mon Sep 17 00:00:00 2001 From: Christopher Crone <christopher.crone@docker.com> Date: Thu, 16 Jul 2020 15:19:28 +0200 Subject: [PATCH 164/198] readme: Update Docker Desktop links Makes it easier to find the Edge build. Signed-off-by: Christopher Crone <christopher.crone@docker.com> Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com> --- ecs/README.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/ecs/README.md b/ecs/README.md index 66e419c68..5faeb696a 100644 --- a/ecs/README.md +++ b/ecs/README.md @@ -11,10 +11,12 @@ Its design and UX will evolve until 1.0 Final release. ## Get started If you're using macOS or Windows you just need to install -[Docker Desktop Edge](https://www.docker.com/products/docker-desktop) and you -will have the ECS integration installed. +Docker Desktop **Edge** and you will have the ECS integration installed. +You can find links here: +- [macOS](https://hub.docker.com/editions/community/docker-ce-desktop-mac) +- [Windows](https://hub.docker.com/editions/community/docker-ce-desktop-windows) -You can find Linux install instructions [here](./docs/get-started-linux.md). +Linux install instructions are [here](./docs/get-started-linux.md). ## Example and documentation From d7d5e6305406c3b268d712b6d9de84f36e19755e Mon Sep 17 00:00:00 2001 From: Nicolas De Loof <nicolas.deloof@gmail.com> Date: Wed, 15 Jul 2020 11:42:16 +0200 Subject: [PATCH 165/198] Introduce x-aws-cluster for cluster to deploy application to Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com> --- ecs/Makefile | 5 +++- ecs/pkg/amazon/backend/list.go | 19 ++++++++------- ecs/pkg/amazon/backend/up.go | 43 ++++++++++++++++++++++++---------- ecs/pkg/compose/x.go | 1 + 4 files changed, 46 insertions(+), 22 deletions(-) diff --git a/ecs/Makefile b/ecs/Makefile index cf9c48cc2..109325861 100644 --- a/ecs/Makefile +++ b/ecs/Makefile @@ -39,7 +39,10 @@ dev: build lint: ## Verify Go files @docker build . --target lint +fmt: ## Format go files + go fmt ./... + clean: rm -rf dist/ -.PHONY: clean build test dev lint e2e cross +.PHONY: clean build test dev lint e2e cross fmt diff --git a/ecs/pkg/amazon/backend/list.go b/ecs/pkg/amazon/backend/list.go index 232b2c816..8647b0d47 100644 --- a/ecs/pkg/amazon/backend/list.go +++ b/ecs/pkg/amazon/backend/list.go @@ -14,25 +14,28 @@ func (b *Backend) Ps(ctx context.Context, options cli.ProjectOptions) ([]compose return nil, err } - cluster := b.Cluster - if cluster == "" { - cluster = project.Name - } - resources, err := b.api.ListStackResources(ctx, project.Name) if err != nil { return nil, err } - var loadBalancer string - if lb, ok := project.Extensions[compose.ExtensionLB]; ok { - loadBalancer = lb.(string) + loadBalancer, err := b.GetLoadBalancer(ctx, project) + if err != nil { + return nil, err } + + cluster, err := b.GetCluster(ctx, project) + if err != nil { + return nil, err + } + servicesARN := []string{} for _, r := range resources { switch r.Type { case "AWS::ECS::Service": servicesARN = append(servicesARN, r.ARN) + case "AWS::ECS::Cluster": + cluster = r.ARN case "AWS::ElasticLoadBalancingV2::LoadBalancer": loadBalancer = r.ARN } diff --git a/ecs/pkg/amazon/backend/up.go b/ecs/pkg/amazon/backend/up.go index 104fef915..e91f11916 100644 --- a/ecs/pkg/amazon/backend/up.go +++ b/ecs/pkg/amazon/backend/up.go @@ -16,14 +16,14 @@ func (b *Backend) Up(ctx context.Context, options cli.ProjectOptions) error { return err } - if b.Cluster != "" { - ok, err := b.api.ClusterExists(ctx, b.Cluster) - if err != nil { - return err - } - if !ok { - return fmt.Errorf("configured cluster %q does not exist", b.Cluster) - } + err = b.api.CheckRequirements(ctx) + if err != nil { + return err + } + + cluster, err := b.GetCluster(ctx, project) + if err != nil { + return err } update, err := b.api.StackExists(ctx, project.Name) @@ -55,7 +55,7 @@ func (b *Backend) Up(ctx context.Context, options cli.ProjectOptions) error { } parameters := map[string]string{ - ParameterClusterName: b.Cluster, + ParameterClusterName: cluster, ParameterVPCId: vpc, ParameterSubnet1Id: subNets[0], ParameterSubnet2Id: subNets[1], @@ -96,15 +96,32 @@ func (b Backend) GetVPC(ctx context.Context, project *types.Project) (string, er func (b Backend) GetLoadBalancer(ctx context.Context, project *types.Project) (string, error) { //check compose file for custom VPC selected - if lb, ok := project.Extensions[compose.ExtensionLB]; ok { - lbName := lb.(string) - ok, err := b.api.LoadBalancerExists(ctx, lbName) + if ext, ok := project.Extensions[compose.ExtensionLB]; ok { + lb := ext.(string) + ok, err := b.api.LoadBalancerExists(ctx, lb) if err != nil { return "", err } if !ok { - return "", fmt.Errorf("Load Balancer does not exist: %s", lb) + return "", fmt.Errorf("load balancer does not exist: %s", lb) } + return lb, nil } return "", nil } + +func (b Backend) GetCluster(ctx context.Context, project *types.Project) (string, error) { + //check compose file for custom VPC selected + if ext, ok := project.Extensions[compose.ExtensionCluster]; ok { + cluster := ext.(string) + ok, err := b.api.ClusterExists(ctx, cluster) + if err != nil { + return "", err + } + if !ok { + return "", fmt.Errorf("cluster does not exist: %s", cluster) + } + return cluster, nil + } + return b.Cluster, nil +} diff --git a/ecs/pkg/compose/x.go b/ecs/pkg/compose/x.go index 7a2a5bd0b..8e52a368f 100644 --- a/ecs/pkg/compose/x.go +++ b/ecs/pkg/compose/x.go @@ -5,4 +5,5 @@ const ( ExtensionVPC = "x-aws-vpc" ExtensionPullCredentials = "x-aws-pull_credentials" ExtensionLB = "x-aws-loadbalancer" + ExtensionCluster = "x-aws-cluster" ) From 37b9e74308a62254deef402433b9730fb4e4b23d Mon Sep 17 00:00:00 2001 From: Nicolas De Loof <nicolas.deloof@gmail.com> Date: Wed, 15 Jul 2020 16:27:24 +0200 Subject: [PATCH 166/198] Implement `ps` without need for the original compose.yaml file close #165 Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com> --- ecs/pkg/amazon/backend/down.go | 22 ++++++++++++++------- ecs/pkg/amazon/backend/list.go | 35 +++++++++++++++++++--------------- ecs/pkg/amazon/sdk/api.go | 1 + ecs/pkg/amazon/sdk/sdk.go | 15 +++++++++++++++ 4 files changed, 51 insertions(+), 22 deletions(-) diff --git a/ecs/pkg/amazon/backend/down.go b/ecs/pkg/amazon/backend/down.go index fcfbc9acf..45a7f844e 100644 --- a/ecs/pkg/amazon/backend/down.go +++ b/ecs/pkg/amazon/backend/down.go @@ -9,13 +9,9 @@ import ( ) func (b *Backend) Down(ctx context.Context, options cli.ProjectOptions) error { - name := options.Name - if name == "" { - project, err := cli.ProjectFromOptions(&options) - if err != nil { - return err - } - name = project.Name + name, err2 := b.projectName(options) + if err2 != nil { + return err2 } err := b.api.DeleteStack(ctx, name) @@ -30,3 +26,15 @@ func (b *Backend) Down(ctx context.Context, options cli.ProjectOptions) error { } return nil } + +func (b *Backend) projectName(options cli.ProjectOptions) (string, error) { + name := options.Name + if name == "" { + project, err := cli.ProjectFromOptions(&options) + if err != nil { + return "", err + } + name = project.Name + } + return name, nil +} diff --git a/ecs/pkg/amazon/backend/list.go b/ecs/pkg/amazon/backend/list.go index 8647b0d47..ff3c4b2af 100644 --- a/ecs/pkg/amazon/backend/list.go +++ b/ecs/pkg/amazon/backend/list.go @@ -3,33 +3,34 @@ package backend import ( "context" "fmt" + "regexp" + "strings" "github.com/compose-spec/compose-go/cli" "github.com/docker/ecs-plugin/pkg/compose" ) +var targetGroupLogicalName = regexp.MustCompile("(.*)(TCP|UDP)([0-9]+)TargetGroup") + func (b *Backend) Ps(ctx context.Context, options cli.ProjectOptions) ([]compose.ServiceStatus, error) { - project, err := cli.ProjectFromOptions(&options) + projectName, err := b.projectName(options) if err != nil { return nil, err } - - resources, err := b.api.ListStackResources(ctx, project.Name) + parameters, err := b.api.ListStackParameters(ctx, projectName) if err != nil { return nil, err } + loadBalancer := parameters[ParameterLoadBalancerARN] + cluster := parameters[ParameterClusterName] - loadBalancer, err := b.GetLoadBalancer(ctx, project) - if err != nil { - return nil, err - } - - cluster, err := b.GetCluster(ctx, project) + resources, err := b.api.ListStackResources(ctx, projectName) if err != nil { return nil, err } servicesARN := []string{} + targetGroups := []string{} for _, r := range resources { switch r.Type { case "AWS::ECS::Service": @@ -38,9 +39,14 @@ func (b *Backend) Ps(ctx context.Context, options cli.ProjectOptions) ([]compose cluster = r.ARN case "AWS::ElasticLoadBalancingV2::LoadBalancer": loadBalancer = r.ARN + case "AWS::ElasticLoadBalancingV2::TargetGroup": + targetGroups = append(targetGroups, r.LogicalID) } } + if len(servicesARN) == 0 { + return nil, nil + } status, err := b.api.DescribeServices(ctx, cluster, servicesARN) if err != nil { return nil, err @@ -52,13 +58,12 @@ func (b *Backend) Ps(ctx context.Context, options cli.ProjectOptions) ([]compose } for i, state := range status { - s, err := project.GetService(state.Name) - if err != nil { - return nil, err - } ports := []string{} - for _, p := range s.Ports { - ports = append(ports, fmt.Sprintf("%s:%d->%d/%s", url, p.Published, p.Target, p.Protocol)) + for _, tg := range targetGroups { + groups := targetGroupLogicalName.FindStringSubmatch(tg) + if groups[0] == state.Name { + ports = append(ports, fmt.Sprintf("%s:%s->%s/%s", url, groups[2], groups[2], strings.ToLower(groups[1]))) + } } state.Ports = ports status[i] = state diff --git a/ecs/pkg/amazon/sdk/api.go b/ecs/pkg/amazon/sdk/api.go index 5de5d5af2..bce65b29a 100644 --- a/ecs/pkg/amazon/sdk/api.go +++ b/ecs/pkg/amazon/sdk/api.go @@ -18,6 +18,7 @@ type API interface { StackExists(ctx context.Context, name string) (bool, error) CreateStack(ctx context.Context, name string, template *cloudformation.Template, parameters map[string]string) error DeleteStack(ctx context.Context, name string) error + ListStackParameters(ctx context.Context, name string) (map[string]string, error) ListStackResources(ctx context.Context, name string) ([]compose.StackResource, error) GetStackID(ctx context.Context, name string) (string, error) WaitStackComplete(ctx context.Context, name string, operation int) error diff --git a/ecs/pkg/amazon/sdk/sdk.go b/ecs/pkg/amazon/sdk/sdk.go index fd0e649fa..2af1f78f3 100644 --- a/ecs/pkg/amazon/sdk/sdk.go +++ b/ecs/pkg/amazon/sdk/sdk.go @@ -238,6 +238,21 @@ func (s sdk) DescribeStackEvents(ctx context.Context, stackID string) ([]*cloudf } } +func (s sdk) ListStackParameters(ctx context.Context, name string) (map[string]string, error) { + st, err := s.CF.DescribeStacksWithContext(ctx, &cloudformation.DescribeStacksInput{ + NextToken: nil, + StackName: aws.String(name), + }) + if err != nil { + return nil, err + } + parameters := map[string]string{} + for _, parameter := range st.Stacks[0].Parameters { + parameters[*parameter.ParameterKey] = *parameter.ParameterValue + } + return parameters, nil +} + func (s sdk) ListStackResources(ctx context.Context, name string) ([]compose.StackResource, error) { // FIXME handle pagination res, err := s.CF.ListStackResourcesWithContext(ctx, &cloudformation.ListStackResourcesInput{ From efeded2670a8ae45ae8dbe0dc2fd58889f120ca8 Mon Sep 17 00:00:00 2001 From: Nicolas De Loof <nicolas.deloof@gmail.com> Date: Thu, 16 Jul 2020 08:28:17 +0200 Subject: [PATCH 167/198] Remove Cluster from context Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com> --- ecs/cmd/commands/compose.go | 10 ++-- ecs/cmd/commands/context.go | 31 +++++++++++ ecs/cmd/commands/secret.go | 8 +-- ecs/cmd/commands/setup.go | 52 ++++++++----------- ecs/pkg/amazon/backend/backend.go | 12 ++--- ecs/pkg/amazon/backend/cloudformation_test.go | 8 +-- ecs/pkg/amazon/backend/context.go | 22 ++++---- ecs/pkg/amazon/backend/down.go | 8 +-- ecs/pkg/amazon/backend/up.go | 2 +- ecs/pkg/docker/contextStore.go | 30 ++--------- ecs/tests/main_test.go | 2 +- 11 files changed, 90 insertions(+), 95 deletions(-) create mode 100644 ecs/cmd/commands/context.go diff --git a/ecs/cmd/commands/compose.go b/ecs/cmd/commands/compose.go index 551b5370a..bceba59b6 100644 --- a/ecs/cmd/commands/compose.go +++ b/ecs/cmd/commands/compose.go @@ -45,7 +45,7 @@ func (o upOptions) LoadBalancerArn() *string { func ConvertCommand(dockerCli command.Cli, options *cli.ProjectOptions) *cobra.Command { cmd := &cobra.Command{ Use: "convert", - RunE: docker.WithAwsContext(dockerCli, func(ctx docker.AwsContext, backend *amazon.Backend, args []string) error { + RunE: WithAwsContext(dockerCli, func(ctx docker.AwsContext, backend *amazon.Backend, args []string) error { opts := options.WithOsEnv() project, err := cli.ProjectFromOptions(&opts) if err != nil { @@ -72,7 +72,7 @@ func UpCommand(dockerCli command.Cli, options *cli.ProjectOptions) *cobra.Comman opts := upOptions{} cmd := &cobra.Command{ Use: "up", - RunE: docker.WithAwsContext(dockerCli, func(clusteropts docker.AwsContext, backend *amazon.Backend, args []string) error { + RunE: WithAwsContext(dockerCli, func(clusteropts docker.AwsContext, backend *amazon.Backend, args []string) error { return backend.Up(context.Background(), *options) }), } @@ -84,7 +84,7 @@ func PsCommand(dockerCli command.Cli, options *cli.ProjectOptions) *cobra.Comman opts := upOptions{} cmd := &cobra.Command{ Use: "ps", - RunE: docker.WithAwsContext(dockerCli, func(ctx docker.AwsContext, backend *amazon.Backend, args []string) error { + RunE: WithAwsContext(dockerCli, func(ctx docker.AwsContext, backend *amazon.Backend, args []string) error { status, err := backend.Ps(context.Background(), *options) if err != nil { return err @@ -109,7 +109,7 @@ func DownCommand(dockerCli command.Cli, projectOpts *cli.ProjectOptions) *cobra. opts := downOptions{} cmd := &cobra.Command{ Use: "down", - RunE: docker.WithAwsContext(dockerCli, func(ctx docker.AwsContext, backend *amazon.Backend, args []string) error { + RunE: WithAwsContext(dockerCli, func(ctx docker.AwsContext, backend *amazon.Backend, args []string) error { return backend.Down(context.Background(), *projectOpts) }), } @@ -120,7 +120,7 @@ func DownCommand(dockerCli command.Cli, projectOpts *cli.ProjectOptions) *cobra. func LogsCommand(dockerCli command.Cli, projectOpts *cli.ProjectOptions) *cobra.Command { cmd := &cobra.Command{ Use: "logs [PROJECT NAME]", - RunE: docker.WithAwsContext(dockerCli, func(clusteropts docker.AwsContext, backend *amazon.Backend, args []string) error { + RunE: WithAwsContext(dockerCli, func(clusteropts docker.AwsContext, backend *amazon.Backend, args []string) error { return backend.Logs(context.Background(), *projectOpts) }), } diff --git a/ecs/cmd/commands/context.go b/ecs/cmd/commands/context.go new file mode 100644 index 000000000..080d3b7d9 --- /dev/null +++ b/ecs/cmd/commands/context.go @@ -0,0 +1,31 @@ +package commands + +import ( + "fmt" + + "github.com/aws/aws-sdk-go/aws/awserr" + "github.com/docker/cli/cli/command" + amazon "github.com/docker/ecs-plugin/pkg/amazon/backend" + "github.com/docker/ecs-plugin/pkg/docker" + "github.com/spf13/cobra" +) + +type ContextFunc func(ctx docker.AwsContext, backend *amazon.Backend, args []string) error + +func WithAwsContext(dockerCli command.Cli, f ContextFunc) func(cmd *cobra.Command, args []string) error { + return func(cmd *cobra.Command, args []string) error { + ctx, err := docker.GetAwsContext(dockerCli) + if err != nil { + return err + } + backend, err := amazon.NewBackend(ctx.Profile, ctx.Region) + if err != nil { + return err + } + err = f(*ctx, backend, args) + if e, ok := err.(awserr.Error); ok { + return fmt.Errorf(e.Message()) + } + return err + } +} diff --git a/ecs/cmd/commands/secret.go b/ecs/cmd/commands/secret.go index 5bbefaa9b..3246e1237 100644 --- a/ecs/cmd/commands/secret.go +++ b/ecs/cmd/commands/secret.go @@ -47,7 +47,7 @@ func CreateSecret(dockerCli command.Cli) *cobra.Command { cmd := &cobra.Command{ Use: "create NAME", Short: "Creates a secret.", - RunE: docker.WithAwsContext(dockerCli, func(clusteropts docker.AwsContext, backend *amazon.Backend, args []string) error { + RunE: WithAwsContext(dockerCli, func(clusteropts docker.AwsContext, backend *amazon.Backend, args []string) error { if len(args) == 0 { return errors.New("Missing mandatory parameter: NAME") } @@ -69,7 +69,7 @@ func InspectSecret(dockerCli command.Cli) *cobra.Command { cmd := &cobra.Command{ Use: "inspect ID", Short: "Displays secret details", - RunE: docker.WithAwsContext(dockerCli, func(clusteropts docker.AwsContext, backend *amazon.Backend, args []string) error { + RunE: WithAwsContext(dockerCli, func(clusteropts docker.AwsContext, backend *amazon.Backend, args []string) error { if len(args) == 0 { return errors.New("Missing mandatory parameter: ID") } @@ -94,7 +94,7 @@ func ListSecrets(dockerCli command.Cli) *cobra.Command { Use: "list", Aliases: []string{"ls"}, Short: "List secrets stored for the existing account.", - RunE: docker.WithAwsContext(dockerCli, func(clusteropts docker.AwsContext, backend *amazon.Backend, args []string) error { + RunE: WithAwsContext(dockerCli, func(clusteropts docker.AwsContext, backend *amazon.Backend, args []string) error { secrets, err := backend.ListSecrets(context.Background()) if err != nil { return err @@ -113,7 +113,7 @@ func DeleteSecret(dockerCli command.Cli) *cobra.Command { Use: "delete NAME", Aliases: []string{"rm", "remove"}, Short: "Removes a secret.", - RunE: docker.WithAwsContext(dockerCli, func(clusteropts docker.AwsContext, backend *amazon.Backend, args []string) error { + RunE: WithAwsContext(dockerCli, func(clusteropts docker.AwsContext, backend *amazon.Backend, args []string) error { if len(args) == 0 { return errors.New("Missing mandatory parameter: [NAME]") } diff --git a/ecs/cmd/commands/setup.go b/ecs/cmd/commands/setup.go index c6ac612ce..da733f715 100644 --- a/ecs/cmd/commands/setup.go +++ b/ecs/cmd/commands/setup.go @@ -22,17 +22,18 @@ const enterLabelPrefix = "Enter " type setupOptions struct { name string - context contextStore.AwsContext + profile string + region string accessKeyID string secretAccessKey string } func (s setupOptions) unsetRequiredArgs() []string { unset := []string{} - if s.context.Profile == "" { + if s.profile == "" { unset = append(unset, "profile") } - if s.context.Region == "" { + if s.region == "" { unset = append(unset, "region") } return unset @@ -51,25 +52,28 @@ func SetupCommand() *cobra.Command { } } if opts.accessKeyID != "" && opts.secretAccessKey != "" { - if err := saveCredentials(opts.context.Profile, opts.accessKeyID, opts.secretAccessKey); err != nil { + if err := saveCredentials(opts.profile, opts.accessKeyID, opts.secretAccessKey); err != nil { return err } } - backend, err := amazon.NewBackend(opts.context.Profile, opts.context.Cluster, opts.context.Region) + backend, err := amazon.NewBackend(opts.profile, opts.region) if err != nil { return err } - _, _, err = backend.CreateContextData(context.Background(), nil) + + context, _, err := backend.CreateContextData(context.Background(), map[string]string{ + amazon.ContextParamProfile: opts.profile, + amazon.ContextParamRegion: opts.region, + }) if err != nil { return err } - return contextStore.NewContext(opts.name, &opts.context) + return contextStore.NewContext(opts.name, context) }, } cmd.Flags().StringVarP(&opts.name, "name", "n", "ecs", "Context Name") - cmd.Flags().StringVarP(&opts.context.Profile, "profile", "p", "", "AWS Profile") - cmd.Flags().StringVarP(&opts.context.Cluster, "cluster", "c", "", "ECS cluster") - cmd.Flags().StringVarP(&opts.context.Region, "region", "r", "", "AWS region") + cmd.Flags().StringVarP(&opts.profile, "profile", "p", "", "AWS Profile") + cmd.Flags().StringVarP(&opts.region, "region", "r", "", "AWS region") cmd.Flags().StringVarP(&opts.accessKeyID, "aws-key-id", "k", "", "AWS Access Key ID") cmd.Flags().StringVarP(&opts.secretAccessKey, "aws-secret-key", "s", "", "AWS Secret Access Key") @@ -88,10 +92,6 @@ func interactiveCli(opts *setupOptions) error { return err } - if err := setCluster(opts, err); err != nil { - return err - } - if err := setRegion(opts, section); err != nil { return err } @@ -156,7 +156,7 @@ func awsProfiles(filename string) (map[string]ini.Section, error) { } func setContextName(opts *setupOptions) error { - if opts.name == "aws" { + if opts.name == "ecs" { result, err := promptString(opts.name, "context name", enterLabelPrefix, 2) if err != nil { return err @@ -171,7 +171,7 @@ func setProfile(opts *setupOptions, section ini.Section) (ini.Section, error) { if err != nil { return ini.Section{}, err } - section, ok := profilesList[opts.context.Profile] + section, ok := profilesList[opts.profile] if !ok { prompt := promptui.Select{ Label: "Select AWS Profile", @@ -179,14 +179,14 @@ func setProfile(opts *setupOptions, section ini.Section) (ini.Section, error) { } _, result, err := prompt.Run() if result == "new profile" { - result, err := promptString(opts.context.Profile, "profile name", enterLabelPrefix, 2) + result, err := promptString(opts.profile, "profile name", enterLabelPrefix, 2) if err != nil { return ini.Section{}, err } - opts.context.Profile = result + opts.profile = result } else { section = profilesList[result] - opts.context.Profile = result + opts.profile = result } if err != nil { return ini.Section{}, err @@ -196,7 +196,7 @@ func setProfile(opts *setupOptions, section ini.Section) (ini.Section, error) { } func setRegion(opts *setupOptions, section ini.Section) error { - defaultRegion := opts.context.Region + defaultRegion := opts.region if defaultRegion == "" && section.Name() != "" { region, err := section.GetKey("region") if err == nil { @@ -207,17 +207,7 @@ func setRegion(opts *setupOptions, section ini.Section) error { if err != nil { return err } - opts.context.Region = result - return nil -} - -func setCluster(opts *setupOptions, err error) error { - result, err := promptString(opts.context.Cluster, "cluster name", enterLabelPrefix, 0) - if err != nil { - return err - } - - opts.context.Cluster = result + opts.region = result return nil } diff --git a/ecs/pkg/amazon/backend/backend.go b/ecs/pkg/amazon/backend/backend.go index 07764df35..f5bba4ed2 100644 --- a/ecs/pkg/amazon/backend/backend.go +++ b/ecs/pkg/amazon/backend/backend.go @@ -6,7 +6,7 @@ import ( "github.com/docker/ecs-plugin/pkg/amazon/sdk" ) -func NewBackend(profile string, cluster string, region string) (*Backend, error) { +func NewBackend(profile string, region string) (*Backend, error) { sess, err := session.NewSessionWithOptions(session.Options{ Profile: profile, Config: aws.Config{ @@ -17,14 +17,12 @@ func NewBackend(profile string, cluster string, region string) (*Backend, error) return nil, err } return &Backend{ - Cluster: cluster, - Region: region, - api: sdk.NewAPI(sess), + Region: region, + api: sdk.NewAPI(sess), }, nil } type Backend struct { - Cluster string - Region string - api sdk.API + Region string + api sdk.API } diff --git a/ecs/pkg/amazon/backend/cloudformation_test.go b/ecs/pkg/amazon/backend/cloudformation_test.go index 032b9cd58..a364bfaff 100644 --- a/ecs/pkg/amazon/backend/cloudformation_test.go +++ b/ecs/pkg/amazon/backend/cloudformation_test.go @@ -21,7 +21,7 @@ import ( func TestSimpleConvert(t *testing.T) { project := load(t, "testdata/input/simple-single-service.yaml") - result := convertResultAsString(t, project, "TestCluster") + result := convertResultAsString(t, project) expected := "simple/simple-cloudformation-conversion.golden" golden.Assert(t, result, expected) } @@ -197,10 +197,10 @@ services: } } -func convertResultAsString(t *testing.T, project *types.Project, clusterName string) string { - client, err := NewBackend("", clusterName, "") +func convertResultAsString(t *testing.T, project *types.Project) string { + backend, err := NewBackend("", "") assert.NilError(t, err) - result, err := client.Convert(project) + result, err := backend.Convert(project) assert.NilError(t, err) resultAsJSON, err := result.JSON() assert.NilError(t, err) diff --git a/ecs/pkg/amazon/backend/context.go b/ecs/pkg/amazon/backend/context.go index bc785d91a..f6f79f4e1 100644 --- a/ecs/pkg/amazon/backend/context.go +++ b/ecs/pkg/amazon/backend/context.go @@ -2,7 +2,13 @@ package backend import ( "context" - "fmt" + + "github.com/docker/ecs-plugin/pkg/docker" +) + +const ( + ContextParamRegion = "region" + ContextParamProfile = "profile" ) func (b *Backend) CreateContextData(ctx context.Context, params map[string]string) (contextData interface{}, description string, err error) { @@ -11,14 +17,8 @@ func (b *Backend) CreateContextData(ctx context.Context, params map[string]strin return "", "", err } - if b.Cluster != "" { - exists, err := b.api.ClusterExists(ctx, b.Cluster) - if err != nil { - return "", "", err - } - if !exists { - return "", "", fmt.Errorf("cluster %s does not exists", b.Cluster) - } - } - return "", "", nil + return docker.AwsContext{ + Profile: params[ContextParamProfile], + Region: params[ContextParamRegion], + }, "Amazon ECS context", nil } diff --git a/ecs/pkg/amazon/backend/down.go b/ecs/pkg/amazon/backend/down.go index 45a7f844e..31df94fca 100644 --- a/ecs/pkg/amazon/backend/down.go +++ b/ecs/pkg/amazon/backend/down.go @@ -9,12 +9,12 @@ import ( ) func (b *Backend) Down(ctx context.Context, options cli.ProjectOptions) error { - name, err2 := b.projectName(options) - if err2 != nil { - return err2 + name, err := b.projectName(options) + if err != nil { + return err } - err := b.api.DeleteStack(ctx, name) + err = b.api.DeleteStack(ctx, name) if err != nil { return err } diff --git a/ecs/pkg/amazon/backend/up.go b/ecs/pkg/amazon/backend/up.go index e91f11916..8b975dc5e 100644 --- a/ecs/pkg/amazon/backend/up.go +++ b/ecs/pkg/amazon/backend/up.go @@ -123,5 +123,5 @@ func (b Backend) GetCluster(ctx context.Context, project *types.Project) (string } return cluster, nil } - return b.Cluster, nil + return "", nil } diff --git a/ecs/pkg/docker/contextStore.go b/ecs/pkg/docker/contextStore.go index 83c055f68..30a12de21 100644 --- a/ecs/pkg/docker/contextStore.go +++ b/ecs/pkg/docker/contextStore.go @@ -3,13 +3,10 @@ package docker import ( "fmt" - "github.com/aws/aws-sdk-go/aws/awserr" "github.com/docker/cli/cli/command" cliconfig "github.com/docker/cli/cli/config" "github.com/docker/cli/cli/context/store" - amazon "github.com/docker/ecs-plugin/pkg/amazon/backend" "github.com/mitchellh/mapstructure" - "github.com/spf13/cobra" ) const contextType = "aws" @@ -24,16 +21,15 @@ func getter() interface{} { type AwsContext struct { Profile string - Cluster string Region string } -func NewContext(name string, awsContext *AwsContext) error { - _, err := NewContextWithStore(name, awsContext, cliconfig.ContextStoreDir()) +func NewContext(name string, awsContext interface{}) error { + _, err := NewContextWithStore(name, awsContext.(AwsContext), cliconfig.ContextStoreDir()) return err } -func NewContextWithStore(name string, awsContext *AwsContext, contextDirectory string) (store.Store, error) { +func NewContextWithStore(name string, awsContext AwsContext, contextDirectory string) (store.Store, error) { contextStore := initContextStore(contextDirectory) endpoints := map[string]interface{}{ "aws": awsContext, @@ -74,26 +70,6 @@ func checkAwsContextExists(contextName string) (*AwsContext, error) { return &awsContext, nil } -type ContextFunc func(ctx AwsContext, backend *amazon.Backend, args []string) error - -func WithAwsContext(dockerCli command.Cli, f ContextFunc) func(cmd *cobra.Command, args []string) error { - return func(cmd *cobra.Command, args []string) error { - ctx, err := GetAwsContext(dockerCli) - if err != nil { - return err - } - backend, err := amazon.NewBackend(ctx.Profile, ctx.Cluster, ctx.Region) - if err != nil { - return err - } - err = f(*ctx, backend, args) - if e, ok := err.(awserr.Error); ok { - return fmt.Errorf(e.Message()) - } - return err - } -} - func GetAwsContext(dockerCli command.Cli) (*AwsContext, error) { contextName := dockerCli.CurrentContext() return checkAwsContextExists(contextName) diff --git a/ecs/tests/main_test.go b/ecs/tests/main_test.go index b8a758144..5849e4060 100644 --- a/ecs/tests/main_test.go +++ b/ecs/tests/main_test.go @@ -58,7 +58,7 @@ func (d dockerCliCommand) createTestCmd(ops ...ConfigFileOperator) (icmd.Cmd, fu Profile: "sandbox.devtools.developer", Region: "eu-west-3", } - testStore, err := docker.NewContextWithStore(testContextName, &awsContext, filepath.Join(configDir, "contexts")) + testStore, err := docker.NewContextWithStore(testContextName, awsContext, filepath.Join(configDir, "contexts")) if err != nil { panic(err) } From 2d931dab9dac20002cc80d357fe465e577dfd53e Mon Sep 17 00:00:00 2001 From: Nicolas De Loof <nicolas.deloof@gmail.com> Date: Thu, 16 Jul 2020 16:07:26 +0200 Subject: [PATCH 168/198] `up` can update an existing stack using CloudFormation Changeset Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com> --- ecs/pkg/amazon/backend/up.go | 29 ++++++++++++------ ecs/pkg/amazon/sdk/api.go | 2 ++ ecs/pkg/amazon/sdk/sdk.go | 59 ++++++++++++++++++++++++++++++++++-- ecs/pkg/compose/types.go | 1 + 4 files changed, 78 insertions(+), 13 deletions(-) diff --git a/ecs/pkg/amazon/backend/up.go b/ecs/pkg/amazon/backend/up.go index 8b975dc5e..3ea21d176 100644 --- a/ecs/pkg/amazon/backend/up.go +++ b/ecs/pkg/amazon/backend/up.go @@ -26,14 +26,6 @@ func (b *Backend) Up(ctx context.Context, options cli.ProjectOptions) error { return err } - update, err := b.api.StackExists(ctx, project.Name) - if err != nil { - return err - } - if update { - return fmt.Errorf("we do not (yet) support updating an existing CloudFormation stack") - } - template, err := b.Convert(project) if err != nil { return err @@ -62,17 +54,34 @@ func (b *Backend) Up(ctx context.Context, options cli.ProjectOptions) error { ParameterLoadBalancerARN: lb, } - err = b.api.CreateStack(ctx, project.Name, template, parameters) + update, err := b.api.StackExists(ctx, project.Name) if err != nil { return err } + operation := compose.StackCreate + if update { + operation = compose.StackUpdate + changeset, err := b.api.CreateChangeSet(ctx, project.Name, template, parameters) + if err != nil { + return err + } + err = b.api.UpdateStack(ctx, changeset) + if err != nil { + return err + } + } else { + err = b.api.CreateStack(ctx, project.Name, template, parameters) + if err != nil { + return err + } + } fmt.Println() w := console.NewProgressWriter() for k := range template.Resources { w.ResourceEvent(k, "PENDING", "") } - return b.WaitStackCompletion(ctx, project.Name, compose.StackCreate, w) + return b.WaitStackCompletion(ctx, project.Name, operation, w) } func (b Backend) GetVPC(ctx context.Context, project *types.Project) (string, error) { diff --git a/ecs/pkg/amazon/sdk/api.go b/ecs/pkg/amazon/sdk/api.go index bce65b29a..c56df268d 100644 --- a/ecs/pkg/amazon/sdk/api.go +++ b/ecs/pkg/amazon/sdk/api.go @@ -23,6 +23,8 @@ type API interface { GetStackID(ctx context.Context, name string) (string, error) WaitStackComplete(ctx context.Context, name string, operation int) error DescribeStackEvents(ctx context.Context, stackID string) ([]*cf.StackEvent, error) + CreateChangeSet(ctx context.Context, name string, template *cloudformation.Template, parameters map[string]string) (string, error) + UpdateStack(ctx context.Context, changeset string) error DescribeServices(ctx context.Context, cluster string, arns []string) ([]compose.ServiceStatus, error) diff --git a/ecs/pkg/amazon/sdk/sdk.go b/ecs/pkg/amazon/sdk/sdk.go index 2af1f78f3..b2d0cb15c 100644 --- a/ecs/pkg/amazon/sdk/sdk.go +++ b/ecs/pkg/amazon/sdk/sdk.go @@ -175,9 +175,8 @@ func (s sdk) CreateStack(ctx context.Context, name string, template *cf.Template param := []*cloudformation.Parameter{} for name, value := range parameters { param = append(param, &cloudformation.Parameter{ - ParameterKey: aws.String(name), - ParameterValue: aws.String(value), - UsePreviousValue: aws.Bool(true), + ParameterKey: aws.String(name), + ParameterValue: aws.String(value), }) } @@ -194,6 +193,60 @@ func (s sdk) CreateStack(ctx context.Context, name string, template *cf.Template return err } +func (s sdk) CreateChangeSet(ctx context.Context, name string, template *cf.Template, parameters map[string]string) (string, error) { + logrus.Debug("Create CloudFormation Changeset") + json, err := template.JSON() + if err != nil { + return "", err + } + + param := []*cloudformation.Parameter{} + for name := range parameters { + param = append(param, &cloudformation.Parameter{ + ParameterKey: aws.String(name), + UsePreviousValue: aws.Bool(true), + }) + } + + update := fmt.Sprintf("Update%s", time.Now().Format("2006-01-02-15-04-05")) + changeset, err := s.CF.CreateChangeSetWithContext(ctx, &cloudformation.CreateChangeSetInput{ + ChangeSetName: aws.String(update), + ChangeSetType: aws.String(cloudformation.ChangeSetTypeUpdate), + StackName: aws.String(name), + TemplateBody: aws.String(string(json)), + Parameters: param, + Capabilities: []*string{ + aws.String(cloudformation.CapabilityCapabilityIam), + }, + }) + if err != nil { + return "", err + } + + err = s.CF.WaitUntilChangeSetCreateCompleteWithContext(ctx, &cloudformation.DescribeChangeSetInput{ + ChangeSetName: changeset.Id, + }) + return *changeset.Id, err +} + +func (s sdk) UpdateStack(ctx context.Context, changeset string) error { + desc, err := s.CF.DescribeChangeSetWithContext(ctx, &cloudformation.DescribeChangeSetInput{ + ChangeSetName: aws.String(changeset), + }) + if err != nil { + return err + } + + if strings.HasPrefix(aws.StringValue(desc.StatusReason), "The submitted information didn't contain changes.") { + return nil + } + + _, err = s.CF.ExecuteChangeSet(&cloudformation.ExecuteChangeSetInput{ + ChangeSetName: aws.String(changeset), + }) + return err +} + func (s sdk) WaitStackComplete(ctx context.Context, name string, operation int) error { input := &cloudformation.DescribeStacksInput{ StackName: aws.String(name), diff --git a/ecs/pkg/compose/types.go b/ecs/pkg/compose/types.go index 370bfc4b5..ec59578a2 100644 --- a/ecs/pkg/compose/types.go +++ b/ecs/pkg/compose/types.go @@ -19,6 +19,7 @@ type ServiceStatus struct { const ( StackCreate = iota + StackUpdate StackDelete ) From 12215130b575f82c7d508ff33c97ee9572e8e341 Mon Sep 17 00:00:00 2001 From: aiordache <anca.iordache@docker.com> Date: Tue, 21 Jul 2020 17:45:16 +0200 Subject: [PATCH 169/198] generic URL/port/protocol retrieval for compose ps Signed-off-by: aiordache <anca.iordache@docker.com> Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com> --- ecs/cmd/commands/compose.go | 2 +- ecs/pkg/amazon/backend/list.go | 24 ++++-------- ecs/pkg/amazon/sdk/sdk.go | 70 +++++++++++++++++++++++++++++++--- ecs/pkg/compose/types.go | 23 ++++++++--- 4 files changed, 91 insertions(+), 28 deletions(-) diff --git a/ecs/cmd/commands/compose.go b/ecs/cmd/commands/compose.go index bceba59b6..75a9555f8 100644 --- a/ecs/cmd/commands/compose.go +++ b/ecs/cmd/commands/compose.go @@ -91,7 +91,7 @@ func PsCommand(dockerCli command.Cli, options *cli.ProjectOptions) *cobra.Comman } printSection(os.Stdout, len(status), func(w io.Writer) { for _, service := range status { - fmt.Fprintf(w, "%s\t%s\t%d/%d\t%s\n", service.ID, service.Name, service.Replicas, service.Desired, strings.Join(service.Ports, " ")) + fmt.Fprintf(w, "%s\t%s\t%d/%d\t%s\n", service.ID, service.Name, service.Replicas, service.Desired, strings.Join(service.Ports, ", ")) } }, "ID", "NAME", "REPLICAS", "PORTS") return nil diff --git a/ecs/pkg/amazon/backend/list.go b/ecs/pkg/amazon/backend/list.go index ff3c4b2af..165241da9 100644 --- a/ecs/pkg/amazon/backend/list.go +++ b/ecs/pkg/amazon/backend/list.go @@ -21,7 +21,6 @@ func (b *Backend) Ps(ctx context.Context, options cli.ProjectOptions) ([]compose if err != nil { return nil, err } - loadBalancer := parameters[ParameterLoadBalancerARN] cluster := parameters[ParameterClusterName] resources, err := b.api.ListStackResources(ctx, projectName) @@ -30,20 +29,14 @@ func (b *Backend) Ps(ctx context.Context, options cli.ProjectOptions) ([]compose } servicesARN := []string{} - targetGroups := []string{} for _, r := range resources { switch r.Type { case "AWS::ECS::Service": servicesARN = append(servicesARN, r.ARN) case "AWS::ECS::Cluster": cluster = r.ARN - case "AWS::ElasticLoadBalancingV2::LoadBalancer": - loadBalancer = r.ARN - case "AWS::ElasticLoadBalancingV2::TargetGroup": - targetGroups = append(targetGroups, r.LogicalID) } } - if len(servicesARN) == 0 { return nil, nil } @@ -52,18 +45,15 @@ func (b *Backend) Ps(ctx context.Context, options cli.ProjectOptions) ([]compose return nil, err } - url, err := b.api.GetLoadBalancerURL(ctx, loadBalancer) - if err != nil { - return nil, err - } - for i, state := range status { ports := []string{} - for _, tg := range targetGroups { - groups := targetGroupLogicalName.FindStringSubmatch(tg) - if groups[0] == state.Name { - ports = append(ports, fmt.Sprintf("%s:%s->%s/%s", url, groups[2], groups[2], strings.ToLower(groups[1]))) - } + for _, lb := range state.LoadBalancers { + ports = append(ports, fmt.Sprintf( + "%s:%d->%d/%s", + lb.URL, + lb.PublishedPort, + lb.TargetPort, + strings.ToLower(lb.Protocol))) } state.Ports = ports status[i] = state diff --git a/ecs/pkg/amazon/sdk/sdk.go b/ecs/pkg/amazon/sdk/sdk.go index b2d0cb15c..7447ecc9f 100644 --- a/ecs/pkg/amazon/sdk/sdk.go +++ b/ecs/pkg/amazon/sdk/sdk.go @@ -449,6 +449,7 @@ func (s sdk) DescribeServices(ctx context.Context, cluster string, arns []string if err != nil { return nil, err } + status := []compose.ServiceStatus{} for _, service := range services.Services { var name string @@ -460,17 +461,76 @@ func (s sdk) DescribeServices(ctx context.Context, cluster string, arns []string if name == "" { return nil, fmt.Errorf("service %s doesn't have a %s tag", *service.ServiceArn, compose.ServiceTag) } + targetGroupArns := []string{} + for _, lb := range service.LoadBalancers { + targetGroupArns = append(targetGroupArns, *lb.TargetGroupArn) + } + // getURLwithPortMapping makes 2 queries + // one to get the target groups and another for load balancers + loadBalancers, err := s.getURLWithPortMapping(ctx, targetGroupArns) + if err != nil { + return nil, err + } status = append(status, compose.ServiceStatus{ - ID: *service.ServiceName, - Name: name, - Replicas: int(*service.RunningCount), - Desired: int(*service.DesiredCount), + ID: *service.ServiceName, + Name: name, + Replicas: int(*service.RunningCount), + Desired: int(*service.DesiredCount), + LoadBalancers: loadBalancers, }) } - return status, nil } +func (s sdk) getURLWithPortMapping(ctx context.Context, targetGroupArns []string) ([]compose.LoadBalancer, error) { + if len(targetGroupArns) == 0 { + return nil, nil + } + groups, err := s.ELB.DescribeTargetGroups(&elbv2.DescribeTargetGroupsInput{ + TargetGroupArns: aws.StringSlice(targetGroupArns), + }) + if err != nil { + return nil, err + } + lbarns := []*string{} + for _, tg := range groups.TargetGroups { + lbarns = append(lbarns, tg.LoadBalancerArns...) + } + + lbs, err := s.ELB.DescribeLoadBalancersWithContext(ctx, &elbv2.DescribeLoadBalancersInput{ + LoadBalancerArns: lbarns, + }) + + if err != nil { + return nil, err + } + filterLB := func(arn *string, lbs []*elbv2.LoadBalancer) *elbv2.LoadBalancer { + for _, lb := range lbs { + if *lb.LoadBalancerArn == *arn { + return lb + } + } + return nil + } + loadBalancers := []compose.LoadBalancer{} + for _, tg := range groups.TargetGroups { + for _, lbarn := range tg.LoadBalancerArns { + lb := filterLB(lbarn, lbs.LoadBalancers) + if lb == nil { + continue + } + loadBalancers = append(loadBalancers, compose.LoadBalancer{ + URL: *lb.DNSName, + TargetPort: int(*tg.Port), + PublishedPort: int(*tg.Port), + Protocol: *tg.Protocol, + }) + + } + } + return loadBalancers, nil +} + func (s sdk) ListTasks(ctx context.Context, cluster string, family string) ([]string, error) { tasks, err := s.ECS.ListTasksWithContext(ctx, &ecs.ListTasksInput{ Cluster: aws.String(cluster), diff --git a/ecs/pkg/compose/types.go b/ecs/pkg/compose/types.go index ec59578a2..1426d1bea 100644 --- a/ecs/pkg/compose/types.go +++ b/ecs/pkg/compose/types.go @@ -9,12 +9,25 @@ type StackResource struct { Status string } +type PortMapping struct { + Source int + Target int +} + +type LoadBalancer struct { + URL string + TargetPort int + PublishedPort int + Protocol string +} + type ServiceStatus struct { - ID string - Name string - Replicas int - Desired int - Ports []string + ID string + Name string + Replicas int + Desired int + Ports []string + LoadBalancers []LoadBalancer } const ( From 3706d8617f058af130576bbaf31a8b7d9ed17362 Mon Sep 17 00:00:00 2001 From: aiordache <anca.iordache@docker.com> Date: Wed, 22 Jul 2020 15:48:04 +0200 Subject: [PATCH 170/198] Pointers value access fixes Signed-off-by: aiordache <anca.iordache@docker.com> (cherry picked from commit 65d6fe57546b4f7a58ecf878d8740155285bc19a) Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com> --- ecs/pkg/amazon/sdk/sdk.go | 52 ++++++++++++++++++++++----------------- 1 file changed, 30 insertions(+), 22 deletions(-) diff --git a/ecs/pkg/amazon/sdk/sdk.go b/ecs/pkg/amazon/sdk/sdk.go index 7447ecc9f..3ccb668c3 100644 --- a/ecs/pkg/amazon/sdk/sdk.go +++ b/ecs/pkg/amazon/sdk/sdk.go @@ -301,7 +301,7 @@ func (s sdk) ListStackParameters(ctx context.Context, name string) (map[string]s } parameters := map[string]string{} for _, parameter := range st.Stacks[0].Parameters { - parameters[*parameter.ParameterKey] = *parameter.ParameterValue + parameters[aws.StringValue(parameter.ParameterKey)] = aws.StringValue(parameter.ParameterValue) } return parameters, nil } @@ -318,10 +318,10 @@ func (s sdk) ListStackResources(ctx context.Context, name string) ([]compose.Sta resources := []compose.StackResource{} for _, r := range res.StackResourceSummaries { resources = append(resources, compose.StackResource{ - LogicalID: *r.LogicalResourceId, - Type: *r.ResourceType, - ARN: *r.PhysicalResourceId, - Status: *r.ResourceStatus, + LogicalID: aws.StringValue(r.LogicalResourceId), + Type: aws.StringValue(r.ResourceType), + ARN: aws.StringValue(r.PhysicalResourceId), + Status: aws.StringValue(r.ResourceStatus), }) } return resources, nil @@ -350,7 +350,7 @@ func (s sdk) CreateSecret(ctx context.Context, secret compose.Secret) (string, e if err != nil { return "", err } - return *response.ARN, nil + return aws.StringValue(response.ARN), nil } func (s sdk) InspectSecret(ctx context.Context, id string) (compose.Secret, error) { @@ -361,11 +361,11 @@ func (s sdk) InspectSecret(ctx context.Context, id string) (compose.Secret, erro } labels := map[string]string{} for _, tag := range response.Tags { - labels[*tag.Key] = *tag.Value + labels[aws.StringValue(tag.Key)] = aws.StringValue(tag.Value) } secret := compose.Secret{ - ID: *response.ARN, - Name: *response.Name, + ID: aws.StringValue(response.ARN), + Name: aws.StringValue(response.Name), Labels: labels, } if response.Description != nil { @@ -431,8 +431,8 @@ func (s sdk) GetLogs(ctx context.Context, name string, consumer compose.LogConsu } for _, event := range events.Events { - p := strings.Split(*event.LogStreamName, "/") - consumer.Log(p[1], p[2], *event.Message) + p := strings.Split(aws.StringValue(event.LogStreamName), "/") + consumer.Log(p[1], p[2], aws.StringValue(event.Message)) startTime = *event.IngestionTime } } @@ -455,7 +455,7 @@ func (s sdk) DescribeServices(ctx context.Context, cluster string, arns []string var name string for _, t := range service.Tags { if *t.Key == compose.ServiceTag { - name = *t.Value + name = aws.StringValue(t.Value) } } if name == "" { @@ -472,10 +472,10 @@ func (s sdk) DescribeServices(ctx context.Context, cluster string, arns []string return nil, err } status = append(status, compose.ServiceStatus{ - ID: *service.ServiceName, + ID: aws.StringValue(service.ServiceName), Name: name, - Replicas: int(*service.RunningCount), - Desired: int(*service.DesiredCount), + Replicas: int(aws.Int64Value(service.RunningCount)), + Desired: int(aws.Int64Value(service.DesiredCount)), LoadBalancers: loadBalancers, }) } @@ -505,8 +505,12 @@ func (s sdk) getURLWithPortMapping(ctx context.Context, targetGroupArns []string return nil, err } filterLB := func(arn *string, lbs []*elbv2.LoadBalancer) *elbv2.LoadBalancer { + if aws.StringValue(arn) == "" { + // load balancer arn is nil/"" + return nil + } for _, lb := range lbs { - if *lb.LoadBalancerArn == *arn { + if aws.StringValue(lb.LoadBalancerArn) == aws.StringValue(arn) { return lb } } @@ -520,10 +524,10 @@ func (s sdk) getURLWithPortMapping(ctx context.Context, targetGroupArns []string continue } loadBalancers = append(loadBalancers, compose.LoadBalancer{ - URL: *lb.DNSName, - TargetPort: int(*tg.Port), - PublishedPort: int(*tg.Port), - Protocol: *tg.Protocol, + URL: aws.StringValue(lb.DNSName), + TargetPort: int(aws.Int64Value(tg.Port)), + PublishedPort: int(aws.Int64Value(tg.Port)), + Protocol: aws.StringValue(tg.Protocol), }) } @@ -556,7 +560,7 @@ func (s sdk) GetPublicIPs(ctx context.Context, interfaces ...string) (map[string publicIPs := map[string]string{} for _, interf := range desc.NetworkInterfaces { if interf.Association != nil { - publicIPs[*interf.NetworkInterfaceId] = *interf.Association.PublicIp + publicIPs[aws.StringValue(interf.NetworkInterfaceId)] = aws.StringValue(interf.Association.PublicIp) } } return publicIPs, nil @@ -581,5 +585,9 @@ func (s sdk) GetLoadBalancerURL(ctx context.Context, arn string) (string, error) if err != nil { return "", err } - return *lbs.LoadBalancers[0].DNSName, nil + dnsName := aws.StringValue(lbs.LoadBalancers[0].DNSName) + if dnsName == "" { + return "", fmt.Errorf("Load balancer %s doesn't have a dns name", aws.StringValue(lbs.LoadBalancers[0].LoadBalancerArn)) + } + return dnsName, nil } From 2ef1e28f1d7da2c0dc3c8005e4110f409ba99bcd Mon Sep 17 00:00:00 2001 From: aiordache <anca.iordache@docker.com> Date: Thu, 23 Jul 2020 14:37:05 +0200 Subject: [PATCH 171/198] Add support for deploy.resources Signed-off-by: aiordache <anca.iordache@docker.com> Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com> --- ecs/pkg/amazon/backend/cloudformation_test.go | 38 +++++++++++++++++++ ecs/pkg/amazon/backend/compatibility.go | 6 +++ ecs/pkg/amazon/backend/convert.go | 28 ++++++++++++-- 3 files changed, 68 insertions(+), 4 deletions(-) diff --git a/ecs/pkg/amazon/backend/cloudformation_test.go b/ecs/pkg/amazon/backend/cloudformation_test.go index a364bfaff..e7efad187 100644 --- a/ecs/pkg/amazon/backend/cloudformation_test.go +++ b/ecs/pkg/amazon/backend/cloudformation_test.go @@ -117,6 +117,44 @@ services: assert.Check(t, s.DesiredCount == 10) } +func TestTaskSizeConvert(t *testing.T) { + template := convertYaml(t, "test", ` +version: "3" +services: + test: + image: nginx + deploy: + resources: + limits: + cpus: '0.5' + memory: 2048M + reservations: + cpus: '0.5' + memory: 2048M +`) + def := template.Resources["TestTaskDefinition"].(*ecs.TaskDefinition) + assert.Equal(t, def.Cpu, "512") + assert.Equal(t, def.Memory, "2048") + + template = convertYaml(t, "test", ` +version: "3" +services: + test: + image: nginx + deploy: + resources: + limits: + cpus: '4' + memory: 8192M + reservations: + cpus: '4' + memory: 8192M +`) + def = template.Resources["TestTaskDefinition"].(*ecs.TaskDefinition) + assert.Equal(t, def.Cpu, "4096") + assert.Equal(t, def.Memory, "8192") +} + func TestLoadBalancerTypeNetwork(t *testing.T) { template := convertYaml(t, "test", ` version: "3" diff --git a/ecs/pkg/amazon/backend/compatibility.go b/ecs/pkg/amazon/backend/compatibility.go index 85a00128a..35d58d836 100644 --- a/ecs/pkg/amazon/backend/compatibility.go +++ b/ecs/pkg/amazon/backend/compatibility.go @@ -16,6 +16,12 @@ var compatibleComposeAttributes = []string{ "services.depends_on", "services.deploy", "services.deploy.replicas", + "services.deploy.resources.limits", + "services.deploy.resources.limits.cpus", + "services.deploy.resources.limits.memory", + "services.deploy.resources.reservations", + "services.deploy.resources.reservations.cpus", + "services.deploy.resources.reservations.memory", "services.entrypoint", "services.environment", "service.image", diff --git a/ecs/pkg/amazon/backend/convert.go b/ecs/pkg/amazon/backend/convert.go index 6fdc0abc3..6bd66255d 100644 --- a/ecs/pkg/amazon/backend/convert.go +++ b/ecs/pkg/amazon/backend/convert.go @@ -21,6 +21,10 @@ func Convert(project *types.Project, service types.ServiceConfig) (*ecs.TaskDefi if err != nil { return nil, err } + _, memReservation, err := toContainerReservation(service) + if err != nil { + return nil, err + } credential := getRepoCredentials(service) // override resolve.conf search directive to also search <project>.local @@ -60,6 +64,7 @@ func Convert(project *types.Project, service types.ServiceConfig) (*ecs.TaskDefi "awslogs-stream-prefix": project.Name, }, }, + MemoryReservation: memReservation, Name: service.Name, PortMappings: toPortMappings(service.Ports), Privileged: service.Privileged, @@ -111,6 +116,8 @@ func toSystemControls(sysctls types.Mapping) []ecs.TaskDefinition_SystemControl return sys } +const Mb = 1024 * 1024 + func toLimits(service types.ServiceConfig) (string, string, error) { // All possible cpu/mem values for Fargate cpuToMem := map[int64][]types.UnitBytes{ @@ -142,9 +149,9 @@ func toLimits(service types.ServiceConfig) (string, string, error) { } for cpu, mem := range cpuToMem { - if v <= cpu*1024*1024 { + if v <= cpu*Mb { for _, m := range mem { - if limits.MemoryBytes <= m*1024*1024 { + if limits.MemoryBytes <= m*Mb { cpuLimit = strconv.FormatInt(cpu, 10) memLimit = strconv.FormatInt(int64(m), 10) return cpuLimit, memLimit, nil @@ -155,6 +162,21 @@ func toLimits(service types.ServiceConfig) (string, string, error) { return "", "", fmt.Errorf("unable to find cpu/mem for the required resources") } +func toContainerReservation(service types.ServiceConfig) (string, int, error) { + cpuReservation := ".0" + memReservation := 0 + + if service.Deploy == nil { + return cpuReservation, memReservation, nil + } + + reservations := service.Deploy.Resources.Reservations + if reservations == nil { + return cpuReservation, memReservation, nil + } + return reservations.NanoCPUs, int(reservations.MemoryBytes / Mb), nil +} + func toRequiresCompatibilities(isolation string) []*string { if isolation == "" { return nil @@ -206,8 +228,6 @@ func toUlimits(ulimits map[string]*types.UlimitsConfig) []ecs.TaskDefinition_Uli return u } -const Mb = 1024 * 1024 - func toLinuxParameters(service types.ServiceConfig) *ecs.TaskDefinition_LinuxParameters { return &ecs.TaskDefinition_LinuxParameters{ Capabilities: toKernelCapabilities(service.CapAdd, service.CapDrop), From ce3ab38e6144169351b68e3527cbe26b83b1b0b2 Mon Sep 17 00:00:00 2001 From: aiordache <anca.iordache@docker.com> Date: Thu, 23 Jul 2020 20:17:41 +0200 Subject: [PATCH 172/198] Add test for convert failure Signed-off-by: aiordache <anca.iordache@docker.com> Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com> --- ecs/pkg/amazon/backend/cloudformation_test.go | 26 ++++++++++++++++--- ecs/pkg/amazon/backend/convert.go | 8 +++--- 2 files changed, 27 insertions(+), 7 deletions(-) diff --git a/ecs/pkg/amazon/backend/cloudformation_test.go b/ecs/pkg/amazon/backend/cloudformation_test.go index e7efad187..c8622a1ac 100644 --- a/ecs/pkg/amazon/backend/cloudformation_test.go +++ b/ecs/pkg/amazon/backend/cloudformation_test.go @@ -154,6 +154,21 @@ services: assert.Equal(t, def.Cpu, "4096") assert.Equal(t, def.Memory, "8192") } +func TestTaskSizeConvertFailure(t *testing.T) { + model := loadConfig(t, "test", ` +version: "3" +services: + test: + image: nginx + deploy: + resources: + limits: + cpus: '0.5' + memory: 2043248M +`) + _, err := Backend{}.Convert(model) + assert.ErrorContains(t, err, "unable to find cpu/mem for the required resources") +} func TestLoadBalancerTypeNetwork(t *testing.T) { template := convertYaml(t, "test", ` @@ -256,6 +271,13 @@ func load(t *testing.T, paths ...string) *types.Project { } func convertYaml(t *testing.T, name string, yaml string) *cloudformation.Template { + model := loadConfig(t, name, yaml) + template, err := Backend{}.Convert(model) + assert.NilError(t, err) + return template +} + +func loadConfig(t *testing.T, name string, yaml string) *types.Project { dict, err := loader.ParseYAML([]byte(yaml)) assert.NilError(t, err) model, err := loader.Load(types.ConfigDetails{ @@ -266,7 +288,5 @@ func convertYaml(t *testing.T, name string, yaml string) *cloudformation.Templat options.Name = "Test" }) assert.NilError(t, err) - template, err := Backend{}.Convert(model) - assert.NilError(t, err) - return template + return model } diff --git a/ecs/pkg/amazon/backend/convert.go b/ecs/pkg/amazon/backend/convert.go index 6bd66255d..828618b45 100644 --- a/ecs/pkg/amazon/backend/convert.go +++ b/ecs/pkg/amazon/backend/convert.go @@ -116,7 +116,7 @@ func toSystemControls(sysctls types.Mapping) []ecs.TaskDefinition_SystemControl return sys } -const Mb = 1024 * 1024 +const MiB = 1024 * 1024 func toLimits(service types.ServiceConfig) (string, string, error) { // All possible cpu/mem values for Fargate @@ -149,9 +149,9 @@ func toLimits(service types.ServiceConfig) (string, string, error) { } for cpu, mem := range cpuToMem { - if v <= cpu*Mb { + if v <= cpu*MiB { for _, m := range mem { - if limits.MemoryBytes <= m*Mb { + if limits.MemoryBytes <= m*MiB { cpuLimit = strconv.FormatInt(cpu, 10) memLimit = strconv.FormatInt(int64(m), 10) return cpuLimit, memLimit, nil @@ -174,7 +174,7 @@ func toContainerReservation(service types.ServiceConfig) (string, int, error) { if reservations == nil { return cpuReservation, memReservation, nil } - return reservations.NanoCPUs, int(reservations.MemoryBytes / Mb), nil + return reservations.NanoCPUs, int(reservations.MemoryBytes / MiB), nil } func toRequiresCompatibilities(isolation string) []*string { From 94671e99e131e9bbd86022d389cff2f6d7902867 Mon Sep 17 00:00:00 2001 From: aiordache <anca.iordache@docker.com> Date: Fri, 24 Jul 2020 09:46:52 +0200 Subject: [PATCH 173/198] improve error message for unsupported resource combination Signed-off-by: aiordache <anca.iordache@docker.com> Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com> --- ecs/pkg/amazon/backend/cloudformation_test.go | 2 +- ecs/pkg/amazon/backend/convert.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/ecs/pkg/amazon/backend/cloudformation_test.go b/ecs/pkg/amazon/backend/cloudformation_test.go index c8622a1ac..679d3fe6e 100644 --- a/ecs/pkg/amazon/backend/cloudformation_test.go +++ b/ecs/pkg/amazon/backend/cloudformation_test.go @@ -167,7 +167,7 @@ services: memory: 2043248M `) _, err := Backend{}.Convert(model) - assert.ErrorContains(t, err, "unable to find cpu/mem for the required resources") + assert.ErrorContains(t, err, "The resources requested are not supported by ECS/Fargate") } func TestLoadBalancerTypeNetwork(t *testing.T) { diff --git a/ecs/pkg/amazon/backend/convert.go b/ecs/pkg/amazon/backend/convert.go index 828618b45..49b9b4454 100644 --- a/ecs/pkg/amazon/backend/convert.go +++ b/ecs/pkg/amazon/backend/convert.go @@ -159,7 +159,7 @@ func toLimits(service types.ServiceConfig) (string, string, error) { } } } - return "", "", fmt.Errorf("unable to find cpu/mem for the required resources") + return "", "", fmt.Errorf("The resources requested are not supported by ECS/Fargate") } func toContainerReservation(service types.ServiceConfig) (string, int, error) { From 716fd1369041bf7480469542bde15020b5b09db3 Mon Sep 17 00:00:00 2001 From: aiordache <anca.iordache@docker.com> Date: Fri, 24 Jul 2020 10:17:48 +0200 Subject: [PATCH 174/198] sort cpu values in conversion to fargate values Signed-off-by: aiordache <anca.iordache@docker.com> Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com> --- ecs/pkg/amazon/backend/cloudformation_test.go | 2 +- ecs/pkg/amazon/backend/convert.go | 12 ++++++++++-- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/ecs/pkg/amazon/backend/cloudformation_test.go b/ecs/pkg/amazon/backend/cloudformation_test.go index 679d3fe6e..0c7edd8f6 100644 --- a/ecs/pkg/amazon/backend/cloudformation_test.go +++ b/ecs/pkg/amazon/backend/cloudformation_test.go @@ -167,7 +167,7 @@ services: memory: 2043248M `) _, err := Backend{}.Convert(model) - assert.ErrorContains(t, err, "The resources requested are not supported by ECS/Fargate") + assert.ErrorContains(t, err, "the resources requested are not supported by ECS/Fargate") } func TestLoadBalancerTypeNetwork(t *testing.T) { diff --git a/ecs/pkg/amazon/backend/convert.go b/ecs/pkg/amazon/backend/convert.go index 49b9b4454..e3591ee70 100644 --- a/ecs/pkg/amazon/backend/convert.go +++ b/ecs/pkg/amazon/backend/convert.go @@ -2,6 +2,7 @@ package backend import ( "fmt" + "sort" "strconv" "strings" "time" @@ -148,7 +149,14 @@ func toLimits(service types.ServiceConfig) (string, string, error) { return "", "", err } - for cpu, mem := range cpuToMem { + var cpus []int64 + for k := range cpuToMem { + cpus = append(cpus, k) + } + sort.Slice(cpus, func(i, j int) bool { return cpus[i] < cpus[j] }) + + for _, cpu := range cpus { + mem := cpuToMem[cpu] if v <= cpu*MiB { for _, m := range mem { if limits.MemoryBytes <= m*MiB { @@ -159,7 +167,7 @@ func toLimits(service types.ServiceConfig) (string, string, error) { } } } - return "", "", fmt.Errorf("The resources requested are not supported by ECS/Fargate") + return "", "", fmt.Errorf("the resources requested are not supported by ECS/Fargate") } func toContainerReservation(service types.ServiceConfig) (string, int, error) { From 1cde94729771cd4188e9fbcf1dba26794cf4cfb4 Mon Sep 17 00:00:00 2001 From: aiordache <anca.iordache@docker.com> Date: Fri, 24 Jul 2020 17:24:26 +0200 Subject: [PATCH 175/198] fix custom vpc setup Signed-off-by: aiordache <anca.iordache@docker.com> Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com> --- ecs/Dockerfile | 2 +- ecs/pkg/amazon/backend/up.go | 4 ++++ ecs/pkg/amazon/sdk/sdk.go | 4 ---- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/ecs/Dockerfile b/ecs/Dockerfile index 185a080e5..e5b506b96 100644 --- a/ecs/Dockerfile +++ b/ecs/Dockerfile @@ -1,6 +1,6 @@ # syntax = docker/dockerfile:experimental ARG GO_VERSION=1.14.4-alpine -ARG ALPINE_PKG_DOCKER_VERSION=19.03.11-r0 +ARG ALPINE_PKG_DOCKER_VERSION=19.03.12-r0 ARG GOLANGCI_LINT_VERSION=v1.27.0-alpine FROM --platform=${BUILDPLATFORM} golang:${GO_VERSION} AS base diff --git a/ecs/pkg/amazon/backend/up.go b/ecs/pkg/amazon/backend/up.go index 3ea21d176..ee871fe50 100644 --- a/ecs/pkg/amazon/backend/up.go +++ b/ecs/pkg/amazon/backend/up.go @@ -40,6 +40,9 @@ func (b *Backend) Up(ctx context.Context, options cli.ProjectOptions) error { if err != nil { return err } + if len(subNets) < 2 { + return fmt.Errorf("VPC %s should have at least 2 associated subnets in different availability zones", vpc) + } lb, err := b.GetLoadBalancer(ctx, project) if err != nil { @@ -95,6 +98,7 @@ func (b Backend) GetVPC(ctx context.Context, project *types.Project) (string, er if !ok { return "", fmt.Errorf("VPC does not exist: %s", vpc) } + return vpcID, nil } defaultVPC, err := b.api.GetDefaultVPC(ctx) if err != nil { diff --git a/ecs/pkg/amazon/sdk/sdk.go b/ecs/pkg/amazon/sdk/sdk.go index 3ccb668c3..7b5af7489 100644 --- a/ecs/pkg/amazon/sdk/sdk.go +++ b/ecs/pkg/amazon/sdk/sdk.go @@ -125,10 +125,6 @@ func (s sdk) GetSubNets(ctx context.Context, vpcID string) ([]string, error) { Name: aws.String("vpc-id"), Values: []*string{aws.String(vpcID)}, }, - { - Name: aws.String("default-for-az"), - Values: []*string{aws.String("true")}, - }, }, }) if err != nil { From f5703310ef07bd681ded961ea2d4789d97981474 Mon Sep 17 00:00:00 2001 From: aiordache <anca.iordache@docker.com> Date: Mon, 27 Jul 2020 09:44:42 +0200 Subject: [PATCH 176/198] Add template description Signed-off-by: aiordache <anca.iordache@docker.com> Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com> --- ecs/pkg/amazon/backend/cloudformation.go | 2 +- .../testdata/simple/simple-cloudformation-conversion.golden | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/ecs/pkg/amazon/backend/cloudformation.go b/ecs/pkg/amazon/backend/cloudformation.go index c07f7766d..2d85a8a78 100644 --- a/ecs/pkg/amazon/backend/cloudformation.go +++ b/ecs/pkg/amazon/backend/cloudformation.go @@ -51,7 +51,7 @@ func (b Backend) Convert(project *types.Project) (*cloudformation.Template, erro } template := cloudformation.NewTemplate() - + template.Description = "CloudFormation template created by Docker for deploying applications on Amazon ECS" template.Parameters[ParameterClusterName] = cloudformation.Parameter{ Type: "String", Description: "Name of the ECS cluster to deploy to (optional)", diff --git a/ecs/pkg/amazon/backend/testdata/simple/simple-cloudformation-conversion.golden b/ecs/pkg/amazon/backend/testdata/simple/simple-cloudformation-conversion.golden index 7e8859050..4db597e4d 100644 --- a/ecs/pkg/amazon/backend/testdata/simple/simple-cloudformation-conversion.golden +++ b/ecs/pkg/amazon/backend/testdata/simple/simple-cloudformation-conversion.golden @@ -18,6 +18,7 @@ ] } }, + "Description": "CloudFormation template created by Docker for deploying applications on Amazon ECS", "Parameters": { "ParameterClusterName": { "Description": "Name of the ECS cluster to deploy to (optional)", From cec3187bbb4a95a23cafe590f7d2a72a3c6bc3ce Mon Sep 17 00:00:00 2001 From: aiordache <anca.iordache@docker.com> Date: Fri, 24 Jul 2020 20:01:17 +0200 Subject: [PATCH 177/198] Set task tags Signed-off-by: aiordache <anca.iordache@docker.com> Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com> --- ecs/pkg/amazon/backend/convert.go | 32 ++++++++++++++----- .../simple-cloudformation-conversion.golden | 14 ++++++++ 2 files changed, 38 insertions(+), 8 deletions(-) diff --git a/ecs/pkg/amazon/backend/convert.go b/ecs/pkg/amazon/backend/convert.go index e3591ee70..4470e116d 100644 --- a/ecs/pkg/amazon/backend/convert.go +++ b/ecs/pkg/amazon/backend/convert.go @@ -37,14 +37,29 @@ func Convert(project *types.Project, service types.ServiceConfig) (*ecs.TaskDefi fmt.Sprintf(" %s.local", project.Name), })) + tags := []tags.Tag{ + { + Key: compose.ProjectTag, + Value: project.Name, + }, + { + Key: compose.ServiceTag, + Value: service.Name, + }, + } + tags = append(tags, toTags(service.Labels)...) + return &ecs.TaskDefinition{ ContainerDefinitions: []ecs.TaskDefinition_ContainerDefinition{ { - Command: service.Command, - DisableNetworking: service.NetworkMode == "none", - DnsSearchDomains: service.DNSSearch, - DnsServers: service.DNS, - DockerLabels: nil, + Command: service.Command, + DisableNetworking: service.NetworkMode == "none", + DnsSearchDomains: service.DNSSearch, + DnsServers: service.DNS, + DockerLabels: map[string]string{ + compose.ProjectTag: project.Name, + compose.ServiceTag: service.Name, + }, DockerSecurityOptions: service.SecurityOpt, EntryPoint: service.Entrypoint, Environment: toKeyValuePair(service.Environment), @@ -78,8 +93,9 @@ func Convert(project *types.Project, service types.ServiceConfig) (*ecs.TaskDefi SystemControls: toSystemControls(service.Sysctls), Ulimits: toUlimits(service.Ulimits), User: service.User, - VolumesFrom: nil, - WorkingDirectory: service.WorkingDir, + + VolumesFrom: nil, + WorkingDirectory: service.WorkingDir, }, }, Cpu: cpu, @@ -91,7 +107,7 @@ func Convert(project *types.Project, service types.ServiceConfig) (*ecs.TaskDefi PlacementConstraints: toPlacementConstraints(service.Deploy), ProxyConfiguration: nil, RequiresCompatibilities: []string{ecsapi.LaunchTypeFargate}, - Tags: toTags(service.Labels), + Tags: tags, }, nil } diff --git a/ecs/pkg/amazon/backend/testdata/simple/simple-cloudformation-conversion.golden b/ecs/pkg/amazon/backend/testdata/simple/simple-cloudformation-conversion.golden index 4db597e4d..2538d4c0b 100644 --- a/ecs/pkg/amazon/backend/testdata/simple/simple-cloudformation-conversion.golden +++ b/ecs/pkg/amazon/backend/testdata/simple/simple-cloudformation-conversion.golden @@ -219,6 +219,10 @@ "Properties": { "ContainerDefinitions": [ { + "DockerLabels": { + "com.docker.compose.project": "TestSimpleConvert", + "com.docker.compose.service": "simple" + }, "Environment": [ { "Name": "LOCALDOMAIN", @@ -270,6 +274,16 @@ "NetworkMode": "awsvpc", "RequiresCompatibilities": [ "FARGATE" + ], + "Tags": [ + { + "Key": "com.docker.compose.project", + "Value": "TestSimpleConvert" + }, + { + "Key": "com.docker.compose.service", + "Value": "simple" + } ] }, "Type": "AWS::ECS::TaskDefinition" From 55531eb6b4cf08b9dc6878cc43174e668d4d3cfa Mon Sep 17 00:00:00 2001 From: aiordache <anca.iordache@docker.com> Date: Mon, 27 Jul 2020 16:23:46 +0200 Subject: [PATCH 178/198] Remove `compose up` timeout Signed-off-by: aiordache <anca.iordache@docker.com> Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com> --- ecs/pkg/amazon/backend/up.go | 12 ++++++++++++ ecs/pkg/amazon/sdk/sdk.go | 2 +- ecs/pkg/compose/types.go | 5 ----- 3 files changed, 13 insertions(+), 6 deletions(-) diff --git a/ecs/pkg/amazon/backend/up.go b/ecs/pkg/amazon/backend/up.go index ee871fe50..d72794164 100644 --- a/ecs/pkg/amazon/backend/up.go +++ b/ecs/pkg/amazon/backend/up.go @@ -3,6 +3,9 @@ package backend import ( "context" "fmt" + "os" + "os/signal" + "syscall" "github.com/compose-spec/compose-go/cli" "github.com/compose-spec/compose-go/types" @@ -84,6 +87,15 @@ func (b *Backend) Up(ctx context.Context, options cli.ProjectOptions) error { for k := range template.Resources { w.ResourceEvent(k, "PENDING", "") } + + signalChan := make(chan os.Signal, 1) + signal.Notify(signalChan, syscall.SIGINT, syscall.SIGTERM) + go func() { + <-signalChan + fmt.Println("user interrupted deployment. Deleting stack...") + b.Down(ctx, options) + }() + return b.WaitStackCompletion(ctx, project.Name, operation, w) } diff --git a/ecs/pkg/amazon/sdk/sdk.go b/ecs/pkg/amazon/sdk/sdk.go index 7b5af7489..2dcd2ca86 100644 --- a/ecs/pkg/amazon/sdk/sdk.go +++ b/ecs/pkg/amazon/sdk/sdk.go @@ -181,7 +181,7 @@ func (s sdk) CreateStack(ctx context.Context, name string, template *cf.Template StackName: aws.String(name), TemplateBody: aws.String(string(json)), Parameters: param, - TimeoutInMinutes: aws.Int64(15), + TimeoutInMinutes: nil, Capabilities: []*string{ aws.String(cloudformation.CapabilityCapabilityIam), }, diff --git a/ecs/pkg/compose/types.go b/ecs/pkg/compose/types.go index 1426d1bea..807b9c7ad 100644 --- a/ecs/pkg/compose/types.go +++ b/ecs/pkg/compose/types.go @@ -9,11 +9,6 @@ type StackResource struct { Status string } -type PortMapping struct { - Source int - Target int -} - type LoadBalancer struct { URL string TargetPort int From 0df99075cc9ce1d985103dcd6a2b9bb1c0b10589 Mon Sep 17 00:00:00 2001 From: David Killmon <killmond+gh@amazon.com> Date: Fri, 31 Jul 2020 11:38:41 -0700 Subject: [PATCH 179/198] add shared config state to session By adding this flag to the session, we force the AWS Go SDK to read the ~/.aws/config file. By default, the Go SDK doesn't read this file which is often not what we or customers expect. Many customers store their assume role based prfoiles in the .aws/config file rather than the .aws/credentials file. (This is what the AWS CLI does, by default - but that's because this parameter is enabled by default in the python SDK). Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com> --- ecs/pkg/amazon/backend/backend.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ecs/pkg/amazon/backend/backend.go b/ecs/pkg/amazon/backend/backend.go index f5bba4ed2..4fd40c2b8 100644 --- a/ecs/pkg/amazon/backend/backend.go +++ b/ecs/pkg/amazon/backend/backend.go @@ -8,7 +8,8 @@ import ( func NewBackend(profile string, region string) (*Backend, error) { sess, err := session.NewSessionWithOptions(session.Options{ - Profile: profile, + Profile: profile, + SharedConfigState: session.SharedConfigEnable, Config: aws.Config{ Region: aws.String(region), }, From 9ac3ce772cbeb9ec572785629d048bbdef2cf179 Mon Sep 17 00:00:00 2001 From: Nicolas De Loof <nicolas.deloof@gmail.com> Date: Mon, 3 Aug 2020 17:12:38 +0200 Subject: [PATCH 180/198] Better diagnostic message for "new ARN format" requirement Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com> --- ecs/pkg/amazon/backend/context.go | 15 ++++++++++++--- ecs/pkg/amazon/backend/up.go | 2 +- ecs/pkg/amazon/sdk/api.go | 2 +- ecs/pkg/amazon/sdk/sdk.go | 7 ++++--- 4 files changed, 18 insertions(+), 8 deletions(-) diff --git a/ecs/pkg/amazon/backend/context.go b/ecs/pkg/amazon/backend/context.go index f6f79f4e1..c1865117d 100644 --- a/ecs/pkg/amazon/backend/context.go +++ b/ecs/pkg/amazon/backend/context.go @@ -2,6 +2,7 @@ package backend import ( "context" + "fmt" "github.com/docker/ecs-plugin/pkg/docker" ) @@ -12,13 +13,21 @@ const ( ) func (b *Backend) CreateContextData(ctx context.Context, params map[string]string) (contextData interface{}, description string, err error) { - err = b.api.CheckRequirements(ctx) + region, ok := params[ContextParamRegion] + if !ok { + return nil, "", fmt.Errorf("%q parameter is required", ContextParamRegion) + } + profile, ok := params[ContextParamProfile] + if !ok { + return nil, "", fmt.Errorf("%q parameter is required", ContextParamProfile) + } + err = b.api.CheckRequirements(ctx, region) if err != nil { return "", "", err } return docker.AwsContext{ - Profile: params[ContextParamProfile], - Region: params[ContextParamRegion], + Profile: profile, + Region: region, }, "Amazon ECS context", nil } diff --git a/ecs/pkg/amazon/backend/up.go b/ecs/pkg/amazon/backend/up.go index d72794164..6593f07b1 100644 --- a/ecs/pkg/amazon/backend/up.go +++ b/ecs/pkg/amazon/backend/up.go @@ -19,7 +19,7 @@ func (b *Backend) Up(ctx context.Context, options cli.ProjectOptions) error { return err } - err = b.api.CheckRequirements(ctx) + err = b.api.CheckRequirements(ctx, b.Region) if err != nil { return err } diff --git a/ecs/pkg/amazon/sdk/api.go b/ecs/pkg/amazon/sdk/api.go index c56df268d..af6dfed45 100644 --- a/ecs/pkg/amazon/sdk/api.go +++ b/ecs/pkg/amazon/sdk/api.go @@ -9,7 +9,7 @@ import ( ) type API interface { - CheckRequirements(ctx context.Context) error + CheckRequirements(ctx context.Context, region string) error GetDefaultVPC(ctx context.Context) (string, error) VpcExists(ctx context.Context, vpcID string) (bool, error) diff --git a/ecs/pkg/amazon/sdk/sdk.go b/ecs/pkg/amazon/sdk/sdk.go index 2dcd2ca86..da40fa524 100644 --- a/ecs/pkg/amazon/sdk/sdk.go +++ b/ecs/pkg/amazon/sdk/sdk.go @@ -2,7 +2,6 @@ package sdk import ( "context" - "errors" "fmt" "strings" "time" @@ -56,7 +55,7 @@ func NewAPI(sess *session.Session) API { } } -func (s sdk) CheckRequirements(ctx context.Context) error { +func (s sdk) CheckRequirements(ctx context.Context, region string) error { settings, err := s.ECS.ListAccountSettingsWithContext(ctx, &ecs.ListAccountSettingsInput{ EffectiveSettings: aws.Bool(true), Name: aws.String("serviceLongArnFormat"), @@ -66,7 +65,9 @@ func (s sdk) CheckRequirements(ctx context.Context) error { } serviceLongArnFormat := settings.Settings[0].Value if *serviceLongArnFormat != "enabled" { - return errors.New("this tool requires the \"new ARN resource ID format\"") + return fmt.Errorf("this tool requires the \"new ARN resource ID format\".\n"+ + "Check https://%s.console.aws.amazon.com/ecs/home#/settings\n"+ + "Learn more: https://aws.amazon.com/blogs/compute/migrating-your-amazon-ecs-deployment-to-the-new-arn-and-resource-id-format-2", region) } return nil } From 1a3c75fa2905390816de12ccf054b33e190879dd Mon Sep 17 00:00:00 2001 From: Nicolas De Loof <nicolas.deloof@gmail.com> Date: Tue, 4 Aug 2020 10:23:21 +0200 Subject: [PATCH 181/198] Update aws go sdk and goformation Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com> --- ecs/go.mod | 5 +++-- ecs/go.sum | 6 ++++++ 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/ecs/go.mod b/ecs/go.mod index 25611f037..aa1145ba2 100644 --- a/ecs/go.mod +++ b/ecs/go.mod @@ -5,8 +5,8 @@ require ( 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 - github.com/aws/aws-sdk-go v1.30.22 - github.com/awslabs/goformation/v4 v4.8.0 + github.com/aws/aws-sdk-go v1.33.18 + github.com/awslabs/goformation/v4 v4.14.0 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 @@ -28,6 +28,7 @@ require ( github.com/gogo/protobuf v1.3.1 // indirect github.com/gorilla/mux v1.7.3 // indirect github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed // indirect + github.com/imdario/mergo v0.3.10 // indirect github.com/jinzhu/gorm v1.9.12 // indirect github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0 // indirect github.com/lib/pq v1.3.0 // indirect diff --git a/ecs/go.sum b/ecs/go.sum index 996e1bb7b..2d32a6692 100644 --- a/ecs/go.sum +++ b/ecs/go.sum @@ -21,8 +21,12 @@ github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRF github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= github.com/aws/aws-sdk-go v1.30.22 h1:wImJ8jQrplgmxaTeUY7FrJFn4te/VtWq+mmmJ1TnWAg= github.com/aws/aws-sdk-go v1.30.22/go.mod h1:5zCpMtNQVjRREroY7sYe8lOMRSxkhG6MZveU8YkpAk0= +github.com/aws/aws-sdk-go v1.33.18 h1:Ccy1SV2SsgJU3rfrD+SOhQ0jvuzfrFuja/oKI86ruPw= +github.com/aws/aws-sdk-go v1.33.18/go.mod h1:5zCpMtNQVjRREroY7sYe8lOMRSxkhG6MZveU8YkpAk0= github.com/awslabs/goformation/v4 v4.8.0 h1:UiUhyokRy3suEqBXTnipvY8klqY3Eyl4GCH17brraEc= github.com/awslabs/goformation/v4 v4.8.0/go.mod h1:GcJULxCJfloT+3pbqCluXftdEK2AD/UqpS3hkaaBntg= +github.com/awslabs/goformation/v4 v4.14.0 h1:E2Pet9eIqA4qzt3dzzzE4YN83V4Kyfbcio0VokBC9TA= +github.com/awslabs/goformation/v4 v4.14.0/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= @@ -160,6 +164,8 @@ 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= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= From 8582cb3928188f7cd36252c9ef3535ef24bcfe21 Mon Sep 17 00:00:00 2001 From: Nicolas De Loof <nicolas.deloof@gmail.com> Date: Thu, 16 Jul 2020 15:25:15 +0200 Subject: [PATCH 182/198] update compose-go and adopt NewProjectOptions and functional parameters Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com> --- ecs/cmd/commands/compose.go | 47 ++++++++++++++++++++++++---------- ecs/cmd/commands/opts.go | 19 +++++++++++++- ecs/go.mod | 2 +- ecs/go.sum | 2 ++ ecs/pkg/amazon/backend/down.go | 6 ++--- ecs/pkg/amazon/backend/list.go | 5 +--- ecs/pkg/amazon/backend/logs.go | 4 +-- ecs/pkg/amazon/backend/up.go | 4 +-- ecs/pkg/compose/api.go | 8 +++--- 9 files changed, 66 insertions(+), 31 deletions(-) diff --git a/ecs/cmd/commands/compose.go b/ecs/cmd/commands/compose.go index 75a9555f8..f4262fd4e 100644 --- a/ecs/cmd/commands/compose.go +++ b/ecs/cmd/commands/compose.go @@ -18,7 +18,7 @@ func ComposeCommand(dockerCli command.Cli) *cobra.Command { cmd := &cobra.Command{ Use: "compose", } - opts := &cli.ProjectOptions{} + opts := &composeOptions{} AddFlags(opts, cmd.Flags()) cmd.AddCommand( @@ -42,12 +42,15 @@ func (o upOptions) LoadBalancerArn() *string { return &o.loadBalancerArn } -func ConvertCommand(dockerCli command.Cli, options *cli.ProjectOptions) *cobra.Command { +func ConvertCommand(dockerCli command.Cli, options *composeOptions) *cobra.Command { cmd := &cobra.Command{ Use: "convert", RunE: WithAwsContext(dockerCli, func(ctx docker.AwsContext, backend *amazon.Backend, args []string) error { - opts := options.WithOsEnv() - project, err := cli.ProjectFromOptions(&opts) + opts, err := options.toProjectOptions() + if err != nil { + return err + } + project, err := cli.ProjectFromOptions(opts) if err != nil { return err } @@ -68,24 +71,32 @@ func ConvertCommand(dockerCli command.Cli, options *cli.ProjectOptions) *cobra.C return cmd } -func UpCommand(dockerCli command.Cli, options *cli.ProjectOptions) *cobra.Command { +func UpCommand(dockerCli command.Cli, options *composeOptions) *cobra.Command { opts := upOptions{} cmd := &cobra.Command{ Use: "up", RunE: WithAwsContext(dockerCli, func(clusteropts docker.AwsContext, backend *amazon.Backend, args []string) error { - return backend.Up(context.Background(), *options) + opts, err := options.toProjectOptions() + if err != nil { + return err + } + return backend.Up(context.Background(), opts) }), } cmd.Flags().StringVar(&opts.loadBalancerArn, "load-balancer", "", "") return cmd } -func PsCommand(dockerCli command.Cli, options *cli.ProjectOptions) *cobra.Command { +func PsCommand(dockerCli command.Cli, options *composeOptions) *cobra.Command { opts := upOptions{} cmd := &cobra.Command{ Use: "ps", - RunE: WithAwsContext(dockerCli, func(ctx docker.AwsContext, backend *amazon.Backend, args []string) error { - status, err := backend.Ps(context.Background(), *options) + RunE: WithAwsContext(dockerCli, func(clusteropts docker.AwsContext, backend *amazon.Backend, args []string) error { + opts, err := options.toProjectOptions() + if err != nil { + return err + } + status, err := backend.Ps(context.Background(), opts) if err != nil { return err } @@ -105,23 +116,31 @@ type downOptions struct { DeleteCluster bool } -func DownCommand(dockerCli command.Cli, projectOpts *cli.ProjectOptions) *cobra.Command { +func DownCommand(dockerCli command.Cli, options *composeOptions) *cobra.Command { opts := downOptions{} cmd := &cobra.Command{ Use: "down", - RunE: WithAwsContext(dockerCli, func(ctx docker.AwsContext, backend *amazon.Backend, args []string) error { - return backend.Down(context.Background(), *projectOpts) + RunE: WithAwsContext(dockerCli, func(clusteropts docker.AwsContext, backend *amazon.Backend, args []string) error { + opts, err := options.toProjectOptions() + if err != nil { + return err + } + return backend.Down(context.Background(), opts) }), } cmd.Flags().BoolVar(&opts.DeleteCluster, "delete-cluster", false, "Delete cluster") return cmd } -func LogsCommand(dockerCli command.Cli, projectOpts *cli.ProjectOptions) *cobra.Command { +func LogsCommand(dockerCli command.Cli, options *composeOptions) *cobra.Command { cmd := &cobra.Command{ Use: "logs [PROJECT NAME]", RunE: WithAwsContext(dockerCli, func(clusteropts docker.AwsContext, backend *amazon.Backend, args []string) error { - return backend.Logs(context.Background(), *projectOpts) + opts, err := options.toProjectOptions() + if err != nil { + return err + } + return backend.Logs(context.Background(), opts) }), } return cmd diff --git a/ecs/cmd/commands/opts.go b/ecs/cmd/commands/opts.go index bb63ec383..2124ca247 100644 --- a/ecs/cmd/commands/opts.go +++ b/ecs/cmd/commands/opts.go @@ -5,7 +5,24 @@ import ( "github.com/spf13/pflag" ) -func AddFlags(o *cli.ProjectOptions, flags *pflag.FlagSet) { +type composeOptions struct { + Name string + WorkingDir string + ConfigPaths []string + Environment []string +} + +func AddFlags(o *composeOptions, flags *pflag.FlagSet) { flags.StringArrayVarP(&o.ConfigPaths, "file", "f", nil, "Specify an alternate compose file") flags.StringVarP(&o.Name, "project-name", "n", "", "Specify an alternate project name (default: directory name)") + flags.StringVarP(&o.WorkingDir, "workdir", "w", "", "Working directory") + flags.StringSliceVarP(&o.Environment, "environment", "e", []string{}, "Environment variables") +} + +func (o *composeOptions) toProjectOptions() (*cli.ProjectOptions, error) { + return cli.NewProjectOptions(o.ConfigPaths, + cli.WithOsEnv, + cli.WithEnv(o.Environment), + cli.WithWorkingDirectory(o.WorkingDir), + cli.WithName(o.Name)) } diff --git a/ecs/go.mod b/ecs/go.mod index aa1145ba2..f3e2949d5 100644 --- a/ecs/go.mod +++ b/ecs/go.mod @@ -14,7 +14,7 @@ require ( 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-20200710075715-6fcc35384ee1 + github.com/compose-spec/compose-go v0.0.0-20200716130117-e87e4f7839e3 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 diff --git a/ecs/go.sum b/ecs/go.sum index 2d32a6692..296309a32 100644 --- a/ecs/go.sum +++ b/ecs/go.sum @@ -66,6 +66,8 @@ github.com/compose-spec/compose-go v0.0.0-20200709084333-492a50989a5a h1:pIiSz5j github.com/compose-spec/compose-go v0.0.0-20200709084333-492a50989a5a/go.mod h1:ArodJ6gsEB7iWKrbV3fSHZ08LlBvSVB0Oqg04fX86t4= github.com/compose-spec/compose-go v0.0.0-20200710075715-6fcc35384ee1 h1:F+YIkKDMHdgZBacawhFY1P9RAIgO+6uv2te6hjsjzF0= github.com/compose-spec/compose-go v0.0.0-20200710075715-6fcc35384ee1/go.mod h1:ArodJ6gsEB7iWKrbV3fSHZ08LlBvSVB0Oqg04fX86t4= +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/containerd/cgroups v0.0.0-20190919134610-bf292b21730f/go.mod h1:OApqhQ4XNSNC13gXIwDjhOQxjWa/NxkwZXJ1EvqT0ko= github.com/containerd/console v0.0.0-20180822173158-c12b1e7919c1/go.mod h1:Tj/on1eG8kiEhd0+fhSDzsPAFESxzBBvdyEgyryXffw= github.com/containerd/containerd v1.3.0-beta.2.0.20190828155532-0293cbd26c69/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= diff --git a/ecs/pkg/amazon/backend/down.go b/ecs/pkg/amazon/backend/down.go index 31df94fca..fe89a9f2c 100644 --- a/ecs/pkg/amazon/backend/down.go +++ b/ecs/pkg/amazon/backend/down.go @@ -8,7 +8,7 @@ import ( "github.com/docker/ecs-plugin/pkg/console" ) -func (b *Backend) Down(ctx context.Context, options cli.ProjectOptions) error { +func (b *Backend) Down(ctx context.Context, options *cli.ProjectOptions) error { name, err := b.projectName(options) if err != nil { return err @@ -27,10 +27,10 @@ func (b *Backend) Down(ctx context.Context, options cli.ProjectOptions) error { return nil } -func (b *Backend) projectName(options cli.ProjectOptions) (string, error) { +func (b *Backend) projectName(options *cli.ProjectOptions) (string, error) { name := options.Name if name == "" { - project, err := cli.ProjectFromOptions(&options) + project, err := cli.ProjectFromOptions(options) if err != nil { return "", err } diff --git a/ecs/pkg/amazon/backend/list.go b/ecs/pkg/amazon/backend/list.go index 165241da9..59150314f 100644 --- a/ecs/pkg/amazon/backend/list.go +++ b/ecs/pkg/amazon/backend/list.go @@ -3,16 +3,13 @@ package backend import ( "context" "fmt" - "regexp" "strings" "github.com/compose-spec/compose-go/cli" "github.com/docker/ecs-plugin/pkg/compose" ) -var targetGroupLogicalName = regexp.MustCompile("(.*)(TCP|UDP)([0-9]+)TargetGroup") - -func (b *Backend) Ps(ctx context.Context, options cli.ProjectOptions) ([]compose.ServiceStatus, error) { +func (b *Backend) Ps(ctx context.Context, options *cli.ProjectOptions) ([]compose.ServiceStatus, error) { projectName, err := b.projectName(options) if err != nil { return nil, err diff --git a/ecs/pkg/amazon/backend/logs.go b/ecs/pkg/amazon/backend/logs.go index 00b8b2d4f..ef5c61e2f 100644 --- a/ecs/pkg/amazon/backend/logs.go +++ b/ecs/pkg/amazon/backend/logs.go @@ -13,10 +13,10 @@ 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) error { name := options.Name if name == "" { - project, err := cli.ProjectFromOptions(&options) + project, err := cli.ProjectFromOptions(options) if err != nil { return err } diff --git a/ecs/pkg/amazon/backend/up.go b/ecs/pkg/amazon/backend/up.go index 6593f07b1..4bbe5bd06 100644 --- a/ecs/pkg/amazon/backend/up.go +++ b/ecs/pkg/amazon/backend/up.go @@ -13,8 +13,8 @@ import ( "github.com/docker/ecs-plugin/pkg/console" ) -func (b *Backend) Up(ctx context.Context, options cli.ProjectOptions) error { - project, err := cli.ProjectFromOptions(&options) +func (b *Backend) Up(ctx context.Context, options *cli.ProjectOptions) error { + project, err := cli.ProjectFromOptions(options) if err != nil { return err } diff --git a/ecs/pkg/compose/api.go b/ecs/pkg/compose/api.go index 66364703e..8e99b8ab6 100644 --- a/ecs/pkg/compose/api.go +++ b/ecs/pkg/compose/api.go @@ -9,14 +9,14 @@ import ( ) type API interface { - Up(ctx context.Context, options cli.ProjectOptions) error - Down(ctx context.Context, options cli.ProjectOptions) error + Up(ctx context.Context, options *cli.ProjectOptions) error + Down(ctx context.Context, options *cli.ProjectOptions) error 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, projectName cli.ProjectOptions) error - Ps(background context.Context, options cli.ProjectOptions) ([]ServiceStatus, error) + Logs(ctx context.Context, options *cli.ProjectOptions) error + Ps(background context.Context, options *cli.ProjectOptions) ([]ServiceStatus, error) CreateSecret(ctx context.Context, secret Secret) (string, error) InspectSecret(ctx context.Context, id string) (Secret, error) From e7bc8081ba755eeecbba8f8dd5f2b1e1b55cb9c9 Mon Sep 17 00:00:00 2001 From: Nicolas De Loof <nicolas.deloof@gmail.com> Date: Tue, 4 Aug 2020 18:20:34 +0200 Subject: [PATCH 183/198] Propagate service tags on Tasks closes #188 Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com> --- ecs/pkg/amazon/backend/cloudformation.go | 1 + ecs/pkg/amazon/backend/convert.go | 30 ++++--------------- .../simple-cloudformation-conversion.golden | 1 + 3 files changed, 8 insertions(+), 24 deletions(-) diff --git a/ecs/pkg/amazon/backend/cloudformation.go b/ecs/pkg/amazon/backend/cloudformation.go index 2d85a8a78..2c6d4bc5f 100644 --- a/ecs/pkg/amazon/backend/cloudformation.go +++ b/ecs/pkg/amazon/backend/cloudformation.go @@ -179,6 +179,7 @@ func (b Backend) Convert(project *types.Project) (*cloudformation.Template, erro }, }, }, + PropagateTags: ecsapi.PropagateTagsService, SchedulingStrategy: ecsapi.SchedulingStrategyReplica, ServiceRegistries: []ecs.Service_ServiceRegistry{serviceRegistry}, Tags: []tags.Tag{ diff --git a/ecs/pkg/amazon/backend/convert.go b/ecs/pkg/amazon/backend/convert.go index 4470e116d..b4ae15ae0 100644 --- a/ecs/pkg/amazon/backend/convert.go +++ b/ecs/pkg/amazon/backend/convert.go @@ -37,29 +37,13 @@ func Convert(project *types.Project, service types.ServiceConfig) (*ecs.TaskDefi fmt.Sprintf(" %s.local", project.Name), })) - tags := []tags.Tag{ - { - Key: compose.ProjectTag, - Value: project.Name, - }, - { - Key: compose.ServiceTag, - Value: service.Name, - }, - } - tags = append(tags, toTags(service.Labels)...) - return &ecs.TaskDefinition{ ContainerDefinitions: []ecs.TaskDefinition_ContainerDefinition{ { - Command: service.Command, - DisableNetworking: service.NetworkMode == "none", - DnsSearchDomains: service.DNSSearch, - DnsServers: service.DNS, - DockerLabels: map[string]string{ - compose.ProjectTag: project.Name, - compose.ServiceTag: service.Name, - }, + Command: service.Command, + DisableNetworking: service.NetworkMode == "none", + DnsSearchDomains: service.DNSSearch, + DnsServers: service.DNS, DockerSecurityOptions: service.SecurityOpt, EntryPoint: service.Entrypoint, Environment: toKeyValuePair(service.Environment), @@ -93,9 +77,8 @@ func Convert(project *types.Project, service types.ServiceConfig) (*ecs.TaskDefi SystemControls: toSystemControls(service.Sysctls), Ulimits: toUlimits(service.Ulimits), User: service.User, - - VolumesFrom: nil, - WorkingDirectory: service.WorkingDir, + VolumesFrom: nil, + WorkingDirectory: service.WorkingDir, }, }, Cpu: cpu, @@ -107,7 +90,6 @@ func Convert(project *types.Project, service types.ServiceConfig) (*ecs.TaskDefi PlacementConstraints: toPlacementConstraints(service.Deploy), ProxyConfiguration: nil, RequiresCompatibilities: []string{ecsapi.LaunchTypeFargate}, - Tags: tags, }, nil } diff --git a/ecs/pkg/amazon/backend/testdata/simple/simple-cloudformation-conversion.golden b/ecs/pkg/amazon/backend/testdata/simple/simple-cloudformation-conversion.golden index 2538d4c0b..c941d5fef 100644 --- a/ecs/pkg/amazon/backend/testdata/simple/simple-cloudformation-conversion.golden +++ b/ecs/pkg/amazon/backend/testdata/simple/simple-cloudformation-conversion.golden @@ -116,6 +116,7 @@ ] } }, + "PropagateTags": "SERVICE", "SchedulingStrategy": "REPLICA", "ServiceRegistries": [ { From 35019564e43bed2c5e84b4221cd9866fc0ffd241 Mon Sep 17 00:00:00 2001 From: Nicolas De Loof <nicolas.deloof@gmail.com> Date: Thu, 6 Aug 2020 10:22:55 +0200 Subject: [PATCH 184/198] Configure Deployment controller Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com> --- ecs/pkg/amazon/backend/cloudformation.go | 12 +++++++++-- .../simple-cloudformation-conversion.golden | 21 +++++++------------ 2 files changed, 17 insertions(+), 16 deletions(-) diff --git a/ecs/pkg/amazon/backend/cloudformation.go b/ecs/pkg/amazon/backend/cloudformation.go index 2c6d4bc5f..74602318d 100644 --- a/ecs/pkg/amazon/backend/cloudformation.go +++ b/ecs/pkg/amazon/backend/cloudformation.go @@ -163,12 +163,20 @@ func (b Backend) Convert(project *types.Project) (*cloudformation.Template, erro for _, dependency := range service.DependsOn { dependsOn = append(dependsOn, serviceResourceName(dependency)) } + template.Resources[serviceResourceName(service.Name)] = &ecs.Service{ AWSCloudFormationDependsOn: dependsOn, Cluster: cluster, DesiredCount: desiredCount, - LaunchType: ecsapi.LaunchTypeFargate, - LoadBalancers: serviceLB, + DeploymentController: &ecs.Service_DeploymentController{ + Type: ecsapi.DeploymentControllerTypeEcs, + }, + DeploymentConfiguration: &ecs.Service_DeploymentConfiguration{ + MaximumPercent: 200, + MinimumHealthyPercent: 100, + }, + LaunchType: ecsapi.LaunchTypeFargate, + LoadBalancers: serviceLB, NetworkConfiguration: &ecs.Service_NetworkConfiguration{ AwsvpcConfiguration: &ecs.Service_AwsVpcConfiguration{ AssignPublicIp: ecsapi.AssignPublicIpEnabled, diff --git a/ecs/pkg/amazon/backend/testdata/simple/simple-cloudformation-conversion.golden b/ecs/pkg/amazon/backend/testdata/simple/simple-cloudformation-conversion.golden index c941d5fef..c61fc6498 100644 --- a/ecs/pkg/amazon/backend/testdata/simple/simple-cloudformation-conversion.golden +++ b/ecs/pkg/amazon/backend/testdata/simple/simple-cloudformation-conversion.golden @@ -87,6 +87,13 @@ } ] }, + "DeploymentConfiguration": { + "MaximumPercent": 200, + "MinimumHealthyPercent": 100 + }, + "DeploymentController": { + "Type": "ECS" + }, "DesiredCount": 1, "LaunchType": "FARGATE", "LoadBalancers": [ @@ -220,10 +227,6 @@ "Properties": { "ContainerDefinitions": [ { - "DockerLabels": { - "com.docker.compose.project": "TestSimpleConvert", - "com.docker.compose.service": "simple" - }, "Environment": [ { "Name": "LOCALDOMAIN", @@ -275,16 +278,6 @@ "NetworkMode": "awsvpc", "RequiresCompatibilities": [ "FARGATE" - ], - "Tags": [ - { - "Key": "com.docker.compose.project", - "Value": "TestSimpleConvert" - }, - { - "Key": "com.docker.compose.service", - "Value": "simple" - } ] }, "Type": "AWS::ECS::TaskDefinition" From 59e8eaf744e08e08f83855284454e393d63e5add Mon Sep 17 00:00:00 2001 From: Nicolas De Loof <nicolas.deloof@gmail.com> Date: Mon, 10 Aug 2020 08:32:52 +0200 Subject: [PATCH 185/198] Don't set targetGroup a name to avoid conflicts Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com> --- ecs/pkg/amazon/backend/cloudformation.go | 1 - 1 file changed, 1 deletion(-) diff --git a/ecs/pkg/amazon/backend/cloudformation.go b/ecs/pkg/amazon/backend/cloudformation.go index 74602318d..54cebb20f 100644 --- a/ecs/pkg/amazon/backend/cloudformation.go +++ b/ecs/pkg/amazon/backend/cloudformation.go @@ -307,7 +307,6 @@ func createTargetGroup(project *types.Project, service types.ServiceConfig, port port.Published, ) template.Resources[targetGroupName] = &elasticloadbalancingv2.TargetGroup{ - Name: targetGroupName, Port: int(port.Target), Protocol: protocol, Tags: []tags.Tag{ From b05af0c0accd89f26506c14571ef5dfd8403e9fd Mon Sep 17 00:00:00 2001 From: Nicolas De Loof <nicolas.deloof@gmail.com> Date: Mon, 10 Aug 2020 08:27:57 +0200 Subject: [PATCH 186/198] Claim support for healthcheck.retries Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com> --- ecs/pkg/amazon/backend/compatibility.go | 1 + 1 file changed, 1 insertion(+) diff --git a/ecs/pkg/amazon/backend/compatibility.go b/ecs/pkg/amazon/backend/compatibility.go index 35d58d836..93806e96c 100644 --- a/ecs/pkg/amazon/backend/compatibility.go +++ b/ecs/pkg/amazon/backend/compatibility.go @@ -28,6 +28,7 @@ var compatibleComposeAttributes = []string{ "services.init", "services.healthcheck", "services.healthcheck.interval", + "services.healthcheck.retries", "services.healthcheck.start_period", "services.healthcheck.test", "services.healthcheck.timeout", From 85b3cbd6ea8f7d6dc88751dd8dbf572d968941bc Mon Sep 17 00:00:00 2001 From: Nicolas De Loof <nicolas.deloof@gmail.com> Date: Tue, 4 Aug 2020 18:04:06 +0200 Subject: [PATCH 187/198] use an initContainer to inject secrets as /run/secrets/xx Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com> --- ecs/cmd/commands/compose.go | 6 +- ecs/go.sum | 14 +- ecs/pkg/amazon/backend/cloudformation.go | 27 ++++ ecs/pkg/amazon/backend/compatibility.go | 6 + ecs/pkg/amazon/backend/convert.go | 157 ++++++++++++++++------ ecs/pkg/amazon/cloudformation/marshall.go | 45 +++++++ ecs/pkg/amazon/sdk/sdk.go | 6 +- ecs/pkg/compose/x.go | 1 + ecs/secrets/Dockerfile | 8 ++ ecs/secrets/main.go | 85 ++++++++++++ 10 files changed, 296 insertions(+), 59 deletions(-) create mode 100644 ecs/pkg/amazon/cloudformation/marshall.go create mode 100644 ecs/secrets/Dockerfile create mode 100644 ecs/secrets/main.go diff --git a/ecs/cmd/commands/compose.go b/ecs/cmd/commands/compose.go index f4262fd4e..f4e6d4e91 100644 --- a/ecs/cmd/commands/compose.go +++ b/ecs/cmd/commands/compose.go @@ -7,6 +7,8 @@ import ( "os" "strings" + "github.com/docker/ecs-plugin/pkg/amazon/cloudformation" + "github.com/compose-spec/compose-go/cli" "github.com/docker/cli/cli/command" amazon "github.com/docker/ecs-plugin/pkg/amazon/backend" @@ -59,11 +61,11 @@ func ConvertCommand(dockerCli command.Cli, options *composeOptions) *cobra.Comma return err } - j, err := template.JSON() + json, err := cloudformation.Marshall(template) if err != nil { fmt.Printf("Failed to generate JSON: %s\n", err) } else { - fmt.Printf("%s\n", string(j)) + fmt.Printf("%s\n", string(json)) } return nil }), diff --git a/ecs/go.sum b/ecs/go.sum index 296309a32..964066997 100644 --- a/ecs/go.sum +++ b/ecs/go.sum @@ -19,12 +19,8 @@ github.com/akavel/rsrc v0.8.0/go.mod h1:uLoCtb9J+EyAqh+26kdrTgmzRBFPGOolLWKpdxkK github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= -github.com/aws/aws-sdk-go v1.30.22 h1:wImJ8jQrplgmxaTeUY7FrJFn4te/VtWq+mmmJ1TnWAg= -github.com/aws/aws-sdk-go v1.30.22/go.mod h1:5zCpMtNQVjRREroY7sYe8lOMRSxkhG6MZveU8YkpAk0= github.com/aws/aws-sdk-go v1.33.18 h1:Ccy1SV2SsgJU3rfrD+SOhQ0jvuzfrFuja/oKI86ruPw= github.com/aws/aws-sdk-go v1.33.18/go.mod h1:5zCpMtNQVjRREroY7sYe8lOMRSxkhG6MZveU8YkpAk0= -github.com/awslabs/goformation/v4 v4.8.0 h1:UiUhyokRy3suEqBXTnipvY8klqY3Eyl4GCH17brraEc= -github.com/awslabs/goformation/v4 v4.8.0/go.mod h1:GcJULxCJfloT+3pbqCluXftdEK2AD/UqpS3hkaaBntg= github.com/awslabs/goformation/v4 v4.14.0 h1:E2Pet9eIqA4qzt3dzzzE4YN83V4Kyfbcio0VokBC9TA= github.com/awslabs/goformation/v4 v4.14.0/go.mod h1:GcJULxCJfloT+3pbqCluXftdEK2AD/UqpS3hkaaBntg= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= @@ -58,14 +54,6 @@ 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-20200624120600-614475470cd8 h1:sVvKsoXizFOuJNc8dM91IeET2/zDNFj3hwHgk437iJ8= -github.com/compose-spec/compose-go v0.0.0-20200624120600-614475470cd8/go.mod h1:ih9anT8po+49hrb+1j3ldIJ/YRAaBH52ErlQLTKE2Yo= -github.com/compose-spec/compose-go v0.0.0-20200707124823-710ff8e60ad9 h1:WkFqc6UpRqxROso9KC+ceaTiXx/VWpeO1x+NV0d4d+o= -github.com/compose-spec/compose-go v0.0.0-20200707124823-710ff8e60ad9/go.mod h1:ArodJ6gsEB7iWKrbV3fSHZ08LlBvSVB0Oqg04fX86t4= -github.com/compose-spec/compose-go v0.0.0-20200709084333-492a50989a5a h1:pIiSz5jML7rQ1aupg/KHlTqCxhyXvIgeDMf4kDTzIg8= -github.com/compose-spec/compose-go v0.0.0-20200709084333-492a50989a5a/go.mod h1:ArodJ6gsEB7iWKrbV3fSHZ08LlBvSVB0Oqg04fX86t4= -github.com/compose-spec/compose-go v0.0.0-20200710075715-6fcc35384ee1 h1:F+YIkKDMHdgZBacawhFY1P9RAIgO+6uv2te6hjsjzF0= -github.com/compose-spec/compose-go v0.0.0-20200710075715-6fcc35384ee1/go.mod h1:ArodJ6gsEB7iWKrbV3fSHZ08LlBvSVB0Oqg04fX86t4= 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/containerd/cgroups v0.0.0-20190919134610-bf292b21730f/go.mod h1:OApqhQ4XNSNC13gXIwDjhOQxjWa/NxkwZXJ1EvqT0ko= @@ -80,6 +68,7 @@ github.com/containerd/fifo v0.0.0-20190226154929-a9fb20d87448/go.mod h1:ODA38xgv github.com/containerd/go-runc v0.0.0-20180907222934-5a6d9f37cfa3/go.mod h1:IV7qH3hrUgRmyYrtgEeGWJfWbgcHL9CSRruz2Vqcph0= github.com/containerd/ttrpc v0.0.0-20190828154514-0e0f228740de/go.mod h1:PvCDdDGpgqzQIzDW1TphrGLssLDZp2GuS+X5DkEJB8o= github.com/containerd/typeurl v0.0.0-20180627222232-a93fcdb778cd/go.mod h1:Cm3kwCdlkCfMSHURc+r6fwoGH6/F1hH3S4sg0rLFWPc= +github.com/coreos/etcd v3.3.10+incompatible h1:jFneRYjIvLMLhDLCzuTuU4rSJUjRplcJQ7pD7MnhC04= github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk= github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= @@ -475,6 +464,7 @@ gotest.tools/v3 v3.0.2 h1:kG1BFyqVHuQoVQiR1bWGnfz/fmHvvuiSPIV7rvl360E= gotest.tools/v3 v3.0.2/go.mod h1:3SzNCllyD9/Y+b5r9JIKQ474KzkZyqLqEfYqMsX94Bk= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +k8s.io/kubernetes v1.13.0 h1:qTfB+u5M92k2fCCCVP2iuhgwwSOv1EkAkvQY1tQODD8= k8s.io/kubernetes v1.13.0/go.mod h1:ocZa8+6APFNC2tX1DZASIbocyYT5jHzqFVsY5aoB7Jk= vbom.ml/util v0.0.0-20180919145318-efcd4e0f9787 h1:O69FD9pJA4WUZlEwYatBEEkRWKQ5cKodWpdKTrCS/iQ= vbom.ml/util v0.0.0-20180919145318-efcd4e0f9787/go.mod h1:so/NYdZXCz+E3ZpW0uAoCj6uzU2+8OWDFv/HxUSs7kI= diff --git a/ecs/pkg/amazon/backend/cloudformation.go b/ecs/pkg/amazon/backend/cloudformation.go index 54cebb20f..a47e1ad6c 100644 --- a/ecs/pkg/amazon/backend/cloudformation.go +++ b/ecs/pkg/amazon/backend/cloudformation.go @@ -2,9 +2,12 @@ package backend import ( "fmt" + "io/ioutil" "regexp" "strings" + "github.com/awslabs/goformation/v4/cloudformation/secretsmanager" + ecsapi "github.com/aws/aws-sdk-go/service/ecs" "github.com/aws/aws-sdk-go/service/elbv2" cloudmapapi "github.com/aws/aws-sdk-go/service/servicediscovery" @@ -93,6 +96,30 @@ func (b Backend) Convert(project *types.Project) (*cloudformation.Template, erro networks[net.Name] = convertNetwork(project, net, cloudformation.Ref(ParameterVPCId), template) } + for i, s := range project.Secrets { + if s.External.External { + continue + } + secret, err := ioutil.ReadFile(s.File) + if err != nil { + return nil, err + } + + name := fmt.Sprintf("%sSecret", normalizeResourceName(s.Name)) + template.Resources[name] = &secretsmanager.Secret{ + Description: "", + SecretString: string(secret), + Tags: []tags.Tag{ + { + Key: compose.ProjectTag, + Value: project.Name, + }, + }, + } + s.Name = cloudformation.Ref(name) + project.Secrets[i] = s + } + logGroup := fmt.Sprintf("/docker-compose/%s", project.Name) template.Resources["LogGroup"] = &logs.LogGroup{ LogGroupName: logGroup, diff --git a/ecs/pkg/amazon/backend/compatibility.go b/ecs/pkg/amazon/backend/compatibility.go index 93806e96c..ebf915889 100644 --- a/ecs/pkg/amazon/backend/compatibility.go +++ b/ecs/pkg/amazon/backend/compatibility.go @@ -37,8 +37,14 @@ var compatibleComposeAttributes = []string{ "services.ports.mode", "services.ports.target", "services.ports.protocol", + "services.secrets", + "services.secrets.source", + "services.secrets.target", "services.user", "services.working_dir", + "secrets.external", + "secrets.name", + "secrets.file", } func (c *FargateCompatibilityChecker) CheckImage(service *types.ServiceConfig) { diff --git a/ecs/pkg/amazon/backend/convert.go b/ecs/pkg/amazon/backend/convert.go index b4ae15ae0..e024bde26 100644 --- a/ecs/pkg/amazon/backend/convert.go +++ b/ecs/pkg/amazon/backend/convert.go @@ -17,6 +17,8 @@ import ( "github.com/docker/ecs-plugin/pkg/compose" ) +const secretsInitContainerImage = "docker/ecs-secrets-sidecar" + func Convert(project *types.Project, service types.ServiceConfig) (*ecs.TaskDefinition, error) { cpu, mem, err := toLimits(service) if err != nil { @@ -37,50 +39,118 @@ func Convert(project *types.Project, service types.ServiceConfig) (*ecs.TaskDefi fmt.Sprintf(" %s.local", project.Name), })) - return &ecs.TaskDefinition{ - ContainerDefinitions: []ecs.TaskDefinition_ContainerDefinition{ - { - Command: service.Command, - DisableNetworking: service.NetworkMode == "none", - DnsSearchDomains: service.DNSSearch, - DnsServers: service.DNS, - DockerSecurityOptions: service.SecurityOpt, - EntryPoint: service.Entrypoint, - Environment: toKeyValuePair(service.Environment), - Essential: true, - ExtraHosts: toHostEntryPtr(service.ExtraHosts), - FirelensConfiguration: nil, - HealthCheck: toHealthCheck(service.HealthCheck), - Hostname: service.Hostname, - Image: service.Image, - Interactive: false, - Links: nil, - LinuxParameters: toLinuxParameters(service), - LogConfiguration: &ecs.TaskDefinition_LogConfiguration{ - LogDriver: ecsapi.LogDriverAwslogs, - Options: map[string]string{ - "awslogs-region": cloudformation.Ref("AWS::Region"), - "awslogs-group": cloudformation.Ref("LogGroup"), - "awslogs-stream-prefix": project.Name, - }, - }, - MemoryReservation: memReservation, - Name: service.Name, - PortMappings: toPortMappings(service.Ports), - Privileged: service.Privileged, - PseudoTerminal: service.Tty, - ReadonlyRootFilesystem: service.ReadOnly, - RepositoryCredentials: credential, - ResourceRequirements: nil, - StartTimeout: 0, - StopTimeout: durationToInt(service.StopGracePeriod), - SystemControls: toSystemControls(service.Sysctls), - Ulimits: toUlimits(service.Ulimits), - User: service.User, - VolumesFrom: nil, - WorkingDirectory: service.WorkingDir, - }, + logConfiguration := &ecs.TaskDefinition_LogConfiguration{ + LogDriver: ecsapi.LogDriverAwslogs, + Options: map[string]string{ + "awslogs-region": cloudformation.Ref("AWS::Region"), + "awslogs-group": cloudformation.Ref("LogGroup"), + "awslogs-stream-prefix": project.Name, }, + } + + var ( + containers []ecs.TaskDefinition_ContainerDefinition + volumes []ecs.TaskDefinition_Volume + mounts []ecs.TaskDefinition_MountPoint + initContainers []ecs.TaskDefinition_ContainerDependency + ) + if len(service.Secrets) > 0 { + volumes = append(volumes, ecs.TaskDefinition_Volume{ + Name: "secrets", + }) + mounts = append(mounts, ecs.TaskDefinition_MountPoint{ + ContainerPath: "/run/secrets/", + ReadOnly: true, + SourceVolume: "secrets", + }) + initContainers = append(initContainers, ecs.TaskDefinition_ContainerDependency{ + Condition: ecsapi.ContainerConditionSuccess, + ContainerName: "Secrets_InitContainer", + }) + + var ( + names []string + secrets []ecs.TaskDefinition_Secret + ) + for _, s := range service.Secrets { + secretConfig := project.Secrets[s.Source] + if s.Target == "" { + s.Target = s.Source + } + secrets = append(secrets, ecs.TaskDefinition_Secret{ + Name: s.Target, + ValueFrom: secretConfig.Name, + }) + name := s.Target + if ext, ok := secretConfig.Extensions[compose.ExtensionKeys]; ok { + var keys []string + if key, ok := ext.(string); ok { + keys = append(keys, key) + } else { + for _, k := range ext.([]interface{}) { + keys = append(keys, k.(string)) + } + } + name = fmt.Sprintf("%s:%s", s.Target, strings.Join(keys, ",")) + } + names = append(names, name) + } + containers = append(containers, ecs.TaskDefinition_ContainerDefinition{ + Name: fmt.Sprintf("%s_Secrets_InitContainer", normalizeResourceName(service.Name)), + Image: secretsInitContainerImage, + Command: names, + Essential: false, // FIXME this will be ignored, see https://github.com/awslabs/goformation/issues/61#issuecomment-625139607 + LogConfiguration: logConfiguration, + MountPoints: []ecs.TaskDefinition_MountPoint{ + { + ContainerPath: "/run/secrets/", + ReadOnly: false, + SourceVolume: "secrets", + }, + }, + Secrets: secrets, + }) + } + + containers = append(containers, ecs.TaskDefinition_ContainerDefinition{ + Command: service.Command, + DisableNetworking: service.NetworkMode == "none", + DependsOnProp: initContainers, + DnsSearchDomains: service.DNSSearch, + DnsServers: service.DNS, + DockerSecurityOptions: service.SecurityOpt, + EntryPoint: service.Entrypoint, + Environment: toKeyValuePair(service.Environment), + Essential: true, + ExtraHosts: toHostEntryPtr(service.ExtraHosts), + FirelensConfiguration: nil, + HealthCheck: toHealthCheck(service.HealthCheck), + Hostname: service.Hostname, + Image: service.Image, + Interactive: false, + Links: nil, + LinuxParameters: toLinuxParameters(service), + LogConfiguration: logConfiguration, + MemoryReservation: memReservation, + MountPoints: mounts, + Name: service.Name, + PortMappings: toPortMappings(service.Ports), + Privileged: service.Privileged, + PseudoTerminal: service.Tty, + ReadonlyRootFilesystem: service.ReadOnly, + RepositoryCredentials: credential, + ResourceRequirements: nil, + StartTimeout: 0, + StopTimeout: durationToInt(service.StopGracePeriod), + SystemControls: toSystemControls(service.Sysctls), + Ulimits: toUlimits(service.Ulimits), + User: service.User, + VolumesFrom: nil, + WorkingDirectory: service.WorkingDir, + }) + + return &ecs.TaskDefinition{ + ContainerDefinitions: containers, Cpu: cpu, Family: fmt.Sprintf("%s-%s", project.Name, service.Name), IpcMode: service.Ipc, @@ -90,6 +160,7 @@ func Convert(project *types.Project, service types.ServiceConfig) (*ecs.TaskDefi PlacementConstraints: toPlacementConstraints(service.Deploy), ProxyConfiguration: nil, RequiresCompatibilities: []string{ecsapi.LaunchTypeFargate}, + Volumes: volumes, }, nil } diff --git a/ecs/pkg/amazon/cloudformation/marshall.go b/ecs/pkg/amazon/cloudformation/marshall.go new file mode 100644 index 000000000..034bf8097 --- /dev/null +++ b/ecs/pkg/amazon/cloudformation/marshall.go @@ -0,0 +1,45 @@ +package cloudformation + +import ( + "encoding/json" + "fmt" + "strings" + + "github.com/awslabs/goformation/v4/cloudformation" +) + +func Marshall(template *cloudformation.Template) ([]byte, error) { + raw, err := template.JSON() + if err != nil { + return nil, err + } + + var unmarshalled interface{} + if err := json.Unmarshal(raw, &unmarshalled); err != nil { + return nil, fmt.Errorf("invalid JSON: %s", err) + } + + if input, ok := unmarshalled.(map[string]interface{}); ok { + if resources, ok := input["Resources"]; ok { + for _, uresource := range resources.(map[string]interface{}) { + if resource, ok := uresource.(map[string]interface{}); ok { + if resource["Type"] == "AWS::ECS::TaskDefinition" { + properties := resource["Properties"].(map[string]interface{}) + for _, def := range properties["ContainerDefinitions"].([]interface{}) { + containerDefinition := def.(map[string]interface{}) + if strings.HasSuffix(containerDefinition["Name"].(string), "_InitContainer") { + containerDefinition["Essential"] = "false" + } + } + } + } + } + } + } + + raw, err = json.MarshalIndent(unmarshalled, "", " ") + if err != nil { + return nil, fmt.Errorf("invalid JSON: %s", err) + } + return raw, err +} diff --git a/ecs/pkg/amazon/sdk/sdk.go b/ecs/pkg/amazon/sdk/sdk.go index da40fa524..057e9fffb 100644 --- a/ecs/pkg/amazon/sdk/sdk.go +++ b/ecs/pkg/amazon/sdk/sdk.go @@ -6,6 +6,8 @@ import ( "strings" "time" + cloudformation2 "github.com/docker/ecs-plugin/pkg/amazon/cloudformation" + "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws/request" "github.com/aws/aws-sdk-go/aws/session" @@ -164,7 +166,7 @@ func (s sdk) StackExists(ctx context.Context, name string) (bool, error) { func (s sdk) CreateStack(ctx context.Context, name string, template *cf.Template, parameters map[string]string) error { logrus.Debug("Create CloudFormation stack") - json, err := template.JSON() + json, err := cloudformation2.Marshall(template) if err != nil { return err } @@ -192,7 +194,7 @@ func (s sdk) CreateStack(ctx context.Context, name string, template *cf.Template func (s sdk) CreateChangeSet(ctx context.Context, name string, template *cf.Template, parameters map[string]string) (string, error) { logrus.Debug("Create CloudFormation Changeset") - json, err := template.JSON() + json, err := cloudformation2.Marshall(template) if err != nil { return "", err } diff --git a/ecs/pkg/compose/x.go b/ecs/pkg/compose/x.go index 8e52a368f..777987afa 100644 --- a/ecs/pkg/compose/x.go +++ b/ecs/pkg/compose/x.go @@ -6,4 +6,5 @@ const ( ExtensionPullCredentials = "x-aws-pull_credentials" ExtensionLB = "x-aws-loadbalancer" ExtensionCluster = "x-aws-cluster" + ExtensionKeys = "x-aws-keys" ) diff --git a/ecs/secrets/Dockerfile b/ecs/secrets/Dockerfile new file mode 100644 index 000000000..7395d1250 --- /dev/null +++ b/ecs/secrets/Dockerfile @@ -0,0 +1,8 @@ +FROM golang:1.14.4-alpine AS builder +WORKDIR $GOPATH/src/github.com/docker/ecs-secrets +COPY . . +RUN GOOS=linux GOARCH=amd64 go build -ldflags="-w -s" -o /go/bin/secrets + +FROM scratch +COPY --from=builder /go/bin/secrets /secrets +ENTRYPOINT ["/secrets"] diff --git a/ecs/secrets/main.go b/ecs/secrets/main.go new file mode 100644 index 000000000..cc37325a5 --- /dev/null +++ b/ecs/secrets/main.go @@ -0,0 +1,85 @@ +package main + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "os" + "path/filepath" + "strings" +) + +// return codes: +// 1: failed to read secret from env +// 2: failed to parse hierarchical secret +// 3: failed to write secret content into file +func main() { + for _, name := range os.Args[1:] { + i := strings.Index(name, ":") + var keys []string + if i > 0 { + keys = strings.Split(name[i+1:], ",") + name = name[:i] + } + value, ok := os.LookupEnv(name) + if !ok { + fmt.Fprintf(os.Stderr, "%q variable not set", name) + os.Exit(1) + } + + secrets := filepath.Join("/run/secrets", name) + + if len(keys) == 0 { + // raw secret + fmt.Printf("inject secret %q info %s\n", name, secrets) + err := ioutil.WriteFile(secrets, []byte(value), 0444) + if err != nil { + fmt.Fprintf(os.Stderr, err.Error()) + os.Exit(3) + } + os.Exit(0) + } + + var unmarshalled interface{} + err := json.Unmarshal([]byte(value), &unmarshalled) + if err == nil { + if dict, ok := unmarshalled.(map[string]interface{}); ok { + os.MkdirAll(secrets, 0555) + for k, v := range dict { + if !contains(keys, k) && !contains(keys, "*") { + continue + } + path := filepath.Join(secrets, k) + fmt.Printf("inject secret %q info %s\n", k, path) + + var raw []byte + if s, ok := v.(string); ok { + raw = []byte(s) + } else { + raw, err = json.Marshal(v) + if err != nil { + fmt.Fprintf(os.Stderr, err.Error()) + os.Exit(2) + } + } + + err = ioutil.WriteFile(path, raw, 0444) + if err != nil { + fmt.Fprintf(os.Stderr, err.Error()) + os.Exit(3) + } + } + os.Exit(0) + } + } + } +} + +func contains(keys []string, s string) bool { + for _, k := range keys { + if k == s { + return true + } + } + return false +} From 4bfab35007abee926201cb4791546f245bb194c4 Mon Sep 17 00:00:00 2001 From: Nicolas De Loof <nicolas.deloof@gmail.com> Date: Mon, 10 Aug 2020 16:15:46 +0200 Subject: [PATCH 188/198] TestCase for the secrets init container Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com> --- ecs/cmd/commands/compose.go | 9 +- ecs/go.mod | 1 + ecs/pkg/amazon/backend/cloudformation.go | 3 +- ecs/secrets/main.go | 149 ++++++++++++++--------- ecs/secrets/main_test.go | 105 ++++++++++++++++ 5 files changed, 203 insertions(+), 64 deletions(-) create mode 100644 ecs/secrets/main_test.go diff --git a/ecs/cmd/commands/compose.go b/ecs/cmd/commands/compose.go index f4e6d4e91..d0f1a224c 100644 --- a/ecs/cmd/commands/compose.go +++ b/ecs/cmd/commands/compose.go @@ -7,11 +7,10 @@ import ( "os" "strings" - "github.com/docker/ecs-plugin/pkg/amazon/cloudformation" - "github.com/compose-spec/compose-go/cli" "github.com/docker/cli/cli/command" 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/spf13/cobra" ) @@ -60,13 +59,11 @@ func ConvertCommand(dockerCli command.Cli, options *composeOptions) *cobra.Comma if err != nil { return err } - json, err := cloudformation.Marshall(template) if err != nil { - fmt.Printf("Failed to generate JSON: %s\n", err) - } else { - fmt.Printf("%s\n", string(json)) + return err } + fmt.Printf("%s\n", string(json)) return nil }), } diff --git a/ecs/go.mod b/ecs/go.mod index f3e2949d5..c7f1ad67a 100644 --- a/ecs/go.mod +++ b/ecs/go.mod @@ -39,6 +39,7 @@ require ( github.com/morikuni/aec v1.0.0 // indirect 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 diff --git a/ecs/pkg/amazon/backend/cloudformation.go b/ecs/pkg/amazon/backend/cloudformation.go index a47e1ad6c..e54108588 100644 --- a/ecs/pkg/amazon/backend/cloudformation.go +++ b/ecs/pkg/amazon/backend/cloudformation.go @@ -6,8 +6,6 @@ import ( "regexp" "strings" - "github.com/awslabs/goformation/v4/cloudformation/secretsmanager" - ecsapi "github.com/aws/aws-sdk-go/service/ecs" "github.com/aws/aws-sdk-go/service/elbv2" cloudmapapi "github.com/aws/aws-sdk-go/service/servicediscovery" @@ -17,6 +15,7 @@ import ( "github.com/awslabs/goformation/v4/cloudformation/elasticloadbalancingv2" "github.com/awslabs/goformation/v4/cloudformation/iam" "github.com/awslabs/goformation/v4/cloudformation/logs" + "github.com/awslabs/goformation/v4/cloudformation/secretsmanager" cloudmap "github.com/awslabs/goformation/v4/cloudformation/servicediscovery" "github.com/awslabs/goformation/v4/cloudformation/tags" "github.com/compose-spec/compose-go/compatibility" diff --git a/ecs/secrets/main.go b/ecs/secrets/main.go index cc37325a5..eeaa992e6 100644 --- a/ecs/secrets/main.go +++ b/ecs/secrets/main.go @@ -7,72 +7,109 @@ import ( "os" "path/filepath" "strings" + + "github.com/pkg/errors" ) -// return codes: -// 1: failed to read secret from env -// 2: failed to parse hierarchical secret -// 3: failed to write secret content into file +type secret struct { + name string + keys []string +} + +const secretsFolder = "/run/secrets" + func main() { - for _, name := range os.Args[1:] { + secrets := parseInput(os.Args[1:]) + + for _, secret := range secrets { + err := createSecretFiles(secret, secretsFolder) + if err != nil { + fmt.Fprintf(os.Stderr, err.Error()) + os.Exit(1) + } + } +} + +func createSecretFiles(secret secret, path string) error { + value, ok := os.LookupEnv(secret.name) + if !ok { + return fmt.Errorf("%q variable not set", secret.name) + } + + secrets := filepath.Join(path, secret.name) + + if len(secret.keys) == 0 { + // raw secret + fmt.Printf("inject secret %q info %s\n", secret.name, secrets) + return ioutil.WriteFile(secrets, []byte(value), 0444) + } + + var unmarshalled interface{} + err := json.Unmarshal([]byte(value), &unmarshalled) + if err != nil { + return errors.Wrapf(err, "%q secret is not a valid JSON document", secret.name) + } + + dict, ok := unmarshalled.(map[string]interface{}) + if !ok { + return errors.Wrapf(err, "%q secret is not a JSON dictionary", secret.name) + } + err = os.MkdirAll(secrets, 0755) + if err != nil { + return err + } + + if contains(secret.keys, "*") { + var keys []string + for k := range dict { + keys = append(keys, k) + } + secret.keys = keys + } + + for _, k := range secret.keys { + path := filepath.Join(secrets, k) + fmt.Printf("inject secret %q info %s\n", k, path) + + v, ok := dict[k] + if !ok { + return fmt.Errorf("%q secret has no %q key", secret.name, k) + } + + var raw []byte + if s, ok := v.(string); ok { + raw = []byte(s) + } else { + raw, err = json.Marshal(v) + if err != nil { + return err + } + } + + err = ioutil.WriteFile(path, raw, 0444) + if err != nil { + return err + } + } + return nil +} + +// parseInput parse secret to be dumped into secret files with syntax `VARIABLE_NAME[:COMA_SEPARATED_KEYS]` +func parseInput(input []string) []secret { + var secrets []secret + for _, name := range input { i := strings.Index(name, ":") var keys []string if i > 0 { keys = strings.Split(name[i+1:], ",") name = name[:i] } - value, ok := os.LookupEnv(name) - if !ok { - fmt.Fprintf(os.Stderr, "%q variable not set", name) - os.Exit(1) - } - - secrets := filepath.Join("/run/secrets", name) - - if len(keys) == 0 { - // raw secret - fmt.Printf("inject secret %q info %s\n", name, secrets) - err := ioutil.WriteFile(secrets, []byte(value), 0444) - if err != nil { - fmt.Fprintf(os.Stderr, err.Error()) - os.Exit(3) - } - os.Exit(0) - } - - var unmarshalled interface{} - err := json.Unmarshal([]byte(value), &unmarshalled) - if err == nil { - if dict, ok := unmarshalled.(map[string]interface{}); ok { - os.MkdirAll(secrets, 0555) - for k, v := range dict { - if !contains(keys, k) && !contains(keys, "*") { - continue - } - path := filepath.Join(secrets, k) - fmt.Printf("inject secret %q info %s\n", k, path) - - var raw []byte - if s, ok := v.(string); ok { - raw = []byte(s) - } else { - raw, err = json.Marshal(v) - if err != nil { - fmt.Fprintf(os.Stderr, err.Error()) - os.Exit(2) - } - } - - err = ioutil.WriteFile(path, raw, 0444) - if err != nil { - fmt.Fprintf(os.Stderr, err.Error()) - os.Exit(3) - } - } - os.Exit(0) - } - } + secrets = append(secrets, secret{ + name: name, + keys: keys, + }) } + return secrets } func contains(keys []string, s string) bool { diff --git a/ecs/secrets/main_test.go b/ecs/secrets/main_test.go new file mode 100644 index 000000000..ff6f630f1 --- /dev/null +++ b/ecs/secrets/main_test.go @@ -0,0 +1,105 @@ +package main + +import ( + "io/ioutil" + "os" + "path/filepath" + "testing" + + "gotest.tools/v3/assert" + "gotest.tools/v3/fs" +) + +func TestParseSecrets(t *testing.T) { + secrets := parseInput([]string{ + "foo", + "bar:*", + "zot:key0,key1", + }) + assert.Check(t, len(secrets) == 3) + assert.Check(t, secrets[0].name == "foo") + assert.Check(t, secrets[0].keys == nil) + + assert.Check(t, secrets[1].name == "bar") + assert.Check(t, len(secrets[1].keys) == 1) + assert.Check(t, secrets[1].keys[0] == "*") + + assert.Check(t, secrets[2].name == "zot") + assert.Check(t, len(secrets[2].keys) == 2) + assert.Check(t, secrets[2].keys[0] == "key0") + assert.Check(t, secrets[2].keys[1] == "key1") +} + +func TestRawSecret(t *testing.T) { + dir := fs.NewDir(t, "secrets").Path() + os.Setenv("raw", "something_secret") + defer os.Unsetenv("raw") + + err := createSecretFiles(secret{ + name: "raw", + keys: nil, + }, dir) + assert.NilError(t, err) + file, err := ioutil.ReadFile(filepath.Join(dir, "raw")) + assert.NilError(t, err) + content := string(file) + assert.Equal(t, content, "something_secret") +} + +func TestSelectedKeysSecret(t *testing.T) { + dir := fs.NewDir(t, "secrets").Path() + os.Setenv("json", ` +{ + "foo": "bar", + "zot": "qix" +}`) + defer os.Unsetenv("json") + + err := createSecretFiles(secret{ + name: "json", + keys: []string{"foo"}, + }, dir) + assert.NilError(t, err) + file, err := ioutil.ReadFile(filepath.Join(dir, "json", "foo")) + assert.NilError(t, err) + content := string(file) + assert.Equal(t, content, "bar") + + _, err = os.Stat(filepath.Join(dir, "json", "zot")) + assert.Check(t, os.IsNotExist(err)) +} + +func TestAllKeysSecret(t *testing.T) { + dir := fs.NewDir(t, "secrets").Path() + os.Setenv("json", ` +{ + "foo": "bar", + "zot": "qix" +}`) + defer os.Unsetenv("json") + + err := createSecretFiles(secret{ + name: "json", + keys: []string{"*"}, + }, dir) + assert.NilError(t, err) + file, err := ioutil.ReadFile(filepath.Join(dir, "json", "foo")) + assert.NilError(t, err) + content := string(file) + assert.Equal(t, content, "bar") + + file, err = ioutil.ReadFile(filepath.Join(dir, "json", "zot")) + assert.NilError(t, err) + content = string(file) + assert.Equal(t, content, "qix") +} + +func TestUnknownSecret(t *testing.T) { + dir := fs.NewDir(t, "secrets").Path() + + err := createSecretFiles(secret{ + name: "not_set", + keys: nil, + }, dir) + assert.Check(t, err != nil) +} From d74796aca22d7ec9d97a620576f6c133998f9ead Mon Sep 17 00:00:00 2001 From: Nicolas De Loof <nicolas.deloof@gmail.com> Date: Mon, 10 Aug 2020 19:02:27 +0200 Subject: [PATCH 189/198] Pass secret definition to init container as json struct this avoid yet another new micro-formats that is poorly documented Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com> --- ecs/pkg/amazon/backend/convert.go | 31 ++++-- ecs/secrets/Dockerfile | 4 +- ecs/secrets/init.go | 87 +++++++++++++++ ecs/secrets/{main_test.go => init_test.go} | 46 +++----- ecs/secrets/main.go | 122 --------------------- ecs/secrets/main/main.go | 33 ++++++ 6 files changed, 155 insertions(+), 168 deletions(-) create mode 100644 ecs/secrets/init.go rename ecs/secrets/{main_test.go => init_test.go} (64%) delete mode 100644 ecs/secrets/main.go create mode 100644 ecs/secrets/main/main.go diff --git a/ecs/pkg/amazon/backend/convert.go b/ecs/pkg/amazon/backend/convert.go index e024bde26..3eddde174 100644 --- a/ecs/pkg/amazon/backend/convert.go +++ b/ecs/pkg/amazon/backend/convert.go @@ -1,12 +1,15 @@ package backend import ( + "encoding/json" "fmt" "sort" "strconv" "strings" "time" + "github.com/docker/ecs-plugin/secrets" + "github.com/aws/aws-sdk-go/aws" ecsapi "github.com/aws/aws-sdk-go/service/ecs" "github.com/awslabs/goformation/v4/cloudformation" @@ -55,6 +58,7 @@ func Convert(project *types.Project, service types.ServiceConfig) (*ecs.TaskDefi initContainers []ecs.TaskDefinition_ContainerDependency ) if len(service.Secrets) > 0 { + initContainerName := fmt.Sprintf("%s_Secrets_InitContainer", normalizeResourceName(service.Name)) volumes = append(volumes, ecs.TaskDefinition_Volume{ Name: "secrets", }) @@ -65,25 +69,24 @@ func Convert(project *types.Project, service types.ServiceConfig) (*ecs.TaskDefi }) initContainers = append(initContainers, ecs.TaskDefinition_ContainerDependency{ Condition: ecsapi.ContainerConditionSuccess, - ContainerName: "Secrets_InitContainer", + ContainerName: initContainerName, }) var ( - names []string - secrets []ecs.TaskDefinition_Secret + args []secrets.Secret + taskSecrets []ecs.TaskDefinition_Secret ) for _, s := range service.Secrets { secretConfig := project.Secrets[s.Source] if s.Target == "" { s.Target = s.Source } - secrets = append(secrets, ecs.TaskDefinition_Secret{ + taskSecrets = append(taskSecrets, ecs.TaskDefinition_Secret{ Name: s.Target, ValueFrom: secretConfig.Name, }) - name := s.Target + var keys []string if ext, ok := secretConfig.Extensions[compose.ExtensionKeys]; ok { - var keys []string if key, ok := ext.(string); ok { keys = append(keys, key) } else { @@ -91,14 +94,20 @@ func Convert(project *types.Project, service types.ServiceConfig) (*ecs.TaskDefi keys = append(keys, k.(string)) } } - name = fmt.Sprintf("%s:%s", s.Target, strings.Join(keys, ",")) } - names = append(names, name) + args = append(args, secrets.Secret{ + Name: s.Target, + Keys: keys, + }) + } + command, err := json.Marshal(args) + if err != nil { + return nil, err } containers = append(containers, ecs.TaskDefinition_ContainerDefinition{ - Name: fmt.Sprintf("%s_Secrets_InitContainer", normalizeResourceName(service.Name)), + Name: initContainerName, Image: secretsInitContainerImage, - Command: names, + Command: []string{string(command)}, Essential: false, // FIXME this will be ignored, see https://github.com/awslabs/goformation/issues/61#issuecomment-625139607 LogConfiguration: logConfiguration, MountPoints: []ecs.TaskDefinition_MountPoint{ @@ -108,7 +117,7 @@ func Convert(project *types.Project, service types.ServiceConfig) (*ecs.TaskDefi SourceVolume: "secrets", }, }, - Secrets: secrets, + Secrets: taskSecrets, }) } diff --git a/ecs/secrets/Dockerfile b/ecs/secrets/Dockerfile index 7395d1250..638e113b4 100644 --- a/ecs/secrets/Dockerfile +++ b/ecs/secrets/Dockerfile @@ -1,7 +1,7 @@ FROM golang:1.14.4-alpine AS builder -WORKDIR $GOPATH/src/github.com/docker/ecs-secrets +WORKDIR $GOPATH/src/github.com/docker/ecs-plugin/secrets COPY . . -RUN GOOS=linux GOARCH=amd64 go build -ldflags="-w -s" -o /go/bin/secrets +RUN GOOS=linux GOARCH=amd64 go build -ldflags="-w -s" -o /go/bin/secrets main/main.go FROM scratch COPY --from=builder /go/bin/secrets /secrets diff --git a/ecs/secrets/init.go b/ecs/secrets/init.go new file mode 100644 index 000000000..1903d3cc9 --- /dev/null +++ b/ecs/secrets/init.go @@ -0,0 +1,87 @@ +package secrets + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "os" + "path/filepath" +) + +type Secret struct { + Name string + Keys []string +} + +func CreateSecretFiles(secret Secret, path string) error { + value, ok := os.LookupEnv(secret.Name) + if !ok { + return fmt.Errorf("%q variable not set", secret.Name) + } + + secrets := filepath.Join(path, secret.Name) + + if len(secret.Keys) == 0 { + // raw Secret + fmt.Printf("inject Secret %q info %s\n", secret.Name, secrets) + return ioutil.WriteFile(secrets, []byte(value), 0444) + } + + var unmarshalled interface{} + err := json.Unmarshal([]byte(value), &unmarshalled) + if err != nil { + return fmt.Errorf("%q Secret is not a valid JSON document: %w", secret.Name, err) + } + + dict, ok := unmarshalled.(map[string]interface{}) + if !ok { + return fmt.Errorf("%q Secret is not a JSON dictionary: %w", secret.Name, err) + } + err = os.MkdirAll(secrets, 0755) + if err != nil { + return err + } + + if contains(secret.Keys, "*") { + var keys []string + for k := range dict { + keys = append(keys, k) + } + secret.Keys = keys + } + + for _, k := range secret.Keys { + path := filepath.Join(secrets, k) + fmt.Printf("inject Secret %q info %s\n", k, path) + + v, ok := dict[k] + if !ok { + return fmt.Errorf("%q Secret has no %q key", secret.Name, k) + } + + var raw []byte + if s, ok := v.(string); ok { + raw = []byte(s) + } else { + raw, err = json.Marshal(v) + if err != nil { + return err + } + } + + err = ioutil.WriteFile(path, raw, 0444) + if err != nil { + return err + } + } + return nil +} + +func contains(keys []string, s string) bool { + for _, k := range keys { + if k == s { + return true + } + } + return false +} diff --git a/ecs/secrets/main_test.go b/ecs/secrets/init_test.go similarity index 64% rename from ecs/secrets/main_test.go rename to ecs/secrets/init_test.go index ff6f630f1..dd068f5af 100644 --- a/ecs/secrets/main_test.go +++ b/ecs/secrets/init_test.go @@ -1,4 +1,4 @@ -package main +package secrets import ( "io/ioutil" @@ -10,34 +10,14 @@ import ( "gotest.tools/v3/fs" ) -func TestParseSecrets(t *testing.T) { - secrets := parseInput([]string{ - "foo", - "bar:*", - "zot:key0,key1", - }) - assert.Check(t, len(secrets) == 3) - assert.Check(t, secrets[0].name == "foo") - assert.Check(t, secrets[0].keys == nil) - - assert.Check(t, secrets[1].name == "bar") - assert.Check(t, len(secrets[1].keys) == 1) - assert.Check(t, secrets[1].keys[0] == "*") - - assert.Check(t, secrets[2].name == "zot") - assert.Check(t, len(secrets[2].keys) == 2) - assert.Check(t, secrets[2].keys[0] == "key0") - assert.Check(t, secrets[2].keys[1] == "key1") -} - func TestRawSecret(t *testing.T) { dir := fs.NewDir(t, "secrets").Path() os.Setenv("raw", "something_secret") defer os.Unsetenv("raw") - err := createSecretFiles(secret{ - name: "raw", - keys: nil, + err := CreateSecretFiles(Secret{ + Name: "raw", + Keys: nil, }, dir) assert.NilError(t, err) file, err := ioutil.ReadFile(filepath.Join(dir, "raw")) @@ -55,9 +35,9 @@ func TestSelectedKeysSecret(t *testing.T) { }`) defer os.Unsetenv("json") - err := createSecretFiles(secret{ - name: "json", - keys: []string{"foo"}, + err := CreateSecretFiles(Secret{ + Name: "json", + Keys: []string{"foo"}, }, dir) assert.NilError(t, err) file, err := ioutil.ReadFile(filepath.Join(dir, "json", "foo")) @@ -78,9 +58,9 @@ func TestAllKeysSecret(t *testing.T) { }`) defer os.Unsetenv("json") - err := createSecretFiles(secret{ - name: "json", - keys: []string{"*"}, + err := CreateSecretFiles(Secret{ + Name: "json", + Keys: []string{"*"}, }, dir) assert.NilError(t, err) file, err := ioutil.ReadFile(filepath.Join(dir, "json", "foo")) @@ -97,9 +77,9 @@ func TestAllKeysSecret(t *testing.T) { func TestUnknownSecret(t *testing.T) { dir := fs.NewDir(t, "secrets").Path() - err := createSecretFiles(secret{ - name: "not_set", - keys: nil, + err := CreateSecretFiles(Secret{ + Name: "not_set", + Keys: nil, }, dir) assert.Check(t, err != nil) } diff --git a/ecs/secrets/main.go b/ecs/secrets/main.go deleted file mode 100644 index eeaa992e6..000000000 --- a/ecs/secrets/main.go +++ /dev/null @@ -1,122 +0,0 @@ -package main - -import ( - "encoding/json" - "fmt" - "io/ioutil" - "os" - "path/filepath" - "strings" - - "github.com/pkg/errors" -) - -type secret struct { - name string - keys []string -} - -const secretsFolder = "/run/secrets" - -func main() { - secrets := parseInput(os.Args[1:]) - - for _, secret := range secrets { - err := createSecretFiles(secret, secretsFolder) - if err != nil { - fmt.Fprintf(os.Stderr, err.Error()) - os.Exit(1) - } - } -} - -func createSecretFiles(secret secret, path string) error { - value, ok := os.LookupEnv(secret.name) - if !ok { - return fmt.Errorf("%q variable not set", secret.name) - } - - secrets := filepath.Join(path, secret.name) - - if len(secret.keys) == 0 { - // raw secret - fmt.Printf("inject secret %q info %s\n", secret.name, secrets) - return ioutil.WriteFile(secrets, []byte(value), 0444) - } - - var unmarshalled interface{} - err := json.Unmarshal([]byte(value), &unmarshalled) - if err != nil { - return errors.Wrapf(err, "%q secret is not a valid JSON document", secret.name) - } - - dict, ok := unmarshalled.(map[string]interface{}) - if !ok { - return errors.Wrapf(err, "%q secret is not a JSON dictionary", secret.name) - } - err = os.MkdirAll(secrets, 0755) - if err != nil { - return err - } - - if contains(secret.keys, "*") { - var keys []string - for k := range dict { - keys = append(keys, k) - } - secret.keys = keys - } - - for _, k := range secret.keys { - path := filepath.Join(secrets, k) - fmt.Printf("inject secret %q info %s\n", k, path) - - v, ok := dict[k] - if !ok { - return fmt.Errorf("%q secret has no %q key", secret.name, k) - } - - var raw []byte - if s, ok := v.(string); ok { - raw = []byte(s) - } else { - raw, err = json.Marshal(v) - if err != nil { - return err - } - } - - err = ioutil.WriteFile(path, raw, 0444) - if err != nil { - return err - } - } - return nil -} - -// parseInput parse secret to be dumped into secret files with syntax `VARIABLE_NAME[:COMA_SEPARATED_KEYS]` -func parseInput(input []string) []secret { - var secrets []secret - for _, name := range input { - i := strings.Index(name, ":") - var keys []string - if i > 0 { - keys = strings.Split(name[i+1:], ",") - name = name[:i] - } - secrets = append(secrets, secret{ - name: name, - keys: keys, - }) - } - return secrets -} - -func contains(keys []string, s string) bool { - for _, k := range keys { - if k == s { - return true - } - } - return false -} diff --git a/ecs/secrets/main/main.go b/ecs/secrets/main/main.go new file mode 100644 index 000000000..703f88f8e --- /dev/null +++ b/ecs/secrets/main/main.go @@ -0,0 +1,33 @@ +package main + +import ( + "encoding/json" + "fmt" + "os" + + "github.com/docker/ecs-plugin/secrets" +) + +const secretsFolder = "/run/secrets" + +func main() { + if len(os.Args) != 2 { + fmt.Fprintf(os.Stderr, "usage: secrets <json encoded []Secret>") + os.Exit(1) + } + + var input []secrets.Secret + err := json.Unmarshal([]byte(os.Args[1]), &input) + if err != nil { + fmt.Fprintf(os.Stderr, err.Error()) + os.Exit(1) + } + + for _, secret := range input { + err := secrets.CreateSecretFiles(secret, secretsFolder) + if err != nil { + fmt.Fprintf(os.Stderr, err.Error()) + os.Exit(1) + } + } +} From 8e538683d33a61ce06a7deb1a9486f79137bffda Mon Sep 17 00:00:00 2001 From: Nicolas De Loof <nicolas.deloof@gmail.com> Date: Tue, 11 Aug 2020 08:37:48 +0200 Subject: [PATCH 190/198] Update Golden Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com> --- .../testdata/simple/simple-cloudformation-conversion.golden | 1 - 1 file changed, 1 deletion(-) diff --git a/ecs/pkg/amazon/backend/testdata/simple/simple-cloudformation-conversion.golden b/ecs/pkg/amazon/backend/testdata/simple/simple-cloudformation-conversion.golden index c61fc6498..d42a2464d 100644 --- a/ecs/pkg/amazon/backend/testdata/simple/simple-cloudformation-conversion.golden +++ b/ecs/pkg/amazon/backend/testdata/simple/simple-cloudformation-conversion.golden @@ -207,7 +207,6 @@ }, "SimpleTCP80TargetGroup": { "Properties": { - "Name": "SimpleTCP80TargetGroup", "Port": 80, "Protocol": "HTTP", "Tags": [ From 7d927ebe4f0aeed964a788d53950c5e2a24b7681 Mon Sep 17 00:00:00 2001 From: Nicolas De Loof <nicolas.deloof@gmail.com> Date: Tue, 11 Aug 2020 11:19:58 +0200 Subject: [PATCH 191/198] Compute rolling update min/max limits Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com> --- ecs/go.mod | 6 +-- ecs/go.sum | 22 +++++++++ ecs/pkg/amazon/backend/cloudformation.go | 49 ++++++++++++++++++- ecs/pkg/amazon/backend/cloudformation_test.go | 41 +++++++++++----- ecs/pkg/amazon/backend/compatibility.go | 2 + ecs/pkg/compose/x.go | 2 + 6 files changed, 105 insertions(+), 17 deletions(-) diff --git a/ecs/go.mod b/ecs/go.mod index c7f1ad67a..066372067 100644 --- a/ecs/go.mod +++ b/ecs/go.mod @@ -14,7 +14,7 @@ require ( 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-20200716130117-e87e4f7839e3 + github.com/compose-spec/compose-go v0.0.0-20200811091145-837f8f4de457 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 @@ -28,14 +28,13 @@ require ( github.com/gogo/protobuf v1.3.1 // indirect github.com/gorilla/mux v1.7.3 // indirect github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed // indirect - github.com/imdario/mergo v0.3.10 // indirect github.com/jinzhu/gorm v1.9.12 // indirect github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0 // indirect github.com/lib/pq v1.3.0 // indirect github.com/manifoldco/promptui v0.7.0 github.com/mattn/go-sqlite3 v2.0.3+incompatible // indirect github.com/miekg/pkcs11 v1.0.3 // indirect - github.com/mitchellh/mapstructure v1.3.2 + github.com/mitchellh/mapstructure v1.3.3 github.com/morikuni/aec v1.0.0 // indirect github.com/onsi/ginkgo v1.11.0 // indirect github.com/opencontainers/image-spec v1.0.1 // indirect @@ -46,7 +45,6 @@ require ( 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/sys v0.0.0-20191022100944-742c48ecaeb7 // indirect 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 964066997..d73c6e3db 100644 --- a/ecs/go.sum +++ b/ecs/go.sum @@ -56,6 +56,8 @@ github.com/cloudflare/go-metrics v0.0.0-20151117154305-6a9aea36fb41/go.mod h1:ea 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/go.mod h1:Tj/on1eG8kiEhd0+fhSDzsPAFESxzBBvdyEgyryXffw= github.com/containerd/containerd v1.3.0-beta.2.0.20190828155532-0293cbd26c69/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= @@ -137,6 +139,8 @@ 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.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= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= @@ -227,6 +231,9 @@ github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQz 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/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= @@ -344,6 +351,7 @@ github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQ github.com/xlab/handysort v0.0.0-20150421192137-fb3537ed64a1 h1:j2hhcujLRHAg872RWAV5yaUrEjHEObwDv3aImCaNLek= github.com/xlab/handysort v0.0.0-20150421192137-fb3537ed64a1/go.mod h1:QcJo0QPSfTONNIgpN5RA8prR7fF8nkF6cTWTcNerRO8= github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= +github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/ziutek/mymysql v1.5.4/go.mod h1:LMSpPZ6DbqWFxNCHW77HeMg9I646SAhApZ/wKdgO/C0= github.com/zmap/rc2 v0.0.0-20131011165748-24b9757f5521/go.mod h1:3YZ9o3WnatTIZhuOtot4IcUfzoKVjUHqu6WALIyI0nE= github.com/zmap/zcertificate v0.0.0-20180516150559-0e3d58b1bac4/go.mod h1:5iU54tB79AMBcySS0R2XIyZBAVmeHranShAFELYx7is= @@ -359,13 +367,17 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90Pveol golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191205180655-e7c4368fe9dd h1:GGJVjV8waZKRHrgwvtH66z9ZGVurTD1MT0n1Bb+q4aM= golang.org/x/crypto v0.0.0-20191205180655-e7c4368fe9dd/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -378,15 +390,19 @@ golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09 h1:KaQtG+aDELoNmXYas3TVkGNYR golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190613194153-d28f0bde5980 h1:dfGZHvZk057jK2MCeWus/TowKpJ8y4AmooUzdBSR9GU= golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b h1:0mm1VjtFUOIlE1SbDlwjYaDxZVDP2S5ou6y0gSgXHu8= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200625001655-4c5254603344 h1:vGXIOMxbNfDTk/aXCmfdLgkrSV+Z2tcbze+pEc3v5W4= +golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 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/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +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= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -405,6 +421,8 @@ golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3 h1:7TYNF4UdlohbFwpNH04CoPMp1 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-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= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= @@ -420,6 +438,10 @@ golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3 golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190624222133-a101b041ded4/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200717024301-6ddee64345a6/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= diff --git a/ecs/pkg/amazon/backend/cloudformation.go b/ecs/pkg/amazon/backend/cloudformation.go index e54108588..482cf1212 100644 --- a/ecs/pkg/amazon/backend/cloudformation.go +++ b/ecs/pkg/amazon/backend/cloudformation.go @@ -190,6 +190,11 @@ func (b Backend) Convert(project *types.Project) (*cloudformation.Template, erro dependsOn = append(dependsOn, serviceResourceName(dependency)) } + minPercent, maxPercent, err := b.computeRollingUpdateLimits(service) + if err != nil { + return nil, err + } + template.Resources[serviceResourceName(service.Name)] = &ecs.Service{ AWSCloudFormationDependsOn: dependsOn, Cluster: cluster, @@ -198,8 +203,8 @@ func (b Backend) Convert(project *types.Project) (*cloudformation.Template, erro Type: ecsapi.DeploymentControllerTypeEcs, }, DeploymentConfiguration: &ecs.Service_DeploymentConfiguration{ - MaximumPercent: 200, - MinimumHealthyPercent: 100, + MaximumPercent: maxPercent, + MinimumHealthyPercent: minPercent, }, LaunchType: ecsapi.LaunchTypeFargate, LoadBalancers: serviceLB, @@ -232,6 +237,46 @@ func (b Backend) Convert(project *types.Project) (*cloudformation.Template, erro return template, nil } +func (b Backend) computeRollingUpdateLimits(service types.ServiceConfig) (int, int, error) { + maxPercent := 200 + minPercent := 100 + if service.Deploy == nil || service.Deploy.UpdateConfig == nil { + return minPercent, maxPercent, nil + } + updateConfig := service.Deploy.UpdateConfig + min, okMin := updateConfig.Extensions[compose.ExtensionMinPercent] + if okMin { + minPercent = min.(int) + } + max, okMax := updateConfig.Extensions[compose.ExtensionMaxPercent] + if okMax { + maxPercent = max.(int) + } + if okMin && okMax { + return minPercent, maxPercent, nil + } + + if updateConfig.Parallelism != nil { + parallelism := int(*updateConfig.Parallelism) + if service.Deploy.Replicas == nil { + return minPercent, maxPercent, + fmt.Errorf("rolling update configuration require deploy.replicas to be set") + } + replicas := int(*service.Deploy.Replicas) + if replicas < parallelism { + return minPercent, maxPercent, + fmt.Errorf("deploy.replicas (%d) must be greater than deploy.update_config.parallelism (%d)", replicas, parallelism) + } + if !okMin { + minPercent = (replicas - parallelism) * 100 / replicas + } + if !okMax { + maxPercent = (replicas + parallelism) * 100 / replicas + } + } + return minPercent, maxPercent, nil +} + func getLoadBalancerType(project *types.Project) string { for _, service := range project.Services { for _, port := range service.Ports { diff --git a/ecs/pkg/amazon/backend/cloudformation_test.go b/ecs/pkg/amazon/backend/cloudformation_test.go index 0c7edd8f6..880de0d67 100644 --- a/ecs/pkg/amazon/backend/cloudformation_test.go +++ b/ecs/pkg/amazon/backend/cloudformation_test.go @@ -26,9 +26,38 @@ func TestSimpleConvert(t *testing.T) { golden.Assert(t, result, expected) } +func TestRollingUpdateLimits(t *testing.T) { + template := convertYaml(t, "test", ` +services: + foo: + image: hello_world + deploy: + replicas: 4 + update_config: + parallelism: 2 +`) + service := template.Resources["FooService"].(*ecs.Service) + assert.Check(t, service.DeploymentConfiguration.MaximumPercent == 150) + assert.Check(t, service.DeploymentConfiguration.MinimumHealthyPercent == 50) +} + +func TestRollingUpdateExtension(t *testing.T) { + template := convertYaml(t, "test", ` +services: + foo: + image: hello_world + deploy: + update_config: + x-aws-min_percent: 25 + x-aws-max_percent: 125 +`) + service := template.Resources["FooService"].(*ecs.Service) + assert.Check(t, service.DeploymentConfiguration.MaximumPercent == 125) + assert.Check(t, service.DeploymentConfiguration.MinimumHealthyPercent == 25) +} + func TestRolePolicy(t *testing.T) { template := convertYaml(t, "test", ` -version: "3" services: foo: image: hello_world @@ -48,7 +77,6 @@ services: func TestMapNetworksToSecurityGroups(t *testing.T) { template := convertYaml(t, "test", ` -version: "3" services: test: image: hello_world @@ -73,7 +101,6 @@ networks: func TestLoadBalancerTypeApplication(t *testing.T) { template := convertYaml(t, "test123456789009876543211234567890", ` -version: "3" services: test: image: nginx @@ -89,7 +116,6 @@ services: func TestNoLoadBalancerIfNoPortExposed(t *testing.T) { template := convertYaml(t, "test", ` -version: "3" services: test: image: nginx @@ -105,7 +131,6 @@ services: func TestServiceReplicas(t *testing.T) { template := convertYaml(t, "test", ` -version: "3" services: test: image: nginx @@ -119,7 +144,6 @@ services: func TestTaskSizeConvert(t *testing.T) { template := convertYaml(t, "test", ` -version: "3" services: test: image: nginx @@ -137,7 +161,6 @@ services: assert.Equal(t, def.Memory, "2048") template = convertYaml(t, "test", ` -version: "3" services: test: image: nginx @@ -156,7 +179,6 @@ services: } func TestTaskSizeConvertFailure(t *testing.T) { model := loadConfig(t, "test", ` -version: "3" services: test: image: nginx @@ -172,7 +194,6 @@ services: func TestLoadBalancerTypeNetwork(t *testing.T) { template := convertYaml(t, "test", ` -version: "3" services: test: image: nginx @@ -187,7 +208,6 @@ services: func TestServiceMapping(t *testing.T) { template := convertYaml(t, "test", ` -version: "3" services: test: image: "image" @@ -227,7 +247,6 @@ func get(l []ecs.TaskDefinition_KeyValuePair, name string) string { func TestResourcesHaveProjectTagSet(t *testing.T) { template := convertYaml(t, "test", ` -version: "3" services: test: image: nginx diff --git a/ecs/pkg/amazon/backend/compatibility.go b/ecs/pkg/amazon/backend/compatibility.go index ebf915889..6857c4986 100644 --- a/ecs/pkg/amazon/backend/compatibility.go +++ b/ecs/pkg/amazon/backend/compatibility.go @@ -22,6 +22,8 @@ var compatibleComposeAttributes = []string{ "services.deploy.resources.reservations", "services.deploy.resources.reservations.cpus", "services.deploy.resources.reservations.memory", + "services.deploy.update_config", + "services.deploy.update_config.parallelism", "services.entrypoint", "services.environment", "service.image", diff --git a/ecs/pkg/compose/x.go b/ecs/pkg/compose/x.go index 777987afa..44ebfae96 100644 --- a/ecs/pkg/compose/x.go +++ b/ecs/pkg/compose/x.go @@ -7,4 +7,6 @@ const ( ExtensionLB = "x-aws-loadbalancer" ExtensionCluster = "x-aws-cluster" ExtensionKeys = "x-aws-keys" + ExtensionMinPercent = "x-aws-min_percent" + ExtensionMaxPercent = "x-aws-max_percent" ) From 5ed328d8df46cf2bc7ebd47369963135d48b57a0 Mon Sep 17 00:00:00 2001 From: Nicolas De Loof <nicolas.deloof@gmail.com> Date: Tue, 11 Aug 2020 15:48:12 +0200 Subject: [PATCH 192/198] Add service.env_file support Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com> --- ecs/go.mod | 1 + ecs/pkg/amazon/backend/cloudformation_test.go | 30 ++++++++ ecs/pkg/amazon/backend/compatibility.go | 1 + ecs/pkg/amazon/backend/convert.go | 74 +++++++++++++------ ecs/pkg/amazon/backend/testdata/input/envfile | 1 + 5 files changed, 85 insertions(+), 22 deletions(-) create mode 100644 ecs/pkg/amazon/backend/testdata/input/envfile diff --git a/ecs/go.mod b/ecs/go.mod index 066372067..594aa539f 100644 --- a/ecs/go.mod +++ b/ecs/go.mod @@ -29,6 +29,7 @@ require ( github.com/gorilla/mux v1.7.3 // indirect github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed // indirect github.com/jinzhu/gorm v1.9.12 // indirect + github.com/joho/godotenv v1.3.0 github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0 // indirect github.com/lib/pq v1.3.0 // indirect github.com/manifoldco/promptui v0.7.0 diff --git a/ecs/pkg/amazon/backend/cloudformation_test.go b/ecs/pkg/amazon/backend/cloudformation_test.go index 880de0d67..84ddf5308 100644 --- a/ecs/pkg/amazon/backend/cloudformation_test.go +++ b/ecs/pkg/amazon/backend/cloudformation_test.go @@ -26,6 +26,36 @@ func TestSimpleConvert(t *testing.T) { golden.Assert(t, result, expected) } +func TestEnvFile(t *testing.T) { + template := convertYaml(t, "test", ` +services: + foo: + image: hello_world + env_file: + - testdata/input/envfile +`) + def := template.Resources["FooTaskDefinition"].(*ecs.TaskDefinition) + env := def.ContainerDefinitions[0].Environment + assert.Equal(t, env[0].Name, "FOO") + assert.Equal(t, env[0].Value, "BAR") +} + +func TestEnvFileAndEnv(t *testing.T) { + template := convertYaml(t, "test", ` +services: + foo: + image: hello_world + env_file: + - testdata/input/envfile + environment: + - "FOO=ZOT" +`) + def := template.Resources["FooTaskDefinition"].(*ecs.TaskDefinition) + env := def.ContainerDefinitions[0].Environment + assert.Equal(t, env[0].Name, "FOO") + assert.Equal(t, env[0].Value, "ZOT") +} + func TestRollingUpdateLimits(t *testing.T) { template := convertYaml(t, "test", ` services: diff --git a/ecs/pkg/amazon/backend/compatibility.go b/ecs/pkg/amazon/backend/compatibility.go index 6857c4986..02ef5e508 100644 --- a/ecs/pkg/amazon/backend/compatibility.go +++ b/ecs/pkg/amazon/backend/compatibility.go @@ -26,6 +26,7 @@ var compatibleComposeAttributes = []string{ "services.deploy.update_config.parallelism", "services.entrypoint", "services.environment", + "services.env_file", "service.image", "services.init", "services.healthcheck", diff --git a/ecs/pkg/amazon/backend/convert.go b/ecs/pkg/amazon/backend/convert.go index 3eddde174..b130d51a8 100644 --- a/ecs/pkg/amazon/backend/convert.go +++ b/ecs/pkg/amazon/backend/convert.go @@ -3,13 +3,13 @@ package backend import ( "encoding/json" "fmt" + "os" + "path/filepath" "sort" "strconv" "strings" "time" - "github.com/docker/ecs-plugin/secrets" - "github.com/aws/aws-sdk-go/aws" ecsapi "github.com/aws/aws-sdk-go/service/ecs" "github.com/awslabs/goformation/v4/cloudformation" @@ -18,6 +18,8 @@ import ( "github.com/compose-spec/compose-go/types" "github.com/docker/cli/opts" "github.com/docker/ecs-plugin/pkg/compose" + "github.com/docker/ecs-plugin/secrets" + "github.com/joho/godotenv" ) const secretsInitContainerImage = "docker/ecs-secrets-sidecar" @@ -121,6 +123,11 @@ func Convert(project *types.Project, service types.ServiceConfig) (*ecs.TaskDefi }) } + pairs, err := createEnvironment(project, service) + if err != nil { + return nil, err + } + containers = append(containers, ecs.TaskDefinition_ContainerDefinition{ Command: service.Command, DisableNetworking: service.NetworkMode == "none", @@ -129,7 +136,7 @@ func Convert(project *types.Project, service types.ServiceConfig) (*ecs.TaskDefi DnsServers: service.DNS, DockerSecurityOptions: service.SecurityOpt, EntryPoint: service.Entrypoint, - Environment: toKeyValuePair(service.Environment), + Environment: pairs, Essential: true, ExtraHosts: toHostEntryPtr(service.ExtraHosts), FirelensConfiguration: nil, @@ -173,6 +180,48 @@ func Convert(project *types.Project, service types.ServiceConfig) (*ecs.TaskDefi }, nil } +func createEnvironment(project *types.Project, service types.ServiceConfig) ([]ecs.TaskDefinition_KeyValuePair, error) { + environment := map[string]*string{} + for _, f := range service.EnvFile { + if !filepath.IsAbs(f) { + f = filepath.Join(project.WorkingDir, f) + } + if _, err := os.Stat(f); os.IsNotExist(err) { + return nil, err + } + file, err := os.Open(f) + if err != nil { + return nil, err + } + defer file.Close() + + env, err := godotenv.Parse(file) + if err != nil { + return nil, err + } + for k, v := range env { + environment[k] = &v + } + } + for k, v := range service.Environment { + environment[k] = v + } + + var pairs []ecs.TaskDefinition_KeyValuePair + for k, v := range environment { + name := k + var value string + if v != nil { + value = *v + } + pairs = append(pairs, ecs.TaskDefinition_KeyValuePair{ + Name: name, + Value: value, + }) + } + return pairs, nil +} + func toTags(labels types.Labels) []tags.Tag { t := []tags.Tag{} for n, v := range labels { @@ -391,25 +440,6 @@ func toHostEntryPtr(hosts types.HostsList) []ecs.TaskDefinition_HostEntry { return e } -func toKeyValuePair(environment types.MappingWithEquals) []ecs.TaskDefinition_KeyValuePair { - if environment == nil || len(environment) == 0 { - return nil - } - pairs := []ecs.TaskDefinition_KeyValuePair{} - for k, v := range environment { - name := k - var value string - if v != nil { - value = *v - } - pairs = append(pairs, ecs.TaskDefinition_KeyValuePair{ - Name: name, - Value: value, - }) - } - return pairs -} - func getRepoCredentials(service types.ServiceConfig) *ecs.TaskDefinition_RepositoryCredentials { // extract registry and namespace string from image name for key, value := range service.Extensions { diff --git a/ecs/pkg/amazon/backend/testdata/input/envfile b/ecs/pkg/amazon/backend/testdata/input/envfile new file mode 100644 index 000000000..6ac867af7 --- /dev/null +++ b/ecs/pkg/amazon/backend/testdata/input/envfile @@ -0,0 +1 @@ +FOO=BAR From d281f6cb3e97d384c208cfd043563f717f1fe810 Mon Sep 17 00:00:00 2001 From: Nicolas De Loof <nicolas.deloof@gmail.com> Date: Tue, 11 Aug 2020 15:48:12 +0200 Subject: [PATCH 193/198] Allow fine tunning of awslogs Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com> --- ecs/pkg/amazon/backend/cloudformation.go | 21 +++++++++---- ecs/pkg/amazon/backend/cloudformation_test.go | 31 +++++++++++++++++-- ecs/pkg/amazon/backend/compatibility.go | 12 +++++-- ecs/pkg/amazon/backend/convert.go | 29 ++++++++++++----- ecs/pkg/compose/x.go | 1 + 5 files changed, 76 insertions(+), 18 deletions(-) diff --git a/ecs/pkg/amazon/backend/cloudformation.go b/ecs/pkg/amazon/backend/cloudformation.go index 482cf1212..aa43dbe98 100644 --- a/ecs/pkg/amazon/backend/cloudformation.go +++ b/ecs/pkg/amazon/backend/cloudformation.go @@ -119,10 +119,7 @@ func (b Backend) Convert(project *types.Project) (*cloudformation.Template, erro project.Secrets[i] = s } - logGroup := fmt.Sprintf("/docker-compose/%s", project.Name) - template.Resources["LogGroup"] = &logs.LogGroup{ - LogGroupName: logGroup, - } + createLogGroup(project, template) // Private DNS namespace will allow DNS name for the services to be <service>.<project>.local createCloudMap(project, template) @@ -190,7 +187,7 @@ func (b Backend) Convert(project *types.Project) (*cloudformation.Template, erro dependsOn = append(dependsOn, serviceResourceName(dependency)) } - minPercent, maxPercent, err := b.computeRollingUpdateLimits(service) + minPercent, maxPercent, err := computeRollingUpdateLimits(service) if err != nil { return nil, err } @@ -237,7 +234,19 @@ func (b Backend) Convert(project *types.Project) (*cloudformation.Template, erro return template, nil } -func (b Backend) computeRollingUpdateLimits(service types.ServiceConfig) (int, int, error) { +func createLogGroup(project *types.Project, template *cloudformation.Template) { + retention := 0 + if v, ok := project.Extensions[compose.ExtensionRetention]; ok { + retention = v.(int) + } + logGroup := fmt.Sprintf("/docker-compose/%s", project.Name) + template.Resources["LogGroup"] = &logs.LogGroup{ + LogGroupName: logGroup, + RetentionInDays: retention, + } +} + +func computeRollingUpdateLimits(service types.ServiceConfig) (int, int, error) { maxPercent := 200 minPercent := 100 if service.Deploy == nil || service.Deploy.UpdateConfig == nil { diff --git a/ecs/pkg/amazon/backend/cloudformation_test.go b/ecs/pkg/amazon/backend/cloudformation_test.go index 84ddf5308..f2a735c51 100644 --- a/ecs/pkg/amazon/backend/cloudformation_test.go +++ b/ecs/pkg/amazon/backend/cloudformation_test.go @@ -11,6 +11,7 @@ import ( "github.com/awslabs/goformation/v4/cloudformation/ecs" "github.com/awslabs/goformation/v4/cloudformation/elasticloadbalancingv2" "github.com/awslabs/goformation/v4/cloudformation/iam" + "github.com/awslabs/goformation/v4/cloudformation/logs" "github.com/compose-spec/compose-go/cli" "github.com/compose-spec/compose-go/loader" "github.com/compose-spec/compose-go/types" @@ -26,6 +27,25 @@ func TestSimpleConvert(t *testing.T) { golden.Assert(t, result, expected) } +func TestLogging(t *testing.T) { + template := convertYaml(t, "test", ` +services: + foo: + image: hello_world + logging: + options: + awslogs-datetime-pattern: "FOO" + +x-aws-logs_retention: 10 +`) + def := template.Resources["FooTaskDefinition"].(*ecs.TaskDefinition) + logging := def.ContainerDefinitions[0].LogConfiguration + assert.Equal(t, logging.Options["awslogs-datetime-pattern"], "FOO") + + logGroup := template.Resources["LogGroup"].(*logs.LogGroup) + assert.Equal(t, logGroup.RetentionInDays, 10) +} + func TestEnvFile(t *testing.T) { template := convertYaml(t, "test", ` services: @@ -36,8 +56,15 @@ services: `) def := template.Resources["FooTaskDefinition"].(*ecs.TaskDefinition) env := def.ContainerDefinitions[0].Environment - assert.Equal(t, env[0].Name, "FOO") - assert.Equal(t, env[0].Value, "BAR") + var found bool + for _, pair := range env { + if pair.Name == "FOO" { + assert.Equal(t, pair.Value, "BAR") + found = true + } + } + assert.Check(t, found, "environment variable FOO not set") + } func TestEnvFileAndEnv(t *testing.T) { diff --git a/ecs/pkg/amazon/backend/compatibility.go b/ecs/pkg/amazon/backend/compatibility.go index 02ef5e508..d217ca48f 100644 --- a/ecs/pkg/amazon/backend/compatibility.go +++ b/ecs/pkg/amazon/backend/compatibility.go @@ -27,14 +27,16 @@ var compatibleComposeAttributes = []string{ "services.entrypoint", "services.environment", "services.env_file", - "service.image", - "services.init", "services.healthcheck", "services.healthcheck.interval", "services.healthcheck.retries", "services.healthcheck.start_period", "services.healthcheck.test", "services.healthcheck.timeout", + "services.image", + "services.init", + "services.logging", + "services.logging.options", "services.networks", "services.ports", "services.ports.mode", @@ -77,3 +79,9 @@ func (c *FargateCompatibilityChecker) CheckCapAdd(service *types.ServiceConfig) } service.CapAdd = add } + +func (c *FargateCompatibilityChecker) CheckLoggingDriver(config *types.LoggingConfig) { + if config.Driver != "" && config.Driver != "awslogs" { + c.Unsupported("services.logging.driver %s is not supported", config.Driver) + } +} diff --git a/ecs/pkg/amazon/backend/convert.go b/ecs/pkg/amazon/backend/convert.go index b130d51a8..2056dad4f 100644 --- a/ecs/pkg/amazon/backend/convert.go +++ b/ecs/pkg/amazon/backend/convert.go @@ -44,14 +44,7 @@ func Convert(project *types.Project, service types.ServiceConfig) (*ecs.TaskDefi fmt.Sprintf(" %s.local", project.Name), })) - logConfiguration := &ecs.TaskDefinition_LogConfiguration{ - LogDriver: ecsapi.LogDriverAwslogs, - Options: map[string]string{ - "awslogs-region": cloudformation.Ref("AWS::Region"), - "awslogs-group": cloudformation.Ref("LogGroup"), - "awslogs-stream-prefix": project.Name, - }, - } + logConfiguration := getLogConfiguration(service, project) var ( containers []ecs.TaskDefinition_ContainerDefinition @@ -222,6 +215,26 @@ func createEnvironment(project *types.Project, service types.ServiceConfig) ([]e return pairs, nil } +func getLogConfiguration(service types.ServiceConfig, project *types.Project) *ecs.TaskDefinition_LogConfiguration { + options := map[string]string{ + "awslogs-region": cloudformation.Ref("AWS::Region"), + "awslogs-group": cloudformation.Ref("LogGroup"), + "awslogs-stream-prefix": project.Name, + } + if service.Logging != nil { + for k, v := range service.Logging.Options { + if strings.HasPrefix(k, "awslogs-") { + options[k] = v + } + } + } + logConfiguration := &ecs.TaskDefinition_LogConfiguration{ + LogDriver: ecsapi.LogDriverAwslogs, + Options: options, + } + return logConfiguration +} + func toTags(labels types.Labels) []tags.Tag { t := []tags.Tag{} for n, v := range labels { diff --git a/ecs/pkg/compose/x.go b/ecs/pkg/compose/x.go index 44ebfae96..63f2e30e7 100644 --- a/ecs/pkg/compose/x.go +++ b/ecs/pkg/compose/x.go @@ -9,4 +9,5 @@ const ( ExtensionKeys = "x-aws-keys" ExtensionMinPercent = "x-aws-min_percent" ExtensionMaxPercent = "x-aws-max_percent" + ExtensionRetention = "x-aws-logs_retention" ) From 83d65c02a0e73c9b452834a4202be55f97b4f7b4 Mon Sep 17 00:00:00 2001 From: Nicolas De Loof <nicolas.deloof@gmail.com> Date: Wed, 12 Aug 2020 14:45:10 +0200 Subject: [PATCH 194/198] Fix flacky test Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com> --- ecs/pkg/amazon/backend/cloudformation_test.go | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/ecs/pkg/amazon/backend/cloudformation_test.go b/ecs/pkg/amazon/backend/cloudformation_test.go index f2a735c51..307b60364 100644 --- a/ecs/pkg/amazon/backend/cloudformation_test.go +++ b/ecs/pkg/amazon/backend/cloudformation_test.go @@ -64,7 +64,6 @@ services: } } assert.Check(t, found, "environment variable FOO not set") - } func TestEnvFileAndEnv(t *testing.T) { @@ -79,8 +78,14 @@ services: `) def := template.Resources["FooTaskDefinition"].(*ecs.TaskDefinition) env := def.ContainerDefinitions[0].Environment - assert.Equal(t, env[0].Name, "FOO") - assert.Equal(t, env[0].Value, "ZOT") + var found bool + for _, pair := range env { + if pair.Name == "FOO" { + assert.Equal(t, pair.Value, "ZOT") + found = true + } + } + assert.Check(t, found, "environment variable FOO not set") } func TestRollingUpdateLimits(t *testing.T) { From de99add26b71b9081b4fa85b87cda1ee6b2f4d3f Mon Sep 17 00:00:00 2001 From: aiordache <anca.iordache@docker.com> Date: Tue, 11 Aug 2020 10:43:11 +0200 Subject: [PATCH 195/198] Use docker/api progress writer Signed-off-by: aiordache <anca.iordache@docker.com> Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com> --- 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 +} From 8182c98abf0b4cca6f1766a05a06ca668ada0a03 Mon Sep 17 00:00:00 2001 From: Nicolas De Loof <nicolas.deloof@gmail.com> Date: Thu, 13 Aug 2020 10:27:01 +0200 Subject: [PATCH 196/198] Don't pretend we know resources to be created some resources are controlled by a CloudFormation Condition and as such won't be created. If we add them to the progresswriter, the latter will never receive status update. Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com> --- ecs/pkg/amazon/backend/up.go | 31 ------------------------------- 1 file changed, 31 deletions(-) diff --git a/ecs/pkg/amazon/backend/up.go b/ecs/pkg/amazon/backend/up.go index 38705e814..098caaacf 100644 --- a/ecs/pkg/amazon/backend/up.go +++ b/ecs/pkg/amazon/backend/up.go @@ -5,13 +5,11 @@ 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/progress" ) func (b *Backend) Up(ctx context.Context, options *cli.ProjectOptions) error { @@ -83,14 +81,6 @@ func (b *Backend) Up(ctx context.Context, options *cli.ProjectOptions) error { } } - for k := range template.Resources { - b.writer.Event(progress.Event{ - ID: k, - Status: progress.Working, - StatusText: "Pending", - }) - } - signalChan := make(chan os.Signal, 1) signal.Notify(signalChan, syscall.SIGINT, syscall.SIGTERM) go func() { @@ -100,27 +90,6 @@ func (b *Backend) Up(ctx context.Context, options *cli.ProjectOptions) error { }() 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 } From f74cc8f0aab03a6fe979cae4794a5e7a40d9933a Mon Sep 17 00:00:00 2001 From: Nicolas De Loof <nicolas.deloof@gmail.com> Date: Thu, 13 Aug 2020 15:43:24 +0200 Subject: [PATCH 197/198] Allow user to customize Roles / ManagedPolicy Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com> --- ecs/pkg/amazon/backend/cloudformation.go | 19 +++++++++++++++---- ecs/pkg/amazon/backend/iam.go | 2 +- ecs/pkg/compose/x.go | 2 ++ 3 files changed, 18 insertions(+), 5 deletions(-) diff --git a/ecs/pkg/amazon/backend/cloudformation.go b/ecs/pkg/amazon/backend/cloudformation.go index aa43dbe98..c6c033db4 100644 --- a/ecs/pkg/amazon/backend/cloudformation.go +++ b/ecs/pkg/amazon/backend/cloudformation.go @@ -440,15 +440,26 @@ func createTaskExecutionRole(service types.ServiceConfig, err error, definition PolicyDocument: policy, PolicyName: fmt.Sprintf("%sGrantAccessToSecrets", service.Name), }) + } + if roles, ok := service.Extensions[compose.ExtensionRole]; ok { + rolePolicies = append(rolePolicies, iam.Role_Policy{ + PolicyDocument: roles, + }) + } + managedPolicies := []string{ + ECSTaskExecutionPolicy, + ECRReadOnlyPolicy, + } + if v, ok := service.Extensions[compose.ExtensionManagedPolicies]; ok { + for _, s := range v.([]interface{}) { + managedPolicies = append(managedPolicies, s.(string)) + } } template.Resources[taskExecutionRole] = &iam.Role{ AssumeRolePolicyDocument: assumeRolePolicyDocument, Policies: rolePolicies, - ManagedPolicyArns: []string{ - ECSTaskExecutionPolicy, - ECRReadOnlyPolicy, - }, + ManagedPolicyArns: managedPolicies, } return taskExecutionRole, nil } diff --git a/ecs/pkg/amazon/backend/iam.go b/ecs/pkg/amazon/backend/iam.go index 81a4fdb0f..4b282020e 100644 --- a/ecs/pkg/amazon/backend/iam.go +++ b/ecs/pkg/amazon/backend/iam.go @@ -22,7 +22,7 @@ var assumeRolePolicyDocument = PolicyDocument{ }, } -// could alternatively depend on https://github.com/kubernetes-sigs/cluster-api-provider-aws/blob/master/pkg/cloud/services/iam/types.go#L52 +// could alternatively depend on https://github.com/kubernetes-sigs/cluster-api-provider-aws/blob/master/cmd/clusterawsadm/api/iam/v1alpha1/types.go type PolicyDocument struct { Version string `json:",omitempty"` Statement []PolicyStatement `json:",omitempty"` diff --git a/ecs/pkg/compose/x.go b/ecs/pkg/compose/x.go index 63f2e30e7..3a4ffdb61 100644 --- a/ecs/pkg/compose/x.go +++ b/ecs/pkg/compose/x.go @@ -10,4 +10,6 @@ const ( ExtensionMinPercent = "x-aws-min_percent" ExtensionMaxPercent = "x-aws-max_percent" ExtensionRetention = "x-aws-logs_retention" + ExtensionRole = "x-aws-role" + ExtensionManagedPolicies = "x-aws-policies" ) From 9eb0a10517744d762f11d6bc06962ae1cc0a08cc Mon Sep 17 00:00:00 2001 From: aiordache <anca.iordache@docker.com> Date: Fri, 14 Aug 2020 12:01:22 +0200 Subject: [PATCH 198/198] Init progress writer in WaitStackCompletion Signed-off-by: aiordache <anca.iordache@docker.com> Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com> --- ecs/cmd/commands/compose.go | 2 -- ecs/pkg/amazon/backend/backend.go | 8 -------- ecs/pkg/amazon/backend/wait.go | 5 +++-- 3 files changed, 3 insertions(+), 12 deletions(-) diff --git a/ecs/cmd/commands/compose.go b/ecs/cmd/commands/compose.go index c0ed60093..7fea85e94 100644 --- a/ecs/cmd/commands/compose.go +++ b/ecs/cmd/commands/compose.go @@ -82,7 +82,6 @@ func UpCommand(dockerCli command.Cli, options *composeOptions) *cobra.Command { } return progress.Run(context.Background(), func(ctx context.Context) error { - backend.SetWriter(ctx) return backend.Up(ctx, opts) }) }), @@ -130,7 +129,6 @@ func DownCommand(dockerCli command.Cli, options *composeOptions) *cobra.Command return err } return progress.Run(context.Background(), func(ctx context.Context) error { - backend.SetWriter(ctx) return backend.Down(ctx, opts) }) }), diff --git a/ecs/pkg/amazon/backend/backend.go b/ecs/pkg/amazon/backend/backend.go index caf7e17de..2b265f3e2 100644 --- a/ecs/pkg/amazon/backend/backend.go +++ b/ecs/pkg/amazon/backend/backend.go @@ -1,12 +1,9 @@ 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) { @@ -30,9 +27,4 @@ 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/wait.go b/ecs/pkg/amazon/backend/wait.go index 82b9863b0..dcc26772c 100644 --- a/ecs/pkg/amazon/backend/wait.go +++ b/ecs/pkg/amazon/backend/wait.go @@ -14,7 +14,8 @@ import ( func (b *Backend) WaitStackCompletion(ctx context.Context, name string, operation int) error { knownEvents := map[string]struct{}{} - + // progress writer + w := progress.ContextWriter(ctx) // Get the unique Stack ID so we can collect events without getting some from previous deployments with same name stackID, err := b.api.GetStackID(ctx, name) if err != nil { @@ -80,7 +81,7 @@ func (b *Backend) WaitStackCompletion(ctx context.Context, name string, operatio } } } - b.writer.Event(progress.Event{ + w.Event(progress.Event{ ID: resource, Status: progressStatus, StatusText: status,