diff --git a/pkg/container/container_types.go b/pkg/container/container_types.go index 37d293a..372d157 100644 --- a/pkg/container/container_types.go +++ b/pkg/container/container_types.go @@ -54,6 +54,7 @@ type Container interface { Remove() common.Executor Close() common.Executor ReplaceLogWriter(io.Writer, io.Writer) (io.Writer, io.Writer) + GetHealth(ctx context.Context) ContainerHealth } // NewDockerBuildExecutorInput the input for the NewDockerBuildExecutor function @@ -73,3 +74,11 @@ type NewDockerPullExecutorInput struct { Username string Password string } + +type ContainerHealth int + +const ( + ContainerHealthStarting ContainerHealth = iota + ContainerHealthHealthy + ContainerHealthUnHealthy +) diff --git a/pkg/container/docker_run.go b/pkg/container/docker_run.go index 03a0b83..5241c4d 100644 --- a/pkg/container/docker_run.go +++ b/pkg/container/docker_run.go @@ -169,6 +169,30 @@ func (cr *containerReference) Remove() common.Executor { ).IfNot(common.Dryrun) } +func (cr *containerReference) GetHealth(ctx context.Context) ContainerHealth { + resp, err := cr.cli.ContainerInspect(ctx, cr.id) + logger := common.Logger(ctx) + if err != nil { + logger.Errorf("failed to query container health %s", err) + return ContainerHealthUnHealthy + } + if resp.Config == nil || resp.Config.Healthcheck == nil || resp.State == nil || resp.State.Health == nil || len(resp.Config.Healthcheck.Test) == 1 && strings.EqualFold(resp.Config.Healthcheck.Test[0], "NONE") { + logger.Debugf("no container health check defined") + return ContainerHealthHealthy + } + + logger.Infof("container health of %s (%s) is %s", cr.id, resp.Config.Image, resp.State.Health.Status) + switch resp.State.Health.Status { + case "starting": + return ContainerHealthStarting + case "healthy": + return ContainerHealthHealthy + case "unhealthy": + return ContainerHealthUnHealthy + } + return ContainerHealthUnHealthy +} + func (cr *containerReference) ReplaceLogWriter(stdout io.Writer, stderr io.Writer) (io.Writer, io.Writer) { out := cr.input.Stdout err := cr.input.Stderr diff --git a/pkg/container/host_environment.go b/pkg/container/host_environment.go index 8130414..55958ed 100644 --- a/pkg/container/host_environment.go +++ b/pkg/container/host_environment.go @@ -452,6 +452,10 @@ func (e *HostEnvironment) GetRunnerContext(_ context.Context) map[string]interfa } } +func (e *HostEnvironment) GetHealth(ctx context.Context) ContainerHealth { + return ContainerHealthHealthy +} + func (e *HostEnvironment) ReplaceLogWriter(stdout io.Writer, _ io.Writer) (io.Writer, io.Writer) { org := e.StdOut e.StdOut = stdout diff --git a/pkg/runner/run_context.go b/pkg/runner/run_context.go index 925ebc6..2bed0f8 100644 --- a/pkg/runner/run_context.go +++ b/pkg/runner/run_context.go @@ -17,6 +17,7 @@ import ( "runtime" "strconv" "strings" + "time" "github.com/docker/go-connections/nat" "github.com/nektos/act/pkg/common" @@ -420,6 +421,7 @@ func (rc *RunContext) startJobContainer() common.Executor { Mode: 0o666, Body: "", }), + rc.waitForServiceContainers(), )(ctx) } } @@ -518,6 +520,40 @@ func (rc *RunContext) startServiceContainers(_ string) common.Executor { } } +func (rc *RunContext) waitForServiceContainer(c container.ExecutionsEnvironment) common.Executor { + return func(ctx context.Context) error { + sctx, cancel := context.WithTimeout(ctx, time.Minute*5) + defer cancel() + health := container.ContainerHealthStarting + delay := time.Second + for i := 0; ; i++ { + health = c.GetHealth(sctx) + if health != container.ContainerHealthStarting || i > 30 { + break + } + time.Sleep(delay) + delay *= 2 + if delay > 10*time.Second { + delay = 10 * time.Second + } + } + if health == container.ContainerHealthHealthy { + return nil + } + return fmt.Errorf("service container failed to start") + } +} + +func (rc *RunContext) waitForServiceContainers() common.Executor { + return func(ctx context.Context) error { + execs := []common.Executor{} + for _, c := range rc.ServiceContainers { + execs = append(execs, rc.waitForServiceContainer(c)) + } + return common.NewParallelExecutor(len(execs), execs...)(ctx) + } +} + func (rc *RunContext) stopServiceContainers() common.Executor { return func(ctx context.Context) error { execs := []common.Executor{} diff --git a/pkg/runner/runner_test.go b/pkg/runner/runner_test.go index 13e6844..a321793 100644 --- a/pkg/runner/runner_test.go +++ b/pkg/runner/runner_test.go @@ -317,6 +317,7 @@ func TestRunEvent(t *testing.T) { {workdir, "services-empty-image", "push", "", platforms, secrets}, {workdir, "services-host-network", "push", "", platforms, secrets}, {workdir, "services-with-container", "push", "", platforms, secrets}, + {workdir, "mysql-service-container-with-health-check", "push", "", platforms, secrets}, // local remote action overrides {workdir, "local-remote-action-overrides", "push", "", platforms, secrets}, diff --git a/pkg/runner/testdata/mysql-service-container-with-health-check/push.yml b/pkg/runner/testdata/mysql-service-container-with-health-check/push.yml new file mode 100644 index 0000000..04d09dc --- /dev/null +++ b/pkg/runner/testdata/mysql-service-container-with-health-check/push.yml @@ -0,0 +1,19 @@ +name: service-container +on: push +jobs: + service-container-test: + runs-on: ubuntu-latest + container: mysql:8 + services: + maindb: + image: mysql:8 + env: + MYSQL_DATABASE: dbname + MYSQL_USER: dbuser + MYSQL_PASSWORD: dbpass + MYSQL_RANDOM_ROOT_PASSWORD: yes + options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3 + steps: + - run: mysql -u dbuser -D dbname -pdbpass -h maindb -e "create table T(id INT NOT NULL AUTO_INCREMENT, val VARCHAR(255), PRIMARY KEY (id))" + - run: mysql -u dbuser -D dbname -pdbpass -h maindb -e "insert into T(val) values ('test'),('h')" + - run: mysql -u dbuser -D dbname -pdbpass -h maindb -e "select * from T"