feat: bootstrap official runner through SPIFFE facade

This commit is contained in:
2026-09-20 20:00:34 +00:00
parent 5fdd39f7ff
commit 8b77b4be63
16 changed files with 470 additions and 28 deletions
+6 -5
View File
@@ -61,7 +61,7 @@ func (b Backend) Find(ctx context.Context, assignmentID string) (*taskworker.Exe
return executor(result.Items[0]), nil
}
func (b Backend) Create(ctx context.Context, assignment taskassignment.Assignment, metadata taskworker.Metadata) (*taskworker.Executor, error) {
func (b Backend) Create(ctx context.Context, assignment taskassignment.Assignment, launch taskworker.LaunchSpec) (*taskworker.Executor, error) {
if err := b.validate(); err != nil {
return nil, err
}
@@ -69,10 +69,11 @@ func (b Backend) Create(ctx context.Context, assignment taskassignment.Assignmen
return nil, fmt.Errorf("OpenSandbox backend cannot create %q assignment", assignment.Backend)
}
environment := clone(b.Config.Env)
environment["CI_ASSIGNMENT_ID"] = assignment.ID
environment["CI_SPIFFE_ID"] = assignment.Identity.SPIFFEID
sandboxMetadata := clone(metadata.Annotations)
for key, value := range metadata.Labels {
for key, value := range launch.Environment {
environment[key] = value
}
sandboxMetadata := clone(launch.Metadata.Annotations)
for key, value := range launch.Metadata.Labels {
sandboxMetadata[key] = value
}
request := opensandbox.CreateSandboxRequest{
+10 -1
View File
@@ -62,13 +62,22 @@ func TestFindRecoversSandboxByMetadata(t *testing.T) {
func TestCreateUsesPoolAndPersistsRecoveryMetadata(t *testing.T) {
lifecycle := &fakeLifecycle{}
metadata := taskworker.BackendMetadata(assignment())
executor, err := backend(lifecycle).Create(context.Background(), assignment(), metadata)
executor, err := backend(lifecycle).Create(context.Background(), assignment(), taskworker.LaunchSpec{
Metadata: metadata,
Environment: map[string]string{
"CI_SPIFFE_ID": assignment().Identity.SPIFFEID,
"CI_RUNNER_CAPABILITY": "capability",
},
})
if err != nil {
t.Fatal(err)
}
if lifecycle.created.Extensions["poolRef"] != "ci-vm" || lifecycle.created.Metadata[assignmentMetadata] != assignment().ID || lifecycle.created.Env["CI_SPIFFE_ID"] != assignment().Identity.SPIFFEID || executor.Name != "sandbox-42" {
t.Fatalf("request=%#v executor=%#v", lifecycle.created, executor)
}
if lifecycle.created.Env["CI_RUNNER_CAPABILITY"] != "capability" {
t.Fatalf("environment = %#v", lifecycle.created.Env)
}
}
func TestBindIdentityVerifiesPersistedMetadata(t *testing.T) {
+5 -3
View File
@@ -35,6 +35,7 @@ type PodManifest struct {
Image string
ServiceAccount string
Args []string
Environment map[string]string
}
// IdentityEntry is a ClusterStaticEntry pinned to one concrete Pod UID.
@@ -89,24 +90,25 @@ func (b Backend) Find(ctx context.Context, assignmentID string) (*taskworker.Exe
return executor(pods[0]), nil
}
func (b Backend) Create(ctx context.Context, assignment taskassignment.Assignment, metadata taskworker.Metadata) (*taskworker.Executor, error) {
func (b Backend) Create(ctx context.Context, assignment taskassignment.Assignment, launch taskworker.LaunchSpec) (*taskworker.Executor, error) {
if err := b.validate(); err != nil {
return nil, err
}
if assignment.Backend != taskassignment.BackendPod {
return nil, fmt.Errorf("Pod backend cannot create %q assignment", assignment.Backend)
}
labels := clone(metadata.Labels)
labels := clone(launch.Metadata.Labels)
labels["app.kubernetes.io/name"] = "gitea-dynamic-runner"
labels["app.kubernetes.io/component"] = "executor"
pod, err := b.API.CreatePod(ctx, PodManifest{
Name: assignment.ID,
Namespace: b.Config.Namespace,
Labels: labels,
Annotations: clone(metadata.Annotations),
Annotations: clone(launch.Metadata.Annotations),
Image: b.Config.Image,
ServiceAccount: b.Config.ServiceAccount,
Args: append(append([]string{}, b.Config.ExecutorArgs...), assignment.ID),
Environment: clone(launch.Environment),
})
if err != nil {
return nil, fmt.Errorf("create assignment Pod: %w", err)
+6 -1
View File
@@ -75,7 +75,9 @@ func TestFindRecoversPodByAssignmentLabel(t *testing.T) {
func TestCreateUsesDeterministicNameAndRecoveryMetadata(t *testing.T) {
api := &fakeAPI{}
metadata := taskworker.BackendMetadata(assignment())
executor, err := backend(api).Create(context.Background(), assignment(), metadata)
executor, err := backend(api).Create(context.Background(), assignment(), taskworker.LaunchSpec{
Metadata: metadata, Environment: map[string]string{"CI_RUNNER_CAPABILITY": "capability"},
})
if err != nil {
t.Fatal(err)
}
@@ -85,6 +87,9 @@ func TestCreateUsesDeterministicNameAndRecoveryMetadata(t *testing.T) {
if api.created.Annotations["ci.ddupan.top/spiffe-id"] != assignment().Identity.SPIFFEID {
t.Fatalf("annotations = %#v", api.created.Annotations)
}
if api.created.Environment["CI_RUNNER_CAPABILITY"] != "capability" {
t.Fatalf("environment = %#v", api.created.Environment)
}
if len(api.created.Args) != 2 || api.created.Args[1] != "gitea-task-42" || executor.IdentityTarget != "pod-uid" {
t.Fatalf("args=%v executor=%#v", api.created.Args, executor)
}
+5 -1
View File
@@ -59,6 +59,10 @@ func (c *Client) ListPods(ctx context.Context, namespace, selector string) ([]Po
}
func (c *Client) CreatePod(ctx context.Context, manifest PodManifest) (Pod, error) {
environment := make([]corev1.EnvVar, 0, len(manifest.Environment))
for name, value := range manifest.Environment {
environment = append(environment, corev1.EnvVar{Name: name, Value: value})
}
document := &corev1.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: manifest.Name, Namespace: manifest.Namespace,
@@ -68,7 +72,7 @@ func (c *Client) CreatePod(ctx context.Context, manifest PodManifest) (Pod, erro
ServiceAccountName: manifest.ServiceAccount,
RestartPolicy: corev1.RestartPolicyNever,
Containers: []corev1.Container{{
Name: "executor", Image: manifest.Image, Args: manifest.Args,
Name: "executor", Image: manifest.Image, Args: manifest.Args, Env: environment,
SecurityContext: &corev1.SecurityContext{Privileged: boolPointer(true)},
VolumeMounts: []corev1.VolumeMount{{
Name: "spire-agent-socket", MountPath: "/run/spire/agent-sockets", ReadOnly: true,
+4 -1
View File
@@ -25,7 +25,7 @@ func TestClientPodLifecycleUsesTypedClient(t *testing.T) {
Name: "gitea-task-42", Namespace: "gitea-actions",
Labels: map[string]string{assignmentLabel: "gitea-task-42"},
Image: "zot/ci-executor:main", ServiceAccount: "gitea-task-executor",
Args: []string{"execute", "gitea-task-42"},
Args: []string{"execute", "gitea-task-42"}, Environment: map[string]string{"CI_RUNNER_CAPABILITY": "capability"},
})
if err != nil {
t.Fatal(err)
@@ -34,6 +34,9 @@ func TestClientPodLifecycleUsesTypedClient(t *testing.T) {
if err != nil {
t.Fatal(err)
}
if got := pod.Spec.Containers[0].Env; len(got) != 1 || got[0].Name != "CI_RUNNER_CAPABILITY" || got[0].Value != "capability" {
t.Fatalf("environment = %#v", got)
}
pod.UID = types.UID("pod-uid")
pod.Status.Phase = corev1.PodRunning
if _, err := client.Kubernetes.CoreV1().Pods("gitea-actions").Update(context.Background(), pod, metav1.UpdateOptions{}); err != nil {
+89
View File
@@ -0,0 +1,89 @@
// Package runnerbootstrap configures an unmodified, one-shot Gitea Runner to
// consume exactly the task assigned by the controller facade.
package runnerbootstrap
import (
"encoding/json"
"errors"
"fmt"
"net/url"
"git.ddupan.top/panxiao81/gitea-dynamic-runner/internal/runnerfacade"
"git.ddupan.top/panxiao81/gitea-dynamic-runner/internal/taskassignment"
)
const (
EnvAssignmentID = "CI_ASSIGNMENT_ID"
EnvCapability = "CI_RUNNER_CAPABILITY"
EnvFacadeURL = "CI_RUNNER_FACADE_URL"
EnvFacadeID = "CI_RUNNER_FACADE_SPIFFE_ID"
EnvSPIFFEID = "CI_SPIFFE_ID"
EnvBackend = "CI_RUNNER_BACKEND"
)
// Bootstrap emits assignment-scoped launch configuration. FacadeURL is the
// controller endpoint reached by the local SPIFFE proxy, not by Runner itself.
type Bootstrap struct {
Capabilities runnerfacade.Capabilities
FacadeURL string
FacadeSPIFFEID string
}
func (b Bootstrap) Environment(assignment taskassignment.Assignment) (map[string]string, error) {
if assignment.ID == "" || assignment.Identity.SPIFFEID == "" || b.FacadeSPIFFEID == "" {
return nil, errors.New("assignment ID and SPIFFE ID are required")
}
parsed, err := url.Parse(b.FacadeURL)
if err != nil || parsed.Scheme != "https" || parsed.Host == "" {
return nil, fmt.Errorf("runner facade URL must be an absolute https URL")
}
capability := b.Capabilities.Issue(assignment.ID)
if capability == "" {
return nil, errors.New("runner capability issuer is not configured")
}
return map[string]string{
EnvAssignmentID: assignment.ID,
EnvCapability: capability,
EnvFacadeURL: b.FacadeURL,
EnvFacadeID: b.FacadeSPIFFEID,
EnvSPIFFEID: assignment.Identity.SPIFFEID,
EnvBackend: string(assignment.Backend),
}, nil
}
// Registration mirrors Gitea Runner v3.5.0's registration file schema. ID is
// intentionally zero: the facade authenticates UUID and token and never uses
// the server-issued runner database ID.
type Registration struct {
Warning string `json:"WARNING"`
ID int64 `json:"id"`
UUID string `json:"uuid"`
Name string `json:"name"`
Token string `json:"token"`
Address string `json:"address"`
Labels []string `json:"labels"`
Ephemeral bool `json:"ephemeral"`
}
func RegistrationJSON(assignmentID, capability, localProxyURL string, backend taskassignment.Backend) ([]byte, error) {
if assignmentID == "" || capability == "" {
return nil, errors.New("assignment ID and runner capability are required")
}
parsed, err := url.Parse(localProxyURL)
if err != nil || parsed.Scheme != "http" || parsed.Host == "" {
return nil, errors.New("local runner proxy URL must be an absolute http URL")
}
if backend != taskassignment.BackendPod && backend != taskassignment.BackendVM {
return nil, fmt.Errorf("unsupported runner backend %q", backend)
}
registration := Registration{
Warning: "Generated for one preassigned task by gitea-dynamic-runner.",
UUID: assignmentID, Name: assignmentID, Token: capability,
Address: localProxyURL, Labels: []string{"self-hosted", string(backend)}, Ephemeral: true,
}
data, err := json.MarshalIndent(registration, "", " ")
if err != nil {
return nil, err
}
return append(data, '\n'), nil
}
@@ -0,0 +1,74 @@
package runnerbootstrap
import (
"encoding/json"
"testing"
"git.ddupan.top/panxiao81/gitea-dynamic-runner/internal/runnerfacade"
"git.ddupan.top/panxiao81/gitea-dynamic-runner/internal/taskassignment"
"git.ddupan.top/panxiao81/gitea-dynamic-runner/internal/taskidentity"
)
func testBootstrap(t *testing.T) Bootstrap {
t.Helper()
capabilities, err := runnerfacade.NewCapabilities([]byte("0123456789abcdef0123456789abcdef"))
if err != nil {
t.Fatal(err)
}
return Bootstrap{
Capabilities: capabilities,
FacadeURL: "https://runner-facade.gitea-actions.svc:8443",
FacadeSPIFFEID: "spiffe://ddupan.top/ns/gitea-actions/sa/gitea-dynamic-runner",
}
}
func testAssignment() taskassignment.Assignment {
return taskassignment.Assignment{
ID: "gitea-task-42", Backend: taskassignment.BackendPod,
Identity: taskidentity.Identity{SPIFFEID: "spiffe://ddupan.top/ci/owner/repo/publish"},
}
}
func TestEnvironmentIsDeterministicAndAssignmentScoped(t *testing.T) {
bootstrap := testBootstrap(t)
first, err := bootstrap.Environment(testAssignment())
if err != nil {
t.Fatal(err)
}
second, err := bootstrap.Environment(testAssignment())
if err != nil {
t.Fatal(err)
}
if first[EnvCapability] == "" || first[EnvCapability] != second[EnvCapability] {
t.Fatalf("capabilities = %q, %q", first[EnvCapability], second[EnvCapability])
}
if first[EnvAssignmentID] != "gitea-task-42" || first[EnvSPIFFEID] != testAssignment().Identity.SPIFFEID {
t.Fatalf("environment = %#v", first)
}
if first[EnvBackend] != "pod" || first[EnvFacadeID] == "" {
t.Fatalf("environment = %#v", first)
}
}
func TestRegistrationMatchesOfficialRunnerSchema(t *testing.T) {
data, err := RegistrationJSON("gitea-task-42", "capability", "http://127.0.0.1:8080", taskassignment.BackendVM)
if err != nil {
t.Fatal(err)
}
var registration Registration
if err := json.Unmarshal(data, &registration); err != nil {
t.Fatal(err)
}
if registration.UUID != "gitea-task-42" || registration.Token != "capability" || registration.Address != "http://127.0.0.1:8080" || !registration.Ephemeral {
t.Fatalf("registration = %#v", registration)
}
if len(registration.Labels) != 2 || registration.Labels[0] != "self-hosted" || registration.Labels[1] != "vm" {
t.Fatalf("labels = %#v", registration.Labels)
}
}
func TestRegistrationRejectsNonLocalTLSAddress(t *testing.T) {
if _, err := RegistrationJSON("id", "capability", "https://facade.example", taskassignment.BackendPod); err == nil {
t.Fatal("expected local proxy URL validation error")
}
}
+116
View File
@@ -0,0 +1,116 @@
package runnerbootstrap
import (
"context"
"errors"
"fmt"
"net"
"net/http"
"os"
"os/exec"
"path/filepath"
"time"
"git.ddupan.top/panxiao81/gitea-dynamic-runner/internal/taskassignment"
)
type ExecutorConfig struct {
AssignmentID string
Capability string
Backend taskassignment.Backend
FacadeURL string
FacadeSPIFFEID string
WorkloadAPIAddr string
RunnerBinary string
ListenAddress string
WorkDir string
Stdout *os.File
Stderr *os.File
}
// RunExecutor runs the SPIFFE proxy and one unmodified official Runner process.
// The generated registration file exists only in the executor's temporary
// work directory and the runner exits after its preassigned task.
func RunExecutor(ctx context.Context, config ExecutorConfig) error {
if config.RunnerBinary == "" {
config.RunnerBinary = "gitea-runner"
}
if config.ListenAddress == "" {
config.ListenAddress = "127.0.0.1:0"
}
listener, err := net.Listen("tcp", config.ListenAddress)
if err != nil {
return fmt.Errorf("listen for local runner proxy: %w", err)
}
defer listener.Close()
address, ok := listener.Addr().(*net.TCPAddr)
if !ok || !address.IP.IsLoopback() {
return errors.New("runner proxy must listen on a loopback address")
}
proxy, err := NewProxy(ctx, config.FacadeURL, config.FacadeSPIFFEID, config.WorkloadAPIAddr)
if err != nil {
return err
}
defer proxy.Close()
workDir := config.WorkDir
removeWorkDir := false
if workDir == "" {
workDir, err = os.MkdirTemp("", "gitea-task-runner-")
if err != nil {
return fmt.Errorf("create runner work directory: %w", err)
}
removeWorkDir = true
}
if removeWorkDir {
defer os.RemoveAll(workDir)
}
registration, err := RegistrationJSON(
config.AssignmentID, config.Capability, "http://"+listener.Addr().String(), config.Backend,
)
if err != nil {
return err
}
if err := os.WriteFile(filepath.Join(workDir, ".runner"), registration, 0o600); err != nil {
return fmt.Errorf("write one-shot runner registration: %w", err)
}
server := &http.Server{Handler: proxy.Handler, ReadHeaderTimeout: 10 * time.Second}
serverErrors := make(chan error, 1)
go func() { serverErrors <- server.Serve(listener) }()
command := exec.CommandContext(ctx, config.RunnerBinary, "daemon", "--once")
command.Dir = workDir
command.Stdout = config.Stdout
command.Stderr = config.Stderr
runnerErr := command.Run()
shutdownContext, cancel := context.WithTimeout(context.Background(), 10*time.Second)
shutdownErr := server.Shutdown(shutdownContext)
cancel()
serverErr := <-serverErrors
if errors.Is(serverErr, http.ErrServerClosed) {
serverErr = nil
}
return errors.Join(runnerErr, shutdownErr, serverErr)
}
// ExecutorConfigFromEnvironment reads the non-secret image configuration and
// the assignment-scoped values injected by the backend. The Workload API
// address follows SPIFFE_ENDPOINT_SOCKET through go-spiffe when not set here.
func ExecutorConfigFromEnvironment() (ExecutorConfig, error) {
backend := taskassignment.Backend(os.Getenv(EnvBackend))
if backend != taskassignment.BackendPod && backend != taskassignment.BackendVM {
return ExecutorConfig{}, fmt.Errorf("invalid %s %q", EnvBackend, backend)
}
config := ExecutorConfig{
AssignmentID: os.Getenv(EnvAssignmentID), Capability: os.Getenv(EnvCapability),
Backend: backend, FacadeURL: os.Getenv(EnvFacadeURL), FacadeSPIFFEID: os.Getenv(EnvFacadeID),
RunnerBinary: os.Getenv("GITEA_RUNNER_BINARY"), ListenAddress: "127.0.0.1:0",
Stdout: os.Stdout, Stderr: os.Stderr,
}
if config.AssignmentID == "" || config.Capability == "" || config.FacadeURL == "" || config.FacadeSPIFFEID == "" {
return ExecutorConfig{}, errors.New("complete runner assignment and facade environment is required")
}
return config, nil
}
+56
View File
@@ -0,0 +1,56 @@
package runnerbootstrap
import (
"context"
"fmt"
"net/http"
"net/http/httputil"
"net/url"
"github.com/spiffe/go-spiffe/v2/spiffeid"
"github.com/spiffe/go-spiffe/v2/spiffetls/tlsconfig"
"github.com/spiffe/go-spiffe/v2/workloadapi"
)
type Proxy struct {
Handler http.Handler
source *workloadapi.X509Source
}
// NewProxy obtains rotating X509-SVIDs from the Workload API and authorizes
// one exact controller identity. The official Runner talks plain HTTP only to
// this executor-local handler.
func NewProxy(ctx context.Context, facadeURL, facadeSPIFFEID, workloadAPIAddr string) (*Proxy, error) {
target, err := url.Parse(facadeURL)
if err != nil || target.Scheme != "https" || target.Host == "" {
return nil, fmt.Errorf("runner facade URL must be an absolute https URL")
}
serverID, err := spiffeid.FromString(facadeSPIFFEID)
if err != nil {
return nil, fmt.Errorf("parse runner facade SPIFFE ID: %w", err)
}
options := []workloadapi.X509SourceOption{}
if workloadAPIAddr != "" {
options = append(options, workloadapi.WithClientOptions(workloadapi.WithAddr(workloadAPIAddr)))
}
source, err := workloadapi.NewX509Source(ctx, options...)
if err != nil {
return nil, fmt.Errorf("open SPIFFE Workload API X509 source: %w", err)
}
transport := http.DefaultTransport.(*http.Transport).Clone()
transport.TLSClientConfig = tlsconfig.MTLSClientConfig(source, source, tlsconfig.AuthorizeID(serverID))
return &Proxy{Handler: NewProxyHandler(target, transport), source: source}, nil
}
func NewProxyHandler(target *url.URL, transport http.RoundTripper) http.Handler {
proxy := httputil.NewSingleHostReverseProxy(target)
proxy.Transport = transport
return proxy
}
func (p *Proxy) Close() error {
if p == nil || p.source == nil {
return nil
}
return p.source.Close()
}
+41 -9
View File
@@ -34,11 +34,26 @@ type Metadata struct {
Annotations map[string]string
}
// LaunchSpec contains the durable resource metadata and the short-lived
// executor environment. Environment values configure the one-shot runner but
// are deliberately excluded from labels and annotations.
type LaunchSpec struct {
Metadata Metadata
Environment map[string]string
}
// Bootstrap produces assignment-scoped executor configuration. Implementations
// must be deterministic so a redelivery after controller restart creates the
// same credentials without storing another lifecycle record.
type Bootstrap interface {
Environment(taskassignment.Assignment) (map[string]string, error)
}
// Backend is implemented by the native Pod and OpenSandbox adapters.
// Every method must be idempotent.
type Backend interface {
Find(context.Context, string) (*Executor, error)
Create(context.Context, taskassignment.Assignment, Metadata) (*Executor, error)
Create(context.Context, taskassignment.Assignment, LaunchSpec) (*Executor, error)
BindIdentity(context.Context, *Executor, taskidentity.Identity) error
Delete(context.Context, *Executor) error
}
@@ -53,16 +68,17 @@ type TaskState interface {
// Worker has no correctness-critical in-memory state. Handle may be called
// again for the same assignment after any operation.
type Worker struct {
Backend Backend
Tasks TaskState
Backend Backend
Tasks TaskState
Bootstrap Bootstrap
}
// Accept completes the durable handoff from JetStream to the backend. Once it
// returns true, all recovery information exists in Kubernetes/OpenSandbox and
// the assignment message can be acknowledged immediately.
func (w Worker) Accept(ctx context.Context, assignment taskassignment.Assignment) (bool, error) {
if w.Backend == nil || w.Tasks == nil {
return false, errors.New("backend and Gitea task state are required")
if w.Backend == nil || w.Tasks == nil || w.Bootstrap == nil {
return false, errors.New("backend, Gitea task state, and runner bootstrap are required")
}
if assignment.ID == "" || assignment.Task == nil {
return false, errors.New("valid assignment is required")
@@ -84,7 +100,11 @@ func (w Worker) Accept(ctx context.Context, assignment taskassignment.Assignment
return true, nil
}
if executor == nil {
executor, err = w.Backend.Create(ctx, assignment, BackendMetadata(assignment))
launch, launchErr := w.launchSpec(assignment)
if launchErr != nil {
return false, launchErr
}
executor, err = w.Backend.Create(ctx, assignment, launch)
if err != nil {
return false, err
}
@@ -101,8 +121,8 @@ func (w Worker) Accept(ctx context.Context, assignment taskassignment.Assignment
// Handle performs one reconciliation. Done means the queue message may be
// acknowledged. A false result should remain pending and be reconciled again.
func (w Worker) Handle(ctx context.Context, assignment taskassignment.Assignment) (done bool, err error) {
if w.Backend == nil || w.Tasks == nil {
return false, errors.New("backend and Gitea task state are required")
if w.Backend == nil || w.Tasks == nil || w.Bootstrap == nil {
return false, errors.New("backend, Gitea task state, and runner bootstrap are required")
}
if assignment.ID == "" || assignment.Task == nil {
return false, errors.New("valid assignment is required")
@@ -126,7 +146,11 @@ func (w Worker) Handle(ctx context.Context, assignment taskassignment.Assignment
}
if executor == nil {
executor, err = w.Backend.Create(ctx, assignment, BackendMetadata(assignment))
launch, launchErr := w.launchSpec(assignment)
if launchErr != nil {
return false, launchErr
}
executor, err = w.Backend.Create(ctx, assignment, launch)
if err != nil {
return false, err
}
@@ -154,6 +178,14 @@ func (w Worker) Handle(ctx context.Context, assignment taskassignment.Assignment
}
}
func (w Worker) launchSpec(assignment taskassignment.Assignment) (LaunchSpec, error) {
environment, err := w.Bootstrap.Environment(assignment)
if err != nil {
return LaunchSpec{}, err
}
return LaunchSpec{Metadata: BackendMetadata(assignment), Environment: environment}, nil
}
// BackendMetadata is the shared metadata contract for Pods and OpenSandbox.
func BackendMetadata(assignment taskassignment.Assignment) Metadata {
return Metadata{
+11 -5
View File
@@ -17,8 +17,14 @@ type fakeBackend struct {
deleted int
}
type fakeBootstrap struct{}
func (fakeBootstrap) Environment(taskassignment.Assignment) (map[string]string, error) {
return map[string]string{"CI_RUNNER_CAPABILITY": "capability"}, nil
}
func (b *fakeBackend) Find(context.Context, string) (*Executor, error) { return b.executor, nil }
func (b *fakeBackend) Create(_ context.Context, _ taskassignment.Assignment, _ Metadata) (*Executor, error) {
func (b *fakeBackend) Create(_ context.Context, _ taskassignment.Assignment, _ LaunchSpec) (*Executor, error) {
b.created++
b.executor = &Executor{Name: "executor", IdentityTarget: "pod-uid", Phase: PhaseRunning}
return b.executor, nil
@@ -60,7 +66,7 @@ func assignment() taskassignment.Assignment {
func TestHandleRecoversExistingExecutorWithoutCreatingAnother(t *testing.T) {
backend := &fakeBackend{executor: &Executor{Name: "existing", IdentityTarget: "uid", Phase: PhaseRunning}}
worker := Worker{Backend: backend, Tasks: &fakeTasks{}}
worker := Worker{Backend: backend, Tasks: &fakeTasks{}, Bootstrap: fakeBootstrap{}}
done, err := worker.Handle(context.Background(), assignment())
if err != nil || done {
@@ -73,7 +79,7 @@ func TestHandleRecoversExistingExecutorWithoutCreatingAnother(t *testing.T) {
func TestAcceptAcknowledgesAfterBackendAndIdentityAreDurable(t *testing.T) {
backend := &fakeBackend{}
worker := Worker{Backend: backend, Tasks: &fakeTasks{}}
worker := Worker{Backend: backend, Tasks: &fakeTasks{}, Bootstrap: fakeBootstrap{}}
accepted, err := worker.Accept(context.Background(), assignment())
if err != nil || !accepted {
@@ -86,7 +92,7 @@ func TestAcceptAcknowledgesAfterBackendAndIdentityAreDurable(t *testing.T) {
func TestAcceptRetriesWhileBackendIdentityTargetIsUnavailable(t *testing.T) {
backend := &fakeBackend{executor: &Executor{Name: "pending", Phase: PhasePending}}
worker := Worker{Backend: backend, Tasks: &fakeTasks{}}
worker := Worker{Backend: backend, Tasks: &fakeTasks{}, Bootstrap: fakeBootstrap{}}
accepted, err := worker.Accept(context.Background(), assignment())
if err != nil || accepted {
@@ -100,7 +106,7 @@ func TestAcceptRetriesWhileBackendIdentityTargetIsUnavailable(t *testing.T) {
func TestHandleReportsBeforeCleanupAndBecomesRecoverable(t *testing.T) {
backend := &fakeBackend{executor: &Executor{Name: "finished", IdentityTarget: "uid", Phase: PhaseSucceeded}}
tasks := &fakeTasks{}
worker := Worker{Backend: backend, Tasks: tasks}
worker := Worker{Backend: backend, Tasks: tasks, Bootstrap: fakeBootstrap{}}
done, err := worker.Handle(context.Background(), assignment())
if err != nil || !done {