Browse Source

introduce up --wait condition

Signed-off-by: Nicolas De Loof <[email protected]>
Nicolas De Loof 4 years ago
parent
commit
72e4519cbf
6 changed files with 72 additions and 22 deletions
  1. 29 16
      cmd/compose/up.go
  2. 2 0
      pkg/api/api.go
  3. 20 5
      pkg/compose/convergence.go
  4. 5 0
      pkg/compose/create.go
  5. 1 1
      pkg/compose/run.go
  6. 15 0
      pkg/compose/start.go

+ 29 - 16
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,
 		},
 	})
 }

+ 2 - 0
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

+ 20 - 5
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
 	}

+ 5 - 0
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 {

+ 1 - 1
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
 		}
 	}

+ 15 - 0
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()
 }