feat: Console Dev Mode onboarding via dev command (#11896)

This commit is contained in:
Dan Jarvis 2023-04-05 13:19:34 -05:00 committed by GitHub
parent 6abea2477a
commit 73d0dc6cf4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 1390 additions and 40 deletions

49
docs/guides/dev.md Normal file
View File

@ -0,0 +1,49 @@
<!--
title: Serverless Framework - Console Dev Mode
menuText: Serverless Console Dev Mode
menuOrder: 12
description: Launch a Serverless Console dev mode session in the terminal
layout: Doc
-->
<!-- DOCS-SITE-LINK:START automatically generated -->
### [Read this on the main serverless docs site](https://www.serverless.com/framework/docs/guides/dev/)
<!-- DOCS-SITE-LINK:END -->
# Serverless Console Dev Mode
The `serverless dev` command will launch a [Serverless Console Dev Mode](https://www.serverless.com/console/docs/application-guide/dev-mode) session in your terminal.
```bash
serverless dev
```
## Options
- `--org` The organization that your AWS account is associated with in Serverless Console.
- `--region` or `-r` The region in that your function was deployed to.
- `--stage` or `-s` The stage in your service was deploy to.
- `--function` or `-f` The name of the function that you want to focus your dev mode activity on. If this option is excluded then all function activity will be streamed to your terminal.
- `--verbose` or `-v` If this flag is included all span input/output and lambda request/response data will be streamed to the terminal.
## Examples
### Start dev mode interactively selecting an organization
```bash
serverless dev
```
### Start dev mode with an org pre selected
```bash
serverless dev --org myorg
```
### Start dev mode with an org pre selected and all input output information logged
```bash
serverless deploy function --function helloWorld --update-config
```

View File

@ -30,6 +30,15 @@ commands.set('', {
usage: 'Enable Serverless Console integration. See: http://slss.io/console',
type: 'boolean',
},
'dev': {
usage: 'Launch dev mode activity feed. See: http://slss.io/console',
type: 'boolean',
},
'function': {
usage: 'Name of the function you would like the dev mode activity feed to observe.',
type: 'string',
shortcut: 'f',
},
},
lifecycleEvents: ['initializeService', 'setupAws', 'autoUpdate', 'end'],
});

View File

@ -0,0 +1,294 @@
'use strict';
const { writeText, style, log, progress } = require('@serverless/utils/log');
const { frontend } = require('@serverless/utils/lib/auth/urls');
const colorize = require('json-colorizer');
const WebSocket = require('ws');
const chalk = require('chalk');
const { devModeFeed } = require('@serverless/utils/lib/auth/urls');
const consoleUi = require('@serverless/utils/console-ui');
const streamBuffers = require('stream-buffers');
const apiRequest = require('@serverless/utils/api-request');
const promptWithHistory = require('@serverless/utils/inquirer/prompt-with-history');
const streamBuff = new streamBuffers.ReadableStreamBuffer({
frequency: 500,
chunkSize: 2048 * 1000000,
});
const consoleMonitoringCounter = {
logBatches: 0,
events: 0,
responses: 0,
};
const jsonError = {
colors: {
BRACE: '#FD5750',
BRACKET: '#FD5750',
COLON: '#FD5750',
COMMA: '#FD5750',
STRING_KEY: '#FD5750',
STRING_LITERAL: '#FD5750',
NUMBER_LITERAL: '#FD5750',
BOOLEAN_LITERAL: '#FD5750',
NULL_LITERAL: '#FD5750',
},
};
const jsonColors = {
colors: {
BRACE: 'white',
BRACKET: 'white',
COLON: 'white.bold',
COMMA: 'white',
STRING_KEY: 'white.bold',
STRING_LITERAL: 'white',
NUMBER_LITERAL: 'white',
BOOLEAN_LITERAL: 'white',
NULL_LITERAL: 'white',
},
};
const headerChalk = chalk.grey;
const errorTracker = {};
const handleSocketMessage = (context) => (data) => {
try {
const verbose = context.options.verbose;
const splitData = data.toString('utf-8').split(';;;');
const jsonArray = splitData
.filter((item) => item !== '' && item.startsWith('['))
.flatMap((item) => JSON.parse(item));
const sortedItems = consoleUi.omitAndSortDevModeActivity(jsonArray);
for (const activity of sortedItems) {
const resourceName = ((activity.tags || {}).aws || {}).resourceName;
const time = consoleUi.formatConsoleDate(new Date(activity.timestamp));
const tryPrintJSON = (str) => {
try {
const parsedBody = JSON.parse(str);
if (typeof parsedBody === 'string') {
throw new Error('Not a JSON object');
}
const colors = activity.severityText === 'ERROR' ? jsonError : jsonColors;
process.stdout.write(`${colorize(JSON.stringify(parsedBody, null, 2), colors)}\n`);
} catch (error) {
process.stdout.write(chalk.white(`${str}${str.endsWith('\n') ? '' : '\n'}`));
}
};
switch (activity.type) {
case 'log':
consoleMonitoringCounter.logBatches += 1;
process.stdout.write(headerChalk(`\n${time}${resourceName} • Log\n`));
tryPrintJSON(activity.body);
break;
case 'span': {
const span = consoleUi.formatConsoleSpan(activity);
process.stdout.write(
headerChalk(`\n${time}${resourceName} • Span • ${span.niceName}\n`)
);
if (verbose) {
if (activity.input) {
process.stdout.write(headerChalk('Input\n'));
tryPrintJSON(activity.input);
}
if (activity.output) {
process.stdout.write(headerChalk('Output\n'));
tryPrintJSON(activity.output);
}
}
break;
}
case 'aws-lambda-request':
process.stdout.write(headerChalk(`\n${time}${resourceName} • Invocation Started\n`));
if (verbose) {
tryPrintJSON(activity.body);
}
break;
case 'aws-lambda-response':
consoleMonitoringCounter.responses += 1;
process.stdout.write(headerChalk(`\n${time}${resourceName} • Invocation Ended\n`));
if (verbose) {
tryPrintJSON(activity.body);
}
if (errorTracker[activity.traceId]) {
const uiLink = `${frontend}/${
context.org.orgName
}/explorer?explorerSubScope=invocations&explorerTraceId=${encodeURIComponent(
activity.traceId
)}&globalScope=awsLambda&globalTimeFrame=24h`;
process.stdout.write(chalk.white(`View full trace: ${uiLink}\n`));
delete errorTracker[activity.traceId];
}
break;
case 'event': {
consoleMonitoringCounter.events += 1;
const { message, payload } = consoleUi.formatConsoleEvent(activity);
const isError = /ERROR •/.test(message);
const headerWriter = isError ? chalk.hex('#FD5750') : headerChalk;
const options = isError ? jsonError : jsonColors;
process.stdout.write(headerWriter(`\n${time}${resourceName}${message}\n`));
process.stdout.write(`${colorize(JSON.stringify(payload, null, 2), options)}\n`);
if (isError) {
errorTracker[activity.traceId] = true;
}
break;
}
default:
}
}
} catch (error) {
process.stdout.write(error, '\n');
}
};
const connectToWebSocket = async ({ functionName, region, accountId, org, state }) => {
const { token } = await apiRequest(`/api/identity/orgs/${org.orgId}/token`);
const ws = new WebSocket(`${devModeFeed}?Auth=${token}`, {
perMessageDeflate: false,
});
ws.on('open', () => {
if (state && state === 'firstConnection') {
const functionNameQueryParams = functionName
.map((name) => `devModeFunctionName=${encodeURIComponent(name)}`)
.join('&');
const uiLink = `${frontend}/${org.orgName}/dev-mode?devModeCloudAccountId=${accountId}&${functionNameQueryParams}`;
writeText(
style.aside(
'\n• Use the `--verbose` flag to see inputs and outputs of all requests (e.g. DynamoDB inputs/outputs).',
`• Use the Console Dev Mode UI for deeper inspection: ${uiLink}\n`,
'Waiting for activity... Invoke your functions now.'
)
);
} else if (state && state === 'resume') {
writeText(style.aside('\nResuming for dev mode activity...'));
}
ws.send(
JSON.stringify({ filters: { functionName, region: [region], accountId: [accountId] } })
);
});
ws.on('message', (data) => {
streamBuff.put(`${data.toString('utf-8')};;;`);
});
return ws;
};
const startDevModeFeed = async (context, devModeFeedConnection) =>
new Promise((resolve) => {
const createStillWorkingTimeout = () =>
setTimeout(async () => {
clearInterval(eventPublishTimer);
clearInterval(connectionRefreshTimer);
devModeFeedConnection.terminate();
writeText(style.aside('Pausing for dev mode activity.\n'));
const shouldContinue = await promptWithHistory({
name: 'shouldContinue',
message: 'Are you still working?',
stepHistory: context.stepHistory,
type: 'confirm',
});
if (shouldContinue) {
await startDevModeFeed(context, 'resume');
}
resolve();
}, 1000 * 60 * 60 * 1.5); // Check for activity every 1.5 hours
let stillWorkingTimer = createStillWorkingTimeout();
const connectionRefreshTimer = setInterval(async () => {
const newConnection = await connectToWebSocket({
functionName: context.consoleDevModeTargetFunctions,
accountId: context.awsAccountId,
region: context.serverless.service.provider.region,
org: context.org,
state: false,
});
const oldConnection = devModeFeedConnection;
oldConnection.terminate();
devModeFeedConnection = newConnection;
watchStream(devModeFeedConnection);
}, 1000 * 60 * 60); // Refresh every hour
const eventPublishTimer = setInterval(async () => {
const { userId } = await apiRequest('/api/identity/me');
const body = {
source: 'web.dev_mode.activity.v1',
event: {
orgUid: context.org.orgId,
userId,
logBatches: consoleMonitoringCounter.logBatches,
responses: consoleMonitoringCounter.responses,
events: consoleMonitoringCounter.events,
source: 'cli:serverless',
},
};
await apiRequest('/api/events/publish', {
method: 'POST',
body,
});
consoleMonitoringCounter.logBatches = 0;
consoleMonitoringCounter.responses = 0;
consoleMonitoringCounter.events = 0;
}, 1000 * 60); // Publish every 60 seconds
const watchStream = (feed) => {
feed.on('message', (data) => {
// Ignore connection message
const parsedData = JSON.parse(data.toString('utf-8'));
if (!parsedData.resetThrottle) {
clearTimeout(stillWorkingTimer);
stillWorkingTimer = createStillWorkingTimeout();
}
});
feed.on('close', () => {
// Clean up if we receive a close event
clearInterval(eventPublishTimer);
clearInterval(connectionRefreshTimer);
clearTimeout(stillWorkingTimer);
resolve();
});
};
watchStream(devModeFeedConnection);
});
module.exports = {
async isApplicable(context) {
const { isConsoleDevMode, org, consoleDevModeTargetFunctions } = context;
if (!isConsoleDevMode) {
context.inapplicabilityReasonCode = 'NON_DEV_MODE_CONTEXT';
return false;
}
if (!org) {
context.inapplicabilityReasonCode = 'UNRESOLVED_ORG';
return false;
}
if (!consoleDevModeTargetFunctions) {
context.inapplicabilityReasonCode = 'NO_TARGET_FUNCTIONS';
return false;
}
return true;
},
async run(context) {
const devModeProgress = progress.get('dev-mode-progress');
devModeProgress.remove();
log.notice.success('Dev Mode Initialized.');
streamBuff.on('data', handleSocketMessage(context));
const devModeFeedConnection = await connectToWebSocket({
functionName: context.consoleDevModeTargetFunctions,
accountId: context.awsAccountId,
region: context.serverless.service.provider.region,
org: context.org,
state: 'firstConnection',
});
await startDevModeFeed(context, devModeFeedConnection);
},
};

