diff --git a/docs/providers/aws/guide/variables.md b/docs/providers/aws/guide/variables.md index 8466520484d..b19f6433227 100644 --- a/docs/providers/aws/guide/variables.md +++ b/docs/providers/aws/guide/variables.md @@ -305,6 +305,32 @@ functions: handler: handler.hello ``` +## Referencing AWS-specific variables + +You can reference AWS-specific values as the source of your variables. Those values are exposed via the Serverless Variables system through the `{aws:}` variable prefix. + +The following variables are available: + +**accountId** + +Account ID of you AWS Account, based on the AWS Credentials that you have configured. + +```yml +service: new-service +provider: aws + +functions: + func1: + name: function-1 + handler: handler.func1 + environment: + ACCOUNT_ID: ${aws:accountId} +``` + +**region** + +The region used by the Serverless CLI. The `${aws:region}` variable is a shortcut for `${opt:region, self:provider.region, "us-east-1"}`. + ### Resolution of non plain string types New variable resolver, ensures that automatically other types as `SecureString` and `StringList` are resolved into expected forms. diff --git a/lib/Serverless.js b/lib/Serverless.js index a5c6711a9c7..ef261d5a06d 100644 --- a/lib/Serverless.js +++ b/lib/Serverless.js @@ -352,6 +352,7 @@ class Serverless { cf: require('./configuration/variables/sources/instance-dependent/get-cf')(this), s3: require('./configuration/variables/sources/instance-dependent/get-s3')(this), ssm: require('./configuration/variables/sources/instance-dependent/get-ssm')(this), + aws: require('./configuration/variables/sources/instance-dependent/get-aws')(this), }); } if (this.configurationInput.org && this.pluginManager.dashboardPlugin) { diff --git a/lib/configuration/variables/sources/instance-dependent/get-aws.js b/lib/configuration/variables/sources/instance-dependent/get-aws.js new file mode 100644 index 00000000000..45b7a021d01 --- /dev/null +++ b/lib/configuration/variables/sources/instance-dependent/get-aws.js @@ -0,0 +1,43 @@ +'use strict'; + +const ensureString = require('type/string/ensure'); +const ServerlessError = require('../../../../serverless-error'); + +module.exports = (serverlessInstance) => { + return { + resolve: async ({ address, options, resolveConfigurationProperty }) => { + if (!address) { + throw new ServerlessError( + 'Missing address argument in variable "aws" source', + 'MISSING_AWS_SOURCE_ADDRESS' + ); + } + + address = ensureString(address, { + Error: ServerlessError, + errorMessage: 'Non-string address argument in variable "sls" source: %v', + errorCode: 'INVALID_AWS_SOURCE_ADDRESS', + }); + + switch (address) { + case 'accountId': { + const { Account } = await serverlessInstance + .getProvider('aws') + .request('STS', 'getCallerIdentity', {}, { useCache: true }); + return { value: Account }; + } + case 'region': { + let region = options.region; + if (!region) region = await resolveConfigurationProperty(['provider', 'region']); + if (!region) region = 'us-east-1'; + return { value: region }; + } + default: + throw new ServerlessError( + `Unsupported "${address}" address argument in variable "aws" source`, + 'UNSUPPORTED_AWS_SOURCE_ADDRESS' + ); + } + }, + }; +}; diff --git a/scripts/serverless.js b/scripts/serverless.js index c44f0a7244f..b07c43e5b14 100755 --- a/scripts/serverless.js +++ b/scripts/serverless.js @@ -544,6 +544,9 @@ const processSpanPromise = (async () => { ssm: require('../lib/configuration/variables/sources/instance-dependent/get-ssm')( serverless ), + aws: require('../lib/configuration/variables/sources/instance-dependent/get-aws')( + serverless + ), }); resolverConfiguration.fulfilledSources.add('cf').add('s3').add('ssm'); } diff --git a/test/unit/lib/configuration/variables/sources/instance-dependent/get-aws.test.js b/test/unit/lib/configuration/variables/sources/instance-dependent/get-aws.test.js new file mode 100644 index 00000000000..3664f0613ea --- /dev/null +++ b/test/unit/lib/configuration/variables/sources/instance-dependent/get-aws.test.js @@ -0,0 +1,110 @@ +'use strict'; + +const { expect } = require('chai'); +const _ = require('lodash'); + +const resolveMeta = require('../../../../../../../lib/configuration/variables/resolve-meta'); +const resolve = require('../../../../../../../lib/configuration/variables/resolve'); +const selfSource = require('../../../../../../../lib/configuration/variables/sources/self'); +const getAwsSource = require('../../../../../../../lib/configuration/variables/sources/instance-dependent/get-aws'); +const Serverless = require('../../../../../../../lib/Serverless'); + +describe('test/unit/lib/configuration/variables/sources/instance-dependent/get-aws.test.js', () => { + let configuration; + let variablesMeta; + let serverlessInstance; + + const initializeServerless = async (configExt, options) => { + configuration = { + service: 'foo', + provider: { + name: 'aws', + }, + custom: { + region: '${aws:region}', + accountId: '${aws:accountId}', + missingAddress: '${aws:}', + invalidAddress: '${aws:invalid}', + nonStringAddress: '${aws:${self:custom.someObject}}', + someObject: {}, + }, + }; + if (configExt) { + configuration = _.merge(configuration, configExt); + } + variablesMeta = resolveMeta(configuration); + serverlessInstance = new Serverless({ + configuration, + serviceDir: process.cwd(), + configurationFilename: 'serverless.yml', + isConfigurationResolved: true, + hasResolvedCommandsExternally: true, + commands: ['package'], + options: {}, + }); + serverlessInstance.init(); + serverlessInstance.getProvider = () => ({ + constructor: { + getProviderName: () => 'aws', + }, + request: async () => { + return { + Account: '1234567890', + }; + }, + }); + await resolve({ + serviceDir: process.cwd(), + configuration, + variablesMeta, + sources: { self: selfSource, aws: getAwsSource(serverlessInstance) }, + options: options || {}, + fulfilledSources: new Set(['self', 'aws']), + }); + }; + + it('should resolve `accountId`', async () => { + await initializeServerless(); + expect(configuration.custom.accountId).to.equal('1234567890'); + }); + + it('should report with an error missing address', () => + expect(variablesMeta.get('custom\0missingAddress').error.code).to.equal( + 'VARIABLE_RESOLUTION_ERROR' + )); + + it('should report with an error invalid address', () => + expect(variablesMeta.get('custom\0invalidAddress').error.code).to.equal( + 'VARIABLE_RESOLUTION_ERROR' + )); + + it('should report with an error a non-string address', () => + expect(variablesMeta.get('custom\0nonStringAddress').error.code).to.equal( + 'VARIABLE_RESOLUTION_ERROR' + )); + + it('should resolve ${aws:region}', async () => { + // us-east-1 by default + await initializeServerless(); + expect(configuration.custom.region).to.equal('us-east-1'); + // Resolves to provider.region if it exists + await initializeServerless({ + provider: { + region: 'eu-west-1', + }, + }); + expect(configuration.custom.region).to.equal('eu-west-1'); + // Resolves to `--region=` if the option is set + await initializeServerless( + { + provider: { + region: 'eu-west-1', + }, + }, + { + region: 'eu-central-1', + } + ); + expect(configuration.custom.region).to.equal('eu-central-1'); + }); +});