UNPKG

serverless-offline-msk

Version:

A serverless offline plugin that enables AWS MSK events

399 lines (366 loc) 12.8 kB
'use strict'; const _ = require('lodash'); const chalk = require('chalk'); const { log } = require('@serverless/utils/log'); const accountUtils = require('@serverless/utils/account'); const configUtils = require('@serverless/utils/config'); const { ServerlessSDK } = require('@serverless/platform-client'); const promptWithHistory = require('@serverless/utils/inquirer/prompt-with-history'); const { writeOrgAndApp, showOnboardingWelcome } = require('./utils'); const { getPlatformClientWithAccessKey, getOrCreateAccessKeyForOrg, } = require('@serverless/dashboard-plugin/lib/client-utils'); const isValidAppName = RegExp.prototype.test.bind(/^[a-z0-9](?:[a-z0-9-]{0,126}[a-z0-9])?$/); const orgUpdateConfirm = async (stepHistory) => { log.notice( 'Service has monitoring enabled, but it is configured with the "org" to which you do not have access' ); log.notice(); const shouldUpdateOrg = await promptWithHistory({ message: 'Would you like to update it?', type: 'confirm', name: 'shouldUpdateOrg', stepHistory, }); return shouldUpdateOrg; }; const appUpdateConfirm = async (appName, orgName, stepHistory) => { log.notice('The "app" used in this Service does not yet exist in your Organization.'); log.notice(); const appUpdateType = await promptWithHistory({ message: 'What would you like to do?', type: 'list', name: 'appUpdateType', stepHistory, choices: [ { name: `Create '${appName}' app in '${orgName}' org`, value: '_create_' }, { name: 'Switch to one of the existing apps (or create new one with different name)', value: '_choose_existing_', }, { name: "Skip, I'll sort this out manually", value: '_skip_' }, ], }); return appUpdateType; }; const orgsChoice = async (orgNames, stepHistory) => { const orgName = await promptWithHistory({ message: 'What org do you want to add this service to?', type: 'list', name: 'orgName', choices: [...orgNames, { name: '[Skip]', value: '_skip_' }], stepHistory, }); return orgName; }; const appNameChoice = async (appNames, stepHistory) => { const appName = await promptWithHistory({ message: 'What application do you want to add this to?', type: 'list', name: 'appName', choices: Array.from(appNames).concat({ name: '[create a new app]', value: '_create_' }), stepHistory, }); return appName; }; const appNameInput = async (appNames, stepHistory) => { const appName = await promptWithHistory({ message: 'What do you want to name this application?', type: 'input', name: 'newAppName', stepHistory, validate: (input) => { input = input.trim(); if (!isValidAppName(input)) { return ( 'App name is not valid.\n' + ' - It should only contain lowercase alphanumeric and hyphens.\n' + ' - It should start and end with an alphanumeric character.\n' + " - Shouldn't exceed 128 characters" ); } if (appNames.includes(input)) return 'App of this name already exists'; return true; }, }); return appName; }; const steps = { resolveOrgNames: async (user) => { if (process.env.SERVERLESS_ACCESS_KEY) { const sdk = new ServerlessSDK({ accessKey: process.env.SERVERLESS_ACCESS_KEY }); const { orgName } = await sdk.accessKeys.get(); return new Set([orgName]); } let orgs = new Set(); if (!user.idToken) { // User registered over CLI hence idToken is not stored. // Still to resolve organizations from platform idToken is needed. // Handling it gently by assuming that orgs listed in config file // make a valid representation for (const org of Object.keys(user.accessKeys)) orgs.add(org); } else { const sdk = new ServerlessSDK(); await accountUtils.refreshToken(sdk); user = configUtils.getLoggedInUser(); sdk.config({ accessKey: user.idToken }); orgs = new Set( (await sdk.organizations.list({ username: user.username })).map((org) => org.tenantName) ); } return orgs; }, setOrgAndApp: async (context, stepData) => { const { history, stepHistory } = context; const { orgNames, isFullyPreconfigured, isMonitoringOverridenByCli } = stepData; let { orgName, apps, newAppName, appName } = stepData; if (!orgName) { orgName = await (async () => { // We only want to automatically select the single available org in situations where user explicitly // logged in/registered during the process and created new service, for existing services we want to always ask // that question. It will also be always asked if `SERVERLESS_ACCESS_KEY` was provided if ( orgNames.size === 1 && history && history.has('dashboardLogin') && history.has('service') ) { return orgNames.values().next().value; } return orgsChoice(orgNames, stepHistory); })(); } if (orgName === '_skip_') { return; } const sdk = await getPlatformClientWithAccessKey(orgName); if (!newAppName && !appName) { if (!apps) apps = await sdk.apps.list({ orgName }); const appNames = apps.map((app) => app.appName); if (!apps.length) { newAppName = context.configuration.service; } else { appName = await appNameChoice(appNames, stepHistory); if (appName === '_create_') { newAppName = await appNameInput(appNames, stepHistory); } } } if (newAppName) { ({ appName } = await sdk.apps.create({ orgName, app: { name: newAppName } })); } if (isMonitoringOverridenByCli && isFullyPreconfigured) { const shouldOverrideDashboardConfig = await promptWithHistory({ message: 'Are you sure you want to update monitoring settings ' + `to ${chalk.bold(`org: ${orgName}, app: ${appName}`)}`, type: 'confirm', name: 'shouldOverrideDashboardConfig', stepHistory, }); if (!shouldOverrideDashboardConfig) { delete context.configuration.app; delete context.configuration.org; return; } } log.notice(); log.notice.success( `Your project is ready to be deployed to Serverless Dashboard (org: "${orgName}", app: "${appName}")` ); await writeOrgAndApp(orgName, appName, context); return; }, }; module.exports = { async isApplicable(context) { const { configuration, options, serviceDir, isConsole } = context; if (!serviceDir) { context.inapplicabilityReasonCode = 'NOT_IN_SERVICE_DIRECTORY'; return false; } if (isConsole) { context.inapplicabilityReasonCode = 'CONSOLE_CONTEXT'; return false; } if ( _.get(configuration, 'provider') !== 'aws' && _.get(configuration, 'provider.name') !== 'aws' ) { context.inapplicabilityReasonCode = 'NON_AWS_PROVIDER'; return false; } const sdk = new ServerlessSDK(); const { supportedRegions, supportedRuntimes } = await sdk.metadata.get(); // We want to still allow onboarding from Dashboard (idenfitied by explicit `--org` passed) if ( !options.org && !supportedRuntimes.includes(_.get(configuration.provider, 'runtime') || 'nodejs12.x') ) { context.inapplicabilityReasonCode = 'UNSUPPORTED_RUNTIME'; return false; } if ( !supportedRegions.includes(options.region || configuration.provider.region || 'us-east-1') ) { context.inapplicabilityReasonCode = 'UNSUPPORTED_REGION'; return false; } const usesServerlessAccessKey = Boolean(process.env.SERVERLESS_ACCESS_KEY); let user = configUtils.getLoggedInUser(); if (!user && !usesServerlessAccessKey) { context.inapplicabilityReasonCode = 'NOT_LOGGED_IN'; return false; } const orgNames = await steps.resolveOrgNames(user); if (!orgNames.size) { context.inapplicabilityReasonCode = 'NO_ORGS_AVAILABLE'; return false; } if (!usesServerlessAccessKey) { user = configUtils.getLoggedInUser(); // Refreshed, as new token might have been generated } const isMonitoringPreconfigured = Boolean(configuration.org); const orgName = options.org || configuration.org; const appName = options.app || configuration.app; const isDashboardAppPreconfigured = Boolean(configuration.app); const isMonitoringOverridenByCli = isMonitoringPreconfigured && ((options.org && options.org !== configuration.org) || (options.app && options.app !== configuration.app)); const isFullyPreconfigured = isMonitoringPreconfigured && isDashboardAppPreconfigured; if (orgName && orgNames.has(orgName)) { if (!isValidAppName(appName)) { return { user, orgName, isFullyPreconfigured, isMonitoringPreconfigured, isDashboardAppPreconfigured, isMonitoringOverridenByCli, }; } const accessKey = await getOrCreateAccessKeyForOrg(orgName); sdk.config({ accessKey }); const apps = await sdk.apps.list({ orgName }); if (options.org || options.app) { if (apps.some((app) => app.appName === appName)) { if ( isMonitoringPreconfigured && isDashboardAppPreconfigured && !isMonitoringOverridenByCli ) { context.inapplicabilityReasonCode = 'MONITORING_NOT_OVERRIDEN_BY_CLI'; return false; } return { user, orgName, appName, isFullyPreconfigured, isMonitoringPreconfigured, isDashboardAppPreconfigured, isMonitoringOverridenByCli, }; } if (options.app) { log.error(); log.error('Passed value for "--app" doesn\'t seem to correspond to chosen organization.'); } return { user, orgName, isFullyPreconfigured, isMonitoringPreconfigured, isDashboardAppPreconfigured, isMonitoringOverridenByCli, }; } else if (apps.some((app) => app.appName === appName)) { if (!isMonitoringOverridenByCli) { context.inapplicabilityReasonCode = 'HAS_MONITORING_SETUP'; return false; } return { user, orgName, appName, isFullyPreconfigured, isMonitoringPreconfigured, isDashboardAppPreconfigured, isMonitoringOverridenByCli, }; } return { user, orgName, apps, newAppName: appName, isFullyPreconfigured, isMonitoringPreconfigured, isDashboardAppPreconfigured, isMonitoringOverridenByCli, }; } else if (orgName) { if (options.org) { log.error(); log.error( 'Passed value for "--org" doesn\'t seem to correspond to account with which you\'re logged in with.' ); } else { log.error(); log.error(`Configured org "${orgName}" is not available in your account.`); } } return { user, orgNames, isFullyPreconfigured, isMonitoringPreconfigured, isDashboardAppPreconfigured, isMonitoringOverridenByCli, }; }, async run(context, stepData) { const { configuration, options, stepHistory } = context; if (context.initial.isInServiceContext && !context.initial.isDashboardEnabled) { showOnboardingWelcome(context); } if (!stepData.orgName) delete configuration.org; if (!stepData.appName && !stepData.newAppName) delete configuration.app; if (!options.org && !options.app) { if (stepData.isMonitoringPreconfigured) { if (!stepData.orgName) { if (!(await orgUpdateConfirm(stepHistory))) return; } else if (stepData.newAppName && stepData.isMonitoringPreconfigured) { const appUpdateTypeChoice = await appUpdateConfirm( stepData.newAppName, stepData.orgName, stepHistory ); switch (appUpdateTypeChoice) { case '_create_': break; case '_choose_existing_': delete stepData.newAppName; break; case '_skip_': return; default: throw new Error('Unexpected app update type'); } } } } await steps.setOrgAndApp(context, stepData); }, steps, configuredQuestions: [ 'orgName', 'appName', 'newAppName', 'shouldUpdateOrg', 'appUpdateType', 'shouldOverrideDashboardConfig', ], };