feat: bootstrap official runner through SPIFFE facade
This commit is contained in:
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user