Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve interactive AWS creds flow #6449

Merged
merged 5 commits into from
Aug 8, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
78 changes: 52 additions & 26 deletions lib/plugins/interactiveCli/setupAws.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ const chalk = require('chalk');
const inquirer = require('./inquirer');
const awsCredentials = require('../aws/utils/credentials');
const { confirm } = require('./utils');
const openBrowser = require('../../utils/openBrowser');

const isValidAwsAccessKeyId = RegExp.prototype.test.bind(/^[A-Z0-9]{10,}$/);
const isValidAwsSecretAccessKey = RegExp.prototype.test.bind(/^[a-zA-Z0-9/+]{10,}$/);
Expand Down Expand Up @@ -51,38 +52,63 @@ module.exports = {
);
});
},
run() {
run(serverless) {
process.stdout.write(
'No AWS credentials were found on your computer, ' +
'you need these to host your application.\n\n'
);
return confirm('Do you want to set them up now?').then(isConfirmed => {
if (!isConfirmed) {
process.stdout.write(`You can setup your AWS account later. More details available here:
return confirm('Do you want to set them up now?')
.then(isConfirmed => {
if (!isConfirmed) {
process.stdout.write(`You can setup your AWS account later. More details available here:

http://slss.io/aws-creds-setup`);
return null;
}
process.stdout.write(
`\nGo here to learn how to create your AWS credentials:\n${chalk.bold(
'http://slss.io/aws-creds-setup'
)}\nThen enter them here:\n\n`
);
return awsAccessKeyIdInput().then(accessKeyId =>
awsSecretAccessKeyInput().then(secretAccessKey =>
awsCredentials
.saveFileProfiles(new Map([['default', { accessKeyId, secretAccessKey }]]))
.then(() =>
process.stdout.write(
`\n${chalk.green(
`AWS credentials saved on your machine at ${chalk.bold(
'~/.aws/credentials'
)}. Go there to change them at any time.`
)}\n`
}
return isConfirmed;
})
.then(isConfirmed => {
if (!isConfirmed) return null;
return confirm('Do you have an AWS account?')
.then(hasAccount => {
if (!hasAccount) {
return openBrowser('https://portal.aws.amazon.com/billing/signup').then(() =>
inquirer.prompt({
message: 'Press Enter to continue after creating an AWS account',
name: 'junk',
})
);
}
return null;
})
.then(() => {
const region = serverless.getProvider('aws').getRegion();
return 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`
);
})
.then(() =>
inquirer.prompt({
message: 'Press Enter to continue after creating an AWS user with access keys',
name: 'junk',
})
)
.then(() => {
return awsAccessKeyIdInput().then(accessKeyId =>
awsSecretAccessKeyInput().then(secretAccessKey =>
awsCredentials
.saveFileProfiles(new Map([['default', { accessKeyId, secretAccessKey }]]))
.then(() =>
process.stdout.write(
`\n${chalk.green(
`AWS credentials saved on your machine at ${chalk.bold(
'~/.aws/credentials'
)}. Go there to change them at any time.`
)}\n`
)
)
)
)
)
);
});
);
});
});
},
};
163 changes: 163 additions & 0 deletions lib/utils/open.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
'use strict';
// copied from https://raw.githubusercontent.com/sindresorhus/open/master/index.js
// and adapted for node 6 support. Because open>6 requries node >= 8 but open<6 fails npm audit
// changes:
// * use bluebird.promisify instead of util.promisfy
// * Object.assign instead of spread
// * use Array.prototype.push.apply(a,b) instead of a.push(...b)
// * async/await -> then :|
// * prettified with our config

const { promisify } = require('bluebird');
const path = require('path');
const childProcess = require('child_process');
const fs = require('fs');
const isWsl = require('is-wsl');

const pAccess = promisify(fs.access);
const pExecFile = promisify(childProcess.execFile);

// Path to included `xdg-open`
const localXdgOpenPath = path.join(__dirname, 'xdg-open');

// Convert a path from WSL format to Windows format:
// `/mnt/c/Program Files/Example/MyApp.exe` → `C:\Program Files\Example\MyApp.exe`
const wslToWindowsPath = filePath =>
pExecFile('wslpath', ['-w', filePath]).then(({ stdout }) => stdout.trim());

module.exports = (target, options) => {
if (typeof target !== 'string') {
throw new TypeError('Expected a `target`');
}

options = Object.assign(
{
wait: false,
background: false,
},
options
);

let command;
let appArguments = [];
const cliArguments = [];
const childProcessOptions = {};

if (Array.isArray(options.app)) {
appArguments = options.app.slice(1);
options.app = options.app[0];
}

return Promise.resolve()
.then(() => {
if (process.platform === 'darwin') {
command = 'open';

if (options.wait) {
cliArguments.push('--wait-apps');
}

if (options.background) {
cliArguments.push('--background');
}

if (options.app) {
cliArguments.push('-a', options.app);
}
return null;
} else if (process.platform === 'win32' || isWsl) {
command = `cmd${isWsl ? '.exe' : ''}`;
cliArguments.push('/c', 'start', '""', '/b');
target = target.replace(/&/g, '^&');

if (options.wait) {
cliArguments.push('/wait');
}

return Promise.resolve()
.then(() => {
if (options.app) {
if (isWsl && options.app.startsWith('/mnt/')) {
return wslToWindowsPath(options.app).then(windowsPath => {
options.app = windowsPath;
cliArguments.push(options.app);
});
}

cliArguments.push(options.app);
}
return null;
})
.then(() => {
if (appArguments.length > 0) {
Array.prototype.push.apply(cliArguments, appArguments);
}
return null;
});
}
return Promise.resolve()
.then(() => {
if (options.app) {
command = options.app;
} else {
// When bundled by Webpack, there's no actual package file path and no local `xdg-open`.
const isBundled = !__dirname || __dirname === '/';

// Check if local `xdg-open` exists and is executable.
return pAccess(localXdgOpenPath, fs.constants.X_OK)
.then(() => true)
.catch(() => false)
.then(exeLocalXdgOpen => {
const useSystemXdgOpen =
process.versions.electron ||
process.platform === 'android' ||
isBundled ||
!exeLocalXdgOpen;
command = useSystemXdgOpen ? 'xdg-open' : localXdgOpenPath;
});
}
return null;
})
.then(() => {
if (appArguments.length > 0) {
Array.prototype.push.apply(cliArguments, appArguments);
}

if (!options.wait) {
// `xdg-open` will block the process unless stdio is ignored
// and it's detached from the parent even if it's unref'd.
childProcessOptions.stdio = 'ignore';
childProcessOptions.detached = true;
}
});
})
.then(() => {
cliArguments.push(target);

if (process.platform === 'darwin' && appArguments.length > 0) {
cliArguments.push('--args');
Array.prototype.push.apply(cliArguments, appArguments);
}

const subprocess = childProcess.spawn(command, cliArguments, childProcessOptions);

if (options.wait) {
return new Promise((resolve, reject) => {
subprocess.once('error', reject);

subprocess.once('close', exitCode => {
if (exitCode > 0) {
reject(new Error(`Exited with code ${exitCode}`));
return;
}

resolve(subprocess);
});
});
}

subprocess.unref();

return subprocess;
});
};
34 changes: 34 additions & 0 deletions lib/utils/openBrowser.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
'use strict';

/* eslint-disable no-console */

const opn = require('./open');
const chalk = require('chalk');
const isDockerContainer = require('is-docker');
const BbPromise = require('bluebird');

function displayManualOpenMessage(url, err) {
// https://github.com/sindresorhus/log-symbols
console.log('---------------------------');
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why we do not rely here on serverless logger? Even if we don't like to use cli.log we may rel on cli.consoleLog.

It'll clearly indicate the intention of generating user facing CLI output.

Direct use of console.log is more for temporary debug output (which we prevent from committing in via lint rules)

const errMsg = err ? `\nError: ${err.message}` : '';
const msg = `Unable to open browser automatically${errMsg}`;
console.log(`🙈 ${chalk.red(msg)}`);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While cute I'm unsure if we want to go the "emotification of all the CLIs" route in an enterprise-facing project.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I copy and pasted this out of platform-sdk 😬

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(as to why i copied, it.. didn't want to add sdk as a direct dep, which is the only way to guarantee it's availability with things like yarn, and now also the fact it requires newer versions of node)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

now also the fact it requires newer versions of node

But SDK Is dependency of a plugin.. so if SDK was updated to not work in v6, then through Plugin it'll also break SLS (?)

console.log(chalk.green('Please open your browser & open the URL below to login:'));
console.log(chalk.yellow(url));
console.log('---------------------------');
return false;
}

module.exports = function openBrowser(url) {
let browser = process.env.BROWSER;
if (browser === 'none' || isDockerContainer()) {
return BbPromise.resolve(displayManualOpenMessage(url));
}
if (process.platform === 'darwin' && browser === 'open') {
browser = undefined;
}
const options = { wait: false, app: browser };
return opn(url, options).catch(err => displayManualOpenMessage(url, err));
};

/* eslint-enable no-console */
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,7 @@
"https-proxy-agent": "^2.2.2",
"inquirer": "^6.5.0",
"is-docker": "^1.1.0",
"is-wsl": "^2.1.0",
"js-yaml": "^3.13.1",
"json-cycle": "^1.3.0",
"json-refs": "^2.1.7",
Expand Down