Skip to content

Commit

Permalink
feat(CLI Onboarding): Add deploy step
Browse files Browse the repository at this point in the history
  • Loading branch information
pgrzesik committed Jun 9, 2021
1 parent 8541b45 commit 841817f
Show file tree
Hide file tree
Showing 12 changed files with 667 additions and 123 deletions.
6 changes: 6 additions & 0 deletions lib/classes/CLI.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,12 @@ class CLI {
this.log = function () {};
}

suppressLog() {
this.log = function () {};
this.printDot = function () {};
this.consoleLog = function () {};
}

displayHelp() {
if (!resolveCliInput().isHelpRequest) return false;
renderHelp(this.serverless.pluginManager.externalPlugins);
Expand Down
42 changes: 5 additions & 37 deletions lib/cli/interactive-setup/aws-credentials.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,19 @@ 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 { confirm, isUserLoggedIn, doesServiceInstanceHaveLinkedProvider } = require('./utils');
const openBrowser = require('../../utils/openBrowser');
const ServerlessError = require('../../serverless-error');
const resolveStage = require('../../utils/resolve-stage');
const resolveRegion = require('../../utils/resolve-region');

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_',
Expand All @@ -36,25 +34,6 @@ const getSdkInstance = memoizee(
{ 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);
Expand Down Expand Up @@ -322,18 +301,6 @@ const steps = {
},
};

const isUserLoggedIn = () => {
if (process.env.SERVERLESS_ACCESS_KEY) {
return true;
}

if (configUtils.getLoggedInUser()) {
return true;
}

return false;
};

module.exports = {
async isApplicable(context) {
const { configuration, history, options } = context;
Expand Down Expand Up @@ -397,7 +364,8 @@ module.exports = {
try {
const createdProviderUid = await steps.handleProviderCreation(context);
const hadExistingProviders = Boolean(providers.length);
if (createdProviderUid && hadExistingProviders) {
const shouldLinkProvider = createdProviderUid && hadExistingProviders;
if (shouldLinkProvider) {
// 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.
Expand Down
188 changes: 188 additions & 0 deletions lib/cli/interactive-setup/deploy.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
'use strict';

const Serverless = require('../../Serverless');
const chalk = require('chalk');
const { confirm, isUserLoggedIn, doesServiceInstanceHaveLinkedProvider } = require('./utils');
const _ = require('lodash');
const overrideStdoutWrite = require('process-utils/override-stdout-write');
const { getDashboardInteractUrl } = require('@serverless/dashboard-plugin/lib/dashboard');
const AWS = require('aws-sdk');

const printMessage = ({
serviceName,
hasBeenDeployed,
dashboardPlugin,
isConfiguredWithDashboard,
}) => {
if (isConfiguredWithDashboard) {
if (hasBeenDeployed) {
process.stdout.write(
`\n${chalk.green('Your project is live and available in ')}${chalk.white.bold(
`./${serviceName}`
)}\n`
);
process.stdout.write(`\n Run ${chalk.bold('serverless info')} in the project directory\n`);
process.stdout.write(' View your endpoints and services\n');
process.stdout.write(`\n Open ${chalk.bold(getDashboardInteractUrl(dashboardPlugin))}\n`);
process.stdout.write(' Invoke your functions and view logs in the dashboard\n');
process.stdout.write(`\n Run ${chalk.bold('serverless deploy')} in the project directory\n`);
process.stdout.write(
" Redeploy your service after you've updated your service code or configuration\n\n"
);

return;
}

process.stdout.write(
`\n${chalk.green('Your project is ready for deployment and available in ')}${chalk.white.bold(
`./${serviceName}`
)}\n`
);
process.stdout.write(`\n Run ${chalk.bold('serverless deploy')} in the project directory\n`);
process.stdout.write(' Deploy your newly created service\n');
process.stdout.write(
`\n Run ${chalk.bold('serverless info')} in the project directory after deployment\n`
);
process.stdout.write(' View your endpoints and services\n');
process.stdout.write('\n Open Serverless Dashboard after deployment\n');
process.stdout.write(' Invoke your functions and view logs in the dashboard\n\n');
return;
}

if (hasBeenDeployed) {
process.stdout.write(
`\n${chalk.green('Your project is live and available in ')}${chalk.white.bold(
`./${serviceName}`
)}\n`
);
process.stdout.write(`\n Run ${chalk.bold('serverless info')} in the project directory\n`);
process.stdout.write(' View your endpoints and services\n');
process.stdout.write(`\n Run ${chalk.bold('serverless deploy')} in the directory\n`);
process.stdout.write(
" Redeploy your service after you've updated your service code or configuration\n"
);
process.stdout.write(
`\n Run ${chalk.bold('serverless invoke')} and ${chalk.bold(
'serverless logs'
)} in the project directory\n`
);
process.stdout.write(' Invoke your functions directly and view the logs\n');
process.stdout.write(`\n Run ${chalk.bold('serverless')} in the project directory\n`);
process.stdout.write(
' Add metrics, alerts, and a log explorer, by enabling the dashboard functionality\n\n'
);
return;
}

process.stdout.write(
`\n${chalk.green('Your project is ready for deployment and available in ')}${chalk.white.bold(
`./${serviceName}`
)}\n`
);
process.stdout.write(`\n Run ${chalk.bold('serverless deploy')} in the project directory\n`);
process.stdout.write(' Deploy your newly created service\n');
process.stdout.write(
`\n Run ${chalk.bold('serverless info')} in the project directory after deployment\n`
);
process.stdout.write(' View your endpoints and services\n');
process.stdout.write(
`\n Run ${chalk.bold('serverless invoke')} and ${chalk.bold(
'serverless logs'
)} in the project directory after deployment\n`
);
process.stdout.write(' Invoke your functions directly and view the logs\n');
process.stdout.write(`\n Run ${chalk.bold('serverless')} in the project directory\n`);
process.stdout.write(
' Add metrics, alerts, and a log explorer, by enabling the dashboard functionality\n\n'
);
};

const configurePlugin = (serverless, originalStdWrite) => {
serverless.pluginManager.addPlugin(require('../../plugins/deploy-interactive'));
const interactivePlugin = serverless.pluginManager.plugins.find(
(plugin) => plugin.constructor.name === 'InteractiveDeployProgress'
);
interactivePlugin.progress._writeOriginalStdout = (data) => originalStdWrite(data);
return interactivePlugin;
};

module.exports = {
async isApplicable({ configuration, serviceDir, history, options }) {
if (!serviceDir) {
return false;
}

// We only want to consider newly created services for deploy step
if (!history.has('service')) {
return false;
}

if (
_.get(configuration, 'provider') !== 'aws' &&
_.get(configuration, 'provider.name') !== 'aws'
) {
return false;
}

// If `awsCredentials` step was not executed, we should proceed as it means that user has available credentials
if (!history.has('awsCredentials')) return true;

// We want to proceed if the service instance has a linked provider
if (
isUserLoggedIn() &&
(await doesServiceInstanceHaveLinkedProvider({ configuration, options }))
) {
return true;
}

// We want to proceed if local credentials are available
if (new AWS.Config().credentials) return true;

return false;
},
async run({ configuration, configurationFilename, serviceDir }) {
const serviceName = configuration.service;
if (!(await confirm('Do you want to deploy your project?', { name: 'shouldDeploy' }))) {
printMessage({
serviceName,
hasBeenDeployed: false,
isConfiguredWithDashboard: Boolean(configuration.org),
});
return;
}

const serverless = new Serverless({
configuration,
serviceDir,
configurationFilename,
isConfigurationResolved: true,
hasResolvedCommandsExternally: true,
isTelemetryReportedExternally: true,
commands: ['deploy'],
options: {},
});

let interactiveOutputPlugin;

try {
await overrideStdoutWrite(
() => {},
async (originalStdWrite) => {
await serverless.init();
interactiveOutputPlugin = configurePlugin(serverless, originalStdWrite);
await serverless.run();
}
);
} catch (err) {
interactiveOutputPlugin.handleError();
throw err;
}

printMessage({
serviceName,
hasBeenDeployed: true,
isConfiguredWithDashboard: Boolean(configuration.org),
dashboardPlugin: serverless.pluginManager.dashboardPlugin,
});
},
};
1 change: 1 addition & 0 deletions lib/cli/interactive-setup/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ const steps = {
dashboardSetOrg: require('@serverless/dashboard-plugin/lib/cli/interactive-setup/dashboard-set-org'),
awsCredentials: require('./aws-credentials'),
autoUpdate: require('./auto-update'),
deploy: require('./deploy'),
};

module.exports = async (context) => {
Expand Down
6 changes: 4 additions & 2 deletions lib/cli/interactive-setup/service.js
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,7 @@ module.exports = {
}

if (hasPackageJson) {
process.stdout.write(`\nInstalling dependencies with "npm" in "${projectName}" folder.\n`);
process.stdout.write(`\nInstalling dependencies with "npm" in "${projectName}" folder\n`);
const npmCommand = await npmCommandDeferred;
try {
await spawn(npmCommand, ['install'], { cwd: projectDir });
Expand Down Expand Up @@ -228,7 +228,9 @@ module.exports = {
}

process.stdout.write(
`\n${chalk.green(`Project successfully created in '${projectName}' folder.`)}\n`
`\n${chalk.green(
`Project successfully created in ${chalk.white.bold(projectName)} folder`
)}\n`
);
context.serviceDir = projectDir;
const configurationPath = await resolveConfigurationPath({ cwd: projectDir, options: {} });
Expand Down
34 changes: 34 additions & 0 deletions lib/cli/interactive-setup/utils.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
'use strict';

const inquirer = require('@serverless/utils/inquirer');
const configUtils = require('@serverless/utils/config');
const ServerlessError = require('../../serverless-error');
const resolveProviderCredentials = require('@serverless/dashboard-plugin/lib/resolveProviderCredentials');
const resolveStage = require('../../utils/resolve-stage');
const resolveRegion = require('../../utils/resolve-region');

module.exports = {
confirm: async (message, options = {}) => {
Expand All @@ -13,4 +18,33 @@ module.exports = {
})
)[name];
},
isUserLoggedIn: () => {
if (process.env.SERVERLESS_ACCESS_KEY) {
return true;
}

if (configUtils.getLoggedInUser()) {
return true;
}

return false;
},
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);
},
};
36 changes: 36 additions & 0 deletions lib/plugins/deploy-interactive.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
'use strict';

const cliProgressFooter = require('cli-progress-footer');
const chalk = require('chalk');

class InteractiveDeployProgress {
constructor(serverless) {
this.serverless = serverless;
this.progress = cliProgressFooter({ overrideStdout: false });
this.progress.shouldAddProgressAnimationPrefix = true;

this.hooks = {
'before:deploy:deploy': async () => {
this.progress.updateProgress('Deploying your project...\n');
},
'deploy:finalize': async () => {
this.progress.updateProgress('');
this.progress.writeStdout(chalk.green('\nDeployment succesful\n'));
},

'package:initialize': async () => {
this.progress.updateProgress('Packaging your project...\n');
},
'package:finalize': async () => {
this.progress.updateProgress('');
this.progress.writeStdout(chalk.green('\nPackaging succesful\n'));
},
};
}

handleError() {
this.progress.updateProgress('');
}
}

module.exports = InteractiveDeployProgress;

0 comments on commit 841817f

Please sign in to comment.