diff --git a/adapter/internal/oasparser/constants/constants.go b/adapter/internal/oasparser/constants/constants.go index df863dc5a..3893d473a 100644 --- a/adapter/internal/oasparser/constants/constants.go +++ b/adapter/internal/oasparser/constants/constants.go @@ -98,6 +98,7 @@ const ( SOAP string = "SOAP" WS string = "WS" GRAPHQL string = "GraphQL" + GRPC string = "GRPC" WEBHOOK string = "WEBHOOK" SSE string = "SSE" Prototyped string = "prototyped" diff --git a/adapter/internal/oasparser/envoyconf/routes_with_clusters.go b/adapter/internal/oasparser/envoyconf/routes_with_clusters.go index acb0edb51..dc2b983c3 100644 --- a/adapter/internal/oasparser/envoyconf/routes_with_clusters.go +++ b/adapter/internal/oasparser/envoyconf/routes_with_clusters.go @@ -197,6 +197,73 @@ func CreateRoutesWithClusters(adapterInternalAPI *model.AdapterInternalAPI, inte } return routes, clusters, endpoints, nil } + if adapterInternalAPI.GetAPIType() == constants.GRPC { + basePath := strings.TrimSuffix(adapterInternalAPI.Endpoints.Endpoints[0].Basepath, "/") + + clusterName := getClusterName(adapterInternalAPI.Endpoints.EndpointPrefix, organizationID, vHost, + adapterInternalAPI.GetTitle(), apiVersion, "") + adapterInternalAPI.Endpoints.HTTP2BackendEnabled = true + cluster, address, err := processEndpoints(clusterName, adapterInternalAPI.Endpoints, timeout, basePath) + if err != nil { + logger.LoggerOasparser.ErrorC(logging.PrintError(logging.Error2239, logging.MAJOR, + "Error while adding grpc endpoints for %s:%v. %v", apiTitle, apiVersion, err.Error())) + return nil, nil, nil, fmt.Errorf("error while adding grpc endpoints for %s:%v. %v", apiTitle, apiVersion, + err.Error()) + } + clusters = append(clusters, cluster) + endpoints = append(endpoints, address...) + + for _, resource := range adapterInternalAPI.GetResources() { + var clusterName string + resourcePath := resource.GetPath() + endpoint := resource.GetEndpoints() + endpoint.HTTP2BackendEnabled = true + basePath := strings.TrimSuffix(endpoint.Endpoints[0].Basepath, "/") + existingClusterName := getExistingClusterName(*endpoint, processedEndpoints) + + if existingClusterName == "" { + clusterName = getClusterName(endpoint.EndpointPrefix, organizationID, vHost, adapterInternalAPI.GetTitle(), apiVersion, resource.GetID()) + cluster, address, err := processEndpoints(clusterName, endpoint, timeout, basePath) + if err != nil { + logger.LoggerOasparser.ErrorC(logging.PrintError(logging.Error2239, logging.MAJOR, "Error while adding resource level endpoints for %s:%v-%v. %v", apiTitle, apiVersion, resourcePath, err.Error())) + } else { + clusters = append(clusters, cluster) + endpoints = append(endpoints, address...) + processedEndpoints[clusterName] = *endpoint + } + } else { + clusterName = existingClusterName + } + // Create resource level interceptor clusters if required + clustersI, endpointsI, operationalReqInterceptors, operationalRespInterceptorVal := createInterceptorResourceClusters(adapterInternalAPI, + interceptorCerts, vHost, organizationID, apiRequestInterceptor, apiResponseInterceptor, resource) + clusters = append(clusters, clustersI...) + endpoints = append(endpoints, endpointsI...) + routeParams := genRouteCreateParams(adapterInternalAPI, resource, vHost, basePath, clusterName, *operationalReqInterceptors, *operationalRespInterceptorVal, organizationID, + false, false) + + routeP, err := createRoutes(routeParams) + if err != nil { + logger.LoggerXds.ErrorC(logging.PrintError(logging.Error2231, logging.MAJOR, + "Error while creating routes for GRPC API %s %s for path: %s Error: %s", adapterInternalAPI.GetTitle(), + adapterInternalAPI.GetVersion(), resource.GetPath(), err.Error())) + return nil, nil, nil, fmt.Errorf("error while creating routes. %v", err) + } + routes = append(routes, routeP...) + if adapterInternalAPI.IsDefaultVersion { + defaultRoutes, errDefaultPath := createRoutes(genRouteCreateParams(adapterInternalAPI, resource, vHost, basePath, clusterName, *operationalReqInterceptors, *operationalRespInterceptorVal, organizationID, + false, true)) + if errDefaultPath != nil { + logger.LoggerXds.ErrorC(logging.PrintError(logging.Error2231, logging.MAJOR, "Error while creating routes for GRPC API %s %s for path: %s Error: %s", adapterInternalAPI.GetTitle(), adapterInternalAPI.GetVersion(), removeFirstOccurrence(resource.GetPath(), adapterInternalAPI.GetVersion()), errDefaultPath.Error())) + return nil, nil, nil, fmt.Errorf("error while creating routes. %v", errDefaultPath) + } + routes = append(routes, defaultRoutes...) + } + + } + + return routes, clusters, endpoints, nil + } for _, resource := range adapterInternalAPI.GetResources() { var clusterName string resourcePath := resource.GetPath() @@ -860,8 +927,14 @@ func createRoutes(params *routeCreateParams) (routes []*routev3.Route, err error decorator *routev3.Decorator ) if params.createDefaultPath { - xWso2Basepath = removeFirstOccurrence(xWso2Basepath, "/"+version) - resourcePath = removeFirstOccurrence(resource.GetPath(), "/"+version) + //check if basepath is separated from version by a . or / + if strings.Contains(basePath, "."+version) { + xWso2Basepath = removeFirstOccurrence(basePath, "."+version) + resourcePath = removeFirstOccurrence(resource.GetPath(), "."+version) + } else { + xWso2Basepath = removeFirstOccurrence(xWso2Basepath, "/"+version) + resourcePath = removeFirstOccurrence(resource.GetPath(), "/"+version) + } } if pathMatchType != gwapiv1.PathMatchExact { @@ -1058,6 +1131,16 @@ func createRoutes(params *routeCreateParams) (routes []*routev3.Route, err error rewritePath := generateRoutePathForReWrite(basePath, resourcePath, pathMatchType) action.Route.RegexRewrite = generateRegexMatchAndSubstitute(rewritePath, resourcePath, pathMatchType) + if apiType == "GRPC" { + match.Headers = nil + newRoutePath := "/" + strings.TrimPrefix(resourcePath, basePath+".") + if newRoutePath == "/"+resourcePath { + temp := removeFirstOccurrence(basePath, "."+version) + newRoutePath = "/" + strings.TrimPrefix(resourcePath, temp+".") + } + action.Route.RegexRewrite = generateRegexMatchAndSubstitute(rewritePath, newRoutePath, pathMatchType) + } + route := generateRouteConfig(xWso2Basepath, match, action, nil, decorator, perRouteFilterConfigs, nil, nil, nil, nil) // general headers to add and remove are included in this methods routes = append(routes, route) @@ -1209,8 +1292,14 @@ func CreateAPIDefinitionEndpoint(adapterInternalAPI *model.AdapterInternalAPI, v matchPath := basePath + endpoint if isDefaultversion { - basePathWithoutVersion := removeLastOccurrence(basePath, "/"+version) - matchPath = basePathWithoutVersion + endpoint + if adapterInternalAPI.GetAPIType() == "GRPC" { + basePathWithoutVersion := removeLastOccurrence(basePath, "."+version) + matchPath = basePathWithoutVersion + "/" + vHost + endpoint + } else { + basePathWithoutVersion := removeLastOccurrence(basePath, "/"+version) + matchPath = basePathWithoutVersion + endpoint + + } } matchPath = strings.Replace(matchPath, basePath, regexp.QuoteMeta(basePath), 1) diff --git a/adapter/internal/oasparser/model/adapter_internal_api.go b/adapter/internal/oasparser/model/adapter_internal_api.go index a5318d66a..0e698a741 100644 --- a/adapter/internal/oasparser/model/adapter_internal_api.go +++ b/adapter/internal/oasparser/model/adapter_internal_api.go @@ -21,6 +21,7 @@ import ( "errors" "fmt" "net/url" + gwapiv1a2 "sigs.k8s.io/gateway-api/apis/v1alpha2" "strconv" "strings" "time" @@ -940,6 +941,182 @@ func (adapterInternalAPI *AdapterInternalAPI) SetInfoGQLRouteCR(gqlRoute *dpv1al return nil } +// SetInfoGRPCRouteCR populates resources and endpoints of adapterInternalAPI. httpRoute.Spec.Rules.Matches +// are used to create resources and httpRoute.Spec.Rules.BackendRefs are used to create EndpointClusters. +func (adapterInternalAPI *AdapterInternalAPI) SetInfoGRPCRouteCR(grpcRoute *gwapiv1a2.GRPCRoute, resourceParams ResourceParams) error { + var resources []*Resource + outputAuthScheme := utils.TieBreaker(utils.GetPtrSlice(maps.Values(resourceParams.AuthSchemes))) + outputAPIPolicy := utils.TieBreaker(utils.GetPtrSlice(maps.Values(resourceParams.APIPolicies))) + outputRatelimitPolicy := utils.TieBreaker(utils.GetPtrSlice(maps.Values(resourceParams.RateLimitPolicies))) + + disableScopes := true + config := config.ReadConfigs() + + var authScheme *dpv1alpha2.Authentication + if outputAuthScheme != nil { + authScheme = *outputAuthScheme + } + var apiPolicy *dpv1alpha2.APIPolicy + if outputAPIPolicy != nil { + apiPolicy = *outputAPIPolicy + } + var ratelimitPolicy *dpv1alpha1.RateLimitPolicy + if outputRatelimitPolicy != nil { + ratelimitPolicy = *outputRatelimitPolicy + } + + //We are only supporting one backend for now + backend := grpcRoute.Spec.Rules[0].BackendRefs[0] + backendName := types.NamespacedName{ + Name: string(backend.Name), + Namespace: utils.GetNamespace(backend.Namespace, grpcRoute.Namespace), + } + resolvedBackend, ok := resourceParams.BackendMapping[backendName.String()] + if ok { + endpointConfig := &EndpointConfig{} + if resolvedBackend.CircuitBreaker != nil { + endpointConfig.CircuitBreakers = &CircuitBreakers{ + MaxConnections: int32(resolvedBackend.CircuitBreaker.MaxConnections), + MaxRequests: int32(resolvedBackend.CircuitBreaker.MaxRequests), + MaxPendingRequests: int32(resolvedBackend.CircuitBreaker.MaxPendingRequests), + MaxRetries: int32(resolvedBackend.CircuitBreaker.MaxRetries), + MaxConnectionPools: int32(resolvedBackend.CircuitBreaker.MaxConnectionPools), + } + } + if resolvedBackend.Timeout != nil { + endpointConfig.TimeoutInMillis = resolvedBackend.Timeout.UpstreamResponseTimeout * 1000 + endpointConfig.IdleTimeoutInSeconds = resolvedBackend.Timeout.DownstreamRequestIdleTimeout + } + if resolvedBackend.Retry != nil { + statusCodes := config.Envoy.Upstream.Retry.StatusCodes + if len(resolvedBackend.Retry.StatusCodes) > 0 { + statusCodes = resolvedBackend.Retry.StatusCodes + } + endpointConfig.RetryConfig = &RetryConfig{ + Count: int32(resolvedBackend.Retry.Count), + StatusCodes: statusCodes, + BaseIntervalInMillis: int32(resolvedBackend.Retry.BaseIntervalMillis), + } + } + adapterInternalAPI.Endpoints = &EndpointCluster{ + Endpoints: GetEndpoints(backendName, resourceParams.BackendMapping), + Config: endpointConfig, + } + if resolvedBackend.HealthCheck != nil { + adapterInternalAPI.Endpoints.HealthCheck = &HealthCheck{ + Interval: resolvedBackend.HealthCheck.Interval, + Timeout: resolvedBackend.HealthCheck.Timeout, + UnhealthyThreshold: resolvedBackend.HealthCheck.UnhealthyThreshold, + HealthyThreshold: resolvedBackend.HealthCheck.HealthyThreshold, + } + } + + var securityConfig []EndpointSecurity + switch resolvedBackend.Security.Type { + case "Basic": + securityConfig = append(securityConfig, EndpointSecurity{ + Password: string(resolvedBackend.Security.Basic.Password), + Username: string(resolvedBackend.Security.Basic.Username), + Type: string(resolvedBackend.Security.Type), + Enabled: true, + }) + } + adapterInternalAPI.EndpointSecurity = utils.GetPtrSlice(securityConfig) + } else { + return fmt.Errorf("backend: %s has not been resolved", backendName) + } + + for _, rule := range grpcRoute.Spec.Rules { + var policies = OperationPolicies{} + var endPoints []Endpoint + resourceAuthScheme := authScheme + resourceRatelimitPolicy := ratelimitPolicy + var scopes []string + for _, filter := range rule.Filters { + if filter.ExtensionRef != nil && filter.ExtensionRef.Kind == constants.KindAuthentication { + if ref, found := resourceParams.ResourceAuthSchemes[types.NamespacedName{ + Name: string(filter.ExtensionRef.Name), + Namespace: grpcRoute.Namespace, + }.String()]; found { + resourceAuthScheme = concatAuthSchemes(authScheme, &ref) + } else { + return fmt.Errorf(`auth scheme: %s has not been resolved, spec.targetRef.kind should be + 'Resource' in resource level Authentications`, filter.ExtensionRef.Name) + } + } + if filter.ExtensionRef != nil && filter.ExtensionRef.Kind == constants.KindScope { + if ref, found := resourceParams.ResourceScopes[types.NamespacedName{ + Name: string(filter.ExtensionRef.Name), + Namespace: grpcRoute.Namespace, + }.String()]; found { + scopes = ref.Spec.Names + disableScopes = false + } else { + return fmt.Errorf("scope: %s has not been resolved in namespace %s", filter.ExtensionRef.Name, grpcRoute.Namespace) + } + } + if filter.ExtensionRef != nil && filter.ExtensionRef.Kind == constants.KindRateLimitPolicy { + if ref, found := resourceParams.ResourceRateLimitPolicies[types.NamespacedName{ + Name: string(filter.ExtensionRef.Name), + Namespace: grpcRoute.Namespace, + }.String()]; found { + resourceRatelimitPolicy = concatRateLimitPolicies(ratelimitPolicy, &ref) + } else { + return fmt.Errorf(`ratelimitpolicy: %s has not been resolved, spec.targetRef.kind should be + 'Resource' in resource level RateLimitPolicies`, filter.ExtensionRef.Name) + } + } + } + resourceAuthScheme = concatAuthSchemes(resourceAuthScheme, nil) + resourceRatelimitPolicy = concatRateLimitPolicies(resourceRatelimitPolicy, nil) + + loggers.LoggerOasparser.Debugf("Calculating auths for API ..., API_UUID = %v", adapterInternalAPI.UUID) + apiAuth := getSecurity(resourceAuthScheme) + + for _, match := range rule.Matches { + resourcePath := adapterInternalAPI.GetXWso2Basepath() + "." + *match.Method.Service + "/" + *match.Method.Method + endPoints = append(endPoints, GetEndpoints(backendName, resourceParams.BackendMapping)...) + resource := &Resource{path: resourcePath, pathMatchType: "Exact", + methods: []*Operation{{iD: uuid.New().String(), method: "post", policies: policies, + auth: apiAuth, rateLimitPolicy: parseRateLimitPolicyToInternal(resourceRatelimitPolicy), scopes: scopes}}, + iD: uuid.New().String(), + } + endpoints := GetEndpoints(backendName, resourceParams.BackendMapping) + resource.endpoints = &EndpointCluster{ + Endpoints: endpoints, + } + resources = append(resources, resource) + } + } + + ratelimitPolicy = concatRateLimitPolicies(ratelimitPolicy, nil) + apiPolicy = concatAPIPolicies(apiPolicy, nil) + authScheme = concatAuthSchemes(authScheme, nil) + + adapterInternalAPI.RateLimitPolicy = parseRateLimitPolicyToInternal(ratelimitPolicy) + adapterInternalAPI.resources = resources + adapterInternalAPI.xWso2Cors = getCorsConfigFromAPIPolicy(apiPolicy) + if authScheme.Spec.Override != nil && authScheme.Spec.Override.Disabled != nil { + adapterInternalAPI.disableAuthentications = *authScheme.Spec.Override.Disabled + } + authSpec := utils.SelectPolicy(&authScheme.Spec.Override, &authScheme.Spec.Default, nil, nil) + if authSpec != nil && authSpec.AuthTypes != nil && authSpec.AuthTypes.Oauth2.Required != "" { + adapterInternalAPI.SetXWSO2ApplicationSecurity(authSpec.AuthTypes.Oauth2.Required == "mandatory") + } else { + adapterInternalAPI.SetXWSO2ApplicationSecurity(true) + } + adapterInternalAPI.disableScopes = disableScopes + // Check whether the API has a backend JWT token + if apiPolicy != nil && apiPolicy.Spec.Override != nil && apiPolicy.Spec.Override.BackendJWTPolicy != nil { + backendJWTPolicy := resourceParams.BackendJWTMapping[types.NamespacedName{ + Name: apiPolicy.Spec.Override.BackendJWTPolicy.Name, + Namespace: grpcRoute.Namespace, + }.String()].Spec + adapterInternalAPI.backendJWTTokenInfo = parseBackendJWTTokenToInternal(backendJWTPolicy) + } + return nil +} + func (endpoint *Endpoint) validateEndpoint() error { if endpoint.Port == 0 || endpoint.Port > 65535 { return errors.New("endpoint port value should be between 0 and 65535") diff --git a/adapter/internal/oasparser/model/common.go b/adapter/internal/oasparser/model/common.go index d1d3ba664..8c5c2f54c 100644 --- a/adapter/internal/oasparser/model/common.go +++ b/adapter/internal/oasparser/model/common.go @@ -108,7 +108,7 @@ func getHostandBasepathandPort(apiType string, rawURL string) (*Endpoint, error) rawURL = strings.Trim(rawURL, " ") if !strings.Contains(rawURL, "://") { - if apiType == constants.REST || apiType == constants.GRAPHQL { + if apiType == constants.REST || apiType == constants.GRAPHQL || apiType == constants.GRPC { rawURL = "http://" + rawURL } else if apiType == constants.WS { rawURL = "ws://" + rawURL diff --git a/adapter/internal/operator/config/crd/bases/dp.wso2.com_apis.yaml b/adapter/internal/operator/config/crd/bases/dp.wso2.com_apis.yaml index fc4e0a2a9..c2a145323 100644 --- a/adapter/internal/operator/config/crd/bases/dp.wso2.com_apis.yaml +++ b/adapter/internal/operator/config/crd/bases/dp.wso2.com_apis.yaml @@ -72,7 +72,7 @@ spec: type: array apiType: description: APIType denotes the type of the API. Possible values - could be REST, GraphQL, Async + could be REST, GraphQL, GRPC Async enum: - REST type: string @@ -244,10 +244,11 @@ spec: type: array apiType: description: APIType denotes the type of the API. Possible values - could be REST, GraphQL, Async + could be REST, GraphQL, GRPC Async enum: - REST - GraphQL + - GRPC type: string apiVersion: description: APIVersion is the version number of the API. diff --git a/adapter/internal/operator/controllers/dp/api_controller.go b/adapter/internal/operator/controllers/dp/api_controller.go index 0c13b4570..ed541a472 100644 --- a/adapter/internal/operator/controllers/dp/api_controller.go +++ b/adapter/internal/operator/controllers/dp/api_controller.go @@ -54,6 +54,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/reconcile" "sigs.k8s.io/controller-runtime/pkg/source" gwapiv1 "sigs.k8s.io/gateway-api/apis/v1" + gwapiv1a2 "sigs.k8s.io/gateway-api/apis/v1alpha2" gwapiv1b1 "sigs.k8s.io/gateway-api/apis/v1beta1" ctrl "sigs.k8s.io/controller-runtime" @@ -67,6 +68,7 @@ import ( ) const ( + grpcRouteAPIIndex = "grpcRouteAPIIndex" httpRouteAPIIndex = "httpRouteAPIIndex" gqlRouteAPIIndex = "gqlRouteAPIIndex" // apiAuthenticationIndex Index for API level authentications @@ -79,6 +81,8 @@ const ( apiRateLimitResourceIndex = "apiRateLimitResourceIndex" // gatewayHTTPRouteIndex Index for gateway httproutes gatewayHTTPRouteIndex = "gatewayHTTPRouteIndex" + // gatewayGRPCRouteIndex Index for gateway grpcroutes + gatewayGRPCRouteIndex = "gatewayGRPCRouteIndex" // apiAPIPolicyIndex Index for API level apipolicies apiAPIPolicyIndex = "apiAPIPolicyIndex" // apiAPIPolicyResourceIndex Index for resource level apipolicies @@ -86,6 +90,7 @@ const ( serviceHTTPRouteIndex = "serviceHTTPRouteIndex" httprouteScopeIndex = "httprouteScopeIndex" gqlRouteScopeIndex = "gqlRouteScopeIndex" + grpcRouteScopeIndex = "grpcRouteScopeIndex" configMapBackend = "configMapBackend" configMapAPIDefinition = "configMapAPIDefinition" secretBackend = "secretBackend" @@ -93,6 +98,7 @@ const ( secretAuthentication = "secretAuthentication" backendHTTPRouteIndex = "backendHTTPRouteIndex" backendGQLRouteIndex = "backendGQLRouteIndex" + backendGRPCRouteIndex = "backendGRPCRouteIndex" interceptorServiceAPIPolicyIndex = "interceptorServiceAPIPolicyIndex" backendInterceptorServiceIndex = "backendInterceptorServiceIndex" backendJWTAPIPolicyIndex = "backendJWTAPIPolicyIndex" @@ -157,6 +163,13 @@ func NewAPIController(mgr manager.Manager, operatorDataStore *synchronizer.Opera return err } + if err := c.Watch(source.Kind(mgr.GetCache(), &gwapiv1a2.GRPCRoute{}), handler.EnqueueRequestsFromMapFunc(apiReconciler.populateAPIReconcileRequestsForGRPCRoute), + predicates...); err != nil { + //TODO change the error number + loggers.LoggerAPKOperator.ErrorC(logging.PrintError(logging.Error2667, logging.BLOCKER, "Error watching GRPCRoute resources: %v", err)) + return err + } + if err := c.Watch(source.Kind(mgr.GetCache(), &gwapiv1b1.Gateway{}), handler.EnqueueRequestsFromMapFunc(apiReconciler.getAPIsForGateway), predicates...); err != nil { loggers.LoggerAPKOperator.ErrorC(logging.PrintError(logging.Error2611, logging.BLOCKER, "Error watching API resources: %v", err)) @@ -232,6 +245,9 @@ func NewAPIController(mgr manager.Manager, operatorDataStore *synchronizer.Opera // +kubebuilder:rbac:groups=dp.wso2.com,resources=gqlroutes,verbs=get;list;watch;create;update;patch;delete // +kubebuilder:rbac:groups=dp.wso2.com,resources=gqlroutes/status,verbs=get;update;patch // +kubebuilder:rbac:groups=dp.wso2.com,resources=gqlroutes/finalizers,verbs=update +// +kubebuilder:rbac:groups=dp.wso2.com,resources=grpcroutes,verbs=get;list;watch;create;update;patch;delete +// +kubebuilder:rbac:groups=dp.wso2.com,resources=grpcroutes/status,verbs=get;update;patch +// +kubebuilder:rbac:groups=dp.wso2.com,resources=grpcroutes/finalizers,verbs=update // +kubebuilder:rbac:groups=dp.wso2.com,resources=authentications,verbs=get;list;watch;create;update;patch;delete // +kubebuilder:rbac:groups=dp.wso2.com,resources=authentications/status,verbs=get;update;patch // +kubebuilder:rbac:groups=dp.wso2.com,resources=authentications/finalizers,verbs=update @@ -419,6 +435,24 @@ func (apiReconciler *APIReconciler) resolveAPIRefs(ctx context.Context, api dpv1 } } + //handle grpc apis + if len(prodRouteRefs) > 0 && apiState.APIDefinition.Spec.APIType == "GRPC" { + apiState.ProdGRPCRoute = &synchronizer.GRPCRouteState{} + if apiState.ProdGRPCRoute, err = apiReconciler.resolveGRPCRouteRefs(ctx, prodRouteRefs, + namespace, api); err != nil { + return nil, fmt.Errorf("error while resolving production grpcRouteref %s in namespace :%s has not found. %s", + prodRouteRefs, namespace, err.Error()) + } + } + if len(sandRouteRefs) > 0 && apiState.APIDefinition.Spec.APIType == "GRPC" { + apiState.SandGRPCRoute = &synchronizer.GRPCRouteState{} + if apiState.SandGRPCRoute, err = apiReconciler.resolveGRPCRouteRefs(ctx, sandRouteRefs, + namespace, api); err != nil { + return nil, fmt.Errorf("error while resolving sandbox grpcRouteref %s in namespace :%s has not found. %s", + sandRouteRefs, namespace, err.Error()) + } + } + // handle gql apis if len(prodRouteRefs) > 0 && apiState.APIDefinition.Spec.APIType == "GraphQL" { if apiState.ProdGQLRoute, err = apiReconciler.resolveGQLRouteRefs(ctx, prodRouteRefs, namespace, @@ -516,6 +550,16 @@ func (apiReconciler *APIReconciler) resolveGQLRouteRefs(ctx context.Context, gql return &gqlRouteState, err } +func (apiReconciler *APIReconciler) resolveGRPCRouteRefs(ctx context.Context, grpcRouteRefs []string, + namespace string, api dpv1alpha2.API) (*synchronizer.GRPCRouteState, error) { + grpcRouteState, err := apiReconciler.concatGRPCRoutes(ctx, grpcRouteRefs, namespace, api) + if err != nil { + return nil, err + } + grpcRouteState.Scopes, err = apiReconciler.getScopesForGRPCRoute(ctx, grpcRouteState.GRPCRouteCombined, api) + return &grpcRouteState, err +} + // resolveHTTPRouteRefs validates following references related to the API // - Authentications func (apiReconciler *APIReconciler) resolveHTTPRouteRefs(ctx context.Context, httpRouteState *synchronizer.HTTPRouteState, @@ -534,7 +578,41 @@ func (apiReconciler *APIReconciler) resolveHTTPRouteRefs(ctx context.Context, ht return httpRouteState, err } +func (apiReconciler *APIReconciler) concatGRPCRoutes(ctx context.Context, grpcRouteRefs []string, + namespace string, api dpv1alpha2.API) (synchronizer.GRPCRouteState, error) { + grpcRouteState := synchronizer.GRPCRouteState{} + grpcRoutePartitions := make(map[string]*gwapiv1a2.GRPCRoute) + for _, grpcRouteRef := range grpcRouteRefs { + var grpcRoute gwapiv1a2.GRPCRoute + namespacedName := types.NamespacedName{Namespace: namespace, Name: grpcRouteRef} + if err := utils.ResolveRef(ctx, apiReconciler.client, &api, namespacedName, true, &grpcRoute); err != nil { + return grpcRouteState, fmt.Errorf("error while getting grpcroute %s in namespace :%s, %s", grpcRouteRef, + namespace, err.Error()) + } + grpcRoutePartitions[namespacedName.String()] = &grpcRoute + if grpcRouteState.GRPCRouteCombined == nil { + grpcRouteState.GRPCRouteCombined = &grpcRoute + } else { + grpcRouteState.GRPCRouteCombined.Spec.Rules = append(grpcRouteState.GRPCRouteCombined.Spec.Rules, + grpcRoute.Spec.Rules...) + } + } + grpcRouteState.GRPCRoutePartitions = grpcRoutePartitions + backendNamespacedName := types.NamespacedName{ + //TODO check if this is correct + Name: string(grpcRouteState.GRPCRouteCombined.Spec.Rules[0].BackendRefs[0].BackendRef.Name), + Namespace: namespace, + } + resolvedBackend := utils.GetResolvedBackend(ctx, apiReconciler.client, backendNamespacedName, &api) + if resolvedBackend != nil { + grpcRouteState.BackendMapping = map[string]*dpv1alpha1.ResolvedBackend{ + backendNamespacedName.String(): resolvedBackend, + } + return grpcRouteState, nil + } + return grpcRouteState, errors.New("error while resolving backend for grpcroute") +} func (apiReconciler *APIReconciler) concatGQLRoutes(ctx context.Context, gqlRouteRefs []string, namespace string, api dpv1alpha2.API) (synchronizer.GQLRouteState, error) { gqlRouteState := synchronizer.GQLRouteState{} @@ -623,7 +701,25 @@ func (apiReconciler *APIReconciler) getRatelimitPoliciesForAPI(ctx context.Conte } return ratelimitPolicies, nil } - +func (apiReconciler *APIReconciler) getScopesForGRPCRoute(ctx context.Context, + grpcRoute *gwapiv1a2.GRPCRoute, api dpv1alpha2.API) (map[string]dpv1alpha1.Scope, error) { + scopes := make(map[string]dpv1alpha1.Scope) + for _, rule := range grpcRoute.Spec.Rules { + for _, filter := range rule.Filters { + if filter.ExtensionRef != nil && filter.ExtensionRef.Kind == constants.KindScope { + scope := &dpv1alpha1.Scope{} + if err := utils.ResolveRef(ctx, apiReconciler.client, &api, + types.NamespacedName{Namespace: grpcRoute.Namespace, Name: string(filter.ExtensionRef.Name)}, false, + scope); err != nil { + return nil, fmt.Errorf("error while getting scope %s in namespace :%s, %s", filter.ExtensionRef.Name, + grpcRoute.Namespace, err.Error()) + } + scopes[utils.NamespacedName(scope).String()] = *scope + } + } + } + return scopes, nil +} func (apiReconciler *APIReconciler) getScopesForGQLRoute(ctx context.Context, gqlRoute *dpv1alpha2.GQLRoute, api dpv1alpha2.API) (map[string]dpv1alpha1.Scope, error) { scopes := make(map[string]dpv1alpha1.Scope) @@ -876,6 +972,12 @@ func (apiReconciler *APIReconciler) populateAPIReconcileRequestsForHTTPRoute(ctx return requests } +func (apiReconciler *APIReconciler) populateAPIReconcileRequestsForGRPCRoute(ctx context.Context, obj k8client.Object) []reconcile.Request { + requests := apiReconciler.getAPIForGRPCRoute(ctx, obj) + apiReconciler.handleOwnerReference(ctx, obj, &requests) + return requests +} + func (apiReconciler *APIReconciler) populateAPIReconcileRequestsForConfigMap(ctx context.Context, obj k8client.Object) []reconcile.Request { requests := apiReconciler.getAPIsForConfigMap(ctx, obj) apiReconciler.handleOwnerReference(ctx, obj, &requests) @@ -952,6 +1054,16 @@ func (apiReconciler *APIReconciler) traverseAPIStateAndUpdateOwnerReferences(ctx apiReconciler.retriveParentAPIsAndUpdateOwnerReferene(ctx, gqlRoute) } } + if apiState.ProdGRPCRoute != nil { + for _, grpcRoute := range apiState.ProdGRPCRoute.GRPCRoutePartitions { + apiReconciler.retriveParentAPIsAndUpdateOwnerReferene(ctx, grpcRoute) + } + } + if apiState.SandGRPCRoute != nil { + for _, grpcRoute := range apiState.SandGRPCRoute.GRPCRoutePartitions { + apiReconciler.retriveParentAPIsAndUpdateOwnerReferene(ctx, grpcRoute) + } + } for _, auth := range apiState.Authentications { apiReconciler.retriveParentAPIsAndUpdateOwnerReferene(ctx, &auth) } @@ -1001,6 +1113,20 @@ func (apiReconciler *APIReconciler) traverseAPIStateAndUpdateOwnerReferences(ctx } } } + if apiState.ProdGRPCRoute != nil { + for _, backend := range apiState.ProdGRPCRoute.BackendMapping { + if &backend != nil { + apiReconciler.retriveParentAPIsAndUpdateOwnerReferene(ctx, &backend.Backend) + } + } + } + if apiState.SandGRPCRoute != nil { + for _, backend := range apiState.SandGRPCRoute.BackendMapping { + if &backend != nil { + apiReconciler.retriveParentAPIsAndUpdateOwnerReferene(ctx, &backend.Backend) + } + } + } for _, backendJwt := range apiState.BackendJWTMapping { apiReconciler.retriveParentAPIsAndUpdateOwnerReferene(ctx, &backendJwt) } @@ -1142,6 +1268,16 @@ func (apiReconciler *APIReconciler) retriveParentAPIsAndUpdateOwnerReferene(ctx } requests = apiReconciler.getAPIForGQLRoute(ctx, &gqlRoute) apiReconciler.handleOwnerReference(ctx, &gqlRoute, &requests) + case *gwapiv1a2.GRPCRoute: + var grpcRoute gwapiv1a2.GRPCRoute + namespaceName := types.NamespacedName{ + Name: string(obj.GetName()), + Namespace: string(obj.GetNamespace()), + } + if err := apiReconciler.client.Get(ctx, namespaceName, &grpcRoute); err != nil { + loggers.LoggerAPKOperator.Errorf("Unexpected error occured while loading the cr object from cluster %+v", err) + return + } default: loggers.LoggerAPKOperator.Errorf("Unexpected type found while processing owner reference %+v", obj) } @@ -1219,6 +1355,44 @@ func (apiReconciler *APIReconciler) getAPIForHTTPRoute(ctx context.Context, obj return requests } +// getAPIForGRPCRoute triggers the API controller reconcile method based on the changes detected +// from GRPCRoute objects. If the changes are done for an API stored in the Operator Data store, +// a new reconcile event will be created and added to the reconcile event queue. +func (apiReconciler *APIReconciler) getAPIForGRPCRoute(ctx context.Context, obj k8client.Object) []reconcile.Request { + grpcRoute, ok := obj.(*gwapiv1a2.GRPCRoute) + if !ok { + loggers.LoggerAPKOperator.ErrorC(logging.PrintError(logging.Error2622, logging.TRIVIAL, "Unexpected object type, bypassing reconciliation: %v", grpcRoute)) + return []reconcile.Request{} + } + + apiList := &dpv1alpha2.APIList{} + + if err := apiReconciler.client.List(ctx, apiList, &k8client.ListOptions{ + FieldSelector: fields.OneTermEqualSelector(grpcRouteAPIIndex, utils.NamespacedName(grpcRoute).String()), + }); err != nil { + loggers.LoggerAPKOperator.ErrorC(logging.PrintError(logging.Error2623, logging.CRITICAL, "Unable to find associated APIs: %s", utils.NamespacedName(grpcRoute).String())) + return []reconcile.Request{} + } + + if len(apiList.Items) == 0 { + loggers.LoggerAPKOperator.Debugf("APIs for GRPCRoute not found: %s", utils.NamespacedName(grpcRoute).String()) + return []reconcile.Request{} + } + + requests := []reconcile.Request{} + for _, api := range apiList.Items { + req := reconcile.Request{ + NamespacedName: types.NamespacedName{ + Name: api.Name, + Namespace: api.Namespace}, + } + requests = append(requests, req) + loggers.LoggerAPKOperator.Infof("Adding reconcile request for API: %s/%s with API UUID: %v", api.Namespace, api.Name, + string(api.ObjectMeta.UID)) + } + return requests +} + // getAPIsForConfigMap triggers the API controller reconcile method based on the changes detected // in configMap resources. func (apiReconciler *APIReconciler) getAPIsForConfigMap(ctx context.Context, obj k8client.Object) []reconcile.Request { @@ -1487,6 +1661,22 @@ func (apiReconciler *APIReconciler) getAPIsForScope(ctx context.Context, obj k8c requests = append(requests, apiReconciler.getAPIForGQLRoute(ctx, &httpRoute)...) } + grpcRouteList := &gwapiv1a2.GRPCRouteList{} + if err := apiReconciler.client.List(ctx, grpcRouteList, &k8client.ListOptions{ + FieldSelector: fields.OneTermEqualSelector(grpcRouteScopeIndex, utils.NamespacedName(scope).String()), + }); err != nil { + loggers.LoggerAPKOperator.ErrorC(logging.PrintError(logging.Error2625, logging.CRITICAL, "Unable to find associated GRPCRoutes: %s", utils.NamespacedName(scope).String())) + return []reconcile.Request{} + } + + if len(grpcRouteList.Items) == 0 { + loggers.LoggerAPKOperator.Debugf("GRPCRoutes for scope not found: %s", utils.NamespacedName(scope).String()) + } + for item := range grpcRouteList.Items { + grpcRoute := grpcRouteList.Items[item] + requests = append(requests, apiReconciler.getAPIForGRPCRoute(ctx, &grpcRoute)...) + } + return requests } @@ -1507,15 +1697,30 @@ func (apiReconciler *APIReconciler) getAPIsForBackend(ctx context.Context, obj k return []reconcile.Request{} } + grpcRouteList := &gwapiv1a2.GRPCRouteList{} + if err := apiReconciler.client.List(ctx, grpcRouteList, &k8client.ListOptions{ + FieldSelector: fields.OneTermEqualSelector(backendGRPCRouteIndex, utils.NamespacedName(backend).String()), + }); err != nil { + loggers.LoggerAPKOperator.ErrorC(logging.PrintError(logging.Error2625, logging.CRITICAL, "Unable to find associated GRPCRoutes: %s", utils.NamespacedName(backend).String())) + return []reconcile.Request{} + } + if len(httpRouteList.Items) == 0 { loggers.LoggerAPKOperator.Debugf("HTTPRoutes for Backend not found: %s", utils.NamespacedName(backend).String()) } + if len(grpcRouteList.Items) == 0 { + loggers.LoggerAPKOperator.Debugf("GRPCRoutes for Backend not found: %s", utils.NamespacedName(backend).String()) + } requests := []reconcile.Request{} for item := range httpRouteList.Items { httpRoute := httpRouteList.Items[item] requests = append(requests, apiReconciler.getAPIForHTTPRoute(ctx, &httpRoute)...) } + for item := range grpcRouteList.Items { + grpcRoute := grpcRouteList.Items[item] + requests = append(requests, apiReconciler.getAPIForGRPCRoute(ctx, &grpcRoute)...) + } gqlRouteList := &dpv1alpha2.GQLRouteList{} if err := apiReconciler.client.List(ctx, gqlRouteList, &k8client.ListOptions{ @@ -1570,17 +1775,33 @@ func (apiReconciler *APIReconciler) getAPIsForGateway(ctx context.Context, obj k loggers.LoggerAPKOperator.ErrorC(logging.PrintError(logging.Error2625, logging.CRITICAL, "Unable to find associated HTTPRoutes: %s", utils.NamespacedName(gateway).String())) return []reconcile.Request{} } + grpcRouteList := &gwapiv1a2.GRPCRouteList{} + if err := apiReconciler.client.List(ctx, grpcRouteList, &k8client.ListOptions{ + FieldSelector: fields.OneTermEqualSelector(gatewayGRPCRouteIndex, utils.NamespacedName(gateway).String()), + }); err != nil { + loggers.LoggerAPKOperator.ErrorC(logging.PrintError(logging.Error2625, logging.CRITICAL, "Unable to find associated GRPCRoutes: %s", utils.NamespacedName(gateway).String())) + return []reconcile.Request{} + } if len(httpRouteList.Items) == 0 { loggers.LoggerAPKOperator.Debugf("HTTPRoutes for Gateway not found: %s", utils.NamespacedName(gateway).String()) return []reconcile.Request{} } + if len(grpcRouteList.Items) == 0 { + loggers.LoggerAPKOperator.Debugf("GRPCRoutes for Gateway not found: %s", utils.NamespacedName(gateway).String()) + return []reconcile.Request{} + } requests := []reconcile.Request{} for item := range httpRouteList.Items { httpRoute := httpRouteList.Items[item] requests = append(requests, apiReconciler.getAPIForHTTPRoute(ctx, &httpRoute)...) } + for item := range grpcRouteList.Items { + grpcRoute := grpcRouteList.Items[item] + requests = append(requests, apiReconciler.getAPIForGRPCRoute(ctx, &grpcRoute)...) + } + return requests } @@ -1656,6 +1877,40 @@ func addIndexes(ctx context.Context, mgr manager.Manager) error { }); err != nil { return err } + if err := mgr.GetFieldIndexer().IndexField(ctx, &dpv1alpha2.API{}, grpcRouteAPIIndex, + func(rawObj k8client.Object) []string { + //check Spec.Kind + api := rawObj.(*dpv1alpha2.API) + if api.Spec.APIType != "GRPC" { + return nil + } + var grpcRoutes []string + if len(api.Spec.Production) > 0 { + for _, ref := range api.Spec.Production[0].RouteRefs { + if ref != "" { + grpcRoutes = append(grpcRoutes, + types.NamespacedName{ + Namespace: api.Namespace, + Name: ref, + }.String()) + } + } + } + if len(api.Spec.Sandbox) > 0 { + for _, ref := range api.Spec.Sandbox[0].RouteRefs { + if ref != "" { + grpcRoutes = append(grpcRoutes, + types.NamespacedName{ + Namespace: api.Namespace, + Name: ref, + }.String()) + } + } + } + return grpcRoutes + }); err != nil { + return err + } if err := mgr.GetFieldIndexer().IndexField(ctx, &dpv1alpha2.API{}, configMapAPIDefinition, func(rawObj k8client.Object) []string { @@ -1734,6 +1989,27 @@ func addIndexes(ctx context.Context, mgr manager.Manager) error { return err } + // Backend to GRPCRoute indexer + if err := mgr.GetFieldIndexer().IndexField(ctx, &gwapiv1a2.GRPCRoute{}, backendGRPCRouteIndex, + func(rawObj k8client.Object) []string { + grpcRoute := rawObj.(*gwapiv1a2.GRPCRoute) + var backends []string + for _, rule := range grpcRoute.Spec.Rules { + for _, backendRef := range rule.BackendRefs { + if backendRef.Kind != nil && *backendRef.Kind == constants.KindBackend { + backends = append(backends, types.NamespacedName{ + Namespace: utils.GetNamespace(backendRef.Namespace, + grpcRoute.ObjectMeta.Namespace), + Name: string(backendRef.Name), + }.String()) + } + } + } + return backends + }); err != nil { + return err + } + // Backend to GQLRoute indexer if err := mgr.GetFieldIndexer().IndexField(ctx, &dpv1alpha2.GQLRoute{}, backendGQLRouteIndex, func(rawObj k8client.Object) []string { @@ -1771,6 +2047,23 @@ func addIndexes(ctx context.Context, mgr manager.Manager) error { return err } + //Gateway to GRPCRoute indexer + if err := mgr.GetFieldIndexer().IndexField(ctx, &gwapiv1a2.GRPCRoute{}, gatewayGRPCRouteIndex, + func(rawObj k8client.Object) []string { + grpcRoute := rawObj.(*gwapiv1a2.GRPCRoute) + var gateways []string + for _, parentRef := range grpcRoute.Spec.ParentRefs { + gateways = append(gateways, types.NamespacedName{ + Namespace: utils.GetNamespace(parentRef.Namespace, + grpcRoute.Namespace), + Name: string(parentRef.Name), + }.String()) + } + return gateways + }); err != nil { + return err + } + // ConfigMap to Backend indexer if err := mgr.GetFieldIndexer().IndexField(ctx, &dpv1alpha1.Backend{}, configMapBackend, func(rawObj k8client.Object) []string { diff --git a/adapter/internal/operator/synchronizer/api_state.go b/adapter/internal/operator/synchronizer/api_state.go index 10e50c223..cbf42bc2a 100644 --- a/adapter/internal/operator/synchronizer/api_state.go +++ b/adapter/internal/operator/synchronizer/api_state.go @@ -20,6 +20,7 @@ package synchronizer import ( "github.com/wso2/apk/common-go-libs/apis/dp/v1alpha1" "github.com/wso2/apk/common-go-libs/apis/dp/v1alpha2" + gwapiv1a2 "sigs.k8s.io/gateway-api/apis/v1alpha2" gwapiv1b1 "sigs.k8s.io/gateway-api/apis/v1beta1" ) @@ -32,6 +33,8 @@ type APIState struct { SandHTTPRoute *HTTPRouteState ProdGQLRoute *GQLRouteState SandGQLRoute *GQLRouteState + ProdGRPCRoute *GRPCRouteState + SandGRPCRoute *GRPCRouteState Authentications map[string]v1alpha2.Authentication RateLimitPolicies map[string]v1alpha1.RateLimitPolicy ResourceAuthentications map[string]v1alpha2.Authentication @@ -64,3 +67,13 @@ type GQLRouteState struct { BackendMapping map[string]*v1alpha1.ResolvedBackend Scopes map[string]v1alpha1.Scope } + +// GRPCRouteState holds the state of the deployed grpcRoutes. This state is compared with +// the state of the Kubernetes controller cache to detect updates. +// +k8s:deepcopy-gen=true +type GRPCRouteState struct { + GRPCRouteCombined *gwapiv1a2.GRPCRoute + GRPCRoutePartitions map[string]*gwapiv1a2.GRPCRoute + BackendMapping map[string]*v1alpha1.ResolvedBackend + Scopes map[string]v1alpha1.Scope +} diff --git a/adapter/internal/operator/synchronizer/data_store.go b/adapter/internal/operator/synchronizer/data_store.go index 9b3afab1d..8c2c23d4b 100644 --- a/adapter/internal/operator/synchronizer/data_store.go +++ b/adapter/internal/operator/synchronizer/data_store.go @@ -18,6 +18,7 @@ package synchronizer import ( + gwapiv1a2 "sigs.k8s.io/gateway-api/apis/v1alpha2" "sync" "github.com/wso2/apk/adapter/internal/loggers" @@ -115,6 +116,23 @@ func (ods *OperatorDataStore) processAPIState(apiNamespacedName types.Namespaced } cachedAPI.ProdGQLRoute = nil } + if apiState.ProdGRPCRoute != nil { + if cachedAPI.ProdGRPCRoute == nil { + cachedAPI.ProdGRPCRoute = apiState.ProdGRPCRoute + updated = true + events = append(events, "Production") + } else if routeEvents, routesUpdated := updateGRPCRoute(apiState.ProdGRPCRoute, cachedAPI.ProdGRPCRoute, + "Production"); routesUpdated { + updated = true + events = append(events, routeEvents...) + } + } else { + if cachedAPI.ProdGRPCRoute != nil { + updated = true + events = append(events, "Production") + } + cachedAPI.ProdGRPCRoute = nil + } if apiState.SandHTTPRoute != nil { if cachedAPI.SandHTTPRoute == nil { cachedAPI.SandHTTPRoute = apiState.SandHTTPRoute @@ -147,6 +165,22 @@ func (ods *OperatorDataStore) processAPIState(apiNamespacedName types.Namespaced } cachedAPI.SandGQLRoute = nil } + if apiState.SandGRPCRoute != nil { + if cachedAPI.SandGRPCRoute == nil { + cachedAPI.SandGRPCRoute = apiState.SandGRPCRoute + updated = true + events = append(events, "Sandbox") + } else if routeEvents, routesUpdated := updateGRPCRoute(apiState.SandGRPCRoute, cachedAPI.SandGRPCRoute, "Sandbox"); routesUpdated { + updated = true + events = append(events, routeEvents...) + } + } else { + if cachedAPI.SandGRPCRoute != nil { + updated = true + events = append(events, "Sandbox") + } + cachedAPI.SandGRPCRoute = nil + } if len(apiState.Authentications) != len(cachedAPI.Authentications) { cachedAPI.Authentications = apiState.Authentications cachedAPI.MutualSSL = apiState.MutualSSL @@ -443,6 +477,61 @@ func updateGQLRoute(gqlRoute *GQLRouteState, cachedGQLRoute *GQLRouteState, endp return events, updated } +func updateGRPCRoute(grpcRoute *GRPCRouteState, cachedGRPCRoute *GRPCRouteState, endpointType string) ([]string, bool) { + var updated bool + events := []string{} + if cachedGRPCRoute.GRPCRouteCombined == nil || !isEqualGRPCRoutes(cachedGRPCRoute.GRPCRoutePartitions, grpcRoute.GRPCRoutePartitions) { + cachedGRPCRoute.GRPCRouteCombined = grpcRoute.GRPCRouteCombined + cachedGRPCRoute.GRPCRoutePartitions = grpcRoute.GRPCRoutePartitions + updated = true + events = append(events, endpointType+" Endpoint") + } + + if len(grpcRoute.Scopes) != len(cachedGRPCRoute.Scopes) { + cachedGRPCRoute.Scopes = grpcRoute.Scopes + updated = true + events = append(events, "Resource Scopes") + } else { + for key, scope := range grpcRoute.Scopes { + if existingScope, found := cachedGRPCRoute.Scopes[key]; found { + if scope.UID != existingScope.UID || scope.Generation > existingScope.Generation { + cachedGRPCRoute.Scopes = grpcRoute.Scopes + updated = true + events = append(events, "Resource Scopes") + break + } + } else { + cachedGRPCRoute.Scopes = grpcRoute.Scopes + updated = true + events = append(events, "Resource Scopes") + break + } + } + } + + if len(grpcRoute.BackendMapping) != len(cachedGRPCRoute.BackendMapping) { + cachedGRPCRoute.BackendMapping = grpcRoute.BackendMapping + updated = true + events = append(events, endpointType+" Backend Properties") + } else { + for key, backend := range grpcRoute.BackendMapping { + if existingBackend, found := cachedGRPCRoute.BackendMapping[key]; found { + if backend.Backend.UID != existingBackend.Backend.UID || backend.Backend.Generation > existingBackend.Backend.Generation { + cachedGRPCRoute.BackendMapping = grpcRoute.BackendMapping + updated = true + events = append(events, endpointType+" Backend Properties") + break + } + } else { + cachedGRPCRoute.BackendMapping = grpcRoute.BackendMapping + updated = true + events = append(events, endpointType+" Backend Properties") + break + } + } + } + return events, updated +} func isEqualHTTPRoutes(cachedHTTPRoutes, newHTTPRoutes map[string]*gwapiv1b1.HTTPRoute) bool { for key, cachedHTTPRoute := range cachedHTTPRoutes { if newHTTPRoutes[key] == nil { @@ -469,6 +558,19 @@ func isEqualGQLRoutes(cachedGQLRoutes, newGQLRoutes map[string]*dpv1alpha2.GQLRo return true } +func isEqualGRPCRoutes(cachedGRPCRoutes, newGRPCRoutes map[string]*gwapiv1a2.GRPCRoute) bool { + for key, cachedGRPCRoute := range cachedGRPCRoutes { + if newGRPCRoutes[key] == nil { + return false + } + if newGRPCRoutes[key].UID == cachedGRPCRoute.UID && + newGRPCRoutes[key].Generation > cachedGRPCRoute.Generation { + return false + } + } + return true +} + // GetCachedAPI get cached apistate func (ods *OperatorDataStore) GetCachedAPI(apiName types.NamespacedName) (APIState, bool) { if cachedAPI, found := ods.apiStore[apiName]; found { diff --git a/adapter/internal/operator/synchronizer/grpc_api.go b/adapter/internal/operator/synchronizer/grpc_api.go new file mode 100644 index 000000000..26c0bd92c --- /dev/null +++ b/adapter/internal/operator/synchronizer/grpc_api.go @@ -0,0 +1,184 @@ +/* + * Copyright (c) 2024, WSO2 LLC. (http://www.wso2.org) All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package synchronizer + +import ( + "errors" + gwapiv1a2 "sigs.k8s.io/gateway-api/apis/v1alpha2" + + "github.com/wso2/apk/adapter/config" + "github.com/wso2/apk/adapter/internal/dataholder" + "github.com/wso2/apk/adapter/internal/discovery/xds" + "github.com/wso2/apk/adapter/internal/discovery/xds/common" + "github.com/wso2/apk/adapter/internal/loggers" + "github.com/wso2/apk/adapter/internal/oasparser/model" + "github.com/wso2/apk/adapter/pkg/logging" + "k8s.io/apimachinery/pkg/types" + gwapiv1b1 "sigs.k8s.io/gateway-api/apis/v1beta1" +) + +// extract APIDetails from the GQLRoute +func updateInternalMapsFromGRPCRoute(apiState APIState, grpcRoute *GRPCRouteState, envType string) (*model.AdapterInternalAPI, map[string]struct{}, error) { + adapterInternalAPI, err := generateGRPCAdapterInternalAPI(apiState, grpcRoute, envType) + if err != nil { + loggers.LoggerAPKOperator.ErrorC(logging.PrintError(logging.Error2632, logging.MAJOR, "Error generating AdapterInternalAPI for GRPCRoute: %v. %v", grpcRoute.GRPCRouteCombined.Name, err)) + return nil, nil, err + } + + vHosts := getVhostsForGRPCAPI(grpcRoute.GRPCRouteCombined) + labels := getLabelsForGRPCAPI(grpcRoute.GRPCRouteCombined) + listeners, relativeSectionNames := getListenersForGRPCAPI(grpcRoute.GRPCRouteCombined, adapterInternalAPI.UUID) + // We dont have a use case where a perticular API's two different grpc routes refer to two different gateway. Hence get the first listener name for the list for processing. + if len(listeners) == 0 || len(relativeSectionNames) == 0 { + loggers.LoggerAPKOperator.ErrorC(logging.PrintError(logging.Error2633, logging.MINOR, "Failed to find a matching listener for grpc route: %v. ", + grpcRoute.GRPCRouteCombined.Name)) + return nil, nil, errors.New("failed to find matching listener name for the provided grpc route") + } + listenerName := listeners[0] + sectionName := relativeSectionNames[0] + + if len(listeners) > 0 { + if err := xds.PopulateInternalMaps(adapterInternalAPI, labels, vHosts, sectionName, listenerName); err != nil { + return nil, nil, err + } + } + return adapterInternalAPI, labels, nil +} + +// generateGRPCAdapterInternalAPI this will populate a AdapterInternalAPI representation for an GRPCRoute +func generateGRPCAdapterInternalAPI(apiState APIState, grpcRoute *GRPCRouteState, envType string) (*model.AdapterInternalAPI, error) { + var adapterInternalAPI model.AdapterInternalAPI + adapterInternalAPI.SetIsDefaultVersion(apiState.APIDefinition.Spec.IsDefaultVersion) + adapterInternalAPI.SetInfoAPICR(*apiState.APIDefinition) + adapterInternalAPI.SetAPIDefinitionFile(apiState.APIDefinitionFile) + adapterInternalAPI.SetAPIDefinitionEndpoint(apiState.APIDefinition.Spec.DefinitionPath) + adapterInternalAPI.SetSubscriptionValidation(apiState.SubscriptionValidation) + adapterInternalAPI.EnvType = envType + + environment := apiState.APIDefinition.Spec.Environment + if environment == "" { + conf := config.ReadConfigs() + environment = conf.Adapter.Environment + } + adapterInternalAPI.SetEnvironment(environment) + adapterInternalAPI.SetXWso2RequestBodyPass(true) + + resourceParams := model.ResourceParams{ + AuthSchemes: apiState.Authentications, + ResourceAuthSchemes: apiState.ResourceAuthentications, + BackendMapping: grpcRoute.BackendMapping, + APIPolicies: apiState.APIPolicies, + ResourceAPIPolicies: apiState.ResourceAPIPolicies, + ResourceScopes: grpcRoute.Scopes, + InterceptorServiceMapping: apiState.InterceptorServiceMapping, + BackendJWTMapping: apiState.BackendJWTMapping, + RateLimitPolicies: apiState.RateLimitPolicies, + ResourceRateLimitPolicies: apiState.ResourceRateLimitPolicies, + } + if err := adapterInternalAPI.SetInfoGRPCRouteCR(grpcRoute.GRPCRouteCombined, resourceParams); err != nil { + loggers.LoggerAPKOperator.ErrorC(logging.PrintError(logging.Error2631, logging.MAJOR, "Error setting GRPCRoute CR info to adapterInternalAPI. %v", err)) + return nil, err + } + if apiState.MutualSSL != nil && apiState.MutualSSL.Required != "" && !adapterInternalAPI.GetDisableAuthentications() { + adapterInternalAPI.SetDisableMtls(apiState.MutualSSL.Disabled) + adapterInternalAPI.SetMutualSSL(apiState.MutualSSL.Required) + adapterInternalAPI.SetClientCerts(apiState.APIDefinition.Name, apiState.MutualSSL.ClientCertificates) + } else { + adapterInternalAPI.SetDisableMtls(true) + } + + return &adapterInternalAPI, nil + +} + +// getVhostForAPI returns the vHosts related to an API. +func getVhostsForGRPCAPI(grpcRoute *gwapiv1a2.GRPCRoute) map[string]struct{} { + vHosts := make(map[string]struct{}) + for _, hostName := range grpcRoute.Spec.Hostnames { + vHosts[string(hostName)] = struct{}{} + } + return vHosts +} + +// getLabelsForAPI returns the labels related to an API. +func getLabelsForGRPCAPI(grpcRoute *gwapiv1a2.GRPCRoute) map[string]struct{} { + labels := make(map[string]struct{}) + for _, parentRef := range grpcRoute.Spec.ParentRefs { + err := xds.SanitizeGateway(string(parentRef.Name), false) + if err != nil { + loggers.LoggerAPKOperator.ErrorC(logging.PrintError(logging.Error2653, logging.CRITICAL, "Gateway Label is invalid: %s", string(parentRef.Name))) + } else { + labels[string(parentRef.Name)] = struct{}{} + } + } + return labels +} + +// getListenersForGRPCAPI returns the listeners related to an API. +func getListenersForGRPCAPI(grpcRoute *gwapiv1a2.GRPCRoute, apiUUID string) ([]string, []string) { + var listeners []string + var sectionNames []string + for _, parentRef := range grpcRoute.Spec.ParentRefs { + namespace := grpcRoute.GetNamespace() + if parentRef.Namespace != nil && *parentRef.Namespace != "" { + namespace = string(*parentRef.Namespace) + } + gateway, found := dataholder.GetGatewayMap()[types.NamespacedName{ + Namespace: namespace, + Name: string(parentRef.Name), + }.String()] + if found { + // find the matching listener + matchedListener, listenerFound := common.FindElement(gateway.Spec.Listeners, func(listener gwapiv1b1.Listener) bool { + if string(listener.Name) == string(*parentRef.SectionName) { + return true + } + return false + }) + if listenerFound { + sectionNames = append(sectionNames, string(matchedListener.Name)) + listeners = append(listeners, common.GetEnvoyListenerName(string(matchedListener.Protocol), uint32(matchedListener.Port))) + continue + } + } + loggers.LoggerAPKOperator.Errorf("Failed to find matching listeners for the grpcroute: %+v", grpcRoute.Name) + } + return listeners, sectionNames +} + +func deleteGRPCAPIFromEnv(grpcRoute *gwapiv1a2.GRPCRoute, apiState APIState) error { + labels := getLabelsForGRPCAPI(grpcRoute) + uuid := string(apiState.APIDefinition.ObjectMeta.UID) + return xds.DeleteAPI(uuid, labels) +} + +// undeployGRPCAPIInGateway undeploys the related API in CREATE and UPDATE events. +func undeployGRPCAPIInGateway(apiState APIState) error { + var err error + if apiState.ProdGRPCRoute != nil { + err = deleteGRPCAPIFromEnv(apiState.ProdGRPCRoute.GRPCRouteCombined, apiState) + } + if err != nil { + loggers.LoggerXds.ErrorC(logging.PrintError(logging.Error2630, logging.MAJOR, "Error undeploying prod grpcRoute of API : %v in Organization %v from environments."+ + " Hence not checking on deleting the sand grpcRoute of the API", string(apiState.APIDefinition.ObjectMeta.UID), apiState.APIDefinition.Spec.Organization)) + return err + } + if apiState.SandGRPCRoute != nil { + err = deleteGRPCAPIFromEnv(apiState.SandGRPCRoute.GRPCRouteCombined, apiState) + } + return err +} diff --git a/adapter/internal/operator/synchronizer/synchronizer.go b/adapter/internal/operator/synchronizer/synchronizer.go index a61aa181c..0ce9bbea7 100644 --- a/adapter/internal/operator/synchronizer/synchronizer.go +++ b/adapter/internal/operator/synchronizer/synchronizer.go @@ -117,12 +117,16 @@ func undeployAPIInGateway(apiEvent *APIEvent) error { if apiState.APIDefinition.Spec.APIType == "GraphQL" { err = undeployGQLAPIInGateway(apiState) } + if apiState.APIDefinition.Spec.APIType == "GRPC" { + return undeployGRPCAPIInGateway(apiState) + } if err != nil { loggers.LoggerAPKOperator.ErrorC(logging.PrintError(logging.Error2629, logging.CRITICAL, "API deployment failed for %s event : %v, %v", apiEvent.EventType, apiState.APIDefinition.Name, err)) } else if config.ReadConfigs().PartitionServer.Enabled { paritionCh <- apiEvent } + return nil } @@ -199,6 +203,34 @@ func deployMultipleAPIsInGateway(event *APIEvent, successChannel *chan SuccessEv } } } + if apiState.APIDefinition.Spec.APIType == "GRPC" { + if apiState.ProdGRPCRoute != nil { + _, updatedLabels, err := updateInternalMapsFromGRPCRoute(apiState, apiState.ProdGRPCRoute, constants.Production) + if err != nil { + loggers.LoggerAPKOperator.ErrorC(logging.PrintError(logging.Error2665, logging.CRITICAL, + "Error deploying prod grpcRoute of API : %v in Organization %v from environments %v. Error: %v", + string(apiState.APIDefinition.Spec.APIName), apiState.APIDefinition.Spec.Organization, + getLabelsForGRPCAPI(apiState.ProdGRPCRoute.GRPCRouteCombined), err)) + continue + } + for label := range updatedLabels { + updatedLabelsMap[label] = struct{}{} + } + } + if apiState.SandGRPCRoute != nil { + _, updatedLabels, err := updateInternalMapsFromGRPCRoute(apiState, apiState.SandGRPCRoute, constants.Sandbox) + if err != nil { + loggers.LoggerAPKOperator.ErrorC(logging.PrintError(logging.Error2665, logging.CRITICAL, + "Error deploying sand grpcRoute of API : %v in Organization %v from environments %v. Error: %v", + string(apiState.APIDefinition.Spec.APIName), apiState.APIDefinition.Spec.Organization, + getLabelsForGRPCAPI(apiState.SandGRPCRoute.GRPCRouteCombined), err)) + continue + } + for label := range updatedLabels { + updatedLabelsMap[label] = struct{}{} + } + } + } updatedAPIs = append(updatedAPIs, utils.NamespacedName(apiState.APIDefinition)) } @@ -254,6 +286,13 @@ func SendEventToPartitionServer() { for _, hostName := range httpRoute.HTTPRouteCombined.Spec.Hostnames { hostNames = append(hostNames, string(hostName)) } + grpcRoute := api.ProdGRPCRoute + if grpcRoute == nil { + grpcRoute = api.SandGRPCRoute + } + for _, hostName := range grpcRoute.GRPCRouteCombined.Spec.Hostnames { + hostNames = append(hostNames, string(hostName)) + } data := PartitionEvent{ EventType: eventType, BasePath: basePath, diff --git a/common-controller/internal/operator/config/crd/bases/dp.wso2.com_apis.yaml b/common-controller/internal/operator/config/crd/bases/dp.wso2.com_apis.yaml index 304529c66..a674ff1d2 100644 --- a/common-controller/internal/operator/config/crd/bases/dp.wso2.com_apis.yaml +++ b/common-controller/internal/operator/config/crd/bases/dp.wso2.com_apis.yaml @@ -247,6 +247,7 @@ spec: enum: - REST - GraphQL + - GRPC type: string apiVersion: description: APIVersion is the version number of the API. diff --git a/common-go-libs/apis/dp/v1alpha2/api_types.go b/common-go-libs/apis/dp/v1alpha2/api_types.go index 4579e5e0b..2943fa784 100644 --- a/common-go-libs/apis/dp/v1alpha2/api_types.go +++ b/common-go-libs/apis/dp/v1alpha2/api_types.go @@ -80,16 +80,15 @@ type APISpec struct { Sandbox []EnvConfig `json:"sandbox"` // APIType denotes the type of the API. - // Possible values could be REST, GraphQL, Async + // Possible values could be REST, GraphQL, Async, GRPC etc. // - // +kubebuilder:validation:Enum=REST;GraphQL + // +kubebuilder:validation:Enum=REST;GraphQL;GRPC APIType string `json:"apiType"` // BasePath denotes the basepath of the API. // e.g: /pet-store-api/1.0.6 // // +kubectl:validation:MaxLength=232 - // +kubebuilder:validation:Pattern=^[/][a-zA-Z0-9~/_.-]*$ BasePath string `json:"basePath"` // Organization denotes the organization. diff --git a/common-go-libs/apis/dp/v1alpha2/api_webhook.go b/common-go-libs/apis/dp/v1alpha2/api_webhook.go index 2e3348be3..67faddea5 100644 --- a/common-go-libs/apis/dp/v1alpha2/api_webhook.go +++ b/common-go-libs/apis/dp/v1alpha2/api_webhook.go @@ -24,6 +24,7 @@ import ( "errors" "fmt" "io" + "regexp" "strings" gqlparser "github.com/vektah/gqlparser" @@ -103,6 +104,8 @@ func (r *API) validateAPI() error { if r.Spec.BasePath == "" { allErrs = append(allErrs, field.Required(field.NewPath("spec").Child("basePath"), "API basePath is required")) + } else if errMsg := validateAPIBasePathRegex(r.Spec.BasePath, r.Spec.APIType); errMsg != "" { + allErrs = append(allErrs, field.Invalid(field.NewPath("spec").Child("basePath"), r.Spec.BasePath, errMsg)) } else if errMsg := validateAPIBasePathFormat(r.Spec.BasePath, r.Spec.APIVersion); errMsg != "" { allErrs = append(allErrs, field.Invalid(field.NewPath("spec").Child("basePath"), r.Spec.BasePath, errMsg)) } else if err := r.validateAPIBasePathExistsAndDefaultVersion(); err != nil { @@ -250,6 +253,23 @@ func validateAPIBasePathFormat(basePath string, apiVersion string) string { return "" } +func validateAPIBasePathRegex(basePath, apiType string) string { + var pattern string + if apiType == "GRPC" { + pattern = `^[/][a-zA-Z][a-zA-Z0-9_.]*$` + } else { + pattern = `^[/][a-zA-Z0-9~/_.-]*$` + } + re, err := regexp.Compile(pattern) + if err != nil { + return "Failed to compile basePath regex pattern" + } + if !re.MatchString(basePath) { + return "API basePath is not in a valid format for the specified API type" + } + return "" +} + // getBasePathWithoutVersion returns the basePath without version func getBasePathWithoutVersion(basePath string) string { lastIndex := strings.LastIndex(basePath, "/") diff --git a/common-go-libs/config/crd/bases/dp.wso2.com_apis.yaml b/common-go-libs/config/crd/bases/dp.wso2.com_apis.yaml index 7846de3ca..385c45e7e 100644 --- a/common-go-libs/config/crd/bases/dp.wso2.com_apis.yaml +++ b/common-go-libs/config/crd/bases/dp.wso2.com_apis.yaml @@ -243,10 +243,11 @@ spec: type: array apiType: description: APIType denotes the type of the API. Possible values - could be REST, GraphQL, Async + could be REST, GraphQL, Async, GRPC etc. enum: - REST - GraphQL + - GRPC type: string apiVersion: description: APIVersion is the version number of the API. @@ -256,7 +257,6 @@ spec: type: string basePath: description: 'BasePath denotes the basepath of the API. e.g: /pet-store-api/1.0.6' - pattern: ^[/][a-zA-Z0-9~/_.-]*$ type: string definitionFileRef: description: DefinitionFileRef contains the definition of the API diff --git a/gateway/enforcer/org.wso2.apk.enforcer/src/main/java/org/wso2/apk/enforcer/api/APIFactory.java b/gateway/enforcer/org.wso2.apk.enforcer/src/main/java/org/wso2/apk/enforcer/api/APIFactory.java index 58c9c3dd2..0b21aaf78 100644 --- a/gateway/enforcer/org.wso2.apk.enforcer/src/main/java/org/wso2/apk/enforcer/api/APIFactory.java +++ b/gateway/enforcer/org.wso2.apk.enforcer/src/main/java/org/wso2/apk/enforcer/api/APIFactory.java @@ -78,6 +78,11 @@ public void addApis(List apis) { graphQLAPI.init(api); String apiKey = getApiKey(graphQLAPI); newApis.put(apiKey, graphQLAPI); + } else if (APIConstants.ApiType.GRPC.equals(api.getApiType())) { + GRPCAPI grpcAPI = new GRPCAPI(); + grpcAPI.init(api); + String apiKey = getApiKey(grpcAPI); + newApis.put(apiKey, grpcAPI); } else { RestAPI enforcerApi = new RestAPI(); enforcerApi.init(api); @@ -133,6 +138,11 @@ public byte[] getAPIDefinition(final String basePath, final String version, fina public ResourceConfig getMatchedResource(API api, String matchedResourcePath, String method) { List resourceConfigList = api.getAPIConfig().getResources(); + if (APIConstants.ApiType.GRPC.equals(api.getAPIConfig().getApiType())) { + return resourceConfigList.stream() + .filter(resourceConfig -> resourceConfig.getPath().equals(matchedResourcePath)) + .findFirst().orElse(null); + } return resourceConfigList.stream() .filter(resourceConfig -> resourceConfig.getPath().equals(matchedResourcePath)). filter(resourceConfig -> (method == null) || resourceConfig.getMethod() diff --git a/gateway/enforcer/org.wso2.apk.enforcer/src/main/java/org/wso2/apk/enforcer/api/GRPCAPI.java b/gateway/enforcer/org.wso2.apk.enforcer/src/main/java/org/wso2/apk/enforcer/api/GRPCAPI.java new file mode 100644 index 000000000..0ec44ae71 --- /dev/null +++ b/gateway/enforcer/org.wso2.apk.enforcer/src/main/java/org/wso2/apk/enforcer/api/GRPCAPI.java @@ -0,0 +1,212 @@ +package org.wso2.apk.enforcer.api; + +import graphql.schema.idl.SchemaParser; +import graphql.schema.idl.TypeDefinitionRegistry; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.wso2.apk.enforcer.analytics.AnalyticsFilter; +import org.wso2.apk.enforcer.commons.Filter; +import org.wso2.apk.enforcer.commons.dto.ClaimValueDTO; +import org.wso2.apk.enforcer.commons.dto.JWTConfigurationDto; +import org.wso2.apk.enforcer.commons.model.APIConfig; +import org.wso2.apk.enforcer.commons.model.ResourceConfig; +import org.wso2.apk.enforcer.commons.model.EndpointSecurity; +import org.wso2.apk.enforcer.commons.model.RequestContext; +import org.wso2.apk.enforcer.commons.model.EndpointCluster; +import org.wso2.apk.enforcer.config.ConfigHolder; +import org.wso2.apk.enforcer.config.EnforcerConfig; +import org.wso2.apk.enforcer.constants.APIConstants; +import org.wso2.apk.enforcer.constants.HttpConstants; +import org.wso2.apk.enforcer.cors.CorsFilter; +import org.wso2.apk.enforcer.discovery.api.Api; +import org.wso2.apk.enforcer.discovery.api.Resource; +import org.wso2.apk.enforcer.discovery.api.Operation; +import org.wso2.apk.enforcer.discovery.api.BackendJWTTokenInfo; +import org.wso2.apk.enforcer.discovery.api.Claim; +import org.wso2.apk.enforcer.security.AuthFilter; +import org.wso2.apk.enforcer.security.mtls.MtlsUtils; +import org.wso2.apk.enforcer.server.swagger.APIDefinitionUtils; +import org.wso2.apk.enforcer.util.EndpointUtils; +import org.wso2.apk.enforcer.util.FilterUtils; + +import java.io.IOException; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class GRPCAPI implements API{ + + private static final Logger logger = LogManager.getLogger(GRPCAPI.class); + private final List filters = new ArrayList<>(); + private APIConfig apiConfig; + @Override + public List getFilters() { + return filters; + } + + @Override + public String init(Api api) { + String vhost = api.getVhost(); + String basePath = api.getBasePath(); + String name = api.getTitle(); + String version = api.getVersion(); + String apiType = api.getApiType(); + List resources = new ArrayList<>(); + Map mtlsCertificateTiers = new HashMap<>(); + String mutualSSL = api.getMutualSSL(); + boolean applicationSecurity = api.getApplicationSecurity(); + + EndpointCluster endpoints = Utils.processEndpoints(api.getEndpoints()); + EndpointSecurity[] endpointSecurity = APIProcessUtils.convertProtoEndpointSecurity( + api.getEndpointSecurityList()); + + for (Resource res : api.getResourcesList()) { + for (Operation operation : res.getMethodsList()) { + ResourceConfig resConfig = Utils.buildResource(operation, res.getPath(), endpointSecurity); + resConfig.setEndpoints(endpoints); + resConfig.setPolicyConfig(Utils.genPolicyConfig(operation.getPolicies())); + resources.add(resConfig); + } + } + + KeyStore trustStore; + try { + trustStore = MtlsUtils.createTrustStore(api.getClientCertificatesList()); + } catch (KeyStoreException e) { + throw new SecurityException(e); + } + + BackendJWTTokenInfo backendJWTTokenInfo = api.getBackendJWTTokenInfo(); + JWTConfigurationDto jwtConfigurationDto = new JWTConfigurationDto(); + + // If backendJWTTokeInfo is available + if (api.hasBackendJWTTokenInfo()) { + Map claims = backendJWTTokenInfo.getCustomClaimsMap(); + Map claimsMap = new HashMap<>(); + for (Map.Entry claimEntry : claims.entrySet()) { + Claim claim = claimEntry.getValue(); + ClaimValueDTO claimVal = new ClaimValueDTO(claim.getValue(), claim.getType()); + claimsMap.put(claimEntry.getKey(), claimVal); + } + EnforcerConfig enforcerConfig = ConfigHolder.getInstance().getConfig(); + jwtConfigurationDto.populateConfigValues(backendJWTTokenInfo.getEnabled(), + backendJWTTokenInfo.getHeader(), backendJWTTokenInfo.getSigningAlgorithm(), + backendJWTTokenInfo.getEncoding(), enforcerConfig.getJwtConfigurationDto().getPublicCert(), + enforcerConfig.getJwtConfigurationDto().getPrivateKey(), backendJWTTokenInfo.getTokenTTL(), + claimsMap, enforcerConfig.getJwtConfigurationDto().useKid(), + enforcerConfig.getJwtConfigurationDto().getKidValue()); + } + + + SchemaParser schemaParser = new SchemaParser(); + + byte[] apiDefinition = api.getApiDefinitionFile().toByteArray(); + TypeDefinitionRegistry registry; + + + String apiLifeCycleState = api.getApiLifeCycleState(); + this.apiConfig = new APIConfig.Builder(name).uuid(api.getId()).vhost(vhost).basePath(basePath).version(version) + .resources(resources).apiType(apiType).apiLifeCycleState(apiLifeCycleState).tier(api.getTier()) + .envType(api.getEnvType()).disableAuthentication(api.getDisableAuthentications()) + .disableScopes(api.getDisableScopes()).trustStore(trustStore).organizationId(api.getOrganizationId()) + .mutualSSL(mutualSSL) + .applicationSecurity(applicationSecurity).jwtConfigurationDto(jwtConfigurationDto) + .apiDefinition(apiDefinition).environment(api.getEnvironment()) + .environment(api.getEnvironment()) + .subscriptionValidation(api.getSubscriptionValidation()).build(); + initFilters(); + logger.info("APIConfig: " + this.apiConfig); + return basePath; + } + + @Override + public ResponseObject process(RequestContext requestContext) { + + ResponseObject responseObject = new ResponseObject(requestContext.getRequestID()); + responseObject.setRequestPath(requestContext.getRequestPath()); + boolean analyticsEnabled = ConfigHolder.getInstance().getConfig().getAnalyticsConfig().isEnabled(); + + Utils.handleCommonHeaders(requestContext); + boolean isExistsMatchedOperations = requestContext.getMatchedResourcePaths() != null && + requestContext.getMatchedResourcePaths().size() > 0; + // This flag is used to apply CORS filter + boolean isOptionCall = requestContext.getRequestMethod().contains(HttpConstants.OPTIONS); + + // handle other not allowed && non option request && not yet handled error + // scenarios. + if ((!isOptionCall && !isExistsMatchedOperations) && !requestContext.getProperties() + .containsKey(APIConstants.MessageFormat.ERROR_CODE)) { + requestContext.getProperties() + .put(APIConstants.MessageFormat.STATUS_CODE, APIConstants.StatusCodes.NOTFOUND.getCode()); + requestContext.getProperties().put(APIConstants.MessageFormat.ERROR_CODE, + APIConstants.StatusCodes.NOTFOUND.getValue()); + requestContext.getProperties().put(APIConstants.MessageFormat.ERROR_MESSAGE, + APIConstants.NOT_FOUND_MESSAGE); + requestContext.getProperties().put(APIConstants.MessageFormat.ERROR_DESCRIPTION, + APIConstants.NOT_FOUND_DESCRIPTION); + } + + if ((isExistsMatchedOperations || isOptionCall) && executeFilterChain(requestContext)) { + EndpointUtils.updateClusterHeaderAndCheckEnv(requestContext); + responseObject.setOrganizationId(requestContext.getMatchedAPI().getOrganizationId()); + responseObject.setRemoveHeaderMap(requestContext.getRemoveHeaders()); + responseObject.setQueryParamsToRemove(requestContext.getQueryParamsToRemove()); + responseObject.setRemoveAllQueryParams(requestContext.isRemoveAllQueryParams()); + responseObject.setQueryParamsToAdd(requestContext.getQueryParamsToAdd()); + responseObject.setQueryParamMap(requestContext.getQueryParameters()); + responseObject.setStatusCode(APIConstants.StatusCodes.OK.getCode()); + if (requestContext.getAddHeaders() != null && requestContext.getAddHeaders().size() > 0) { + responseObject.setHeaderMap(requestContext.getAddHeaders()); + } + if (analyticsEnabled) { + AnalyticsFilter.getInstance().handleSuccessRequest(requestContext); + } + // set metadata for interceptors + responseObject.setMetaDataMap(requestContext.getMetadataMap()); + } else { + // If enforcer stops with a false, it will be passed directly to the client. + responseObject.setDirectResponse(true); + responseObject.setStatusCode(Integer.parseInt( + requestContext.getProperties().get(APIConstants.MessageFormat.STATUS_CODE).toString())); + if (requestContext.getProperties().containsKey(APIConstants.MessageFormat.ERROR_CODE)) { + responseObject.setErrorCode( + requestContext.getProperties().get(APIConstants.MessageFormat.ERROR_CODE).toString()); + } + if (requestContext.getProperties().get(APIConstants.MessageFormat.ERROR_MESSAGE) != null) { + responseObject.setErrorMessage(requestContext.getProperties() + .get(APIConstants.MessageFormat.ERROR_MESSAGE).toString()); + } + if (requestContext.getProperties().get(APIConstants.MessageFormat.ERROR_DESCRIPTION) != null) { + responseObject.setErrorDescription(requestContext.getProperties() + .get(APIConstants.MessageFormat.ERROR_DESCRIPTION).toString()); + } + if (requestContext.getAddHeaders() != null && requestContext.getAddHeaders().size() > 0) { + responseObject.setHeaderMap(requestContext.getAddHeaders()); + } + if (analyticsEnabled && !FilterUtils.isSkippedAnalyticsFaultEvent(responseObject.getErrorCode())) { + AnalyticsFilter.getInstance().handleFailureRequest(requestContext); + responseObject.setMetaDataMap(new HashMap<>(0)); + } + } + + return responseObject; + } + + @Override + public APIConfig getAPIConfig() { + return this.apiConfig; + } + + private void initFilters() { + AuthFilter authFilter = new AuthFilter(); + authFilter.init(apiConfig, null); + this.filters.add(authFilter); + + // CORS filter is added as the first filter, and it is not customizable. + CorsFilter corsFilter = new CorsFilter(); + this.filters.add(0, corsFilter); + } +} diff --git a/gateway/enforcer/org.wso2.apk.enforcer/src/main/java/org/wso2/apk/enforcer/constants/APIConstants.java b/gateway/enforcer/org.wso2.apk.enforcer/src/main/java/org/wso2/apk/enforcer/constants/APIConstants.java index 348f29150..a911a06af 100644 --- a/gateway/enforcer/org.wso2.apk.enforcer/src/main/java/org/wso2/apk/enforcer/constants/APIConstants.java +++ b/gateway/enforcer/org.wso2.apk.enforcer/src/main/java/org/wso2/apk/enforcer/constants/APIConstants.java @@ -270,6 +270,7 @@ public static class ApiType { public static final String WEB_SOCKET = "WS"; public static final String GRAPHQL = "GraphQL"; + public static final String GRPC = "GRPC"; } /** diff --git a/gateway/router/.gitignore b/gateway/router/.gitignore new file mode 100644 index 000000000..e47e1b33a --- /dev/null +++ b/gateway/router/.gitignore @@ -0,0 +1 @@ +/resources diff --git a/helm-charts/templates/crds/dp.wso2.com_apis.yaml b/helm-charts/templates/crds/dp.wso2.com_apis.yaml index 399c0c01f..c398f3988 100644 --- a/helm-charts/templates/crds/dp.wso2.com_apis.yaml +++ b/helm-charts/templates/crds/dp.wso2.com_apis.yaml @@ -256,10 +256,11 @@ spec: type: array apiType: description: APIType denotes the type of the API. Possible values - could be REST, GraphQL, Async + could be REST, GraphQL, Async, GRPC etc. enum: - REST - GraphQL + - GRPC type: string apiVersion: description: APIVersion is the version number of the API. diff --git a/helm-charts/templates/serviceAccount/apk-cluster-role.yaml b/helm-charts/templates/serviceAccount/apk-cluster-role.yaml index ccbb849a5..428ce7abf 100644 --- a/helm-charts/templates/serviceAccount/apk-cluster-role.yaml +++ b/helm-charts/templates/serviceAccount/apk-cluster-role.yaml @@ -24,7 +24,7 @@ rules: resources: ["services","configmaps","secrets"] verbs: ["get","list","watch","update","delete","create"] - apiGroups: ["gateway.networking.k8s.io"] - resources: ["httproutes","gateways","gatewayclasses"] + resources: ["httproutes","gateways","grpcroutes","gatewayclasses"] verbs: ["get","list","watch","update","delete","create"] - apiGroups: [ "gateway.networking.k8s.io" ] resources: [ "gateways/status","gatewayclasses/status","httproutes/status" ] @@ -122,6 +122,15 @@ rules: - apiGroups: ["dp.wso2.com"] resources: ["gqlroutes/status"] verbs: ["get","patch","update"] + - apiGroups: ["dp.wso2.com"] + resources: ["grpcroutes"] + verbs: ["get","list","watch","update","delete","create"] + - apiGroups: ["dp.wso2.com"] + resources: ["grpcroutes/finalizers"] + verbs: ["update"] + - apiGroups: ["dp.wso2.com"] + resources: ["grpcroutes/status"] + verbs: ["get","patch","update"] - apiGroups: ["cp.wso2.com"] resources: ["applications"] verbs: ["get","list","watch","update","delete","create"] diff --git a/test/integration/integration/tests/grpc-api.go b/test/integration/integration/tests/grpc-api.go new file mode 100644 index 000000000..43632c1f9 --- /dev/null +++ b/test/integration/integration/tests/grpc-api.go @@ -0,0 +1,76 @@ +/* + * Copyright (c) 2024, WSO2 LLC. (http://www.wso2.org) All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package tests + +import ( + "github.com/wso2/apk/test/integration/integration/utils/grpc-code/student" + "github.com/wso2/apk/test/integration/integration/utils/grpc-code/student_default_version" + "github.com/wso2/apk/test/integration/integration/utils/grpcutils" + "github.com/wso2/apk/test/integration/integration/utils/suite" + "testing" +) + +func init() { + IntegrationTests = append(IntegrationTests, GRPCAPI) +} + +// GRPCAPI tests gRPC API +var GRPCAPI = suite.IntegrationTest{ + ShortName: "GRPCAPI", + Description: "Tests gRPC API", + Manifests: []string{"tests/grpc-api.yaml"}, + Test: func(t *testing.T, suite *suite.IntegrationTestSuite) { + gwAddr := "grpc.test.gw.wso2.com:9095" + + testCases := []grpcutils.GRPCTestCase{ + { + ExpectedResponse: grpcutils.ExpectedResponse{ + Out: &student.StudentResponse{ + Name: "Dineth", + Age: 10, + }, + Err: nil, + }, + ActualResponse: &student.StudentResponse{}, + Name: "Get Student Details", + Method: student.GetStudent, + Satisfier: student.StudentResponseSatisfier{}, + }, + { + ExpectedResponse: grpcutils.ExpectedResponse{ + Out: &student_default_version.StudentResponse{ + Name: "Dineth", + Age: 10, + }, + Err: nil, + }, + ActualResponse: &student_default_version.StudentResponse{}, + Name: "Get Student Details (Default API Version)", + Method: student_default_version.GetStudent, + Satisfier: student_default_version.StudentResponseSatisfier{}, + }, + } + for i := range testCases { + tc := testCases[i] + t.Run("Invoke gRPC API", func(t *testing.T) { + t.Parallel() + grpcutils.InvokeGRPCClientUntilSatisfied(gwAddr, t, tc, tc.Satisfier, tc.Method) + }) + } + }, +} diff --git a/test/integration/integration/tests/resources/base/manifests.yaml b/test/integration/integration/tests/resources/base/manifests.yaml index 5d4ef2460..58883fa98 100644 --- a/test/integration/integration/tests/resources/base/manifests.yaml +++ b/test/integration/integration/tests/resources/base/manifests.yaml @@ -113,6 +113,53 @@ spec: --- apiVersion: v1 kind: Service +metadata: + name: grpc-backend-v1 + namespace: gateway-integration-test-infra +spec: + selector: + app: grpc-backend-v1 + ports: + - protocol: TCP + port: 6565 + targetPort: 6565 +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: grpc-backend-v1 + namespace: gateway-integration-test-infra + labels: + app: grpc-backend-v1 +spec: + replicas: 1 + selector: + matchLabels: + app: grpc-backend-v1 + template: + metadata: + labels: + app: grpc-backend-v1 + spec: + containers: + - name: grpc-backend-v1 + image: ddh13/dineth-grpc-demo-server:1.0.0 + imagePullPolicy: Always + env: + - name: POD_NAME + valueFrom: + fieldRef: + fieldPath: metadata.name + - name: NAMESPACE + valueFrom: + fieldRef: + fieldPath: metadata.namespace + resources: + requests: + cpu: 10m +--- +apiVersion: v1 +kind: Service metadata: name: infra-backend-v2 namespace: gateway-integration-test-infra diff --git a/test/integration/integration/tests/resources/tests/grpc-api.yaml b/test/integration/integration/tests/resources/tests/grpc-api.yaml new file mode 100644 index 000000000..6329c8777 --- /dev/null +++ b/test/integration/integration/tests/resources/tests/grpc-api.yaml @@ -0,0 +1,109 @@ +# Copyright (c) 2024, WSO2 LLC. (https://www.wso2.com) All Rights Reserved. +# +# WSO2 LLC. licenses this file to you under the Apache License, +# Version 2.0 (the "License"); you may not use this file except +# in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +apiVersion: dp.wso2.com/v1alpha2 +kind: API +metadata: + name: grpc-api + namespace: gateway-integration-test-infra +spec: + apiName: GRPC API + apiType: GRPC + apiVersion: v1 + basePath: /dineth.grpc.api.v1 + isDefaultVersion: true + production: + - routeRefs: + - grpc-api-grpcroute + organization: wso2-org + +--- + +apiVersion: gateway.networking.k8s.io/v1alpha2 +kind: GRPCRoute +metadata: + name: grpc-api-grpcroute + namespace: gateway-integration-test-infra +spec: + parentRefs: + - group: gateway.networking.k8s.io + kind: Gateway + name: wso2-apk-default + namespace: apk-integration-test + sectionName: httpslistener + hostnames: + - grpc.test.gw.wso2.com + rules: + - matches: + - method: + service: student.StudentService + method: GetStudent + backendRefs: + - name: grpc-backend-v1 + kind: Backend + port: 6565 + - matches: + - method: + service: student.StudentService + method: SendStudentStream + backendRefs: + - name: grpc-backend-v1 + kind: Backend + port: 6565 + - matches: + - method: + service: student.StudentService + method: GetStudentStream + backendRefs: + - name: grpc-backend-v1 + kind: Backend + port: 6565 + - matches: + - method: + service: student.StudentService + method: SendAndGetStudentStream + backendRefs: + - name: grpc-backend-v1 + kind: Backend + port: 6565 + +--- + +apiVersion: dp.wso2.com/v1alpha1 +kind: Authentication +metadata: + name: disable-grpc-api-security + namespace: gateway-integration-test-infra +spec: + override: + disabled: true + targetRef: + group: gateway.networking.k8s.io + kind: API + namespace: gateway-integration-test-infra + name: grpc-api +--- +apiVersion: dp.wso2.com/v1alpha1 +kind: Backend +metadata: + name: grpc-backend-v1 + namespace: gateway-integration-test-infra +spec: + services: + - host: grpc-backend-v1.gateway-integration-test-infra + port: 6565 + basePath: "" + protocol: http diff --git a/test/integration/integration/utils/grpc-code/student/student.go b/test/integration/integration/utils/grpc-code/student/student.go new file mode 100644 index 000000000..36143eb56 --- /dev/null +++ b/test/integration/integration/utils/grpc-code/student/student.go @@ -0,0 +1,46 @@ +package student + +import ( + "context" + "github.com/wso2/apk/test/integration/integration/utils/grpcutils" + "google.golang.org/grpc" + "log" + "time" +) + +type StudentResponseSatisfier struct{} + +// IsSatisfactory checks if the given response is satisfactory based on the expected response. +func (srs StudentResponseSatisfier) IsSatisfactory(response any, expectedResponse grpcutils.ExpectedResponse) bool { + // Type assert the response to *student.StudentResponse + resp, respOk := response.(*StudentResponse) + if !respOk { + log.Println("Failed to assert response as *student.StudentResponse") + return false + } + // Type assert the expected output to *student.StudentResponse + expectedResp, expOk := expectedResponse.Out.(*StudentResponse) + if !expOk { + log.Println("Failed to assert expectedResponse.Out as *student.StudentResponse") + return false + } + + // Compare the actual response with the expected response + if resp.Name == expectedResp.Name && resp.Age == expectedResp.Age { + return true + } else { + log.Println("Response does not match the expected output") + return false + } +} +func GetStudent(conn *grpc.ClientConn) (any, error) { + c := NewStudentServiceClient(conn) + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + r := &StudentRequest{Id: 1234} + response, err := c.GetStudent(ctx, r) + if err != nil { + return nil, err + } + return response, nil +} diff --git a/test/integration/integration/utils/grpc-code/student/student.pb.go b/test/integration/integration/utils/grpc-code/student/student.pb.go new file mode 100644 index 000000000..c8efeffa3 --- /dev/null +++ b/test/integration/integration/utils/grpc-code/student/student.pb.go @@ -0,0 +1,254 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.33.0 +// protoc v3.12.4 +// source: student.proto + +package student + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type StudentRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Id int32 `protobuf:"varint,3,opt,name=id,proto3" json:"id,omitempty"` +} + +func (x *StudentRequest) Reset() { + *x = StudentRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_student_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *StudentRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*StudentRequest) ProtoMessage() {} + +func (x *StudentRequest) ProtoReflect() protoreflect.Message { + mi := &file_student_proto_msgTypes[0] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use StudentRequest.ProtoReflect.Descriptor instead. +func (*StudentRequest) Descriptor() ([]byte, []int) { + return file_student_proto_rawDescGZIP(), []int{0} +} + +func (x *StudentRequest) GetId() int32 { + if x != nil { + return x.Id + } + return 0 +} + +type StudentResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + Age int32 `protobuf:"varint,2,opt,name=age,proto3" json:"age,omitempty"` +} + +func (x *StudentResponse) Reset() { + *x = StudentResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_student_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *StudentResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*StudentResponse) ProtoMessage() {} + +func (x *StudentResponse) ProtoReflect() protoreflect.Message { + mi := &file_student_proto_msgTypes[1] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use StudentResponse.ProtoReflect.Descriptor instead. +func (*StudentResponse) Descriptor() ([]byte, []int) { + return file_student_proto_rawDescGZIP(), []int{1} +} + +func (x *StudentResponse) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *StudentResponse) GetAge() int32 { + if x != nil { + return x.Age + } + return 0 +} + +var File_student_proto protoreflect.FileDescriptor + +var file_student_proto_rawDesc = []byte{ + 0x0a, 0x0d, 0x73, 0x74, 0x75, 0x64, 0x65, 0x6e, 0x74, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, + 0x1a, 0x64, 0x69, 0x6e, 0x65, 0x74, 0x68, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e, 0x61, 0x70, 0x69, + 0x2e, 0x76, 0x31, 0x2e, 0x73, 0x74, 0x75, 0x64, 0x65, 0x6e, 0x74, 0x22, 0x20, 0x0a, 0x0e, 0x53, + 0x74, 0x75, 0x64, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x0e, 0x0a, + 0x02, 0x69, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x05, 0x52, 0x02, 0x69, 0x64, 0x22, 0x37, 0x0a, + 0x0f, 0x53, 0x74, 0x75, 0x64, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, + 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x61, 0x67, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, + 0x05, 0x52, 0x03, 0x61, 0x67, 0x65, 0x32, 0xd6, 0x03, 0x0a, 0x0e, 0x53, 0x74, 0x75, 0x64, 0x65, + 0x6e, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x67, 0x0a, 0x0a, 0x47, 0x65, 0x74, + 0x53, 0x74, 0x75, 0x64, 0x65, 0x6e, 0x74, 0x12, 0x2a, 0x2e, 0x64, 0x69, 0x6e, 0x65, 0x74, 0x68, + 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x31, 0x2e, 0x73, 0x74, 0x75, + 0x64, 0x65, 0x6e, 0x74, 0x2e, 0x53, 0x74, 0x75, 0x64, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x1a, 0x2b, 0x2e, 0x64, 0x69, 0x6e, 0x65, 0x74, 0x68, 0x2e, 0x67, 0x72, 0x70, + 0x63, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x31, 0x2e, 0x73, 0x74, 0x75, 0x64, 0x65, 0x6e, 0x74, + 0x2e, 0x53, 0x74, 0x75, 0x64, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x22, 0x00, 0x12, 0x6f, 0x0a, 0x10, 0x47, 0x65, 0x74, 0x53, 0x74, 0x75, 0x64, 0x65, 0x6e, 0x74, + 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x12, 0x2a, 0x2e, 0x64, 0x69, 0x6e, 0x65, 0x74, 0x68, 0x2e, + 0x67, 0x72, 0x70, 0x63, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x31, 0x2e, 0x73, 0x74, 0x75, 0x64, + 0x65, 0x6e, 0x74, 0x2e, 0x53, 0x74, 0x75, 0x64, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x1a, 0x2b, 0x2e, 0x64, 0x69, 0x6e, 0x65, 0x74, 0x68, 0x2e, 0x67, 0x72, 0x70, 0x63, + 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x31, 0x2e, 0x73, 0x74, 0x75, 0x64, 0x65, 0x6e, 0x74, 0x2e, + 0x53, 0x74, 0x75, 0x64, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, + 0x00, 0x30, 0x01, 0x12, 0x70, 0x0a, 0x11, 0x53, 0x65, 0x6e, 0x64, 0x53, 0x74, 0x75, 0x64, 0x65, + 0x6e, 0x74, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x12, 0x2a, 0x2e, 0x64, 0x69, 0x6e, 0x65, 0x74, + 0x68, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x31, 0x2e, 0x73, 0x74, + 0x75, 0x64, 0x65, 0x6e, 0x74, 0x2e, 0x53, 0x74, 0x75, 0x64, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2b, 0x2e, 0x64, 0x69, 0x6e, 0x65, 0x74, 0x68, 0x2e, 0x67, 0x72, + 0x70, 0x63, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x31, 0x2e, 0x73, 0x74, 0x75, 0x64, 0x65, 0x6e, + 0x74, 0x2e, 0x53, 0x74, 0x75, 0x64, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, + 0x65, 0x22, 0x00, 0x28, 0x01, 0x12, 0x78, 0x0a, 0x17, 0x53, 0x65, 0x6e, 0x64, 0x41, 0x6e, 0x64, + 0x47, 0x65, 0x74, 0x53, 0x74, 0x75, 0x64, 0x65, 0x6e, 0x74, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, + 0x12, 0x2a, 0x2e, 0x64, 0x69, 0x6e, 0x65, 0x74, 0x68, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e, 0x61, + 0x70, 0x69, 0x2e, 0x76, 0x31, 0x2e, 0x73, 0x74, 0x75, 0x64, 0x65, 0x6e, 0x74, 0x2e, 0x53, 0x74, + 0x75, 0x64, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2b, 0x2e, 0x64, + 0x69, 0x6e, 0x65, 0x74, 0x68, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, + 0x31, 0x2e, 0x73, 0x74, 0x75, 0x64, 0x65, 0x6e, 0x74, 0x2e, 0x53, 0x74, 0x75, 0x64, 0x65, 0x6e, + 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x28, 0x01, 0x30, 0x01, 0x42, + 0x30, 0x5a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x67, 0x6f, 0x6c, 0x61, 0x6e, 0x67, + 0x2e, 0x6f, 0x72, 0x67, 0x2f, 0x67, 0x72, 0x70, 0x63, 0x2f, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, + 0x65, 0x73, 0x2f, 0x73, 0x63, 0x68, 0x6f, 0x6f, 0x6c, 0x2f, 0x73, 0x74, 0x75, 0x64, 0x65, 0x6e, + 0x74, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_student_proto_rawDescOnce sync.Once + file_student_proto_rawDescData = file_student_proto_rawDesc +) + +func file_student_proto_rawDescGZIP() []byte { + file_student_proto_rawDescOnce.Do(func() { + file_student_proto_rawDescData = protoimpl.X.CompressGZIP(file_student_proto_rawDescData) + }) + return file_student_proto_rawDescData +} + +var file_student_proto_msgTypes = make([]protoimpl.MessageInfo, 2) +var file_student_proto_goTypes = []interface{}{ + (*StudentRequest)(nil), // 0: dineth.grpc.api.v1.student.StudentRequest + (*StudentResponse)(nil), // 1: dineth.grpc.api.v1.student.StudentResponse +} +var file_student_proto_depIdxs = []int32{ + 0, // 0: dineth.grpc.api.v1.student.StudentService.GetStudent:input_type -> dineth.grpc.api.v1.student.StudentRequest + 0, // 1: dineth.grpc.api.v1.student.StudentService.GetStudentStream:input_type -> dineth.grpc.api.v1.student.StudentRequest + 0, // 2: dineth.grpc.api.v1.student.StudentService.SendStudentStream:input_type -> dineth.grpc.api.v1.student.StudentRequest + 0, // 3: dineth.grpc.api.v1.student.StudentService.SendAndGetStudentStream:input_type -> dineth.grpc.api.v1.student.StudentRequest + 1, // 4: dineth.grpc.api.v1.student.StudentService.GetStudent:output_type -> dineth.grpc.api.v1.student.StudentResponse + 1, // 5: dineth.grpc.api.v1.student.StudentService.GetStudentStream:output_type -> dineth.grpc.api.v1.student.StudentResponse + 1, // 6: dineth.grpc.api.v1.student.StudentService.SendStudentStream:output_type -> dineth.grpc.api.v1.student.StudentResponse + 1, // 7: dineth.grpc.api.v1.student.StudentService.SendAndGetStudentStream:output_type -> dineth.grpc.api.v1.student.StudentResponse + 4, // [4:8] is the sub-list for method output_type + 0, // [0:4] is the sub-list for method input_type + 0, // [0:0] is the sub-list for extension type_name + 0, // [0:0] is the sub-list for extension extendee + 0, // [0:0] is the sub-list for field type_name +} + +func init() { file_student_proto_init() } +func file_student_proto_init() { + if File_student_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_student_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*StudentRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_student_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*StudentResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_student_proto_rawDesc, + NumEnums: 0, + NumMessages: 2, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_student_proto_goTypes, + DependencyIndexes: file_student_proto_depIdxs, + MessageInfos: file_student_proto_msgTypes, + }.Build() + File_student_proto = out.File + file_student_proto_rawDesc = nil + file_student_proto_goTypes = nil + file_student_proto_depIdxs = nil +} diff --git a/test/integration/integration/utils/grpc-code/student/student_grpc.pb.go b/test/integration/integration/utils/grpc-code/student/student_grpc.pb.go new file mode 100644 index 000000000..be4b29e7d --- /dev/null +++ b/test/integration/integration/utils/grpc-code/student/student_grpc.pb.go @@ -0,0 +1,314 @@ +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. +// versions: +// - protoc-gen-go-grpc v1.3.0 +// - protoc v3.12.4 +// source: student.proto + +package student + +import ( + context "context" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" +) + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +// Requires gRPC-Go v1.32.0 or later. +const _ = grpc.SupportPackageIsVersion7 + +const ( + StudentService_GetStudent_FullMethodName = "/dineth.grpc.api.v1.student.StudentService/GetStudent" + StudentService_GetStudentStream_FullMethodName = "/dineth.grpc.api.v1.student.StudentService/GetStudentStream" + StudentService_SendStudentStream_FullMethodName = "/dineth.grpc.api.v1.student.StudentService/SendStudentStream" + StudentService_SendAndGetStudentStream_FullMethodName = "/dineth.grpc.api.v1.student.StudentService/SendAndGetStudentStream" +) + +// StudentServiceClient is the client API for StudentService service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +type StudentServiceClient interface { + GetStudent(ctx context.Context, in *StudentRequest, opts ...grpc.CallOption) (*StudentResponse, error) + GetStudentStream(ctx context.Context, in *StudentRequest, opts ...grpc.CallOption) (StudentService_GetStudentStreamClient, error) + SendStudentStream(ctx context.Context, opts ...grpc.CallOption) (StudentService_SendStudentStreamClient, error) + SendAndGetStudentStream(ctx context.Context, opts ...grpc.CallOption) (StudentService_SendAndGetStudentStreamClient, error) +} + +type studentServiceClient struct { + cc grpc.ClientConnInterface +} + +func NewStudentServiceClient(cc grpc.ClientConnInterface) StudentServiceClient { + return &studentServiceClient{cc} +} + +func (c *studentServiceClient) GetStudent(ctx context.Context, in *StudentRequest, opts ...grpc.CallOption) (*StudentResponse, error) { + out := new(StudentResponse) + err := c.cc.Invoke(ctx, StudentService_GetStudent_FullMethodName, in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *studentServiceClient) GetStudentStream(ctx context.Context, in *StudentRequest, opts ...grpc.CallOption) (StudentService_GetStudentStreamClient, error) { + stream, err := c.cc.NewStream(ctx, &StudentService_ServiceDesc.Streams[0], StudentService_GetStudentStream_FullMethodName, opts...) + if err != nil { + return nil, err + } + x := &studentServiceGetStudentStreamClient{stream} + if err := x.ClientStream.SendMsg(in); err != nil { + return nil, err + } + if err := x.ClientStream.CloseSend(); err != nil { + return nil, err + } + return x, nil +} + +type StudentService_GetStudentStreamClient interface { + Recv() (*StudentResponse, error) + grpc.ClientStream +} + +type studentServiceGetStudentStreamClient struct { + grpc.ClientStream +} + +func (x *studentServiceGetStudentStreamClient) Recv() (*StudentResponse, error) { + m := new(StudentResponse) + if err := x.ClientStream.RecvMsg(m); err != nil { + return nil, err + } + return m, nil +} + +func (c *studentServiceClient) SendStudentStream(ctx context.Context, opts ...grpc.CallOption) (StudentService_SendStudentStreamClient, error) { + stream, err := c.cc.NewStream(ctx, &StudentService_ServiceDesc.Streams[1], StudentService_SendStudentStream_FullMethodName, opts...) + if err != nil { + return nil, err + } + x := &studentServiceSendStudentStreamClient{stream} + return x, nil +} + +type StudentService_SendStudentStreamClient interface { + Send(*StudentRequest) error + CloseAndRecv() (*StudentResponse, error) + grpc.ClientStream +} + +type studentServiceSendStudentStreamClient struct { + grpc.ClientStream +} + +func (x *studentServiceSendStudentStreamClient) Send(m *StudentRequest) error { + return x.ClientStream.SendMsg(m) +} + +func (x *studentServiceSendStudentStreamClient) CloseAndRecv() (*StudentResponse, error) { + if err := x.ClientStream.CloseSend(); err != nil { + return nil, err + } + m := new(StudentResponse) + if err := x.ClientStream.RecvMsg(m); err != nil { + return nil, err + } + return m, nil +} + +func (c *studentServiceClient) SendAndGetStudentStream(ctx context.Context, opts ...grpc.CallOption) (StudentService_SendAndGetStudentStreamClient, error) { + stream, err := c.cc.NewStream(ctx, &StudentService_ServiceDesc.Streams[2], StudentService_SendAndGetStudentStream_FullMethodName, opts...) + if err != nil { + return nil, err + } + x := &studentServiceSendAndGetStudentStreamClient{stream} + return x, nil +} + +type StudentService_SendAndGetStudentStreamClient interface { + Send(*StudentRequest) error + Recv() (*StudentResponse, error) + grpc.ClientStream +} + +type studentServiceSendAndGetStudentStreamClient struct { + grpc.ClientStream +} + +func (x *studentServiceSendAndGetStudentStreamClient) Send(m *StudentRequest) error { + return x.ClientStream.SendMsg(m) +} + +func (x *studentServiceSendAndGetStudentStreamClient) Recv() (*StudentResponse, error) { + m := new(StudentResponse) + if err := x.ClientStream.RecvMsg(m); err != nil { + return nil, err + } + return m, nil +} + +// StudentServiceServer is the server API for StudentService service. +// All implementations must embed UnimplementedStudentServiceServer +// for forward compatibility +type StudentServiceServer interface { + GetStudent(context.Context, *StudentRequest) (*StudentResponse, error) + GetStudentStream(*StudentRequest, StudentService_GetStudentStreamServer) error + SendStudentStream(StudentService_SendStudentStreamServer) error + SendAndGetStudentStream(StudentService_SendAndGetStudentStreamServer) error + mustEmbedUnimplementedStudentServiceServer() +} + +// UnimplementedStudentServiceServer must be embedded to have forward compatible implementations. +type UnimplementedStudentServiceServer struct { +} + +func (UnimplementedStudentServiceServer) GetStudent(context.Context, *StudentRequest) (*StudentResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method GetStudent not implemented") +} +func (UnimplementedStudentServiceServer) GetStudentStream(*StudentRequest, StudentService_GetStudentStreamServer) error { + return status.Errorf(codes.Unimplemented, "method GetStudentStream not implemented") +} +func (UnimplementedStudentServiceServer) SendStudentStream(StudentService_SendStudentStreamServer) error { + return status.Errorf(codes.Unimplemented, "method SendStudentStream not implemented") +} +func (UnimplementedStudentServiceServer) SendAndGetStudentStream(StudentService_SendAndGetStudentStreamServer) error { + return status.Errorf(codes.Unimplemented, "method SendAndGetStudentStream not implemented") +} +func (UnimplementedStudentServiceServer) mustEmbedUnimplementedStudentServiceServer() {} + +// UnsafeStudentServiceServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to StudentServiceServer will +// result in compilation errors. +type UnsafeStudentServiceServer interface { + mustEmbedUnimplementedStudentServiceServer() +} + +func RegisterStudentServiceServer(s grpc.ServiceRegistrar, srv StudentServiceServer) { + s.RegisterService(&StudentService_ServiceDesc, srv) +} + +func _StudentService_GetStudent_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(StudentRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(StudentServiceServer).GetStudent(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: StudentService_GetStudent_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(StudentServiceServer).GetStudent(ctx, req.(*StudentRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _StudentService_GetStudentStream_Handler(srv interface{}, stream grpc.ServerStream) error { + m := new(StudentRequest) + if err := stream.RecvMsg(m); err != nil { + return err + } + return srv.(StudentServiceServer).GetStudentStream(m, &studentServiceGetStudentStreamServer{stream}) +} + +type StudentService_GetStudentStreamServer interface { + Send(*StudentResponse) error + grpc.ServerStream +} + +type studentServiceGetStudentStreamServer struct { + grpc.ServerStream +} + +func (x *studentServiceGetStudentStreamServer) Send(m *StudentResponse) error { + return x.ServerStream.SendMsg(m) +} + +func _StudentService_SendStudentStream_Handler(srv interface{}, stream grpc.ServerStream) error { + return srv.(StudentServiceServer).SendStudentStream(&studentServiceSendStudentStreamServer{stream}) +} + +type StudentService_SendStudentStreamServer interface { + SendAndClose(*StudentResponse) error + Recv() (*StudentRequest, error) + grpc.ServerStream +} + +type studentServiceSendStudentStreamServer struct { + grpc.ServerStream +} + +func (x *studentServiceSendStudentStreamServer) SendAndClose(m *StudentResponse) error { + return x.ServerStream.SendMsg(m) +} + +func (x *studentServiceSendStudentStreamServer) Recv() (*StudentRequest, error) { + m := new(StudentRequest) + if err := x.ServerStream.RecvMsg(m); err != nil { + return nil, err + } + return m, nil +} + +func _StudentService_SendAndGetStudentStream_Handler(srv interface{}, stream grpc.ServerStream) error { + return srv.(StudentServiceServer).SendAndGetStudentStream(&studentServiceSendAndGetStudentStreamServer{stream}) +} + +type StudentService_SendAndGetStudentStreamServer interface { + Send(*StudentResponse) error + Recv() (*StudentRequest, error) + grpc.ServerStream +} + +type studentServiceSendAndGetStudentStreamServer struct { + grpc.ServerStream +} + +func (x *studentServiceSendAndGetStudentStreamServer) Send(m *StudentResponse) error { + return x.ServerStream.SendMsg(m) +} + +func (x *studentServiceSendAndGetStudentStreamServer) Recv() (*StudentRequest, error) { + m := new(StudentRequest) + if err := x.ServerStream.RecvMsg(m); err != nil { + return nil, err + } + return m, nil +} + +// StudentService_ServiceDesc is the grpc.ServiceDesc for StudentService service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var StudentService_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "dineth.grpc.api.v1.student.StudentService", + HandlerType: (*StudentServiceServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "GetStudent", + Handler: _StudentService_GetStudent_Handler, + }, + }, + Streams: []grpc.StreamDesc{ + { + StreamName: "GetStudentStream", + Handler: _StudentService_GetStudentStream_Handler, + ServerStreams: true, + }, + { + StreamName: "SendStudentStream", + Handler: _StudentService_SendStudentStream_Handler, + ClientStreams: true, + }, + { + StreamName: "SendAndGetStudentStream", + Handler: _StudentService_SendAndGetStudentStream_Handler, + ServerStreams: true, + ClientStreams: true, + }, + }, + Metadata: "student.proto", +} diff --git a/test/integration/integration/utils/grpc-code/student_default_version/student2.go b/test/integration/integration/utils/grpc-code/student_default_version/student2.go new file mode 100644 index 000000000..939bb3b90 --- /dev/null +++ b/test/integration/integration/utils/grpc-code/student_default_version/student2.go @@ -0,0 +1,46 @@ +package student_default_version + +import ( + "context" + "github.com/wso2/apk/test/integration/integration/utils/grpcutils" + "google.golang.org/grpc" + "log" + "time" +) + +type StudentResponseSatisfier struct{} + +// IsSatisfactory checks if the given response is satisfactory based on the expected response. +func (srs StudentResponseSatisfier) IsSatisfactory(response any, expectedResponse grpcutils.ExpectedResponse) bool { + // Type assert the response to *student.StudentResponse + resp, respOk := response.(*StudentResponse) + if !respOk { + log.Println("Failed to assert response as *student.StudentResponse") + return false + } + // Type assert the expected output to *student.StudentResponse + expectedResp, expOk := expectedResponse.Out.(*StudentResponse) + if !expOk { + log.Println("Failed to assert expectedResponse.Out as *student.StudentResponse") + return false + } + + // Compare the actual response with the expected response + if resp.Name == expectedResp.Name && resp.Age == expectedResp.Age { + return true + } else { + log.Println("Response does not match the expected output") + return false + } +} +func GetStudent(conn *grpc.ClientConn) (any, error) { + c := NewStudentServiceClient(conn) + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + r := &StudentRequest{Id: 1234} + response, err := c.GetStudent(ctx, r) + if err != nil { + return nil, err + } + return response, nil +} diff --git a/test/integration/integration/utils/grpc-code/student_default_version/student_default_version.pb.go b/test/integration/integration/utils/grpc-code/student_default_version/student_default_version.pb.go new file mode 100644 index 000000000..a006069e8 --- /dev/null +++ b/test/integration/integration/utils/grpc-code/student_default_version/student_default_version.pb.go @@ -0,0 +1,251 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.33.0 +// protoc v3.12.4 +// source: student_default_version.proto + +package student_default_version + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type StudentRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Id int32 `protobuf:"varint,3,opt,name=id,proto3" json:"id,omitempty"` +} + +func (x *StudentRequest) Reset() { + *x = StudentRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_student_default_version_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *StudentRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*StudentRequest) ProtoMessage() {} + +func (x *StudentRequest) ProtoReflect() protoreflect.Message { + mi := &file_student_default_version_proto_msgTypes[0] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use StudentRequest.ProtoReflect.Descriptor instead. +func (*StudentRequest) Descriptor() ([]byte, []int) { + return file_student_default_version_proto_rawDescGZIP(), []int{0} +} + +func (x *StudentRequest) GetId() int32 { + if x != nil { + return x.Id + } + return 0 +} + +type StudentResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + Age int32 `protobuf:"varint,2,opt,name=age,proto3" json:"age,omitempty"` +} + +func (x *StudentResponse) Reset() { + *x = StudentResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_student_default_version_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *StudentResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*StudentResponse) ProtoMessage() {} + +func (x *StudentResponse) ProtoReflect() protoreflect.Message { + mi := &file_student_default_version_proto_msgTypes[1] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use StudentResponse.ProtoReflect.Descriptor instead. +func (*StudentResponse) Descriptor() ([]byte, []int) { + return file_student_default_version_proto_rawDescGZIP(), []int{1} +} + +func (x *StudentResponse) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *StudentResponse) GetAge() int32 { + if x != nil { + return x.Age + } + return 0 +} + +var File_student_default_version_proto protoreflect.FileDescriptor + +var file_student_default_version_proto_rawDesc = []byte{ + 0x0a, 0x1d, 0x73, 0x74, 0x75, 0x64, 0x65, 0x6e, 0x74, 0x5f, 0x64, 0x65, 0x66, 0x61, 0x75, 0x6c, + 0x74, 0x5f, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, + 0x17, 0x64, 0x69, 0x6e, 0x65, 0x74, 0x68, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e, 0x61, 0x70, 0x69, + 0x2e, 0x73, 0x74, 0x75, 0x64, 0x65, 0x6e, 0x74, 0x22, 0x20, 0x0a, 0x0e, 0x53, 0x74, 0x75, 0x64, + 0x65, 0x6e, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, + 0x18, 0x03, 0x20, 0x01, 0x28, 0x05, 0x52, 0x02, 0x69, 0x64, 0x22, 0x37, 0x0a, 0x0f, 0x53, 0x74, + 0x75, 0x64, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x12, 0x0a, + 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, + 0x65, 0x12, 0x10, 0x0a, 0x03, 0x61, 0x67, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x05, 0x52, 0x03, + 0x61, 0x67, 0x65, 0x32, 0xbe, 0x03, 0x0a, 0x0e, 0x53, 0x74, 0x75, 0x64, 0x65, 0x6e, 0x74, 0x53, + 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x61, 0x0a, 0x0a, 0x47, 0x65, 0x74, 0x53, 0x74, 0x75, + 0x64, 0x65, 0x6e, 0x74, 0x12, 0x27, 0x2e, 0x64, 0x69, 0x6e, 0x65, 0x74, 0x68, 0x2e, 0x67, 0x72, + 0x70, 0x63, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x73, 0x74, 0x75, 0x64, 0x65, 0x6e, 0x74, 0x2e, 0x53, + 0x74, 0x75, 0x64, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x28, 0x2e, + 0x64, 0x69, 0x6e, 0x65, 0x74, 0x68, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e, 0x61, 0x70, 0x69, 0x2e, + 0x73, 0x74, 0x75, 0x64, 0x65, 0x6e, 0x74, 0x2e, 0x53, 0x74, 0x75, 0x64, 0x65, 0x6e, 0x74, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x69, 0x0a, 0x10, 0x47, 0x65, 0x74, + 0x53, 0x74, 0x75, 0x64, 0x65, 0x6e, 0x74, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x12, 0x27, 0x2e, + 0x64, 0x69, 0x6e, 0x65, 0x74, 0x68, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e, 0x61, 0x70, 0x69, 0x2e, + 0x73, 0x74, 0x75, 0x64, 0x65, 0x6e, 0x74, 0x2e, 0x53, 0x74, 0x75, 0x64, 0x65, 0x6e, 0x74, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x28, 0x2e, 0x64, 0x69, 0x6e, 0x65, 0x74, 0x68, 0x2e, + 0x67, 0x72, 0x70, 0x63, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x73, 0x74, 0x75, 0x64, 0x65, 0x6e, 0x74, + 0x2e, 0x53, 0x74, 0x75, 0x64, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x22, 0x00, 0x30, 0x01, 0x12, 0x6a, 0x0a, 0x11, 0x53, 0x65, 0x6e, 0x64, 0x53, 0x74, 0x75, 0x64, + 0x65, 0x6e, 0x74, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x12, 0x27, 0x2e, 0x64, 0x69, 0x6e, 0x65, + 0x74, 0x68, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x73, 0x74, 0x75, 0x64, + 0x65, 0x6e, 0x74, 0x2e, 0x53, 0x74, 0x75, 0x64, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x1a, 0x28, 0x2e, 0x64, 0x69, 0x6e, 0x65, 0x74, 0x68, 0x2e, 0x67, 0x72, 0x70, 0x63, + 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x73, 0x74, 0x75, 0x64, 0x65, 0x6e, 0x74, 0x2e, 0x53, 0x74, 0x75, + 0x64, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x28, 0x01, + 0x12, 0x72, 0x0a, 0x17, 0x53, 0x65, 0x6e, 0x64, 0x41, 0x6e, 0x64, 0x47, 0x65, 0x74, 0x53, 0x74, + 0x75, 0x64, 0x65, 0x6e, 0x74, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x12, 0x27, 0x2e, 0x64, 0x69, + 0x6e, 0x65, 0x74, 0x68, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x73, 0x74, + 0x75, 0x64, 0x65, 0x6e, 0x74, 0x2e, 0x53, 0x74, 0x75, 0x64, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x1a, 0x28, 0x2e, 0x64, 0x69, 0x6e, 0x65, 0x74, 0x68, 0x2e, 0x67, 0x72, + 0x70, 0x63, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x73, 0x74, 0x75, 0x64, 0x65, 0x6e, 0x74, 0x2e, 0x53, + 0x74, 0x75, 0x64, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, + 0x28, 0x01, 0x30, 0x01, 0x42, 0x0f, 0x0a, 0x0b, 0x6f, 0x72, 0x67, 0x2e, 0x65, 0x78, 0x61, 0x6d, + 0x70, 0x6c, 0x65, 0x50, 0x01, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_student_default_version_proto_rawDescOnce sync.Once + file_student_default_version_proto_rawDescData = file_student_default_version_proto_rawDesc +) + +func file_student_default_version_proto_rawDescGZIP() []byte { + file_student_default_version_proto_rawDescOnce.Do(func() { + file_student_default_version_proto_rawDescData = protoimpl.X.CompressGZIP(file_student_default_version_proto_rawDescData) + }) + return file_student_default_version_proto_rawDescData +} + +var file_student_default_version_proto_msgTypes = make([]protoimpl.MessageInfo, 2) +var file_student_default_version_proto_goTypes = []interface{}{ + (*StudentRequest)(nil), // 0: dineth.grpc.api.student.StudentRequest + (*StudentResponse)(nil), // 1: dineth.grpc.api.student.StudentResponse +} +var file_student_default_version_proto_depIdxs = []int32{ + 0, // 0: dineth.grpc.api.student.StudentService.GetStudent:input_type -> dineth.grpc.api.student.StudentRequest + 0, // 1: dineth.grpc.api.student.StudentService.GetStudentStream:input_type -> dineth.grpc.api.student.StudentRequest + 0, // 2: dineth.grpc.api.student.StudentService.SendStudentStream:input_type -> dineth.grpc.api.student.StudentRequest + 0, // 3: dineth.grpc.api.student.StudentService.SendAndGetStudentStream:input_type -> dineth.grpc.api.student.StudentRequest + 1, // 4: dineth.grpc.api.student.StudentService.GetStudent:output_type -> dineth.grpc.api.student.StudentResponse + 1, // 5: dineth.grpc.api.student.StudentService.GetStudentStream:output_type -> dineth.grpc.api.student.StudentResponse + 1, // 6: dineth.grpc.api.student.StudentService.SendStudentStream:output_type -> dineth.grpc.api.student.StudentResponse + 1, // 7: dineth.grpc.api.student.StudentService.SendAndGetStudentStream:output_type -> dineth.grpc.api.student.StudentResponse + 4, // [4:8] is the sub-list for method output_type + 0, // [0:4] is the sub-list for method input_type + 0, // [0:0] is the sub-list for extension type_name + 0, // [0:0] is the sub-list for extension extendee + 0, // [0:0] is the sub-list for field type_name +} + +func init() { file_student_default_version_proto_init() } +func file_student_default_version_proto_init() { + if File_student_default_version_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_student_default_version_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*StudentRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_student_default_version_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*StudentResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_student_default_version_proto_rawDesc, + NumEnums: 0, + NumMessages: 2, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_student_default_version_proto_goTypes, + DependencyIndexes: file_student_default_version_proto_depIdxs, + MessageInfos: file_student_default_version_proto_msgTypes, + }.Build() + File_student_default_version_proto = out.File + file_student_default_version_proto_rawDesc = nil + file_student_default_version_proto_goTypes = nil + file_student_default_version_proto_depIdxs = nil +} diff --git a/test/integration/integration/utils/grpc-code/student_default_version/student_default_version_grpc.pb.go b/test/integration/integration/utils/grpc-code/student_default_version/student_default_version_grpc.pb.go new file mode 100644 index 000000000..b3fe9b74b --- /dev/null +++ b/test/integration/integration/utils/grpc-code/student_default_version/student_default_version_grpc.pb.go @@ -0,0 +1,314 @@ +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. +// versions: +// - protoc-gen-go-grpc v1.3.0 +// - protoc v3.12.4 +// source: student_default_version.proto + +package student_default_version + +import ( + context "context" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" +) + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +// Requires gRPC-Go v1.32.0 or later. +const _ = grpc.SupportPackageIsVersion7 + +const ( + StudentService_GetStudent_FullMethodName = "/dineth.grpc.api.student.StudentService/GetStudent" + StudentService_GetStudentStream_FullMethodName = "/dineth.grpc.api.student.StudentService/GetStudentStream" + StudentService_SendStudentStream_FullMethodName = "/dineth.grpc.api.student.StudentService/SendStudentStream" + StudentService_SendAndGetStudentStream_FullMethodName = "/dineth.grpc.api.student.StudentService/SendAndGetStudentStream" +) + +// StudentServiceClient is the client API for StudentService service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +type StudentServiceClient interface { + GetStudent(ctx context.Context, in *StudentRequest, opts ...grpc.CallOption) (*StudentResponse, error) + GetStudentStream(ctx context.Context, in *StudentRequest, opts ...grpc.CallOption) (StudentService_GetStudentStreamClient, error) + SendStudentStream(ctx context.Context, opts ...grpc.CallOption) (StudentService_SendStudentStreamClient, error) + SendAndGetStudentStream(ctx context.Context, opts ...grpc.CallOption) (StudentService_SendAndGetStudentStreamClient, error) +} + +type studentServiceClient struct { + cc grpc.ClientConnInterface +} + +func NewStudentServiceClient(cc grpc.ClientConnInterface) StudentServiceClient { + return &studentServiceClient{cc} +} + +func (c *studentServiceClient) GetStudent(ctx context.Context, in *StudentRequest, opts ...grpc.CallOption) (*StudentResponse, error) { + out := new(StudentResponse) + err := c.cc.Invoke(ctx, StudentService_GetStudent_FullMethodName, in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *studentServiceClient) GetStudentStream(ctx context.Context, in *StudentRequest, opts ...grpc.CallOption) (StudentService_GetStudentStreamClient, error) { + stream, err := c.cc.NewStream(ctx, &StudentService_ServiceDesc.Streams[0], StudentService_GetStudentStream_FullMethodName, opts...) + if err != nil { + return nil, err + } + x := &studentServiceGetStudentStreamClient{stream} + if err := x.ClientStream.SendMsg(in); err != nil { + return nil, err + } + if err := x.ClientStream.CloseSend(); err != nil { + return nil, err + } + return x, nil +} + +type StudentService_GetStudentStreamClient interface { + Recv() (*StudentResponse, error) + grpc.ClientStream +} + +type studentServiceGetStudentStreamClient struct { + grpc.ClientStream +} + +func (x *studentServiceGetStudentStreamClient) Recv() (*StudentResponse, error) { + m := new(StudentResponse) + if err := x.ClientStream.RecvMsg(m); err != nil { + return nil, err + } + return m, nil +} + +func (c *studentServiceClient) SendStudentStream(ctx context.Context, opts ...grpc.CallOption) (StudentService_SendStudentStreamClient, error) { + stream, err := c.cc.NewStream(ctx, &StudentService_ServiceDesc.Streams[1], StudentService_SendStudentStream_FullMethodName, opts...) + if err != nil { + return nil, err + } + x := &studentServiceSendStudentStreamClient{stream} + return x, nil +} + +type StudentService_SendStudentStreamClient interface { + Send(*StudentRequest) error + CloseAndRecv() (*StudentResponse, error) + grpc.ClientStream +} + +type studentServiceSendStudentStreamClient struct { + grpc.ClientStream +} + +func (x *studentServiceSendStudentStreamClient) Send(m *StudentRequest) error { + return x.ClientStream.SendMsg(m) +} + +func (x *studentServiceSendStudentStreamClient) CloseAndRecv() (*StudentResponse, error) { + if err := x.ClientStream.CloseSend(); err != nil { + return nil, err + } + m := new(StudentResponse) + if err := x.ClientStream.RecvMsg(m); err != nil { + return nil, err + } + return m, nil +} + +func (c *studentServiceClient) SendAndGetStudentStream(ctx context.Context, opts ...grpc.CallOption) (StudentService_SendAndGetStudentStreamClient, error) { + stream, err := c.cc.NewStream(ctx, &StudentService_ServiceDesc.Streams[2], StudentService_SendAndGetStudentStream_FullMethodName, opts...) + if err != nil { + return nil, err + } + x := &studentServiceSendAndGetStudentStreamClient{stream} + return x, nil +} + +type StudentService_SendAndGetStudentStreamClient interface { + Send(*StudentRequest) error + Recv() (*StudentResponse, error) + grpc.ClientStream +} + +type studentServiceSendAndGetStudentStreamClient struct { + grpc.ClientStream +} + +func (x *studentServiceSendAndGetStudentStreamClient) Send(m *StudentRequest) error { + return x.ClientStream.SendMsg(m) +} + +func (x *studentServiceSendAndGetStudentStreamClient) Recv() (*StudentResponse, error) { + m := new(StudentResponse) + if err := x.ClientStream.RecvMsg(m); err != nil { + return nil, err + } + return m, nil +} + +// StudentServiceServer is the server API for StudentService service. +// All implementations must embed UnimplementedStudentServiceServer +// for forward compatibility +type StudentServiceServer interface { + GetStudent(context.Context, *StudentRequest) (*StudentResponse, error) + GetStudentStream(*StudentRequest, StudentService_GetStudentStreamServer) error + SendStudentStream(StudentService_SendStudentStreamServer) error + SendAndGetStudentStream(StudentService_SendAndGetStudentStreamServer) error + mustEmbedUnimplementedStudentServiceServer() +} + +// UnimplementedStudentServiceServer must be embedded to have forward compatible implementations. +type UnimplementedStudentServiceServer struct { +} + +func (UnimplementedStudentServiceServer) GetStudent(context.Context, *StudentRequest) (*StudentResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method GetStudent not implemented") +} +func (UnimplementedStudentServiceServer) GetStudentStream(*StudentRequest, StudentService_GetStudentStreamServer) error { + return status.Errorf(codes.Unimplemented, "method GetStudentStream not implemented") +} +func (UnimplementedStudentServiceServer) SendStudentStream(StudentService_SendStudentStreamServer) error { + return status.Errorf(codes.Unimplemented, "method SendStudentStream not implemented") +} +func (UnimplementedStudentServiceServer) SendAndGetStudentStream(StudentService_SendAndGetStudentStreamServer) error { + return status.Errorf(codes.Unimplemented, "method SendAndGetStudentStream not implemented") +} +func (UnimplementedStudentServiceServer) mustEmbedUnimplementedStudentServiceServer() {} + +// UnsafeStudentServiceServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to StudentServiceServer will +// result in compilation errors. +type UnsafeStudentServiceServer interface { + mustEmbedUnimplementedStudentServiceServer() +} + +func RegisterStudentServiceServer(s grpc.ServiceRegistrar, srv StudentServiceServer) { + s.RegisterService(&StudentService_ServiceDesc, srv) +} + +func _StudentService_GetStudent_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(StudentRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(StudentServiceServer).GetStudent(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: StudentService_GetStudent_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(StudentServiceServer).GetStudent(ctx, req.(*StudentRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _StudentService_GetStudentStream_Handler(srv interface{}, stream grpc.ServerStream) error { + m := new(StudentRequest) + if err := stream.RecvMsg(m); err != nil { + return err + } + return srv.(StudentServiceServer).GetStudentStream(m, &studentServiceGetStudentStreamServer{stream}) +} + +type StudentService_GetStudentStreamServer interface { + Send(*StudentResponse) error + grpc.ServerStream +} + +type studentServiceGetStudentStreamServer struct { + grpc.ServerStream +} + +func (x *studentServiceGetStudentStreamServer) Send(m *StudentResponse) error { + return x.ServerStream.SendMsg(m) +} + +func _StudentService_SendStudentStream_Handler(srv interface{}, stream grpc.ServerStream) error { + return srv.(StudentServiceServer).SendStudentStream(&studentServiceSendStudentStreamServer{stream}) +} + +type StudentService_SendStudentStreamServer interface { + SendAndClose(*StudentResponse) error + Recv() (*StudentRequest, error) + grpc.ServerStream +} + +type studentServiceSendStudentStreamServer struct { + grpc.ServerStream +} + +func (x *studentServiceSendStudentStreamServer) SendAndClose(m *StudentResponse) error { + return x.ServerStream.SendMsg(m) +} + +func (x *studentServiceSendStudentStreamServer) Recv() (*StudentRequest, error) { + m := new(StudentRequest) + if err := x.ServerStream.RecvMsg(m); err != nil { + return nil, err + } + return m, nil +} + +func _StudentService_SendAndGetStudentStream_Handler(srv interface{}, stream grpc.ServerStream) error { + return srv.(StudentServiceServer).SendAndGetStudentStream(&studentServiceSendAndGetStudentStreamServer{stream}) +} + +type StudentService_SendAndGetStudentStreamServer interface { + Send(*StudentResponse) error + Recv() (*StudentRequest, error) + grpc.ServerStream +} + +type studentServiceSendAndGetStudentStreamServer struct { + grpc.ServerStream +} + +func (x *studentServiceSendAndGetStudentStreamServer) Send(m *StudentResponse) error { + return x.ServerStream.SendMsg(m) +} + +func (x *studentServiceSendAndGetStudentStreamServer) Recv() (*StudentRequest, error) { + m := new(StudentRequest) + if err := x.ServerStream.RecvMsg(m); err != nil { + return nil, err + } + return m, nil +} + +// StudentService_ServiceDesc is the grpc.ServiceDesc for StudentService service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var StudentService_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "dineth.grpc.api.student.StudentService", + HandlerType: (*StudentServiceServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "GetStudent", + Handler: _StudentService_GetStudent_Handler, + }, + }, + Streams: []grpc.StreamDesc{ + { + StreamName: "GetStudentStream", + Handler: _StudentService_GetStudentStream_Handler, + ServerStreams: true, + }, + { + StreamName: "SendStudentStream", + Handler: _StudentService_SendStudentStream_Handler, + ClientStreams: true, + }, + { + StreamName: "SendAndGetStudentStream", + Handler: _StudentService_SendAndGetStudentStream_Handler, + ServerStreams: true, + ClientStreams: true, + }, + }, + Metadata: "student_default_version.proto", +} diff --git a/test/integration/integration/utils/grpcutils/helpers.go b/test/integration/integration/utils/grpcutils/helpers.go new file mode 100644 index 000000000..66d339215 --- /dev/null +++ b/test/integration/integration/utils/grpcutils/helpers.go @@ -0,0 +1,90 @@ +package grpcutils + +import ( + "crypto/tls" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials" + "testing" + "time" +) + +type Request struct { + Host string + Headers map[string]string +} +type ClientCreator[T any] func(conn *grpc.ClientConn) T +type ExpectedResponse struct { + Out any + Err error +} + +type GRPCTestCase struct { + Request Request + ExpectedResponse ExpectedResponse + ActualResponse any + Name string + Method func(conn *grpc.ClientConn) (any, error) + Satisfier ResponseSatisfier +} +type ResponseSatisfier interface { + IsSatisfactory(response interface{}, expectedResponse ExpectedResponse) bool +} + +func DialGRPCServer(gwAddr string, t *testing.T) (*grpc.ClientConn, error) { + // Set up a connection to the server. + t.Logf("Dialing gRPC server at %s...", gwAddr) + creds := credentials.NewTLS(&tls.Config{InsecureSkipVerify: true}) + conn, err := grpc.Dial(gwAddr, grpc.WithTransportCredentials(creds)) + if err != nil { + t.Fatalf("Could not connect to the server: %v", err) + } + return conn, nil +} +func InvokeGRPCClientUntilSatisfied(gwAddr string, t *testing.T, testCase GRPCTestCase, satisfier ResponseSatisfier, fn ExecuteClientCall) { + //(delay to allow CRs to be applied) + time.Sleep(5 * time.Second) + + var out any + var err error + attempt := 0 + maxAttempts := 4 + expected := testCase.ExpectedResponse + timeoutDuration := 50 * time.Second + for attempt < maxAttempts { + t.Logf("Attempt %d to invoke gRPC client...", attempt+1) + out, err = InvokeGRPCClient(gwAddr, t, fn) + + if err != nil { + t.Logf("Error on attempt %d: %v", attempt+1, err) + } else { + if satisfier.IsSatisfactory(out, expected) { + return + } + } + + if attempt < maxAttempts-1 { + t.Logf("Waiting %s seconds before next attempt...", timeoutDuration) + time.Sleep(timeoutDuration) + } + attempt++ + } + + t.Logf("Failed to receive a satisfactory response after %d attempts", maxAttempts) + t.Fail() +} + +type ExecuteClientCall func(conn *grpc.ClientConn) (any, error) + +func InvokeGRPCClient(gwAddr string, t *testing.T, fn ExecuteClientCall) (any, error) { + + conn, err := DialGRPCServer(gwAddr, t) + if err != nil { + t.Fatalf("Could not connect to the server: %v", err) + } + + response, err := fn(conn) + if err != nil { + return nil, err + } + return response, nil +} diff --git a/test/integration/scripts/run-tests.sh b/test/integration/scripts/run-tests.sh index 7a50bd056..ecdd329ac 100644 --- a/test/integration/scripts/run-tests.sh +++ b/test/integration/scripts/run-tests.sh @@ -79,6 +79,7 @@ sudo echo "$IP ratelimit-priority.test.gw.wso2.com" | sudo tee -a /etc/hosts sudo echo "$IP different-endpoint-with-same-route.test.gw.wso2.com" | sudo tee -a /etc/hosts sudo echo "$IP custom-auth-header.test.gw.wso2.com" | sudo tee -a /etc/hosts sudo echo "$IP gql.test.gw.wso2.com" | sudo tee -a /etc/hosts +sudo echo "$IP grpc.test.gw.wso2.com" | sudo tee -a /etc/hosts sudo echo "$IP api-level-jwt.test.gw.wso2.com" | sudo tee -a /etc/hosts sudo echo "$IP resource-level-jwt.test.gw.wso2.com" | sudo tee -a /etc/hosts sudo echo "255.255.255.255 broadcasthost" | sudo tee -a /etc/hosts diff --git a/test/integration/scripts/setup-hosts.sh b/test/integration/scripts/setup-hosts.sh index 7ff4ff8af..ec36022eb 100644 --- a/test/integration/scripts/setup-hosts.sh +++ b/test/integration/scripts/setup-hosts.sh @@ -38,6 +38,7 @@ sudo echo "$IP ratelimit-priority.test.gw.wso2.com" | sudo tee -a /etc/hosts sudo echo "$IP different-endpoint-with-same-route.test.gw.wso2.com" | sudo tee -a /etc/hosts sudo echo "$IP custom-auth-header.test.gw.wso2.com" | sudo tee -a /etc/hosts sudo echo "$IP gql.test.gw.wso2.com" | sudo tee -a /etc/hosts +sudo echo "$IP grpc.test.gw.wso2.com" | sudo tee -a /etc/hosts sudo echo "$IP api-level-jwt.test.gw.wso2.com" | sudo tee -a /etc/hosts sudo echo "$IP resource-level-jwt.test.gw.wso2.com" | sudo tee -a /etc/hosts sudo echo "255.255.255.255 broadcasthost" | sudo tee -a /etc/hosts