Skip to content

Commit

Permalink
feat(CLI Onboarding): Add telemetry for interactive flow
Browse files Browse the repository at this point in the history
  • Loading branch information
pgrzesik committed Jun 28, 2021
1 parent f5c1c47 commit 0eba2dc
Show file tree
Hide file tree
Showing 10 changed files with 508 additions and 197 deletions.
74 changes: 55 additions & 19 deletions lib/cli/interactive-setup/aws-credentials.js
Expand Up @@ -77,8 +77,8 @@ const getProviders = memoizee(
}
);

const awsAccessKeyIdInput = async () =>
(
const awsAccessKeyIdInput = async ({ stepHistory }) => {
const accessKeyId = (
await inquirer.prompt({
message: 'AWS Access Key Id:',
type: 'input',
Expand All @@ -89,9 +89,12 @@ const awsAccessKeyIdInput = async () =>
},
})
).accessKeyId.trim();
stepHistory.set('accessKeyId', '_user_provided_');
return accessKeyId;
};

const awsSecretAccessKeyInput = async () =>
(
const awsSecretAccessKeyInput = async ({ stepHistory }) => {
const secretAccessKey = (
await inquirer.prompt({
message: 'AWS Secret Access Key:',
type: 'input',
Expand All @@ -105,8 +108,11 @@ const awsSecretAccessKeyInput = async () =>
},
})
).secretAccessKey.trim();
stepHistory.set('secretAccessKey', '_user_provided_');
return secretAccessKey;
};

