Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(Console): External Console integration
- Loading branch information
Showing
20 changed files
with
564 additions
and
99 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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'], | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,143 @@ | ||
'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 = `ServerlessIncIAMRole${ | ||
process.env.SERVERLESS_PLATFORM_STAGE === 'dev' ? 'Dev' : '' | ||
}`; | ||
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}-${context.org.orgName}`, | ||
}) | ||
).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 status = stackEvents.find( | ||
({ ResourceType: resourceType }) => resourceType === 'AWS::CloudFormation::Stack' | ||
).ResourceStatus; | ||
if (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', | ||
}); | ||
if (integrations.some(({ vendorAccount }) => vendorAccount === context.awsAccountId)) return true; | ||
return waitUntilIntegrationIsReady(context); | ||
}; | ||
|
||
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', | ||
}); | ||
|
||
if (integrations.some(({ vendorAccount }) => vendorAccount === context.awsAccountId)) { | ||
log.notice(); | ||
log.notice.success('Your service is configured with Serverless Console'); | ||
context.inapplicabilityReasonCode = 'INTEGRATED'; | ||
return false; | ||
} | ||
return true; | ||
}, | ||
|
||
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: 'shouldEnableConsoleMonitoring', | ||
stepHistory, | ||
})) | ||
) { | ||
return; | ||
} | ||
|
||
log.notice(); | ||
|
||
const iamRoleCreationProgress = progress.get('iam-role-creation'); | ||
iamRoleCreationProgress.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}-${context.org.orgName}`, | ||
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; | ||
|
||
iamRoleCreationProgress.notice('Enabling Serverless Console Integration'); | ||
|
||
await waitUntilIntegrationIsReady(context); | ||
|
||
log.notice.success('Your service is configured with Serverless Console'); | ||
} finally { | ||
iamRoleCreationProgress.remove(); | ||
} | ||
}, | ||
configuredQuestions: ['shouldEnableConsoleMonitoring'], | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.