// 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 }