Skip to content

Commit ce84aa5

Browse files
davireisclaude
andcommitted
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.<name>: service:<dep>`), 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) <noreply@anthropic.com>
1 parent 63f6f21 commit ce84aa5

5 files changed

Lines changed: 73 additions & 11 deletions

File tree

modules/compose/compose_api.go

Lines changed: 18 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ import (
77
"io"
88
"os"
99
"path/filepath"
10-
"sort"
1110
"strconv"
1211
"strings"
1312
"sync"
@@ -321,17 +320,25 @@ func (d *DockerCompose) Up(ctx context.Context, opts ...StackUpOption) (err erro
321320
}
322321

323322
if len(upOptions.Services) != len(d.project.Services) {
324-
sort.Strings(upOptions.Services)
325-
326-
filteredServices := types.Services{}
327-
328-
for _, srv := range upOptions.Services {
329-
if srvConfig, ok := d.project.Services[srv]; ok {
330-
filteredServices[srv] = srvConfig
331-
}
323+
// Select the requested services WITHOUT deleting the rest of the project.
324+
// WithSelectedServices *disables* the non-selected services (keeping them
325+
// in the project's disabled set) instead of removing them, so a selected
326+
// service's `service:` additional build-contexts can still be resolved at
327+
// build time — compose re-enables build dependencies just for the build
328+
// phase. The previous approach deleted them, which broke
329+
// Up(RunServices(...)) for any service whose build references another via
330+
// `additional_contexts: <name>: service:<dep>` with
331+
// "service ... declares unknown service ... as additional contexts",
332+
// even though `docker compose up <svc>` on the same project succeeds.
333+
// IgnoreDependencies preserves the historical behavior of starting only
334+
// the explicitly requested services (not their runtime depends_on).
335+
d.project, err = d.project.WithSelectedServices(upOptions.Services, types.IgnoreDependencies)
336+
if err != nil {
337+
return err
332338
}
333-
334-
d.project.Services = filteredServices
339+
// WithSelectedServices returns a new project; keep the up options pointing
340+
// at it so create and start operate on the same (scoped) project.
341+
upOptions.Project = d.project
335342
}
336343

337344
err = d.composeService.Up(ctx, d.project, api.UpOptions{

modules/compose/compose_api_test.go

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,37 @@ func TestDockerComposeAPIWithRunServices(t *testing.T) {
107107
assert.Contains(t, serviceNames, "api-nginx")
108108
}
109109

110+
func TestDockerComposeAPIWithRunServicesAndServiceBuildContext(t *testing.T) {
111+
// "app" builds with a `service:build-dep` additional build context, so the
112+
// build needs build-dep to remain in the project even though RunServices
113+
// selects only "app" and build-dep is never started as a container.
114+
//
115+
// Regression test: Up used to shrink d.project down to the selected
116+
// services, which dropped the build-only build-dep service and made the
117+
// build fail with "service ... declares unknown service ... as additional
118+
// contexts" — even though `docker compose up app` on the same project works.
119+
path := filepath.Join(testdataPackage, "docker-compose-service-build-context.yml")
120+
compose, err := NewDockerCompose(path)
121+
require.NoError(t, err, "NewDockerCompose()")
122+
123+
ctx, cancel := context.WithCancel(context.Background())
124+
t.Cleanup(cancel)
125+
126+
err = compose.
127+
WaitForService("app", wait.ForHTTP("/env").WithPort("8080/tcp")).
128+
Up(ctx, Wait(true), RunServices("app"))
129+
cleanup(t, compose)
130+
require.NoError(t, err, "compose.Up()")
131+
132+
// Only the selected runtime service is started; the build-only context is not.
133+
serviceNames := compose.Services()
134+
require.Len(t, serviceNames, 1)
135+
assert.Contains(t, serviceNames, "app")
136+
137+
_, err = compose.ServiceContainer(context.Background(), "build-dep")
138+
require.Error(t, err, "build-dep is a build-only context and must not be started")
139+
}
140+
110141
func TestDockerComposeAPIWithProfiles(t *testing.T) {
111142
path := RenderComposeProfiles(t)
112143

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
services:
2+
# build-only dependency: never started, only used as a build context by "app".
3+
build-dep:
4+
build:
5+
context: .
6+
dockerfile: service-build-context-dep.Dockerfile
7+
image: testcontainers/compose-service-build-dep:latest
8+
app:
9+
build:
10+
context: .
11+
dockerfile: service-build-context-app.Dockerfile
12+
additional_contexts:
13+
build-dep: service:build-dep
14+
image: testcontainers/compose-service-build-app:latest
15+
ports:
16+
- "8080/tcp"
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
FROM golang:1.24-alpine@sha256:fc2cff6625f3c1c92e6c85938ac5bd09034ad0d4bc2dfb08278020b68540dbb5
2+
WORKDIR /app
3+
# Consumes the `service:build-dep` additional build context.
4+
COPY --from=build-dep /artifact /artifact
5+
COPY echoserver.go .
6+
CMD go run echoserver.go
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
FROM golang:1.24-alpine@sha256:fc2cff6625f3c1c92e6c85938ac5bd09034ad0d4bc2dfb08278020b68540dbb5
2+
RUN echo "service-build-context-dep" > /artifact

0 commit comments

Comments
 (0)