diff --git a/cmd/compose/config.go b/cmd/compose/config.go index 331a0b0ea4..9e83293108 100644 --- a/cmd/compose/config.go +++ b/cmd/compose/config.go @@ -189,7 +189,7 @@ func runConfig(ctx context.Context, dockerCli command.Cli, opts configOptions, s } if !opts.noInterpolate { - content = escapeDollarSign(content) + content = bytes.ReplaceAll(content, []byte{'$'}, []byte{'$', '$'}) } if opts.quiet { @@ -696,9 +696,3 @@ func runEnvironment(ctx context.Context, dockerCli command.Cli, opts configOptio } return nil } - -func escapeDollarSign(marshal []byte) []byte { - dollar := []byte{'$'} - escDollar := []byte{'$', '$'} - return bytes.ReplaceAll(marshal, dollar, escDollar) -} diff --git a/cmd/compose/create.go b/cmd/compose/create.go index 5f9f790831..41cac7de96 100644 --- a/cmd/compose/create.go +++ b/cmd/compose/create.go @@ -164,7 +164,7 @@ func (opts createOptions) GetTimeout() *time.Duration { func (opts createOptions) Apply(project *types.Project) error { if opts.pullChanged { - if !opts.isPullPolicyValid() { + if !slices.Contains(validPullPolicies, opts.Pull) { return fmt.Errorf("invalid --pull option %q", opts.Pull) } for i, service := range project.Services { @@ -214,10 +214,7 @@ func applyScaleOpts(project *types.Project, opts []string) error { return nil } -func (opts createOptions) isPullPolicyValid() bool { - pullPolicies := []string{ - types.PullPolicyAlways, types.PullPolicyNever, types.PullPolicyBuild, - types.PullPolicyMissing, types.PullPolicyIfNotPresent, - } - return slices.Contains(pullPolicies, opts.Pull) +var validPullPolicies = []string{ + types.PullPolicyAlways, types.PullPolicyNever, types.PullPolicyBuild, + types.PullPolicyMissing, types.PullPolicyIfNotPresent, } diff --git a/cmd/compose/list.go b/cmd/compose/list.go index 8a7f875da2..4e396f7cad 100644 --- a/cmd/compose/list.go +++ b/cmd/compose/list.go @@ -118,7 +118,14 @@ func runList(ctx context.Context, dockerCli command.Cli, backendOptions *Backend return nil } - view := viewFromStackList(stackList) + view := make([]stackView, len(stackList)) + for i, s := range stackList { + view[i] = stackView{ + Name: s.Name, + Status: strings.TrimSpace(s.Status + " " + s.Reason), + ConfigFiles: s.ConfigFiles, + } + } return formatter.Print(view, lsOpts.Format, dockerCli.Out(), func(w io.Writer) { for _, stack := range view { _, _ = fmt.Fprintf(w, "%s\t%s\t%s\n", stack.Name, stack.Status, stack.ConfigFiles) @@ -131,15 +138,3 @@ type stackView struct { Status string ConfigFiles string } - -func viewFromStackList(stackList []api.Stack) []stackView { - retList := make([]stackView, len(stackList)) - for i, s := range stackList { - retList[i] = stackView{ - Name: s.Name, - Status: strings.TrimSpace(fmt.Sprintf("%s %s", s.Status, s.Reason)), - ConfigFiles: s.ConfigFiles, - } - } - return retList -} diff --git a/cmd/compose/options.go b/cmd/compose/options.go index 42ecd52dbd..b639afabde 100644 --- a/cmd/compose/options.go +++ b/cmd/compose/options.go @@ -23,7 +23,6 @@ import ( "os" "slices" "sort" - "strings" "text/tabwriter" "github.com/compose-spec/compose-go/v2/cli" @@ -177,7 +176,7 @@ func promptForInterpolatedVariables(ctx context.Context, dockerCli command.Cli, } func extractInterpolationVariablesFromModel(ctx context.Context, dockerCli command.Cli, projectOptions *ProjectOptions, cmdEnvs []string) ([]varInfo, bool, error) { - cmdEnvMap := extractEnvCLIDefined(cmdEnvs) + cmdEnvMap := types.NewMappingWithEquals(cmdEnvs).ToMapping() // Create a model without interpolation to extract variables opts := configOptions{ @@ -229,18 +228,6 @@ func extractInterpolationVariablesFromModel(ctx context.Context, dockerCli comma return varsInfo, false, nil } -func extractEnvCLIDefined(cmdEnvs []string) map[string]string { - // Parse command-line environment variables - cmdEnvMap := make(map[string]string) - for _, env := range cmdEnvs { - key, val, ok := strings.Cut(env, "=") - if ok { - cmdEnvMap[key] = val - } - } - return cmdEnvMap -} - func displayInterpolationVariables(writer io.Writer, varsInfo []varInfo) { // Display all variables in a table format _, _ = fmt.Fprintln(writer, "\nFound the following variables in configuration:") diff --git a/cmd/compose/scale.go b/cmd/compose/scale.go index 94fd6e15ec..d82c1e5b43 100644 --- a/cmd/compose/scale.go +++ b/cmd/compose/scale.go @@ -93,17 +93,24 @@ func runScale(ctx context.Context, dockerCli command.Cli, backendOptions *Backen } for key, value := range serviceReplicaTuples { - service, err := project.GetService(key) - if err != nil { + if err := setServiceScale(project, key, value); err != nil { return err } - service.SetScale(value) - project.Services[key] = service } return backend.Scale(ctx, project, api.ScaleOptions{Services: services}) } +func setServiceScale(project *types.Project, name string, replicas int) error { + service, err := project.GetService(name) + if err != nil { + return err + } + service.SetScale(replicas) + project.Services[name] = service + return nil +} + func parseServicesReplicasArgs(args []string) (map[string]int, error) { serviceReplicaTuples := map[string]int{} for _, arg := range args { diff --git a/cmd/compose/up.go b/cmd/compose/up.go index cda2678bbb..abd4fd10d9 100644 --- a/cmd/compose/up.go +++ b/cmd/compose/up.go @@ -83,7 +83,7 @@ func (opts upOptions) apply(project *types.Project, services []string) (*types.P return project, nil } -func (opts *upOptions) validateNavigationMenu(dockerCli command.Cli) { +func (opts *upOptions) resolveNavigationMenu(dockerCli command.Cli) { if !dockerCli.Out().IsTerminal() { opts.navigationMenu = false return @@ -135,7 +135,7 @@ func upCommand(p *ProjectOptions, dockerCli command.Cli, backendOptions *Backend return errors.New("cannot combine --attach and --attach-dependencies") } - up.validateNavigationMenu(dockerCli) + up.resolveNavigationMenu(dockerCli) if !p.All && len(project.Services) == 0 { return fmt.Errorf("no service selected") @@ -351,13 +351,3 @@ func runUp( }, }) } - -func setServiceScale(project *types.Project, name string, replicas int) error { - service, err := project.GetService(name) - if err != nil { - return err - } - service.SetScale(replicas) - project.Services[name] = service - return nil -} diff --git a/cmd/display/tty.go b/cmd/display/tty.go index 6090969259..447ecd61e7 100644 --- a/cmd/display/tty.go +++ b/cmd/display/tty.go @@ -120,7 +120,7 @@ func (t *task) update(e api.Resource) { t.stop() } case api.Working: - t.hasMore() + t.spinner.Restart() } t.status = e.Status t.text = e.Text @@ -142,10 +142,6 @@ func (t *task) stop() { t.spinner.Stop() } -func (t *task) hasMore() { - t.spinner.Restart() -} - func (t *task) Completed() bool { switch t.status { case api.Done, api.Error, api.Warning: diff --git a/pkg/compose/attach.go b/pkg/compose/attach.go index 0739a021ed..15bb50cbaf 100644 --- a/pkg/compose/attach.go +++ b/pkg/compose/attach.go @@ -25,7 +25,6 @@ import ( "github.com/compose-spec/compose-go/v2/types" "github.com/moby/moby/api/pkg/stdcopy" - containerType "github.com/moby/moby/api/types/container" "github.com/moby/moby/client" "github.com/sirupsen/logrus" @@ -55,7 +54,8 @@ func (s *composeService) attach(ctx context.Context, project *types.Project, lis } for _, ctr := range containers { - err := s.attachContainer(ctx, ctr, listener) + service := ctr.Labels[api.ServiceLabel] + err := s.doAttachContainer(ctx, service, ctr.ID, getContainerNameWithoutProject(ctr), listener) if err != nil { return nil, err } @@ -63,12 +63,6 @@ func (s *composeService) attach(ctx context.Context, project *types.Project, lis return containers, nil } -func (s *composeService) attachContainer(ctx context.Context, container containerType.Summary, listener api.ContainerEventListener) error { - service := container.Labels[api.ServiceLabel] - name := getContainerNameWithoutProject(container) - return s.doAttachContainer(ctx, service, container.ID, name, listener) -} - func (s *composeService) doAttachContainer(ctx context.Context, service, id, name string, listener api.ContainerEventListener) error { inspect, err := s.apiClient().ContainerInspect(ctx, id, client.ContainerInspectOptions{}) if err != nil { diff --git a/pkg/compose/build_bake.go b/pkg/compose/build_bake.go index 8ec6a272ee..699f7da319 100644 --- a/pkg/compose/build_bake.go +++ b/pkg/compose/build_bake.go @@ -586,7 +586,16 @@ func (s *composeService) dryRunBake(cfg bakeConfig) map[string]string { bakeResponse := map[string]string{} for name, target := range cfg.Targets { dryRunUUID := fmt.Sprintf("dryRun-%x", sha1.Sum([]byte(name))) - s.displayDryRunBuildEvent(name, dryRunUUID, target.Tags[0]) + s.events.On(api.Resource{ + ID: name + " ==>", + Status: api.Done, + Text: fmt.Sprintf("==> writing image %s", dryRunUUID), + }) + s.events.On(api.Resource{ + ID: name + " ==> ==>", + Status: api.Done, + Text: fmt.Sprintf(`naming to %s`, target.Tags[0]), + }) bakeResponse[name] = dryRunUUID } for name := range bakeResponse { @@ -594,16 +603,3 @@ func (s *composeService) dryRunBake(cfg bakeConfig) map[string]string { } return bakeResponse } - -func (s *composeService) displayDryRunBuildEvent(name, dryRunUUID, tag string) { - s.events.On(api.Resource{ - ID: name + " ==>", - Status: api.Done, - Text: fmt.Sprintf("==> writing image %s", dryRunUUID), - }) - s.events.On(api.Resource{ - ID: name + " ==> ==>", - Status: api.Done, - Text: fmt.Sprintf(`naming to %s`, tag), - }) -} diff --git a/pkg/compose/create.go b/pkg/compose/create.go index 1c08e1501e..9f4decfd40 100644 --- a/pkg/compose/create.go +++ b/pkg/compose/create.go @@ -257,10 +257,21 @@ func (s *composeService) getCreateConfigs(ctx context.Context, inherit *container.Summary, opts createOptions, ) (createConfigs, error) { - labels, err := s.prepareLabels(opts.Labels, service, number) + labels := opts.Labels + hash, err := ServiceHash(service) if err != nil { return createConfigs{}, err } + labels[api.ConfigHashLabel] = hash + if number > 0 { + // One-off containers are not indexed + labels[api.ContainerNumberLabel] = strconv.Itoa(number) + } + var dependencies []string + for dep, d := range service.DependsOn { + dependencies = append(dependencies, fmt.Sprintf("%s:%s:%t", dep, d.Condition, d.Restart)) + } + labels[api.DependenciesLabel] = strings.Join(dependencies, ",") var runCmd, entrypoint []string if service.Command != nil { @@ -578,26 +589,6 @@ func parseSecurityOpts(p *types.Project, securityOpts []string) ([]string, bool, return parsed, unconfined, nil } -func (s *composeService) prepareLabels(labels types.Labels, service types.ServiceConfig, number int) (map[string]string, error) { - hash, err := ServiceHash(service) - if err != nil { - return nil, err - } - labels[api.ConfigHashLabel] = hash - - if number > 0 { - // One-off containers are not indexed - labels[api.ContainerNumberLabel] = strconv.Itoa(number) - } - - var dependencies []string - for s, d := range service.DependsOn { - dependencies = append(dependencies, fmt.Sprintf("%s:%s:%t", s, d.Condition, d.Restart)) - } - labels[api.DependenciesLabel] = strings.Join(dependencies, ",") - return labels, nil -} - // defaultNetworkSettings determines the container.NetworkMode and corresponding network.NetworkingConfig (nil if not applicable). func defaultNetworkSettings(project *types.Project, service types.ServiceConfig, serviceIndex int, @@ -1338,11 +1329,28 @@ func buildMountOptions(volume types.ServiceVolumeConfig) (*mount.BindOptions, *m case "bind": return buildBindOption(volume.Bind), nil, nil, nil case "volume": - return nil, buildVolumeOptions(volume.Volume), nil, nil + if volume.Volume == nil { + return nil, nil, nil, nil + } + return nil, &mount.VolumeOptions{ + NoCopy: volume.Volume.NoCopy, + Subpath: volume.Volume.Subpath, + Labels: volume.Volume.Labels, + // DriverConfig: , // FIXME missing from model ? + }, nil, nil case "tmpfs": - return nil, nil, buildTmpfsOptions(volume.Tmpfs), nil + if volume.Tmpfs == nil { + return nil, nil, nil, nil + } + return nil, nil, &mount.TmpfsOptions{ + SizeBytes: int64(volume.Tmpfs.Size), + Mode: os.FileMode(volume.Tmpfs.Mode), + }, nil case "image": - return nil, nil, nil, buildImageOptions(volume.Image) + if volume.Image == nil { + return nil, nil, nil, nil + } + return nil, nil, nil, &mount.ImageOptions{Subpath: volume.Image.SubPath} } return nil, nil, nil, nil } @@ -1366,37 +1374,6 @@ func buildBindOption(bind *types.ServiceVolumeBind) *mount.BindOptions { return opts } -func buildVolumeOptions(vol *types.ServiceVolumeVolume) *mount.VolumeOptions { - if vol == nil { - return nil - } - return &mount.VolumeOptions{ - NoCopy: vol.NoCopy, - Subpath: vol.Subpath, - Labels: vol.Labels, - // DriverConfig: , // FIXME missing from model ? - } -} - -func buildTmpfsOptions(tmpfs *types.ServiceVolumeTmpfs) *mount.TmpfsOptions { - if tmpfs == nil { - return nil - } - return &mount.TmpfsOptions{ - SizeBytes: int64(tmpfs.Size), - Mode: os.FileMode(tmpfs.Mode), - } -} - -func buildImageOptions(image *types.ServiceVolumeImage) *mount.ImageOptions { - if image == nil { - return nil - } - return &mount.ImageOptions{ - Subpath: image.SubPath, - } -} - // createNetwork creates the given (managed) network with its compose labels and // config-hash. It is executed as a plan operation (OpCreateNetwork); resolution // of external networks lives in resolveExternalNetwork, and reuse of legacy diff --git a/pkg/compose/down.go b/pkg/compose/down.go index abc6f3a826..9969c84e68 100644 --- a/pkg/compose/down.go +++ b/pkg/compose/down.go @@ -61,10 +61,18 @@ func (s *composeService) down(ctx context.Context, projectName string, options a } } - // Check requested services exists in model - services, err := checkSelectedServices(options, project) - if err != nil { - return err + // keep only the requested services that exist in the model + var services []string + for _, service := range options.Services { + if _, err := project.GetService(service); err != nil { + if options.Project != nil { + // ran with an explicit compose.yaml file, so we should not ignore + return err + } + // ran without an explicit compose.yaml file, so can't distinguish typo vs container already removed + continue + } + services = append(services, service) } if len(options.Services) > 0 && len(services) == 0 { @@ -124,23 +132,6 @@ func (s *composeService) down(ctx context.Context, projectName string, options a return eg.Wait() } -func checkSelectedServices(options api.DownOptions, project *types.Project) ([]string, error) { - var services []string - for _, service := range options.Services { - _, err := project.GetService(service) - if err != nil { - if options.Project != nil { - // ran with an explicit compose.yaml file, so we should not ignore - return nil, err - } - // ran without an explicit compose.yaml file, so can't distinguish typo vs container already removed - } else { - services = append(services, service) - } - } - return services, nil -} - func (s *composeService) ensureVolumesDown(ctx context.Context, project *types.Project) []downOp { var ops []downOp for _, vol := range project.Volumes { @@ -171,7 +162,10 @@ func (s *composeService) ensureImagesDown(ctx context.Context, project *types.Pr for i := range images { img := images[i] ops = append(ops, func() error { - return s.removeImage(ctx, img) + return s.removeResource("Image "+img, func() error { + _, err := s.apiClient().ImageRemove(ctx, img, client.ImageRemoveOptions{}) + return err + }) }) } return ops, nil @@ -250,14 +244,6 @@ func (s *composeService) removeNetwork(ctx context.Context, composeNetworkName s return nil } -func (s *composeService) removeImage(ctx context.Context, image string) error { - id := fmt.Sprintf("Image %s", image) - return s.removeResource(id, func() error { - _, err := s.apiClient().ImageRemove(ctx, image, client.ImageRemoveOptions{}) - return err - }) -} - func (s *composeService) removeVolume(ctx context.Context, id string) error { resource := fmt.Sprintf("Volume %s", id) diff --git a/pkg/compose/exec.go b/pkg/compose/exec.go index a50d2c53aa..30a5d08002 100644 --- a/pkg/compose/exec.go +++ b/pkg/compose/exec.go @@ -23,14 +23,13 @@ import ( "github.com/docker/cli/cli" "github.com/docker/cli/cli/command/container" - containerType "github.com/moby/moby/api/types/container" "github.com/docker/compose/v5/pkg/api" ) func (s *composeService) Exec(ctx context.Context, projectName string, options api.RunOptions) (int, error) { projectName = strings.ToLower(projectName) - target, err := s.getExecTarget(ctx, projectName, options) + target, err := s.getSpecifiedContainer(ctx, projectName, oneOffInclude, false, options.Service, options.Index) if err != nil { return 0, err } @@ -57,7 +56,3 @@ func (s *composeService) Exec(ctx context.Context, projectName string, options a } return 0, err } - -func (s *composeService) getExecTarget(ctx context.Context, projectName string, opts api.RunOptions) (containerType.Summary, error) { - return s.getSpecifiedContainer(ctx, projectName, oneOffInclude, false, opts.Service, opts.Index) -} diff --git a/pkg/compose/logs.go b/pkg/compose/logs.go index 64e05119a3..46be4bd8b1 100644 --- a/pkg/compose/logs.go +++ b/pkg/compose/logs.go @@ -62,7 +62,11 @@ func (s *composeService) Logs( eg, ctx := errgroup.WithContext(ctx) for _, ctr := range containers { eg.Go(func() error { - err := s.logContainer(ctx, consumer, ctr, options) + res, err := s.apiClient().ContainerInspect(ctx, ctr.ID, client.ContainerInspectOptions{}) + if err != nil { + return err + } + err = s.doLogContainer(ctx, consumer, getContainerNameWithoutProject(ctr), res.Container, options) if errdefs.IsNotImplemented(err) { logrus.Warnf("Can't retrieve logs for %q: %s", getCanonicalContainerName(ctr), err.Error()) return nil @@ -113,15 +117,6 @@ func (s *composeService) Logs( return eg.Wait() } -func (s *composeService) logContainer(ctx context.Context, consumer api.LogConsumer, c container.Summary, options api.LogOptions) error { - res, err := s.apiClient().ContainerInspect(ctx, c.ID, client.ContainerInspectOptions{}) - if err != nil { - return err - } - name := getContainerNameWithoutProject(c) - return s.doLogContainer(ctx, consumer, name, res.Container, options) -} - func (s *composeService) doLogContainer(ctx context.Context, consumer api.LogConsumer, name string, ctr container.InspectResponse, options api.LogOptions) error { r, err := s.apiClient().ContainerLogs(ctx, ctr.ID, client.ContainerLogsOptions{ ShowStdout: true, diff --git a/pkg/compose/secrets.go b/pkg/compose/secrets.go index ac64684f8f..39051b62ae 100644 --- a/pkg/compose/secrets.go +++ b/pkg/compose/secrets.go @@ -59,7 +59,15 @@ func (s *composeService) injectFileReferences(ctx context.Context, project *type return fmt.Errorf("cannot create %s %q in read-only service %s: `file` is the sole supported option", mountType, sources[mount.Source].Name, service.Name) } - s.setDefaultTarget(&mount, mountType) + if mount.Target == "" { + if mountType == secretMount { + mount.Target = "/run/secrets/" + mount.Source + } else { + mount.Target = "/" + mount.Source + } + } else if mountType == secretMount && !isAbsTarget(mount.Target) { + mount.Target = "/run/secrets/" + mount.Target + } if err := s.copyFileToContainer(ctx, id, content, mount); err != nil { return err @@ -110,18 +118,6 @@ func (s *composeService) resolveFileContent(project *types.Project, source types return "", nil } -func (s *composeService) setDefaultTarget(file *types.FileReferenceConfig, mountType mountType) { - if file.Target == "" { - if mountType == secretMount { - file.Target = "/run/secrets/" + file.Source - } else { - file.Target = "/" + file.Source - } - } else if mountType == secretMount && !isAbsTarget(file.Target) { - file.Target = "/run/secrets/" + file.Target - } -} - func (s *composeService) copyFileToContainer(ctx context.Context, id, content string, file types.FileReferenceConfig) error { b, err := createTar(content, file) if err != nil {