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-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,
+ };
+});
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..10db043
--- /dev/null
+++ b/lib/src/wso2/wso2-api-subscription/handler/index.ts
@@ -0,0 +1,170 @@
+/* eslint-disable no-console */
+import { AxiosInstance } from 'axios';
+import { CdkCustomResourceEvent, CdkCustomResourceResponse } from 'aws-lambda';
+
+import { prepareAxiosForWso2Calls } from '../../wso2-utils';
+import { applyRetryDefaults, truncateStr } from '../../utils';
+import { Wso2ApiSubscriptionProps } from '../types';
+
+import {
+ createWso2ApiSubscription,
+ findWso2ApiSubscription,
+ getWso2Api,
+ getWso2Application,
+ removeWso2ApiSubscription,
+ updateWso2ApiSubscription,
+} from './wso2-v1';
+
+export type Wso2ApiSubscriptionCustomResourceEvent = 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: Wso2ApiSubscriptionCustomResourceEvent,
+): 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 Subscription...');
+
+ await removeWso2ApiSubscription({
+ wso2Axios,
+ subscriptionId: event.PhysicalResourceId,
+ retryOptions: applyRetryDefaults(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: Wso2ApiSubscriptionCustomResourceEvent,
+ wso2Axios: AxiosInstance,
+): Promise<{ wso2ApiId: string; subscriptionId: string; applicationId: string }> => {
+ console.log(`Verifying if WSO2 API ${event.ResourceProperties.apiId} exists in WSO2...`);
+ const wso2Api = await getWso2Api({
+ wso2Axios,
+ apiId: event.ResourceProperties.apiId,
+ wso2Tenant: event.ResourceProperties.wso2Config.tenant,
+ 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,
+ applicationSearchParameters: event.ResourceProperties.applicationSearchParameters,
+ });
+
+ const wso2Subscription = await findWso2ApiSubscription({
+ wso2Axios,
+ apiId: wso2Api.id!,
+ 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
+ ) {
+ 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: applyRetryDefaults(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: applyRetryDefaults(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..be4c8f1
--- /dev/null
+++ b/lib/src/wso2/wso2-api-subscription/handler/wso2-v1.ts
@@ -0,0 +1,236 @@
+/* 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 {
+ Wso2SubscriptionDefinition,
+ Wso2SubscriptionInfo,
+ Wso2SubscriptionList,
+} 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 from the provided search parameters (name=${apiSearchParameters.name}; version=${apiSearchParameters.version}; context=${apiSearchParameters.context})`,
+ );
+ }
+
+ 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: Wso2SubscriptionInfo['throttlingPolicy'];
+};
+
+export const createWso2ApiSubscription = async ({
+ wso2Axios,
+ apiId,
+ applicationId,
+ throttlingPolicy,
+ retryOptions,
+}: CreateWso2ApiSubscriptionArgs): Promise => {
+ const payload: Wso2SubscriptionDefinition = {
+ 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: Wso2SubscriptionDefinition = {
+ applicationId,
+ apiId,
+ throttlingPolicy,
+ };
+
+ const res = await backOff(
+ async () =>
+ wso2Axios.put(
+ `/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}`, {
+ validateStatus(status) {
+ // If it returns 404, the api is already deleted
+ return status === 200 || status === 404;
+ },
+ }),
+ 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..27b7eff
--- /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' | string;
+};
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..6107f70
--- /dev/null
+++ b/lib/src/wso2/wso2-api-subscription/v1/types.ts
@@ -0,0 +1,71 @@
+import { ApiFromListV1 } from '../../wso2-api/v1/types';
+import { Wso2ApplicationInfo } from '../../wso2-application/v1/types';
+
+export type Wso2SubscriptionInfo = Wso2SubscriptionDefinition & {
+ /**
+ * Subscription Id
+ * @example 123-456-789
+ */
+ subscriptionId: string;
+};
+
+export type Wso2SubscriptionDefinition = {
+ /**
+ * 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: 'Unlimited' | 'Bronze' | 'Silver' | 'Gold' | 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 Wso2SubscriptionList = {
+ /**
+ * 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.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',
+ };
+};
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..a4da447
--- /dev/null
+++ b/lib/src/wso2/wso2-api-subscription/wso2-api-subscription.ts
@@ -0,0 +1,122 @@
+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 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
+ *
+ * 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);
+
+ // 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,
+ 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(),
+ tenant: z.string().optional(),
+ apiVersion: z.string().optional(),
+ credentialsSecretKMSKeyId: z.string().optional(),
+ }),
+ })
+ .and(apiSchema)
+ .and(applicationSchema);
+
+ schema.parse(props);
+};
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,
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.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',
- );
- };
});
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();
}
}