From ba4213bb852eae2a26612dd7980af944eef98121 Mon Sep 17 00:00:00 2001 From: Davi De Castro Reis Date: Fri, 19 Jun 2026 11:17:03 -0700 Subject: [PATCH] fix(compose): keep build-only services when Up starts a subset `Up(RunServices(...))` (any partial-service Up) rebuilt `project.Services` down to just the selected services before calling the compose service. When a selected service builds from a `service:` additional build context (`build.additional_contexts.: service:`), the referenced build-only service was dropped too, so the build failed with: service "X" declares unknown service "Y" as additional contexts even though `docker compose up X` on the same project builds the context and starts only X. Use `project.WithSelectedServices(services, IgnoreDependencies)` instead: it disables (not deletes) the non-selected services, keeping them in the project's disabled set so compose can re-enable build dependencies for the build phase while still only creating/starting the selected ones. `IgnoreDependencies` preserves the existing behavior of starting only the explicitly requested services. The returned project is reassigned to the up options so create and start operate on the same scoped project. Adds a regression test with a runtime service that builds from a `service:` additional context: it fails on the previous code ("no such service") and passes now. Co-Authored-By: Claude Opus 4.8 (1M context) --- modules/compose/compose_api.go | 20 ++++++------ modules/compose/compose_api_test.go | 31 +++++++++++++++++++ .../docker-compose-service-build-context.yml | 16 ++++++++++ .../service-build-context-app.Dockerfile | 6 ++++ .../service-build-context-dep.Dockerfile | 2 ++ 5 files changed, 64 insertions(+), 11 deletions(-) create mode 100644 modules/compose/testdata/docker-compose-service-build-context.yml create mode 100644 modules/compose/testdata/service-build-context-app.Dockerfile create mode 100644 modules/compose/testdata/service-build-context-dep.Dockerfile diff --git a/modules/compose/compose_api.go b/modules/compose/compose_api.go index f899632ef6..ca42ff911c 100644 --- a/modules/compose/compose_api.go +++ b/modules/compose/compose_api.go @@ -7,7 +7,6 @@ import ( "io" "os" "path/filepath" - "sort" "strconv" "strings" "sync" @@ -321,17 +320,16 @@ func (d *DockerCompose) Up(ctx context.Context, opts ...StackUpOption) (err erro } if len(upOptions.Services) != len(d.project.Services) { - sort.Strings(upOptions.Services) - - filteredServices := types.Services{} - - for _, srv := range upOptions.Services { - if srvConfig, ok := d.project.Services[srv]; ok { - filteredServices[srv] = srvConfig - } + // Disable (rather than delete) the non-selected services so a selected + // service's `service:` additional build-contexts stay resolvable at build + // time; only the selected services are created and started. + // IgnoreDependencies excludes their runtime depends_on from the selection. + d.project, err = d.project.WithSelectedServices(upOptions.Services, types.IgnoreDependencies) + if err != nil { + return err } - - d.project.Services = filteredServices + // WithSelectedServices returns a new project; point the up options at it too. + upOptions.Project = d.project } err = d.composeService.Up(ctx, d.project, api.UpOptions{ diff --git a/modules/compose/compose_api_test.go b/modules/compose/compose_api_test.go index 124552aeb2..e426571bf9 100644 --- a/modules/compose/compose_api_test.go +++ b/modules/compose/compose_api_test.go @@ -107,6 +107,37 @@ func TestDockerComposeAPIWithRunServices(t *testing.T) { assert.Contains(t, serviceNames, "api-nginx") } +func TestDockerComposeAPIWithRunServicesAndServiceBuildContext(t *testing.T) { + // "app" builds with a `service:build-dep` additional build context, so the + // build needs build-dep to remain in the project even though RunServices + // selects only "app" and build-dep is never started as a container. + // + // Regression test: Up used to shrink d.project down to the selected + // services, which dropped the build-only build-dep service and made the + // build fail with "service ... declares unknown service ... as additional + // contexts" — even though `docker compose up app` on the same project works. + path := filepath.Join(testdataPackage, "docker-compose-service-build-context.yml") + compose, err := NewDockerCompose(path) + require.NoError(t, err, "NewDockerCompose()") + + ctx, cancel := context.WithCancel(context.Background()) + t.Cleanup(cancel) + + err = compose. + WaitForService("app", wait.ForHTTP("/env").WithPort("8080/tcp")). + Up(ctx, Wait(true), RunServices("app")) + cleanup(t, compose) + require.NoError(t, err, "compose.Up()") + + // Only the selected runtime service is started; the build-only context is not. + serviceNames := compose.Services() + require.Len(t, serviceNames, 1) + assert.Contains(t, serviceNames, "app") + + _, err = compose.ServiceContainer(context.Background(), "build-dep") + require.Error(t, err, "build-dep is a build-only context and must not be started") +} + func TestDockerComposeAPIWithProfiles(t *testing.T) { path := RenderComposeProfiles(t) diff --git a/modules/compose/testdata/docker-compose-service-build-context.yml b/modules/compose/testdata/docker-compose-service-build-context.yml new file mode 100644 index 0000000000..bc3eace8e6 --- /dev/null +++ b/modules/compose/testdata/docker-compose-service-build-context.yml @@ -0,0 +1,16 @@ +services: + # build-only dependency: never started, only used as a build context by "app". + build-dep: + build: + context: . + dockerfile: service-build-context-dep.Dockerfile + image: testcontainers/compose-service-build-dep:latest + app: + build: + context: . + dockerfile: service-build-context-app.Dockerfile + additional_contexts: + build-dep: service:build-dep + image: testcontainers/compose-service-build-app:latest + ports: + - "8080/tcp" diff --git a/modules/compose/testdata/service-build-context-app.Dockerfile b/modules/compose/testdata/service-build-context-app.Dockerfile new file mode 100644 index 0000000000..83934193f7 --- /dev/null +++ b/modules/compose/testdata/service-build-context-app.Dockerfile @@ -0,0 +1,6 @@ +FROM golang:1.24-alpine@sha256:fc2cff6625f3c1c92e6c85938ac5bd09034ad0d4bc2dfb08278020b68540dbb5 +WORKDIR /app +# Consumes the `service:build-dep` additional build context. +COPY --from=build-dep /artifact /artifact +COPY echoserver.go . +CMD go run echoserver.go diff --git a/modules/compose/testdata/service-build-context-dep.Dockerfile b/modules/compose/testdata/service-build-context-dep.Dockerfile new file mode 100644 index 0000000000..c44c311989 --- /dev/null +++ b/modules/compose/testdata/service-build-context-dep.Dockerfile @@ -0,0 +1,2 @@ +FROM golang:1.24-alpine@sha256:fc2cff6625f3c1c92e6c85938ac5bd09034ad0d4bc2dfb08278020b68540dbb5 +RUN echo "service-build-context-dep" > /artifact