From fbba60c69f33a6ceff4f4e5926718c5289c2866d Mon Sep 17 00:00:00 2001 From: Piotr Grzesik Date: Wed, 22 Dec 2021 10:06:20 +0100 Subject: [PATCH] feat(Telemetry): Report `newStackCreatedDuringDeploy` property --- lib/cli/interactive-setup/deploy.js | 4 + lib/cli/interactive-setup/index.js | 5 +- lib/plugins/aws/deploy/lib/createStack.js | 1 + .../aws/lib/get-create-change-set-params.js | 80 +++++++++++++++++++ lib/plugins/aws/lib/updateStack.js | 1 + lib/utils/telemetry/generatePayload.js | 19 ++++- scripts/serverless.js | 10 +-- .../utils/telemetry/generatePayload.test.js | 47 ++++++++++- 8 files changed, 153 insertions(+), 14 deletions(-) create mode 100644 lib/plugins/aws/lib/get-create-change-set-params.js diff --git a/lib/cli/interactive-setup/deploy.js b/lib/cli/interactive-setup/deploy.js index 8f9c01123482..2db2f1e922fe 100644 --- a/lib/cli/interactive-setup/deploy.js +++ b/lib/cli/interactive-setup/deploy.js @@ -185,6 +185,8 @@ module.exports = { delete serverless.isLocallyInstalled; await serverless.run(); context.awsAccountId = serverless.getProvider('aws').accountId; + context.newStackCreatedDuringDeploy = + serverless.getProvider('aws').newStackCreatedDuringDeploy; } else { try { await overrideStdoutWrite( @@ -200,6 +202,8 @@ module.exports = { delete serverless.isLocallyInstalled; await serverless.run(); context.awsAccountId = serverless.getProvider('aws').accountId; + context.newStackCreatedDuringDeploy = + serverless.getProvider('aws').newStackCreatedDuringDeploy; } ); } catch (err) { diff --git a/lib/cli/interactive-setup/index.js b/lib/cli/interactive-setup/index.js index aeadd596b631..2c12edcda863 100644 --- a/lib/cli/interactive-setup/index.js +++ b/lib/cli/interactive-setup/index.js @@ -57,8 +57,5 @@ module.exports = async (context) => { } } - return { - configuration: context.configuration, - awsAccountId: context.awsAccountId, - }; + return context; }; diff --git a/lib/plugins/aws/deploy/lib/createStack.js b/lib/plugins/aws/deploy/lib/createStack.js index fd16257e487d..8aac5f1005f0 100644 --- a/lib/plugins/aws/deploy/lib/createStack.js +++ b/lib/plugins/aws/deploy/lib/createStack.js @@ -58,6 +58,7 @@ module.exports = { params.DisableRollback = this.serverless.service.provider.disableRollback; } + this.provider.newStackCreatedDuringDeploy = true; return this.provider .request('CloudFormation', 'createStack', params) .then((cfData) => this.monitorStack('create', cfData)); diff --git a/lib/plugins/aws/lib/get-create-change-set-params.js b/lib/plugins/aws/lib/get-create-change-set-params.js new file mode 100644 index 000000000000..4d78461dad78 --- /dev/null +++ b/lib/plugins/aws/lib/get-create-change-set-params.js @@ -0,0 +1,80 @@ +'use strict'; + +module.exports = { + getCreateChangeSetParams({ changeSetType, templateUrl, templateBody }) { + let stackTags = { STAGE: this.provider.getStage() }; + const stackName = this.provider.naming.getStackName(); + const changeSetName = this.provider.naming.getStackChangeSetName(); + + // Merge additional stack tags + if (this.serverless.service.provider.stackTags) { + const customKeys = Object.keys(this.serverless.service.provider.stackTags); + const collisions = Object.keys(stackTags).filter((defaultKey) => + customKeys.some((key) => defaultKey.toLowerCase() === key.toLowerCase()) + ); + + // Delete collisions upfront + for (const key of collisions) { + delete stackTags[key]; + } + + stackTags = Object.assign(stackTags, this.serverless.service.provider.stackTags); + } + + const createChangeSetParams = { + StackName: stackName, + ChangeSetName: changeSetName, + ChangeSetType: changeSetType, + Capabilities: ['CAPABILITY_IAM', 'CAPABILITY_NAMED_IAM'], + Parameters: [], + Tags: Object.keys(stackTags).map((key) => ({ Key: key, Value: stackTags[key] })), + }; + + if (templateUrl) { + createChangeSetParams.TemplateURL = templateUrl; + } + + if (templateBody) { + createChangeSetParams.TemplateBody = JSON.stringify(templateBody); + } + + if ( + (templateUrl && + this.serverless.service.provider.compiledCloudFormationTemplate && + this.serverless.service.provider.compiledCloudFormationTemplate.Transform) || + (templateBody && templateBody.Transform) + ) { + createChangeSetParams.Capabilities.push('CAPABILITY_AUTO_EXPAND'); + } + + const customDeploymentRole = this.provider.getCustomDeploymentRole(); + if (customDeploymentRole) { + createChangeSetParams.RoleARN = customDeploymentRole; + } + + if (this.serverless.service.provider.notificationArns) { + createChangeSetParams.NotificationARNs = this.serverless.service.provider.notificationArns; + } + + if (this.serverless.service.provider.stackParameters) { + createChangeSetParams.Parameters = this.serverless.service.provider.stackParameters; + } + + // Policy must have at least one statement, otherwise no updates would be possible at all + if ( + this.serverless.service.provider.stackPolicy && + Object.keys(this.serverless.service.provider.stackPolicy).length + ) { + createChangeSetParams.StackPolicyBody = JSON.stringify({ + Statement: this.serverless.service.provider.stackPolicy, + }); + } + + if (this.serverless.service.provider.rollbackConfiguration) { + createChangeSetParams.RollbackConfiguration = + this.serverless.service.provider.rollbackConfiguration; + } + + return createChangeSetParams; + }, +}; diff --git a/lib/plugins/aws/lib/updateStack.js b/lib/plugins/aws/lib/updateStack.js index 1313705e092a..d0a206caea5b 100644 --- a/lib/plugins/aws/lib/updateStack.js +++ b/lib/plugins/aws/lib/updateStack.js @@ -67,6 +67,7 @@ module.exports = { params.DisableRollback = this.serverless.service.provider.disableRollback; } + this.provider.newStackCreatedDuringDeploy = true; return this.provider .request('CloudFormation', 'createStack', params) .then((cfData) => this.monitorStack('create', cfData)); diff --git a/lib/utils/telemetry/generatePayload.js b/lib/utils/telemetry/generatePayload.js index cfb6bb1c2793..d27886555d20 100644 --- a/lib/utils/telemetry/generatePayload.js +++ b/lib/utils/telemetry/generatePayload.js @@ -138,7 +138,7 @@ module.exports = ({ serverless, commandUsage, variableSources, - awsAccountId, + interactiveContext, }) => { let commandDurationMs; @@ -303,12 +303,14 @@ module.exports = ({ if ( isAwsProvider && - ((serverless && command === 'deploy') || (command === '' && awsAccountId)) + ((serverless && command === 'deploy') || + (command === '' && interactiveContext && interactiveContext.awsAccountId)) ) { const serviceName = isObject(configuration.service) ? configuration.service.name : configuration.service; - const accountId = awsAccountId || (serverless && serverless.getProvider('aws').accountId); + const accountId = + interactiveContext.awsAccountId || (serverless && serverless.getProvider('aws').accountId); if (serviceName && accountId) { payload.projectId = crypto .createHash('sha256') @@ -316,6 +318,17 @@ module.exports = ({ .digest('base64'); } } + + if ( + isAwsProvider && + ((serverless && command === 'deploy') || + (command === '' && interactiveContext && interactiveContext.awsAccountId)) + ) { + payload.newStackCreatedDuringDeploy = Boolean( + interactiveContext.newStackCreatedDuringDeploy || + (serverless && serverless.getProvider('aws').newStackCreatedDuringDeploy) + ); + } } if (commandUsage) { diff --git a/scripts/serverless.js b/scripts/serverless.js index ebae14697f2e..9eec0d07e720 100755 --- a/scripts/serverless.js +++ b/scripts/serverless.js @@ -514,7 +514,7 @@ const processSpanPromise = (async () => { const isStandaloneCommand = notIntegratedCommands.has(command); if (!isHelpRequest && (isInteractiveSetup || isStandaloneCommand)) { - let interactiveResult; + let interactiveContext; if (configuration) require('../lib/cli/ensure-supported-command')(configuration); if (isInteractiveSetup) { if (!process.stdin.isTTY && !process.env.SLS_INTERACTIVE_SETUP_ENABLE) { @@ -524,15 +524,15 @@ const processSpanPromise = (async () => { 'INTERACTIVE_SETUP_IN_NON_TTY' ); } - interactiveResult = await require('../lib/cli/interactive-setup')({ + interactiveContext = await require('../lib/cli/interactive-setup')({ configuration, serviceDir, configurationFilename, options, commandUsage, }); - if (interactiveResult.configuration) { - configuration = interactiveResult.configuration; + if (interactiveContext.configuration) { + configuration = interactiveContext.configuration; } } else { await require(`../commands/${commands.join('-')}`)({ @@ -559,7 +559,7 @@ const processSpanPromise = (async () => { configuration, commandUsage, variableSources: variableSourcesInConfig, - awsAccountId: interactiveResult.awsAccountId, + interactiveContext, }), outcome: 'success', }); diff --git a/test/unit/lib/utils/telemetry/generatePayload.test.js b/test/unit/lib/utils/telemetry/generatePayload.test.js index 3e5c7dd9cc41..a64cba7dfeed 100644 --- a/test/unit/lib/utils/telemetry/generatePayload.test.js +++ b/test/unit/lib/utils/telemetry/generatePayload.test.js @@ -681,7 +681,7 @@ describe('test/unit/lib/utils/telemetry/generatePayload.test.js', () => { expect(payload.projectId).to.deep.equal('35dsFwCaexwLHppAP4uDsjKW4ci54q1AKcN5JTNaDtw='); }); - it('Should correctly resolve projectId property when account passed externally', async () => { + it('Should correctly resolve projectId property when account passed via interactiveContext', async () => { const { serverless } = await runServerless({ fixture: 'httpApi', command: 'print', @@ -695,9 +695,52 @@ describe('test/unit/lib/utils/telemetry/generatePayload.test.js', () => { commandSchema: commandsSchema.get('deploy'), serviceDir: serverless.serviceDir, configuration: serverless.configurationInput, - awsAccountId: '1234567890', + interactiveContext: { awsAccountId: '1234567890' }, }); expect(payload.projectId).to.deep.equal('35dsFwCaexwLHppAP4uDsjKW4ci54q1AKcN5JTNaDtw='); }); + + it('Should correctly resolve `newStackCreatedDuringDeploy` property', async () => { + const { serverless } = await runServerless({ + fixture: 'httpApi', + command: 'print', + configExt: { + service: 'to-ensure-unique-serivce-name', + }, + }); + serverless.getProvider('aws').newStackCreatedDuringDeploy = true; + const payload = generatePayload({ + command: 'deploy', + options: {}, + commandSchema: commandsSchema.get('deploy'), + serviceDir: serverless.serviceDir, + configuration: serverless.configurationInput, + serverless, + }); + + expect(payload.newStackCreatedDuringDeploy).to.be.true; + }); + + it('Should correctly resolve `newStackCreatedDuringDeploy` property when passed via interactiveContext', async () => { + const { serverless } = await runServerless({ + fixture: 'httpApi', + command: 'print', + configExt: { + service: 'to-ensure-unique-serivce-name', + }, + }); + const payload = generatePayload({ + command: 'deploy', + options: {}, + commandSchema: commandsSchema.get('deploy'), + serviceDir: serverless.serviceDir, + configuration: serverless.configurationInput, + interactiveContext: { + newStackCreatedDuringDeploy: true, + }, + }); + + expect(payload.newStackCreatedDuringDeploy).to.be.true; + }); });