View File

@ -0,0 +1,191 @@
'use strict';
const wait = require('timers-ext/promise/sleep');
const { log, progress } = require('@serverless/utils/log');
const apiRequest = require('@serverless/utils/api-request');
const progressKey = 'dev-mode-progress';
const allFunctionsExist = async (context) => {
const { total, hits } = await apiRequest(`/api/search/orgs/${context.org.orgId}/search`, {
method: 'POST',
body: {
from: 0,
size: context.consoleDevModeTargetFunctions.length,
query: {
bool: {
must: [
{
match: { type: 'resource_aws_lambda' },
},
{
match: { tag_account_id: context.awsAccountId },
},
{
terms: { 'aws_lambda_name.keyword': context.consoleDevModeTargetFunctions },
},
],
},
},
},
});
return {
hits,
allExist: total === context.consoleDevModeTargetFunctions.length,
total: context.consoleDevModeTargetFunctions.length,
functionCount: total,
};
};
const checkInstrumentationStatus = async (context) => {
const { total } = await apiRequest(`/api/search/orgs/${context.org.orgId}/search`, {
method: 'POST',
body: {
from: 0,
size: context.consoleDevModeTargetFunctions.length,
query: {
bool: {
must: [
{
match: { type: 'resource_aws_lambda' },
},
{
match: { tag_account_id: context.awsAccountId },
},
{
match: { instrument_mode: 'dev' },
},
{
terms: { 'aws_lambda_name.keyword': context.consoleDevModeTargetFunctions },
},
],
},
},
},
});
return {
isInstrumented: total === context.consoleDevModeTargetFunctions.length,
total: context.consoleDevModeTargetFunctions.length,
instrumented: total,
};
};
const waitForInstrumentation = async (context) => {
const instrumentationProgress = progress.get(progressKey);
let isInstrumenting = true;
while (isInstrumenting) {
const { isInstrumented: done, total, instrumented } = await checkInstrumentationStatus(context);
instrumentationProgress.update(`Instrumenting ${instrumented}/${total} functions`);
if (done) {
isInstrumenting = false;
} else {
await wait(1000);
}
}
};
module.exports = {
async isApplicable(context) {
const { isConsoleDevMode, org } = context;
if (!isConsoleDevMode) {
context.inapplicabilityReasonCode = 'NON_DEV_MODE_CONTEXT';
return false;
}
if (!org) {
context.inapplicabilityReasonCode = 'UNRESOLVED_ORG';
return false;
}
const instrumentationProgress = progress.get(progressKey);
instrumentationProgress.update('Validating Serverless Console instrumentation status');
// Add single function name or all function names to the list
const targetFunctions = [];
const targetInstrumentations = [];
context.serverless.service.setFunctionNames(context.options);
if (context.options.function) {
const func = context.serverless.service.getFunction(context.options.function);
const functionName = func.name;
targetInstrumentations.push({
instrumentations: {
mode: 'dev',
},
resourceKey: `aws_${context.awsAccountId}_function_${context.serverless.service.provider.region}_${functionName}`,
});
targetFunctions.push(functionName);
} else {
const names = context.serverless.service.getAllFunctionsNames();
for (const name of names) {
const functionName = name;
targetInstrumentations.push({
instrumentations: {
mode: 'dev',
},
resourceKey: `aws_${context.awsAccountId}_function_${context.serverless.service.provider.region}_${functionName}`,
});
targetFunctions.push(functionName);
}
}
context.targetInstrumentations = targetInstrumentations;
context.consoleDevModeTargetFunctions = targetFunctions;
const { allExist, total, functionCount, hits } = await allFunctionsExist(context);
if (!allExist) {
const foundFunctionNames = hits.map(({ aws_lambda_name: awsLambdaName }) => awsLambdaName);
log.notice();
const promptLogger = functionCount === 0 ? log.error : log.warning;
promptLogger(
`${functionCount} of ${total} functions exist in your console integration. Deploy your service now to add these functions to your integration.\n`
);
if (functionCount === 0) {
context.inapplicabilityReasonCode = 'NO_FUNCTIONS_EXIST';
context.targetInstrumentations = undefined;
context.consoleDevModeTargetFunctions = undefined;
return false;
}
context.consoleDevModeTargetFunctions = foundFunctionNames;
context.targetInstrumentations = context.targetInstrumentations.filter((target) => {
const name = target.resourceKey.split('_').pop();
return foundFunctionNames.includes(name);
});
}
const { isInstrumented } = await checkInstrumentationStatus(context);
if (isInstrumented) {
context.inapplicabilityReasonCode = 'ALREADY_INSTRUMENTED';
}
return !isInstrumented;
},
async run(context) {
const instrumentationProgress = progress.get(progressKey);
instrumentationProgress.notice('Instrumenting functions', 'This may take a few minutes...');
// Chunk targetInstrumentations into 50 resources per request
const distributeArrayBy50 = (array) => {
const result = [];
let index = 0;
while (index < array.length) result.push(array.slice(index, (index += 50)));
return result;
};
const chunkedResources = distributeArrayBy50(context.targetInstrumentations);
// Send requests to instrument
for (const chunk of chunkedResources) {
await apiRequest('/api/integrations/aws/instrumentations', {
urlName: 'integrationsBackend',
method: 'POST',
body: {
orgId: context.org.orgId,
resources: chunk,
},
});
}
// Wait for instrumentation to complete
await waitForInstrumentation(context);
return true;
},
};

