mirror of
https://gitea.com/gitea/act_runner
synced 2026-09-21 19:37:07 +02:00
Containers a job starts through its Docker socket, for example `docker run -v "$PWD:/src"`, `./data:/data` in docker compose, or actions like dockerfile-roast, can now bind-mount the workspace and other paths the job sees, as on a host, without `bind_workdir`. The per-job Docker proxy rewrites container and volume create requests. A bind source, or the device of a `local` volume with `o: bind`, that lies under one of the job container's mounts is pointed at that mount's path on the daemon, read from inspecting the job container. Paths that already name a daemon path, like `GITEA_DOCKER_WORKSPACE`, and paths outside the job's mounts pass through unchanged. The proxy now also starts when the runner runs in a container given the host's Docker socket, by placing its socket in the runner's working directory, and in rootless dind, by granting the daemon socket's group through an ACL. Fixes https://gitea.com/gitea/runner/issues/1219 Fixes https://gitea.com/gitea/runner/issues/1193 Reviewed-on: https://gitea.com/gitea/runner/pulls/1226 Reviewed-by: bircni <bircni@icloud.com> Co-authored-by: silverwind <2021+silverwind@noreply.gitea.com>
111 lines
4.3 KiB
Go
111 lines
4.3 KiB
Go
// 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,
|
|
ContainerOptions: "-v /usr/local/bin/docker:/usr/local/bin/docker:ro -v /usr/local/libexec/docker/cli-plugins:/usr/local/libexec/docker/cli-plugins:ro",
|
|
ValidVolumes: []string{"/usr/local/bin/docker", "/usr/local/libexec/docker/cli-plugins"},
|
|
Env: map[string]string{"PROXY_TEST_RESOURCE": resourceName, "PROXY_TEST_MODE": mode, "PROXY_TEST_IMAGE": baseImage, "COMPOSE_PROJECT_NAME": resourceName},
|
|
})
|
|
require.NoError(t, err)
|
|
planner, err := model.NewWorkflowPlanner(filepath.Join(fixtureDir, "push.yml"), true)
|
|
require.NoError(t, err)
|
|
plan, err := planner.PlanEvent("push")
|
|
if mode == "direct" {
|
|
plan, err = planner.PlanJob("proxy")
|
|
}
|
|
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")
|
|
if mode == "proxy" {
|
|
require.Contains(t, messages, "docker binds 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)
|
|
}
|
|
}
|