diff --git a/.github/workflows/publish-to-npm.yml b/.github/workflows/publish-to-npm.yml index 82185b1e..b5e94b64 100644 --- a/.github/workflows/publish-to-npm.yml +++ b/.github/workflows/publish-to-npm.yml @@ -1,20 +1,71 @@ name: publish-to-npm on: - pull_request: - types: [closed] - branches: - - main + pull_request: + types: [ closed ] + branches: + - main + - develop + - epic/** jobs: - publish: - if: github.repository == 'adobe/aio-cli-plugin-api-mesh' - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: actions/setup-node@v3 - with: - node-version: 18 - - run: yarn install --frozen-lockfile - - uses: JS-DevTools/npm-publish@v1 - with: - token: ${{ secrets.ADOBE_BOT_NPM_TOKEN }} - access: 'public' + publish: + if: github.event.pull_request.merged == true + runs-on: ubuntu-latest + steps: + - name: Checkout source + uses: actions/checkout@v4 + + - name: Setup node + uses: actions/setup-node@v3 + with: + node-version: 18 + + - name: Yarn install + run: yarn install --frozen-lockfile + + - name: Get version from package.json + id: get_version + run: | + version=$(jq -r '.version' package.json) + echo "version=$version" >> $GITHUB_OUTPUT + echo "Read version $version from package.json" + + - name: Verify version corresponds to branch + id: verify_version + run: | + target_branch="${GITHUB_REF#refs/heads/}" + version_tag="" + + if [ "$target_branch" == "main" ]; then + version_tag="latest" + + elif [ "$target_branch" == "develop" ]; then + if [[ "${{ steps.get_version.outputs.version }}" =~ beta ]]; then + version_tag="beta" + else + echo "Will not publish. Version on branch \"$target_branch\" is not beta."; + exit 0; + fi + + elif [[ $target_branch == epic/* ]]; then + if [[ "${{ steps.get_version.outputs.version }}" =~ alpha ]]; then + version_tag="alpha" + else + echo "Will not publish. Version on branch \"$target_branch\" is not alpha."; + exit 0; + fi + + else + echo "Will not publish. Branch \"$target_branch\" is not designated for publish."; + exit 0; + fi + + echo "version_tag=$version_tag" >> $GITHUB_OUTPUT; + echo "Will publish version ${{ steps.get_version.outputs.version }} as $version_tag" + + - name: Publish to npm + if: ${{ steps.verify_version.outputs.version_tag != '' }} + uses: JS-DevTools/npm-publish@v1 + with: + token: ${{ secrets.ADOBE_BOT_NPM_TOKEN }} + access: 'public' + tag: ${{ steps.verify_version.outputs.version_tag }} diff --git a/package.json b/package.json index 3d3d0119..6466bea1 100644 --- a/package.json +++ b/package.json @@ -37,7 +37,7 @@ "version": "oclif-dev readme && git add README.md" }, "dependencies": { - "@adobe-apimesh/mesh-builder": "1.4.2", + "@adobe-apimesh/mesh-builder": "1.4.4", "@adobe/aio-cli-lib-console": "^4.0.0", "@adobe/aio-lib-core-config": "^3.0.0", "@adobe/aio-lib-core-logging": "^2.0.0", diff --git a/src/commands/__fixtures__/openapi-schema.json b/src/commands/__fixtures__/openapi-schema.json index 1f0a2351..5c9f2ef9 100644 --- a/src/commands/__fixtures__/openapi-schema.json +++ b/src/commands/__fixtures__/openapi-schema.json @@ -1,4 +1,4 @@ { - "$schema": "http://json-schema.org/draft-04/schema" + "$schema": "http://json-schema.org/draft-04/schema", "id": "2" } \ No newline at end of file diff --git a/src/commands/api-mesh/__tests__/create.test.js b/src/commands/api-mesh/__tests__/create.test.js index 3162308e..0b588abe 100644 --- a/src/commands/api-mesh/__tests__/create.test.js +++ b/src/commands/api-mesh/__tests__/create.test.js @@ -12,6 +12,22 @@ governing permissions and limitations under the License. const mockConsoleCLIInstance = {}; +jest.mock('axios'); +jest.mock('@adobe/aio-lib-ims'); +jest.mock('@adobe/aio-lib-env'); +jest.mock('@adobe/aio-cli-lib-console', () => ({ + init: jest.fn().mockResolvedValue(mockConsoleCLIInstance), + cleanStdOut: jest.fn(), +})); +jest.mock('../../../helpers', () => ({ + initSdk: jest.fn().mockResolvedValue({}), + initRequestId: jest.fn().mockResolvedValue({}), + promptConfirm: jest.fn().mockResolvedValue(true), + interpolateMesh: jest.fn().mockResolvedValue({}), + importFiles: jest.fn().mockResolvedValue(), +})); +jest.mock('../../../lib/devConsole'); + const CreateCommand = require('../create'); const sampleCreateMeshConfig = require('../../__fixtures__/sample_mesh.json'); const meshConfigWithComposerFiles = require('../../__fixtures__/sample_mesh_with_composer_files.json'); @@ -27,6 +43,7 @@ const { createMesh, createAPIMeshCredentials, subscribeCredentialToMeshService, + getTenantFeatures, } = require('../../../lib/devConsole'); const selectedOrg = { id: '1234', code: 'CODE1234@AdobeOrg', name: 'ORG01', type: 'entp' }; @@ -34,7 +51,6 @@ const selectedOrg = { id: '1234', code: 'CODE1234@AdobeOrg', name: 'ORG01', type const os = require('os'); const selectedProject = { id: '5678', title: 'Project01' }; - const selectedWorkspace = { id: '123456789', title: 'Workspace01' }; jest.mock('@adobe/aio-cli-lib-console', () => ({ @@ -56,11 +72,11 @@ jest.mock('../../../helpers', () => ({ jest.mock('../../../lib/devConsole'); jest.mock('chalk', () => ({ red: jest.fn(text => text), // Return the input text without any color formatting + bold: jest.fn(text => text), })); let logSpy = null; let errorLogSpy = null; - let parseSpy = null; let platformSpy = null; @@ -71,9 +87,12 @@ describe('create command tests', () => { beforeEach(() => { initSdk.mockResolvedValue({ imsOrgId: selectedOrg.id, + imsOrgCode: selectedOrg.code, projectId: selectedProject.id, workspaceId: selectedWorkspace.id, workspaceName: selectedWorkspace.title, + orgName: selectedOrg.name, + projectName: selectedProject.title, }); global.requestId = 'dummy_request_id'; @@ -95,14 +114,23 @@ describe('create command tests', () => { apiKey: 'dummy_api_key', id: 'dummy_id', }); + subscribeCredentialToMeshService.mockResolvedValue(['dummy_service']); - let fetchedMeshConfig = sampleCreateMeshConfig; - fetchedMeshConfig.meshId = 'dummy_id'; - fetchedMeshConfig.meshURL = ''; + getMesh.mockResolvedValue({ + meshId: 'dummy_id', + meshURL: '', + }); - getMesh.mockResolvedValue(fetchedMeshConfig); + getTenantFeatures.mockResolvedValue({ + imsOrgId: selectedProject.code, + showCloudflareURL: false, + }); + global.requestId = 'dummy_request_id'; + + logSpy = jest.spyOn(CreateCommand.prototype, 'log'); + errorLogSpy = jest.spyOn(CreateCommand.prototype, 'error'); parseSpy = jest.spyOn(CreateCommand.prototype, 'parse'); parseSpy.mockResolvedValue({ args: { file: 'src/commands/__fixtures__/sample_mesh.json' }, @@ -300,6 +328,8 @@ describe('create command tests', () => { "5678", "123456789", "Workspace01", + "ORG01", + "Project01", { "meshConfig": { "sources": [ @@ -390,6 +420,8 @@ describe('create command tests', () => { "5678", "123456789", "Workspace01", + "ORG01", + "Project01", { "meshConfig": { "sources": [ @@ -949,6 +981,8 @@ describe('create command tests', () => { "5678", "123456789", "Workspace01", + "ORG01", + "Project01", { "meshConfig": { "files": [ @@ -1153,7 +1187,8 @@ describe('create command tests', () => { `); }); - test('should not override if prompt returns No, if there is files array', async () => { + // Temporarily skipping since it is not actually testing the file import override prompt. The function which performs the prompt is mocked. + test.skip('should not override if prompt returns No, if there is files array', async () => { let meshConfig = { sources: [ { @@ -1211,6 +1246,8 @@ describe('create command tests', () => { "5678", "123456789", "Workspace01", + "ORG01", + "Project01", { "files": [ { @@ -1280,7 +1317,8 @@ describe('create command tests', () => { `); }); - test('should override if prompt returns Yes, if there is files array', async () => { + // Temporarily skipping since it is not actually testing the file import override prompt. The function which performs the prompt is mocked. + test.skip('should override if prompt returns Yes, if there is files array', async () => { let meshConfig = { sources: [ { @@ -1340,6 +1378,8 @@ describe('create command tests', () => { "5678", "123456789", "Workspace01", + "ORG01", + "Project01", { "meshConfig": { "files": [ @@ -1472,6 +1512,8 @@ describe('create command tests', () => { "5678", "123456789", "Workspace01", + "ORG01", + "Project01", { "meshConfig": { "files": [ @@ -1601,6 +1643,8 @@ describe('create command tests', () => { "5678", "123456789", "Workspace01", + "ORG01", + "Project01", { "meshConfig": { "files": [ @@ -1763,6 +1807,76 @@ describe('create command tests', () => { `); }); + test('should show prod edge mesh url on workspace named "Production" if feature is enabled', async () => { + // mock the edge mesh url feature to be enabled + getTenantFeatures.mockResolvedValue({ + imsOrgId: selectedOrg.code, + showCloudflareURL: true, + }); + + // mock the workspace name to "Production" + initSdk.mockResolvedValue({ + workspaceName: 'Production', + }); + + await CreateCommand.run(); + + expect(logSpy).toHaveBeenCalledWith( + expect.stringContaining('Legacy Mesh Endpoint:'), + 'https://graph.adobe.io/api/dummy_mesh_id/graphql?api_key=dummy_api_key', + ); + + expect(logSpy).toHaveBeenCalledWith( + expect.stringContaining('Edge Mesh Endpoint:'), + 'https://edge-graph.adobe.io/api/dummy_mesh_id/graphql', + ); + }); + + test('should show sandbox edge mesh url on workspace NOT named "Production" if feature is enabled', async () => { + // mock the edge mesh url feature to be enabled + getTenantFeatures.mockResolvedValueOnce({ + imsOrgId: selectedOrg.code, + showCloudflareURL: true, + }); + + // mock the workspace name to a value not equal to "Production" + initSdk.mockResolvedValueOnce({ + workspaceName: 'AnythingButProduction', + }); + + await CreateCommand.run(); + + expect(logSpy).toHaveBeenCalledWith( + expect.stringContaining('Legacy Mesh Endpoint:'), + 'https://graph.adobe.io/api/dummy_mesh_id/graphql?api_key=dummy_api_key', + ); + + expect(logSpy).toHaveBeenCalledWith( + expect.stringContaining('Edge Mesh Endpoint:'), + 'https://edge-sandbox-graph.adobe.io/api/dummy_mesh_id/graphql', + ); + }); + + test('should not show edge mesh url if feature is disabled', async () => { + // mock the edge mesh url feature to be disabled + getTenantFeatures.mockResolvedValueOnce({ + imsOrgId: selectedOrg.code, + showCloudflareURL: false, + }); + + await CreateCommand.run(); + + expect(logSpy).not.toHaveBeenCalledWith( + expect.stringContaining('Edge Mesh Endpoint:'), + expect.any(String), + ); + + expect(logSpy).toHaveBeenCalledWith( + expect.stringContaining('Mesh Endpoint:'), + 'https://graph.adobe.io/api/dummy_mesh_id/graphql?api_key=dummy_api_key', + ); + }); + test('should return error if mesh has placeholders and the provided secrets file is invalid', async () => { parseSpy.mockResolvedValueOnce({ args: { file: 'src/commands/__fixtures__/sample_secrets_mesh.json' }, diff --git a/src/commands/api-mesh/__tests__/describe.test.js b/src/commands/api-mesh/__tests__/describe.test.js index 2115ceae..b1b2f465 100644 --- a/src/commands/api-mesh/__tests__/describe.test.js +++ b/src/commands/api-mesh/__tests__/describe.test.js @@ -14,7 +14,6 @@ const mockConsoleCLIInstance = {}; jest.mock('axios'); jest.mock('@adobe/aio-lib-env'); -jest.mock('@adobe/aio-cli-lib-console'); jest.mock('@adobe/aio-lib-ims'); jest.mock('../../../helpers', () => ({ initSdk: jest.fn().mockResolvedValue({}), @@ -28,7 +27,7 @@ jest.mock('../../../lib/devConsole'); const DescribeCommand = require('../describe'); const { initSdk, initRequestId } = require('../../../helpers'); -const { describeMesh, getMesh } = require('../../../lib/devConsole'); +const { describeMesh, getMesh, getTenantFeatures } = require('../../../lib/devConsole'); const sampleCreateMeshConfig = require('../../__fixtures__/sample_mesh.json'); const selectedOrg = { id: '1234', code: 'CODE1234@AdobeOrg', name: 'ORG01', type: 'entp' }; @@ -43,35 +42,39 @@ const mockIgnoreCacheFlag = jest.fn().mockResolvedValue(true); describe('describe command tests', () => { beforeEach(() => { - describeMesh.mockResolvedValue({ - meshId: 'dummy_meshId', - apiKey: 'dummy_apiKey', - }); - initSdk.mockResolvedValue({ imsOrgId: selectedOrg.id, + imsOrgCode: selectedOrg.code, projectId: selectedProject.id, workspaceId: selectedWorkspace.id, workspaceName: selectedWorkspace.title, }); + describeMesh.mockResolvedValue({ + meshId: 'dummy_meshId', + apiKey: 'dummy_apiKey', + }); + + getMesh.mockResolvedValue({ + meshId: 'dummy_id', + meshURL: '', + }); + + getTenantFeatures.mockResolvedValue({ + imsOrgId: selectedOrg.code, + showCloudflareURL: false, + }); + global.requestId = 'dummy_request_id'; logSpy = jest.spyOn(DescribeCommand.prototype, 'log'); errorLogSpy = jest.spyOn(DescribeCommand.prototype, 'error'); - parseSpy = jest.spyOn(DescribeCommand.prototype, 'parse'); parseSpy.mockResolvedValue({ flags: { ignoreCache: mockIgnoreCacheFlag, }, }); - - let fetchedMeshConfig = sampleCreateMeshConfig; - fetchedMeshConfig.meshId = 'dummy_id'; - fetchedMeshConfig.meshURL = ''; - - getMesh.mockResolvedValue(fetchedMeshConfig); }); afterEach(() => { @@ -284,4 +287,74 @@ describe('describe command tests', () => { `); expect(errorLogSpy.mock.calls).toMatchInlineSnapshot(`[]`); }); + + test('should show prod edge mesh url on workspace named "Production" if feature is enabled', async () => { + // mock the edge mesh url feature to be enabled + getTenantFeatures.mockResolvedValueOnce({ + imsOrgId: selectedOrg.code, + showCloudflareURL: true, + }); + + // mock the workspace name to "Production" + initSdk.mockResolvedValueOnce({ + workspaceName: 'Production', + }); + + await DescribeCommand.run(); + + expect(logSpy).toHaveBeenCalledWith( + expect.stringContaining('Legacy Mesh Endpoint:'), + 'https://graph.adobe.io/api/dummy_meshId/graphql?api_key=dummy_apiKey', + ); + + expect(logSpy).toHaveBeenCalledWith( + expect.stringContaining('Edge Mesh Endpoint:'), + 'https://edge-graph.adobe.io/api/dummy_meshId/graphql', + ); + }); + + test('should show sandbox edge mesh url on workspace NOT named "Production" if feature is enabled', async () => { + // mock the edge mesh url feature to be enabled + getTenantFeatures.mockResolvedValueOnce({ + imsOrgId: selectedOrg.code, + showCloudflareURL: true, + }); + + // mock the workspace name to a value not equal to "Production" + initSdk.mockResolvedValueOnce({ + workspaceName: 'AnythingButProduction', + }); + + await DescribeCommand.run(); + + expect(logSpy).toHaveBeenCalledWith( + expect.stringContaining('Legacy Mesh Endpoint:'), + 'https://graph.adobe.io/api/dummy_meshId/graphql?api_key=dummy_apiKey', + ); + + expect(logSpy).toHaveBeenCalledWith( + expect.stringContaining('Edge Mesh Endpoint:'), + 'https://edge-sandbox-graph.adobe.io/api/dummy_meshId/graphql', + ); + }); + + test('should not show edge mesh url if feature is disabled', async () => { + // mock the edge mesh url feature to be disabled + getTenantFeatures.mockResolvedValueOnce({ + imsOrgId: selectedOrg.code, + showCloudflareURL: false, + }); + + await DescribeCommand.run(); + + expect(logSpy).not.toHaveBeenCalledWith( + expect.stringContaining('Edge Mesh Endpoint:'), + expect.any(String), + ); + + expect(logSpy).toHaveBeenCalledWith( + expect.stringContaining('Mesh Endpoint:'), + 'https://graph.adobe.io/api/dummy_meshId/graphql?api_key=dummy_apiKey', + ); + }); }); diff --git a/src/commands/api-mesh/__tests__/update.test.js b/src/commands/api-mesh/__tests__/update.test.js index 85d59e77..1b00c8c6 100644 --- a/src/commands/api-mesh/__tests__/update.test.js +++ b/src/commands/api-mesh/__tests__/update.test.js @@ -52,6 +52,9 @@ describe('update command tests', () => { imsOrgId: selectedOrg.id, projectId: selectedProject.id, workspaceId: selectedWorkspace.id, + workspaceName: selectedWorkspace.title, + orgName: selectedOrg.name, + projectName: selectedProject.title, }); global.requestId = 'dummy_request_id'; @@ -554,6 +557,9 @@ describe('update command tests', () => { "1234", "5678", "123456789", + "Workspace01", + "ORG01", + "Project01", "mesh_id", { "meshConfig": { diff --git a/src/commands/api-mesh/create.js b/src/commands/api-mesh/create.js index c15efea6..404d1d30 100644 --- a/src/commands/api-mesh/create.js +++ b/src/commands/api-mesh/create.js @@ -10,10 +10,9 @@ governing permissions and limitations under the License. */ const { Command } = require('@oclif/core'); - +const chalk = require('chalk'); const { initSdk, initRequestId, promptConfirm, importFiles } = require('../../helpers'); const logger = require('../../classes/logger'); -const CONSTANTS = require('../../constants'); const { ignoreCacheFlag, autoConfirmActionFlag, @@ -27,9 +26,8 @@ const { interpolateSecrets, validateSecretsFile, } = require('../../utils'); -const { getMesh, createMesh } = require('../../lib/devConsole'); - -const { MULTITENANT_GRAPHQL_SERVER_BASE_URL } = CONSTANTS; +const { createMesh, getTenantFeatures } = require('../../lib/devConsole'); +const { buildEdgeMeshUrl, buildMeshUrl } = require('../../urlBuilder'); class CreateCommand extends Command { static args = [{ name: 'file' }]; @@ -60,8 +58,15 @@ class CreateCommand extends Command { const autoConfirmAction = await flags.autoConfirmAction; const envFilePath = await flags.env; const secretsFilePath = await flags.secrets; - - const { imsOrgId, projectId, workspaceId, workspaceName } = await initSdk({ + const { + imsOrgId, + imsOrgCode, + projectId, + workspaceId, + workspaceName, + orgName, + projectName, + } = await initSdk({ ignoreCache, }); @@ -127,6 +132,8 @@ class CreateCommand extends Command { projectId, workspaceId, workspaceName, + orgName, + projectName, data, ); @@ -150,25 +157,23 @@ class CreateCommand extends Command { if (sdkList) { this.log('Successfully subscribed API Key %s to API Mesh service', apiKey); - const { meshURL } = await getMesh( + const meshUrl = await buildMeshUrl( imsOrgId, projectId, workspaceId, workspaceName, mesh.meshId, + apiKey, ); - const meshUrl = - meshURL === '' || meshURL === undefined - ? MULTITENANT_GRAPHQL_SERVER_BASE_URL - : meshURL; - - if (apiKey && MULTITENANT_GRAPHQL_SERVER_BASE_URL.includes(meshUrl)) { - this.log( - 'Mesh Endpoint: %s\n', - `${meshUrl}/${mesh.meshId}/graphql?api_key=${apiKey}`, - ); + + const { showCloudflareURL: showEdgeMeshUrl } = await getTenantFeatures(imsOrgCode); + + if (showEdgeMeshUrl) { + const edgeMeshUrl = buildEdgeMeshUrl(mesh.meshId, workspaceName); + this.log('Legacy Mesh Endpoint: %s', meshUrl); + this.log(chalk.bold('Edge Mesh Endpoint: %s\n'), edgeMeshUrl); } else { - this.log('Mesh Endpoint: %s\n', `${meshUrl}/${mesh.meshId}/graphql`); + this.log('Mesh Endpoint: %s\n', meshUrl); } } else { this.log('Unable to subscribe API Key %s to API Mesh service', apiKey); diff --git a/src/commands/api-mesh/describe.js b/src/commands/api-mesh/describe.js index 77e14b98..795b3386 100644 --- a/src/commands/api-mesh/describe.js +++ b/src/commands/api-mesh/describe.js @@ -10,17 +10,16 @@ governing permissions and limitations under the License. */ const { Command } = require('@oclif/command'); +const chalk = require('chalk'); const logger = require('../../classes/logger'); const { initSdk, initRequestId } = require('../../helpers'); -const CONSTANTS = require('../../constants'); const { ignoreCacheFlag } = require('../../utils'); -const { describeMesh, getMesh } = require('../../lib/devConsole'); +const { describeMesh, getTenantFeatures } = require('../../lib/devConsole'); +const { buildMeshUrl, buildEdgeMeshUrl } = require('../../urlBuilder'); require('dotenv').config(); -const { MULTITENANT_GRAPHQL_SERVER_BASE_URL } = CONSTANTS; - class DescribeCommand extends Command { static flags = { ignoreCache: ignoreCacheFlag, @@ -32,10 +31,8 @@ class DescribeCommand extends Command { logger.info(`RequestId: ${global.requestId}`); const { flags } = await this.parse(DescribeCommand); - const ignoreCache = await flags.ignoreCache; - - const { imsOrgId, projectId, workspaceId, workspaceName } = await initSdk({ + const { imsOrgId, imsOrgCode, projectId, workspaceId, workspaceName } = await initSdk({ ignoreCache, }); @@ -44,29 +41,32 @@ class DescribeCommand extends Command { if (meshDetails) { const { meshId, apiKey } = meshDetails; + const { showCloudflareURL: showEdgeMeshUrl } = await getTenantFeatures(imsOrgCode); if (meshId) { - this.log('Successfully retrieved mesh details \n'); - this.log('Org ID: %s', imsOrgId); - this.log('Project ID: %s', projectId); - this.log('Workspace ID: %s', workspaceId); - this.log('Mesh ID: %s', meshId); - - const { meshURL } = await getMesh( + const meshUrl = await buildMeshUrl( imsOrgId, projectId, workspaceId, workspaceName, meshId, + apiKey, ); - const meshUrl = - meshURL === '' || meshURL === undefined ? MULTITENANT_GRAPHQL_SERVER_BASE_URL : meshURL; - if (apiKey && MULTITENANT_GRAPHQL_SERVER_BASE_URL.includes(meshUrl)) { - this.log('Mesh Endpoint: %s\n', `${meshUrl}/${meshId}/graphql?api_key=${apiKey}`); + this.log('Successfully retrieved mesh details \n'); + this.log('Org ID: %s', imsOrgId); + this.log('Project ID: %s', projectId); + this.log('Workspace ID: %s', workspaceId); + this.log('Mesh ID: %s', meshId); + + if (showEdgeMeshUrl) { + const edgeMeshUrl = buildEdgeMeshUrl(meshId, workspaceName); + this.log('Legacy Mesh Endpoint: %s', meshUrl); + this.log(chalk.bold('Edge Mesh Endpoint: %s\n'), edgeMeshUrl); } else { - this.log('Mesh Endpoint: %s\n', `${meshUrl}/${meshId}/graphql`); + this.log('Mesh Endpoint: %s\n', meshUrl); } + return meshDetails; } else { logger.error( diff --git a/src/commands/api-mesh/source/__tests__/install.test.js b/src/commands/api-mesh/source/__tests__/install.test.js index 9f467c5b..60ac5ab7 100644 --- a/src/commands/api-mesh/source/__tests__/install.test.js +++ b/src/commands/api-mesh/source/__tests__/install.test.js @@ -40,6 +40,8 @@ initSdk.mockResolvedValue({ projectId: selectedProject.id, workspaceId: selectedWorkspace.id, workspaceName: selectedWorkspace.title, + organizationName: selectedOrg.name, + projectName: selectedProject.title, }); initRequestId.mockResolvedValue({}); promptInput.mockResolvedValueOnce('test-03'); @@ -148,6 +150,8 @@ describe('source:install command tests', () => { selectedProject.id, selectedWorkspace.id, selectedWorkspace.title, + selectedOrg.name, + selectedProject.title, 'dummy_meshId', res, ); diff --git a/src/commands/api-mesh/source/install.js b/src/commands/api-mesh/source/install.js index 9df0837b..8d6e38c3 100644 --- a/src/commands/api-mesh/source/install.js +++ b/src/commands/api-mesh/source/install.js @@ -36,7 +36,14 @@ class InstallCommand extends Command { await initRequestId(); logger.info(`RequestId: ${global.requestId}`); const ignoreCache = await flags.ignoreCache; - const { imsOrgId, projectId, workspaceId, workspaceName } = await initSdk({ ignoreCache }); + const { + imsOrgId, + projectId, + workspaceId, + organizationName, + projectName, + workspaceName, + } = await initSdk({ ignoreCache }); const filepath = flags['variable-file']; let variables = flags.variable ? flags.variable.reduce((obj, val) => { @@ -199,9 +206,18 @@ class InstallCommand extends Command { } try { - const response = await updateMesh(imsOrgId, projectId, workspaceId, workspaceName, meshId, { - meshConfig: mesh.meshConfig, - }); + const response = await updateMesh( + imsOrgId, + projectId, + workspaceId, + workspaceName, + organizationName, + projectName, + meshId, + { + meshConfig: mesh.meshConfig, + }, + ); this.log('Successfully updated the mesh with the id: %s', meshId); diff --git a/src/commands/api-mesh/status.js b/src/commands/api-mesh/status.js index f84b76fe..27f0989f 100644 --- a/src/commands/api-mesh/status.js +++ b/src/commands/api-mesh/status.js @@ -1,7 +1,14 @@ const { Command } = require('@oclif/core'); +const chalk = require('chalk'); + const logger = require('../../classes/logger'); const { initRequestId, initSdk } = require('../../helpers'); -const { getMeshId, getMesh } = require('../../lib/devConsole'); +const { + getMeshId, + getMesh, + getTenantFeatures, + getMeshDeployments, +} = require('../../lib/devConsole'); const { ignoreCacheFlag } = require('../../utils'); require('dotenv').config(); @@ -17,8 +24,7 @@ class StatusCommand extends Command { const { flags } = await this.parse(StatusCommand); const ignoreCache = await flags.ignoreCache; - - const { imsOrgId, projectId, workspaceId, workspaceName } = await initSdk({ + const { imsOrgId, imsOrgCode, projectId, workspaceId, workspaceName } = await initSdk({ ignoreCache, }); @@ -35,50 +41,18 @@ class StatusCommand extends Command { if (meshId) { try { + const { showCloudflareURL: showEdgeMeshUrl } = await getTenantFeatures(imsOrgCode); const mesh = await getMesh(imsOrgId, projectId, workspaceId, workspaceName, meshId); - switch (mesh.meshStatus) { - case 'success': - this.log( - '******************************************************************************************************', - ); - this.log('Your mesh has been successfully built.'); - this.log( - '******************************************************************************************************', - ); - break; - case 'pending': - this.log( - '******************************************************************************************************', - ); - this.log('Your mesh is awaiting processing.'); - this.log( - '******************************************************************************************************', - ); - break; - case 'building': - this.log( - '******************************************************************************************************', - ); - this.log( - 'Your mesh is currently being provisioned. Please wait a few minutes before checking again.', - ); - this.log( - '******************************************************************************************************', - ); - break; - case 'error': - this.log( - '******************************************************************************************************', - ); - this.log('Your mesh errored out with the following error. ', mesh.error); - this.log( - '******************************************************************************************************', - ); - break; + const meshLabel = showEdgeMeshUrl ? chalk.bold(`Legacy Mesh:`) : 'Your mesh'; + + this.log(''.padEnd(102, '*')); + this.displayMeshStatus(mesh, meshLabel); + if (showEdgeMeshUrl) { + await this.displayEdgeMeshStatus(mesh, imsOrgCode, projectId, workspaceId); } + this.log(''.padEnd(102, '*')); } catch (err) { this.log(err.message); - this.error( `Unable to get the mesh status. If the error persists please contact support. RequestId: ${global.requestId}`, ); @@ -89,6 +63,88 @@ class StatusCommand extends Command { ); } } + + /** + * Display the status of the mesh. + * + * @param mesh - Mesh data + * @param meshLabel - Label to display for the mesh based on the mesh type + */ + displayMeshStatus(mesh, meshLabel = 'Your mesh') { + switch (mesh.meshStatus) { + case 'success': + this.log(`${meshLabel} has been successfully built.`); + break; + case 'pending': + this.log(`${meshLabel} is awaiting processing.`); + break; + case 'building': + this.log( + `${meshLabel} is currently being provisioned. Please wait a few minutes before checking again.`, + ); + break; + case 'error': + this.log( + meshLabel === 'Your mesh' + ? `${meshLabel} errored out with the following error.` + : `${meshLabel} build has errors.`, + ); + this.log(mesh.error); + break; + } + } + + /** + * Display the status of the edge mesh. + * + * While the mesh is not successfully built, the edge mesh status will match the legacy mesh status. + * Once the build is successful, the edge mesh status will reflect the deployment status + * @param mesh + * @param imsOrgCode + * @param projectId + * @param workspaceId + * @returns {Promise} + */ + async displayEdgeMeshStatus(mesh, imsOrgCode, projectId, workspaceId) { + const edgeMeshLabel = chalk.bold(`Edge Mesh:`); + const buildStatus = mesh.meshStatus; + + if (buildStatus !== 'success') { + this.displayMeshStatus(mesh, edgeMeshLabel); + } else { + const meshDeployments = await getMeshDeployments( + imsOrgCode, + projectId, + workspaceId, + mesh.meshId, + ); + + const edgeDeploymentStatus = String(meshDeployments.status).toLowerCase(); + + switch (edgeDeploymentStatus) { + case 'success': + this.log(`${edgeMeshLabel} has been successfully built.`); + break; + case 'provisioning': + this.log( + `${edgeMeshLabel} is currently being provisioned. Please wait a few minutes before checking again.`, + ); + break; + case 'de-provisioning': + this.log( + `${edgeMeshLabel} is currently being de-provisioned. Please wait a few minutes before checking again.`, + ); + break; + case 'error': + this.log(`${edgeMeshLabel} ${meshDeployments.error}`); + break; + default: + this.log( + `${edgeMeshLabel} status is not available. Please wait for a while and try again.`, + ); + } + } + } } StatusCommand.description = 'Get a mesh status with a given meshid.'; diff --git a/src/commands/api-mesh/update.js b/src/commands/api-mesh/update.js index 4c558aeb..54e3eb18 100644 --- a/src/commands/api-mesh/update.js +++ b/src/commands/api-mesh/update.js @@ -54,9 +54,11 @@ class UpdateCommand extends Command { const envFilePath = await flags.env; const secretsFilePath = await flags.secrets; - const { imsOrgId, projectId, workspaceId } = await initSdk({ - ignoreCache, - }); + const { imsOrgId, projectId, workspaceId, orgName, projectName, workspaceName } = await initSdk( + { + ignoreCache, + }, + ); //Input the mesh data from the input file let inputMeshData = await readFileContents(args.file, this, 'mesh'); @@ -128,7 +130,16 @@ class UpdateCommand extends Command { if (shouldContinue) { try { - const response = await updateMesh(imsOrgId, projectId, workspaceId, meshId, data); + const response = await updateMesh( + imsOrgId, + projectId, + workspaceId, + workspaceName, + orgName, + projectName, + meshId, + data, + ); this.log( '******************************************************************************************************', diff --git a/src/constants.js b/src/constants.js index 362692d0..48346aed 100644 --- a/src/constants.js +++ b/src/constants.js @@ -9,6 +9,7 @@ const StageConstants = { DEV_CONSOLE_TRANSPORTER_API_KEY: 'UDPWeb1', AIO_CLI_API_KEY: 'aio-cli-console-auth-stage', SMS_BASE_URL: 'https://graph-stage.adobe.io/api-admin', + EDGE_MESH_BASE_URL: 'https://edge-stage-graph.adobe.io/api', }; const ProdConstants = { @@ -18,6 +19,8 @@ const ProdConstants = { DEV_CONSOLE_TRANSPORTER_API_KEY: 'UDPWeb1', AIO_CLI_API_KEY: 'aio-cli-console-auth', SMS_BASE_URL: 'https://graph.adobe.io/api-admin', + EDGE_MESH_BASE_URL: 'https://edge-graph.adobe.io/api', + EDGE_MESH_SANDBOX_BASE_URL: 'https://edge-sandbox-graph.adobe.io/api', }; const envConstants = clientEnv === 'stage' ? StageConstants : ProdConstants; diff --git a/src/helpers.js b/src/helpers.js index f39664f2..052219bd 100644 --- a/src/helpers.js +++ b/src/helpers.js @@ -423,9 +423,12 @@ async function initSdk(options) { return { imsOrgId: org.id, + imsOrgCode: org.code, projectId: project.id, workspaceId: workspace.id, workspaceName: workspace.title, + orgName: org.name, + projectName: project.title, }; } diff --git a/src/lib/devConsole.js b/src/lib/devConsole.js index 1843558e..a3c2dd04 100644 --- a/src/lib/devConsole.js +++ b/src/lib/devConsole.js @@ -11,7 +11,7 @@ const util = require('util'); const exec = util.promisify(require('child_process').exec); const contentDisposition = require('content-disposition'); -const { DEV_CONSOLE_TRANSPORTER_API_KEY } = CONSTANTS; +const { DEV_CONSOLE_TRANSPORTER_API_KEY, SMS_BASE_URL } = CONSTANTS; const { objToString, getDevConsoleConfig } = require('../helpers'); @@ -193,7 +193,15 @@ const getMesh = async (organizationId, projectId, workspaceId, workspaceName, me } }; -const createMesh = async (organizationId, projectId, workspaceId, workspaceName, data) => { +const createMesh = async ( + organizationId, + projectId, + workspaceId, + workspaceName, + orgName, + projectName, + data, +) => { const { baseUrl: devConsoleUrl, accessToken, apiKey } = await getDevConsoleConfig(); const config = { method: 'post', @@ -202,6 +210,9 @@ const createMesh = async (organizationId, projectId, workspaceId, workspaceName, 'Authorization': `Bearer ${accessToken}`, 'Content-Type': 'application/json', 'x-request-id': global.requestId, + 'workspaceName': workspaceName, + 'orgName': orgName, + 'projectName': projectName, }, data: JSON.stringify(data), }; @@ -314,7 +325,28 @@ const createMesh = async (organizationId, projectId, workspaceId, workspaceName, } }; -const updateMesh = async (organizationId, projectId, workspaceId, meshId, data) => { +/** + * Update an API Mesh. + * @param {string} organizationId Organization identifier. + * @param {string} projectId Project identifier. + * @param {string} workspaceId Workspace identifier. + * @param {string} workspaceName Workspace Name. + * @param {string} orgName Organization name. + * @param {string} projectName Project name. + * @param {string} meshId Mesh identifier. + * @param {unknown} data Mesh configuration data. + * @returns {Promise} + */ +const updateMesh = async ( + organizationId, + projectId, + workspaceId, + workspaceName, + orgName, + projectName, + meshId, + data, +) => { const { baseUrl: devConsoleUrl, accessToken, apiKey } = await getDevConsoleConfig(); const config = { method: 'put', @@ -323,6 +355,9 @@ const updateMesh = async (organizationId, projectId, workspaceId, meshId, data) 'Authorization': `Bearer ${accessToken}`, 'Content-Type': 'application/json', 'x-request-id': global.requestId, + 'workspaceName': workspaceName, + 'orgName': orgName, + 'projectName': projectName, }, data: JSON.stringify(data), }; @@ -821,6 +856,118 @@ const getMeshArtifact = async (organizationId, projectId, workspaceId, workspace } }; +/** + * Gets the enabled features for the tenant. + * + * This request bypasses the Dev Console and is sent directly to the Schema Management Service. + * As a result, we provide the orgCode instead of orgId since Dev Console usually performs the translation. + * The near-term goal is to stop using Dev Console as a proxy for all routes. + * @param organizationCode + * @returns {Promise} + */ +const getTenantFeatures = async organizationCode => { + const { accessToken, apiKey } = await getDevConsoleConfig(); + const config = { + method: 'get', + url: `${SMS_BASE_URL}/organizations/${organizationCode}/features?API_KEY=${apiKey}`, + headers: { + 'Authorization': `Bearer ${accessToken}`, + 'x-request-id': global.requestId, + }, + }; + + logger.info( + 'Initiating GET %s', + `${SMS_BASE_URL}/organizations/${organizationCode}/features?API_KEY=${apiKey}`, + ); + + try { + const response = await axios(config); + + logger.info('Response from GET %s', response.status); + + if (response?.status === 200) { + logger.info(`Tenant Features : ${objToString(response, ['data'])}`); + + return response.data; + } else { + let errorMessage = `Something went wrong: ${objToString( + response, + ['data'], + 'Unable to get tenant features.', + )}`; + logger.error(`${errorMessage}. Received ${response.status} response instead of 200`); + + throw new Error(errorMessage); + } + } catch (error) { + logger.error(`Error getting features for organization: ${organizationCode}`); + + return { + imsOrgId: organizationCode, + showCloudflareURL: false, + }; + } +}; + +/** + * Gets the deployments value for mesh. + * + * This request bypasses the Dev Console and is sent directly to the Schema Management Service. + * As a result, we provide the orgCode instead of orgId since Dev Console usually performs the translation. + * The near-term goal is to stop using Dev Console as a proxy for all routes. + * @param organizationCode + * @param projectId + * @param workspaceId + * @param meshId + * @returns {Promise} + */ +const getMeshDeployments = async (organizationCode, projectId, workspaceId, meshId) => { + const { accessToken, apiKey } = await getDevConsoleConfig(); + const config = { + method: 'get', + url: `${SMS_BASE_URL}/organizations/${organizationCode}/projects/${projectId}/workspaces/${workspaceId}/meshes/${meshId}/deployments/latest?API_KEY=${apiKey}`, + headers: { + 'Authorization': `Bearer ${accessToken}`, + 'x-request-id': global.requestId, + }, + }; + + logger.info( + 'Initiating GET %s', + `${SMS_BASE_URL}/organizations/${organizationCode}/projects/${projectId}/workspaces/${workspaceId}/meshes/${meshId}/deployments/latest?API_KEY=${apiKey}`, + ); + + try { + const response = await axios(config); + + logger.info('Response from GET %s', response.status); + + if (response?.status === 200) { + logger.info(`Tenant mesh deployments : ${objToString(response, ['data'])}`); + + return response.data; + } else { + let errorMessage = `Something went wrong: ${objToString( + response, + ['data'], + 'Unable to get mesh deployment.', + )}`; + logger.error(`${errorMessage}. Received ${response.status} response instead of 200`); + + throw new Error(errorMessage); + } + } catch (error) { + logger.error(`Error fetching deployments for mesh: ${meshId}`); + + return { + status: 'ERROR', + meshId: meshId, + error: 'Mesh status is not available.', + }; + } +}; + module.exports = { getApiKeyCredential, describeMesh, @@ -835,4 +982,6 @@ module.exports = { subscribeCredentialToMeshService, unsubscribeCredentialFromMeshService, getMeshArtifact, + getTenantFeatures, + getMeshDeployments, }; diff --git a/src/urlBuilder.js b/src/urlBuilder.js new file mode 100644 index 00000000..50ea4776 --- /dev/null +++ b/src/urlBuilder.js @@ -0,0 +1,58 @@ +const CONSTANTS = require('./constants'); +const { getMesh } = require('./lib/devConsole'); + +const { + MULTITENANT_GRAPHQL_SERVER_BASE_URL, + EDGE_MESH_BASE_URL, + EDGE_MESH_SANDBOX_BASE_URL, +} = CONSTANTS; + +/** + * Build the mesh url for the multitenant mesh. + * + * Gets the mesh details to checks for a custom domain in the case of a TI mesh. + * @param imsOrgId + * @param projectId + * @param workspaceId + * @param workspaceName + * @param meshId + * @param apiKey + * @returns {Promise} + */ +async function buildMeshUrl(imsOrgId, projectId, workspaceId, workspaceName, meshId, apiKey) { + const { meshURL: customBaseUrl } = await getMesh( + imsOrgId, + projectId, + workspaceId, + workspaceName, + meshId, + ); + + return customBaseUrl + ? `${customBaseUrl}/${meshId}/graphql` + : `${MULTITENANT_GRAPHQL_SERVER_BASE_URL}/${meshId}/graphql${ + apiKey ? `?api_key=${apiKey}` : '' + }`; +} + +/** + * Builds the mesh url for the edge mesh. + * + * Uses the url for the appropriate Cloudflare namespace based on the console workspace name. + * @param meshId + * @param workspaceName + * @returns {string} + */ +function buildEdgeMeshUrl(meshId, workspaceName) { + let baseUrl; + + if (EDGE_MESH_BASE_URL.includes('stage')) { + baseUrl = EDGE_MESH_BASE_URL; + } else { + baseUrl = workspaceName === 'Production' ? EDGE_MESH_BASE_URL : EDGE_MESH_SANDBOX_BASE_URL; + } + + return `${baseUrl}/${meshId}/graphql`; +} + +module.exports = { buildMeshUrl, buildEdgeMeshUrl }; diff --git a/yarn.lock b/yarn.lock index b5290110..9f4ced80 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7,10 +7,10 @@ resolved "https://registry.yarnpkg.com/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz#bd9154aec9983f77b3a034ecaa015c2e4201f6cf" integrity sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA== -"@adobe-apimesh/mesh-builder@1.4.2": - version "1.4.2" - resolved "https://registry.yarnpkg.com/@adobe-apimesh/mesh-builder/-/mesh-builder-1.4.2.tgz#bbe0a24c0668faece7d2918b223afb6fb92078bb" - integrity sha512-aTHZ353md/HVHXvk4NJ4qhCqk3nQylpDBLDfB6IiI/pRYf0dthWIF0Pfpu3RGfr6G7R8NbR5/MVrvkqM320UVw== +"@adobe-apimesh/mesh-builder@1.4.4": + version "1.4.4" + resolved "https://registry.yarnpkg.com/@adobe-apimesh/mesh-builder/-/mesh-builder-1.4.4.tgz#860c13f3f66d7aea9a37f9a7f6a6d5cb0bf3a773" + integrity sha512-Dh2Gh0Wzq06BTU06Ey5VvNgdC39SzyDf/QkRkWoXPINz2wXpCyHnFdUcKWOUI4RduSb1MFtKKiDsBOohe2qgRQ== dependencies: "@fastify/request-context" "^4.1.0" eslint "^8.39.0"