func()

in internal/compose/compose.go [348:411]


func (p *Project) WaitForHealthy(ctx context.Context, opts CommandOptions) error {
	// Read container IDs
	args := p.baseArgs()
	args = append(args, "ps", "-a", "-q")

	var b bytes.Buffer
	if err := p.runDockerComposeCmd(ctx, dockerComposeOptions{args: args, env: opts.Env, stdout: &b}); err != nil {
		return err
	}

	ctx, stop := context.WithTimeout(ctx, waitForHealthyTimeout)
	defer stop()

	containerIDs := strings.Fields(b.String())
	for {
		// NOTE: healthy must be reinitialized at each iteration
		healthy := true

		logger.Debugf("Wait for healthy containers: %s", strings.Join(containerIDs, ","))
		descriptions, err := docker.InspectContainers(containerIDs...)
		if err != nil {
			return err
		}

		for _, d := range descriptions {
			switch {
			// No healthcheck defined for service
			case d.State.Status == "running" && d.State.Health == nil:
				logger.Debugf("Container %s (%s) status: %s (no health status)", d.Config.Labels.ComposeService, d.ID, d.State.Status)
				// Service is up and running and it's healthy
			case d.State.Status == "running" && d.State.Health.Status == "healthy":
				logger.Debugf("Container %s (%s) status: %s (health: %s)", d.Config.Labels.ComposeService, d.ID, d.State.Status, d.State.Health.Status)
				// Container started and finished with exit code 0
			case d.State.Status == "exited" && d.State.ExitCode == 0:
				logger.Debugf("Container %s (%s) status: %s (exit code: %d)", d.Config.Labels.ComposeService, d.ID, d.State.Status, d.State.ExitCode)
				// Container exited with code > 0
			case d.State.Status == "exited" && d.State.ExitCode > 0:
				logger.Debugf("Container %s (%s) status: %s (exit code: %d)", d.Config.Labels.ComposeService, d.ID, d.State.Status, d.State.ExitCode)
				return fmt.Errorf("container (ID: %s) exited with code %d", d.ID, d.State.ExitCode)
			// Any different status is considered unhealthy
			default:
				logger.Debugf("Container %s (%s) status: unhealthy", d.Config.Labels.ComposeService, d.ID)
				healthy = false
			}
		}

		// end loop before timeout if healthy
		if healthy {
			break
		}

		select {
		case <-ctx.Done():
			if errors.Is(ctx.Err(), context.DeadlineExceeded) {
				return errors.New("timeout waiting for healthy container")
			}
			return ctx.Err()
		// NOTE: using after does not guarantee interval but it's ok for this use case
		case <-time.After(waitForHealthyInterval):
		}
	}

	return nil
}