From f9e76211654d05819bf5cabbcb567aa996851051 Mon Sep 17 00:00:00 2001 From: Pietralberto Mazza <18440657+altafan@users.noreply.github.com> Date: Fri, 19 Apr 2024 17:11:59 +0200 Subject: [PATCH] Add grpc-gateway and /healthz endpoint (#133) * Add grpc-gateway and /healthz endpoint * Add nolint * nosec --- server/internal/interface/grpc/config.go | 9 +- .../interface/grpc/handlers/healthservice.go | 29 +++++ server/internal/interface/grpc/service.go | 120 +++++++++++++++++- 3 files changed, 144 insertions(+), 14 deletions(-) create mode 100644 server/internal/interface/grpc/handlers/healthservice.go diff --git a/server/internal/interface/grpc/config.go b/server/internal/interface/grpc/config.go index ef21062..7e28e11 100644 --- a/server/internal/interface/grpc/config.go +++ b/server/internal/interface/grpc/config.go @@ -32,13 +32,8 @@ func (c Config) address() string { return fmt.Sprintf(":%d", c.Port) } -func (c Config) listener() net.Listener { - lis, _ := net.Listen("tcp", c.address()) - - if c.insecure() { - return lis - } - return tls.NewListener(lis, c.tlsConfig()) +func (c Config) gatewayAddress() string { + return fmt.Sprintf("localhost:%d", c.Port) } func (c Config) tlsConfig() *tls.Config { diff --git a/server/internal/interface/grpc/handlers/healthservice.go b/server/internal/interface/grpc/handlers/healthservice.go new file mode 100644 index 0000000..b994ee6 --- /dev/null +++ b/server/internal/interface/grpc/handlers/healthservice.go @@ -0,0 +1,29 @@ +package handlers + +import ( + "context" + + grpchealth "google.golang.org/grpc/health/grpc_health_v1" +) + +type healthHandler struct{} + +func NewHealthHandler() grpchealth.HealthServer { + return &healthHandler{} +} + +func (h *healthHandler) Check( + _ context.Context, + _ *grpchealth.HealthCheckRequest, +) (*grpchealth.HealthCheckResponse, error) { + return &grpchealth.HealthCheckResponse{ + Status: grpchealth.HealthCheckResponse_SERVING, + }, nil +} + +func (h *healthHandler) Watch( + _ *grpchealth.HealthCheckRequest, + _ grpchealth.Health_WatchServer, +) error { + return nil +} diff --git a/server/internal/interface/grpc/service.go b/server/internal/interface/grpc/service.go index 0632ea1..f1ab1ef 100644 --- a/server/internal/interface/grpc/service.go +++ b/server/internal/interface/grpc/service.go @@ -1,22 +1,32 @@ package grpcservice import ( + "context" + "crypto/tls" "fmt" + "net/http" + "strings" arkv1 "github.com/ark-network/ark/api-spec/protobuf/gen/ark/v1" appconfig "github.com/ark-network/ark/internal/app-config" interfaces "github.com/ark-network/ark/internal/interface" "github.com/ark-network/ark/internal/interface/grpc/handlers" "github.com/ark-network/ark/internal/interface/grpc/interceptors" + "github.com/grpc-ecosystem/grpc-gateway/v2/runtime" log "github.com/sirupsen/logrus" + "golang.org/x/net/http2" + "golang.org/x/net/http2/h2c" "google.golang.org/grpc" + "google.golang.org/grpc/credentials" "google.golang.org/grpc/credentials/insecure" + grpchealth "google.golang.org/grpc/health/grpc_health_v1" + "google.golang.org/protobuf/encoding/protojson" ) type service struct { config Config appConfig *appconfig.Config - server *grpc.Server + server *http.Server } func NewService( @@ -36,16 +46,79 @@ func NewService( return nil, fmt.Errorf("tls termination not supported yet") } creds := insecure.NewCredentials() + if !svcConfig.insecure() { + creds = credentials.NewTLS(svcConfig.tlsConfig()) + } grpcConfig = append(grpcConfig, grpc.Creds(creds)) - server := grpc.NewServer(grpcConfig...) - handler := handlers.NewHandler(appConfig.AppService()) - arkv1.RegisterArkServiceServer(server, handler) + + // Server grpc. + grpcServer := grpc.NewServer(grpcConfig...) + appHandler := handlers.NewHandler(appConfig.AppService()) + arkv1.RegisterArkServiceServer(grpcServer, appHandler) + healthHandler := handlers.NewHealthHandler() + grpchealth.RegisterHealthServer(grpcServer, healthHandler) + + // Creds for grpc gateway reverse proxy. + gatewayCreds := insecure.NewCredentials() + if !svcConfig.insecure() { + gatewayCreds = credentials.NewTLS(&tls.Config{ + InsecureSkipVerify: true, // #nosec + }) + } + gatewayOpts := grpc.WithTransportCredentials(gatewayCreds) + ctx := context.Background() + conn, err := grpc.DialContext( + ctx, svcConfig.gatewayAddress(), gatewayOpts, + ) + if err != nil { + return nil, err + } + // Reverse proxy grpc-gateway. + gwmux := runtime.NewServeMux( + runtime.WithHealthzEndpoint(grpchealth.NewHealthClient(conn)), + runtime.WithMarshalerOption("application/json+pretty", &runtime.JSONPb{ + MarshalOptions: protojson.MarshalOptions{ + Indent: " ", + Multiline: true, + }, + UnmarshalOptions: protojson.UnmarshalOptions{ + DiscardUnknown: true, + }, + }), + ) + if err := arkv1.RegisterArkServiceHandler( + ctx, gwmux, conn, + ); err != nil { + return nil, err + } + grpcGateway := http.Handler(gwmux) + + handler := router(grpcServer, grpcGateway) + mux := http.NewServeMux() + mux.Handle("/", handler) + + httpServerHandler := http.Handler(mux) + if svcConfig.insecure() { + httpServerHandler = h2c.NewHandler(httpServerHandler, &http2.Server{}) + } + + server := &http.Server{ + Addr: svcConfig.address(), + Handler: httpServerHandler, + TLSConfig: svcConfig.tlsConfig(), + } + return &service{svcConfig, appConfig, server}, nil } func (s *service) Start() error { - // nolint:all - go s.server.Serve(s.config.listener()) + if s.config.insecure() { + // nolint:all + go s.server.ListenAndServe() + } else { + // nolint:all + go s.server.ListenAndServeTLS("", "") + } log.Infof("started listening at %s", s.config.address()) if err := s.appConfig.AppService().Start(); err != nil { @@ -57,8 +130,41 @@ func (s *service) Start() error { } func (s *service) Stop() { - s.server.Stop() + // nolint:all + s.server.Shutdown(context.Background()) log.Info("stopped grpc server") s.appConfig.AppService().Stop() log.Info("stopped app service") } + +func router( + grpcServer *grpc.Server, grpcGateway http.Handler, +) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if isOptionRequest(r) { + w.Header().Set("Access-Control-Allow-Origin", "*") + w.Header().Set("Access-Control-Allow-Headers", "*") + w.Header().Add("Access-Control-Allow-Methods", "POST, GET, OPTIONS") + return + } + + if isHttpRequest(r) { + w.Header().Set("Access-Control-Allow-Origin", "*") + w.Header().Set("Access-Control-Allow-Headers", "*") + w.Header().Add("Access-Control-Allow-Methods", "POST, GET, OPTIONS") + + grpcGateway.ServeHTTP(w, r) + return + } + grpcServer.ServeHTTP(w, r) + }) +} + +func isOptionRequest(req *http.Request) bool { + return req.Method == http.MethodOptions +} + +func isHttpRequest(req *http.Request) bool { + return req.Method == http.MethodGet || + strings.Contains(req.Header.Get("Content-Type"), "application/json") +}