From dd2bf0441faddda04311a632ca5fd99df1b2f69d Mon Sep 17 00:00:00 2001 From: Marcio Meier Date: Wed, 22 Jan 2025 18:37:25 +0100 Subject: [PATCH 01/13] feat: wso2 api subscription --- .../wso2-api-subscription/handler/index.ts | 161 +++++++++++++ .../wso2-api-subscription/handler/wso2-v1.ts | 218 ++++++++++++++++++ lib/src/wso2/wso2-api-subscription/types.ts | 15 ++ .../wso2/wso2-api-subscription/v1/types.ts | 63 +++++ .../wso2-api-subscription.ts | 106 +++++++++ lib/src/wso2/wso2-api/handler/index.ts | 11 +- lib/src/wso2/wso2-api/handler/wso2-v1.ts | 46 ++-- lib/src/wso2/wso2-api/wso2-api.ts | 8 +- .../wso2/wso2-application/handler/index.ts | 25 +- .../wso2/wso2-application/handler/wso2-v1.ts | 55 ++++- .../wso2/wso2-application/wso2-application.ts | 8 +- 11 files changed, 673 insertions(+), 43 deletions(-) create mode 100644 lib/src/wso2/wso2-api-subscription/handler/index.ts create mode 100644 lib/src/wso2/wso2-api-subscription/handler/wso2-v1.ts create mode 100644 lib/src/wso2/wso2-api-subscription/types.ts create mode 100644 lib/src/wso2/wso2-api-subscription/v1/types.ts create mode 100644 lib/src/wso2/wso2-api-subscription/wso2-api-subscription.ts diff --git a/lib/src/wso2/wso2-api-subscription/handler/index.ts b/lib/src/wso2/wso2-api-subscription/handler/index.ts new file mode 100644 index 0000000..f0362d9 --- /dev/null +++ b/lib/src/wso2/wso2-api-subscription/handler/index.ts @@ -0,0 +1,161 @@ +/* eslint-disable no-console */ +import { AxiosInstance } from 'axios'; +import { CdkCustomResourceEvent, CdkCustomResourceResponse } from 'aws-lambda'; + +import { prepareAxiosForWso2Calls } from '../../wso2-utils'; +import { truncateStr } from '../../utils'; +import { Wso2ApiSubscriptionProps } from '../types'; + +import { + createWso2ApiSubscription, + findWso2ApiSubscription, + getWso2Api, + getWso2Application, + removeWso2ApiSubscription, + updateWso2ApiSubscription, +} from './wso2-v1'; + +export type Wso2ApiCustomResourceEvent = CdkCustomResourceEvent & { + ResourceProperties: Wso2ApiSubscriptionProps; +}; + +export type Wso2ApiCustomResourceResponse = CdkCustomResourceResponse & { + Data?: { + Wso2ApiId?: string; + SubscriptionId?: string; + ApplicationId?: string; + Error?: unknown; + }; + Status?: 'SUCCESS' | 'FAILED'; + Reason?: string; +}; + +export const handler = async ( + event: Wso2ApiCustomResourceEvent, +): Promise => { + const response: Wso2ApiCustomResourceResponse = { + StackId: event.StackId, + RequestId: event.RequestId, + LogicalResourceId: event.LogicalResourceId, + }; + + try { + console.log('>>> Prepare WSO2 API client...'); + const wso2Axios = await prepareAxiosForWso2Calls(event.ResourceProperties.wso2Config); + + if (event.RequestType === 'Create' || event.RequestType === 'Update') { + console.log('>>> Creating or Updating WSO2 API Subscription...'); + + const { wso2ApiId, subscriptionId, applicationId } = await createOrUpdateWso2ApiSubscription( + event, + wso2Axios, + ); + + return { + ...response, + PhysicalResourceId: subscriptionId, + Data: { + Wso2ApiId: wso2ApiId, + SubscriptionId: subscriptionId, + ApplicationId: applicationId, + }, + Status: 'SUCCESS', + }; + } + + if (event.RequestType === 'Delete') { + console.log('>>> Deleting WSO2 API...'); + + await removeWso2ApiSubscription({ + wso2Axios, + subscriptionId: event.PhysicalResourceId, + retryOptions: event.ResourceProperties.retryOptions, + }); + + return { + ...response, + PhysicalResourceId: event.PhysicalResourceId, + Status: 'SUCCESS', + }; + } + throw new Error('Unrecognized RequestType'); + } catch (error) { + console.log(`An error has occurred. err=${error}`); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const err = error as any; + if (err.stack) { + console.log(err.stack); + } + throw new Error(truncateStr(`${error}`, 1000)); + } +}; + +const createOrUpdateWso2ApiSubscription = async ( + event: Wso2ApiCustomResourceEvent, + wso2Axios: AxiosInstance, +): Promise<{ wso2ApiId: string; subscriptionId: string; applicationId: string }> => { + console.log('Searching for the API in WSO2...'); + const wso2Api = await getWso2Api({ + wso2Axios, + apiId: event.ResourceProperties.apiId, + wso2Tenant: event.ResourceProperties.wso2Config.tenant, + apiSearchParameters: event.ResourceProperties.apiSearchParameters, + }); + + const wso2Application = await getWso2Application({ + wso2Axios, + applicationId: event.ResourceProperties.applicationId, + applicationSearchParameters: event.ResourceProperties.applicationSearchParameters, + }); + + const wso2Subscription = await findWso2ApiSubscription({ + wso2Axios, + apiId: wso2Api.id!, + applicationId: wso2Application.applicationId!, + }); + + if ( + wso2Subscription && + wso2Subscription.throttlingPolicy === event.ResourceProperties.throttlingPolicy + ) { + console.log('Current subscription already exists with the same configuration. Skipping update'); + return { + wso2ApiId: wso2Api.id!, + subscriptionId: wso2Subscription.subscriptionId!, + applicationId: wso2Application.applicationId!, + }; + } + + if (wso2Subscription) { + console.log('Subscription already exists. Updating...'); + const result = await updateWso2ApiSubscription({ + wso2Axios, + subscriptionId: wso2Subscription.subscriptionId!, + apiId: wso2Api.id!, + applicationId: wso2Application.applicationId!, + throttlingPolicy: event.ResourceProperties.throttlingPolicy, + retryOptions: event.ResourceProperties.retryOptions, + }); + + return { + wso2ApiId: wso2Api.id!, + subscriptionId: result.subscriptionId!, + applicationId: wso2Application.applicationId!, + }; + } + + console.log('Creating a new subscription...'); + const result = await createWso2ApiSubscription({ + wso2Axios, + apiId: wso2Api.id!, + applicationId: wso2Application.applicationId!, + throttlingPolicy: event.ResourceProperties.throttlingPolicy, + retryOptions: event.ResourceProperties.retryOptions, + }); + + return { + wso2ApiId: wso2Api.id!, + subscriptionId: result.subscriptionId!, + applicationId: wso2Application.applicationId!, + }; +}; diff --git a/lib/src/wso2/wso2-api-subscription/handler/wso2-v1.ts b/lib/src/wso2/wso2-api-subscription/handler/wso2-v1.ts new file mode 100644 index 0000000..362452f --- /dev/null +++ b/lib/src/wso2/wso2-api-subscription/handler/wso2-v1.ts @@ -0,0 +1,218 @@ +/* eslint-disable no-console */ +import { AxiosInstance } from 'axios'; +import { backOff } from 'exponential-backoff'; + +import { Wso2ApiSubscriptionProps } from '../types'; +import { findWso2Api, getWso2ApiById } from '../../wso2-api/handler/wso2-v1'; +import { ApiFromListV1 } from '../../wso2-api/v1/types'; +import { + findWso2Application, + getWso2ApplicationById, +} from '../../wso2-application/handler/wso2-v1'; +import { Wso2ApplicationInfo } from '../../wso2-application/v1/types'; +import { Subscription, SubscriptionList } from '../v1/types'; + +export type GetWso2ApiArgs = Pick & { + wso2Axios: AxiosInstance; + wso2Tenant?: string; +}; + +export const getWso2Api = async ({ + wso2Axios, + apiId, + wso2Tenant, + apiSearchParameters, +}: GetWso2ApiArgs): Promise => { + if (apiId) { + console.log('Getting WSO2 API by id...'); + const apiDetails = await getWso2ApiById({ wso2Axios, wso2ApiId: apiId }); + return apiDetails; + } + + if (!apiSearchParameters) { + throw new Error('apiSearchParameters is required for searching API'); + } + + console.log('Getting WSO2 API by search parameters...'); + const apiDetails = await findWso2Api({ + wso2Axios, + apiContext: apiSearchParameters.context, + apiName: apiSearchParameters.name, + apiVersion: apiSearchParameters.version, + wso2Tenant, + }); + + if (!apiDetails) { + throw new Error('Cannot find the WSO2 API is related to this Custom Resource.'); + } + + return apiDetails; +}; + +export type GetWso2ApplicationArgs = Pick< + Wso2ApiSubscriptionProps, + 'applicationId' | 'applicationSearchParameters' +> & { + wso2Axios: AxiosInstance; +}; + +export const getWso2Application = async ({ + wso2Axios, + applicationId, + applicationSearchParameters, +}: GetWso2ApplicationArgs): Promise => { + if (applicationId) { + console.log('Getting WSO2 Application by id...'); + const application = await getWso2ApplicationById({ wso2Axios, applicationId }); + return application; + } + + if (!applicationSearchParameters) { + throw new Error('applicationSearchParameters is required for searching application'); + } + + console.log('Getting WSO2 API by search parameters...'); + const application = await findWso2Application({ + wso2Axios, + name: applicationSearchParameters.name, + }); + + if (!application) { + throw new Error('Cannot find the WSO2 application is related to this Custom Resource.'); + } + + return application; +}; + +export type FindWso2ApiSubscriptionArgs = { + wso2Axios: AxiosInstance; + apiId: string; + applicationId: string; +}; +export const findWso2ApiSubscription = async ({ + wso2Axios, + apiId, + applicationId, +}: FindWso2ApiSubscriptionArgs): Promise => { + const apil = await wso2Axios.get(`/api/am/store/v1/subscriptions`, { + params: { + apiId, + applicationId, + }, + }); + + const apiRes = apil.data.list; + + if (!apiRes) { + throw new Error('find subscription response is empty'); + } + + if (apiRes.length > 1) { + throw new Error( + `More than one subscription found for api '${apiId}' and application '${applicationId}' so we cannot determine it's id automatically`, + ); + } + + if (apiRes.length === 0) { + // eslint-disable-next-line no-undefined + return undefined; + } + + const existingSubscription = apiRes[0]; + console.log( + `Found existing WSO2 Subscription. subscriptionId=${existingSubscription.subscriptionId};`, + ); + + return existingSubscription; +}; + +export type GetWso2ApiSubscriptionByIdArgs = { + wso2Axios: AxiosInstance; + subscriptionId: string; +}; + +const getWso2ApiSubscriptionById = async ({ + wso2Axios, + subscriptionId, +}: GetWso2ApiSubscriptionByIdArgs): Promise => { + const res = await wso2Axios.get(`/api/am/store/v1/subscriptions/${subscriptionId}`); + return res.data; +}; + +export type CreateWso2ApiSubscriptionArgs = Pick & { + wso2Axios: AxiosInstance; + apiId: string; + applicationId: string; + throttlingPolicy: string; +}; + +export const createWso2ApiSubscription = async ({ + wso2Axios, + apiId, + applicationId, + throttlingPolicy, + retryOptions, +}: CreateWso2ApiSubscriptionArgs): Promise => { + const payload: Subscription = { + applicationId, + apiId, + throttlingPolicy, + }; + + const res = await backOff( + async () => wso2Axios.post(`/api/am/store/v1/subscriptions`, payload), + retryOptions?.mutationRetries, + ); + + // wait for Application to be created by retrying checks + await backOff(async () => { + await getWso2ApiSubscriptionById({ + wso2Axios, + subscriptionId: res.data.subscriptionId!, + }); + }, retryOptions?.checkRetries); + + return res.data; +}; + +export type UpdateWso2ApiSubscriptionArgs = CreateWso2ApiSubscriptionArgs & { + subscriptionId: string; +}; + +export const updateWso2ApiSubscription = async ({ + wso2Axios, + subscriptionId, + apiId, + applicationId, + throttlingPolicy, + retryOptions, +}: UpdateWso2ApiSubscriptionArgs): Promise => { + const payload: Subscription = { + applicationId, + apiId, + throttlingPolicy, + }; + + const res = await backOff( + async () => wso2Axios.post(`/api/am/store/v1/subscriptions/${subscriptionId}`, payload), + retryOptions?.mutationRetries, + ); + + return res.data; +}; + +export type RemoveWso2ApiSubscriptionArgs = Pick & { + wso2Axios: AxiosInstance; + subscriptionId: string; +}; + +export const removeWso2ApiSubscription = async ({ + wso2Axios, + subscriptionId, + retryOptions, +}: RemoveWso2ApiSubscriptionArgs): Promise => { + await backOff( + async () => wso2Axios.delete(`/api/am/store/v1/subscriptions/${subscriptionId}`), + retryOptions?.mutationRetries, + ); +}; diff --git a/lib/src/wso2/wso2-api-subscription/types.ts b/lib/src/wso2/wso2-api-subscription/types.ts new file mode 100644 index 0000000..a10174a --- /dev/null +++ b/lib/src/wso2/wso2-api-subscription/types.ts @@ -0,0 +1,15 @@ +import { Wso2BaseProperties } from '../types'; + +export type Wso2ApiSubscriptionProps = Wso2BaseProperties & { + apiId?: string; + apiSearchParameters?: { + name: string; + version: string; + context: string; + }; + applicationId?: string; + applicationSearchParameters?: { + name: string; + }; + throttlingPolicy: 'Unlimited' | 'Gold' | 'Silver' | 'Bronze'; +}; diff --git a/lib/src/wso2/wso2-api-subscription/v1/types.ts b/lib/src/wso2/wso2-api-subscription/v1/types.ts new file mode 100644 index 0000000..2a59c7f --- /dev/null +++ b/lib/src/wso2/wso2-api-subscription/v1/types.ts @@ -0,0 +1,63 @@ +import { ApiFromListV1 } from '../../wso2-api/v1/types'; +import { Wso2ApplicationInfo } from '../../wso2-application/v1/types'; + +export type Subscription = { + /** + * The UUID of the subscription + */ + subscriptionId?: string; + + /** + * The UUID of the application + */ + applicationId: string; + + /** + * The unique identifier of the API. + */ + apiId?: string; + + apiInfo?: ApiFromListV1; + + applicationInfo?: Wso2ApplicationInfo; + + throttlingPolicy: string; + + requestedThrottlingPolicy?: string; + + status?: + | 'BLOCKED' + | 'PROD_ONLY_BLOCKED' + | 'UNBLOCKED' + | 'ON_HOLD' + | 'REJECTED' + | 'TIER_UPDATE_PENDING'; + + /** + * A url and other parameters the subscriber can be redirected. + */ + redirectionParams?: string; +}; + +export type SubscriptionList = { + /** + * Number of Subscriptions returned. + */ + count?: number; + list?: Array; + pagination?: Pagination; +}; + +export type Pagination = { + offset?: number; + limit?: number; + total?: number; + /** + * Link to the next subset of resources qualified. Empty if no more resources are to be returned. + */ + next?: string; + /** + * Link to the previous subset of resources qualified. Empty if current subset is the first subset returned. + */ + previous?: string; +}; diff --git a/lib/src/wso2/wso2-api-subscription/wso2-api-subscription.ts b/lib/src/wso2/wso2-api-subscription/wso2-api-subscription.ts new file mode 100644 index 0000000..9d9424c --- /dev/null +++ b/lib/src/wso2/wso2-api-subscription/wso2-api-subscription.ts @@ -0,0 +1,106 @@ +import { Construct } from 'constructs'; +import { CustomResource, RemovalPolicy } from 'aws-cdk-lib/core'; +import { IFunction } from 'aws-cdk-lib/aws-lambda'; +import z from 'zod'; + +import { addLambdaAndProviderForWso2Operations } from '../utils-cdk'; + +import { Wso2ApiSubscriptionProps } from './types'; + +/** + * WSO2 API CDK construct for subscribing a WSO2 Application into a WSO2 API + * + * @example + * + * const wso2Api = new Wso2Api(...); + * const wso2Application = new Wso2Application(...); + * + * const wso2ApiSubscription = new Wso2ApiSubscription(this, 'Wso2ApiSubscription', { + * apiId: wso2Api.wso2ApiId, + * applicationId: wso2Application.wso2ApplicationId, + * }); + * + * --- + * + * const wso2ApiSubscription = new Wso2ApiSubscription(this, 'Wso2ApiSubscription', { + * apiSearchParameters: { + * name: 'MyApi', + * version: 'v1', + * context: 'my-api', + * }, + * applicationSearchParameters: { + * applicationName: 'MyApplication', + * }, + * }); + */ +export class Wso2ApiSubscription extends Construct { + readonly customResourceFunction: IFunction; + + readonly wso2SubscriptionId: string; + + constructor(scope: Construct, id: string, props: Wso2ApiSubscriptionProps) { + super(scope, id); + + validateProps(props); + + const { customResourceProvider, customResourceFunction } = + addLambdaAndProviderForWso2Operations({ + scope: this, + id: 'Wso2ApiSubscription', + props, + baseDir: __dirname, + }); + + // eslint-disable-next-line no-new + const resource = new CustomResource(this, 'Wso2ApiSubscription-custom-resource', { + serviceToken: customResourceProvider.serviceToken, + properties: props, + resourceType: 'Custom::Wso2ApiSubscription', + removalPolicy: props.removalPolicy ?? RemovalPolicy.RETAIN, + }); + + this.customResourceFunction = customResourceFunction.nodeJsFunction; + + // TODO: check for a better way to retrieve the subscription id + // https://github.com/aws-samples/aws-cdk-examples/discussions/641 + this.wso2SubscriptionId = resource.getAtt('SubscriptionId').toString(); + } +} + +export const validateProps = (props: Wso2ApiSubscriptionProps): void => { + const apiSchema = z.union([ + z.object({ + apiId: z.string(), + }), + z.object({ + apiSearchParameters: z.object({ + name: z.string(), + version: z.string(), + context: z.string(), + }), + }), + ]); + + const applicationSchema = z.union([ + z.object({ + applicationId: z.string(), + }), + z.object({ + applicationSearchParameters: z.object({ + name: z.string(), + }), + }), + ]); + + const schema = z + .object({ + wso2Config: z.object({ + baseApiUrl: z.string(), + credentialsSecretId: z.string(), + }), + }) + .and(apiSchema) + .and(applicationSchema); + + schema.parse(props); +}; diff --git a/lib/src/wso2/wso2-api/handler/index.ts b/lib/src/wso2/wso2-api/handler/index.ts index 0fdc6e0..aeea410 100644 --- a/lib/src/wso2/wso2-api/handler/index.ts +++ b/lib/src/wso2/wso2-api/handler/index.ts @@ -2,7 +2,6 @@ import { AxiosInstance } from 'axios'; import { CdkCustomResourceEvent, CdkCustomResourceResponse } from 'aws-lambda'; -import { PublisherPortalAPIv1 } from '../v1/types'; import { Wso2ApiCustomResourceProperties } from '../types'; import { prepareAxiosForWso2Calls } from '../../wso2-utils'; import { applyRetryDefaults, truncateStr } from '../../utils'; @@ -10,6 +9,7 @@ import { applyRetryDefaults, truncateStr } from '../../utils'; import { createUpdateAndChangeLifecycleStatusInWso2, findWso2Api, + getWso2ApiById, removeApiInWso2, } from './wso2-v1'; @@ -99,17 +99,18 @@ const createOrUpdateWso2Api = async ( console.log('Searching if API already exists in WSO2...'); const existingApi = await findWso2Api({ wso2Axios, - apiDefinition: event.ResourceProperties.apiDefinition, + apiName: event.ResourceProperties.apiDefinition.name, + apiVersion: event.ResourceProperties.apiDefinition.version, + apiContext: event.ResourceProperties.apiDefinition.context, wso2Tenant: event.ResourceProperties.wso2Config.tenant ?? '', }); let apiBeforeUpdate; - if (existingApi) { + if (existingApi && existingApi.id) { console.log( `Found existing WSO2 API. apiId=${existingApi.id}; name=${existingApi.name}; version=${existingApi.version} context=${existingApi.context}`, ); - const apir = await wso2Axios.get(`/api/am/publisher/v1/apis/${existingApi.id}`); - apiBeforeUpdate = apir.data as PublisherPortalAPIv1; + apiBeforeUpdate = await getWso2ApiById({ wso2Axios, wso2ApiId: existingApi.id }); } if (event.RequestType === 'Create' && existingApi && event.ResourceProperties.failIfExists) { diff --git a/lib/src/wso2/wso2-api/handler/wso2-v1.ts b/lib/src/wso2/wso2-api/handler/wso2-v1.ts index 5535d03..c25dbda 100644 --- a/lib/src/wso2/wso2-api/handler/wso2-v1.ts +++ b/lib/src/wso2/wso2-api/handler/wso2-v1.ts @@ -21,10 +21,12 @@ import { Wso2ApiProps } from '../types'; export const findWso2Api = async (args: { wso2Axios: AxiosInstance; - apiDefinition: Wso2ApiDefinitionV1; - wso2Tenant: string; + apiName: string; + apiVersion: string; + apiContext: string; + wso2Tenant?: string; }): Promise => { - const searchQuery = `name:${args.apiDefinition.name} version:${args.apiDefinition.version} context:${args.apiDefinition.context}`; + const searchQuery = `name:${args.apiName} version:${args.apiVersion} context:${args.apiContext}`; const res = await args.wso2Axios.get(`/api/am/publisher/v1/apis`, { params: { query: searchQuery }, @@ -45,12 +47,12 @@ export const findWso2Api = async (args: { // filter out apis that were found but don't match our tenant const filteredApis = apilist.list.filter((api) => { - if (api.name !== args.apiDefinition.name || api.version !== args.apiDefinition.version) { + if (api.name !== args.apiName || api.version !== args.apiVersion) { return false; } if (!api.context) return false; // 'api.context' may contain the full context name in wso2, which means '/t/[tenant]/[api context]' - if (api.context.endsWith(args.apiDefinition.context)) { + if (api.context.endsWith(args.apiContext)) { if (args.wso2Tenant) { return api.context.startsWith(`/t/${args.wso2Tenant}`); } @@ -69,10 +71,20 @@ export const findWso2Api = async (args: { return undefined; } throw new Error( - `Cannot determine which WSO2 API is related to this Custom Resource. More than 1 API with search query '${searchQuery}' matches. name=${args.apiDefinition.name} context=${args.apiDefinition.context} version=${args.apiDefinition.version} tenant=${args.wso2Tenant}`, + `Cannot determine which WSO2 API is related to this Custom Resource. More than 1 API with search query '${searchQuery}' matches. name=${args.apiName} context=${args.apiContext} version=${args.apiVersion} tenant=${args.wso2Tenant}`, ); }; +export const getWso2ApiById = async (args: { + wso2Axios: AxiosInstance; + wso2ApiId: string; +}): Promise => { + const apir = await args.wso2Axios.get( + `/api/am/publisher/v1/apis/${args.wso2ApiId}`, + ); + return apir.data; +}; + export type UpsertWso2Args = Pick & Required> & { wso2Axios: AxiosInstance; @@ -227,7 +239,9 @@ export const changeLifecycleStatusInWso2AndCheck = async ( console.log(''); console.log(`Checking if API is in lifecycle status '${args.lifecycleStatus}'...`); const fapi = await findWso2Api({ - apiDefinition: args.apiDefinition, + apiName: args.apiDefinition.name, + apiVersion: args.apiDefinition.version, + apiContext: args.apiDefinition.context, wso2Axios: args.wso2Axios, wso2Tenant: args.wso2Tenant, }); @@ -340,19 +354,20 @@ const checkApiExistsAndMatches = async ( console.log(''); console.log('Checking if API exists and matches the desired definition in WSO2...'); const searchApi = await findWso2Api({ - apiDefinition: args.apiDefinition, + apiName: args.apiDefinition.name, + apiVersion: args.apiDefinition.version, + apiContext: args.apiDefinition.context, wso2Axios: args.wso2Axios, wso2Tenant: args.wso2Tenant, }); - if (!searchApi) { + if (!searchApi || !searchApi.id) { throw new Error(`API couldn't be found on WSO2`); } console.log(`API '${searchApi.id}' found in WSO2 search`); - const apir = await args.wso2Axios.get(`/api/am/publisher/v1/apis/${searchApi.id}`); - const apiDetails = apir.data as PublisherPortalAPIv1; + const apiDetails = await getWso2ApiById({ wso2Axios: args.wso2Axios, wso2ApiId: searchApi.id }); if (!apiDetails.lastUpdatedTime) { throw new Error('lastUpdatedTime is null in api'); @@ -513,17 +528,18 @@ const checkApiDefAndOpenapiOverlap = async (args: UpsertWso2Args): Promise { const searchApi = await findWso2Api({ - apiDefinition: args.apiDefinition, + apiName: args.apiDefinition.name, + apiVersion: args.apiDefinition.version, + apiContext: args.apiDefinition.context, wso2Axios: args.wso2Axios, wso2Tenant: args.wso2Tenant, }); - if (!searchApi) { + if (!searchApi || !searchApi.id) { throw new Error('WSO2 API not found'); } - const apir = await args.wso2Axios.get(`/api/am/publisher/v1/apis/${searchApi.id}`); - const apiDetails = apir.data as PublisherPortalAPIv1; + const apiDetails = await getWso2ApiById({ wso2Axios: args.wso2Axios, wso2ApiId: searchApi.id }); const { isEquivalent } = checkWSO2Equivalence(apiDetails, args.apiDefinition); diff --git a/lib/src/wso2/wso2-api/wso2-api.ts b/lib/src/wso2/wso2-api/wso2-api.ts index 7fa0523..aea8b06 100644 --- a/lib/src/wso2/wso2-api/wso2-api.ts +++ b/lib/src/wso2/wso2-api/wso2-api.ts @@ -25,6 +25,8 @@ export class Wso2Api extends Construct { readonly openapiDocument: OpenAPIObject; + readonly wso2ApiId: string; + constructor(scope: Construct, id: string, props: Wso2ApiProps) { super(scope, id); @@ -51,7 +53,7 @@ export class Wso2Api extends Construct { // TODO check if large open api documents can be passed by Custom Resource properties // eslint-disable-next-line no-new - new CustomResource(this, `${id}-wso2api-custom-resource`, { + const resource = new CustomResource(this, `${id}-wso2api-custom-resource`, { serviceToken: customResourceProvider.serviceToken, properties: { wso2Config: props.wso2Config, @@ -67,6 +69,10 @@ export class Wso2Api extends Construct { this.apiDefinition = wso2ApiDefs; this.openapiDocument = props.openapiDocument; this.customResourceFunction = customResourceFunction.nodeJsFunction; + + // TODO: check for a better way to retrieve the api id + // https://github.com/aws-samples/aws-cdk-examples/discussions/641 + this.wso2ApiId = resource.getAtt('Wso2ApiId').toString(); } } diff --git a/lib/src/wso2/wso2-application/handler/index.ts b/lib/src/wso2/wso2-application/handler/index.ts index 235dbe0..2d4101d 100644 --- a/lib/src/wso2/wso2-application/handler/index.ts +++ b/lib/src/wso2/wso2-application/handler/index.ts @@ -5,10 +5,13 @@ import type { AxiosInstance } from 'axios'; import { prepareAxiosForWso2Calls } from '../../wso2-utils'; import { applyRetryDefaults, truncateStr } from '../../utils'; -import type { Wso2ApplicationInfo } from '../v1/types'; import type { Wso2ApplicationCustomResourceProperties } from '../types'; -import { createUpdateApplicationInWso2, removeApplicationInWso2 } from './wso2-v1'; +import { + createUpdateApplicationInWso2, + removeApplicationInWso2, + findWso2Application, +} from './wso2-v1'; export type Wso2ApplicationCustomResourceEvent = CdkCustomResourceEvent & { ResourceProperties: Wso2ApplicationCustomResourceProperties; @@ -90,22 +93,10 @@ const createOrUpdateWso2Application = async ( // find existing WSO2 application console.log('Searching if Application already exists in WSO2...'); - let existingApplication: Wso2ApplicationInfo | undefined; - const apil = await wso2Axios.get(`/api/am/store/v1/applications`, { - params: { query: event.ResourceProperties.applicationDefinition.name }, + const existingApplication = await findWso2Application({ + wso2Axios, + name: event.ResourceProperties.applicationDefinition.name, }); - const apiRes = apil.data.list as Wso2ApplicationInfo[]; - if (apiRes.length > 1) { - throw new Error( - `More than one Application with name '${event.ResourceProperties.applicationDefinition.name}' was found in WSO2 so we cannot determine it's id automatically`, - ); - } - if (apiRes.length === 1) { - existingApplication = apiRes[0]; - console.log( - `Found existing WSO2 Application. applicationId=${existingApplication.applicationId}; name=${existingApplication.name}`, - ); - } if ( event.RequestType === 'Create' && diff --git a/lib/src/wso2/wso2-application/handler/wso2-v1.ts b/lib/src/wso2/wso2-application/handler/wso2-v1.ts index ad12cac..f5e7442 100644 --- a/lib/src/wso2/wso2-application/handler/wso2-v1.ts +++ b/lib/src/wso2/wso2-application/handler/wso2-v1.ts @@ -65,7 +65,10 @@ export const createUpdateApplicationInWso2AndCheck = async ( // wait for Application to be created by retrying checks await backOff(async () => { - await args.wso2Axios.get(`/api/am/store/v1/applications/${dataRes.applicationId}`); + await getWso2ApplicationById({ + wso2Axios: args.wso2Axios, + applicationId: dataRes.applicationId!, + }); }, args.retryOptions.checkRetries); return dataRes.applicationId; @@ -85,10 +88,54 @@ export const createUpdateApplicationInWso2AndCheck = async ( // wait for Application to be created by retrying checks await backOff(async () => { - await args.wso2Axios.get( - `/api/am/store/v1/applications/${args.existingApplication?.applicationId}`, - ); + await getWso2ApplicationById({ + wso2Axios: args.wso2Axios, + applicationId: args.existingApplication!.applicationId!, + }); }, args.retryOptions.checkRetries); return args.existingApplication.applicationId; }; + +export const getWso2ApplicationById = async (args: { + wso2Axios: AxiosInstance; + applicationId: string; +}): Promise => { + const res = await args.wso2Axios.get( + `/api/am/store/v1/applications/${args.applicationId}`, + ); + return res.data; +}; + +export const findWso2Application = async (args: { + wso2Axios: AxiosInstance; + name: string; +}): Promise => { + const apil = await args.wso2Axios.get<{ list: Wso2ApplicationInfo[] }>( + `/api/am/store/v1/applications`, + { + params: { + query: args.name, + }, + }, + ); + const apiRes = apil.data.list; + + if (apiRes.length > 1) { + throw new Error( + `More than one Application with name '${args.name}' was found in WSO2 so we cannot determine it's id automatically`, + ); + } + + if (apiRes.length === 0) { + // eslint-disable-next-line no-undefined + return undefined; + } + + const existingApplication = apiRes[0]; + console.log( + `Found existing WSO2 Application. applicationId=${existingApplication.applicationId}; name=${existingApplication.name}`, + ); + + return existingApplication; +}; diff --git a/lib/src/wso2/wso2-application/wso2-application.ts b/lib/src/wso2/wso2-application/wso2-application.ts index c3e8d8b..27bb1f4 100644 --- a/lib/src/wso2/wso2-application/wso2-application.ts +++ b/lib/src/wso2/wso2-application/wso2-application.ts @@ -17,6 +17,8 @@ import { Wso2ApplicationCustomResourceProperties, Wso2ApplicationProps } from '. export class Wso2Application extends Construct { readonly customResourceFunction: IFunction; + readonly wso2ApplicationId: string; + constructor(scope: Construct, id: string, props: Wso2ApplicationProps) { super(scope, id); @@ -39,7 +41,7 @@ export class Wso2Application extends Construct { }); // eslint-disable-next-line no-new - new CustomResource(this, `${id}-wso2app-custom-resource`, { + const resource = new CustomResource(this, `${id}-wso2app-custom-resource`, { serviceToken: customResourceProvider.serviceToken, properties: { wso2Config: props.wso2Config, @@ -51,6 +53,10 @@ export class Wso2Application extends Construct { }); this.customResourceFunction = customResourceFunction.nodeJsFunction; + + // TODO: check for a better way to retrieve the application id + // https://github.com/aws-samples/aws-cdk-examples/discussions/641 + this.wso2ApplicationId = resource.getAtt('Wso2ApplicationId').toString(); } } From 8986c81ad574ef53cee26b5f760c0954e2769a08 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A1rcio=20Henrique=20Meier?= <34343596+MarcioMeier@users.noreply.github.com> Date: Wed, 29 Jan 2025 14:55:42 +0100 Subject: [PATCH 02/13] Apply suggestions from code review Co-authored-by: Flavio Stutz --- lib/src/wso2/wso2-api-subscription/handler/index.ts | 5 +++-- lib/src/wso2/wso2-api-subscription/handler/wso2-v1.ts | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/src/wso2/wso2-api-subscription/handler/index.ts b/lib/src/wso2/wso2-api-subscription/handler/index.ts index f0362d9..1fdfd9f 100644 --- a/lib/src/wso2/wso2-api-subscription/handler/index.ts +++ b/lib/src/wso2/wso2-api-subscription/handler/index.ts @@ -64,7 +64,7 @@ export const handler = async ( } if (event.RequestType === 'Delete') { - console.log('>>> Deleting WSO2 API...'); + console.log('>>> Deleting WSO2 API Subscription...'); await removeWso2ApiSubscription({ wso2Axios, @@ -94,7 +94,7 @@ const createOrUpdateWso2ApiSubscription = async ( event: Wso2ApiCustomResourceEvent, wso2Axios: AxiosInstance, ): Promise<{ wso2ApiId: string; subscriptionId: string; applicationId: string }> => { - console.log('Searching for the API in WSO2...'); + console.log(`Verifying if WSO2 API ${event.ResourceProperties.apiId} exists in WSO2...`); const wso2Api = await getWso2Api({ wso2Axios, apiId: event.ResourceProperties.apiId, @@ -102,6 +102,7 @@ const createOrUpdateWso2ApiSubscription = async ( apiSearchParameters: event.ResourceProperties.apiSearchParameters, }); + console.log(`Verifying if WSO2 Application ${event.ResourceProperties.applicationId} exists in WSO2...`); const wso2Application = await getWso2Application({ wso2Axios, applicationId: event.ResourceProperties.applicationId, diff --git a/lib/src/wso2/wso2-api-subscription/handler/wso2-v1.ts b/lib/src/wso2/wso2-api-subscription/handler/wso2-v1.ts index 362452f..0ea6910 100644 --- a/lib/src/wso2/wso2-api-subscription/handler/wso2-v1.ts +++ b/lib/src/wso2/wso2-api-subscription/handler/wso2-v1.ts @@ -43,7 +43,7 @@ export const getWso2Api = async ({ }); if (!apiDetails) { - throw new Error('Cannot find the WSO2 API is related to this Custom Resource.'); + throw new Error(`Cannot find the WSO2 API from the provided search parameters (name=${apiSearchParameters.name}; version=${apiSearchParameters.version}; context=${apiSearchParameters.context})`); } return apiDetails; From cbf3e2f7b18a21c0327c0c484a459d0281e02fdd Mon Sep 17 00:00:00 2001 From: Marcio Meier Date: Wed, 29 Jan 2025 14:57:08 +0100 Subject: [PATCH 03/13] chore: lint suggestions from code review --- lib/src/wso2/wso2-api-subscription/handler/index.ts | 4 +++- lib/src/wso2/wso2-api-subscription/handler/wso2-v1.ts | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/lib/src/wso2/wso2-api-subscription/handler/index.ts b/lib/src/wso2/wso2-api-subscription/handler/index.ts index 1fdfd9f..d23f603 100644 --- a/lib/src/wso2/wso2-api-subscription/handler/index.ts +++ b/lib/src/wso2/wso2-api-subscription/handler/index.ts @@ -102,7 +102,9 @@ const createOrUpdateWso2ApiSubscription = async ( apiSearchParameters: event.ResourceProperties.apiSearchParameters, }); - console.log(`Verifying if WSO2 Application ${event.ResourceProperties.applicationId} exists in WSO2...`); + console.log( + `Verifying if WSO2 Application ${event.ResourceProperties.applicationId} exists in WSO2...`, + ); const wso2Application = await getWso2Application({ wso2Axios, applicationId: event.ResourceProperties.applicationId, diff --git a/lib/src/wso2/wso2-api-subscription/handler/wso2-v1.ts b/lib/src/wso2/wso2-api-subscription/handler/wso2-v1.ts index 0ea6910..1e46268 100644 --- a/lib/src/wso2/wso2-api-subscription/handler/wso2-v1.ts +++ b/lib/src/wso2/wso2-api-subscription/handler/wso2-v1.ts @@ -43,7 +43,9 @@ export const getWso2Api = async ({ }); if (!apiDetails) { - throw new Error(`Cannot find the WSO2 API from the provided search parameters (name=${apiSearchParameters.name}; version=${apiSearchParameters.version}; context=${apiSearchParameters.context})`); + throw new Error( + `Cannot find the WSO2 API from the provided search parameters (name=${apiSearchParameters.name}; version=${apiSearchParameters.version}; context=${apiSearchParameters.context})`, + ); } return apiDetails; From b4b5fd3049d96b8d40fd765460a3fb57ce3ddb70 Mon Sep 17 00:00:00 2001 From: Marcio Meier Date: Wed, 29 Jan 2025 15:16:21 +0100 Subject: [PATCH 04/13] chore: update type definitions --- .../wso2-api-subscription/handler/wso2-v1.ts | 36 ++++++++++++------- .../wso2/wso2-api-subscription/v1/types.ts | 18 +++++++--- 2 files changed, 36 insertions(+), 18 deletions(-) diff --git a/lib/src/wso2/wso2-api-subscription/handler/wso2-v1.ts b/lib/src/wso2/wso2-api-subscription/handler/wso2-v1.ts index 1e46268..211c7af 100644 --- a/lib/src/wso2/wso2-api-subscription/handler/wso2-v1.ts +++ b/lib/src/wso2/wso2-api-subscription/handler/wso2-v1.ts @@ -10,7 +10,11 @@ import { getWso2ApplicationById, } from '../../wso2-application/handler/wso2-v1'; import { Wso2ApplicationInfo } from '../../wso2-application/v1/types'; -import { Subscription, SubscriptionList } from '../v1/types'; +import { + Wso2SubscriptionDefinition, + Wso2SubscriptionInfo, + Wso2SubscriptionList, +} from '../v1/types'; export type GetWso2ApiArgs = Pick & { wso2Axios: AxiosInstance; @@ -95,8 +99,8 @@ export const findWso2ApiSubscription = async ({ wso2Axios, apiId, applicationId, -}: FindWso2ApiSubscriptionArgs): Promise => { - const apil = await wso2Axios.get(`/api/am/store/v1/subscriptions`, { +}: FindWso2ApiSubscriptionArgs): Promise => { + const apil = await wso2Axios.get(`/api/am/store/v1/subscriptions`, { params: { apiId, applicationId, @@ -136,8 +140,10 @@ export type GetWso2ApiSubscriptionByIdArgs = { const getWso2ApiSubscriptionById = async ({ wso2Axios, subscriptionId, -}: GetWso2ApiSubscriptionByIdArgs): Promise => { - const res = await wso2Axios.get(`/api/am/store/v1/subscriptions/${subscriptionId}`); +}: GetWso2ApiSubscriptionByIdArgs): Promise => { + const res = await wso2Axios.get( + `/api/am/store/v1/subscriptions/${subscriptionId}`, + ); return res.data; }; @@ -145,7 +151,7 @@ export type CreateWso2ApiSubscriptionArgs = Pick => { - const payload: Subscription = { +}: CreateWso2ApiSubscriptionArgs): Promise => { + const payload: Wso2SubscriptionDefinition = { applicationId, apiId, throttlingPolicy, }; const res = await backOff( - async () => wso2Axios.post(`/api/am/store/v1/subscriptions`, payload), + async () => wso2Axios.post(`/api/am/store/v1/subscriptions`, payload), retryOptions?.mutationRetries, ); @@ -170,7 +176,7 @@ export const createWso2ApiSubscription = async ({ await backOff(async () => { await getWso2ApiSubscriptionById({ wso2Axios, - subscriptionId: res.data.subscriptionId!, + subscriptionId: res.data.subscriptionId, }); }, retryOptions?.checkRetries); @@ -188,15 +194,19 @@ export const updateWso2ApiSubscription = async ({ applicationId, throttlingPolicy, retryOptions, -}: UpdateWso2ApiSubscriptionArgs): Promise => { - const payload: Subscription = { +}: UpdateWso2ApiSubscriptionArgs): Promise => { + const payload: Wso2SubscriptionDefinition = { applicationId, apiId, throttlingPolicy, }; const res = await backOff( - async () => wso2Axios.post(`/api/am/store/v1/subscriptions/${subscriptionId}`, payload), + async () => + wso2Axios.put( + `/api/am/store/v1/subscriptions/${subscriptionId}`, + payload, + ), retryOptions?.mutationRetries, ); diff --git a/lib/src/wso2/wso2-api-subscription/v1/types.ts b/lib/src/wso2/wso2-api-subscription/v1/types.ts index 2a59c7f..6107f70 100644 --- a/lib/src/wso2/wso2-api-subscription/v1/types.ts +++ b/lib/src/wso2/wso2-api-subscription/v1/types.ts @@ -1,7 +1,15 @@ import { ApiFromListV1 } from '../../wso2-api/v1/types'; import { Wso2ApplicationInfo } from '../../wso2-application/v1/types'; -export type Subscription = { +export type Wso2SubscriptionInfo = Wso2SubscriptionDefinition & { + /** + * Subscription Id + * @example 123-456-789 + */ + subscriptionId: string; +}; + +export type Wso2SubscriptionDefinition = { /** * The UUID of the subscription */ @@ -15,13 +23,13 @@ export type Subscription = { /** * The unique identifier of the API. */ - apiId?: string; + apiId: string; apiInfo?: ApiFromListV1; applicationInfo?: Wso2ApplicationInfo; - throttlingPolicy: string; + throttlingPolicy: 'Unlimited' | 'Bronze' | 'Silver' | 'Gold' | string; requestedThrottlingPolicy?: string; @@ -39,12 +47,12 @@ export type Subscription = { redirectionParams?: string; }; -export type SubscriptionList = { +export type Wso2SubscriptionList = { /** * Number of Subscriptions returned. */ count?: number; - list?: Array; + list?: Array; pagination?: Pagination; }; From 9ad4a467f0e1ddcadb1ddc372891ac08c359e205 Mon Sep 17 00:00:00 2001 From: Marcio Meier Date: Wed, 29 Jan 2025 15:16:53 +0100 Subject: [PATCH 05/13] chore: add failIfExists capabilities --- lib/src/wso2/wso2-api-subscription/handler/index.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/lib/src/wso2/wso2-api-subscription/handler/index.ts b/lib/src/wso2/wso2-api-subscription/handler/index.ts index d23f603..2458fae 100644 --- a/lib/src/wso2/wso2-api-subscription/handler/index.ts +++ b/lib/src/wso2/wso2-api-subscription/handler/index.ts @@ -117,6 +117,12 @@ const createOrUpdateWso2ApiSubscription = async ( applicationId: wso2Application.applicationId!, }); + if (event.RequestType === 'Create' && wso2Subscription && event.ResourceProperties.failIfExists) { + throw new Error( + `WSO2 Subscription '${wso2Subscription.subscriptionId}' already exists but cannot be managed by this resource. Change 'failIfExists' to change this behavior`, + ); + } + if ( wso2Subscription && wso2Subscription.throttlingPolicy === event.ResourceProperties.throttlingPolicy From 4f681b2296a54158db1b44ac81c0ad782fe88419 Mon Sep 17 00:00:00 2001 From: Marcio Meier Date: Wed, 29 Jan 2025 17:45:23 +0100 Subject: [PATCH 06/13] feat: apply defaults for retry options --- .../wso2/wso2-api-subscription/handler/index.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/lib/src/wso2/wso2-api-subscription/handler/index.ts b/lib/src/wso2/wso2-api-subscription/handler/index.ts index 2458fae..10db043 100644 --- a/lib/src/wso2/wso2-api-subscription/handler/index.ts +++ b/lib/src/wso2/wso2-api-subscription/handler/index.ts @@ -3,7 +3,7 @@ import { AxiosInstance } from 'axios'; import { CdkCustomResourceEvent, CdkCustomResourceResponse } from 'aws-lambda'; import { prepareAxiosForWso2Calls } from '../../wso2-utils'; -import { truncateStr } from '../../utils'; +import { applyRetryDefaults, truncateStr } from '../../utils'; import { Wso2ApiSubscriptionProps } from '../types'; import { @@ -15,7 +15,7 @@ import { updateWso2ApiSubscription, } from './wso2-v1'; -export type Wso2ApiCustomResourceEvent = CdkCustomResourceEvent & { +export type Wso2ApiSubscriptionCustomResourceEvent = CdkCustomResourceEvent & { ResourceProperties: Wso2ApiSubscriptionProps; }; @@ -31,7 +31,7 @@ export type Wso2ApiCustomResourceResponse = CdkCustomResourceResponse & { }; export const handler = async ( - event: Wso2ApiCustomResourceEvent, + event: Wso2ApiSubscriptionCustomResourceEvent, ): Promise => { const response: Wso2ApiCustomResourceResponse = { StackId: event.StackId, @@ -69,7 +69,7 @@ export const handler = async ( await removeWso2ApiSubscription({ wso2Axios, subscriptionId: event.PhysicalResourceId, - retryOptions: event.ResourceProperties.retryOptions, + retryOptions: applyRetryDefaults(event.ResourceProperties.retryOptions), }); return { @@ -91,7 +91,7 @@ export const handler = async ( }; const createOrUpdateWso2ApiSubscription = async ( - event: Wso2ApiCustomResourceEvent, + event: Wso2ApiSubscriptionCustomResourceEvent, wso2Axios: AxiosInstance, ): Promise<{ wso2ApiId: string; subscriptionId: string; applicationId: string }> => { console.log(`Verifying if WSO2 API ${event.ResourceProperties.apiId} exists in WSO2...`); @@ -143,7 +143,7 @@ const createOrUpdateWso2ApiSubscription = async ( apiId: wso2Api.id!, applicationId: wso2Application.applicationId!, throttlingPolicy: event.ResourceProperties.throttlingPolicy, - retryOptions: event.ResourceProperties.retryOptions, + retryOptions: applyRetryDefaults(event.ResourceProperties.retryOptions), }); return { @@ -159,7 +159,7 @@ const createOrUpdateWso2ApiSubscription = async ( apiId: wso2Api.id!, applicationId: wso2Application.applicationId!, throttlingPolicy: event.ResourceProperties.throttlingPolicy, - retryOptions: event.ResourceProperties.retryOptions, + retryOptions: applyRetryDefaults(event.ResourceProperties.retryOptions), }); return { From ad1c3c04229fab52b8eb4024b2ac3f5958efd5e1 Mon Sep 17 00:00:00 2001 From: Marcio Meier Date: Wed, 29 Jan 2025 17:45:41 +0100 Subject: [PATCH 07/13] feat: allow 404 status code for deleting subscriptions --- lib/src/wso2/wso2-api-subscription/handler/wso2-v1.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/lib/src/wso2/wso2-api-subscription/handler/wso2-v1.ts b/lib/src/wso2/wso2-api-subscription/handler/wso2-v1.ts index 211c7af..be4c8f1 100644 --- a/lib/src/wso2/wso2-api-subscription/handler/wso2-v1.ts +++ b/lib/src/wso2/wso2-api-subscription/handler/wso2-v1.ts @@ -224,7 +224,13 @@ export const removeWso2ApiSubscription = async ({ retryOptions, }: RemoveWso2ApiSubscriptionArgs): Promise => { await backOff( - async () => wso2Axios.delete(`/api/am/store/v1/subscriptions/${subscriptionId}`), + async () => + wso2Axios.delete(`/api/am/store/v1/subscriptions/${subscriptionId}`, { + validateStatus(status) { + // If it returns 404, the api is already deleted + return status === 200 || status === 404; + }, + }), retryOptions?.mutationRetries, ); }; From 3fab069fa2eecb28ee408941871996141ed6cd61 Mon Sep 17 00:00:00 2001 From: Marcio Meier Date: Wed, 29 Jan 2025 17:46:06 +0100 Subject: [PATCH 08/13] chore: enhance types and zod schema --- lib/src/wso2/wso2-api-subscription/types.ts | 2 +- lib/src/wso2/wso2-api-subscription/wso2-api-subscription.ts | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/src/wso2/wso2-api-subscription/types.ts b/lib/src/wso2/wso2-api-subscription/types.ts index a10174a..27b7eff 100644 --- a/lib/src/wso2/wso2-api-subscription/types.ts +++ b/lib/src/wso2/wso2-api-subscription/types.ts @@ -11,5 +11,5 @@ export type Wso2ApiSubscriptionProps = Wso2BaseProperties & { applicationSearchParameters?: { name: string; }; - throttlingPolicy: 'Unlimited' | 'Gold' | 'Silver' | 'Bronze'; + throttlingPolicy: 'Unlimited' | 'Gold' | 'Silver' | 'Bronze' | string; }; diff --git a/lib/src/wso2/wso2-api-subscription/wso2-api-subscription.ts b/lib/src/wso2/wso2-api-subscription/wso2-api-subscription.ts index 9d9424c..51b0597 100644 --- a/lib/src/wso2/wso2-api-subscription/wso2-api-subscription.ts +++ b/lib/src/wso2/wso2-api-subscription/wso2-api-subscription.ts @@ -97,6 +97,9 @@ export const validateProps = (props: Wso2ApiSubscriptionProps): void => { wso2Config: z.object({ baseApiUrl: z.string(), credentialsSecretId: z.string(), + tenant: z.string().optional(), + apiVersion: z.string().optional(), + credentialsSecretKMSKeyId: z.string().optional(), }), }) .and(apiSchema) From 4db369c32fed7668808057fd88b90250b08d8e9f Mon Sep 17 00:00:00 2001 From: Marcio Meier Date: Wed, 29 Jan 2025 17:48:51 +0100 Subject: [PATCH 09/13] test: add utility fot nock wso2 sdk --- lib/src/wso2/__tests__/wso2-utils.ts | 30 ++++++++++++++++ .../wso2-application/handler/index.test.ts | 35 +++---------------- 2 files changed, 34 insertions(+), 31 deletions(-) create mode 100644 lib/src/wso2/__tests__/wso2-utils.ts diff --git a/lib/src/wso2/__tests__/wso2-utils.ts b/lib/src/wso2/__tests__/wso2-utils.ts new file mode 100644 index 0000000..96b50fb --- /dev/null +++ b/lib/src/wso2/__tests__/wso2-utils.ts @@ -0,0 +1,30 @@ +import { GetSecretValueCommand, SecretsManagerClient } from '@aws-sdk/client-secrets-manager'; +import { mockClient } from 'aws-sdk-client-mock'; +import nock from 'nock'; + +export const nockBasicWso2SDK = (baseWso2Url: string): void => { + const secretMock = mockClient(SecretsManagerClient); + secretMock.on(GetSecretValueCommand).resolves({ + SecretBinary: Buffer.from(JSON.stringify({ user: 'user1', pwd: 'pwd1' })), + }); + + // register client mock + nock(baseWso2Url).post('/client-registration/v0.17/register').reply(200, { + clientId: 'clientId1', + clientSecret: 'clientSecret1', + }); + + // get token mock + nock(baseWso2Url).post('/oauth2/token').reply(200, { + // eslint-disable-next-line camelcase + access_token: '1111-1111-1111', + }); + + // mock server check + nock(baseWso2Url) + .get('/services/Version') + .reply( + 200, + 'WSO2 API Manager-3.2.0', + ); +}; diff --git a/lib/src/wso2/wso2-application/handler/index.test.ts b/lib/src/wso2/wso2-application/handler/index.test.ts index 4096e1d..916b9e6 100644 --- a/lib/src/wso2/wso2-application/handler/index.test.ts +++ b/lib/src/wso2/wso2-application/handler/index.test.ts @@ -3,11 +3,10 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ import nock from 'nock'; -import { mockClient } from 'aws-sdk-client-mock'; -import { GetSecretValueCommand, SecretsManagerClient } from '@aws-sdk/client-secrets-manager'; import { Wso2ApplicationDefinition } from '../v1/types'; import { Wso2ApplicationCustomResourceProperties } from '../types'; +import { nockBasicWso2SDK } from '../../__tests__/wso2-utils'; import { Wso2ApplicationCustomResourceEvent, handler } from './index'; @@ -43,7 +42,7 @@ describe('wso2 application custom resource lambda', () => { }); it('wso2 application delete', async () => { - nockBasicWso2SDK(); + nockBasicWso2SDK(baseWso2Url); // application get mock nock(baseWso2Url) @@ -56,7 +55,7 @@ describe('wso2 application custom resource lambda', () => { }); it('basic wso2 application update', async () => { - nockBasicWso2SDK(); + nockBasicWso2SDK(baseWso2Url); const testDefs: Wso2ApplicationDefinition = testApplicationDefs(); @@ -89,7 +88,7 @@ describe('wso2 application custom resource lambda', () => { }); it('basic wso2 application create', async () => { - nockBasicWso2SDK(); + nockBasicWso2SDK(baseWso2Url); // api list mock nock(baseWso2Url) @@ -183,30 +182,4 @@ describe('wso2 application custom resource lambda', () => { applicationDefinition: testApplicationDefs(), retryOptions: testRetryOptions, }; - - const nockBasicWso2SDK = (): void => { - const secretMock = mockClient(SecretsManagerClient); - secretMock.on(GetSecretValueCommand).resolves({ - SecretBinary: Buffer.from(JSON.stringify({ user: 'user1', pwd: 'pwd1' })), - }); - - // register client mock - nock(baseWso2Url).post('/client-registration/v0.17/register').reply(200, { - clientId: 'clientId1', - clientSecret: 'clientSecret1', - }); - - // get token mock - nock(baseWso2Url).post('/oauth2/token').reply(200, { - access_token: '1111-1111-1111', - }); - - // mock server check - nock(baseWso2Url) - .get('/services/Version') - .reply( - 200, - 'WSO2 API Manager-3.2.0', - ); - }; }); From e97f39e7b8b507634e67962c2b1c6e64d8cc6b29 Mon Sep 17 00:00:00 2001 From: Marcio Meier Date: Wed, 29 Jan 2025 17:49:56 +0100 Subject: [PATCH 10/13] test: add tests for wso2 api subscription handler --- .../handler/index.test.ts | 645 ++++++++++++++++++ 1 file changed, 645 insertions(+) create mode 100644 lib/src/wso2/wso2-api-subscription/handler/index.test.ts diff --git a/lib/src/wso2/wso2-api-subscription/handler/index.test.ts b/lib/src/wso2/wso2-api-subscription/handler/index.test.ts new file mode 100644 index 0000000..c36debb --- /dev/null +++ b/lib/src/wso2/wso2-api-subscription/handler/index.test.ts @@ -0,0 +1,645 @@ +/* eslint-disable no-console */ +import nock from 'nock'; + +import { Wso2SubscriptionDefinition } from '../v1/types'; +import { Wso2ApiSubscriptionProps } from '../types'; +import { nockBasicWso2SDK } from '../../__tests__/wso2-utils'; + +import { handler, Wso2ApiSubscriptionCustomResourceEvent } from './index'; + +const baseWso2Url = 'https://mywso2.com'; + +const testRetryOptions = { + checkRetries: { + startingDelay: 100, + delayFirstAttempt: true, + maxDelay: 100, + numOfAttempts: 3, + timeMultiple: 1.1, + }, + mutationRetries: { + startingDelay: 100, + delayFirstAttempt: true, + maxDelay: 100, + numOfAttempts: 3, + timeMultiple: 1.1, + }, +}; + +const originalConsoleLog = console.log; + +describe('wso2 subscription custom resource lambda', () => { + describe('request type create', () => { + beforeEach(() => { + nock.cleanAll(); + // silence verbose console logs. comment this for debugging + console.log = (): void => {}; + }); + + afterEach(() => { + console.log = originalConsoleLog; + }); + + it('should create the subscription', async () => { + nockBasicWso2SDK(baseWso2Url); + const testDefs: Wso2SubscriptionDefinition = testSubscriptionDefs(); + + // get api mock + nock(baseWso2Url) + .get(/.*\/publisher\/v1\/apis\/111-222/) + .times(1) + .reply(200, { id: '111-222' }); + + // get application mock + nock(baseWso2Url) + .get(/.*\/store\/v1\/applications\/333-444/) + .times(1) + .reply(200, { applicationId: '333-444' }); + + // subscriptions list mock + nock(baseWso2Url) + .get(/.*\/store\/v1\/subscriptions.*/) + .query(true) + .times(1) + .reply(200, { list: [] }); + + // subscription get mock + const getSubscriptionNock = nock(baseWso2Url) + .get(/.*\/store\/v1\/subscriptions\/123-456/) + .times(1) // check if was created + .reply(200, { ...testDefs, throttlingPolicy: 'Unlimited' }); + + // subscription create mock + const createNock = nock(baseWso2Url) + .post(/.*\/store\/v1\/subscriptions/) + .times(1) + .reply(200, { subscriptionId: '123-456' }); + + const eres = await handler(testCFNEventCreate(testEvent)); + expect(eres.Status).toBe('SUCCESS'); + + // make sure that the subscription create was called + createNock.done(); + getSubscriptionNock.done(); + }); + + it('should create the subscription by searching for api and application', async () => { + nockBasicWso2SDK(baseWso2Url); + const testDefs: Wso2SubscriptionDefinition = testSubscriptionDefs(); + + const apiSearchParameters = { + name: 'my-api', + version: 'v1', + context: '/my-api', + }; + + // search api mock + nock(baseWso2Url) + .get(/.*\/publisher\/v1\/apis/) + .query({ query: 'name:my-api version:v1 context:/my-api' }) + .times(1) + .reply(200, { list: [{ id: '111-222', ...apiSearchParameters }] }); + + // get application mock + nock(baseWso2Url) + .get(/.*\/store\/v1\/applications/) + .query({ query: 'my-app' }) + .times(1) + .reply(200, { list: [{ applicationId: '333-444' }] }); + + // subscriptions list mock + nock(baseWso2Url) + .get(/.*\/store\/v1\/subscriptions.*/) + .query(true) + .times(1) // check create or update + .reply(200, { list: [] }); + + // subscription get mock + const getSubscriptionNock = nock(baseWso2Url) + .get(/.*\/store\/v1\/subscriptions\/123-456/) + .times(1) + .reply(200, { ...testDefs, throttlingPolicy: 'Unlimited' }); + + // subscription create mock + const createNock = nock(baseWso2Url) + .post(/.*\/store\/v1\/subscriptions/) + .times(1) + .reply(200, { subscriptionId: '123-456' }); + + const eres = await handler( + testCFNEventCreate({ + ...testEvent, + // eslint-disable-next-line no-undefined + apiId: undefined, + // eslint-disable-next-line no-undefined + applicationId: undefined, + apiSearchParameters, + applicationSearchParameters: { + name: 'my-app', + }, + }), + ); + expect(eres.Status).toBe('SUCCESS'); + + // make sure that the subscription create was called + createNock.done(); + getSubscriptionNock.done(); + }); + + it('should skip update subscription when no changes are detected', async () => { + nockBasicWso2SDK(baseWso2Url); + const testDefs: Wso2SubscriptionDefinition = testSubscriptionDefs(); + + // get api mock + nock(baseWso2Url) + .get(/.*\/publisher\/v1\/apis\/111-222/) + .times(1) + .reply(200, { id: '111-222' }); + + // get application mock + nock(baseWso2Url) + .get(/.*\/store\/v1\/applications\/333-444/) + .times(1) + .reply(200, { applicationId: '333-444' }); + + // subscriptions list mock + nock(baseWso2Url) + .get(/.*\/store\/v1\/subscriptions.*/) + .query(true) + .times(1) + .reply(200, { list: [{ ...testDefs, subscriptionId: '123-456' }] }); + + // subscription create mock + const createNock = nock(baseWso2Url) + .post(/.*\/store\/v1\/subscriptions/) + .times(1) + .reply(200, { subscriptionId: '123-456' }); + + // subscription update mock + const updateNock = nock(baseWso2Url) + .put(/.*\/store\/v1\/subscriptions\/123-456/) + .times(1) + .reply(200); + + const eres = await handler(testCFNEventCreate(testEvent)); + expect(eres.Status).toBe('SUCCESS'); + + expect(updateNock.isDone()).toBeFalsy(); + expect(createNock.isDone()).toBeFalsy(); + }); + + it('should update subscription when throttling policy changes', async () => { + nockBasicWso2SDK(baseWso2Url); + const testDefs: Wso2SubscriptionDefinition = testSubscriptionDefs('Gold'); + + // get api mock + nock(baseWso2Url) + .get(/.*\/publisher\/v1\/apis\/111-222/) + .times(1) + .reply(200, { id: '111-222' }); + + // get application mock + nock(baseWso2Url) + .get(/.*\/store\/v1\/applications\/333-444/) + .times(1) + .reply(200, { applicationId: '333-444' }); + + // subscriptions list mock + nock(baseWso2Url) + .get(/.*\/store\/v1\/subscriptions.*/) + .query(true) + .times(1) + .reply(200, { list: [{ ...testDefs, subscriptionId: '123-456' }] }); + + // subscription create mock + const createNock = nock(baseWso2Url) + .post(/.*\/store\/v1\/subscriptions/) + .times(1) + .reply(200, { subscriptionId: '123-456' }); + + // subscription update mock + const updateNock = nock(baseWso2Url) + .put(/.*\/store\/v1\/subscriptions\/123-456/) + .times(1) + .reply(200); + + const eres = await handler(testCFNEventCreate(testEvent)); + expect(eres.Status).toBe('SUCCESS'); + + updateNock.done(); + expect(createNock.isDone()).toBeFalsy(); + }); + + it('should fail if subscription already exists and failIfExists is true', async () => { + nockBasicWso2SDK(baseWso2Url); + const testDefs: Wso2SubscriptionDefinition = testSubscriptionDefs(); + + // get api mock + nock(baseWso2Url) + .get(/.*\/publisher\/v1\/apis\/111-222/) + .times(1) + .reply(200, { id: '111-222' }); + + // get application mock + nock(baseWso2Url) + .get(/.*\/store\/v1\/applications\/333-444/) + .times(1) + .reply(200, { applicationId: '333-444' }); + + // subscriptions list mock + nock(baseWso2Url) + .get(/.*\/store\/v1\/subscriptions.*/) + .query(true) + .times(1) + .reply(200, { list: [{ ...testDefs, subscriptionId: '123-456' }] }); + + // subscription create mock + const createNock = nock(baseWso2Url) + .post(/.*\/store\/v1\/subscriptions/) + .times(1) + .reply(200, { subscriptionId: '123-456' }); + + // subscription update mock + const updateNock = nock(baseWso2Url) + .put(/.*\/store\/v1\/subscriptions\/123-456/) + .times(1) + .reply(200); + + await expect( + handler(testCFNEventCreate({ ...testEvent, failIfExists: true })), + ).rejects.toThrow( + `Error: WSO2 Subscription '123-456' already exists but cannot be managed by this resource. Change 'failIfExists' to change this behavior`, + ); + + expect(updateNock.isDone()).toBeFalsy(); + expect(createNock.isDone()).toBeFalsy(); + }); + }); + + describe('request type update', () => { + beforeEach(() => { + nock.cleanAll(); + // silence verbose console logs. comment this for debugging + console.log = (): void => {}; + }); + afterEach(() => { + console.log = originalConsoleLog; + }); + + it('should update the subscription', async () => { + nockBasicWso2SDK(baseWso2Url); + const testDefs: Wso2SubscriptionDefinition = testSubscriptionDefs('Gold'); + + // get api mock + nock(baseWso2Url) + .get(/.*\/publisher\/v1\/apis\/111-222/) + .times(1) + .reply(200, { id: '111-222' }); + + // get application mock + nock(baseWso2Url) + .get(/.*\/store\/v1\/applications\/333-444/) + .times(1) + .reply(200, { applicationId: '333-444' }); + + // subscriptions list mock + nock(baseWso2Url) + .get(/.*\/store\/v1\/subscriptions.*/) + .query(true) + .times(1) + .reply(200, { list: [{ ...testDefs, subscriptionId: '123-456' }] }); + + // subscription update mock + const updateNock = nock(baseWso2Url) + .put(/.*\/store\/v1\/subscriptions\/123-456/) + .times(1) + .reply(200); + + const eres = await handler(testCFNEventUpdate(testEvent, '123-456')); + expect(eres.Status).toBe('SUCCESS'); + + // make sure that the subscription update was called + updateNock.done(); + }); + + it('should update the subscription by searching for api and application', async () => { + nockBasicWso2SDK(baseWso2Url); + const testDefs: Wso2SubscriptionDefinition = testSubscriptionDefs('Gold'); + + const apiSearchParameters = { + name: 'my-api', + version: 'v1', + context: '/my-api', + }; + + // search api mock + nock(baseWso2Url) + .get(/.*\/publisher\/v1\/apis/) + .query({ query: 'name:my-api version:v1 context:/my-api' }) + .times(1) + .reply(200, { list: [{ id: '111-222', ...apiSearchParameters }] }); + + // get application mock + nock(baseWso2Url) + .get(/.*\/store\/v1\/applications/) + .query({ query: 'my-app' }) + .times(1) + .reply(200, { list: [{ applicationId: '333-444' }] }); + + // subscriptions list mock + nock(baseWso2Url) + .get(/.*\/store\/v1\/subscriptions.*/) + .query(true) + .times(1) // check create or update + .reply(200, { list: [{ ...testDefs, subscriptionId: '123-456' }] }); + + // subscription update mock + const updateNock = nock(baseWso2Url) + .put(/.*\/store\/v1\/subscriptions\/123-456/) + .times(1) + .reply(200); + + const eres = await handler( + testCFNEventUpdate( + { + ...testEvent, + // eslint-disable-next-line no-undefined + apiId: undefined, + // eslint-disable-next-line no-undefined + applicationId: undefined, + apiSearchParameters, + applicationSearchParameters: { + name: 'my-app', + }, + }, + '123-456', + ), + ); + expect(eres.Status).toBe('SUCCESS'); + + // make sure that the subscription update was called + updateNock.done(); + }); + + it('should skip update subscription when no changes are detected', async () => { + nockBasicWso2SDK(baseWso2Url); + const testDefs: Wso2SubscriptionDefinition = testSubscriptionDefs(); + + // get api mock + nock(baseWso2Url) + .get(/.*\/publisher\/v1\/apis\/111-222/) + .times(1) + .reply(200, { id: '111-222' }); + + // get application mock + nock(baseWso2Url) + .get(/.*\/store\/v1\/applications\/333-444/) + .times(1) + .reply(200, { applicationId: '333-444' }); + + // subscriptions list mock + nock(baseWso2Url) + .get(/.*\/store\/v1\/subscriptions.*/) + .query(true) + .times(1) // check create or update + .reply(200, { list: [{ ...testDefs, subscriptionId: '123-456' }] }); + + // subscription update mock + const updateNock = nock(baseWso2Url) + .put(/.*\/store\/v1\/subscriptions\/123-456/) + .reply(200); + + const eres = await handler(testCFNEventUpdate(testEvent, '123-456')); + expect(eres.Status).toBe('SUCCESS'); + expect(updateNock.isDone()).toBeFalsy(); + }); + }); + + describe('request type delete', () => { + beforeEach(() => { + nock.cleanAll(); + // silence verbose console logs. comment this for debugging + console.log = (): void => {}; + }); + afterEach(() => { + console.log = originalConsoleLog; + }); + + it('should delete the subscription', async () => { + nockBasicWso2SDK(baseWso2Url); + + // subscription delete mock + nock(baseWso2Url) + .delete(/.*\/store\/v1\/subscriptions\/123-456/) + .times(1) + .reply(200); + + const eres = await handler(testCFNEventDelete(testEvent, '123-456')); + expect(eres.Status).toBe('SUCCESS'); + }); + + it('should return success when the subscription is already deleted', async () => { + nockBasicWso2SDK(baseWso2Url); + + // subscription delete mock + nock(baseWso2Url) + .delete(/.*\/store\/v1\/subscriptions\/123-456/) + .times(1) + .reply(404, { message: 'subscription not found' }); + + const eres = await handler(testCFNEventDelete(testEvent, '123-456')); + expect(eres.Status).toBe('SUCCESS'); + }); + }); + + describe('retries', () => { + beforeEach(() => { + nock.cleanAll(); + // silence verbose console logs. comment this for debugging + console.log = (): void => {}; + }); + afterEach(() => { + console.log = originalConsoleLog; + }); + + it('should retry on create subscription', async () => { + nockBasicWso2SDK(baseWso2Url); + const testDefs: Wso2SubscriptionDefinition = testSubscriptionDefs(); + + // get api mock + nock(baseWso2Url) + .get(/.*\/publisher\/v1\/apis\/111-222/) + .times(1) + .reply(200, { id: '111-222' }); + + // get application mock + nock(baseWso2Url) + .get(/.*\/store\/v1\/applications\/333-444/) + .times(1) + .reply(200, { applicationId: '333-444' }); + + // subscriptions list mock + nock(baseWso2Url) + .get(/.*\/store\/v1\/subscriptions.*/) + .query(true) + .times(1) + .reply(200, { list: [] }); + + // subscription get mock (checks if the api was created) + const getSubscriptionFailNock = nock(baseWso2Url) + .get(/.*\/store\/v1\/subscriptions\/123-456/) + .times(2) + .reply(500); + const getSubscriptionSuccessNock = nock(baseWso2Url) + .get(/.*\/store\/v1\/subscriptions\/123-456/) + .times(1) + .reply(200, testDefs); + + // subscription create mock + const createFailNock = nock(baseWso2Url) + .post(/.*\/store\/v1\/subscriptions/) + .times(2) + .reply(500); + + const createSuccessNock = nock(baseWso2Url) + .post(/.*\/store\/v1\/subscriptions/) + .times(1) + .reply(200, { subscriptionId: '123-456' }); + + const eres = await handler(testCFNEventCreate(testEvent)); + expect(eres.Status).toBe('SUCCESS'); + + createFailNock.done(); + createSuccessNock.done(); + getSubscriptionFailNock.done(); + getSubscriptionSuccessNock.done(); + }); + + it('should retry on update subscription', async () => { + nockBasicWso2SDK(baseWso2Url); + const testDefs: Wso2SubscriptionDefinition = testSubscriptionDefs('Gold'); + + // get api mock + nock(baseWso2Url) + .get(/.*\/publisher\/v1\/apis\/111-222/) + .times(1) + .reply(200, { id: '111-222' }); + + // get application mock + nock(baseWso2Url) + .get(/.*\/store\/v1\/applications\/333-444/) + .times(1) + .reply(200, { applicationId: '333-444' }); + + // subscriptions list mock + nock(baseWso2Url) + .get(/.*\/store\/v1\/subscriptions.*/) + .query(true) + .times(1) + .reply(200, { list: [{ ...testDefs, subscriptionId: '123-456' }] }); + + // subscription update mock + const updateFailNock = nock(baseWso2Url) + .put(/.*\/store\/v1\/subscriptions\/123-456/) + .times(2) + .reply(500); + + const updateSuccessNock = nock(baseWso2Url) + .put(/.*\/store\/v1\/subscriptions\/123-456/) + .times(1) + .reply(200, { subscriptionId: '123-456' }); + + const eres = await handler(testCFNEventUpdate(testEvent, '123-456')); + expect(eres.Status).toBe('SUCCESS'); + + updateFailNock.done(); + updateSuccessNock.done(); + }); + + it('should retry on delete subscription', async () => { + nockBasicWso2SDK(baseWso2Url); + + // subscription delete mock + const deleteFailMock = nock(baseWso2Url) + .delete(/.*\/store\/v1\/subscriptions\/123-456/) + .times(2) + .reply(500); + + const deleteSuccessMock = nock(baseWso2Url) + .delete(/.*\/store\/v1\/subscriptions\/123-456/) + .times(1) + .reply(200); + + const eres = await handler(testCFNEventDelete(testEvent, '123-456')); + expect(eres.Status).toBe('SUCCESS'); + + deleteFailMock.done(); + deleteSuccessMock.done(); + }); + }); + + const testSubscriptionDefs = ( + throttlingPolicy?: Wso2SubscriptionDefinition['throttlingPolicy'], + ): Wso2SubscriptionDefinition => { + return { + apiId: '111-222', + applicationId: '333-444', + throttlingPolicy: throttlingPolicy ?? 'Unlimited', + }; + }; + + const commonEvt = { + StackId: 'test-stack', + RequestId: '123-123123', + LogicalResourceId: 'abc abc', + ServiceToken: 'arn:somelambdatest', + ResponseURL: 's3bucketxxx', + ResourceType: 'wso2subscription', + }; + + const testCFNEventCreate = ( + baseProperties: Wso2ApiSubscriptionProps, + ): Wso2ApiSubscriptionCustomResourceEvent => { + return { + ...commonEvt, + RequestType: 'Create', + ResourceProperties: { ...baseProperties, ServiceToken: 'arn:somelambdatest' }, + }; + }; + + const testCFNEventUpdate = ( + baseProperties: Wso2ApiSubscriptionProps, + PhysicalResourceId: string, + OldResourceProperties: Record = {}, + ): Wso2ApiSubscriptionCustomResourceEvent => { + return { + ...commonEvt, + RequestType: 'Update', + ResourceProperties: { ...baseProperties, ServiceToken: 'arn:somelambdatest' }, + PhysicalResourceId, + OldResourceProperties, + }; + }; + + const testCFNEventDelete = ( + baseProperties: Wso2ApiSubscriptionProps, + PhysicalResourceId: string, + ): Wso2ApiSubscriptionCustomResourceEvent => { + return { + ...commonEvt, + RequestType: 'Delete', + ResourceProperties: { ...baseProperties, ServiceToken: 'arn:somelambdatest' }, + PhysicalResourceId, + }; + }; + + const testEvent: Wso2ApiSubscriptionProps = { + wso2Config: { + baseApiUrl: baseWso2Url, + credentialsSecretId: 'arn:aws:secretsmanager:us-east-1:123123123:secret:MySecret', + apiVersion: 'v1', + }, + ...testSubscriptionDefs(), + retryOptions: testRetryOptions, + }; +}); From 5e84deb567408a06f4514292e28f27e4e9ed9550 Mon Sep 17 00:00:00 2001 From: Marcio Meier Date: Wed, 29 Jan 2025 17:54:27 +0100 Subject: [PATCH 11/13] test: use utility in wso2 api handler test --- lib/src/wso2/wso2-api/handler/index.test.ts | 45 +++++---------------- 1 file changed, 9 insertions(+), 36 deletions(-) diff --git a/lib/src/wso2/wso2-api/handler/index.test.ts b/lib/src/wso2/wso2-api/handler/index.test.ts index e3270b1..780aa67 100644 --- a/lib/src/wso2/wso2-api/handler/index.test.ts +++ b/lib/src/wso2/wso2-api/handler/index.test.ts @@ -3,12 +3,11 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ import nock from 'nock'; -import { mockClient } from 'aws-sdk-client-mock'; -import { GetSecretValueCommand, SecretsManagerClient } from '@aws-sdk/client-secrets-manager'; import { petstoreOpenapi } from '../__tests__/petstore'; import { ApiFromListV1, PublisherPortalAPIv1, Wso2ApiDefinitionV1 } from '../v1/types'; import { Wso2ApiCustomResourceProperties } from '../types'; +import { nockBasicWso2SDK } from '../../__tests__/wso2-utils'; import { Wso2ApiCustomResourceEvent, handler } from './index'; @@ -44,7 +43,7 @@ describe('wso2 custom resource lambda', () => { }); it('basic wso2 api create', async () => { - nockBasicWso2SDK(); + nockBasicWso2SDK(baseWso2Url); // api list mock nock(baseWso2Url) @@ -87,7 +86,7 @@ describe('wso2 custom resource lambda', () => { }); it('wso2 api create and PUBLISH', async () => { - nockBasicWso2SDK(); + nockBasicWso2SDK(baseWso2Url); // api list mock nock(baseWso2Url) @@ -134,7 +133,7 @@ describe('wso2 custom resource lambda', () => { }); it('basic wso2 api update', async () => { - nockBasicWso2SDK(); + nockBasicWso2SDK(baseWso2Url); // api list mock const testDefs: Wso2ApiDefinitionV1 = { @@ -182,7 +181,7 @@ describe('wso2 custom resource lambda', () => { }); it('basic wso2 api change on UPDATE operation', async () => { - nockBasicWso2SDK(); + nockBasicWso2SDK(baseWso2Url); // api list mock const testDefs: Wso2ApiDefinitionV1 = { @@ -227,7 +226,7 @@ describe('wso2 custom resource lambda', () => { }); it('should pass with success if wso2 answers properly after a few retries', async () => { - nockBasicWso2SDK(); + nockBasicWso2SDK(baseWso2Url); // api list mock const testDefs: Wso2ApiDefinitionV1 = { @@ -303,7 +302,7 @@ describe('wso2 custom resource lambda', () => { }); it('should fail after retrying checking WSO2 api for a few times', async () => { - nockBasicWso2SDK(); + nockBasicWso2SDK(baseWso2Url); // api list mock const testDefs: Wso2ApiDefinitionV1 = { @@ -363,7 +362,7 @@ describe('wso2 custom resource lambda', () => { }); it('basic wso2 api delete on DELETE operation', async () => { - nockBasicWso2SDK(); + nockBasicWso2SDK(baseWso2Url); // api update mock nock(baseWso2Url) @@ -382,7 +381,7 @@ describe('wso2 custom resource lambda', () => { }); it('should return success when api does not exists on DELETE operation', async () => { - nockBasicWso2SDK(); + nockBasicWso2SDK(baseWso2Url); // api update mock nock(baseWso2Url) @@ -480,32 +479,6 @@ describe('wso2 custom resource lambda', () => { retryOptions: testRetryOptions, }; - const nockBasicWso2SDK = (): void => { - const secretMock = mockClient(SecretsManagerClient); - secretMock.on(GetSecretValueCommand).resolves({ - SecretBinary: Buffer.from(JSON.stringify({ user: 'user1', pwd: 'pwd1' })), - }); - - // register client mock - nock(baseWso2Url).post('/client-registration/v0.17/register').reply(200, { - clientId: 'clientId1', - clientSecret: 'clientSecret1', - }); - - // get token mock - nock(baseWso2Url).post('/oauth2/token').reply(200, { - access_token: '1111-1111-1111', - }); - - // mock server check - nock(baseWso2Url) - .get('/services/Version') - .reply( - 200, - 'WSO2 API Manager-3.2.0', - ); - }; - const nockAfterCreateUpdateAndChangeLifecycleStatusInWso2 = ( testDefs: Wso2ApiDefinitionV1, lifecycleAction?: string, From 821c1a707ac8c99d320c3e0dc6262f6b8dc7628a Mon Sep 17 00:00:00 2001 From: Marcio Meier Date: Wed, 29 Jan 2025 17:59:26 +0100 Subject: [PATCH 12/13] chore(docs): enhance the construct docs --- .../wso2-api-subscription.ts | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/lib/src/wso2/wso2-api-subscription/wso2-api-subscription.ts b/lib/src/wso2/wso2-api-subscription/wso2-api-subscription.ts index 51b0597..a4da447 100644 --- a/lib/src/wso2/wso2-api-subscription/wso2-api-subscription.ts +++ b/lib/src/wso2/wso2-api-subscription/wso2-api-subscription.ts @@ -8,7 +8,12 @@ import { addLambdaAndProviderForWso2Operations } from '../utils-cdk'; import { Wso2ApiSubscriptionProps } from './types'; /** - * WSO2 API CDK construct for subscribing a WSO2 Application into a WSO2 API + * WSO2 API CDK construct for creating a WSO2 subscription from one application to an API + * This construct is related to one "physical" subscription in WSO2. + * + * The internal implementation tries to protect itself from various scenarios where larger or more complex + * WSO2 clusters might lead to out-of-order or delays in operations that happen asynchronously after the API + * accepts the requests, so for every mutation, there is a check to verify sanity. * * @example * @@ -43,6 +48,14 @@ export class Wso2ApiSubscription extends Construct { validateProps(props); + // Do as much of the logic in the construct as possible and leave only + // the minimal complexity to the Lambda Custom Resource as it's harder + // to debug and eventual errors will rollback the entire stack and will + // make the feedback cycle much longer. + + // Keep this construct stateless (don't access WSO2 apis) and + // leave the stateful part to the Lambda Custom Resource (accessing WSO2 apis etc) + const { customResourceProvider, customResourceFunction } = addLambdaAndProviderForWso2Operations({ scope: this, From ea609d8126976729bb5a2d27ec71542093fe3579 Mon Sep 17 00:00:00 2001 From: Marcio Meier Date: Wed, 29 Jan 2025 18:01:44 +0100 Subject: [PATCH 13/13] test: add tests for api subscription construct --- .../wso2-api-subscription.test.ts | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 lib/src/wso2/wso2-api-subscription/wso2-api-subscription.test.ts diff --git a/lib/src/wso2/wso2-api-subscription/wso2-api-subscription.test.ts b/lib/src/wso2/wso2-api-subscription/wso2-api-subscription.test.ts new file mode 100644 index 0000000..5041aff --- /dev/null +++ b/lib/src/wso2/wso2-api-subscription/wso2-api-subscription.test.ts @@ -0,0 +1,36 @@ +import { App, Stack } from 'aws-cdk-lib/core'; +import { Template } from 'aws-cdk-lib/assertions'; + +import { Wso2ApiSubscriptionProps } from './types'; +import { Wso2ApiSubscription } from './wso2-api-subscription'; + +describe('wso2-subscription-construct', () => { + it('minimal wso2 api', async () => { + const app = new App(); + const stack = new Stack(app); + + const testProps1 = testProps(); + const wso2Subscription = new Wso2ApiSubscription(stack, 'wso2', testProps1); + + expect(wso2Subscription.customResourceFunction).toBeDefined(); + + const template = Template.fromStack(stack); + + template.hasResourceProperties('Custom::Wso2ApiSubscription', { + // Should forward the props to the custom resource + ...testProps1, + }); + }); +}); + +const testProps = (): Wso2ApiSubscriptionProps => { + return { + wso2Config: { + baseApiUrl: 'http://localhost:8080/wso2', + credentialsSecretId: 'arn::creds', + }, + apiId: '1111-2222', + applicationId: '3333-4444', + throttlingPolicy: 'Unlimited', + }; +};