diff --git a/runtime/config-deployer-service/ballerina/APIClient.bal b/runtime/config-deployer-service/ballerina/APIClient.bal index 0a6d3d8a5..7c919c728 100644 --- a/runtime/config-deployer-service/ballerina/APIClient.bal +++ b/runtime/config-deployer-service/ballerina/APIClient.bal @@ -787,14 +787,17 @@ public class APIClient { return e909022("Provided Type currently not supported for GraphQL APIs.", error("Provided Type currently not supported for GraphQL APIs.")); } } else if apkConf.'type == API_TYPE_REST { - { - model:HTTPRouteRule httpRouteRule = { - matches: self.retrieveHTTPMatches(apkConf, operation, organization), - backendRefs: self.retrieveGeneratedBackend(apkConf, endpointToUse, endpointType), - filters: self.generateFilters(apiArtifact, apkConf, endpointToUse, operation, endpointType, organization) - }; - return httpRouteRule; + model:HTTPRouteFilter[] filters = []; + boolean hasRedirectPolicy = false; + [filters, hasRedirectPolicy] = self.generateFilters(apiArtifact, apkConf, endpointToUse, operation, endpointType, organization); + model:HTTPRouteRule httpRouteRule = { + matches: self.retrieveHTTPMatches(apkConf, operation, organization), + filters: filters + }; + if !hasRedirectPolicy { + httpRouteRule.backendRefs = self.retrieveGeneratedBackend(apkConf, endpointToUse, endpointType); } + return httpRouteRule; } else { return e909018("Invalid API Type specified"); } @@ -808,19 +811,9 @@ public class APIClient { } } - private isolated function generateFilters(model:APIArtifact apiArtifact, APKConf apkConf, model:Endpoint endpoint, APKOperations operation, string endpointType, commons:Organization organization) returns model:HTTPRouteFilter[] { + private isolated function generateFilters(model:APIArtifact apiArtifact, APKConf apkConf, model:Endpoint endpoint, APKOperations operation, string endpointType, commons:Organization organization) returns [model:HTTPRouteFilter[], boolean] { model:HTTPRouteFilter[] routeFilters = []; - string generatedPath = self.generatePrefixMatch(endpoint, operation); - model:HTTPRouteFilter replacePathFilter = { - 'type: "URLRewrite", - urlRewrite: { - path: { - 'type: "ReplaceFullPath", - replaceFullPath: generatedPath - } - } - }; - routeFilters.push(replacePathFilter); + boolean hasRedirectPolicy = false; APIOperationPolicies? operationPoliciesToUse = (); APIOperationPolicies? operationPolicies = apkConf.apiPolicies; if (operationPolicies is APIOperationPolicies && operationPolicies != {}) { @@ -833,61 +826,151 @@ public class APIClient { if operationPoliciesToUse is APIOperationPolicies { APKOperationPolicy[]? requestPolicies = operationPoliciesToUse.request; APKOperationPolicy[]? responsePolicies = operationPoliciesToUse.response; - if requestPolicies is APKOperationPolicy[] && requestPolicies.length() > 0 { - model:HTTPRouteFilter headerModifierFilter = {'type: "RequestHeaderModifier"}; - headerModifierFilter.requestHeaderModifier = self.extractHttpHeaderFilterData(requestPolicies, organization); - routeFilters.push(headerModifierFilter); + model:HTTPRouteFilter[] requestHttpRouteFilters = []; + [requestHttpRouteFilters, hasRedirectPolicy] = self.extractHttpRouteFilter(apiArtifact, apkConf, operation, endpoint, requestPolicies, organization, true); + routeFilters.push(...requestHttpRouteFilters); } if responsePolicies is APKOperationPolicy[] && responsePolicies.length() > 0 { - model:HTTPRouteFilter headerModifierFilter = {'type: "ResponseHeaderModifier"}; - headerModifierFilter.responseHeaderModifier = self.extractHttpHeaderFilterData(responsePolicies, organization); - routeFilters.push(headerModifierFilter); + model:HTTPRouteFilter[] responseHttpRouteFilters = []; + [responseHttpRouteFilters, _] = self.extractHttpRouteFilter(apiArtifact, apkConf, operation, endpoint, responsePolicies, organization, false); + routeFilters.push(...responseHttpRouteFilters); } } - return routeFilters; + + if !hasRedirectPolicy { + string generatedPath = self.generatePrefixMatch(endpoint, operation); + model:HTTPRouteFilter replacePathFilter = { + 'type: "URLRewrite", + urlRewrite: { + path: { + 'type: "ReplaceFullPath", + replaceFullPath: generatedPath + } + } + }; + routeFilters.push(replacePathFilter); + } + return [routeFilters, hasRedirectPolicy]; } - isolated function extractHttpHeaderFilterData(APKOperationPolicy[] operationPolicy, commons:Organization organization) returns model:HTTPHeaderFilter { - model:HTTPHeader[] addPolicies = []; - model:HTTPHeader[] setPolicies = []; - string[] removePolicies = []; - foreach APKOperationPolicy policy in operationPolicy { + isolated function extractHttpRouteFilter(model:APIArtifact apiArtifact, APKConf apkConf, APKOperations apiOperation, model:Endpoint endpoint, APKOperationPolicy[] operationPolicies, commons:Organization organization, boolean isRequest) returns [model:HTTPRouteFilter[], boolean] { + model:HTTPRouteFilter[] httpRouteFilters = []; + model:HTTPHeader[] addHeaders = []; + model:HTTPHeader[] setHeaders = []; + string[] removeHeaders = []; + boolean hasRedirectPolicy = false; + model:HTTPRouteFilter headerModifierFilter = {'type: "RequestHeaderModifier"}; + if !isRequest { + headerModifierFilter.'type = "ResponseHeaderModifier"; + } + foreach APKOperationPolicy policy in operationPolicies { if policy is HeaderModifierPolicy { HeaderModifierPolicyParameters policyParameters = policy.parameters; match policy.policyName { AddHeaders => { - ModifierHeader[] addHeaders = policyParameters.headers; - foreach ModifierHeader header in addHeaders { - addPolicies.push(header); + ModifierHeader[] headers = policyParameters.headers; + foreach ModifierHeader header in headers { + headers.push(header); } } SetHeaders => { - ModifierHeader[] setHeaders = policyParameters.headers; - foreach ModifierHeader header in setHeaders { - setPolicies.push(header); + ModifierHeader[] headers = policyParameters.headers; + foreach ModifierHeader header in headers { + headers.push(header); } } RemoveHeaders => { - string[] removeHeaders = policyParameters.headers; - foreach string header in removeHeaders { - removePolicies.push(header); + string[] headers = policyParameters.headers; + foreach string header in headers { + headers.push(header); } } } + } else if policy is RequestMirrorPolicy { + RequestMirrorPolicyParameters policyParameters = policy.parameters; + string[] urls = policyParameters.urls; + foreach string url in urls { + model:HTTPRouteFilter mirrorFilter = {'type: "RequestMirror"}; + if !isRequest { + log:printError("Mirror filter cannot be appended as a response policy."); + } + string host = self.getHost(url); + int|error port = self.getPort(url); + if port is int { + model:Backend backendService = { + metadata: { + name: self.getBackendServiceUid(apkConf, apiOperation, "", organization), + labels: self.getLabels(apkConf, organization) + }, + spec: { + services: [ + { + host: host, + port: port + } + ], + basePath: getPath(url), + protocol: self.getProtocol(url) + } + }; + apiArtifact.backendServices[backendService.metadata.name] = backendService; + model:Endpoint mirrorEndpoint = { + url: url, + name: backendService.metadata.name + }; + model:BackendRef backendRef = self.retrieveGeneratedBackend(apkConf, mirrorEndpoint, "")[0]; + mirrorFilter.requestMirror = { + backendRef: { + name: backendRef.name, + namespace: backendRef.namespace, + group: backendRef.group, + kind: backendRef.kind, + port: backendRef.port + } + }; + } + httpRouteFilters.push(mirrorFilter); + } + } else if policy is RequestRedirectPolicy { + hasRedirectPolicy = true; + if !isRequest { + log:printError("Redirect filter cannot be appended as a response policy."); + } + RequestRedirectPolicyParameters policyParameters = policy.parameters; + string url = policyParameters.url; + int statusCode = policyParameters.statusCode; + model:HTTPRouteFilter redirectFilter = {'type: "RequestRedirect"}; + int|error port = self.getPort(url); + if port is int { + redirectFilter.requestRedirect = { + hostname: self.getHost(url), + scheme: self.getProtocol(url), + statusCode: statusCode, + path: { + 'type: "ReplaceFullPath", + replaceFullPath: self.getPath(url) + } + }; + } + httpRouteFilters.push(redirectFilter); } } - model:HTTPHeaderFilter headerModifier = {}; - if addPolicies != [] { - headerModifier.add = addPolicies; + + if addHeaders != [] { + headerModifierFilter.requestHeaderModifier.add = addHeaders; + } + if setHeaders != [] { + headerModifierFilter.requestHeaderModifier.set = setHeaders; } - if setPolicies != [] { - headerModifier.set = setPolicies; + if removeHeaders != [] { + headerModifierFilter.requestHeaderModifier.remove = removeHeaders; } - if removePolicies != [] { - headerModifier.remove = removePolicies; + if addHeaders.length() > 0 || setHeaders.length() > 0 || removeHeaders.length() > 0 { + httpRouteFilters.push(headerModifierFilter); } - return headerModifier; + + return [httpRouteFilters, hasRedirectPolicy]; } isolated function generatePrefixMatch(model:Endpoint endpoint, APKOperations operation) returns string { @@ -923,6 +1006,23 @@ public class APIClient { return generatedPath; } + isolated function getPath(string url) returns string { + string host = ""; + if url.startsWith("https://") { + host = url.substring(8, url.length()); + } else if url.startsWith("http://") { + host = url.substring(7, url.length()); + } else { + return ""; + } + int? indexOfSlash = host.indexOf("/", 0); + if indexOfSlash is int { + return host.substring(indexOfSlash); + } else { + return ""; + } + } + public isolated function retrievePathPrefix(string basePath, string 'version, string operation, commons:Organization organization) returns string { string[] splitValues = regex:split(operation, "/"); string generatedPath = ""; @@ -980,7 +1080,8 @@ public class APIClient { } private isolated function retrieveGQLRouteMatch(APKOperations apiOperation) returns model:GQLRouteMatch|error { - model:GQLType? routeMatch = model:getGQLRouteMatch(apiOperation.verb); + model:GQLType + ? routeMatch = model:getGQLRouteMatch(apiOperation.verb); if routeMatch is model:GQLType { return {'type: routeMatch, path: apiOperation.target}; } else { @@ -1273,7 +1374,7 @@ public class APIClient { model:BackendJWT backendJwt = self.retrieveBackendJWTPolicy(apkConf, apiArtifact, backendJWTPolicy, operations, organization); apiArtifact.backendJwt = backendJwt; policyReferences.push({name: backendJwt.metadata.name}); - } else if policyName != AddHeaders && policyName != SetHeaders && policyName != RemoveHeaders { + } else if policyName != AddHeaders && policyName != SetHeaders && policyName != RemoveHeaders && policyName != RequestMirror && policyName != RequestRedirect { return e909052(error("Incorrect API Policy name provided.")); } } diff --git a/runtime/config-deployer-service/ballerina/modules/model/HttpRoute.bal b/runtime/config-deployer-service/ballerina/modules/model/HttpRoute.bal index 38ff5974c..382f8540f 100644 --- a/runtime/config-deployer-service/ballerina/modules/model/HttpRoute.bal +++ b/runtime/config-deployer-service/ballerina/modules/model/HttpRoute.bal @@ -38,7 +38,6 @@ public type HTTPQueryParamMatch record { }; public type HTTPRouteMatch record { - HTTPPathMatch path?; HTTPHeaderMatch headers?; HTTPQueryParamMatch queryParams?; @@ -79,14 +78,13 @@ public type HTTPRequestRedirectFilter record { string scheme?; string hostname?; HTTPPathModifier path?; - string port?; + int port?; int statusCode?; }; public type HTTPURLRewriteFilter record { string hostname?; HTTPPathModifier path?; - }; public type LocalObjectReference record { diff --git a/runtime/config-deployer-service/ballerina/resources/apk-conf-schema.yaml b/runtime/config-deployer-service/ballerina/resources/apk-conf-schema.yaml index 1a8814ad9..2adc6185e 100644 --- a/runtime/config-deployer-service/ballerina/resources/apk-conf-schema.yaml +++ b/runtime/config-deployer-service/ballerina/resources/apk-conf-schema.yaml @@ -257,6 +257,8 @@ components: - $ref: "#/components/schemas/InterceptorPolicy" - $ref: "#/components/schemas/BackendJWTPolicy" - $ref: "#/components/schemas/HeaderModifierPolicy" + - $ref: "#/components/schemas/RequestMirrorPolicy" + - $ref: "#/components/schemas/RequestRedirectPolicy" discriminator: propertyName: "policyName" mapping: @@ -264,7 +266,9 @@ components: Interceptor: "#/components/schemas/InterceptorPolicy" AddHeaders: "#/components/schemas/HeaderModifierPolicy" SetHeaders: "#/components/schemas/HeaderModifierPolicy" - RemoveHeadersHeaders: "#/components/schemas/HeaderModifierPolicy" + RemoveHeaders: "#/components/schemas/HeaderModifierPolicy" + RequestMirror: "#/components/schemas/RequestMirrorPolicy" + RequstRedirect: "#/components/schemas/RequestRedirectPolicy" BaseOperationPolicy: title: API Operation Policy required: @@ -521,6 +525,33 @@ components: type: string value: type: string + RequestMirrorPolicy: + title: Request Mirror Parameters + type: object + properties: + urls: + type: array + items: + - type: string + additionalProperties: false + RequestRedirectPolicy: + title: Request Redirect Parameters + type: object + properties: + url: + type: string + description: The URL to redirect the request to. + statusCode: + type: integer + description: The status code to show upon redirecting the request. + default: 301 + enum: + - 301 + - 302 + - 303 + - 307 + - 308 + additionalProperties: false CustomClaims: type: object required: diff --git a/runtime/config-deployer-service/ballerina/types.bal b/runtime/config-deployer-service/ballerina/types.bal index 1f59fd5d3..eecc9c4e9 100644 --- a/runtime/config-deployer-service/ballerina/types.bal +++ b/runtime/config-deployer-service/ballerina/types.bal @@ -127,7 +127,63 @@ public type APKOperations record { }; # Common type for operation policies. -public type APKOperationPolicy InterceptorPolicy|BackendJWTPolicy|HeaderModifierPolicy; +public type APKOperationPolicy InterceptorPolicy|BackendJWTPolicy|HeaderModifierPolicy|RequestMirrorPolicy|RequestRedirectPolicy; + +# Header modification configuration for an operation. +# +# + parameters - Contains header name and value of the header. +public type HeaderModifierPolicy record { + *BaseOperationPolicy; + HeaderModifierPolicyParameters parameters; +}; + +# Configuration for header modifiers as received from the apk-conf file. +# +# + headers - Headers to be added, set or removed. +public type HeaderModifierPolicyParameters record {| + ModifierHeader[]|string[] headers; +|}; + +# Configuration for headers. +# +# + name - The name of the header. +# + value - The value of the header. +public type ModifierHeader record {| + string name; + string value; +|}; + +# Request mirror configuration for an operation. +# +# + parameters - Contains the urls to request the mirror to. +public type RequestMirrorPolicy record { + *BaseOperationPolicy; + RequestMirrorPolicyParameters parameters; +}; + +# Configuration containing the different headers. +# +# + urls - The urls to mirror the filters to. +public type RequestMirrorPolicyParameters record {| + string[] urls; +|}; + +# Request redirect configuration for an operation. +# +# + parameters - Contains the url to redirect the request to. +public type RequestRedirectPolicy record { + *BaseOperationPolicy; + RequestRedirectPolicyParameters parameters; +}; + +# Configuration containing the different headers. +# +# + url - The url to redirect the filters to. +# + statusCode - The status code to be sent as response to the client. +public type RequestRedirectPolicyParameters record {| + string url; + int statusCode; +|}; # Configuration for API deployment using the apk-conf file. # @@ -319,7 +375,9 @@ public enum PolicyName { Interceptor, AddHeaders, SetHeaders, - RemoveHeaders + RemoveHeaders, + RequestMirror, + RequestRedirect } # Configuration for authentication types. @@ -331,14 +389,6 @@ public type Authentication record {| boolean enabled = true; |}; -# Header modification configuration for an operation. -# -# + parameters - Contains header name and value of the header. -public type HeaderModifierPolicy record { - *BaseOperationPolicy; - HeaderModifierPolicyParameters parameters; -}; - # Interceptor policy configuration for an operation. # # + parameters - Contains interceptor policy parameters @@ -387,33 +437,6 @@ public type APKConf record { CORSConfiguration corsConfiguration?; }; -# Configuration for header modifiers as received from the apk-conf file. -# -# + headers - Headers to be added, set or removed. -public type HeaderModifierPolicyParameters record {| - ModifierHeader[]|string[] headers; -|}; - -# Configuration containing the different headers. -# -# + addHeaders - Headers to be added. -# + setHeaders - Headers to be set. -# + removeHeaders - Headers to be removed. -public type HeaderModifierFilterParameters record {| - ModifierHeader[] addHeaders; - ModifierHeader[] setHeaders; - string[] removeHeaders; -|}; - -# Configuration for headers. -# -# + name - The name of the header. -# + value - The value of the header. -public type ModifierHeader record {| - string name; - string value; -|}; - # Configuration for Interceptor Policy parameters. # # + backendUrl - Backend URL of the interceptor service. diff --git a/runtime/config-deployer-service/docker/config-deployer/conf/apk-schema.json b/runtime/config-deployer-service/docker/config-deployer/conf/apk-schema.json index 09149e9f5..9f202e8d1 100644 --- a/runtime/config-deployer-service/docker/config-deployer/conf/apk-schema.json +++ b/runtime/config-deployer-service/docker/config-deployer/conf/apk-schema.json @@ -362,7 +362,9 @@ "RemoveHeaders", "SetHeaders", "Interceptor", - "BackendJwt" + "BackendJwt", + "RequestMirror", + "RequestRedirect" ] }, "policyVersion": { @@ -385,6 +387,12 @@ }, { "$ref": "#/schemas/HeaderModifierProperties" + }, + { + "$ref": "#/schemas/RequestMirrorProperties" + }, + { + "$ref": "#/schemas/RequestRedirectProperties" } ] } @@ -736,6 +744,42 @@ "type": "string" } }, + "RequestMirrorProperties": { + "title": "Request Mirror Parameters", + "type": "object", + "properties": { + "urls": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "additionalProperties": false + }, + "RequestRedirectProperties": { + "title": "Request Redirect Parameters", + "type": "object", + "properties": { + "url": { + "type": "string", + "description": "The URL to redirect the request to." + }, + "statusCode": { + "type": "integer", + "description": "The status code to show upon redirecting the request.", + "default": 301, + "enum": [ + 301, + 302, + 303, + 307, + 308 + ] + } + }, + "additionalProperties": false + }, "CustomClaims": { "type": "object", "required": [