Browse Source

prompt user to confirm volume recreation

Signed-off-by: Nicolas De Loof <[email protected]>
Nicolas De Loof 11 months ago
parent
commit
8e0520e71e

+ 3 - 0
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,
 	})
 }
 

+ 2 - 0
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 {

+ 1 - 0
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                               |
 
 
 <!---MARKER_GEN_END-->

+ 1 - 0
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                                                                                     |
 
 
 <!---MARKER_GEN_END-->

+ 11 - 0
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

+ 11 - 0
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

+ 2 - 0
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

+ 73 - 13
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
 		}
@@ -1434,7 +1435,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) {
@@ -1444,7 +1445,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 {
@@ -1456,8 +1457,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)
@@ -1466,17 +1467,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,