From 72e4519cbfb6cdfc600e6ebfa377ce4b8e162c78 Mon Sep 17 00:00:00 2001 From: Nicolas De Loof Date: Mon, 11 Oct 2021 17:52:31 +0200 Subject: [PATCH] introduce up --wait condition Signed-off-by: Nicolas De Loof --- cmd/compose/up.go | 45 ++++++++++++++++++++++++-------------- pkg/api/api.go | 2 ++ pkg/compose/convergence.go | 25 ++++++++++++++++----- pkg/compose/create.go | 5 +++++ pkg/compose/run.go | 2 +- pkg/compose/start.go | 15 +++++++++++++ 6 files changed, 72 insertions(+), 22 deletions(-) diff --git a/cmd/compose/up.go b/cmd/compose/up.go index a8d15ac7e..d7a201ab0 100644 --- a/cmd/compose/up.go +++ b/cmd/compose/up.go @@ -50,6 +50,7 @@ type upOptions struct { noPrefix bool attachDependencies bool attach []string + wait bool } func (opts upOptions) apply(project *types.Project, services []string) error { @@ -100,22 +101,7 @@ func upCommand(p *projectOptions, backend api.Service) *cobra.Command { Short: "Create and start containers", PreRunE: AdaptCmd(func(ctx context.Context, cmd *cobra.Command, args []string) error { create.timeChanged = cmd.Flags().Changed("timeout") - if up.exitCodeFrom != "" { - up.cascadeStop = true - } - if create.Build && create.noBuild { - return fmt.Errorf("--build and --no-build are incompatible") - } - if up.Detach && (up.attachDependencies || up.cascadeStop || len(up.attach) > 0) { - return fmt.Errorf("--detach cannot be combined with --abort-on-container-exit, --attach or --attach-dependencies") - } - if create.forceRecreate && create.noRecreate { - return fmt.Errorf("--force-recreate and --no-recreate are incompatible") - } - if create.recreateDeps && create.noRecreate { - return fmt.Errorf("--always-recreate-deps and --no-recreate are incompatible") - } - return nil + return validateFlags(&up, &create) }), RunE: p.WithServices(func(ctx context.Context, project *types.Project, services []string) error { ignore := project.Environment["COMPOSE_IGNORE_ORPHANS"] @@ -148,10 +134,36 @@ func upCommand(p *projectOptions, backend api.Service) *cobra.Command { flags.BoolVar(&up.attachDependencies, "attach-dependencies", false, "Attach to dependent containers.") flags.BoolVar(&create.quietPull, "quiet-pull", false, "Pull without printing progress information.") flags.StringArrayVar(&up.attach, "attach", []string{}, "Attach to service output.") + flags.BoolVar(&up.wait, "wait", false, "Wait for services to be running|healthy. Implies detached mode.") return upCmd } +func validateFlags(up *upOptions, create *createOptions) error { + if up.exitCodeFrom != "" { + up.cascadeStop = true + } + if up.wait { + if up.attachDependencies || up.cascadeStop || len(up.attach) > 0 { + return fmt.Errorf("--wait cannot be combined with --abort-on-container-exit, --attach or --attach-dependencies") + } + up.Detach = true + } + if create.Build && create.noBuild { + return fmt.Errorf("--build and --no-build are incompatible") + } + if up.Detach && (up.attachDependencies || up.cascadeStop || len(up.attach) > 0) { + return fmt.Errorf("--detach cannot be combined with --abort-on-container-exit, --attach or --attach-dependencies") + } + if create.forceRecreate && create.noRecreate { + return fmt.Errorf("--force-recreate and --no-recreate are incompatible") + } + if create.recreateDeps && create.noRecreate { + return fmt.Errorf("--always-recreate-deps and --no-recreate are incompatible") + } + return nil +} + func runUp(ctx context.Context, backend api.Service, createOptions createOptions, upOptions upOptions, project *types.Project, services []string) error { if len(project.Services) == 0 { return fmt.Errorf("no service selected") @@ -199,6 +211,7 @@ func runUp(ctx context.Context, backend api.Service, createOptions createOptions AttachTo: attachTo, ExitCodeFrom: upOptions.exitCodeFrom, CascadeStop: upOptions.cascadeStop, + Wait: upOptions.wait, }, }) } diff --git a/pkg/api/api.go b/pkg/api/api.go index 9efaa270b..58ed2cc77 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -124,6 +124,8 @@ type StartOptions struct { CascadeStop bool // ExitCodeFrom return exit code from specified service ExitCodeFrom string + // Wait won't return until containers reached the running|healthy state + Wait bool } // RestartOptions group options of the Restart API diff --git a/pkg/compose/convergence.go b/pkg/compose/convergence.go index 29e76a87d..80d143a57 100644 --- a/pkg/compose/convergence.go +++ b/pkg/compose/convergence.go @@ -261,9 +261,11 @@ func getContainerProgressName(container moby.Container) string { return "Container " + getCanonicalContainerName(container) } -func (s *composeService) waitDependencies(ctx context.Context, project *types.Project, service types.ServiceConfig) error { +const ServiceConditionRuningOrHealthy = "running_or_healthy" + +func (s *composeService) waitDependencies(ctx context.Context, project *types.Project, dependencies types.DependsOnConfig) error { eg, _ := errgroup.WithContext(ctx) - for dep, config := range service.DependsOn { + for dep, config := range dependencies { dep, config := dep, config eg.Go(func() error { ticker := time.NewTicker(500 * time.Millisecond) @@ -271,8 +273,16 @@ func (s *composeService) waitDependencies(ctx context.Context, project *types.Pr for { <-ticker.C switch config.Condition { + case ServiceConditionRuningOrHealthy: + healthy, err := s.isServiceHealthy(ctx, project, dep, true) + if err != nil { + return err + } + if healthy { + return nil + } case types.ServiceConditionHealthy: - healthy, err := s.isServiceHealthy(ctx, project, dep) + healthy, err := s.isServiceHealthy(ctx, project, dep, false) if err != nil { return err } @@ -502,7 +512,7 @@ func (s *composeService) connectContainerToNetwork(ctx context.Context, id strin return nil } -func (s *composeService) isServiceHealthy(ctx context.Context, project *types.Project, service string) (bool, error) { +func (s *composeService) isServiceHealthy(ctx context.Context, project *types.Project, service string, fallbackRunning bool) (bool, error) { containers, err := s.getContainers(ctx, project.Name, oneOffExclude, false, service) if err != nil { return false, err @@ -516,6 +526,11 @@ func (s *composeService) isServiceHealthy(ctx context.Context, project *types.Pr if err != nil { return false, err } + if container.Config.Healthcheck == nil && fallbackRunning { + // Container does not define a health check, but we can fall back to "running" state + return container.State != nil && container.State.Status == "running", nil + } + if container.State == nil || container.State.Health == nil { return false, fmt.Errorf("container for service %q has no healthcheck configured", service) } @@ -544,7 +559,7 @@ func (s *composeService) isServiceCompleted(ctx context.Context, project *types. } func (s *composeService) startService(ctx context.Context, project *types.Project, service types.ServiceConfig) error { - err := s.waitDependencies(ctx, project, service) + err := s.waitDependencies(ctx, project, service.DependsOn) if err != nil { return err } diff --git a/pkg/compose/create.go b/pkg/compose/create.go index 04564dac5..d458fc032 100644 --- a/pkg/compose/create.go +++ b/pkg/compose/create.go @@ -493,6 +493,7 @@ func getDeployResources(s types.ServiceConfig) container.Resources { MemorySwap: int64(s.MemSwapLimit), MemorySwappiness: swappiness, MemoryReservation: int64(s.MemReservation), + OomKillDisable: &s.OomKillDisable, CPUCount: s.CPUCount, CPUPeriod: s.CPUPeriod, CPUQuota: s.CPUQuota, @@ -503,6 +504,10 @@ func getDeployResources(s types.ServiceConfig) container.Resources { CpusetCpus: s.CPUSet, } + if s.PidsLimit != 0 { + resources.PidsLimit = &s.PidsLimit + } + setBlkio(s.BlkioConfig, &resources) if s.Deploy != nil { diff --git a/pkg/compose/run.go b/pkg/compose/run.go index fee2e7f40..b9418d7c1 100644 --- a/pkg/compose/run.go +++ b/pkg/compose/run.go @@ -160,7 +160,7 @@ func (s *composeService) prepareRun(ctx context.Context, project *types.Project, return "", err } if !opts.NoDeps { - if err := s.waitDependencies(ctx, project, service); err != nil { + if err := s.waitDependencies(ctx, project, service.DependsOn); err != nil { return "", err } } diff --git a/pkg/compose/start.go b/pkg/compose/start.go index 273fee6ab..f06fd2280 100644 --- a/pkg/compose/start.go +++ b/pkg/compose/start.go @@ -58,11 +58,26 @@ func (s *composeService) start(ctx context.Context, project *types.Project, opti if err != nil { return err } + return s.startService(ctx, project, service) }) if err != nil { return err } + + if options.Wait { + depends := types.DependsOnConfig{} + for _, s := range project.Services { + depends[s.Name] = types.ServiceDependency{ + Condition: ServiceConditionRuningOrHealthy, + } + } + err = s.waitDependencies(ctx, project, depends) + if err != nil { + return err + } + } + return eg.Wait() }