Skip to content

Commit

Permalink
feat(Console): External Console integration
Browse files Browse the repository at this point in the history
  • Loading branch information
medikoo committed Oct 27, 2022
1 parent 2a15971 commit 9eb8a7e
Show file tree
Hide file tree
Showing 22 changed files with 878 additions and 115 deletions.
34 changes: 4 additions & 30 deletions lib/cli/interactive-setup/console-login.js
@@ -1,20 +1,13 @@
'use strict';

const _ = require('lodash');
const resolveAuthMode = require('@serverless/utils/auth/resolve-mode');
const promptWithHistory = require('@serverless/utils/inquirer/prompt-with-history');
const login = require('../../commands/login/console');
const { showOnboardingWelcome } = require('./utils');

const loginOrRegisterQuestion = async (context) => {
let message;
if (context.initial.isInServiceContext) {
message = 'Press [Enter] to create a free Serverless Console account';
} else {
message = 'Do you want to login/register to Serverless Console?';
}
return promptWithHistory({
message,
message: 'Press [Enter] to login to Serverless Console.',
type: 'confirm',
name: 'shouldLoginOrRegister',
stepHistory: context.stepHistory,
Expand All @@ -23,48 +16,29 @@ const loginOrRegisterQuestion = async (context) => {

const steps = {
loginOrRegister: async (context) => {
const shouldLoginOrRegister =
context.options.org || context.configuration.org || (await loginOrRegisterQuestion(context));
const shouldLoginOrRegister = await loginOrRegisterQuestion(context);
if (shouldLoginOrRegister) await login({ clientOriginCommand: 'onboarding' });
},
};

module.exports = {
async isApplicable(context) {
const { isConsole, configuration, serviceDir } = context;
const { isConsole } = context;

if (!isConsole) {
context.inapplicabilityReasonCode = 'NON_CONSOLE_CONTEXT';
return false;
}

if (!serviceDir) {
context.inapplicabilityReasonCode = 'NOT_IN_SERVICE_DIRECTORY';
return false;
}

if (await resolveAuthMode()) {
context.inapplicabilityReasonCode = 'ALREADY_LOGGED_IN';
return false;
}

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

const runtime = _.get(configuration.provider, 'runtime') || 'nodejs14.x';
if (!runtime.startsWith('nodejs')) {
context.inapplicabilityReasonCode = 'UNSUPPORTED_RUNTIME';
return false;
}
return true;
},
async run(context) {
if (context.initial.isInServiceContext) showOnboardingWelcome(context);
showOnboardingWelcome(context);

return steps.loginOrRegister(context);
},
Expand Down
85 changes: 85 additions & 0 deletions lib/cli/interactive-setup/console-resolve-org.js
@@ -0,0 +1,85 @@
'use strict';

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?',
type: 'list',
name: 'orgName',
choices: [
...orgs.map((org) => ({ name: org.orgName, value: org })),
{ name: '[Skip]', value: '_skip_' },
],
stepHistory,
});

const resolveOrgs = async () => {
const { userId } = await apiRequest('/api/identity/me');
return (await apiRequest(`/api/identity/users/${userId}/orgs`)).orgs;
};

module.exports = {
async isApplicable(context) {
const { options, isConsole } = context;

if (!isConsole) {
context.inapplicabilityReasonCode = 'NON_CONSOLE_CONTEXT';
return false;
}

if (!(await resolveAuthMode())) {
context.inapplicabilityReasonCode = 'NOT_LOGGED_IN';
return false;
}

const orgs = await resolveOrgs();

const orgName = options.org;
if (!orgs.length) {
context.inapplicabilityReasonCode = 'NO_ORGS_AVAILABLE';
return false;
}

log.notice();
showOnboardingWelcome(context);

if (orgName) {
const org = orgs.find((someOrg) => someOrg.orgName === orgName);
if (org) {
context.org = org;
context.inapplicabilityReasonCode = 'RESOLVED_FROM_OPTIONS';
return false;
}

log.error(
'Passed value for "--org" doesn\'t seem to correspond to account with which ' +
"you're logged in with. Please choose applicable org"
);

return { orgs, isOrgMismatch: true };
} else if (orgs.length === 1) {
context.org = orgs[0];
context.inapplicabilityReasonCode = 'ONLY_ORG';
return false;
}
return { orgs };
},
async run(context, stepData) {
const { stepHistory } = context;

const org = await orgsChoice(stepData.orgs, stepHistory);

if (org === '_skip_') {
log.error('Console integraton aborted');
context.isConsole = false;
return;
}
context.org = org;
},
configuredQuestions: ['orgName'],
};
178 changes: 178 additions & 0 deletions lib/cli/interactive-setup/console-setup-iam-role.js
@@ -0,0 +1,178 @@
'use strict';

const wait = require('timers-ext/promise/sleep');
const { log, style, progress } = 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 { awsRequest } = require('./utils');

const iamRoleStackName = 'Serverless-Inc-Role-Stack';
const cloudFormationServiceConfig = { name: 'CloudFormation', params: { region: 'us-east-1' } };

const waitUntilStackIsCreated = async (context) => {
await wait(2000);
const stackEvents = (
await awsRequest(context, cloudFormationServiceConfig, 'describeStackEvents', {
StackName: iamRoleStackName,
})
).StackEvents;
const failedStatusReasons = stackEvents
.filter(({ ResourceStatus: status }) => {
return status && status.endsWith('_FAILED');
})
.map(({ ResourceStatusReason: reason }) => reason);

if (failedStatusReasons.length) {
log.error(`Creating IAM Role failed:\n - ${failedStatusReasons.join('\n - ')}`);
return false;
}
const statusEvent = stackEvents.find(
({ ResourceType: resourceType }) => resourceType === 'AWS::CloudFormation::Stack'
);
const status = statusEvent ? statusEvent.ResourceStatus : null;
if (status && status.endsWith('_COMPLETE')) {
if (status === 'CREATE_COMPLETE') return true;
log.error('Creating IAM Role failed');
return false;
}
return waitUntilStackIsCreated(context);
};

const waitUntilIntegrationIsReady = async (context) => {
await wait(2000);
const { integrations } = await apiRequest(`/api/integrations/?orgId=${context.org.orgId}`, {
urlName: 'integrationsBackend',
});
const integration = integrations.find(
({ vendorAccount }) => vendorAccount === context.awsAccountId
);
if (integration && integration.status === 'alive' && integration.syncStatus !== 'pending') return;
await waitUntilIntegrationIsReady(context);
return;
};

module.exports = {
async isApplicable(context) {
const { isConsole } = context;

if (!isConsole) {
context.inapplicabilityReasonCode = 'NON_CONSOLE_CONTEXT';
return false;
}

if (!(await resolveAuthMode())) {
context.inapplicabilityReasonCode = 'NOT_LOGGED_IN';
return false;
}

if (!context.org) {
context.inapplicabilityReasonCode = 'UNRESOLVED_ORG';
return false;
}

const { integrations } = await apiRequest(`/api/integrations/?orgId=${context.org.orgId}`, {
urlName: 'integrationsBackend',
});

const integration = integrations.find(
({ vendorAccount }) => vendorAccount === context.awsAccountId
);

if (integration) {
if (integration.status !== 'alive' || integration.syncStatus === 'pending') {
const integrationSetupProgress = progress.get('integration-setup');
try {
integrationSetupProgress.notice('Setting up Serverless Console Integration');
await waitUntilIntegrationIsReady(context);
} finally {
integrationSetupProgress.remove();
}
}

log.notice();
log.notice.success('Your AWS account is integrated with Serverless Console');
context.inapplicabilityReasonCode = 'INTEGRATED';
return false;
}

try {
await awsRequest(context, cloudFormationServiceConfig, 'describeStacks', {
StackName: iamRoleStackName,
});
log.warning(
'Cannot integrate with Serverless Console: ' +
'AWS account seems already integrated with other org'
);
context.inapplicabilityReasonCode = 'AWS_ACCOUNT_ALREADY_INTEGRATED';
return false;
} catch (error) {
if (error.Code === 'ValidationError') return true;
if (error.providerErrorCodeExtension === 'VALIDATION_ERROR') return true;
throw error;
}
},

async run(context) {
const { stepHistory } = 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',
`See the IAM Permissions transparently here: ${style.link(
'https://slss.io/iam-role-permissions'
)}`,
]
)}`,
type: 'confirm',
name: 'shouldSetupConsoleIamRole',
stepHistory,
}))
) {
return false;
}

log.notice();

const integrationSetupProgress = progress.get('integration-setup');
integrationSetupProgress.notice('Creating IAM Role for Serverless Console');

try {
const { cfnTemplateUrl, params } = await apiRequest(
`/api/integrations/aws/initial?orgId=${context.org.orgId}`,
{ urlName: 'integrationsBackend' }
);

await awsRequest(context, cloudFormationServiceConfig, 'createStack', {
Capabilities: ['CAPABILITY_NAMED_IAM'],
StackName: iamRoleStackName,
TemplateURL: cfnTemplateUrl,
Parameters: [
{ ParameterKey: 'AccountId', ParameterValue: params.accountId },
{ ParameterKey: 'ReportServiceToken', ParameterValue: params.reportServiceToken },
{ ParameterKey: 'ExternalId', ParameterValue: params.externalId },
{ ParameterKey: 'Version', ParameterValue: params.version },
],
});

if (!(await waitUntilStackIsCreated(context))) return false;

integrationSetupProgress.notice(
'Setting up Serverless Console Integration (this may take 5-10 minutes)'
);

await waitUntilIntegrationIsReady(context);

log.notice.success('Your AWS account is integrated with Serverless Console');
} finally {
integrationSetupProgress.remove();
}
return true;
},
configuredQuestions: ['shouldSetupConsoleIamRole'],
};
4 changes: 2 additions & 2 deletions lib/cli/interactive-setup/dashboard-login.js
Expand Up @@ -25,9 +25,9 @@ const steps = {

module.exports = {
async isApplicable(context) {
const { isConsole, configuration, options, serviceDir } = context;
const { configuration, options, serviceDir } = context;

if (isConsole) {
if (options.console) {
context.inapplicabilityReasonCode = 'CONSOLE_CONTEXT';
return false;
}
Expand Down
4 changes: 2 additions & 2 deletions lib/cli/interactive-setup/dashboard-set-org.js
Expand Up @@ -193,14 +193,14 @@ const steps = {

module.exports = {
async isApplicable(context) {
const { configuration, options, serviceDir, isConsole } = context;
const { configuration, options, serviceDir } = context;

if (!serviceDir) {
context.inapplicabilityReasonCode = 'NOT_IN_SERVICE_DIRECTORY';
return false;
}

if (isConsole) {
if (options.console) {
context.inapplicabilityReasonCode = 'CONSOLE_CONTEXT';
return false;
}
Expand Down
7 changes: 6 additions & 1 deletion lib/cli/interactive-setup/deploy.js
Expand Up @@ -23,12 +23,17 @@ const printMessage = () => {

module.exports = {
async isApplicable(context) {
const { configuration, serviceDir, options } = context;
const { configuration, serviceDir, options, initial } = context;
if (!serviceDir) {
context.inapplicabilityReasonCode = 'NOT_IN_SERVICE_DIRECTORY';
return false;
}

if (options.console && initial.isInServiceContext) {
context.inapplicabilityReasonCode = 'CONSOLE_INTEGRATION';
return false;
}

if (
_.get(configuration, 'provider') !== 'aws' &&
_.get(configuration, 'provider.name') !== 'aws'
Expand Down

0 comments on commit 9eb8a7e

Please sign in to comment.