introduce up --wait condition

Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
This commit is contained in:
Nicolas De Loof 2021-10-11 17:52:31 +02:00 committed by Nicolas De loof
parent 9e6f51d262
commit 72e4519cbf
6 changed files with 72 additions and 22 deletions

View File

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

View File

@ -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

View File

@ -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
}

View File

@ -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 {

View File

@ -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
}
}

View File

@ -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()
}