diff --git a/e2e/tests/up-docker-compose/testdata/docker-compose-run-services-invalid/.devcontainer/devcontainer.json b/e2e/tests/up-docker-compose/testdata/docker-compose-run-services-invalid/.devcontainer/devcontainer.json new file mode 100644 index 000000000..c293987db --- /dev/null +++ b/e2e/tests/up-docker-compose/testdata/docker-compose-run-services-invalid/.devcontainer/devcontainer.json @@ -0,0 +1,7 @@ +{ + "name": "Go", + "dockerComposeFile": "../docker-compose.yaml", + "service": "app", + "runServices": ["app", "nonexistent-service"], + "workspaceFolder": "/workspaces" +} diff --git a/e2e/tests/up-docker-compose/testdata/docker-compose-run-services-invalid/docker-compose.yaml b/e2e/tests/up-docker-compose/testdata/docker-compose-run-services-invalid/docker-compose.yaml new file mode 100644 index 000000000..43dcebf56 --- /dev/null +++ b/e2e/tests/up-docker-compose/testdata/docker-compose-run-services-invalid/docker-compose.yaml @@ -0,0 +1,8 @@ +version: '3' + +services: + app: + image: ghcr.io/devsy-org/test-images/go:1 + command: sleep infinity + volumes: + - .:/workspaces:cached diff --git a/e2e/tests/up-docker-compose/up_docker_compose.go b/e2e/tests/up-docker-compose/up_docker_compose.go index b762db989..7811effb8 100644 --- a/e2e/tests/up-docker-compose/up_docker_compose.go +++ b/e2e/tests/up-docker-compose/up_docker_compose.go @@ -502,6 +502,15 @@ var _ = ginkgo.Describe( gomega.Expect(dbIDs).To(gomega.BeEmpty(), "db container not to be created") }, ginkgo.SpecTimeout(framework.TimeoutShort())) + ginkgo.It("invalid runServices returns error", func(ctx context.Context) { + _, _, err := tc.setupAndStartWorkspace( + ctx, + "tests/up-docker-compose/testdata/docker-compose-run-services-invalid", + ) + gomega.Expect(err).To(gomega.HaveOccurred()) + gomega.Expect(err.Error()).To(gomega.ContainSubstring("nonexistent-service")) + }, ginkgo.SpecTimeout(framework.TimeoutShort())) + ginkgo.It("user lookup with no remoteUser", func(ctx context.Context) { _, _, err := tc.setupAndStartWorkspace( ctx, diff --git a/pkg/devcontainer/compose.go b/pkg/devcontainer/compose.go index b1e3fa3ed..82c4f8083 100644 --- a/pkg/devcontainer/compose.go +++ b/pkg/devcontainer/compose.go @@ -176,6 +176,10 @@ func (r *runner) runDockerCompose( project.Name = composeHelper.GetProjectName(r.ID) log.Debugf("Loaded project %s", project.Name) + if err := validateRunServices(parsedConfig.Config.RunServices, project); err != nil { + return nil, err + } + containerDetails, err := composeHelper.FindDevContainer( ctx, project.Name, @@ -315,6 +319,22 @@ func (r *runner) runDockerCompose( }) } +func validateRunServices(runServices []string, project *composetypes.Project) error { + if len(runServices) == 0 { + return nil + } + var invalid []string + for _, svc := range runServices { + if _, ok := project.Services[svc]; !ok { + invalid = append(invalid, svc) + } + } + if len(invalid) > 0 { + return fmt.Errorf("runServices: service(s) not found in compose file: %v", invalid) + } + return nil +} + // onlyRunServices appends the services defined in .devcontainer.json runServices to the upArgs. func (r *runner) onlyRunServices(upArgs []string, parsedConfig *config.SubstitutedConfig) []string { if len(parsedConfig.Config.RunServices) > 0 { diff --git a/pkg/devcontainer/compose_test.go b/pkg/devcontainer/compose_test.go index 61957c0dd..b87136442 100644 --- a/pkg/devcontainer/compose_test.go +++ b/pkg/devcontainer/compose_test.go @@ -2,6 +2,7 @@ package devcontainer import ( "path/filepath" + "strings" "testing" composetypes "github.com/compose-spec/compose-go/v2/types" @@ -309,3 +310,43 @@ func (s *PrepareBuildContextSuite) TestCustomBuildContextEmptyContext() { func TestPrepareBuildContextSuite(t *testing.T) { suite.Run(t, new(PrepareBuildContextSuite)) } + +func TestValidateRunServices(t *testing.T) { + project := &composetypes.Project{ + Services: map[string]composetypes.ServiceConfig{ + "app": {Name: "app"}, + "db": {Name: "db"}, + }, + } + emptyProject := &composetypes.Project{Services: map[string]composetypes.ServiceConfig{}} + + tests := []struct { + name string + runServices []string + project *composetypes.Project + wantErr bool + errContains string + }{ + {"empty runServices returns nil", nil, project, false, ""}, + {"valid services returns nil", []string{"app", "db"}, project, false, ""}, + {"invalid service returns error", []string{"nonexistent"}, project, true, "nonexistent"}, + {"mix of valid and invalid", []string{"app", "typo-svc", "bad"}, project, true, "typo-svc"}, + {"project with no services", []string{"app"}, emptyProject, true, "app"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := validateRunServices(tt.runServices, tt.project) + if tt.wantErr { + if err == nil { + t.Fatal("expected error, got nil") + } + if !strings.Contains(err.Error(), tt.errContains) { + t.Errorf("error %q should contain %q", err.Error(), tt.errContains) + } + } else if err != nil { + t.Fatalf("unexpected error: %v", err) + } + }) + } +}