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:
Livio Spring
2025-07-04 10:06:20 -04:00
committed by GitHub
parent 82cd1cee08
commit 9ebf2316c6
133 changed files with 5191 additions and 1187 deletions

View File

@@ -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