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:
Zettat123
2026-09-08 04:16:02 +00:00
committed by bircni
co-authored by silverwind
parent ba4d3c5b4f
commit ff9965e940
18 changed files with 935 additions and 295 deletions
+102
View File
@@ -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)
}
}
+7 -6
View File
@@ -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
+38 -5
View File
@@ -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
View File
@@ -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)
}
}
+46 -13
View File
@@ -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) {
+6
View File
@@ -0,0 +1,6 @@
name: docker-proxy
description: Verify mounted Docker access through post steps
runs:
using: node24
main: index.js
post: index.js
+53
View File
@@ -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
View File
@@ -0,0 +1,8 @@
name: docker-proxy
on: push
jobs:
proxy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: ./action