mirror of
https://gitea.com/gitea/runner.git
synced 2026-08-23 12:27:44 +00:00
11ac12efa4
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>
212 lines
7.8 KiB
Go
212 lines
7.8 KiB
Go
// Copyright 2026 The Gitea Authors. All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package run
|
|
|
|
import (
|
|
"context"
|
|
"net/http"
|
|
"strings"
|
|
"testing"
|
|
|
|
"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"
|
|
runnerv1 "gitea.dev/actionslib/runner/v1"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/mock"
|
|
"github.com/stretchr/testify/require"
|
|
"google.golang.org/protobuf/types/known/structpb"
|
|
)
|
|
|
|
func TestRunnerCapabilitiesAndDeclare(t *testing.T) {
|
|
require.Equal(t, []string{CapabilityCancelling}, RunnerCapabilities())
|
|
|
|
cli := clientmocks.NewClient(t)
|
|
cli.On("Declare", mock.Anything, mock.MatchedBy(func(req *connect.Request[runnerv1.DeclareRequest]) bool {
|
|
return req.Msg.Version == ver.Version() &&
|
|
len(req.Msg.Labels) == 1 &&
|
|
req.Msg.Labels[0] == "ubuntu" &&
|
|
len(req.Msg.Capabilities) == 1 &&
|
|
req.Msg.Capabilities[0] == CapabilityCancelling
|
|
})).Return(connect.NewResponse(&runnerv1.DeclareResponse{}), nil)
|
|
|
|
r := &Runner{client: cli}
|
|
_, err := r.Declare(context.Background(), []string{"ubuntu"})
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
func TestRunnerSetCapabilitiesFromDeclare(t *testing.T) {
|
|
r := &Runner{}
|
|
r.SetCapabilitiesFromDeclare(nil)
|
|
require.Empty(t, r.capabilities)
|
|
|
|
resp := connect.NewResponse(&runnerv1.DeclareResponse{})
|
|
resp.Header().Set("X-Gitea-Actions-Capabilities", " cancelling,cache-v2 ")
|
|
r.SetCapabilitiesFromDeclare(resp)
|
|
require.Equal(t, "cancelling,cache-v2", r.capabilities)
|
|
}
|
|
|
|
func TestRunnerDefaultActionsURLUsesMirrorOnlyForGithub(t *testing.T) {
|
|
r := &Runner{cfg: &config.Config{}}
|
|
r.cfg.Runner.GithubMirror = "https://mirror.example"
|
|
|
|
task := taskWithDefaultActionsURL("https://github.com")
|
|
require.Equal(t, "https://mirror.example", r.getDefaultActionsURL(task))
|
|
|
|
task = taskWithDefaultActionsURL("https://gitea.example")
|
|
require.Equal(t, "https://gitea.example", r.getDefaultActionsURL(task))
|
|
}
|
|
|
|
func TestRunnerRunningCountAndNullLogger(t *testing.T) {
|
|
r := &Runner{}
|
|
require.Equal(t, int64(0), r.RunningCount())
|
|
r.runningCount.Add(2)
|
|
require.Equal(t, int64(2), r.RunningCount())
|
|
|
|
logger := NullLogger{}.WithJobLogger()
|
|
require.NotNil(t, logger)
|
|
require.NotNil(t, logger.Out)
|
|
}
|
|
|
|
func TestNewRunnerInitializesLabelsAndEnvironment(t *testing.T) {
|
|
cacheEnabled := false
|
|
cfg := &config.Config{}
|
|
cfg.Cache.Enabled = &cacheEnabled
|
|
cfg.Runner.Envs = map[string]string{"EXISTING": "value"}
|
|
reg := &config.Registration{
|
|
Name: "runner",
|
|
Labels: []string{"ubuntu:host", "", "pool:e57e18d4"},
|
|
}
|
|
cli := clientmocks.NewClient(t)
|
|
cli.On("Address").Return("https://gitea.example/").Maybe()
|
|
|
|
r := NewRunner(cfg, reg, cli)
|
|
|
|
require.Equal(t, "runner", r.name)
|
|
require.Len(t, r.labels, 2)
|
|
require.Equal(t, []string{"ubuntu", "pool:e57e18d4"}, r.labels.Names())
|
|
require.Equal(t, "value", r.envs["EXISTING"])
|
|
require.Equal(t, "https://gitea.example/api/actions_pipeline/", r.envs["ACTIONS_RUNTIME_URL"])
|
|
require.Equal(t, "https://gitea.example", r.envs["ACTIONS_RESULTS_URL"])
|
|
require.Equal(t, "true", r.envs["GITEA_ACTIONS"])
|
|
require.NotEmpty(t, r.envs["GITEA_ACTIONS_RUNNER_VERSION"])
|
|
require.Nil(t, r.cacheHandler)
|
|
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) {
|
|
clearProxyEnv(t)
|
|
t.Setenv("http_proxy", "http://proxy:3128")
|
|
|
|
cfg := &config.Config{}
|
|
cfg.Cache.ExternalServer = "http://cache.local:8088/"
|
|
reg := &config.Registration{Name: "runner"}
|
|
cli := clientmocks.NewClient(t)
|
|
cli.On("Address").Return("https://gitea.example/").Maybe()
|
|
|
|
r := NewRunner(cfg, reg, cli)
|
|
|
|
require.NotContains(t, r.envs, "http_proxy")
|
|
require.NotContains(t, r.envs, "no_proxy")
|
|
}
|
|
|
|
func taskWithDefaultActionsURL(url string) *runnerv1.Task {
|
|
return &runnerv1.Task{
|
|
Context: &structpb.Struct{
|
|
Fields: map[string]*structpb.Value{
|
|
"gitea_default_actions_url": structpb.NewStringValue(url),
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
// The results service is decided per task, because that is where the job's token and its
|
|
// instance are both known. NewRunner only leaves the address Gitea serves.
|
|
func TestNewRunnerCacheServiceV2(t *testing.T) {
|
|
cfg := &config.Config{}
|
|
cfg.Cache.Dir, cfg.Cache.Host = t.TempDir(), "127.0.0.1"
|
|
cli := clientmocks.NewClient(t)
|
|
cli.On("Address").Return("https://gitea.example/").Maybe()
|
|
|
|
r := NewRunner(cfg, &config.Registration{Name: "runner"}, cli)
|
|
t.Cleanup(func() { _ = r.Close() })
|
|
const token = "task-token"
|
|
|
|
assert.Equal(t, "https://gitea.example", r.envs["ACTIONS_RESULTS_URL"])
|
|
assert.Empty(t, r.envs[runner.CacheServiceV2Env], "a promise the runner has not made yet")
|
|
assert.True(t, r.patchToolkit())
|
|
|
|
// The registration is what makes it true: the cache server takes the results service over,
|
|
// having been told which instance to forward the artifact half to.
|
|
revoke, resultsURL := r.registerCacheForTask(token, "owner/repo", nil)
|
|
defer revoke()
|
|
require.Equal(t, r.cacheHandler.ExternalURL(), resultsURL)
|
|
|
|
// And what is advertised has to answer the cache service. A client without the GHES escape
|
|
// hatch, docker buildx among them, posts its cache calls at exactly this URL and nowhere else,
|
|
// so a 404 here is the failure this whole change is about.
|
|
req, err := http.NewRequestWithContext(t.Context(), http.MethodPost,
|
|
resultsURL+"/twirp/github.actions.results.api.v1.CacheService/GetCacheEntryDownloadURL",
|
|
strings.NewReader(`{"key":"k","version":"v"}`))
|
|
require.NoError(t, err)
|
|
req.Header.Set("Authorization", "Bearer "+token)
|
|
resp, err := http.DefaultClient.Do(req)
|
|
require.NoError(t, err)
|
|
defer resp.Body.Close()
|
|
|
|
assert.Equal(t, http.StatusOK, resp.StatusCode, "the advertised results service serves no cache service")
|
|
}
|
|
|
|
// The v1 cache client appends its path to ACTIONS_CACHE_URL without a separator, so a configured
|
|
// server that is missing the slash would send it to a URL whose port swallows the path.
|
|
func TestNewRunnerNormalizesTheExternalCacheServer(t *testing.T) {
|
|
cfg := &config.Config{}
|
|
cfg.Cache.ExternalServer = "http://cache.local:8088//"
|
|
cli := clientmocks.NewClient(t)
|
|
cli.On("Address").Return("https://gitea.example/").Maybe()
|
|
|
|
r := NewRunner(cfg, &config.Registration{Name: "runner"}, cli)
|
|
|
|
assert.Equal(t, "http://cache.local:8088/", r.envs["ACTIONS_CACHE_URL"])
|
|
// Nothing to front the results service with, so the variable stays unset, but the bundles are
|
|
// still patched: artifacts v4 need that, and the patch keeps the cache client on the cache URL.
|
|
assert.Equal(t, "https://gitea.example", r.envs["ACTIONS_RESULTS_URL"])
|
|
assert.Empty(t, r.envs[runner.CacheServiceV2Env])
|
|
assert.True(t, r.patchToolkit())
|
|
}
|