From 69de5dd492e93f057cff41ec53bb8276b2df23b9 Mon Sep 17 00:00:00 2001 From: Justin Alvarez Date: Thu, 21 Nov 2024 21:53:29 +0000 Subject: [PATCH] feat: add force-recreate and no-recreate for compose up command Signed-off-by: Justin Alvarez --- cmd/nerdctl/compose/compose_create.go | 2 +- cmd/nerdctl/compose/compose_up.go | 15 ++++++ docs/command-reference.md | 4 +- pkg/composer/container.go | 21 ++++++++ pkg/composer/logs.go | 21 ++++++-- pkg/composer/run.go | 2 +- pkg/composer/up.go | 13 +++++ pkg/composer/up_service.go | 69 ++++++++++++++++++--------- 8 files changed, 117 insertions(+), 30 deletions(-) diff --git a/cmd/nerdctl/compose/compose_create.go b/cmd/nerdctl/compose/compose_create.go index 211cc4758f9..9621398ae49 100644 --- a/cmd/nerdctl/compose/compose_create.go +++ b/cmd/nerdctl/compose/compose_create.go @@ -65,7 +65,7 @@ func composeCreateAction(cmd *cobra.Command, args []string) error { } noRecreate, err := cmd.Flags().GetBool("no-recreate") if err != nil { - return nil + return err } if forceRecreate && noRecreate { return errors.New("flag --force-recreate and --no-recreate cannot be specified together") diff --git a/cmd/nerdctl/compose/compose_up.go b/cmd/nerdctl/compose/compose_up.go index f2829ae1e02..f4670947c3f 100644 --- a/cmd/nerdctl/compose/compose_up.go +++ b/cmd/nerdctl/compose/compose_up.go @@ -47,6 +47,8 @@ func newComposeUpCommand() *cobra.Command { composeUpCommand.Flags().Bool("ipfs", false, "Allow pulling base images from IPFS during build") composeUpCommand.Flags().Bool("quiet-pull", false, "Pull without printing progress information") composeUpCommand.Flags().Bool("remove-orphans", false, "Remove containers for services not defined in the Compose file.") + 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.") return composeUpCommand } @@ -102,6 +104,17 @@ func composeUpAction(cmd *cobra.Command, services []string) error { if err != nil { return err } + forceRecreate, err := cmd.Flags().GetBool("force-recreate") + if err != nil { + return err + } + noRecreate, err := cmd.Flags().GetBool("no-recreate") + if err != nil { + return err + } + if forceRecreate && noRecreate { + return errors.New("flag --force-recreate and --no-recreate cannot be specified together") + } scale := make(map[string]int) for _, s := range scaleSlice { parts := strings.Split(s, "=") @@ -141,6 +154,8 @@ func composeUpAction(cmd *cobra.Command, services []string) error { QuietPull: quietPull, RemoveOrphans: removeOrphans, Scale: scale, + ForceRecreate: forceRecreate, + NoRecreate: noRecreate, } return c.Up(ctx, uo, services) } diff --git a/docs/command-reference.md b/docs/command-reference.md index 95dfddcfd45..4f7a314678a 100644 --- a/docs/command-reference.md +++ b/docs/command-reference.md @@ -1410,8 +1410,10 @@ Flags: - :whale: `--quiet-pull`: Pull without printing progress information - :whale: `--scale`: Scale SERVICE to NUM instances. Overrides the `scale` setting in the Compose file if present. - :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 -Unimplemented `docker-compose up` (V1) flags: `--no-deps`, `--force-recreate`, `--always-recreate-deps`, `--no-recreate`, +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` Unimplemented `docker compose up` (V2) flags: `--environment` diff --git a/pkg/composer/container.go b/pkg/composer/container.go index b1e101b7238..c789e42696a 100644 --- a/pkg/composer/container.go +++ b/pkg/composer/container.go @@ -63,3 +63,24 @@ func (c *Composer) containerExists(ctx context.Context, name, service string) (b // container doesn't exist return false, nil } + +func (c *Composer) containerID(ctx context.Context, name, service string) (string, error) { + // get list of containers for service + containers, err := c.Containers(ctx, service) + if err != nil { + return "", err + } + + for _, container := range containers { + containerLabels, err := container.Labels(ctx) + if err != nil { + return "", err + } + if name == containerLabels[labels.Name] { + // container exists + return container.ID(), nil + } + } + // container doesn't exist + return "", nil +} diff --git a/pkg/composer/logs.go b/pkg/composer/logs.go index b14722270ec..daa3ed0dd6b 100644 --- a/pkg/composer/logs.go +++ b/pkg/composer/logs.go @@ -41,6 +41,7 @@ type LogsOptions struct { Tail string NoColor bool NoLogPrefix bool + LatestRun bool } func (c *Composer) Logs(ctx context.Context, lo LogsOptions, services []string) error { @@ -62,9 +63,10 @@ func (c *Composer) Logs(ctx context.Context, lo LogsOptions, services []string) func (c *Composer) logs(ctx context.Context, containers []containerd.Container, lo LogsOptions) error { var logTagMaxLen int type containerState struct { - name string - logTag string - logCmd *exec.Cmd + name string + logTag string + logCmd *exec.Cmd + startedAt string } containerStates := make(map[string]containerState, len(containers)) // key: containerID @@ -78,9 +80,15 @@ func (c *Composer) logs(ctx context.Context, containers []containerd.Container, if l := len(logTag); l > logTagMaxLen { logTagMaxLen = l } + ts, err := info.UpdatedAt.MarshalText() + if err != nil { + return err + } + containerStates[container.ID()] = containerState{ - name: name, - logTag: logTag, + name: name, + logTag: logTag, + startedAt: string(ts), } } @@ -102,6 +110,9 @@ func (c *Composer) logs(ctx context.Context, containers []containerd.Container, args = append(args, lo.Tail) } } + if lo.LatestRun { + args = append(args, fmt.Sprintf("--since=%s", state.startedAt)) + } args = append(args, id) state.logCmd = c.createNerdctlCmd(ctx, args...) diff --git a/pkg/composer/run.go b/pkg/composer/run.go index 82e3a347b52..b2e98ea2b1d 100644 --- a/pkg/composer/run.go +++ b/pkg/composer/run.go @@ -242,7 +242,7 @@ func (c *Composer) runServices(ctx context.Context, parsedServices []*servicepar container := ps.Containers[0] runEG.Go(func() error { - id, err := c.upServiceContainer(ctx, ps, container) + id, err := c.upServiceContainer(ctx, ps, container, RecreateForce) if err != nil { return err } diff --git a/pkg/composer/up.go b/pkg/composer/up.go index 15545a977cf..b508b8ddc3d 100644 --- a/pkg/composer/up.go +++ b/pkg/composer/up.go @@ -39,9 +39,22 @@ type UpOptions struct { IPFS bool QuietPull bool RemoveOrphans bool + ForceRecreate bool + NoRecreate bool Scale map[string]int // map of service name to replicas } +func (opts UpOptions) recreateStrategy() string { + switch { + case opts.ForceRecreate: + return RecreateForce + case opts.NoRecreate: + return RecreateNever + default: + return RecreateDiverged + } +} + func (c *Composer) Up(ctx context.Context, uo UpOptions, services []string) error { for shortName := range c.project.Networks { if err := c.upNetwork(ctx, shortName); err != nil { diff --git a/pkg/composer/up_service.go b/pkg/composer/up_service.go index f3f9b26136d..7bd6ea48c92 100644 --- a/pkg/composer/up_service.go +++ b/pkg/composer/up_service.go @@ -21,6 +21,7 @@ import ( "errors" "fmt" "os" + "os/exec" "path/filepath" "strings" "sync" @@ -45,6 +46,8 @@ func (c *Composer) upServices(ctx context.Context, parsedServices []*servicepars } } + recreate := uo.recreateStrategy() + var ( containers = make(map[string]serviceparser.Container) // key: container ID services = []string{} @@ -57,7 +60,7 @@ func (c *Composer) upServices(ctx context.Context, parsedServices []*servicepars for _, container := range ps.Containers { container := container runEG.Go(func() error { - id, err := c.upServiceContainer(ctx, ps, container) + id, err := c.upServiceContainer(ctx, ps, container, recreate) if err != nil { return err } @@ -87,6 +90,7 @@ func (c *Composer) upServices(ctx context.Context, parsedServices []*servicepars Follow: true, NoColor: uo.NoColor, NoLogPrefix: uo.NoLogPrefix, + LatestRun: recreate == RecreateNever, } if err := c.Logs(ctx, lo, services); err != nil { return err @@ -118,15 +122,35 @@ func (c *Composer) ensureServiceImage(ctx context.Context, ps *serviceparser.Ser // upServiceContainer must be called after ensureServiceImage // upServiceContainer returns container ID -func (c *Composer) upServiceContainer(ctx context.Context, service *serviceparser.Service, container serviceparser.Container) (string, error) { +func (c *Composer) upServiceContainer(ctx context.Context, service *serviceparser.Service, container serviceparser.Container, recreate string) (string, error) { // check if container already exists - exists, err := c.containerExists(ctx, container.Name, service.Unparsed.Name) + existingCid, err := c.containerID(ctx, container.Name, service.Unparsed.Name) if err != nil { return "", fmt.Errorf("error while checking for containers with name %q: %s", container.Name, err) } + // FIXME + if service.Unparsed.StdinOpen != service.Unparsed.Tty { + return "", fmt.Errorf("currently StdinOpen(-i) and Tty(-t) should be same") + } + + var runFlagD bool + if !service.Unparsed.StdinOpen && !service.Unparsed.Tty { + container.RunArgs = append([]string{"-d"}, container.RunArgs...) + runFlagD = true + } + + // start the existing container and exit early + if existingCid != "" && recreate == RecreateNever { + cmd := c.createNerdctlCmd(ctx, append([]string{"start"}, existingCid)...) + if err := c.executeUpCmd(ctx, cmd, container.Name, runFlagD, service.Unparsed.StdinOpen); err != nil { + return "", fmt.Errorf("error while starting existing container %s: %w", container.Name, err) + } + return existingCid, nil + } + // delete container if it already exists - if exists { + if existingCid != "" { log.G(ctx).Debugf("Container %q already exists, deleting", container.Name) delCmd := c.createNerdctlCmd(ctx, "rm", "-f", container.Name) if err = delCmd.Run(); err != nil { @@ -151,12 +175,6 @@ func (c *Composer) upServiceContainer(ctx context.Context, service *serviceparse defer os.RemoveAll(tempDir) cidFilename := filepath.Join(tempDir, "cid") - var runFlagD bool - if !service.Unparsed.StdinOpen && !service.Unparsed.Tty { - container.RunArgs = append([]string{"-d"}, container.RunArgs...) - runFlagD = true - } - //add metadata labels to container https://github.com/compose-spec/compose-spec/blob/master/spec.md#labels container.RunArgs = append([]string{ "--cidfile=" + cidFilename, @@ -169,12 +187,24 @@ func (c *Composer) upServiceContainer(ctx context.Context, service *serviceparse log.G(ctx).Debugf("Running %v", cmd.Args) } - // FIXME - if service.Unparsed.StdinOpen != service.Unparsed.Tty { - return "", fmt.Errorf("currently StdinOpen(-i) and Tty(-t) should be same") + if err := c.executeUpCmd(ctx, cmd, container.Name, runFlagD, service.Unparsed.StdinOpen); err != nil { + return "", fmt.Errorf("error while creating container %s: %w", container.Name, err) + } + + cid, err := os.ReadFile(cidFilename) + if err != nil { + return "", fmt.Errorf("error while creating container %s: %w", container.Name, err) + } + return strings.TrimSpace(string(cid)), nil +} + +func (c *Composer) executeUpCmd(ctx context.Context, cmd *exec.Cmd, containerName string, runFlagD, stdinOpen bool) error { + log.G(ctx).Infof("Running %v", cmd.Args) + if c.DebugPrintFull { + log.G(ctx).Debugf("Running %v", cmd.Args) } - if service.Unparsed.StdinOpen { + if stdinOpen { cmd.Stdin = os.Stdin } if !runFlagD { @@ -183,14 +213,9 @@ func (c *Composer) upServiceContainer(ctx context.Context, service *serviceparse // Always propagate stderr to print detailed error messages (https://github.com/containerd/nerdctl/issues/1942) cmd.Stderr = os.Stderr - err = cmd.Run() - if err != nil { - return "", fmt.Errorf("error while creating container %s: %w", container.Name, err) + if err := cmd.Run(); err != nil { + return fmt.Errorf("error while creating container %s: %w", containerName, err) } - cid, err := os.ReadFile(cidFilename) - if err != nil { - return "", fmt.Errorf("error while creating container %s: %w", container.Name, err) - } - return strings.TrimSpace(string(cid)), nil + return nil }