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>
This commit is contained in:
@@ -54,6 +54,7 @@ type Container interface {
|
|||||||
Remove() common.Executor
|
Remove() common.Executor
|
||||||
Close() common.Executor
|
Close() common.Executor
|
||||||
ReplaceLogWriter(io.Writer, io.Writer) (io.Writer, io.Writer)
|
ReplaceLogWriter(io.Writer, io.Writer) (io.Writer, io.Writer)
|
||||||
|
GetHealth(ctx context.Context) ContainerHealth
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewDockerBuildExecutorInput the input for the NewDockerBuildExecutor function
|
// NewDockerBuildExecutorInput the input for the NewDockerBuildExecutor function
|
||||||
@@ -73,3 +74,11 @@ type NewDockerPullExecutorInput struct {
|
|||||||
Username string
|
Username string
|
||||||
Password string
|
Password string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ContainerHealth int
|
||||||
|
|
||||||
|
const (
|
||||||
|
ContainerHealthStarting ContainerHealth = iota
|
||||||
|
ContainerHealthHealthy
|
||||||
|
ContainerHealthUnHealthy
|
||||||
|
)
|
||||||
|
|||||||
@@ -169,6 +169,30 @@ func (cr *containerReference) Remove() common.Executor {
|
|||||||
).IfNot(common.Dryrun)
|
).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) {
|
func (cr *containerReference) ReplaceLogWriter(stdout io.Writer, stderr io.Writer) (io.Writer, io.Writer) {
|
||||||
out := cr.input.Stdout
|
out := cr.input.Stdout
|
||||||
err := cr.input.Stderr
|
err := cr.input.Stderr
|
||||||
|
|||||||
@@ -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) {
|
func (e *HostEnvironment) ReplaceLogWriter(stdout io.Writer, _ io.Writer) (io.Writer, io.Writer) {
|
||||||
org := e.StdOut
|
org := e.StdOut
|
||||||
e.StdOut = stdout
|
e.StdOut = stdout
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import (
|
|||||||
"runtime"
|
"runtime"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/docker/go-connections/nat"
|
"github.com/docker/go-connections/nat"
|
||||||
"github.com/nektos/act/pkg/common"
|
"github.com/nektos/act/pkg/common"
|
||||||
@@ -420,6 +421,7 @@ func (rc *RunContext) startJobContainer() common.Executor {
|
|||||||
Mode: 0o666,
|
Mode: 0o666,
|
||||||
Body: "",
|
Body: "",
|
||||||
}),
|
}),
|
||||||
|
rc.waitForServiceContainers(),
|
||||||
)(ctx)
|
)(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 {
|
func (rc *RunContext) stopServiceContainers() common.Executor {
|
||||||
return func(ctx context.Context) error {
|
return func(ctx context.Context) error {
|
||||||
execs := []common.Executor{}
|
execs := []common.Executor{}
|
||||||
|
|||||||
@@ -317,6 +317,7 @@ func TestRunEvent(t *testing.T) {
|
|||||||
{workdir, "services-empty-image", "push", "", platforms, secrets},
|
{workdir, "services-empty-image", "push", "", platforms, secrets},
|
||||||
{workdir, "services-host-network", "push", "", platforms, secrets},
|
{workdir, "services-host-network", "push", "", platforms, secrets},
|
||||||
{workdir, "services-with-container", "push", "", platforms, secrets},
|
{workdir, "services-with-container", "push", "", platforms, secrets},
|
||||||
|
{workdir, "mysql-service-container-with-health-check", "push", "", platforms, secrets},
|
||||||
|
|
||||||
// local remote action overrides
|
// local remote action overrides
|
||||||
{workdir, "local-remote-action-overrides", "push", "", platforms, secrets},
|
{workdir, "local-remote-action-overrides", "push", "", platforms, secrets},
|
||||||
|
|||||||
19
pkg/runner/testdata/mysql-service-container-with-health-check/push.yml
vendored
Normal file
19
pkg/runner/testdata/mysql-service-container-with-health-check/push.yml
vendored
Normal file
@@ -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"
|
||||||
Reference in New Issue
Block a user