diff --git a/cmd/compose/run.go b/cmd/compose/run.go index 7d5f522b43..46a673ce87 100644 --- a/cmd/compose/run.go +++ b/cmd/compose/run.go @@ -241,6 +241,7 @@ func runCommand(p *ProjectOptions, dockerCli command.Cli, backendOptions *Backen flags.BoolVar(&buildOpts.quiet, "quiet-build", false, "Suppress progress output from the build process") flags.BoolVar(&options.quietPull, "quiet-pull", false, "Pull without printing progress information") flags.BoolVar(&createOpts.Build, "build", false, "Build image before starting container") + flags.BoolVar(&createOpts.noRecreate, "no-recreate", false, "If dependent containers already exist, don't recreate them, even if their configuration diverged") flags.BoolVar(&options.removeOrphans, "remove-orphans", false, "Remove containers for services not defined in the Compose file") cmd.Flags().BoolVarP(&options.interactive, "interactive", "i", true, "Keep STDIN open even if not attached") @@ -329,6 +330,11 @@ func runRun(ctx context.Context, backend api.Compose, project *types.Project, op RemoveOrphans: options.removeOrphans, IgnoreOrphans: options.ignoreOrphans, QuietPull: options.quietPull, + // Only the dependencies run starts are subject to a recreate + // policy (the one-off itself is always a fresh container), but + // their create sees them as targeted services — set both. + Recreate: createOpts.recreateStrategy(), + RecreateDependencies: createOpts.dependenciesRecreateStrategy(), }, Name: options.name, Service: options.Service, diff --git a/docs/reference/compose_run.md b/docs/reference/compose_run.md index 14fb854792..f7681f6c9c 100644 --- a/docs/reference/compose_run.md +++ b/docs/reference/compose_run.md @@ -57,33 +57,34 @@ specified in the service configuration. ### Options -| Name | Type | Default | Description | -|:------------------------|:--------------|:---------|:---------------------------------------------------------------------------------| -| `--build` | `bool` | | Build image before starting container | -| `--cap-add` | `list` | | Add Linux capabilities | -| `--cap-drop` | `list` | | Drop Linux capabilities | -| `-d`, `--detach` | `bool` | | Run container in background and print container ID | -| `--dry-run` | `bool` | | Execute command in dry run mode | -| `--entrypoint` | `string` | | Override the entrypoint of the image | -| `-e`, `--env` | `stringArray` | | Set environment variables | -| `--env-from-file` | `stringArray` | | Set environment variables from file | -| `-i`, `--interactive` | `bool` | `true` | Keep STDIN open even if not attached | -| `-l`, `--label` | `stringArray` | | Add or override a label | -| `--name` | `string` | | Assign a name to the container | -| `--no-deps` | `bool` | | Don't start linked services | -| `-T`, `--no-tty` | `bool` | `true` | Disable pseudo-TTY allocation (default: auto-detected) | -| `-p`, `--publish` | `stringArray` | | Publish a container's port(s) to the host | -| `--pull` | `string` | `policy` | Pull image before running ("always"\|"missing"\|"never") | -| `-q`, `--quiet` | `bool` | | Don't print anything to STDOUT | -| `--quiet-build` | `bool` | | Suppress progress output from the build process | -| `--quiet-pull` | `bool` | | Pull without printing progress information | -| `--remove-orphans` | `bool` | | Remove containers for services not defined in the Compose file | -| `--rm` | `bool` | | Automatically remove the container when it exits | -| `-P`, `--service-ports` | `bool` | | Run command with all service's ports enabled and mapped to the host | -| `--use-aliases` | `bool` | | Use the service's network useAliases in the network(s) the container connects to | -| `-u`, `--user` | `string` | | Run as specified username or uid | -| `-v`, `--volume` | `stringArray` | | Bind mount a volume | -| `-w`, `--workdir` | `string` | | Working directory inside the container | +| Name | Type | Default | Description | +|:------------------------|:--------------|:---------|:-------------------------------------------------------------------------------------------------| +| `--build` | `bool` | | Build image before starting container | +| `--cap-add` | `list` | | Add Linux capabilities | +| `--cap-drop` | `list` | | Drop Linux capabilities | +| `-d`, `--detach` | `bool` | | Run container in background and print container ID | +| `--dry-run` | `bool` | | Execute command in dry run mode | +| `--entrypoint` | `string` | | Override the entrypoint of the image | +| `-e`, `--env` | `stringArray` | | Set environment variables | +| `--env-from-file` | `stringArray` | | Set environment variables from file | +| `-i`, `--interactive` | `bool` | `true` | Keep STDIN open even if not attached | +| `-l`, `--label` | `stringArray` | | Add or override a label | +| `--name` | `string` | | Assign a name to the container | +| `--no-deps` | `bool` | | Don't start linked services | +| `--no-recreate` | `bool` | | If dependent containers already exist, don't recreate them, even if their configuration diverged | +| `-T`, `--no-tty` | `bool` | `true` | Disable pseudo-TTY allocation (default: auto-detected) | +| `-p`, `--publish` | `stringArray` | | Publish a container's port(s) to the host | +| `--pull` | `string` | `policy` | Pull image before running ("always"\|"missing"\|"never") | +| `-q`, `--quiet` | `bool` | | Don't print anything to STDOUT | +| `--quiet-build` | `bool` | | Suppress progress output from the build process | +| `--quiet-pull` | `bool` | | Pull without printing progress information | +| `--remove-orphans` | `bool` | | Remove containers for services not defined in the Compose file | +| `--rm` | `bool` | | Automatically remove the container when it exits | +| `-P`, `--service-ports` | `bool` | | Run command with all service's ports enabled and mapped to the host | +| `--use-aliases` | `bool` | | Use the service's network useAliases in the network(s) the container connects to | +| `-u`, `--user` | `string` | | Run as specified username or uid | +| `-v`, `--volume` | `stringArray` | | Bind mount a volume | +| `-w`, `--workdir` | `string` | | Working directory inside the container | diff --git a/docs/reference/docker_compose_run.yaml b/docs/reference/docker_compose_run.yaml index bc9f81e9ed..dca423ab9a 100644 --- a/docs/reference/docker_compose_run.yaml +++ b/docs/reference/docker_compose_run.yaml @@ -168,6 +168,17 @@ options: experimentalcli: false kubernetes: false swarm: false + - option: no-recreate + value_type: bool + default_value: "false" + description: | + If dependent containers already exist, don't recreate them, even if their configuration diverged + deprecated: false + hidden: false + experimental: false + experimentalcli: false + kubernetes: false + swarm: false - option: no-tty shorthand: T value_type: bool diff --git a/pkg/compose/run.go b/pkg/compose/run.go index 3b3308493d..afdfaabf28 100644 --- a/pkg/compose/run.go +++ b/pkg/compose/run.go @@ -289,6 +289,10 @@ func (s *composeService) startDependencies(ctx context.Context, project *types.P IgnoreOrphans: options.IgnoreOrphans, RemoveOrphans: options.RemoveOrphans, QuietPull: options.QuietPull, + // empty means the default "diverged" policy; `run --no-recreate` + // passes "never" so an existing dependency is reused as-is + Recreate: options.Recreate, + RecreateDependencies: options.RecreateDependencies, }) if err != nil { return err diff --git a/pkg/e2e/compose_run_test.go b/pkg/e2e/compose_run_test.go index 31feafe504..3012302e3c 100644 --- a/pkg/e2e/compose_run_test.go +++ b/pkg/e2e/compose_run_test.go @@ -92,6 +92,26 @@ func TestComposeRunDeps(t *testing.T) { ServiceNotCreated("service_b")) } +func TestComposeRunNoRecreate(t *testing.T) { + // Regression test for https://github.com/docker/compose/issues/13769 + // A dependency whose config-hash diverged is recreated by the default + // "diverged" policy — correct, but state-destroying for a database + // warmed up by a previous run. --no-recreate opts out and reuses the + // existing container as-is. + NewScenario(t, "run --no-recreate must reuse an existing dependency even when its configuration diverged"). + Step("run creates and starts the dependency", + ComposeCmd("run", "task").WithEnv("TAG=one"), + StdoutContains("task done"), + ServiceState("dep", "running")). + Step("by default a diverged dependency is recreated", + ComposeCmd("run", "task").WithEnv("TAG=two", "COMPOSE_IGNORE_ORPHANS=True"), + Recreated("dep")). + Step("run --no-recreate reuses the diverged dependency as-is", + ComposeCmd("run", "--no-recreate", "task").WithEnv("TAG=three", "COMPOSE_IGNORE_ORPHANS=True"), + NotRecreated("dep"), + ServiceState("dep", "running")) +} + func TestComposeRunNotRequiredDeps(t *testing.T) { NewScenario(t, "run must skip a dependency marked required: false when its profile is inactive"). Step("run executes the service without materializing the optional dependency", diff --git a/pkg/e2e/testdata/TestComposeRunNoRecreate/compose.yaml b/pkg/e2e/testdata/TestComposeRunNoRecreate/compose.yaml new file mode 100644 index 0000000000..b7985319a0 --- /dev/null +++ b/pkg/e2e/testdata/TestComposeRunNoRecreate/compose.yaml @@ -0,0 +1,14 @@ +services: + task: + image: alpine + command: echo "task done" + depends_on: + - dep + dep: + image: alpine + init: true + command: sleep infinity + environment: + # divergence vector: changing TAG between invocations changes the + # config-hash of dep, which the default policy answers by recreating + TAG: ${TAG:-initial}