mirror of
https://github.com/zitadel/zitadel.git
synced 2025-08-11 21:07:31 +00:00
feat: exchange gRPC server implementation to connectRPC (#10145)
# Which Problems Are Solved The current maintained gRPC server in combination with a REST (grpc) gateway is getting harder and harder to maintain. Additionally, there have been and still are issues with supporting / displaying `oneOf`s correctly. We therefore decided to exchange the server implementation to connectRPC, which apart from supporting connect as protocol, also also "standard" gRCP clients as well as HTTP/1.1 / rest like clients, e.g. curl directly call the server without any additional gateway. # How the Problems Are Solved - All v2 services are moved to connectRPC implementation. (v1 services are still served as pure grpc servers) - All gRPC server interceptors were migrated / copied to a corresponding connectRPC interceptor. - API.ListGrpcServices and API. ListGrpcMethods were changed to include the connect services and endpoints. - gRPC server reflection was changed to a `StaticReflector` using the `ListGrpcServices` list. - The `grpc.Server` interfaces was split into different combinations to be able to handle the different cases (grpc server and prefixed gateway, connect server with grpc gateway, connect server only, ...) - Docs of services serving connectRPC only with no additional gateway (instance, webkey, project, app, org v2 beta) are changed to expose that - since the plugin is not yet available on buf, we download it using `postinstall` hook of the docs # Additional Changes - WebKey service is added as v2 service (in addition to the current v2beta) # Additional Context closes #9483 --------- Co-authored-by: Elio Bischof <elio@zitadel.com>
This commit is contained in:
@@ -7,16 +7,18 @@ import (
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"connectrpc.com/grpcreflect"
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/improbable-eng/grpc-web/go/grpcweb"
|
||||
"github.com/zitadel/logging"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/health"
|
||||
healthpb "google.golang.org/grpc/health/grpc_health_v1"
|
||||
"google.golang.org/grpc/reflection"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/api/authz"
|
||||
grpc_api "github.com/zitadel/zitadel/internal/api/grpc"
|
||||
"github.com/zitadel/zitadel/internal/api/grpc/server"
|
||||
"github.com/zitadel/zitadel/internal/api/grpc/server/connect_middleware"
|
||||
http_util "github.com/zitadel/zitadel/internal/api/http"
|
||||
http_mw "github.com/zitadel/zitadel/internal/api/http/middleware"
|
||||
"github.com/zitadel/zitadel/internal/api/ui/login"
|
||||
@@ -24,10 +26,16 @@ import (
|
||||
"github.com/zitadel/zitadel/internal/telemetry/metrics"
|
||||
"github.com/zitadel/zitadel/internal/telemetry/tracing"
|
||||
"github.com/zitadel/zitadel/internal/zerrors"
|
||||
system_pb "github.com/zitadel/zitadel/pkg/grpc/system"
|
||||
)
|
||||
|
||||
var (
|
||||
metricTypes = []metrics.MetricType{metrics.MetricTypeTotalCount, metrics.MetricTypeRequestCount, metrics.MetricTypeStatusCode}
|
||||
)
|
||||
|
||||
type API struct {
|
||||
port uint16
|
||||
externalDomain string
|
||||
grpcServer *grpc.Server
|
||||
verifier authz.APITokenVerifier
|
||||
health healthCheck
|
||||
@@ -37,16 +45,23 @@ type API struct {
|
||||
healthServer *health.Server
|
||||
accessInterceptor *http_mw.AccessInterceptor
|
||||
queries *query.Queries
|
||||
authConfig authz.Config
|
||||
systemAuthZ authz.Config
|
||||
connectServices map[string][]string
|
||||
}
|
||||
|
||||
func (a *API) ListGrpcServices() []string {
|
||||
serviceInfo := a.grpcServer.GetServiceInfo()
|
||||
services := make([]string, len(serviceInfo))
|
||||
services := make([]string, len(serviceInfo)+len(a.connectServices))
|
||||
i := 0
|
||||
for servicename := range serviceInfo {
|
||||
services[i] = servicename
|
||||
i++
|
||||
}
|
||||
for prefix := range a.connectServices {
|
||||
services[i] = strings.Trim(prefix, "/")
|
||||
i++
|
||||
}
|
||||
sort.Strings(services)
|
||||
return services
|
||||
}
|
||||
@@ -59,6 +74,11 @@ func (a *API) ListGrpcMethods() []string {
|
||||
methods = append(methods, "/"+servicename+"/"+method.Name)
|
||||
}
|
||||
}
|
||||
for service, methodList := range a.connectServices {
|
||||
for _, method := range methodList {
|
||||
methods = append(methods, service+method)
|
||||
}
|
||||
}
|
||||
sort.Strings(methods)
|
||||
return methods
|
||||
}
|
||||
@@ -82,12 +102,16 @@ func New(
|
||||
) (_ *API, err error) {
|
||||
api := &API{
|
||||
port: port,
|
||||
externalDomain: externalDomain,
|
||||
verifier: verifier,
|
||||
health: queries,
|
||||
router: router,
|
||||
queries: queries,
|
||||
accessInterceptor: accessInterceptor,
|
||||
hostHeaders: hostHeaders,
|
||||
authConfig: authZ,
|
||||
systemAuthZ: systemAuthz,
|
||||
connectServices: make(map[string][]string),
|
||||
}
|
||||
|
||||
api.grpcServer = server.CreateServer(api.verifier, systemAuthz, authZ, queries, externalDomain, tlsConfig, accessInterceptor.AccessService())
|
||||
@@ -100,10 +124,15 @@ func New(
|
||||
api.RegisterHandlerOnPrefix("/debug", api.healthHandler())
|
||||
api.router.Handle("/", http.RedirectHandler(login.HandlerPrefix, http.StatusFound))
|
||||
|
||||
reflection.Register(api.grpcServer)
|
||||
return api, nil
|
||||
}
|
||||
|
||||
func (a *API) serverReflection() {
|
||||
reflector := grpcreflect.NewStaticReflector(a.ListGrpcServices()...)
|
||||
a.RegisterHandlerOnPrefix(grpcreflect.NewHandlerV1(reflector))
|
||||
a.RegisterHandlerOnPrefix(grpcreflect.NewHandlerV1Alpha(reflector))
|
||||
}
|
||||
|
||||
// RegisterServer registers a grpc service on the grpc server,
|
||||
// creates a new grpc gateway and registers it as a separate http handler
|
||||
//
|
||||
@@ -131,17 +160,50 @@ func (a *API) RegisterServer(ctx context.Context, grpcServer server.WithGatewayP
|
||||
// and its gateway on the gateway handler
|
||||
//
|
||||
// used for >= v2 api (e.g. user, session, ...)
|
||||
func (a *API) RegisterService(ctx context.Context, grpcServer server.Server) error {
|
||||
grpcServer.RegisterServer(a.grpcServer)
|
||||
err := server.RegisterGateway(ctx, a.grpcGateway, grpcServer)
|
||||
if err != nil {
|
||||
return err
|
||||
func (a *API) RegisterService(ctx context.Context, srv server.Server) error {
|
||||
switch service := srv.(type) {
|
||||
case server.GrpcServer:
|
||||
service.RegisterServer(a.grpcServer)
|
||||
case server.ConnectServer:
|
||||
a.registerConnectServer(service)
|
||||
}
|
||||
a.verifier.RegisterServer(grpcServer.AppName(), grpcServer.MethodPrefix(), grpcServer.AuthMethods())
|
||||
a.healthServer.SetServingStatus(grpcServer.MethodPrefix(), healthpb.HealthCheckResponse_SERVING)
|
||||
if withGateway, ok := srv.(server.WithGateway); ok {
|
||||
err := server.RegisterGateway(ctx, a.grpcGateway, withGateway)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
a.verifier.RegisterServer(srv.AppName(), srv.MethodPrefix(), srv.AuthMethods())
|
||||
a.healthServer.SetServingStatus(srv.MethodPrefix(), healthpb.HealthCheckResponse_SERVING)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *API) registerConnectServer(service server.ConnectServer) {
|
||||
prefix, handler := service.RegisterConnectServer(
|
||||
connect_middleware.CallDurationHandler(),
|
||||
connect_middleware.MetricsHandler(metricTypes, grpc_api.Probes...),
|
||||
connect_middleware.NoCacheInterceptor(),
|
||||
connect_middleware.InstanceInterceptor(a.queries, a.externalDomain, system_pb.SystemService_ServiceDesc.ServiceName, healthpb.Health_ServiceDesc.ServiceName),
|
||||
connect_middleware.AccessStorageInterceptor(a.accessInterceptor.AccessService()),
|
||||
connect_middleware.ErrorHandler(),
|
||||
connect_middleware.LimitsInterceptor(system_pb.SystemService_ServiceDesc.ServiceName),
|
||||
connect_middleware.AuthorizationInterceptor(a.verifier, a.systemAuthZ, a.authConfig),
|
||||
connect_middleware.TranslationHandler(),
|
||||
connect_middleware.QuotaExhaustedInterceptor(a.accessInterceptor.AccessService(), system_pb.SystemService_ServiceDesc.ServiceName),
|
||||
connect_middleware.ExecutionHandler(a.queries),
|
||||
connect_middleware.ValidationHandler(),
|
||||
connect_middleware.ServiceHandler(),
|
||||
connect_middleware.ActivityInterceptor(),
|
||||
)
|
||||
methods := service.FileDescriptor().Services().Get(0).Methods()
|
||||
methodNames := make([]string, methods.Len())
|
||||
for i := 0; i < methods.Len(); i++ {
|
||||
methodNames[i] = string(methods.Get(i).Name())
|
||||
}
|
||||
a.connectServices[prefix] = methodNames
|
||||
a.RegisterHandlerPrefixes(handler, prefix)
|
||||
}
|
||||
|
||||
// HandleFunc allows registering a [http.HandlerFunc] on an exact
|
||||
// path, instead of prefix like RegisterHandlerOnPrefix.
|
||||
func (a *API) HandleFunc(path string, f http.HandlerFunc) {
|
||||
@@ -173,6 +235,9 @@ func (a *API) registerHealthServer() {
|
||||
}
|
||||
|
||||
func (a *API) RouteGRPC() {
|
||||
// since all services are now registered, we can build the grpc server reflection and register the handler
|
||||
a.serverReflection()
|
||||
|
||||
http2Route := a.router.
|
||||
MatcherFunc(func(r *http.Request, _ *mux.RouteMatch) bool {
|
||||
return r.ProtoMajor == 2
|
||||
|
Reference in New Issue
Block a user