View File

@ -30,6 +30,8 @@ module.exports = {
return false;
}
showOnboardingWelcome(context);
if (await resolveAuthMode()) {
context.inapplicabilityReasonCode = 'ALREADY_LOGGED_IN';
return false;
@ -38,8 +40,6 @@ module.exports = {
return true;
},
async run(context) {
showOnboardingWelcome(context);
return steps.loginOrRegister(context);
},
steps,

View File

@ -4,11 +4,10 @@ const { log } = require('@serverless/utils/log');
const resolveAuthMode = require('@serverless/utils/auth/resolve-mode');
const apiRequest = require('@serverless/utils/api-request');
const promptWithHistory = require('@serverless/utils/inquirer/prompt-with-history');
const { showOnboardingWelcome } = require('./utils');
const orgsChoice = async (orgs, stepHistory) =>
promptWithHistory({
message: 'What org do you want to add this service to?',
message: 'What org do you want to use this service with?',
type: 'list',
name: 'orgName',
choices: [
@ -45,9 +44,6 @@ module.exports = {
return false;
}
log.notice();
showOnboardingWelcome(context);
if (orgName) {
const org = orgs.find((someOrg) => someOrg.orgName === orgName);
if (org) {

View File

@ -54,7 +54,7 @@ const waitUntilIntegrationIsReady = async (context) => {
module.exports = {
async isApplicable(context) {
const { isConsole } = context;
const { isConsole, isConsoleDevMode } = context;
if (!isConsole) {
context.inapplicabilityReasonCode = 'NON_CONSOLE_CONTEXT';
@ -91,7 +91,9 @@ module.exports = {
}
log.notice();
log.notice.success('Your AWS account is integrated with Serverless Console');
if (!isConsoleDevMode) {
log.notice.success('Your AWS account is integrated with Serverless Console');
}
context.inapplicabilityReasonCode = 'INTEGRATED';
return false;
}
@ -100,10 +102,13 @@ module.exports = {
await awsRequest(context, cloudFormationServiceConfig, 'describeStacks', {
StackName: iamRoleStackName,
});
log.warning(
log.error(
'Cannot integrate with Serverless Console: ' +
'AWS account is already integrated with another org'
'AWS account is already integrated with another org. ' +
'You can set the AWS_PROFILE environment variable to use a different AWS Profile.'
);
context.isConsole = false;
context.isConsoleDevMode = false;
context.inapplicabilityReasonCode = 'AWS_ACCOUNT_ALREADY_INTEGRATED';
return false;
} catch (error) {
@ -114,19 +119,20 @@ module.exports = {
},
async run(context) {
const { stepHistory } = context;
const { stepHistory, isConsoleDevMode } = context;
if (
!(await promptWithHistory({
message: `Press [Enter] to enable Serverless Console's next-generation monitoring.\n\n${style.aside(
[
'This will create an IAM Role in your AWS account with the following permissions:',
'- Subscribe to CloudWatch logs and metrics',
'- Update Lambda layers and env vars to add tracing and real-time logging',
'- Read resource info for security alerts',
' Subscribe to CloudWatch logs and metrics',
' Update Lambda layers and env vars to add tracing and real-time logging',
' Read resource info for security alerts',
`See the IAM Permissions transparently here: ${style.link(
'https://slss.io/iam-role-permissions'
)}`,
'Would you like to proceed?',
]
)}`,
type: 'confirm',
@ -168,7 +174,9 @@ module.exports = {
await waitUntilIntegrationIsReady(context);
log.notice.success('Your AWS account is integrated with Serverless Console');
if (!isConsoleDevMode) {
log.notice.success('Your AWS account is integrated with Serverless Console');
}
} finally {
integrationSetupProgress.remove();
}

View File

@ -26,9 +26,9 @@ const steps = {
module.exports = {
async isApplicable(context) {
const { configuration, options, serviceDir } = context;
const { isDashboard, configuration, options, serviceDir } = context;
if (options.console) {
if (!isDashboard) {
context.inapplicabilityReasonCode = 'CONSOLE_CONTEXT';
return false;
}

View File

@ -193,15 +193,15 @@ const steps = {
module.exports = {
async isApplicable(context) {
const { configuration, options, serviceDir } = context;
const { isDashboard, configuration, options, serviceDir } = context;
if (!serviceDir) {
context.inapplicabilityReasonCode = 'NOT_IN_SERVICE_DIRECTORY';
if (!isDashboard) {
context.inapplicabilityReasonCode = 'CONSOLE_CONTEXT';
return false;
}
if (options.console) {
context.inapplicabilityReasonCode = 'CONSOLE_CONTEXT';
if (!serviceDir) {
context.inapplicabilityReasonCode = 'NOT_IN_SERVICE_DIRECTORY';
return false;
}
@ -243,6 +243,7 @@ module.exports = {
context.inapplicabilityReasonCode = 'NO_ORGS_AVAILABLE';
return false;
}
if (!usesServerlessAccessKey) {
user = configUtils.getLoggedInUser(); // Refreshed, as new token might have been generated
}

View File

@ -23,13 +23,14 @@ const printMessage = () => {
module.exports = {
async isApplicable(context) {
const { configuration, serviceDir, options, initial } = context;
const { isConsole, isOnboarding, configuration, serviceDir, options, initial } = context;
if (!serviceDir) {
context.inapplicabilityReasonCode = 'NOT_IN_SERVICE_DIRECTORY';
return false;
}
if (options.console && initial.isInServiceContext) {
if ((isConsole || !isOnboarding) && initial.isInServiceContext) {
context.inapplicabilityReasonCode = 'CONSOLE_INTEGRATION';
return false;
}

View File

@ -1,6 +1,8 @@
'use strict';
const apiRequest = require('@serverless/utils/api-request');
const inquirer = require('@serverless/utils/inquirer');
const ServerlessError = require('@serverless/utils/serverless-error');
const { StepHistory } = require('@serverless/utils/telemetry');
const log = require('@serverless/utils/log').log.get('onboarding');
const { resolveInitialContext } = require('./utils');
@ -15,6 +17,8 @@ const steps = {
dashboardSetOrg: require('./dashboard-set-org'),
awsCredentials: require('./aws-credentials'),
deploy: require('./deploy'),
consoleEnableDevMode: require('./console-enable-dev-mode'),
consoleDevModeFeed: require('./console-dev-mode-feed'),
};
const resolveAwsAccountId = async (context) => {
@ -51,8 +55,9 @@ module.exports = async (context) => {
commandUsage.initialContext = initialContext;
context.initial = initialContext;
context.awsAccountId = await resolveAwsAccountId(context);
if (options.console) {
context.isOnboarding = !options.dev;
context.isDashboard = !options.console && !options.dev;
if (options.console || (options.dev && initialContext.isInServiceContext)) {
if (!context.awsAccountId) {
log.error(
'Were unable to connect Console via the CLI - No local AWS credentials found\n' +
@ -63,6 +68,26 @@ module.exports = async (context) => {
}
}
if (context.isConsole && options.dev && initialContext.isInServiceContext) {
const compatibilityMap = await apiRequest('/api/inventories/compatibility', {
method: 'GET',
noAuth: true,
});
const devModeRuntimeCompatibility = compatibilityMap.mode.dev.runtimes;
const { provider } = context.serverless.service;
if (!devModeRuntimeCompatibility.includes(provider.runtime)) {
log.error('This services runtime is not currently supported by Serverless Console Dev Mode.');
context.isConsole = false;
} else {
context.isConsoleDevMode = true;
}
} else if (options.dev && !initialContext.isInServiceContext) {
throw new ServerlessError(
'Cannot launch dev mode when not in a service context.',
'NOT_APPLICABLE_DEV_MODE_CONTEXT'
);
}
for (const [stepName, step] of Object.entries(steps)) {
delete context.stepHistory;
delete context.inapplicabilityReasonCode;

View File

@ -114,7 +114,9 @@ module.exports = {
},
showOnboardingWelcome: memoizee(
(context) => {
if (context.isConsole) {
if (context.isConsoleDevMode) {
log.notice('Initializing Dev Mode via Serverless Console...');
} else if (context.isConsole) {
log.notice("Enabling Serverless Console's next-generation monitoring.");
log.notice(style.aside('Learn more at https://serverless.com/console'));
} else {

View File

@ -52,6 +52,7 @@
"https-proxy-agent": "^5.0.1",
"is-docker": "^2.2.1",
"js-yaml": "^4.1.0",
"json-colorizer": "^2.2.2",
"json-cycle": "^1.3.0",
"json-refs": "^3.0.15",
"lodash": "^4.17.21",
@ -67,6 +68,7 @@
"require-from-string": "^2.0.2",
"semver": "^7.3.8",
"signal-exit": "^3.0.7",
"stream-buffers": "^3.0.2",
"strip-ansi": "^6.0.1",
"supports-color": "^8.1.1",
"tar": "^6.1.13",
@ -74,6 +76,7 @@
"type": "^2.7.2",
"untildify": "^4.0.0",
"uuid": "^9.0.0",
"ws": "^7.5.9",
"yaml-ast-parser": "0.0.43"
},
"devDependencies": {
@ -105,7 +108,6 @@
"sinon": "^13.0.2",
"sinon-chai": "^3.7.0",
"standard-version": "^9.5.0",
"ws": "^7.5.9",
"xml2js": "^0.4.23"
},
"eslintConfig": {

View File

@ -135,11 +135,18 @@ processSpanPromise = (async () => {
(() => {
// Rewrite eventual `sls deploy -f` into `sls deploy function -f`
// Also rewrite `serverless dev` to `serverless --dev``
const isParamName = RegExp.prototype.test.bind(require('../lib/cli/param-reg-exp'));
const args = process.argv.slice(2);
const firstParamIndex = args.findIndex(isParamName);
const commands = args.slice(0, firstParamIndex === -1 ? Infinity : firstParamIndex);
if (commands.join('') === 'dev') {
process.argv[2] = '--dev';
return;
}
if (commands.join(' ') !== 'deploy') return;
if (!args.includes('-f') && !args.includes('--function')) return;
logDeprecation(

View File

@ -0,0 +1,381 @@
'use strict';
const chai = require('chai');
const WebSocket = require('ws');
const sinon = require('sinon');
const sleep = require('timers-ext/promise/sleep');
const consoleUi = require('@serverless/utils/console-ui');
const proxyquire = require('proxyquire').noPreserveCache();
const { expect } = chai;
chai.use(require('chai-as-promised'));
let step;
const originalSetInterval = setInterval;
describe('test/unit/lib/cli/interactive-setup/console-dev-mode-feed.test.js', function () {
this.timeout(1000 * 60 * 3);
const fakeOrgId = '123';
const fakeAWSAccountId = 'account1';
const publishFake = sinon.fake();
const fakeRegion = 'us-east-1';
const fakeTime = 'fakeTime';
const consoleDevModeTargetFunctions = ['function1'];
const fakeGreyWriter = sinon.fake.returns('');
const fakeJSONWriter = sinon.fake.returns('');
const fakeErrorWriter = sinon.fake.returns('');
let socketConnection;
let socketServer;
let timers = [];
before(() => {
step = proxyquire('../../../../../lib/cli/interactive-setup/console-dev-mode-feed', {
'@serverless/utils/api-request': async (pathname, options) => {
if (pathname === `/api/identity/orgs/${fakeOrgId}/token`) {
return { token: 'fakeToken' };
}
if (pathname === '/api/identity/me') {
return { userId: 'user123' };
}
if (pathname === '/api/events/publish') {
publishFake(options);
return { success: true };
}
throw new Error(`Unexpected pathname "${pathname}"`);
},
'@serverless/utils/console-ui': {
omitAndSortDevModeActivity: consoleUi.omitAndSortDevModeActivity,
formatConsoleDate: () => fakeTime,
formatConsoleSpan: (span) => ({
niceName: span.name,
}),
formatConsoleEvent: (event) => ({
message: /\.error\./.test(event.eventName) ? 'ERROR • fake' : 'WARNING • fake',
payload: /\.error\./.test(event.eventName) ? event.tags.error : event.tags.warning,
}),
},
'@serverless/utils/lib/auth/urls': {
devModeFeed: 'ws://localhost:9988',
},
'chalk': {
white: fakeGreyWriter,
grey: fakeGreyWriter,
hex: () => fakeErrorWriter,
},
'json-colorizer': fakeJSONWriter,
});
});
beforeEach(() => {
timers = [];
// eslint-disable-next-line no-global-assign
setInterval = (cb) => {
timers.push(cb);
};
});
afterEach(() => {
if (socketConnection) {
socketConnection.terminate();
}
if (socketServer) {
socketServer.close();
}
// eslint-disable-next-line no-global-assign
setInterval = originalSetInterval;
});
it('Should be ineffective, when not in console dev mode context', async () => {
const context = { isConsoleDevMode: false, options: {} };
expect(await step.isApplicable(context)).to.be.false;
expect(context.inapplicabilityReasonCode).to.equal('NON_DEV_MODE_CONTEXT');
});
it('Should be ineffective, when no org is selected', async () => {
const context = { isConsoleDevMode: true, options: {}, org: null };
expect(await step.isApplicable(context)).to.be.false;
expect(context.inapplicabilityReasonCode).to.equal('UNRESOLVED_ORG');
});
it('Should be ineffective, when functions are targeted', async () => {
const context = { isConsoleDevMode: true, options: {}, org: { orgId: fakeOrgId } };
expect(await step.isApplicable(context)).to.be.false;
expect(context.inapplicabilityReasonCode).to.equal('NO_TARGET_FUNCTIONS');
});
it('Should be effective and connect to websocket', async () => {
const context = {
isConsoleDevMode: true,
options: {
verbose: true,
},
org: { orgId: fakeOrgId },
consoleDevModeTargetFunctions,
awsAccountId: fakeAWSAccountId,
serverless: {
service: {
provider: fakeRegion,
},
},
};
expect(await step.isApplicable(context)).to.be.true;
const waitForConnection = () =>
new Promise((resolve) => {
socketServer = new WebSocket.Server({ port: 9988 });
step.run(context);
socketServer.on('connection', (ws) => {
ws.on('message', () => {
ws.send(
JSON.stringify({ message: 'filters successfully applied', resetThrottle: true })
);
});
resolve(ws);
});
});
socketConnection = await waitForConnection();
/**
* Set of messages containing 👇
*
* 1. request
* 2. JSON log
* 3. text log
* 4. JSON parsable text log
* 5. s3 span
* 6. Warning event
* 7. Error event
* 8. response
*
* It also included the aws.lambda* spans that should be ignored :)
*/
const mockMessages = [
[
{
body: '{"key1":"value1","key2":"value2","key3":"value3"}',
timestamp: '2023-03-20T21:26:10.790Z',
tags: {
aws: {
resourceName: 'example-dev-function1',
},
},
type: 'aws-lambda-request',
sequenceId: 1679347571057,
},
],
[
{
name: 'aws.lambda.initialization',
timestamp: '2023-03-20T21:26:10.365Z',
tags: {
aws: {
resourceName: 'example-dev-function1',
},
},
type: 'span',
sequenceId: 1679347571276,
},
],
[
{
body: '{"message":"Hi dev mode 👋"}\n',
severityNumber: '1',
severityText: 'INFO',
timestamp: '2023-03-20T21:26:10.802Z',
tags: {
aws: {
resourceName: 'example-dev-function1',
},
},
type: 'log',
sequenceId: 1679344258090,
},
{
body: 'text log\n',
severityNumber: '1',
severityText: 'INFO',
timestamp: '2023-03-20T21:26:10.802Z',
tags: {
aws: {
resourceName: 'example-dev-function1',
},
},
type: 'log',
sequenceId: 1679344258091,
},
{
body: '"hello"',
severityNumber: '1',
severityText: 'INFO',
timestamp: '2023-03-20T21:26:10.802Z',
tags: {
aws: {
resourceName: 'example-dev-function1',
},
},
type: 'log',
sequenceId: 1679344258091,
},
],
[
{
customTags: '{}',
input: '{"Bucket":"fake-bucket"}',
name: 'aws.sdk.s3.listobjectsv2',
output: '{"message": "s3 output"}',
timestamp: '2023-03-20T21:26:10.804Z',
tags: {
aws: {
resourceName: 'example-dev-function1',
},
},
type: 'span',
sequenceId: 1679347571306,
},
{
customTags: '{"foo":"bar"}',
eventName: 'telemetry.warning.generated.v1',
tags: {
aws: {
resourceName: 'example-dev-function1',
},
warning: {
message: 'This is a warning',
stacktrace:
'at module.exports.handler (/var/task/index.js:12:7)\nat process.processTicksAndRejections (node:internal/process/task_queues:95:5)',
type: 'WARNING_TYPE_USER',
},
},
timestamp: '2023-03-20T21:26:10.916Z',
type: 'event',
sequenceId: 1679347571307,
},
{
customTags: '{"foo":"bar"}',
eventName: 'telemetry.error.generated.v1',
tags: {
aws: {
resourceName: 'example-dev-function1',
},
error: {
message: 'Oh no!',
name: 'Error',
stacktrace:
'at module.exports.handler (/var/task/index.js:13:20)\nat process.processTicksAndRejections (node:internal/process/task_queues:95:5)',
type: 'ERROR_TYPE_CAUGHT_USER',
},
},
timestamp: '2023-03-20T21:26:10.924Z',
type: 'event',
sequenceId: 1679347571308,
},
],
[
{
customTags: '{}',
name: 'aws.lambda.invocation',
timestamp: '2023-03-20T21:26:10.790Z',
type: 'span',
tags: {
aws: {
resourceName: 'example-dev-function1',
},
},
sequenceId: 1679347572067,
},
{
customTags: '{}',
isHistorical: false,
name: 'aws.lambda',
timestamp: '2023-03-20T21:26:10.365Z',
tags: {
aws: {
resourceName: 'example-dev-function1',
},
},
type: 'span',
sequenceId: 1679347572068,
},
],
[
{
body: '{"response":"hello there"}',
timestamp: '2023-03-20T21:26:11.934Z',
tags: {
aws: {
resourceName: 'example-dev-function1',
},
},
type: 'aws-lambda-response',
sequenceId: 1679347572127,
},
],
];
// Send all messages
for (const message of mockMessages) {
socketConnection.send(JSON.stringify(message));
}
// Wait for all messages to be processed
await sleep(600);
// Publish dev mode events
await timers[1]();
// Close connection to socket
socketConnection.terminate();
// Assert that each message had a header and our text log was written
expect(fakeGreyWriter.callCount).to.equal(12);
expect(fakeGreyWriter.getCall(0).args[0]).to.equal(
`\n${fakeTime} • example-dev-function1 • Invocation Started\n`
);
// Plain text log message
expect(fakeGreyWriter.getCall(3).args[0]).to.equal('text log\n');
// Empty text log message
expect(fakeGreyWriter.getCall(5).args[0]).to.equal('"hello"\n');
expect(fakeGreyWriter.getCall(6).args[0]).to.equal(
`\n${fakeTime} • example-dev-function1 • Span • aws.sdk.s3.listobjectsv2\n`
);
// Check end message is last
expect(fakeGreyWriter.getCall(10).args[0]).to.equal(
`\n${fakeTime} • example-dev-function1 • Invocation Ended\n`
);
// Assert that our first log message was processed as JSON and both the warning and error event were printed to the console
expect(fakeJSONWriter.callCount).to.equal(7);
expect(fakeJSONWriter.getCall(0).args[0]).to.equal(
`${JSON.stringify(JSON.parse(mockMessages[0][0].body), null, 2)}`
);
expect(fakeJSONWriter.getCall(1).args[0]).to.equal(
`${JSON.stringify(JSON.parse(mockMessages[2][0].body), null, 2)}`
);
expect(fakeJSONWriter.getCall(2).args[0]).to.equal(
`${JSON.stringify(JSON.parse(mockMessages[3][0].input), null, 2)}`
);
expect(fakeJSONWriter.getCall(3).args[0]).to.equal(
`${JSON.stringify(JSON.parse(mockMessages[3][0].output), null, 2)}`
);
expect(fakeJSONWriter.getCall(4).args[0]).to.equal(
`${JSON.stringify(mockMessages[3][1].tags.warning, null, 2)}`
);
expect(fakeJSONWriter.getCall(5).args[0]).to.equal(
`${JSON.stringify(mockMessages[3][2].tags.error, null, 2)}`
);
expect(fakeJSONWriter.getCall(5).args[1].colors.BRACE).to.equal('#FD5750');
// Assert that the error event was printed with the error
expect(fakeErrorWriter.callCount).to.equal(1);
expect(fakeErrorWriter.getCall(0).args[0]).to.equal(
`\n${fakeTime} • example-dev-function1 • ERROR • fake\n`
);
// Validate publish event was called
expect(publishFake.callCount).to.equal(1);
expect(publishFake.getCall(0).args[0].body.event.logBatches).to.equal(3);
expect(publishFake.getCall(0).args[0].body.event.responses).to.equal(1);
expect(publishFake.getCall(0).args[0].body.event.events).to.equal(2);
expect(publishFake.getCall(0).args[0].body.event.source).to.equal('cli:serverless');
});
});

View File

@ -0,0 +1,298 @@
'use strict';
const chai = require('chai');
const proxyquire = require('proxyquire').noPreserveCache();
const { expect } = chai;
chai.use(require('chai-as-promised'));
let step;
describe('test/unit/lib/cli/interactive-setup/console-enable-dev-mode.test.js', () => {
let fakeOrgId;
let expectedFunctionCount;
let fakeRegion;
let expectedFunctionHits;
let expectedServiceFunctionNames;
beforeEach(() => {
fakeOrgId = '123';
expectedFunctionHits = [
{
aws_lambda_name: 'function1',
},
];
expectedServiceFunctionNames = expectedFunctionHits.map((hit) => hit.aws_lambda_name);
expectedFunctionCount = expectedFunctionHits.length;
fakeRegion = 'us-east-1';
});
const configureStep = ({
functionExistResponse,
checkInstrumentationResponse,
instrumentFunctionResponse = { success: true },
}) => {
step = proxyquire('../../../../../lib/cli/interactive-setup/console-enable-dev-mode', {
'@serverless/utils/api-request': async (pathname, options) => {
if (
pathname === `/api/search/orgs/${fakeOrgId}/search` &&
options.body.query.bool.must.length === 3
) {
return functionExistResponse;
}
if (
pathname === `/api/search/orgs/${fakeOrgId}/search` &&
options.body.query.bool.must.length === 4
) {
return checkInstrumentationResponse;
}
if (pathname === '/api/integrations/aws/instrumentations') {
if (options.body.resources.length > 50) {
throw new Error('Too many resources to instrument');
}
return instrumentFunctionResponse;
}
throw new Error(`Unexpected pathname "${pathname}"`);
},
});
};
it('Should be ineffective, when not in console dev mode context', async () => {
configureStep({
functionExistResponse: {},
checkInstrumentationResponse: {},
});
const context = { isConsoleDevMode: false, options: {} };
expect(await step.isApplicable(context)).to.be.false;
expect(context.inapplicabilityReasonCode).to.equal('NON_DEV_MODE_CONTEXT');
});
it('Should be ineffective, when no org is selected', async () => {
configureStep({
functionExistResponse: {},
checkInstrumentationResponse: {},
});
const context = { isConsoleDevMode: true, options: {}, org: null };
expect(await step.isApplicable(context)).to.be.false;
expect(context.inapplicabilityReasonCode).to.equal('UNRESOLVED_ORG');
});
it('Should be ineffective, when functions are already instrumented', async () => {
configureStep({
functionExistResponse: {
total: expectedFunctionCount,
hits: expectedFunctionHits,
},
checkInstrumentationResponse: {
total: expectedFunctionCount,
hits: expectedFunctionHits,
},
});
const context = {
isConsoleDevMode: true,
options: {},
org: {
orgId: fakeOrgId,
},
serverless: {
service: {
provider: {
region: fakeRegion,
},
setFunctionNames: () => {},
getAllFunctionsNames: () => expectedServiceFunctionNames,
},
},
};
expect(await step.isApplicable(context)).to.be.false;
expect(context.inapplicabilityReasonCode).to.equal('ALREADY_INSTRUMENTED');
expect(context.targetInstrumentations.length).to.equal(1);
expect(context.consoleDevModeTargetFunctions.length).to.equal(1);
});
it('Should be ineffective and cancel, when only one function exists and it is not included in the integration', async () => {
configureStep({
functionExistResponse: {
total: 0,
hits: [],
},
checkInstrumentationResponse: {
total: 0,
hits: [],
},
});
const context = {
isConsoleDevMode: true,
options: {},
org: {
orgId: fakeOrgId,
},
serverless: {
service: {
provider: {
region: fakeRegion,
},
setFunctionNames: () => {},
getAllFunctionsNames: () => expectedServiceFunctionNames,
},
},
};
expect(await step.isApplicable(context)).to.be.false;
expect(context.inapplicabilityReasonCode).to.equal('NO_FUNCTIONS_EXIST');
expect(context.targetInstrumentations).to.be.undefined;
expect(context.consoleDevModeTargetFunctions).to.be.undefined;
});
it('Should be effective and only update functions that were found in the integration', async () => {
// Add a function that is not in the integration to the serverless service
expectedServiceFunctionNames.push('function2');
// Set up the expected responses from the API
const functionExistResponse = {
total: expectedFunctionCount,
hits: expectedFunctionHits,
};
const checkInstrumentationResponse = {
total: 0,
hits: [],
};
configureStep({
functionExistResponse,
checkInstrumentationResponse,
});
const context = {
isConsoleDevMode: true,
options: {},
org: {
orgId: fakeOrgId,
},
serverless: {
service: {
provider: {
region: fakeRegion,
},
setFunctionNames: () => {},
getAllFunctionsNames: () => expectedServiceFunctionNames,
},
},
};
expect(await step.isApplicable(context)).to.be.true;
expect(context.targetInstrumentations.length).to.equal(1);
expect(context.consoleDevModeTargetFunctions.length).to.equal(1);
// Re-proxyquire step so we can update the response to the checkInstrumentation call
configureStep({
functionExistResponse,
checkInstrumentationResponse: {
total: expectedFunctionCount,
hits: expectedFunctionHits,
},
});
expect(await step.run(context)).to.be.true;
});
it('Should be effective and only target function from -f option', async () => {
const functionExistResponse = {
total: expectedFunctionCount,
hits: expectedFunctionHits,
};
const checkInstrumentationResponse = {
total: 0,
hits: [],
};
configureStep({
functionExistResponse,
checkInstrumentationResponse,
});
const context = {
isConsoleDevMode: true,
options: {
function: expectedServiceFunctionNames[0],
},
org: {
orgId: fakeOrgId,
},
serverless: {
service: {
provider: {
region: fakeRegion,
},
setFunctionNames: () => {},
getFunction: (name) => ({
name,
}),
getAllFunctionsNames: () => [],
},
},
};
expect(await step.isApplicable(context)).to.be.true;
expect(context.targetInstrumentations.length).to.equal(1);
expect(context.consoleDevModeTargetFunctions.length).to.equal(1);
// Re-proxyquire step so we can update the response to the checkInstrumentation call
configureStep({
functionExistResponse,
checkInstrumentationResponse: {
total: expectedFunctionCount,
hits: expectedFunctionHits,
},
});
expect(await step.run(context)).to.be.true;
});
it('Should be effective and update 50 functions at a time', async () => {
expectedFunctionHits = new Array(100)
.fill(0)
.map((_, i) => ({ aws_lambda_name: `function${i + 1}` }));
expectedFunctionCount = expectedFunctionHits.length;
expectedServiceFunctionNames = expectedFunctionHits.map((hit) => hit.aws_lambda_name);
const functionExistResponse = {
total: expectedFunctionCount,
hits: expectedFunctionHits,
};
const checkInstrumentationResponse = {
total: 0,
hits: [],
};
configureStep({
functionExistResponse,
checkInstrumentationResponse,
});
const context = {
isConsoleDevMode: true,
options: {},
org: {
orgId: fakeOrgId,
},
serverless: {
service: {
provider: {
region: fakeRegion,
},
setFunctionNames: () => {},
getFunction: () => ({}),
getAllFunctionsNames: () => expectedServiceFunctionNames,
},
},
};
expect(await step.isApplicable(context)).to.be.true;
expect(context.targetInstrumentations.length).to.equal(expectedFunctionCount);
expect(context.consoleDevModeTargetFunctions.length).to.equal(expectedFunctionCount);
// Re-proxyquire step so we can update the response to the checkInstrumentation call
configureStep({
functionExistResponse,
checkInstrumentationResponse: {
total: expectedFunctionCount,
hits: expectedFunctionHits,
},
});
expect(await step.run(context)).to.be.true;
});
});

View File

@ -53,18 +53,25 @@ describe('test/unit/lib/cli/interactive-setup/dashboard-login.test.js', function
loginStub.resetHistory();
});
it('Should be ineffective in console context', async () => {
const context = { isConsole: true, options: { console: true } };
expect(await step.isApplicable(context)).to.be.false;
expect(context.inapplicabilityReasonCode).to.equal('CONSOLE_CONTEXT');
});
it('Should be ineffective, when not at service path', async () => {
const context = { options: {} };
const context = { options: {}, isDashboard: true };
expect(await step.isApplicable(context)).to.be.false;
expect(context.inapplicabilityReasonCode).to.equal('NOT_IN_SERVICE_DIRECTORY');
});
it('Should be ineffective, when not in dashboard context', async () => {
const context = {
serviceDir: process.cwd(),
configuration: {},
configurationFilename: 'serverless.yml',
options: {},
initial: {},
inquirer,
};
expect(await step.isApplicable(context)).to.equal(false);
expect(context.inapplicabilityReasonCode).to.equal('CONSOLE_CONTEXT');
});
it('Should be ineffective, when not at AWS service path', async () => {
const context = {
serviceDir: process.cwd(),
@ -72,6 +79,7 @@ describe('test/unit/lib/cli/interactive-setup/dashboard-login.test.js', function
configurationFilename: 'serverless.yml',
options: {},
initial: {},
isDashboard: true,
inquirer,
};
expect(await step.isApplicable(context)).to.equal(false);
@ -85,6 +93,7 @@ describe('test/unit/lib/cli/interactive-setup/dashboard-login.test.js', function
configurationFilename: 'serverless.yml',
options: {},
initial: {},
isDashboard: true,
inquirer,
};
expect(await step.isApplicable(context)).to.equal(false);
@ -101,6 +110,7 @@ describe('test/unit/lib/cli/interactive-setup/dashboard-login.test.js', function
configurationFilename: 'serverless.yml',
options: {},
initial: {},
isDashboard: true,
inquirer,
};
expect(await overrideCwd(serviceDir, async () => await step.isApplicable(context))).to.equal(
@ -125,6 +135,7 @@ describe('test/unit/lib/cli/interactive-setup/dashboard-login.test.js', function
configurationFilename: 'serverless.yml',
options: {},
initial: {},
isDashboard: true,
inquirer,
stepHistory: new StepHistory(),
};
@ -148,6 +159,7 @@ describe('test/unit/lib/cli/interactive-setup/dashboard-login.test.js', function
configurationFilename: 'serverless.yml',
options: { org: 'someorg' },
initial: {},
isDashboard: true,
inquirer,
stepHistory: new StepHistory(),
};
@ -168,6 +180,7 @@ describe('test/unit/lib/cli/interactive-setup/dashboard-login.test.js', function
configurationFilename: 'serverless.yml',
options: {},
initial: {},
isDashboard: true,
inquirer,
stepHistory: new StepHistory(),
};
@ -191,6 +204,7 @@ describe('test/unit/lib/cli/interactive-setup/dashboard-login.test.js', function
configurationFilename: 'serverless.yml',
options: {},
initial: {},
isDashboard: true,
inquirer,
stepHistory: new StepHistory(),
};

View File

@ -116,21 +116,23 @@ describe('test/unit/lib/cli/interactive-setup/dashboard-set-org.test.js', functi
it('Should be ineffective, when not at service path', async () => {
const context = {
initial: {},
isDashboard: true,
};
expect(await step.isApplicable(context)).to.be.false;
expect(context.inapplicabilityReasonCode).to.equal('NOT_IN_SERVICE_DIRECTORY');
});
it('Should be ineffective, when in console context', async () => {
it('Should be ineffective, when not in dashboard context', async () => {
const context = {
initial: {},
serviceDir: process.cwd(),
configuration: {},
configurationFilename: 'serverless.yml',
options: { console: true },
isConsole: true,
options: {},
isDashboard: false,
isConsole: false,
};
expect(await step.isApplicable(context)).to.be.false;
expect(await step.isApplicable(context)).to.equal(false);
expect(context.inapplicabilityReasonCode).to.equal('CONSOLE_CONTEXT');
});
@ -141,6 +143,7 @@ describe('test/unit/lib/cli/interactive-setup/dashboard-set-org.test.js', functi
configurationFilename: 'serverless.yml',
options: {},
initial: {},
isDashboard: true,
inquirer,
};
expect(await step.isApplicable(context)).to.equal(false);
@ -154,6 +157,7 @@ describe('test/unit/lib/cli/interactive-setup/dashboard-set-org.test.js', functi
configurationFilename: 'serverless.yml',
options: {},
initial: {},
isDashboard: true,
inquirer,
};
expect(await step.isApplicable(context)).to.equal(false);
@ -170,6 +174,7 @@ describe('test/unit/lib/cli/interactive-setup/dashboard-set-org.test.js', functi
configurationFilename: 'serverless.yml',
options: {},
initial: {},
isDashboard: true,
inquirer,
};
expect(await step.isApplicable(context)).to.equal(false);
@ -208,6 +213,7 @@ describe('test/unit/lib/cli/interactive-setup/dashboard-set-org.test.js', functi
configurationFilename: 'serverless.yml',
options: {},
initial: {},
isDashboard: true,
inquirer,
};
await overrideCwd(serviceDir, async () => {
@ -226,6 +232,7 @@ describe('test/unit/lib/cli/interactive-setup/dashboard-set-org.test.js', functi
configurationFilename: 'serverless.yml',
options: {},
initial: {},
isDashboard: true,
inquirer,
};
await overrideCwd(serviceDir, async () => {
@ -251,6 +258,7 @@ describe('test/unit/lib/cli/interactive-setup/dashboard-set-org.test.js', functi
configurationFilename: 'serverless.yml',
options: {},
initial: {},
isDashboard: true,
inquirer,
stepHistory: new StepHistory(),
};
@ -283,6 +291,7 @@ describe('test/unit/lib/cli/interactive-setup/dashboard-set-org.test.js', functi
configurationFilename: 'serverless.yml',
options: {},
initial: {},
isDashboard: true,
inquirer,
stepHistory: new StepHistory(),
};
@ -309,6 +318,7 @@ describe('test/unit/lib/cli/interactive-setup/dashboard-set-org.test.js', functi
configurationFilename: 'serverless.yml',
options: {},
initial: {},
isDashboard: true,
inquirer,
stepHistory: new StepHistory(),
};
@ -336,6 +346,7 @@ describe('test/unit/lib/cli/interactive-setup/dashboard-set-org.test.js', functi
configurationFilename: 'serverless.yml',
options: {},
initial: {},
isDashboard: true,
inquirer,
stepHistory: new StepHistory(),
};
@ -373,6 +384,7 @@ describe('test/unit/lib/cli/interactive-setup/dashboard-set-org.test.js', functi
configurationFilename: 'serverless.yml',
options: {},
initial: {},
isDashboard: true,
inquirer,
stepHistory: new StepHistory(),
};
@ -411,6 +423,7 @@ describe('test/unit/lib/cli/interactive-setup/dashboard-set-org.test.js', functi
configurationFilename: 'serverless.yml',
options: {},
initial: {},
isDashboard: true,
inquirer,
stepHistory: new StepHistory(),
};
@ -444,6 +457,7 @@ describe('test/unit/lib/cli/interactive-setup/dashboard-set-org.test.js', functi
configurationFilename: 'serverless.yml',
options: {},
initial: {},
isDashboard: true,
inquirer,
stepHistory: new StepHistory(),
};
@ -489,6 +503,7 @@ describe('test/unit/lib/cli/interactive-setup/dashboard-set-org.test.js', functi
configurationFilename: 'serverless.yml',
options: {},
initial: {},
isDashboard: true,
inquirer,
stepHistory: new StepHistory(),
};
@ -520,6 +535,7 @@ describe('test/unit/lib/cli/interactive-setup/dashboard-set-org.test.js', functi
configurationFilename: 'serverless.yml',
options: {},
initial: {},
isDashboard: true,
inquirer,
stepHistory: new StepHistory(),
};
@ -551,6 +567,7 @@ describe('test/unit/lib/cli/interactive-setup/dashboard-set-org.test.js', functi
configurationFilename: 'serverless.yml',
options: {},
initial: {},
isDashboard: true,
inquirer,
history: new Map([['dashboardLogin', []]]),
stepHistory: new StepHistory(),
@ -580,6 +597,7 @@ describe('test/unit/lib/cli/interactive-setup/dashboard-set-org.test.js', functi
configurationFilename: 'serverless.yml',
options: {},
initial: {},
isDashboard: true,
inquirer,
history: new Map([
['dashboardLogin', []],
@ -615,6 +633,7 @@ describe('test/unit/lib/cli/interactive-setup/dashboard-set-org.test.js', functi
configurationFilename: 'serverless.yml',
options: { org: 'testinteractivecli', app: 'other-app' },
initial: {},
isDashboard: true,
inquirer,
history: new Map(),
stepHistory: new StepHistory(),
@ -648,6 +667,7 @@ describe('test/unit/lib/cli/interactive-setup/dashboard-set-org.test.js', functi
configurationFilename: 'serverless.yml',
options: { org: 'otherorg', app: 'app-from-flag' },
initial: {},
isDashboard: true,
inquirer,
history: new Map(),
stepHistory: new StepHistory(),
@ -684,6 +704,7 @@ describe('test/unit/lib/cli/interactive-setup/dashboard-set-org.test.js', functi
configurationFilename: 'serverless.yml',
options: { org: 'otherorg', app: 'app-from-flag' },
initial: {},
isDashboard: true,
inquirer,
history: new Map(),
stepHistory: new StepHistory(),
@ -714,6 +735,7 @@ describe('test/unit/lib/cli/interactive-setup/dashboard-set-org.test.js', functi
configurationFilename: 'serverless.yml',
options: { org: 'invalid-testinteractivecli', app: 'irrelevant' },
initial: {},
isDashboard: true,
inquirer,
history: new Map(),
stepHistory: new StepHistory(),
@ -755,6 +777,7 @@ describe('test/unit/lib/cli/interactive-setup/dashboard-set-org.test.js', functi
configurationFilename: 'serverless.yml',
options: { org: 'invalid-testinteractivecli', app: 'irrelevant' },
initial: {},
isDashboard: true,
inquirer,
history: new Map(),
stepHistory: new StepHistory(),
@ -796,6 +819,7 @@ describe('test/unit/lib/cli/interactive-setup/dashboard-set-org.test.js', functi
configurationFilename: 'serverless.yml',
options: { org: 'invalid-testinteractivecli', app: 'irrelevant' },
initial: {},
isDashboard: true,
inquirer,
history: new Map(),
stepHistory: new StepHistory(),
@ -834,6 +858,7 @@ describe('test/unit/lib/cli/interactive-setup/dashboard-set-org.test.js', functi
configurationFilename: 'serverless.yml',
options: { org: 'testinteractivecli', app: 'invalid' },
initial: {},
isDashboard: true,
inquirer,
history: new Map(),
stepHistory: new StepHistory(),
@ -872,6 +897,7 @@ describe('test/unit/lib/cli/interactive-setup/dashboard-set-org.test.js', functi
inquirer,
options: {},
initial: {},
isDashboard: true,
history: new Map(),
stepHistory: new StepHistory(),
};
@ -914,6 +940,7 @@ describe('test/unit/lib/cli/interactive-setup/dashboard-set-org.test.js', functi
inquirer,
options: {},
initial: {},
isDashboard: true,
history: new Map(),
stepHistory: new StepHistory(),
};
@ -954,6 +981,7 @@ describe('test/unit/lib/cli/interactive-setup/dashboard-set-org.test.js', functi
inquirer,
options: {},
initial: {},
isDashboard: true,
history: new Map(),
stepHistory: new StepHistory(),
};
@ -988,6 +1016,7 @@ describe('test/unit/lib/cli/interactive-setup/dashboard-set-org.test.js', functi
inquirer,
options: { app: 'app-from-flag' },
initial: {},
isDashboard: true,
history: new Map(),
stepHistory: new StepHistory(),
};
@ -1019,6 +1048,7 @@ describe('test/unit/lib/cli/interactive-setup/dashboard-set-org.test.js', functi
inquirer,
options: {},
initial: {},
isDashboard: true,
history: new Map(),
stepHistory: new StepHistory(),
};
@ -1052,6 +1082,7 @@ describe('test/unit/lib/cli/interactive-setup/dashboard-set-org.test.js', functi
inquirer,
options: { app: 'invalid-app-from-flag' },
initial: {},
isDashboard: true,
history: new Map(),
stepHistory: new StepHistory(),
};
@ -1095,6 +1126,7 @@ describe('test/unit/lib/cli/interactive-setup/dashboard-set-org.test.js', functi
inquirer,
options: {},
initial: {},
isDashboard: true,
history: new Map(),
stepHistory: new StepHistory(),
};
@ -1136,6 +1168,7 @@ describe('test/unit/lib/cli/interactive-setup/dashboard-set-org.test.js', functi
inquirer,
options: {},
initial: {},
isDashboard: true,
history: new Map(),
stepHistory: new StepHistory(),
};

View File

@ -29,12 +29,40 @@ describe('test/unit/lib/cli/interactive-setup/deploy.test.js', () => {
configuration: { provider: { name: 'notaws' } },
serviceDir: '/foo',
options: {},
isOnboarding: true,
history: new Map([['service', []]]),
};
expect(await step.isApplicable(context)).to.equal(false);
expect(context.inapplicabilityReasonCode).to.equal('NON_AWS_PROVIDER');
});
it('Should be not applied, when service is not in onboarding context', async () => {
const context = {
configuration: { provider: { name: 'aws' } },
serviceDir: '/foo',
options: {},
isOnboarding: false,
history: new Map([['awsCredentials', []]]),
initial: { isInServiceContext: true },
};
expect(await step.isApplicable(context)).to.equal(false);
expect(context.inapplicabilityReasonCode).to.equal('CONSOLE_INTEGRATION');
});
it('Should be not applied, when in console context', async () => {
const context = {
configuration: { provider: { name: 'aws' } },
serviceDir: '/foo',
options: {},
isOnboarding: true,
isConsole: true,
history: new Map([['awsCredentials', []]]),
initial: { isInServiceContext: true },
};
expect(await step.isApplicable(context)).to.equal(false);
expect(context.inapplicabilityReasonCode).to.equal('CONSOLE_INTEGRATION');
});
it('Should be applied if user configured local credentials', async () => {
await overrideEnv(
{ variables: { AWS_ACCESS_KEY_ID: 'somekey', AWS_SECRET_ACCESS_KEY: 'somesecret' } },
@ -44,6 +72,7 @@ describe('test/unit/lib/cli/interactive-setup/deploy.test.js', () => {
configuration: { provider: { name: 'aws' } },
serviceDir: '/foo',
options: {},
isOnboarding: true,
history: new Map([['awsCredentials', []]]),
})
).to.equal(true);
@ -64,6 +93,7 @@ describe('test/unit/lib/cli/interactive-setup/deploy.test.js', () => {
configuration: { provider: { name: 'aws' }, org: 'someorg', app: 'someapp' },
serviceDir: '/foo',
options: {},
isOnboarding: true,
history: new Map([['awsCredentials', []]]),
})
).to.equal(true);
@ -87,6 +117,7 @@ describe('test/unit/lib/cli/interactive-setup/deploy.test.js', () => {
},
serviceDir: '/foo',
options: {},
isOnboarding: true,
history: new Map([['awsCredentials', []]]),
})
).to.equal(true);
@ -110,6 +141,7 @@ describe('test/unit/lib/cli/interactive-setup/deploy.test.js', () => {
initial: {
isInServiceContext: false,
},
isOnboarding: true,
};
await step.run(context);
@ -133,6 +165,7 @@ describe('test/unit/lib/cli/interactive-setup/deploy.test.js', () => {
initial: {
isInServiceContext: true,
},
isOnboarding: true,
};
await step.run(context);
@ -158,6 +191,7 @@ describe('test/unit/lib/cli/interactive-setup/deploy.test.js', () => {
initial: {
isInServiceContext: false,
},
isOnboarding: true,
};
await step.run(context);
@ -183,6 +217,7 @@ describe('test/unit/lib/cli/interactive-setup/deploy.test.js', () => {
initial: {
isInServiceContext: true,
},
isOnboarding: true,
};
await step.run(context);
@ -236,6 +271,7 @@ describe('test/unit/lib/cli/interactive-setup/deploy.test.js', () => {
initial: {
isInServiceContext: false,
},
isOnboarding: true,
};
await mockedStep.run(context);
@ -289,6 +325,7 @@ describe('test/unit/lib/cli/interactive-setup/deploy.test.js', () => {
initial: {
isInServiceContext: true,
},
isOnboarding: true,
};
await mockedStep.run(context);
@ -336,6 +373,7 @@ describe('test/unit/lib/cli/interactive-setup/deploy.test.js', () => {
initial: {
isInServiceContext: false,
},
isOnboarding: true,
};
await mockedStep.run(context);
@ -383,6 +421,7 @@ describe('test/unit/lib/cli/interactive-setup/deploy.test.js', () => {
initial: {
isInServiceContext: true,
},
isOnboarding: true,
};
await mockedStep.run(context);