Repository: docker/compose (go)

Issue:
Can't use --no-build with services with a build section
<!--
If you are reporting a new issue, make sure that we do not have any duplicates
already open. You can ensure this by searching the issue list for this
repository. If there is a duplicate, please close your issue and add a comment
to the existing issue instead.

If you suspect your issue is a bug, please edit your issue description to
include the BUG REPORT INFORMATION shown below. If you fail to provide this
information within 7 days, we cannot debug your issue and will close it. We
will, however, reopen it if you later provide the information.

For more information about reporting issues, see
https://github.com/docker/compose-cli/blob/master/CONTRIBUTING.md#reporting-other-issues

---------------------------------------------------
GENERAL SUPPORT INFORMATION
---------------------------------------------------

The GitHub issue tracker is for bug reports and feature requests.
General support can be found at the following locations:

- Docker Support Forums - https://forums.docker.com
- Docker Community Slack - https://dockr.ly/community
- Post a question on StackOverflow, using the Docker tag

---------------------------------------------------
BUG REPORT INFORMATION
---------------------------------------------------
Use the commands below to provide key information from your environment:
You do NOT have to include this information if this is a FEATURE REQUEST
-->

When using `docker compose up --no-build` a check is done to ensure either `image` or `build` exists in a service: https://github.com/docker/compose/blob/7365917244ea791d304d5284a706aff09965636e/pkg/compose/build.go#L98 called from https://github.com/docker/compose/blob/ef786f9245eef1759c1b6da2661a2703b243fb54/pkg/compose/create.go#L66

However as the `build` section is removed in `createOptions.Apply` when `--no-build` is specified https://github.com/docker/compose/blob/7c47673d4af41d79900e6c70bc1a3f9f17bdd387/cmd/compose/create.go#L117

This means that `docker compose up --no-build` always fails even if you've ensured these images have been built.

Full repro here: https://github.com/z0w13/docker-compose-no-build-repro

**Output of `docker compose version`:**

```
Docker Compose version 2.0.1
```

The behaviour in 1.x was (in my opinion) correct, failing if the image hasn't been preivously built and otherwise succeeding:

```
docker-compose up --no-build
ERROR: Service 'repro' needs to be built, but --no-build was passed.
```

```
docker-compose build
  Building repro
  [+] Building 0.0s (3/3) FINISHED
   => [internal] load build definition from Dockerfile
  0.0s => => transferring dockerfile: 31B
    0.0s => [internal] load .dockerignore
  0.0s => => transferring context: 2B
  0.0s => exporting to image
  0.0s => => writing image sha256:78f8bb0557f61538b4fbb173d0ca3a8f466d77cfeee0c62a1a6f969288eeff6b
  0.0s => => naming to docker.io/library/repro_repro
docker-compose up --no-build
  Creating repro_repro_1 ...
```

Pull request description:
as --no-build is set, set service image to default image name
**What I did**
when `--no-build` is set, enforce service image to have a value, using default image name logic.

**Related issue**
close #8785


Patch:
diff --git a/Dockerfile b/Dockerfile
index e24a0fc17..45d2cc213 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -90,7 +90,7 @@ RUN --mount=target=. \
     make -f builder.Makefile test
 
 FROM base AS check-license-headers
-RUN go install github.com/google/addlicense@latest
+RUN go install github.com/kunalkushwaha/ltag@latest
 RUN --mount=target=. \
     make -f builder.Makefile check-license-headers
 
