mirror of
https://github.com/serverless/serverless.git
synced 2025-12-08 19:46:03 +00:00
* fix: remove bluebird from zip-service * fix: remove bluebird and set concurrency limits for packaging
2688 lines
93 KiB
JavaScript
2688 lines
93 KiB
JavaScript
import AWS from '../../aws/sdk-v2.js'
|
|
import _ from 'lodash'
|
|
import naming from './lib/naming.js'
|
|
import fsp from 'fs/promises'
|
|
import getS3EndpointForRegion from './utils/get-s3-endpoint-for-region.js'
|
|
import memoizeeMethods from 'memoizee/methods.js'
|
|
import readline from 'readline'
|
|
import albValidate from './package/compile/events/alb/lib/validate.js'
|
|
import awsS3ConfigSchema from './package/compile/events/s3/config-schema.js'
|
|
import d from 'd'
|
|
import path from 'path'
|
|
import spawnExt from 'child-process-ext/spawn.js'
|
|
import ServerlessError from '../../serverless-error.js'
|
|
import awsRequest from '../../aws/request.js'
|
|
import { cfValue } from '../../utils/aws-schema-get-cf-value.js'
|
|
import reportDeprecatedProperties from '../../utils/report-deprecated-properties.js'
|
|
import deepSortObjectByKey from '../../utils/deep-sort-object-by-key.js'
|
|
import utils from '@serverlessinc/sf-core/src/utils.js'
|
|
|
|
const { log, progress } = utils
|
|
const { ALB_LISTENER_REGEXP } = albValidate
|
|
|
|
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() }
|
|
}
|
|
|
|
function resolveRuntimeManagement(input) {
|
|
if (typeof input === 'string') {
|
|
return {
|
|
mode: input,
|
|
}
|
|
}
|
|
return input
|
|
}
|
|
|
|
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)
|
|
|
|
if ('aws' in serverless.credentialProviders) {
|
|
this.resolveCredentials = serverless.credentialProviders.aws
|
|
} else {
|
|
throw new Error('Credential Resolver must be defined')
|
|
}
|
|
|
|
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()
|
|
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' },
|
|
{ $ref: '#/definitions/awsCfBase64' },
|
|
{ $ref: '#/definitions/awsCfToJsonString' },
|
|
],
|
|
},
|
|
awsCfGetAtt: {
|
|
type: 'object',
|
|
properties: {
|
|
'Fn::GetAtt': {
|
|
type: 'array',
|
|
minItems: 2,
|
|
maxItems: 2,
|
|
items: { type: 'string', minLength: 1 },
|
|
},
|
|
},
|
|
required: ['Fn::GetAtt'],
|
|
additionalProperties: false,
|
|
},
|
|
awsCfGetAZs: {
|
|
type: 'object',
|
|
properties: {
|
|
'Fn::GetAZs': {
|
|
oneOf: [
|
|
{ type: 'string', minLength: 1 },
|
|
{ $ref: '#/definitions/awsCfRef' },
|
|
],
|
|
},
|
|
},
|
|
required: ['Fn::GetAZs'],
|
|
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,
|
|
},
|
|
awsCfSelect: {
|
|
type: 'object',
|
|
properties: {
|
|
'Fn::Select': {
|
|
type: 'array',
|
|
minItems: 2,
|
|
maxItems: 2,
|
|
items: {
|
|
anyOf: [
|
|
{ type: 'number' },
|
|
{ type: 'string' },
|
|
{ type: 'array' },
|
|
{ $ref: '#/definitions/awsCfFindInMap' },
|
|
{ $ref: '#/definitions/awsCfGetAtt' },
|
|
{ $ref: '#/definitions/awsCfGetAZs' },
|
|
{ $ref: '#/definitions/awsCfIf' },
|
|
{ $ref: '#/definitions/awsCfSplit' },
|
|
{ $ref: '#/definitions/awsCfRef' },
|
|
],
|
|
},
|
|
},
|
|
},
|
|
required: ['Fn::Select'],
|
|
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,
|
|
},
|
|
awsCfBase64: {
|
|
type: 'object',
|
|
properties: {
|
|
'Fn::Base64': {},
|
|
},
|
|
},
|
|
awsCfToJsonString: {
|
|
type: 'object',
|
|
properties: {
|
|
'Fn::ToJsonString': {
|
|
anyOf: [{ type: 'object' }, { type: 'array' }],
|
|
},
|
|
},
|
|
required: ['Fn::ToJsonString'],
|
|
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' },
|
|
{ $ref: '#/definitions/awsCfSelect' },
|
|
],
|
|
},
|
|
},
|
|
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',
|
|
'dotnet8',
|
|
'go1.x',
|
|
'java21',
|
|
'java17',
|
|
'java11',
|
|
'java8',
|
|
'java8.al2',
|
|
'nodejs14.x',
|
|
'nodejs16.x',
|
|
'nodejs18.x',
|
|
'nodejs20.x',
|
|
'provided',
|
|
'provided.al2',
|
|
'provided.al2023',
|
|
'python3.7',
|
|
'python3.8',
|
|
'python3.9',
|
|
'python3.10',
|
|
'python3.11',
|
|
'python3.12',
|
|
'ruby2.7',
|
|
'ruby3.2',
|
|
],
|
|
},
|
|
awsLambdaRuntimeManagement: {
|
|
oneOf: [
|
|
{ enum: ['auto', 'onFunctionUpdate'] },
|
|
{
|
|
type: 'object',
|
|
properties: {
|
|
mode: { enum: ['auto', 'onFunctionUpdate', 'manual'] },
|
|
arn: { $ref: '#/definitions/awsArn' },
|
|
},
|
|
additionalProperties: false,
|
|
},
|
|
],
|
|
},
|
|
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,
|
|
],
|
|
},
|
|
awsLambdaLoggingConfiguration: {
|
|
type: 'object',
|
|
properties: {
|
|
applicationLogLevel: {
|
|
type: 'string',
|
|
enum: ['DEBUG', 'ERROR', 'FATAL', 'INFO', 'TRACE', 'WARN'],
|
|
},
|
|
logFormat: { type: 'string', enum: ['JSON', 'Text'] },
|
|
logGroup: {
|
|
type: 'string',
|
|
pattern: '[\\.\\-_/#A-Za-z0-9]+',
|
|
minLength: 1,
|
|
maxLength: 512,
|
|
},
|
|
systemLogLevel: {
|
|
type: 'string',
|
|
enum: ['DEBUG', 'INFO', 'WARN'],
|
|
},
|
|
},
|
|
additionalProperties: false,
|
|
},
|
|
awsLogDataProtectionPolicy: {
|
|
type: 'object',
|
|
properties: {
|
|
Name: { type: 'string' },
|
|
Description: { type: 'string' },
|
|
Version: { type: 'string' },
|
|
Statement: { type: 'array' },
|
|
},
|
|
additionalProperties: false,
|
|
required: ['Name', 'Version', 'Statement'],
|
|
},
|
|
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' },
|
|
stage: { type: 'string' },
|
|
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' },
|
|
},
|
|
buildOptions: {
|
|
type: 'array',
|
|
items: { 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',
|
|
},
|
|
logDataProtectionPolicy: {
|
|
$ref: '#/definitions/awsLogDataProtectionPolicy',
|
|
},
|
|
logs: {
|
|
type: 'object',
|
|
properties: {
|
|
frameworkLambda: { type: 'boolean' },
|
|
lambda: { $ref: '#/definitions/awsLambdaLoggingConfiguration' },
|
|
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-south-2',
|
|
'ap-southeast-1',
|
|
'ap-southeast-2',
|
|
'ap-southeast-3',
|
|
'ap-southeast-4',
|
|
'ca-central-1',
|
|
'cn-north-1',
|
|
'cn-northwest-1',
|
|
'eu-central-1',
|
|
'eu-central-2',
|
|
'eu-north-1',
|
|
'eu-south-1',
|
|
'eu-south-2',
|
|
'eu-west-1',
|
|
'eu-west-2',
|
|
'eu-west-3',
|
|
'il-central-1',
|
|
'me-central-1',
|
|
'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' },
|
|
runtimeManagement: {
|
|
$ref: '#/definitions/awsLambdaRuntimeManagement',
|
|
},
|
|
build: { type: 'string' },
|
|
deploymentMethod: { enum: ['changesets', 'direct'] },
|
|
s3: {
|
|
type: 'object',
|
|
additionalProperties: awsS3ConfigSchema,
|
|
},
|
|
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' },
|
|
kinesis: {
|
|
type: 'object',
|
|
properties: {
|
|
consumerNamingMode: { const: 'serviceSpecific' },
|
|
},
|
|
additionalProperties: false,
|
|
},
|
|
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' },
|
|
snapStart: { type: 'boolean' },
|
|
layers: { $ref: '#/definitions/awsLambdaLayers' },
|
|
logRetentionInDays: {
|
|
$ref: '#/definitions/awsLogRetentionInDays',
|
|
},
|
|
logDataProtectionPolicy: {
|
|
$ref: '#/definitions/awsLogDataProtectionPolicy',
|
|
},
|
|
logs: {
|
|
$ref: '#/definitions/awsLambdaLoggingConfiguration',
|
|
},
|
|
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: cfValue({ type: 'integer', minimum: 0 }),
|
|
reservedConcurrency: cfValue({ type: 'integer', minimum: 0 }),
|
|
role: { $ref: '#/definitions/awsLambdaRole' },
|
|
runtime: { $ref: '#/definitions/awsLambdaRuntime' },
|
|
build: { type: 'string' },
|
|
runtimeManagement: {
|
|
$ref: '#/definitions/awsLambdaRuntimeManagement',
|
|
},
|
|
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,
|
|
},
|
|
],
|
|
},
|
|
invokeMode: {
|
|
type: 'string',
|
|
enum: ['BUFFERED', 'RESPONSE_STREAM'],
|
|
},
|
|
},
|
|
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: 15,
|
|
},
|
|
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 = await 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: *}}
|
|
*/
|
|
async getCredentials() {
|
|
if (this.cachedCredentials) {
|
|
// We have already created the credentials object once, so return it.
|
|
return this.cachedCredentials
|
|
}
|
|
const creds = await this.resolveCredentials()
|
|
|
|
this.cachedCredentials = {
|
|
accessKeyId: creds.accessKeyId,
|
|
secretAccessKey: creds.secretAccessKey,
|
|
sessionToken: creds.sessionToken,
|
|
accountId: creds.accountId,
|
|
callerUserId: creds.callerUserId,
|
|
callerArn: creds.callerArn,
|
|
}
|
|
return this.cachedCredentials
|
|
}
|
|
|
|
// 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 = 'nodejs20.x'
|
|
const runtimeSourceValue = this.getRuntimeSourceValue()
|
|
return runtime || runtimeSourceValue.value || defaultRuntime
|
|
}
|
|
|
|
resolveFunctionRuntimeManagement(functionRuntimeManagement) {
|
|
return {
|
|
mode: 'auto',
|
|
...resolveRuntimeManagement(
|
|
this.serverless.service.provider.runtimeManagement,
|
|
),
|
|
...resolveRuntimeManagement(functionRuntimeManagement),
|
|
}
|
|
}
|
|
|
|
getProfileSourceValue() {
|
|
const values = this.getValues(this, [
|
|
['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 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
|
|
}
|
|
|
|
getLogDataProtectionPolicy() {
|
|
return this.serverless.service.provider.logDataProtectionPolicy
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
getApiGatewayStage() {
|
|
const apiGatewayConfig = this.serverless.service.provider.apiGateway
|
|
return (apiGatewayConfig && apiGatewayConfig.stage) || this.getStage()
|
|
}
|
|
|
|
/**
|
|
* 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,
|
|
buildOptions,
|
|
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,
|
|
...buildOptions,
|
|
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 defaultBuildOptions = []
|
|
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.buildOptions
|
|
) {
|
|
throw new ServerlessError(
|
|
`You can't use the "buildOptions" and the "uri" properties at the same time "${imageName}"`,
|
|
'ECR_IMAGE_URI_AND_BUILDOPTIONS_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,
|
|
buildOptions:
|
|
imageDefinedInProvider.buildOptions || defaultBuildOptions,
|
|
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,
|
|
buildOptions:
|
|
imageDefinedInProvider.buildOptions || defaultBuildOptions,
|
|
cacheFrom: imageDefinedInProvider.cacheFrom || defaultCacheFrom,
|
|
platform: imageDefinedInProvider.platform || defaultPlatform,
|
|
scanOnPush: imageScanDefinedInProvider,
|
|
})
|
|
},
|
|
{ promise: true },
|
|
),
|
|
}),
|
|
)
|
|
|
|
export default AwsProvider
|