diff --git a/adapter/internal/api/apis_impl.go b/adapter/internal/api/apis_impl.go index 3e80657a67..e81611f8ea 100644 --- a/adapter/internal/api/apis_impl.go +++ b/adapter/internal/api/apis_impl.go @@ -260,7 +260,7 @@ func ApplyAPIProjectFromAPIM( if err != nil { return nil, err } - apiYaml := apiProject.APIYaml.Data + apiYaml := &apiProject.APIYaml.Data if apiEnvProps, found := apiEnvs[apiProject.APIYaml.Data.ID]; found { loggers.LoggerAPI.Infof("Environment specific values found for the API %v ", apiProject.APIYaml.Data.ID) apiProject.APIEnvProps = apiEnvProps @@ -298,6 +298,16 @@ func ApplyAPIProjectFromAPIM( allEnvironments := xds.GetAllEnvironments(apiYaml.ID, vhost, environments) loggers.LoggerAPI.Debugf("Update all environments (%v) of API %v %v:%v with UUID \"%v\".", allEnvironments, vhost, apiYaml.Name, apiYaml.Version, apiYaml.ID) + // We don't need to be environment specific when checking default version. It's applied at API level + // hence picking 0th index here. + if api, ok := xds.APIListMap[allEnvironments[0]][apiYaml.ID]; ok { + apiYaml.IsDefaultVersion = api.IsDefaultVersion + } else { + // APIListMap is synchronously updated only for default version changes. In other API deployment + // events, this may not be updated. We can safely ignore this case since runtime artifact's + // `isDefaultVersion` prop is anyway updated for deployment events. + loggers.LoggerAPI.Debugf("API %s is not found in API Metadata map.", apiYaml.ID) + } // first update the API for vhost deployedRevision, err := xds.UpdateAPI(vhost, apiProject, allEnvironments) if err != nil { diff --git a/adapter/internal/discovery/xds/server.go b/adapter/internal/discovery/xds/server.go index b364a87b14..9777485241 100644 --- a/adapter/internal/discovery/xds/server.go +++ b/adapter/internal/discovery/xds/server.go @@ -482,6 +482,18 @@ func GetAllEnvironments(apiUUID, vhost string, newEnvironments []string) []strin return allEnvironments } +// GetDeployedEnvironments returns all the environments the API with `apiUUID` is deployed to +func GetDeployedEnvironments(apiUUID string) []string { + var envs []string + if envMap, ok := apiUUIDToGatewayToVhosts[apiUUID]; ok { + envs = make([]string, 0, len(envMap)) + for k := range envMap { + envs = append(envs, k) + } + } + return envs +} + // GetVhostOfAPI returns the vhost of API deployed in the given gateway environment func GetVhostOfAPI(apiUUID, environment string) (vhost string, exists bool) { if envToVhost, ok := apiUUIDToGatewayToVhosts[apiUUID]; ok { diff --git a/adapter/internal/eventhub/subscription.go b/adapter/internal/eventhub/subscription.go index 16dc345362..4f241db1ac 100644 --- a/adapter/internal/eventhub/subscription.go +++ b/adapter/internal/eventhub/subscription.go @@ -205,6 +205,15 @@ func LoadSubscriptionData(configFile *config.Config, initialAPIUUIDListMap map[s go retrieveAPIListFromChannel(APIListChannel, nil) } +// UpdateAPIMetadataFromCP Invokes `ApisEndpoint` and updates APIList synchronously. +func UpdateAPIMetadataFromCP(params map[string]string) { + var apiList *types.APIList + var responseChannel = make(chan response) + go InvokeService(ApisEndpoint, apiList, params, responseChannel, 0) + response := <-responseChannel + retrieveAPIList(response, nil) +} + // InvokeService invokes the internal data resource func InvokeService(endpoint string, responseType interface{}, queryParamMap map[string]string, c chan response, retryAttempt int) { diff --git a/adapter/internal/messaging/notification_listener.go b/adapter/internal/messaging/notification_listener.go index 0311dec1cd..c999460e14 100644 --- a/adapter/internal/messaging/notification_listener.go +++ b/adapter/internal/messaging/notification_listener.go @@ -56,6 +56,7 @@ const ( policyUpdate = "POLICY_UPDATE" policyDelete = "POLICY_DELETE" blockedStatus = "BLOCKED" + apiUpdate = "API_UPDATE" ) // var variables @@ -131,11 +132,34 @@ func processNotificationEvent(conf *config.Config, notification *msg.EventNotifi return nil } +// handleDefaultVersionUpdate will redeploy default versioned API. +// API runtime artifact doesn't get updated in CP side when default version is updated +// (isDefaultVersion prop in apiYaml is not updated). API deployment or should happen +// for it to get updated. However we need to redeploy the API when there is a default +// version change. For that we call `/apis` endpoint to get updated API metadata (this +// contains the updated `isDefaultVersion` field). Now we proceed with fetching runtime +// artifact from the CP. When creating CC deployment objects we refer to updated `APIList` +// map and update runtime artifact's `isDefaultVersion` field to correctly deploy default +// versioned API. +func handleDefaultVersionUpdate(event msg.APIEvent) { + deployedEnvs := xds.GetDeployedEnvironments(event.UUID) + for _, env := range deployedEnvs { + query := make(map[string]string, 3) + query[eh.GatewayLabelParam] = env + query[eh.ContextParam] = event.APIContext + query[eh.VersionParam] = event.APIVersion + eh.UpdateAPIMetadataFromCP(query) + } + + synchronizer.FetchAPIsFromControlPlane(event.UUID, deployedEnvs) +} + // handleAPIEvents to process api related data func handleAPIEvents(data []byte, eventType string) { var ( - apiEvent msg.APIEvent - currentTimeStamp int64 = apiEvent.Event.TimeStamp + apiEvent msg.APIEvent + currentTimeStamp int64 = apiEvent.Event.TimeStamp + isDefaultVersionEvent bool ) apiEventErr := json.Unmarshal([]byte(string(data)), &apiEvent) @@ -162,6 +186,13 @@ func handleAPIEvents(data []byte, eventType string) { return } + isDefaultVersionEvent = isDefaultVersionUpdate(apiEvent) + + if isDefaultVersionEvent { + handleDefaultVersionUpdate(apiEvent) + return + } + // Per each revision, synchronization should happen. if strings.EqualFold(deployAPIToGateway, apiEvent.Event.Type) { go synchronizer.FetchAPIsFromControlPlane(apiEvent.UUID, apiEvent.GatewayLabels) @@ -169,7 +200,7 @@ func handleAPIEvents(data []byte, eventType string) { for _, env := range apiEvent.GatewayLabels { if isLaterEvent(apiListTimeStampMap, apiEvent.UUID+":"+env, currentTimeStamp) { - return + break } // removeFromGateway event with multiple labels could only appear when the API is subjected // to delete. Hence we could simply delete after checking against just one iteration. @@ -181,7 +212,7 @@ func handleAPIEvents(data []byte, eventType string) { xds.UpdateEnforcerAPIList(env, xdsAPIList) } } - return + break } if strings.EqualFold(deployAPIToGateway, apiEvent.Event.Type) { conf, _ := config.ReadConfigs() @@ -195,13 +226,13 @@ func handleAPIEvents(data []byte, eventType string) { logger.LoggerInternalMsg.Debugf("API Metadata for api Id: %s is not updated as it already exists", apiEvent.UUID) continue } + logger.LoggerInternalMsg.Debugf("Fetching Metadata for api Id: %s ", apiEvent.UUID) queryParamMap := make(map[string]string, 3) queryParamMap[eh.GatewayLabelParam] = configuredEnv queryParamMap[eh.ContextParam] = apiEvent.Context queryParamMap[eh.VersionParam] = apiEvent.Version var apiList *types.APIList - go eh.InvokeService(eh.ApisEndpoint, apiList, queryParamMap, - eh.APIListChannel, 0) + go eh.InvokeService(eh.ApisEndpoint, apiList, queryParamMap, eh.APIListChannel, 0) } } } @@ -423,6 +454,10 @@ func isLaterEvent(timeStampMap map[string]int64, mapKey string, currentTimeStamp return false } +func isDefaultVersionUpdate(event msg.APIEvent) bool { + return strings.EqualFold(apiUpdate, event.Event.Type) && strings.EqualFold("DEFAULT_VERSION", event.Action) +} + func belongsToTenant(tenantDomain string) bool { // TODO : enable this once the events are fixed in apim // return config.GetControlPlaneConnectedTenantDomain() == tenantDomain diff --git a/adapter/internal/oasparser/envoyconf/envoyconf_internal_test.go b/adapter/internal/oasparser/envoyconf/envoyconf_internal_test.go index ec56208f31..52ef5b3575 100644 --- a/adapter/internal/oasparser/envoyconf/envoyconf_internal_test.go +++ b/adapter/internal/oasparser/envoyconf/envoyconf_internal_test.go @@ -18,6 +18,7 @@ package envoyconf import ( + "fmt" "io/ioutil" "regexp" "strings" @@ -46,19 +47,16 @@ func TestGenerateRoutePaths(t *testing.T) { basePath := "/basePath" resourcePath := "/resource" - completeRoutePath := generateRoutePaths(xWso2BasePath, basePath, resourcePath) + filteredBasePath := getFilteredBasePath(xWso2BasePath, basePath) + completeRoutePath := generateRoutePath(filteredBasePath, resourcePath) // TODO: (VirajSalaka) check if it is possible to perform an equals operation instead of prefix if !strings.HasPrefix(completeRoutePath, "^/xWso2BasePath/resource") { t.Error("The generated path should contain xWso2BasePath as a prefix if xWso2Basepath is available.") } - xWso2BasePath = "/xWso2BasePath/" - if !strings.HasPrefix(completeRoutePath, "^/xWso2BasePath/resource") { - t.Error("The generated path should not contain the trailing '\\' of xWso2BasePath property within the generated route path.") - } - xWso2BasePath = "" - completeRoutePath = generateRoutePaths(xWso2BasePath, basePath, resourcePath) + filteredBasePath = getFilteredBasePath(xWso2BasePath, basePath) + completeRoutePath = generateRoutePath(filteredBasePath, resourcePath) if !strings.HasPrefix(completeRoutePath, "^/basePath/resource") { t.Error("The generated path should contain basePath as a prefix if xWso2Basepath is unavailable.") } @@ -136,7 +134,7 @@ func TestCreateRoute(t *testing.T) { } generatedRouteWithXWso2BasePath := createRoute(generateRouteCreateParamsForUnitTests(title, apiType, vHost, xWso2BasePath, version, - endpoint.Basepath, resourceWithGet.GetPath(), resourceWithGet.GetMethodList(), clusterName, "", nil)) + endpoint.Basepath, resourceWithGet.GetPath(), resourceWithGet.GetMethodList(), clusterName, "", nil, false)) assert.NotNil(t, generatedRouteWithXWso2BasePath, "Route should not be null.") assert.Equal(t, expectedRouteActionWithXWso2BasePath, generatedRouteWithXWso2BasePath.Action, "Route generation mismatch when xWso2BasePath option is provided.") @@ -145,7 +143,7 @@ func TestCreateRoute(t *testing.T) { "Assigned HTTP Method Regex is incorrect when single method is available.") generatedRouteWithoutXWso2BasePath := createRoute(generateRouteCreateParamsForUnitTests(title, apiType, vHost, "", version, - endpoint.Basepath, resourceWithGetPost.GetPath(), resourceWithGetPost.GetMethodList(), clusterName, "", nil)) + endpoint.Basepath, resourceWithGetPost.GetPath(), resourceWithGetPost.GetMethodList(), clusterName, "", nil, false)) assert.NotNil(t, generatedRouteWithoutXWso2BasePath, "Route should not be null") assert.NotNil(t, generatedRouteWithoutXWso2BasePath.GetMatch().Headers, "Headers property should not be null") assert.Equal(t, "^(GET|POST|OPTIONS)$", generatedRouteWithoutXWso2BasePath.GetMatch().Headers[0].GetStringMatch().GetSafeRegex().Regex, @@ -154,6 +152,13 @@ func TestCreateRoute(t *testing.T) { assert.Equal(t, expectedRouteActionWithoutXWso2BasePath, generatedRouteWithoutXWso2BasePath.Action, "Route generation mismatch when xWso2BasePath option is provided") + context := fmt.Sprintf("%s/%s", xWso2BasePath, version) + generatedRouteWithDefaultVersion := createRoute(generateRouteCreateParamsForUnitTests(title, apiType, vHost, context, version, + endpoint.Basepath, resourceWithGetPost.GetPath(), resourceWithGetPost.GetMethodList(), clusterName, "", nil, true)) + assert.NotNil(t, generatedRouteWithDefaultVersion, "Route should not be null") + assert.True(t, strings.HasPrefix(generatedRouteWithDefaultVersion.GetMatch().GetSafeRegex().Regex, fmt.Sprintf("^(%s|%s)", context, xWso2BasePath)), + "Default version basepath is not generated correctly") + } func TestCreateRouteClusterSpecifier(t *testing.T) { @@ -175,21 +180,21 @@ func TestCreateRouteClusterSpecifier(t *testing.T) { "resource_operation_id", []model.Endpoint{}, []model.Endpoint{}) routeWithProdEp := createRoute(generateRouteCreateParamsForUnitTests(title, apiType, vHost, xWso2BasePath, version, endpointBasePath, - resourceWithGet.GetPath(), resourceWithGet.GetMethodList(), prodClusterName, "", nil)) + resourceWithGet.GetPath(), resourceWithGet.GetMethodList(), prodClusterName, "", nil, false)) assert.NotNil(t, routeWithProdEp, "Route should not be null") assert.NotNil(t, routeWithProdEp.GetRoute().GetClusterHeader(), "Route Cluster Header should not be null.") assert.Empty(t, routeWithProdEp.GetRoute().GetCluster(), "Route Cluster Name should be empty.") assert.Equal(t, clusterHeaderName, routeWithProdEp.GetRoute().GetClusterHeader(), "Route Cluster Name mismatch.") routeWithSandEp := createRoute(generateRouteCreateParamsForUnitTests(title, apiType, vHost, xWso2BasePath, version, endpointBasePath, - resourceWithGet.GetPath(), resourceWithGet.GetMethodList(), "", sandClusterName, nil)) + resourceWithGet.GetPath(), resourceWithGet.GetMethodList(), "", sandClusterName, nil, false)) assert.NotNil(t, routeWithSandEp, "Route should not be null") assert.NotNil(t, routeWithSandEp.GetRoute().GetClusterHeader(), "Route Cluster Header should not be null.") assert.Empty(t, routeWithSandEp.GetRoute().GetCluster(), "Route Cluster Name should be empty.") assert.Equal(t, clusterHeaderName, routeWithSandEp.GetRoute().GetClusterHeader(), "Route Cluster Name mismatch.") routeWithProdSandEp := createRoute(generateRouteCreateParamsForUnitTests(title, apiType, vHost, xWso2BasePath, version, endpointBasePath, - resourceWithGet.GetPath(), resourceWithGet.GetMethodList(), prodClusterName, sandClusterName, nil)) + resourceWithGet.GetPath(), resourceWithGet.GetMethodList(), prodClusterName, sandClusterName, nil, false)) assert.NotNil(t, routeWithProdSandEp, "Route should not be null") assert.NotNil(t, routeWithProdSandEp.GetRoute().GetClusterHeader(), "Route Cluster Header should not be null.") assert.Empty(t, routeWithProdSandEp.GetRoute().GetCluster(), "Route Cluster Name should be empty.") @@ -214,7 +219,7 @@ func TestCreateRouteExtAuthzContext(t *testing.T) { "resource_operation_id", []model.Endpoint{}, []model.Endpoint{}) routeWithProdEp := createRoute(generateRouteCreateParamsForUnitTests(title, apiType, vHost, xWso2BasePath, version, - endpointBasePath, resourceWithGet.GetPath(), resourceWithGet.GetMethodList(), prodClusterName, sandClusterName, nil)) + endpointBasePath, resourceWithGet.GetPath(), resourceWithGet.GetMethodList(), prodClusterName, sandClusterName, nil, false)) assert.NotNil(t, routeWithProdEp, "Route should not be null") assert.NotNil(t, routeWithProdEp.GetTypedPerFilterConfig(), "TypedPerFilter config should not be null") assert.NotNil(t, routeWithProdEp.GetTypedPerFilterConfig()[wellknown.HTTPExternalAuthorization], @@ -487,18 +492,18 @@ func TestGetCorsPolicy(t *testing.T) { // Route without CORS configuration routeWithoutCors := createRoute(generateRouteCreateParamsForUnitTests("test", "HTTP", "localhost", "/test", "1.0.0", "/test", - "/testPath", []string{"GET"}, "test-cluster", "", nil)) + "/testPath", []string{"GET"}, "test-cluster", "", nil, false)) assert.Nil(t, routeWithoutCors.GetRoute().Cors, "Cors Configuration should be null.") // Route with CORS configuration routeWithCors := createRoute(generateRouteCreateParamsForUnitTests("test", "HTTP", "localhost", "/test", "1.0.0", "/test", - "/testPath", []string{"GET"}, "test-cluster", "", corsConfigModel3)) + "/testPath", []string{"GET"}, "test-cluster", "", corsConfigModel3, false)) assert.NotNil(t, routeWithCors.GetRoute().Cors, "Cors Configuration should not be null.") } func generateRouteCreateParamsForUnitTests(title string, apiType string, vhost string, xWso2Basepath string, version string, endpointBasepath string, resourcePathParam string, resourceMethods []string, prodClusterName string, sandClusterName string, - corsConfig *model.CorsConfig) *routeCreateParams { + corsConfig *model.CorsConfig, isDefaultVersion bool) *routeCreateParams { return &routeCreateParams{ title: title, apiType: apiType, @@ -511,5 +516,6 @@ func generateRouteCreateParamsForUnitTests(title string, apiType string, vhost s corsPolicy: corsConfig, resourcePathParam: resourcePathParam, resourceMethods: resourceMethods, + isDefaultVersion: isDefaultVersion, } } diff --git a/adapter/internal/oasparser/envoyconf/internal_dtos.go b/adapter/internal/oasparser/envoyconf/internal_dtos.go index de2ddc06c7..cdd38a3284 100644 --- a/adapter/internal/oasparser/envoyconf/internal_dtos.go +++ b/adapter/internal/oasparser/envoyconf/internal_dtos.go @@ -43,4 +43,5 @@ type routeCreateParams struct { rewritePath string rewriteMethod bool passRequestPayloadToEnforcer bool + isDefaultVersion bool } diff --git a/adapter/internal/oasparser/envoyconf/listener_test.go b/adapter/internal/oasparser/envoyconf/listener_test.go index ce03dc37be..a0c8f53b11 100644 --- a/adapter/internal/oasparser/envoyconf/listener_test.go +++ b/adapter/internal/oasparser/envoyconf/listener_test.go @@ -107,11 +107,11 @@ func testCreateRoutesForUnitTests(t *testing.T) []*routev3.Route { } route1 := createRoute(generateRouteCreateParamsForUnitTests("test", "HTTP", "localhost", "/test", "1.0.0", "/test", - "/testPath", []string{"GET"}, "test-cluster", "", corsConfigModel3)) + "/testPath", []string{"GET"}, "test-cluster", "", corsConfigModel3, false)) route2 := createRoute(generateRouteCreateParamsForUnitTests("test", "HTTP", "localhost", "/test", "1.0.0", "/test", - "/testPath", []string{"POST"}, "test-cluster", "", corsConfigModel3)) + "/testPath", []string{"POST"}, "test-cluster", "", corsConfigModel3, false)) route3 := createRoute(generateRouteCreateParamsForUnitTests("test", "HTTP", "localhost", "/test", "1.0.0", "/test", - "/testPath", []string{"PUT"}, "test-cluster", "", corsConfigModel3)) + "/testPath", []string{"PUT"}, "test-cluster", "", corsConfigModel3, false)) routes := []*routev3.Route{route1, route2, route3} diff --git a/adapter/internal/oasparser/envoyconf/routes_with_clusters.go b/adapter/internal/oasparser/envoyconf/routes_with_clusters.go index 13822df9bc..6c1375e135 100644 --- a/adapter/internal/oasparser/envoyconf/routes_with_clusters.go +++ b/adapter/internal/oasparser/envoyconf/routes_with_clusters.go @@ -662,9 +662,6 @@ func createTLSProtocolVersion(tlsVersion string) tlsv3.TlsParameters_TlsProtocol // endpoint's basePath, resource Object (Microgateway's internal representation), production clusterName and // sandbox clusterName needs to be provided. func createRoute(params *routeCreateParams) *routev3.Route { - // func createRoute(title string, apiType string, xWso2Basepath string, version string, endpointBasepath string, - // resourcePathParam string, resourceMethods []string, prodClusterName string, sandClusterName string, - // corsPolicy *routev3.CorsPolicy) *routev3.Route { title := params.title version := params.version vHost := params.vHost @@ -680,6 +677,7 @@ func createRoute(params *routeCreateParams) *routev3.Route { endpointBasepath := params.endpointBasePath requestInterceptor := params.requestInterceptor responseInterceptor := params.responseInterceptor + isDefaultVersion := params.isDefaultVersion config, _ := config.ReadConfigs() logger.LoggerOasparser.Debug("creating a route....") @@ -691,7 +689,11 @@ func createRoute(params *routeCreateParams) *routev3.Route { responseHeadersToRemove []string ) - routePath := generateRoutePaths(xWso2Basepath, endpointBasepath, resourcePath) + basePath := getFilteredBasePath(xWso2Basepath, endpointBasepath) + if isDefaultVersion { + basePath = getDefaultVersionBasepath(basePath, version) + } + routePath := generateRoutePath(basePath, resourcePath) match = &routev3.RouteMatch{ PathSpecifier: &routev3.RouteMatch_SafeRegex{ @@ -708,7 +710,7 @@ func createRoute(params *routeCreateParams) *routev3.Route { // if any of the operations on the route path has a method rewrite policy, // we remove the :method header matching, - // because envoy does not allow method rewrting later if the following method regex doesnot have the new method. + // because envoy does not allow method rewrting later if the following method regex does not have the new method. // hence when method rewriting is enabled for the resource, the method validation will be handled by the enforcer instead of the router. if !params.rewriteMethod { // OPTIONS is always added even if it is not listed under resources @@ -827,7 +829,7 @@ func createRoute(params *routeCreateParams) *routev3.Route { Value: luaMarshelled.Bytes(), } - pathRegex := xWso2Basepath + pathRegex := basePath substitutionString := endpointBasepath if params.rewritePath != "" { pathRegex = routePath @@ -1138,28 +1140,28 @@ func CreateReadyEndpoint() *routev3.Route { return &router } -// generateRoutePaths generates route paths for the api resources. -func generateRoutePaths(xWso2Basepath, basePath, resourcePath string) string { - prefix := "" +// generateRoutePath generates route paths for the api resources. +func generateRoutePath(basePath, resourcePath string) string { newPath := "" - if strings.TrimSpace(xWso2Basepath) != "" { - prefix = basepathConsistent(xWso2Basepath) - - } else { - prefix = basepathConsistent(basePath) - // TODO: (VirajSalaka) Decide if it is possible to proceed without both basepath options - } if strings.Contains(resourcePath, "?") { resourcePath = strings.Split(resourcePath, "?")[0] } - fullpath := prefix + resourcePath + fullpath := basePath + resourcePath newPath = generateRegex(fullpath) return newPath } -func basepathConsistent(basePath string) string { - modifiedBasePath := basePath - if !strings.HasPrefix(basePath, "/") { +func getFilteredBasePath(xWso2Basepath string, basePath string) string { + var modifiedBasePath string + + if strings.TrimSpace(xWso2Basepath) != "" { + modifiedBasePath = xWso2Basepath + } else { + modifiedBasePath = basePath + // TODO: (VirajSalaka) Decide if it is possible to proceed without both basepath options + } + + if !strings.HasPrefix(modifiedBasePath, "/") { modifiedBasePath = "/" + modifiedBasePath } modifiedBasePath = strings.TrimSuffix(modifiedBasePath, "/") @@ -1271,6 +1273,7 @@ func genRouteCreateParams(swagger *model.MgwSwagger, resource *model.Resource, v rewritePath: "", rewriteMethod: false, passRequestPayloadToEnforcer: swagger.GetXWso2RequestBodyPass(), + isDefaultVersion: swagger.IsDefaultVersion, } if resource != nil { @@ -1322,3 +1325,8 @@ func getDefaultResourceMethods(apiType string) []string { } return defaultResourceMethods } + +func getDefaultVersionBasepath(basePath string, version string) string { + context := strings.ReplaceAll(basePath, "/"+version, "") + return fmt.Sprintf("(%s|%s)", basePath, context) +} diff --git a/adapter/internal/oasparser/model/api_yaml.go b/adapter/internal/oasparser/model/api_yaml.go index d8e20f28bd..8b03ea828e 100644 --- a/adapter/internal/oasparser/model/api_yaml.go +++ b/adapter/internal/oasparser/model/api_yaml.go @@ -46,6 +46,7 @@ type APIYaml struct { AuthorizationHeader string `json:"authorizationHeader,omitempty"` SecurityScheme []string `json:"securityScheme,omitempty"` OrganizationID string `json:"organizationId,omitempty"` + IsDefaultVersion bool `json:"isDefaultVersion,omitempty"` EndpointConfig struct { EndpointType string `json:"endpoint_type,omitempty"` LoadBalanceAlgo string `json:"algoCombo,omitempty"` diff --git a/adapter/internal/oasparser/model/mgw_swagger.go b/adapter/internal/oasparser/model/mgw_swagger.go index 819c0a65aa..904dc60f63 100644 --- a/adapter/internal/oasparser/model/mgw_swagger.go +++ b/adapter/internal/oasparser/model/mgw_swagger.go @@ -68,6 +68,7 @@ type MgwSwagger struct { EndpointImplementationType string LifecycleStatus string xWso2RequestBodyPass bool + IsDefaultVersion bool } // EndpointCluster represent an upstream cluster @@ -1139,6 +1140,7 @@ func (swagger *MgwSwagger) PopulateFromAPIYaml(apiYaml APIYaml) error { // context value in api.yaml is assigned as xWso2Basepath swagger.xWso2Basepath = data.Context + "/" + swagger.version swagger.LifecycleStatus = data.LifeCycleStatus + swagger.IsDefaultVersion = data.IsDefaultVersion // productionURL & sandBoxURL values are extracted from endpointConfig in api.yaml endpointConfig := data.EndpointConfig diff --git a/adapter/pkg/messaging/azure_listener.go b/adapter/pkg/messaging/azure_listener.go index 72b9eb6cd6..da7b1b2633 100644 --- a/adapter/pkg/messaging/azure_listener.go +++ b/adapter/pkg/messaging/azure_listener.go @@ -79,9 +79,9 @@ func startBrokerConsumer(connectionString string, sub Subscription, reconnectInt logger.LoggerMsg.Errorf("Failed to parse the ASB message. %v", err) } - logger.LoggerMsg.Debugf("Message %s from ASB waits to be processed.", message.MessageID) + logger.LoggerMsg.Debugf("Message %s from ASB is waiting to be processed.", message.MessageID) dataChannel <- body - logger.LoggerMsg.Debugf("Message %s from ASB is complete", message.MessageID) + logger.LoggerMsg.Debugf("Message %s from ASB is processed", message.MessageID) err = receiver.CompleteMessage(ctx, message) if err != nil { diff --git a/adapter/pkg/messaging/event_types.go b/adapter/pkg/messaging/event_types.go index fbebad681f..44b8216076 100644 --- a/adapter/pkg/messaging/event_types.go +++ b/adapter/pkg/messaging/event_types.go @@ -118,6 +118,7 @@ type APIEvent struct { Version string `json:"version"` Context string `json:"context"` Name string `json:"name"` + Action string `json:"action"` } // ApplicationRegistrationEvent for struct application registration events diff --git a/integration/test-integration/src/test/java/org/wso2/choreo/connect/tests/testcases/standalone/apiDeploy/DefaultVersionApiTestCase.java b/integration/test-integration/src/test/java/org/wso2/choreo/connect/tests/testcases/standalone/apiDeploy/DefaultVersionApiTestCase.java new file mode 100644 index 0000000000..f3a92f5672 --- /dev/null +++ b/integration/test-integration/src/test/java/org/wso2/choreo/connect/tests/testcases/standalone/apiDeploy/DefaultVersionApiTestCase.java @@ -0,0 +1,108 @@ +/* + * Copyright (c) 2022, WSO2 Inc. (http://www.wso2.org). + * WSO2 Inc. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.wso2.choreo.connect.tests.testcases.standalone.apiDeploy; + +import org.apache.http.HttpStatus; +import org.testng.Assert; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Test; +import org.wso2.choreo.connect.tests.common.model.API; +import org.wso2.choreo.connect.tests.common.model.ApplicationDTO; +import org.wso2.choreo.connect.tests.context.CCTestException; +import org.wso2.choreo.connect.tests.util.*; + +import java.util.HashMap; +import java.util.Map; + +/** + * Test default versioned APIs. + */ +public class DefaultVersionApiTestCase { + private String testKeyV1, testKeyV2; + private final String basePath = "/defaultVersion/"; + private final String v1 = "1.0.0"; + private final String v2 = "2.0.0"; + private final String v1Context = basePath + v1; + private final String v2Context = basePath + v2; + + @BeforeClass + public void deployAPIs() throws Exception { + ApictlUtils.login("test"); + ApictlUtils.createProject("default_version_v1_OpenAPI.yaml", "default_version_v1", null, null, null, + "default_version_v1.yaml"); + ApictlUtils.createProject("default_version_v2_OpenAPI.yaml", "default_version_v2", null, null, null, + "default_version_v2.yaml"); + + ApictlUtils.deployAPI("default_version_v1", "test"); + ApictlUtils.deployAPI("default_version_v2", "test"); + + Utils.delay(TestConstant.DEPLOYMENT_WAIT_TIME, "Couldn't wait till deployment completion."); + + API api = new API(); + api.setName("DefaultVersion"); + api.setContext(v1Context); + api.setVersion(v1); + api.setProvider("admin"); + + ApplicationDTO app = new ApplicationDTO(); + app.setName("jwtApp"); + app.setTier("Unlimited"); + app.setId((int) (Math.random() * 1000)); + + testKeyV1 = TokenUtil.getJWT(api, app, "Unlimited", TestConstant.KEY_TYPE_PRODUCTION, 3600, null, true); + api.setContext(v2Context); + api.setVersion(v2); + testKeyV2 = TokenUtil.getJWT(api, app, "Unlimited", TestConstant.KEY_TYPE_PRODUCTION, 3600, null, true); + } + + @Test(description = "Test invoking default versioned API without version in the context") + public void testInvokingDefaultVersion() throws CCTestException { + Map headers = new HashMap<>(); + headers.put("Internal-Key", testKeyV2); + HttpResponse response = HttpsClientRequest.doGet("https://localhost:9095" + basePath + "store/inventory", headers); + Assert.assertNotNull(response); + Assert.assertEquals(response.getResponseCode(), HttpStatus.SC_OK, "Response code mismatched"); + Assert.assertTrue(response.getData().contains("233539"), "Response body mismatched"); + } + + @Test(description = "Test invoking default versioned API with version in the context") + public void testInvokingDefaultVersionWithVersion() throws CCTestException { + Map headers = new HashMap<>(); + headers.put("Internal-Key", testKeyV2); + HttpResponse response = HttpsClientRequest.doGet("https://localhost:9095" + v2Context + "/store/inventory", headers); + Assert.assertNotNull(response); + Assert.assertEquals(response.getResponseCode(), HttpStatus.SC_OK, "Response code mismatched"); + Assert.assertTrue(response.getData().contains("233539"), "Response body mismatched"); + } + + @Test(description = "Test invoking `non` default versioned API") + public void testInvokingNoneDefaultVersion() throws CCTestException { + Map headers = new HashMap<>(); + headers.put("Internal-Key", testKeyV1); + HttpResponse response = HttpsClientRequest.doGet("https://localhost:9095" + v1Context + "/pet/3", headers); + Assert.assertNotNull(response); + Assert.assertEquals(response.getResponseCode(), HttpStatus.SC_OK, "Response code mismatched"); + Assert.assertTrue(response.getData().contains("John Doe"), "Response body mismatched"); + + // V1 doesn't have /user/john resource. We should try to invoke it and see if the traffic is routing to the + // correct API + response = HttpsClientRequest.doGet("https://localhost:9095" + v1Context + "/store/inventory", headers); + Assert.assertNotNull(response); + Assert.assertEquals(response.getResponseCode(), HttpStatus.SC_NOT_FOUND, "Response code mismatched"); + } +} diff --git a/integration/test-integration/src/test/resources/apiYaml/default_version_v1.yaml b/integration/test-integration/src/test/resources/apiYaml/default_version_v1.yaml new file mode 100644 index 0000000000..60612858c7 --- /dev/null +++ b/integration/test-integration/src/test/resources/apiYaml/default_version_v1.yaml @@ -0,0 +1,24 @@ +type: api +version: v4.0.0 +data: + name: DefaultVersion + context: /defaultVersion/1.0.0 + version: 1.0.0 + provider: admin + lifeCycleStatus: PUBLISHED + isDefaultVersion: false + isRevision: false + revisionId: 0 + type: HTTP + transport: + - http + - https + policies: + - Unlimited + visibility: PUBLIC + endpointConfig: + endpoint_type: http + production_endpoints: + url: http://mockBackend:2383/v2 + endpointImplementationType: ENDPOINT + websubSubscriptionConfiguration: null diff --git a/integration/test-integration/src/test/resources/apiYaml/default_version_v2.yaml b/integration/test-integration/src/test/resources/apiYaml/default_version_v2.yaml new file mode 100644 index 0000000000..aaf58e6134 --- /dev/null +++ b/integration/test-integration/src/test/resources/apiYaml/default_version_v2.yaml @@ -0,0 +1,24 @@ +type: api +version: v4.0.0 +data: + name: DefaultVersion + context: /defaultVersion/2.0.0 + version: 2.0.0 + provider: admin + lifeCycleStatus: PUBLISHED + isDefaultVersion: true + isRevision: false + revisionId: 0 + type: HTTP + transport: + - http + - https + policies: + - Unlimited + visibility: PUBLIC + endpointConfig: + endpoint_type: http + production_endpoints: + url: http://mockBackend:2383/v2 + endpointImplementationType: ENDPOINT + websubSubscriptionConfiguration: null diff --git a/integration/test-integration/src/test/resources/openAPIs/default_version_v1_OpenAPI.yaml b/integration/test-integration/src/test/resources/openAPIs/default_version_v1_OpenAPI.yaml new file mode 100644 index 0000000000..ba9aa45d87 --- /dev/null +++ b/integration/test-integration/src/test/resources/openAPIs/default_version_v1_OpenAPI.yaml @@ -0,0 +1,82 @@ +swagger: '2.0' +info: + version: 1.0.0 + title: DefaultVersion +x-wso2-production-endpoints: + urls: + - 'http://mockBackend:2383/v2' +x-wso2-basePath: /defaultVersion/1.0.0 +schemes: + - http +paths: + '/pet/{petId}': + get: + produces: + - application/json + - application/xml + parameters: + - name: petId + in: path + description: ID of pet to return + required: true + type: integer + format: int64 + responses: + '200': + description: successful operation + schema: + $ref: '#/definitions/Pet' + '400': + description: Invalid ID supplied + '404': + description: Pet not found +definitions: + Pet: + type: object + required: + - name + - photoUrls + properties: + id: + type: integer + format: int64 + category: + $ref: '#/definitions/Category' + name: + type: string + example: doggie + photoUrls: + type: array + xml: + wrapped: true + items: + type: string + xml: + name: photoUrl + tags: + type: array + xml: + wrapped: true + items: + xml: + name: tag + $ref: '#/definitions/Tag' + status: + type: string + description: pet status in the store + enum: + - available + - pending + - sold + xml: + name: Pet + Tag: + type: object + properties: + id: + type: integer + format: int64 + name: + type: string + xml: + name: Tag diff --git a/integration/test-integration/src/test/resources/openAPIs/default_version_v2_OpenAPI.yaml b/integration/test-integration/src/test/resources/openAPIs/default_version_v2_OpenAPI.yaml new file mode 100644 index 0000000000..86efd8066e --- /dev/null +++ b/integration/test-integration/src/test/resources/openAPIs/default_version_v2_OpenAPI.yaml @@ -0,0 +1,95 @@ +swagger: '2.0' +info: + version: 2.0.0 + title: DefaultVersion +x-wso2-production-endpoints: + urls: + - 'http://mockBackend:2383/v2' +x-wso2-basePath: /defaultVersion/2.0.0 +schemes: + - http +paths: + /store/inventory: + get: + produces: + - application/json + parameters: [] + responses: + '200': + description: successful operation + schema: + type: object + additionalProperties: + type: integer + format: int32 + '/pet/{petId}': + get: + produces: + - application/json + - application/xml + parameters: + - name: petId + in: path + description: ID of pet to return + required: true + type: integer + format: int64 + responses: + '200': + description: successful operation + schema: + $ref: '#/definitions/Pet' + '400': + description: Invalid ID supplied + '404': + description: Pet not found +definitions: + Pet: + type: object + required: + - name + - photoUrls + properties: + id: + type: integer + format: int64 + category: + $ref: '#/definitions/Category' + name: + type: string + example: doggie + photoUrls: + type: array + xml: + wrapped: true + items: + type: string + xml: + name: photoUrl + tags: + type: array + xml: + wrapped: true + items: + xml: + name: tag + $ref: '#/definitions/Tag' + status: + type: string + description: pet status in the store + enum: + - available + - pending + - sold + xml: + name: Pet + Tag: + type: object + properties: + id: + type: integer + format: int64 + name: + type: string + xml: + name: Tag diff --git a/integration/test-integration/src/test/resources/testng-cc-standalone.xml b/integration/test-integration/src/test/resources/testng-cc-standalone.xml index 3b96f4758f..36a7faa2ed 100644 --- a/integration/test-integration/src/test/resources/testng-cc-standalone.xml +++ b/integration/test-integration/src/test/resources/testng-cc-standalone.xml @@ -55,6 +55,7 @@ +