// Package grpcserver implements the MCR gRPC admin API server. // // It provides the same business logic as the REST admin API in // internal/server/, using shared internal/db and internal/gc packages. // The server enforces TLS 1.3 minimum, auth via MCIAS token validation, // and admin role checks on privileged RPCs. package grpcserver import ( "crypto/tls" "fmt" "log" "net" "sync" "google.golang.org/grpc" "google.golang.org/grpc/credentials" pb "git.wntrmute.dev/kyle/mcr/gen/mcr/v1" "git.wntrmute.dev/kyle/mcr/internal/db" "git.wntrmute.dev/kyle/mcr/internal/gc" "git.wntrmute.dev/kyle/mcr/internal/policy" "git.wntrmute.dev/kyle/mcr/internal/server" ) // AuditFunc is a callback for recording audit events. It follows the same // signature as db.WriteAuditEvent but without an error return -- audit // failures should not block request processing. type AuditFunc func(eventType, actorID, repository, digest, ip string, details map[string]string) // Deps holds the dependencies injected into the gRPC server. type Deps struct { DB *db.DB Validator server.TokenValidator Engine PolicyReloader AuditFn AuditFunc Collector *gc.Collector } // PolicyReloader can reload policy rules from a store. type PolicyReloader interface { Reload(store policy.RuleStore) error } // GCStatus tracks the current state of garbage collection for the gRPC server. type GCStatus struct { mu sync.Mutex running bool lastRun *gcLastRun } type gcLastRun struct { StartedAt string CompletedAt string BlobsRemoved int BytesFreed int64 } // Server wraps a grpc.Server with MCR-specific configuration. type Server struct { gs *grpc.Server deps Deps gcStatus *GCStatus } // New creates a configured gRPC server with the interceptor chain: // [Request Logger] -> [Auth Interceptor] -> [Admin Interceptor] -> [Handler] // // The TLS config enforces TLS 1.3 minimum. If certFile or keyFile is // empty, the server is created without TLS (for testing only). func New(certFile, keyFile string, deps Deps) (*Server, error) { authInt := newAuthInterceptor(deps.Validator) adminInt := newAdminInterceptor() chain := grpc.ChainUnaryInterceptor( loggingInterceptor, authInt.unary, adminInt.unary, ) var opts []grpc.ServerOption opts = append(opts, chain) // Configure TLS if cert and key are provided. if certFile != "" && keyFile != "" { cert, err := tls.LoadX509KeyPair(certFile, keyFile) if err != nil { return nil, fmt.Errorf("grpcserver: load TLS cert: %w", err) } tlsCfg := &tls.Config{ Certificates: []tls.Certificate{cert}, MinVersion: tls.VersionTLS13, } opts = append(opts, grpc.Creds(credentials.NewTLS(tlsCfg))) } // The JSON codec is registered globally via init() in gen/mcr/v1/codec.go. // The client must use grpc.ForceCodecV2(mcrv1.JSONCodec{}) to match. _ = pb.JSONCodec{} // ensure the gen/mcr/v1 init() runs (codec registration) gs := grpc.NewServer(opts...) gcStatus := &GCStatus{} s := &Server{gs: gs, deps: deps, gcStatus: gcStatus} // Register all services. pb.RegisterRegistryServiceServer(gs, ®istryService{ db: deps.DB, collector: deps.Collector, gcStatus: gcStatus, auditFn: deps.AuditFn, }) pb.RegisterPolicyServiceServer(gs, &policyService{ db: deps.DB, engine: deps.Engine, auditFn: deps.AuditFn, }) pb.RegisterAuditServiceServer(gs, &auditService{ db: deps.DB, }) pb.RegisterAdminServiceServer(gs, &adminService{}) return s, nil } // Serve starts the gRPC server on the given listener. func (s *Server) Serve(lis net.Listener) error { log.Printf("grpc server listening on %s", lis.Addr()) return s.gs.Serve(lis) } // GracefulStop gracefully stops the gRPC server. func (s *Server) GracefulStop() { s.gs.GracefulStop() } // GRPCServer returns the underlying grpc.Server for testing. func (s *Server) GRPCServer() *grpc.Server { return s.gs }