serverless
Version:
Serverless Framework - Build web, mobile and IoT applications with serverless architectures using AWS Lambda, Azure Functions, Google CloudFunctions & more
1,314 lines (1,277 loc) • 62.7 kB
JavaScript
'use strict';
const AWS = require('aws-sdk');
const BbPromise = require('bluebird');
const HttpsProxyAgent = require('https-proxy-agent');
const url = require('url');
const chalk = require('chalk');
const _ = require('lodash');
const naming = require('./lib/naming.js');
const https = require('https');
const fs = require('fs');
const objectHash = require('object-hash');
const PromiseQueue = require('promise-queue');
const getS3EndpointForRegion = require('./utils/getS3EndpointForRegion');
const readline = require('readline');
const { ALB_LISTENER_REGEXP } = require('./package/compile/events/alb/lib/validate');
const path = require('path');
const isLambdaArn = RegExp.prototype.test.bind(/^arn:[^:]+:lambda:/);
function caseInsensitive(str) {
return { type: 'string', regexp: new RegExp(`^${str}$`, 'i').toString() };
}
const constants = {
providerName: 'aws',
};
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,
};
PromiseQueue.configure(BbPromise.Promise);
const MAX_RETRIES = (() => {
const userValue = Number(process.env.SLS_AWS_REQUEST_MAX_RETRIES);
return userValue >= 0 ? userValue : 4;
})();
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
* whak-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; // eslint-disable-line no-param-reassign
}
},
/**
* 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 Error(`Profile ${profile} does not exist`);
}
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' },
clientId: { type: 'string' },
clientSecret: { type: 'string' },
issuer: { format: 'uri' },
tokenEndpoint: { format: 'uri' },
userInfoEndpoint: { format: 'uri' },
...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;
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;
}
}
}
},
};
if (this.serverless.service.provider.name === 'aws') {
// Below ideally should be in hooks.intialize, but variables resolution depend on this
this.serverless.service.provider.region = this.getRegion();
require('../../utils/awsSdkPatch');
// TODO: Complete schema, see https://github.com/serverless/serverless/issues/8016
serverless.configSchemaHandler.defineProvider('aws', {
definitions: {
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' },
{
type: 'object',
properties: {
name: { type: 'string' },
value: { type: 'string' },
description: { type: 'string' },
customerId: { type: 'string' },
},
anyOf: [{ required: ['name'] }, { required: ['value'] }],
additionalProperties: false,
},
{
type: 'object',
maxProperties: 1,
additionalProperties: {
type: 'array',
items: { type: 'string' },
},
},
],
},
},
awsArn: {
anyOf: [
{ $ref: '#/definitions/awsArnString' },
{ $ref: '#/definitions/awsCfFunction' },
],
},
awsArnString: {
type: 'string',
pattern: '^arn:',
},
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'],
},
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,
},
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: '*' },
{ type: 'array', items: { $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' },
{
type: 'array',
items: { anyOf: [{ const: '*' }, { $ref: '#/definitions/awsArn' }] },
},
],
},
// 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'] }] },
],
},
},
awsLambdaEnvironment: {
type: 'object',
patternProperties: {
'^[A-Za-z_][a-zA-Z0-9_]*$': {
anyOf: [{ const: '' }, { $ref: '#/definitions/awsCfInstruction' }],
},
},
additionalProperties: false,
},
awsLambdaLayers: {
type: 'array',
items: { $ref: '#/definitions/awsArn' },
},
awsLambdaMemorySize: { type: 'integer', minimum: 128, maximum: 10240 },
awsLambdaRole: {
anyOf: [
{ type: 'string', minLength: 1 },
{ $ref: '#/definitions/awsCfImport' },
{ $ref: '#/definitions/awsCfGetAtt' },
],
},
awsLambdaRuntime: {
enum: [
'dotnetcore2.1',
'dotnetcore3.1',
'go1.x',
'java11',
'java8',
'java8.al2',
'nodejs10.x',
'nodejs12.x',
'provided',
'provided.al2',
'python2.7',
'python3.6',
'python3.7',
'python3.8',
'ruby2.5',
'ruby2.7',
],
},
awsLambdaTimeout: { type: 'integer', minimum: 1, maximum: 900 },
awsLambdaTracing: { anyOf: [{ enum: ['Active', 'PassThrough'] }, { type: 'boolean' }] },
awsLambdaVersionning: { type: 'boolean' },
awsLambdaVpcConfig: {
type: 'object',
properties: {
securityGroupIds: {
type: 'array',
items: { $ref: '#/definitions/awsCfInstruction' },
maxItems: 5,
},
subnetIds: {
type: 'array',
items: { $ref: '#/definitions/awsCfInstruction' },
maxItems: 16,
},
},
},
awsLogGroupName: {
type: 'string',
pattern: '^[/#A-Za-z0-9-_.]+$',
},
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,
},
},
provider: {
properties: {
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' },
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' },
shouldStartNameWithService: { const: true },
usagePlan: {
anyOf: [
apiGatewayUsagePlan,
{
type: 'array',
items: {
type: 'object',
additionalProperties: apiGatewayUsagePlan,
maxProperties: 1,
},
},
],
},
websocketApiId: { $ref: '#/definitions/awsCfInstruction' },
},
additionalProperties: false,
},
apiKeys: { $ref: '#/definitions/awsApiGatewayApiKeys' },
apiName: { type: 'string' },
alb: {
type: 'object',
properties: {
targetGroupPrefix: { type: 'string', maxLength: 16 },
authorizers: {
type: 'object',
additionalProperties: {
anyOf: [oidcAlbAuthorizer, cognitoAlbAuthorizer],
},
},
},
additionalProperties: false,
},
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,
},
},
requires: [
'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' },
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' },
endpointType: {
anyOf: ['REGIONAL', 'EDGE', 'PRIVATE'].map(caseInsensitive),
},
environment: { $ref: '#/definitions/awsLambdaEnvironment' },
httpApi: {
type: 'object',
properties: {
authorizers: {
type: 'object',
additionalProperties: {
type: 'object',
properties: {
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,
},
},
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' },
},
additionalProperties: false,
},
iamManagedPolicies: { type: 'array', items: { $ref: '#/definitions/awsArnString' } },
iamRoleStatements: { $ref: '#/definitions/awsIamPolicyStatements' },
kmsKeyArn: { $ref: '#/definitions/awsKmsArn' },
lambdaHashingVersion: {
type: 'string',
enum: ['20201221'],
},
layers: { $ref: '#/definitions/awsLambdaLayers' },
logRetentionInDays: {
enum: [1, 3, 5, 7, 14, 30, 60, 90, 120, 150, 180, 365, 400, 545, 731, 1827, 3653],
},
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: {
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-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',
'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',
],
},
resourcePolicy: { $ref: '#/definitions/awsResourcePolicyStatements' },
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' },
s3: {
type: 'object',
additionalProperties: require('./package/compile/events/s3/configSchema'),
},
stage: { type: 'string' },
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,
},
usagePlan: {
anyOf: [
apiGatewayUsagePlan,
{
type: 'array',
items: {
type: 'object',
additionalProperties: apiGatewayUsagePlan,
maxProperties: 1,
},
},
],
},
vpc: { $ref: '#/definitions/awsLambdaVpcConfig' },
vpcEndpointIds: {
type: 'array',
items: { $ref: '#/definitions/awsCfInstruction' },
},
versionFunctions: { $ref: '#/definitions/awsLambdaVersionning' },
websocketsApiName: { type: 'string' },
websocketsApiRouteSelectionExpression: { type: 'string' },
},
},
function: {
properties: {
awsKmsKeyArn: { $ref: '#/definitions/awsKmsArn' },
condition: { $ref: '#/definitions/awsResourceCondition' },
dependsOn: { $ref: '#/definitions/awsResourceDependsOn' },
description: { type: 'string', maxLength: 256 },
destinations: {
type: 'object',
properties: {
onSuccess: { type: 'string', minLength: 1 },
onFailure: { type: 'string', minLength: 1 },
},
additionalProperties: false,
},
disableLogs: { type: 'boolean' },
environment: { $ref: '#/definitions/awsLambdaEnvironment' },
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: {
type: 'string',
pattern:
'^\\d+\\.dkr\\.ecr\\.[a-z0-9-]+..amazonaws.com\\/([^@]+)|([^@:]+@sha256:[a-f0-9]{64})$',
},
kmsKeyArn: { $ref: '#/definitions/awsKmsArn' },
layers: { $ref: '#/definitions/awsLambdaLayers' },
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' },
},
additionalProperties: false,
},
provisionedConcurrency: { type: 'integer', minimum: 1 },
reservedConcurrency: { type: 'integer', minimum: 0 },
role: { $ref: '#/definitions/awsLambdaRole' },
runtime: { $ref: '#/definitions/awsLambdaRuntime' },
tags: { $ref: '#/definitions/awsResourceTags' },
timeout: { $ref: '#/definitions/awsLambdaTimeout' },
tracing: { $ref: '#/definitions/awsLambdaTracing' },
versionFunction: { $ref: '#/definitions/awsLambdaVersionning' },
vpc: { $ref: '#/definitions/awsLambdaVpcConfig' },
},
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)$',
},
},
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' } },
},
additionalProperties: false,
},
path: { type: 'string' },
retain: { type: 'boolean' },
},
additionalProperties: false,
},
},
resources: {
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,
},
});
}
this.requestCache = {};
this.requestQueue = new PromiseQueue(2, Infinity);
// Store credentials in this variable to avoid creating them several times (messes up MFA).
this.cachedCredentials = null;
Object.assign(this.naming, naming);
// Activate AWS SDK logging
if (process.env.SLS_DEBUG) {
AWS.config.logger = this.serverless.cli;
}
// Use HTTPS Proxy (Optional)
const proxy =
process.env.proxy ||
process.env.HTTP_PROXY ||
process.env.http_proxy ||
process.env.HTTPS_PROXY ||
process.env.https_proxy;
const proxyOptions = {};
if (proxy) {
Object.assign(proxyOptions, url.parse(proxy));
}
const ca = process.env.ca || process.env.HTTPS_CA || process.env.https_ca;
let caCerts = [];
if (ca) {
// Can be a single certificate or multiple, comma separated.
const caArr = ca.split(',');
// Replace the newline -- https://stackoverflow.com/questions/30400341
caCerts = caCerts.concat(caArr.map((cert) => cert.replace(/\\n/g, '\n')));
}
const cafile = process.env.cafile || process.env.HTTPS_CAFILE || process.env.https_cafile;
if (cafile) {
// Can be a single certificate file path or multiple paths, comma separated.
const caPathArr = cafile.split(',');
caCerts = caCerts.concat(caPathArr.map((cafilePath) => fs.readFileSync(cafilePath.trim())));
}
if (caCerts.length > 0) {
Object.assign(proxyOptions, {
rejectUnauthorized: true,
ca: caCerts,
});
}
// Passes also certifications
if (proxy) {
AWS.config.httpOptions.agent = new HttpsProxyAgent(proxyOptions);
} else if (proxyOptions.ca) {
// Update the agent -- http://docs.aws.amazon.com/sdk-for-javascript/v2/developer-guide/node-registering-certs.html
AWS.config.httpOptions.agent = new https.Agent(proxyOptions);
}
// Configure the AWS Client timeout (Optional). The default is 120000 (2 minutes)
const timeout = process.env.AWS_CLIENT_TIMEOUT || process.env.aws_client_timeout;
if (timeout) {
AWS.config.httpOptions.timeout = parseInt(timeout, 10);
}
}
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
*/
request(service, method, params, options) {
const credentials = Object.assign({}, this.getCredentials());
credentials.region = this.getRegion();
// Make sure options is an object (honors wrong calls of request)
const requestOptions = _.isObject(options) ? options : {};
const shouldCache = _.get(requestOptions, 'useCache', false);
const paramsWithRegion = _.merge({}, params, {
region: _.get(options, 'region'),
});
const paramsHash = objectHash.sha1(paramsWithRegion);
const BASE_BACKOFF = 5000;
const persistentRequest = (f) =>
new BbPromise((resolve, reject) => {
const doCall = (numTry) => {
f()
// We're resembling if/else logic, therefore single `then` instead of `then`/`catch` pair
.then(resolve, (e) => {
const { providerError } = e;
if (
numTry < MAX_RETRIES &&
providerError &&
((providerError.retryable &&
providerError.statusCode !== 403 &&
providerError.code !== 'CredentialsError') ||
providerError.statusCode === 429)
) {
const nextTryNum = numTry + 1;
const jitter = Math.random() * 3000 - 1000;
// backoff is between 4 and 7 seconds
const backOff = BASE_BACKOFF + jitter;
this.serverless.cli.log(
[
`Recoverable error occurred (${e.message}), sleeping for ~${Math.round(
backOff / 1000
)} seconds.`,
`Try ${nextTryNum} of ${MAX_RETRIES}`,
].join(' ')
);
setTimeout(doCall, backOff, nextTryNum);
} else {
reject(e);
}
});
};
return doCall(0);
});
// Emit a warning for misuses of the old signature including stage and region
// TODO: Determine calling module and log that
if (process.env.SLS_DEBUG && options != null && !_.isObject(options)) {
this.serverless.cli.log('WARNING: Inappropriate call of provider.request()');
}
// Support S3 Transfer Acceleration
if (this.canUseS3TransferAcceleration(service, method)) {
this.enableS3TransferAcceleration(credentials);
}
if (shouldCache) {
const cachedRequest = _.get(this.requestCache, `${service}.${method}.${paramsHash}`);
if (cachedRequest) {
return BbPromise.resolve(cachedRequest);
}
}
const request = this.requestQueue.add(() =>
persistentRequest(() => {
if (options && options.region) {
credentials.region = options.region;
}
const Service = _.get(this.sdk, service);
const awsService = new Service(credentials);
const req = awsService[method](params);
// TODO: Add listeners, put Debug statements here...
// req.on('send', function (r) {console.log(r)});
const promise = req.promise
? req.promise()
: BbPromise.fromCallback((cb) => {
req.send(cb);
});
return promise.catch((err) => {
let message = err.message != null ? err.message : String(err.code);
if (message.startsWith('Missing credentials in config')) {
// Credentials error
// If failed at last resort (EC2 Metadata check) expose a meaningful error
// with link to AWS documentation
// Otherwise, it's likely that user relied on some AWS creds, which appeared not correct
// therefore expose an AWS message directly
let bottomError