소스 검색

Merge pull request #9797 from laurazard/start-only-services

 Only attempt to start specified services on `compose start [services]`
Laura Brehm 3 년 전
부모
커밋
88df5ede42
7개의 변경된 파일248개의 추가작업 그리고 10개의 파일을 삭제
  1. 1 0
      cmd/compose/start.go
  2. 2 0
      pkg/api/api.go
  3. 17 10
      pkg/compose/dependencies.go
  4. 180 0
      pkg/compose/dependencies_test.go
  5. 7 0
      pkg/compose/start.go
  6. 17 0
      pkg/e2e/fixtures/start-stop/start-stop-deps.yaml
  7. 24 0
      pkg/e2e/start_stop_test.go

+ 1 - 0
cmd/compose/start.go

@@ -51,5 +51,6 @@ func runStart(ctx context.Context, backend api.Service, opts startOptions, servi
 	return backend.Start(ctx, name, api.StartOptions{
 		AttachTo: services,
 		Project:  project,
+		Services: services,
 	})
 }

+ 2 - 0
pkg/api/api.go

@@ -129,6 +129,8 @@ type StartOptions struct {
 	ExitCodeFrom string
 	// Wait won't return until containers reached the running|healthy state
 	Wait bool
+	// Services passed in the command line to be started
+	Services []string
 }
 
 // RestartOptions group options of the Restart API

+ 17 - 10
pkg/compose/dependencies.go

@@ -63,21 +63,24 @@ var (
 )
 
 // InDependencyOrder applies the function to the services of the project taking in account the dependency order
