prompt user to confirm volume recreation

Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
This commit is contained in:
Nicolas De Loof 2024-12-12 09:36:24 +01:00 committed by Nicolas De loof
parent 332311358e
commit 8e0520e71e
8 changed files with 104 additions and 13 deletions

View File

@ -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,
})
}

View File

@ -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 {

View File

@ -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-->

View File

@ -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-->

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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,