Repository: docker/compose-cli
Pull request #849 (2020-10-29 11:46:29)

ECS E2E test using multi-container , multi-port publish app and validating cross-container communication


Signed-off-by: Guillaume Tardif <guillaume.tardif@docker.com>

**What I did**
* Reuse multi-container compose file from ACI e2e tests

**Related issue**
https://github.com/docker/compose-cli/issues/761

<!-- optional tests
You can add a / mention to run tests executed by default only on main branch : 
* `test-aci` to run ACI E2E tests
* `test-ecs` to run ECS E2E tests
* `test-windows` to run tests & E2E tests on windows
-->
/test-aci
/test-ecs
/test-windows

**(not mandatory) A picture of a cute animal, if possible in relation with what you did**
![](https://i.pinimg.com/originals/58/5a/bc/585abcda65cfe8d6286ef2dba5b7365f.jpg)


Patch:
diff --git a/cli/cmd/context/ls.go b/cli/cmd/context/ls.go
index 835fd7fd..f4485d62 100644
--- a/cli/cmd/context/ls.go
+++ b/cli/cmd/context/ls.go
@@ -70,7 +70,7 @@ func runList(cmd *cobra.Command, opts lsOpts) error {
 		return err
 	}
 	format := strings.ToLower(strings.ReplaceAll(opts.format, " ", ""))
-	if format != "" && format != formatter.JSON && format != formatter.PRETTY && format != formatter.TemplateJSON {
+	if format != "" && format != formatter.JSON && format != formatter.PRETTY && format != formatter.TemplateLegacyJSON {
 		mobycli.Exec(cmd.Root())
 		return nil
 	}
@@ -94,9 +94,12 @@ func runList(cmd *cobra.Command, opts lsOpts) error {
 		return nil
 	}
 
-	if opts.json || format == formatter.JSON || format == formatter.TemplateJSON {
+	if opts.json || format == formatter.JSON {
 		opts.format = formatter.JSON
 	}
+	if format == formatter.TemplateLegacyJSON {
+		opts.format = formatter.TemplateLegacyJSON
+	}
 
 	view := viewFromContextList(contexts, currentContext)
 	return formatter.Print(view, opts.format, os.Stdout,
@@ -108,7 +111,7 @@ func runList(cmd *cobra.Command, opts lsOpts) error {
 				}
 				_, _ = fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\t%s\n",
 					contextName,
-					c.Type,
+					c.ContextType,
 					c.Description,
 					c.DockerEndpoint,
 					c.KubernetesEndpoint,
@@ -141,7 +144,7 @@ type contextView struct {
 	Description        string
 	DockerEndpoint     string
 	KubernetesEndpoint string
-	Type               string
+	ContextType        string
 	Name               string
 	StackOrchestrator  string
 }
@@ -155,7 +158,7 @@ func viewFromContextList(contextList []*store.DockerContext, currentContext stri
 			DockerEndpoint:     getEndpoint("docker", c.Endpoints),
 			KubernetesEndpoint: getEndpoint("kubernetes", c.Endpoints),
 			Name:               c.Name,
-			Type:               c.Type(),
+			ContextType:        c.Type(),
 			StackOrchestrator:  c.Metadata.StackOrchestrator,
 		}
 	}
diff --git a/cli/cmd/version.go b/cli/cmd/version.go
index 77bf060c..f1238ab9 100644
--- a/cli/cmd/version.go
+++ b/cli/cmd/version.go
@@ -59,7 +59,7 @@ func runVersion(cmd *cobra.Command) {
 	case formatter.PRETTY, "":
 		versionString = strings.Replace(getOutFromMoby(cmd, fixedPrettyArgs(os.Args[1:])...),
 			"\n Version:", "\n Cloud integration: "+displayedVersion+"\n Version:", 1)
-	case formatter.JSON, formatter.TemplateJSON: // Try to catch full JSON formats
+	case formatter.JSON, formatter.TemplateLegacyJSON: // Try to catch full JSON formats
 		versionString = strings.Replace(getOutFromMoby(cmd, fixedJSONArgs(os.Args[1:])...),
 			`"Version":`, fmt.Sprintf(`"CloudIntegration":%q,"Version":`, displayedVersion), 1)
 	default:
diff --git a/cli/main.go b/cli/main.go
index 6a717ed5..13ea7911 100644
--- a/cli/main.go
+++ b/cli/main.go
@@ -24,6 +24,7 @@ import (
 	"os/signal"
 	"path/filepath"
 	"regexp"
+	"strings"
 	"syscall"
 	"time"
 
@@ -73,13 +74,21 @@ func init() {
 	if err != nil {
 		fatal(errors.Wrap(err, "unable to get absolute bin path"))
 	}
-	if err := os.Setenv("PATH", fmt.Sprintf("%s:%s", os.Getenv("PATH"), path)); err != nil {
+
+	if err := os.Setenv("PATH", appendPaths(os.Getenv("PATH"), path)); err != nil {
 		panic(err)
 	}
 	// Seed random
 	rand.Seed(time.Now().UnixNano())
 }
 
+func appendPaths(envPath string, path string) string {
+	if envPath == "" {
+		return path
+	}
+	return strings.Join([]string{envPath, path}, string(os.PathListSeparator))
+}
+
 func isContextAgnosticCommand(cmd *cobra.Command) bool {
 	if cmd == nil {
 		return false
diff --git a/formatter/consts.go b/formatter/consts.go
index bdc30b22..0bb06bf7 100644
--- a/formatter/consts.go
+++ b/formatter/consts.go
@@ -19,8 +19,8 @@ package formatter
 const (
 	// JSON is the constant for Json formats on list commands
 	JSON = "json"
-	// TemplateJSON the legacy json formatting value using go template
-	TemplateJSON = "{{json.}}"
+	// TemplateLegacyJSON the legacy json formatting value using go template
+	TemplateLegacyJSON = "{{json.}}"
 	// PRETTY is the constant for default formats on list commands
 	PRETTY = "pretty"
 )
diff --git a/formatter/formatter.go b/formatter/formatter.go
index c3e84af7..4f8df536 100644
--- a/formatter/formatter.go
+++ b/formatter/formatter.go
@@ -32,6 +32,25 @@ func Print(toJSON interface{}, format string, outWriter io.Writer, writerFn func
 	switch strings.ToLower(format) {
 	case PRETTY, "":
 		return PrintPrettySection(outWriter, writerFn, headers...)
+	case TemplateLegacyJSON:
+		switch reflect.TypeOf(toJSON).Kind() {
+		case reflect.Slice:
+			s := reflect.ValueOf(toJSON)
+			for i := 0; i < s.Len(); i++ {
+				obj := s.Index(i).Interface()
+				outJSON, err := ToJSON(obj, "", "")
+				if err != nil {
+					return err
+				}
+				_, _ = fmt.Fprint(outWriter, outJSON)
+			}
+		default:
+			outJSON, err := ToStandardJSON(toJSON)
+			if err != nil {
+				return err
+			}
+			_, _ = fmt.Fprintln(outWriter, outJSON)
+		}
 	case JSON:
 		switch reflect.TypeOf(toJSON).Kind() {
 		case reflect.Slice:


Test patch:
diff --git a/cli/main_test.go b/cli/main_test.go
index c151ce0d..20426de3 100644
--- a/cli/main_test.go
+++ b/cli/main_test.go
@@ -69,3 +69,8 @@ func TestCheckOwnCommand(t *testing.T) {
 	assert.Assert(t, !isContextAgnosticCommand(cmd.LogsCommand()))
 	assert.Assert(t, !isContextAgnosticCommand(cmd.PsCommand()))
 }
+
+func TestAppendPaths(t *testing.T) {
+	assert.Equal(t, appendPaths("", "/bin/path"), "/bin/path")
+	assert.Equal(t, appendPaths("path1", "binaryPath"), "path1"+string(os.PathListSeparator)+"binaryPath")
+}
diff --git a/formatter/formatter_test.go b/formatter/formatter_test.go
index 7b86773f..be7f0bd2 100644
--- a/formatter/formatter_test.go
+++ b/formatter/formatter_test.go
@@ -58,5 +58,16 @@ func TestPrint(t *testing.T) {
 		}
 	}, "NAME", "STATUS"))
 	assert.Equal(t, b.String(), `[{"Name":"myName1","Status":"myStatus1"},{"Name":"myName2","Status":"myStatus2"}]
+`)
+
+	b.Reset()
+	assert.NilError(t, Print(testList, TemplateLegacyJSON, b, func(w io.Writer) {
+		for _, t := range testList {
+			_, _ = fmt.Fprintf(w, "%s\t%s\n", t.Name, t.Status)
+		}
+	}, "NAME", "STATUS"))
+	json := b.String()
+	assert.Equal(t, json, `{"Name":"myName1","Status":"myStatus1"}
+{"Name":"myName2","Status":"myStatus2"}
 `)
 }
diff --git a/tests/aci-e2e/e2e-aci_test.go b/tests/aci-e2e/e2e-aci_test.go
index 9e77e257..1c580c81 100644
--- a/tests/aci-e2e/e2e-aci_test.go
+++ b/tests/aci-e2e/e2e-aci_test.go
@@ -33,6 +33,8 @@ import (
 	"testing"
 	"time"
 
+	"github.com/docker/docker/pkg/fileutils"
+
 	"gotest.tools/v3/assert"
 	is "gotest.tools/v3/assert/cmp"
 	"gotest.tools/v3/icmd"
@@ -434,14 +436,7 @@ func TestContainerRunAttached(t *testing.T) {
 		endpoint = fmt.Sprintf("http://%s:%d", fqdn, port.HostPort)
 
 		assert.Assert(t, !strings.Contains(followLogsProcess.Stdout(), "/test"))
-		checkRequest := func(t poll.LogT) poll.Result {
-			r, _ := http.Get(endpoint + "/test")
-			if r != nil && r.StatusCode == http.StatusNotFound {
-				return poll.Success()
-			}
-			return poll.Continue("waiting for container to serve request")
-		}
-		poll.WaitOn(t, checkRequest, poll.WithDelay(1*time.Second), poll.WithTimeout(60*time.Second))
+		HTTPGetWithRetry(t, endpoint+"/test", http.StatusNotFound, 2*time.Second, 60*time.Second)
 
 		checkLog := func(t poll.LogT) poll.Result {
 			if strings.Contains(followLogsProcess.Stdout(), "/test") {
@@ -619,7 +614,7 @@ func TestUpUpdate(t *testing.T) {
 	)
 	var (
 		singlePortVolumesComposefile = "aci_demo_port_volumes.yaml"
-		multiPortComposefile         = "aci_demo_multi_port.yaml"
+		multiPortComposefile         = "demo_multi_port.yaml"
 	)
 	c := NewParallelE2eCLI(t, binDir)
 	sID, groupID, location := setupTestResourceGroup(t, c)
@@ -634,6 +629,8 @@ func TestUpUpdate(t *testing.T) {
 	t.Cleanup(func() {
 		assert.NilError(t, os.RemoveAll(dstDir))
 	})
+	_, err = fileutils.CopyFile(filepath.Join(filepath.Join("..", "composefiles"), multiPortComposefile), filepath.Join(dstDir, multiPortComposefile))
+	assert.NilError(t, err)
 
 	singlePortVolumesComposefile = filepath.Join(dstDir, singlePortVolumesComposefile)
 	overwriteFileStorageAccount(t, singlePortVolumesComposefile, composeAccountName)
diff --git a/tests/composefiles/aci-demo/aci_demo_multi_port.yaml b/tests/composefiles/demo_multi_port.yaml
similarity index 100%
rename from tests/composefiles/aci-demo/aci_demo_multi_port.yaml
rename to tests/composefiles/demo_multi_port.yaml
diff --git a/tests/e2e/e2e_test.go b/tests/e2e/e2e_test.go
index 01651df9..b79775ea 100644
--- a/tests/e2e/e2e_test.go
+++ b/tests/e2e/e2e_test.go
@@ -86,7 +86,7 @@ func TestContextDefault(t *testing.T) {
 		golden.Assert(t, res.Stdout(), GoldenFile("ls-out-json"))
 
 		res = c.RunDockerCmd("context", "ls", "--format", "{{ json . }}")
-		golden.Assert(t, res.Stdout(), GoldenFile("ls-out-json"))
+		golden.Assert(t, res.Stdout(), GoldenFile("ls-out-legacy-json"))
 	})
 
 	t.Run("inspect", func(t *testing.T) {
diff --git a/tests/e2e/testdata/ls-out-json-windows.golden b/tests/e2e/testdata/ls-out-json-windows.golden
index c1635424..4d2c3306 100644
--- a/tests/e2e/testdata/ls-out-json-windows.golden
+++ b/tests/e2e/testdata/ls-out-json-windows.golden
@@ -1,1 +1,1 @@
-[{"Current":true,"Description":"Current DOCKER_HOST based configuration","DockerEndpoint":"npipe:////./pipe/docker_engine","KubernetesEndpoint":"","Type":"moby","Name":"default","StackOrchestrator":"swarm"}]
+[{"Current":true,"Description":"Current DOCKER_HOST based configuration","DockerEndpoint":"npipe:////./pipe/docker_engine","KubernetesEndpoint":"","ContextType":"moby","Name":"default","StackOrchestrator":"swarm"}]
diff --git a/tests/e2e/testdata/ls-out-json.golden b/tests/e2e/testdata/ls-out-json.golden
index 82882753..c387b2cd 100644
--- a/tests/e2e/testdata/ls-out-json.golden
+++ b/tests/e2e/testdata/ls-out-json.golden
@@ -1,1 +1,1 @@
-[{"Current":true,"Description":"Current DOCKER_HOST based configuration","DockerEndpoint":"unix:///var/run/docker.sock","KubernetesEndpoint":"","Type":"moby","Name":"default","StackOrchestrator":"swarm"}]
+[{"Current":true,"Description":"Current DOCKER_HOST based configuration","DockerEndpoint":"unix:///var/run/docker.sock","KubernetesEndpoint":"","ContextType":"moby","Name":"default","StackOrchestrator":"swarm"}]
diff --git a/tests/e2e/testdata/ls-out-legacy-json-windows.golden b/tests/e2e/testdata/ls-out-legacy-json-windows.golden
new file mode 100644
index 00000000..8417af14
--- /dev/null
+++ b/tests/e2e/testdata/ls-out-legacy-json-windows.golden
@@ -0,0 +1,1 @@
+{"Current":true,"Description":"Current DOCKER_HOST based configuration","DockerEndpoint":"npipe:////./pipe/docker_engine","KubernetesEndpoint":"","ContextType":"moby","Name":"default","StackOrchestrator":"swarm"}
diff --git a/tests/e2e/testdata/ls-out-legacy-json.golden b/tests/e2e/testdata/ls-out-legacy-json.golden
new file mode 100644
index 00000000..dadb250b
--- /dev/null
+++ b/tests/e2e/testdata/ls-out-legacy-json.golden
@@ -0,0 +1,1 @@
+{"Current":true,"Description":"Current DOCKER_HOST based configuration","DockerEndpoint":"unix:///var/run/docker.sock","KubernetesEndpoint":"","ContextType":"moby","Name":"default","StackOrchestrator":"swarm"}
diff --git a/tests/ecs-e2e/e2e-ecs_test.go b/tests/ecs-e2e/e2e-ecs_test.go
index 9acd30be..23e7d78c 100644
--- a/tests/ecs-e2e/e2e-ecs_test.go
+++ b/tests/ecs-e2e/e2e-ecs_test.go
@@ -81,22 +81,41 @@ func TestCompose(t *testing.T) {
 	c, stack := setupTest(t)
 
 	t.Run("compose up", func(t *testing.T) {
-		c.RunDockerCmd("compose", "up", "--project-name", stack, "-f", "../composefiles/nginx.yaml")
+		c.RunDockerCmd("compose", "up", "--project-name", stack, "-f", "../composefiles/demo_multi_port.yaml")
 	})
 
-	var url string
+	var webURL, wordsURL string
 	t.Run("compose ps", func(t *testing.T) {
 		res := c.RunDockerCmd("compose", "ps", "--project-name", stack)
-		lines := strings.Split(res.Stdout(), "\n")
+		fmt.Println(strings.TrimSpace(res.Stdout()))
+		lines := strings.Split(strings.TrimSpace(res.Stdout()), "\n")
 
-		assert.Equal(t, 3, len(lines))
-		fields := strings.Fields(lines[1])
-		assert.Equal(t, 4, len(fields))
-		assert.Check(t, strings.Contains(fields[0], stack))
-		assert.Equal(t, "nginx", fields[1])
-		assert.Equal(t, "1/1", fields[2])
-		assert.Check(t, strings.Contains(fields[3], "->80/http"))
-		url = "http://" + strings.Replace(fields[3], "->80/http", "", 1)
+		assert.Equal(t, 4, len(lines))
+
+		var dbDisplayed, wordsDisplayed, webDisplayed bool
+		for _, line := range lines {
+			fields := strings.Fields(line)
+			containerID := fields[0]
+			serviceName := fields[1]
+			switch serviceName {
+			case "db":
+				dbDisplayed = true
+				assert.DeepEqual(t, fields, []string{containerID, serviceName, "1/1"})
+			case "words":
+				wordsDisplayed = true
+				assert.Check(t, strings.Contains(fields[3], ":8080->8080/tcp"))
+				wordsURL = "http://" + strings.Replace(fields[3], "->8080/tcp", "", 1) + "/noun"
+			case "web":
+				webDisplayed = true
+				assert.Equal(t, fields[1], "web")
+				assert.Check(t, strings.Contains(fields[3], ":80->80/tcp"))
+				webURL = "http://" + strings.Replace(fields[3], "->80/tcp", "", 1)
+			}
+		}
+
+		assert.Check(t, dbDisplayed)
+		assert.Check(t, wordsDisplayed)
+		assert.Check(t, webDisplayed)
 	})
 
 	t.Run("compose ls", func(t *testing.T) {
@@ -110,21 +129,17 @@ func TestCompose(t *testing.T) {
 		assert.Equal(t, "Running", fields[1])
 	})
 
-	t.Run("nginx GET", func(t *testing.T) {
-		checkUp := func(t poll.LogT) poll.Result {
-			r, err := http.Get(url)
-			if err != nil {
-				return poll.Continue("Err while getting %s : %v", url, err)
-			} else if r.StatusCode != http.StatusOK {
-				return poll.Continue("status %s while getting %s", r.Status, url)
-			}
-			b, err := ioutil.ReadAll(r.Body)
-			if err == nil && strings.Contains(string(b), "Welcome to nginx!") {
-				return poll.Success()
-			}
-			return poll.Error(fmt.Errorf("No nginx welcome page received at %s: \n%s", url, string(b)))
-		}
-		poll.WaitOn(t, checkUp, poll.WithDelay(2*time.Second), poll.WithTimeout(60*time.Second))
+	t.Run("Words GET validating cross service connection", func(t *testing.T) {
+		out := HTTPGetWithRetry(t, wordsURL, http.StatusOK, 5*time.Second, 180*time.Second)
+		assert.Assert(t, strings.Contains(out, `"word":`))
+	})
+
+	t.Run("web app GET", func(t *testing.T) {
+		out := HTTPGetWithRetry(t, webURL, http.StatusOK, 3*time.Second, 120*time.Second)
+		assert.Assert(t, strings.Contains(out, "Docker Compose demo"))
+
+		out = HTTPGetWithRetry(t, webURL+"/words/noun", http.StatusOK, 2*time.Second, 60*time.Second)
+		assert.Assert(t, strings.Contains(out, `"word":`))
 	})
 
 	t.Run("compose down", func(t *testing.T) {