serverless/lib/plugins/aws/provider.js
Sidharth RK 447704f404
fix(AWS Lambda): Ensure image region matches provider region (#11081)
Co-authored-by: steviekong <sidharth@pingthis.in>
2022-05-19 17:35:19 +02:00

2341 lines
84 KiB
JavaScript

'use strict';
const AWS = require('aws-sdk');
const BbPromise = require('bluebird');
const _ = require('lodash');
const naming = require('./lib/naming.js');
const fsp = require('fs').promises;
const getS3EndpointForRegion = require('./utils/get-s3-endpoint-for-region');
const memoizeeMethods = require('memoizee/methods');
const readline = require('readline');
const { ALB_LISTENER_REGEXP } = require('./package/compile/events/alb/lib/validate');
const d = require('d');
const path = require('path');
const spawnExt = require('child-process-ext/spawn');
const ServerlessError = require('../../serverless-error');
const awsRequest = require('../../aws/request');
const { cfValue } = require('../../utils/aws-schema-get-cf-value');
const reportDeprecatedProperties = require('../../utils/report-deprecated-properties');
const deepSortObjectByKey = require('../../utils/deep-sort-object-by-key');
const { progress, log } = require('@serverless/utils/log');
const isLambdaArn = RegExp.prototype.test.bind(/^arn:[^:]+:lambda:/);
const isEcrUri = RegExp.prototype.test.bind(
/^\d+\.dkr\.ecr\.[a-z0-9-]+..amazonaws.com\/([^@]+)|([^@:]+@sha256:[a-f0-9]{64})$/
);
function caseInsensitive(str) {
return { type: 'string', regexp: new RegExp(`^${str}$`, 'i').toString() };
}
const constants = {
providerName: 'aws',
};
const imageNamePattern = '^[a-z][a-z0-9-_]{1,31}$';
const apiGatewayUsagePlan = {
type: 'object',
properties: {
quota: {
type: 'object',
properties: {
limit: { type: 'integer', minimum: 0 },
offset: { type: 'integer', minimum: 0 },
period: { enum: ['DAY', 'WEEK', 'MONTH'] },
},
additionalProperties: false,
},
throttle: {
type: 'object',
properties: {
burstLimit: { type: 'integer', minimum: 0 },
rateLimit: { type: 'integer', minimum: 0 },
},
additionalProperties: false,
},
},
additionalProperties: false,
};
const impl = {
/**
* Determine whether the given credentials are valid. It turned out that detecting invalid
* credentials was more difficult than detecting the positive cases we know about. Hooray for
* whack-a-mole!
* @param credentials The credentials to test for validity
* @return {boolean} Whether the given credentials were valid
*/
validCredentials: (credentials) => {
let result = false;
if (credentials) {
if (
// valid credentials loaded
(credentials.accessKeyId &&
credentials.accessKeyId !== 'undefined' &&
credentials.secretAccessKey &&
credentials.secretAccessKey !== 'undefined') ||
// a role to assume has been successfully loaded, the associated STS request has been
// sent, and the temporary credentials will be asynchronously delivered.
credentials.roleArn
) {
result = true;
}
}
return result;
},
/**
* Add credentials, if present, to the given results
* @param results The results to add the given credentials to if they are valid
* @param credentials The credentials to validate and add to the results if valid
*/
addCredentials: (results, credentials) => {
if (impl.validCredentials(credentials)) {
results.credentials = credentials;
}
},
/**
* Add credentials, if present, from the environment
* @param results The results to add environment credentials to
* @param prefix The environment variable prefix to use in extracting credentials
*/
addEnvironmentCredentials: (results, prefix) => {
if (prefix) {
const environmentCredentials = new AWS.EnvironmentCredentials(prefix);
impl.addCredentials(results, environmentCredentials);
}
},
/**
* Add credentials from a profile, if the profile and credentials for it exists
* @param results The results to add profile credentials to
* @param profile The profile to load credentials from
*/
addProfileCredentials: (results, profile) => {
if (profile) {
const params = { profile };
if (process.env.AWS_SHARED_CREDENTIALS_FILE) {
params.filename = process.env.AWS_SHARED_CREDENTIALS_FILE;
}
// Setup a MFA callback for asking the code from the user.
params.tokenCodeFn = (mfaSerial, callback) => {
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
rl.question(`Enter MFA code for ${mfaSerial}: `, (answer) => {
rl.close();
callback(null, answer);
});
};
const profileCredentials = new AWS.SharedIniFileCredentials(params);
if (
!(
profileCredentials.accessKeyId ||
profileCredentials.sessionToken ||
profileCredentials.roleArn
)
) {
throw new ServerlessError(
`AWS profile "${profile}" doesn't seem to be configured`,
'UNRECOGNIZED_AWS_PROFILE'
);
}
impl.addCredentials(results, profileCredentials);
}
},
/**
* Add credentials, if present, from a profile that is specified within the environment
* @param results The prefix of the profile's declaration in the environment
* @param prefix The prefix for the environment variable
*/
addEnvironmentProfile: (results, prefix) => {
if (prefix) {
const profile = process.env[`${prefix}_PROFILE`];
impl.addProfileCredentials(results, profile);
}
},
};
const baseAlbAuthorizerProperties = {
onUnauthenticatedRequest: { enum: ['allow', 'authenticate', 'deny'] },
requestExtraParams: {
type: 'object',
maxProperties: 10,
additionalProperties: { type: 'string' },
},
scope: { type: 'string' },
sessionCookieName: { type: 'string' },
sessionTimeout: { type: 'integer', minimum: 0 },
};
const oidcAlbAuthorizer = {
type: 'object',
properties: {
type: { const: 'oidc' },
authorizationEndpoint: { format: 'uri', type: 'string' },
clientId: { type: 'string' },
clientSecret: { type: 'string' },
issuer: { format: 'uri', type: 'string' },
tokenEndpoint: { format: 'uri', type: 'string' },
userInfoEndpoint: { format: 'uri', type: 'string' },
...baseAlbAuthorizerProperties,
},
required: [
'type',
'authorizationEndpoint',
'clientId',
'issuer',
'tokenEndpoint',
'userInfoEndpoint',
],
additionalProperties: false,
};
const cognitoAlbAuthorizer = {
type: 'object',
properties: {
type: { const: 'cognito' },
userPoolArn: { $ref: '#/definitions/awsArn' },
userPoolClientId: { type: 'string' },
userPoolDomain: { type: 'string' },
...baseAlbAuthorizerProperties,
},
required: ['type', 'userPoolArn', 'userPoolClientId', 'userPoolDomain'],
additionalProperties: false,
};
class AwsProvider {
constructor(serverless, options) {
this.naming = { provider: this };
this.options = options;
this.provider = this; // only load plugin in an AWS service context
this.serverless = serverless;
// Notice: provider.sdk is used by plugins. Do not remove without deprecating first and
// offering a reliable alternative
this.sdk = AWS;
this.serverless.setProvider(constants.providerName, this);
this.hooks = {
initialize: () => {
// Support deploymentBucket configuration as an object
const provider = this.serverless.service.provider;
if (provider && provider.deploymentBucket) {
if (_.isObject(provider.deploymentBucket)) {
// store the object in a new variable so that it can be reused later on
provider.deploymentBucketObject = provider.deploymentBucket;
if (provider.deploymentBucket.name) {
// (re)set the value of the deploymentBucket property to the name (which is a string)
provider.deploymentBucket = provider.deploymentBucket.name;
} else {
provider.deploymentBucket = null;
}
}
}
reportDeprecatedProperties(
'PROVIDER_IAM_SETTINGS_V3',
{
'provider.role': 'provider.iam.role',
'provider.rolePermissionsBoundary': 'provider.iam.role.permissionsBoundary',
'provider.iam.role.permissionBoundary': 'provider.iam.role.permissionsBoundary',
'provider.iamManagedPolicies': 'provider.iam.role.managedPolicies',
'provider.iamRoleStatements': 'provider.iam.role.statements',
'provider.cfnRole': 'provider.iam.deploymentRole',
},
{ serviceConfig: this.serverless.service }
);
},
};
if (this.serverless.service.provider.name === 'aws') {
// Below ideally should be in hooks.initialize, but variables resolution depend on this
this.serverless.service.provider.region = this.getRegion();
require('../../utils/aws-sdk-patch');
serverless.configSchemaHandler.defineProvider('aws', {
definitions: {
awsAccountId: {
type: 'string',
pattern: '^\\d{12}$',
},
awsAlbListenerArn: {
type: 'string',
pattern: ALB_LISTENER_REGEXP.source,
},
awsAlexaEventToken: {
type: 'string',
minLength: 0,
maxLength: 256,
pattern: '^[a-zA-Z0-9._\\-]+$',
},
awsApiGatewayAbbreviatedArn: {
type: 'string',
pattern: '^execute-api:/',
},
awsApiGatewayApiKeys: {
type: 'array',
items: {
anyOf: [
{ type: 'string' },
{
$ref: '#/definitions/awsApiGatewayApiKeysProperties',
},
{
type: 'object',
maxProperties: 1,
additionalProperties: {
type: 'array',
items: {
oneOf: [
{ type: 'string' },
{ $ref: '#/definitions/awsApiGatewayApiKeysProperties' },
],
},
},
},
],
},
},
awsApiGatewayApiKeysProperties: {
type: 'object',
properties: {
name: { type: 'string' },
value: { type: 'string' },
description: { type: 'string' },
customerId: { type: 'string' },
enabled: { type: 'boolean' },
},
additionalProperties: false,
},
awsHttpApiPayload: {
type: 'string',
enum: ['1.0', '2.0'],
},
awsArn: {
anyOf: [
{ $ref: '#/definitions/awsArnString' },
{ $ref: '#/definitions/awsCfFunction' },
],
},
awsArnString: {
type: 'string',
pattern: '^arn:',
},
awsCfArrayInstruction: {
anyOf: [
{
type: 'array',
items: { $ref: '#/definitions/awsCfInstruction' },
},
{ $ref: '#/definitions/awsCfSplit' },
],
},
awsSecretsManagerArnString: {
type: 'string',
pattern: 'arn:[a-z-]+:secretsmanager:[a-z0-9-]+:\\d+:secret:[A-Za-z0-9/_+=.@-]+',
},
awsCfFunction: {
anyOf: [
{ $ref: '#/definitions/awsCfImport' },
{ $ref: '#/definitions/awsCfJoin' },
{ $ref: '#/definitions/awsCfGetAtt' },
{ $ref: '#/definitions/awsCfRef' },
{ $ref: '#/definitions/awsCfSub' },
],
},
awsCfGetAtt: {
type: 'object',
properties: {
'Fn::GetAtt': {
type: 'array',
minItems: 2,
maxItems: 2,
items: { type: 'string', minLength: 1 },
},
},
required: ['Fn::GetAtt'],
additionalProperties: false,
},
awsCfImport: {
type: 'object',
properties: {
'Fn::ImportValue': {},
},
additionalProperties: false,
required: ['Fn::ImportValue'],
},
awsCfIf: {
type: 'object',
properties: {
'Fn::If': {
type: 'array',
minItems: 3,
maxItems: 3,
items: { $ref: '#/definitions/awsCfInstruction' },
},
},
required: ['Fn::If'],
additionalProperties: false,
},
awsCfImportLocallyResolvable: {
type: 'object',
properties: {
'Fn::ImportValue': { type: 'string' },
},
additionalProperties: false,
required: ['Fn::ImportValue'],
},
awsCfInstruction: {
anyOf: [{ type: 'string', minLength: 1 }, { $ref: '#/definitions/awsCfFunction' }],
},
awsCfJoin: {
type: 'object',
properties: {
'Fn::Join': {
type: 'array',
minItems: 2,
maxItems: 2,
items: [{ type: 'string' }, { type: 'array' }],
additionalItems: false,
},
},
required: ['Fn::Join'],
additionalProperties: false,
},
awsCfRef: {
type: 'object',
properties: {
Ref: { type: 'string', minLength: 1 },
},
required: ['Ref'],
additionalProperties: false,
},
awsCfSplit: {
type: 'object',
properties: {
'Fn::Split': {
type: 'array',
minItems: 2,
maxItems: 2,
items: {
oneOf: [{ type: 'string' }, { $ref: '#/definitions/awsCfFunction' }],
},
},
},
required: ['Fn::Split'],
additionalProperties: false,
},
awsCfFindInMap: {
type: 'object',
properties: {
'Fn::FindInMap': {
type: 'array',
minItems: 3,
maxItems: 3,
items: {
oneOf: [{ type: 'string' }, { $ref: '#/definitions/awsCfFunction' }],
},
},
},
required: ['Fn::FindInMap'],
additionalProperties: false,
},
awsCfSub: {
type: 'object',
properties: {
'Fn::Sub': {},
},
required: ['Fn::Sub'],
additionalProperties: false,
},
awsIamPolicyAction: { type: 'array', items: { type: 'string' } },
awsIamPolicyPrincipal: {
anyOf: [
{ const: '*' },
{
type: 'object',
properties: {
AWS: {
anyOf: [
{ const: '*' },
{ $ref: '#/definitions/awsCfIf' },
{
type: 'array',
items: {
anyOf: [
{ $ref: '#/definitions/awsAccountId' },
{ $ref: '#/definitions/awsArn' },
],
},
},
],
},
Federated: { type: 'array', items: { type: 'string' } },
Service: { type: 'array', items: { type: 'string' } },
CanonicalUser: { type: 'array', items: { type: 'string' } },
},
additionalProperties: false,
},
],
},
awsIamPolicyResource: {
anyOf: [
{ const: '*' },
{ $ref: '#/definitions/awsArn' },
{ $ref: '#/definitions/awsLogicalResourceId' },
{
type: 'array',
items: {
anyOf: [
{ const: '*' },
{ $ref: '#/definitions/awsArn' },
{ $ref: '#/definitions/awsLogicalResourceId' },
],
},
},
],
},
// Definition of Statement taken from https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_grammar.html#policies-grammar-bnf
awsIamPolicyStatements: {
type: 'array',
items: {
type: 'object',
properties: {
Sid: { type: 'string' },
Effect: { enum: ['Allow', 'Deny'] },
Action: { $ref: '#/definitions/awsIamPolicyAction' },
NotAction: { $ref: '#/definitions/awsIamPolicyAction' },
Principal: { $ref: '#/definitions/awsIamPolicyPrincipal' },
NotPrincipal: { $ref: '#/definitions/awsIamPolicyPrincipal' },
Resource: { $ref: '#/definitions/awsIamPolicyResource' },
NotResource: { $ref: '#/definitions/awsIamPolicyResource' },
Condition: { type: 'object' },
},
additionalProperties: false,
allOf: [
{ required: ['Effect'] },
{ oneOf: [{ required: ['Action'] }, { required: ['NotAction'] }] },
{ oneOf: [{ required: ['Resource'] }, { required: ['NotResource'] }] },
],
},
},
awsKmsArn: {
anyOf: [
{ $ref: '#/definitions/awsCfFunction' },
{ type: 'string', pattern: '^arn:aws[a-z-]*:kms' },
],
},
awsLambdaArchitecture: { enum: ['arm64', 'x86_64'] },
awsLambdaEnvironment: {
type: 'object',
patternProperties: {
'^[A-Za-z_][a-zA-Z0-9_]*$': {
anyOf: [
{ const: '' },
{ $ref: '#/definitions/awsCfInstruction' },
{ $ref: '#/definitions/awsCfIf' },
],
},
},
additionalProperties: false,
},
awsLambdaLayers: {
type: 'array',
items: { $ref: '#/definitions/awsArn' },
},
awsLambdaMemorySize: { type: 'integer', minimum: 128, maximum: 10240 },
awsLambdaRole: {
anyOf: [
{ type: 'string', minLength: 1 },
{ $ref: '#/definitions/awsCfSub' },
{ $ref: '#/definitions/awsCfImport' },
{ $ref: '#/definitions/awsCfGetAtt' },
],
},
awsLambdaRuntime: {
enum: [
'dotnet6',
'dotnetcore3.1',
'go1.x',
'java11',
'java8',
'java8.al2',
'nodejs12.x',
'nodejs14.x',
'nodejs16.x',
'provided',
'provided.al2',
'python3.6',
'python3.7',
'python3.8',
'python3.9',
'ruby2.7',
],
},
awsLambdaTimeout: { type: 'integer', minimum: 1, maximum: 900 },
awsLambdaTracing: { anyOf: [{ enum: ['Active', 'PassThrough'] }, { type: 'boolean' }] },
awsLambdaVersioning: { type: 'boolean' },
awsLambdaVpcConfig: {
type: 'object',
properties: {
securityGroupIds: {
anyOf: [
{
type: 'array',
items: {
anyOf: [
{ $ref: '#/definitions/awsCfInstruction' },
{ $ref: '#/definitions/awsCfIf' },
],
},
maxItems: 5,
},
{ $ref: '#/definitions/awsCfSplit' },
{ $ref: '#/definitions/awsCfFindInMap' },
],
},
subnetIds: {
anyOf: [
{
type: 'array',
items: {
anyOf: [
{ $ref: '#/definitions/awsCfInstruction' },
{ $ref: '#/definitions/awsCfIf' },
],
},
maxItems: 16,
},
{ $ref: '#/definitions/awsCfSplit' },
{ $ref: '#/definitions/awsCfFindInMap' },
],
},
},
additionalProperties: false,
required: ['securityGroupIds', 'subnetIds'],
},
awsLogicalResourceId: {
type: 'string',
pattern: '^[#A-Za-z0-9-_./]+[*]?$',
},
awsLogGroupName: {
type: 'string',
pattern: '^[/#A-Za-z0-9-_.]+$',
},
awsLogRetentionInDays: {
type: 'number',
// https://docs.aws.amazon.com/AmazonCloudWatchLogs/latest/APIReference/API_PutRetentionPolicy.html#API_PutRetentionPolicy_RequestSyntax
enum: [
1, 3, 5, 7, 14, 30, 60, 90, 120, 150, 180, 365, 400, 545, 731, 1827, 2192, 2557, 2922,
3288, 3653,
],
},
awsResourceCondition: { type: 'string' },
awsResourceDependsOn: { type: 'array', items: { type: 'string' } },
awsResourcePolicyResource: {
anyOf: [
{ const: '*' },
{ $ref: '#/definitions/awsArn' },
// API Gateway Resource Policy resource property abbreviated syntax - https://docs.aws.amazon.com/apigateway/latest/developerguide/apigateway-resource-policies-examples.html
{ $ref: '#/definitions/awsApiGatewayAbbreviatedArn' },
{
type: 'array',
items: {
anyOf: [
{ const: '*' },
{ $ref: '#/definitions/awsArn' },
// API Gateway Resource Policy resource property abbreviated syntax - https://docs.aws.amazon.com/apigateway/latest/developerguide/apigateway-resource-policies-examples.html
{ $ref: '#/definitions/awsApiGatewayAbbreviatedArn' },
],
},
},
],
},
awsResourcePolicyStatements: {
type: 'array',
items: {
type: 'object',
properties: {
Sid: { type: 'string' },
Effect: { enum: ['Allow', 'Deny'] },
Action: { $ref: '#/definitions/awsIamPolicyAction' },
NotAction: { $ref: '#/definitions/awsIamPolicyAction' },
Principal: { $ref: '#/definitions/awsIamPolicyPrincipal' },
NotPrincipal: { $ref: '#/definitions/awsIamPolicyPrincipal' },
Resource: { $ref: '#/definitions/awsResourcePolicyResource' },
NotResource: { $ref: '#/definitions/awsResourcePolicyResource' },
Condition: { type: 'object' },
},
additionalProperties: false,
allOf: [
{ required: ['Effect'] },
{ oneOf: [{ required: ['Action'] }, { required: ['NotAction'] }] },
{ oneOf: [{ required: ['Resource'] }, { required: ['NotResource'] }] },
],
},
},
awsResourceTags: {
type: 'object',
patternProperties: {
'^(?!aws:)[\\w./=+:\\-_\\x20]{1,128}$': {
type: 'string',
maxLength: 256,
},
},
additionalProperties: false,
},
awsS3BucketName: {
type: 'string',
// pattern sourced from https://stackoverflow.com/questions/50480924/regex-for-s3-bucket-name
pattern:
'(?!^(\\d{1,3}\\.){3}\\d{1,3}$)(^(([a-z0-9]|[a-z0-9][a-z0-9-]*[a-z0-9])\\.)*([a-z0-9]|[a-z0-9][a-z0-9-]*[a-z0-9])$)',
minLength: 3,
maxLength: 63,
},
ecrImageUri: {
type: 'string',
pattern:
'^\\d+\\.dkr\\.ecr\\.[a-z0-9-]+..amazonaws.com\\/([^@]+)|([^@:]+@sha256:[a-f0-9]{64})$',
},
filterPatterns: {
type: 'array',
minItems: 1,
maxItems: 10,
items: { type: 'object' },
},
},
provider: {
properties: {
alb: {
type: 'object',
properties: {
targetGroupPrefix: { type: 'string', maxLength: 16 },
authorizers: {
type: 'object',
additionalProperties: {
anyOf: [oidcAlbAuthorizer, cognitoAlbAuthorizer],
},
},
},
additionalProperties: false,
},
apiGateway: {
type: 'object',
properties: {
apiKeys: { $ref: '#/definitions/awsApiGatewayApiKeys' },
apiKeySourceType: {
anyOf: ['HEADER', 'AUTHORIZER'].map(caseInsensitive),
},
binaryMediaTypes: {
type: 'array',
items: { type: 'string', pattern: '^\\S+\\/\\S+$' },
},
description: { type: 'string' },
disableDefaultEndpoint: { type: 'boolean' },
metrics: { type: 'boolean' },
minimumCompressionSize: { type: 'integer', minimum: 0, maximum: 10485760 },
resourcePolicy: { $ref: '#/definitions/awsResourcePolicyStatements' },
restApiId: { $ref: '#/definitions/awsCfInstruction' },
restApiResources: {
anyOf: [
{
type: 'array',
items: {
type: 'object',
properties: {
path: { type: 'string' },
resourceId: { type: 'string' },
},
required: [],
additionalProperties: false,
},
},
{ type: 'object' },
],
},
restApiRootResourceId: { $ref: '#/definitions/awsCfInstruction' },
request: {
type: 'object',
properties: {
schemas: {
type: 'object',
additionalProperties: {
type: 'object',
properties: {
schema: { type: 'object' },
name: { type: 'string' },
description: { type: 'string' },
},
required: ['schema'],
additionalProperties: false,
},
},
},
additionalProperties: false,
},
shouldStartNameWithService: { type: 'boolean' },
usagePlan: {
anyOf: [
apiGatewayUsagePlan,
{
type: 'array',
items: {
type: 'object',
additionalProperties: apiGatewayUsagePlan,
maxProperties: 1,
},
},
],
},
websocketApiId: { $ref: '#/definitions/awsCfInstruction' },
},
additionalProperties: false,
},
apiName: { type: 'string' },
architecture: { $ref: '#/definitions/awsLambdaArchitecture' },
cfnRole: { $ref: '#/definitions/awsArn' },
cloudFront: {
type: 'object',
properties: {
cachePolicies: {
type: 'object',
additionalProperties: {
type: 'object',
properties: {
Comment: { type: 'string' },
DefaultTTL: { type: 'integer', minimum: 0 },
MaxTTL: { type: 'integer', minimum: 0 },
MinTTL: { type: 'integer', minimum: 0 },
ParametersInCacheKeyAndForwardedToOrigin: {
type: 'object',
properties: {
CookiesConfig: {
type: 'object',
properties: {
CookieBehavior: { enum: ['none', 'whitelist', 'allExcept', 'all'] },
Cookies: { type: 'array', items: { type: 'string' } },
},
required: ['CookieBehavior'],
additionalProperties: false,
},
EnableAcceptEncodingBrotli: { type: 'boolean' },
EnableAcceptEncodingGzip: { type: 'boolean' },
HeadersConfig: {
type: 'object',
properties: {
HeaderBehavior: { enum: ['none', 'whitelist'] },
Headers: { type: 'array', items: { type: 'string' } },
},
required: ['HeaderBehavior'],
additionalProperties: false,
},
QueryStringsConfig: {
type: 'object',
properties: {
QueryStringBehavior: {
enum: ['none', 'whitelist', 'allExcept', 'all'],
},
QueryStrings: { type: 'array', items: { type: 'string' } },
},
required: ['QueryStringBehavior'],
additionalProperties: false,
},
},
required: [
'CookiesConfig',
'EnableAcceptEncodingGzip',
'HeadersConfig',
'QueryStringsConfig',
],
additionalProperties: false,
},
},
required: [
'DefaultTTL',
'MaxTTL',
'MinTTL',
'ParametersInCacheKeyAndForwardedToOrigin',
],
additionalProperties: false,
},
},
},
additionalProperties: false,
},
deploymentBucket: {
anyOf: [
{ $ref: '#/definitions/awsS3BucketName' },
{
type: 'object',
properties: {
blockPublicAccess: { type: 'boolean' },
skipPolicySetup: { type: 'boolean' },
maxPreviousDeploymentArtifacts: { type: 'integer', minimum: 0 },
name: { $ref: '#/definitions/awsS3BucketName' },
versioning: { type: 'boolean' },
serverSideEncryption: { enum: ['AES256', 'aws:kms'] },
sseCustomerAlgorithim: { type: 'string' },
sseCustomerKey: { type: 'string' },
sseCustomerKeyMD5: { type: 'string' },
sseKMSKeyId: { type: 'string' },
tags: { $ref: '#/definitions/awsResourceTags' },
},
additionalProperties: false,
},
],
},
deploymentPrefix: { type: 'string' },
disableRollback: { type: 'boolean' },
endpointType: {
anyOf: ['REGIONAL', 'EDGE', 'PRIVATE'].map(caseInsensitive),
},
environment: { $ref: '#/definitions/awsLambdaEnvironment' },
eventBridge: {
type: 'object',
properties: {
useCloudFormation: { type: 'boolean' },
},
additionalProperties: false,
},
httpApi: {
type: 'object',
properties: {
authorizers: {
type: 'object',
additionalProperties: {
oneOf: [
{
type: 'object',
properties: {
type: { const: 'jwt' },
name: { type: 'string' },
identitySource: { $ref: '#/definitions/awsCfInstruction' },
issuerUrl: { $ref: '#/definitions/awsCfInstruction' },
audience: {
anyOf: [
{ $ref: '#/definitions/awsCfInstruction' },
{
type: 'array',
items: { $ref: '#/definitions/awsCfInstruction' },
},
],
},
},
required: ['identitySource', 'issuerUrl', 'audience'],
additionalProperties: false,
},
{
type: 'object',
properties: {
type: { const: 'request' },
name: { type: 'string' },
functionName: { type: 'string' },
functionArn: { $ref: '#/definitions/awsCfInstruction' },
managedExternally: { type: 'boolean' },
resultTtlInSeconds: { type: 'integer', minimum: 0, maximum: 3600 },
enableSimpleResponses: { type: 'boolean' },
payloadVersion: { $ref: '#/definitions/awsHttpApiPayload' },
identitySource: {
anyOf: [
{ $ref: '#/definitions/awsCfInstruction' },
{
type: 'array',
items: { $ref: '#/definitions/awsCfInstruction' },
},
],
},
},
required: ['type'],
},
],
},
},
cors: {
anyOf: [
{ type: 'boolean' },
{
type: 'object',
properties: {
allowCredentials: { type: 'boolean' },
allowedHeaders: { type: 'array', items: { type: 'string' } },
allowedMethods: { type: 'array', items: { type: 'string' } },
allowedOrigins: { type: 'array', items: { type: 'string' } },
exposedResponseHeaders: { type: 'array', items: { type: 'string' } },
maxAge: { type: 'integer', minimum: 0 },
},
additionalProperties: false,
},
],
},
id: {
anyOf: [
{ type: 'string' },
{ $ref: '#/definitions/awsCfImportLocallyResolvable' },
],
},
name: { type: 'string' },
payload: { type: 'string' },
metrics: { type: 'boolean' },
useProviderTags: { const: true },
disableDefaultEndpoint: { type: 'boolean' },
shouldStartNameWithService: { type: 'boolean' },
},
additionalProperties: false,
},
iam: {
type: 'object',
properties: {
role: {
anyOf: [
{ $ref: '#/definitions/awsLambdaRole' },
{
type: 'object',
properties: {
name: {
type: 'string',
pattern: '^[A-Za-z0-9/_+=,.@-]{1,64}$',
},
path: {
type: 'string',
pattern: '(^\\/$)|(^\\/[\u0021-\u007f]+\\/$)',
},
managedPolicies: {
type: 'array',
items: { $ref: '#/definitions/awsArn' },
},
statements: { $ref: '#/definitions/awsIamPolicyStatements' },
permissionBoundary: { $ref: '#/definitions/awsArn' },
permissionsBoundary: { $ref: '#/definitions/awsArn' },
tags: { $ref: '#/definitions/awsResourceTags' },
},
additionalProperties: false,
},
],
},
deploymentRole: { $ref: '#/definitions/awsArn' },
},
additionalProperties: false,
},
iamManagedPolicies: { type: 'array', items: { $ref: '#/definitions/awsArn' } },
iamRoleStatements: { $ref: '#/definitions/awsIamPolicyStatements' },
ecr: {
type: 'object',
properties: {
scanOnPush: { type: 'boolean' },
images: {
type: 'object',
patternProperties: {
[imageNamePattern]: {
anyOf: [
{
type: 'object',
properties: {
uri: { $ref: '#/definitions/ecrImageUri' },
path: { type: 'string' },
file: { type: 'string' },
buildArgs: { type: 'object', additionalProperties: { type: 'string' } },
cacheFrom: { type: 'array', items: { type: 'string' } },
platform: { type: 'string' },
},
additionalProperties: false,
},
{
type: 'string',
},
],
},
},
},
},
required: ['images'],
additionalProperties: false,
},
kmsKeyArn: { $ref: '#/definitions/awsKmsArn' },
lambdaHashingVersion: {
type: 'string',
enum: ['20200924', '20201221'],
},
layers: { $ref: '#/definitions/awsLambdaLayers' },
logRetentionInDays: {
$ref: '#/definitions/awsLogRetentionInDays',
},
logs: {
type: 'object',
properties: {
frameworkLambda: { type: 'boolean' },
httpApi: {
anyOf: [
{ type: 'boolean' },
{
type: 'object',
properties: {
format: { type: 'string' },
},
additionalProperties: false,
},
],
},
restApi: {
anyOf: [
{ type: 'boolean' },
{
type: 'object',
properties: {
accessLogging: { type: 'boolean' },
executionLogging: { type: 'boolean' },
format: { type: 'string' },
fullExecutionData: { type: 'boolean' },
level: { enum: ['INFO', 'ERROR'] },
role: { $ref: '#/definitions/awsArn' },
roleManagedExternally: { type: 'boolean' },
},
additionalProperties: false,
},
],
},
websocket: {
anyOf: [
{ type: 'boolean' },
{
type: 'object',
properties: {
accessLogging: { type: 'boolean' },
executionLogging: { type: 'boolean' },
format: { type: 'string' },
fullExecutionData: { type: 'boolean' },
level: { enum: ['INFO', 'ERROR'] },
},
additionalProperties: false,
},
],
},
},
},
memorySize: { $ref: '#/definitions/awsLambdaMemorySize' },
notificationArns: { type: 'array', items: { $ref: '#/definitions/awsArnString' } },
profile: { type: 'string' },
region: {
enum: [
'us-east-1',
'us-east-2',
'us-gov-east-1',
'us-gov-west-1',
'us-iso-east-1',
'us-iso-west-1',
'us-isob-east-1',
'us-west-1',
'us-west-2',
'af-south-1',
'ap-east-1',
'ap-northeast-1',
'ap-northeast-2',
'ap-northeast-3',
'ap-south-1',
'ap-southeast-1',
'ap-southeast-2',
'ap-southeast-3',
'ca-central-1',
'cn-north-1',
'cn-northwest-1',
'eu-central-1',
'eu-north-1',
'eu-south-1',
'eu-west-1',
'eu-west-2',
'eu-west-3',
'me-south-1',
'sa-east-1',
],
},
role: { $ref: '#/definitions/awsLambdaRole' },
rolePermissionsBoundary: { $ref: '#/definitions/awsArnString' },
rollbackConfiguration: {
type: 'object',
properties: {
RollbackTriggers: {
type: 'array',
items: {
type: 'object',
properties: {
Arn: { $ref: '#/definitions/awsArnString' },
Type: { const: 'AWS::CloudWatch::Alarm' },
},
additionalProperties: false,
required: ['Arn', 'Type'],
},
},
MonitoringTimeInMinutes: { type: 'integer', minimum: 0 },
},
additionalProperties: false,
},
runtime: { $ref: '#/definitions/awsLambdaRuntime' },
deploymentMethod: { enum: ['changesets', 'direct'] },
s3: {
type: 'object',
additionalProperties: require('./package/compile/events/s3/config-schema'),
},
stage: { $ref: '#/definitions/stage' },
stackName: {
type: 'string',
pattern: '^[a-zA-Z][a-zA-Z0-9-]*$',
maxLength: 128,
},
stackParameters: {
type: 'array',
items: {
type: 'object',
properties: {
ParameterKey: { type: 'string' },
ParameterValue: { type: 'string' },
UsePreviousValue: { type: 'boolean' },
ResolvedValue: { type: 'string' },
},
additionalProperties: false,
},
},
stackPolicy: { $ref: '#/definitions/awsIamPolicyStatements' },
stackTags: { $ref: '#/definitions/awsResourceTags' },
tags: { $ref: '#/definitions/awsResourceTags' },
timeout: { $ref: '#/definitions/awsLambdaTimeout' },
tracing: {
type: 'object',
properties: {
apiGateway: { type: 'boolean' },
lambda: { $ref: '#/definitions/awsLambdaTracing' },
},
additionalProperties: false,
},
vpc: { $ref: '#/definitions/awsLambdaVpcConfig' },
vpcEndpointIds: { $ref: '#/definitions/awsCfArrayInstruction' },
versionFunctions: { $ref: '#/definitions/awsLambdaVersioning' },
websocket: {
type: 'object',
properties: {
useProviderTags: { type: 'boolean' },
},
additionalProperties: false,
},
websocketsApiName: { type: 'string' },
websocketsApiRouteSelectionExpression: { type: 'string' },
websocketsDescription: { type: 'string' },
},
},
function: {
properties: {
architecture: { $ref: '#/definitions/awsLambdaArchitecture' },
awsKmsKeyArn: { $ref: '#/definitions/awsKmsArn' },
condition: { $ref: '#/definitions/awsResourceCondition' },
dependsOn: { $ref: '#/definitions/awsResourceDependsOn' },
description: { type: 'string', maxLength: 256 },
destinations: {
type: 'object',
properties: {
onSuccess: {
anyOf: [
{ type: 'string', minLength: 1 },
{
type: 'object',
properties: {
arn: { $ref: '#/definitions/awsCfFunction' },
type: { enum: ['function', 'sns', 'sqs', 'eventBus'] },
},
additionalProperties: false,
required: ['arn', 'type'],
},
],
},
onFailure: {
anyOf: [
{ type: 'string', minLength: 1 },
{
type: 'object',
properties: {
arn: { $ref: '#/definitions/awsCfFunction' },
type: { enum: ['function', 'sns', 'sqs', 'eventBus'] },
},
additionalProperties: false,
required: ['arn', 'type'],
},
],
},
},
additionalProperties: false,
},
disableLogs: { type: 'boolean' },
environment: { $ref: '#/definitions/awsLambdaEnvironment' },
ephemeralStorageSize: { type: 'integer', minimum: 512, maximum: 10240 },
fileSystemConfig: {
type: 'object',
properties: {
arn: {
anyOf: [
{
type: 'string',
pattern:
'^arn:aws[a-zA-Z-]*:elasticfilesystem:[a-z]{2}((-gov)|(-iso(b?)))?-[a-z]+-[1-9]{1}:[0-9]{12}:access-point/fsap-[a-f0-9]{17}$',
},
{ $ref: '#/definitions/awsCfGetAtt' },
{ $ref: '#/definitions/awsCfJoin' },
{ $ref: '#/definitions/awsCfImport' },
],
},
localMountPath: { type: 'string', pattern: '^/mnt/[a-zA-Z0-9-_.]+$' },
},
additionalProperties: false,
required: ['localMountPath', 'arn'],
},
handler: { type: 'string' },
image: {
anyOf: [
{ $ref: '#/definitions/ecrImageUri' },
{
type: 'string',
pattern: imageNamePattern,
},
{
type: 'object',
properties: {
name: {
type: 'string',
pattern: imageNamePattern,
},
uri: { $ref: '#/definitions/ecrImageUri' },
workingDirectory: {
type: 'string',
},
command: {
type: 'array',
items: {
type: 'string',
},
},
entryPoint: {
type: 'array',
items: {
type: 'string',
},
},
},
additionalProperties: false,
},
],
},
kmsKeyArn: { $ref: '#/definitions/awsKmsArn' },
layers: { $ref: '#/definitions/awsLambdaLayers' },
logRetentionInDays: {
$ref: '#/definitions/awsLogRetentionInDays',
},
maximumEventAge: { type: 'integer', minimum: 60, maximum: 21600 },
maximumRetryAttempts: { type: 'integer', minimum: 0, maximum: 2 },
memorySize: { $ref: '#/definitions/awsLambdaMemorySize' },
onError: {
anyOf: [
{ type: 'string', pattern: '^arn:aws[a-z-]*:sns' },
{ $ref: '#/definitions/awsCfFunction' },
],
},
package: {
type: 'object',
properties: {
artifact: { type: 'string' },
exclude: { type: 'array', items: { type: 'string' } },
include: { type: 'array', items: { type: 'string' } },
individually: { type: 'boolean' },
patterns: { type: 'array', items: { type: 'string' } },
},
additionalProperties: false,
},
provisionedConcurrency: { type: 'integer', minimum: 0 },
reservedConcurrency: cfValue({ type: 'integer', minimum: 0 }),
role: { $ref: '#/definitions/awsLambdaRole' },
runtime: { $ref: '#/definitions/awsLambdaRuntime' },
tags: { $ref: '#/definitions/awsResourceTags' },
timeout: { $ref: '#/definitions/awsLambdaTimeout' },
tracing: { $ref: '#/definitions/awsLambdaTracing' },
url: {
anyOf: [
{ type: 'boolean' },
{
type: 'object',
properties: {
authorizer: { type: 'string', enum: ['aws_iam'] },
cors: {
anyOf: [
{ type: 'boolean' },
{
type: 'object',
properties: {
allowCredentials: { type: 'boolean' },
allowedHeaders: {
type: 'array',
minItems: 1,
maxItems: 100,
items: { type: 'string' },
},
allowedMethods: {
type: 'array',
minItems: 1,
maxItems: 6,
items: { type: 'string' },
},
allowedOrigins: {
type: 'array',
minItems: 1,
maxItems: 100,
items: { type: 'string' },
},
exposedResponseHeaders: {
type: 'array',
minItems: 1,
maxItems: 100,
items: { type: 'string' },
},
maxAge: { type: 'integer', minimum: 0 },
},
additionalProperties: false,
},
],
},
},
additionalProperties: false,
},
],
},
versionFunction: { $ref: '#/definitions/awsLambdaVersioning' },
vpc: { $ref: '#/definitions/awsLambdaVpcConfig' },
httpApi: {
type: 'object',
properties: {
payload: { $ref: '#/definitions/awsHttpApiPayload' },
},
additionalProperties: false,
},
},
additionalProperties: false,
},
layers: {
type: 'object',
additionalProperties: {
type: 'object',
properties: {
allowedAccounts: {
type: 'array',
items: {
type: 'string',
pattern: '^(\\d{12}|\\*|arn:(aws[a-zA-Z-]*):iam::\\d{12}:root)$',
},
},
compatibleArchitectures: {
type: 'array',
items: { $ref: '#/definitions/awsLambdaArchitecture' },
maxItems: 2,
},
compatibleRuntimes: {
type: 'array',
items: { $ref: '#/definitions/awsLambdaRuntime' },
maxItems: 5,
},
description: { type: 'string', maxLength: 256 },
licenseInfo: { type: 'string', maxLength: 512 },
name: {
type: 'string',
minLength: 1,
maxLength: 140,
pattern:
'^((arn:[a-zA-Z0-9-]+:lambda:[a-zA-Z0-9-]+:\\d{12}:layer:[a-zA-Z0-9-_]+)|[a-zA-Z0-9-_]+)$',
},
package: {
type: 'object',
properties: {
artifact: { type: 'string' },
exclude: { type: 'array', items: { type: 'string' } },
include: { type: 'array', items: { type: 'string' } },
patterns: { type: 'array', items: { type: 'string' } },
},
additionalProperties: false,
},
path: { type: 'string' },
retain: { type: 'boolean' },
},
additionalProperties: false,
},
},
resources: {
type: 'object',
properties: {
AWSTemplateFormatVersion: {
type: 'string',
},
Conditions: {
type: 'object',
},
Description: {
type: 'string',
},
Mappings: {
type: 'object',
},
Metadata: {
type: 'object',
},
// According to https://s3.amazonaws.com/cfn-resource-specifications-us-east-1-prod/schemas/2.15.0/all-spec.json
// `Outputs` is just an "object", though it seems like this is under-specifying that section a bit.
// See also https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/outputs-section-structure.html
Outputs: {
type: 'object',
},
Parameters: {
type: 'object',
},
// Not replicating the full JSON schema from https://s3.amazonaws.com/cfn-resource-specifications-us-east-1-prod/schemas/2.15.0/all-spec.json
// as that gets into the specifics for each resource type.
//
// The only required attribute is `Type`; `Properties` and other common attributes are optional.
// See also https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/resources-section-structure.html
Resources: {
type: 'object',
properties: {
'Fn::Transform': {
type: 'object',
properties: {
Name: { type: 'string' },
Parameters: { type: 'object' },
},
required: ['Name'],
additionalProperties: false,
},
},
patternProperties: {
'^[a-zA-Z0-9]{1,255}$': {
type: 'object',
properties: {
Type: { type: 'string' },
Properties: { type: 'object' },
CreationPolicy: { type: 'object' },
DeletionPolicy: { type: 'string' },
DependsOn: { $ref: '#/definitions/awsResourceDependsOn' },
Metadata: { type: 'object' },
UpdatePolicy: { type: 'object' },
UpdateReplacePolicy: { type: 'string' },
Condition: { $ref: '#/definitions/awsResourceCondition' },
},
required: ['Type'],
additionalProperties: false,
},
},
additionalProperties: false,
},
Transform: {
type: 'array',
items: { type: 'string' },
},
extensions: {
type: 'object',
patternProperties: {
// names have the same restrictions as CloudFormation Resources section
'^[a-zA-Z0-9]{1,255}$': {
type: 'object',
// this lists the supported properties, other properties are "Not supported. An error will be thrown
// if you try to extend an unsupported attribute."
// this is different than the above schema for `Resources`, which allows the `Type` attribute.
// extensions are explicitly meant to extend the definition of existing resources.
properties: {
Properties: { type: 'object' },
CreationPolicy: { type: 'object' },
DeletionPolicy: { type: 'string' },
DependsOn: { $ref: '#/definitions/awsResourceDependsOn' },
Metadata: { type: 'object' },
UpdatePolicy: { type: 'object' },
UpdateReplacePolicy: { type: 'string' },
Condition: { $ref: '#/definitions/awsResourceCondition' },
},
additionalProperties: false,
},
},
additionalProperties: false,
},
},
additionalProperties: false,
},
});
}
// 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);
}
static getProviderName() {
return constants.providerName;
}
/**
* Execute an AWS request by calling the AWS SDK
* @param {string} service - Service name
* @param {string} method - Method name
* @param {Object} params - Parameters
* @param {Object} [options] - Options to modify the request behavior
* @prop [options.useCache] - Utilize cache to retrieve results
* @prop [options.region] - Specify when to request to different region
*/
async request(service, method, params, options) {
// TODO: Determine calling module and log that
const requestOptions = _.isObject(options) ? options : {};
const shouldCache = _.get(requestOptions, 'useCache', false);
// Copy is required as the credentials may be modified during the request
const credentials = Object.assign({}, this.getCredentials());
const serviceOptions = {
name: service,
params: {
...credentials,
region: _.get(requestOptions, 'region', this.getRegion()),
isS3TransferAccelerationEnabled: this.isS3TransferAccelerationEnabled(),
},
};
return (shouldCache ? awsRequest.memoized : awsRequest)(serviceOptions, method, params);
}
/**
* Fetch credentials directly or using a profile from serverless yml configuration or from the
* well known environment variables
* @returns {{region: *}}
*/
getCredentials() {
if (this.cachedCredentials) {
// We have already created the credentials object once, so return it.
return this.cachedCredentials;
}
const result = {};
const stageUpper = this.getStage() ? this.getStage().toUpperCase() : null;
// add specified credentials, overriding with more specific declarations
const awsDefaultProfile = process.env.AWS_DEFAULT_PROFILE || 'default';
try {
impl.addProfileCredentials(result, awsDefaultProfile);
} catch (err) {
if (err.code !== 'UNRECOGNIZED_AWS_PROFILE') throw err;
}
if (this.serverless.service.provider.profile && !this.options['aws-profile']) {
// config profile
impl.addProfileCredentials(result, this.serverless.service.provider.profile);
}
impl.addEnvironmentCredentials(result, 'AWS'); // creds for all stages
impl.addEnvironmentProfile(result, 'AWS');
impl.addEnvironmentCredentials(result, `AWS_${stageUpper}`); // stage specific creds
impl.addEnvironmentProfile(result, `AWS_${stageUpper}`);
if (this.options['aws-profile']) {
impl.addProfileCredentials(result, this.options['aws-profile']); // CLI option profile
}
const deploymentBucketObject = this.serverless.service.provider.deploymentBucketObject;
if (
deploymentBucketObject &&
deploymentBucketObject.serverSideEncryption &&
deploymentBucketObject.serverSideEncryption === 'aws:kms'
) {
result.signatureVersion = 'v4';
}
// Store the credentials to avoid creating them again (messes up MFA).
this.cachedCredentials = result;
return result;
}
// This function will be used to block the addition of transfer acceleration options
// to the cloudformation template for regions where acceleration is not supported (ie, govcloud)
isS3TransferAccelerationSupported() {
// Only enable s3 transfer acceleration for standard regions (non govcloud/china)
// since those regions do not yet support it
const endpoint = getS3EndpointForRegion(this.getRegion());
return endpoint === 's3.amazonaws.com';
}
isS3TransferAccelerationEnabled() {
return !!this.options['aws-s3-accelerate'];
}
isS3TransferAccelerationDisabled() {
return this.options['aws-s3-accelerate'] === false;
}
disableTransferAccelerationForCurrentDeploy() {
delete this.options['aws-s3-accelerate'];
}
getValues(source, objectPaths) {
return objectPaths.map((objectPath) => ({
path: objectPath,
value: _.get(source, objectPath.join('.')),
}));
}
firstValue(values) {
return values.reduce((result, current) => {
return result.value ? result : current;
}, {});
}
getRegionSourceValue() {
const values = this.getValues(this, [
['options', 'region'],
['serverless', 'config', 'region'],
['serverless', 'service', 'provider', 'region'],
]);
return this.firstValue(values);
}
getRegion() {
const defaultRegion = 'us-east-1';
const regionSourceValue = this.getRegionSourceValue();
return regionSourceValue.value || defaultRegion;
}
getRuntimeSourceValue() {
const values = this.getValues(this, [['serverless', 'service', 'provider', 'runtime']]);
return this.firstValue(values);
}
getRuntime(runtime) {
const defaultRuntime = 'nodejs14.x';
const runtimeSourceValue = this.getRuntimeSourceValue();
return runtime || runtimeSourceValue.value || defaultRuntime;
}
getProfileSourceValue() {
const values = this.getValues(this, [
['options', 'aws-profile'],
['options', 'profile'],
['serverless', 'config', 'profile'],
['serverless', 'service', 'provider', 'profile'],
]);
const firstVal = this.firstValue(values);
return firstVal ? firstVal.value : null;
}
getProfile() {
return this.getProfileSourceValue();
}
async getServerlessDeploymentBucketName() {
if (this.serverless.service.provider.deploymentBucket) {
return BbPromise.resolve(this.serverless.service.provider.deploymentBucket);
}
return this.request('CloudFormation', 'describeStackResource', {
StackName: this.naming.getStackName(),
LogicalResourceId: this.naming.getDeploymentBucketLogicalId(),
}).then((result) => result.StackResourceDetail.PhysicalResourceId);
}
getDeploymentPrefix() {
const provider = this.serverless.service.provider;
if (provider.deploymentPrefix === null || provider.deploymentPrefix === undefined) {
return 'serverless';
}
return `${provider.deploymentPrefix}`;
}
getCustomDeploymentRole() {
const { provider } = this.serverless.service;
return _.get(provider, 'iam.deploymentRole', provider.cfnRole);
}
// Check if role is provided as a string or a CF function reference to existing role
isExistingRoleProvided(role) {
return (
typeof role === 'string' ||
(_.isObject(role) && Object.keys(role).some((key) => key.includes('::')))
);
}
getCustomExecutionRole(functionObj) {
const { provider } = this.serverless.service;
if (functionObj.role) return functionObj.role;
const role = _.get(provider, 'iam.role');
if (this.isExistingRoleProvided(role)) return role;
return provider.role;
}
resolveFunctionArn(functionAddress) {
if (isLambdaArn(functionAddress)) return functionAddress;
const functionData = this.serverless.service.getFunction(functionAddress);
if (functionData) {
const logicalId = this.naming.getLambdaLogicalId(functionAddress);
const alias = functionData.targetAlias;
const arnGetter = { 'Fn::GetAtt': [logicalId, 'Arn'] };
if (!alias) return arnGetter;
return { 'Fn::Join': [':', [arnGetter, alias.name]] };
}
throw new ServerlessError(
`Unrecognized function address ${functionAddress}`,
'UNRECOGNIZED_FUNCTION_ADDRESS'
);
}
resolveLayerArtifactName(layerName) {
const serverlessLayerObject = this.serverless.service.getLayer(layerName);
return serverlessLayerObject.package && serverlessLayerObject.package.artifact
? serverlessLayerObject.package.artifact
: path.join(
this.serverless.serviceDir,
'.serverless',
this.provider.naming.getLayerArtifactName(layerName)
);
}
resolveFunctionIamRoleResourceName(functionObj) {
const customRole = this.getCustomExecutionRole(functionObj);
if (customRole) {
if (typeof customRole === 'string') {
// check whether the custom role is an ARN
if (customRole.includes(':')) return null;
return customRole;
}
if (
// otherwise, check if we have an in-service reference to a role ARN
customRole['Fn::GetAtt'] &&
Array.isArray(customRole['Fn::GetAtt']) &&
customRole['Fn::GetAtt'].length === 2 &&
typeof customRole['Fn::GetAtt'][0] === 'string' &&
typeof customRole['Fn::GetAtt'][1] === 'string' &&
customRole['Fn::GetAtt'][1] === 'Arn'
) {
return customRole['Fn::GetAtt'][0];
}
if (
// otherwise, check if we have an import, parameters ref or sub
customRole['Fn::ImportValue'] ||
customRole.Ref ||
customRole['Fn::Sub']
) {
return null;
}
}
return 'IamRoleLambdaExecution';
}
getAlbTargetGroupPrefix() {
const provider = this.serverless.service.provider;
if (!provider.alb || !provider.alb.targetGroupPrefix) {
return '';
}
return provider.alb.targetGroupPrefix;
}
getLogRetentionInDays() {
return this.serverless.service.provider.logRetentionInDays;
}
getStageSourceValue() {
const values = this.getValues(this, [
['options', 'stage'],
['serverless', 'config', 'stage'],
['serverless', 'service', 'provider', 'stage'],
]);
return this.firstValue(values);
}
getStage() {
const defaultStage = 'dev';
const stageSourceValue = this.getStageSourceValue();
return stageSourceValue.value || defaultStage;
}
/**
* Get API Gateway Rest API ID from serverless config
*/
getApiGatewayRestApiId() {
if (
this.serverless.service.provider.apiGateway &&
this.serverless.service.provider.apiGateway.restApiId
) {
return this.serverless.service.provider.apiGateway.restApiId;
}
return { Ref: this.naming.getRestApiLogicalId() };
}
getApiGatewayDescription() {
if (
this.serverless.service.provider.apiGateway &&
this.serverless.service.provider.apiGateway.description
) {
return this.serverless.service.provider.apiGateway.description;
}
return undefined;
}
/**
* Get Rest API Root Resource ID from serverless config
*/
getApiGatewayRestApiRootResourceId() {
if (
this.serverless.service.provider.apiGateway &&
this.serverless.service.provider.apiGateway.restApiRootResourceId
) {
return this.serverless.service.provider.apiGateway.restApiRootResourceId;
}
return { 'Fn::GetAtt': [this.naming.getRestApiLogicalId(), 'RootResourceId'] };
}
/**
* Get Rest API Predefined Resources from serverless config
*/
getApiGatewayPredefinedResources() {
if (
!this.serverless.service.provider.apiGateway ||
!this.serverless.service.provider.apiGateway.restApiResources
) {
return [];
}
if (Array.isArray(this.serverless.service.provider.apiGateway.restApiResources)) {
return this.serverless.service.provider.apiGateway.restApiResources;
}
return Object.keys(this.serverless.service.provider.apiGateway.restApiResources).map((key) => ({
path: key,
resourceId: this.serverless.service.provider.apiGateway.restApiResources[key],
}));
}
/**
* Get API Gateway websocket API ID from serverless config
*/
getApiGatewayWebsocketApiId() {
if (
this.serverless.service.provider.apiGateway &&
this.serverless.service.provider.apiGateway.websocketApiId
) {
return this.serverless.service.provider.apiGateway.websocketApiId;
}
return { Ref: this.naming.getWebsocketsApiLogicalId() };
}
getStackResources(next, resourcesParam) {
let resources = resourcesParam;
const params = {
StackName: this.naming.getStackName(),
};
if (!resources) resources = [];
if (next) params.NextToken = next;
return this.request('CloudFormation', 'listStackResources', params).then((res) => {
const allResources = resources.concat(res.StackResourceSummaries);
if (!res.NextToken) {
return allResources;
}
return this.getStackResources(res.NextToken, allResources);
});
}
async dockerPushToEcr(remoteTag, options = {}) {
const pushDockerArgs = ['push', remoteTag];
try {
const { stdBuffer: pushDockerStdBuffer } = await spawnExt('docker', pushDockerArgs);
if (pushDockerStdBuffer) {
log.info(pushDockerStdBuffer.toString().trimRight());
}
return pushDockerStdBuffer.toString();
} catch (err) {
if (
!options.isLoggedIn &&
err.stdBuffer &&
(err.stdBuffer.includes('no basic auth credentials') ||
err.stdBuffer.includes('authorization token has expired'))
) {
await this.dockerLoginToEcr();
return await this.dockerPushToEcr(remoteTag, { isLoggedIn: true });
}
throw new ServerlessError(
`Encountered error during executing: docker ${pushDockerArgs}\nOutput of the command:\n${err.stdBuffer}`,
'DOCKER_PUSH_ERROR'
);
}
}
}
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();
return result.accountId;
},
{ promise: true }
),
ensureDockerIsAvailable: d(
async () => {
try {
await spawnExt('docker', ['--version']);
} catch (err) {
throw new ServerlessError(
'Could not find Docker installation. Ensure Docker is installed before continuing.',
'DOCKER_COMMAND_NOT_AVAILABLE'
);
}
},
{ promise: true }
),
dockerLoginToEcr: d(
async function () {
const registryId = await this.getAccountId();
const result = await this.request('ECR', 'getAuthorizationToken', {
registryIds: [registryId],
});
const { authorizationToken, proxyEndpoint } = result.authorizationData[0];
const decodedAuthToken = Buffer.from(authorizationToken, 'base64').toString().split(':')[1];
const dockerArgs = [
'login',
'--username',
'AWS',
'--password',
decodedAuthToken,
proxyEndpoint,
];
try {
const { stdBuffer } = await spawnExt('docker', dockerArgs);
log.info('Login to Docker succeeded!');
if (stdBuffer.includes('password will be stored unencrypted')) {
log.warning(
'Docker authentication token will be stored unencrypted in docker config. Configure Docker credential helper to remove this warning.'
);
}
} catch (err) {
throw new ServerlessError(
`Encountered error during executing: docker ${dockerArgs.join(
' '
)}\nOutput of the command:\n${err.stdBuffer}`,
'DOCKER_LOGIN_ERROR'
);
}
},
{ promise: true }
),
getOrCreateEcrRepository: d(
async function (scanOnPush) {
const registryId = await this.getAccountId();
const repositoryName = this.naming.getEcrRepositoryName();
let repositoryUri;
try {
const result = await this.request('ECR', 'describeRepositories', {
repositoryNames: [repositoryName],
registryId,
});
repositoryUri = result.repositories[0].repositoryUri;
} catch (err) {
if (!(err.providerError && err.providerError.code === 'RepositoryNotFoundException')) {
throw err;
}
const result = await this.request('ECR', 'createRepository', {
repositoryName,
imageScanningConfiguration: { scanOnPush },
});
repositoryUri = result.repository.repositoryUri;
}
return {
repositoryUri,
repositoryName,
};
},
{ promise: true }
),
resolveImageUriAndShaFromPath: d(
async function ({
imageName,
imagePath,
imageFilename,
buildArgs,
cacheFrom,
platform,
scanOnPush,
}) {
const imageProgress = progress.get(`containerImage:${imageName}`);
await this.ensureDockerIsAvailable();
let isDockerfileAvailable = false;
const pathToDockerfile = path.resolve(this.serverless.serviceDir, imagePath, imageFilename);
try {
const stats = await fsp.stat(pathToDockerfile);
isDockerfileAvailable = stats.isFile();
} catch {
// pass and handle after catch block
}
if (!isDockerfileAvailable) {
throw new ServerlessError(
`Could not access Dockerfile under path: "${pathToDockerfile}"`,
'DOCKERFILE_NOT_AVAILABLE_ERROR'
);
}
const { repositoryUri, repositoryName } = await this.getOrCreateEcrRepository(scanOnPush);
const localTag = `${repositoryName}:${imageName}`;
const remoteTag = `${repositoryUri}:${imageName}`;
const buildArgsArr = Object.keys(buildArgs)
.map((key) => `${key}=${buildArgs[key]}`)
.reduce((accumulator, current) => [...accumulator, '--build-arg', current], []);
const cacheFromArr = cacheFrom.reduce(
(accumulator, current) => [...accumulator, '--cache-from', current],
[]
);
const buildDockerArgs = [
'build',
'-t',
localTag,
'-f',
pathToDockerfile,
...buildArgsArr,
...cacheFromArr,
imagePath,
];
// This is an optional argument, so we only append to the arguments if "platform" is specified.
if (platform !== '') buildDockerArgs.push(`--platform=${platform}`);
let imageSha;
try {
imageProgress.notice(`Building image "${imageName}"`);
try {
const { stdBuffer: buildDockerStdBuffer } = await spawnExt('docker', buildDockerArgs);
if (buildDockerStdBuffer) {
log.info(buildDockerStdBuffer.toString().trimRight());
}
} catch (err) {
throw new ServerlessError(
`Encountered error during executing: docker ${buildDockerArgs.join(
' '
)}\nOutput of the command:\n${err.stdBuffer}`,
'DOCKER_BUILD_ERROR'
);
}
imageProgress.notice(`Tagging image "${imageName}"`);
const tagDockerArgs = ['tag', localTag, remoteTag];
try {
const { stdBuffer: tagDockerStdBuffer } = await spawnExt('docker', tagDockerArgs);
if (tagDockerStdBuffer) {
log.info(tagDockerStdBuffer.toString().trimRight());
}
} catch (err) {
throw new ServerlessError(
`Encountered error during executing: docker ${tagDockerArgs.join(
' '
)}\nOutput of the command:\n${err.stdBuffer}`,
'DOCKER_TAG_ERROR'
);
}
imageProgress.notice(`Uploading image "${imageName}"`);
const dockerPushOutput = await this.dockerPushToEcr(remoteTag);
// Extract imageSha from `docker push` output
imageSha = dockerPushOutput.match(/(sha256:[a-f0-9]{64})/)[0];
} finally {
imageProgress.remove();
}
return {
functionImageSha: imageSha.slice('sha256:'.length),
functionImageUri: `${repositoryUri}@${imageSha}`,
};
},
{
promise: true,
normalizer: (args) => {
return JSON.stringify(deepSortObjectByKey(args[0]));
},
}
),
resolveImageUriAndShaFromUri: d(
async function (image) {
const providedImageSha = image.split('@')[1];
if (providedImageSha) {
return {
functionImageSha: providedImageSha.slice('sha256:'.length),
functionImageUri: image,
};
}
const [repositoryName, imageTag] = image.slice(image.indexOf('/') + 1).split(':');
const parts = image.split('.');
const registryId = parts[0];
const region = parts[3];
const serviceRegion = this.getRegion();
if (region !== serviceRegion) {
throw new ServerlessError(
`The region "${region}" of the ECR image "${image}" must match provider region "${serviceRegion}".`,
'LAMBDA_ECR_REGION_MISMATCH_ERROR'
);
}
const describeImagesResponse = await this.request('ECR', 'describeImages', {
imageIds: [
{
imageTag,
},
],
repositoryName,
registryId,
});
const imageDigest = describeImagesResponse.imageDetails[0].imageDigest;
const functionImageUri = `${image.split(':')[0]}@${imageDigest}`;
return {
functionImageUri,
functionImageSha: imageDigest.slice('sha256:'.length),
};
},
{ promise: true }
),
resolveImageUriAndSha: d(
async function (functionName) {
const { image } = this.serverless.service.getFunction(functionName);
// Resolve if image on function was defined with uri or with name (reference to image in `provider.ecr.images`)
const resolveImageUriOrName = () => {
let uri;
let name;
if (_.isObject(image)) {
if (!image.uri && !image.name) {
throw new ServerlessError(
`Either "uri" or "name" property needs to be set on image for function: ${functionName}`,
'FUNCTION_IMAGE_NEITHER_URI_NOR_NAME_DEFINED_ERROR'
);
}
if (image.uri && image.name) {
throw new ServerlessError(
`Either "uri" or "name" property (not both) needs to be set on image for function: ${functionName}`,
'FUNCTION_IMAGE_BOTH_URI_AND_NAME_DEFINED_ERROR'
);
}
if (image.uri) {
uri = image.uri;
} else {
name = image.name;
}
} else if (isEcrUri(image)) {
uri = image;
} else {
name = image;
}
return { imageUri: uri, imageName: name };
};
const { imageUri, imageName } = resolveImageUriOrName();
const defaultDockerfile = 'Dockerfile';
const defaultBuildArgs = {};
const defaultCacheFrom = [];
const defaultScanOnPush = false;
const defaultPlatform = '';
if (imageUri) {
return await this.resolveImageUriAndShaFromUri(imageUri);
}
const imageDefinedInProvider = _.get(
this.serverless.service.provider,
`ecr.images.${imageName}`
);
const imageScanDefinedInProvider = _.get(
this.serverless.service.provider,
'ecr.scanOnPush',
defaultScanOnPush
);
if (!imageDefinedInProvider) {
throw new ServerlessError(
`Referenced "${imageName}" not defined in "provider.ecr.images"`,
'REFERENCED_FUNCTION_IMAGE_NOT_DEFINED_IN_PROVIDER'
);
}
if (_.isObject(imageDefinedInProvider)) {
if (!imageDefinedInProvider.uri && !imageDefinedInProvider.path) {
throw new ServerlessError(
`Either "uri" or "path" property needs to be set on image "${imageName}"`,
'ECR_IMAGE_NEITHER_URI_NOR_PATH_DEFINED_ERROR'
);
}
if (imageDefinedInProvider.uri && imageDefinedInProvider.path) {
throw new ServerlessError(
`Either "uri" or "path" property (not both) needs to be set on image "${imageName}"`,
'ECR_IMAGE_BOTH_URI_AND_PATH_DEFINED_ERROR'
);
}
if (imageDefinedInProvider.uri && imageDefinedInProvider.buildArgs) {
throw new ServerlessError(
`The "buildArgs" property cannot be used with "uri" property "${imageName}"`,
'ECR_IMAGE_BOTH_URI_AND_BUILDARGS_DEFINED_ERROR'
);
}
if (imageDefinedInProvider.uri && imageDefinedInProvider.cacheFrom) {
throw new ServerlessError(
`The "cacheFrom" property cannot be used with "uri" property "${imageName}"`,
'ECR_IMAGE_BOTH_URI_AND_CACHEFROM_DEFINED_ERROR'
);
}
if (imageDefinedInProvider.uri && imageDefinedInProvider.platform) {
throw new ServerlessError(
`The "platform" property cannot be used with "uri" property "${imageName}"`,
'ECR_IMAGE_BOTH_URI_AND_PLATFORM_DEFINED_ERROR'
);
}
if (imageDefinedInProvider.path) {
return await this.resolveImageUriAndShaFromPath({
imageName,
imagePath: imageDefinedInProvider.path,
imageFilename: imageDefinedInProvider.file || defaultDockerfile,
buildArgs: imageDefinedInProvider.buildArgs || defaultBuildArgs,
cacheFrom: imageDefinedInProvider.cacheFrom || defaultCacheFrom,
platform: imageDefinedInProvider.platform || defaultPlatform,
scanOnPush: imageScanDefinedInProvider,
});
}
return await this.resolveImageUriAndShaFromUri(imageDefinedInProvider.uri);
}
if (isEcrUri(imageDefinedInProvider)) {
return await this.resolveImageUriAndShaFromUri(imageDefinedInProvider);
}
return await this.resolveImageUriAndShaFromPath({
imageName,
imagePath: imageDefinedInProvider,
imageFilename: defaultDockerfile,
buildArgs: imageDefinedInProvider.buildArgs || defaultBuildArgs,
cacheFrom: imageDefinedInProvider.cacheFrom || defaultCacheFrom,
platform: imageDefinedInProvider.platform || defaultPlatform,
scanOnPush: imageScanDefinedInProvider,
});
},
{ promise: true }
),
})
);
module.exports = AwsProvider;