Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 29 additions & 16 deletions cmd/compose/up.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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"]
Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -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,
},
})
}
Expand Down
2 changes: 2 additions & 0 deletions pkg/api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
25 changes: 20 additions & 5 deletions pkg/compose/convergence.go
Original file line number Diff line number Diff line change
Expand Up @@ -261,18 +261,28 @@ 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)
defer ticker.Stop()
for {
<-ticker.C
switch config.Condition {
case ServiceConditionRuningOrHealthy:
healthy, err := s.isServiceHealthy(ctx, project, dep, true)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(just thinking out loud); perhaps we need to have a look at the events API, and see if we can improve it enough so that compose can (more easily) use that. We already have a health_status event, but perhaps that alone is not sufficient, but we could look at enhancing it. (that way compose wouldn't have to poll docker inspect calls)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

would ne a nice addition to the event stream indeed

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
}
Expand Down Expand Up @@ -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
Expand All @@ -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)
}
Expand Down Expand Up @@ -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
}
Expand Down
5 changes: 5 additions & 0 deletions pkg/compose/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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 {
Expand Down
2 changes: 1 addition & 1 deletion pkg/compose/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
Expand Down
15 changes: 15 additions & 0 deletions pkg/compose/start.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}

Expand Down