From 3737a3ad1badd574d68cc613bbd3066161464894 Mon Sep 17 00:00:00 2001 From: Pubudu Gunatilaka Date: Tue, 30 Jan 2024 16:37:20 +0530 Subject: [PATCH 01/14] Add basic changes of semantic versioning --- .../discovery/xds/semantic_versioning.go | 356 ++++++++++++++++++ adapter/internal/discovery/xds/server.go | 10 +- adapter/pkg/loggers/logger.go | 23 +- .../pkg/semanticversion/semantic_version.go | 112 ++++++ 4 files changed, 490 insertions(+), 11 deletions(-) create mode 100644 adapter/internal/discovery/xds/semantic_versioning.go create mode 100644 adapter/pkg/semanticversion/semantic_version.go diff --git a/adapter/internal/discovery/xds/semantic_versioning.go b/adapter/internal/discovery/xds/semantic_versioning.go new file mode 100644 index 000000000..373e08ef8 --- /dev/null +++ b/adapter/internal/discovery/xds/semantic_versioning.go @@ -0,0 +1,356 @@ +/* + * Copyright (c) 2023, 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 xds + +import ( + "strconv" + "strings" + + routev3 "github.com/envoyproxy/go-control-plane/envoy/config/route/v3" + envoy_type_matcherv3 "github.com/envoyproxy/go-control-plane/envoy/type/matcher/v3" + logger "github.com/wso2/apk/adapter/internal/loggers" + logging "github.com/wso2/apk/adapter/internal/logging" + "github.com/wso2/apk/adapter/internal/oasparser/model" + semantic_version "github.com/wso2/apk/adapter/pkg/semanticversion" +) + +// GetVersionMatchRegex returns the regex to match the full version string +func GetVersionMatchRegex(version string) string { + // Match "." character in the version by replacing it with "\\." + return strings.ReplaceAll(version, ".", "\\.") +} + +// GetMajorMinorVersionRangeRegex generates major and minor version compatible range regex for the given version +func GetMajorMinorVersionRangeRegex(semVersion semantic_version.SemVersion) string { + majorVersion := strconv.Itoa(semVersion.Major) + minorVersion := strconv.Itoa(semVersion.Minor) + if semVersion.Patch == nil { + return "v" + majorVersion + "(?:\\." + minorVersion + ")?" + } + patchVersion := strconv.Itoa(*semVersion.Patch) + return "v" + majorVersion + "(?:\\." + minorVersion + "(?:\\." + patchVersion + ")?)?" +} + +// GetMinorVersionRangeRegex generates minor version compatible range regex for the given version +func GetMinorVersionRangeRegex(semVersion semantic_version.SemVersion) string { + if semVersion.Patch == nil { + return GetVersionMatchRegex(semVersion.Version) + } + majorVersion := strconv.Itoa(semVersion.Major) + minorVersion := strconv.Itoa(semVersion.Minor) + patchVersion := strconv.Itoa(*semVersion.Patch) + return "v" + majorVersion + "\\." + minorVersion + "(?:\\." + patchVersion + ")?" +} + +// GetMajorVersionRange generates major version range for the given version +func GetMajorVersionRange(semVersion semantic_version.SemVersion) string { + return "v" + strconv.Itoa(semVersion.Major) +} + +// GetMinorVersionRange generates minor version range for the given version +func GetMinorVersionRange(semVersion semantic_version.SemVersion) string { + return "v" + strconv.Itoa(semVersion.Major) + "." + strconv.Itoa(semVersion.Minor) +} + +func updateRoutingRulesOnAPIUpdate(organizationID, apiIdentifier, apiName, apiVersion, vHost, envType string) { + apiSemVersion, err := semantic_version.ValidateAndGetVersionComponents(apiVersion, apiName) + // If the version validation is not success, we just proceed without intelligent version + // Valid version pattern: vx.y.z or vx.y where x, y and z are non-negative integers and v is a prefix + if err != nil && apiSemVersion == nil { + return + } + + apiRangeIdentifier := GenerateIdentifierForAPIWithoutVersion(vHost, apiName) + // Check the major and minor version ranges of the current API + existingMajorRangeLatestSemVersion, isMajorRangeRegexAvailable := + orgIDLatestAPIVersionMap[organizationID][apiRangeIdentifier][GetMajorVersionRange(*apiSemVersion)] + existingMinorRangeLatestSemVersion, isMinorRangeRegexAvailable := + orgIDLatestAPIVersionMap[organizationID][apiRangeIdentifier][GetMinorVersionRange(*apiSemVersion)] + + // Check whether the current API is the latest version in the major and minor version ranges + isLatestMajorVersion := !isMajorRangeRegexAvailable || existingMajorRangeLatestSemVersion.Compare(*apiSemVersion) + isLatestMinorVersion := !isMinorRangeRegexAvailable || existingMinorRangeLatestSemVersion.Compare(*apiSemVersion) + + // Remove the existing regexes from the path specifier when latest major and/or minor version is available + if (isMajorRangeRegexAvailable || isMinorRangeRegexAvailable) && (isLatestMajorVersion || isLatestMinorVersion) { + // Organization's all apis + for apiUUID, envoyInternalAPI := range orgAPIMap[organizationID] { + // API's all versions in the same vHost + if envoyInternalAPI.adapterInternalAPI.GetTitle() == apiName && isVHostMatched(organizationID, apiUUID, vHost, envType) { + if (isMajorRangeRegexAvailable && envoyInternalAPI.adapterInternalAPI.GetVersion() == existingMajorRangeLatestSemVersion.Version) || + (isMinorRangeRegexAvailable && envoyInternalAPI.adapterInternalAPI.GetVersion() == existingMinorRangeLatestSemVersion.Version) { + for _, route := range envoyInternalAPI.routes { + regex := route.GetMatch().GetSafeRegex().GetRegex() + regexRewritePattern := route.GetRoute().GetRegexRewrite().GetPattern().GetRegex() + existingMinorRangeLatestVersionRegex := GetVersionMatchRegex(existingMinorRangeLatestSemVersion.Version) + existingMajorRangeLatestVersionRegex := GetVersionMatchRegex(existingMajorRangeLatestSemVersion.Version) + if isMinorRangeRegexAvailable && envoyInternalAPI.adapterInternalAPI.GetVersion() == existingMinorRangeLatestSemVersion.Version && isLatestMinorVersion { + regex = strings.Replace(regex, GetMinorVersionRangeRegex(existingMinorRangeLatestSemVersion), existingMinorRangeLatestVersionRegex, 1) + regex = strings.Replace(regex, GetMajorMinorVersionRangeRegex(existingMajorRangeLatestSemVersion), existingMajorRangeLatestVersionRegex, 1) + regexRewritePattern = strings.Replace(regexRewritePattern, GetMinorVersionRangeRegex(existingMinorRangeLatestSemVersion), existingMinorRangeLatestVersionRegex, 1) + regexRewritePattern = strings.Replace(regexRewritePattern, GetMajorMinorVersionRangeRegex(existingMajorRangeLatestSemVersion), existingMajorRangeLatestVersionRegex, 1) + } + if isMajorRangeRegexAvailable && envoyInternalAPI.adapterInternalAPI.GetVersion() == existingMajorRangeLatestSemVersion.Version && isLatestMajorVersion { + regex = strings.Replace(regex, GetMajorMinorVersionRangeRegex(existingMajorRangeLatestSemVersion), GetMinorVersionRangeRegex(existingMajorRangeLatestSemVersion), 1) + regexRewritePattern = strings.Replace(regexRewritePattern, GetMajorMinorVersionRangeRegex(existingMajorRangeLatestSemVersion), GetMinorVersionRangeRegex(existingMajorRangeLatestSemVersion), 1) + } + pathSpecifier := &routev3.RouteMatch_SafeRegex{ + SafeRegex: &envoy_type_matcherv3.RegexMatcher{ + Regex: regex, + }, + } + route.Match.PathSpecifier = pathSpecifier + action := route.Action.(*routev3.Route_Route) + action.Route.RegexRewrite.Pattern.Regex = regexRewritePattern + route.Action = action + } + } + } + } + } + + if isLatestMajorVersion || isLatestMinorVersion { + // Update local memory map with the latest version ranges + majorVersionRange := GetMajorVersionRange(*apiSemVersion) + minorVersionRange := GetMinorVersionRange(*apiSemVersion) + if _, orgExists := orgIDLatestAPIVersionMap[organizationID]; !orgExists { + orgIDLatestAPIVersionMap[organizationID] = make(map[string]map[string]semantic_version.SemVersion) + } + if _, apiRangeExists := orgIDLatestAPIVersionMap[organizationID][apiRangeIdentifier]; !apiRangeExists { + orgIDLatestAPIVersionMap[organizationID][apiRangeIdentifier] = make(map[string]semantic_version.SemVersion) + } + latestVersions := orgIDLatestAPIVersionMap[organizationID][apiRangeIdentifier] + latestVersions[minorVersionRange] = *apiSemVersion + if isLatestMajorVersion { + latestVersions[majorVersionRange] = *apiSemVersion + } + + // Add the major and/or minor version range matching regexes to the path specifier when + // latest major and/or minor version is available + + apiRoutes := getRoutes(organizationID, apiIdentifier, vHost) + for _, route := range apiRoutes { + regex := route.GetMatch().GetSafeRegex().GetRegex() + regexRewritePattern := route.GetRoute().GetRegexRewrite().GetPattern().GetRegex() + apiVersionRegex := GetVersionMatchRegex(apiVersion) + if isLatestMajorVersion { + regex = strings.Replace(regex, apiVersionRegex, GetMajorMinorVersionRangeRegex(*apiSemVersion), 1) + regexRewritePattern = strings.Replace(regexRewritePattern, apiVersionRegex, GetMajorMinorVersionRangeRegex(*apiSemVersion), 1) + } else if isLatestMinorVersion { + regex = strings.Replace(regex, apiVersionRegex, GetMinorVersionRangeRegex(*apiSemVersion), 1) + regexRewritePattern = strings.Replace(regexRewritePattern, apiVersionRegex, GetMinorVersionRangeRegex(*apiSemVersion), 1) + } + pathSpecifier := &routev3.RouteMatch_SafeRegex{ + SafeRegex: &envoy_type_matcherv3.RegexMatcher{ + Regex: regex, + }, + } + route.Match.PathSpecifier = pathSpecifier + action := &routev3.Route_Route{} + action = route.Action.(*routev3.Route_Route) + action.Route.RegexRewrite.Pattern.Regex = regexRewritePattern + route.Action = action + } + } +} + +func updateRoutingRulesOnAPIDelete(organizationID, apiIdentifier string, api model.AdapterInternalAPI) { + // Update the intelligent routing if the deleting API is the latest version of the API range + // and the API range has other versions + vhost, err := ExtractVhostFromAPIIdentifier(apiIdentifier) + if err != nil { + logger.LoggerXds.ErrorC(logging.PrintError(logging.Error1411, logging.MAJOR, + "Error extracting vhost from API identifier: %v for Organization %v. Ignore deploying the API, error: %v", + apiIdentifier, organizationID, err)) + } + apiRangeIdentifier := GenerateIdentifierForAPIWithoutVersion(vhost, api.GetTitle()) + + latestAPIVersionMap, latestAPIVersionMapExists := orgIDLatestAPIVersionMap[organizationID][apiRangeIdentifier] + if !latestAPIVersionMapExists { + return + } + deletingAPISemVersion, _ := semantic_version.ValidateAndGetVersionComponents(api.GetVersion(), api.GetTitle()) + if deletingAPISemVersion == nil { + return + } + majorVersionRange := GetMajorVersionRange(*deletingAPISemVersion) + newLatestMajorRangeAPIIdentifier := "" + if deletingAPIsMajorRangeLatestAPISemVersion, ok := latestAPIVersionMap[majorVersionRange]; ok { + if deletingAPIsMajorRangeLatestAPISemVersion.Version == api.GetVersion() { + newLatestMajorRangeAPI := &semantic_version.SemVersion{ + Version: "", + Major: deletingAPISemVersion.Major, + Minor: 0, + Patch: nil, + } + for currentAPIIdentifier, envoyInternalAPI := range orgAPIMap[organizationID] { + // Iterate all the API versions other than the deleting API itself + if envoyInternalAPI.adapterInternalAPI.GetTitle() == api.GetTitle() && currentAPIIdentifier != apiIdentifier { + currentAPISemVersion, _ := semantic_version.ValidateAndGetVersionComponents(envoyInternalAPI.adapterInternalAPI.GetVersion(), envoyInternalAPI.adapterInternalAPI.GetTitle()) + if currentAPISemVersion != nil { + if currentAPISemVersion.Major == deletingAPISemVersion.Major { + if newLatestMajorRangeAPI.Compare(*currentAPISemVersion) { + newLatestMajorRangeAPI = currentAPISemVersion + newLatestMajorRangeAPIIdentifier = currentAPIIdentifier + } + } + } + } + } + if newLatestMajorRangeAPIIdentifier != "" { + orgIDLatestAPIVersionMap[organizationID][apiRangeIdentifier][majorVersionRange] = *newLatestMajorRangeAPI + apiRoutes := getRoutesForAPIIdentifier(organizationID, apiIdentifier) + for _, route := range apiRoutes { + regex := route.GetMatch().GetSafeRegex().GetRegex() + regexRewritePattern := route.GetRoute().GetRegexRewrite().GetPattern().GetRegex() + newLatestMajorRangeAPIVersionRegex := GetVersionMatchRegex(newLatestMajorRangeAPI.Version) + // Remove any available minor version range regexes and apply the minor range regex + regex = strings.Replace( + regex, + GetMinorVersionRangeRegex(*newLatestMajorRangeAPI), + newLatestMajorRangeAPIVersionRegex, + 1, + ) + regexRewritePattern = strings.Replace( + regexRewritePattern, + GetMinorVersionRangeRegex(*newLatestMajorRangeAPI), + newLatestMajorRangeAPIVersionRegex, + 1, + ) + regex = strings.Replace( + regex, + newLatestMajorRangeAPIVersionRegex, + GetMajorMinorVersionRangeRegex(*newLatestMajorRangeAPI), + 1, + ) + regexRewritePattern = strings.Replace( + regexRewritePattern, + newLatestMajorRangeAPIVersionRegex, + GetMajorMinorVersionRangeRegex(*newLatestMajorRangeAPI), + 1, + ) + pathSpecifier := &routev3.RouteMatch_SafeRegex{ + SafeRegex: &envoy_type_matcherv3.RegexMatcher{ + Regex: regex, + }, + } + route.Match.PathSpecifier = pathSpecifier + action := &routev3.Route_Route{} + action = route.Action.(*routev3.Route_Route) + action.Route.RegexRewrite.Pattern.Regex = regexRewritePattern + route.Action = action + } + } else { + delete(orgIDLatestAPIVersionMap[organizationID][apiRangeIdentifier], majorVersionRange) + } + } + } + minorVersionRange := GetMinorVersionRange(*deletingAPISemVersion) + if deletingAPIsMinorRangeLatestAPI, ok := latestAPIVersionMap[minorVersionRange]; ok { + if deletingAPIsMinorRangeLatestAPI.Version == api.GetVersion() { + newLatestMinorRangeAPI := &semantic_version.SemVersion{ + Version: "", + Major: deletingAPISemVersion.Major, + Minor: deletingAPISemVersion.Minor, + Patch: nil, + } + newLatestMinorRangeAPIIdentifier := "" + for currentAPIIdentifier, envoyInternalAPI := range orgAPIMap[organizationID] { + // Iterate all the API versions other than the deleting API itself + if envoyInternalAPI.adapterInternalAPI.GetTitle() == api.GetTitle() && currentAPIIdentifier != apiIdentifier { + currentAPISemVersion, _ := semantic_version.ValidateAndGetVersionComponents(envoyInternalAPI.adapterInternalAPI.GetVersion(), envoyInternalAPI.adapterInternalAPI.GetTitle()) + if currentAPISemVersion != nil { + if currentAPISemVersion.Major == deletingAPISemVersion.Major && + currentAPISemVersion.Minor == deletingAPISemVersion.Minor { + if newLatestMinorRangeAPI.Compare(*currentAPISemVersion) { + newLatestMinorRangeAPI = currentAPISemVersion + newLatestMinorRangeAPIIdentifier = currentAPIIdentifier + } + } + } + } + } + if newLatestMinorRangeAPIIdentifier != "" && newLatestMinorRangeAPIIdentifier != newLatestMajorRangeAPIIdentifier { + orgIDLatestAPIVersionMap[organizationID][apiRangeIdentifier][minorVersionRange] = *newLatestMinorRangeAPI + apiRoutes := getRoutesForAPIIdentifier(organizationID, newLatestMinorRangeAPIIdentifier) + for _, route := range apiRoutes { + regex := route.GetMatch().GetSafeRegex().GetRegex() + newLatestMinorRangeAPIVersionRegex := GetVersionMatchRegex(newLatestMinorRangeAPI.Version) + regex = strings.Replace( + regex, + newLatestMinorRangeAPIVersionRegex, + GetMinorVersionRangeRegex(*newLatestMinorRangeAPI), + 1, + ) + pathSpecifier := &routev3.RouteMatch_SafeRegex{ + SafeRegex: &envoy_type_matcherv3.RegexMatcher{ + Regex: regex, + }, + } + regexRewritePattern := route.GetRoute().GetRegexRewrite().GetPattern().GetRegex() + regexRewritePattern = strings.Replace( + regexRewritePattern, + newLatestMinorRangeAPIVersionRegex, + GetMinorVersionRangeRegex(*newLatestMinorRangeAPI), + 1, + ) + route.Match.PathSpecifier = pathSpecifier + action := &routev3.Route_Route{} + action = route.Action.(*routev3.Route_Route) + action.Route.RegexRewrite.Pattern.Regex = regexRewritePattern + route.Action = action + } + } else { + delete(orgIDLatestAPIVersionMap[organizationID][apiRangeIdentifier], minorVersionRange) + } + } + } +} + +func isVHostMatched(organizationID, apiUUID, vHost, envType string) bool { + + if _, ok := orgIDAPIvHostsMap[organizationID]; ok { + vHosts := orgIDAPIvHostsMap[organizationID][GetvHostsIdentifier(apiUUID, envType)] + + for _, vHostEntry := range vHosts { + if vHostEntry == vHost { + return true + } + } + } + return false +} + +func getRoutes(organizationID, apiUUID, vHost string) []*routev3.Route { + + var routes []*routev3.Route + if _, ok := orgAPIMap[organizationID]; ok { + routes = orgAPIMap[organizationID][GenerateIdentifierForAPIWithUUID(apiUUID, vHost)].routes + } + + return routes +} + +func getRoutesForAPIIdentifier(organizationID, apiIdentifier string) []*routev3.Route { + + var routes []*routev3.Route + if _, ok := orgAPIMap[organizationID]; ok { + routes = orgAPIMap[organizationID][apiIdentifier].routes + } + + return routes +} diff --git a/adapter/internal/discovery/xds/server.go b/adapter/internal/discovery/xds/server.go index 630f23aa2..e9daff6b2 100644 --- a/adapter/internal/discovery/xds/server.go +++ b/adapter/internal/discovery/xds/server.go @@ -53,6 +53,7 @@ import ( wso2_cache "github.com/wso2/apk/adapter/pkg/discovery/protocol/cache/v3" wso2_resource "github.com/wso2/apk/adapter/pkg/discovery/protocol/resource/v3" eventhubTypes "github.com/wso2/apk/adapter/pkg/eventhub/types" + semantic_version "github.com/wso2/apk/adapter/pkg/semanticversion" "github.com/wso2/apk/adapter/pkg/utils/stringutils" gwapiv1b1 "sigs.k8s.io/gateway-api/apis/v1beta1" ) @@ -101,8 +102,9 @@ var ( orgAPIMap map[string]map[string]*EnvoyInternalAPI // organizationID -> Vhost:API_UUID -> EnvoyInternalAPI struct map orgIDvHostBasepathMap map[string]map[string]string // organizationID -> Vhost:basepath -> Vhost:API_UUID - orgIDAPIvHostsMap map[string]map[string][]string // organizationID -> UUID -> prod/sand -> Envoy Vhost Array map + orgIDAPIvHostsMap map[string]map[string][]string // organizationID -> API_UUID-prod/sand -> Envoy Vhost Array map + orgIDLatestAPIVersionMap map[string]map[string]map[string]semantic_version.SemVersion // organizationID -> Vhost:APIName -> Version Range -> Latest API Version // Envoy Label as map key gatewayLabelConfigMap map[string]*EnvoyGatewayConfig // GW-Label -> EnvoyGatewayConfig struct map @@ -159,6 +161,7 @@ func init() { orgAPIMap = make(map[string]map[string]*EnvoyInternalAPI) orgIDAPIvHostsMap = make(map[string]map[string][]string) // organizationID -> UUID-prod/sand -> Envoy Vhost Array map orgIDvHostBasepathMap = make(map[string]map[string]string) + orgIDLatestAPIVersionMap = make(map[string]map[string]map[string]semantic_version.SemVersion) enforcerLabelMap = make(map[string]*EnforcerInternalAPI) // currently subscriptions, configs, applications, applicationPolicies, subscriptionPolicies, @@ -613,6 +616,11 @@ func GenerateHashedAPINameVersionIDWithoutVhost(name, version string) string { return generateHashValue(name, version) } +// GenerateIdentifierForAPIWithoutVersion generates an identifier unique to the API despite of the version +func GenerateIdentifierForAPIWithoutVersion(vhost, name string) string { + return fmt.Sprint(vhost, apiKeyFieldSeparator, name) +} + func generateHashValue(apiName string, apiVersion string) string { apiNameVersionHash := sha1.New() apiNameVersionHash.Write([]byte(apiName + ":" + apiVersion)) diff --git a/adapter/pkg/loggers/logger.go b/adapter/pkg/loggers/logger.go index c8f754261..178a01f4d 100644 --- a/adapter/pkg/loggers/logger.go +++ b/adapter/pkg/loggers/logger.go @@ -32,20 +32,22 @@ When you add a new logger instance add the related package name as a constant // package name constants const ( - pkgAuth = "github.com/wso2/apk/adapter/pkg/auth" - pkgSync = "github.com/wso2/apk/adapter/pkg/synchronizer" - pkgTLSUtils = "github.com/wso2/apk/adapter/pkg/utils/tlsutils" - pkgHealth = "github.com/wso2/apk/adapter/pkg/health" - pkgSoapUtils = "github.com/wso2/apk/adapter/pkg/utils/soaputils" + pkgAuth = "github.com/wso2/apk/adapter/pkg/auth" + pkgSync = "github.com/wso2/apk/adapter/pkg/synchronizer" + pkgTLSUtils = "github.com/wso2/apk/adapter/pkg/utils/tlsutils" + pkgHealth = "github.com/wso2/apk/adapter/pkg/health" + pkgSoapUtils = "github.com/wso2/apk/adapter/pkg/utils/soaputils" + pkgSemanticVersion = "github.com/wso2/product-microgateway/adapter/pkg/semanticversion" ) // logger package references var ( - LoggerAuth logging.Log - LoggerSync logging.Log - LoggerTLSUtils logging.Log - LoggerHealth logging.Log - LoggerSoapUtils logging.Log + LoggerAuth logging.Log + LoggerSync logging.Log + LoggerTLSUtils logging.Log + LoggerHealth logging.Log + LoggerSoapUtils logging.Log + LoggerSemanticVersion logging.Log ) func init() { @@ -59,5 +61,6 @@ func UpdateLoggers() { LoggerTLSUtils = logging.InitPackageLogger(pkgTLSUtils) LoggerHealth = logging.InitPackageLogger(pkgHealth) LoggerSoapUtils = logging.InitPackageLogger(pkgSoapUtils) + LoggerSemanticVersion = logging.InitPackageLogger(pkgSemanticVersion) logrus.Info("Updated loggers") } diff --git a/adapter/pkg/semanticversion/semantic_version.go b/adapter/pkg/semanticversion/semantic_version.go new file mode 100644 index 000000000..d4ece4526 --- /dev/null +++ b/adapter/pkg/semanticversion/semantic_version.go @@ -0,0 +1,112 @@ +/* + * Copyright (c) 2023, 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 semanticversion + +import ( + "errors" + "fmt" + "strconv" + "strings" + + logger "github.com/wso2/apk/adapter/pkg/loggers" +) + +// SemVersion is the struct to store the version components of an API +type SemVersion struct { + Version string + Major int + Minor int + Patch *int +} + +// ValidateAndGetVersionComponents validates version string and extracts version components +func ValidateAndGetVersionComponents(version string, apiName string) (*SemVersion, error) { + versionComponents := strings.Split(version, ".") + + // If the versionComponents length is less than 2, return error + if len(versionComponents) < 2 { + logger.LoggerSemanticVersion.Errorf("API version validation failed for API: %v. API Version: %v", apiName, version) + errMessage := "Invalid version: " + version + " for API: " + apiName + + ". API version should be in the format x.y.z, x.y, vx.y.z or vx.y where x,y,z are non-negative integers" + + " and v is version prefix" + return nil, errors.New(errMessage) + } + + majorVersionStr := strings.TrimPrefix(versionComponents[0], "v") + + majorVersion, majorVersionConvErr := strconv.Atoi(majorVersionStr) + minorVersion, minorVersionConvErr := strconv.Atoi(versionComponents[1]) + if majorVersionConvErr != nil || majorVersion < 0 { + logger.LoggerSemanticVersion.Errorf(fmt.Sprintf("API major version should be a non-negative integer in API: %v. API Version: %v", apiName, version), majorVersionConvErr) + return nil, errors.New("invalid version format") + } + + if minorVersionConvErr != nil || minorVersion < 0 { + logger.LoggerSemanticVersion.Errorf(fmt.Sprintf("API minor version should be a non-negative integer in API: %v. API Version: %v", apiName, version), minorVersionConvErr) + return nil, errors.New("invalid version format") + } + + if len(versionComponents) == 2 { + return &SemVersion{ + Version: version, + Major: majorVersion, + Minor: minorVersion, + Patch: nil, + }, nil + } + + patchVersion, patchVersionConvErr := strconv.Atoi(versionComponents[2]) + if patchVersionConvErr != nil { + logger.LoggerSemanticVersion.Errorf(fmt.Sprintf("API patch version should be an integer in API: %v. API Version: %v", apiName, version), patchVersionConvErr) + return nil, errors.New("invalid version format") + } + return &SemVersion{ + Version: version, + Major: majorVersion, + Minor: minorVersion, + Patch: &patchVersion, + }, nil +} + +// Compare - compares two semantic versions and returns true +// if `version` is greater or equal than `baseVersion` +func (baseVersion SemVersion) Compare(version SemVersion) bool { + if baseVersion.Major < version.Major { + return true + } else if baseVersion.Major > version.Major { + return false + } else { + if baseVersion.Minor < version.Minor { + return true + } else if baseVersion.Minor > version.Minor { + return false + } else { + if baseVersion.Patch != nil && version.Patch != nil { + if *baseVersion.Patch < *version.Patch { + return true + } else if *baseVersion.Patch > *version.Patch { + return false + } + } else if baseVersion.Patch == nil && version.Patch != nil { + return true + } else if baseVersion.Patch != nil && version.Patch == nil { + return false + } + } + } + return true +} From 8f2cda8886166836bb34e427f876064c23040132 Mon Sep 17 00:00:00 2001 From: Pubudu Gunatilaka Date: Thu, 8 Feb 2024 09:33:41 +0530 Subject: [PATCH 02/14] Enable Semantic versioning based on a configuration --- adapter/config/default_config.go | 1 + adapter/config/types.go | 7 ++-- .../discovery/xds/semantic_versioning.go | 35 ++++++++++++------- adapter/internal/discovery/xds/server.go | 23 ++++++++++-- .../envoyconf/routes_with_clusters.go | 20 +++++++++-- .../gateway-components/log-conf.yaml | 3 ++ helm-charts/values.yaml.template | 2 ++ 7 files changed, 70 insertions(+), 21 deletions(-) diff --git a/adapter/config/default_config.go b/adapter/config/default_config.go index df213701c..c16823a2c 100644 --- a/adapter/config/default_config.go +++ b/adapter/config/default_config.go @@ -139,6 +139,7 @@ var defaultConfig = &Config{ CaCertFilePath: "/home/wso2/security/truststore/ratelimiter.crt", SSLCertSANHostname: "", }, + EnableIntelligentRouting: false, }, Enforcer: enforcer{ Management: management{ diff --git a/adapter/config/types.go b/adapter/config/types.go index 5741dab3e..b4a4f77ee 100644 --- a/adapter/config/types.go +++ b/adapter/config/types.go @@ -115,9 +115,10 @@ type envoy struct { Connection connection PayloadPassingToEnforcer payloadPassingToEnforcer // If configured true, router appends the immediate downstream ip address to the x-forward-for header - UseRemoteAddress bool - Filters filters - RateLimit rateLimit + UseRemoteAddress bool + Filters filters + RateLimit rateLimit + EnableIntelligentRouting bool } type connectionTimeouts struct { diff --git a/adapter/internal/discovery/xds/semantic_versioning.go b/adapter/internal/discovery/xds/semantic_versioning.go index 373e08ef8..271b08b2c 100644 --- a/adapter/internal/discovery/xds/semantic_versioning.go +++ b/adapter/internal/discovery/xds/semantic_versioning.go @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023, WSO2 LLC. (http://www.wso2.org) All Rights Reserved. + * 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. @@ -67,6 +67,7 @@ func GetMinorVersionRange(semVersion semantic_version.SemVersion) string { } func updateRoutingRulesOnAPIUpdate(organizationID, apiIdentifier, apiName, apiVersion, vHost, envType string) { + apiSemVersion, err := semantic_version.ValidateAndGetVersionComponents(apiVersion, apiName) // If the version validation is not success, we just proceed without intelligent version // Valid version pattern: vx.y.z or vx.y where x, y and z are non-negative integers and v is a prefix @@ -93,6 +94,7 @@ func updateRoutingRulesOnAPIUpdate(organizationID, apiIdentifier, apiName, apiVe if envoyInternalAPI.adapterInternalAPI.GetTitle() == apiName && isVHostMatched(organizationID, apiUUID, vHost, envType) { if (isMajorRangeRegexAvailable && envoyInternalAPI.adapterInternalAPI.GetVersion() == existingMajorRangeLatestSemVersion.Version) || (isMinorRangeRegexAvailable && envoyInternalAPI.adapterInternalAPI.GetVersion() == existingMinorRangeLatestSemVersion.Version) { + for _, route := range envoyInternalAPI.routes { regex := route.GetMatch().GetSafeRegex().GetRegex() regexRewritePattern := route.GetRoute().GetRegexRewrite().GetPattern().GetRegex() @@ -133,6 +135,7 @@ func updateRoutingRulesOnAPIUpdate(organizationID, apiIdentifier, apiName, apiVe if _, apiRangeExists := orgIDLatestAPIVersionMap[organizationID][apiRangeIdentifier]; !apiRangeExists { orgIDLatestAPIVersionMap[organizationID][apiRangeIdentifier] = make(map[string]semantic_version.SemVersion) } + latestVersions := orgIDLatestAPIVersionMap[organizationID][apiRangeIdentifier] latestVersions[minorVersionRange] = *apiSemVersion if isLatestMajorVersion { @@ -141,12 +144,13 @@ func updateRoutingRulesOnAPIUpdate(organizationID, apiIdentifier, apiName, apiVe // Add the major and/or minor version range matching regexes to the path specifier when // latest major and/or minor version is available + apiRoutes := getRoutesForAPIIdentifier(organizationID, apiIdentifier) - apiRoutes := getRoutes(organizationID, apiIdentifier, vHost) for _, route := range apiRoutes { regex := route.GetMatch().GetSafeRegex().GetRegex() regexRewritePattern := route.GetRoute().GetRegexRewrite().GetPattern().GetRegex() apiVersionRegex := GetVersionMatchRegex(apiVersion) + if isLatestMajorVersion { regex = strings.Replace(regex, apiVersionRegex, GetMajorMinorVersionRangeRegex(*apiSemVersion), 1) regexRewritePattern = strings.Replace(regexRewritePattern, apiVersionRegex, GetMajorMinorVersionRangeRegex(*apiSemVersion), 1) @@ -159,12 +163,14 @@ func updateRoutingRulesOnAPIUpdate(organizationID, apiIdentifier, apiName, apiVe Regex: regex, }, } + route.Match.PathSpecifier = pathSpecifier action := &routev3.Route_Route{} action = route.Action.(*routev3.Route_Route) action.Route.RegexRewrite.Pattern.Regex = regexRewritePattern route.Action = action } + } } @@ -189,6 +195,7 @@ func updateRoutingRulesOnAPIDelete(organizationID, apiIdentifier string, api mod } majorVersionRange := GetMajorVersionRange(*deletingAPISemVersion) newLatestMajorRangeAPIIdentifier := "" + if deletingAPIsMajorRangeLatestAPISemVersion, ok := latestAPIVersionMap[majorVersionRange]; ok { if deletingAPIsMajorRangeLatestAPISemVersion.Version == api.GetVersion() { newLatestMajorRangeAPI := &semantic_version.SemVersion{ @@ -248,6 +255,7 @@ func updateRoutingRulesOnAPIDelete(organizationID, apiIdentifier string, api mod Regex: regex, }, } + route.Match.PathSpecifier = pathSpecifier action := &routev3.Route_Route{} action = route.Action.(*routev3.Route_Route) @@ -260,6 +268,7 @@ func updateRoutingRulesOnAPIDelete(organizationID, apiIdentifier string, api mod } } minorVersionRange := GetMinorVersionRange(*deletingAPISemVersion) + if deletingAPIsMinorRangeLatestAPI, ok := latestAPIVersionMap[minorVersionRange]; ok { if deletingAPIsMinorRangeLatestAPI.Version == api.GetVersion() { newLatestMinorRangeAPI := &semantic_version.SemVersion{ @@ -319,6 +328,14 @@ func updateRoutingRulesOnAPIDelete(organizationID, apiIdentifier string, api mod } } } + + if orgAPIMap, apiAvailable := orgIDLatestAPIVersionMap[organizationID][apiRangeIdentifier]; apiAvailable && len(orgAPIMap) == 0 { + delete(orgIDLatestAPIVersionMap[organizationID], apiRangeIdentifier) + if orgMap := orgIDLatestAPIVersionMap[organizationID]; len(orgMap) == 0 { + delete(orgIDLatestAPIVersionMap, organizationID) + } + } + } func isVHostMatched(organizationID, apiUUID, vHost, envType string) bool { @@ -335,21 +352,13 @@ func isVHostMatched(organizationID, apiUUID, vHost, envType string) bool { return false } -func getRoutes(organizationID, apiUUID, vHost string) []*routev3.Route { - - var routes []*routev3.Route - if _, ok := orgAPIMap[organizationID]; ok { - routes = orgAPIMap[organizationID][GenerateIdentifierForAPIWithUUID(apiUUID, vHost)].routes - } - - return routes -} - func getRoutesForAPIIdentifier(organizationID, apiIdentifier string) []*routev3.Route { var routes []*routev3.Route if _, ok := orgAPIMap[organizationID]; ok { - routes = orgAPIMap[organizationID][apiIdentifier].routes + if _, ok := orgAPIMap[organizationID][apiIdentifier]; ok { + routes = orgAPIMap[organizationID][apiIdentifier].routes + } } return routes diff --git a/adapter/internal/discovery/xds/server.go b/adapter/internal/discovery/xds/server.go index e9daff6b2..b32a6ac2c 100644 --- a/adapter/internal/discovery/xds/server.go +++ b/adapter/internal/discovery/xds/server.go @@ -246,11 +246,16 @@ func DeleteAPICREvent(labels []string, apiUUID string, organizationID string) er // deleteAPI deletes an API, its resources and updates the caches of given environments func deleteAPI(apiIdentifier string, environments []string, organizationID string) error { apiUUID, _ := ExtractUUIDFromAPIIdentifier(apiIdentifier) + var api *EnvoyInternalAPI + if _, orgExists := orgAPIMap[organizationID]; orgExists { - if _, apiExists := orgAPIMap[organizationID][apiIdentifier]; !apiExists { + if oldAPI, apiExists := orgAPIMap[organizationID][apiIdentifier]; apiExists { + api = oldAPI + } else { logger.LoggerXds.Infof("Unable to delete API: %v from Organization: %v. API Does not exist. API_UUID: %v", apiIdentifier, organizationID, apiUUID) return errors.New(constants.NotFound) } + } else { logger.LoggerXds.Infof("Unable to delete API: %v from Organization: %v. Organization Does not exist. API_UUID: %v", apiIdentifier, organizationID, apiUUID) return errors.New(constants.NotFound) @@ -259,6 +264,11 @@ func deleteAPI(apiIdentifier string, environments []string, organizationID strin existingLabels := orgAPIMap[organizationID][apiIdentifier].envoyLabels toBeDelEnvs, toBeKeptEnvs := getEnvironmentsToBeDeleted(existingLabels, environments) + conf := config.ReadConfigs() + if conf.Envoy.EnableIntelligentRouting && strings.HasPrefix(api.adapterInternalAPI.GetVersion(), "v") { + updateRoutingRulesOnAPIDelete(organizationID, apiIdentifier, api.adapterInternalAPI) + } + var isAllowedToDelete bool updatedLabelsMap := make(map[string]struct{}) for _, val := range toBeDelEnvs { @@ -697,7 +707,7 @@ func UpdateAPICache(vHosts []string, newLabels []string, listener string, sectio updatedLabelsMap := make(map[string]struct{}, 0) - // Remove internal mappigs for old vHosts + // Remove internal mappings for old vHosts for _, oldvhost := range oldvHosts { apiIdentifier := GenerateIdentifierForAPIWithUUID(oldvhost, adapterInternalAPI.UUID) if orgMap, orgExists := orgAPIMap[adapterInternalAPI.GetOrganizationID()]; orgExists { @@ -748,7 +758,16 @@ func UpdateAPICache(vHosts []string, newLabels []string, listener string, sectio endpointAddresses: endpoints, enforcerAPI: oasParser.GetEnforcerAPI(adapterInternalAPI, vHost), } + + conf := config.ReadConfigs() + apiVersion := adapterInternalAPI.GetVersion() + if conf.Envoy.EnableIntelligentRouting && strings.HasPrefix(apiVersion, "v") { + apiName := adapterInternalAPI.GetTitle() + envType := adapterInternalAPI.EnvType + updateRoutingRulesOnAPIUpdate(adapterInternalAPI.OrganizationID, apiIdentifier, apiName, apiVersion, vHost, envType) + } } + return updatedLabelsMap, nil } diff --git a/adapter/internal/oasparser/envoyconf/routes_with_clusters.go b/adapter/internal/oasparser/envoyconf/routes_with_clusters.go index 379ba81c9..d2f273267 100644 --- a/adapter/internal/oasparser/envoyconf/routes_with_clusters.go +++ b/adapter/internal/oasparser/envoyconf/routes_with_clusters.go @@ -864,7 +864,13 @@ func createRoutes(params *routeCreateParams) (routes []*routev3.Route, err error xWso2Basepath = removeFirstOccurrence(xWso2Basepath, "/"+version) resourcePath = removeFirstOccurrence(resource.GetPath(), "/"+version) } + + conf := config.ReadConfigs() + if conf.Envoy.EnableIntelligentRouting && strings.HasPrefix(version, "v") { + resourcePath = strings.Replace(resourcePath, basePath, regexp.QuoteMeta(basePath), 1) + } routePath := generateRoutePath(resourcePath, pathMatchType) + // route path could be empty only if there is no basePath for API or the endpoint available, // and resourcePath is also an empty string. // Empty check is added to run the gateway in failsafe mode, as if the decorator string is @@ -1205,9 +1211,17 @@ func CreateAPIDefinitionEndpoint(basePath string, vHost string, methods []string matchPath = basePathWithoutVersion + endpoint } + conf := config.ReadConfigs() + if conf.Envoy.EnableIntelligentRouting && strings.HasPrefix(version, "v") { + matchPath = strings.Replace(matchPath, basePath, regexp.QuoteMeta(basePath), 1) + } + routePath := generateRoutePath(matchPath, gwapiv1b1.PathMatchRegularExpression) + match = &routev3.RouteMatch{ - PathSpecifier: &routev3.RouteMatch_Path{ - Path: matchPath, + PathSpecifier: &routev3.RouteMatch_SafeRegex{ + SafeRegex: &envoy_type_matcherv3.RegexMatcher{ + Regex: routePath, + }, }, Headers: generateHTTPMethodMatcher(methodRegex, apiDefinitionClusterName), } @@ -1240,7 +1254,7 @@ func CreateAPIDefinitionEndpoint(basePath string, vHost string, methods []string }, }, ClusterSpecifier: directClusterSpecifier, - PrefixRewrite: rewritePath, + RegexRewrite: generateRegexMatchAndSubstitute(routePath, rewritePath, gwapiv1b1.PathMatchExact), }, } diff --git a/helm-charts/templates/data-plane/gateway-components/log-conf.yaml b/helm-charts/templates/data-plane/gateway-components/log-conf.yaml index eacbe2d7f..7da8ba20d 100644 --- a/helm-charts/templates/data-plane/gateway-components/log-conf.yaml +++ b/helm-charts/templates/data-plane/gateway-components/log-conf.yaml @@ -26,6 +26,9 @@ data: {{ if .Values.wso2.apk.dp.gatewayRuntime.deployment.router.configs.enforcerResponseTimeoutInSeconds }} enforcerResponseTimeoutInSeconds = {{ .Values.wso2.apk.dp.gatewayRuntime.deployment.router.configs.enforcerResponseTimeoutInSeconds }} {{end}} + {{ if .Values.wso2.apk.dp.gatewayRuntime.deployment.router.configs.enableIntelligentRouting }} + enableIntelligentRouting = {{ .Values.wso2.apk.dp.gatewayRuntime.deployment.router.configs.enableIntelligentRouting }} + {{ end }} {{ if .Values.wso2.apk.dp.gatewayRuntime.deployment.router.configs.upstream }} {{ if .Values.wso2.apk.dp.gatewayRuntime.deployment.router.configs.upstream.tls }} diff --git a/helm-charts/values.yaml.template b/helm-charts/values.yaml.template index 0a80f6ee3..0c5d8331f 100644 --- a/helm-charts/values.yaml.template +++ b/helm-charts/values.yaml.template @@ -400,6 +400,8 @@ wso2: useRemoteAddress: false # -- System hostname for system API resources (eg: /testkey and /health) systemHost: "localhost" + # -- Enable Semantic Versioning based Intelligent Routing for Gateway + enableIntelligentRouting: false tls: # -- TLS secret name for router public certificate. secretName: "router-cert" From d0cceaf6143b7b3d5e7652015cb4453e7e9d2dc3 Mon Sep 17 00:00:00 2001 From: Pubudu Gunatilaka Date: Thu, 8 Feb 2024 13:14:29 +0530 Subject: [PATCH 03/14] Enable Semantic versioning based on the API version --- .../discovery/xds/semantic_versioning.go | 13 +++++++++++++ adapter/internal/discovery/xds/server.go | 8 +++----- .../oasparser/envoyconf/routes_with_clusters.go | 16 +++++++++------- 3 files changed, 25 insertions(+), 12 deletions(-) diff --git a/adapter/internal/discovery/xds/semantic_versioning.go b/adapter/internal/discovery/xds/semantic_versioning.go index 271b08b2c..919891460 100644 --- a/adapter/internal/discovery/xds/semantic_versioning.go +++ b/adapter/internal/discovery/xds/semantic_versioning.go @@ -22,6 +22,7 @@ import ( routev3 "github.com/envoyproxy/go-control-plane/envoy/config/route/v3" envoy_type_matcherv3 "github.com/envoyproxy/go-control-plane/envoy/type/matcher/v3" + "github.com/wso2/apk/adapter/config" logger "github.com/wso2/apk/adapter/internal/loggers" logging "github.com/wso2/apk/adapter/internal/logging" "github.com/wso2/apk/adapter/internal/oasparser/model" @@ -363,3 +364,15 @@ func getRoutesForAPIIdentifier(organizationID, apiIdentifier string) []*routev3. return routes } + +func isSemanticVersioningEnabled(apiName, apiVersion string) bool { + + conf := config.ReadConfigs() + apiSemVersion, err := semantic_version.ValidateAndGetVersionComponents(apiVersion, apiName) + + if err != nil && apiSemVersion == nil { + return false + } + + return conf.Envoy.EnableIntelligentRouting +} diff --git a/adapter/internal/discovery/xds/server.go b/adapter/internal/discovery/xds/server.go index b32a6ac2c..43e28b452 100644 --- a/adapter/internal/discovery/xds/server.go +++ b/adapter/internal/discovery/xds/server.go @@ -264,8 +264,7 @@ func deleteAPI(apiIdentifier string, environments []string, organizationID strin existingLabels := orgAPIMap[organizationID][apiIdentifier].envoyLabels toBeDelEnvs, toBeKeptEnvs := getEnvironmentsToBeDeleted(existingLabels, environments) - conf := config.ReadConfigs() - if conf.Envoy.EnableIntelligentRouting && strings.HasPrefix(api.adapterInternalAPI.GetVersion(), "v") { + if isSemanticVersioningEnabled(api.adapterInternalAPI.GetTitle(), api.adapterInternalAPI.GetVersion()) { updateRoutingRulesOnAPIDelete(organizationID, apiIdentifier, api.adapterInternalAPI) } @@ -759,10 +758,9 @@ func UpdateAPICache(vHosts []string, newLabels []string, listener string, sectio enforcerAPI: oasParser.GetEnforcerAPI(adapterInternalAPI, vHost), } - conf := config.ReadConfigs() apiVersion := adapterInternalAPI.GetVersion() - if conf.Envoy.EnableIntelligentRouting && strings.HasPrefix(apiVersion, "v") { - apiName := adapterInternalAPI.GetTitle() + apiName := adapterInternalAPI.GetTitle() + if isSemanticVersioningEnabled(apiName, apiVersion) { envType := adapterInternalAPI.EnvType updateRoutingRulesOnAPIUpdate(adapterInternalAPI.OrganizationID, apiIdentifier, apiName, apiVersion, vHost, envType) } diff --git a/adapter/internal/oasparser/envoyconf/routes_with_clusters.go b/adapter/internal/oasparser/envoyconf/routes_with_clusters.go index d2f273267..629787c5e 100644 --- a/adapter/internal/oasparser/envoyconf/routes_with_clusters.go +++ b/adapter/internal/oasparser/envoyconf/routes_with_clusters.go @@ -44,7 +44,8 @@ import ( upstreams "github.com/envoyproxy/go-control-plane/envoy/extensions/upstreams/http/v3" envoy_type_matcherv3 "github.com/envoyproxy/go-control-plane/envoy/type/matcher/v3" "github.com/envoyproxy/go-control-plane/pkg/wellknown" - + "github.com/golang/protobuf/ptypes/any" + "github.com/golang/protobuf/ptypes/wrappers" "github.com/wso2/apk/adapter/config" "github.com/wso2/apk/adapter/internal/interceptor" logger "github.com/wso2/apk/adapter/internal/loggers" @@ -53,9 +54,6 @@ import ( "github.com/wso2/apk/adapter/internal/oasparser/model" "github.com/wso2/apk/adapter/internal/svcdiscovery" dpv1alpha2 "github.com/wso2/apk/common-go-libs/apis/dp/v1alpha2" - - "github.com/golang/protobuf/ptypes/any" - "github.com/golang/protobuf/ptypes/wrappers" "google.golang.org/protobuf/proto" gwapiv1b1 "sigs.k8s.io/gateway-api/apis/v1beta1" ) @@ -124,10 +122,10 @@ func CreateRoutesWithClusters(adapterInternalAPI *model.AdapterInternalAPI, inte } else { methods = append(methods, "GET") } - routeP := CreateAPIDefinitionEndpoint(adapterInternalAPI.GetXWso2Basepath(), vHost, methods, false, adapterInternalAPI.GetVersion(), adapterInternalAPI.GetAPIDefinitionEndpoint()) + routeP := CreateAPIDefinitionEndpoint(adapterInternalAPI, vHost, methods, false) routes = append(routes, routeP) if (adapterInternalAPI).IsDefaultVersion { - defaultDefRoutes := CreateAPIDefinitionEndpoint(adapterInternalAPI.GetXWso2Basepath(), vHost, methods, true, adapterInternalAPI.GetVersion(), adapterInternalAPI.GetAPIDefinitionEndpoint()) + defaultDefRoutes := CreateAPIDefinitionEndpoint(adapterInternalAPI, vHost, methods, true) routes = append(routes, defaultDefRoutes) } var endpointForAPIDefinitions []model.Endpoint @@ -1192,7 +1190,11 @@ func CreateAPIDefinitionRoute(basePath string, vHost string, methods []string, i } // CreateAPIDefinitionEndpoint generates a route for the api defition endpoint -func CreateAPIDefinitionEndpoint(basePath string, vHost string, methods []string, isDefaultversion bool, version string, providedAPIDefinitionPath string) *routev3.Route { +func CreateAPIDefinitionEndpoint(adapterInternalAPI *model.AdapterInternalAPI, vHost string, methods []string, isDefaultversion bool) *routev3.Route { + + basePath := adapterInternalAPI.GetXWso2Basepath() + version := adapterInternalAPI.GetVersion() + providedAPIDefinitionPath := adapterInternalAPI.GetAPIDefinitionEndpoint() endpoint := providedAPIDefinitionPath rewritePath := basePath + "/" + vHost + "?" + apiDefinitionQueryParam basePath = strings.TrimSuffix(basePath, "/") From 13eac2249ab8d4c2517aa56564ff9c8641db562a Mon Sep 17 00:00:00 2001 From: Pubudu Gunatilaka Date: Sun, 11 Feb 2024 15:34:13 +0530 Subject: [PATCH 04/14] Fix adapter revive failure --- .../discovery/xds/semantic_versioning.go | 7 +++- .../envoyconf/routes_with_clusters.go | 10 ++--- .../envoyconf/routes_with_clusters_test.go | 2 +- .../pkg/semanticversion/semantic_version.go | 41 +++++++++---------- 4 files changed, 28 insertions(+), 32 deletions(-) diff --git a/adapter/internal/discovery/xds/semantic_versioning.go b/adapter/internal/discovery/xds/semantic_versioning.go index 919891460..2d2038bfc 100644 --- a/adapter/internal/discovery/xds/semantic_versioning.go +++ b/adapter/internal/discovery/xds/semantic_versioning.go @@ -368,11 +368,14 @@ func getRoutesForAPIIdentifier(organizationID, apiIdentifier string) []*routev3. func isSemanticVersioningEnabled(apiName, apiVersion string) bool { conf := config.ReadConfigs() - apiSemVersion, err := semantic_version.ValidateAndGetVersionComponents(apiVersion, apiName) + if !conf.Envoy.EnableIntelligentRouting { + return false + } + apiSemVersion, err := semantic_version.ValidateAndGetVersionComponents(apiVersion, apiName) if err != nil && apiSemVersion == nil { return false } - return conf.Envoy.EnableIntelligentRouting + return true } diff --git a/adapter/internal/oasparser/envoyconf/routes_with_clusters.go b/adapter/internal/oasparser/envoyconf/routes_with_clusters.go index 629787c5e..2addbd7a2 100644 --- a/adapter/internal/oasparser/envoyconf/routes_with_clusters.go +++ b/adapter/internal/oasparser/envoyconf/routes_with_clusters.go @@ -863,8 +863,7 @@ func createRoutes(params *routeCreateParams) (routes []*routev3.Route, err error resourcePath = removeFirstOccurrence(resource.GetPath(), "/"+version) } - conf := config.ReadConfigs() - if conf.Envoy.EnableIntelligentRouting && strings.HasPrefix(version, "v") { + if pathMatchType != gwapiv1b1.PathMatchExact { resourcePath = strings.Replace(resourcePath, basePath, regexp.QuoteMeta(basePath), 1) } routePath := generateRoutePath(resourcePath, pathMatchType) @@ -1213,10 +1212,7 @@ func CreateAPIDefinitionEndpoint(adapterInternalAPI *model.AdapterInternalAPI, v matchPath = basePathWithoutVersion + endpoint } - conf := config.ReadConfigs() - if conf.Envoy.EnableIntelligentRouting && strings.HasPrefix(version, "v") { - matchPath = strings.Replace(matchPath, basePath, regexp.QuoteMeta(basePath), 1) - } + matchPath = strings.Replace(matchPath, basePath, regexp.QuoteMeta(basePath), 1) routePath := generateRoutePath(matchPath, gwapiv1b1.PathMatchRegularExpression) match = &routev3.RouteMatch{ @@ -1390,7 +1386,7 @@ func generateRoutePath(resourcePath string, pathMatchType gwapiv1b1.PathMatchTyp case gwapiv1b1.PathMatchPathPrefix: fallthrough default: - return fmt.Sprintf("^%s((?:/.*)*)", regexp.QuoteMeta(newPath)) + return fmt.Sprintf("^%s((?:/.*)*)", newPath) } } diff --git a/adapter/internal/oasparser/envoyconf/routes_with_clusters_test.go b/adapter/internal/oasparser/envoyconf/routes_with_clusters_test.go index a341222ec..03ca8957c 100644 --- a/adapter/internal/oasparser/envoyconf/routes_with_clusters_test.go +++ b/adapter/internal/oasparser/envoyconf/routes_with_clusters_test.go @@ -173,7 +173,7 @@ func TestCreateRoutesWithClustersWithExactAndRegularExpressionRules(t *testing.T assert.Equal(t, 3, len(routes), "Created number of routes are incorrect.") assert.Contains(t, []string{"^/test-api/2\\.0\\.0/exact-path-api/2\\.0\\.0/\\(\\.\\*\\)/exact-path([/]{0,1})"}, routes[1].GetMatch().GetSafeRegex().Regex) - assert.Contains(t, []string{"^/test-api/2.0.0/regex-path/2.0.0/userId/([^/]+)/orderId/([^/]+)([/]{0,1})"}, routes[2].GetMatch().GetSafeRegex().Regex) + assert.Contains(t, []string{"^/test-api/2\\.0\\.0/regex-path/2.0.0/userId/([^/]+)/orderId/([^/]+)([/]{0,1})"}, routes[2].GetMatch().GetSafeRegex().Regex) assert.NotEqual(t, routes[1].GetMatch().GetSafeRegex().Regex, routes[2].GetMatch().GetSafeRegex().Regex, "The route regex for the two paths should not be the same") } diff --git a/adapter/pkg/semanticversion/semantic_version.go b/adapter/pkg/semanticversion/semantic_version.go index d4ece4526..6377834a0 100644 --- a/adapter/pkg/semanticversion/semantic_version.go +++ b/adapter/pkg/semanticversion/semantic_version.go @@ -38,7 +38,7 @@ func ValidateAndGetVersionComponents(version string, apiName string) (*SemVersio versionComponents := strings.Split(version, ".") // If the versionComponents length is less than 2, return error - if len(versionComponents) < 2 { + if len(versionComponents) < 2 || !strings.HasPrefix(versionComponents[0], "v") { logger.LoggerSemanticVersion.Errorf("API version validation failed for API: %v. API Version: %v", apiName, version) errMessage := "Invalid version: " + version + " for API: " + apiName + ". API version should be in the format x.y.z, x.y, vx.y.z or vx.y where x,y,z are non-negative integers" + @@ -85,28 +85,25 @@ func ValidateAndGetVersionComponents(version string, apiName string) (*SemVersio // Compare - compares two semantic versions and returns true // if `version` is greater or equal than `baseVersion` func (baseVersion SemVersion) Compare(version SemVersion) bool { - if baseVersion.Major < version.Major { - return true - } else if baseVersion.Major > version.Major { + // Compare major version + if baseVersion.Major != version.Major { + return baseVersion.Major < version.Major + } + + // Compare minor version + if baseVersion.Minor != version.Minor { + return baseVersion.Minor < version.Minor + } + + // Compare patch version + if baseVersion.Patch != nil && version.Patch != nil { + return *baseVersion.Patch < *version.Patch + } else if baseVersion.Patch != nil { return false - } else { - if baseVersion.Minor < version.Minor { - return true - } else if baseVersion.Minor > version.Minor { - return false - } else { - if baseVersion.Patch != nil && version.Patch != nil { - if *baseVersion.Patch < *version.Patch { - return true - } else if *baseVersion.Patch > *version.Patch { - return false - } - } else if baseVersion.Patch == nil && version.Patch != nil { - return true - } else if baseVersion.Patch != nil && version.Patch == nil { - return false - } - } + } else if version.Patch != nil { + return true } + + // Versions are equal return true } From 0b2114268cb188b89a8a5cec6d62a6df27802d26 Mon Sep 17 00:00:00 2001 From: Pubudu Gunatilaka Date: Mon, 19 Feb 2024 11:10:43 +0530 Subject: [PATCH 05/14] Fix semantic versioning issues --- .github/workflows/integration-test.yml | 3 ++- .../discovery/xds/semantic_versioning.go | 22 ++++++++++--------- adapter/internal/discovery/xds/server.go | 3 +-- helm-charts/README.md | 1 + 4 files changed, 16 insertions(+), 13 deletions(-) diff --git a/.github/workflows/integration-test.yml b/.github/workflows/integration-test.yml index 20052938f..79cea3cda 100644 --- a/.github/workflows/integration-test.yml +++ b/.github/workflows/integration-test.yml @@ -176,7 +176,8 @@ jobs: --set idp.idpds.deployment.image=${{ secrets.AZURE_ACR_NAME }}.azurecr.io/apk-idp-domain-service:${{ github.sha }} \ --set idp.idpui.deployment.image=${{ secrets.AZURE_ACR_NAME }}.azurecr.io/apk-idp-ui:${{ github.sha }} \ --set wso2.apk.dp.ratelimiter.deployment.image=${{ secrets.AZURE_ACR_NAME }}.azurecr.io/apk-ratelimiter:${{ github.sha }} \ - --set wso2.apk.dp.gateway.httpListener.enabled=true + --set wso2.apk.dp.gateway.httpListener.enabled=true \ + --set wso2.apk.dp.gatewayRuntime.deployment.router.configs.enableIntelligentRouting=true kubectl get pods -n apk-integration-test kubectl get svc -n apk-integration-test - name: Run test cases diff --git a/adapter/internal/discovery/xds/semantic_versioning.go b/adapter/internal/discovery/xds/semantic_versioning.go index 2d2038bfc..ca40951d3 100644 --- a/adapter/internal/discovery/xds/semantic_versioning.go +++ b/adapter/internal/discovery/xds/semantic_versioning.go @@ -67,7 +67,7 @@ func GetMinorVersionRange(semVersion semantic_version.SemVersion) string { return "v" + strconv.Itoa(semVersion.Major) + "." + strconv.Itoa(semVersion.Minor) } -func updateRoutingRulesOnAPIUpdate(organizationID, apiIdentifier, apiName, apiVersion, vHost, envType string) { +func updateRoutingRulesOnAPIUpdate(organizationID, apiIdentifier, apiName, apiVersion, vHost string) { apiSemVersion, err := semantic_version.ValidateAndGetVersionComponents(apiVersion, apiName) // If the version validation is not success, we just proceed without intelligent version @@ -90,9 +90,10 @@ func updateRoutingRulesOnAPIUpdate(organizationID, apiIdentifier, apiName, apiVe // Remove the existing regexes from the path specifier when latest major and/or minor version is available if (isMajorRangeRegexAvailable || isMinorRangeRegexAvailable) && (isLatestMajorVersion || isLatestMinorVersion) { // Organization's all apis - for apiUUID, envoyInternalAPI := range orgAPIMap[organizationID] { + for _, envoyInternalAPI := range orgAPIMap[organizationID] { // API's all versions in the same vHost - if envoyInternalAPI.adapterInternalAPI.GetTitle() == apiName && isVHostMatched(organizationID, apiUUID, vHost, envType) { + if envoyInternalAPI.adapterInternalAPI.GetTitle() == apiName && isVHostMatched(organizationID, vHost) { + if (isMajorRangeRegexAvailable && envoyInternalAPI.adapterInternalAPI.GetVersion() == existingMajorRangeLatestSemVersion.Version) || (isMinorRangeRegexAvailable && envoyInternalAPI.adapterInternalAPI.GetVersion() == existingMinorRangeLatestSemVersion.Version) { @@ -221,7 +222,7 @@ func updateRoutingRulesOnAPIDelete(organizationID, apiIdentifier string, api mod } if newLatestMajorRangeAPIIdentifier != "" { orgIDLatestAPIVersionMap[organizationID][apiRangeIdentifier][majorVersionRange] = *newLatestMajorRangeAPI - apiRoutes := getRoutesForAPIIdentifier(organizationID, apiIdentifier) + apiRoutes := getRoutesForAPIIdentifier(organizationID, newLatestMajorRangeAPIIdentifier) for _, route := range apiRoutes { regex := route.GetMatch().GetSafeRegex().GetRegex() regexRewritePattern := route.GetRoute().GetRegexRewrite().GetPattern().GetRegex() @@ -339,14 +340,15 @@ func updateRoutingRulesOnAPIDelete(organizationID, apiIdentifier string, api mod } -func isVHostMatched(organizationID, apiUUID, vHost, envType string) bool { +func isVHostMatched(organizationID, vHost string) bool { - if _, ok := orgIDAPIvHostsMap[organizationID]; ok { - vHosts := orgIDAPIvHostsMap[organizationID][GetvHostsIdentifier(apiUUID, envType)] + if apis, ok := orgIDAPIvHostsMap[organizationID]; ok { - for _, vHostEntry := range vHosts { - if vHostEntry == vHost { - return true + for _, vHosts := range apis { + for _, vHostEntry := range vHosts { + if vHostEntry == vHost { + return true + } } } } diff --git a/adapter/internal/discovery/xds/server.go b/adapter/internal/discovery/xds/server.go index 43e28b452..79bf4333b 100644 --- a/adapter/internal/discovery/xds/server.go +++ b/adapter/internal/discovery/xds/server.go @@ -761,8 +761,7 @@ func UpdateAPICache(vHosts []string, newLabels []string, listener string, sectio apiVersion := adapterInternalAPI.GetVersion() apiName := adapterInternalAPI.GetTitle() if isSemanticVersioningEnabled(apiName, apiVersion) { - envType := adapterInternalAPI.EnvType - updateRoutingRulesOnAPIUpdate(adapterInternalAPI.OrganizationID, apiIdentifier, apiName, apiVersion, vHost, envType) + updateRoutingRulesOnAPIUpdate(adapterInternalAPI.OrganizationID, apiIdentifier, apiName, apiVersion, vHost) } } diff --git a/helm-charts/README.md b/helm-charts/README.md index 48782f80e..f37f76c89 100644 --- a/helm-charts/README.md +++ b/helm-charts/README.md @@ -168,6 +168,7 @@ A Helm chart for APK components | wso2.apk.dp.gatewayRuntime.deployment.router.configs.upstream.tls.disableSslVerification | bool | `false` | Disable SSL verification | | wso2.apk.dp.gatewayRuntime.deployment.router.configs.upstream.dns.dnsRefreshRate | int | `5000` | DNS refresh rate in miliseconds | | wso2.apk.dp.gatewayRuntime.deployment.router.configs.upstream.dns.respectDNSTtl | bool | `false` | set cluster’s DNS refresh rate to resource record’s TTL which comes from DNS resolution | +| wso2.apk.dp.gatewayRuntime.deployment.router.configs.enableIntelligentRouting | bool | `false` | Enable/Disable Semantic Versioning based Intelligent Routing | | wso2.apk.dp.gatewayRuntime.deployment.router.logging.wireLogs | object | `{"enable":true}` | Optionally configure logging for router. | | wso2.apk.dp.gatewayRuntime.deployment.router.logging.wireLogs.enable | bool | `true` | Enable wire logs for router. | | wso2.apk.dp.gatewayRuntime.deployment.router.logging.accessLogs.enable | bool | `true` | Enable access logs for router. | From db8061ebde0255b899248fc493f286abeed014a5 Mon Sep 17 00:00:00 2001 From: Pubudu Gunatilaka Date: Wed, 21 Feb 2024 12:12:41 +0530 Subject: [PATCH 06/14] Address PR comments --- adapter/internal/discovery/xds/semantic_versioning.go | 4 ++-- adapter/internal/discovery/xds/server.go | 2 +- adapter/pkg/semanticversion/semantic_version.go | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/adapter/internal/discovery/xds/semantic_versioning.go b/adapter/internal/discovery/xds/semantic_versioning.go index ca40951d3..6a94e19d2 100644 --- a/adapter/internal/discovery/xds/semantic_versioning.go +++ b/adapter/internal/discovery/xds/semantic_versioning.go @@ -76,7 +76,7 @@ func updateRoutingRulesOnAPIUpdate(organizationID, apiIdentifier, apiName, apiVe return } - apiRangeIdentifier := GenerateIdentifierForAPIWithoutVersion(vHost, apiName) + apiRangeIdentifier := generateIdentifierForAPIWithoutVersion(vHost, apiName) // Check the major and minor version ranges of the current API existingMajorRangeLatestSemVersion, isMajorRangeRegexAvailable := orgIDLatestAPIVersionMap[organizationID][apiRangeIdentifier][GetMajorVersionRange(*apiSemVersion)] @@ -185,7 +185,7 @@ func updateRoutingRulesOnAPIDelete(organizationID, apiIdentifier string, api mod "Error extracting vhost from API identifier: %v for Organization %v. Ignore deploying the API, error: %v", apiIdentifier, organizationID, err)) } - apiRangeIdentifier := GenerateIdentifierForAPIWithoutVersion(vhost, api.GetTitle()) + apiRangeIdentifier := generateIdentifierForAPIWithoutVersion(vhost, api.GetTitle()) latestAPIVersionMap, latestAPIVersionMapExists := orgIDLatestAPIVersionMap[organizationID][apiRangeIdentifier] if !latestAPIVersionMapExists { diff --git a/adapter/internal/discovery/xds/server.go b/adapter/internal/discovery/xds/server.go index 79bf4333b..84e68e2c2 100644 --- a/adapter/internal/discovery/xds/server.go +++ b/adapter/internal/discovery/xds/server.go @@ -626,7 +626,7 @@ func GenerateHashedAPINameVersionIDWithoutVhost(name, version string) string { } // GenerateIdentifierForAPIWithoutVersion generates an identifier unique to the API despite of the version -func GenerateIdentifierForAPIWithoutVersion(vhost, name string) string { +func generateIdentifierForAPIWithoutVersion(vhost, name string) string { return fmt.Sprint(vhost, apiKeyFieldSeparator, name) } diff --git a/adapter/pkg/semanticversion/semantic_version.go b/adapter/pkg/semanticversion/semantic_version.go index 6377834a0..88afaf9a0 100644 --- a/adapter/pkg/semanticversion/semantic_version.go +++ b/adapter/pkg/semanticversion/semantic_version.go @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023, WSO2 LLC. (http://www.wso2.org) All Rights Reserved. + * 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. From f7ad0af1e51cc824a1a9d871b1067dccd64278d5 Mon Sep 17 00:00:00 2001 From: Pubudu Gunatilaka Date: Tue, 27 Feb 2024 14:11:05 +0530 Subject: [PATCH 07/14] Enable Semantic versioning for Cucumber tests --- .github/workflows/integration-test.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/integration-test.yml b/.github/workflows/integration-test.yml index 79cea3cda..dad0b6413 100644 --- a/.github/workflows/integration-test.yml +++ b/.github/workflows/integration-test.yml @@ -284,7 +284,8 @@ jobs: --set wso2.apk.dp.gatewayRuntime.deployment.router.readinessProbe.failureThreshold=10 \ --set idp.idpds.deployment.image=${{ secrets.AZURE_ACR_NAME }}.azurecr.io/apk-idp-domain-service:${{ github.sha }} \ --set idp.idpui.deployment.image=${{ secrets.AZURE_ACR_NAME }}.azurecr.io/apk-idp-ui:${{ github.sha }} \ - --set wso2.apk.dp.ratelimiter.deployment.image=${{ secrets.AZURE_ACR_NAME }}.azurecr.io/apk-ratelimiter:${{ github.sha }} + --set wso2.apk.dp.ratelimiter.deployment.image=${{ secrets.AZURE_ACR_NAME }}.azurecr.io/apk-ratelimiter:${{ github.sha }} \ + --set wso2.apk.dp.gatewayRuntime.deployment.router.configs.enableIntelligentRouting=true kubectl get pods -n apk-integration-test kubectl get svc -n apk-integration-test - name: Run test cases From d110c3fa2030f3c1029083a1476b4fedde065ea1 Mon Sep 17 00:00:00 2001 From: tharindu1st Date: Wed, 28 Feb 2024 13:58:51 +0530 Subject: [PATCH 08/14] archieve log support --- .github/workflows/integration-test.yml | 40 +++++++++++++++++++++++++- 1 file changed, 39 insertions(+), 1 deletion(-) diff --git a/.github/workflows/integration-test.yml b/.github/workflows/integration-test.yml index dad0b6413..10b64ed8b 100644 --- a/.github/workflows/integration-test.yml +++ b/.github/workflows/integration-test.yml @@ -180,6 +180,19 @@ jobs: --set wso2.apk.dp.gatewayRuntime.deployment.router.configs.enableIntelligentRouting=true kubectl get pods -n apk-integration-test kubectl get svc -n apk-integration-test + - name: Archieve Logs + shell: sh + run: | + cd apk-repo + mkdir -p apk-repo/test/integration/podlogs + kubectl logs -n apk-integration-test -l app.kubernetes.io/app=adapter -f > apk-repo/test/integration/podlogs/adapter.log & + kubectl logs -n apk-integration-test -l app.kubernetes.io/app=commoncontroller -f > apk-repo/test/integration/podlogs/common-controller.log & + kubectl logs -n apk-integration-test -l app.kubernetes.io/app=gateway -c enforcer -f > apk-repo/test/integration/podlogs/enforcer.log & + kubectl logs -n apk-integration-test -l app.kubernetes.io/app=gateway -c router -f > apk-repo/test/integration/podlogs/router.log & + kubectl logs -n apk-integration-test -l app.kubernetes.io/app=configdeployer-ds -f> apk-repo/test/integration/podlogs/config-deployer.log & + kubectl logs -n apk-integration-test -l app.kubernetes.io/app=idp-ds -f > apk-repo/test/integration/podlogs/idpds.log & + kubectl logs -n apk-integration-test -l app.kubernetes.io/app=idp-ui -f> apk-repo/test/integration/podlogs/idpui.log & + kubectl logs -n apk-integration-test -l app.kubernetes.io/app=ratelimiter -f> apk-repo/test/integration/podlogs/ratelimiter.log & - name: Run test cases shell: sh run: | @@ -215,7 +228,12 @@ jobs: with: report_paths: 'apk-repo/test/postman-tests/build/*.xml' fail_on_test_failures: true - + - name: Archieve Logs + if: always() + uses: actions/upload-artifact@v2 + with: + name: apk-integration-test-go-logs + path: apk-repo/test/integration/podlogs runs_cucumber_integration_tests_on_pull_request: if: github.event_name == 'pull_request_target' && contains(github.event.label.name, 'trigger-action') needs: [build_adapter, build_common_controller, build_enforcer, build_router, build_config,build_idpds,build_idpui,build_ratelimiter] @@ -288,6 +306,19 @@ jobs: --set wso2.apk.dp.gatewayRuntime.deployment.router.configs.enableIntelligentRouting=true kubectl get pods -n apk-integration-test kubectl get svc -n apk-integration-test + - name: Archieve Logs + shell: sh + run: | + cd apk-repo + mkdir -p apk-repo/test/integration/podlogs + kubectl logs -n apk-integration-test -l app.kubernetes.io/app=adapter -f > apk-repo/test/integration/podlogs/adapter.log & + kubectl logs -n apk-integration-test -l app.kubernetes.io/app=commoncontroller -f > apk-repo/test/integration/podlogs/common-controller.log & + kubectl logs -n apk-integration-test -l app.kubernetes.io/app=gateway -c enforcer -f > apk-repo/test/integration/podlogs/enforcer.log & + kubectl logs -n apk-integration-test -l app.kubernetes.io/app=gateway -c router -f > apk-repo/test/integration/podlogs/router.log & + kubectl logs -n apk-integration-test -l app.kubernetes.io/app=configdeployer-ds -f> apk-repo/test/integration/podlogs/config-deployer.log & + kubectl logs -n apk-integration-test -l app.kubernetes.io/app=idp-ds -f > apk-repo/test/integration/podlogs/idpds.log & + kubectl logs -n apk-integration-test -l app.kubernetes.io/app=idp-ui -f> apk-repo/test/integration/podlogs/idpui.log & + kubectl logs -n apk-integration-test -l app.kubernetes.io/app=ratelimiter -f> apk-repo/test/integration/podlogs/ratelimiter.log & - name: Run test cases shell: sh run: | @@ -323,3 +354,10 @@ jobs: with: report_paths: 'apk-repo/test/cucumber-tests/build/test-output/junitreports/*.xml' fail_on_test_failures: true + - name: Archieve Logs + if: always() + uses: actions/upload-artifact@v2 + with: + name: apk-integration-test-cucmber-logs + path: apk-repo/test/integration/podlogs + \ No newline at end of file From 55c6b24b9e4e59a3c6ca06cca46f4f26c085408a Mon Sep 17 00:00:00 2001 From: Pubudu Gunatilaka Date: Mon, 19 Feb 2024 15:26:17 +0530 Subject: [PATCH 09/14] Add Semantic versioning related integration tests --- test/cucumber-tests/CRs/artifacts.yaml | 76 +++++++++++ .../semantic-versioning/sem_api_v1-0.yaml | 28 +++++ .../semantic-versioning/sem_api_v1-1.yaml | 28 +++++ .../semantic-versioning/sem_api_v1-5.yaml | 28 +++++ .../semantic-versioning/sem_api_v2-1.yaml | 27 ++++ .../tests/api/SemanticVersioning.feature | 118 ++++++++++++++++++ 6 files changed, 305 insertions(+) create mode 100644 test/cucumber-tests/src/test/resources/artifacts/apk-confs/semantic-versioning/sem_api_v1-0.yaml create mode 100644 test/cucumber-tests/src/test/resources/artifacts/apk-confs/semantic-versioning/sem_api_v1-1.yaml create mode 100644 test/cucumber-tests/src/test/resources/artifacts/apk-confs/semantic-versioning/sem_api_v1-5.yaml create mode 100644 test/cucumber-tests/src/test/resources/artifacts/apk-confs/semantic-versioning/sem_api_v2-1.yaml create mode 100644 test/cucumber-tests/src/test/resources/tests/api/SemanticVersioning.feature diff --git a/test/cucumber-tests/CRs/artifacts.yaml b/test/cucumber-tests/CRs/artifacts.yaml index a9ae0b918..2da9444f3 100644 --- a/test/cucumber-tests/CRs/artifacts.yaml +++ b/test/cucumber-tests/CRs/artifacts.yaml @@ -491,6 +491,61 @@ data: "body": "{\n \"keys\":[\n {\n \"kty\":\"RSA\",\n \"n\":\"m0YNpM5MVYToWZMZ9wL4KQOygvG0f6y0dw4wZ02T4C3SxiC1zEBCZLh2clj7bncyA3EV2bFrTIBNeq-1pFEfbNDMZB88Jcg0S9QyYujr6GM0AqLA7WjZQ6lLxLpeQdEQroEZI-c8rnGmzU8Qb25aiPbRf6Vh7vFYGQz5FnZ8E0LcEMYQ-4KPMkAqnMon1UKWDkqszTY5a-DGMAi5w7imKzXaU4qiEKVKIcezv9nLUVC5Od0T4FkUQi462ZA9SoHx1HNhcVAj8Nf9TG_C65GbsMMFJVcRXwZR99cVzVxVqEtxGlK7Qr0woYKQ3S5kHZPRFcMFXI6WHhEQXqyOMBdUfQ\",\n \"e\":\"AQAB\",\n \"alg\":\"RS256\",\n \"kid\":\"123-456\",\n \"use\":\"sig\"\n }\n ]\n}" } } + sem-versioning.json: | + {"mappings": [ + { + "request": { + "method": "GET", + "url": "/sem-api/v1.0/employee" + }, + "response": { + "status": 200, + "headers": { + "Content-Type": "application/json" + }, + "body": "{\n \"version\":\"v1.0\" \n}" + } + }, + { + "request": { + "method": "GET", + "url": "/sem-api/v1.1/employee" + }, + "response": { + "status": 200, + "headers": { + "Content-Type": "application/json" + }, + "body": "{\n \"version\":\"v1.1\" \n}" + } + }, + { + "request": { + "method": "GET", + "url": "/sem-api/v1.5/employee" + }, + "response": { + "status": 200, + "headers": { + "Content-Type": "application/json" + }, + "body": "{\n \"version\":\"v1.5\" \n}" + } + }, + { + "request": { + "method": "GET", + "url": "/sem-api/v2.1/employee" + }, + "response": { + "status": 200, + "headers": { + "Content-Type": "application/json" + }, + "body": "{\n \"version\":\"v2.1\" \n}" + } + } + ]} --- kind: TokenIssuer apiVersion: dp.wso2.com/v1alpha1 @@ -818,3 +873,24 @@ data: y5Oi4A4+id+xO0XnHIkkqCfPtFzxl3hwytcy8EqISynzzHWNJ8bFZIYX4tgX+PLq u0/ITEw= -----END CERTIFICATE----- +--- +apiVersion: cp.wso2.com/v1alpha2 +kind: Subscription +metadata: + name: semantic-versioning-subscription + namespace: apk-integration-test +spec: + organization: "default" + subscriptionStatus: "ACTIVE" + api: + name: "Semantic Versioning API" + version: "v\\d+(\\.\\d+)?" +--- +apiVersion: cp.wso2.com/v1alpha2 +kind: ApplicationMapping +metadata: + name: semantic-versioning-app-mapping + namespace: apk-integration-test +spec: + applicationRef: 583e4146-7ef5-11ee-b962-0242ac120003 + subscriptionRef: semantic-versioning-subscription diff --git a/test/cucumber-tests/src/test/resources/artifacts/apk-confs/semantic-versioning/sem_api_v1-0.yaml b/test/cucumber-tests/src/test/resources/artifacts/apk-confs/semantic-versioning/sem_api_v1-0.yaml new file mode 100644 index 000000000..2933aae06 --- /dev/null +++ b/test/cucumber-tests/src/test/resources/artifacts/apk-confs/semantic-versioning/sem_api_v1-0.yaml @@ -0,0 +1,28 @@ +--- +name: "Semantic Versioning API" +basePath: "/sem-api" +version: "v1.0" +id: "sem-api-v1-0" +type: "REST" +defaultVersion: false +subscriptionValidation: true +endpointConfigurations: + production: + endpoint: "http://dynamic-backend-service:8080/sem-api/v1.0" +operations: + - target: "/employee" + verb: "GET" + secured: true + scopes: [] + - target: "/employee" + verb: "POST" + secured: true + scopes: [] + - target: "/employee/{employeeId}" + verb: "PUT" + secured: true + scopes: [] + - target: "/employee/{employeeId}" + verb: "DELETE" + secured: true + scopes: [] diff --git a/test/cucumber-tests/src/test/resources/artifacts/apk-confs/semantic-versioning/sem_api_v1-1.yaml b/test/cucumber-tests/src/test/resources/artifacts/apk-confs/semantic-versioning/sem_api_v1-1.yaml new file mode 100644 index 000000000..79a65908b --- /dev/null +++ b/test/cucumber-tests/src/test/resources/artifacts/apk-confs/semantic-versioning/sem_api_v1-1.yaml @@ -0,0 +1,28 @@ +--- +name: "Semantic Versioning API" +basePath: "/sem-api" +version: "v1.1" +id: "sem-api-v1-1" +type: "REST" +defaultVersion: false +subscriptionValidation: true +endpointConfigurations: + production: + endpoint: "http://dynamic-backend-service:8080/sem-api/v1.1" +operations: + - target: "/employee" + verb: "GET" + secured: true + scopes: [] + - target: "/employee" + verb: "POST" + secured: true + scopes: [] + - target: "/employee/{employeeId}" + verb: "PUT" + secured: true + scopes: [] + - target: "/employee/{employeeId}" + verb: "DELETE" + secured: true + scopes: [] diff --git a/test/cucumber-tests/src/test/resources/artifacts/apk-confs/semantic-versioning/sem_api_v1-5.yaml b/test/cucumber-tests/src/test/resources/artifacts/apk-confs/semantic-versioning/sem_api_v1-5.yaml new file mode 100644 index 000000000..ba9650c17 --- /dev/null +++ b/test/cucumber-tests/src/test/resources/artifacts/apk-confs/semantic-versioning/sem_api_v1-5.yaml @@ -0,0 +1,28 @@ +--- +name: "Semantic Versioning API" +basePath: "/sem-api" +version: "v1.5" +id: "sem-api-v1-5" +type: "REST" +defaultVersion: false +subscriptionValidation: true +endpointConfigurations: + production: + endpoint: "http://dynamic-backend-service:8080/sem-api/v1.5" +operations: + - target: "/employee" + verb: "GET" + secured: true + scopes: [] + - target: "/employee" + verb: "POST" + secured: true + scopes: [] + - target: "/employee/{employeeId}" + verb: "PUT" + secured: true + scopes: [] + - target: "/employee/{employeeId}" + verb: "DELETE" + secured: true + scopes: [] diff --git a/test/cucumber-tests/src/test/resources/artifacts/apk-confs/semantic-versioning/sem_api_v2-1.yaml b/test/cucumber-tests/src/test/resources/artifacts/apk-confs/semantic-versioning/sem_api_v2-1.yaml new file mode 100644 index 000000000..f02706214 --- /dev/null +++ b/test/cucumber-tests/src/test/resources/artifacts/apk-confs/semantic-versioning/sem_api_v2-1.yaml @@ -0,0 +1,27 @@ +--- +name: "Semantic Versioning API" +basePath: "/sem-api" +version: "v2.1" +id: "sem-api-v2-1" +type: "REST" +defaultVersion: false +endpointConfigurations: + production: + endpoint: "http://dynamic-backend-service:8080/sem-api/v2.1" +operations: + - target: "/employee" + verb: "GET" + secured: true + scopes: [] + - target: "/employee" + verb: "POST" + secured: true + scopes: [] + - target: "/employee/{employeeId}" + verb: "PUT" + secured: true + scopes: [] + - target: "/employee/{employeeId}" + verb: "DELETE" + secured: true + scopes: [] diff --git a/test/cucumber-tests/src/test/resources/tests/api/SemanticVersioning.feature b/test/cucumber-tests/src/test/resources/tests/api/SemanticVersioning.feature new file mode 100644 index 000000000..e7555f763 --- /dev/null +++ b/test/cucumber-tests/src/test/resources/tests/api/SemanticVersioning.feature @@ -0,0 +1,118 @@ +Feature: Semantic Versioning Based Intelligent Routing + + Scenario: API version with Major and Minor + Given The system is ready + And I have a valid subscription + When I use the APK Conf file "artifacts/apk-confs/semantic-versioning/sem_api_v1-0.yaml" + And the definition file "artifacts/definitions/employees_api.json" + And make the API deployment request + Then the response status code should be 200 + Then I generate JWT token from idp1 with kid "123-456" and consumer_key "45f1c5c8-a92e-11ed-afa1-0242ac120005" + Then I set headers + |Authorization|bearer ${idp-1-45f1c5c8-a92e-11ed-afa1-0242ac120005-token}| + + And I send "GET" request to "https://default.gw.wso2.com:9095/sem-api/v1.0/employee/" with body "" + Then the response status code should be 200 + And the response body should contain "\"version\":\"v1.0\"" + + And I send "GET" request to "https://default.gw.wso2.com:9095/sem-api/v1/employee/" with body "" + Then the response status code should be 200 + And the response body should contain "\"version\":\"v1.0\"" + + When I use the APK Conf file "artifacts/apk-confs/semantic-versioning/sem_api_v1-1.yaml" + And the definition file "artifacts/definitions/employees_api.json" + And make the API deployment request + Then the response status code should be 200 + And I send "GET" request to "https://default.gw.wso2.com:9095/sem-api/v1.1/employee/" with body "" + Then the response status code should be 200 + And the response body should contain "\"version\":\"v1.1\"" + And I send "GET" request to "https://default.gw.wso2.com:9095/sem-api/v1/employee/" with body "" + Then the response status code should be 200 + And the response body should contain "\"version\":\"v1.1\"" + + When I use the APK Conf file "artifacts/apk-confs/semantic-versioning/sem_api_v1-5.yaml" + And the definition file "artifacts/definitions/employees_api.json" + And make the API deployment request + Then the response status code should be 200 + And I send "GET" request to "https://default.gw.wso2.com:9095/sem-api/v1.5/employee/" with body "" + Then the response status code should be 200 + And the response body should contain "\"version\":\"v1.5\"" + And I send "GET" request to "https://default.gw.wso2.com:9095/sem-api/v1/employee/" with body "" + Then the response status code should be 200 + And the response body should contain "\"version\":\"v1.5\"" + + When I undeploy the API whose ID is "sem-api-v1-5" + Then the response status code should be 202 + And I wait for 1 seconds + And I send "GET" request to "https://default.gw.wso2.com:9095/sem-api/v1/employee/" with body "" + And the response body should contain "\"version\":\"v1.1\"" + + When I undeploy the API whose ID is "sem-api-v1-1" + Then the response status code should be 202 + And I wait for 1 seconds + And I send "GET" request to "https://default.gw.wso2.com:9095/sem-api/v1/employee/" with body "" + And the response body should contain "\"version\":\"v1.0\"" + + When I undeploy the API whose ID is "sem-api-v1-0" + Then the response status code should be 202 + + Scenario: Multiple Major and minor versions for an API + Given The system is ready + And I have a valid subscription + When I use the APK Conf file "artifacts/apk-confs/semantic-versioning/sem_api_v1-0.yaml" + And the definition file "artifacts/definitions/employees_api.json" + And make the API deployment request + Then the response status code should be 200 + When I use the APK Conf file "artifacts/apk-confs/semantic-versioning/sem_api_v1-1.yaml" + And the definition file "artifacts/definitions/employees_api.json" + And make the API deployment request + Then the response status code should be 200 + When I use the APK Conf file "artifacts/apk-confs/semantic-versioning/sem_api_v1-5.yaml" + And the definition file "artifacts/definitions/employees_api.json" + And make the API deployment request + Then the response status code should be 200 + Then I generate JWT token from idp1 with kid "123-456" and consumer_key "45f1c5c8-a92e-11ed-afa1-0242ac120005" + Then I set headers + |Authorization|bearer ${idp-1-45f1c5c8-a92e-11ed-afa1-0242ac120005-token}| + And I send "GET" request to "https://default.gw.wso2.com:9095/sem-api/v1/employee/" with body "" + Then the response status code should be 200 + And the response body should contain "\"version\":\"v1.5\"" + + When I undeploy the API whose ID is "sem-api-v1-1" + Then the response status code should be 202 + And I wait for 1 seconds + And I send "GET" request to "https://default.gw.wso2.com:9095/sem-api/v1/employee/" with body "" + Then the response status code should be 200 + And the response body should contain "\"version\":\"v1.5\"" + + When I use the APK Conf file "artifacts/apk-confs/semantic-versioning/sem_api_v2-1.yaml" + And the definition file "artifacts/definitions/employees_api.json" + And make the API deployment request + Then the response status code should be 200 + + And I send "GET" request to "https://default.gw.wso2.com:9095/sem-api/v2/employee/" with body "" + Then the response status code should be 200 + And the response body should contain "\"version\":\"v2.1\"" + And I send "GET" request to "https://default.gw.wso2.com:9095/sem-api/v2.1/employee/" with body "" + Then the response status code should be 200 + And the response body should contain "\"version\":\"v2.1\"" + + When I undeploy the API whose ID is "sem-api-v1-0" + Then the response status code should be 202 + And I wait for 1 seconds + And I send "GET" request to "https://default.gw.wso2.com:9095/sem-api/v1/employee/" with body "" + Then the response status code should be 200 + And the response body should contain "\"version\":\"v1.5\"" + + When I undeploy the API whose ID is "sem-api-v1-5" + Then the response status code should be 202 + And I wait for 1 seconds + And I send "GET" request to "https://default.gw.wso2.com:9095/sem-api/v1/employee/" with body "" + Then the response status code should be 404 + + And I send "GET" request to "https://default.gw.wso2.com:9095/sem-api/v2/employee/" with body "" + Then the response status code should be 200 + And the response body should contain "\"version\":\"v2.1\"" + + When I undeploy the API whose ID is "sem-api-v2-1" + Then the response status code should be 202 From 796b93df7d9a823dab1bfba98d89ca0e9e9dd9bd Mon Sep 17 00:00:00 2001 From: Pubudu Gunatilaka Date: Wed, 21 Feb 2024 12:06:06 +0530 Subject: [PATCH 10/14] Adding unit tests for the semantic versioning feature --- .../discovery/xds/semantic_versioning_test.go | 328 ++++++++++++++++++ .../pkg/semanticversion/semantic_version.go | 2 +- .../semanticversion/semantic_version_test.go | 186 ++++++++++ 3 files changed, 515 insertions(+), 1 deletion(-) create mode 100644 adapter/internal/discovery/xds/semantic_versioning_test.go create mode 100644 adapter/pkg/semanticversion/semantic_version_test.go diff --git a/adapter/internal/discovery/xds/semantic_versioning_test.go b/adapter/internal/discovery/xds/semantic_versioning_test.go new file mode 100644 index 000000000..942f13348 --- /dev/null +++ b/adapter/internal/discovery/xds/semantic_versioning_test.go @@ -0,0 +1,328 @@ +/* + * 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 xds + +import ( + "regexp" + "testing" + + "github.com/wso2/apk/adapter/config" + semantic_version "github.com/wso2/apk/adapter/pkg/semanticversion" +) + +func TestGetVersionMatchRegex(t *testing.T) { + tests := []struct { + name string + version string + expectedResult string + }{ + { + name: "Version with single digit components", + version: "1.2.3", + expectedResult: "1\\.2\\.3", + }, + { + name: "Version with multi-digit components", + version: "123.456.789", + expectedResult: "123\\.456\\.789", + }, + { + name: "Version with alpha components", + version: "v1.0-alpha", + expectedResult: "v1\\.0-alpha", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := GetVersionMatchRegex(tt.version) + + if result != tt.expectedResult { + t.Errorf("Expected regex: %s, Got: %s", tt.expectedResult, result) + } + + // Test if the regex works correctly + match, err := regexp.MatchString(result, tt.version) + if err != nil { + t.Errorf("Error when matching regex: %v", err) + } + if !match { + t.Errorf("Regex failed to match the version: %s %s", tt.version, result) + } + }) + } +} + +func TestGetMajorMinorVersionRangeRegex(t *testing.T) { + tests := []struct { + name string + semVersion semantic_version.SemVersion + expectedResult string + }{ + { + name: "Major and minor version only", + semVersion: semantic_version.SemVersion{Major: 1, Minor: 2}, + expectedResult: "v1(?:\\.2)?", + }, + { + name: "Major, minor, and patch version", + semVersion: semantic_version.SemVersion{Major: 1, Minor: 2, Patch: PtrInt(3)}, + expectedResult: "v1(?:\\.2(?:\\.3)?)?", + }, + { + name: "Major version only", + semVersion: semantic_version.SemVersion{Major: 1}, + expectedResult: "v1(?:\\.0)?", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := GetMajorMinorVersionRangeRegex(tt.semVersion) + + if result != tt.expectedResult { + t.Errorf("Expected regex: %s, Got: %s", tt.expectedResult, result) + } + }) + } +} + +func TestGetMinorVersionRangeRegex(t *testing.T) { + tests := []struct { + name string + semVersion semantic_version.SemVersion + expectedResult string + }{ + { + name: "Major, minor, and patch version", + semVersion: semantic_version.SemVersion{Version: "v1.2.3", Major: 1, Minor: 2, Patch: PtrInt(3)}, + expectedResult: "v1\\.2(?:\\.3)?", + }, + { + name: "Major and minor version only", + semVersion: semantic_version.SemVersion{Version: "v1.2", Major: 1, Minor: 2}, + expectedResult: "v1\\.2", + }, + { + name: "Major version only", + semVersion: semantic_version.SemVersion{Version: "v1", Major: 1}, + expectedResult: "v1", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := GetMinorVersionRangeRegex(tt.semVersion) + + if result != tt.expectedResult { + t.Errorf("Expected regex: %s, Got: %s", tt.expectedResult, result) + } + }) + } +} + +func TestGetMajorVersionRange(t *testing.T) { + tests := []struct { + name string + semVersion semantic_version.SemVersion + expectedResult string + }{ + { + name: "Major and minor version 1.2.3", + semVersion: semantic_version.SemVersion{Version: "v1.2.3", Major: 1, Minor: 2, Patch: PtrInt(3)}, + expectedResult: "v1", + }, + { + name: "Major version 2", + semVersion: semantic_version.SemVersion{Major: 2}, + expectedResult: "v2", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := GetMajorVersionRange(tt.semVersion) + + if result != tt.expectedResult { + t.Errorf("Expected result: %s, Got: %s", tt.expectedResult, result) + } + }) + } +} + +func TestGetMinorVersionRange(t *testing.T) { + tests := []struct { + name string + semVersion semantic_version.SemVersion + expectedResult string + }{ + { + name: "Major and minor version 1.2", + semVersion: semantic_version.SemVersion{Major: 1, Minor: 2}, + expectedResult: "v1.2", + }, + { + name: "Major and minor version 1.2.3", + semVersion: semantic_version.SemVersion{Version: "v1.2.3", Major: 1, Minor: 2, Patch: PtrInt(3)}, + expectedResult: "v1.2", + }, + { + name: "Major only", + semVersion: semantic_version.SemVersion{Major: 10}, + expectedResult: "v10.0", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := GetMinorVersionRange(tt.semVersion) + + if result != tt.expectedResult { + t.Errorf("Expected result: %s, Got: %s", tt.expectedResult, result) + } + }) + } +} + +func TestIsSemanticVersioningEnabled(t *testing.T) { + + conf := config.ReadConfigs() + + tests := []struct { + name string + apiName string + apiVersion string + intelligentRoutingEnabled bool + expectedResult bool + }{ + { + name: "Semantic versioning enabled and valid version provided", + apiName: "TestAPI", + apiVersion: "v1.2.3", + intelligentRoutingEnabled: true, + expectedResult: true, + }, + { + name: "Semantic versioning enabled and valid version provided", + apiName: "TestAPI", + apiVersion: "v1.2", + intelligentRoutingEnabled: true, + expectedResult: true, + }, + { + name: "Semantic versioning enabled and version only contains major version", + apiName: "TestAPI", + apiVersion: "v1", + intelligentRoutingEnabled: true, + expectedResult: false, + }, + { + name: "Semantic versioning enabled and invalid version provided", + apiName: "TestAPI", + apiVersion: "1.2.3", + intelligentRoutingEnabled: true, + expectedResult: false, + }, + { + name: "Semantic versioning disabled and valid version provided", + apiName: "TestAPI", + apiVersion: "v1.2.3", + intelligentRoutingEnabled: false, + expectedResult: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + + conf.Envoy.EnableIntelligentRouting = tt.intelligentRoutingEnabled + result := isSemanticVersioningEnabled(tt.apiName, tt.apiVersion) + + if result != tt.expectedResult { + t.Errorf("Expected result: %v, Got: %v", tt.expectedResult, result) + } + }) + } +} + +func TestIsVHostMatched(t *testing.T) { + // Mock orgIDAPIvHostsMap for testing + orgIDAPIvHostsMap = map[string]map[string][]string{ + "org1": { + "api1": {"example.com", "api.example.com"}, + "api2": {"test.com"}, + }, + "org2": { + "api3": {"example.org"}, + "api4": {"test.org"}, + }, + } + + tests := []struct { + name string + organizationID string + vHost string + expectedResult bool + }{ + { + name: "Matching vHost in org1", + organizationID: "org1", + vHost: "example.com", + expectedResult: true, + }, + { + name: "Matching vHost in org2", + organizationID: "org2", + vHost: "example.org", + expectedResult: true, + }, + { + name: "Non-matching vHost in org1", + organizationID: "org1", + vHost: "nonexistent.com", + expectedResult: false, + }, + { + name: "Non-matching vHost in org2", + organizationID: "org2", + vHost: "nonexistent.org", + expectedResult: false, + }, + { + name: "VHost not found for organization", + organizationID: "org3", + vHost: "example.com", + expectedResult: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := isVHostMatched(tt.organizationID, tt.vHost) + + if result != tt.expectedResult { + t.Errorf("Expected result: %v, Got: %v", tt.expectedResult, result) + } + }) + } +} + +// PtrInt returns a pointer to an integer value +func PtrInt(i int) *int { + return &i +} diff --git a/adapter/pkg/semanticversion/semantic_version.go b/adapter/pkg/semanticversion/semantic_version.go index 88afaf9a0..3750e5940 100644 --- a/adapter/pkg/semanticversion/semantic_version.go +++ b/adapter/pkg/semanticversion/semantic_version.go @@ -97,7 +97,7 @@ func (baseVersion SemVersion) Compare(version SemVersion) bool { // Compare patch version if baseVersion.Patch != nil && version.Patch != nil { - return *baseVersion.Patch < *version.Patch + return *baseVersion.Patch <= *version.Patch } else if baseVersion.Patch != nil { return false } else if version.Patch != nil { diff --git a/adapter/pkg/semanticversion/semantic_version_test.go b/adapter/pkg/semanticversion/semantic_version_test.go new file mode 100644 index 000000000..53a7e1bf2 --- /dev/null +++ b/adapter/pkg/semanticversion/semantic_version_test.go @@ -0,0 +1,186 @@ +/* + * 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 semanticversion + +import ( + "errors" + "testing" +) + +func TestSemVersionCompare(t *testing.T) { + tests := []struct { + name string + baseVersion SemVersion + compareVersion SemVersion + expected bool + }{ + { + name: "Same versions", + baseVersion: SemVersion{Major: 1, Minor: 2, Patch: PtrInt(3)}, + compareVersion: SemVersion{Major: 1, Minor: 2, Patch: PtrInt(3)}, + expected: true, + }, + { + name: "Base version major is greater", + baseVersion: SemVersion{Major: 2, Minor: 1, Patch: PtrInt(3)}, + compareVersion: SemVersion{Major: 1, Minor: 2, Patch: PtrInt(3)}, + expected: false, + }, + { + name: "Base version minor is greater", + baseVersion: SemVersion{Major: 1, Minor: 3, Patch: PtrInt(3)}, + compareVersion: SemVersion{Major: 1, Minor: 4, Patch: PtrInt(3)}, + expected: true, + }, + { + name: "Base version patch is greater", + baseVersion: SemVersion{Major: 1, Minor: 2, Patch: PtrInt(4)}, + compareVersion: SemVersion{Major: 1, Minor: 2, Patch: PtrInt(3)}, + expected: false, + }, + { + name: "Compare version major is greater", + baseVersion: SemVersion{Major: 1, Minor: 2, Patch: PtrInt(3)}, + compareVersion: SemVersion{Major: 2, Minor: 2, Patch: PtrInt(3)}, + expected: true, + }, + { + name: "Compare version minor is greater", + baseVersion: SemVersion{Major: 1, Minor: 2, Patch: PtrInt(3)}, + compareVersion: SemVersion{Major: 1, Minor: 3, Patch: PtrInt(3)}, + expected: true, + }, + { + name: "Compare version patch is greater", + baseVersion: SemVersion{Major: 1, Minor: 2, Patch: PtrInt(3)}, + compareVersion: SemVersion{Major: 1, Minor: 2, Patch: PtrInt(4)}, + expected: true, + }, + { + name: "Base version patch is nil", + baseVersion: SemVersion{Major: 1, Minor: 2}, + compareVersion: SemVersion{Major: 1, Minor: 2, Patch: PtrInt(4)}, + expected: true, + }, + { + name: "Compare version patch is nil", + baseVersion: SemVersion{Major: 1, Minor: 2, Patch: PtrInt(3)}, + compareVersion: SemVersion{Major: 1, Minor: 2}, + expected: false, + }, + { + name: "Both patch versions are nil", + baseVersion: SemVersion{Major: 1, Minor: 2}, + compareVersion: SemVersion{Major: 1, Minor: 2}, + expected: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := tt.baseVersion.Compare(tt.compareVersion) + if result != tt.expected { + t.Errorf("Expected %v, but got %v", tt.expected, result) + } + }) + } +} + +func TestValidateAndGetVersionComponents(t *testing.T) { + tests := []struct { + name string + version string + apiName string + expectedResult *SemVersion + expectedError error + }{ + { + name: "Valid version format", + version: "v1.2.3", + apiName: "TestAPI", + expectedResult: &SemVersion{Version: "v1.2.3", Major: 1, Minor: 2, Patch: PtrInt(3)}, + expectedError: nil, + }, + { + name: "Valid version format without patch", + version: "v1.2", + apiName: "TestAPI", + expectedResult: &SemVersion{Version: "v1.2", Major: 1, Minor: 2, Patch: nil}, + expectedError: nil, + }, + { + name: "Invalid version format - missing 'v' prefix", + version: "1.2.3", + apiName: "TestAPI", + expectedResult: nil, + expectedError: errors.New("Invalid version: 1.2.3 for API: TestAPI. API version should be in the format x.y.z, x.y, vx.y.z or vx.y where x,y,z are non-negative integers and v is version prefix"), + }, + { + name: "Invalid version format - negative major version", + version: "v-1.2.3", + apiName: "TestAPI", + expectedResult: nil, + expectedError: errors.New("invalid version format"), + }, + { + name: "Invalid version format - negative minor version", + version: "v1.-2.3", + apiName: "TestAPI", + expectedResult: nil, + expectedError: errors.New("invalid version format"), + }, + { + name: "Invalid version format - patch version not an integer", + version: "v1.2.three", + apiName: "TestAPI", + expectedResult: nil, + expectedError: errors.New("invalid version format"), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result, err := ValidateAndGetVersionComponents(tt.version, tt.apiName) + + // Check for errors + if (err != nil && tt.expectedError == nil) || (err == nil && tt.expectedError != nil) || (err != nil && tt.expectedError != nil && err.Error() != tt.expectedError.Error()) { + t.Errorf("Unexpected error. Expected: %v, Got: %v", tt.expectedError, err) + } + + // Check for nil results + if result == nil && tt.expectedResult != nil { + t.Errorf("Unexpected nil result") + } else if result != nil && tt.expectedResult == nil { + t.Errorf("Unexpected non-nil result") + } + + // Check for result equality + if result != nil && tt.expectedResult != nil { + if result.Version != tt.expectedResult.Version || result.Major != tt.expectedResult.Major || result.Minor != tt.expectedResult.Minor || (result.Patch != nil && (*result.Patch != *tt.expectedResult.Patch)) { + t.Errorf("Unexpected result. Expected: %v, Got: %v", tt.expectedResult, result) + } + } + }) + } + +} + +// PtrInt returns a pointer to an integer value +func PtrInt(i int) *int { + return &i +} From 39ac4e7ac5c71adf43b8ff0ba71d07cfbd64014b Mon Sep 17 00:00:00 2001 From: Pubudu Gunatilaka Date: Mon, 26 Feb 2024 17:09:12 +0530 Subject: [PATCH 11/14] Add unit tests for update and delete methods in sem versioning --- .../discovery/xds/semantic_versioning_test.go | 356 ++++++++++++++++++ 1 file changed, 356 insertions(+) diff --git a/adapter/internal/discovery/xds/semantic_versioning_test.go b/adapter/internal/discovery/xds/semantic_versioning_test.go index 942f13348..d08d27718 100644 --- a/adapter/internal/discovery/xds/semantic_versioning_test.go +++ b/adapter/internal/discovery/xds/semantic_versioning_test.go @@ -18,10 +18,14 @@ package xds import ( + "reflect" "regexp" "testing" + routev3 "github.com/envoyproxy/go-control-plane/envoy/config/route/v3" + envoy_type_matcherv3 "github.com/envoyproxy/go-control-plane/envoy/type/matcher/v3" "github.com/wso2/apk/adapter/config" + "github.com/wso2/apk/adapter/internal/oasparser/model" semantic_version "github.com/wso2/apk/adapter/pkg/semanticversion" ) @@ -322,6 +326,358 @@ func TestIsVHostMatched(t *testing.T) { } } +func TestGetRoutesForAPIIdentifier(t *testing.T) { + + orgAPIMap = map[string]map[string]*EnvoyInternalAPI{ + "org1": { + "gw.com:apiID1": &EnvoyInternalAPI{ + routes: []*routev3.Route{ + { + Name: "route1", + }, + { + Name: "route2", + }, + }, + }, + "gw.com:apiID2": &EnvoyInternalAPI{ + routes: []*routev3.Route{ + { + Name: "route3", + }, + }, + }, + }, + "org2": { + "test.gw.com:apiID1": &EnvoyInternalAPI{ + routes: []*routev3.Route{ + { + Name: "route4", + }, + }, + }, + }, + } + + tests := []struct { + name string + organizationID string + apiIdentifier string + expectedRoutes []*routev3.Route + expectedNumRoute int + }{ + { + name: "Existing organization and API identifier", + organizationID: "org1", + apiIdentifier: "gw.com:apiID1", + expectedRoutes: []*routev3.Route{ + { + Name: "route1", + }, + { + Name: "route2", + }, + }, + expectedNumRoute: 2, + }, + { + name: "Non-existing organization", + organizationID: "org3", + apiIdentifier: "dev.gw.com:apiID1", + expectedRoutes: []*routev3.Route{}, + expectedNumRoute: 0, + }, + { + name: "Non-existing API identifier", + organizationID: "org1", + apiIdentifier: "api3", + expectedRoutes: []*routev3.Route{}, + expectedNumRoute: 0, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := getRoutesForAPIIdentifier(tt.organizationID, tt.apiIdentifier) + + if len(result) != tt.expectedNumRoute { + t.Errorf("Expected number of routes: %d, Got: %d", tt.expectedNumRoute, len(result)) + } + + if len(result) > 0 { + if !reflect.DeepEqual(result, tt.expectedRoutes) { + t.Errorf("Expected routes: %v, Got: %v", tt.expectedRoutes, result) + } + } + }) + } +} + +func TestUpdateRoutingRulesOnAPIUpdate(t *testing.T) { + + var apiID1 model.AdapterInternalAPI + apiID1.SetName("Test API") + apiID1.SetVersion("v1.0") + apiID1ResourcePath := "^/test-api/v1\\.0/orders([/]{0,1})" + + var apiID2 model.AdapterInternalAPI + apiID2.SetName("Mock API") + apiID2.SetVersion("v1.1") + apiID2ResourcePath := "^/mock-api/v1\\.1/orders([/]{0,1})" + + var apiID3 model.AdapterInternalAPI + apiID3.SetName("Test API") + apiID3.SetVersion("v1.1") + apiID3ResourcePath := "^/test-api/v1\\.1/orders([/]{0,1})" + + orgAPIMap = map[string]map[string]*EnvoyInternalAPI{ + "org1": { + "gw.com:apiID1": &EnvoyInternalAPI{ + adapterInternalAPI: apiID1, + routes: generateRoutes(apiID1ResourcePath), + }, + "gw.com:apiID2": &EnvoyInternalAPI{ + adapterInternalAPI: apiID2, + routes: generateRoutes(apiID2ResourcePath), + }, + "gw.com:apiID3": &EnvoyInternalAPI{ + adapterInternalAPI: apiID3, + routes: generateRoutes(apiID3ResourcePath), + }, + }, + } + + orgIDAPIvHostsMap = map[string]map[string][]string{ + "org1": { + "api1": {"gw.com", "api.example.com"}, + "api2": {"test.com"}, + }, + } + + tests := []struct { + name string + organizationID string + apiIdentifier string + apiName string + apiVersion string + vHost string + expectedRegex string + expectedRewrite string + finalRegex string + finalRewrite string + }{ + { + name: "Create an API with major version", + organizationID: "org1", + apiIdentifier: "gw.com:apiID1", + apiName: "Test API", + apiVersion: "v1.0", + vHost: "gw.com", + expectedRegex: "^/test-api/v1(?:\\.0)?/orders([/]{0,1})", + expectedRewrite: "^/test-api/v1(?:\\.0)?/orders([/]{0,1})", + finalRegex: apiID1ResourcePath, + finalRewrite: apiID1ResourcePath, + }, + { + name: "Create an API with major and minor version", + organizationID: "org1", + apiIdentifier: "gw.com:apiID2", + apiName: "Mock API", + apiVersion: "v1.1", + vHost: "gw.com", + expectedRegex: "^/mock-api/v1(?:\\.1)?/orders([/]{0,1})", + expectedRewrite: "^/mock-api/v1(?:\\.1)?/orders([/]{0,1})", + finalRegex: "^/mock-api/v1(?:\\.1)?/orders([/]{0,1})", + finalRewrite: "^/mock-api/v1(?:\\.1)?/orders([/]{0,1})", + }, + { + name: "Create an API with major and minor version", + organizationID: "org1", + apiIdentifier: "gw.com:apiID3", + apiName: "Test API", + apiVersion: "v1.1", + vHost: "gw.com", + expectedRegex: "^/test-api/v1(?:\\.1)?/orders([/]{0,1})", + expectedRewrite: "^/test-api/v1(?:\\.1)?/orders([/]{0,1})", + finalRegex: "^/test-api/v1(?:\\.1)?/orders([/]{0,1})", + finalRewrite: "^/test-api/v1(?:\\.1)?/orders([/]{0,1})", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + + updateRoutingRulesOnAPIUpdate(tt.organizationID, tt.apiIdentifier, tt.apiName, tt.apiVersion, tt.vHost) + api1 := orgAPIMap[tt.organizationID][tt.apiIdentifier] + routes := api1.routes + + if routes[0].GetMatch().GetSafeRegex().GetRegex() != tt.expectedRegex { + t.Errorf("Expected regex: %s, Got: %s", tt.expectedRegex, routes[0].GetMatch().GetSafeRegex().GetRegex()) + } + if routes[0].GetRoute().GetRegexRewrite().GetPattern().GetRegex() != tt.expectedRewrite { + t.Errorf("Expected rewrite pattern: %s, Got: %s", tt.expectedRewrite, routes[0].GetRoute().GetRegexRewrite().GetPattern().GetRegex()) + } + }) + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + + api1 := orgAPIMap[tt.organizationID][tt.apiIdentifier] + routes := api1.routes + + if routes[0].GetMatch().GetSafeRegex().GetRegex() != tt.finalRegex { + t.Errorf("Expected final regex: %s, Got: %s", tt.finalRegex, routes[0].GetMatch().GetSafeRegex().GetRegex()) + } + if routes[0].GetRoute().GetRegexRewrite().GetPattern().GetRegex() != tt.finalRewrite { + t.Errorf("Expected final rewrite pattern: %s, Got: %s", tt.finalRewrite, routes[0].GetRoute().GetRegexRewrite().GetPattern().GetRegex()) + } + }) + } +} + +func generateRoutes(resourcePath string) []*routev3.Route { + + var routes []*routev3.Route + match := &routev3.RouteMatch{ + PathSpecifier: &routev3.RouteMatch_SafeRegex{ + SafeRegex: &envoy_type_matcherv3.RegexMatcher{ + Regex: resourcePath, + }, + }, + } + + action := &routev3.Route_Route{ + Route: &routev3.RouteAction{ + RegexRewrite: &envoy_type_matcherv3.RegexMatchAndSubstitute{ + Pattern: &envoy_type_matcherv3.RegexMatcher{ + Regex: resourcePath, + }, + Substitution: "/bar", + }, + }, + } + + route := routev3.Route{ + Name: "example-route", + Match: match, + Action: action, + Metadata: nil, + Decorator: nil, + } + + return append(routes, &route) +} + +func TestUpdateRoutingRulesOnAPIDelete(t *testing.T) { + + orgIDLatestAPIVersionMap = map[string]map[string]map[string]semantic_version.SemVersion{ + "org3": { + "gw.com:Test API": { + "v1": { + Version: "v1.0", + Major: 1, + Minor: 0, + Patch: nil, + }, + }, + }, + "org4": { + "gw.com:Mock API": { + "v1.0": { + Version: "v1.0", + Major: 1, + Minor: 0, + Patch: nil, + }, + "v1.5": { + Version: "v1.5", + Major: 1, + Minor: 5, + Patch: nil, + }, + "v1": { + Version: "v1.5", + Major: 1, + Minor: 5, + Patch: nil, + }, + }, + }, + } + + var apiID1 model.AdapterInternalAPI + apiID1.SetName("Test API") + apiID1.SetVersion("v1.0") + apiID1ResourcePath := "^/test-api/v1\\.0/orders([/]{0,1})" + + var apiID2 model.AdapterInternalAPI + apiID2.SetName("Mock API") + apiID2.SetVersion("v1.0") + apiID2ResourcePath := "^/mock-api/v1\\.0/orders([/]{0,1})" + + var apiID3 model.AdapterInternalAPI + apiID3.SetName("Mock API") + apiID3.SetVersion("v1.5") + apiID3ResourcePath := "^/mock-api/v1(?:\\.5)?/orders([/]{0,1})" + + orgAPIMap = map[string]map[string]*EnvoyInternalAPI{ + "org3": { + "gw.com:apiID1": &EnvoyInternalAPI{ + adapterInternalAPI: apiID1, + routes: generateRoutes(apiID1ResourcePath), + }, + }, + "org4": { + "gw.com:apiID2": &EnvoyInternalAPI{ + adapterInternalAPI: apiID2, + routes: generateRoutes(apiID2ResourcePath), + }, + "gw.com:apiID3": &EnvoyInternalAPI{ + adapterInternalAPI: apiID3, + routes: generateRoutes(apiID3ResourcePath), + }, + }, + } + + tests := []struct { + name string + organizationID string + apiIdentifier string + api model.AdapterInternalAPI + deleteVersion string + }{ + { + name: "Delete latest major version", + organizationID: "org3", + apiIdentifier: "gw.com:apiID1", + api: apiID1, + deleteVersion: "v1.0", + }, + { + name: "Delete latest minor version", + organizationID: "org4", + apiIdentifier: "gw.com:apiID3", + api: apiID3, + deleteVersion: "v1.5", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + updateRoutingRulesOnAPIDelete(tt.organizationID, tt.apiIdentifier, tt.api) + + if _, ok := orgIDLatestAPIVersionMap[tt.organizationID]; ok { + if _, ok := orgIDLatestAPIVersionMap[tt.organizationID][tt.apiIdentifier]; ok { + if _, ok := orgIDLatestAPIVersionMap[tt.organizationID][tt.apiIdentifier][tt.deleteVersion]; ok { + t.Errorf("API deletion is not successful: %s", tt.deleteVersion) + } + } + } + }) + } +} + // PtrInt returns a pointer to an integer value func PtrInt(i int) *int { return &i From ea9675b052bce7f41942caa2c7f1be6f81efd103 Mon Sep 17 00:00:00 2001 From: Pubudu Gunatilaka Date: Tue, 27 Feb 2024 11:17:39 +0530 Subject: [PATCH 12/14] Increase timeout for api deployment --- .../resources/tests/api/SemanticVersioning.feature | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/test/cucumber-tests/src/test/resources/tests/api/SemanticVersioning.feature b/test/cucumber-tests/src/test/resources/tests/api/SemanticVersioning.feature index e7555f763..6aa4ca1d2 100644 --- a/test/cucumber-tests/src/test/resources/tests/api/SemanticVersioning.feature +++ b/test/cucumber-tests/src/test/resources/tests/api/SemanticVersioning.feature @@ -43,13 +43,13 @@ Feature: Semantic Versioning Based Intelligent Routing When I undeploy the API whose ID is "sem-api-v1-5" Then the response status code should be 202 - And I wait for 1 seconds + And I wait for 2 seconds And I send "GET" request to "https://default.gw.wso2.com:9095/sem-api/v1/employee/" with body "" And the response body should contain "\"version\":\"v1.1\"" When I undeploy the API whose ID is "sem-api-v1-1" Then the response status code should be 202 - And I wait for 1 seconds + And I wait for 2 seconds And I send "GET" request to "https://default.gw.wso2.com:9095/sem-api/v1/employee/" with body "" And the response body should contain "\"version\":\"v1.0\"" @@ -80,7 +80,7 @@ Feature: Semantic Versioning Based Intelligent Routing When I undeploy the API whose ID is "sem-api-v1-1" Then the response status code should be 202 - And I wait for 1 seconds + And I wait for 2 seconds And I send "GET" request to "https://default.gw.wso2.com:9095/sem-api/v1/employee/" with body "" Then the response status code should be 200 And the response body should contain "\"version\":\"v1.5\"" @@ -99,14 +99,14 @@ Feature: Semantic Versioning Based Intelligent Routing When I undeploy the API whose ID is "sem-api-v1-0" Then the response status code should be 202 - And I wait for 1 seconds + And I wait for 2 seconds And I send "GET" request to "https://default.gw.wso2.com:9095/sem-api/v1/employee/" with body "" Then the response status code should be 200 And the response body should contain "\"version\":\"v1.5\"" When I undeploy the API whose ID is "sem-api-v1-5" Then the response status code should be 202 - And I wait for 1 seconds + And I wait for 2 seconds And I send "GET" request to "https://default.gw.wso2.com:9095/sem-api/v1/employee/" with body "" Then the response status code should be 404 From 749308893380063399f956b102efae376c74c91e Mon Sep 17 00:00:00 2001 From: tharindu1st Date: Wed, 28 Feb 2024 18:12:00 +0530 Subject: [PATCH 13/14] fix action --- .github/workflows/integration-test.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/integration-test.yml b/.github/workflows/integration-test.yml index 10b64ed8b..84875cdbf 100644 --- a/.github/workflows/integration-test.yml +++ b/.github/workflows/integration-test.yml @@ -233,7 +233,7 @@ jobs: uses: actions/upload-artifact@v2 with: name: apk-integration-test-go-logs - path: apk-repo/test/integration/podlogs + path: 'apk-repo/test/integration/podlogs/*.log' runs_cucumber_integration_tests_on_pull_request: if: github.event_name == 'pull_request_target' && contains(github.event.label.name, 'trigger-action') needs: [build_adapter, build_common_controller, build_enforcer, build_router, build_config,build_idpds,build_idpui,build_ratelimiter] @@ -359,5 +359,5 @@ jobs: uses: actions/upload-artifact@v2 with: name: apk-integration-test-cucmber-logs - path: apk-repo/test/integration/podlogs + path: 'apk-repo/test/integration/podlogs/*.log' \ No newline at end of file From 07525ad04d8cdb5e91c0d7f43c26fd46c3fd4eba Mon Sep 17 00:00:00 2001 From: Tharsanan1 Date: Wed, 28 Feb 2024 11:11:50 +0530 Subject: [PATCH 14/14] Fix NPE --- .../apk/enforcer/security/jwt/JWTAuthenticator.java | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/gateway/enforcer/org.wso2.apk.enforcer/src/main/java/org/wso2/apk/enforcer/security/jwt/JWTAuthenticator.java b/gateway/enforcer/org.wso2.apk.enforcer/src/main/java/org/wso2/apk/enforcer/security/jwt/JWTAuthenticator.java index 6e3c52c35..fc99a686c 100644 --- a/gateway/enforcer/org.wso2.apk.enforcer/src/main/java/org/wso2/apk/enforcer/security/jwt/JWTAuthenticator.java +++ b/gateway/enforcer/org.wso2.apk.enforcer/src/main/java/org/wso2/apk/enforcer/security/jwt/JWTAuthenticator.java @@ -49,6 +49,7 @@ import org.wso2.apk.enforcer.security.jwt.validator.RevokedJWTDataHolder; import org.wso2.apk.enforcer.subscription.SubscriptionDataHolder; import org.wso2.apk.enforcer.server.RevokedTokenRedisClient; +import org.wso2.apk.enforcer.subscription.SubscriptionDataStore; import org.wso2.apk.enforcer.tracing.TracingConstants; import org.wso2.apk.enforcer.tracing.TracingSpan; import org.wso2.apk.enforcer.tracing.TracingTracer; @@ -472,8 +473,14 @@ private JWTValidationInfo getJwtValidationInfo(String jwtToken, String organizat try { // Get issuer String issuer = jwtClaimsSet.getIssuer(); - JWTValidator jwtValidator = SubscriptionDataHolder.getInstance().getSubscriptionDataStore(organization) - .getJWTValidatorByIssuer(issuer, environment); + SubscriptionDataStore subscriptionDataStore = SubscriptionDataHolder.getInstance() + .getSubscriptionDataStore(organization); + if (subscriptionDataStore == null) { + throw new APISecurityException(APIConstants.StatusCodes.UNAUTHENTICATED.getCode(), + APISecurityConstants.API_AUTH_INVALID_CREDENTIALS, + APISecurityConstants.API_AUTH_INVALID_CREDENTIALS_MESSAGE); + } + JWTValidator jwtValidator = subscriptionDataStore.getJWTValidatorByIssuer(issuer, environment); // If no validator found for the issuer, we are not caching the token. if (jwtValidator == null) { throw new APISecurityException(APIConstants.StatusCodes.UNAUTHENTICATED.getCode(),