mirror of
https://gitea.com/gitea/act_runner
synced 2026-09-21 19:37:07 +02:00
fix: stop the job's docker socket from becoming a directory (#1215)
Fixes https://gitea.com/gitea/runner/issues/1213 Fix the DooD regression that mounts `/var/run/docker.sock` as a directory. Keep the Docker proxy available through job and post steps. Clean stale resources before opening it, then remove containers before their networks and volumes during teardown. Use a unique filesystem probe and preserve socket ownership. Fall back to direct access when proxying is unsupported. Preserve exec output and clean up active streams and failed starts. Add a real Docker job test for mounted socket access, post steps and resource cleanup. --------- Co-authored-by: silverwind <me@silverwind.io> Reviewed-on: https://gitea.com/gitea/runner/pulls/1215 Reviewed-by: silverwind <2021+silverwind@noreply.gitea.com> Co-authored-by: Zettat123 <zettat123@gmail.com>
This commit is contained in:
committed by
bircni
co-authored by
silverwind
parent
ba4d3c5b4f
commit
ff9965e940
@@ -0,0 +1,102 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
//go:build !(WITHOUT_DOCKER || !(linux || darwin || windows || netbsd))
|
||||
|
||||
package runner
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"gitea.com/gitea/runner/act/common"
|
||||
"gitea.com/gitea/runner/act/container"
|
||||
|
||||
"gitea.dev/actionslib/pkg/model"
|
||||
cerrdefs "github.com/containerd/errdefs"
|
||||
"github.com/moby/moby/client"
|
||||
"github.com/sirupsen/logrus/hooks/test"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestDockerProxyMountedJob(t *testing.T) {
|
||||
mode := os.Getenv("ACT_TEST_DOCKER_PROXY")
|
||||
if mode == "" {
|
||||
t.Skip("set ACT_TEST_DOCKER_PROXY=proxy or direct to verify mounted Docker access")
|
||||
}
|
||||
require.Contains(t, []string{"proxy", "direct"}, mode)
|
||||
ctx, cancel := context.WithTimeout(t.Context(), 2*time.Minute)
|
||||
defer cancel()
|
||||
dockerClient, err := container.GetDockerClient(ctx)
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() { assert.NoError(t, dockerClient.Close()) })
|
||||
require.True(t, strings.HasPrefix(dockerClient.DaemonHost(), "unix://"), "mounted Docker coverage requires a Unix daemon socket")
|
||||
|
||||
fixtureDir, err := filepath.Abs("testdata/docker-proxy")
|
||||
require.NoError(t, err)
|
||||
resourceName := "gitea-proxy-test-" + strings.ToLower(rand.Text())
|
||||
runner, err := New(&Config{
|
||||
Workdir: fixtureDir,
|
||||
ActionCacheDir: t.TempDir(),
|
||||
EventName: "push",
|
||||
PlatformPicker: mapPlatformPicker(platforms),
|
||||
ContainerNamePrefix: resourceName,
|
||||
ContainerDaemonSocket: dockerClient.DaemonHost(),
|
||||
ContainerMaxLifetime: 2 * time.Minute,
|
||||
Env: map[string]string{"PROXY_TEST_RESOURCE": resourceName, "PROXY_TEST_MODE": mode},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
planner, err := model.NewWorkflowPlanner(filepath.Join(fixtureDir, "push.yml"), true)
|
||||
require.NoError(t, err)
|
||||
plan, err := planner.PlanEvent("push")
|
||||
require.NoError(t, err)
|
||||
runContext, err := runner.newRunContext(ctx, plan.Stages[0].Runs[0], nil)
|
||||
require.NoError(t, err)
|
||||
jobName := runContext.jobContainerName()
|
||||
jobNetwork, _ := runContext.networkNameForGitea()
|
||||
t.Cleanup(func() {
|
||||
cleanCtx, cleanCancel := context.WithTimeout(context.WithoutCancel(ctx), 30*time.Second)
|
||||
defer cleanCancel()
|
||||
if _, err := dockerClient.ContainerRemove(cleanCtx, jobName, client.ContainerRemoveOptions{Force: true, RemoveVolumes: true}); !cerrdefs.IsNotFound(err) {
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
for _, name := range []string{resourceName, jobNetwork} {
|
||||
if _, err := dockerClient.NetworkRemove(cleanCtx, name, client.NetworkRemoveOptions{}); !cerrdefs.IsNotFound(err) {
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
}
|
||||
for _, name := range []string{resourceName, jobName, jobName + "-env"} {
|
||||
if _, err := dockerClient.VolumeRemove(cleanCtx, name, client.VolumeRemoveOptions{}); !cerrdefs.IsNotFound(err) {
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
hook := &test.Hook{}
|
||||
require.NoError(t, runner.NewPlanExecutor(plan)(common.WithLoggerHook(ctx, hook)))
|
||||
var messages []string
|
||||
for _, entry := range hook.AllEntries() {
|
||||
messages = append(messages, strings.TrimSpace(entry.Message))
|
||||
}
|
||||
require.Contains(t, messages, "docker proxy post verified")
|
||||
_, err = dockerClient.ContainerInspect(ctx, jobName, client.ContainerInspectOptions{})
|
||||
assert.True(t, cerrdefs.IsNotFound(err), "job container survived cleanup: %v", err)
|
||||
_, err = dockerClient.NetworkInspect(ctx, resourceName, client.NetworkInspectOptions{})
|
||||
if mode == "proxy" {
|
||||
assert.True(t, cerrdefs.IsNotFound(err), "labelled network survived cleanup: %v", err)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
_, err = dockerClient.VolumeInspect(ctx, resourceName, client.VolumeInspectOptions{})
|
||||
if mode == "proxy" {
|
||||
assert.True(t, cerrdefs.IsNotFound(err), "labelled volume survived cleanup: %v", err)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
}
|
||||
@@ -263,18 +263,19 @@ func newJobExecutor(info jobInfo, sf stepFactory, rc *RunContext) common.Executo
|
||||
return err
|
||||
})
|
||||
|
||||
stepsExecutor := newStepsExecutor(rc, preSteps, steps)
|
||||
|
||||
return common.NewPipelineExecutor(info.startContainer(), stepsExecutor.
|
||||
Finally(func(ctx context.Context) error {
|
||||
return common.Executor(func(ctx context.Context) error {
|
||||
if err := info.startContainer()(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
return newStepsExecutor(rc, preSteps, steps).Finally(func(ctx context.Context) error {
|
||||
// Record an interrupt (backstop for interrupts that land outside the main
|
||||
// step loop) so the post steps observe the cancelled/failed job status.
|
||||
rc.markInterrupted(ctx.Err())
|
||||
postCtx, cancel := postStepsContext(ctx)
|
||||
defer cancel()
|
||||
return postExecutor(postCtx)
|
||||
}).
|
||||
Finally(info.closeContainer()))
|
||||
})(ctx)
|
||||
}).Finally(info.closeContainer())
|
||||
}
|
||||
|
||||
// postStepsContext derives the context used to run the job's post/cleanup steps from the
|
||||
|
||||
@@ -300,6 +300,8 @@ func TestNewJobExecutor(t *testing.T) {
|
||||
result string
|
||||
hasError bool
|
||||
output string
|
||||
startError error
|
||||
cancelOnStart bool
|
||||
}{
|
||||
{
|
||||
name: "zeroSteps",
|
||||
@@ -435,6 +437,21 @@ func TestNewJobExecutor(t *testing.T) {
|
||||
result: "failure",
|
||||
output: "${{ 'test' != test }}",
|
||||
},
|
||||
{
|
||||
name: "start failure",
|
||||
steps: []*model.Step{{ID: "1"}},
|
||||
executedSteps: []string{"startContainer", "closeContainer"},
|
||||
startError: errors.New("start failed"),
|
||||
},
|
||||
{
|
||||
name: "cancelled at startup boundary",
|
||||
steps: []*model.Step{{ID: "1"}},
|
||||
preSteps: []bool{false},
|
||||
postSteps: []bool{true},
|
||||
executedSteps: []string{"startContainer", "step1", "post1", "interpolateOutputs", "stopContainer", "closeContainer"},
|
||||
result: "cancelled",
|
||||
cancelOnStart: true,
|
||||
},
|
||||
}
|
||||
|
||||
contains := func(needle string, haystack []string) bool {
|
||||
@@ -445,7 +462,8 @@ func TestNewJobExecutor(t *testing.T) {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
fmt.Printf("::group::%s\n", tt.name) //nolint:forbidigo // pre-existing issue from nektos/act
|
||||
|
||||
ctx := common.WithJobErrorContainer(context.Background())
|
||||
ctx, cancel := context.WithCancel(common.WithJobErrorContainer(context.Background()))
|
||||
defer cancel()
|
||||
jim := &jobInfoMock{}
|
||||
sfm := &stepFactoryMock{}
|
||||
rc := &RunContext{
|
||||
@@ -470,9 +488,12 @@ func TestNewJobExecutor(t *testing.T) {
|
||||
jim.On("steps").Return(tt.steps)
|
||||
|
||||
if len(tt.steps) > 0 {
|
||||
jim.On("startContainer").Return(func(ctx context.Context) error {
|
||||
jim.On("startContainer").Return(func(_ context.Context) error {
|
||||
executorOrder = append(executorOrder, "startContainer")
|
||||
return nil
|
||||
if tt.cancelOnStart {
|
||||
cancel()
|
||||
}
|
||||
return tt.startError
|
||||
})
|
||||
}
|
||||
|
||||
@@ -506,7 +527,7 @@ func TestNewJobExecutor(t *testing.T) {
|
||||
defer sm.AssertExpectations(t)
|
||||
}
|
||||
|
||||
if len(tt.steps) > 0 {
|
||||
if len(tt.steps) > 0 && tt.startError == nil {
|
||||
jim.On("matrix").Return(map[string]any{})
|
||||
|
||||
jim.On("interpolateOutputs").Return(func(ctx context.Context) error {
|
||||
@@ -520,12 +541,17 @@ func TestNewJobExecutor(t *testing.T) {
|
||||
if contains("stopContainer", tt.executedSteps) {
|
||||
jim.On("stopContainer").Return(func(ctx context.Context) error {
|
||||
executorOrder = append(executorOrder, "stopContainer")
|
||||
require.NoError(t, ctx.Err())
|
||||
_, bounded := ctx.Deadline()
|
||||
require.True(t, bounded)
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
jim.On("result", tt.result)
|
||||
}
|
||||
|
||||
if len(tt.steps) > 0 {
|
||||
jim.On("closeContainer").Return(func(ctx context.Context) error {
|
||||
executorOrder = append(executorOrder, "closeContainer")
|
||||
return nil
|
||||
@@ -534,7 +560,14 @@ func TestNewJobExecutor(t *testing.T) {
|
||||
|
||||
executor := newJobExecutor(jim, sfm, rc)
|
||||
err := executor(ctx)
|
||||
assert.NoError(t, err) //nolint:testifylint // pre-existing issue from nektos/act
|
||||
switch {
|
||||
case tt.startError != nil:
|
||||
require.ErrorIs(t, err, tt.startError)
|
||||
case tt.cancelOnStart:
|
||||
require.ErrorIs(t, err, context.Canceled)
|
||||
default:
|
||||
require.NoError(t, err)
|
||||
}
|
||||
assert.Empty(t, rc.Run.Job().Outputs["bad"])
|
||||
assert.Equal(t, tt.executedSteps, executorOrder)
|
||||
|
||||
|
||||
+55
-64
@@ -270,20 +270,10 @@ func (rc *RunContext) jobDockerSocket() string {
|
||||
}
|
||||
|
||||
func (rc *RunContext) startDockerProxy(ctx context.Context) {
|
||||
daemonSocket := rc.containerDaemonSocket()
|
||||
if daemonSocket == "-" || strings.HasPrefix(strings.ToLower(daemonSocket), "npipe://") {
|
||||
if !filepath.IsAbs(strings.TrimPrefix(rc.containerDaemonSocket(), "unix://")) || common.Dryrun(ctx) {
|
||||
return
|
||||
}
|
||||
dir := container.DockerProxyDir(ctx)
|
||||
if dir == "" {
|
||||
return
|
||||
}
|
||||
proxy, err := container.StartDockerProxy(getDockerDaemonSocketMountPath(daemonSocket), dir, rc.jobContainerName())
|
||||
if err != nil {
|
||||
common.Logger(ctx).Warnf("docker proxy not started, the job gets the daemon socket directly: %v", err)
|
||||
return
|
||||
}
|
||||
rc.dockerProxy = proxy
|
||||
rc.dockerProxy = container.NewDockerProxy(ctx, rc.jobContainerName())
|
||||
}
|
||||
|
||||
// toolCache returns the tool cache path the job sees, relocatable through RUNNER_TOOL_CACHE.
|
||||
@@ -483,23 +473,17 @@ func (rc *RunContext) startJobContainer() common.Executor {
|
||||
// For gitea, to support --volumes-from <container_name_or_id> in options.
|
||||
// We need to set the container name to the environment variable.
|
||||
rc.Env["JOB_CONTAINER_NAME"] = name
|
||||
rc.startDockerProxy(ctx)
|
||||
|
||||
envList := make([]string, 0)
|
||||
|
||||
envList = append(envList, rc.runnerEnv(ctx)...)
|
||||
envList = append(envList, fmt.Sprintf("%s=%s", "LANG", "C.UTF-8")) // Use same locale as GitHub Actions
|
||||
|
||||
ext := container.LinuxContainerEnvironmentExtensions{}
|
||||
binds, mounts, err := rc.GetBindsAndMounts()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// specify the network to which the container will connect when `docker create` stage. (like execute command line: docker create --network <networkName> <image>)
|
||||
// if using service containers, will create a new network for the containers.
|
||||
// and it will be removed after at last.
|
||||
networkName, createAndDeleteNetwork := rc.networkNameForGitea()
|
||||
rc.cleanUpJobContainer = rc.cleanupJobResources(networkName, createAndDeleteNetwork)
|
||||
|
||||
// add service containers
|
||||
for serviceID, spec := range rc.Run.Job().Services {
|
||||
@@ -590,8 +574,6 @@ func (rc *RunContext) startJobContainer() common.Executor {
|
||||
rc.serviceContainers = append(rc.serviceContainers, &serviceContainer{name: serviceID, image: serviceImage, container: c})
|
||||
}
|
||||
|
||||
rc.cleanUpJobContainer = rc.cleanupJobResources(networkName, createAndDeleteNetwork)
|
||||
|
||||
// For Gitea, `jobContainerNetwork` should be the same as `networkName`
|
||||
jobContainerNetwork := networkName
|
||||
|
||||
@@ -599,7 +581,8 @@ func (rc *RunContext) startJobContainer() common.Executor {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
rc.JobContainer = newContainer(&container.NewContainerInput{
|
||||
ext := container.LinuxContainerEnvironmentExtensions{}
|
||||
containerInput := &container.NewContainerInput{
|
||||
Cmd: nil,
|
||||
Entrypoint: []string{"/bin/sleep", fmt.Sprint(rc.Config.ContainerMaxLifetime.Round(time.Second).Seconds())},
|
||||
WorkingDir: ext.ToContainerPath(rc.Config.Workdir),
|
||||
@@ -608,10 +591,8 @@ func (rc *RunContext) startJobContainer() common.Executor {
|
||||
Password: password,
|
||||
Name: name,
|
||||
Env: envList,
|
||||
Mounts: mounts,
|
||||
NetworkMode: jobContainerNetwork,
|
||||
NetworkAliases: []string{rc.Name},
|
||||
Binds: binds,
|
||||
Stdout: logWriter,
|
||||
Stderr: logWriter,
|
||||
Privileged: rc.Config.Privileged,
|
||||
@@ -620,20 +601,29 @@ func (rc *RunContext) startJobContainer() common.Executor {
|
||||
RunnerOptions: rc.Config.ContainerOptions,
|
||||
WorkflowOptions: workflowOptions,
|
||||
AutoRemove: true,
|
||||
ValidVolumes: rc.validVolumes(),
|
||||
AllocatePTY: rc.Config.AllocatePTY,
|
||||
})
|
||||
}
|
||||
rc.JobContainer = newContainer(containerInput)
|
||||
if rc.JobContainer == nil {
|
||||
return errors.New("failed to create job container")
|
||||
}
|
||||
defer printStartJobContainerGroup(ctx, image, name, networkName)()
|
||||
if err := common.NewPipelineExecutor(
|
||||
rc.stopJobContainer(),
|
||||
rc.pullServicesImages(rc.Config.ForcePull),
|
||||
rc.JobContainer.Pull(rc.Config.ForcePull),
|
||||
).Finally(rc.closeContainer())(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
rc.startDockerProxy(ctx)
|
||||
if containerInput.Binds, containerInput.Mounts, err = rc.GetBindsAndMounts(); err != nil {
|
||||
return err
|
||||
}
|
||||
containerInput.ValidVolumes = rc.validVolumes()
|
||||
|
||||
rc.jobNetworkName = networkName
|
||||
|
||||
defer printStartJobContainerGroup(ctx, image, name, networkName)()
|
||||
return common.NewPipelineExecutor(
|
||||
rc.pullServicesImages(rc.Config.ForcePull),
|
||||
rc.JobContainer.Pull(rc.Config.ForcePull),
|
||||
rc.stopJobContainer(),
|
||||
container.NewDockerNetworkCreateExecutor(networkName, rc.Config.ContainerNetworkCreateOptions).
|
||||
IfBool(createAndDeleteNetwork),
|
||||
rc.startServiceContainers(),
|
||||
@@ -663,47 +653,46 @@ func (rc *RunContext) commandLogWriter(ctx context.Context) io.Writer {
|
||||
})
|
||||
}
|
||||
|
||||
// cleanupJobResources removes everything the job created, continuing past failures.
|
||||
// Only job container and volume errors are returned, the rest are logged.
|
||||
func (rc *RunContext) cleanupJobResources(networkName string, createAndDeleteNetwork bool) common.Executor {
|
||||
return func(ctx context.Context) error {
|
||||
logger := common.Logger(ctx)
|
||||
removeJobContainer := rc.JobContainer != nil
|
||||
|
||||
var errs []error
|
||||
if removeJobContainer {
|
||||
errs := []error{rc.closeDockerProxy(ctx)}
|
||||
if rc.JobContainer != nil {
|
||||
errs = append(errs, rc.JobContainer.Remove()(ctx))
|
||||
}
|
||||
if len(rc.serviceContainers) > 0 {
|
||||
logger.Infof("Cleaning up services for job %s", rc.JobName)
|
||||
if err := rc.stopServiceContainers()(ctx); err != nil {
|
||||
logger.Errorf("Error while cleaning services: %v", err)
|
||||
}
|
||||
errs = append(errs, rc.stopServiceContainers()(ctx))
|
||||
}
|
||||
if rc.dockerProxy != nil {
|
||||
if err := rc.dockerProxy.Close(ctx); err != nil {
|
||||
logger.Errorf("Error while removing what the job created: %v", err)
|
||||
}
|
||||
rc.dockerProxy = nil
|
||||
if !common.Dryrun(ctx) {
|
||||
errs = append(errs, container.RemoveDockerJobResources(ctx, rc.jobContainerName()))
|
||||
}
|
||||
if removeJobContainer {
|
||||
// after the containers using them, services can hold these via `--volumes-from`
|
||||
if rc.JobContainer != nil {
|
||||
name := rc.jobContainerName()
|
||||
errs = append(errs,
|
||||
container.NewDockerVolumeRemoveExecutor(name, false)(ctx),
|
||||
container.NewDockerVolumeRemoveExecutor(name+"-env", false)(ctx))
|
||||
}
|
||||
if createAndDeleteNetwork {
|
||||
// last, once every container has detached
|
||||
logger.Infof("Cleaning up network for job %s, and network name is: %s", rc.JobName, networkName)
|
||||
if err := container.NewDockerNetworkRemoveExecutor(networkName)(ctx); err != nil {
|
||||
logger.Errorf("Error while cleaning network: %v", err)
|
||||
}
|
||||
errs = append(errs, container.NewDockerNetworkRemoveExecutor(networkName)(ctx))
|
||||
}
|
||||
return errors.Join(errs...)
|
||||
}
|
||||
}
|
||||
|
||||
func (rc *RunContext) closeDockerProxy(ctx context.Context) error {
|
||||
if rc.dockerProxy == nil {
|
||||
return nil
|
||||
}
|
||||
err := rc.dockerProxy.Close(ctx)
|
||||
rc.dockerProxy = nil
|
||||
if err != nil {
|
||||
return fmt.Errorf("close docker proxy: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (rc *RunContext) ApplyExtraPath(ctx context.Context, env *map[string]string) {
|
||||
if len(rc.ExtraPath) > 0 {
|
||||
path := rc.JobContainer.GetPathVariableName()
|
||||
@@ -763,7 +752,6 @@ func (rc *RunContext) UpdateExtraPath(ctx context.Context, githubEnvPath string)
|
||||
return nil
|
||||
}
|
||||
|
||||
// stopJobContainer removes the job container (if it exists) and its volume (if it exists)
|
||||
func (rc *RunContext) stopJobContainer() common.Executor {
|
||||
return func(ctx context.Context) error {
|
||||
if rc.cleanUpJobContainer != nil {
|
||||
@@ -800,10 +788,17 @@ func (rc *RunContext) startServiceContainers() common.Executor {
|
||||
func (rc *RunContext) stopServiceContainers() common.Executor {
|
||||
return func(ctx context.Context) error {
|
||||
execs := []common.Executor{}
|
||||
for _, svc := range rc.serviceContainers {
|
||||
execs = append(execs, svc.container.Remove().Finally(svc.container.Close()))
|
||||
errs := make([]error, len(rc.serviceContainers))
|
||||
for index, svc := range rc.serviceContainers {
|
||||
execs = append(execs, func(ctx context.Context) error {
|
||||
if err := errors.Join(svc.container.Remove()(ctx), svc.container.Close()(ctx)); err != nil {
|
||||
errs[index] = fmt.Errorf("clean service %s: %w", svc.name, err)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
return common.NewParallelExecutor(len(execs), execs...)(ctx)
|
||||
errs = append(errs, common.NewParallelExecutor(len(execs), execs...)(ctx))
|
||||
return errors.Join(errs...)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1033,17 +1028,13 @@ func (rc *RunContext) startContainer() common.Executor {
|
||||
}
|
||||
|
||||
func (rc *RunContext) cleanupFailedStart(ctx context.Context) {
|
||||
if rc.cleanUpJobContainer == nil {
|
||||
return
|
||||
cleanCtx, cancel := context.WithTimeout(context.WithoutCancel(ctx), time.Minute)
|
||||
defer cancel()
|
||||
cleanup := rc.cleanUpJobContainer
|
||||
if cleanup == nil {
|
||||
cleanup = rc.closeDockerProxy
|
||||
}
|
||||
cleanCtx := ctx
|
||||
if ctx.Err() != nil {
|
||||
// the start likely failed because ctx was cancelled, detach so teardown still runs
|
||||
var cancel context.CancelFunc
|
||||
cleanCtx, cancel = context.WithTimeout(common.WithLogger(context.Background(), common.Logger(ctx)), time.Minute)
|
||||
defer cancel()
|
||||
}
|
||||
if err := rc.cleanUpJobContainer(cleanCtx); err != nil {
|
||||
if err := cleanup(cleanCtx); err != nil {
|
||||
common.Logger(ctx).Errorf("Error while cleaning up after failed container start for job %s: %v", rc.JobName, err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,8 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"runtime"
|
||||
"strings"
|
||||
@@ -221,8 +223,9 @@ func (fakeContainer) Start(bool) common.Executor { return func(context.Context)
|
||||
|
||||
func (fakeContainer) Remove() common.Executor { return func(context.Context) error { return nil } }
|
||||
|
||||
func (fakeContainer) Close() common.Executor { return func(context.Context) error { return nil } }
|
||||
func (fakeContainer) GetActPath() string { return "/var/run/act" }
|
||||
func (fakeContainer) Close() common.Executor { return func(context.Context) error { return nil } }
|
||||
func (fakeContainer) GetActPath() string { return "/var/run/act" }
|
||||
func (fakeContainer) ToContainerPath(path string) string { return path }
|
||||
func (fakeContainer) Create([]string, []string) common.Executor {
|
||||
return func(context.Context) error { return nil }
|
||||
}
|
||||
@@ -269,9 +272,26 @@ func startJobContainerInputs(t *testing.T, workflowYAML string, cfg *Config) []*
|
||||
rc.ExprEval = rc.NewExpressionEvaluator(t.Context())
|
||||
require.NoError(t, rc.resolvePlatformImage(t.Context()))
|
||||
|
||||
// the inputs are built before the missing daemon fails the first call
|
||||
t.Setenv("DOCKER_HOST", "unix:///nonexistent.sock")
|
||||
require.Error(t, rc.startJobContainer()(t.Context()))
|
||||
daemon := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
switch {
|
||||
case strings.HasSuffix(r.URL.Path, "/_ping"):
|
||||
w.Header().Set("API-Version", "1.47")
|
||||
_, _ = io.WriteString(w, "OK")
|
||||
case strings.HasSuffix(r.URL.Path, "/containers/json"), strings.HasSuffix(r.URL.Path, "/networks"):
|
||||
_, _ = io.WriteString(w, "[]")
|
||||
case strings.HasSuffix(r.URL.Path, "/volumes"):
|
||||
_, _ = io.WriteString(w, `{"Volumes":[]}`)
|
||||
case strings.HasSuffix(r.URL.Path, "/info"):
|
||||
_, _ = io.WriteString(w, `{"Architecture":"amd64","OSType":"linux"}`)
|
||||
default:
|
||||
t.Errorf("unexpected Docker request: %s %s", r.Method, r.URL)
|
||||
http.NotFound(w, r)
|
||||
}
|
||||
}))
|
||||
t.Cleanup(daemon.Close)
|
||||
t.Setenv("DOCKER_HOST", daemon.URL)
|
||||
require.NoError(t, rc.startJobContainer()(t.Context()))
|
||||
|
||||
return inputs
|
||||
}
|
||||
@@ -627,7 +647,7 @@ func TestCleanupJobResourcesCleansServicesWithoutJobContainer(t *testing.T) {
|
||||
serviceContainers: []*serviceContainer{{name: "svc", container: service}},
|
||||
}
|
||||
|
||||
err := rc.cleanupJobResources("external-network", false)(context.Background())
|
||||
err := rc.cleanupJobResources("external-network", false)(common.WithDryrun(t.Context(), true))
|
||||
require.NoError(t, err)
|
||||
service.AssertExpectations(t)
|
||||
}
|
||||
@@ -636,11 +656,12 @@ func TestCleanupJobResourcesCleansServicesWithoutJobContainer(t *testing.T) {
|
||||
func TestCleanupJobResourcesContinuesAfterFailure(t *testing.T) {
|
||||
t.Setenv("DOCKER_HOST", "unix:///nonexistent.sock")
|
||||
|
||||
removeError, closeError := errors.New("remove service"), errors.New("close service")
|
||||
jobContainer := &containerMock{}
|
||||
jobContainer.On("Remove").Return(func(context.Context) error { return errors.New("removal failed") }).Once()
|
||||
service := &containerMock{}
|
||||
service.On("Remove").Return(func(context.Context) error { return nil }).Once()
|
||||
service.On("Close").Return(func(context.Context) error { return nil }).Once()
|
||||
service.On("Remove").Return(func(context.Context) error { return removeError }).Once()
|
||||
service.On("Close").Return(func(context.Context) error { return closeError }).Once()
|
||||
|
||||
rc := &RunContext{
|
||||
Name: "job",
|
||||
@@ -652,7 +673,11 @@ func TestCleanupJobResourcesContinuesAfterFailure(t *testing.T) {
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
cancel()
|
||||
require.Error(t, rc.cleanupJobResources("job-network", true)(ctx))
|
||||
err := rc.cleanupJobResources("job-network", true)(ctx)
|
||||
require.ErrorContains(t, err, "removal failed")
|
||||
require.ErrorIs(t, err, removeError)
|
||||
require.ErrorIs(t, err, closeError)
|
||||
require.ErrorIs(t, err, context.Canceled)
|
||||
jobContainer.AssertExpectations(t)
|
||||
service.AssertExpectations(t)
|
||||
}
|
||||
@@ -1069,12 +1094,19 @@ func TestRunContext_cleanupFailedStart(t *testing.T) {
|
||||
calls int
|
||||
err error
|
||||
sentinel any
|
||||
cancel context.CancelFunc
|
||||
}
|
||||
newRC := func(c *capture) *RunContext {
|
||||
return &RunContext{
|
||||
JobName: "job",
|
||||
cleanUpJobContainer: func(ctx context.Context) error {
|
||||
c.calls++
|
||||
if c.cancel != nil {
|
||||
c.cancel()
|
||||
}
|
||||
deadline, ok := ctx.Deadline()
|
||||
require.True(t, ok)
|
||||
assert.WithinDuration(t, time.Now().Add(time.Minute), deadline, time.Second)
|
||||
c.err = ctx.Err()
|
||||
c.sentinel = ctx.Value(sentinel)
|
||||
return nil
|
||||
@@ -1082,9 +1114,10 @@ func TestRunContext_cleanupFailedStart(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
t.Run("runs teardown on the live context", func(t *testing.T) {
|
||||
var c capture
|
||||
ctx := context.WithValue(context.Background(), sentinel, "v")
|
||||
t.Run("detaches teardown from cancellation during cleanup", func(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.WithValue(context.Background(), sentinel, "v"))
|
||||
defer cancel()
|
||||
c := capture{cancel: cancel}
|
||||
|
||||
newRC(&c).cleanupFailedStart(ctx)
|
||||
|
||||
@@ -1102,7 +1135,7 @@ func TestRunContext_cleanupFailedStart(t *testing.T) {
|
||||
|
||||
assert.Equal(t, 1, c.calls)
|
||||
require.NoError(t, c.err)
|
||||
assert.Nil(t, c.sentinel)
|
||||
assert.Equal(t, "v", c.sentinel)
|
||||
})
|
||||
|
||||
t.Run("no-op when there is nothing to clean up", func(t *testing.T) {
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
name: docker-proxy
|
||||
description: Verify mounted Docker access through post steps
|
||||
runs:
|
||||
using: node24
|
||||
main: index.js
|
||||
post: index.js
|
||||
@@ -0,0 +1,53 @@
|
||||
const assert = require('node:assert/strict');
|
||||
const {once} = require('node:events');
|
||||
const fs = require('node:fs');
|
||||
const http = require('node:http');
|
||||
|
||||
async function request(method, path, body) {
|
||||
const req = http.request({
|
||||
socketPath: '/var/run/docker.sock',
|
||||
method,
|
||||
path,
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
signal: AbortSignal.timeout(10000),
|
||||
});
|
||||
req.end(JSON.stringify(body));
|
||||
const [res] = await once(req, 'response');
|
||||
req.on('error', (error) => res.destroy(error));
|
||||
let data = '';
|
||||
for await (const chunk of res.setEncoding('utf8')) {
|
||||
data += chunk;
|
||||
}
|
||||
assert(res.statusCode >= 200 && res.statusCode < 300, `${method} ${path}: ${res.statusCode} ${data}`);
|
||||
return data;
|
||||
}
|
||||
|
||||
async function main() {
|
||||
assert(fs.statSync('/var/run/docker.sock').isSocket(), 'Docker mount must be a socket');
|
||||
assert.equal(await request('GET', '/_ping'), 'OK');
|
||||
const api = `/v${JSON.parse(await request('GET', '/version')).ApiVersion}`;
|
||||
const name = process.env.PROXY_TEST_RESOURCE;
|
||||
const job = process.env.JOB_CONTAINER_NAME;
|
||||
const post = process.env.STATE_post === 'true';
|
||||
assert(name);
|
||||
assert(job);
|
||||
if (!post) {
|
||||
await request('POST', `${api}/volumes/create`, {Name: name});
|
||||
await request('POST', `${api}/networks/create`, {Name: name});
|
||||
await request('POST', `${api}/networks/${name}/connect`, {Container: job});
|
||||
fs.appendFileSync(process.env.GITHUB_STATE, 'post=true\n');
|
||||
}
|
||||
const label = process.env.PROXY_TEST_MODE === 'proxy' ? job : undefined;
|
||||
assert.equal(JSON.parse(await request('GET', `${api}/volumes/${name}`)).Labels?.['com.gitea.runner.job'], label);
|
||||
const network = JSON.parse(await request('GET', `${api}/networks/${name}`));
|
||||
assert.equal(network.Labels?.['com.gitea.runner.job'], label);
|
||||
assert(Object.values(network.Containers).some((container) => container.Name === job));
|
||||
if (post) {
|
||||
console.log('docker proxy post verified');
|
||||
}
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error(error);
|
||||
process.exitCode = 1;
|
||||
});
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
name: docker-proxy
|
||||
on: push
|
||||
jobs:
|
||||
proxy:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: ./action
|
||||
Reference in New Issue
Block a user