From 251c52664ad0c9bc72004ca901c18b995fff8fcc Mon Sep 17 00:00:00 2001 From: Nicolas De Loof Date: Thu, 19 Nov 2020 10:10:48 +0100 Subject: [PATCH] Implement service_healthy dependency condition Signed-off-by: Nicolas De Loof --- local/compose.go | 6 ++--- local/convergence.go | 55 ++++++++++++++++++++++++++++++++++++++++++++ local/convert.go | 53 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 111 insertions(+), 3 deletions(-) diff --git a/local/compose.go b/local/compose.go index f95cd829a..1a5fb7014 100644 --- a/local/compose.go +++ b/local/compose.go @@ -389,10 +389,10 @@ func getContainerCreateOptions(p *types.Project, s types.ServiceConfig, number i MacAddress: s.MacAddress, Labels: labels, StopSignal: s.StopSignal, - // Env: s.Environment, FIXME conversion - // Healthcheck: s.HealthCheck, FIXME conversion + Env: toMobyEnv(s.Environment), + Healthcheck: toMobyHealthCheck(s.HealthCheck), // Volumes: // FIXME unclear to me the overlap with HostConfig.Mounts - // StopTimeout: s.StopGracePeriod FIXME conversion + StopTimeout: toSeconds(s.StopGracePeriod), } mountOptions := buildContainerMountOptions(p, s, inherit) diff --git a/local/convergence.go b/local/convergence.go index b9b356028..bc5b89fc5 100644 --- a/local/convergence.go +++ b/local/convergence.go @@ -22,6 +22,7 @@ import ( "context" "fmt" "strconv" + "time" "github.com/compose-spec/compose-go/types" moby "github.com/docker/docker/api/types" @@ -39,6 +40,8 @@ const ( ) func (s *local) ensureService(ctx context.Context, project *types.Project, service types.ServiceConfig) error { + s.waitDependencies(ctx, project, service) + actual, err := s.containerService.apiClient.ContainerList(ctx, moby.ContainerListOptions{ Filters: filters.NewArgs( filters.Arg("label", fmt.Sprintf("%s=%s", projectLabel, project.Name)), @@ -108,6 +111,28 @@ func (s *local) ensureService(ctx context.Context, project *types.Project, servi return eg.Wait() } +func (s *local) waitDependencies(ctx context.Context, project *types.Project, service types.ServiceConfig) error { + eg, ctx := errgroup.WithContext(ctx) + for dep, config := range service.DependsOn { + switch config.Condition { + case "service_healthy": + eg.Go(func() error { + for range time.Tick(500 * time.Millisecond) { + healthy, err := s.isServiceHealthy(ctx, project, dep) + if err != nil { + return err + } + if healthy { + return nil + } + } + return nil + }) + } + } + return eg.Wait() +} + func nextContainerNumber(containers []moby.Container) (int, error) { max := 0 for _, c := range containers { @@ -257,3 +282,33 @@ func (s *local) connectContainerToNetwork(ctx context.Context, id string, servic } return nil } + +func (s *local) isServiceHealthy(ctx context.Context, project *types.Project, service string) (bool, error) { + containers, err := s.containerService.apiClient.ContainerList(ctx, moby.ContainerListOptions{ + Filters: filters.NewArgs( + filters.Arg("label", fmt.Sprintf("%s=%s", projectLabel, project.Name)), + filters.Arg("label", fmt.Sprintf("%s=%s", serviceLabel, service)), + ), + }) + if err != nil { + return false, err + } + + for _, c := range containers { + container, err := s.containerService.apiClient.ContainerInspect(ctx, c.ID) + if err != nil { + return false, err + } + if container.State == nil || container.State.Health == nil { + return false, fmt.Errorf("container for service %q has no healthcheck configured", service) + } + switch container.State.Health.Status { + case "starting": + return false, nil + case "unhealthy": + return false, nil + } + } + return true, nil + +} diff --git a/local/convert.go b/local/convert.go index 05931a366..941ec95d3 100644 --- a/local/convert.go +++ b/local/convert.go @@ -23,7 +23,9 @@ import ( "sort" "strconv" "strings" + "time" + compose "github.com/compose-spec/compose-go/types" "github.com/docker/docker/api/types" "github.com/docker/docker/api/types/container" "github.com/docker/go-connections/nat" @@ -93,6 +95,57 @@ func toPorts(ports []types.Port) []containers.Port { return result } +func toMobyEnv(environment compose.MappingWithEquals) []string { + var env []string + for k, v := range environment { + if v == nil { + env = append(env, k) + } else { + env = append(env, fmt.Sprintf("%s=%s", k, v)) + } + } + return env +} + +func toMobyHealthCheck(check *compose.HealthCheckConfig) *container.HealthConfig { + if check == nil { + return nil + } + var ( + interval time.Duration + timeout time.Duration + period time.Duration + retries int + ) + if check.Interval != nil { + interval = time.Duration(*check.Interval) + } + if check.Timeout != nil { + timeout = time.Duration(*check.Timeout) + } + if check.StartPeriod != nil { + period = time.Duration(*check.StartPeriod) + } + if check.Retries != nil { + retries = int(*check.Retries) + } + return &container.HealthConfig{ + Test: check.Test, + Interval: interval, + Timeout: timeout, + StartPeriod: period, + Retries: retries, + } +} + +func toSeconds(d *compose.Duration) *int { + if d == nil { + return nil + } + s := int(time.Duration(*d).Seconds()) + return &s +} + func fromPorts(ports []containers.Port) (map[nat.Port]struct{}, map[nat.Port][]nat.PortBinding, error) { var ( exposedPorts = make(map[nat.Port]struct{}, len(ports))