github.com/authzed/spicedb@v1.32.1-0.20240520085336-ebda56537386/internal/gateway/gateway.go (about) 1 // Package gateway implements an HTTP server that forwards JSON requests to 2 // an upstream SpiceDB gRPC server. 3 package gateway 4 5 import ( 6 "context" 7 "fmt" 8 "io" 9 "net/http" 10 11 "github.com/authzed/authzed-go/proto" 12 v1 "github.com/authzed/authzed-go/proto/authzed/api/v1" 13 "github.com/authzed/grpcutil" 14 "github.com/grpc-ecosystem/grpc-gateway/v2/runtime" 15 "github.com/prometheus/client_golang/prometheus" 16 "github.com/prometheus/client_golang/prometheus/promauto" 17 "github.com/prometheus/client_golang/prometheus/promhttp" 18 "go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc" 19 "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" 20 "go.opentelemetry.io/otel" 21 "go.opentelemetry.io/otel/propagation" 22 "google.golang.org/grpc" 23 "google.golang.org/grpc/credentials/insecure" 24 healthpb "google.golang.org/grpc/health/grpc_health_v1" 25 "google.golang.org/grpc/metadata" 26 ) 27 28 var histogram = promauto.NewHistogramVec(prometheus.HistogramOpts{ 29 Namespace: "spicedb", 30 Subsystem: "rest_gateway", 31 Name: "request_duration_seconds", 32 Help: "A histogram of the duration spent processing requests to the SpiceDB REST Gateway.", 33 }, []string{"method"}) 34 35 // NewHandler creates an REST gateway HTTP CloserHandler with the provided upstream 36 // configuration. 37 func NewHandler(ctx context.Context, upstreamAddr, upstreamTLSCertPath string) (*CloserHandler, error) { 38 if upstreamAddr == "" { 39 return nil, fmt.Errorf("upstreamAddr must not be empty") 40 } 41 42 opts := []grpc.DialOption{ 43 grpc.WithStatsHandler(otelgrpc.NewClientHandler()), 44 } 45 if upstreamTLSCertPath == "" { 46 opts = append(opts, grpc.WithTransportCredentials(insecure.NewCredentials())) 47 } else { 48 certsOpt, err := grpcutil.WithCustomCerts(grpcutil.SkipVerifyCA, upstreamTLSCertPath) 49 if err != nil { 50 return nil, err 51 } 52 opts = append(opts, certsOpt) 53 } 54 55 healthConn, err := grpc.DialContext(ctx, upstreamAddr, opts...) 56 if err != nil { 57 return nil, err 58 } 59 60 gwMux := runtime.NewServeMux(runtime.WithMetadata(OtelAnnotator), runtime.WithHealthzEndpoint(healthpb.NewHealthClient(healthConn))) 61 schemaConn, err := registerHandler(ctx, gwMux, upstreamAddr, opts, v1.RegisterSchemaServiceHandler) 62 if err != nil { 63 return nil, err 64 } 65 66 permissionsConn, err := registerHandler(ctx, gwMux, upstreamAddr, opts, v1.RegisterPermissionsServiceHandler) 67 if err != nil { 68 return nil, err 69 } 70 71 watchConn, err := registerHandler(ctx, gwMux, upstreamAddr, opts, v1.RegisterWatchServiceHandler) 72 if err != nil { 73 return nil, err 74 } 75 76 experimentalConn, err := registerHandler(ctx, gwMux, upstreamAddr, opts, v1.RegisterExperimentalServiceHandler) 77 if err != nil { 78 return nil, err 79 } 80 81 mux := http.NewServeMux() 82 mux.Handle("/openapi.json", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 83 _, _ = io.WriteString(w, proto.OpenAPISchema) 84 })) 85 mux.Handle("/", gwMux) 86 87 finalHandler := promhttp.InstrumentHandlerDuration(histogram, otelhttp.NewHandler(mux, "gateway")) 88 return newCloserHandler(finalHandler, schemaConn, permissionsConn, watchConn, healthConn, experimentalConn), nil 89 } 90 91 // CloserHandler is a http.Handler and a io.Closer. Meant to keep track of resources to closer 92 // for a handler. 93 type CloserHandler struct { 94 closers []io.Closer 95 delegate http.Handler 96 } 97 98 func (cdh CloserHandler) ServeHTTP(writer http.ResponseWriter, request *http.Request) { 99 cdh.delegate.ServeHTTP(writer, request) 100 } 101 102 // newCloserHandler creates a new delegated http.Handler that will keep track of io.Closer to closer 103 func newCloserHandler(delegate http.Handler, closers ...io.Closer) *CloserHandler { 104 return &CloserHandler{ 105 closers: closers, 106 delegate: delegate, 107 } 108 } 109 110 func (cdh CloserHandler) Close() error { 111 for _, closer := range cdh.closers { 112 if err := closer.Close(); err != nil { 113 return err 114 } 115 } 116 return nil 117 } 118 119 // HandlerRegisterer is a function that registers a Gateway Handler in a ServeMux 120 type HandlerRegisterer func(ctx context.Context, mux *runtime.ServeMux, conn *grpc.ClientConn) error 121 122 // registerHandler will open a connection with the provided grpc.DialOptions against the endpoint, and 123 // will use it to invoke an HTTP Gateway handler factory method HandlerRegisterer. It returns the gRPC 124 // connection. 125 // 126 // gRPC generated code does not expose a means to close the opened connections other than implicitly via 127 // context cancellation. This factory method makes closing them explicit. 128 func registerHandler(ctx context.Context, mux *runtime.ServeMux, endpoint string, opts []grpc.DialOption, 129 registerer HandlerRegisterer, 130 ) (*grpc.ClientConn, error) { 131 conn, err := grpc.Dial(endpoint, opts...) 132 if err != nil { 133 return nil, err 134 } 135 if err := registerer(ctx, mux, conn); err != nil { 136 if connerr := conn.Close(); connerr != nil { 137 return nil, err 138 } 139 return nil, err 140 } 141 142 return conn, nil 143 } 144 145 var defaultOtelOpts = []otelgrpc.Option{ 146 otelgrpc.WithPropagators(otel.GetTextMapPropagator()), 147 otelgrpc.WithTracerProvider(otel.GetTracerProvider()), 148 } 149 150 // OtelAnnotator propagates the OpenTelemetry tracing context to the outgoing 151 // gRPC metadata. 152 func OtelAnnotator(ctx context.Context, r *http.Request) metadata.MD { 153 requestMetadata, _ := metadata.FromOutgoingContext(ctx) 154 metadataCopy := requestMetadata.Copy() 155 156 ctx = otel.GetTextMapPropagator().Extract(ctx, propagation.HeaderCarrier(r.Header)) 157 otelgrpc.Inject(ctx, &metadataCopy, defaultOtelOpts...) 158 return metadataCopy 159 }