diff --git a/lib/plugins/aws/deploy/index.js b/lib/plugins/aws/deploy/index.js index 981fa55eb012..74a3c3cdefc7 100644 --- a/lib/plugins/aws/deploy/index.js +++ b/lib/plugins/aws/deploy/index.js @@ -80,6 +80,13 @@ class AwsDeploy { .getStage()} ${style.aside(`(${this.serverless.getProvider('aws').getRegion()})`)}` ); log.info(); // Ensure gap between verbose logging + + // This is used to ensure that for `deploy` command, the `accountId` will be resolved and available + // for `generatePayload` telemetry logic + this.serverless + .getProvider('aws') + .getAccountId() + .then((accountId) => (this.serverless.getProvider('aws').accountId = accountId)); } }, @@ -165,7 +172,7 @@ class AwsDeploy { } }, - 'finalize': () => { + 'finalize': async () => { if (this.serverless.processedInput.commands.join(' ') !== 'deploy') return; if (this.options.function) return; if (this.serverless.service.provider.shouldNotDeploy) { diff --git a/lib/plugins/aws/provider.js b/lib/plugins/aws/provider.js index 4703b9ded04a..f3d9d3ccd159 100644 --- a/lib/plugins/aws/provider.js +++ b/lib/plugins/aws/provider.js @@ -1437,6 +1437,9 @@ class AwsProvider { // Store credentials in this variable to avoid creating them several times (messes up MFA). this.cachedCredentials = null; + // Store accountId to be used in `generateTelemetry` logic + this.accountId = null; + Object.assign(this.naming, naming); } @@ -1725,20 +1728,6 @@ class AwsProvider { return stageSourceValue.value || defaultStage; } - getAccountInfo() { - return this.request('STS', 'getCallerIdentity', {}).then((result) => { - const arn = result.Arn; - const accountId = result.Account; - const partition = arn.split(':')[1]; // ex: arn:aws:iam:acctId:user/xyz - return { - accountId, - partition, - arn: result.Arn, - userId: result.UserId, - }; - }); - } - /** * Get API Gateway Rest API ID from serverless config */ @@ -1858,6 +1847,21 @@ class AwsProvider { Object.defineProperties( AwsProvider.prototype, memoizeeMethods({ + getAccountInfo: d( + async function () { + const result = await this.request('STS', 'getCallerIdentity', {}); + const arn = result.Arn; + const accountId = result.Account; + const partition = arn.split(':')[1]; // ex: arn:aws:iam:acctId:user/xyz + return { + accountId, + partition, + arn: result.Arn, + userId: result.UserId, + }; + }, + { promise: true } + ), getAccountId: d( async function () { const result = await this.getAccountInfo(); diff --git a/lib/utils/telemetry/generatePayload.js b/lib/utils/telemetry/generatePayload.js index b124ac8f6441..1ea86a87760b 100644 --- a/lib/utils/telemetry/generatePayload.js +++ b/lib/utils/telemetry/generatePayload.js @@ -3,6 +3,7 @@ const path = require('path'); const os = require('os'); const fs = require('fs'); +const crypto = require('crypto'); const resolveSync = require('ncjsm/resolve/sync'); const _ = require('lodash'); const isPlainObject = require('type/plain-object/is'); @@ -298,6 +299,17 @@ module.exports = ({ payload.config = getServiceConfig({ configuration, options, variableSources }); payload.isConfigValid = getConfigurationValidationResult(configuration); payload.dashboard.orgUid = serverless && serverless.service.orgUid; + + if (command === 'deploy' && isAwsProvider) { + const serviceName = configuration && configuration.service.name; + const accountId = serverless && serverless.getProvider('aws').accountId; + if (serviceName && accountId) { + payload.projectId = crypto + .createHash('sha256') + .update(`${serviceName}-${accountId}`) + .digest('base64'); + } + } } if (commandUsage) { diff --git a/test/unit/lib/plugins/aws/deploy/lib/extendedValidate.test.js b/test/unit/lib/plugins/aws/deploy/lib/extendedValidate.test.js index 746d3478ae38..a3da6edef93b 100644 --- a/test/unit/lib/plugins/aws/deploy/lib/extendedValidate.test.js +++ b/test/unit/lib/plugins/aws/deploy/lib/extendedValidate.test.js @@ -191,6 +191,16 @@ describe('test/unit/lib/plugins/aws/deploy/lib/extendedValidate.test.js', () => }, }, }, + awsRequestStubMap: { + STS: { + getCallerIdentity: { + ResponseMetadata: { RequestId: 'ffffffff-ffff-ffff-ffff-ffffffffffff' }, + UserId: 'XXXXXXXXXXXXXXXXXXXXX', + Account: '1234567890', + Arn: 'arn:aws:iam::1234567890:user/test', + }, + }, + }, command: 'deploy', lastLifecycleHookName: 'before:deploy:deploy', }); diff --git a/test/unit/lib/plugins/aws/deployFunction.test.js b/test/unit/lib/plugins/aws/deployFunction.test.js index 46280c76b560..42747c749962 100644 --- a/test/unit/lib/plugins/aws/deployFunction.test.js +++ b/test/unit/lib/plugins/aws/deployFunction.test.js @@ -128,6 +128,8 @@ describe('AwsDeployFunction', () => { let getRoleStub; beforeEach(() => { + // Ensure that memoized function will be properly stubbed + awsDeployFunction.provider.getAccountInfo; getAccountInfoStub = sinon .stub(awsDeployFunction.provider, 'getAccountInfo') .resolves({ accountId: '123456789012', partition: 'aws' }); diff --git a/test/unit/lib/plugins/aws/package/compile/events/apiGateway/lib/hack/updateStage.test.js b/test/unit/lib/plugins/aws/package/compile/events/apiGateway/lib/hack/updateStage.test.js index 97c386f52f9d..c27313708b00 100644 --- a/test/unit/lib/plugins/aws/package/compile/events/apiGateway/lib/hack/updateStage.test.js +++ b/test/unit/lib/plugins/aws/package/compile/events/apiGateway/lib/hack/updateStage.test.js @@ -33,6 +33,8 @@ describe('#updateStage()', () => { options = { stage: 'dev', region: 'us-east-1' }; awsProvider = new AwsProvider(serverless, options); serverless.setProvider('aws', awsProvider); + // Ensure that memoized function will be properly stubbed + awsProvider.getAccountInfo; providerGetAccountInfoStub = sinon.stub(awsProvider, 'getAccountInfo').resolves({ accountId: '123456', partition: 'aws', diff --git a/test/unit/lib/plugins/aws/package/lib/generateCoreTemplate.test.js b/test/unit/lib/plugins/aws/package/lib/generateCoreTemplate.test.js index 86b791c3380b..6c0ffb604d59 100644 --- a/test/unit/lib/plugins/aws/package/lib/generateCoreTemplate.test.js +++ b/test/unit/lib/plugins/aws/package/lib/generateCoreTemplate.test.js @@ -147,10 +147,20 @@ describe('#generateCoreTemplate()', () => { deploymentBucket: bucketName, }, }, + awsRequestStubMap: { + S3: { getBucketLocation: { LocationConstraint: '' } }, + STS: { + getCallerIdentity: { + ResponseMetadata: { RequestId: 'ffffffff-ffff-ffff-ffff-ffffffffffff' }, + UserId: 'XXXXXXXXXXXXXXXXXXXXX', + Account: '1234567890', + Arn: 'arn:aws:iam::1234567890:user/test', + }, + }, + }, command: 'deploy', options: { 'aws-s3-accelerate': true }, lastLifecycleHookName: 'before:deploy:deploy', - awsRequestStubMap: { S3: { getBucketLocation: { LocationConstraint: '' } } }, }) ).to.eventually.be.rejected.and.have.property( 'code', @@ -185,6 +195,16 @@ describe('#generateCoreTemplate()', () => { command: 'deploy', options: { 'aws-s3-accelerate': true }, lastLifecycleHookName: 'before:deploy:deploy', + awsRequestStubMap: { + STS: { + getCallerIdentity: { + ResponseMetadata: { RequestId: 'ffffffff-ffff-ffff-ffff-ffffffffffff' }, + UserId: 'XXXXXXXXXXXXXXXXXXXXX', + Account: '1234567890', + Arn: 'arn:aws:iam::1234567890:user/test', + }, + }, + }, }).then(({ cfTemplate: template }) => { expect(template.Outputs.ServerlessDeploymentBucketAccelerated).to.not.equal(null); expect(template.Outputs.ServerlessDeploymentBucketAccelerated.Value).to.equal(true); @@ -196,6 +216,16 @@ describe('#generateCoreTemplate()', () => { command: 'deploy', options: { 'aws-s3-accelerate': false }, lastLifecycleHookName: 'before:deploy:deploy', + awsRequestStubMap: { + STS: { + getCallerIdentity: { + ResponseMetadata: { RequestId: 'ffffffff-ffff-ffff-ffff-ffffffffffff' }, + UserId: 'XXXXXXXXXXXXXXXXXXXXX', + Account: '1234567890', + Arn: 'arn:aws:iam::1234567890:user/test', + }, + }, + }, }).then(({ cfTemplate: template }) => { expect(template.Resources.ServerlessDeploymentBucket).to.be.deep.equal({ Type: 'AWS::S3::Bucket', @@ -220,6 +250,16 @@ describe('#generateCoreTemplate()', () => { runServerless({ config: { service: 'irrelevant', provider: { name: 'aws', region: 'us-gov-west-1' } }, command: 'deploy', + awsRequestStubMap: { + STS: { + getCallerIdentity: { + ResponseMetadata: { RequestId: 'ffffffff-ffff-ffff-ffff-ffffffffffff' }, + UserId: 'XXXXXXXXXXXXXXXXXXXXX', + Account: '1234567890', + Arn: 'arn:aws:iam::1234567890:user/test', + }, + }, + }, options: { 'aws-s3-accelerate': false }, lastLifecycleHookName: 'before:deploy:deploy', }).then(({ cfTemplate: template }) => { diff --git a/test/unit/lib/plugins/aws/package/lib/stripNullPropsFromTemplateResources.test.js b/test/unit/lib/plugins/aws/package/lib/stripNullPropsFromTemplateResources.test.js index dfd3bcdcbba1..b8f3113d8c2a 100644 --- a/test/unit/lib/plugins/aws/package/lib/stripNullPropsFromTemplateResources.test.js +++ b/test/unit/lib/plugins/aws/package/lib/stripNullPropsFromTemplateResources.test.js @@ -11,7 +11,7 @@ describe('test/unit/lib/plugins/aws/package/lib/stripNullPropsFromTemplateResour before(async () => { const result = await runServerless({ fixture: 'aws', - command: 'deploy', + command: 'package', lastLifecycleHookName: 'package:finalize', configExt: { resources: { diff --git a/test/unit/lib/utils/telemetry/generatePayload.test.js b/test/unit/lib/utils/telemetry/generatePayload.test.js index 740ac2a4fad6..62d2f630b163 100644 --- a/test/unit/lib/utils/telemetry/generatePayload.test.js +++ b/test/unit/lib/utils/telemetry/generatePayload.test.js @@ -659,4 +659,25 @@ describe('test/unit/lib/utils/telemetry/generatePayload.test.js', () => { expect(payload.config.variableSources).to.deep.equal(['ssm', 'opt']); }); + + it('Should correctly resolve projectId property', async () => { + const { serverless } = await runServerless({ + fixture: 'httpApi', + command: 'print', + configExt: { + service: 'to-ensure-unique-serivce-name', + }, + }); + serverless.getProvider('aws').accountId = '1234567890'; + const payload = generatePayload({ + command: 'deploy', + options: {}, + commandSchema: commandsSchema.get('deploy'), + serviceDir: serverless.serviceDir, + configuration: serverless.configurationInput, + serverless, + }); + + expect(payload.projectId).to.deep.equal('35dsFwCaexwLHppAP4uDsjKW4ci54q1AKcN5JTNaDtw='); + }); });