Skip to content

Commit 2ad5ff7

Browse files
fix: add missing service container health check (#2354)
* fix: Implement missing health ceck for Services * Add test case * linter doesn't support min builtin and fix check --------- Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
1 parent 1ac4b60 commit 2ad5ff7

File tree

6 files changed

+93
-0
lines changed

6 files changed

+93
-0
lines changed

pkg/container/container_types.go

+9
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ type Container interface {
5454
Remove() common.Executor
5555
Close() common.Executor
5656
ReplaceLogWriter(io.Writer, io.Writer) (io.Writer, io.Writer)
57+
GetHealth(ctx context.Context) ContainerHealth
5758
}
5859

5960
// NewDockerBuildExecutorInput the input for the NewDockerBuildExecutor function
@@ -73,3 +74,11 @@ type NewDockerPullExecutorInput struct {
7374
Username string
7475
Password string
7576
}
77+
78+
type ContainerHealth int
79+
80+
const (
81+
ContainerHealthStarting ContainerHealth = iota
82+
ContainerHealthHealthy
83+
ContainerHealthUnHealthy
84+
)

pkg/container/docker_run.go

+24
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,30 @@ func (cr *containerReference) Remove() common.Executor {
169169
).IfNot(common.Dryrun)
170170
}
171171

172+
func (cr *containerReference) GetHealth(ctx context.Context) ContainerHealth {
173+
resp, err := cr.cli.ContainerInspect(ctx, cr.id)
174+
logger := common.Logger(ctx)
175+
if err != nil {
176+
logger.Errorf("failed to query container health %s", err)
177+
return ContainerHealthUnHealthy
178+
}
179+
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") {
180+
logger.Debugf("no container health check defined")
181+
return ContainerHealthHealthy
182+
}
183+
184+
logger.Infof("container health of %s (%s) is %s", cr.id, resp.Config.Image, resp.State.Health.Status)
185+
switch resp.State.Health.Status {
186+
case "starting":
187+
return ContainerHealthStarting
188+
case "healthy":
189+
return ContainerHealthHealthy
190+
case "unhealthy":
191+
return ContainerHealthUnHealthy
192+
}
193+
return ContainerHealthUnHealthy
194+
}
195+
172196
func (cr *containerReference) ReplaceLogWriter(stdout io.Writer, stderr io.Writer) (io.Writer, io.Writer) {
173197
out := cr.input.Stdout
174198
err := cr.input.Stderr

pkg/container/host_environment.go

+4
Original file line numberDiff line numberDiff line change
@@ -452,6 +452,10 @@ func (e *HostEnvironment) GetRunnerContext(_ context.Context) map[string]interfa
452452
}
453453
}
454454

455+
func (e *HostEnvironment) GetHealth(ctx context.Context) ContainerHealth {
456+
return ContainerHealthHealthy
457+
}
458+
455459
func (e *HostEnvironment) ReplaceLogWriter(stdout io.Writer, _ io.Writer) (io.Writer, io.Writer) {
456460
org := e.StdOut
457461
e.StdOut = stdout

pkg/runner/run_context.go

+36
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import (
1717
"runtime"
1818
"strconv"
1919
"strings"
20+
"time"
2021

2122
"github.com/docker/go-connections/nat"
2223
"github.com/nektos/act/pkg/common"
@@ -420,6 +421,7 @@ func (rc *RunContext) startJobContainer() common.Executor {
420421
Mode: 0o666,
421422
Body: "",
422423
}),
424+
rc.waitForServiceContainers(),
423425
)(ctx)
424426
}
425427
}
@@ -518,6 +520,40 @@ func (rc *RunContext) startServiceContainers(_ string) common.Executor {
518520
}
519521
}
520522

523+
func (rc *RunContext) waitForServiceContainer(c container.ExecutionsEnvironment) common.Executor {
524+
return func(ctx context.Context) error {
525+
sctx, cancel := context.WithTimeout(ctx, time.Minute*5)
526+
defer cancel()
527+
health := container.ContainerHealthStarting
528+
delay := time.Second
529+
for i := 0; ; i++ {
530+
health = c.GetHealth(sctx)
531+
if health != container.ContainerHealthStarting || i > 30 {
532+
break
533+
}
534+
time.Sleep(delay)
535+
delay *= 2
536+
if delay > 10*time.Second {
537+
delay = 10 * time.Second
538+
}
539+
}
540+
if health == container.ContainerHealthHealthy {
541+
return nil
542+
}
543+
return fmt.Errorf("service container failed to start")
544+
}
545+
}
546+
547+
func (rc *RunContext) waitForServiceContainers() common.Executor {
548+
return func(ctx context.Context) error {
549+
execs := []common.Executor{}
550+
for _, c := range rc.ServiceContainers {
551+
execs = append(execs, rc.waitForServiceContainer(c))
552+
}
553+
return common.NewParallelExecutor(len(execs), execs...)(ctx)
554+
}
555+
}
556+
521557
func (rc *RunContext) stopServiceContainers() common.Executor {
522558
return func(ctx context.Context) error {
523559
execs := []common.Executor{}

pkg/runner/runner_test.go

+1
Original file line numberDiff line numberDiff line change
@@ -317,6 +317,7 @@ func TestRunEvent(t *testing.T) {
317317
{workdir, "services-empty-image", "push", "", platforms, secrets},
318318
{workdir, "services-host-network", "push", "", platforms, secrets},
319319
{workdir, "services-with-container", "push", "", platforms, secrets},
320+
{workdir, "mysql-service-container-with-health-check", "push", "", platforms, secrets},
320321

321322
// local remote action overrides
322323
{workdir, "local-remote-action-overrides", "push", "", platforms, secrets},
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
name: service-container
2+
on: push
3+
jobs:
4+
service-container-test:
5+
runs-on: ubuntu-latest
6+
container: mysql:8
7+
services:
8+
maindb:
9+
image: mysql:8
10+
env:
11+
MYSQL_DATABASE: dbname
12+
MYSQL_USER: dbuser
13+
MYSQL_PASSWORD: dbpass
14+
MYSQL_RANDOM_ROOT_PASSWORD: yes
15+
options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3
16+
steps:
17+
- 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))"
18+
- run: mysql -u dbuser -D dbname -pdbpass -h maindb -e "insert into T(val) values ('test'),('h')"
19+
- run: mysql -u dbuser -D dbname -pdbpass -h maindb -e "select * from T"

0 commit comments

Comments
 (0)