mirror of
https://gitea.com/gitea/runner.git
synced 2026-11-04 14:35:32 +00:00
Adds a Kubernetes backend. A `name:kubernetes://image` label runs the job as one pod through the Kubernetes API, with services as containers of the same pod. Step env is exported by a script sent over exec stdin, keeping it out of the pod spec and process arguments, and service env goes into a per-job Secret. Docker actions and container options are rejected. See `docs/kubernetes.md`. Operators shape job pods with `kubernetes.pod_template`, with per-label `pod_templates` merged in `runs-on` order and a `$services` entry for service containers. The idle cleanup removes pods and Secrets a runner left behind. The client calls the Kubernetes API directly and runs step commands over `coder/websocket` (same dependency used in gitea) instead of pulling in the heavyweight `kubernetes/client-go`. Includes an e2e test against k3s. Replaces: https://gitea.com/gitea/runner/pulls/1000 Fixes: https://gitea.com/gitea/runner/issues/31 Assisted-by: Claude Code:claude-opus-5-5 Co-authored-by: Zettat123 <[email protected]> Co-authored-by: bircni <[email protected]> Reviewed-on: https://gitea.com/gitea/runner/pulls/1260 Reviewed-by: Lunny Xiao <[email protected]> Reviewed-by: Zettat123 <[email protected]> Co-authored-by: silverwind <[email protected]>
130 lines
3.7 KiB
Go
130 lines
3.7 KiB
Go
// Copyright 2026 The Gitea Authors. All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
//go:build e2e
|
|
|
|
package e2e
|
|
|
|
import (
|
|
"cmp"
|
|
"context"
|
|
"errors"
|
|
"os"
|
|
"testing"
|
|
"time"
|
|
|
|
"gitea.com/gitea/runner/internal/app/poll"
|
|
"gitea.com/gitea/runner/internal/app/run"
|
|
"gitea.com/gitea/runner/internal/pkg/client"
|
|
"gitea.com/gitea/runner/internal/pkg/config"
|
|
"gitea.com/gitea/runner/internal/pkg/labels"
|
|
|
|
"connectrpc.com/connect"
|
|
pingv1 "gitea.dev/actionslib/ping/v1"
|
|
runnerv1 "gitea.dev/actionslib/runner/v1"
|
|
)
|
|
|
|
type runnerOptions struct {
|
|
capacity int
|
|
ephemeral bool
|
|
cacheV2 *bool
|
|
cacheHost string
|
|
image string
|
|
kubernetes *config.Kubernetes
|
|
}
|
|
|
|
func startRunner(t *testing.T, repo, labelName string, options runnerOptions) *poll.Poller {
|
|
t.Helper()
|
|
ctx := t.Context()
|
|
token, err := fixture.RegistrationToken(ctx, repo)
|
|
if err != nil {
|
|
t.Fatalf("get registration token: %v", err)
|
|
}
|
|
|
|
cfg, err := config.LoadDefault("")
|
|
if err != nil {
|
|
t.Fatalf("load default config: %v", err)
|
|
}
|
|
if options.cacheV2 != nil {
|
|
cfg.Cache.V2 = options.cacheV2
|
|
}
|
|
cfg.Container.DockerHost = os.Getenv("DOCKER_HOST")
|
|
if cfg.Container.DockerHost == "" {
|
|
cfg.Container.DockerHost = "unix:///var/run/docker.sock"
|
|
}
|
|
cfg.Cache.Dir = t.TempDir() + "/cache"
|
|
cfg.Cache.Host = options.cacheHost
|
|
cfg.Runner.Insecure = true
|
|
cfg.Runner.FetchInterval = 250 * time.Millisecond // faster than prod defaults for local fixture
|
|
cfg.Runner.FetchIntervalMax = 250 * time.Millisecond
|
|
cfg.Runner.StateReportInterval = 500 * time.Millisecond // so cancel reaches the job quickly
|
|
cfg.Runner.LogReportInterval = 500 * time.Millisecond
|
|
cfg.Runner.Capacity = max(options.capacity, 1)
|
|
if fixture.network != "" {
|
|
cfg.Container.Network = fixture.network
|
|
}
|
|
|
|
scheme := labels.SchemeDocker
|
|
if options.kubernetes != nil {
|
|
scheme, cfg.Kubernetes = labels.SchemeKubernetes, *options.kubernetes
|
|
}
|
|
rawLabel := labelName + ":" + scheme + "://" + cmp.Or(options.image, os.Getenv("E2E_JOB_IMAGE"))
|
|
label, err := labels.Parse(rawLabel)
|
|
if err != nil {
|
|
t.Fatalf("parse label %q: %v", rawLabel, err)
|
|
}
|
|
labelNames := []string{label.Name}
|
|
|
|
pingCli := client.New(fixture.baseURL, cfg.Runner.Insecure, "", "", config.RequestTimeout)
|
|
if _, err := pingCli.Ping(ctx, connect.NewRequest(&pingv1.PingRequest{Data: t.Name()})); err != nil {
|
|
t.Fatalf("ping %s: %v", fixture.baseURL, err)
|
|
}
|
|
|
|
regResp, err := pingCli.Register(ctx, connect.NewRequest(&runnerv1.RegisterRequest{
|
|
Name: t.Name(),
|
|
Token: token,
|
|
Version: "e2e",
|
|
Labels: labelNames,
|
|
Ephemeral: options.ephemeral,
|
|
Capabilities: run.RunnerCapabilities(),
|
|
}))
|
|
if err != nil {
|
|
t.Fatalf("register runner: %v", err)
|
|
}
|
|
if options.ephemeral && !regResp.Msg.Runner.Ephemeral {
|
|
t.Fatal("gitea did not grant ephemeral registration")
|
|
}
|
|
|
|
reg := &config.Registration{
|
|
ID: regResp.Msg.Runner.Id,
|
|
UUID: regResp.Msg.Runner.Uuid,
|
|
Name: regResp.Msg.Runner.Name,
|
|
Token: regResp.Msg.Runner.Token,
|
|
Address: fixture.baseURL,
|
|
Labels: []string{rawLabel},
|
|
Ephemeral: regResp.Msg.Runner.Ephemeral,
|
|
}
|
|
|
|
cli := client.New(fixture.baseURL, cfg.Runner.Insecure, reg.UUID, reg.Token, config.RequestTimeout)
|
|
|
|
runner := run.NewRunner(cfg, reg, cli)
|
|
declResp, err := runner.Declare(ctx, labelNames)
|
|
if err != nil {
|
|
_ = runner.Close()
|
|
t.Fatalf("declare runner: %v", err)
|
|
}
|
|
runner.SetCapabilitiesFromDeclare(declResp)
|
|
|
|
poller := poll.New(cfg, cli, runner)
|
|
go poller.Poll()
|
|
|
|
t.Cleanup(func() {
|
|
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
|
defer cancel()
|
|
if err := errors.Join(poller.Shutdown(ctx), runner.Close()); err != nil {
|
|
t.Logf("runner shutdown: %v", err)
|
|
}
|
|
})
|
|
return poller
|
|
}
|