'use strict'; const _ = require('lodash'); const chalk = require('chalk'); const { legacy, 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/clientUtils'); const isValidAppName = RegExp.prototype.test.bind(/^[a-z0-9](?:[a-z0-9-]{0,126}[a-z0-9])?$/); const orgUpdateConfirm = async (stepHistory) => { legacy.write( "Service has monitoring enabled, but it is configured with the 'org' to which you do not have access\n\n" ); 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) => { legacy.write( "Service seems to have monitoring enabled, but configured app doesn't seem to exist in an organization.\n\n" ); log.notice( "Service seems to have monitoring enabled, but configured app doesn't seem to exist in an 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, { orgNames, orgName, apps, appName, newAppName, isDashboardMonitoringOverridenByCli, isDashboardAppPreconfigured, } ) => { const { history, stepHistory } = context; 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 (isDashboardMonitoringOverridenByCli && isDashboardAppPreconfigured) { const shouldOverrideDashboardConfig = await promptWithHistory({ message: 'Are you sure you want to update monitoring settings ' + `to ${chalk.bold(`app: ${appName}, org: ${orgName}`)}`, type: 'confirm', name: 'shouldOverrideDashboardConfig', stepHistory, }); if (!shouldOverrideDashboardConfig) { delete context.configuration.app; delete context.configuration.org; return; } } legacy.write( `\n${chalk.green('Your project has been setup with org ')}${chalk.white.bold( orgName )}${chalk.green(' and app ')}${chalk.white.bold(appName)}\n` ); log.notice(); log.notice.success(`Your project has been setup with org "${orgName}" and app "${appName}"`); await writeOrgAndApp(orgName, appName, context); return; }, }; module.exports = { async isApplicable(context) { const { configuration, options, serviceDir } = context; if (!serviceDir) { context.inapplicabilityReasonCode = 'NOT_IN_SERVICE_DIRECTORY'; 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(); if (!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 orgName = options.org || configuration.org; const appName = options.app || configuration.app; const isDashboardMonitoringPreconfigured = Boolean(configuration.org); const isDashboardAppPreconfigured = Boolean(configuration.app); const isDashboardMonitoringOverridenByCli = isDashboardMonitoringPreconfigured && ((options.org && options.org !== configuration.org) || (options.app && options.app !== configuration.app)); if (orgName && orgNames.has(orgName)) { if (!isValidAppName(appName)) { return { user, orgName, isDashboardMonitoringPreconfigured, isDashboardAppPreconfigured, isDashboardMonitoringOverridenByCli, }; } 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 ( isDashboardMonitoringPreconfigured && isDashboardAppPreconfigured && !isDashboardMonitoringOverridenByCli ) { context.inapplicabilityReasonCode = 'MONITORING_NOT_OVERRIDEN_BY_CLI'; return false; } return { user, orgName, appName, isDashboardMonitoringPreconfigured, isDashboardAppPreconfigured, isDashboardMonitoringOverridenByCli, }; } if (options.app) { legacy.write( chalk.red( "\nPassed value for `--app` doesn't seem to correspond to chosen organization.\n" ) ); log.error(); log.error('Passed value for "--app" doesn\'t seem to correspond to chosen organization.'); } return { user, orgName, isDashboardMonitoringPreconfigured, isDashboardAppPreconfigured, isDashboardMonitoringOverridenByCli, }; } else if (apps.some((app) => app.appName === appName)) { context.inapplicabilityReasonCode = 'HAS_MONITORING_SETUP'; return false; } return { user, orgName, apps, newAppName: appName, isDashboardMonitoringPreconfigured, isDashboardAppPreconfigured, isDashboardMonitoringOverridenByCli, }; } else if (orgName) { if (options.org) { legacy.write( chalk.red( "\nPassed value for `--org` doesn't seem to correspond to account with which you're logged in with.\n" ) ); log.error(); log.error( 'Passed value for "--org" doesn\'t seem to correspond to account with which you\'re logged in with.' ); } else { legacy.write( chalk.red(`\nConfigured org '${orgName}' is not available in your account.\n`) ); log.error(); log.error(`Configured org "${orgName}" is not available in your account.`); } } return { user, orgNames, isDashboardMonitoringPreconfigured, isDashboardAppPreconfigured, isDashboardMonitoringOverridenByCli, }; }, 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.isDashboardMonitoringPreconfigured) { if (!stepData.orgName) { if (!(await orgUpdateConfirm(stepHistory))) return; } else if (stepData.newAppName) { 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', ], };