diff --git a/.mailmap b/.mailmap new file mode 100644 index 0000000..08715e6 --- /dev/null +++ b/.mailmap @@ -0,0 +1,6 @@ +# This is used to normalise git reports +# The format is "Proper Name Commit Name " +# for more info, https://git-scm.com/docs/gitmailmap + +Flavio Stutz flaviostutz +Flavio Stutz Flávio Stutz diff --git a/examples/package.json b/examples/package.json index 7a047a8..95cdad0 100644 --- a/examples/package.json +++ b/examples/package.json @@ -30,4 +30,4 @@ "openapi3-ts": "^4.2.1", "zod": "^3.23.8" } -} \ No newline at end of file +} diff --git a/examples/pnpm-lock.yaml b/examples/pnpm-lock.yaml index 2837c6e..8664a34 100644 --- a/examples/pnpm-lock.yaml +++ b/examples/pnpm-lock.yaml @@ -2592,12 +2592,12 @@ packages: '@typescript-eslint/visitor-keys': 7.18.0 dev: true - /@typescript-eslint/scope-manager@8.13.0: - resolution: {integrity: sha512-XsGWww0odcUT0gJoBZ1DeulY1+jkaHUciUq4jKNv4cpInbvvrtDoyBH9rE/n2V29wQJPk8iCH1wipra9BhmiMA==} + /@typescript-eslint/scope-manager@8.14.0: + resolution: {integrity: sha512-aBbBrnW9ARIDn92Zbo7rguLnqQ/pOrUguVpbUwzOhkFg2npFDwTgPGqFqE0H5feXcOoJOfX3SxlJaKEVtq54dw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} dependencies: - '@typescript-eslint/types': 8.13.0 - '@typescript-eslint/visitor-keys': 8.13.0 + '@typescript-eslint/types': 8.14.0 + '@typescript-eslint/visitor-keys': 8.14.0 dev: true /@typescript-eslint/type-utils@7.18.0(eslint@8.57.0)(typescript@5.5.4): @@ -2625,8 +2625,8 @@ packages: engines: {node: ^18.18.0 || >=20.0.0} dev: true - /@typescript-eslint/types@8.13.0: - resolution: {integrity: sha512-4cyFErJetFLckcThRUFdReWJjVsPCqyBlJTi6IDEpc1GWCIIZRFxVppjWLIMcQhNGhdWJJRYFHpHoDWvMlDzng==} + /@typescript-eslint/types@8.14.0: + resolution: {integrity: sha512-yjeB9fnO/opvLJFAsPNYlKPnEM8+z4og09Pk504dkqonT02AyL5Z9SSqlE0XqezS93v6CXn49VHvB2G7XSsl0g==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} dev: true @@ -2652,8 +2652,8 @@ packages: - supports-color dev: true - /@typescript-eslint/typescript-estree@8.13.0(typescript@5.5.4): - resolution: {integrity: sha512-v7SCIGmVsRK2Cy/LTLGN22uea6SaUIlpBcO/gnMGT/7zPtxp90bphcGf4fyrCQl3ZtiBKqVTG32hb668oIYy1g==} + /@typescript-eslint/typescript-estree@8.14.0(typescript@5.5.4): + resolution: {integrity: sha512-OPXPLYKGZi9XS/49rdaCbR5j/S14HazviBlUQFvSKz3npr3NikF+mrgK7CFVur6XEt95DZp/cmke9d5i3vtVnQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '*' @@ -2661,8 +2661,8 @@ packages: typescript: optional: true dependencies: - '@typescript-eslint/types': 8.13.0 - '@typescript-eslint/visitor-keys': 8.13.0 + '@typescript-eslint/types': 8.14.0 + '@typescript-eslint/visitor-keys': 8.14.0 debug: 4.3.7 fast-glob: 3.3.2 is-glob: 4.0.3 @@ -2690,16 +2690,16 @@ packages: - typescript dev: true - /@typescript-eslint/utils@8.13.0(eslint@8.57.0)(typescript@5.5.4): - resolution: {integrity: sha512-A1EeYOND6Uv250nybnLZapeXpYMl8tkzYUxqmoKAWnI4sei3ihf2XdZVd+vVOmHGcp3t+P7yRrNsyyiXTvShFQ==} + /@typescript-eslint/utils@8.14.0(eslint@8.57.0)(typescript@5.5.4): + resolution: {integrity: sha512-OGqj6uB8THhrHj0Fk27DcHPojW7zKwKkPmHXHvQ58pLYp4hy8CSUdTKykKeh+5vFqTTVmjz0zCOOPKRovdsgHA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 dependencies: '@eslint-community/eslint-utils': 4.4.1(eslint@8.57.0) - '@typescript-eslint/scope-manager': 8.13.0 - '@typescript-eslint/types': 8.13.0 - '@typescript-eslint/typescript-estree': 8.13.0(typescript@5.5.4) + '@typescript-eslint/scope-manager': 8.14.0 + '@typescript-eslint/types': 8.14.0 + '@typescript-eslint/typescript-estree': 8.14.0(typescript@5.5.4) eslint: 8.57.0 transitivePeerDependencies: - supports-color @@ -2714,11 +2714,11 @@ packages: eslint-visitor-keys: 3.4.3 dev: true - /@typescript-eslint/visitor-keys@8.13.0: - resolution: {integrity: sha512-7N/+lztJqH4Mrf0lb10R/CbI1EaAMMGyF5y0oJvFoAhafwgiRA7TXyd8TFn8FC8k5y2dTsYogg238qavRGNnlw==} + /@typescript-eslint/visitor-keys@8.14.0: + resolution: {integrity: sha512-vG0XZo8AdTH9OE6VFRwAZldNc7qtJ/6NLGWak+BtENuEUXGZgFpihILPiBvKXvJ2nFu27XNGC6rKiwuaoMbYzQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} dependencies: - '@typescript-eslint/types': 8.13.0 + '@typescript-eslint/types': 8.14.0 eslint-visitor-keys: 3.4.3 dev: true @@ -3910,7 +3910,6 @@ packages: /escalade@3.2.0: resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} engines: {node: '>=6'} - dev: false /escape-string-regexp@1.0.5: resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==} @@ -4137,7 +4136,7 @@ packages: optional: true dependencies: '@typescript-eslint/eslint-plugin': 7.18.0(@typescript-eslint/parser@7.18.0)(eslint@8.57.0)(typescript@5.5.4) - '@typescript-eslint/utils': 8.13.0(eslint@8.57.0)(typescript@5.5.4) + '@typescript-eslint/utils': 8.14.0(eslint@8.57.0)(typescript@5.5.4) eslint: 8.57.0 jest: 29.7.0(@types/node@20.16.2)(ts-node@10.9.2) transitivePeerDependencies: @@ -4609,6 +4608,7 @@ packages: has-proto: 1.0.1 has-symbols: 1.0.3 hasown: 2.0.0 + dev: false /get-intrinsic@1.2.4: resolution: {integrity: sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==} @@ -4682,6 +4682,7 @@ packages: /glob@7.2.3: resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} + deprecated: Glob versions prior to v9 are no longer supported dependencies: fs.realpath: 1.0.0 inflight: 1.0.6 @@ -4724,7 +4725,7 @@ packages: /gopd@1.0.1: resolution: {integrity: sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==} dependencies: - get-intrinsic: 1.2.2 + get-intrinsic: 1.2.4 /graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} @@ -4753,6 +4754,7 @@ packages: /has-proto@1.0.1: resolution: {integrity: sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==} engines: {node: '>= 0.4'} + dev: false /has-proto@1.0.3: resolution: {integrity: sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==} @@ -4811,6 +4813,7 @@ packages: engines: {node: '>= 0.4'} dependencies: function-bind: 1.1.2 + dev: false /hasown@2.0.2: resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} @@ -4874,6 +4877,7 @@ packages: /inflight@1.0.6: resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} + deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful. dependencies: once: 1.4.0 wrappy: 1.0.2 @@ -4893,7 +4897,7 @@ packages: resolution: {integrity: sha512-YBUanLI8Yoihw923YeFUS5fs0fF2f5TSFTNiYAAzhhDscDa3lEqYuz1pDOEP5KvX94I9ey3vsqjJcLVFVU+3QA==} engines: {node: '>= 0.10'} dependencies: - hasown: 2.0.0 + hasown: 2.0.2 dev: true /is-arguments@1.1.1: @@ -4948,23 +4952,17 @@ packages: ci-info: 2.0.0 dev: true - /is-core-module@2.13.1: - resolution: {integrity: sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==} - dependencies: - hasown: 2.0.0 - /is-core-module@2.15.1: resolution: {integrity: sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ==} engines: {node: '>= 0.4'} dependencies: hasown: 2.0.2 - dev: true /is-data-descriptor@1.0.1: resolution: {integrity: sha512-bc4NlCDiCr28U4aEsQ3Qs2491gVq4V8G7MQyws968ImqjKuYtTJXrl7Vq7jsN7Ly/C3xj5KWFrY7sHNeDkAzXw==} engines: {node: '>= 0.4'} dependencies: - hasown: 2.0.0 + hasown: 2.0.2 dev: true /is-data-view@1.0.1: @@ -6546,7 +6544,7 @@ packages: resolution: {integrity: sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==} hasBin: true dependencies: - is-core-module: 2.13.1 + is-core-module: 2.15.1 path-parse: 1.0.7 supports-preserve-symlinks-flag: 1.0.0 @@ -7237,7 +7235,7 @@ packages: browserslist: '>= 4.21.0' dependencies: browserslist: 4.22.2 - escalade: 3.1.1 + escalade: 3.2.0 picocolors: 1.0.0 dev: true @@ -7484,7 +7482,7 @@ packages: dev: false file:../lib/dist/cdk-practical-constructs-0.0.1.tgz(@asteasolutions/zod-to-openapi@7.0.0)(aws-cdk-lib@2.155.0)(zod@3.23.8): - resolution: {integrity: sha512-5H/Y7HRg2tf0OEC6ec5v9o4shBUJOg2YdekWB2qC0Du/Rwv9cL/iH9SYJCevv9sv2wqZ+Pg/skYEHczgUenm8Q==, tarball: file:../lib/dist/cdk-practical-constructs-0.0.1.tgz} + resolution: {integrity: sha512-Ib21RU+8A6//9zs0vzK1E9sSEyOmYPwgV4kx5HW6oGFeBtfqkrYbn2Xyvb09BWW6u1fYQiXxMzRjvknlLYTdkg==, tarball: file:../lib/dist/cdk-practical-constructs-0.0.1.tgz} id: file:../lib/dist/cdk-practical-constructs-0.0.1.tgz name: cdk-practical-constructs version: 0.0.1 diff --git a/lib/src/wso2/wso2-api/handler/index.test.ts b/lib/src/wso2/wso2-api/handler/index.test.ts index 438a4ff..e00c2a0 100644 --- a/lib/src/wso2/wso2-api/handler/index.test.ts +++ b/lib/src/wso2/wso2-api/handler/index.test.ts @@ -63,12 +63,48 @@ describe('wso2 custom resource lambda', () => { .post(/.*\/publisher\/v1\/apis\?.*$/) .reply(201, testDefs); + // api get mock + nock(baseWso2Url) + .get(/.*\/publisher\/v1\/apis\/[^\\/]*$/) + .times(2) // check if content matches + .reply(200, { ...testDefs, lastUpdatedTime: '2020-10-10' }); + + const nocksUpdateCreate = nockAfterCreateUpdateAndChangeLifecycleStatusInWso2(testDefs); + + const eres = await handler( + testCFNEventCreate({ + ...testEvent, + }), + ); + expect(eres.PhysicalResourceId).toBe('123-456'); + expect(eres.Status).toBe('SUCCESS'); + + // check if the correct endpoints were invoked during update/create process + // eslint-disable-next-line no-restricted-syntax + for (const n of nocksUpdateCreate) { + n.done(); + } + }); + + it('wso2 api create and PUBLISH', async () => { + nockBasicWso2SDK(); + // api list mock nock(baseWso2Url) .get(/.*\/publisher\/v1\/apis$/) .query(true) - .times(2) // check if updated, check if published - .reply(200, toResultList(testDefs)); + .times(1) // check create or update + .reply(200, { list: [] }); + + const testDefs: Wso2ApiDefinitionV1 = { + ...testBasicWso2ApiDefs(), + id: '123-456', + }; + + // api create mock + nock(baseWso2Url) + .post(/.*\/publisher\/v1\/apis\?.*$/) + .reply(201, testDefs); // api get mock nock(baseWso2Url) @@ -76,15 +112,25 @@ describe('wso2 custom resource lambda', () => { .times(2) // check if content matches .reply(200, { ...testDefs, lastUpdatedTime: '2020-10-10' }); - nockAfterUpdateCreate(testDefs); + const nocksUpdateCreate = nockAfterCreateUpdateAndChangeLifecycleStatusInWso2( + testDefs, + 'Publish', + ); const eres = await handler( testCFNEventCreate({ ...testEvent, + lifecycleStatus: 'PUBLISHED', }), ); expect(eres.PhysicalResourceId).toBe('123-456'); expect(eres.Status).toBe('SUCCESS'); + + // check if the correct endpoints were invoked during update/create process + // eslint-disable-next-line no-restricted-syntax + for (const n of nocksUpdateCreate) { + n.done(); + } }); it('basic wso2 api update', async () => { @@ -124,7 +170,7 @@ describe('wso2 custom resource lambda', () => { .times(1) // check if content matches .reply(200, { ...testDefs, lastUpdatedTime: '2020-10-11' }); - nockAfterUpdateCreate(testDefs); + nockAfterCreateUpdateAndChangeLifecycleStatusInWso2(testDefs); const eres = await handler( testCFNEventCreate({ @@ -165,7 +211,7 @@ describe('wso2 custom resource lambda', () => { .put(/.*\/publisher\/v1\/apis\/.*$/) .reply(201, testDefs); - nockAfterUpdateCreate(testDefs); + nockAfterCreateUpdateAndChangeLifecycleStatusInWso2(testDefs); const eres = await handler( testCFNEventUpdate( @@ -231,7 +277,7 @@ describe('wso2 custom resource lambda', () => { .times(1) // check if content matches .reply(200, { ...testDefs, lastUpdatedTime: '2020-10-11' }); - nockAfterUpdateCreate(testDefs); + nockAfterCreateUpdateAndChangeLifecycleStatusInWso2(testDefs); const eres = await handler( testCFNEventUpdate( @@ -441,37 +487,66 @@ describe('wso2 custom resource lambda', () => { ); }; - const nockAfterUpdateCreate = (testDefs: Wso2ApiDefinitionV1): void => { + const nockAfterCreateUpdateAndChangeLifecycleStatusInWso2 = ( + testDefs: Wso2ApiDefinitionV1, + lifecycleAction?: string, + ): nock.Scope[] => { + const nocks: nock.Scope[] = []; + // api openapi update mock - nock(baseWso2Url) - .put(/.*\/publisher\/v1\/apis\/123-456\/swagger/) - .times(1) - .reply(200); + nocks.push( + nock(baseWso2Url) + .put(/.*\/publisher\/v1\/apis\/123-456\/swagger/) + .times(1) + .reply(200), + ); // api openapi get mock - nock(baseWso2Url) - .get(/.*\/publisher\/v1\/apis\/123-456\/swagger/) - .times(1) - .reply(200, JSON.stringify(testEvent.openapiDocument), ['Content-Type', 'text/plain']); + nocks.push( + nock(baseWso2Url) + .get(/.*\/publisher\/v1\/apis\/123-456\/swagger/) + .times(1) + .reply(200, JSON.stringify(testEvent.openapiDocument), ['Content-Type', 'text/plain']), + ); - // api change to published mock - nock(baseWso2Url) - .post(/.*\/publisher\/v1\/apis\/change-lifecycle/) - .query({ apiId: '123-456', action: 'Publish' }) - .times(1) - .reply(200); + if (lifecycleAction) { + console.log('NOCK CHANGE LIFE CYCLE'); + // api change to published mock + nocks.push( + nock(baseWso2Url) + .post(/.*\/publisher\/v1\/apis\/change-lifecycle/) + .query({ apiId: '123-456', action: lifecycleAction }) + .times(1) + .reply(200), + ); + + // api get apis mock for checking if lifecycle was changed + nocks.push( + nock(baseWso2Url) + .get(/.*\/publisher\/v1\/apis$/) + .query(true) + .times(1) // check if it is published + .reply(200, toResultList(testDefs)), + ); + } // api get endpoint urls mock - nock(baseWso2Url) - .get(/.*\/store\/v1\/apis\/123-456$/) - .times(1) - .reply(200, testDefs); + nocks.push( + nock(baseWso2Url) + .get(/.*\/store\/v1\/apis\/123-456$/) + .times(1) + .reply(200, testDefs), + ); // api get apis mock - nock(baseWso2Url) - .get(/.*\/publisher\/v1\/apis$/) - .query(true) - .times(1) // check if it is published - .reply(200, toResultList(testDefs)); + nocks.push( + nock(baseWso2Url) + .get(/.*\/publisher\/v1\/apis$/) + .query(true) + .times(1) // check if it is published + .reply(200, toResultList(testDefs)), + ); + + return nocks; }; }); diff --git a/lib/src/wso2/wso2-api/handler/index.ts b/lib/src/wso2/wso2-api/handler/index.ts index 2b070ef..47e2859 100644 --- a/lib/src/wso2/wso2-api/handler/index.ts +++ b/lib/src/wso2/wso2-api/handler/index.ts @@ -7,7 +7,11 @@ import { Wso2ApiCustomResourceProperties } from '../types'; import { prepareAxiosForWso2Calls } from '../../wso2-utils'; import { applyRetryDefaults, truncateStr } from '../../utils'; -import { createUpdateAndPublishApiInWso2, findWso2Api, removeApiInWso2 } from './wso2-v1'; +import { + createUpdateAndChangeLifecycleStatusInWso2, + findWso2Api, + removeApiInWso2, +} from './wso2-v1'; export type Wso2ApiCustomResourceEvent = CdkCustomResourceEvent & { ResourceProperties: Wso2ApiCustomResourceProperties; @@ -45,7 +49,6 @@ export const handler = async ( try { console.log('>>> Prepare WSO2 API client...'); - // const wso2Client = await prepareWso2ApiClient(event.ResourceProperties.wso2Config); const wso2Axios = await prepareAxiosForWso2Calls(event.ResourceProperties.wso2Config); if (event.RequestType === 'Create' || event.RequestType === 'Update') { @@ -122,13 +125,14 @@ const createOrUpdateWso2Api = async ( } if (event.RequestType === 'Create' || event.RequestType === 'Update') { - return createUpdateAndPublishApiInWso2({ + return createUpdateAndChangeLifecycleStatusInWso2({ wso2Axios, apiDefinition: event.ResourceProperties.apiDefinition, openapiDocument: event.ResourceProperties.openapiDocument, wso2Tenant: event.ResourceProperties.wso2Config.tenant ?? '', apiBeforeUpdate, retryOptions: applyRetryDefaults(event.ResourceProperties.retryOptions), + lifecycleStatus: event.ResourceProperties.lifecycleStatus, }); } diff --git a/lib/src/wso2/wso2-api/handler/wso2-v1.ts b/lib/src/wso2/wso2-api/handler/wso2-v1.ts index 0c06e9e..b5af323 100644 --- a/lib/src/wso2/wso2-api/handler/wso2-v1.ts +++ b/lib/src/wso2/wso2-api/handler/wso2-v1.ts @@ -1,5 +1,4 @@ /* eslint-disable no-console */ -import { OpenAPIObject } from 'openapi3-ts/oas30'; import { oas30 } from 'openapi3-ts'; import FormData from 'form-data'; import { backOff } from 'exponential-backoff'; @@ -18,7 +17,7 @@ import { normalizeCorsConfigurationValues, objectWithContentOrUndefined, } from '../utils'; -import { RetryOptions } from '../../types'; +import { Wso2ApiProps } from '../types'; export const findWso2Api = async (args: { wso2Axios: AxiosInstance; @@ -74,17 +73,15 @@ export const findWso2Api = async (args: { ); }; -export type UpsertWso2Args = { - wso2Axios: AxiosInstance; - wso2Tenant: string; - apiBeforeUpdate?: { - id?: string; - lastUpdatedTime?: string; +export type UpsertWso2Args = Pick & + Required> & { + wso2Axios: AxiosInstance; + wso2Tenant: string; + apiBeforeUpdate?: { + id?: string; + lastUpdatedTime?: string; + }; }; - apiDefinition: Wso2ApiDefinitionV1; - openapiDocument: OpenAPIObject; - retryOptions: RetryOptions; -}; /** * Delete API in WSO2 server @@ -101,10 +98,10 @@ export const removeApiInWso2 = async (args: { /** * Perform calls in WSO2 API to create or update an API, update Openapi definitions - * and change the API lifecycle to PUBLISHED + * and change the API lifecycle to the desired status (if defined) * @returns {string} Id of the API in WSO2 */ -export const createUpdateAndPublishApiInWso2 = async ( +export const createUpdateAndChangeLifecycleStatusInWso2 = async ( args: UpsertWso2Args, ): Promise<{ wso2ApiId: string; endpointUrl?: string }> => { const needWaitBeforeUpdateOpenapiDocument = await checkApiDefAndOpenapiOverlap(args); @@ -117,6 +114,8 @@ export const createUpdateAndPublishApiInWso2 = async ( args.retryOptions.mutationRetries, ); + console.log(`API created/updated in WSO2. apiId='${wso2ApiId}'`); + if (needWaitBeforeUpdateOpenapiDocument) { console.log(''); console.log( @@ -138,47 +137,90 @@ export const createUpdateAndPublishApiInWso2 = async ( args.retryOptions.mutationRetries, ); - console.log(''); - console.log(`>>> Change api status to 'PUBLISHED'...`); - // will retry changing to PUBLISHED if fails - const endpointUrl = await backOff(async () => - publishApiInWso2AndCheck({ - wso2Axios: args.wso2Axios, - wso2ApiId, - apiDefinition: args.apiDefinition, - wso2Tenant: args.wso2Tenant, - retryOptions: args.retryOptions, - }), - ); + if (args.lifecycleStatus) { + console.log(''); + console.log(`>>> Changing lifecycle status to '${args.lifecycleStatus}'...`); + // will retry changing to PUBLISHED if fails + await backOff(async () => + changeLifecycleStatusInWso2AndCheck({ + wso2Axios: args.wso2Axios, + wso2ApiId, + apiDefinition: args.apiDefinition, + wso2Tenant: args.wso2Tenant, + retryOptions: args.retryOptions, + lifecycleStatus: args.lifecycleStatus, + }), + ); + } - console.log('API created/updated and published successfully on WSO2 server'); + // get endpoint url + console.log(`Getting API endpoint url`); + const apir = await args.wso2Axios.get(`/api/am/store/v1/apis/${wso2ApiId}`); + + // find the endpoint URL of the environment that was defined in this API + const apid = apir.data as DevPortalAPIv1; + const endpointUrl = apid.endpointURLs?.reduce((acc, elem) => { + if ( + elem.environmentName && + args.apiDefinition.gatewayEnvironments?.includes(elem.environmentName) + ) { + if (elem.URLs?.https) { + return elem.URLs?.https; + } + if (elem.defaultVersionURLs?.https) { + return elem.defaultVersionURLs?.https; + } + } + return acc; + }, ''); + + console.log('API created/updated successfully on WSO2 server'); return { wso2ApiId, endpointUrl }; }; -export const publishApiInWso2AndCheck = async (args: { - wso2Axios: AxiosInstance; - apiDefinition: Wso2ApiDefinitionV1; - wso2ApiId: string; - wso2Tenant: string; - retryOptions: RetryOptions; -}): Promise => { - console.log(`Changing API status to PUBLISHED in WSO2`); +const actionMap = { + PUBLISHED: 'Publish', + CREATED: 'Demote to created', + DEPRECATED: 'Deprecate', + BLOCKED: 'Block', + RETIRED: 'Retire', + PROTOTYPED: 'Deploy as a Prototype', +}; + +export const changeLifecycleStatusInWso2AndCheck = async ( + args: Pick & + Required> & { + wso2Axios: AxiosInstance; + wso2ApiId: string; + wso2Tenant: string; + }, +): Promise => { + if (!args.lifecycleStatus) throw new Error(`'lifecycleStatus' is required`); + console.log(`Changing API status to '${args.lifecycleStatus}' in WSO2`); + + // define the action to be taken based on target lifecycle status + const action = actionMap[args.lifecycleStatus]; + if (!action) { + throw new Error(`Lifecycle status '${args.lifecycleStatus}' is not supported`); + } + + console.log(`Using action '${action}' for lifecycle change`); await args.wso2Axios.post( '/api/am/publisher/v1/apis/change-lifecycle', {}, { params: { apiId: args.wso2ApiId, - action: 'Publish', + action, }, }, ); - // wait for API to be created by retrying checks + // wait for API to have the desired status by retrying checks await backOff(async () => { console.log(''); - console.log('Checking if API is PUBLISHED...'); + console.log(`Checking if API is in lifecycle status '${args.lifecycleStatus}'...`); const fapi = await findWso2Api({ apiDefinition: args.apiDefinition, wso2Axios: args.wso2Axios, @@ -188,44 +230,32 @@ export const publishApiInWso2AndCheck = async (args: { if (!fapi) { throw new Error(`API ${args.wso2ApiId} could not be found`); } - if (fapi.lifeCycleStatus !== 'PUBLISHED') { - throw new Error(`API ${args.wso2ApiId} is in status ${fapi.lifeCycleStatus} (not PUBLISHED)`); - } - console.log('API status PUBLISHED check OK'); - }, args.retryOptions.checkRetries); - // get endpoint url - console.log(`Getting API endpoint url`); - const apir = await args.wso2Axios.get(`/api/am/store/v1/apis/${args.wso2ApiId}`); - - // find the endpoint URL of the environment that was defined in this API - const apid = apir.data as DevPortalAPIv1; - const endpointUrl = apid.endpointURLs?.reduce((acc, elem) => { - if ( - elem.environmentName && - args.apiDefinition.gatewayEnvironments?.includes(elem.environmentName) - ) { - if (elem.URLs?.https) { - return elem.URLs?.https; - } - if (elem.defaultVersionURLs?.https) { - return elem.defaultVersionURLs?.https; - } + // Workflow trigger detected. It might indicate the need for manual approval or a slow process + // so we will ignore checking the API lifecycle status check for now + if (fapi.workflowStatus !== '' && fapi.workflowStatus !== 'APPROVED') { + console.log( + `API lifecycle status check SKIPPED. API ${args.wso2ApiId} has workflow status '${fapi.workflowStatus}', which might require manual approval before the actual lifecycle status is changed`, + ); + return; } - return acc; - }, ''); - return endpointUrl; + if (fapi.lifeCycleStatus !== args.lifecycleStatus) { + throw new Error( + `API ${args.wso2ApiId} is in status ${fapi.lifeCycleStatus} (not '${args.lifecycleStatus}')`, + ); + } + console.log(`API lifecycle status check OK. lifecycleStatus='${args.lifecycleStatus}'`); + }, args.retryOptions.checkRetries); }; -export const updateOpenapiInWso2AndCheck = async (args: { - wso2Axios: AxiosInstance; - wso2ApiId: string; - wso2Tenant: string; - openapiDocument: oas30.OpenAPIObject; - apiDefinition: Wso2ApiDefinitionV1; - retryOptions: RetryOptions; -}): Promise => { +export const updateOpenapiInWso2AndCheck = async ( + args: Required> & { + wso2Axios: AxiosInstance; + wso2ApiId: string; + wso2Tenant: string; + }, +): Promise => { console.log('Updating Openapi document in WSO2'); const fdata = new FormData(); const openapiDocumentStr = JSON.stringify(args.openapiDocument); diff --git a/lib/src/wso2/wso2-api/types.ts b/lib/src/wso2/wso2-api/types.ts index 3b77595..93a6bfd 100644 --- a/lib/src/wso2/wso2-api/types.ts +++ b/lib/src/wso2/wso2-api/types.ts @@ -17,4 +17,11 @@ export type Wso2ApiProps = Wso2BaseProperties & { * The paths/operations in this document will be used to configure routes in WSO2 */ openapiDocument: oas30.OpenAPIObject; + /** + * The desired lifecycle status of the API in WSO2. + * If not defined, the status of the API won't be checked and no changes to the status will be performed. + * If a workflow trigger is detected during the lifecycle change (e.g for manual approval), we skip the status change check + * as the workflow might take a long time to complete. + */ + lifecycleStatus?: 'CREATED' | 'PUBLISHED' | 'DEPRECATED' | 'BLOCKED' | 'RETIRED' | 'PROTOTYPED'; }; diff --git a/lib/src/wso2/wso2-api/v1/types.ts b/lib/src/wso2/wso2-api/v1/types.ts index 29cc128..e152b28 100644 --- a/lib/src/wso2/wso2-api/v1/types.ts +++ b/lib/src/wso2/wso2-api/v1/types.ts @@ -33,5 +33,5 @@ export type Wso2ApiListV1 = { export type ApiFromListV1 = Pick< PublisherPortalAPIv1, - 'id' | 'name' | 'type' | 'context' | 'version' | 'provider' | 'lifeCycleStatus' + 'id' | 'name' | 'type' | 'context' | 'version' | 'provider' | 'lifeCycleStatus' | 'workflowStatus' >; diff --git a/lib/src/wso2/wso2-api/wso2-api.ts b/lib/src/wso2/wso2-api/wso2-api.ts index bd511f4..7fa0523 100644 --- a/lib/src/wso2/wso2-api/wso2-api.ts +++ b/lib/src/wso2/wso2-api/wso2-api.ts @@ -58,6 +58,7 @@ export class Wso2Api extends Construct { apiDefinition: wso2ApiDefs, openapiDocument: props.openapiDocument, retryOptions: props.retryOptions, + lifecycleStatus: props.lifecycleStatus, } satisfies Wso2ApiCustomResourceProperties, resourceType: 'Custom::Wso2Api', removalPolicy: props.removalPolicy ?? RemovalPolicy.RETAIN, @@ -73,7 +74,7 @@ export const validateProps = (props: Wso2ApiProps): void => { if (!props.wso2Config) throw new Error('wso2Config is required'); if (!props.wso2Config.baseApiUrl) throw new Error('wso2Config.baseApiUrl is required'); if (!props.wso2Config.credentialsSecretId) { - throw new Error('wso2Config.credentialsSecretManagerPath is required'); + throw new Error('wso2Config.credentialsSecretId is required'); } validateWso2ApiDefs(props.apiDefinition); if (!props.openapiDocument.openapi.startsWith('3.0')) {