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 }