diff --git a/README.md b/README.md index d6442c27..429b4067 100644 --- a/README.md +++ b/README.md @@ -264,7 +264,7 @@ volumes: - ${GITEA_DOCKER_WORKSPACE:-.}/data:/app/data ``` -Linux container jobs use a Docker proxy when socket sharing and permissions allow it. Other setups use the daemon socket directly. The proxy removes containers, networks and volumes after post steps and the completed hook. Named volumes created through it are job-scoped. To retain resources, mount the daemon socket explicitly in `container.options`. Host jobs use their existing Docker access, where `.` works. +Linux container jobs use a Docker proxy when socket sharing and permissions allow it. Other setups use the daemon socket directly. Host jobs use their existing Docker access, where `.` works. The proxy removes the containers, networks and volumes a job creates, `docker compose` services included, after post steps and the completed hook. Set `container.sweep: false` when jobs deploy services to the runner's Docker daemon. #### Proxy diff --git a/act/runner/run_context.go b/act/runner/run_context.go index 3bb77151..f64b2191 100644 --- a/act/runner/run_context.go +++ b/act/runner/run_context.go @@ -725,7 +725,7 @@ func (rc *RunContext) cleanupJobResources(networkName string, createAndDeleteNet logger.Infof("Cleaning up services for job %s", rc.JobName) errs = append(errs, rc.stopServiceContainers()(ctx)) } - if !common.Dryrun(ctx) && (preclean || rc.hadDockerProxy) { + if !common.Dryrun(ctx) && !rc.Config.NoSweep && (preclean || rc.hadDockerProxy) { errs = append(errs, container.RemoveDockerJobResources(ctx, rc.jobContainerName())) } if preclean || rc.deferVolumeCleanup == nil { diff --git a/act/runner/run_context_test.go b/act/runner/run_context_test.go index bf10cf3c..46c3a39e 100644 --- a/act/runner/run_context_test.go +++ b/act/runner/run_context_test.go @@ -778,9 +778,10 @@ func TestRemoveDockerActionImagesByTag(t *testing.T) { func TestCleanupJobResourcesContinuesAfterFailure(t *testing.T) { t.Setenv("TMPDIR", "/tmp") proxyDir := t.TempDir() - for _, name := range []string{"synchronous", "proxy", "closed proxy", "deferred", "preclean deferred", "canceled"} { + for _, name := range []string{"synchronous", "proxy", "closed proxy", "kept proxy", "deferred", "preclean deferred", "canceled"} { t.Run(name, func(t *testing.T) { proxy, preclean, deferred := strings.HasSuffix(name, "proxy"), strings.HasPrefix(name, "preclean"), strings.HasSuffix(name, "deferred") + removesLabelled := (proxy || preclean) && name != "kept proxy" if proxy && runtime.GOOS == "windows" { t.Skip("Unix socket ownership is unavailable on Windows") } @@ -790,7 +791,7 @@ func TestCleanupJobResourcesContinuesAfterFailure(t *testing.T) { service.On("Remove").Return(func(context.Context) error { return removeError }).Once() service.On("Close").Return(func(context.Context) error { return closeError }).Once() rc := &RunContext{ - Config: &Config{CacheContainer: "cache-container"}, + Config: &Config{CacheContainer: "cache-container", NoSweep: name == "kept proxy"}, Run: &model.Run{Workflow: &model.Workflow{Name: "wf"}, JobID: "job"}, JobContainer: job, serviceContainers: []*serviceContainer{{name: "svc", container: service}}, @@ -846,8 +847,8 @@ func TestCleanupJobResourcesContinuesAfterFailure(t *testing.T) { } assert.Equal(t, 2, volumeRemovals) } - assert.Equal(t, proxy || preclean, strings.Contains(err.Error(), "labelled GET /containers/json")) - if proxy || preclean { + assert.Equal(t, removesLabelled, strings.Contains(err.Error(), "labelled GET /containers/json")) + if removesLabelled { require.ErrorContains(t, err, "labelled GET /networks") require.ErrorContains(t, err, "labelled GET /volumes") } diff --git a/act/runner/runner.go b/act/runner/runner.go index 2f1b1aaa..0d401352 100644 --- a/act/runner/runner.go +++ b/act/runner/runner.go @@ -87,6 +87,7 @@ type Config struct { MaxParallel int // max parallel jobs to run across all workflows (0 = no limit, uses CPU count) AllocatePTY bool // allocate a pseudo-TTY for each step's process ServiceReadyTimeout time.Duration // how long a job waits for its service containers to report healthy (0 uses the default) + NoSweep bool // keep the containers, networks and volumes a job creates through its docker proxy RunnerName string // name this runner registered with, reported as `runner.name`, defaults to the hostname JobStartedHook string // script run inside the job environment before the job's first step; ACTIONS_RUNNER_HOOK_JOB_STARTED is read from Env when empty JobCompletedHook string // script run inside the job environment after the job's last step; ACTIONS_RUNNER_HOOK_JOB_COMPLETED is read from Env when empty diff --git a/internal/app/run/runner.go b/internal/app/run/runner.go index c8c1d92f..a49d10d9 100644 --- a/internal/app/run/runner.go +++ b/internal/app/run/runner.go @@ -617,6 +617,7 @@ func (r *Runner) run(ctx context.Context, task *runnerv1.Task, reporter *report. }, ContainerOptions: r.cfg.Container.Options, ServiceReadyTimeout: r.cfg.Container.ServiceReadyTimeout, + NoSweep: r.cfg.Container.Sweep != nil && !*r.cfg.Container.Sweep, ContainerDaemonSocket: r.cfg.Container.DockerHost, Privileged: r.cfg.Container.Privileged, DefaultActionInstance: r.getDefaultActionsURL(task), diff --git a/internal/pkg/config/config.example.yaml b/internal/pkg/config/config.example.yaml index 033af9bf..ca44160a 100644 --- a/internal/pkg/config/config.example.yaml +++ b/internal/pkg/config/config.example.yaml @@ -285,6 +285,9 @@ container: # How long a job waits for a service container that declares a healthcheck to become # healthy. A negative value (e.g. -1s) starts the steps without waiting. #service_ready_timeout: 5m + # Remove the containers, networks and volumes a job creates through its Docker proxy when + # the job ends. Set to false when jobs deploy services to the runner's Docker daemon. + #sweep: true host: # The parent directory of a job's working directory. diff --git a/internal/pkg/config/config.go b/internal/pkg/config/config.go index c5d11625..73b19012 100644 --- a/internal/pkg/config/config.go +++ b/internal/pkg/config/config.go @@ -181,6 +181,7 @@ type Container struct { DockerTimeout time.Duration `yaml:"docker_timeout"` // Timeout to wait for the docker daemon to be reachable, if docker is required by require_docker or runner BindWorkdir bool `yaml:"bind_workdir"` // BindWorkdir mounts the workspace from a host directory instead of a Docker volume. ServiceReadyTimeout time.Duration `yaml:"service_ready_timeout"` // ServiceReadyTimeout bounds how long a job waits for a service container that declares a healthcheck to report healthy. Negative disables waiting. + Sweep *bool `yaml:"sweep"` // Sweep removes the containers, networks and volumes a job creates through its Docker proxy when the job ends. Unset means enabled. } // Values of Runner.ToolCacheMode: the runner mounts no tool cache, or one that every job reuses.