From cc4f194295f9758ce6bb51fa02215d47e49b1f9e Mon Sep 17 00:00:00 2001 From: Laura Brehm Date: Tue, 30 Aug 2022 12:42:56 +0200 Subject: [PATCH 1/5] Add E2E tests for starting/stopping single services Signed-off-by: Laura Brehm --- .../fixtures/start-stop/start-stop-deps.yaml | 17 +++++++ pkg/e2e/start_stop_test.go | 51 ++++++++++++------- 2 files changed, 51 insertions(+), 17 deletions(-) create mode 100644 pkg/e2e/fixtures/start-stop/start-stop-deps.yaml diff --git a/pkg/e2e/fixtures/start-stop/start-stop-deps.yaml b/pkg/e2e/fixtures/start-stop/start-stop-deps.yaml new file mode 100644 index 000000000..fb1f7fad7 --- /dev/null +++ b/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 diff --git a/pkg/e2e/start_stop_test.go b/pkg/e2e/start_stop_test.go index 1a16d089d..9640b4fef 100644 --- a/pkg/e2e/start_stop_test.go +++ b/pkg/e2e/start_stop_test.go @@ -222,29 +222,46 @@ func TestStopAlreadyStopped(t *testing.T) { } func TestStartStopMultipleServices(t *testing.T) { - cli := NewParallelCLI(t, WithEnv( - "COMPOSE_PROJECT_NAME=e2e-start-stop-svc-multiple", - "COMPOSE_FILE=./fixtures/start-stop/compose.yaml")) + cli := NewParallelCLI(t, WithEnv("COMPOSE_PROJECT_NAME=e2e-start-stop-svc-multiple")) t.Cleanup(func() { cli.RunDockerComposeCmd(t, "down", "--remove-orphans", "-v", "-t", "0") }) - cli.RunDockerComposeCmd(t, "up", "-d", "--wait") + t.Run("starts/stops multiple services", func(t *testing.T) { + cli.RunDockerComposeCmd(t, "-f", "./fixtures/start-stop/compose.yaml", "up", "-d", "--wait") - res := cli.RunDockerComposeCmd(t, "stop", "simple", "another") - services := []string{"simple", "another"} - for _, svc := range services { - stopMsg := fmt.Sprintf("Container e2e-start-stop-svc-multiple-%s-1 Stopped", svc) - assert.Assert(t, strings.Contains(res.Stderr(), stopMsg), - fmt.Sprintf("Missing stop message for %s\n%s", svc, res.Combined())) - } + res := cli.RunDockerComposeCmd(t, "-f", "./fixtures/start-stop/compose.yaml", "stop", "simple", "another") + services := []string{"simple", "another"} + for _, svc := range services { + stopMsg := fmt.Sprintf("Container e2e-start-stop-svc-multiple-%s-1 Stopped", svc) + assert.Assert(t, strings.Contains(res.Stderr(), stopMsg), + fmt.Sprintf("Missing stop message for %s\n%s", svc, res.Combined())) + } - res = cli.RunDockerComposeCmd(t, "start", "simple", "another") - for _, svc := range services { - startMsg := fmt.Sprintf("Container e2e-start-stop-svc-multiple-%s-1 Started", svc) - assert.Assert(t, strings.Contains(res.Stderr(), startMsg), - fmt.Sprintf("Missing start message for %s\n%s", svc, res.Combined())) - } + res = cli.RunDockerComposeCmd(t, "-f", "./fixtures/start-stop/compose.yaml", "start", "simple", "another") + for _, svc := range services { + startMsg := fmt.Sprintf("Container e2e-start-stop-svc-multiple-%s-1 Started", svc) + assert.Assert(t, strings.Contains(res.Stderr(), startMsg), + fmt.Sprintf("Missing start message for %s\n%s", svc, res.Combined())) + } + }) + + t.Run("starts one service out of many", func(t *testing.T) { + cli.RunDockerComposeCmd(t, "down") + cli.RunDockerComposeCmd(t, "-f", "./fixtures/start-stop/start-stop-deps.yaml", "create", "desired") + + res := cli.RunDockerComposeCmd(t, "-f", "./fixtures/start-stop/start-stop-deps.yaml", "start", "desired") + assert.Assert(t, strings.Contains(res.Combined(), "Container e2e-start-stop-svc-multiple-desired-1 Started"), + fmt.Sprintf("Missing start message for service: desired\n%s", res.Combined())) + assert.Assert(t, strings.Contains(res.Combined(), "Container e2e-start-stop-svc-multiple-dep_1-1 Started"), + fmt.Sprintf("Missing start message for service: dep_1\n%s", res.Combined())) + assert.Assert(t, strings.Contains(res.Combined(), "Container e2e-start-stop-svc-multiple-dep_2-1 Started"), + fmt.Sprintf("Missing start message for service: dep_2\n%s", res.Combined())) + assert.Assert(t, !strings.Contains(res.Combined(), "Container e2e-start-stop-svc-multiple-another-1 Started"), + fmt.Sprintf("Shouldn't have tried to start service: another\n%s", res.Combined())) + assert.Assert(t, !strings.Contains(res.Combined(), "Container e2e-start-stop-svc-multiple-another_2-1 Started"), + fmt.Sprintf("Shouldn't have tried to start service: another_2\n%s", res.Combined())) + }) } func TestStartStopMultipleFiles(t *testing.T) { From 361194472edb2efff9f99477721fe2ea392b5557 Mon Sep 17 00:00:00 2001 From: Laura Brehm Date: Tue, 30 Aug 2022 16:37:02 +0200 Subject: [PATCH 2/5] Cleanup E2E tests Signed-off-by: Laura Brehm --- pkg/e2e/start_stop_test.go | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/pkg/e2e/start_stop_test.go b/pkg/e2e/start_stop_test.go index 9640b4fef..892142c32 100644 --- a/pkg/e2e/start_stop_test.go +++ b/pkg/e2e/start_stop_test.go @@ -246,21 +246,21 @@ func TestStartStopMultipleServices(t *testing.T) { } }) - t.Run("starts one service out of many", func(t *testing.T) { + t.Run("starts one service out of many, and it's dependencies", func(t *testing.T) { cli.RunDockerComposeCmd(t, "down") cli.RunDockerComposeCmd(t, "-f", "./fixtures/start-stop/start-stop-deps.yaml", "create", "desired") res := cli.RunDockerComposeCmd(t, "-f", "./fixtures/start-stop/start-stop-deps.yaml", "start", "desired") - assert.Assert(t, strings.Contains(res.Combined(), "Container e2e-start-stop-svc-multiple-desired-1 Started"), - fmt.Sprintf("Missing start message for service: desired\n%s", res.Combined())) - assert.Assert(t, strings.Contains(res.Combined(), "Container e2e-start-stop-svc-multiple-dep_1-1 Started"), - fmt.Sprintf("Missing start message for service: dep_1\n%s", res.Combined())) - assert.Assert(t, strings.Contains(res.Combined(), "Container e2e-start-stop-svc-multiple-dep_2-1 Started"), - fmt.Sprintf("Missing start message for service: dep_2\n%s", res.Combined())) - assert.Assert(t, !strings.Contains(res.Combined(), "Container e2e-start-stop-svc-multiple-another-1 Started"), - fmt.Sprintf("Shouldn't have tried to start service: another\n%s", res.Combined())) - assert.Assert(t, !strings.Contains(res.Combined(), "Container e2e-start-stop-svc-multiple-another_2-1 Started"), - fmt.Sprintf("Shouldn't have tried to start service: another_2\n%s", res.Combined())) + desiredServices := []string{"desired", "dep_1", "dep_2"} + for _, s := range desiredServices { + assert.Assert(t, strings.Contains(res.Combined(), "Container e2e-start-stop-svc-multiple-desired-1 Started"), + 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(), "Container e2e-start-stop-svc-multiple-dep_2-1 Started"), + fmt.Sprintf("Missing start message for service: %s\n%s", s, res.Combined())) + } }) } From 209293e449260f37e949a888149ee7f92f2a73c8 Mon Sep 17 00:00:00 2001 From: Laura Brehm Date: Tue, 6 Sep 2022 20:31:42 +0200 Subject: [PATCH 3/5] Restrict compose project to selected services and dependencies on `compose start` Signed-off-by: Laura Brehm --- cmd/compose/start.go | 1 + pkg/api/api.go | 2 ++ pkg/compose/dependencies.go | 27 +++++++++++++++++---------- pkg/compose/start.go | 7 +++++++ 4 files changed, 27 insertions(+), 10 deletions(-) diff --git a/cmd/compose/start.go b/cmd/compose/start.go index 45da76122..7b7a96265 100644 --- a/cmd/compose/start.go +++ b/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, }) } diff --git a/pkg/api/api.go b/pkg/api/api.go index 38af772ca..5e75cd571 100644 --- a/pkg/api/api.go +++ b/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 diff --git a/pkg/compose/dependencies.go b/pkg/compose/dependencies.go index ed33ec2b2..75e08914f 100644 --- a/pkg/compose/dependencies.go +++ b/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 diff --git a/pkg/compose/start.go b/pkg/compose/start.go index d1af0637c..0dcef8ded 100644 --- a/pkg/compose/start.go +++ b/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) From 4c474fe0291f2605d2e39da18bdc992734687e17 Mon Sep 17 00:00:00 2001 From: Laura Brehm Date: Tue, 6 Sep 2022 21:12:43 +0200 Subject: [PATCH 4/5] Add unit tests to graph building logic in `dependencies.go` Signed-off-by: Laura Brehm --- pkg/compose/dependencies_test.go | 180 +++++++++++++++++++++++++++++++ 1 file changed, 180 insertions(+) diff --git a/pkg/compose/dependencies_test.go b/pkg/compose/dependencies_test.go index 61bf0fd9c..baaa98ce8 100644 --- a/pkg/compose/dependencies_test.go +++ b/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 +} From a7cc406187161a99f406220d0337b58dca208b78 Mon Sep 17 00:00:00 2001 From: Laura Brehm Date: Thu, 8 Sep 2022 12:47:05 -0400 Subject: [PATCH 5/5] Cleanup E2E tests Signed-off-by: Laura Brehm --- pkg/e2e/start_stop_test.go | 69 +++++++++++++++++++++----------------- 1 file changed, 38 insertions(+), 31 deletions(-) diff --git a/pkg/e2e/start_stop_test.go b/pkg/e2e/start_stop_test.go index 892142c32..9c19c48db 100644 --- a/pkg/e2e/start_stop_test.go +++ b/pkg/e2e/start_stop_test.go @@ -222,46 +222,53 @@ func TestStopAlreadyStopped(t *testing.T) { } func TestStartStopMultipleServices(t *testing.T) { - cli := NewParallelCLI(t, WithEnv("COMPOSE_PROJECT_NAME=e2e-start-stop-svc-multiple")) + cli := NewParallelCLI(t, WithEnv( + "COMPOSE_PROJECT_NAME=e2e-start-stop-svc-multiple", + "COMPOSE_FILE=./fixtures/start-stop/compose.yaml")) t.Cleanup(func() { cli.RunDockerComposeCmd(t, "down", "--remove-orphans", "-v", "-t", "0") }) - t.Run("starts/stops multiple services", func(t *testing.T) { - cli.RunDockerComposeCmd(t, "-f", "./fixtures/start-stop/compose.yaml", "up", "-d", "--wait") + cli.RunDockerComposeCmd(t, "up", "-d", "--wait") - res := cli.RunDockerComposeCmd(t, "-f", "./fixtures/start-stop/compose.yaml", "stop", "simple", "another") - services := []string{"simple", "another"} - for _, svc := range services { - stopMsg := fmt.Sprintf("Container e2e-start-stop-svc-multiple-%s-1 Stopped", svc) - assert.Assert(t, strings.Contains(res.Stderr(), stopMsg), - fmt.Sprintf("Missing stop message for %s\n%s", svc, res.Combined())) - } + res := cli.RunDockerComposeCmd(t, "stop", "simple", "another") + services := []string{"simple", "another"} + for _, svc := range services { + stopMsg := fmt.Sprintf("Container e2e-start-stop-svc-multiple-%s-1 Stopped", svc) + assert.Assert(t, strings.Contains(res.Stderr(), stopMsg), + fmt.Sprintf("Missing stop message for %s\n%s", svc, res.Combined())) + } - res = cli.RunDockerComposeCmd(t, "-f", "./fixtures/start-stop/compose.yaml", "start", "simple", "another") - for _, svc := range services { - startMsg := fmt.Sprintf("Container e2e-start-stop-svc-multiple-%s-1 Started", svc) - assert.Assert(t, strings.Contains(res.Stderr(), startMsg), - fmt.Sprintf("Missing start message for %s\n%s", svc, res.Combined())) - } + res = cli.RunDockerComposeCmd(t, "start", "simple", "another") + for _, svc := range services { + startMsg := fmt.Sprintf("Container e2e-start-stop-svc-multiple-%s-1 Started", svc) + assert.Assert(t, strings.Contains(res.Stderr(), startMsg), + fmt.Sprintf("Missing start message for %s\n%s", svc, res.Combined())) + } +} + +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") }) - t.Run("starts one service out of many, and it's dependencies", func(t *testing.T) { - cli.RunDockerComposeCmd(t, "down") - cli.RunDockerComposeCmd(t, "-f", "./fixtures/start-stop/start-stop-deps.yaml", "create", "desired") + cli.RunDockerComposeCmd(t, "create", "desired") - res := cli.RunDockerComposeCmd(t, "-f", "./fixtures/start-stop/start-stop-deps.yaml", "start", "desired") - desiredServices := []string{"desired", "dep_1", "dep_2"} - for _, s := range desiredServices { - assert.Assert(t, strings.Contains(res.Combined(), "Container e2e-start-stop-svc-multiple-desired-1 Started"), - 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(), "Container e2e-start-stop-svc-multiple-dep_2-1 Started"), - fmt.Sprintf("Missing start message for service: %s\n%s", s, res.Combined())) - } - }) + 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) {