feat: run official executor behind SPIFFE mTLS
This commit is contained in:
@@ -68,10 +68,14 @@ func (f *Facade) Register(context.Context, *connect.Request[runnerv1.RegisterReq
|
||||
}
|
||||
|
||||
func (f *Facade) Declare(ctx context.Context, request *connect.Request[runnerv1.DeclareRequest]) (*connect.Response[runnerv1.DeclareResponse], error) {
|
||||
if _, _, err := f.authenticate(ctx, request); err != nil {
|
||||
assignmentID, _, err := f.authenticate(ctx, request)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return connect.NewResponse(&runnerv1.DeclareResponse{}), nil
|
||||
return connect.NewResponse(&runnerv1.DeclareResponse{Runner: &runnerv1.Runner{
|
||||
Uuid: assignmentID, Name: assignmentID, Status: runnerv1.RunnerStatus_RUNNER_STATUS_IDLE,
|
||||
Version: request.Msg.GetVersion(), Labels: append([]string(nil), request.Msg.GetLabels()...), Ephemeral: true,
|
||||
}}), nil
|
||||
}
|
||||
|
||||
func (f *Facade) FetchTask(ctx context.Context, request *connect.Request[runnerv1.FetchTaskRequest]) (*connect.Response[runnerv1.FetchTaskResponse], error) {
|
||||
|
||||
@@ -2,11 +2,17 @@ package runnerfacade
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"testing"
|
||||
|
||||
"connectrpc.com/connect"
|
||||
"gitea.dev/actionslib/pkg/protocol"
|
||||
runnerv1 "gitea.dev/actionslib/runner/v1"
|
||||
"gitea.dev/actionslib/runner/v1/runnerv1connect"
|
||||
"google.golang.org/protobuf/types/known/structpb"
|
||||
|
||||
"git.ddupan.top/panxiao81/gitea-dynamic-runner/internal/taskassignment"
|
||||
@@ -81,6 +87,49 @@ func TestFacadeReturnsOnlyPreassignedTaskAndSignalsClaim(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestFacadeDeclareReturnsOfficialRunnerMetadata(t *testing.T) {
|
||||
facade, assignment, token := testFacade(t)
|
||||
ctx := WithSPIFFEID(context.Background(), assignment.Identity.SPIFFEID)
|
||||
response, err := facade.Declare(ctx, authenticatedRequest(&runnerv1.DeclareRequest{
|
||||
Version: "v3.5.0", Labels: []string{"self-hosted", "pod"},
|
||||
}, assignment.ID, token))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
runner := response.Msg.GetRunner()
|
||||
if runner.GetUuid() != assignment.ID || runner.GetName() != assignment.ID || runner.GetVersion() != "v3.5.0" || !runner.GetEphemeral() {
|
||||
t.Fatalf("runner = %#v", runner)
|
||||
}
|
||||
if len(runner.GetLabels()) != 2 || runner.GetLabels()[0] != "self-hosted" || runner.GetLabels()[1] != "pod" {
|
||||
t.Fatalf("labels = %#v", runner.GetLabels())
|
||||
}
|
||||
}
|
||||
|
||||
func TestAPIHandlerMatchesOfficialRunnerBasePath(t *testing.T) {
|
||||
facade, assignment, token := testFacade(t)
|
||||
identityURL, err := url.Parse(assignment.Identity.SPIFFEID)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
handler := APIHandler(facade)
|
||||
server := httptest.NewServer(http.HandlerFunc(func(response http.ResponseWriter, request *http.Request) {
|
||||
request.TLS = &tls.ConnectionState{PeerCertificates: []*x509.Certificate{{URIs: []*url.URL{identityURL}}}}
|
||||
handler.ServeHTTP(response, request)
|
||||
}))
|
||||
defer server.Close()
|
||||
client := runnerv1connect.NewRunnerServiceClient(server.Client(), server.URL+APIBasePath)
|
||||
request := authenticatedRequest(&runnerv1.DeclareRequest{
|
||||
Version: "v3.5.0", Labels: []string{"self-hosted", "pod"},
|
||||
}, assignment.ID, token)
|
||||
response, err := client.Declare(context.Background(), request)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if response.Msg.GetRunner().GetUuid() != assignment.ID {
|
||||
t.Fatalf("runner = %#v", response.Msg.GetRunner())
|
||||
}
|
||||
}
|
||||
|
||||
func TestFacadeRejectsWrongIdentityOrCapability(t *testing.T) {
|
||||
facade, assignment, token := testFacade(t)
|
||||
wrongIdentity := WithSPIFFEID(context.Background(), "spiffe://ddupan.top/ci/owner/repo/other")
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
package runnerfacade
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/spiffe/go-spiffe/v2/spiffeid"
|
||||
"github.com/spiffe/go-spiffe/v2/spiffetls/tlsconfig"
|
||||
"github.com/spiffe/go-spiffe/v2/workloadapi"
|
||||
)
|
||||
|
||||
const APIBasePath = "/api/actions"
|
||||
|
||||
// APIHandler exposes the facade at the base path used by the official Runner.
|
||||
// SPIFFE middleware runs after the TLS listener has authenticated the peer.
|
||||
func APIHandler(facade *Facade) http.Handler {
|
||||
path, handler := facade.Handler()
|
||||
mux := http.NewServeMux()
|
||||
mux.Handle(APIBasePath+path, http.StripPrefix(APIBasePath, SPIFFEMiddleware(handler)))
|
||||
return mux
|
||||
}
|
||||
|
||||
type Server struct {
|
||||
Facade *Facade
|
||||
ListenAddress string
|
||||
TrustDomain string
|
||||
WorkloadAPIAddr string
|
||||
}
|
||||
|
||||
// Run serves the RunnerService facade with workload-to-workload mTLS. Any
|
||||
// identity in the local trust domain may complete TLS; the facade then requires
|
||||
// the exact logical task identity stored in its assignment registry.
|
||||
func (s Server) Run(ctx context.Context) error {
|
||||
if s.Facade == nil || s.ListenAddress == "" || s.TrustDomain == "" {
|
||||
return errors.New("runner facade, listen address, and trust domain are required")
|
||||
}
|
||||
trustDomain, err := spiffeid.TrustDomainFromString(s.TrustDomain)
|
||||
if err != nil {
|
||||
return fmt.Errorf("parse facade trust domain: %w", err)
|
||||
}
|
||||
options := []workloadapi.X509SourceOption{}
|
||||
if s.WorkloadAPIAddr != "" {
|
||||
options = append(options, workloadapi.WithClientOptions(workloadapi.WithAddr(s.WorkloadAPIAddr)))
|
||||
}
|
||||
source, err := workloadapi.NewX509Source(ctx, options...)
|
||||
if err != nil {
|
||||
return fmt.Errorf("open facade SPIFFE Workload API X509 source: %w", err)
|
||||
}
|
||||
defer source.Close()
|
||||
|
||||
listener, err := net.Listen("tcp", s.ListenAddress)
|
||||
if err != nil {
|
||||
return fmt.Errorf("listen for runner facade: %w", err)
|
||||
}
|
||||
defer listener.Close()
|
||||
tlsListener := tls.NewListener(listener, tlsconfig.MTLSServerConfig(
|
||||
source, source, tlsconfig.AuthorizeMemberOf(trustDomain),
|
||||
))
|
||||
httpServer := &http.Server{Handler: APIHandler(s.Facade), ReadHeaderTimeout: 10 * time.Second}
|
||||
serverErrors := make(chan error, 1)
|
||||
go func() { serverErrors <- httpServer.Serve(tlsListener) }()
|
||||
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
shutdownContext, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
shutdownErr := httpServer.Shutdown(shutdownContext)
|
||||
cancel()
|
||||
serverErr := <-serverErrors
|
||||
if errors.Is(serverErr, http.ErrServerClosed) {
|
||||
serverErr = nil
|
||||
}
|
||||
return errors.Join(shutdownErr, serverErr)
|
||||
case err := <-serverErrors:
|
||||
if errors.Is(err, http.ErrServerClosed) {
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("serve runner facade: %w", err)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user