From c924cec7dc11d4cb171ead39549a9da7d6ae6ff0 Mon Sep 17 00:00:00 2001 From: sgayangi Date: Mon, 17 Jun 2024 15:12:42 +0530 Subject: [PATCH 1/2] Add support for request redirect filter --- .../internal/oasparser/constants/constants.go | 7 ++++ .../envoyconf/envoyconf_internal_test.go | 8 ++--- .../oasparser/envoyconf/listener_test.go | 8 ++--- .../envoyconf/routes_with_clusters.go | 7 ++-- adapter/internal/oasparser/model/resource.go | 36 ++++++++++--------- .../internal/oasparser/model/resource_test.go | 2 +- 6 files changed, 40 insertions(+), 28 deletions(-) diff --git a/adapter/internal/oasparser/constants/constants.go b/adapter/internal/oasparser/constants/constants.go index df863dc5a..919b3966c 100644 --- a/adapter/internal/oasparser/constants/constants.go +++ b/adapter/internal/oasparser/constants/constants.go @@ -76,6 +76,8 @@ const ( ActionRewriteMethod string = "REWRITE_RESOURCE_METHOD" ActionInterceptorService string = "CALL_INTERCEPTOR_SERVICE" ActionRewritePath string = "REWRITE_RESOURCE_PATH" + ActionRedirectRequest string = "REDIRECT_REQUEST" + ActionMirrorRequest string = "MIRROR_REQUEST" PolicyRequestInterceptor string = "PolicyRequestInterceptor" PolicyResponseInterceptor string = "PolicyResponseInterceptor" @@ -90,6 +92,11 @@ const ( HeaderValue string = "headerValue" CurrentMethod string = "currentMethod" UpdatedMethod string = "updatedMethod" + RedirectScheme string = "scheme" + RedirectHostname string = "hostname" + RedirectPath string = "path" + RedirectPort string = "port" + RedirectStatusCode string = "statusCode" ) // API Type Constants diff --git a/adapter/internal/oasparser/envoyconf/envoyconf_internal_test.go b/adapter/internal/oasparser/envoyconf/envoyconf_internal_test.go index dd99527ee..1be24e1da 100644 --- a/adapter/internal/oasparser/envoyconf/envoyconf_internal_test.go +++ b/adapter/internal/oasparser/envoyconf/envoyconf_internal_test.go @@ -73,7 +73,7 @@ func TestCreateRoute(t *testing.T) { resourceWithGet := model.CreateMinimalDummyResourceForTests("/xWso2BasePath/resourcePath", []*model.Operation{model.NewOperationWithPolicies("GET", policies)}, - "resource_operation_id", []model.Endpoint{endpoint}, true) + "resource_operation_id", []model.Endpoint{endpoint}, true, false) clusterName := "resource_operation_id" hostRewriteSpecifier := &routev3.RouteAction_AutoHostRewrite{ AutoHostRewrite: &wrapperspb.BoolValue{ @@ -143,7 +143,7 @@ func TestCreateRouteClusterSpecifier(t *testing.T) { RawURL: "http://abc.com", } resourceWithGet := model.CreateMinimalDummyResourceForTests("/resourcePath", []*model.Operation{model.NewOperation("GET", nil, nil)}, - "resource_operation_id", []model.Endpoint{endpoint}, false) + "resource_operation_id", []model.Endpoint{endpoint}, false, false) route, err := createRoutes(generateRouteCreateParamsForUnitTests(title, apiType, vHost, xWso2BasePath, version, endpointBasePath, &resourceWithGet, clusterName, nil, false)) @@ -174,7 +174,7 @@ func TestCreateRouteExtAuthzContext(t *testing.T) { RawURL: "http://abc.com", } resourceWithGet := model.CreateMinimalDummyResourceForTests("/resourcePath", []*model.Operation{model.NewOperation("GET", nil, nil)}, - "resource_operation_id", []model.Endpoint{endpoint}, false) + "resource_operation_id", []model.Endpoint{endpoint}, false, false) route, err := createRoutes(generateRouteCreateParamsForUnitTests(title, apiType, vHost, xWso2BasePath, version, endpointBasePath, &resourceWithGet, clusterName, nil, false)) @@ -571,7 +571,7 @@ func TestGetCorsPolicy(t *testing.T) { assert.Empty(t, corsPolicy3.GetAllowCredentials(), "Allow Credential property should not be assigned.") resourceWithGet := model.CreateMinimalDummyResourceForTests("/resourcePath", []*model.Operation{model.NewOperation("GET", nil, nil)}, - "resource_operation_id", []model.Endpoint{endpoint}, false) + "resource_operation_id", []model.Endpoint{endpoint}, false, false) // Route without CORS configuration routeWithoutCors, err := createRoutes(generateRouteCreateParamsForUnitTests("test", "HTTP", "localhost", "/test", "1.0.0", "/test", diff --git a/adapter/internal/oasparser/envoyconf/listener_test.go b/adapter/internal/oasparser/envoyconf/listener_test.go index f24064ef0..38308b854 100644 --- a/adapter/internal/oasparser/envoyconf/listener_test.go +++ b/adapter/internal/oasparser/envoyconf/listener_test.go @@ -200,13 +200,13 @@ func testCreateRoutesForUnitTests(t *testing.T) []*routev3.Route { operationPost := model.NewOperation("POST", nil, nil) operationPut := model.NewOperation("PUT", nil, nil) resourceWithGet := model.CreateMinimalDummyResourceForTests("/resourcePath", []*model.Operation{operationGet}, - "resource_operation_id", []model.Endpoint{endpoint}, false) + "resource_operation_id", []model.Endpoint{endpoint}, false, false) resourceWithPost := model.CreateMinimalDummyResourceForTests("/resourcePath", []*model.Operation{operationPost}, - "resource_operation_id", []model.Endpoint{endpoint}, false) + "resource_operation_id", []model.Endpoint{endpoint}, false, false) resourceWithPut := model.CreateMinimalDummyResourceForTests("/resourcePath", []*model.Operation{operationPut}, - "resource_operation_id", []model.Endpoint{endpoint}, false) + "resource_operation_id", []model.Endpoint{endpoint}, false, false) resourceWithMultipleOperations := model.CreateMinimalDummyResourceForTests("/resourcePath", []*model.Operation{operationGet, operationPut}, - "resource_operation_id", []model.Endpoint{endpoint}, false) + "resource_operation_id", []model.Endpoint{endpoint}, false, false) route1, err := createRoutes(generateRouteCreateParamsForUnitTests("test", "HTTP", "localhost", "/test", "1.0.0", "/test", &resourceWithGet, "test-cluster", corsConfigModel3, false)) diff --git a/adapter/internal/oasparser/envoyconf/routes_with_clusters.go b/adapter/internal/oasparser/envoyconf/routes_with_clusters.go index acb0edb51..e0a7200b1 100644 --- a/adapter/internal/oasparser/envoyconf/routes_with_clusters.go +++ b/adapter/internal/oasparser/envoyconf/routes_with_clusters.go @@ -176,7 +176,7 @@ func CreateRoutesWithClusters(adapterInternalAPI *model.AdapterInternalAPI, inte }, } gqlop := model.NewOperationWithPolicies("POST", policies) - resource := model.CreateMinimalResource(adapterInternalAPI.GetXWso2Basepath(), []*model.Operation{gqlop}, "", adapterInternalAPI.Endpoints, true, gwapiv1.PathMatchExact) + resource := model.CreateMinimalResource(adapterInternalAPI.GetXWso2Basepath(), []*model.Operation{gqlop}, "", adapterInternalAPI.Endpoints, true, false, gwapiv1.PathMatchExact) routesP, err := createRoutes(genRouteCreateParams(adapterInternalAPI, &resource, vHost, basePath, clusterName, nil, nil, organizationID, false, false)) if err != nil { @@ -201,7 +201,10 @@ func CreateRoutesWithClusters(adapterInternalAPI *model.AdapterInternalAPI, inte var clusterName string resourcePath := resource.GetPath() endpoint := resource.GetEndpoints() - basePath := strings.TrimSuffix(endpoint.Endpoints[0].Basepath, "/") + basePath := "" + if len(endpoint.Endpoints) > 0 { + basePath = strings.TrimSuffix(endpoint.Endpoints[0].Basepath, "/") + } existingClusterName := getExistingClusterName(*endpoint, processedEndpoints) if existingClusterName == "" { diff --git a/adapter/internal/oasparser/model/resource.go b/adapter/internal/oasparser/model/resource.go index b66dd87a5..9fafd7f87 100644 --- a/adapter/internal/oasparser/model/resource.go +++ b/adapter/internal/oasparser/model/resource.go @@ -36,14 +36,15 @@ import ( // These values are populated from extensions/properties // mentioned under pathItem. type Resource struct { - path string - pathMatchType gwapiv1.PathMatchType - methods []*Operation - iD string - endpoints *EndpointCluster - endpointSecurity []*EndpointSecurity - vendorExtensions map[string]interface{} - hasPolicies bool + path string + pathMatchType gwapiv1.PathMatchType + methods []*Operation + iD string + endpoints *EndpointCluster + endpointSecurity []*EndpointSecurity + vendorExtensions map[string]interface{} + hasPolicies bool + hasRequestRedirectFilter bool } // GetEndpointSecurity returns the endpoint security object of a given resource. @@ -107,21 +108,22 @@ func (resource *Resource) HasPolicies() bool { // CreateMinimalDummyResourceForTests create a resource object with minimal required set of values // which could be used for unit tests. -func CreateMinimalDummyResourceForTests(path string, methods []*Operation, id string, urls []Endpoint, hasPolicies bool) Resource { +func CreateMinimalDummyResourceForTests(path string, methods []*Operation, id string, urls []Endpoint, hasPolicies bool, hasRequestRedirectPolicy bool) Resource { endpoints := generateEndpointCluster(urls, constants.LoadBalance) - return CreateMinimalResource(path, methods, id, endpoints, hasPolicies, gwapiv1.PathMatchPathPrefix) + return CreateMinimalResource(path, methods, id, endpoints, hasPolicies, hasRequestRedirectPolicy, gwapiv1.PathMatchPathPrefix) } // CreateMinimalResource create a resource object with minimal required set of values -func CreateMinimalResource(path string, methods []*Operation, id string, endpoints *EndpointCluster, hasPolicies bool, pathMatchType gwapiv1.PathMatchType) Resource { +func CreateMinimalResource(path string, methods []*Operation, id string, endpoints *EndpointCluster, hasPolicies bool, hasRequestRedirectPolicy bool, pathMatchType gwapiv1.PathMatchType) Resource { return Resource{ - path: path, - methods: methods, - iD: id, - endpoints: endpoints, - pathMatchType: pathMatchType, - hasPolicies: hasPolicies, + path: path, + methods: methods, + iD: id, + endpoints: endpoints, + pathMatchType: pathMatchType, + hasPolicies: hasPolicies, + hasRequestRedirectFilter: hasRequestRedirectPolicy, } } diff --git a/adapter/internal/oasparser/model/resource_test.go b/adapter/internal/oasparser/model/resource_test.go index c8c409efc..4b2526350 100644 --- a/adapter/internal/oasparser/model/resource_test.go +++ b/adapter/internal/oasparser/model/resource_test.go @@ -57,7 +57,7 @@ func getResources() []*Resource { resources := make([]*Resource, len(paths)) for index := range paths { res := CreateMinimalDummyResourceForTests(paths[index], make([]*Operation, 0), "", - make([]Endpoint, 0), false) + make([]Endpoint, 0), false, false) resources[index] = &res } return resources From 25f66835ed6e4d99d81a3b8641b9af635e2e5a32 Mon Sep 17 00:00:00 2001 From: sgayangi Date: Mon, 17 Jun 2024 15:18:08 +0530 Subject: [PATCH 2/2] Add support for request mirror filter --- .../oasparser/envoyconf/internal_dtos.go | 1 + .../oasparser/envoyconf/routes_configs.go | 68 ++++++++++++++++- .../envoyconf/routes_with_clusters.go | 74 +++++++++++++----- .../oasparser/model/adapter_internal_api.go | 76 +++++++++++++++++-- .../internal/oasparser/model/api_operation.go | 18 +++-- .../internal/oasparser/model/http_route.go | 17 +++-- .../operator/controllers/dp/api_controller.go | 20 +++++ 7 files changed, 233 insertions(+), 41 deletions(-) diff --git a/adapter/internal/oasparser/envoyconf/internal_dtos.go b/adapter/internal/oasparser/envoyconf/internal_dtos.go index 27fe51132..1ca8d5f21 100644 --- a/adapter/internal/oasparser/envoyconf/internal_dtos.go +++ b/adapter/internal/oasparser/envoyconf/internal_dtos.go @@ -44,6 +44,7 @@ type routeCreateParams struct { apiProperties []dpv1alpha2.Property environment string envType string + mirrorClusterNames map[string][]string } // RatelimitCriteria criterias of rate limiting diff --git a/adapter/internal/oasparser/envoyconf/routes_configs.go b/adapter/internal/oasparser/envoyconf/routes_configs.go index 06c4ada95..dbafbec47 100644 --- a/adapter/internal/oasparser/envoyconf/routes_configs.go +++ b/adapter/internal/oasparser/envoyconf/routes_configs.go @@ -41,7 +41,7 @@ import ( gwapiv1 "sigs.k8s.io/gateway-api/apis/v1" ) -func generateRouteConfig(routeName string, match *routev3.RouteMatch, action *routev3.Route_Route, +func generateRouteConfig(routeName string, match *routev3.RouteMatch, action *routev3.Route_Route, redirectAction *routev3.Route_Redirect, metadata *corev3.Metadata, decorator *routev3.Decorator, typedPerFilterConfig map[string]*anypb.Any, requestHeadersToAdd []*corev3.HeaderValueOption, requestHeadersToRemove []string, responseHeadersToAdd []*corev3.HeaderValueOption, responseHeadersToRemove []string) *routev3.Route { @@ -49,7 +49,6 @@ func generateRouteConfig(routeName string, match *routev3.RouteMatch, action *ro route := &routev3.Route{ Name: routeName, Match: match, - Action: action, Metadata: metadata, Decorator: decorator, TypedPerFilterConfig: typedPerFilterConfig, @@ -62,6 +61,12 @@ func generateRouteConfig(routeName string, match *routev3.RouteMatch, action *ro ResponseHeadersToRemove: responseHeadersToRemove, } + if redirectAction != nil { + route.Action = redirectAction + } else if action != nil { + route.Action = action + } + return route } @@ -76,7 +81,7 @@ func generateRouteMatch(routeRegex string) *routev3.RouteMatch { return match } -func generateRouteAction(apiType string, routeConfig *model.EndpointConfig, ratelimitCriteria *ratelimitCriteria) (action *routev3.Route_Route) { +func generateRouteAction(apiType string, routeConfig *model.EndpointConfig, ratelimitCriteria *ratelimitCriteria, mirrorClusterNames []string) (action *routev3.Route_Route) { action = &routev3.Route_Route{ Route: &routev3.RouteAction{ HostRewriteSpecifier: &routev3.RouteAction_AutoHostRewrite{ @@ -108,9 +113,66 @@ func generateRouteAction(apiType string, routeConfig *model.EndpointConfig, rate action.Route.RateLimits = generateRateLimitPolicy(ratelimitCriteria) } + // Add request mirroring configurations + if mirrorClusterNames != nil && len(mirrorClusterNames) > 0 { + mirrorPolicies := []*routev3.RouteAction_RequestMirrorPolicy{} + for _, clusterName := range mirrorClusterNames { + mirrorPolicy := &routev3.RouteAction_RequestMirrorPolicy{ + Cluster: clusterName, + } + mirrorPolicies = append(mirrorPolicies, mirrorPolicy) + } + action.Route.RequestMirrorPolicies = mirrorPolicies + } + return action } +func generateRequestRedirectRoute(route string, policyParams interface{}) (action *routev3.Route_Redirect) { + policyParameters, _ := policyParams.(map[string]interface{}) + scheme, _ := policyParameters[constants.RedirectScheme].(string) + hostname, _ := policyParameters[constants.RedirectHostname].(string) + port, _ := policyParameters[constants.RedirectPort].(int) + statusCode, _ := policyParameters[constants.RedirectStatusCode].(int) + replaceFullPath, _ := policyParameters[constants.RedirectPath].(string) + redirectActionStatusCode := mapStatusCodeToEnum(statusCode) + if redirectActionStatusCode == -1 { + _ = fmt.Errorf("Invalid status code provided") + } + + action = &routev3.Route_Redirect{ + Redirect: &routev3.RedirectAction{ + SchemeRewriteSpecifier: &routev3.RedirectAction_HttpsRedirect{ + HttpsRedirect: scheme == "https", + }, + HostRedirect: hostname, + PortRedirect: uint32(port), + PathRewriteSpecifier: &routev3.RedirectAction_PathRedirect{ + PathRedirect: replaceFullPath, + }, + ResponseCode: routev3.RedirectAction_RedirectResponseCode(redirectActionStatusCode), + }, + } + return action +} + +func mapStatusCodeToEnum(statusCode int) int { + switch statusCode { + case 301: + return 0 + case 302: + return 1 + case 303: + return 2 + case 307: + return 3 + case 308: + return 4 + default: + return -1 + } +} + func generateRateLimitPolicy(ratelimitCriteria *ratelimitCriteria) []*routev3.RateLimit { environmentValue := ratelimitCriteria.environment diff --git a/adapter/internal/oasparser/envoyconf/routes_with_clusters.go b/adapter/internal/oasparser/envoyconf/routes_with_clusters.go index e0a7200b1..869246b19 100644 --- a/adapter/internal/oasparser/envoyconf/routes_with_clusters.go +++ b/adapter/internal/oasparser/envoyconf/routes_with_clusters.go @@ -178,7 +178,7 @@ func CreateRoutesWithClusters(adapterInternalAPI *model.AdapterInternalAPI, inte gqlop := model.NewOperationWithPolicies("POST", policies) resource := model.CreateMinimalResource(adapterInternalAPI.GetXWso2Basepath(), []*model.Operation{gqlop}, "", adapterInternalAPI.Endpoints, true, false, gwapiv1.PathMatchExact) routesP, err := createRoutes(genRouteCreateParams(adapterInternalAPI, &resource, vHost, basePath, clusterName, nil, - nil, organizationID, false, false)) + nil, organizationID, false, false, nil)) if err != nil { logger.LoggerXds.ErrorC(logging.PrintError(logging.Error2231, logging.MAJOR, "Error while creating routes for GQL API %s %s Error: %s", adapterInternalAPI.GetTitle(), @@ -188,7 +188,7 @@ func CreateRoutesWithClusters(adapterInternalAPI *model.AdapterInternalAPI, inte routes = append(routes, routesP...) if adapterInternalAPI.IsDefaultVersion { defaultRoutes, errDefaultPath := createRoutes(genRouteCreateParams(adapterInternalAPI, &resource, vHost, basePath, clusterName, nil, nil, organizationID, - false, true)) + false, true, nil)) if errDefaultPath != nil { logger.LoggerXds.ErrorC(logging.PrintError(logging.Error2231, logging.MAJOR, "Error while creating routes for 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) @@ -199,6 +199,7 @@ func CreateRoutesWithClusters(adapterInternalAPI *model.AdapterInternalAPI, inte } for _, resource := range adapterInternalAPI.GetResources() { var clusterName string + mirrorClusterNames := map[string][]string{} resourcePath := resource.GetPath() endpoint := resource.GetEndpoints() basePath := "" @@ -220,13 +221,43 @@ func CreateRoutesWithClusters(adapterInternalAPI *model.AdapterInternalAPI, inte } else { clusterName = existingClusterName } + + // Creating clusters for request mirroring endpoints + for _, op := range resource.GetOperations() { + if op.GetMirrorEndpoints() != nil && len(op.GetMirrorEndpoints().Endpoints) > 0 { + mirrorEndpointCluster := op.GetMirrorEndpoints() + for _, mirrorEndpoint := range mirrorEndpointCluster.Endpoints { + mirrorBasepath := strings.TrimSuffix(mirrorEndpoint.Basepath, "/") + existingMirrorClusterName := getExistingClusterName(*mirrorEndpointCluster, processedEndpoints) + var mirrorClusterName string + if existingMirrorClusterName == "" { + mirrorClusterName = getClusterName(mirrorEndpointCluster.EndpointPrefix, organizationID, vHost, adapterInternalAPI.GetTitle(), apiVersion, resource.GetID()) + mirrorCluster, mirrorAddress, err := processEndpoints(mirrorClusterName, mirrorEndpointCluster, timeout, mirrorBasepath) + if err != nil { + logger.LoggerOasparser.ErrorC(logging.PrintError(logging.Error2239, logging.MAJOR, "Error while adding resource level mirror filter endpoints for %s:%v-%v. %v", apiTitle, apiVersion, resourcePath, err.Error())) + } else { + clusters = append(clusters, mirrorCluster) + endpoints = append(endpoints, mirrorAddress...) + processedEndpoints[mirrorClusterName] = *mirrorEndpointCluster + } + } else { + mirrorClusterName = existingMirrorClusterName + } + if _, exists := mirrorClusterNames[op.GetID()]; !exists { + mirrorClusterNames[op.GetID()] = []string{} + } + mirrorClusterNames[op.GetID()] = append(mirrorClusterNames[op.GetID()], mirrorClusterName) + } + } + } + // 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) + false, false, mirrorClusterNames) routeP, err := createRoutes(routeParams) if err != nil { @@ -238,7 +269,7 @@ func CreateRoutesWithClusters(adapterInternalAPI *model.AdapterInternalAPI, inte routes = append(routes, routeP...) if adapterInternalAPI.IsDefaultVersion { defaultRoutes, errDefaultPath := createRoutes(genRouteCreateParams(adapterInternalAPI, resource, vHost, basePath, clusterName, *operationalReqInterceptors, *operationalRespInterceptorVal, organizationID, - false, true)) + false, true, mirrorClusterNames)) if errDefaultPath != nil { logger.LoggerXds.ErrorC(logging.PrintError(logging.Error2231, logging.MAJOR, "Error while creating routes for 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) @@ -704,6 +735,7 @@ func createRoutes(params *routeCreateParams) (routes []*routev3.Route, err error vHost := params.vHost xWso2Basepath := params.xWSO2BasePath apiType := params.apiType + mirrorClusterNames := params.mirrorClusterNames // cors policy corsPolicy := getCorsPolicy(params.corsPolicy) @@ -907,7 +939,7 @@ func createRoutes(params *routeCreateParams) (routes []*routev3.Route, err error var responseHeadersToAdd []*corev3.HeaderValueOption var responseHeadersToRemove []string var pathRewriteConfig *envoy_type_matcherv3.RegexMatchAndSubstitute - + var requestRedirectAction *routev3.Route_Redirect hasMethodRewritePolicy := false var newMethod string @@ -963,6 +995,10 @@ func createRoutes(params *routeCreateParams) (routes []*routev3.Route, err error if err != nil { return nil, err } + case constants.ActionRedirectRequest: + logger.LoggerOasparser.Debugf("Adding %s policy to request flow for %s %s", + constants.ActionRedirectRequest, resourcePath, operation.GetMethod()) + requestRedirectAction = generateRequestRedirectRoute(resourcePath, requestPolicy.Parameters) } } @@ -980,7 +1016,6 @@ func createRoutes(params *routeCreateParams) (routes []*routev3.Route, err error " %v", responsePolicy.Action, operation.GetMethod(), resourcePath, err) } responseHeadersToAdd = append(responseHeadersToAdd, responseHeaderToAdd) - case constants.ActionHeaderRemove: logger.LoggerOasparser.Debugf("Adding %s policy to response flow for %s %s", constants.ActionHeaderRemove, resourcePath, operation.GetMethod()) @@ -1010,12 +1045,12 @@ func createRoutes(params *routeCreateParams) (routes []*routev3.Route, err error metadataValue := operation.GetMethod() + "_to_" + newMethod match2.DynamicMetadata = generateMetadataMatcherForInternalRoutes(metadataValue) - action1 := generateRouteAction(apiType, routeConfig, rateLimitPolicyCriteria) - action2 := generateRouteAction(apiType, routeConfig, rateLimitPolicyCriteria) + action1 := generateRouteAction(apiType, routeConfig, rateLimitPolicyCriteria, mirrorClusterNames[operation.GetID()]) + action2 := generateRouteAction(apiType, routeConfig, rateLimitPolicyCriteria, mirrorClusterNames[operation.GetID()]) // Create route1 for current method. // Do not add policies to route config. Send via enforcer - route1 := generateRouteConfig(xWso2Basepath+operation.GetMethod(), match1, action1, nil, decorator, perRouteFilterConfigs, + route1 := generateRouteConfig(xWso2Basepath+operation.GetMethod(), match1, action1, requestRedirectAction, nil, decorator, perRouteFilterConfigs, nil, nil, nil, nil) // Create route2 for new method. @@ -1026,24 +1061,27 @@ func createRoutes(params *routeCreateParams) (routes []*routev3.Route, err error action2.Route.RegexRewrite = generateRegexMatchAndSubstitute(routePath, resourcePath, pathMatchType) } configToSkipEnforcer := generateFilterConfigToSkipEnforcer() - route2 := generateRouteConfig(xWso2Basepath, match2, action2, nil, decorator, configToSkipEnforcer, + route2 := generateRouteConfig(xWso2Basepath, match2, action2, requestRedirectAction, nil, decorator, configToSkipEnforcer, requestHeadersToAdd, requestHeadersToRemove, responseHeadersToAdd, responseHeadersToRemove) routes = append(routes, route1) routes = append(routes, route2) } else { + var action *routev3.Route_Route + if requestRedirectAction == nil { + action = generateRouteAction(apiType, routeConfig, rateLimitPolicyCriteria, mirrorClusterNames[operation.GetID()]) + } logger.LoggerOasparser.Debug("Creating routes for resource with policies", resourcePath, operation.GetMethod()) // create route for current method. Add policies to route config. Send via enforcer - action := generateRouteAction(apiType, routeConfig, rateLimitPolicyCriteria) match := generateRouteMatch(routePath) match.Headers = generateHTTPMethodMatcher(operation.GetMethod(), clusterName) match.DynamicMetadata = generateMetadataMatcherForExternalRoutes() - if pathRewriteConfig != nil { + if pathRewriteConfig != nil && requestRedirectAction == nil { action.Route.RegexRewrite = pathRewriteConfig - } else { + } else if requestRedirectAction == nil { action.Route.RegexRewrite = generateRegexMatchAndSubstitute(routePath, resourcePath, pathMatchType) } - route := generateRouteConfig(xWso2Basepath, match, action, nil, decorator, perRouteFilterConfigs, + route := generateRouteConfig(xWso2Basepath, match, action, requestRedirectAction, nil, decorator, perRouteFilterConfigs, requestHeadersToAdd, requestHeadersToRemove, responseHeadersToAdd, responseHeadersToRemove) routes = append(routes, route) } @@ -1057,11 +1095,11 @@ func createRoutes(params *routeCreateParams) (routes []*routev3.Route, err error } match := generateRouteMatch(routePath) match.Headers = generateHTTPMethodMatcher(methodRegex, clusterName) - action := generateRouteAction(apiType, routeConfig, rateLimitPolicyCriteria) + action := generateRouteAction(apiType, routeConfig, rateLimitPolicyCriteria, nil) rewritePath := generateRoutePathForReWrite(basePath, resourcePath, pathMatchType) action.Route.RegexRewrite = generateRegexMatchAndSubstitute(rewritePath, resourcePath, pathMatchType) - route := generateRouteConfig(xWso2Basepath, match, action, nil, decorator, perRouteFilterConfigs, + route := generateRouteConfig(xWso2Basepath, match, action, nil, nil, decorator, perRouteFilterConfigs, nil, nil, nil, nil) // general headers to add and remove are included in this methods routes = append(routes, route) } @@ -1528,7 +1566,8 @@ func getCorsPolicy(corsConfig *model.CorsConfig) *cors_filter_v3.CorsPolicy { func genRouteCreateParams(swagger *model.AdapterInternalAPI, resource *model.Resource, vHost, endpointBasePath string, clusterName string, requestInterceptor map[string]model.InterceptEndpoint, - responseInterceptor map[string]model.InterceptEndpoint, organizationID string, isSandbox bool, createDefaultPath bool) *routeCreateParams { + responseInterceptor map[string]model.InterceptEndpoint, organizationID string, isSandbox bool, createDefaultPath bool, + mirrorClusterNames map[string][]string) *routeCreateParams { params := &routeCreateParams{ organizationID: organizationID, @@ -1551,6 +1590,7 @@ func genRouteCreateParams(swagger *model.AdapterInternalAPI, resource *model.Res createDefaultPath: createDefaultPath, environment: swagger.GetEnvironment(), envType: swagger.EnvType, + mirrorClusterNames: mirrorClusterNames, } return params } diff --git a/adapter/internal/oasparser/model/adapter_internal_api.go b/adapter/internal/oasparser/model/adapter_internal_api.go index 6e962ba29..d760b256f 100644 --- a/adapter/internal/oasparser/model/adapter_internal_api.go +++ b/adapter/internal/oasparser/model/adapter_internal_api.go @@ -429,7 +429,7 @@ func (adapterInternalAPI *AdapterInternalAPI) GetEnvironment() string { // This needs to be checked prior to generate router/enforcer related resources. func (adapterInternalAPI *AdapterInternalAPI) Validate() error { for _, res := range adapterInternalAPI.resources { - if res.endpoints == nil || len(res.endpoints.Endpoints) == 0 { + if res.endpoints == nil || (len(res.endpoints.Endpoints) == 0 && !res.hasRequestRedirectFilter) { loggers.LoggerOasparser.Errorf("No Endpoints are provided for the resources in %s:%s, API_UUID: %v", adapterInternalAPI.title, adapterInternalAPI.version, adapterInternalAPI.UUID) return errors.New("no endpoints are provided for the API") @@ -486,6 +486,8 @@ func (adapterInternalAPI *AdapterInternalAPI) SetInfoHTTPRouteCR(httpRoute *gwap statusCodes = append(statusCodes, config.Envoy.Upstream.Retry.StatusCodes...) var baseIntervalInMillis uint32 hasURLRewritePolicy := false + hasRequestRedirectPolicy := false + var mirrorEndpointsList []Endpoint var securityConfig []EndpointSecurity backendBasePath := "" for _, backend := range rule.BackendRefs { @@ -672,6 +674,54 @@ func (adapterInternalAPI *AdapterInternalAPI) SetInfoHTTPRouteCR(httpRoute *gwap Parameters: policyParameters, }) } + case gwapiv1.HTTPRouteFilterRequestRedirect: + hasRequestRedirectPolicy = true + policyParameters := make(map[string]interface{}) + + policyParameters[constants.RedirectScheme] = *filter.RequestRedirect.Scheme + policyParameters[constants.RedirectHostname] = string(*filter.RequestRedirect.Hostname) + if filter.RequestRedirect.Port != nil { + policyParameters[constants.RedirectPort] = strconv.Itoa(int(*filter.RequestRedirect.Port)) + } + + if filter.RequestRedirect.StatusCode != nil { + policyParameters[constants.RedirectStatusCode] = *filter.RequestRedirect.StatusCode + } + + switch filter.RequestRedirect.Path.Type { + case gwapiv1.FullPathHTTPPathModifier: + policyParameters[constants.RedirectPath] = backendBasePath + *filter.RequestRedirect.Path.ReplaceFullPath + case gwapiv1.PrefixMatchHTTPPathModifier: + policyParameters[constants.RedirectPath] = backendBasePath + *filter.RequestRedirect.Path.ReplacePrefixMatch + } + + policies.Request = append(policies.Request, Policy{ + PolicyName: string(gwapiv1.HTTPRouteFilterRequestRedirect), + Action: constants.ActionRedirectRequest, + Parameters: policyParameters, + }) + + case gwapiv1.HTTPRouteFilterRequestMirror: + policyParameters := make(map[string]interface{}) + backend := &filter.RequestMirror.BackendRef + backendName := types.NamespacedName{ + Name: string(backend.Name), + Namespace: utils.GetNamespace(backend.Namespace, httpRoute.Namespace), + } + _, ok := resourceParams.BackendMapping[backendName.String()] + if !ok { + return fmt.Errorf("backend: %s has not been resolved", backendName) + } + mirrorEndpoints := GetEndpoints(backendName, resourceParams.BackendMapping) + if len(mirrorEndpoints) > 0 { + policyParameters["endpoints"] = mirrorEndpoints + } + mirrorEndpointsList = append(mirrorEndpointsList, mirrorEndpoints...) + policies.Request = append(policies.Request, Policy{ + PolicyName: string(gwapiv1.HTTPRouteFilterRequestMirror), + Action: constants.ActionMirrorRequest, + Parameters: policyParameters, + }) } } resourceAPIPolicy = concatAPIPolicies(resourceAPIPolicy, nil) @@ -681,11 +731,15 @@ func (adapterInternalAPI *AdapterInternalAPI) SetInfoHTTPRouteCR(httpRoute *gwap loggers.LoggerOasparser.Debugf("Calculating auths for API ..., API_UUID = %v", adapterInternalAPI.UUID) apiAuth := getSecurity(resourceAuthScheme) - if len(rule.BackendRefs) < 1 { + + if !hasRequestRedirectPolicy && len(rule.BackendRefs) < 1 { return fmt.Errorf("no backendref were provided") } for _, match := range rule.Matches { + if hasURLRewritePolicy && hasRequestRedirectPolicy { + return fmt.Errorf("cannot have URL Rewrite and Request Redirect under the same rule") + } if !hasURLRewritePolicy { policyParameters := make(map[string]interface{}) if *match.Path.Type == gwapiv1.PathMatchPathPrefix { @@ -702,12 +756,20 @@ func (adapterInternalAPI *AdapterInternalAPI) SetInfoHTTPRouteCR(httpRoute *gwap }) } resourcePath := adapterInternalAPI.xWso2Basepath + *match.Path.Value + var mirrorEndpointCluster *EndpointCluster + if len(mirrorEndpointsList) > 0 { + mirrorEndpointCluster = &EndpointCluster{ + Endpoints: mirrorEndpointsList, + } + } + operations := getAllowedOperations(match.Method, policies, apiAuth, + parseRateLimitPolicyToInternal(resourceRatelimitPolicy), scopes, mirrorEndpointCluster) resource := &Resource{path: resourcePath, - methods: getAllowedOperations(match.Method, policies, apiAuth, - parseRateLimitPolicyToInternal(resourceRatelimitPolicy), scopes), - pathMatchType: *match.Path.Type, - hasPolicies: true, - iD: uuid.New().String(), + methods: operations, + pathMatchType: *match.Path.Type, + hasPolicies: true, + iD: uuid.New().String(), + hasRequestRedirectFilter: hasRequestRedirectPolicy, } resource.endpoints = &EndpointCluster{ diff --git a/adapter/internal/oasparser/model/api_operation.go b/adapter/internal/oasparser/model/api_operation.go index d26dde0f2..0547223d1 100644 --- a/adapter/internal/oasparser/model/api_operation.go +++ b/adapter/internal/oasparser/model/api_operation.go @@ -43,21 +43,22 @@ type Operation struct { policies OperationPolicies mockedAPIConfig *api.MockedApiConfig rateLimitPolicy *RateLimitPolicy + mirrorEndpoints *EndpointCluster } // Authentication holds authentication related configurations type Authentication struct { - Disabled bool - JWT *JWT - APIKey []APIKey - Oauth2 *Oauth2 + Disabled bool + JWT *JWT + APIKey []APIKey + Oauth2 *Oauth2 } // JWT holds JWT related configurations type JWT struct { Header string SendTokenToUpstream bool - Audience []string + Audience []string } // Oauth2 holds Oauth2 related configurations @@ -125,6 +126,11 @@ func (operation *Operation) GetID() string { return operation.iD } +// GetMirrorEndpoints returns the endpoints if a mirror filter has been applied. +func (operation *Operation) GetMirrorEndpoints() *EndpointCluster { + return operation.mirrorEndpoints +} + // GetCallInterceptorService returns the interceptor configs for a given operation. func (operation *Operation) GetCallInterceptorService(isIn bool) InterceptEndpoint { var policies []Policy @@ -174,7 +180,7 @@ func NewOperation(method string, security []string, extensions map[string]interf tier := ResolveThrottlingTier(extensions) disableSecurity := ResolveDisableSecurity(extensions) id := uuid.New().String() - return &Operation{id, method, security, nil, tier, disableSecurity, extensions, OperationPolicies{}, &api.MockedApiConfig{}, nil} + return &Operation{id, method, security, nil, tier, disableSecurity, extensions, OperationPolicies{}, &api.MockedApiConfig{}, nil, nil} } // NewOperationWithPolicies Creates and returns operation with given method and policies diff --git a/adapter/internal/oasparser/model/http_route.go b/adapter/internal/oasparser/model/http_route.go index 58bdeebde..09b3fed9b 100644 --- a/adapter/internal/oasparser/model/http_route.go +++ b/adapter/internal/oasparser/model/http_route.go @@ -287,25 +287,26 @@ func getSecurity(authScheme *dpv1alpha2.Authentication) *Authentication { // getAllowedOperations retuns a list of allowed operatons, if httpMethod is not specified then all methods are allowed. func getAllowedOperations(httpMethod *gwapiv1.HTTPMethod, policies OperationPolicies, auth *Authentication, - ratelimitPolicy *RateLimitPolicy, scopes []string) []*Operation { + ratelimitPolicy *RateLimitPolicy, scopes []string, mirrorEndpoints *EndpointCluster) []*Operation { if httpMethod != nil { return []*Operation{{iD: uuid.New().String(), method: string(*httpMethod), policies: policies, - auth: auth, rateLimitPolicy: ratelimitPolicy, scopes: scopes}} + auth: auth, rateLimitPolicy: ratelimitPolicy, scopes: scopes, mirrorEndpoints: mirrorEndpoints}} + } return []*Operation{{iD: uuid.New().String(), method: string(gwapiv1.HTTPMethodGet), policies: policies, auth: auth, rateLimitPolicy: ratelimitPolicy, scopes: scopes}, {iD: uuid.New().String(), method: string(gwapiv1.HTTPMethodPost), policies: policies, - auth: auth, rateLimitPolicy: ratelimitPolicy, scopes: scopes}, + auth: auth, rateLimitPolicy: ratelimitPolicy, scopes: scopes, mirrorEndpoints: mirrorEndpoints}, {iD: uuid.New().String(), method: string(gwapiv1.HTTPMethodDelete), policies: policies, - auth: auth, rateLimitPolicy: ratelimitPolicy, scopes: scopes}, + auth: auth, rateLimitPolicy: ratelimitPolicy, scopes: scopes, mirrorEndpoints: mirrorEndpoints}, {iD: uuid.New().String(), method: string(gwapiv1.HTTPMethodPatch), policies: policies, - auth: auth, rateLimitPolicy: ratelimitPolicy, scopes: scopes}, + auth: auth, rateLimitPolicy: ratelimitPolicy, scopes: scopes, mirrorEndpoints: mirrorEndpoints}, {iD: uuid.New().String(), method: string(gwapiv1.HTTPMethodPut), policies: policies, - auth: auth, rateLimitPolicy: ratelimitPolicy, scopes: scopes}, + auth: auth, rateLimitPolicy: ratelimitPolicy, scopes: scopes, mirrorEndpoints: mirrorEndpoints}, {iD: uuid.New().String(), method: string(gwapiv1.HTTPMethodHead), policies: policies, - auth: auth, rateLimitPolicy: ratelimitPolicy, scopes: scopes}, + auth: auth, rateLimitPolicy: ratelimitPolicy, scopes: scopes, mirrorEndpoints: mirrorEndpoints}, {iD: uuid.New().String(), method: string(gwapiv1.HTTPMethodOptions), policies: policies, - auth: auth, rateLimitPolicy: ratelimitPolicy, scopes: scopes}} + auth: auth, rateLimitPolicy: ratelimitPolicy, scopes: scopes, mirrorEndpoints: mirrorEndpoints}} } // SetInfoAPICR populates ID, ApiType, Version and XWso2BasePath of adapterInternalAPI. diff --git a/adapter/internal/operator/controllers/dp/api_controller.go b/adapter/internal/operator/controllers/dp/api_controller.go index 02238d41c..306bda663 100644 --- a/adapter/internal/operator/controllers/dp/api_controller.go +++ b/adapter/internal/operator/controllers/dp/api_controller.go @@ -847,6 +847,26 @@ func (apiReconciler *APIReconciler) getResolvedBackendsMapping(ctx context.Conte } } } + + for _, filter := range rule.Filters { + if filter.RequestMirror != nil { + mirrorBackend := filter.RequestMirror.BackendRef + + mirrorBackendNamespacedName := types.NamespacedName{ + Name: string(mirrorBackend.Name), + Namespace: utils.GetNamespace(mirrorBackend.Namespace, httpRoute.Namespace), + } + if _, exists := backendMapping[mirrorBackendNamespacedName.String()]; !exists { + resolvedMirrorBackend := utils.GetResolvedBackend(ctx, apiReconciler.client, mirrorBackendNamespacedName, &api) + if resolvedMirrorBackend != nil { + backendMapping[mirrorBackendNamespacedName.String()] = resolvedMirrorBackend + } else { + return nil, fmt.Errorf("unable to find backend %s", mirrorBackendNamespacedName.String()) + } + } + } + + } } // Resolve backends in InterceptorServices