Files
gitea-dynamic-runner/internal/runnerbootstrap/bootstrap.go
panxiao81 53732699df
test / shell (pull_request) Successful in 3m31s
test / python (pull_request) Failing after 10m54s
test / go (pull_request) Failing after 12m12s
fix: 解耦 runner 与 placement 实现
2026-09-25 19:03:13 +00:00

101 lines
3.5 KiB
Go

// 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"
EnvRunnerLabels = "CI_RUNNER_LABELS_JSON"
)
// 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
WorkloadAPIAddr string
}
func (b Bootstrap) Environment(assignment taskassignment.Assignment) (map[string]string, error) {
if assignment.ID == "" || assignment.Identity.SPIFFEID == "" || b.FacadeSPIFFEID == "" || b.WorkloadAPIAddr == "" {
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")
}
labels, err := json.Marshal([]string{"self-hosted", string(assignment.Placement.Class), string(assignment.Placement.Driver)})
if err != nil {
return nil, fmt.Errorf("encode runner labels: %w", err)
}
return map[string]string{
EnvAssignmentID: assignment.ID,
EnvCapability: capability,
EnvFacadeURL: b.FacadeURL,
EnvFacadeID: b.FacadeSPIFFEID,
EnvSPIFFEID: assignment.Identity.SPIFFEID,
EnvRunnerLabels: string(labels),
"SPIFFE_ENDPOINT_SOCKET": b.WorkloadAPIAddr,
}, 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, labels []string) ([]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 len(labels) == 0 {
return nil, errors.New("runner labels are required")
}
for _, label := range labels {
if label == "" {
return nil, errors.New("runner labels must not be empty")
}
}
registration := Registration{
Warning: "Generated for one preassigned task by gitea-dynamic-runner.",
UUID: assignmentID, Name: assignmentID, Token: capability,
Address: localProxyURL, Labels: append([]string(nil), labels...), Ephemeral: true,
}
data, err := json.MarshalIndent(registration, "", " ")
if err != nil {
return nil, err
}
return append(data, '\n'), nil
}