diff --git a/cmd/nerdctl/compose/compose_up.go b/cmd/nerdctl/compose/compose_up.go index f4670947c3f..0cf2d6bccd2 100644 --- a/cmd/nerdctl/compose/compose_up.go +++ b/cmd/nerdctl/compose/compose_up.go @@ -50,6 +50,7 @@ func newComposeUpCommand() *cobra.Command { composeUpCommand.Flags().Bool("force-recreate", false, "Recreate containers even if their configuration and image haven't changed.") composeUpCommand.Flags().Bool("no-recreate", false, "Don't recreate containers if they exist, conflict with --force-recreate.") composeUpCommand.Flags().StringArray("scale", []string{}, "Scale SERVICE to NUM instances. Overrides the `scale` setting in the Compose file if present.") + composeUpCommand.Flags().String("pull", "", "Pull image before running (\"always\"|\"missing\"|\"never\")") return composeUpCommand } @@ -96,6 +97,10 @@ func composeUpAction(cmd *cobra.Command, services []string) error { if err != nil { return err } + pull, err := cmd.Flags().GetString("pull") + if err != nil { + return err + } removeOrphans, err := cmd.Flags().GetBool("remove-orphans") if err != nil { return err @@ -154,6 +159,7 @@ func composeUpAction(cmd *cobra.Command, services []string) error { QuietPull: quietPull, RemoveOrphans: removeOrphans, Scale: scale, + Pull: pull, ForceRecreate: forceRecreate, NoRecreate: noRecreate, } diff --git a/cmd/nerdctl/compose/compose_up_linux_test.go b/cmd/nerdctl/compose/compose_up_linux_test.go index 6dbd6b7cf34..6da3162b4b9 100644 --- a/cmd/nerdctl/compose/compose_up_linux_test.go +++ b/cmd/nerdctl/compose/compose_up_linux_test.go @@ -579,3 +579,57 @@ services: } c.Assert(expected) } + +func TestComposeUpPull(t *testing.T) { + base := testutil.NewBase(t) + + var dockerComposeYAML = fmt.Sprintf(` +services: + test: + image: %s + command: sh -euxc "echo hi" +`, testutil.CommonImage) + + comp := testutil.NewComposeDir(t, dockerComposeYAML) + defer comp.CleanUp() + + // Cases where pull is required + for _, pull := range []string{"missing", "always"} { + t.Run(fmt.Sprintf("pull=%s", pull), func(t *testing.T) { + base.Cmd("rmi", "-f", testutil.CommonImage).Run() + base.Cmd("images").AssertOutNotContains(testutil.CommonImage) + t.Cleanup(func() { + base.ComposeCmd("-f", comp.YAMLFullPath(), "down").AssertOK() + }) + base.ComposeCmd("-f", comp.YAMLFullPath(), "up", "--pull", pull).AssertOutContains("hi") + }) + } + + t.Run("pull=never, no pull", func(t *testing.T) { + base.Cmd("rmi", "-f", testutil.CommonImage).Run() + base.Cmd("images").AssertOutNotContains(testutil.CommonImage) + t.Cleanup(func() { + base.ComposeCmd("-f", comp.YAMLFullPath(), "down").AssertOK() + }) + base.ComposeCmd("-f", comp.YAMLFullPath(), "up", "--pull", "never").AssertExitCode(1) + }) +} + +func TestComposeUpServicePullPolicy(t *testing.T) { + base := testutil.NewBase(t) + + var dockerComposeYAML = fmt.Sprintf(` +services: + test: + image: %s + command: sh -euxc "echo hi" + pull_policy: "never" +`, testutil.CommonImage) + + comp := testutil.NewComposeDir(t, dockerComposeYAML) + defer comp.CleanUp() + + base.Cmd("rmi", "-f", testutil.CommonImage).Run() + base.Cmd("images").AssertOutNotContains(testutil.CommonImage) + base.ComposeCmd("-f", comp.YAMLFullPath(), "up").AssertExitCode(1) +} diff --git a/docs/command-reference.md b/docs/command-reference.md index 628a1e50b9e..28c4da551de 100644 --- a/docs/command-reference.md +++ b/docs/command-reference.md @@ -1413,6 +1413,7 @@ Flags: - :whale: `--remove-orphans`: Remove containers for services not defined in the Compose file - :whale: `--force-recreate`: force Compose to stop and recreate all containers - :whale: `--no-recreate`: force Compose to reuse existing containers +- :whale: `--pull`: Pull image before running ("always"|"missing"|"never") Unimplemented `docker-compose up` (V1) flags: `--no-deps`, `--always-recreate-deps`, `--no-start`, `--abort-on-container-exit`, `--attach-dependencies`, `--timeout`, `--renew-anon-volumes`, `--exit-code-from` diff --git a/pkg/composer/create.go b/pkg/composer/create.go index 25d6872590b..a2ae9180160 100644 --- a/pkg/composer/create.go +++ b/pkg/composer/create.go @@ -119,7 +119,7 @@ func (c *Composer) Create(ctx context.Context, opt CreateOptions, services []str return err } for _, ps := range parsedServices { - if err := c.ensureServiceImage(ctx, ps, !opt.NoBuild, opt.Build, BuildOptions{}, false); err != nil { + if err := c.ensureServiceImage(ctx, ps, !opt.NoBuild, opt.Build, BuildOptions{}, false, ""); err != nil { return err } } diff --git a/pkg/composer/run.go b/pkg/composer/run.go index b2e98ea2b1d..0b3c4c72342 100644 --- a/pkg/composer/run.go +++ b/pkg/composer/run.go @@ -215,7 +215,7 @@ func (c *Composer) runServices(ctx context.Context, parsedServices []*servicepar // TODO: parallelize loop for ensuring images (make sure not to mess up tty) for _, ps := range parsedServices { - if err := c.ensureServiceImage(ctx, ps, !ro.NoBuild, ro.ForceBuild, BuildOptions{}, ro.QuietPull); err != nil { + if err := c.ensureServiceImage(ctx, ps, !ro.NoBuild, ro.ForceBuild, BuildOptions{}, ro.QuietPull, ""); err != nil { return err } } diff --git a/pkg/composer/up.go b/pkg/composer/up.go index b508b8ddc3d..98106c81ae4 100644 --- a/pkg/composer/up.go +++ b/pkg/composer/up.go @@ -42,6 +42,7 @@ type UpOptions struct { ForceRecreate bool NoRecreate bool Scale map[string]int // map of service name to replicas + Pull string } func (opts UpOptions) recreateStrategy() string { diff --git a/pkg/composer/up_service.go b/pkg/composer/up_service.go index 85010f25ce9..f0da7c9b72a 100644 --- a/pkg/composer/up_service.go +++ b/pkg/composer/up_service.go @@ -41,7 +41,7 @@ func (c *Composer) upServices(ctx context.Context, parsedServices []*servicepars // TODO: parallelize loop for ensuring images (make sure not to mess up tty) for _, ps := range parsedServices { - if err := c.ensureServiceImage(ctx, ps, !uo.NoBuild, uo.ForceBuild, BuildOptions{}, uo.QuietPull); err != nil { + if err := c.ensureServiceImage(ctx, ps, !uo.NoBuild, uo.ForceBuild, BuildOptions{}, uo.QuietPull, uo.Pull); err != nil { return err } } @@ -101,7 +101,7 @@ func (c *Composer) upServices(ctx context.Context, parsedServices []*servicepars return nil } -func (c *Composer) ensureServiceImage(ctx context.Context, ps *serviceparser.Service, allowBuild, forceBuild bool, bo BuildOptions, quiet bool) error { +func (c *Composer) ensureServiceImage(ctx context.Context, ps *serviceparser.Service, allowBuild, forceBuild bool, bo BuildOptions, quiet bool, pullModeArg string) error { if ps.Build != nil && allowBuild { if ps.Build.Force || forceBuild { return c.buildServiceImage(ctx, ps.Image, ps.Build, ps.Unparsed.Platform, bo) @@ -117,6 +117,9 @@ func (c *Composer) ensureServiceImage(ctx context.Context, ps *serviceparser.Ser } log.G(ctx).Infof("Ensuring image %s", ps.Image) + if pullModeArg != "" { + return c.EnsureImage(ctx, ps.Image, pullModeArg, ps.Unparsed.Platform, ps, quiet) + } return c.EnsureImage(ctx, ps.Image, ps.PullMode, ps.Unparsed.Platform, ps, quiet) }