Repository: docker/compose-cli
Pull request #980 (2020-11-27 13:52:46)

filter compose project to remove all services not required by command
**What I did**
`up` can get a subset of compose services to run as arg.
for this purpose, the compose app model is "filtered" to remove all unrelated services, so that the implementation code doesn't have to handle this.

*(not mandatory) A picture of a cute animal, if possible in relation with what you did**
![image](https://user-images.githubusercontent.com/132757/100455895-c8c25d80-30bf-11eb-9ba4-f7e0ee8d4bcc.png)



Patch:
diff --git a/cli/cmd/compose/compose.go b/cli/cmd/compose/compose.go
index 76eda7d9..9108dc88 100644
--- a/cli/cmd/compose/compose.go
+++ b/cli/cmd/compose/compose.go
@@ -20,9 +20,9 @@ import (
 	"context"
 
 	"github.com/compose-spec/compose-go/cli"
-	"github.com/spf13/pflag"
-
+	"github.com/compose-spec/compose-go/types"
 	"github.com/spf13/cobra"
+	"github.com/spf13/pflag"
 
 	"github.com/docker/compose-cli/api/client"
 	"github.com/docker/compose-cli/errdefs"
@@ -100,3 +100,40 @@ func checkComposeSupport(ctx context.Context) error {
 
 	return err
 }
+
+//
+func filter(project *types.Project, services []string) error {
+	if len(services) == 0 {
+		// All services
+		return nil
+	}
+
+	names := map[string]bool{}
+	err := addServiceNames(project, services, names)
+	if err != nil {
+		return err
+	}
+	var filtered types.Services
+	for _, s := range project.Services {
+		if _, ok := names[s.Name]; ok {
+			filtered = append(filtered, s)
+		}
+	}
+	project.Services = filtered
+	return nil
+}
+
+func addServiceNames(project *types.Project, services []string, names map[string]bool) error {
+	for _, name := range services {
+		names[name] = true
+		service, err := project.GetService(name)
+		if err != nil {
+			return err
+		}
+		err = addServiceNames(project, service.GetDependencies(), names)
+		if err != nil {
+			return err
+		}
+	}
+	return nil
+}
diff --git a/cli/cmd/compose/up.go b/cli/cmd/compose/up.go
index 359810f7..aabdf578 100644
--- a/cli/cmd/compose/up.go
+++ b/cli/cmd/compose/up.go
@@ -19,9 +19,8 @@ package compose
 import (
 	"context"
 
-	"github.com/spf13/cobra"
-
 	"github.com/compose-spec/compose-go/cli"
+	"github.com/spf13/cobra"
 
 	"github.com/docker/compose-cli/api/client"
 	"github.com/docker/compose-cli/context/store"
@@ -31,9 +30,9 @@ import (
 func upCommand(contextType string) *cobra.Command {
 	opts := composeOptions{}
 	upCmd := &cobra.Command{
-		Use: "up",
+		Use: "up [SERVICE...]",
 		RunE: func(cmd *cobra.Command, args []string) error {
-			return runUp(cmd.Context(), opts)
+			return runUp(cmd.Context(), opts, args)
 		},
 	}
 	upCmd.Flags().StringVarP(&opts.Name, "project-name", "p", "", "Project name")
@@ -49,7 +48,7 @@ func upCommand(contextType string) *cobra.Command {
 	return upCmd
 }
 
-func runUp(ctx context.Context, opts composeOptions) error {
+func runUp(ctx context.Context, opts composeOptions, services []string) error {
 	c, err := client.New(ctx)
 	if err != nil {
 		return err
@@ -61,10 +60,15 @@ func runUp(ctx context.Context, opts composeOptions) error {
 			return "", err
 		}
 		project, err := cli.ProjectFromOptions(options)
+		if err != nil {
+			return "", err
+		}
 		if opts.DomainName != "" {
 			//arbitrarily set the domain name on the first service ; ACI backend will expose the entire project
 			project.Services[0].DomainName = opts.DomainName
 		}
+
+		err = filter(project, services)
 		if err != nil {
 			return "", err
 		}
diff --git a/local/build.go b/local/build.go
index c2cf2827..50300069 100644
--- a/local/build.go
+++ b/local/build.go
@@ -85,9 +85,6 @@ func (s *composeService) needPull(ctx context.Context, service types.ServiceConf
 }
 
 func (s *composeService) build(ctx context.Context, project *types.Project, opts map[string]build.Options) error {
-	if len(opts) == 0 {
-		return nil
-	}
 	const drivername = "default"
 	d, err := driver.GetDriver(ctx, drivername, nil, s.apiClient, nil, nil, "", nil, project.WorkingDir)
 	if err != nil {
diff --git a/local/compose.go b/local/compose.go
index 4651492f..d6f72caa 100644
--- a/local/compose.go
+++ b/local/compose.go
@@ -54,10 +54,6 @@ type composeService struct {
 }
 
 func (s *composeService) Up(ctx context.Context, project *types.Project, detach bool) error {
-	err := s.ensureImagesExists(ctx, project)
-	if err != nil {
-		return err
-	}
 	for k, network := range project.Networks {
 		if !network.External.External && network.Name != "" {
 			network.Name = fmt.Sprintf("%s_%s", project.Name, k)
@@ -86,6 +82,11 @@ func (s *composeService) Up(ctx context.Context, project *types.Project, detach
 		}
 	}
 
+	err := s.ensureImagesExists(ctx, project)
+	if err != nil {
+		return err
+	}
+
 	err = inDependencyOrder(ctx, project, func(c context.Context, service types.ServiceConfig) error {
 		return s.ensureService(c, project, service)
 	})
@@ -143,25 +144,6 @@ func (s *composeService) Down(ctx context.Context, projectName string) error {
 			return nil
 		})
 	}
-	err = eg.Wait()
-	if err != nil {
-		return err
-	}
-	networks, err := s.apiClient.NetworkList(ctx, moby.NetworkListOptions{
-		Filters: filters.NewArgs(
-			projectFilter(projectName),
-		),
-	})
-	if err != nil {
-		return err
-	}
-	for _, network := range networks {
-		networkID := network.ID
-		networkName := network.Name
-		eg.Go(func() error {
-			return s.ensureNetworkDown(ctx, networkID, networkName)
-		})
-	}
 	return eg.Wait()
 }
 
@@ -612,32 +594,6 @@ func (s *composeService) ensureNetwork(ctx context.Context, n types.NetworkConfi
 	return nil
 }
 
-func (s *composeService) ensureNetworkDown(ctx context.Context, networkID string, networkName string) error {
-	w := progress.ContextWriter(ctx)
-	w.Event(progress.Event{
-		ID:         fmt.Sprintf("Network %q", networkName),
-		Status:     progress.Working,
-		StatusText: "Delete",
-	})
-
-	if err := s.apiClient.NetworkRemove(ctx, networkID); err != nil {
-		msg := fmt.Sprintf("failed to create network %s", networkID)
-		w.Event(progress.Event{
-			ID:         fmt.Sprintf("Network %q", networkName),
-			Status:     progress.Error,
-			StatusText: "Error: " + msg,
-		})
-		return errors.Wrapf(err, msg)
-	}
-
-	w.Event(progress.Event{
-		ID:         fmt.Sprintf("Network %q", networkName),
-		Status:     progress.Done,
-		StatusText: "Deleted",
-	})
-	return nil
-}
-
 func (s *composeService) ensureVolume(ctx context.Context, volume types.VolumeConfig) error {
 	// TODO could identify volume by label vs name
 	_, err := s.apiClient.VolumeInspect(ctx, volume.Name)


Test patch:
diff --git a/cli/cmd/compose/compose_test.go b/cli/cmd/compose/compose_test.go
new file mode 100644
index 00000000..49308d70
--- /dev/null
+++ b/cli/cmd/compose/compose_test.go
@@ -0,0 +1,53 @@
+/*
+   Copyright 2020 Docker Compose CLI authors
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
+*/
+
+package compose
+
+import (
+	"testing"
+
+	"github.com/compose-spec/compose-go/types"
+	"gotest.tools/v3/assert"
+)
+
+func TestFilterServices(t *testing.T) {
+	p := types.Project{
+		Services: []types.ServiceConfig{
+			{
+				Name:  "foo",
+				Links: []string{"bar"},
+			},
+			{
+				Name:        "bar",
+				NetworkMode: "service:zot",
+			},
+			{
+				Name: "zot",
+			},
+			{
+				Name: "qix",
+			},
+		},
+	}
+	err := filter(&p, []string{"bar"})
+	assert.NilError(t, err)
+
+	assert.Equal(t, len(p.Services), 2)
+	_, err = p.GetService("bar")
+	assert.NilError(t, err)
+	_, err = p.GetService("zot")
+	assert.NilError(t, err)
+}
diff --git a/local/e2e/compose_test.go b/local/e2e/compose_test.go
index 5507c29f..8aa477a7 100644
--- a/local/e2e/compose_test.go
+++ b/local/e2e/compose_test.go
@@ -35,11 +35,12 @@ func TestLocalBackendComposeUp(t *testing.T) {
 
 	const projectName = "compose-e2e-demo"
 
-	networkList := c.RunDockerCmd("--context", "default", "network", "ls")
-
 	t.Run("up", func(t *testing.T) {
 		c.RunDockerCmd("compose", "up", "-f", "../../tests/composefiles/demo_multi_port.yaml", "--project-name", projectName)
 	})
+	t.Cleanup(func() {
+		_ = c.RunDockerCmd("compose", "down", "--project-name", projectName)
+	})
 
 	t.Run("check running project", func(t *testing.T) {
 		res := c.RunDockerCmd("compose", "ps", "-p", projectName)
@@ -48,9 +49,6 @@ func TestLocalBackendComposeUp(t *testing.T) {
 		endpoint := "http://localhost:80"
 		output := HTTPGetWithRetry(t, endpoint+"/words/noun", http.StatusOK, 2*time.Second, 20*time.Second)
 		assert.Assert(t, strings.Contains(output, `"word":`))
-
-		res = c.RunDockerCmd("--context", "default", "network", "ls")
-		assert.Equal(t, len(Lines(res.Stdout())), len(Lines(networkList.Stdout()))+1)
 	})
 
 	t.Run("check compose labels", func(t *testing.T) {
@@ -69,13 +67,4 @@ func TestLocalBackendComposeUp(t *testing.T) {
 		res.Assert(t, icmd.Expected{Out: `"com.docker.compose.project": `})
 		res.Assert(t, icmd.Expected{Out: `"com.docker.compose.version": `})
 	})
-
-	t.Run("down", func(t *testing.T) {
-		_ = c.RunDockerCmd("compose", "down", "--project-name", projectName)
-	})
-
-	t.Run("check compose labels", func(t *testing.T) {
-		networksAfterDown := c.RunDockerCmd("--context", "default", "network", "ls")
-		assert.Equal(t, networkList.Stdout(), networksAfterDown.Stdout())
-	})
 }
diff --git a/tests/aci-e2e/e2e-aci_test.go b/tests/aci-e2e/e2e-aci_test.go
index ccc950ee..8c860a55 100644
--- a/tests/aci-e2e/e2e-aci_test.go
+++ b/tests/aci-e2e/e2e-aci_test.go
@@ -206,7 +206,7 @@ func TestRunVolume(t *testing.T) {
 	t.Cleanup(func() {
 		c.RunDockerCmd("volume", "rm", volumeID)
 		res := c.RunDockerCmd("volume", "ls")
-		lines := Lines(res.Stdout())
+		lines := lines(res.Stdout())
 		assert.Equal(t, len(lines), 1)
 	})
 
@@ -217,13 +217,13 @@ func TestRunVolume(t *testing.T) {
 
 	t.Run("list volumes", func(t *testing.T) {
 		res := c.RunDockerCmd("volume", "ls", "--quiet")
-		l := Lines(res.Stdout())
+		l := lines(res.Stdout())
 		assert.Equal(t, len(l), 2)
 		assert.Equal(t, l[0], volumeID)
 		assert.Equal(t, l[1], volumeID2)
 
 		res = c.RunDockerCmd("volume", "ls")
-		l = Lines(res.Stdout())
+		l = lines(res.Stdout())
 		assert.Equal(t, len(l), 3)
 		firstAccount := l[1]
 		fields := strings.Fields(firstAccount)
@@ -251,7 +251,7 @@ func TestRunVolume(t *testing.T) {
 	t.Run("delete only fileshare", func(t *testing.T) {
 		c.RunDockerCmd("volume", "rm", volumeID2)
 		res := c.RunDockerCmd("volume", "ls")
-		lines := Lines(res.Stdout())
+		lines := lines(res.Stdout())
 		assert.Equal(t, len(lines), 2)
 		assert.Assert(t, !strings.Contains(res.Stdout(), fileshareName2), "second fileshare still visible after rm")
 	})
@@ -288,7 +288,7 @@ func TestRunVolume(t *testing.T) {
 
 	t.Run("ps", func(t *testing.T) {
 		res := c.RunDockerCmd("ps")
-		out := Lines(res.Stdout())
+		out := lines(res.Stdout())
 		l := out[len(out)-1]
 		assert.Assert(t, strings.Contains(l, container), "Looking for %q in line: %s", container, l)
 		assert.Assert(t, strings.Contains(l, "nginx"))
@@ -379,6 +379,10 @@ func TestRunVolume(t *testing.T) {
 	})
 }
 
+func lines(output string) []string {
+	return strings.Split(strings.TrimSpace(output), "\n")
+}
+
 func TestContainerRunAttached(t *testing.T) {
 	c := NewParallelE2eCLI(t, binDir)
 	_, groupID, location := setupTestResourceGroup(t, c)
@@ -468,11 +472,11 @@ func TestContainerRunAttached(t *testing.T) {
 
 	t.Run("ps stopped container with --all", func(t *testing.T) {
 		res := c.RunDockerCmd("ps", container)
-		out := Lines(res.Stdout())
+		out := lines(res.Stdout())
 		assert.Assert(t, is.Len(out, 1))
 
 		res = c.RunDockerCmd("ps", "--all", container)
-		out = Lines(res.Stdout())
+		out = lines(res.Stdout())
 		assert.Assert(t, is.Len(out, 2))
 	})
 
@@ -493,14 +497,14 @@ func TestContainerRunAttached(t *testing.T) {
 		res := c.RunDockerCmd("prune")
 		assert.Equal(t, "Deleted resources:\nTotal CPUs reclaimed: 0.00, total memory reclaimed: 0.00 GB\n", res.Stdout())
 		res = c.RunDockerCmd("ps")
-		l := Lines(res.Stdout())
+		l := lines(res.Stdout())
 		assert.Equal(t, 2, len(l))
 
 		res = c.RunDockerCmd("prune", "--force")
 		assert.Equal(t, "Deleted resources:\n"+container+"\nTotal CPUs reclaimed: 0.10, total memory reclaimed: 0.10 GB\n", res.Stdout())
 
 		res = c.RunDockerCmd("ps", "--all")
-		l = Lines(res.Stdout())
+		l = lines(res.Stdout())
 		assert.Equal(t, 1, len(l))
 	})
 }
@@ -535,7 +539,7 @@ func TestUpSecretsResources(t *testing.T) {
 	t.Run("compose up", func(t *testing.T) {
 		c.RunDockerCmd("compose", "up", "-f", composefilePath, "--project-name", composeProjectName)
 		res := c.RunDockerCmd("ps")
-		out := Lines(res.Stdout())
+		out := lines(res.Stdout())
 		// Check 2 containers running
 		assert.Assert(t, is.Len(out, 3))
 	})
@@ -543,7 +547,7 @@ func TestUpSecretsResources(t *testing.T) {
 	t.Cleanup(func() {
 		c.RunDockerCmd("compose", "down", "--project-name", composeProjectName)
 		res := c.RunDockerCmd("ps")
-		out := Lines(res.Stdout())
+		out := lines(res.Stdout())
 		assert.Equal(t, len(out), 1)
 	})
 
@@ -685,7 +689,7 @@ func TestUpUpdate(t *testing.T) {
 
 	t.Run("check deployed compose app", func(t *testing.T) {
 		res := c.RunDockerCmd("ps")
-		out := Lines(res.Stdout())
+		out := lines(res.Stdout())
 		// Check three containers are running
 		assert.Assert(t, is.Len(out, 4))
 		webRunning := false
@@ -725,11 +729,11 @@ func TestUpUpdate(t *testing.T) {
 
 	t.Run("compose ps", func(t *testing.T) {
 		res := c.RunDockerCmd("compose", "ps", "--project-name", composeProjectName, "--quiet")
-		l := Lines(res.Stdout())
+		l := lines(res.Stdout())
 		assert.Assert(t, is.Len(l, 3))
 
 		res = c.RunDockerCmd("compose", "ps", "--project-name", composeProjectName)
-		l = Lines(res.Stdout())
+		l = lines(res.Stdout())
 		assert.Assert(t, is.Len(l, 4))
 		var wordsDisplayed, webDisplayed, dbDisplayed bool
 		for _, line := range l {
@@ -753,10 +757,10 @@ func TestUpUpdate(t *testing.T) {
 
 	t.Run("compose ls", func(t *testing.T) {
 		res := c.RunDockerCmd("compose", "ls", "--quiet")
-		l := Lines(res.Stdout())
+		l := lines(res.Stdout())
 		assert.Assert(t, is.Len(l, 1))
 		res = c.RunDockerCmd("compose", "ls")
-		l = Lines(res.Stdout())
+		l = lines(res.Stdout())
 
 		assert.Equal(t, 2, len(l))
 		fields := strings.Fields(l[1])
@@ -773,7 +777,7 @@ func TestUpUpdate(t *testing.T) {
 	t.Run("update", func(t *testing.T) {
 		c.RunDockerCmd("compose", "up", "-f", multiPortComposefile, "--project-name", composeProjectName)
 		res := c.RunDockerCmd("ps")
-		out := Lines(res.Stdout())
+		out := lines(res.Stdout())
 		// Check three containers are running
 		assert.Assert(t, is.Len(out, 4))
 
@@ -808,7 +812,7 @@ func TestUpUpdate(t *testing.T) {
 	t.Run("down", func(t *testing.T) {
 		c.RunDockerCmd("compose", "down", "--project-name", composeProjectName)
 		res := c.RunDockerCmd("ps")
-		out := Lines(res.Stdout())
+		out := lines(res.Stdout())
 		assert.Equal(t, len(out), 1)
 	})
 }
@@ -831,7 +835,7 @@ func TestRunEnvVars(t *testing.T) {
 		cmd.Env = append(cmd.Env, "MYSQL_USER=user1")
 		res := icmd.RunCmd(cmd)
 		res.Assert(t, icmd.Success)
-		out := Lines(res.Stdout())
+		out := lines(res.Stdout())
 		container := strings.TrimSpace(out[len(out)-1])
 
 		res = c.RunDockerCmd("inspect", container)
@@ -878,7 +882,7 @@ func setupTestResourceGroup(t *testing.T, c *E2eCLI) (string, string, string) {
 	createAciContextAndUseIt(t, c, sID, rg, location)
 	// Check nothing is running
 	res := c.RunDockerCmd("ps")
-	assert.Assert(t, is.Len(Lines(res.Stdout()), 1))
+	assert.Assert(t, is.Len(lines(res.Stdout()), 1))
 	return sID, rg, location
 }
 
@@ -941,7 +945,7 @@ func uploadFile(t *testing.T, cred azfile.SharedKeyCredential, baseURL, fileName
 }
 
 func getContainerName(stdout string) string {
-	out := Lines(stdout)
+	out := lines(stdout)
 	return strings.TrimSpace(out[len(out)-1])
 }
 
diff --git a/tests/framework/e2e.go b/tests/framework/e2e.go
index 5844806f..edf450c0 100644
--- a/tests/framework/e2e.go
+++ b/tests/framework/e2e.go
@@ -192,11 +192,6 @@ func GoldenFile(name string) string {
 	return name + ".golden"
 }
 
-//Lines split output into lines
-func Lines(output string) []string {
-	return strings.Split(strings.TrimSpace(output), "\n")
-}
-
 // HTTPGetWithRetry performs an HTTP GET on an `endpoint`, using retryDelay also as a request timeout.
 // In the case of an error or the response status is not the expeted one, it retries the same request,
 // returning the response body as a string (empty if we could not reach it)