github.com/authzed/spicedb@v1.32.1-0.20240520085336-ebda56537386/pkg/cmd/devtools.go (about)

     1  package cmd
     2  
     3  import (
     4  	"context"
     5  	"errors"
     6  	"fmt"
     7  	"os"
     8  	"os/signal"
     9  	"time"
    10  
    11  	v0 "github.com/authzed/authzed-go/proto/authzed/api/v0"
    12  	"github.com/authzed/grpcutil"
    13  	"github.com/aws/aws-sdk-go/aws"
    14  	"github.com/aws/aws-sdk-go/aws/credentials"
    15  	"github.com/go-logr/zerologr"
    16  	grpclog "github.com/grpc-ecosystem/go-grpc-middleware/v2/interceptors/logging"
    17  	"github.com/jzelinskie/cobrautil/v2"
    18  	"github.com/jzelinskie/cobrautil/v2/cobragrpc"
    19  	"github.com/jzelinskie/cobrautil/v2/cobrahttp"
    20  	"github.com/jzelinskie/stringz"
    21  	"github.com/spf13/cobra"
    22  	"go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc"
    23  	"google.golang.org/grpc"
    24  	healthpb "google.golang.org/grpc/health/grpc_health_v1"
    25  	"google.golang.org/grpc/reflection"
    26  
    27  	log "github.com/authzed/spicedb/internal/logging"
    28  	v0svc "github.com/authzed/spicedb/internal/services/v0"
    29  	"github.com/authzed/spicedb/pkg/cmd/server"
    30  	"github.com/authzed/spicedb/pkg/cmd/termination"
    31  )
    32  
    33  func RegisterDevtoolsFlags(cmd *cobra.Command) {
    34  	grpcServiceBuilder().RegisterFlags(cmd.Flags())
    35  	httpMetricsServiceBuilder().RegisterFlags(cmd.Flags())
    36  	httpDownloadServiceBuilder().RegisterFlags(cmd.Flags())
    37  
    38  	cmd.Flags().String("share-store", "inmemory", "kind of share store to use")
    39  	cmd.Flags().String("share-store-salt", "", "salt for share store hashing")
    40  	cmd.Flags().String("s3-access-key", "", "s3 access key for s3 share store")
    41  	cmd.Flags().String("s3-secret-key", "", "s3 secret key for s3 share store")
    42  	cmd.Flags().String("s3-bucket", "", "s3 bucket name for s3 share store")
    43  	cmd.Flags().String("s3-endpoint", "", "s3 endpoint for s3 share store")
    44  	cmd.Flags().String("s3-region", "auto", "s3 region for s3 share store")
    45  }
    46  
    47  func NewDevtoolsCommand(programName string) *cobra.Command {
    48  	return &cobra.Command{
    49  		Use:     "serve-devtools",
    50  		Short:   "runs the developer tools service",
    51  		Long:    "Serves the authzed.api.v0.DeveloperService which is used for development tooling such as the Authzed Playground",
    52  		PreRunE: server.DefaultPreRunE(programName),
    53  		RunE:    termination.PublishError(runfunc),
    54  		Args:    cobra.ExactArgs(0),
    55  	}
    56  }
    57  
    58  func runfunc(cmd *cobra.Command, _ []string) error {
    59  	grpcUnaryInterceptor, _ := server.GRPCMetrics(false)
    60  	grpcBuilder := grpcServiceBuilder()
    61  	grpcServer, err := grpcBuilder.ServerFromFlags(cmd,
    62  		grpc.StatsHandler(otelgrpc.NewServerHandler()),
    63  		grpc.ChainUnaryInterceptor(
    64  			grpclog.UnaryServerInterceptor(server.InterceptorLogger(log.Logger)),
    65  			grpcUnaryInterceptor,
    66  		))
    67  	if err != nil {
    68  		log.Ctx(cmd.Context()).Fatal().Err(err).Msg("failed to create gRPC server")
    69  	}
    70  
    71  	shareStore, err := shareStoreFromCmd(cmd)
    72  	if err != nil {
    73  		log.Ctx(cmd.Context()).Fatal().Err(err).Msg("failed to configure share store")
    74  	}
    75  
    76  	registerDeveloperGrpcServices(grpcServer, shareStore)
    77  
    78  	go func() {
    79  		if err := grpcBuilder.ListenFromFlags(cmd, grpcServer); err != nil {
    80  			log.Ctx(cmd.Context()).Warn().Err(err).Msg("gRPC service did not shutdown cleanly")
    81  		}
    82  	}()
    83  
    84  	// Start the metrics endpoint.
    85  	metricsHTTP := httpMetricsServiceBuilder()
    86  	metricsSrv := metricsHTTP.ServerFromFlags(cmd)
    87  	go func() {
    88  		if err := metricsHTTP.ListenFromFlags(cmd, metricsSrv); err != nil {
    89  			log.Ctx(cmd.Context()).Fatal().Err(err).Msg("failed while serving metrics")
    90  		}
    91  	}()
    92  
    93  	// start the http download api
    94  	downloadHTTP := httpDownloadServiceBuilder(cobrahttp.WithHandler(v0svc.DownloadHandler(shareStore)))
    95  	downloadSrv := downloadHTTP.ServerFromFlags(cmd)
    96  	downloadSrv.ReadHeaderTimeout = 5 * time.Second
    97  	go func() {
    98  		if err := downloadHTTP.ListenFromFlags(cmd, downloadSrv); err != nil {
    99  			log.Ctx(cmd.Context()).Fatal().Err(err).Msg("failed while serving download http api")
   100  		}
   101  	}()
   102  	signalctx, _ := signal.NotifyContext(context.Background(), os.Interrupt)
   103  	<-signalctx.Done()
   104  	log.Ctx(cmd.Context()).Info().Msg("received interrupt")
   105  	grpcServer.GracefulStop()
   106  	if err := metricsSrv.Close(); err != nil {
   107  		log.Ctx(cmd.Context()).Err(err).Msg("failed while shutting down metrics server")
   108  		return err
   109  	}
   110  	if err := downloadSrv.Close(); err != nil {
   111  		log.Ctx(cmd.Context()).Err(err).Msg("failed while shutting down download server")
   112  		return err
   113  	}
   114  
   115  	return nil
   116  }
   117  
   118  func httpDownloadServiceBuilder(option ...cobrahttp.Option) *cobrahttp.Builder {
   119  	option = append(option,
   120  		cobrahttp.WithLogger(zerologr.New(&log.Logger)),
   121  		cobrahttp.WithFlagPrefix("http"))
   122  	return cobrahttp.New("download", option...)
   123  }
   124  
   125  func httpMetricsServiceBuilder() *cobrahttp.Builder {
   126  	return cobrahttp.New("metrics",
   127  		cobrahttp.WithLogger(zerologr.New(&log.Logger)),
   128  		cobrahttp.WithFlagPrefix("metrics"),
   129  		cobrahttp.WithHandler(server.MetricsHandler(server.DisableTelemetryHandler, nil)),
   130  	)
   131  }
   132  
   133  func grpcServiceBuilder() *cobragrpc.Builder {
   134  	return cobragrpc.New("grpc",
   135  		cobragrpc.WithLogger(zerologr.New(&log.Logger)),
   136  		cobragrpc.WithFlagPrefix("grpc"),
   137  		cobragrpc.WithDefaultEnabled(true),
   138  	)
   139  }
   140  
   141  func shareStoreFromCmd(cmd *cobra.Command) (v0svc.ShareStore, error) {
   142  	shareStoreSalt := cobrautil.MustGetStringExpanded(cmd, "share-store-salt")
   143  	shareStoreKind := cobrautil.MustGetStringExpanded(cmd, "share-store")
   144  	event := log.Ctx(cmd.Context()).Info()
   145  
   146  	var shareStore v0svc.ShareStore
   147  	switch shareStoreKind {
   148  	case "inmemory":
   149  		shareStore = v0svc.NewInMemoryShareStore(shareStoreSalt)
   150  
   151  	case "s3":
   152  		bucketName := cobrautil.MustGetStringExpanded(cmd, "s3-bucket")
   153  		accessKey := cobrautil.MustGetStringExpanded(cmd, "s3-access-key")
   154  		secretKey := cobrautil.MustGetStringExpanded(cmd, "s3-secret-key")
   155  		endpoint := cobrautil.MustGetStringExpanded(cmd, "s3-endpoint")
   156  		region := stringz.DefaultEmpty(cobrautil.MustGetStringExpanded(cmd, "s3-region"), "auto")
   157  
   158  		optsNames := []string{"s3-bucket", "s3-access-key", "s3-secret-key", "s3-endpoint"}
   159  		opts := []string{bucketName, accessKey, secretKey, endpoint}
   160  		if i := stringz.SliceIndex(opts, ""); i >= 0 {
   161  			return nil, fmt.Errorf("missing required field: %s", optsNames[i])
   162  		}
   163  
   164  		config := &aws.Config{
   165  			Credentials: credentials.NewStaticCredentials(
   166  				accessKey,
   167  				secretKey,
   168  				"",
   169  			),
   170  			Endpoint: aws.String(endpoint),
   171  			Region:   aws.String(region),
   172  		}
   173  
   174  		var err error
   175  		shareStore, err = v0svc.NewS3ShareStore(bucketName, shareStoreSalt, config)
   176  		if err != nil {
   177  			return nil, fmt.Errorf("failed to create S3 share store: %w", err)
   178  		}
   179  
   180  		event = event.Str("endpoint", endpoint).Str("region", region).Str("bucketName", bucketName).Str("accessKey", accessKey)
   181  
   182  	default:
   183  		return nil, errors.New("unknown share store")
   184  	}
   185  
   186  	event.Str("kind", shareStoreKind).Msg("configured share store")
   187  	return shareStore, nil
   188  }
   189  
   190  func registerDeveloperGrpcServices(srv *grpc.Server, shareStore v0svc.ShareStore) {
   191  	healthSrv := grpcutil.NewAuthlessHealthServer()
   192  
   193  	v0.RegisterDeveloperServiceServer(srv, v0svc.NewDeveloperServer(shareStore))
   194  	healthSrv.SetServingStatus("DeveloperService", healthpb.HealthCheckResponse_SERVING)
   195  
   196  	healthpb.RegisterHealthServer(srv, healthSrv)
   197  	reflection.Register(srv)
   198  }