From a9cf704f5191884082469f6d177dc36387dd012a Mon Sep 17 00:00:00 2001 From: Jeremy Lewi Date: Thu, 4 Apr 2024 19:59:47 -0700 Subject: [PATCH] Create a grpc gateway on the same port as the gin server. * Using the same port avoids CORS issues with the client --- app/pkg/server/server.go | 95 ++++++++++++++++++++++++- protos/foyle/v1alpha1/agent.proto | 9 ++- protos/go/foyle/v1alpha1/agent.pb.go | 27 +++---- protos/go/foyle/v1alpha1/agent.pb.gw.go | 12 ++-- 4 files changed, 119 insertions(+), 24 deletions(-) diff --git a/app/pkg/server/server.go b/app/pkg/server/server.go index 2b6e0872..5bd3c526 100644 --- a/app/pkg/server/server.go +++ b/app/pkg/server/server.go @@ -1,6 +1,7 @@ package server import ( + "context" "encoding/json" "fmt" "html/template" @@ -15,13 +16,18 @@ import ( "github.com/gin-gonic/gin" "github.com/go-logr/zapr" + "github.com/grpc-ecosystem/grpc-gateway/v2/runtime" "github.com/jlewi/foyle/app/pkg/config" "github.com/jlewi/foyle/app/pkg/executor" + "github.com/jlewi/foyle/app/pkg/logs" "github.com/jlewi/foyle/protos/go/foyle/v1alpha1" "github.com/pkg/errors" "go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc" "go.uber.org/zap" "google.golang.org/grpc" + "google.golang.org/grpc/connectivity" + "google.golang.org/grpc/credentials/insecure" + "google.golang.org/grpc/grpclog" "google.golang.org/grpc/health" "google.golang.org/grpc/health/grpc_health_v1" "google.golang.org/grpc/reflection" @@ -36,6 +42,7 @@ type Server struct { builtinExtensionPaths []string executor *executor.Executor + conn *grpc.ClientConn } // NewServer creates a new server @@ -62,7 +69,7 @@ func (s *Server) createGinEngine() error { log.Info("Setting up server") router := gin.Default() - router.GET("/healthz", healthCheck) + router.GET("/healthz", s.healthCheck) // Serve the static assets for vscode. // There should be several directories located in ${ASSETS_DIR}/vscode @@ -112,6 +119,7 @@ func (s *Server) createGinEngine() error { // The workbench endpoint serves the workbench.html page which is the main entry point for vscode for web router.GET("/workbench", s.handleGetWorkbench) + s.engine = router return nil } @@ -219,6 +227,9 @@ func (s *Server) Run() error { }() + if err := s.registerGRPCGatewayRoutes(); err != nil { + return err + } address := fmt.Sprintf("%s:%d", s.config.Server.BindAddress, s.config.Server.HttpPort) log.Info("Starting http server", "address", address) if err := http.ListenAndServe(address, s.engine); err != nil { @@ -249,6 +260,72 @@ func (s *Server) startGRPCServer(lis net.Listener) error { return s.grpcServer.Serve(lis) } +// registerGRPCGateway starts the gRPC gateway which provides a REST proxy to the grpc server. +func (s *Server) registerGRPCGatewayRoutes() error { + // TODO(jeremy): I think we could use a ctx with Cancel and then potentially trigger cancel to shutdown the + // connection. + ctx := context.Background() + + // Create a connection to the gRPC server + opts := []grpc.DialOption{grpc.WithTransportCredentials(insecure.NewCredentials())} + + log := zapr.NewLogger(zap.L()) + + grpcServerEndpoint := fmt.Sprintf("%s:%d", s.config.Server.BindAddress, s.config.Server.GRPCPort) + log.Info("Dialing grpc server", "endpoint", grpcServerEndpoint) + conn, err := grpc.DialContext(ctx, grpcServerEndpoint, opts...) + if err != nil { + return err + } + go func() { + <-ctx.Done() + if err := conn.Close(); err != nil { + grpclog.Errorf("failed to close connection to the gRPC server: %v", err) + } + }() + s.conn = conn + log.Info("Connected to grpc server", "connectionState", conn.GetState()) + + // TODO(jeremy): Should we add a handler for openapi spec; e.g. + // https://github.com/grpc-ecosystem/grpc-gateway/blob/10d49ec19ecab090aa3318245e3fe0d5db666c3f/examples/internal/gateway/main.go#L51C2-L51C49 + + gwMux := runtime.NewServeMux() + + if err := v1alpha1.RegisterExecuteServiceHandler(ctx, gwMux, conn); err != nil { + return err + } + + // Configure gin to delegate to the grpc gateway + handleFunc := func(c *gin.Context) { + log.V(logs.Debug).Info("Delegating request to grpc gateway") + gwMux.ServeHTTP(c.Writer, c.Request) + } + + // N.B since we want to to server our grpc gateway on the same port as our gin server + // we need to configure the gin server to delegate to the gateway mux for the appropriate routes. + // There currently doesn't seem to be anyway to do this programmatically. So if we add new routes we'd + // have to update the code here. + pathPrefix := "/api/v1alpha1" + + type method struct { + Method string + Path string + } + + methods := []method{ + {Method: http.MethodPost, Path: "execute"}, + {Method: http.MethodPost, Path: "generate"}, + } + + for _, m := range methods { + fullPath := pathPrefix + "/" + m.Path + log.Info("configuring gin to delegate to the grpc gateway", "path", fullPath, "methods", m.Method) + s.engine.Handle(m.Method, fullPath, handleFunc) + } + + return nil +} + // trapInterrupt shutdowns the server if the appropriate signals are sent func trapInterrupt(s *grpc.Server) { log := zapr.NewLogger(zap.L()) @@ -266,6 +343,18 @@ func trapInterrupt(s *grpc.Server) { }() } -func healthCheck(ctx *gin.Context) { - ctx.String(http.StatusOK, "foyle is healthy") +func (s *Server) healthCheck(ctx *gin.Context) { + // TODO(jeremy): We should return the version + connState := s.conn.GetState() + d := gin.H{ + "server": "foyle", + "status": "healthy", + "grpcConnectionState": connState.String(), + } + code := http.StatusOK + if connState != connectivity.Ready { + d["status"] = "unhealthy" + code = http.StatusServiceUnavailable + } + ctx.JSON(code, d) } diff --git a/protos/foyle/v1alpha1/agent.proto b/protos/foyle/v1alpha1/agent.proto index 82a3baba..5623ac1d 100644 --- a/protos/foyle/v1alpha1/agent.proto +++ b/protos/foyle/v1alpha1/agent.proto @@ -9,6 +9,11 @@ option go_package = "github.com/jlewi/foyle/protos/go/v1alpha1"; // https://github.com/grpc-ecosystem/grpc-gateway import "google/api/annotations.proto"; + +// IMPORTANT: +// If you update or add any new methods to this file then you need to update +// Server.registerGRPCGatewayRoutes to have gin delegate the appropriate requests to the grpc gateway server. + message GenerateRequest { Doc doc = 1; } @@ -22,7 +27,7 @@ service GenerateService { // Generate generates new cells given an existing document. rpc Generate (GenerateRequest) returns (GenerateResponse) { option (google.api.http) = { - post: "/v1alpha1/generate" + post: "/api/v1alpha1/generate" }; } } @@ -40,7 +45,7 @@ service ExecuteService { // Execute executes a cell in an existing document. rpc Execute(ExecuteRequest) returns (ExecuteResponse) { option (google.api.http) = { - post: "/v1alpha1/execute" + post: "/api/v1alpha1/execute" }; } } diff --git a/protos/go/foyle/v1alpha1/agent.pb.go b/protos/go/foyle/v1alpha1/agent.pb.go index abea3694..e3675c46 100644 --- a/protos/go/foyle/v1alpha1/agent.pb.go +++ b/protos/go/foyle/v1alpha1/agent.pb.go @@ -232,22 +232,23 @@ var file_foyle_v1alpha1_agent_proto_rawDesc = []byte{ 0x6b, 0x22, 0x39, 0x0a, 0x0f, 0x45, 0x78, 0x65, 0x63, 0x75, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x26, 0x0a, 0x07, 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0c, 0x2e, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x4f, 0x75, 0x74, - 0x70, 0x75, 0x74, 0x52, 0x07, 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x73, 0x32, 0x5e, 0x0a, 0x0f, + 0x70, 0x75, 0x74, 0x52, 0x07, 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x73, 0x32, 0x62, 0x0a, 0x0f, 0x47, 0x65, 0x6e, 0x65, 0x72, 0x61, 0x74, 0x65, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, - 0x4b, 0x0a, 0x08, 0x47, 0x65, 0x6e, 0x65, 0x72, 0x61, 0x74, 0x65, 0x12, 0x10, 0x2e, 0x47, 0x65, + 0x4f, 0x0a, 0x08, 0x47, 0x65, 0x6e, 0x65, 0x72, 0x61, 0x74, 0x65, 0x12, 0x10, 0x2e, 0x47, 0x65, 0x6e, 0x65, 0x72, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x11, 0x2e, 0x47, 0x65, 0x6e, 0x65, 0x72, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, - 0x22, 0x1a, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x14, 0x22, 0x12, 0x2f, 0x76, 0x31, 0x61, 0x6c, 0x70, - 0x68, 0x61, 0x31, 0x2f, 0x67, 0x65, 0x6e, 0x65, 0x72, 0x61, 0x74, 0x65, 0x32, 0x59, 0x0a, 0x0e, - 0x45, 0x78, 0x65, 0x63, 0x75, 0x74, 0x65, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x47, - 0x0a, 0x07, 0x45, 0x78, 0x65, 0x63, 0x75, 0x74, 0x65, 0x12, 0x0f, 0x2e, 0x45, 0x78, 0x65, 0x63, - 0x75, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x10, 0x2e, 0x45, 0x78, 0x65, - 0x63, 0x75, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x19, 0x82, 0xd3, - 0xe4, 0x93, 0x02, 0x13, 0x22, 0x11, 0x2f, 0x76, 0x31, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x31, 0x2f, - 0x65, 0x78, 0x65, 0x63, 0x75, 0x74, 0x65, 0x42, 0x2b, 0x5a, 0x29, 0x67, 0x69, 0x74, 0x68, 0x75, - 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x6a, 0x6c, 0x65, 0x77, 0x69, 0x2f, 0x66, 0x6f, 0x79, 0x6c, - 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x73, 0x2f, 0x67, 0x6f, 0x2f, 0x76, 0x31, 0x61, 0x6c, - 0x70, 0x68, 0x61, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x22, 0x1e, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x18, 0x22, 0x16, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, + 0x31, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x31, 0x2f, 0x67, 0x65, 0x6e, 0x65, 0x72, 0x61, 0x74, 0x65, + 0x32, 0x5d, 0x0a, 0x0e, 0x45, 0x78, 0x65, 0x63, 0x75, 0x74, 0x65, 0x53, 0x65, 0x72, 0x76, 0x69, + 0x63, 0x65, 0x12, 0x4b, 0x0a, 0x07, 0x45, 0x78, 0x65, 0x63, 0x75, 0x74, 0x65, 0x12, 0x0f, 0x2e, + 0x45, 0x78, 0x65, 0x63, 0x75, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x10, + 0x2e, 0x45, 0x78, 0x65, 0x63, 0x75, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x22, 0x1d, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x17, 0x22, 0x15, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, + 0x31, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x31, 0x2f, 0x65, 0x78, 0x65, 0x63, 0x75, 0x74, 0x65, 0x42, + 0x2b, 0x5a, 0x29, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x6a, 0x6c, + 0x65, 0x77, 0x69, 0x2f, 0x66, 0x6f, 0x79, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x73, + 0x2f, 0x67, 0x6f, 0x2f, 0x76, 0x31, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x31, 0x62, 0x06, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x33, } var ( diff --git a/protos/go/foyle/v1alpha1/agent.pb.gw.go b/protos/go/foyle/v1alpha1/agent.pb.gw.go index 8094ba0f..3e91914b 100644 --- a/protos/go/foyle/v1alpha1/agent.pb.gw.go +++ b/protos/go/foyle/v1alpha1/agent.pb.gw.go @@ -117,7 +117,7 @@ func RegisterGenerateServiceHandlerServer(ctx context.Context, mux *runtime.Serv inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) var err error var annotatedContext context.Context - annotatedContext, err = runtime.AnnotateIncomingContext(ctx, mux, req, "/.GenerateService/Generate", runtime.WithHTTPPathPattern("/v1alpha1/generate")) + annotatedContext, err = runtime.AnnotateIncomingContext(ctx, mux, req, "/.GenerateService/Generate", runtime.WithHTTPPathPattern("/api/v1alpha1/generate")) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return @@ -151,7 +151,7 @@ func RegisterExecuteServiceHandlerServer(ctx context.Context, mux *runtime.Serve inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) var err error var annotatedContext context.Context - annotatedContext, err = runtime.AnnotateIncomingContext(ctx, mux, req, "/.ExecuteService/Execute", runtime.WithHTTPPathPattern("/v1alpha1/execute")) + annotatedContext, err = runtime.AnnotateIncomingContext(ctx, mux, req, "/.ExecuteService/Execute", runtime.WithHTTPPathPattern("/api/v1alpha1/execute")) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return @@ -215,7 +215,7 @@ func RegisterGenerateServiceHandlerClient(ctx context.Context, mux *runtime.Serv inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) var err error var annotatedContext context.Context - annotatedContext, err = runtime.AnnotateContext(ctx, mux, req, "/.GenerateService/Generate", runtime.WithHTTPPathPattern("/v1alpha1/generate")) + annotatedContext, err = runtime.AnnotateContext(ctx, mux, req, "/.GenerateService/Generate", runtime.WithHTTPPathPattern("/api/v1alpha1/generate")) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return @@ -235,7 +235,7 @@ func RegisterGenerateServiceHandlerClient(ctx context.Context, mux *runtime.Serv } var ( - pattern_GenerateService_Generate_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"v1alpha1", "generate"}, "")) + pattern_GenerateService_Generate_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"api", "v1alpha1", "generate"}, "")) ) var ( @@ -286,7 +286,7 @@ func RegisterExecuteServiceHandlerClient(ctx context.Context, mux *runtime.Serve inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) var err error var annotatedContext context.Context - annotatedContext, err = runtime.AnnotateContext(ctx, mux, req, "/.ExecuteService/Execute", runtime.WithHTTPPathPattern("/v1alpha1/execute")) + annotatedContext, err = runtime.AnnotateContext(ctx, mux, req, "/.ExecuteService/Execute", runtime.WithHTTPPathPattern("/api/v1alpha1/execute")) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return @@ -306,7 +306,7 @@ func RegisterExecuteServiceHandlerClient(ctx context.Context, mux *runtime.Serve } var ( - pattern_ExecuteService_Execute_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"v1alpha1", "execute"}, "")) + pattern_ExecuteService_Execute_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"api", "v1alpha1", "execute"}, "")) ) var (