mirror of
https://gitea.com/gitea/act_runner
synced 2026-09-21 19:37:07 +02:00
feat: add GITEA_DOCKER_WORKSPACE and container cleanup (#1204)
1. Add `GITEA_DOCKER_WORKSPACE` which holds the workspace path as the daemon sees it, enabling `${GITEA_DOCKER_WORKSPACE:-.}/data:/app/data` in a compose file without having to resort to `bind_workdir` (which causes much more problems like breaking `actions/cache` because of unstable workspace paths).
2. Add container/network/volume cleanup for containers started within jobs, for example via `docker compose` inside a job. It works by running a lightweight docker socket proxy and injecting a `com.gitea.runner.job` label into every container creation and that label is used to remove containers started by that job at the end. Perf impact of this is near-zero.
Docs: https://gitea.com/gitea/docs/pulls/535
Assisted by Claude (Fable 5.1).
Co-authored-by: bircni <bircni@icloud.com>
Reviewed-on: https://gitea.com/gitea/runner/pulls/1204
Reviewed-by: bircni <bircni@icloud.com>
Co-authored-by: silverwind <me@silverwind.io>
This commit is contained in:
committed by
bircni
co-authored by
bircni
parent
c158ac5472
commit
9e3647395a
@@ -96,6 +96,7 @@ type RunContext struct {
|
||||
jobContainerID string
|
||||
hasBash *bool // memoized implicit-shell probe, only set on the top-level RunContext
|
||||
jobNetworkName string
|
||||
dockerProxy *container.DockerProxy
|
||||
// stepEnv is a copy of the running step's environment, so that workflow commands parsed out
|
||||
// of the container's output can be judged against it. Written by runStepExecutor and read on
|
||||
// the log-writer goroutine, hence unsecureCommandMu, which also guards unsecureCommandErr.
|
||||
@@ -253,11 +254,38 @@ func (rc *RunContext) validVolumes() []string {
|
||||
if rc.Config.BindWorkdir {
|
||||
volumes = append(volumes, rc.Config.Workdir)
|
||||
}
|
||||
if rc.dockerProxy != nil {
|
||||
volumes = append(volumes, rc.dockerProxy.Socket)
|
||||
}
|
||||
// TODO: add a new configuration to control whether the docker daemon can be mounted
|
||||
return append(volumes, name, name+"-env",
|
||||
getDockerDaemonSocketMountPath(rc.containerDaemonSocket()))
|
||||
}
|
||||
|
||||
func (rc *RunContext) jobDockerSocket() string {
|
||||
if rc.dockerProxy != nil {
|
||||
return rc.dockerProxy.Socket
|
||||
}
|
||||
return getDockerDaemonSocketMountPath(rc.containerDaemonSocket())
|
||||
}
|
||||
|
||||
func (rc *RunContext) startDockerProxy(ctx context.Context) {
|
||||
daemonSocket := rc.containerDaemonSocket()
|
||||
if daemonSocket == "-" || strings.HasPrefix(strings.ToLower(daemonSocket), "npipe://") {
|
||||
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
|
||||
}
|
||||
|
||||
// toolCache returns the tool cache path the job sees, relocatable through RUNNER_TOOL_CACHE.
|
||||
func (rc *RunContext) toolCache(fallback string) string {
|
||||
if path := rc.GetEnv()["RUNNER_TOOL_CACHE"]; path != "" {
|
||||
@@ -327,8 +355,8 @@ func (rc *RunContext) GetBindsAndMounts() ([]string, map[string]string, error) {
|
||||
// the runner's own mounts below yield to the targets the job claims
|
||||
binds, mounts, claimed := splitVolumes(volumes)
|
||||
|
||||
if daemonSocket := rc.containerDaemonSocket(); daemonSocket != "-" && !claimed["/var/run/docker.sock"] {
|
||||
binds = append(binds, getDockerDaemonSocketMountPath(daemonSocket)+":/var/run/docker.sock")
|
||||
if rc.containerDaemonSocket() != "-" && !claimed["/var/run/docker.sock"] {
|
||||
binds = append(binds, rc.jobDockerSocket()+":/var/run/docker.sock")
|
||||
}
|
||||
if rc.Config.SharedToolCache {
|
||||
if toolCache := rc.toolCache(container.DefaultToolCache); !claimed[toolCache] {
|
||||
@@ -455,6 +483,7 @@ 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)
|
||||
|
||||
@@ -651,6 +680,12 @@ func (rc *RunContext) cleanupJobResources(networkName string, createAndDeleteNet
|
||||
logger.Errorf("Error while cleaning services: %v", err)
|
||||
}
|
||||
}
|
||||
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 removeJobContainer {
|
||||
// after the containers using them, services can hold these via `--volumes-from`
|
||||
name := rc.jobContainerName()
|
||||
@@ -914,6 +949,9 @@ func (rc *RunContext) captureJobContainerInfo() common.Executor {
|
||||
return nil
|
||||
}
|
||||
rc.jobContainerID = info.ID
|
||||
if source := info.Mounts[rc.githubWorkspace()]; source != "" {
|
||||
rc.Env["GITEA_DOCKER_WORKSPACE"] = source
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1294,6 +1294,29 @@ func TestGetJobContextReportsContainers(t *testing.T) {
|
||||
}, jobContext.Services)
|
||||
}
|
||||
|
||||
func TestCaptureJobContainerInfoExportsDockerWorkspace(t *testing.T) {
|
||||
job := &containerMock{}
|
||||
job.On("Inspect", mock.Anything).Return(&container.Info{
|
||||
ID: "job-container-id",
|
||||
Mounts: map[string]string{"/workspace/owner/repo": "/var/lib/docker/volumes/job/_data"},
|
||||
}, nil)
|
||||
rc := &RunContext{
|
||||
Config: &Config{Workdir: "/workspace/owner/repo/"},
|
||||
Env: map[string]string{},
|
||||
JobContainer: job,
|
||||
}
|
||||
|
||||
require.NoError(t, rc.captureJobContainerInfo()(context.Background()))
|
||||
|
||||
assert.Equal(t, "job-container-id", rc.jobContainerID)
|
||||
assert.Equal(t, "/var/lib/docker/volumes/job/_data", rc.Env["GITEA_DOCKER_WORKSPACE"])
|
||||
|
||||
rc.Config.Workdir = "/elsewhere"
|
||||
rc.Env = map[string]string{}
|
||||
require.NoError(t, rc.captureJobContainerInfo()(context.Background()))
|
||||
assert.NotContains(t, rc.Env, "GITEA_DOCKER_WORKSPACE")
|
||||
}
|
||||
|
||||
// A job that never started a container reports an empty context, not a placeholder.
|
||||
func TestGetJobContextWithoutContainer(t *testing.T) {
|
||||
jobContext := (&RunContext{}).getJobContext()
|
||||
|
||||
Reference in New Issue
Block a user