From feb04219f6be186cc54462906394bbd82f9747b5 Mon Sep 17 00:00:00 2001 From: Piotr Grzesik Date: Tue, 8 Jun 2021 15:52:28 +0200 Subject: [PATCH] feat(CLI Onboarding): Support provider creation flow --- lib/cli/interactive-setup/aws-credentials.js | 369 ++++++++++- .../interactive-setup/aws-credentials.test.js | 599 +++++++++++++++++- .../lib/cli/interactive-setup/index.test.js | 5 +- 3 files changed, 935 insertions(+), 38 deletions(-) diff --git a/lib/cli/interactive-setup/aws-credentials.js b/lib/cli/interactive-setup/aws-credentials.js index 8dbf5d657b9..b543a7ce7bb 100644 --- a/lib/cli/interactive-setup/aws-credentials.js +++ b/lib/cli/interactive-setup/aws-credentials.js @@ -3,15 +3,100 @@ const chalk = require('chalk'); const _ = require('lodash'); const inquirer = require('@serverless/utils/inquirer'); +const memoizee = require('memoizee'); +const configUtils = require('@serverless/utils/config'); const AWS = require('aws-sdk'); const awsCredentials = require('../../plugins/aws/utils/credentials'); const { confirm } = require('./utils'); const openBrowser = require('../../utils/openBrowser'); +const ServerlessError = require('../../serverless-error'); const isValidAwsAccessKeyId = RegExp.prototype.test.bind(/^[A-Z0-9]{10,}$/); const isValidAwsSecretAccessKey = RegExp.prototype.test.bind(/^[a-zA-Z0-9/+]{10,}$/); +const { getPlatformClientWithAccessKey } = require('@serverless/dashboard-plugin/lib/clientUtils'); +const resolveProviderCredentials = require('@serverless/dashboard-plugin/lib/resolveProviderCredentials'); +const resolveStage = require('../../utils/resolve-stage'); +const resolveRegion = require('../../utils/resolve-region'); + +const CREDENTIALS_SETUP_CHOICE = { + LOCAL: '_local_', + CREATE_PROVIDER: '_create_provider_', + SKIP: '_skip_', +}; + +const getProviderLinkUid = ({ app, service, stage, region }) => + `appName|${app}|serviceName|${service}|stage|${stage}|region|${region}`; + +const getSdkInstance = memoizee( + async (orgName) => { + return getPlatformClientWithAccessKey(orgName); + }, + { promise: true } +); + +const doesServiceInstanceHaveLinkedProvider = async ({ configuration, options }) => { + const region = resolveRegion({ configuration, options }); + const stage = resolveStage({ configuration, options }); + let result; + try { + result = await resolveProviderCredentials({ configuration, region, stage }); + } catch (err) { + if (err.statusCode && err.statusCode >= 500) { + throw new ServerlessError( + 'Dashboard service is currently unavailable, please try again later', + 'DASHBOARD_UNAVAILABLE' + ); + } + throw err; + } + + return Boolean(result); +}; + +const getOrgUidByName = memoizee( + async (orgName) => { + const sdk = await getSdkInstance(orgName); + let organization; + try { + organization = await sdk.organizations.get({ orgName }); + } catch (err) { + if (err.statusCode && err.statusCode >= 500) { + throw new ServerlessError( + 'Dashboard service is currently unavailable, please try again later', + 'DASHBOARD_UNAVAILABLE' + ); + } + throw err; + } + return organization.orgUid; + }, + { promise: true } +); + +const getProviders = memoizee( + async (orgName) => { + const sdk = await getSdkInstance(orgName); + const orgUid = await getOrgUidByName(orgName); + let providers; + try { + providers = await sdk.getProviders(orgUid); + } catch (err) { + if (err.statusCode && err.statusCode >= 500) { + throw new ServerlessError( + 'Dashboard service is currently unavailable, please try again later', + 'DASHBOARD_UNAVAILABLE' + ); + } + throw err; + } + return providers.result; + }, + { + promise: true, + } +); const awsAccessKeyIdInput = async () => ( @@ -42,20 +127,58 @@ const awsSecretAccessKeyInput = async () => }) ).secretAccessKey.trim(); +const credentialsSetupChoice = async (providers) => { + let credentialsSetupChoices = []; + let message = 'No AWS credentials found, what credentials do you want to use?'; + + if (providers) { + // This is situation where we know that user has decided to link his service to an org + const hasExistingProviders = Boolean(providers.length); + if (hasExistingProviders) { + message = 'What credentials do you want to use?'; + } + const createAccessRoleName = hasExistingProviders + ? 'Create a new AWS Access Role provider' + : 'AWS Access Role (most secure)'; + + const formatProviderName = (provider) => { + if (provider.providerType === 'roleArn') { + return `${provider.alias} (${provider.providerDetails.roleArn})`; + } + // Otherwise its `accessKey`-based provider + + return `${provider.alias} (${provider.providerDetails.accessKeyId})`; + }; + credentialsSetupChoices = [ + ...providers.map((provider) => ({ + name: formatProviderName(provider), + value: provider.providerUid, + })), + { name: createAccessRoleName, value: CREDENTIALS_SETUP_CHOICE.CREATE_PROVIDER }, + ]; + } + + credentialsSetupChoices.push( + { name: 'Local AWS Access Keys', value: CREDENTIALS_SETUP_CHOICE.LOCAL }, + { name: 'Skip', value: CREDENTIALS_SETUP_CHOICE.SKIP } + ); + + return ( + await inquirer.prompt({ + message, + type: 'list', + name: 'credentialsSetupChoice', + choices: credentialsSetupChoices, + }) + ).credentialsSetupChoice; +}; + const steps = { writeOnSetupSkip: () => process.stdout.write(`\nYou can setup your AWS account later. More details available here: http://slss.io/aws-creds-setup\n`), - shouldSetupAwsCredentials: async () => { - if (await confirm('Do you want to set them up now?', { name: 'shouldSetupAwsCredentials' })) { - return true; - } - steps.writeOnSetupSkip(); - return false; - }, - ensureAwsAccount: async () => { if (await confirm('Do you have an AWS account?', { name: 'hasAwsAccount' })) return; openBrowser('https://portal.aws.amazon.com/billing/signup'); @@ -86,10 +209,133 @@ const steps = { )}\n` ); }, + handleProviderCreation: async ({ configuration: { org: orgName } }) => { + const providersUrl = `https://app.serverless.com/${orgName}/settings/providers?source=cli&providerId=new&provider=aws`; + openBrowser(chalk.bold.white(providersUrl)); + process.stdout.write( + 'To learn more about providers, visit: http://slss.io/add-providers-dashboard\n' + ); + process.stdout.write('\nWaiting for creation of AWS Access Role provider...\n'); + + let onEvent; + let showSkipPromptTimeout; + + const p = new Promise((resolve) => { + let inquirerPrompt; + + const timeoutDuration = 1000 * 30; // 30 seconds + showSkipPromptTimeout = setTimeout(() => { + inquirerPrompt = inquirer.prompt({ + message: + '\n [If you approached an issue when setting up a provider, you may press Enter to skip this step]', + name: 'skipProviderSetup', + }); + + inquirerPrompt.then(() => resolve(null)); + }, timeoutDuration); + + onEvent = (event) => { + if (inquirerPrompt) { + // Disable inquirer prompt asking to skip without setting provider + inquirerPrompt.ui.close(); + } + + clearTimeout(showSkipPromptTimeout); + resolve(event); + }; + }); + + // Listen for `provider.created` event to detect creation of new provider + const sdk = await getSdkInstance(orgName); + try { + await sdk.connect({ + orgName, + onEvent, + filter: { + events: ['provider.created'], + }, + }); + } catch (err) { + // Ensure that prompt timeout is cleared in case of error + clearTimeout(showSkipPromptTimeout); + + if (err.statusCode && err.statusCode >= 500) { + throw new ServerlessError( + 'Dashboard service is currently unavailable, please try again later', + 'DASHBOARD_UNAVAILABLE' + ); + } + throw err; + } + + let maybeEvent; + try { + maybeEvent = await p; + } finally { + sdk.disconnect(); + } + + if (maybeEvent) { + process.stdout.write( + `\n${chalk.green('AWS Access Role provider was successfully created')}\n` + ); + return maybeEvent.data.object.provider_uid; + } + + process.stdout.write( + '\nSkipping credentials provider setup. You can still setup credentials provider later.\n' + ); + return null; + }, + linkProviderToServiceInstance: async ({ providerUid, configuration, options }) => { + const { app, service, org } = configuration; + const stage = resolveStage({ configuration, options }); + const region = resolveRegion({ configuration, options }); + const sdk = await getSdkInstance(org); + const linkType = 'instance'; + const linkUid = getProviderLinkUid({ app, service, region, stage }); + let orgUid; + try { + orgUid = await getOrgUidByName(org); + } catch (err) { + if (err.code === 'DASHBOARD_UNAVAILABLE') { + process.stdout.write(`\n${chalk.yellow(err.message)}\n`); + return false; + } + throw err; + } + try { + await sdk.createProviderLink(orgUid, linkType, linkUid, providerUid); + return true; + } catch (err) { + if (err.statusCode && err.statusCode >= 500) { + process.stdout.write( + `\n${chalk.yellow( + 'Dashboard service is currently unavailable, please try again later' + )}\n` + ); + return false; + } + throw err; + } + }, +}; + +const isUserLoggedIn = () => { + if (process.env.SERVERLESS_ACCESS_KEY) { + return true; + } + + if (configUtils.getLoggedInUser()) { + return true; + } + + return false; }; module.exports = { - async isApplicable({ configuration }) { + async isApplicable(context) { + const { configuration, history, options } = context; if ( _.get(configuration, 'provider') !== 'aws' && _.get(configuration, 'provider.name') !== 'aws' @@ -97,22 +343,99 @@ module.exports = { return false; } if (new AWS.S3().config.credentials) return false; - return !(await awsCredentials.resolveFileProfiles()).size; - }, - async run(data) { - process.stdout.write( - 'No AWS credentials were found on your computer, ' + - 'you need these to host your application.\n\n' - ); + if ((await awsCredentials.resolveFileProfiles()).size) return false; + + const orgName = configuration.org; + if (orgName && isUserLoggedIn()) { + let providers; + try { + providers = await getProviders(orgName); + } catch (err) { + if (err.code === 'DASHBOARD_UNAVAILABLE') { + process.stdout.write(`\n${chalk.yellow(err.message)}\n`); + return false; + } + throw err; + } + const hasDefaultProvider = providers.some((provider) => provider.isDefault); + + if (hasDefaultProvider) return false; + + // For situation where it is invoked for already existing service + // We need to check if service already has a linked provider + if ( + providers && + !history.has('service') && + (await doesServiceInstanceHaveLinkedProvider({ configuration, options })) + ) { + return false; + } + } - return module.exports.runSteps(data); + return true; }, - steps, - runSteps: async (context) => { - if (!(await steps.shouldSetupAwsCredentials())) return; - await steps.ensureAwsAccount(); + async run(context) { + let providers; + + // It is possible that user decides to not configure org for his service and + // we still should allow setup of local credentials in such case + if (context.configuration.org && isUserLoggedIn()) { + try { + providers = await getProviders(context.configuration.org); + } catch (err) { + if (err.code === 'DASHBOARD_UNAVAILABLE') { + process.stdout.write(`\n${chalk.yellow(err.message)}\n`); + return; + } + throw err; + } + } + const credentialsSetupChoiceAnswer = await credentialsSetupChoice(providers); + + if (credentialsSetupChoiceAnswer === CREDENTIALS_SETUP_CHOICE.CREATE_PROVIDER) { + try { + const createdProviderUid = await steps.handleProviderCreation(context); + const hadExistingProviders = Boolean(providers.length); + if (createdProviderUid && hadExistingProviders) { + // This is situation where user decided to create a new provider and already had previous providers setup + // In this case, we want to setup an explicit link between provider and service as the newly created provider + // might not be the default one. + await steps.linkProviderToServiceInstance({ + configuration: context.configuration, + providerUid: createdProviderUid, + options: context.options, + }); + } + return; + } catch (err) { + if (err.code === 'DASHBOARD_UNAVAILABLE') { + process.stdout.write(`\n${chalk.yellow(err.message)}\n`); + return; + } + throw err; + } + } else if (credentialsSetupChoiceAnswer === CREDENTIALS_SETUP_CHOICE.SKIP) { + steps.writeOnSetupSkip(); + return; + } else if (credentialsSetupChoiceAnswer === CREDENTIALS_SETUP_CHOICE.LOCAL) { + await steps.ensureAwsAccount(); + await steps.ensureAwsCredentials(context); + await steps.inputAwsCredentials(); + return; + } - await steps.ensureAwsCredentials(context); - await steps.inputAwsCredentials(); + // Otherwise user selected an existing provider + const linked = await steps.linkProviderToServiceInstance({ + configuration: context.configuration, + providerUid: credentialsSetupChoiceAnswer, + options: context.options, + }); + + if (linked) { + process.stdout.write( + `\n${chalk.green('Selected provider was successfully linked to your service')}\n` + ); + } }, + steps, }; diff --git a/test/unit/lib/cli/interactive-setup/aws-credentials.test.js b/test/unit/lib/cli/interactive-setup/aws-credentials.test.js index 847678a51af..01ab849d4bf 100644 --- a/test/unit/lib/cli/interactive-setup/aws-credentials.test.js +++ b/test/unit/lib/cli/interactive-setup/aws-credentials.test.js @@ -4,30 +4,46 @@ const chai = require('chai'); const sinon = require('sinon'); const proxyquire = require('proxyquire'); const overrideEnv = require('process-utils/override-env'); +const overrideStdoutWrite = require('process-utils/override-stdout-write'); const requireUncached = require('ncjsm/require-uncached'); +const chalk = require('chalk'); const { expect } = chai; chai.use(require('chai-as-promised')); +chai.use(require('sinon-chai')); const { join, resolve } = require('path'); -const fsp = require('fs').promises; const { remove: rmDir, outputFile: writeFile } = require('fs-extra'); const { resolveFileProfiles } = require('../../../../../lib/plugins/aws/utils/credentials'); +const mockedSdk = { + organizations: { + get: () => { + return { + orgUid: 'org-uid', + }; + }, + }, + getProviders: async () => { + return { result: [] }; + }, +}; + const step = proxyquire('../../../../../lib/cli/interactive-setup/aws-credentials', { '../../utils/openBrowser': async (url) => { openBrowserUrls.push(url); }, + '@serverless/dashboard-plugin/lib/clientUtils': { + getPlatformClientWithAccessKey: async () => mockedSdk, + }, }); + const inquirer = require('@serverless/utils/inquirer'); const configureInquirerStub = require('@serverless/test/configure-inquirer-stub'); const openBrowserUrls = []; -const confirmEmptyWorkingDir = async () => - expect(await fsp.readdir(process.cwd())).to.deep.equal([]); - describe('test/unit/lib/cli/interactive-setup/aws-credentials.test.js', () => { const accessKeyId = 'AKIAIOSFODNN7EXAMPLE'; const secretAccessKey = 'wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY'; @@ -49,6 +65,202 @@ describe('test/unit/lib/cli/interactive-setup/aws-credentials.test.js', () => { }) ).to.equal(false)); + it('Should be ineffective, when user has default provider set', async () => { + const internalMockedSdk = { + ...mockedSdk, + getProviders: async () => { + return { + result: [ + { + alias: 'someprovider', + providerName: 'aws', + providerType: 'roleArn', + providerUid: 'provideruid', + isDefault: true, + providerDetails: { + roleArn: 'arn:xxx', + }, + }, + ], + }; + }, + }; + const mockedStep = proxyquire('../../../../../lib/cli/interactive-setup/aws-credentials', { + '@serverless/dashboard-plugin/lib/clientUtils': { + getPlatformClientWithAccessKey: async () => internalMockedSdk, + }, + '@serverless/utils/config': { + getLoggedInUser: () => { + return { + accessKeys: { + someorg: 'xxx', + }, + }; + }, + }, + }); + + expect( + await mockedStep.isApplicable({ + serviceDir: process.cwd(), + configuration: { provider: { name: 'aws' }, org: 'someorg' }, + configurationFilename: 'serverless.yml', + }) + ).to.be.false; + }); + + it('Should be ineffective, when existing service already has a provider set', async () => { + const internalMockedSdk = { + ...mockedSdk, + getProviders: async () => { + return { + result: [ + { + alias: 'someprovider', + providerName: 'aws', + providerType: 'roleArn', + providerUid: 'provideruid', + isDefault: false, + providerDetails: { + roleArn: 'arn:xxx', + }, + }, + ], + }; + }, + }; + const mockedStep = proxyquire('../../../../../lib/cli/interactive-setup/aws-credentials', { + '@serverless/dashboard-plugin/lib/clientUtils': { + getPlatformClientWithAccessKey: async () => internalMockedSdk, + }, + '@serverless/utils/config': { + getLoggedInUser: () => { + return { + accessKeys: { + someorg: 'xxx', + }, + }; + }, + }, + '@serverless/dashboard-plugin/lib/resolveProviderCredentials': () => { + return { + accessKeyId: 'somekey', + secretAccessKey: 'somesecret', + sessionToken: 'somesessiontoken', + }; + }, + }); + + expect( + await mockedStep.isApplicable({ + history: new Set(), + serviceDir: process.cwd(), + configuration: { + provider: { name: 'aws' }, + org: 'someorg', + app: 'someapp', + service: 'service', + }, + options: {}, + configurationFilename: 'serverless.yml', + }) + ).to.be.false; + }); + + it('Should be effective, when existing service instance does not have a provider set', async () => { + const internalMockedSdk = { + ...mockedSdk, + getProviders: async () => { + return { + result: [ + { + alias: 'someprovider', + providerName: 'aws', + providerType: 'roleArn', + providerUid: 'provideruid', + isDefault: false, + providerDetails: { + roleArn: 'arn:xxx', + }, + }, + ], + }; + }, + }; + const mockedStep = proxyquire('../../../../../lib/cli/interactive-setup/aws-credentials', { + '@serverless/dashboard-plugin/lib/clientUtils': { + getPlatformClientWithAccessKey: async () => internalMockedSdk, + }, + '@serverless/utils/config': { + getLoggedInUser: () => { + return { + accessKeys: { + someorg: 'xxx', + }, + }; + }, + }, + '@serverless/dashboard-plugin/lib/resolveProviderCredentials': () => { + return null; + }, + }); + + expect( + await mockedStep.isApplicable({ + history: new Set(), + serviceDir: process.cwd(), + configuration: { + provider: { name: 'aws' }, + org: 'someorg', + app: 'someapp', + service: 'service', + }, + options: {}, + configurationFilename: 'serverless.yml', + }) + ).to.be.true; + }); + + it('Should emit warning when dashboard is not available when fetching providers', async () => { + const internalMockedSdk = { + ...mockedSdk, + getProviders: async () => { + const err = new Error('unavailable'); + err.statusCode = 500; + throw err; + }, + }; + const mockedStep = proxyquire('../../../../../lib/cli/interactive-setup/aws-credentials', { + '@serverless/dashboard-plugin/lib/clientUtils': { + getPlatformClientWithAccessKey: async () => internalMockedSdk, + }, + '@serverless/utils/config': { + getLoggedInUser: () => { + return { + accessKeys: { + someorg: 'xxx', + }, + }; + }, + }, + }); + + let stdoutData = ''; + await overrideStdoutWrite( + (data) => (stdoutData += data), + async () => + expect( + await mockedStep.isApplicable({ + serviceDir: process.cwd(), + configuration: { provider: { name: 'aws' }, org: 'someorg' }, + configurationFilename: 'serverless.yml', + }) + ).to.be.false + ); + + expect(stdoutData).to.include('Dashboard service is currently unavailable'); + }); + it('Should be effective, at AWS service and no credentials are set', async () => expect( await step.isApplicable({ @@ -58,6 +270,25 @@ describe('test/unit/lib/cli/interactive-setup/aws-credentials.test.js', () => { }) ).to.equal(true)); + it('Should emit a message when user decides to skip credentials setup', async () => { + configureInquirerStub(inquirer, { + list: { credentialsSetupChoice: '_skip_' }, + }); + + let stdoutData = ''; + await overrideStdoutWrite( + (data) => (stdoutData += data), + async () => + await step.run({ + serviceDir: process.cwd(), + configuration: { provider: { name: 'aws' }, org: 'someorg' }, + configurationFilename: 'serverless.yml', + }) + ); + + expect(stdoutData).to.include('You can setup your AWS account later'); + }); + describe('In environment credentials', () => { let restoreEnv; let uncachedStep; @@ -84,12 +315,6 @@ describe('test/unit/lib/cli/interactive-setup/aws-credentials.test.js', () => { }); }); - it("Should not setup if user doesn't the setup", async () => { - configureInquirerStub(inquirer, { confirm: { shouldSetupAwsCredentials: false } }); - await step.run({ configuration: { provider: {} }, options: {} }); - return confirmEmptyWorkingDir(); - }); - describe('AWS config handling', () => { let credentialsDirPath; let credentialsFilePath; @@ -125,7 +350,8 @@ describe('test/unit/lib/cli/interactive-setup/aws-credentials.test.js', () => { it('Should setup credentials for users not having an AWS account', async () => { configureInquirerStub(inquirer, { - confirm: { shouldSetupAwsCredentials: true, hasAwsAccount: false }, + list: { credentialsSetupChoice: '_local_' }, + confirm: { hasAwsAccount: false }, input: { createAwsAccountPrompt: '', generateAwsCredsPrompt: '', @@ -144,7 +370,8 @@ describe('test/unit/lib/cli/interactive-setup/aws-credentials.test.js', () => { it('Should setup credentials for users having an AWS account', async () => { configureInquirerStub(inquirer, { - confirm: { shouldSetupAwsCredentials: true, hasAwsAccount: true }, + list: { credentialsSetupChoice: '_local_' }, + confirm: { hasAwsAccount: true }, input: { generateAwsCredsPrompt: '', accessKeyId, secretAccessKey }, }); await step.run({ configuration: { provider: {} }, options: {} }); @@ -157,7 +384,8 @@ describe('test/unit/lib/cli/interactive-setup/aws-credentials.test.js', () => { it('Should not accept invalid access key id', async () => { configureInquirerStub(inquirer, { - confirm: { shouldSetupAwsCredentials: true, hasAwsAccount: true }, + list: { credentialsSetupChoice: '_local_' }, + confirm: { hasAwsAccount: true }, input: { generateAwsCredsPrompt: '', accessKeyId: 'foo', secretAccessKey }, }); await expect( @@ -170,7 +398,8 @@ describe('test/unit/lib/cli/interactive-setup/aws-credentials.test.js', () => { it('Should not accept invalid secret access key', async () => { configureInquirerStub(inquirer, { - confirm: { shouldSetupAwsCredentials: true, hasAwsAccount: true }, + list: { credentialsSetupChoice: '_local_' }, + confirm: { hasAwsAccount: true }, input: { generateAwsCredsPrompt: '', accessKeyId, secretAccessKey: 'foo' }, }); await expect( @@ -181,4 +410,346 @@ describe('test/unit/lib/cli/interactive-setup/aws-credentials.test.js', () => { ).to.eventually.be.rejected.and.have.property('code', 'INVALID_ANSWER'); }); }); + + describe('Provider config handling', () => { + it('Should correctly setup with newly created provider when no previous providers exist', async () => { + const mockedOpenBrowser = sinon.stub().returns(); + const mockedDisconnect = sinon.stub().returns(); + const mockedCreateProviderLink = sinon.stub().resolves(); + const providerUid = 'provideruid'; + const internalMockedSdk = { + ...mockedSdk, + connect: ({ onEvent }) => { + onEvent({ + data: { + object: { + provider_uid: providerUid, + }, + }, + }); + }, + disconnect: mockedDisconnect, + createProviderLink: mockedCreateProviderLink, + }; + const mockedStep = proxyquire('../../../../../lib/cli/interactive-setup/aws-credentials', { + '@serverless/dashboard-plugin/lib/clientUtils': { + getPlatformClientWithAccessKey: async () => internalMockedSdk, + }, + '../../utils/openBrowser': mockedOpenBrowser, + '@serverless/utils/config': { + getLoggedInUser: () => ({}), + }, + }); + + configureInquirerStub(inquirer, { + list: { credentialsSetupChoice: '_create_provider_' }, + }); + + let stdoutData = ''; + await overrideStdoutWrite( + (data) => (stdoutData += data), + async () => + await mockedStep.run({ + serviceDir: process.cwd(), + configuration: { + service: 'someservice', + provider: { name: 'aws' }, + org: 'someorg', + app: 'someapp', + }, + options: {}, + configurationFilename: 'serverless.yml', + }) + ); + + expect(stdoutData).to.include('AWS Access Role provider was successfully created'); + expect(mockedOpenBrowser).to.have.been.calledWith( + chalk.bold.white( + 'https://app.serverless.com/someorg/settings/providers?source=cli&providerId=new&provider=aws' + ) + ); + expect(mockedDisconnect).to.have.been.called; + expect(mockedCreateProviderLink).not.to.have.been.called; + }); + + it('Should correctly setup with newly created provider when previous providers exist', async () => { + const mockedOpenBrowser = sinon.stub().returns(); + const mockedDisconnect = sinon.stub().returns(); + const mockedCreateProviderLink = sinon.stub().resolves(); + const providerUid = 'provideruid'; + const internalMockedSdk = { + ...mockedSdk, + connect: ({ onEvent }) => { + onEvent({ + data: { + object: { + provider_uid: providerUid, + }, + }, + }); + }, + getProviders: async () => { + return { + result: [ + { + alias: 'someprovider', + providerName: 'aws', + providerType: 'roleArn', + providerUid, + providerDetails: { + roleArn: 'arn:xxx', + }, + }, + ], + }; + }, + disconnect: mockedDisconnect, + createProviderLink: mockedCreateProviderLink, + }; + const mockedStep = proxyquire('../../../../../lib/cli/interactive-setup/aws-credentials', { + '@serverless/dashboard-plugin/lib/clientUtils': { + getPlatformClientWithAccessKey: async () => internalMockedSdk, + }, + '../../utils/openBrowser': mockedOpenBrowser, + '@serverless/utils/config': { + getLoggedInUser: () => ({}), + }, + }); + + configureInquirerStub(inquirer, { + list: { credentialsSetupChoice: '_create_provider_' }, + }); + + let stdoutData = ''; + await overrideStdoutWrite( + (data) => (stdoutData += data), + async () => + await mockedStep.run({ + serviceDir: process.cwd(), + configuration: { + service: 'someservice', + provider: { name: 'aws' }, + org: 'someorg', + app: 'someapp', + }, + options: {}, + configurationFilename: 'serverless.yml', + }) + ); + + expect(stdoutData).to.include('AWS Access Role provider was successfully created'); + expect(mockedOpenBrowser).to.have.been.calledWith( + chalk.bold.white( + 'https://app.serverless.com/someorg/settings/providers?source=cli&providerId=new&provider=aws' + ) + ); + expect(mockedDisconnect).to.have.been.called; + expect(mockedCreateProviderLink).to.have.been.calledWith( + 'org-uid', + 'instance', + 'appName|someapp|serviceName|someservice|stage|dev|region|us-east-1', + providerUid + ); + }); + + it('Should emit warning when dashboard unavailable when connecting to it', async () => { + const mockedOpenBrowser = sinon.stub().returns(); + const internalMockedSdk = { + ...mockedSdk, + connect: () => { + const err = new Error('error'); + err.statusCode = 500; + throw err; + }, + }; + const mockedStep = proxyquire('../../../../../lib/cli/interactive-setup/aws-credentials', { + '@serverless/dashboard-plugin/lib/clientUtils': { + getPlatformClientWithAccessKey: async () => internalMockedSdk, + }, + '../../utils/openBrowser': mockedOpenBrowser, + }); + + configureInquirerStub(inquirer, { + list: { credentialsSetupChoice: '_create_provider_' }, + }); + + let stdoutData = ''; + await overrideStdoutWrite( + (data) => (stdoutData += data), + async () => + await mockedStep.run({ + serviceDir: process.cwd(), + configuration: { + service: 'someservice', + provider: { name: 'aws' }, + org: 'someorg', + app: 'someapp', + }, + configurationFilename: 'serverless.yml', + }) + ); + + expect(stdoutData).to.include('Dashboard service is currently unavailable'); + expect(mockedOpenBrowser).to.have.been.calledWith( + chalk.bold.white( + 'https://app.serverless.com/someorg/settings/providers?source=cli&providerId=new&provider=aws' + ) + ); + }); + + it('Should correctly setup with existing provider', async () => { + const providerUid = 'provideruid'; + const mockedCreateProviderLink = sinon.stub().resolves(); + const internalMockedSdk = { + ...mockedSdk, + getProviders: async () => { + return { + result: [ + { + alias: 'someprovider', + providerName: 'aws', + providerType: 'accessKey', + providerUid, + providerDetails: { + accessKeyId: 'axx', + }, + }, + ], + }; + }, + createProviderLink: mockedCreateProviderLink, + }; + const mockedStep = proxyquire('../../../../../lib/cli/interactive-setup/aws-credentials', { + '@serverless/dashboard-plugin/lib/clientUtils': { + getPlatformClientWithAccessKey: async () => internalMockedSdk, + }, + }); + + configureInquirerStub(inquirer, { + list: { credentialsSetupChoice: providerUid }, + }); + + let stdoutData = ''; + await overrideStdoutWrite( + (data) => (stdoutData += data), + async () => + await mockedStep.run({ + serviceDir: process.cwd(), + configuration: { + service: 'someservice', + provider: { name: 'aws' }, + org: 'someorg', + app: 'someapp', + }, + options: {}, + configurationFilename: 'serverless.yml', + }) + ); + + expect(mockedCreateProviderLink).to.have.been.calledWith( + 'org-uid', + 'instance', + 'appName|someapp|serviceName|someservice|stage|dev|region|us-east-1', + 'provideruid' + ); + expect(stdoutData).to.include('Selected provider was successfully linked'); + }); + + it('Should emit a warning when dashboard is not available and link cannot be created', async () => { + const providerUid = 'provideruid'; + const mockedCreateProviderLink = sinon.stub().callsFake(async () => { + const err = new Error('error'); + err.statusCode = 500; + throw err; + }); + const internalMockedSdk = { + ...mockedSdk, + getProviders: async () => { + return { + result: [ + { + alias: 'someprovider', + providerName: 'aws', + providerType: 'roleArn', + providerUid, + providerDetails: { + roleArn: 'arn:xxx', + }, + }, + ], + }; + }, + createProviderLink: mockedCreateProviderLink, + }; + const mockedStep = proxyquire('../../../../../lib/cli/interactive-setup/aws-credentials', { + '@serverless/dashboard-plugin/lib/clientUtils': { + getPlatformClientWithAccessKey: async () => internalMockedSdk, + }, + }); + + configureInquirerStub(inquirer, { + list: { credentialsSetupChoice: providerUid }, + }); + + let stdoutData = ''; + await overrideStdoutWrite( + (data) => (stdoutData += data), + async () => + await mockedStep.run({ + serviceDir: process.cwd(), + configuration: { + service: 'someservice', + provider: { name: 'aws' }, + org: 'someorg', + app: 'someapp', + }, + options: {}, + configurationFilename: 'serverless.yml', + }) + ); + + expect(stdoutData).to.include( + 'Dashboard service is currently unavailable, please try again later' + ); + expect(stdoutData).not.to.include('Selected provider was successfully linked'); + expect(mockedCreateProviderLink).to.have.been.calledWith( + 'org-uid', + 'instance', + 'appName|someapp|serviceName|someservice|stage|dev|region|us-east-1', + 'provideruid' + ); + }); + + it('Should emit a warning when dashboard is not available when fetching providers', async () => { + const internalMockedSdk = { + ...mockedSdk, + getProviders: async () => { + const err = new Error('unavailable'); + err.statusCode = 500; + throw err; + }, + }; + const mockedStep = proxyquire('../../../../../lib/cli/interactive-setup/aws-credentials', { + '@serverless/dashboard-plugin/lib/clientUtils': { + getPlatformClientWithAccessKey: async () => internalMockedSdk, + }, + '@serverless/utils/config': { + getLoggedInUser: () => ({}), + }, + }); + + let stdoutData = ''; + await overrideStdoutWrite( + (data) => (stdoutData += data), + async () => + await mockedStep.run({ + serviceDir: process.cwd(), + configuration: { provider: { name: 'aws' }, org: 'someorg' }, + configurationFilename: 'serverless.yml', + }) + ); + + expect(stdoutData).to.include('Dashboard service is currently unavailable'); + }); + }); }); diff --git a/test/unit/lib/cli/interactive-setup/index.test.js b/test/unit/lib/cli/interactive-setup/index.test.js index 78cadf53267..7a65ad77c06 100644 --- a/test/unit/lib/cli/interactive-setup/index.test.js +++ b/test/unit/lib/cli/interactive-setup/index.test.js @@ -34,13 +34,16 @@ describe('test/unit/lib/cli/interactive-setup/index.test.js', () => { // dashboard-login { instructionString: 'Do you want to login/register to Serverless Dashboard?', + input: '\u001b[B', // Move cursor down by one line }, // dashboard-set-org // Skipped, as internally depends on remote state of data and cannot be easily tested offline // aws-credentials - { instructionString: 'Do you want to set them up now?', input: 'Y' }, + { + instructionString: 'No AWS credentials found, what credentials do you want to use?', + }, { instructionString: 'AWS account', input: 'Y' }, { instructionString: 'Press Enter to continue' }, {