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 timeout int
quietPull bool quietPull bool
scale []string scale []string
AssumeYes bool
} }
func createCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *cobra.Command { 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.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.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.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 return cmd
} }
@ -107,6 +109,7 @@ func runCreate(ctx context.Context, _ command.Cli, backend api.Service, createOp
Inherit: !createOpts.noInherit, Inherit: !createOpts.noInherit,
Timeout: createOpts.GetTimeout(), Timeout: createOpts.GetTimeout(),
QuietPull: createOpts.quietPull, 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 := upCmd.Flags()
flags.BoolVarP(&up.Detach, "detach", "d", false, "Detached mode: Run containers in the background") 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.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.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.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") 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, Inherit: !createOptions.noInherit,
Timeout: createOptions.GetTimeout(), Timeout: createOptions.GetTimeout(),
QuietPull: createOptions.quietPull, QuietPull: createOptions.quietPull,
AssumeYes: createOptions.AssumeYes,
} }
if upOptions.noStart { if upOptions.noStart {

View File

@ -16,6 +16,7 @@ Creates containers for a service
| `--quiet-pull` | `bool` | | Pull without printing progress information | | `--quiet-pull` | `bool` | | Pull without printing progress information |
| `--remove-orphans` | `bool` | | Remove containers for services not defined in the Compose file | | `--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. | | `--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--> <!---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` | `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 | | `--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. | | `-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--> <!---MARKER_GEN_END-->

View File

@ -88,6 +88,17 @@ options:
experimentalcli: false experimentalcli: false
kubernetes: false kubernetes: false
swarm: 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: inherited_options:
- option: dry-run - option: dry-run
value_type: bool value_type: bool

View File

@ -309,6 +309,17 @@ options:
experimentalcli: false experimentalcli: false
kubernetes: false kubernetes: false
swarm: 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: inherited_options:
- option: dry-run - option: dry-run
value_type: bool value_type: bool

View File

@ -207,6 +207,8 @@ type CreateOptions struct {
Timeout *time.Duration Timeout *time.Duration
// QuietPull makes the pulling process quiet // QuietPull makes the pulling process quiet
QuietPull bool QuietPull bool
// AssumeYes assume "yes" as answer to all prompts and run non-interactively
AssumeYes bool
} }
// StartOptions group options of the Start API // StartOptions group options of the Start API

View File

@ -34,6 +34,7 @@ import (
pathutil "github.com/docker/compose/v2/internal/paths" pathutil "github.com/docker/compose/v2/internal/paths"
"github.com/docker/compose/v2/pkg/api" "github.com/docker/compose/v2/pkg/api"
"github.com/docker/compose/v2/pkg/progress" "github.com/docker/compose/v2/pkg/progress"
"github.com/docker/compose/v2/pkg/prompt"
"github.com/docker/compose/v2/pkg/utils" "github.com/docker/compose/v2/pkg/utils"
moby "github.com/docker/docker/api/types" moby "github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/blkiodev" "github.com/docker/docker/api/types/blkiodev"
@ -92,7 +93,7 @@ func (s *composeService) create(ctx context.Context, project *types.Project, opt
return err return err
} }
volumes, err := s.ensureProjectVolumes(ctx, project) volumes, err := s.ensureProjectVolumes(ctx, project, options.AssumeYes)
if err != nil { if err != nil {
return err return err
} }
@ -142,13 +143,13 @@ func (s *composeService) ensureNetworks(ctx context.Context, project *types.Proj
return networks, nil 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{} ids := map[string]string{}
for k, volume := range project.Volumes { for k, volume := range project.Volumes {
volume.Labels = volume.Labels.Add(api.VolumeLabel, k) volume.CustomLabels = volume.CustomLabels.Add(api.VolumeLabel, k)
volume.Labels = volume.Labels.Add(api.ProjectLabel, project.Name) volume.CustomLabels = volume.CustomLabels.Add(api.ProjectLabel, project.Name)
volume.Labels = volume.Labels.Add(api.VersionLabel, api.ComposeVersion) volume.CustomLabels = volume.CustomLabels.Add(api.VersionLabel, api.ComposeVersion)
id, err := s.ensureVolume(ctx, volume, project.Name) id, err := s.ensureVolume(ctx, k, volume, project, assumeYes)
if err != nil { if err != nil {
return nil, err 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) inspected, err := s.apiClient().VolumeInspect(ctx, volume.Name)
if err != nil { if err != nil {
if !errdefs.IsNotFound(err) { 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) return "", fmt.Errorf("external volume %q not found", volume.Name)
} }
err = s.createVolume(ctx, volume) err = s.createVolume(ctx, volume)
return "", err return volume.Name, err
} }
if volume.External { if volume.External {
@ -1456,8 +1457,8 @@ func (s *composeService) ensureVolume(ctx context.Context, volume types.VolumeCo
if !ok { 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) 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 { 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) 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) expected, err := VolumeHash(volume)
@ -1466,17 +1467,76 @@ func (s *composeService) ensureVolume(ctx context.Context, volume types.VolumeCo
} }
actual, ok := inspected.Labels[api.ConfigHashLabel] actual, ok := inspected.Labels[api.ConfigHashLabel]
if ok && actual != expected { 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 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 { func (s *composeService) createVolume(ctx context.Context, volume types.VolumeConfig) error {
eventName := fmt.Sprintf("Volume %q", volume.Name) eventName := fmt.Sprintf("Volume %q", volume.Name)
w := progress.ContextWriter(ctx) w := progress.ContextWriter(ctx)
w.Event(progress.CreatingEvent(eventName)) w.Event(progress.CreatingEvent(eventName))
_, err := s.apiClient().VolumeCreate(ctx, volumetypes.CreateOptions{ hash, err := VolumeHash(volume)
Labels: volume.Labels, 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, Name: volume.Name,
Driver: volume.Driver, Driver: volume.Driver,
DriverOpts: volume.DriverOpts, DriverOpts: volume.DriverOpts,