mirror of
https://gitea.com/gitea/runner.git
synced 2026-08-26 22:07:45 +00:00
enhance: add runner.default_image for jobs matching no label (#1164)
A job whose `runs-on` matches none of the runner's labels, which includes every job that sets no `runs-on` at all, runs in `runner.default_image`. It defaults to `docker.gitea.com/runner-images:ubuntu-latest` as before, so a mirror can be pointed at instead. A runner with no reachable docker daemon now runs such a job on the host, rather than failing on an image it cannot pull. Runners that use docker are unaffected and never probe for one. This matters most to a host-mode runner, one whose labels are all `host`. Such a runner has no daemon to pull an image with, so a job matching none of its labels used to fail at container start. It now runs on the host, where that runner runs everything else anyway, and it takes no configuration to get there. A host-mode runner that does have a daemon within reach keeps using the image, unchanged. Supersedes https://gitea.com/gitea/runner/pulls/642 --------- Co-authored-by: silverwind <me@silverwind.io> Reviewed-on: https://gitea.com/gitea/runner/pulls/1164 Reviewed-by: silverwind <2021+silverwind@noreply.gitea.com>
This commit is contained in:
@@ -558,7 +558,7 @@ func loadExecCmd(ctx context.Context) *cobra.Command {
|
||||
execCmd.PersistentFlags().BoolVarP(&execArg.noSkipCheckout, "no-skip-checkout", "", false, "Do not skip actions/checkout")
|
||||
execCmd.PersistentFlags().BoolVarP(&execArg.debug, "debug", "d", false, "enable debug log")
|
||||
execCmd.PersistentFlags().BoolVarP(&execArg.dryrun, "dryrun", "n", false, "dryrun mode")
|
||||
execCmd.PersistentFlags().StringVarP(&execArg.image, "image", "i", "docker.gitea.com/runner-images:ubuntu-latest", "Docker image to use. Use \"-self-hosted\" to run directly on the host.")
|
||||
execCmd.PersistentFlags().StringVarP(&execArg.image, "image", "i", config.DefaultImage, "Docker image to use. Use \"-self-hosted\" to run directly on the host.")
|
||||
execCmd.PersistentFlags().StringVarP(&execArg.toolCacheMode, "tool-cache-mode", "", config.ToolCacheModeNone, "What to mount at RUNNER_TOOL_CACHE: none, or shared to reuse one tool cache across runs")
|
||||
execCmd.PersistentFlags().StringVarP(&execArg.network, "network", "", "", "Specify the network to which the container will connect")
|
||||
execCmd.PersistentFlags().StringVarP(&execArg.githubInstance, "gitea-instance", "", "", "Gitea instance to use.")
|
||||
|
||||
@@ -28,6 +28,7 @@ import (
|
||||
"gitea.com/gitea/runner/internal/pkg/client"
|
||||
"gitea.com/gitea/runner/internal/pkg/config"
|
||||
"gitea.com/gitea/runner/internal/pkg/disk"
|
||||
"gitea.com/gitea/runner/internal/pkg/envcheck"
|
||||
"gitea.com/gitea/runner/internal/pkg/labels"
|
||||
"gitea.com/gitea/runner/internal/pkg/metrics"
|
||||
"gitea.com/gitea/runner/internal/pkg/report"
|
||||
@@ -172,7 +173,7 @@ func (r *Runner) OnIdle(ctx context.Context) {
|
||||
// directories above, a task beginning during the pass is safe because the cutoff keeps a
|
||||
// network it has created but not yet attached a container to out of scope.
|
||||
func (r *Runner) cleanupOrphanNetworks(ctx context.Context) {
|
||||
if r.uuid == "" || !r.labels.RequireDocker() && !r.cfg.Container.RequireDocker {
|
||||
if r.uuid == "" || !r.requiresDocker() {
|
||||
return
|
||||
}
|
||||
cutoff := r.now().Add(-r.cfg.Runner.WorkdirCleanupAge)
|
||||
@@ -347,6 +348,27 @@ func (r *Runner) isSelfHostedActionsURL(task *runnerv1.Task) bool {
|
||||
return giteaDefaultActionsURL != "" && giteaDefaultActionsURL != "https://github.com"
|
||||
}
|
||||
|
||||
// dockerReachable is a variable so tests can substitute one that needs no Docker daemon. It
|
||||
// probes the environment act connects through, not container.docker_host, which act ignores.
|
||||
var dockerReachable = func(ctx context.Context) bool {
|
||||
ctx, cancel := context.WithTimeout(ctx, 2*time.Second)
|
||||
defer cancel()
|
||||
return envcheck.CheckIfDockerRunning(ctx, "") == nil
|
||||
}
|
||||
|
||||
func (r *Runner) requiresDocker() bool {
|
||||
return r.labels.RequireDocker() || r.cfg.Container.RequireDocker
|
||||
}
|
||||
|
||||
// fallbackPlatform is where a job runs whose runs-on matches no label, as any job without a
|
||||
// runs-on does, since Gitea sends those to every runner.
|
||||
func (r *Runner) fallbackPlatform(ctx context.Context) string {
|
||||
if r.requiresDocker() || dockerReachable(ctx) {
|
||||
return r.cfg.Runner.DefaultImage
|
||||
}
|
||||
return labels.SelfHostedPlatform
|
||||
}
|
||||
|
||||
func (r *Runner) run(ctx context.Context, task *runnerv1.Task, reporter *report.Reporter) (err error) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
@@ -475,6 +497,15 @@ func (r *Runner) run(ctx context.Context, task *runnerv1.Task, reporter *report.
|
||||
// Without bind_workdir, the workspace path omits the task id; concurrent host-mode jobs
|
||||
// for the same repository would share this directory and can race with per-job cleanup.
|
||||
|
||||
// act asks for the platform once per step, so resolve the fallback at most once per task.
|
||||
fallbackPlatform := sync.OnceValue(func() string { return r.fallbackPlatform(ctx) })
|
||||
platformPicker := func(runsOn []string) string {
|
||||
if platform := r.labels.PickPlatform(runsOn); platform != "" {
|
||||
return platform
|
||||
}
|
||||
return fallbackPlatform()
|
||||
}
|
||||
|
||||
runnerConfig := &runner.Config{
|
||||
// On Linux, Workdir will be like "/<parent_directory>/<owner>/<repo>"
|
||||
// On Windows, Workdir will be like "\<parent_directory>\<owner>\<repo>"
|
||||
@@ -517,7 +548,7 @@ func (r *Runner) run(ctx context.Context, task *runnerv1.Task, reporter *report.
|
||||
Privileged: r.cfg.Container.Privileged,
|
||||
DefaultActionInstance: r.getDefaultActionsURL(task),
|
||||
DefaultActionInstanceIsSelfHosted: r.isSelfHostedActionsURL(task),
|
||||
PlatformPicker: r.labels.PickPlatform,
|
||||
PlatformPicker: platformPicker,
|
||||
JobStartedHook: r.cfg.Runner.Hooks.JobStarted,
|
||||
JobCompletedHook: r.cfg.Runner.Hooks.JobCompleted,
|
||||
Vars: task.Vars,
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
"gitea.com/gitea/runner/act/runner"
|
||||
clientmocks "gitea.com/gitea/runner/internal/pkg/client/mocks"
|
||||
"gitea.com/gitea/runner/internal/pkg/config"
|
||||
"gitea.com/gitea/runner/internal/pkg/labels"
|
||||
"gitea.com/gitea/runner/internal/pkg/ver"
|
||||
|
||||
"connectrpc.com/connect"
|
||||
@@ -98,6 +99,34 @@ func TestNewRunnerInitializesLabelsAndEnvironment(t *testing.T) {
|
||||
require.Empty(t, r.envs[runner.CacheServiceV2Env], "no cache server, nothing to serve v2 from")
|
||||
}
|
||||
|
||||
func TestRunnerFallbackPlatform(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
label string
|
||||
dockerRunning bool
|
||||
want string
|
||||
}{
|
||||
{"a docker label needs no daemon probe", "ubuntu:docker://node:18", false, "mirror.example/ci:noble"},
|
||||
{"host labels keep the image where docker runs", "ubuntu:host", true, "mirror.example/ci:noble"},
|
||||
{"host labels without docker run on the host", "ubuntu:host", false, labels.SelfHostedPlatform},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
reachable := dockerReachable
|
||||
dockerReachable = func(context.Context) bool { return tt.dockerRunning }
|
||||
t.Cleanup(func() { dockerReachable = reachable })
|
||||
|
||||
label, err := labels.Parse(tt.label)
|
||||
require.NoError(t, err)
|
||||
cfg := &config.Config{}
|
||||
cfg.Runner.DefaultImage = "mirror.example/ci:noble"
|
||||
r := &Runner{cfg: cfg, labels: labels.Labels{label}}
|
||||
|
||||
require.Equal(t, tt.want, r.fallbackPlatform(t.Context()))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Proxy variables are assembled per task, because a job's service containers have to be
|
||||
// reached directly and they are only known once the workflow is parsed.
|
||||
func TestNewRunnerLeavesProxyToTheTask(t *testing.T) {
|
||||
|
||||
Reference in New Issue
Block a user