From 4e7b6dfc3d0f725bc561cfe88df68833b5863811 Mon Sep 17 00:00:00 2001 From: Nicolas De Loof Date: Thu, 12 Dec 2024 09:36:24 +0100 Subject: [PATCH] prompt user to confirm volume recreation Signed-off-by: Nicolas De Loof --- cmd/compose/create.go | 3 + cmd/compose/up.go | 2 + docs/reference/compose_create.md | 1 + docs/reference/compose_up.md | 1 + docs/reference/docker_compose_create.yaml | 11 +++ docs/reference/docker_compose_up.yaml | 11 +++ pkg/api/api.go | 2 + pkg/compose/create.go | 86 +++++++++++++++++++---- 8 files changed, 104 insertions(+), 13 deletions(-) diff --git a/cmd/compose/create.go b/cmd/compose/create.go index 1fe05e98dc4..856b911fcff 100644 --- a/cmd/compose/create.go +++ b/cmd/compose/create.go @@ -46,6 +46,7 @@ type createOptions struct { timeout int quietPull bool scale []string + AssumeYes bool } func createCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *cobra.Command { @@ -80,6 +81,7 @@ func createCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service flags.BoolVar(&opts.noRecreate, "no-recreate", false, "If containers already exist, don't recreate them. Incompatible with --force-recreate.") flags.BoolVar(&opts.removeOrphans, "remove-orphans", false, "Remove containers for services not defined in the Compose file") flags.StringArrayVar(&opts.scale, "scale", []string{}, "Scale SERVICE to NUM instances. Overrides the `scale` setting in the Compose file if present.") + flags.BoolVarP(&opts.AssumeYes, "y", "y", false, `Assume "yes" as answer to all prompts and run non-interactively`) return cmd } @@ -107,6 +109,7 @@ func runCreate(ctx context.Context, _ command.Cli, backend api.Service, createOp Inherit: !createOpts.noInherit, Timeout: createOpts.GetTimeout(), QuietPull: createOpts.quietPull, + AssumeYes: createOpts.AssumeYes, }) } diff --git a/cmd/compose/up.go b/cmd/compose/up.go index 31756d6a846..e41a9549bdf 100644 --- a/cmd/compose/up.go +++ b/cmd/compose/up.go @@ -145,6 +145,7 @@ func upCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *c flags := upCmd.Flags() flags.BoolVarP(&up.Detach, "detach", "d", false, "Detached mode: Run containers in the background") flags.BoolVar(&create.Build, "build", false, "Build images before starting containers") + flags.BoolVarP(&create.AssumeYes, "y", "y", false, `Assume "yes" as answer to all prompts and run non-interactively`) flags.BoolVar(&create.noBuild, "no-build", false, "Don't build an image, even if it's policy") flags.StringVar(&create.Pull, "pull", "policy", `Pull image before running ("always"|"missing"|"never")`) flags.BoolVar(&create.removeOrphans, "remove-orphans", false, "Remove containers for services not defined in the Compose file") @@ -255,6 +256,7 @@ func runUp( Inherit: !createOptions.noInherit, Timeout: createOptions.GetTimeout(), QuietPull: createOptions.quietPull, + AssumeYes: createOptions.AssumeYes, } if upOptions.noStart { diff --git a/docs/reference/compose_create.md b/docs/reference/compose_create.md index 7ae60c549da..b87cce8572b 100644 --- a/docs/reference/compose_create.md +++ b/docs/reference/compose_create.md @@ -16,6 +16,7 @@ Creates containers for a service | `--quiet-pull` | `bool` | | Pull without printing progress information | | `--remove-orphans` | `bool` | | Remove containers for services not defined in the Compose file | | `--scale` | `stringArray` | | Scale SERVICE to NUM instances. Overrides the `scale` setting in the Compose file if present. | +| `-y`, `--y` | `bool` | | Assume "yes" as answer to all prompts and run non-interactively | diff --git a/docs/reference/compose_up.md b/docs/reference/compose_up.md index 410ec84b52d..d1478cdd336 100644 --- a/docs/reference/compose_up.md +++ b/docs/reference/compose_up.md @@ -53,6 +53,7 @@ If the process is interrupted using `SIGINT` (ctrl + C) or `SIGTERM`, the contai | `--wait` | `bool` | | Wait for services to be running\|healthy. Implies detached mode. | | `--wait-timeout` | `int` | `0` | Maximum duration in seconds to wait for the project to be running\|healthy | | `-w`, `--watch` | `bool` | | Watch source code and rebuild/refresh containers when files are updated. | +| `-y`, `--y` | `bool` | | Assume "yes" as answer to all prompts and run non-interactively | diff --git a/docs/reference/docker_compose_create.yaml b/docs/reference/docker_compose_create.yaml index a07e1c88cc5..7cb764098b9 100644 --- a/docs/reference/docker_compose_create.yaml +++ b/docs/reference/docker_compose_create.yaml @@ -88,6 +88,17 @@ options: experimentalcli: false kubernetes: false swarm: false + - option: "y" + shorthand: "y" + value_type: bool + default_value: "false" + description: Assume "yes" as answer to all prompts and run non-interactively + deprecated: false + hidden: false + experimental: false + experimentalcli: false + kubernetes: false + swarm: false inherited_options: - option: dry-run value_type: bool diff --git a/docs/reference/docker_compose_up.yaml b/docs/reference/docker_compose_up.yaml index 1474cfe2ee6..0ed31b3599c 100644 --- a/docs/reference/docker_compose_up.yaml +++ b/docs/reference/docker_compose_up.yaml @@ -309,6 +309,17 @@ options: experimentalcli: false kubernetes: false swarm: false + - option: "y" + shorthand: "y" + value_type: bool + default_value: "false" + description: Assume "yes" as answer to all prompts and run non-interactively + deprecated: false + hidden: false + experimental: false + experimentalcli: false + kubernetes: false + swarm: false inherited_options: - option: dry-run value_type: bool diff --git a/pkg/api/api.go b/pkg/api/api.go index ccd56b74b6a..f0e813803d1 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -207,6 +207,8 @@ type CreateOptions struct { Timeout *time.Duration // QuietPull makes the pulling process quiet QuietPull bool + // AssumeYes assume "yes" as answer to all prompts and run non-interactively + AssumeYes bool } // StartOptions group options of the Start API diff --git a/pkg/compose/create.go b/pkg/compose/create.go index 0228fecb46e..858f8555f70 100644 --- a/pkg/compose/create.go +++ b/pkg/compose/create.go @@ -34,6 +34,7 @@ import ( pathutil "github.com/docker/compose/v2/internal/paths" "github.com/docker/compose/v2/pkg/api" "github.com/docker/compose/v2/pkg/progress" + "github.com/docker/compose/v2/pkg/prompt" "github.com/docker/compose/v2/pkg/utils" moby "github.com/docker/docker/api/types" "github.com/docker/docker/api/types/blkiodev" @@ -92,7 +93,7 @@ func (s *composeService) create(ctx context.Context, project *types.Project, opt return err } - volumes, err := s.ensureProjectVolumes(ctx, project) + volumes, err := s.ensureProjectVolumes(ctx, project, options.AssumeYes) if err != nil { return err } @@ -142,13 +143,13 @@ func (s *composeService) ensureNetworks(ctx context.Context, project *types.Proj return networks, nil } -func (s *composeService) ensureProjectVolumes(ctx context.Context, project *types.Project) (map[string]string, error) { +func (s *composeService) ensureProjectVolumes(ctx context.Context, project *types.Project, assumeYes bool) (map[string]string, error) { ids := map[string]string{} for k, volume := range project.Volumes { - volume.Labels = volume.Labels.Add(api.VolumeLabel, k) - volume.Labels = volume.Labels.Add(api.ProjectLabel, project.Name) - volume.Labels = volume.Labels.Add(api.VersionLabel, api.ComposeVersion) - id, err := s.ensureVolume(ctx, volume, project.Name) + volume.CustomLabels = volume.CustomLabels.Add(api.VolumeLabel, k) + volume.CustomLabels = volume.CustomLabels.Add(api.ProjectLabel, project.Name) + volume.CustomLabels = volume.CustomLabels.Add(api.VersionLabel, api.ComposeVersion) + id, err := s.ensureVolume(ctx, k, volume, project, assumeYes) if err != nil { return nil, err } @@ -1429,7 +1430,7 @@ func (s *composeService) resolveExternalNetwork(ctx context.Context, n *types.Ne } } -func (s *composeService) ensureVolume(ctx context.Context, volume types.VolumeConfig, project string) (string, error) { +func (s *composeService) ensureVolume(ctx context.Context, name string, volume types.VolumeConfig, project *types.Project, assumeYes bool) (string, error) { inspected, err := s.apiClient().VolumeInspect(ctx, volume.Name) if err != nil { if !errdefs.IsNotFound(err) { @@ -1439,7 +1440,7 @@ func (s *composeService) ensureVolume(ctx context.Context, volume types.VolumeCo return "", fmt.Errorf("external volume %q not found", volume.Name) } err = s.createVolume(ctx, volume) - return "", err + return volume.Name, err } if volume.External { @@ -1451,8 +1452,8 @@ func (s *composeService) ensureVolume(ctx context.Context, volume types.VolumeCo if !ok { logrus.Warnf("volume %q already exists but was not created by Docker Compose. Use `external: true` to use an existing volume", volume.Name) } - if ok && p != project { - logrus.Warnf("volume %q already exists but was created for project %q (expected %q). Use `external: true` to use an existing volume", volume.Name, p, project) + if ok && p != project.Name { + logrus.Warnf("volume %q already exists but was created for project %q (expected %q). Use `external: true` to use an existing volume", volume.Name, p, project.Name) } expected, err := VolumeHash(volume) @@ -1461,17 +1462,76 @@ func (s *composeService) ensureVolume(ctx context.Context, volume types.VolumeCo } actual, ok := inspected.Labels[api.ConfigHashLabel] if ok && actual != expected { - logrus.Warnf("volume %q exists but doesn't match configuration in compose file. You should remove it so it get recreated", volume.Name) + var confirm = assumeYes + if !assumeYes { + msg := fmt.Sprintf("Volume %q exists but doesn't match configuration in compose file. Recreate (data will be lost)?", volume.Name) + confirm, err = prompt.NewPrompt(s.stdin(), s.stdout()).Confirm(msg, false) + if err != nil { + return "", err + } + } + if confirm { + err = s.removeDivergedVolume(ctx, name, volume, project) + if err != nil { + return "", err + } + return volume.Name, s.createVolume(ctx, volume) + } } return inspected.Name, nil } +func (s *composeService) removeDivergedVolume(ctx context.Context, name string, volume types.VolumeConfig, project *types.Project) error { + // Remove services mounting divergent volume + var services []string + for _, service := range project.Services.Filter(func(config types.ServiceConfig) bool { + for _, cfg := range config.Volumes { + if cfg.Source == name { + return true + } + } + return false + }) { + services = append(services, service.Name) + } + + err := s.stop(ctx, project.Name, api.StopOptions{ + Services: services, + Project: project, + }) + if err != nil { + return err + } + + containers, err := s.getContainers(ctx, project.Name, oneOffExclude, true, services...) + if err != nil { + return err + } + + // FIXME (ndeloof) we have to remove container so we can recreate volume + // but doing so we can't inherit anonymous volumes from previous instance + err = s.remove(ctx, containers, api.RemoveOptions{ + Services: services, + Project: project, + }) + if err != nil { + return err + } + + return s.apiClient().VolumeRemove(ctx, volume.Name, true) +} + func (s *composeService) createVolume(ctx context.Context, volume types.VolumeConfig) error { eventName := fmt.Sprintf("Volume %q", volume.Name) w := progress.ContextWriter(ctx) w.Event(progress.CreatingEvent(eventName)) - _, err := s.apiClient().VolumeCreate(ctx, volumetypes.CreateOptions{ - Labels: volume.Labels, + hash, err := VolumeHash(volume) + if err != nil { + return err + } + volume.CustomLabels.Add(api.ConfigHashLabel, hash) + _, err = s.apiClient().VolumeCreate(ctx, volumetypes.CreateOptions{ + Labels: mergeLabels(volume.Labels, volume.CustomLabels), Name: volume.Name, Driver: volume.Driver, DriverOpts: volume.DriverOpts,