const credentialsSetupChoice = async (providers) => {
const credentialsSetupChoice = async (context, providers) => {
let credentialsSetupChoices = [];
let message = 'No AWS credentials found, what credentials do you want to use?';

Expand Down Expand Up @@ -142,14 +148,20 @@ const credentialsSetupChoice = async (providers) => {
{ name: 'Skip', value: CREDENTIALS_SETUP_CHOICE.SKIP }
);

return (
const result = (
await inquirer.prompt({
message,
type: 'list',
name: 'credentialsSetupChoice',
choices: credentialsSetupChoices,
})
).credentialsSetupChoice;

context.stepHistory.set(
'credentialsSetupChoice',
result.startsWith('_') ? result : '_user_provided_'
);
return result;
};

const steps = {
Expand All @@ -158,15 +170,16 @@ const steps = {
http://slss.io/aws-creds-setup\n`),

ensureAwsAccount: async () => {
ensureAwsAccount: async ({ stepHistory }) => {
if (await confirm('Do you have an AWS account?', { name: 'hasAwsAccount' })) return;
openBrowser('https://portal.aws.amazon.com/billing/signup');
await inquirer.prompt({
message: 'Press Enter to continue after creating an AWS account',
name: 'createAwsAccountPrompt',
});
stepHistory.set('createAwsAccountPrompt', true);
},
ensureAwsCredentials: async ({ options, configuration }) => {
ensureAwsCredentials: async ({ options, configuration, stepHistory }) => {
const region = options.region || configuration.provider.region || 'us-east-1';
openBrowser(
`https://console.aws.amazon.com/iam/home?region=${region}#/users$new?step=final&accessKey&userNames=serverless&permissionType=policies&policies=arn:aws:iam::aws:policy%2FAdministratorAccess`
Expand All @@ -175,10 +188,11 @@ const steps = {
message: 'Press Enter to continue after creating an AWS user with access keys',
name: 'generateAwsCredsPrompt',
});
stepHistory.set('generateAwsCredsPrompt', true);
},
inputAwsCredentials: async () => {
const accessKeyId = await awsAccessKeyIdInput();
const secretAccessKey = await awsSecretAccessKeyInput();
inputAwsCredentials: async (context) => {
const accessKeyId = await awsAccessKeyIdInput(context);
const secretAccessKey = await awsSecretAccessKeyInput(context);
await awsCredentials.saveFileProfiles(new Map([['default', { accessKeyId, secretAccessKey }]]));
process.stdout.write(
`\n${chalk.green(
Expand All @@ -188,7 +202,7 @@ const steps = {
)}\n`
);
},
handleProviderCreation: async ({ configuration: { org: orgName } }) => {
handleProviderCreation: async ({ configuration: { org: orgName }, stepHistory }) => {
const providersUrl = `https://app.serverless.com/${orgName}/settings/providers?source=cli&providerId=new&provider=aws`;
openBrowser(chalk.bold.white(providersUrl));
process.stdout.write(
Expand All @@ -210,7 +224,10 @@ const steps = {
name: 'skipProviderSetup',
});

inquirerPrompt.then(() => resolve(null));
inquirerPrompt.then(() => {
stepHistory.set('skipProviderSetup', true);
resolve(null);
});
}, timeoutDuration);

onEvent = (event) => {
Expand Down Expand Up @@ -307,10 +324,17 @@ module.exports = {
_.get(configuration, 'provider') !== 'aws' &&
_.get(configuration, 'provider.name') !== 'aws'
) {
context.inapplicabilityReasonCode = 'NON_AWS_PROVIDER';
return false;
}
if (new AWS.S3().config.credentials) {
context.inapplicabilityReasonCode = 'LOCAL_CREDENTIALS_CONFIGURED';
return false;
}
if ((await awsCredentials.resolveFileProfiles()).size) {
context.inapplicabilityReasonCode = 'LOCAL_CREDENTIAL_PROFILES_CONFIGURED';
return false;
}
if (new AWS.S3().config.credentials) return false;
if ((await awsCredentials.resolveFileProfiles()).size) return false;

const orgName = configuration.org;
if (orgName && isAuthenticated()) {
Expand All @@ -326,7 +350,10 @@ module.exports = {
}
const hasDefaultProvider = providers.some((provider) => provider.isDefault);

if (hasDefaultProvider) return false;
if (hasDefaultProvider) {
context.inapplicabilityReasonCode = 'DEFAULT_PROVIDER_CONFIGURED';
return false;
}

// For situation where it is invoked for already existing service
// We need to check if service already has a linked provider
Expand All @@ -335,6 +362,7 @@ module.exports = {
!history.has('service') &&
(await doesServiceInstanceHaveLinkedProvider({ configuration, options }))
) {
context.inapplicabilityReasonCode = 'LINKED_PROVIDER_CONFIGURED';
return false;
}
}
Expand All @@ -357,7 +385,7 @@ module.exports = {
throw err;
}
}
const credentialsSetupChoiceAnswer = await credentialsSetupChoice(providers);
const credentialsSetupChoiceAnswer = await credentialsSetupChoice(context, providers);

if (credentialsSetupChoiceAnswer === CREDENTIALS_SETUP_CHOICE.CREATE_PROVIDER) {
try {
Expand Down Expand Up @@ -386,9 +414,9 @@ module.exports = {
steps.writeOnSetupSkip();
return;
} else if (credentialsSetupChoiceAnswer === CREDENTIALS_SETUP_CHOICE.LOCAL) {
await steps.ensureAwsAccount();
await steps.ensureAwsAccount(context);
await steps.ensureAwsCredentials(context);
await steps.inputAwsCredentials();
await steps.inputAwsCredentials(context);
return;
}

Expand All @@ -406,4 +434,12 @@ module.exports = {
}
},
steps,
configuredQuestions: [
'credentialsSetupChoice',
'createAwsAccountPrompt',
'generateAwsCredsPrompt',
'accessKeyId',
'secretAccessKey',
'skipProviderSetup',
],
};
16 changes: 13 additions & 3 deletions lib/cli/interactive-setup/deploy.js
Expand Up @@ -100,15 +100,18 @@ const configurePlugin = (serverless, originalStdWrite) => {
};

module.exports = {
async isApplicable({ configuration, serviceDir, history, options }) {
async isApplicable(context) {
const { configuration, serviceDir, history, options } = 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;
}

Expand All @@ -127,11 +130,17 @@ module.exports = {
// We want to proceed if local credentials are available
if (new AWS.Config().credentials) return true;

context.inapplicabilityReasonCode = 'NO_CREDENTIALS_CONFIGURED';
return false;
},
async run({ configuration, configurationFilename, serviceDir }) {
async run({ configuration, configurationFilename, serviceDir, stepHistory }) {
const serviceName = configuration.service;
if (!(await confirm('Do you want to deploy your project?', { name: 'shouldDeploy' }))) {
const shouldDeploy = await confirm('Do you want to deploy your project?', {
name: 'shouldDeploy',
});
stepHistory.set('shouldDeploy', shouldDeploy);

if (!shouldDeploy) {
printMessage({
serviceName,
hasBeenDeployed: false,
Expand Down Expand Up @@ -174,4 +183,5 @@ module.exports = {
dashboardPlugin: serverless.pluginManager.dashboardPlugin,
});
},
configuredQuestions: ['shouldDeploy'],
};
21 changes: 20 additions & 1 deletion lib/cli/interactive-setup/index.js
@@ -1,6 +1,7 @@
'use strict';

const inquirer = require('@serverless/utils/inquirer');
const { StepHistory } = require('@serverless/utils/telemetry');

const steps = {
service: require('./service'),
Expand All @@ -12,14 +13,32 @@ const steps = {

module.exports = async (context) => {
context = { ...context, inquirer, history: new Map() };
const stepsDetails = new Map();
for (const [stepName, step] of Object.entries(steps)) {
delete context.stepHistory;
delete context.inapplicabilityReasonCode;
const stepData = await step.isApplicable(context);
stepsDetails.set(stepName, {
isApplicable: Boolean(stepData),
inapplicabilityReasonCode: context.inapplicabilityReasonCode,
timestamp: Date.now(),
configuredQuestions: step.configuredQuestions,
});
if (stepData) {
process.stdout.write('\n');
context.stepHistory = [];
context.stepHistory = new StepHistory();
context.history.set(stepName, context.stepHistory);
await step.run(context, stepData);
}
}

const commandUsage = Array.from(stepsDetails.entries()).map(([step, stepDetails]) => {
const stepHistory = context.history.get(step);
return {
name: step,
...stepDetails,
history: stepHistory ? stepHistory.toJSON() : [],
};
});
return { commandUsage, configuration: context.configuration };
};

0 comments on commit 0eba2dc

Please sign in to comment.