Browse Source

project.Services is a map

Signed-off-by: Nicolas De Loof <[email protected]>
Nicolas De Loof 1 year ago
parent
commit
138facea62

+ 0 - 1
cmd/compose/build.go

@@ -23,7 +23,6 @@ import (
 	"strings"
 
 	"github.com/compose-spec/compose-go/v2/cli"
-	"github.com/compose-spec/compose-go/v2/loader"
 	"github.com/compose-spec/compose-go/v2/types"
 	"github.com/docker/cli/cli/command"
 	cliopts "github.com/docker/cli/opts"

+ 4 - 4
cmd/compose/compose_test.go

@@ -26,20 +26,20 @@ import (
 func TestFilterServices(t *testing.T) {
 	p := &types.Project{
 		Services: types.Services{
-			{
+			"foo": {
 				Name:  "foo",
 				Links: []string{"bar"},
 			},
-			{
+			"bar": {
 				Name: "bar",
 				DependsOn: map[string]types.ServiceDependency{
 					"zot": {},
 				},
 			},
-			{
+			"zot": {
 				Name: "zot",
 			},
-			{
+			"qix": {
 				Name: "qix",
 			},
 		},

+ 1 - 1
cmd/compose/create_test.go

@@ -86,7 +86,7 @@ func sampleProject() *types.Project {
 	return &types.Project{
 		Name: "test",
 		Services: types.Services{
-			{
+			"svc": {
 				Name: "svc",
 				Build: &types.BuildConfig{
 					Context: ".",

+ 1 - 4
cmd/compose/options.go

@@ -25,10 +25,7 @@ import (
 
 func applyPlatforms(project *types.Project, buildForSinglePlatform bool) error {
 	defaultPlatform := project.Environment["DOCKER_DEFAULT_PLATFORM"]
-	for i := range project.Services {
-		// mutable reference so platform fields can be updated
-		service := &project.Services[i]
-
+	for _, service := range project.Services {
 		if service.Build == nil {
 			continue
 		}

+ 7 - 7
cmd/compose/options_test.go

@@ -27,7 +27,7 @@ func TestApplyPlatforms_InferFromRuntime(t *testing.T) {
 	makeProject := func() *types.Project {
 		return &types.Project{
 			Services: types.Services{
-				{
+				"test": {
 					Name:  "test",
 					Image: "foo",
 					Build: &types.BuildConfig{
@@ -47,14 +47,14 @@ func TestApplyPlatforms_InferFromRuntime(t *testing.T) {
 	t.Run("SinglePlatform", func(t *testing.T) {
 		project := makeProject()
 		require.NoError(t, applyPlatforms(project, true))
-		require.EqualValues(t, []string{"alice/32"}, project.Services[0].Build.Platforms)
+		require.EqualValues(t, []string{"alice/32"}, project.Services["test"].Build.Platforms)
 	})
 
 	t.Run("MultiPlatform", func(t *testing.T) {
 		project := makeProject()
 		require.NoError(t, applyPlatforms(project, false))
 		require.EqualValues(t, []string{"linux/amd64", "linux/arm64", "alice/32"},
-			project.Services[0].Build.Platforms)
+			project.Services["test"].Build.Platforms)
 	})
 }
 
@@ -65,7 +65,7 @@ func TestApplyPlatforms_DockerDefaultPlatform(t *testing.T) {
 				"DOCKER_DEFAULT_PLATFORM": "linux/amd64",
 			},
 			Services: types.Services{
-				{
+				"test": {
 					Name:  "test",
 					Image: "foo",
 					Build: &types.BuildConfig{
@@ -83,14 +83,14 @@ func TestApplyPlatforms_DockerDefaultPlatform(t *testing.T) {
 	t.Run("SinglePlatform", func(t *testing.T) {
 		project := makeProject()
 		require.NoError(t, applyPlatforms(project, true))
-		require.EqualValues(t, []string{"linux/amd64"}, project.Services[0].Build.Platforms)
+		require.EqualValues(t, []string{"linux/amd64"}, project.Services["test"].Build.Platforms)
 	})
 
 	t.Run("MultiPlatform", func(t *testing.T) {
 		project := makeProject()
 		require.NoError(t, applyPlatforms(project, false))
 		require.EqualValues(t, []string{"linux/amd64", "linux/arm64"},
-			project.Services[0].Build.Platforms)
+			project.Services["test"].Build.Platforms)
 	})
 }
 
@@ -101,7 +101,7 @@ func TestApplyPlatforms_UnsupportedPlatform(t *testing.T) {
 				"DOCKER_DEFAULT_PLATFORM": "commodore/64",
 			},
 			Services: types.Services{
-				{
+				"foo": {
 					Name:  "test",
 					Image: "foo",
 					Build: &types.BuildConfig{

+ 6 - 6
cmd/compose/pullOptions_test.go

@@ -26,21 +26,21 @@ import (
 func TestApplyPullOptions(t *testing.T) {
 	project := &types.Project{
 		Services: types.Services{
-			{
+			"must-build": {
 				Name: "must-build",
 				// No image, local build only
 				Build: &types.BuildConfig{
 					Context: ".",
 				},
 			},
-			{
+			"has-build": {
 				Name:  "has-build",
 				Image: "registry.example.com/myservice",
 				Build: &types.BuildConfig{
 					Context: ".",
 				},
 			},
-			{
+			"must-pull": {
 				Name:  "must-pull",
 				Image: "registry.example.com/another-service",
 			},
@@ -51,7 +51,7 @@ func TestApplyPullOptions(t *testing.T) {
 	}.apply(project, nil)
 	assert.NilError(t, err)
 
-	assert.Equal(t, project.Services[0].PullPolicy, "") // still default
-	assert.Equal(t, project.Services[1].PullPolicy, types.PullPolicyMissing)
-	assert.Equal(t, project.Services[2].PullPolicy, types.PullPolicyMissing)
+	assert.Equal(t, project.Services["must-build"].PullPolicy, "") // still default
+	assert.Equal(t, project.Services["has-build"].PullPolicy, types.PullPolicyMissing)
+	assert.Equal(t, project.Services["must-pull"].PullPolicy, types.PullPolicyMissing)
 }

+ 3 - 3
cmd/compose/run.go

@@ -300,16 +300,16 @@ func runRun(ctx context.Context, backend api.Service, project *types.Project, op
 func startDependencies(ctx context.Context, backend api.Service, project types.Project, buildOpts *api.BuildOptions, requestedServiceName string, ignoreOrphans bool) error {
 	dependencies := types.Services{}
 	var requestedService types.ServiceConfig
-	for _, service := range project.Services {
+	for name, service := range project.Services {
 		if service.Name != requestedServiceName {
-			dependencies = append(dependencies, service)
+			dependencies[name] = service
 		} else {
 			requestedService = service
 		}
 	}
 
 	project.Services = dependencies
-	project.DisabledServices = append(project.DisabledServices, requestedService)
+	project.DisabledServices[requestedServiceName] = requestedService
 	err := backend.Create(ctx, &project, api.CreateOptions{
 		Build:         buildOpts,
 		IgnoreOrphans: ignoreOrphans,

+ 2 - 2
cmd/compose/up_test.go

@@ -26,10 +26,10 @@ import (
 func TestApplyScaleOpt(t *testing.T) {
 	p := types.Project{
 		Services: types.Services{
-			{
+			"foo": {
 				Name: "foo",
 			},
-			{
+			"bar": {
 				Name: "bar",
 				Deploy: &types.DeployConfig{
 					Mode: "test",

+ 1 - 1
go.mod

@@ -6,7 +6,7 @@ require (
 	github.com/AlecAivazis/survey/v2 v2.3.7
 	github.com/Microsoft/go-winio v0.6.1
 	github.com/buger/goterm v1.0.4
-	github.com/compose-spec/compose-go/v2 v2.0.0-20231121074112-593b77722992
+	github.com/compose-spec/compose-go/v2 v2.0.0-20231123162526-11ef9572f1a4
 	github.com/containerd/console v1.0.3
 	github.com/containerd/containerd v1.7.7
 	github.com/davecgh/go-spew v1.1.1

+ 2 - 2
go.sum

@@ -132,8 +132,8 @@ github.com/cncf/xds/go v0.0.0-20230607035331-e9ce68804cb4 h1:/inchEIKaYC1Akx+H+g
 github.com/cncf/xds/go v0.0.0-20230607035331-e9ce68804cb4/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
 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.0.0-20231121074112-593b77722992 h1:0BM7GPtSRK7djjvG3h67aJYH8eRikBgxkrEG7wNtgaU=
-github.com/compose-spec/compose-go/v2 v2.0.0-20231121074112-593b77722992/go.mod h1:uAthZuC/GWStR8mxGMRaQyaOeSqA4V+MZIiAIfuBoIU=
+github.com/compose-spec/compose-go/v2 v2.0.0-20231123162526-11ef9572f1a4 h1:Lr78By808iuG+2gTyxIDslRpKQCk/lcRqElKsrhzp+U=
+github.com/compose-spec/compose-go/v2 v2.0.0-20231123162526-11ef9572f1a4/go.mod h1:PWCgeD8cxiI/DmdpBM407CuLDrZ2W4xuS6/Z9jAi0YQ=
 github.com/containerd/cgroups v1.1.0 h1:v8rEWFl6EoqHB+swVNjVoCJE8o3jX7e8nqBGPLaDFBM=
 github.com/containerd/cgroups v1.1.0/go.mod h1:6ppBcbh/NOOUU+dMKrykgaBnK9lCIBxHqJDGwsa1mIw=
 github.com/containerd/console v1.0.3 h1:lIr7SlA5PxZyMV30bDW0MGbiOPXwc63yRuCP0ARubLw=

+ 1 - 6
internal/tracing/attributes.go

@@ -58,17 +58,12 @@ func ProjectOptions(proj *types.Project) SpanOptions {
 		return nil
 	}
 
-	disabledServiceNames := make([]string, len(proj.DisabledServices))
-	for i := range proj.DisabledServices {
-		disabledServiceNames[i] = proj.DisabledServices[i].Name
-	}
-
 	attrs := []attribute.KeyValue{
 		attribute.String("project.name", proj.Name),
 		attribute.String("project.dir", proj.WorkingDir),
 		attribute.StringSlice("project.compose_files", proj.ComposeFiles),
 		attribute.StringSlice("project.services.active", proj.ServiceNames()),
-		attribute.StringSlice("project.services.disabled", disabledServiceNames),
+		attribute.StringSlice("project.services.disabled", proj.DisabledServiceNames()),
 		attribute.StringSlice("project.profiles", proj.Profiles),
 		attribute.StringSlice("project.volumes", proj.VolumeNames()),
 		attribute.StringSlice("project.networks", proj.NetworkNames()),

+ 21 - 24
pkg/compose/build.go

@@ -67,7 +67,7 @@ func (s *composeService) Build(ctx context.Context, project *types.Project, opti
 }
 
 type serviceToBuild struct {
-	idx     int
+	name    string
 	service types.ServiceConfig
 }
 
@@ -85,7 +85,7 @@ func (s *composeService) build(ctx context.Context, project *types.Project, opti
 		if len(options.Services) > 0 && !utils.Contains(options.Services, name) {
 			return nil
 		}
-		service, idx := getServiceIndex(project, name)
+		service := project.Services[name]
 
 		if service.Build == nil {
 			return nil
@@ -97,7 +97,7 @@ func (s *composeService) build(ctx context.Context, project *types.Project, opti
 			return nil
 		}
 		mapServiceMutx.Lock()
-		serviceToBeBuild[name] = serviceToBuild{idx: idx, service: service}
+		serviceToBeBuild[name] = serviceToBuild{name: name, service: service}
 		mapServiceMutx.Unlock()
 		return nil
 	}, func(traversal *graphTraversal) {
@@ -146,7 +146,17 @@ func (s *composeService) build(ctx context.Context, project *types.Project, opti
 		}
 	}
 
+	// we use a pre-allocated []string to collect build digest by service index while running concurrent goroutines
 	builtDigests := make([]string, len(project.Services))
+	names := project.ServiceNames()
+	getServiceIndex := func(name string) int {
+		for idx, n := range names {
+			if n == name {
+				return idx
+			}
+		}
+		return -1
+	}
 	err = InDependencyOrder(ctx, project, func(ctx context.Context, name string) error {
 		if len(options.Services) > 0 && !utils.Contains(options.Services, name) {
 			return nil
@@ -156,14 +166,13 @@ func (s *composeService) build(ctx context.Context, project *types.Project, opti
 			return nil
 		}
 		service := serviceToBuild.service
-		idx := serviceToBuild.idx
 
 		if !buildkitEnabled {
 			id, err := s.doBuildClassic(ctx, project, service, options)
 			if err != nil {
 				return err
 			}
-			builtDigests[idx] = id
+			builtDigests[getServiceIndex(name)] = id
 
 			if options.Push {
 				return s.push(ctx, project, api.PushOptions{})
@@ -184,7 +193,7 @@ func (s *composeService) build(ctx context.Context, project *types.Project, opti
 		if err != nil {
 			return err
 		}
-		builtDigests[idx] = digest
+		builtDigests[getServiceIndex(name)] = digest
 
 		return nil
 	}, func(traversal *graphTraversal) {
@@ -204,25 +213,13 @@ func (s *composeService) build(ctx context.Context, project *types.Project, opti
 
 	for i, imageDigest := range builtDigests {
 		if imageDigest != "" {
-			imageRef := api.GetImageNameOrDefault(project.Services[i], project.Name)
+			imageRef := api.GetImageNameOrDefault(project.Services[names[i]], project.Name)
 			imageIDs[imageRef] = imageDigest
 		}
 	}
 	return imageIDs, err
 }
 
-func getServiceIndex(project *types.Project, name string) (types.ServiceConfig, int) {
-	var service types.ServiceConfig
-	var idx int
-	for i, s := range project.Services {
-		if s.Name == name {
-			idx, service = i, s
-			break
-		}
-	}
-	return service, idx
-}
-
 func (s *composeService) ensureImagesExists(ctx context.Context, project *types.Project, buildOpts *api.BuildOptions, quietPull bool) error {
 	for _, service := range project.Services {
 		if service.Image == "" && service.Build == nil {
@@ -264,14 +261,14 @@ func (s *composeService) ensureImagesExists(ctx context.Context, project *types.
 	}
 
 	// set digest as com.docker.compose.image label so we can detect outdated containers
-	for i, service := range project.Services {
+	for _, service := range project.Services {
 		image := api.GetImageNameOrDefault(service, project.Name)
 		digest, ok := images[image]
 		if ok {
-			if project.Services[i].Labels == nil {
-				project.Services[i].Labels = types.Labels{}
+			if service.Labels == nil {
+				service.Labels = types.Labels{}
 			}
-			project.Services[i].CustomLabels.Add(api.ImageDigestLabel, digest)
+			service.CustomLabels.Add(api.ImageDigestLabel, digest)
 		}
 	}
 	return nil
@@ -440,7 +437,7 @@ func (s *composeService) toBuildOptions(project *types.Project, service types.Se
 		Platforms:   plats,
 		Labels:      imageLabels,
 		NetworkMode: service.Build.Network,
-		ExtraHosts:  service.Build.ExtraHosts.AsList(),
+		ExtraHosts:  service.Build.ExtraHosts.AsList(":"),
 		Ulimits:     toUlimitOpt(service.Build.Ulimits),
 		Session:     sessionConfig,
 		Allow:       allow,

+ 1 - 1
pkg/compose/build_classic.go

@@ -229,7 +229,7 @@ func imageBuildOptions(dockerCli command.Cli, project *types.Project, service ty
 		BuildArgs:   resolveAndMergeBuildArgs(dockerCli, project, service, options),
 		Labels:      config.Labels,
 		NetworkMode: config.Network,
-		ExtraHosts:  config.ExtraHosts.AsList(),
+		ExtraHosts:  config.ExtraHosts.AsList(":"),
 		Target:      config.Target,
 		Isolation:   container.Isolation(config.Isolation),
 	}

+ 5 - 3
pkg/compose/compose.go

@@ -183,7 +183,7 @@ func (s *composeService) projectFromName(containers Containers, projectName stri
 	if len(containers) == 0 {
 		return project, fmt.Errorf("no container found for project %q: %w", projectName, api.ErrNotFound)
 	}
-	set := map[string]types.ServiceConfig{}
+	set := types.Services{}
 	for _, c := range containers {
 		serviceLabel := c.Labels[api.ServiceLabel]
 		service, ok := set[serviceLabel]
@@ -197,7 +197,7 @@ func (s *composeService) projectFromName(containers Containers, projectName stri
 		}
 		service.Scale = increment(service.Scale)
 	}
-	for _, service := range set {
+	for name, service := range set {
 		dependencies := service.Labels[api.DependenciesLabel]
 		if len(dependencies) > 0 {
 			service.DependsOn = types.DependsOnConfig{}
@@ -218,9 +218,11 @@ func (s *composeService) projectFromName(containers Containers, projectName stri
 				}
 				service.DependsOn[dependency] = types.ServiceDependency{Condition: condition, Restart: restart, Required: required}
 			}
+			set[name] = service
 		}
-		project.Services = append(project.Services, service)
 	}
+	project.Services = set
+
 SERVICES:
 	for _, qs := range services {
 		for _, es := range project.Services {

+ 8 - 2
pkg/compose/convergence_test.go

@@ -227,7 +227,10 @@ func TestWaitDependencies(t *testing.T) {
 	t.Run("should skip dependencies with scale 0", func(t *testing.T) {
 		dbService := types.ServiceConfig{Name: "db", Scale: intPtr(0)}
 		redisService := types.ServiceConfig{Name: "redis", Scale: intPtr(0)}
-		project := types.Project{Name: strings.ToLower(testProject), Services: types.Services{dbService, redisService}}
+		project := types.Project{Name: strings.ToLower(testProject), Services: types.Services{
+			"db":    dbService,
+			"redis": redisService,
+		}}
 		dependencies := types.DependsOnConfig{
 			"db":    {Condition: ServiceConditionRunningOrHealthy},
 			"redis": {Condition: ServiceConditionRunningOrHealthy},
@@ -237,7 +240,10 @@ func TestWaitDependencies(t *testing.T) {
 	t.Run("should skip dependencies with condition service_started", func(t *testing.T) {
 		dbService := types.ServiceConfig{Name: "db", Scale: intPtr(1)}
 		redisService := types.ServiceConfig{Name: "redis", Scale: intPtr(1)}
-		project := types.Project{Name: strings.ToLower(testProject), Services: types.Services{dbService, redisService}}
+		project := types.Project{Name: strings.ToLower(testProject), Services: types.Services{
+			"db":    dbService,
+			"redis": redisService,
+		}}
 		dependencies := types.DependsOnConfig{
 			"db":    {Condition: types.ServiceConditionStarted, Required: true},
 			"redis": {Condition: types.ServiceConditionStarted, Required: true},

+ 2 - 6
pkg/compose/create.go

@@ -94,11 +94,7 @@ func (s *composeService) create(ctx context.Context, project *types.Project, opt
 		return err
 	}
 
-	allServices := project.AllServices()
-	allServiceNames := []string{}
-	for _, service := range allServices {
-		allServiceNames = append(allServiceNames, service.Name)
-	}
+	allServiceNames := append(project.ServiceNames(), project.DisabledServiceNames()...)
 	orphans := observedState.filter(isNotService(allServiceNames...))
 	if len(orphans) > 0 && !options.IgnoreOrphans {
 		if options.RemoveOrphans {
@@ -263,7 +259,7 @@ func (s *composeService) getCreateConfigs(ctx context.Context,
 		DNS:            service.DNS,
 		DNSSearch:      service.DNSSearch,
 		DNSOptions:     service.DNSOpts,
-		ExtraHosts:     service.ExtraHosts.AsList(),
+		ExtraHosts:     service.ExtraHosts.AsList(":"),
 		SecurityOpt:    securityOpts,
 		UsernsMode:     container.UsernsMode(service.UserNSMode),
 		UTSMode:        container.UTSMode(service.Uts),

+ 11 - 11
pkg/compose/create_test.go

@@ -101,8 +101,8 @@ func TestPrepareNetworkLabels(t *testing.T) {
 func TestBuildContainerMountOptions(t *testing.T) {
 	project := composetypes.Project{
 		Name: "myProject",
-		Services: []composetypes.ServiceConfig{
-			{
+		Services: composetypes.Services{
+			"myService": {
 				Name: "myService",
 				Volumes: []composetypes.ServiceVolumeConfig{
 					{
@@ -144,7 +144,7 @@ func TestBuildContainerMountOptions(t *testing.T) {
 		},
 	}
 
-	mounts, err := buildContainerMountOptions(project, project.Services[0], moby.ImageInspect{}, inherit)
+	mounts, err := buildContainerMountOptions(project, project.Services["myService"], moby.ImageInspect{}, inherit)
 	sort.Slice(mounts, func(i, j int) bool {
 		return mounts[i].Target < mounts[j].Target
 	})
@@ -154,7 +154,7 @@ func TestBuildContainerMountOptions(t *testing.T) {
 	assert.Equal(t, mounts[1].Target, "/var/myvolume2")
 	assert.Equal(t, mounts[2].Target, "\\\\.\\pipe\\docker_engine")
 
-	mounts, err = buildContainerMountOptions(project, project.Services[0], moby.ImageInspect{}, inherit)
+	mounts, err = buildContainerMountOptions(project, project.Services["myService"], moby.ImageInspect{}, inherit)
 	sort.Slice(mounts, func(i, j int) bool {
 		return mounts[i].Target < mounts[j].Target
 	})
@@ -180,8 +180,8 @@ func TestDefaultNetworkSettings(t *testing.T) {
 		}
 		project := composetypes.Project{
 			Name: "myProject",
-			Services: []composetypes.ServiceConfig{
-				service,
+			Services: composetypes.Services{
+				"myService": service,
 			},
 			Networks: composetypes.Networks(map[string]composetypes.NetworkConfig{
 				"myNetwork1": {
@@ -205,8 +205,8 @@ func TestDefaultNetworkSettings(t *testing.T) {
 		}
 		project := composetypes.Project{
 			Name: "myProject",
-			Services: []composetypes.ServiceConfig{
-				service,
+			Services: composetypes.Services{
+				"myService": service,
 			},
 			Networks: composetypes.Networks(map[string]composetypes.NetworkConfig{
 				"myNetwork1": {
@@ -233,8 +233,8 @@ func TestDefaultNetworkSettings(t *testing.T) {
 		}
 		project := composetypes.Project{
 			Name: "myProject",
-			Services: []composetypes.ServiceConfig{
-				service,
+			Services: composetypes.Services{
+				"myService": service,
 			},
 		}
 
@@ -250,7 +250,7 @@ func TestDefaultNetworkSettings(t *testing.T) {
 		}
 		project := composetypes.Project{
 			Name:     "myProject",
-			Services: []composetypes.ServiceConfig{service},
+			Services: composetypes.Services{"myService": service},
 			Networks: composetypes.Networks(map[string]composetypes.NetworkConfig{
 				"default": {
 					Name: "myProject_default",

+ 13 - 13
pkg/compose/dependencies_test.go

@@ -33,19 +33,19 @@ import (
 func createTestProject() *types.Project {
 	return &types.Project{
 		Services: types.Services{
-			{
+			"test1": {
 				Name: "test1",
 				DependsOn: map[string]types.ServiceDependency{
 					"test2": {},
 				},
 			},
-			{
+			"test2": {
 				Name: "test2",
 				DependsOn: map[string]types.ServiceDependency{
 					"test3": {},
 				},
 			},
-			{
+			"test3": {
 				Name: "test3",
 			},
 		},
@@ -59,7 +59,7 @@ func TestTraversalWithMultipleParents(t *testing.T) {
 	}
 
 	project := types.Project{
-		Services: types.Services{dependent},
+		Services: types.Services{"dependent": dependent},
 	}
 
 	for i := 1; i <= 100; i++ {
@@ -67,7 +67,7 @@ func TestTraversalWithMultipleParents(t *testing.T) {
 		dependent.DependsOn[name] = types.ServiceDependency{}
 
 		svc := types.ServiceConfig{Name: name}
-		project.Services = append(project.Services, svc)
+		project.Services[name] = svc
 	}
 
 	ctx, cancel := context.WithCancel(context.Background())
@@ -132,7 +132,7 @@ func TestBuildGraph(t *testing.T) {
 		{
 			desc: "builds graph with single service",
 			services: types.Services{
-				{
+				"test": {
 					Name:      "test",
 					DependsOn: types.DependsOnConfig{},
 				},
@@ -150,11 +150,11 @@ func TestBuildGraph(t *testing.T) {
 		{
 			desc: "builds graph with two separate services",
 			services: types.Services{
-				{
+				"test": {
 					Name:      "test",
 					DependsOn: types.DependsOnConfig{},
 				},
-				{
+				"another": {
 					Name:      "another",
 					DependsOn: types.DependsOnConfig{},
 				},
@@ -179,13 +179,13 @@ func TestBuildGraph(t *testing.T) {
 		{
 			desc: "builds graph with a service and a dependency",
 			services: types.Services{
-				{
+				"test": {
 					Name: "test",
 					DependsOn: types.DependsOnConfig{
 						"another": types.ServiceDependency{},
 					},
 				},
-				{
+				"another": {
 					Name:      "another",
 					DependsOn: types.DependsOnConfig{},
 				},
@@ -214,19 +214,19 @@ func TestBuildGraph(t *testing.T) {
 		{
 			desc: "builds graph with multiple dependency levels",
 			services: types.Services{
-				{
+				"test": {
 					Name: "test",
 					DependsOn: types.DependsOnConfig{
 						"another": types.ServiceDependency{},
 					},
 				},
-				{
+				"another": {
 					Name: "another",
 					DependsOn: types.DependsOnConfig{
 						"another_dep": types.ServiceDependency{},
 					},
 				},
-				{
+				"another_dep": {
 					Name:      "another_dep",
 					DependsOn: types.DependsOnConfig{},
 				},

+ 6 - 6
pkg/compose/down_test.go

@@ -184,12 +184,12 @@ func TestDownRemoveImages(t *testing.T) {
 		Project: &types.Project{
 			Name: strings.ToLower(testProject),
 			Services: types.Services{
-				{Name: "local-anonymous"},
-				{Name: "local-named", Image: "local-named-image"},
-				{Name: "remote", Image: "remote-image"},
-				{Name: "remote-tagged", Image: "registry.example.com/remote-image-tagged:v1.0"},
-				{Name: "no-images-anonymous"},
-				{Name: "no-images-named", Image: "missing-named-image"},
+				"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"},
+				"no-images-anonymous": {Name: "no-images-anonymous"},
+				"no-images-named":     {Name: "no-images-named", Image: "missing-named-image"},
 			},
 		},
 	}

+ 3 - 3
pkg/compose/logs_test.go

@@ -83,7 +83,7 @@ func TestComposeService_Logs_Demux(t *testing.T) {
 	opts := compose.LogOptions{
 		Project: &types.Project{
 			Services: types.Services{
-				{Name: "service"},
+				"service": {Name: "service"},
 			},
 		},
 	}
@@ -153,8 +153,8 @@ func TestComposeService_Logs_ServiceFiltering(t *testing.T) {
 	// reference `serviceB` even though it has running services for this proj
 	proj := &types.Project{
 		Services: types.Services{
-			{Name: "serviceA"},
-			{Name: "serviceC"},
+			"serviceA": {Name: "serviceA"},
+			"serviceC": {Name: "serviceC"},
 		},
 	}
 	consumer := &testLogConsumer{}

+ 6 - 4
pkg/compose/pull.go

@@ -63,8 +63,8 @@ func (s *composeService) pull(ctx context.Context, project *types.Project, opts
 		imagesBeingPulled = map[string]string{}
 	)
 
-	for i, service := range project.Services {
-		i, service := i, service
+	i := 0
+	for _, service := range project.Services {
 		if service.Image == "" {
 			w.Event(progress.Event{
 				ID:     service.Name,
@@ -113,10 +113,11 @@ func (s *composeService) pull(ctx context.Context, project *types.Project, opts
 
 		imagesBeingPulled[service.Image] = service.Name
 
+		idx, service := i, service
 		eg.Go(func() error {
 			_, err := s.pullServiceImage(ctx, service, s.configFile(), w, false, project.Environment["DOCKER_DEFAULT_PLATFORM"])
 			if err != nil {
-				pullErrors[i] = err
+				pullErrors[idx] = err
 				if service.Build != nil {
 					mustBuild = append(mustBuild, service.Name)
 				}
@@ -134,6 +135,7 @@ func (s *composeService) pull(ctx context.Context, project *types.Project, opts
 			}
 			return nil
 		})
+		i++
 	}
 
 	err = eg.Wait()
@@ -260,7 +262,7 @@ func encodedAuth(ref reference.Named, configFile driver.Auth) (string, error) {
 }
 
 func (s *composeService) pullRequiredImages(ctx context.Context, project *types.Project, images map[string]string, quietPull bool) error {
-	var needPull types.Services
+	var needPull []types.ServiceConfig
 	for _, service := range project.Services {
 		if service.Image == "" {
 			continue

+ 5 - 5
pkg/compose/viz_test.go

@@ -34,7 +34,7 @@ func TestViz(t *testing.T) {
 		Name:       "viz-test",
 		WorkingDir: "/home",
 		Services: types.Services{
-			{
+			"service1": {
 				Name:  "service1",
 				Image: "image-for-service1",
 				Ports: []types.ServicePortConfig{
@@ -53,12 +53,12 @@ func TestViz(t *testing.T) {
 					"internal": nil,
 				},
 			},
-			{
+			"service2": {
 				Name:  "service2",
 				Image: "image-for-service2",
 				Ports: []types.ServicePortConfig{},
 			},
-			{
+			"service3": {
 				Name:  "service3",
 				Image: "some-image",
 				DependsOn: map[string]types.ServiceDependency{
@@ -66,7 +66,7 @@ func TestViz(t *testing.T) {
 					"service1": {},
 				},
 			},
-			{
+			"service4": {
 				Name:  "service4",
 				Image: "another-image",
 				DependsOn: map[string]types.ServiceDependency{
@@ -82,7 +82,7 @@ func TestViz(t *testing.T) {
 					"external": nil,
 				},
 			},
-			{
+			"With host IP": {
 				Name:  "With host IP",
 				Image: "user/image-name",
 				DependsOn: map[string]types.ServiceDependency{

+ 1 - 1
pkg/compose/watch_test.go

@@ -106,7 +106,7 @@ func TestWatch_Sync(t *testing.T) {
 
 	proj := types.Project{
 		Services: types.Services{
-			{
+			"test": {
 				Name: "test",
 			},
 		},

+ 4 - 4
pkg/e2e/restart_test.go

@@ -79,10 +79,10 @@ func TestRestartWithDependencies(t *testing.T) {
 	c.RunDockerComposeCmd(t, "-f", "./fixtures/restart-test/compose-depends-on.yaml", "up", "-d")
 
 	res := c.RunDockerComposeCmd(t, "restart", baseService)
-	fmt.Println(res.Combined())
-	assert.Assert(t, strings.Contains(res.Combined(), fmt.Sprintf("Container e2e-restart-deps-%s-1  Started", baseService)), res.Combined())
-	assert.Assert(t, strings.Contains(res.Combined(), fmt.Sprintf("Container e2e-restart-deps-%s-1  Started", depWithRestart)), res.Combined())
-	assert.Assert(t, !strings.Contains(res.Combined(), depNoRestart), res.Combined())
+	out := res.Combined()
+	assert.Assert(t, strings.Contains(out, fmt.Sprintf("Container e2e-restart-deps-%s-1  Started", baseService)), out)
+	assert.Assert(t, strings.Contains(out, fmt.Sprintf("Container e2e-restart-deps-%s-1  Started", depWithRestart)), out)
+	assert.Assert(t, !strings.Contains(out, depNoRestart), out)
 }
 
 func TestRestartWithProfiles(t *testing.T) {

+ 6 - 4
pkg/e2e/start_stop_test.go

@@ -106,12 +106,14 @@ func TestStartStopWithDependencies(t *testing.T) {
 		assert.Assert(t, strings.Contains(res.Combined(), "Container e2e-start-stop-with-dependencies-bar-1  Stopped"), res.Combined())
 
 		res = c.RunDockerComposeCmd(t, "--project-name", projectName, "start", "foo")
-		assert.Assert(t, strings.Contains(res.Combined(), "Container e2e-start-stop-with-dependencies-bar-1  Started"), res.Combined())
-		assert.Assert(t, strings.Contains(res.Combined(), "Container e2e-start-stop-with-dependencies-foo-1  Started"), res.Combined())
+		out := res.Combined()
+		assert.Assert(t, strings.Contains(out, "Container e2e-start-stop-with-dependencies-bar-1  Started"), out)
+		assert.Assert(t, strings.Contains(out, "Container e2e-start-stop-with-dependencies-foo-1  Started"), out)
 
 		res = c.RunDockerComposeCmd(t, "--project-name", projectName, "ps", "--status", "running")
-		assert.Assert(t, strings.Contains(res.Combined(), "e2e-start-stop-with-dependencies-bar-1"), res.Combined())
-		assert.Assert(t, strings.Contains(res.Combined(), "e2e-start-stop-with-dependencies-foo-1"), res.Combined())
+		out = res.Combined()
+		assert.Assert(t, strings.Contains(out, "e2e-start-stop-with-dependencies-bar-1"), out)
+		assert.Assert(t, strings.Contains(out, "e2e-start-stop-with-dependencies-foo-1"), out)
 	})
 
 	t.Run("Up no-deps links", func(t *testing.T) {