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  }