-func InDependencyOrder(ctx context.Context, project *types.Project, fn func(context.Context, string) error) error {
-	return visit(ctx, project, upDirectionTraversalConfig, fn, ServiceStopped)
+func InDependencyOrder(ctx context.Context, project *types.Project, fn func(context.Context, string) error, options ...func(*graphTraversalConfig)) error {
+	graph, err := NewGraph(project.Services, ServiceStopped)
+	if err != nil {
+		return err
+	}
+	return visit(ctx, graph, upDirectionTraversalConfig, fn)
 }
 
 // InReverseDependencyOrder applies the function to the services of the project in reverse order of dependencies
 func InReverseDependencyOrder(ctx context.Context, project *types.Project, fn func(context.Context, string) error) error {
-	return visit(ctx, project, downDirectionTraversalConfig, fn, ServiceStarted)
-}
-
-func visit(ctx context.Context, project *types.Project, traversalConfig graphTraversalConfig, fn func(context.Context, string) error, initialStatus ServiceStatus) error {
-	g := NewGraph(project.Services, initialStatus)
-	if b, err := g.HasCycles(); b {
+	graph, err := NewGraph(project.Services, ServiceStarted)
+	if err != nil {
 		return err
 	}
+	return visit(ctx, graph, downDirectionTraversalConfig, fn)
+}
 
+func visit(ctx context.Context, g *Graph, traversalConfig graphTraversalConfig, fn func(context.Context, string) error) error {
 	nodes := traversalConfig.extremityNodesFn(g)
 
 	eg, _ := errgroup.WithContext(ctx)
@@ -155,7 +158,7 @@ func (v *Vertex) GetChildren() []*Vertex {
 }
 
 // NewGraph returns the dependency graph of the services
-func NewGraph(services types.Services, initialStatus ServiceStatus) *Graph {
+func NewGraph(services types.Services, initialStatus ServiceStatus) (*Graph, error) {
 	graph := &Graph{
 		lock:     sync.RWMutex{},
 		Vertices: map[string]*Vertex{},
@@ -171,7 +174,11 @@ func NewGraph(services types.Services, initialStatus ServiceStatus) *Graph {
 		}
 	}
 
-	return graph
+	if b, err := graph.HasCycles(); b {
+		return nil, err
+	}
+
+	return graph, nil
 }
 
 // NewVertex is the constructor function for the Vertex

+ 180 - 0
pkg/compose/dependencies_test.go

@@ -18,10 +18,12 @@ package compose
 
 import (
 	"context"
+	"fmt"
 	"testing"
 
 	"github.com/compose-spec/compose-go/types"
 	"github.com/stretchr/testify/require"
+	"gotest.tools/assert"
 )
 
 var project = types.Project{
@@ -69,3 +71,181 @@ func TestInDependencyReverseDownCommandOrder(t *testing.T) {
 	require.NoError(t, err, "Error during iteration")
 	require.Equal(t, []string{"test1", "test2", "test3"}, order)
 }
+
+func TestBuildGraph(t *testing.T) {
+	testCases := []struct {
+		desc             string
+		services         types.Services
+		expectedVertices map[string]*Vertex
+	}{
+		{
+			desc: "builds graph with single service",
+			services: types.Services{
+				{
+					Name:      "test",
+					DependsOn: types.DependsOnConfig{},
+				},
+			},
+			expectedVertices: map[string]*Vertex{
+				"test": {
+					Key:      "test",
+					Service:  "test",
+					Status:   ServiceStopped,
+					Children: map[string]*Vertex{},
+					Parents:  map[string]*Vertex{},
+				},
+			},
+		},
+		{
+			desc: "builds graph with two separate services",
+			services: types.Services{
+				{
+					Name:      "test",
+					DependsOn: types.DependsOnConfig{},
+				},
+				{
+					Name:      "another",
+					DependsOn: types.DependsOnConfig{},
+				},
+			},
+			expectedVertices: map[string]*Vertex{
+				"test": {
+					Key:      "test",
+					Service:  "test",
+					Status:   ServiceStopped,
+					Children: map[string]*Vertex{},
+					Parents:  map[string]*Vertex{},
+				},
+				"another": {
+					Key:      "another",
+					Service:  "another",
+					Status:   ServiceStopped,
+					Children: map[string]*Vertex{},
+					Parents:  map[string]*Vertex{},
+				},
+			},
+		},
+		{
+			desc: "builds graph with a service and a dependency",
+			services: types.Services{
+				{
+					Name: "test",
+					DependsOn: types.DependsOnConfig{
+						"another": types.ServiceDependency{},
+					},
+				},
+				{
+					Name:      "another",
+					DependsOn: types.DependsOnConfig{},
+				},
+			},
+			expectedVertices: map[string]*Vertex{
+				"test": {
+					Key:     "test",
+					Service: "test",
+					Status:  ServiceStopped,
+					Children: map[string]*Vertex{
+						"another": {},
+					},
+					Parents: map[string]*Vertex{},
+				},
+				"another": {
+					Key:      "another",
+					Service:  "another",
+					Status:   ServiceStopped,
+					Children: map[string]*Vertex{},
+					Parents: map[string]*Vertex{
+						"test": {},
+					},
+				},
+			},
+		},
+		{
+			desc: "builds graph with multiple dependency levels",
+			services: types.Services{
+				{
+					Name: "test",
+					DependsOn: types.DependsOnConfig{
+						"another": types.ServiceDependency{},
+					},
+				},
+				{
+					Name: "another",
+					DependsOn: types.DependsOnConfig{
+						"another_dep": types.ServiceDependency{},
+					},
+				},
+				{
+					Name:      "another_dep",
+					DependsOn: types.DependsOnConfig{},
+				},
+			},
+			expectedVertices: map[string]*Vertex{
+				"test": {
+					Key:     "test",
+					Service: "test",
+					Status:  ServiceStopped,
+					Children: map[string]*Vertex{
+						"another": {},
+					},
+					Parents: map[string]*Vertex{},
+				},
+				"another": {
+					Key:     "another",
+					Service: "another",
+					Status:  ServiceStopped,
+					Children: map[string]*Vertex{
+						"another_dep": {},
+					},
+					Parents: map[string]*Vertex{
+						"test": {},
+					},
+				},
+				"another_dep": {
+					Key:      "another_dep",
+					Service:  "another_dep",
+					Status:   ServiceStopped,
+					Children: map[string]*Vertex{},
+					Parents: map[string]*Vertex{
+						"another": {},
+					},
+				},
+			},
+		},
+	}
+	for _, tC := range testCases {
+		t.Run(tC.desc, func(t *testing.T) {
+			project := types.Project{
+				Services: tC.services,
+			}
+
+			graph, err := NewGraph(project.Services, ServiceStopped)
+			assert.NilError(t, err, fmt.Sprintf("failed to build graph for: %s", tC.desc))
+
+			for k, vertex := range graph.Vertices {
+				expected, ok := tC.expectedVertices[k]
+				assert.Equal(t, true, ok)
+				assert.Equal(t, true, isVertexEqual(*expected, *vertex))
+			}
+		})
+	}
+}
+
+func isVertexEqual(a, b Vertex) bool {
+	childrenEquality := true
+	for c := range a.Children {
+		if _, ok := b.Children[c]; !ok {
+			childrenEquality = false
+		}
+	}
+	parentEquality := true
+	for p := range a.Parents {
+		if _, ok := b.Parents[p]; !ok {
+			parentEquality = false
+		}
+	}
+	return a.Key == b.Key &&
+		a.Service == b.Service &&
+		childrenEquality &&
+		parentEquality
+}

+ 7 - 0
pkg/compose/start.go

@@ -50,6 +50,13 @@ func (s *composeService) start(ctx context.Context, projectName string, options
 		}
 	}
 
+	if len(options.Services) > 0 {
+		err := project.ForServices(options.Services)
+		if err != nil {
+			return err
+		}
+	}
+
 	eg, ctx := errgroup.WithContext(ctx)
 	if listener != nil {
 		attached, err := s.attach(ctx, project, listener, options.AttachTo)

+ 17 - 0
pkg/e2e/fixtures/start-stop/start-stop-deps.yaml

@@ -0,0 +1,17 @@
+services:
+  another_2:
+    image:  nginx:alpine
+  another:
+    image:  nginx:alpine
+    depends_on:
+    - another_2
+  dep_2:
+    image:  nginx:alpine
+  dep_1:
+    image:  nginx:alpine
+    depends_on:
+    - dep_2
+  desired:
+    image:  nginx:alpine
+    depends_on:
+    - dep_1

+ 24 - 0
pkg/e2e/start_stop_test.go

@@ -247,6 +247,30 @@ func TestStartStopMultipleServices(t *testing.T) {
 	}
 }
 
+func TestStartSingleServiceAndDependency(t *testing.T) {
+	cli := NewParallelCLI(t, WithEnv(
+		"COMPOSE_PROJECT_NAME=e2e-start-single-deps",
+		"COMPOSE_FILE=./fixtures/start-stop/start-stop-deps.yaml"))
+	t.Cleanup(func() {
+		cli.RunDockerComposeCmd(t, "down", "--remove-orphans", "-v", "-t", "0")
+	})
+
+	cli.RunDockerComposeCmd(t, "create", "desired")
+
+	res := cli.RunDockerComposeCmd(t, "start", "desired")
+	desiredServices := []string{"desired", "dep_1", "dep_2"}
+	for _, s := range desiredServices {
+		startMsg := fmt.Sprintf("Container e2e-start-single-deps-%s-1  Started", s)
+		assert.Assert(t, strings.Contains(res.Combined(), startMsg),
+			fmt.Sprintf("Missing start message for service: %s\n%s", s, res.Combined()))
+	}
+	undesiredServices := []string{"another", "another_2"}
+	for _, s := range undesiredServices {
+		assert.Assert(t, !strings.Contains(res.Combined(), s),
+			fmt.Sprintf("Shouldn't have message for service: %s\n%s", s, res.Combined()))
+	}
+}
+
 func TestStartStopMultipleFiles(t *testing.T) {
 	cli := NewParallelCLI(t, WithEnv("COMPOSE_PROJECT_NAME=e2e-start-stop-svc-multiple-files"))
 	t.Cleanup(func() {