diff --git a/cmd/compose/create.go b/cmd/compose/create.go
index 717c28b31..50e7a6f79 100644
--- a/cmd/compose/create.go
+++ b/cmd/compose/create.go
@@ -126,6 +126,9 @@ func (opts createOptions) Apply(project *types.Project) {
 	if opts.noBuild {
 		for i, service := range project.Services {
 			service.Build = nil
+			if service.Image == "" {
+				service.Image = api.GetImageNameOrDefault(service, project.Name)
+			}
 			project.Services[i] = service
 		}
 	}
diff --git a/cmd/compose/list.go b/cmd/compose/list.go
index d4d53b726..618ad03c7 100644
--- a/cmd/compose/list.go
+++ b/cmd/compose/list.go
@@ -46,7 +46,6 @@ func listCommand(backend api.Service) *cobra.Command {
 		RunE: Adapt(func(ctx context.Context, args []string) error {
 			return runList(ctx, backend, lsOpts)
 		}),
-		Args:              cobra.NoArgs,
 		ValidArgsFunction: noCompletion(),
 	}
 	lsCmd.Flags().StringVar(&lsOpts.Format, "format", "pretty", "Format the output. Values: [pretty | json].")
diff --git a/cmd/compose/restart.go b/cmd/compose/restart.go
index f09d6fc83..f691c31fb 100644
--- a/cmd/compose/restart.go
+++ b/cmd/compose/restart.go
@@ -35,8 +35,8 @@ func restartCommand(p *projectOptions, backend api.Service) *cobra.Command {
 		projectOptions: p,
 	}
 	restartCmd := &cobra.Command{
-		Use:   "restart [SERVICE...]",
-		Short: "Restart service containers",
+		Use:   "restart",
+		Short: "Restart containers",
 		RunE: Adapt(func(ctx context.Context, args []string) error {
 			return runRestart(ctx, backend, opts, args)
 		}),
diff --git a/cmd/compose/version.go b/cmd/compose/version.go
index 06aff04d9..30e0e753e 100644
--- a/cmd/compose/version.go
+++ b/cmd/compose/version.go
@@ -37,7 +37,7 @@ func versionCommand() *cobra.Command {
 	cmd := &cobra.Command{
 		Use:   "version",
 		Short: "Show the Docker Compose version information",
-		Args:  cobra.NoArgs,
+		Args:  cobra.MaximumNArgs(0),
 		RunE: func(cmd *cobra.Command, _ []string) error {
 			runVersion(opts)
 			return nil
diff --git a/docs/reference/compose.md b/docs/reference/compose.md
index e1b2c1fd0..46d3a1a72 100644
--- a/docs/reference/compose.md
+++ b/docs/reference/compose.md
@@ -23,7 +23,7 @@ Docker Compose
 | [`ps`](compose_ps.md) | List containers |
 | [`pull`](compose_pull.md) | Pull service images |
 | [`push`](compose_push.md) | Push service images |
-| [`restart`](compose_restart.md) | Restart service containers |
+| [`restart`](compose_restart.md) | Restart containers |
 | [`rm`](compose_rm.md) | Removes stopped service containers |
 | [`run`](compose_run.md) | Run a one-off command on a service. |
 | [`start`](compose_start.md) | Start services |
diff --git a/docs/reference/compose_restart.md b/docs/reference/compose_restart.md
index cc152b562..12326662b 100644
--- a/docs/reference/compose_restart.md
+++ b/docs/reference/compose_restart.md
@@ -1,7 +1,7 @@
 # docker compose restart
 
 <!---MARKER_GEN_START-->
-Restart service containers
+Restart containers
 
 ### Options
 
@@ -14,7 +14,7 @@ Restart service containers
 
 ## Description
 
-Restarts all stopped and running services, or the specified services only.
+Restarts all stopped and running services.
 
 If you make changes to your `compose.yml` configuration, these changes are not reflected
 after running this command. For example, changes to environment variables (which are added
diff --git a/docs/reference/docker_compose_restart.yaml b/docs/reference/docker_compose_restart.yaml
index 733c3b271..8864aabdd 100644
--- a/docs/reference/docker_compose_restart.yaml
+++ b/docs/reference/docker_compose_restart.yaml
@@ -1,7 +1,7 @@
 command: docker compose restart
-short: Restart service containers
+short: Restart containers
 long: |-
-    Restarts all stopped and running services, or the specified services only.
+    Restarts all stopped and running services.
 
     If you make changes to your `compose.yml` configuration, these changes are not reflected
     after running this command. For example, changes to environment variables (which are added
@@ -11,7 +11,7 @@ long: |-
     If you are looking to configure a service's restart policy, please refer to
     [restart](https://github.com/compose-spec/compose-spec/blob/master/spec.md#restart)
     or [restart_policy](https://github.com/compose-spec/compose-spec/blob/master/deploy.md#restart_policy).
-usage: docker compose restart [SERVICE...]
+usage: docker compose restart
 pname: docker compose
 plink: docker_compose.yaml
 options:
diff --git a/pkg/api/api.go b/pkg/api/api.go
index 08f6433d8..43f6745a6 100644
--- a/pkg/api/api.go
+++ b/pkg/api/api.go
@@ -445,3 +445,12 @@ const (
 	// UserCancel user cancelled compose up, we are stopping containers
 	UserCancel
 )
+
+// GetImageNameOrDefault computes the default image name for a service, used to tag built images
+func GetImageNameOrDefault(service types.ServiceConfig, projectName string) string {
+	imageName := service.Image
+	if imageName == "" {
+		imageName = projectName + "_" + service.Name
+	}
+	return imageName
+}
diff --git a/pkg/compose/build.go b/pkg/compose/build.go
index 7581a664d..937bfd361 100644
--- a/pkg/compose/build.go
+++ b/pkg/compose/build.go
@@ -64,7 +64,7 @@ func (s *composeService) build(ctx context.Context, project *types.Project, opti
 		if service.Build == nil {
 			continue
 		}
-		imageName := getImageName(service, project.Name)
+		imageName := api.GetImageNameOrDefault(service, project.Name)
 		imagesToBuild = append(imagesToBuild, imageName)
 		buildOptions, err := s.toBuildOptions(project, service, imageName, options.SSHs)
 		if err != nil {
@@ -135,7 +135,7 @@ 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 {
-		image := getImageName(service, project.Name)
+		image := api.GetImageNameOrDefault(service, project.Name)
 		digest, ok := images[image]
 		if ok {
 			if project.Services[i].Labels == nil {
@@ -154,7 +154,7 @@ func (s *composeService) getBuildOptions(project *types.Project, images map[stri
 		if service.Image == "" && service.Build == nil {
 			return nil, fmt.Errorf("invalid service %q. Must specify either image or build", service.Name)
 		}
-		imageName := getImageName(service, project.Name)
+		imageName := api.GetImageNameOrDefault(service, project.Name)
 		_, localImagePresent := images[imageName]
 
 		if service.Build != nil {
@@ -176,7 +176,7 @@ func (s *composeService) getBuildOptions(project *types.Project, images map[stri
 func (s *composeService) getLocalImagesDigests(ctx context.Context, project *types.Project) (map[string]string, error) {
 	var imageNames []string
 	for _, s := range project.Services {
-		imgName := getImageName(s, project.Name)
+		imgName := api.GetImageNameOrDefault(s, project.Name)
 		if !utils.StringContains(imageNames, imgName) {
 			imageNames = append(imageNames, imgName)
 		}
@@ -191,7 +191,7 @@ func (s *composeService) getLocalImagesDigests(ctx context.Context, project *typ
 	}
 
 	for i := range project.Services {
-		imgName := getImageName(project.Services[i], project.Name)
+		imgName := api.GetImageNameOrDefault(project.Services[i], project.Name)
 		digest, ok := images[imgName]
 		if ok {
 			project.Services[i].CustomLabels.Add(api.ImageDigestLabel, digest)
diff --git a/pkg/compose/build_classic.go b/pkg/compose/build_classic.go
index db5d948e3..dc88dee3f 100644
--- a/pkg/compose/build_classic.go
+++ b/pkg/compose/build_classic.go
@@ -29,6 +29,7 @@ import (
 	"github.com/compose-spec/compose-go/types"
 	buildx "github.com/docker/buildx/build"
 	"github.com/docker/cli/cli/command/image/build"
+	"github.com/docker/compose/v2/pkg/api"
 	dockertypes "github.com/docker/docker/api/types"
 	"github.com/docker/docker/cli"
 	"github.com/docker/docker/pkg/archive"
@@ -45,7 +46,7 @@ func (s *composeService) doBuildClassic(ctx context.Context, project *types.Proj
 	var nameDigests = make(map[string]string)
 	var errs error
 	err := project.WithServices(nil, func(service types.ServiceConfig) error {
-		imageName := getImageName(service, project.Name)
+		imageName := api.GetImageNameOrDefault(service, project.Name)
 		o, ok := opts[imageName]
 		if !ok {
 			return nil
diff --git a/pkg/compose/create.go b/pkg/compose/create.go
index 8801436fc..2b0d0b841 100644
--- a/pkg/compose/create.go
+++ b/pkg/compose/create.go
@@ -227,14 +227,6 @@ func (s *composeService) ensureProjectVolumes(ctx context.Context, project *type
 	return nil
 }
 
-func getImageName(service types.ServiceConfig, projectName string) string {
-	imageName := service.Image
-	if imageName == "" {
-		imageName = projectName + "_" + service.Name
-	}
-	return imageName
-}
-
 func (s *composeService) getCreateOptions(ctx context.Context, p *types.Project, service types.ServiceConfig,
 	number int, inherit *moby.Container, autoRemove bool, attachStdin bool) (*container.Config, *container.HostConfig, *network.NetworkingConfig, error) {
 
@@ -279,7 +271,7 @@ func (s *composeService) getCreateOptions(ctx context.Context, p *types.Project,
 		AttachStderr:    true,
 		AttachStdout:    true,
 		Cmd:             runCmd,
-		Image:           getImageName(service, p.Name),
+		Image:           api.GetImageNameOrDefault(service, p.Name),
 		WorkingDir:      service.WorkingDir,
 		Entrypoint:      entrypoint,
 		NetworkDisabled: service.NetworkMode == "disabled",
@@ -712,7 +704,7 @@ func (s *composeService) buildContainerVolumes(ctx context.Context, p types.Proj
 	inherit *moby.Container) (map[string]struct{}, []string, []mount.Mount, error) {
 	var mounts = []mount.Mount{}
 
-	image := getImageName(service, p.Name)
+	image := api.GetImageNameOrDefault(service, p.Name)
 	imgInspect, _, err := s.apiClient().ImageInspectWithRaw(ctx, image)
 	if err != nil {
 		return nil, nil, nil, err
@@ -727,12 +719,8 @@ func (s *composeService) buildContainerVolumes(ctx context.Context, p types.Proj
 	binds := []string{}
 MOUNTS:
 	for _, m := range mountOptions {
-		if m.Type == mount.TypeNamedPipe {
-			mounts = append(mounts, m)
-			continue
-		}
 		volumeMounts[m.Target] = struct{}{}
-		if m.Type == mount.TypeBind {
+		if m.Type == mount.TypeBind || m.Type == mount.TypeNamedPipe {
 			// `Mount` is preferred but does not offer option to created host path if missing
 			// so `Bind` API is used here with raw volume string
 			// see https://github.com/moby/moby/issues/43483
diff --git a/pkg/compose/down.go b/pkg/compose/down.go
index 1ea7b4dcb..4251a8dc1 100644
--- a/pkg/compose/down.go
+++ b/pkg/compose/down.go
@@ -195,7 +195,7 @@ func (s *composeService) getServiceImages(options api.DownOptions, project *type
 			continue
 		}
 		if image == "" {
-			image = getImageName(service, project.Name)
+			image = api.GetImageNameOrDefault(service, project.Name)
 		}
 		images[image] = struct{}{}
 	}
diff --git a/scripts/validate/fileheader b/scripts/validate/fileheader
index abb30b03f..8b503df36 100755
--- a/scripts/validate/fileheader
+++ b/scripts/validate/fileheader
@@ -1,6 +1,6 @@
 #!/usr/bin/env sh
 
-#   Copyright 2020,2022 Docker Compose CLI authors
+#   Copyright Docker Compose CLI authors
 
 #   Licensed under the Apache License, Version 2.0 (the "License");
 #   you may not use this file except in compliance with the License.
@@ -16,12 +16,12 @@
 
 set -eu -o pipefail
 
-if ! command -v addlicense; then
-    >&2 echo "ERROR: addlicense not found. Install with:"
-    >&2 echo "    go install -u github.com/google/addlicense@latest"
+if ! command -v ltag; then
+    >&2 echo "ERROR: ltag not found. Install with:"
+    >&2 echo "    go get -u github.com/kunalkushwaha/ltag"
     exit 1
 fi
 
 BASEPATH="${1-}"
 
-find . -regex '.*\.sh' -o -regex '.*\.go' -o -regex '.*Makefile' -o -regex '.*Dockerfile' | xargs addlicense -check -l apache -c 'Docker Compose CLI authors' -ignore validate -ignore testdata -ignore resolvepath -v 1>&2
\ No newline at end of file
+ltag -t "${BASEPATH}scripts/validate/template" -excludes "validate testdata resolvepath" --check -v
\ No newline at end of file


Test patch:
diff --git a/pkg/compose/create_test.go b/pkg/compose/create_test.go
index cdcc4ad61..dceed1361 100644
--- a/pkg/compose/create_test.go
+++ b/pkg/compose/create_test.go
@@ -22,10 +22,12 @@ import (
 	"sort"
 	"testing"
 
-	composetypes "github.com/compose-spec/compose-go/types"
 	"github.com/docker/compose/v2/pkg/api"
+
+	composetypes "github.com/compose-spec/compose-go/types"
 	moby "github.com/docker/docker/api/types"
 	mountTypes "github.com/docker/docker/api/types/mount"
+
 	"gotest.tools/v3/assert"
 )
 
@@ -44,18 +46,6 @@ func TestBuildBindMount(t *testing.T) {
 	assert.Equal(t, mount.Type, mountTypes.TypeBind)
 }
 
-func TestBuildNamedPipeMount(t *testing.T) {
-	project := composetypes.Project{}
-	volume := composetypes.ServiceVolumeConfig{
-		Type:   composetypes.VolumeTypeNamedPipe,
-		Source: "\\\\.\\pipe\\docker_engine_windows",
-		Target: "\\\\.\\pipe\\docker_engine",
-	}
-	mount, err := buildMount(project, volume)
-	assert.NilError(t, err)
-	assert.Equal(t, mount.Type, mountTypes.TypeNamedPipe)
-}
-
 func TestBuildVolumeMount(t *testing.T) {
 	project := composetypes.Project{
 		Name: "myProject",
@@ -77,8 +67,8 @@ func TestBuildVolumeMount(t *testing.T) {
 }
 
 func TestServiceImageName(t *testing.T) {
-	assert.Equal(t, getImageName(composetypes.ServiceConfig{Image: "myImage"}, "myProject"), "myImage")
-	assert.Equal(t, getImageName(composetypes.ServiceConfig{Name: "aService"}, "myProject"), "myProject_aService")
+	assert.Equal(t, api.GetImageNameOrDefault(composetypes.ServiceConfig{Image: "myImage"}, "myProject"), "myImage")
+	assert.Equal(t, api.GetImageNameOrDefault(composetypes.ServiceConfig{Name: "aService"}, "myProject"), "myProject_aService")
 }
 
 func TestPrepareNetworkLabels(t *testing.T) {
@@ -109,11 +99,6 @@ func TestBuildContainerMountOptions(t *testing.T) {
 						Type:   composetypes.VolumeTypeVolume,
 						Target: "/var/myvolume2",
 					},
-					{
-						Type:   composetypes.VolumeTypeNamedPipe,
-						Source: "\\\\.\\pipe\\docker_engine_windows",
-						Target: "\\\\.\\pipe\\docker_engine",
-					},
 				},
 			},
 		},
@@ -145,20 +130,18 @@ func TestBuildContainerMountOptions(t *testing.T) {
 		return mounts[i].Target < mounts[j].Target
 	})
 	assert.NilError(t, err)
-	assert.Assert(t, len(mounts) == 3)
+	assert.Assert(t, len(mounts) == 2)
 	assert.Equal(t, mounts[0].Target, "/var/myvolume1")
 	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)
 	sort.Slice(mounts, func(i, j int) bool {
 		return mounts[i].Target < mounts[j].Target
 	})
 	assert.NilError(t, err)
-	assert.Assert(t, len(mounts) == 3)
+	assert.Assert(t, len(mounts) == 2)
 	assert.Equal(t, mounts[0].Target, "/var/myvolume1")
 	assert.Equal(t, mounts[1].Target, "/var/myvolume2")
-	assert.Equal(t, mounts[2].Target, "\\\\.\\pipe\\docker_engine")
 }
 
 func TestGetDefaultNetworkMode(t *testing.T) {