Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Recreate container on volume configuration change #12363

Merged
merged 2 commits into from
Dec 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions cmd/compose/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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
}

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

Expand Down
2 changes: 2 additions & 0 deletions cmd/compose/up.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down Expand Up @@ -255,6 +256,7 @@ func runUp(
Inherit: !createOptions.noInherit,
Timeout: createOptions.GetTimeout(),
QuietPull: createOptions.quietPull,
AssumeYes: createOptions.AssumeYes,
}

if upOptions.noStart {
Expand Down
1 change: 1 addition & 0 deletions docs/reference/compose_create.md
Original file line number Diff line number Diff line change
Expand Up @@ -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-->
Expand Down
1 change: 1 addition & 0 deletions docs/reference/compose_up.md
Original file line number Diff line number Diff line change
Expand Up @@ -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-->
Expand Down
11 changes: 11 additions & 0 deletions docs/reference/docker_compose_create.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
11 changes: 11 additions & 0 deletions docs/reference/docker_compose_up.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions pkg/api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
74 changes: 56 additions & 18 deletions pkg/compose/convergence.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import (
"github.com/docker/compose/v2/internal/tracing"
moby "github.com/docker/docker/api/types"
containerType "github.com/docker/docker/api/types/container"
mmount "github.com/docker/docker/api/types/mount"
"github.com/docker/docker/api/types/versions"
specs "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/sirupsen/logrus"
Expand All @@ -60,6 +61,7 @@ type convergence struct {
service *composeService
services map[string]Containers
networks map[string]string
volumes map[string]string
stateMutex sync.Mutex
}

Expand All @@ -75,7 +77,7 @@ func (c *convergence) setObservedState(serviceName string, containers Containers
c.services[serviceName] = containers
}

func newConvergence(services []string, state Containers, networks map[string]string, s *composeService) *convergence {
func newConvergence(services []string, state Containers, networks map[string]string, volumes map[string]string, s *composeService) *convergence {
observedState := map[string]Containers{}
for _, s := range services {
observedState[s] = Containers{}
Expand All @@ -88,6 +90,7 @@ func newConvergence(services []string, state Containers, networks map[string]str
service: s,
services: observedState,
networks: networks,
volumes: volumes,
}
}

Expand Down Expand Up @@ -341,28 +344,63 @@ func (c *convergence) mustRecreate(expected types.ServiceConfig, actual moby.Con
}

if c.networks != nil && actual.State == "running" {
// check the networks container is connected to are the expected ones
for net := range expected.Networks {
id := c.networks[net]
if id == "swarm" {
// corner-case : swarm overlay network isn't visible until a container is attached
continue
if checkExpectedNetworks(expected, actual, c.networks) {
return true, nil
}
}

if c.volumes != nil {
if checkExpectedVolumes(expected, actual, c.volumes) {
return true, nil
}
}

return false, nil
}

func checkExpectedNetworks(expected types.ServiceConfig, actual moby.Container, networks map[string]string) bool {
// check the networks container is connected to are the expected ones
for net := range expected.Networks {
id := networks[net]
if id == "swarm" {
// corner-case : swarm overlay network isn't visible until a container is attached
continue
}
found := false
for _, settings := range actual.NetworkSettings.Networks {
if settings.NetworkID == id {
found = true
break
}
found := false
for _, settings := range actual.NetworkSettings.Networks {
if settings.NetworkID == id {
found = true
break
}
}
if !found {
// config is up-to-date but container is not connected to network
return true
}
}
return false
}

func checkExpectedVolumes(expected types.ServiceConfig, actual moby.Container, volumes map[string]string) bool {
// check container's volume mounts and search for the expected ones
for _, vol := range expected.Volumes {
id := volumes[vol.Source]
found := false
for _, mount := range actual.Mounts {
if mount.Type != mmount.TypeVolume {
continue
}
if !found {
// config is up-to-date but container is not connected to network - maybe recreated ?
return true, nil
if mount.Name == id {
found = true
break
}
}
if !found {
// config is up-to-date but container doesn't have volume mounted
return true
}
}

return false, nil
return false
}

func getContainerName(projectName string, service types.ServiceConfig, number int) string {
Expand Down
112 changes: 92 additions & 20 deletions pkg/compose/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -92,7 +93,8 @@ func (s *composeService) create(ctx context.Context, project *types.Project, opt
return err
}

if err := s.ensureProjectVolumes(ctx, project); err != nil {
volumes, err := s.ensureProjectVolumes(ctx, project, options.AssumeYes)
if err != nil {
return err
}

Expand All @@ -115,7 +117,7 @@ func (s *composeService) create(ctx context.Context, project *types.Project, opt
"--remove-orphans flag to clean it up.", orphans.names())
}
}
return newConvergence(options.Services, observedState, networks, s).apply(ctx, project, options)
return newConvergence(options.Services, observedState, networks, volumes, s).apply(ctx, project, options)
}

func prepareNetworks(project *types.Project) {
Expand All @@ -141,15 +143,17 @@ func (s *composeService) ensureNetworks(ctx context.Context, project *types.Proj
return networks, nil
}

func (s *composeService) ensureProjectVolumes(ctx context.Context, project *types.Project) 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)
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 err
return nil, err
}
ids[k] = id
}

err := func() error {
Expand Down Expand Up @@ -205,7 +209,7 @@ func (s *composeService) ensureProjectVolumes(ctx context.Context, project *type
if err != nil {
progress.ContextWriter(ctx).TailMsgf("Failed to prepare Synchronized file shares: %v", err)
}
return nil
return ids, nil
}

func (s *composeService) getCreateConfigs(ctx context.Context,
Expand Down Expand Up @@ -1426,40 +1430,108 @@ func (s *composeService) resolveExternalNetwork(ctx context.Context, n *types.Ne
}
}

func (s *composeService) ensureVolume(ctx context.Context, volume types.VolumeConfig, project 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) {
return err
return "", err
}
if volume.External {
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)
return err
err = s.createVolume(ctx, volume)
return volume.Name, err
}

if volume.External {
return nil
return volume.Name, nil
}

// Volume exists with name, but let's double-check this is the expected one
p, ok := inspected.Labels[api.ProjectLabel]
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)
}
return nil

expected, err := VolumeHash(volume)
if err != nil {
return "", err
}
actual, ok := inspected.Labels[api.ConfigHashLabel]
if ok && actual != expected {
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,
Expand Down
Loading
Loading