From e96ca020f2dc943aefe4abb62a16c5b5a3dea214 Mon Sep 17 00:00:00 2001 From: Nicolas De Loof Date: Thu, 10 Sep 2026 17:49:06 +0200 Subject: [PATCH] build(deps): adopt compose-go jobs and the container-spec layering MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit compose-spec/compose-go#866 is merged: jobs top-level element, container specification layered as ContainerSpec/WorkloadSpec, pre_start hooks carrying the full container specification resolved at load time. Bump to the merged head and adapt in the same movement — composite literals setting moved fields wrap them into the embedded ContainerSpec / WorkloadSpec (promoted field access was already source-compatible, so this is literal-only, no behavior change), and pre_start handling is typed against PreStartHook. Co-Authored-By: Claude Fable 5 Signed-off-by: Nicolas De Loof --- cmd/compose/compose_test.go | 5 +- cmd/compose/config.go | 4 +- cmd/compose/config_test.go | 29 +-- cmd/compose/options_test.go | 23 +-- cmd/compose/pullOptions_test.go | 15 +- cmd/compose/up_test.go | 3 +- go.mod | 2 +- go.sum | 4 +- internal/locker/pidfile_windows.go | 3 +- internal/tracing/attributes_test.go | 14 +- pkg/api/api_test.go | 45 ++-- pkg/bridge/convert_test.go | 7 +- pkg/compose/build_test.go | 65 +++--- pkg/compose/compose.go | 8 +- pkg/compose/create_test.go | 25 +-- pkg/compose/dependencies_test.go | 52 ++--- pkg/compose/down_test.go | 12 +- pkg/compose/generate.go | 8 +- pkg/compose/hash_test.go | 3 +- pkg/compose/hook_test.go | 7 +- pkg/compose/images_test.go | 11 +- pkg/compose/pre_start.go | 4 +- pkg/compose/pre_start_test.go | 272 +++++++++++++------------ pkg/compose/publish.go | 2 +- pkg/compose/publish_test.go | 64 +++--- pkg/compose/pull.go | 14 +- pkg/compose/pull_test.go | 22 +- pkg/compose/reconcile_test.go | 131 ++++++------ pkg/compose/restart_test.go | 10 +- pkg/compose/service_containers_test.go | 20 +- pkg/compose/start_test.go | 27 ++- pkg/compose/viz_test.go | 72 +++---- pkg/compose/watch_test.go | 15 +- pkg/e2e/compose_test.go | 4 +- 34 files changed, 507 insertions(+), 495 deletions(-) diff --git a/cmd/compose/compose_test.go b/cmd/compose/compose_test.go index 708929ff8..c2492dfa2 100644 --- a/cmd/compose/compose_test.go +++ b/cmd/compose/compose_test.go @@ -31,10 +31,9 @@ func TestFilterServices(t *testing.T) { Links: []string{"bar"}, }, "bar": { - Name: "bar", - DependsOn: map[string]types.ServiceDependency{ + Name: "bar", WorkloadSpec: types.WorkloadSpec{DependsOn: map[string]types.ServiceDependency{ "zot": {}, - }, + }}, }, "zot": { Name: "zot", diff --git a/cmd/compose/config.go b/cmd/compose/config.go index 8c4065f43..08fcd9fb4 100644 --- a/cmd/compose/config.go +++ b/cmd/compose/config.go @@ -262,7 +262,7 @@ func imagesOnly(project *types.Project) *types.Project { digests := types.Services{} for name, config := range project.Services { service := types.ServiceConfig{ - Image: config.Image, + ContainerSpec: types.ContainerSpec{Image: config.Image}, } for _, vol := range config.Volumes { if vol.Type == types.VolumeTypeImage { @@ -386,7 +386,7 @@ func resolveImageDigests(ctx context.Context, dockerCli command.Cli, model map[s } for _, hook := range preStartHooks(service) { image, _ := hook["image"].(string) - config.PreStart = append(config.PreStart, types.ServiceHook{Image: image}) + config.PreStart = append(config.PreStart, types.PreStartHook{ContainerSpec: types.ContainerSpec{Image: image}}) } for _, volume := range imageVolumes(service) { source, _ := volume["source"].(string) diff --git a/cmd/compose/config_test.go b/cmd/compose/config_test.go index 8807850a2..db557cb42 100644 --- a/cmd/compose/config_test.go +++ b/cmd/compose/config_test.go @@ -135,14 +135,17 @@ func TestImagesOnly(t *testing.T) { Name: "test", Services: types.Services{ "test": types.ServiceConfig{ - Name: "test", - Image: "docker.io/library/nginx@" + testDigest, - Command: types.ShellCommand{"echo", "hello"}, + Name: "test", + // hooks can't be overridden element-wise on merge, so the lock must not carry them - PreStart: []types.ServiceHook{{Image: "docker.io/library/hookimage@" + testDigest}}, - Volumes: []types.ServiceVolumeConfig{ - {Type: types.VolumeTypeImage, Source: "docker.io/library/someimage@" + testDigest, Target: "/data"}, - {Type: types.VolumeTypeBind, Source: "/host", Target: "/bind"}, + PreStart: []types.PreStartHook{{ContainerSpec: types.ContainerSpec{Image: "docker.io/library/hookimage@" + testDigest}}}, ContainerSpec: types.ContainerSpec{ + Image: "docker.io/library/nginx@" + testDigest, + Command: types.ShellCommand{"echo", "hello"}, + + Volumes: []types.ServiceVolumeConfig{ + {Type: types.VolumeTypeImage, Source: "docker.io/library/someimage@" + testDigest, Target: "/data"}, + {Type: types.VolumeTypeBind, Source: "/host", Target: "/bind"}, + }, }, }, }, @@ -154,9 +157,11 @@ func TestImagesOnly(t *testing.T) { assert.DeepEqual(t, locked, &types.Project{ Services: types.Services{ "test": types.ServiceConfig{ - Image: "docker.io/library/nginx@" + testDigest, - Volumes: []types.ServiceVolumeConfig{ - {Type: types.VolumeTypeImage, Source: "docker.io/library/someimage@" + testDigest, Target: "/data"}, + ContainerSpec: types.ContainerSpec{ + Image: "docker.io/library/nginx@" + testDigest, + Volumes: []types.ServiceVolumeConfig{ + {Type: types.VolumeTypeImage, Source: "docker.io/library/someimage@" + testDigest, Target: "/data"}, + }, }, }, }, @@ -167,8 +172,8 @@ func TestWarnHooksNotLockable(t *testing.T) { messages := captureWarnings(t, func() { warnHooksNotLockable(&types.Project{ Services: types.Services{ - "with-hook-image": types.ServiceConfig{PreStart: []types.ServiceHook{{Image: "alpine:latest"}}}, - "inline-hook": types.ServiceConfig{PreStart: []types.ServiceHook{{Command: types.ShellCommand{"echo"}}}}, + "with-hook-image": types.ServiceConfig{PreStart: []types.PreStartHook{{ContainerSpec: types.ContainerSpec{Image: "alpine:latest"}}}}, + "inline-hook": types.ServiceConfig{PreStart: []types.PreStartHook{{ContainerSpec: types.ContainerSpec{Command: types.ShellCommand{"echo"}}}}}, "without-hook": types.ServiceConfig{}, }, }) diff --git a/cmd/compose/options_test.go b/cmd/compose/options_test.go index c015a7a72..07df91285 100644 --- a/cmd/compose/options_test.go +++ b/cmd/compose/options_test.go @@ -38,17 +38,18 @@ func TestApplyPlatforms_InferFromRuntime(t *testing.T) { return &types.Project{ Services: types.Services{ "test": { - Name: "test", - Image: "foo", - Build: &types.BuildConfig{ + Name: "test", ContainerSpec: types.ContainerSpec{ + Image: "foo", + + Platform: "alice/32", + }, WorkloadSpec: types.WorkloadSpec{Build: &types.BuildConfig{ Context: ".", Platforms: []string{ "linux/amd64", "linux/arm64", "alice/32", }, - }, - Platform: "alice/32", + }}, }, }, } @@ -75,15 +76,13 @@ func TestApplyPlatforms_DockerDefaultPlatform(t *testing.T) { }, Services: types.Services{ "test": { - Name: "test", - Image: "foo", - Build: &types.BuildConfig{ + Name: "test", ContainerSpec: types.ContainerSpec{Image: "foo"}, WorkloadSpec: types.WorkloadSpec{Build: &types.BuildConfig{ Context: ".", Platforms: []string{ "linux/amd64", "linux/arm64", }, - }, + }}, }, }, } @@ -110,15 +109,13 @@ func TestApplyPlatforms_UnsupportedPlatform(t *testing.T) { }, Services: types.Services{ "test": { - Name: "test", - Image: "foo", - Build: &types.BuildConfig{ + Name: "test", ContainerSpec: types.ContainerSpec{Image: "foo"}, WorkloadSpec: types.WorkloadSpec{Build: &types.BuildConfig{ Context: ".", Platforms: []string{ "linux/amd64", "linux/arm64", }, - }, + }}, }, }, } diff --git a/cmd/compose/pullOptions_test.go b/cmd/compose/pullOptions_test.go index 05dd868ed..f13537923 100644 --- a/cmd/compose/pullOptions_test.go +++ b/cmd/compose/pullOptions_test.go @@ -27,22 +27,19 @@ func TestApplyPullOptions(t *testing.T) { project := &types.Project{ Services: types.Services{ "must-build": { - Name: "must-build", + Name: "must-build", WorkloadSpec: // No image, local build only - Build: &types.BuildConfig{ + types.WorkloadSpec{Build: &types.BuildConfig{ Context: ".", - }, + }}, }, "has-build": { - Name: "has-build", - Image: "registry.example.com/myservice", - Build: &types.BuildConfig{ + Name: "has-build", ContainerSpec: types.ContainerSpec{Image: "registry.example.com/myservice"}, WorkloadSpec: types.WorkloadSpec{Build: &types.BuildConfig{ Context: ".", - }, + }}, }, "must-pull": { - Name: "must-pull", - Image: "registry.example.com/another-service", + Name: "must-pull", ContainerSpec: types.ContainerSpec{Image: "registry.example.com/another-service"}, }, }, } diff --git a/cmd/compose/up_test.go b/cmd/compose/up_test.go index e6e7fd222..ee159d08b 100644 --- a/cmd/compose/up_test.go +++ b/cmd/compose/up_test.go @@ -155,8 +155,7 @@ services: WorkingDir: dir, Services: types.Services{ "web": { - Name: "web", - Image: "nginx", + Name: "web", ContainerSpec: types.ContainerSpec{Image: "nginx"}, }, }, } diff --git a/go.mod b/go.mod index 39063352a..58b25f1c6 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,7 @@ require ( github.com/Microsoft/go-winio v0.6.3-0.20251027160822-ad3df93bed29 github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d github.com/buger/goterm v1.0.4 - github.com/compose-spec/compose-go/v2 v2.15.1-0.20260908103050-cda18529aca7 + github.com/compose-spec/compose-go/v2 v2.15.1-0.20260910154416-11feead015fc github.com/containerd/console v1.0.5 github.com/containerd/containerd/v2 v2.3.5 github.com/containerd/errdefs v1.0.0 diff --git a/go.sum b/go.sum index 49835e4d1..bd7ac8241 100644 --- a/go.sum +++ b/go.sum @@ -28,8 +28,8 @@ github.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4= github.com/codahale/rfc6979 v0.0.0-20141003034818-6a90f24967eb h1:EDmT6Q9Zs+SbUoc7Ik9EfrFqcylYqgPZ9ANSbTAntnE= github.com/codahale/rfc6979 v0.0.0-20141003034818-6a90f24967eb/go.mod h1:ZjrT6AXHbDs86ZSdt/osfBi5qfexBrKUdONk989Wnk4= -github.com/compose-spec/compose-go/v2 v2.15.1-0.20260908103050-cda18529aca7 h1:D9jScaeUAk7DjyB71SD9zvOFPR6klw0FxPcR2bgu72c= -github.com/compose-spec/compose-go/v2 v2.15.1-0.20260908103050-cda18529aca7/go.mod h1:Q1+qtN4vhzEjGrnqRtzx1xa8raDZQlMUe3WJxndYNiQ= +github.com/compose-spec/compose-go/v2 v2.15.1-0.20260910154416-11feead015fc h1:rmi1Yz/JylJUNR068ay6XZI9mIOEyMFqWvWS0rWDgt4= +github.com/compose-spec/compose-go/v2 v2.15.1-0.20260910154416-11feead015fc/go.mod h1:Q1+qtN4vhzEjGrnqRtzx1xa8raDZQlMUe3WJxndYNiQ= github.com/containerd/cgroups/v3 v3.1.3 h1:eUNflyMddm18+yrDmZPn3jI7C5hJ9ahABE5q6dyLYXQ= github.com/containerd/cgroups/v3 v3.1.3/go.mod h1:PKZ2AcWmSBsY/tJUVhtS/rluX0b1uq1GmPO1ElCmbOw= github.com/containerd/console v1.0.5 h1:R0ymNeydRqH2DmakFNdmjR2k0t7UPuiOV/N/27/qqsc= diff --git a/internal/locker/pidfile_windows.go b/internal/locker/pidfile_windows.go index 2dac9ff03..f953f9dce 100644 --- a/internal/locker/pidfile_windows.go +++ b/internal/locker/pidfile_windows.go @@ -21,8 +21,9 @@ package locker import ( "os" - "github.com/docker/compose/v5/internal/pidfile" "github.com/mitchellh/go-ps" + + "github.com/docker/compose/v5/internal/pidfile" ) func (f *Pidfile) Lock() error { diff --git a/internal/tracing/attributes_test.go b/internal/tracing/attributes_test.go index 8416f6972..acbbe2f55 100644 --- a/internal/tracing/attributes_test.go +++ b/internal/tracing/attributes_test.go @@ -28,27 +28,27 @@ func TestProjectHash(t *testing.T) { Name: "fake-proj", WorkingDir: "/tmp", Services: map[string]types.ServiceConfig{ - "foo": {Image: "fake-image"}, + "foo": {ContainerSpec: types.ContainerSpec{Image: "fake-image"}}, }, DisabledServices: map[string]types.ServiceConfig{ - "bar": {Image: "diff-image"}, + "bar": {ContainerSpec: types.ContainerSpec{Image: "diff-image"}}, }, } projB := &types.Project{ Name: "fake-proj", WorkingDir: "/tmp", Services: map[string]types.ServiceConfig{ - "foo": {Image: "fake-image"}, - "bar": {Image: "diff-image"}, + "foo": {ContainerSpec: types.ContainerSpec{Image: "fake-image"}}, + "bar": {ContainerSpec: types.ContainerSpec{Image: "diff-image"}}, }, } projC := &types.Project{ Name: "fake-proj", WorkingDir: "/tmp", Services: map[string]types.ServiceConfig{ - "foo": {Image: "fake-image"}, - "bar": {Image: "diff-image"}, - "baz": {Image: "yet-another-image"}, + "foo": {ContainerSpec: types.ContainerSpec{Image: "fake-image"}}, + "bar": {ContainerSpec: types.ContainerSpec{Image: "diff-image"}}, + "baz": {ContainerSpec: types.ContainerSpec{Image: "yet-another-image"}}, }, } diff --git a/pkg/api/api_test.go b/pkg/api/api_test.go index 98844a1c8..65f1fbbb8 100644 --- a/pkg/api/api_test.go +++ b/pkg/api/api_test.go @@ -62,58 +62,57 @@ func TestGetDependentImages(t *testing.T) { }{ { name: "no hooks", - service: types.ServiceConfig{Image: "alpine:3.20"}, + service: types.ServiceConfig{ContainerSpec: types.ContainerSpec{Image: "alpine:3.20"}}, expected: nil, }, { name: "pre_start hook with explicit image", service: types.ServiceConfig{ - Image: "alpine:3.20", - PreStart: []types.ServiceHook{ - {Image: "alpine:3.19", Command: types.ShellCommand{"echo", "init"}}, - }, + PreStart: []types.PreStartHook{ + {ContainerSpec: types.ContainerSpec{Image: "alpine:3.19", Command: types.ShellCommand{"echo", "init"}}}, + }, ContainerSpec: types.ContainerSpec{Image: "alpine:3.20"}, }, expected: []string{"alpine:3.19"}, }, { name: "pre_start hook without image is ignored", service: types.ServiceConfig{ - Image: "alpine:3.20", - PreStart: []types.ServiceHook{ - {Image: "busybox", Command: types.ShellCommand{"echo", "a"}}, - {Command: types.ShellCommand{"echo", "b"}}, - }, + PreStart: []types.PreStartHook{ + {ContainerSpec: types.ContainerSpec{Image: "busybox", Command: types.ShellCommand{"echo", "a"}}}, + {ContainerSpec: types.ContainerSpec{Command: types.ShellCommand{"echo", "b"}}}, + }, ContainerSpec: types.ContainerSpec{Image: "alpine:3.20"}, }, expected: []string{"busybox"}, }, { name: "pre_start hook reusing the service image is ignored", service: types.ServiceConfig{ - Image: "alpine:3.20", - PreStart: []types.ServiceHook{ - {Image: "alpine:3.20", Command: types.ShellCommand{"echo", "same"}}, - {Image: "alpine:3.19", Command: types.ShellCommand{"echo", "other"}}, - }, + PreStart: []types.PreStartHook{ + {ContainerSpec: types.ContainerSpec{Image: "alpine:3.20", Command: types.ShellCommand{"echo", "same"}}}, + {ContainerSpec: types.ContainerSpec{Image: "alpine:3.19", Command: types.ShellCommand{"echo", "other"}}}, + }, ContainerSpec: types.ContainerSpec{Image: "alpine:3.20"}, }, expected: []string{"alpine:3.19"}, }, { name: "pre_start hook reusing the default (build) image name is ignored", service: types.ServiceConfig{ - Name: "web", - Build: &types.BuildConfig{Context: "."}, - PreStart: []types.ServiceHook{ - {Image: "demo-web", Command: types.ShellCommand{"echo", "same"}}, - }, + Name: "web", + + PreStart: []types.PreStartHook{ + {ContainerSpec: types.ContainerSpec{Image: "demo-web", Command: types.ShellCommand{"echo", "same"}}}, + }, WorkloadSpec: types.WorkloadSpec{Build: &types.BuildConfig{Context: "."}}, }, expected: nil, }, { + // exec hooks carry no image at all since the container spec + // layering: nothing to collect, by construction name: "post_start and pre_stop hooks are not collected", service: types.ServiceConfig{ - Image: "alpine:3.20", - PostStart: []types.ServiceHook{{Image: "ignored:post", Command: types.ShellCommand{"echo"}}}, - PreStop: []types.ServiceHook{{Image: "ignored:stop", Command: types.ShellCommand{"echo"}}}, + PostStart: []types.ServiceHook{{Command: types.ShellCommand{"echo"}}}, + PreStop: []types.ServiceHook{{Command: types.ShellCommand{"echo"}}}, + ContainerSpec: types.ContainerSpec{Image: "alpine:3.20"}, }, expected: nil, }, diff --git a/pkg/bridge/convert_test.go b/pkg/bridge/convert_test.go index bc36fa3c0..de0e5cdaa 100644 --- a/pkg/bridge/convert_test.go +++ b/pkg/bridge/convert_test.go @@ -42,9 +42,10 @@ func TestLoadAdditionalResources_BuildOnlySkipsPull(t *testing.T) { Name: "test", Services: types.Services{ "api": { - Name: "api", - Build: &types.BuildConfig{Context: "."}, - Expose: []string{"8080"}, + Name: "api", WorkloadSpec: types.WorkloadSpec{ + Build: &types.BuildConfig{Context: "."}, + Expose: []string{"8080"}, + }, }, }, } diff --git a/pkg/compose/build_test.go b/pkg/compose/build_test.go index a7fbf2e2c..1dbba84de 100644 --- a/pkg/compose/build_test.go +++ b/pkg/compose/build_test.go @@ -84,26 +84,22 @@ func Test_dockerFilePath(t *testing.T) { func Test_addBuildDependencies(t *testing.T) { project := &types.Project{Services: types.Services{ "test": types.ServiceConfig{ - Build: &types.BuildConfig{ + WorkloadSpec: types.WorkloadSpec{Build: &types.BuildConfig{ AdditionalContexts: map[string]string{ "foo": "service:foo", "bar": "service:bar", }, - }, + }}, }, "foo": types.ServiceConfig{ - Build: &types.BuildConfig{ + WorkloadSpec: types.WorkloadSpec{Build: &types.BuildConfig{ AdditionalContexts: map[string]string{ "zot": "service:zot", }, - }, - }, - "bar": types.ServiceConfig{ - Build: &types.BuildConfig{}, - }, - "zot": types.ServiceConfig{ - Build: &types.BuildConfig{}, + }}, }, + "bar": types.ServiceConfig{WorkloadSpec: types.WorkloadSpec{Build: &types.BuildConfig{}}}, + "zot": types.ServiceConfig{WorkloadSpec: types.WorkloadSpec{Build: &types.BuildConfig{}}}, }} services := addBuildDependencies([]string{"test"}, project) @@ -122,11 +118,11 @@ func TestGetLocalImagesDigests_PreStartHook(t *testing.T) { Name: "demo", Services: types.Services{ "web": types.ServiceConfig{ - Name: "web", - Image: "alpine:3.20", - PreStart: []types.ServiceHook{ - {Image: "alpine:3.19", Command: types.ShellCommand{"echo", "init"}}, - }, + Name: "web", + + PreStart: []types.PreStartHook{ + {ContainerSpec: types.ContainerSpec{Image: "alpine:3.19", Command: types.ShellCommand{"echo", "init"}}}, + }, ContainerSpec: types.ContainerSpec{Image: "alpine:3.20"}, }, }, } @@ -155,9 +151,10 @@ func TestResolveImageVolumes(t *testing.T) { t.Run("source is an image name", func(t *testing.T) { service := types.ServiceConfig{ - Name: "web", - CustomLabels: types.Labels{}, - Volumes: []types.ServiceVolumeConfig{imageVolume("content:1", "/data")}, + Name: "web", ContainerSpec: types.ContainerSpec{ + CustomLabels: types.Labels{}, + Volumes: []types.ServiceVolumeConfig{imageVolume("content:1", "/data")}, + }, } resolveImageVolumes(&service, images, "p") assert.Equal(t, service.Volumes[0].Source, "content:1") @@ -166,9 +163,10 @@ func TestResolveImageVolumes(t *testing.T) { t.Run("source is another service resolves to its image name", func(t *testing.T) { service := types.ServiceConfig{ - Name: "web", - CustomLabels: types.Labels{}, - Volumes: []types.ServiceVolumeConfig{imageVolume("source", "/data")}, + Name: "web", ContainerSpec: types.ContainerSpec{ + CustomLabels: types.Labels{}, + Volumes: []types.ServiceVolumeConfig{imageVolume("source", "/data")}, + }, } resolveImageVolumes(&service, images, "p") // the mount Source must stay a daemon-resolvable name, never a digest @@ -178,9 +176,10 @@ func TestResolveImageVolumes(t *testing.T) { t.Run("unresolvable source is left untouched and unlabelled", func(t *testing.T) { service := types.ServiceConfig{ - Name: "web", - CustomLabels: types.Labels{}, - Volumes: []types.ServiceVolumeConfig{imageVolume("ghost:1", "/data")}, + Name: "web", ContainerSpec: types.ContainerSpec{ + CustomLabels: types.Labels{}, + Volumes: []types.ServiceVolumeConfig{imageVolume("ghost:1", "/data")}, + }, } resolveImageVolumes(&service, images, "p") assert.Equal(t, service.Volumes[0].Source, "ghost:1") @@ -190,11 +189,12 @@ func TestResolveImageVolumes(t *testing.T) { t.Run("several volumes produce a deterministic sorted label", func(t *testing.T) { service := types.ServiceConfig{ - Name: "web", - CustomLabels: types.Labels{}, - Volumes: []types.ServiceVolumeConfig{ - imageVolume("assets:2", "/b"), - imageVolume("content:1", "/a"), + Name: "web", ContainerSpec: types.ContainerSpec{ + CustomLabels: types.Labels{}, + Volumes: []types.ServiceVolumeConfig{ + imageVolume("assets:2", "/b"), + imageVolume("content:1", "/a"), + }, }, } resolveImageVolumes(&service, images, "p") @@ -203,9 +203,10 @@ func TestResolveImageVolumes(t *testing.T) { t.Run("no image volumes writes no label", func(t *testing.T) { service := types.ServiceConfig{ - Name: "web", - CustomLabels: types.Labels{}, - Volumes: []types.ServiceVolumeConfig{{Type: types.VolumeTypeVolume, Source: "vol", Target: "/data"}}, + Name: "web", ContainerSpec: types.ContainerSpec{ + CustomLabels: types.Labels{}, + Volumes: []types.ServiceVolumeConfig{{Type: types.VolumeTypeVolume, Source: "vol", Target: "/data"}}, + }, } resolveImageVolumes(&service, images, "p") _, labelled := service.CustomLabels[api.ImageVolumeDigestLabel] diff --git a/pkg/compose/compose.go b/pkg/compose/compose.go index ee797274f..7f39461ed 100644 --- a/pkg/compose/compose.go +++ b/pkg/compose/compose.go @@ -376,9 +376,11 @@ func (s *composeService) projectFromName(containers Containers, projectName stri service, ok := set[serviceLabel] if !ok { service = types.ServiceConfig{ - Name: serviceLabel, - Image: ctr.Image, - Labels: ctr.Labels, + Name: serviceLabel, + ContainerSpec: types.ContainerSpec{ + Image: ctr.Image, + Labels: ctr.Labels, + }, } } service.Scale = increment(service.Scale) diff --git a/pkg/compose/create_test.go b/pkg/compose/create_test.go index e08e4227d..0600cf4ed 100644 --- a/pkg/compose/create_test.go +++ b/pkg/compose/create_test.go @@ -86,7 +86,7 @@ func TestBuildVolumeMount(t *testing.T) { } func TestServiceImageName(t *testing.T) { - assert.Equal(t, api.GetImageNameOrDefault(composetypes.ServiceConfig{Image: "myImage"}, "myProject"), "myImage") + assert.Equal(t, api.GetImageNameOrDefault(composetypes.ServiceConfig{ContainerSpec: composetypes.ContainerSpec{Image: "myImage"}}, "myProject"), "myImage") assert.Equal(t, api.GetImageNameOrDefault(composetypes.ServiceConfig{Name: "aService"}, "myProject"), "myProject-aService") } @@ -108,8 +108,7 @@ func TestBuildContainerMountOptions(t *testing.T) { Name: "myProject", Services: composetypes.Services{ "myService": { - Name: "myService", - Volumes: []composetypes.ServiceVolumeConfig{ + Name: "myService", ContainerSpec: composetypes.ContainerSpec{Volumes: []composetypes.ServiceVolumeConfig{ { Type: composetypes.VolumeTypeVolume, Target: "/var/myvolume1", @@ -131,7 +130,7 @@ func TestBuildContainerMountOptions(t *testing.T) { Source: "\\\\.\\pipe\\docker_engine_windows", Target: "\\\\.\\pipe\\docker_engine", }, - }, + }}, }, }, Volumes: composetypes.Volumes(map[string]composetypes.VolumeConfig{ @@ -194,15 +193,14 @@ func TestBuildContainerMountOptions(t *testing.T) { func TestDefaultNetworkSettings(t *testing.T) { t.Run("returns the network with the highest priority as primary when service has multiple networks", func(t *testing.T) { service := composetypes.ServiceConfig{ - Name: "myService", - Networks: map[string]*composetypes.ServiceNetworkConfig{ + Name: "myService", ContainerSpec: composetypes.ContainerSpec{Networks: map[string]*composetypes.ServiceNetworkConfig{ "myNetwork1": { Priority: 10, }, "myNetwork2": { Priority: 1000, }, - }, + }}, } project := composetypes.Project{ Name: "myProject", @@ -275,11 +273,10 @@ func TestDefaultNetworkSettings(t *testing.T) { t.Run("returns only primary network in EndpointsConfig for API < 1.44", func(t *testing.T) { service := composetypes.ServiceConfig{ - Name: "myService", - Networks: map[string]*composetypes.ServiceNetworkConfig{ + Name: "myService", ContainerSpec: composetypes.ContainerSpec{Networks: map[string]*composetypes.ServiceNetworkConfig{ "myNetwork1": {Priority: 10}, "myNetwork2": {Priority: 1000}, - }, + }}, } project := composetypes.Project{ Name: "myProject", @@ -299,8 +296,7 @@ func TestDefaultNetworkSettings(t *testing.T) { t.Run("returns defined network mode if explicitly set", func(t *testing.T) { service := composetypes.ServiceConfig{ - Name: "myService", - NetworkMode: "host", + Name: "myService", ContainerSpec: composetypes.ContainerSpec{NetworkMode: "host"}, } project := composetypes.Project{ Name: "myProject", @@ -324,8 +320,7 @@ func TestCreateEndpointSettings(t *testing.T) { Name: "projName", }, composetypes.ServiceConfig{ Name: "serviceName", - ContainerName: "containerName", - Networks: map[string]*composetypes.ServiceNetworkConfig{ + ContainerName: "containerName", ContainerSpec: composetypes.ContainerSpec{Networks: map[string]*composetypes.ServiceNetworkConfig{ "netName": { Priority: 100, Aliases: []string{"alias1", "alias2"}, @@ -338,7 +333,7 @@ func TestCreateEndpointSettings(t *testing.T) { "driverOpt2": "optval2", }, }, - }, + }}, }, 0, "netName", []string{"link1", "link2"}, true) assert.NilError(t, err) macAddr, _ := net.ParseMAC("02:00:00:00:00:01") diff --git a/pkg/compose/dependencies_test.go b/pkg/compose/dependencies_test.go index 65dccbeaf..af11f0c7a 100644 --- a/pkg/compose/dependencies_test.go +++ b/pkg/compose/dependencies_test.go @@ -34,16 +34,14 @@ func createTestProject() *types.Project { return &types.Project{ Services: types.Services{ "test1": { - Name: "test1", - DependsOn: map[string]types.ServiceDependency{ + Name: "test1", WorkloadSpec: types.WorkloadSpec{DependsOn: map[string]types.ServiceDependency{ "test2": {}, - }, + }}, }, "test2": { - Name: "test2", - DependsOn: map[string]types.ServiceDependency{ + Name: "test2", WorkloadSpec: types.WorkloadSpec{DependsOn: map[string]types.ServiceDependency{ "test3": {}, - }, + }}, }, "test3": { Name: "test3", @@ -54,8 +52,7 @@ func createTestProject() *types.Project { func TestTraversalWithMultipleParents(t *testing.T) { dependent := types.ServiceConfig{ - Name: "dependent", - DependsOn: make(types.DependsOnConfig), + Name: "dependent", WorkloadSpec: types.WorkloadSpec{DependsOn: make(types.DependsOnConfig)}, } project := types.Project{ @@ -122,8 +119,7 @@ func TestBuildGraph(t *testing.T) { desc: "builds graph with single service", services: types.Services{ "test": { - Name: "test", - DependsOn: types.DependsOnConfig{}, + Name: "test", WorkloadSpec: types.WorkloadSpec{DependsOn: types.DependsOnConfig{}}, }, }, expectedVertices: map[string]*Vertex{ @@ -140,12 +136,10 @@ func TestBuildGraph(t *testing.T) { desc: "builds graph with two separate services", services: types.Services{ "test": { - Name: "test", - DependsOn: types.DependsOnConfig{}, + Name: "test", WorkloadSpec: types.WorkloadSpec{DependsOn: types.DependsOnConfig{}}, }, "another": { - Name: "another", - DependsOn: types.DependsOnConfig{}, + Name: "another", WorkloadSpec: types.WorkloadSpec{DependsOn: types.DependsOnConfig{}}, }, }, expectedVertices: map[string]*Vertex{ @@ -169,14 +163,12 @@ func TestBuildGraph(t *testing.T) { desc: "builds graph with a service and a dependency", services: types.Services{ "test": { - Name: "test", - DependsOn: types.DependsOnConfig{ + Name: "test", WorkloadSpec: types.WorkloadSpec{DependsOn: types.DependsOnConfig{ "another": types.ServiceDependency{}, - }, + }}, }, "another": { - Name: "another", - DependsOn: types.DependsOnConfig{}, + Name: "another", WorkloadSpec: types.WorkloadSpec{DependsOn: types.DependsOnConfig{}}, }, }, expectedVertices: map[string]*Vertex{ @@ -204,20 +196,17 @@ func TestBuildGraph(t *testing.T) { desc: "builds graph with multiple dependency levels", services: types.Services{ "test": { - Name: "test", - DependsOn: types.DependsOnConfig{ + Name: "test", WorkloadSpec: types.WorkloadSpec{DependsOn: types.DependsOnConfig{ "another": types.ServiceDependency{}, - }, + }}, }, "another": { - Name: "another", - DependsOn: types.DependsOnConfig{ + Name: "another", WorkloadSpec: types.WorkloadSpec{DependsOn: types.DependsOnConfig{ "another_dep": types.ServiceDependency{}, - }, + }}, }, "another_dep": { - Name: "another_dep", - DependsOn: types.DependsOnConfig{}, + Name: "another_dep", WorkloadSpec: types.WorkloadSpec{DependsOn: types.DependsOnConfig{}}, }, }, expectedVertices: map[string]*Vertex{ @@ -281,15 +270,14 @@ func TestBuildGraphDependsOn(t *testing.T) { desc: "service depends on init container which is already removed", services: types.Services{ "test": { - Name: "test", - DependsOn: types.DependsOnConfig{ + Name: "test", WorkloadSpec: types.WorkloadSpec{DependsOn: types.DependsOnConfig{ "test-removed-init-container": types.ServiceDependency{ Condition: "service_completed_successfully", Restart: false, Extensions: types.Extensions(nil), Required: false, }, - }, + }}, }, }, expectedVertices: map[string]*Vertex{ @@ -331,10 +319,10 @@ func TestNewGraphDoesNotMutateProject(t *testing.T) { Services: types.Services{ "app": { Name: "app", - DependsOn: types.DependsOnConfig{ + WorkloadSpec: types.WorkloadSpec{DependsOn: types.DependsOnConfig{ "db": {Condition: types.ServiceConditionStarted, Required: true}, "debug": {Condition: types.ServiceConditionStarted, Required: false}, - }, + }}, }, "db": {Name: "db"}, }, diff --git a/pkg/compose/down_test.go b/pkg/compose/down_test.go index d8fa9338f..06817b8da 100644 --- a/pkg/compose/down_test.go +++ b/pkg/compose/down_test.go @@ -295,11 +295,11 @@ func TestDownRemoveImages(t *testing.T) { Name: strings.ToLower(testProject), Services: types.Services{ "local-anonymous": {Name: "local-anonymous"}, - "local-named": {Name: "local-named", Image: "local-named-image"}, - "remote": {Name: "remote", Image: "remote-image"}, - "remote-tagged": {Name: "remote-tagged", Image: "registry.example.com/remote-image-tagged:v1.0"}, + "local-named": {Name: "local-named", ContainerSpec: types.ContainerSpec{Image: "local-named-image"}}, + "remote": {Name: "remote", ContainerSpec: types.ContainerSpec{Image: "remote-image"}}, + "remote-tagged": {Name: "remote-tagged", ContainerSpec: types.ContainerSpec{Image: "registry.example.com/remote-image-tagged:v1.0"}}, "no-images-anonymous": {Name: "no-images-anonymous"}, - "no-images-named": {Name: "no-images-named", Image: "missing-named-image"}, + "no-images-named": {Name: "no-images-named", ContainerSpec: types.ContainerSpec{Image: "missing-named-image"}}, }, }, } @@ -531,7 +531,7 @@ func TestEnsureImagesDown_SparesDanglingImagesOfOrphanedServices(t *testing.T) { project := &types.Project{ Name: "prj", Services: types.Services{ - "web": {Name: "web", Image: "web-image"}, + "web": {Name: "web", ContainerSpec: types.ContainerSpec{Image: "web-image"}}, }, } apiClient.EXPECT().ImageList(gomock.Any(), client.ImageListOptions{ @@ -570,7 +570,7 @@ func TestEnsureImagesDown_RemoveOrphansAlsoTakesDanglingImages(t *testing.T) { project := &types.Project{ Name: "prj", Services: types.Services{ - "web": {Name: "web", Image: "web-image"}, + "web": {Name: "web", ContainerSpec: types.ContainerSpec{Image: "web-image"}}, }, } apiClient.EXPECT().ImageList(gomock.Any(), client.ImageListOptions{ diff --git a/pkg/compose/generate.go b/pkg/compose/generate.go index fed2461a5..4ee63352e 100644 --- a/pkg/compose/generate.go +++ b/pkg/compose/generate.go @@ -85,9 +85,11 @@ func (s *composeService) createProjectFromContainers(containers []container.Summ service, ok := services[serviceLabel] if !ok { service = types.ServiceConfig{ - Name: serviceLabel, - Image: ctr.Image, - Labels: ctr.Labels, + Name: serviceLabel, + ContainerSpec: types.ContainerSpec{ + Image: ctr.Image, + Labels: ctr.Labels, + }, } } service.Scale = increment(service.Scale) diff --git a/pkg/compose/hash_test.go b/pkg/compose/hash_test.go index 73b7f3877..b25ea9b88 100644 --- a/pkg/compose/hash_test.go +++ b/pkg/compose/hash_test.go @@ -37,7 +37,6 @@ func serviceConfig(replicas int) types.ServiceConfig { Deploy: &types.DeployConfig{ Replicas: &replicas, }, - Name: "foo", - Image: "bar", + Name: "foo", ContainerSpec: types.ContainerSpec{Image: "bar"}, } } diff --git a/pkg/compose/hook_test.go b/pkg/compose/hook_test.go index 3f5ef1490..1d768fc9e 100644 --- a/pkg/compose/hook_test.go +++ b/pkg/compose/hook_test.go @@ -87,8 +87,7 @@ func TestRunHook_ConsoleSize(t *testing.T) { mockCli.EXPECT().Out().Return(streams.NewOut(tty)).AnyTimes() service := types.ServiceConfig{ - Name: "test", - Tty: tc.tty, + Name: "test", WorkloadSpec: types.WorkloadSpec{Tty: tc.tty}, } hook := types.ServiceHook{Command: []string{"echo", "hello"}} ctr := container.Summary{ID: "container123"} @@ -450,7 +449,7 @@ func TestRunHook_CopyError(t *testing.T) { mockCli.EXPECT().Out().Return(streams.NewOut(os.Stdout)).AnyTimes() ctr := container.Summary{ID: "ctr-1"} - service := types.ServiceConfig{Name: "svc", Tty: false} + service := types.ServiceConfig{Name: "svc", WorkloadSpec: types.WorkloadSpec{Tty: false}} hook := types.ServiceHook{Command: types.ShellCommand{"true"}} mockAPI.EXPECT(). @@ -572,7 +571,7 @@ func TestRunHook_ExecInspectError(t *testing.T) { mockCli.EXPECT().Out().Return(streams.NewOut(os.Stdout)).AnyTimes() ctr := container.Summary{ID: "ctr-1"} - service := types.ServiceConfig{Name: "svc", Tty: false} + service := types.ServiceConfig{Name: "svc", WorkloadSpec: types.WorkloadSpec{Tty: false}} hook := types.ServiceHook{Command: types.ShellCommand{"true"}} mockAPI.EXPECT(). diff --git a/pkg/compose/images_test.go b/pkg/compose/images_test.go index 9775fd14d..a1b53ad47 100644 --- a/pkg/compose/images_test.go +++ b/pkg/compose/images_test.go @@ -389,10 +389,11 @@ func TestPlatformPinnedDigest(t *testing.T) { Name: "p", Services: types.Services{ "app": { - Name: "app", - Image: "foo:1", - Platform: "linux/s390x", - CustomLabels: types.Labels{}, + Name: "app", ContainerSpec: types.ContainerSpec{ + Image: "foo:1", + Platform: "linux/s390x", + CustomLabels: types.Labels{}, + }, }, }, } @@ -455,7 +456,7 @@ func TestPlatformPinnedDigest(t *testing.T) { // whichever service triggered it, so services sharing the image with another // pinned platform must re-resolve theirs. func TestServiceImageDigest(t *testing.T) { - pinnedService := types.ServiceConfig{Name: "app", Platform: "linux/s390x"} + pinnedService := types.ServiceConfig{Name: "app", ContainerSpec: types.ContainerSpec{Platform: "linux/s390x"}} t.Run("unpinned service uses the shared digest, no inspect", func(t *testing.T) { mockCtrl := gomock.NewController(t) diff --git a/pkg/compose/pre_start.go b/pkg/compose/pre_start.go index 4d70c54bb..1d350f605 100644 --- a/pkg/compose/pre_start.go +++ b/pkg/compose/pre_start.go @@ -88,7 +88,7 @@ func (s *composeService) runPreStart(ctx context.Context, project *types.Project func (s *composeService) runPreStartHook( ctx context.Context, project *types.Project, service types.ServiceConfig, - ctr container.Summary, index int, hook types.ServiceHook, listener api.ContainerEventListener, + ctr container.Summary, index int, hook types.PreStartHook, listener api.ContainerEventListener, ) error { created, err := s.createPreStartContainer(ctx, project, service, ctr, hook) if err != nil { @@ -164,7 +164,7 @@ func (s *composeService) runPreStartHook( func (s *composeService) createPreStartContainer( ctx context.Context, project *types.Project, service types.ServiceConfig, - ctr container.Summary, hook types.ServiceHook, + ctr container.Summary, hook types.PreStartHook, ) (client.ContainerCreateResult, error) { image := hook.Image if image == "" { diff --git a/pkg/compose/pre_start_test.go b/pkg/compose/pre_start_test.go index 635eef673..1f803f036 100644 --- a/pkg/compose/pre_start_test.go +++ b/pkg/compose/pre_start_test.go @@ -91,12 +91,12 @@ func TestPreStart_SuccessTwoHooksInOrder(t *testing.T) { project := &types.Project{Name: "demo"} service := types.ServiceConfig{ - Name: "web", - Image: "alpine", - PreStart: []types.ServiceHook{ - {Image: "alpine", Command: types.ShellCommand{"echo", "first"}}, - {Image: "alpine", Command: types.ShellCommand{"echo", "second"}}, - }, + Name: "web", + + PreStart: []types.PreStartHook{ + {ContainerSpec: types.ContainerSpec{Image: "alpine", Command: types.ShellCommand{"echo", "first"}}}, + {ContainerSpec: types.ContainerSpec{Image: "alpine", Command: types.ShellCommand{"echo", "second"}}}, + }, ContainerSpec: types.ContainerSpec{Image: "alpine"}, } ctr := container.Summary{ID: "service-ctr-id"} @@ -134,12 +134,12 @@ func TestPreStart_FirstHookFailsStopsExecution(t *testing.T) { project := &types.Project{Name: "demo"} service := types.ServiceConfig{ - Name: "web", - Image: "alpine", - PreStart: []types.ServiceHook{ - {Image: "alpine", Command: types.ShellCommand{"false"}}, - {Image: "alpine", Command: types.ShellCommand{"echo", "never"}}, - }, + Name: "web", + + PreStart: []types.PreStartHook{ + {ContainerSpec: types.ContainerSpec{Image: "alpine", Command: types.ShellCommand{"false"}}}, + {ContainerSpec: types.ContainerSpec{Image: "alpine", Command: types.ShellCommand{"echo", "never"}}}, + }, ContainerSpec: types.ContainerSpec{Image: "alpine"}, } ctr := container.Summary{ID: "service-ctr-id"} @@ -164,11 +164,11 @@ func TestPreStart_PerReplicaRejected(t *testing.T) { project := &types.Project{Name: "demo"} service := types.ServiceConfig{ - Name: "web", - Image: "alpine", - PreStart: []types.ServiceHook{ - {Image: "alpine", Command: types.ShellCommand{"true"}, PerReplica: true}, - }, + Name: "web", + + PreStart: []types.PreStartHook{ + {PerReplica: true, ContainerSpec: types.ContainerSpec{Image: "alpine", Command: types.ShellCommand{"true"}}}, + }, ContainerSpec: types.ContainerSpec{Image: "alpine"}, } ctr := container.Summary{ID: "service-ctr-id"} @@ -184,8 +184,8 @@ func TestPreStart_ImageFallsBackToBuiltImage(t *testing.T) { // Service with no explicit image (build-only); hook image also empty. service := types.ServiceConfig{ Name: "web", - PreStart: []types.ServiceHook{ - {Command: types.ShellCommand{"echo", "hi"}}, + PreStart: []types.PreStartHook{ + {ContainerSpec: types.ContainerSpec{Command: types.ShellCommand{"echo", "hi"}}}, }, } ctr := container.Summary{ID: "service-ctr-id"} @@ -215,11 +215,11 @@ func TestPreStart_ExplicitHookImageUsed(t *testing.T) { project := &types.Project{Name: "demo"} service := types.ServiceConfig{ - Name: "web", - Image: "service-image:latest", - PreStart: []types.ServiceHook{ - {Image: "custom-hook-image:1.2.3", Command: types.ShellCommand{"echo"}}, - }, + Name: "web", + + PreStart: []types.PreStartHook{ + {ContainerSpec: types.ContainerSpec{Image: "custom-hook-image:1.2.3", Command: types.ShellCommand{"echo"}}}, + }, ContainerSpec: types.ContainerSpec{Image: "service-image:latest"}, } ctr := container.Summary{ID: "service-ctr-id"} @@ -248,11 +248,11 @@ func TestPreStart_VolumesFromServiceContainer(t *testing.T) { project := &types.Project{Name: "demo"} service := types.ServiceConfig{ - Name: "web", - Image: "alpine", - PreStart: []types.ServiceHook{ - {Image: "alpine", Command: types.ShellCommand{"true"}}, - }, + Name: "web", + + PreStart: []types.PreStartHook{ + {ContainerSpec: types.ContainerSpec{Image: "alpine", Command: types.ShellCommand{"true"}}}, + }, ContainerSpec: types.ContainerSpec{Image: "alpine"}, } ctr := container.Summary{ID: "service-ctr-id"} @@ -290,12 +290,12 @@ func TestPreStart_ContainerCreateFailurePropagates(t *testing.T) { project := &types.Project{Name: "demo"} service := types.ServiceConfig{ - Name: "web", - Image: "alpine", - PreStart: []types.ServiceHook{ - {Image: "missing:latest", Command: types.ShellCommand{"true"}}, - {Image: "alpine", Command: types.ShellCommand{"never"}}, - }, + Name: "web", + + PreStart: []types.PreStartHook{ + {ContainerSpec: types.ContainerSpec{Image: "missing:latest", Command: types.ShellCommand{"true"}}}, + {ContainerSpec: types.ContainerSpec{Image: "alpine", Command: types.ShellCommand{"never"}}}, + }, ContainerSpec: types.ContainerSpec{Image: "alpine"}, } ctr := container.Summary{ID: "service-ctr-id"} @@ -312,11 +312,11 @@ func TestPreStart_ContainerStartFailurePropagates(t *testing.T) { project := &types.Project{Name: "demo"} service := types.ServiceConfig{ - Name: "web", - Image: "alpine", - PreStart: []types.ServiceHook{ - {Image: "alpine", Command: types.ShellCommand{"true"}}, - }, + Name: "web", + + PreStart: []types.PreStartHook{ + {ContainerSpec: types.ContainerSpec{Image: "alpine", Command: types.ShellCommand{"true"}}}, + }, ContainerSpec: types.ContainerSpec{Image: "alpine"}, } ctr := container.Summary{ID: "service-ctr-id"} @@ -350,11 +350,11 @@ func TestPreStart_WaitResultPreferredOverNilError(t *testing.T) { project := &types.Project{Name: "demo"} service := types.ServiceConfig{ - Name: "web", - Image: "alpine", - PreStart: []types.ServiceHook{ - {Image: "alpine", Command: types.ShellCommand{"true"}}, - }, + Name: "web", + + PreStart: []types.PreStartHook{ + {ContainerSpec: types.ContainerSpec{Image: "alpine", Command: types.ShellCommand{"true"}}}, + }, ContainerSpec: types.ContainerSpec{Image: "alpine"}, } ctr := container.Summary{ID: "service-ctr-id"} @@ -434,10 +434,10 @@ func TestPreStart_DetachedModeAttachesLogs(t *testing.T) { project := &types.Project{Name: "demo"} service := types.ServiceConfig{ - Name: "web", - Image: "alpine", - PreStart: []types.ServiceHook{ - {Image: "alpine", Command: types.ShellCommand{"true"}}, + Name: "web", + ContainerSpec: types.ContainerSpec{Image: "alpine"}, + PreStart: []types.PreStartHook{ + {ContainerSpec: types.ContainerSpec{Image: "alpine", Command: types.ShellCommand{"true"}}}, }, } ctr := container.Summary{ID: "service-ctr-id"} @@ -466,10 +466,10 @@ func TestPreStart_FailureIncludesTail(t *testing.T) { project := &types.Project{Name: "demo"} service := types.ServiceConfig{ - Name: "db", - Image: "postgres", - PreStart: []types.ServiceHook{ - {Image: "postgres", Command: types.ShellCommand{"migrate"}}, + Name: "db", + ContainerSpec: types.ContainerSpec{Image: "postgres"}, + PreStart: []types.PreStartHook{ + {ContainerSpec: types.ContainerSpec{Image: "postgres", Command: types.ShellCommand{"migrate"}}}, }, } ctr := container.Summary{ID: "service-ctr-id"} @@ -510,10 +510,10 @@ func TestPreStart_SuccessRemovesContainer(t *testing.T) { project := &types.Project{Name: "proj"} service := types.ServiceConfig{ - Name: "web", - Image: "alpine", - PreStart: []types.ServiceHook{ - {Image: "alpine", Command: types.ShellCommand{"true"}}, + Name: "web", + ContainerSpec: types.ContainerSpec{Image: "alpine"}, + PreStart: []types.PreStartHook{ + {ContainerSpec: types.ContainerSpec{Image: "alpine", Command: types.ShellCommand{"true"}}}, }, } ctr := container.Summary{ID: "svc-ctr"} @@ -549,10 +549,10 @@ func TestPreStart_FailureRetainsContainer(t *testing.T) { project := &types.Project{Name: "proj"} service := types.ServiceConfig{ - Name: "web", - Image: "alpine", - PreStart: []types.ServiceHook{ - {Image: "alpine", Command: types.ShellCommand{"migrate"}}, + Name: "web", + ContainerSpec: types.ContainerSpec{Image: "alpine"}, + PreStart: []types.PreStartHook{ + {ContainerSpec: types.ContainerSpec{Image: "alpine", Command: types.ShellCommand{"migrate"}}}, }, } ctr := container.Summary{ID: "svc-ctr"} @@ -588,10 +588,10 @@ func TestPreStart_CancellationRemovesContainer(t *testing.T) { project := &types.Project{Name: "proj"} service := types.ServiceConfig{ - Name: "web", - Image: "alpine", - PreStart: []types.ServiceHook{ - {Image: "alpine", Command: types.ShellCommand{"long-running-op"}}, + Name: "web", + ContainerSpec: types.ContainerSpec{Image: "alpine"}, + PreStart: []types.PreStartHook{ + {ContainerSpec: types.ContainerSpec{Image: "alpine", Command: types.ShellCommand{"long-running-op"}}}, }, } ctr := container.Summary{ID: "svc-ctr"} @@ -634,10 +634,10 @@ func TestPreStart_RemovesOrphanBeforeRun(t *testing.T) { project := &types.Project{Name: "proj"} service := types.ServiceConfig{ - Name: "web", - Image: "alpine", - PreStart: []types.ServiceHook{ - {Image: "alpine", Command: types.ShellCommand{"migrate"}}, + Name: "web", + ContainerSpec: types.ContainerSpec{Image: "alpine"}, + PreStart: []types.PreStartHook{ + {ContainerSpec: types.ContainerSpec{Image: "alpine", Command: types.ShellCommand{"migrate"}}}, }, } ctr := container.Summary{ID: "svc-ctr"} @@ -671,10 +671,10 @@ func TestPreStart_SuccessRemoveFailureIsNonFatal(t *testing.T) { project := &types.Project{Name: "proj"} service := types.ServiceConfig{ - Name: "web", - Image: "alpine", - PreStart: []types.ServiceHook{ - {Image: "alpine", Command: types.ShellCommand{"true"}}, + Name: "web", + ContainerSpec: types.ContainerSpec{Image: "alpine"}, + PreStart: []types.PreStartHook{ + {ContainerSpec: types.ContainerSpec{Image: "alpine", Command: types.ShellCommand{"true"}}}, }, } ctr := container.Summary{ID: "svc-ctr"} @@ -763,10 +763,10 @@ func TestPreStart_StreamLogsError_NilListener(t *testing.T) { project := &types.Project{Name: "proj"} service := types.ServiceConfig{ - Name: "web", - Image: "alpine", - PreStart: []types.ServiceHook{ - {Image: "alpine", Command: types.ShellCommand{"true"}}, + Name: "web", + ContainerSpec: types.ContainerSpec{Image: "alpine"}, + PreStart: []types.PreStartHook{ + {ContainerSpec: types.ContainerSpec{Image: "alpine", Command: types.ShellCommand{"true"}}}, }, } ctr := container.Summary{ID: "svc-ctr"} @@ -795,10 +795,10 @@ func TestPreStart_StreamLogsError_WithListener(t *testing.T) { project := &types.Project{Name: "proj"} service := types.ServiceConfig{ - Name: "web", - Image: "alpine", - PreStart: []types.ServiceHook{ - {Image: "alpine", Command: types.ShellCommand{"true"}}, + Name: "web", + ContainerSpec: types.ContainerSpec{Image: "alpine"}, + PreStart: []types.PreStartHook{ + {ContainerSpec: types.ContainerSpec{Image: "alpine", Command: types.ShellCommand{"true"}}}, }, } ctr := container.Summary{ID: "svc-ctr"} @@ -838,13 +838,15 @@ func TestPreStart_OldAPIVersion(t *testing.T) { }, } service := types.ServiceConfig{ - Name: "web", - Image: "alpine", - Networks: map[string]*types.ServiceNetworkConfig{ - "default": nil, + Name: "web", + ContainerSpec: types.ContainerSpec{ + Image: "alpine", + Networks: map[string]*types.ServiceNetworkConfig{ + "default": nil, + }, }, - PreStart: []types.ServiceHook{ - {Image: "alpine", Command: types.ShellCommand{"true"}}, + PreStart: []types.PreStartHook{ + {ContainerSpec: types.ContainerSpec{Image: "alpine", Command: types.ShellCommand{"true"}}}, }, } ctr := container.Summary{ID: "svc-ctr"} @@ -878,11 +880,13 @@ func TestPreStart_ConnectExtraNetworksSuccess(t *testing.T) { }, } service := types.ServiceConfig{ - Name: "web", - Image: "alpine", - Networks: map[string]*types.ServiceNetworkConfig{ - "default": nil, - "extra": nil, + Name: "web", + ContainerSpec: types.ContainerSpec{ + Image: "alpine", + Networks: map[string]*types.ServiceNetworkConfig{ + "default": nil, + "extra": nil, + }, }, } @@ -907,11 +911,13 @@ func TestPreStart_ConnectExtraNetworksFails(t *testing.T) { }, } service := types.ServiceConfig{ - Name: "web", - Image: "alpine", - Networks: map[string]*types.ServiceNetworkConfig{ - "default": nil, - "extra": nil, + Name: "web", + ContainerSpec: types.ContainerSpec{ + Image: "alpine", + Networks: map[string]*types.ServiceNetworkConfig{ + "default": nil, + "extra": nil, + }, }, } @@ -931,10 +937,10 @@ func TestPreStart_ContainerStartFailureAndRemoveFails(t *testing.T) { project := &types.Project{Name: "proj"} service := types.ServiceConfig{ - Name: "web", - Image: "alpine", - PreStart: []types.ServiceHook{ - {Image: "alpine", Command: types.ShellCommand{"true"}}, + Name: "web", + ContainerSpec: types.ContainerSpec{Image: "alpine"}, + PreStart: []types.PreStartHook{ + {ContainerSpec: types.ContainerSpec{Image: "alpine", Command: types.ShellCommand{"true"}}}, }, } ctr := container.Summary{ID: "svc-ctr"} @@ -965,10 +971,10 @@ func TestPreStart_OrphanScanFails(t *testing.T) { project := &types.Project{Name: "proj"} service := types.ServiceConfig{ - Name: "web", - Image: "alpine", - PreStart: []types.ServiceHook{ - {Image: "alpine", Command: types.ShellCommand{"true"}}, + Name: "web", + ContainerSpec: types.ContainerSpec{Image: "alpine"}, + PreStart: []types.PreStartHook{ + {ContainerSpec: types.ContainerSpec{Image: "alpine", Command: types.ShellCommand{"true"}}}, }, } ctr := container.Summary{ID: "svc-ctr"} @@ -998,10 +1004,10 @@ func TestPreStart_OrphanRemovalFails(t *testing.T) { project := &types.Project{Name: "proj"} service := types.ServiceConfig{ - Name: "web", - Image: "alpine", - PreStart: []types.ServiceHook{ - {Image: "alpine", Command: types.ShellCommand{"true"}}, + Name: "web", + ContainerSpec: types.ContainerSpec{Image: "alpine"}, + PreStart: []types.PreStartHook{ + {ContainerSpec: types.ContainerSpec{Image: "alpine", Command: types.ShellCommand{"true"}}}, }, } ctr := container.Summary{ID: "svc-ctr"} @@ -1042,14 +1048,16 @@ func TestPreStart_OldAPINetworkConnectFails(t *testing.T) { }, } service := types.ServiceConfig{ - Name: "web", - Image: "alpine", - Networks: map[string]*types.ServiceNetworkConfig{ - "default": nil, - "extra": nil, + Name: "web", + ContainerSpec: types.ContainerSpec{ + Image: "alpine", + Networks: map[string]*types.ServiceNetworkConfig{ + "default": nil, + "extra": nil, + }, }, - PreStart: []types.ServiceHook{ - {Image: "alpine", Command: types.ShellCommand{"true"}}, + PreStart: []types.PreStartHook{ + {ContainerSpec: types.ContainerSpec{Image: "alpine", Command: types.ShellCommand{"true"}}}, }, } ctr := container.Summary{ID: "svc-ctr"} @@ -1082,14 +1090,16 @@ func TestPreStart_OldAPINetworkConnectAndRemoveFails(t *testing.T) { }, } service := types.ServiceConfig{ - Name: "web", - Image: "alpine", - Networks: map[string]*types.ServiceNetworkConfig{ - "default": nil, - "extra": nil, + Name: "web", + ContainerSpec: types.ContainerSpec{ + Image: "alpine", + Networks: map[string]*types.ServiceNetworkConfig{ + "default": nil, + "extra": nil, + }, }, - PreStart: []types.ServiceHook{ - {Image: "alpine", Command: types.ShellCommand{"true"}}, + PreStart: []types.PreStartHook{ + {ContainerSpec: types.ContainerSpec{Image: "alpine", Command: types.ShellCommand{"true"}}}, }, } ctr := container.Summary{ID: "svc-ctr"} @@ -1125,10 +1135,10 @@ func TestPreStart_RuntimeAPIVersionError(t *testing.T) { project := &types.Project{Name: "proj"} service := types.ServiceConfig{ - Name: "web", - Image: "alpine", - PreStart: []types.ServiceHook{ - {Image: "alpine", Command: types.ShellCommand{"true"}}, + Name: "web", + ContainerSpec: types.ContainerSpec{Image: "alpine"}, + PreStart: []types.PreStartHook{ + {ContainerSpec: types.ContainerSpec{Image: "alpine", Command: types.ShellCommand{"true"}}}, }, } ctr := container.Summary{ID: "svc-ctr"} @@ -1153,10 +1163,10 @@ func TestPreStart_FailureStdoutOnlyTail(t *testing.T) { project := &types.Project{Name: "proj"} service := types.ServiceConfig{ - Name: "db", - Image: "postgres", - PreStart: []types.ServiceHook{ - {Image: "postgres", Command: types.ShellCommand{"migrate"}}, + Name: "db", + ContainerSpec: types.ContainerSpec{Image: "postgres"}, + PreStart: []types.PreStartHook{ + {ContainerSpec: types.ContainerSpec{Image: "postgres", Command: types.ShellCommand{"migrate"}}}, }, } ctr := container.Summary{ID: "svc-ctr"} diff --git a/pkg/compose/publish.go b/pkg/compose/publish.go index 6d4380fe9..ae27ca4e5 100644 --- a/pkg/compose/publish.go +++ b/pkg/compose/publish.go @@ -333,7 +333,7 @@ func (s *composeService) generateImageDigestsOverride(ctx context.Context, proje } for name, service := range project.Services { override.Services[name] = types.ServiceConfig{ - Image: service.Image, + ContainerSpec: types.ContainerSpec{Image: service.Image}, } } return override.MarshalYAML() diff --git a/pkg/compose/publish_test.go b/pkg/compose/publish_test.go index c78808ac2..4a8519c15 100644 --- a/pkg/compose/publish_test.go +++ b/pkg/compose/publish_test.go @@ -121,10 +121,11 @@ func Test_preChecks_sensitive_data_detected_decline(t *testing.T) { project := &types.Project{ Services: types.Services{ "web": { - Name: "web", - Image: "nginx", - EnvFiles: []types.EnvFile{ - {Path: envPath, Required: true}, + Name: "web", ContainerSpec: types.ContainerSpec{ + Image: "nginx", + EnvFiles: []types.EnvFile{ + {Path: envPath, Required: true}, + }, }, }, }, @@ -214,10 +215,11 @@ func Test_checkForSensitiveData_optional_env_file_missing(t *testing.T) { project := &types.Project{ Services: types.Services{ "web": { - Name: "web", - Image: "nginx", - EnvFiles: []types.EnvFile{ - {Path: filepath.Join(dir, "missing.env"), Required: false}, + Name: "web", ContainerSpec: types.ContainerSpec{ + Image: "nginx", + EnvFiles: []types.EnvFile{ + {Path: filepath.Join(dir, "missing.env"), Required: false}, + }, }, }, }, @@ -237,10 +239,11 @@ func Test_checkForSensitiveData_optional_env_file_present(t *testing.T) { project := &types.Project{ Services: types.Services{ "web": { - Name: "web", - Image: "nginx", - EnvFiles: []types.EnvFile{ - {Path: envPath, Required: false}, + Name: "web", ContainerSpec: types.ContainerSpec{ + Image: "nginx", + EnvFiles: []types.EnvFile{ + {Path: envPath, Required: false}, + }, }, }, }, @@ -257,10 +260,11 @@ func Test_checkForSensitiveData_required_env_file_missing(t *testing.T) { project := &types.Project{ Services: types.Services{ "web": { - Name: "web", - Image: "nginx", - EnvFiles: []types.EnvFile{ - {Path: filepath.Join(dir, "missing.env"), Required: true}, + Name: "web", ContainerSpec: types.ContainerSpec{ + Image: "nginx", + EnvFiles: []types.EnvFile{ + {Path: filepath.Join(dir, "missing.env"), Required: true}, + }, }, }, }, @@ -703,13 +707,14 @@ func Test_publish_decline_returns_ErrCanceled(t *testing.T) { project := &types.Project{ Services: types.Services{ "web": { - Name: "web", - Image: "nginx", - Volumes: []types.ServiceVolumeConfig{ - { - Type: types.VolumeTypeBind, - Source: "/host/path", - Target: "/container/path", + Name: "web", ContainerSpec: types.ContainerSpec{ + Image: "nginx", + Volumes: []types.ServiceVolumeConfig{ + { + Type: types.VolumeTypeBind, + Source: "/host/path", + Target: "/container/path", + }, }, }, }, @@ -747,11 +752,14 @@ func Test_generateImageDigestsOverride_resolvesDependentImages(t *testing.T) { Name: "test", Services: types.Services{ "app": types.ServiceConfig{ - Name: "app", - Image: "nginx:latest", - PreStart: []types.ServiceHook{{Image: "hookimage:latest"}}, - Volumes: []types.ServiceVolumeConfig{ - {Type: types.VolumeTypeImage, Source: "someimage:latest", Target: "/data"}, + Name: "app", + + PreStart: []types.PreStartHook{{ContainerSpec: types.ContainerSpec{Image: "hookimage:latest"}}}, ContainerSpec: types.ContainerSpec{ + Image: "nginx:latest", + + Volumes: []types.ServiceVolumeConfig{ + {Type: types.VolumeTypeImage, Source: "someimage:latest", Target: "/data"}, + }, }, }, }, diff --git a/pkg/compose/pull.go b/pkg/compose/pull.go index 4cfc3fcc3..b8a935fbf 100644 --- a/pkg/compose/pull.go +++ b/pkg/compose/pull.go @@ -180,7 +180,7 @@ func (p *imagePuller) pullHookImages(ctx context.Context) error { hookPolicy = types.PullPolicyMissing } for _, img := range api.GetDependentImages(service, p.project.Name) { - pullRequired, skipReason, err := shouldPullImage(types.ServiceConfig{Name: name, Image: img, PullPolicy: hookPolicy}, p.images) + pullRequired, skipReason, err := shouldPullImage(types.ServiceConfig{Name: name, ContainerSpec: types.ContainerSpec{Image: img, PullPolicy: hookPolicy}}, p.images) if err != nil { return err } @@ -194,7 +194,7 @@ func (p *imagePuller) pullHookImages(ctx context.Context) error { continue } p.scheduled[img] = name - hookService := types.ServiceConfig{Name: name, Image: img} + hookService := types.ServiceConfig{Name: name, ContainerSpec: types.ContainerSpec{Image: img}} p.eg.Go(func() error { err := p.pullServiceImage(ctx, hookService, p.opts.Quiet, p.project.Environment["DOCKER_DEFAULT_PLATFORM"]) if err != nil && !p.opts.IgnoreFailures { @@ -410,8 +410,8 @@ func (s *composeService) pullRequiredImages(ctx context.Context, project *types. // Hack: create a fake ServiceConfig so we pull missing volume image n := fmt.Sprintf("%s:volume %d", name, i) needPull[n] = types.ServiceConfig{ - Name: n, - Image: vol.Source, + Name: n, + ContainerSpec: types.ContainerSpec{Image: vol.Source}, } scheduled[vol.Source] = true } @@ -506,7 +506,7 @@ func addPreStartHookPulls(project *types.Project, images map[string]api.ImageSum hookPolicy = types.PullPolicyMissing } for i, img := range api.GetDependentImages(service, project.Name) { - pull, err := mustPull(types.ServiceConfig{Name: name, Image: img, PullPolicy: hookPolicy}, images) + pull, err := mustPull(types.ServiceConfig{Name: name, ContainerSpec: types.ContainerSpec{Image: img, PullPolicy: hookPolicy}}, images) if err != nil { return err } @@ -517,8 +517,8 @@ func addPreStartHookPulls(project *types.Project, images map[string]api.ImageSum // Hack: create a fake ServiceConfig so we pull missing pre_start hook image n := fmt.Sprintf("%s:pre_start %d", name, i) needPull[n] = types.ServiceConfig{ - Name: n, - Image: img, + Name: n, + ContainerSpec: types.ContainerSpec{Image: img}, } } } diff --git a/pkg/compose/pull_test.go b/pkg/compose/pull_test.go index 606ea175b..b0f622513 100644 --- a/pkg/compose/pull_test.go +++ b/pkg/compose/pull_test.go @@ -64,10 +64,12 @@ func scheduledHookImages(t *testing.T, project *types.Project, present map[strin func serviceWithHook(name, img, policy string) types.ServiceConfig { return types.ServiceConfig{ - Name: name, - Image: img, - PullPolicy: policy, - PreStart: []types.ServiceHook{{Image: "init:latest"}}, + Name: name, + + PreStart: []types.PreStartHook{{ContainerSpec: types.ContainerSpec{Image: "init:latest"}}}, ContainerSpec: types.ContainerSpec{ + Image: img, + PullPolicy: policy, + }, } } @@ -166,8 +168,8 @@ func TestPullRequiredImagesUsesContentDigest(t *testing.T) { project := &types.Project{ Name: "demo", Services: types.Services{ - "web": {Name: "web", Image: ref}, - "pinned": {Name: "pinned", Image: ref, Platform: "linux/amd64"}, + "web": {Name: "web", ContainerSpec: types.ContainerSpec{Image: ref}}, + "pinned": {Name: "pinned", ContainerSpec: types.ContainerSpec{Image: ref, Platform: "linux/amd64"}}, }, } images := map[string]api.ImageSummary{} @@ -197,7 +199,7 @@ func TestShouldPullImage(t *testing.T) { "old:1": {LastTagTime: time.Now().Add(-48 * time.Hour)}, } svc := func(image, policy string) types.ServiceConfig { - return types.ServiceConfig{Name: "web", Image: image, PullPolicy: policy} + return types.ServiceConfig{Name: "web", ContainerSpec: types.ContainerSpec{Image: image, PullPolicy: policy}} } t.Run("no explicit policy always refreshes", func(t *testing.T) { @@ -286,9 +288,9 @@ func TestShouldPullImageProvider(t *testing.T) { images := map[string]api.ImageSummary{} pull, _, err := shouldPullImage(types.ServiceConfig{ - Name: "db", - Image: "db:1", - Provider: &types.ServiceProviderConfig{Type: "acme"}, + Name: "db", + + Provider: &types.ServiceProviderConfig{Type: "acme"}, ContainerSpec: types.ContainerSpec{Image: "db:1"}, }, images) assert.NilError(t, err) assert.Assert(t, pull, "provider service with a declared image must be pulled") diff --git a/pkg/compose/reconcile_test.go b/pkg/compose/reconcile_test.go index c0336ce16..9d89275de 100644 --- a/pkg/compose/reconcile_test.go +++ b/pkg/compose/reconcile_test.go @@ -156,7 +156,7 @@ func networkAttachedContainer(t *testing.T, svc types.ServiceConfig, id string) // network is removed then recreated, and the same containers are reconnected — // not recreated. func TestReconcileNetworks_Diverged(t *testing.T) { - web := types.ServiceConfig{Name: "web", Scale: intPtr(1), Networks: map[string]*types.ServiceNetworkConfig{"frontend": {}}} + web := types.ServiceConfig{Name: "web", Scale: intPtr(1), ContainerSpec: types.ContainerSpec{Networks: map[string]*types.ServiceNetworkConfig{"frontend": {}}}} project := &types.Project{ Name: "myproject", Networks: types.Networks{"frontend": {Name: "myproject_frontend", Driver: "overlay"}}, @@ -189,7 +189,7 @@ func TestReconcileNetworks_Diverged(t *testing.T) { // recreated (by reconcileContainers), with the reconnect ordered before the old // container's removal so they don't race. func TestReconcileNetworks_DivergedAlsoRecreatesChangedContainer(t *testing.T) { - web := types.ServiceConfig{Name: "web", Scale: intPtr(1), Networks: map[string]*types.ServiceNetworkConfig{"frontend": {}}} + web := types.ServiceConfig{Name: "web", Scale: intPtr(1), ContainerSpec: types.ContainerSpec{Networks: map[string]*types.ServiceNetworkConfig{"frontend": {}}}} project := &types.Project{ Name: "myproject", Networks: types.Networks{"frontend": {Name: "myproject_frontend", Driver: "overlay"}}, @@ -232,14 +232,12 @@ func TestReconcileNetworks_DivergedMultipleServices(t *testing.T) { }, Services: types.Services{ "web": { - Name: "web", - Scale: intPtr(1), - Networks: map[string]*types.ServiceNetworkConfig{"frontend": {}}, + Name: "web", + Scale: intPtr(1), ContainerSpec: types.ContainerSpec{Networks: map[string]*types.ServiceNetworkConfig{"frontend": {}}}, }, "api": { - Name: "api", - Scale: intPtr(1), - Networks: map[string]*types.ServiceNetworkConfig{"frontend": {}}, + Name: "api", + Scale: intPtr(1), ContainerSpec: types.ContainerSpec{Networks: map[string]*types.ServiceNetworkConfig{"frontend": {}}}, }, }, } @@ -281,7 +279,7 @@ func TestReconcileNetworks_DivergedMultipleServices(t *testing.T) { // migration), so a network still in use by non-Compose containers cannot block a // rename. func TestReconcileNetworks_Renamed(t *testing.T) { - web := types.ServiceConfig{Name: "web", Scale: intPtr(1), Networks: map[string]*types.ServiceNetworkConfig{"frontend": {}}} + web := types.ServiceConfig{Name: "web", Scale: intPtr(1), ContainerSpec: types.ContainerSpec{Networks: map[string]*types.ServiceNetworkConfig{"frontend": {}}}} project := &types.Project{ Name: "myproject", Networks: types.Networks{"frontend": {Name: "myproject_frontend_v2", Driver: "overlay"}}, @@ -431,9 +429,8 @@ func divergedVolumeProject(t *testing.T, count, scale int) (*types.Project, *Obs for s := 0; s < count; s++ { name := fmt.Sprintf("db%d", s) svc := types.ServiceConfig{ - Name: name, - Scale: intPtr(scale), - Volumes: []types.ServiceVolumeConfig{{Source: "data", Type: "volume"}}, + Name: name, + Scale: intPtr(scale), ContainerSpec: types.ContainerSpec{Volumes: []types.ServiceVolumeConfig{{Source: "data", Type: "volume"}}}, } project.Services[name] = svc hash := mustServiceHash(t, svc) @@ -569,11 +566,10 @@ func TestReconcileVolumes_DivergedConfirmedSharedContainer(t *testing.T) { vol2 := types.VolumeConfig{Name: "myproject_data2", Driver: "local"} svc := types.ServiceConfig{ Name: "db", - Scale: intPtr(1), - Volumes: []types.ServiceVolumeConfig{ + Scale: intPtr(1), ContainerSpec: types.ContainerSpec{Volumes: []types.ServiceVolumeConfig{ {Source: "data1", Type: "volume"}, {Source: "data2", Type: "volume"}, - }, + }}, } project := &types.Project{ Name: "myproject", @@ -620,8 +616,8 @@ func TestReconcileVolumes_DivergedConfirmedSharedContainer(t *testing.T) { func TestReconcileVolumes_DivergedPartialConfirm(t *testing.T) { vol1 := types.VolumeConfig{Name: "myproject_data1", Driver: "local"} vol2 := types.VolumeConfig{Name: "myproject_data2", Driver: "local"} - svc1 := types.ServiceConfig{Name: "db1", Scale: intPtr(1), Volumes: []types.ServiceVolumeConfig{{Source: "data1", Type: "volume"}}} - svc2 := types.ServiceConfig{Name: "db2", Scale: intPtr(1), Volumes: []types.ServiceVolumeConfig{{Source: "data2", Type: "volume"}}} + svc1 := types.ServiceConfig{Name: "db1", Scale: intPtr(1), ContainerSpec: types.ContainerSpec{Volumes: []types.ServiceVolumeConfig{{Source: "data1", Type: "volume"}}}} + svc2 := types.ServiceConfig{Name: "db2", Scale: intPtr(1), ContainerSpec: types.ContainerSpec{Volumes: []types.ServiceVolumeConfig{{Source: "data2", Type: "volume"}}}} project := &types.Project{ Name: "myproject", Volumes: types.Volumes{"data1": vol1, "data2": vol2}, @@ -680,17 +676,22 @@ func TestReconcileVolumes_DivergedPartialConfirm(t *testing.T) { func TestReconcileVolumes_DivergedCascadesToDependent(t *testing.T) { vol := types.VolumeConfig{Name: "myproject_data", Driver: "local"} owner := types.ServiceConfig{ - Name: "owner", - Image: "alpine", - Scale: intPtr(1), - Volumes: []types.ServiceVolumeConfig{{Source: "data", Type: "volume"}}, + Name: "owner", + + Scale: intPtr(1), ContainerSpec: types.ContainerSpec{ + Image: "alpine", + + Volumes: []types.ServiceVolumeConfig{{Source: "data", Type: "volume"}}, + }, } dependent := types.ServiceConfig{ - Name: "dependent", - Image: "alpine", - Scale: intPtr(1), - VolumesFrom: []string{"owner"}, - DependsOn: types.DependsOnConfig{"owner": {Condition: types.ServiceConditionStarted, Restart: true, Required: true}}, + Name: "dependent", + + Scale: intPtr(1), ContainerSpec: types.ContainerSpec{ + Image: "alpine", + + VolumesFrom: []string{"owner"}, + }, WorkloadSpec: types.WorkloadSpec{DependsOn: types.DependsOnConfig{"owner": {Condition: types.ServiceConditionStarted, Restart: true, Required: true}}}, } project := &types.Project{ Name: "myproject", @@ -748,19 +749,24 @@ func TestReconcileVolumes_DivergedCascadesToDependent(t *testing.T) { func TestReconcileVolumes_DivergedVolumesFromRemovedBeforeVolume(t *testing.T) { vol := types.VolumeConfig{Name: "myproject_data", Driver: "local"} owner := types.ServiceConfig{ - Name: "owner", - Image: "alpine", - Scale: intPtr(1), - Volumes: []types.ServiceVolumeConfig{{Source: "data", Type: "volume"}}, + Name: "owner", + + Scale: intPtr(1), ContainerSpec: types.ContainerSpec{ + Image: "alpine", + + Volumes: []types.ServiceVolumeConfig{{Source: "data", Type: "volume"}}, + }, } // consumer inherits owner's mounts (including data) but never declares the // volume itself. consumer := types.ServiceConfig{ - Name: "consumer", - Image: "alpine", - Scale: intPtr(1), - VolumesFrom: []string{"owner"}, - DependsOn: types.DependsOnConfig{"owner": {Condition: types.ServiceConditionStarted, Required: true}}, + Name: "consumer", + + Scale: intPtr(1), ContainerSpec: types.ContainerSpec{ + Image: "alpine", + + VolumesFrom: []string{"owner"}, + }, WorkloadSpec: types.WorkloadSpec{DependsOn: types.DependsOnConfig{"owner": {Condition: types.ServiceConditionStarted, Required: true}}}, } project := &types.Project{ Name: "myproject", @@ -831,7 +837,7 @@ func TestReconcileVolumes_UnmanagedMatchReused(t *testing.T) { Name: "myproject", Volumes: types.Volumes{"data": {Name: "myproject_data", Driver: "local"}}, Services: types.Services{ - "db": {Name: "db", Scale: intPtr(1), Volumes: []types.ServiceVolumeConfig{{Source: "data", Type: "volume"}}}, + "db": {Name: "db", Scale: intPtr(1), ContainerSpec: types.ContainerSpec{Volumes: []types.ServiceVolumeConfig{{Source: "data", Type: "volume"}}}}, }, } dbHash := mustServiceHash(t, project.Services["db"]) @@ -894,7 +900,7 @@ func TestReconcileVolumes_RenamedMigratesContainers(t *testing.T) { Name: "myproject", Volumes: types.Volumes{"data": {Name: "myproject_data_v2", Driver: "local"}}, Services: types.Services{ - "db": {Name: "db", Scale: intPtr(1), Volumes: []types.ServiceVolumeConfig{{Source: "data", Type: "volume"}}}, + "db": {Name: "db", Scale: intPtr(1), ContainerSpec: types.ContainerSpec{Volumes: []types.ServiceVolumeConfig{{Source: "data", Type: "volume"}}}}, }, } dbHash := mustServiceHash(t, project.Services["db"]) @@ -1258,10 +1264,9 @@ func TestReconcileContainers_DependsOnChain(t *testing.T) { "db": {Name: "db", Scale: intPtr(1)}, "web": { Name: "web", - Scale: intPtr(1), - DependsOn: types.DependsOnConfig{ + Scale: intPtr(1), WorkloadSpec: types.WorkloadSpec{DependsOn: types.DependsOnConfig{ "db": {Condition: types.ServiceConditionStarted}, - }, + }}, }, }, } @@ -1296,10 +1301,9 @@ func TestReconcileContainers_DependsOnScaleDown(t *testing.T) { "db": svc, "web": { Name: "web", - Scale: intPtr(1), - DependsOn: types.DependsOnConfig{ + Scale: intPtr(1), WorkloadSpec: types.WorkloadSpec{DependsOn: types.DependsOnConfig{ "db": {Condition: types.ServiceConditionStarted}, - }, + }}, }, }, } @@ -1428,8 +1432,8 @@ func TestReconcileContainers_ServiceReference_NoRecreate(t *testing.T) { } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { - parent := types.ServiceConfig{Name: "parent", Image: "alpine", Scale: intPtr(1)} - dependent := types.ServiceConfig{Name: "dependent", Image: "alpine", Scale: intPtr(1)} + parent := types.ServiceConfig{Name: "parent", Scale: intPtr(1), ContainerSpec: types.ContainerSpec{Image: "alpine"}} + dependent := types.ServiceConfig{Name: "dependent", Scale: intPtr(1), ContainerSpec: types.ContainerSpec{Image: "alpine"}} tc.mutate(&dependent) project := &types.Project{ Name: "myproject", @@ -1450,10 +1454,11 @@ func TestReconcileContainers_ServiceReference_NoRecreate(t *testing.T) { // produces for namespace-sharing services, so planStopDependents fires too — // the test also asserts the resulting Stop is not duplicated. func TestReconcileContainers_NamespaceParentRecreated_CascadesToDependent(t *testing.T) { - parent := types.ServiceConfig{Name: "parent", Image: "alpine", Scale: intPtr(1)} + parent := types.ServiceConfig{Name: "parent", Scale: intPtr(1), ContainerSpec: types.ContainerSpec{Image: "alpine"}} dependent := types.ServiceConfig{ - Name: "dependent", Image: "alpine", Scale: intPtr(1), NetworkMode: "service:parent", - DependsOn: types.DependsOnConfig{"parent": {Condition: types.ServiceConditionStarted, Restart: true, Required: true}}, + Name: "dependent", Scale: intPtr(1), + ContainerSpec: types.ContainerSpec{Image: "alpine", NetworkMode: "service:parent"}, + WorkloadSpec: types.WorkloadSpec{DependsOn: types.DependsOnConfig{"parent": {Condition: types.ServiceConditionStarted, Restart: true, Required: true}}}, } project := &types.Project{ Name: "myproject", @@ -1479,17 +1484,20 @@ func TestReconcileContainers_NamespaceParentRecreated_CascadesToDependent(t *tes // non-matching parent: a dependent sharing namespace with two parents must // cascade-recreate when either parent is scheduled for recreation. func TestReconcileContainers_MultipleParents_EitherTriggersCascade(t *testing.T) { - netParent := types.ServiceConfig{Name: "netparent", Image: "alpine", Scale: intPtr(1)} - volParent := types.ServiceConfig{Name: "volparent", Image: "alpine", Scale: intPtr(1)} + netParent := types.ServiceConfig{Name: "netparent", Scale: intPtr(1), ContainerSpec: types.ContainerSpec{Image: "alpine"}} + volParent := types.ServiceConfig{Name: "volparent", Scale: intPtr(1), ContainerSpec: types.ContainerSpec{Image: "alpine"}} dependent := types.ServiceConfig{ - Name: "dependent", Image: "alpine", Scale: intPtr(1), - NetworkMode: "service:netparent", - VolumesFrom: []string{"volparent"}, - // Mirrors what compose-go's normalizer injects for namespace-sharing - // references, so the dependency graph orders parents before dependent. - DependsOn: types.DependsOnConfig{ - "netparent": {Condition: types.ServiceConditionStarted, Restart: true, Required: true}, - "volparent": {Condition: types.ServiceConditionStarted, Restart: true, Required: true}, + Name: "dependent", Scale: intPtr(1), ContainerSpec: types.ContainerSpec{ + Image: "alpine", + NetworkMode: "service:netparent", + VolumesFrom: []string{"volparent"}, + }, WorkloadSpec: types.WorkloadSpec{ + // Mirrors what compose-go's normalizer injects for namespace-sharing + // references, so the dependency graph orders parents before dependent. + DependsOn: types.DependsOnConfig{ + "netparent": {Condition: types.ServiceConditionStarted, Restart: true, Required: true}, + "volparent": {Condition: types.ServiceConditionStarted, Restart: true, Required: true}, + }, }, } project := &types.Project{ @@ -1548,10 +1556,11 @@ func TestReconcileContainers_MultipleParents_EitherTriggersCascade(t *testing.T) // TestReconcileContainers_RegularDependsOn_NoCascade ensures the cascade fires // only for namespace/volume-sharing dependencies, not for plain depends_on. func TestReconcileContainers_RegularDependsOn_NoCascade(t *testing.T) { - parent := types.ServiceConfig{Name: "parent", Image: "alpine", Scale: intPtr(1)} + parent := types.ServiceConfig{Name: "parent", Scale: intPtr(1), ContainerSpec: types.ContainerSpec{Image: "alpine"}} dependent := types.ServiceConfig{ - Name: "dependent", Image: "alpine", Scale: intPtr(1), - DependsOn: types.DependsOnConfig{"parent": {Condition: types.ServiceConditionStarted, Restart: true}}, + Name: "dependent", Scale: intPtr(1), + ContainerSpec: types.ContainerSpec{Image: "alpine"}, + WorkloadSpec: types.WorkloadSpec{DependsOn: types.DependsOnConfig{"parent": {Condition: types.ServiceConditionStarted, Restart: true}}}, } project := &types.Project{ Name: "myproject", diff --git a/pkg/compose/restart_test.go b/pkg/compose/restart_test.go index 9e4041dd5..9c04f1147 100644 --- a/pkg/compose/restart_test.go +++ b/pkg/compose/restart_test.go @@ -51,16 +51,14 @@ func TestPrepareRestartProject(t *testing.T) { Name: "prj", Services: types.Services{ "proxy": { - Name: "proxy", - DependsOn: types.DependsOnConfig{ + Name: "proxy", WorkloadSpec: types.WorkloadSpec{DependsOn: types.DependsOnConfig{ "web": {Condition: types.ServiceConditionStarted, Restart: true, Required: true}, - }, + }}, }, "web": { - Name: "web", - DependsOn: types.DependsOnConfig{ + Name: "web", WorkloadSpec: types.WorkloadSpec{DependsOn: types.DependsOnConfig{ "db": {Condition: types.ServiceConditionStarted, Restart: false, Required: true}, - }, + }}, }, "db": {Name: "db"}, }, diff --git a/pkg/compose/service_containers_test.go b/pkg/compose/service_containers_test.go index d04a87979..6440401e9 100644 --- a/pkg/compose/service_containers_test.go +++ b/pkg/compose/service_containers_test.go @@ -249,13 +249,12 @@ func TestWaitDependencies(t *testing.T) { replicas := 0 project := types.Project{Name: strings.ToLower(testProject), Services: types.Services{ "app": { - Name: "app", - DependsOn: types.DependsOnConfig{ + Name: "app", WorkloadSpec: types.WorkloadSpec{DependsOn: types.DependsOnConfig{ "disabled": { Condition: ServiceConditionRunningOrHealthy, Required: true, }, - }, + }}, }, "disabled": { Name: "disabled", @@ -487,15 +486,14 @@ func TestCreateMobyContainer(t *testing.T) { apiClient.EXPECT().ClientVersion().Return("1.44").AnyTimes() service := types.ServiceConfig{ - Name: "test", - Networks: map[string]*types.ServiceNetworkConfig{ + Name: "test", ContainerSpec: types.ContainerSpec{Networks: map[string]*types.ServiceNetworkConfig{ "a": { Priority: 10, }, "b": { Priority: 100, }, - }, + }}, } project := types.Project{ Name: "bork", @@ -586,11 +584,10 @@ func TestCreateMobyContainerLegacyAPI(t *testing.T) { apiClient.EXPECT().ClientVersion().Return("1.43").AnyTimes() service := types.ServiceConfig{ - Name: "test", - Networks: map[string]*types.ServiceNetworkConfig{ + Name: "test", ContainerSpec: types.ContainerSpec{Networks: map[string]*types.ServiceNetworkConfig{ "a": {Priority: 10}, "b": {Priority: 100}, - }, + }}, } project := types.Project{ Name: "bork", @@ -675,11 +672,10 @@ func TestCreateMobyContainerLegacyAPI_NetworkConnectFailure(t *testing.T) { apiClient.EXPECT().ClientVersion().Return("1.43").AnyTimes() service := types.ServiceConfig{ - Name: "test", - Networks: map[string]*types.ServiceNetworkConfig{ + Name: "test", ContainerSpec: types.ContainerSpec{Networks: map[string]*types.ServiceNetworkConfig{ "a": {Priority: 10}, "b": {Priority: 100}, - }, + }}, } project := types.Project{ Name: "bork", diff --git a/pkg/compose/start_test.go b/pkg/compose/start_test.go index be1d80e7b..b392112a0 100644 --- a/pkg/compose/start_test.go +++ b/pkg/compose/start_test.go @@ -26,6 +26,7 @@ import ( "testing" "github.com/compose-spec/compose-go/v2/types" + "github.com/docker/cli/cli/config/configfile" "github.com/moby/moby/api/types/container" "github.com/moby/moby/client" "go.uber.org/mock/gomock" @@ -89,6 +90,10 @@ func newStartTestService(t *testing.T) (*composeService, *mocks.MockAPIClient, * apiClient.EXPECT().Ping(gomock.Any(), client.PingOptions{NegotiateAPIVersion: true}). Return(client.PingResult{APIVersion: "1.44"}, nil).AnyTimes() apiClient.EXPECT().ClientVersion().Return("1.44").AnyTimes() + // the generic pre_start inheritance goes through getCreateConfigs, which + // reads the CLI configuration and the daemon host + cli.EXPECT().ConfigFile().Return(&configfile.ConfigFile{}).AnyTimes() + apiClient.EXPECT().DaemonHost().Return("unix:///var/run/docker.sock").AnyTimes() rec := &recordingEventProcessor{} svc, err := NewComposeService(cli, WithEventProcessor(rec)) @@ -164,7 +169,7 @@ func TestStartService_StartsOnlyStoppedReplicas(t *testing.T) { project := &types.Project{Name: "prj"} service := types.ServiceConfig{ Name: "web", - PreStart: []types.ServiceHook{{Command: types.ShellCommand{"init"}}}, + PreStart: []types.PreStartHook{{ContainerSpec: types.ContainerSpec{Command: types.ShellCommand{"init"}}}}, } running := serviceContainer("web", 1, container.StateRunning) stopped := serviceContainer("web", 2, container.StateExited) @@ -194,13 +199,14 @@ func TestStartService_PreStartOnLowestReplica(t *testing.T) { project := &types.Project{Name: "prj"} service := types.ServiceConfig{ - Name: "web", - Image: "alpine", - PreStart: []types.ServiceHook{{Command: types.ShellCommand{"init"}}}, + Name: "web", + ContainerSpec: types.ContainerSpec{Image: "alpine"}, + PreStart: []types.PreStartHook{{ContainerSpec: types.ContainerSpec{Command: types.ShellCommand{"init"}}}}, } - // Listed out of order on purpose: replica 2 first. + replica2 := serviceContainer("web", 2, container.StateExited) replica1 := serviceContainer("web", 1, container.StateExited) + // Listed out of order on purpose: replica 2 first. containers := Containers{replica2, replica1} // runPreStart sweeps orphan hook containers from any previous failed run @@ -276,9 +282,9 @@ func TestStartServiceContainer_Order(t *testing.T) { }, } service := types.ServiceConfig{ - Name: "web", - Secrets: []types.ServiceSecretConfig{{Source: "token"}}, - PostStart: []types.ServiceHook{{Command: types.ShellCommand{"notify"}}}, + Name: "web", + + PostStart: []types.ServiceHook{{Command: types.ShellCommand{"notify"}}}, ContainerSpec: types.ContainerSpec{Secrets: []types.ServiceSecretConfig{{Source: "token"}}}, } ctr := serviceContainer("web", 1, container.StateExited) @@ -350,10 +356,9 @@ func TestStartServiceContainer_FailedPostStart(t *testing.T) { func TestGetDependencyCondition(t *testing.T) { oneShot := types.ServiceConfig{Name: "migrate"} web := types.ServiceConfig{ - Name: "web", - DependsOn: types.DependsOnConfig{ + Name: "web", WorkloadSpec: types.WorkloadSpec{DependsOn: types.DependsOnConfig{ "migrate": {Condition: types.ServiceConditionCompletedSuccessfully}, - }, + }}, } project := &types.Project{ Name: "prj", diff --git a/pkg/compose/viz_test.go b/pkg/compose/viz_test.go index 2ecbb5ab0..85c58e2b2 100644 --- a/pkg/compose/viz_test.go +++ b/pkg/compose/viz_test.go @@ -35,9 +35,13 @@ func TestViz(t *testing.T) { WorkingDir: "/home", Services: types.Services{ "service1": { - Name: "service1", - Image: "image-for-service1", - Ports: []types.ServicePortConfig{ + Name: "service1", ContainerSpec: types.ContainerSpec{ + Image: "image-for-service1", + + Networks: map[string]*types.ServiceNetworkConfig{ + "internal": nil, + }, + }, WorkloadSpec: types.WorkloadSpec{Ports: []types.ServicePortConfig{ { Published: "80", Target: 80, @@ -48,51 +52,47 @@ func TestViz(t *testing.T) { Target: 533, Protocol: "udp", }, - }, - Networks: map[string]*types.ServiceNetworkConfig{ - "internal": nil, - }, + }}, }, "service2": { - Name: "service2", - Image: "image-for-service2", - Ports: []types.ServicePortConfig{}, + Name: "service2", ContainerSpec: types.ContainerSpec{Image: "image-for-service2"}, WorkloadSpec: types.WorkloadSpec{Ports: []types.ServicePortConfig{}}, }, "service3": { - Name: "service3", - Image: "some-image", - DependsOn: map[string]types.ServiceDependency{ + Name: "service3", ContainerSpec: types.ContainerSpec{Image: "some-image"}, WorkloadSpec: types.WorkloadSpec{DependsOn: map[string]types.ServiceDependency{ "service2": {}, "service1": {}, - }, + }}, }, "service4": { - Name: "service4", - Image: "another-image", - DependsOn: map[string]types.ServiceDependency{ - "service3": {}, - }, - Ports: []types.ServicePortConfig{ - { - Published: "8080", - Target: 80, + Name: "service4", ContainerSpec: types.ContainerSpec{ + Image: "another-image", + + Networks: map[string]*types.ServiceNetworkConfig{ + "external": nil, + }, + }, WorkloadSpec: types.WorkloadSpec{ + DependsOn: map[string]types.ServiceDependency{ + "service3": {}, + }, + Ports: []types.ServicePortConfig{ + { + Published: "8080", + Target: 80, + }, }, - }, - Networks: map[string]*types.ServiceNetworkConfig{ - "external": nil, }, }, "With host IP": { - Name: "With host IP", - Image: "user/image-name", - DependsOn: map[string]types.ServiceDependency{ - "service1": {}, - }, - Ports: []types.ServicePortConfig{ - { - Published: "8888", - Target: 8080, - HostIP: "127.0.0.1", + Name: "With host IP", ContainerSpec: types.ContainerSpec{Image: "user/image-name"}, WorkloadSpec: types.WorkloadSpec{ + DependsOn: map[string]types.ServiceDependency{ + "service1": {}, + }, + Ports: []types.ServicePortConfig{ + { + Published: "8888", + Target: 8080, + HostIP: "127.0.0.1", + }, }, }, }, diff --git a/pkg/compose/watch_test.go b/pkg/compose/watch_test.go index e217d627e..33fb48c76 100644 --- a/pkg/compose/watch_test.go +++ b/pkg/compose/watch_test.go @@ -280,8 +280,7 @@ func TestInitialSyncFilesRegularFile(t *testing.T) { syncer := &fakeSyncer{synced: make(chan []*sync.PathMapping, 1)} err := (&composeService{}).initialSync(t.Context(), types.ServiceConfig{ - Name: "svc", - Build: &types.BuildConfig{Context: hostDir}, + Name: "svc", WorkloadSpec: types.WorkloadSpec{Build: &types.BuildConfig{Context: hostDir}}, }, types.Trigger{ Path: hostFile, Target: "/app/test.txt", @@ -305,8 +304,8 @@ func TestInitialSync_ExcludesDockerfileAndComposeFiles(t *testing.T) { syncer := &fakeSyncer{synced: make(chan []*sync.PathMapping, 1)} err := (&composeService{}).initialSync(t.Context(), types.ServiceConfig{ - Name: "svc", - Build: &types.BuildConfig{Context: hostDir}, + Name: "svc", + WorkloadSpec: types.WorkloadSpec{Build: &types.BuildConfig{Context: hostDir}}, }, types.Trigger{ Path: hostDir, Target: "/app", @@ -331,8 +330,8 @@ func TestInitialSync_ExcludesCustomNamedDockerfile(t *testing.T) { syncer := &fakeSyncer{synced: make(chan []*sync.PathMapping, 1)} err := (&composeService{}).initialSync(t.Context(), types.ServiceConfig{ - Name: "svc", - Build: &types.BuildConfig{Context: hostDir, Dockerfile: "Dockerfile.prod"}, + Name: "svc", + WorkloadSpec: types.WorkloadSpec{Build: &types.BuildConfig{Context: hostDir, Dockerfile: "Dockerfile.prod"}}, }, types.Trigger{ Path: hostDir, Target: "/app", @@ -360,8 +359,8 @@ func TestInitialSync_ExcludesNestedCustomNamedDockerfile(t *testing.T) { syncer := &fakeSyncer{synced: make(chan []*sync.PathMapping, 1)} err := (&composeService{}).initialSync(t.Context(), types.ServiceConfig{ - Name: "svc", - Build: &types.BuildConfig{Context: hostDir, Dockerfile: "docker/Dockerfile.prod"}, + Name: "svc", + WorkloadSpec: types.WorkloadSpec{Build: &types.BuildConfig{Context: hostDir, Dockerfile: "docker/Dockerfile.prod"}}, }, types.Trigger{ Path: hostDir, Target: "/app", diff --git a/pkg/e2e/compose_test.go b/pkg/e2e/compose_test.go index 7c29a0565..20fee1cc1 100644 --- a/pkg/e2e/compose_test.go +++ b/pkg/e2e/compose_test.go @@ -213,11 +213,11 @@ func TestConfig(t *testing.T) { OutputContains(fmt.Sprintf(`name: %s services: nginx: + networks: + default: null build: context: %s dockerfile: Dockerfile - networks: - default: null networks: default: name: %s_default