From d96015a6244de5a3294aaa6d7a2e481dbf7194ca Mon Sep 17 00:00:00 2001 From: Graham Campbell Date: Sun, 8 Jun 2025 18:49:01 +0100 Subject: [PATCH 01/27] Get started on aws sdk v3 upgrade --- lib/aws/client-factory.js | 99 ++++++++ lib/aws/commands.js | 264 +++++++++++++++++++++ lib/aws/config.js | 182 ++++++++++++++ lib/aws/error-utils.js | 219 +++++++++++++++++ lib/plugins/aws/deploy/lib/create-stack.js | 23 +- lib/plugins/aws/provider.js | 95 ++++++++ package.json | 7 + 7 files changed, 883 insertions(+), 6 deletions(-) create mode 100644 lib/aws/client-factory.js create mode 100644 lib/aws/commands.js create mode 100644 lib/aws/config.js create mode 100644 lib/aws/error-utils.js diff --git a/lib/aws/client-factory.js b/lib/aws/client-factory.js new file mode 100644 index 0000000000..c450d0d0fb --- /dev/null +++ b/lib/aws/client-factory.js @@ -0,0 +1,99 @@ +'use strict'; + +const { APIGatewayClient } = require('@aws-sdk/client-api-gateway'); +const { ApiGatewayV2Client } = require('@aws-sdk/client-apigatewayv2'); +const { CloudFormationClient } = require('@aws-sdk/client-cloudformation'); +const { CloudWatchClient } = require('@aws-sdk/client-cloudwatch'); +const { CloudWatchLogsClient } = require('@aws-sdk/client-cloudwatch-logs'); +const { CognitoIdentityProviderClient } = require('@aws-sdk/client-cognito-identity-provider'); +const { ECRClient } = require('@aws-sdk/client-ecr'); +const { EventBridgeClient } = require('@aws-sdk/client-eventbridge'); +const { IAMClient } = require('@aws-sdk/client-iam'); +const { LambdaClient } = require('@aws-sdk/client-lambda'); +const { S3Client } = require('@aws-sdk/client-s3'); +const { SSMClient } = require('@aws-sdk/client-ssm'); + +// Map service names to their client classes +const CLIENT_MAP = { + APIGateway: APIGatewayClient, + ApiGatewayV2: ApiGatewayV2Client, + CloudFormation: CloudFormationClient, + CloudWatch: CloudWatchClient, + CloudWatchLogs: CloudWatchLogsClient, + CognitoIdentityProvider: CognitoIdentityProviderClient, + ECR: ECRClient, + EventBridge: EventBridgeClient, + IAM: IAMClient, + Lambda: LambdaClient, + S3: S3Client, + SSM: SSMClient, +}; + +class AWSClientFactory { + constructor(baseConfig = {}) { + this.baseConfig = baseConfig; + this.clients = new Map(); + } + + /** + * Get a configured AWS service client + * @param {string} serviceName - Name of the AWS service (e.g., 'S3', 'CloudFormation') + * @param {Object} overrideConfig - Configuration to override base config + * @returns {Object} AWS SDK v3 client instance + */ + getClient(serviceName, overrideConfig = {}) { + const ClientClass = CLIENT_MAP[serviceName]; + if (!ClientClass) { + throw new Error(`Unknown AWS service: ${serviceName}`); + } + + // Create a cache key based on service and config + const configKey = JSON.stringify({ serviceName, ...this.baseConfig, ...overrideConfig }); + + if (!this.clients.has(configKey)) { + const clientConfig = { ...this.baseConfig, ...overrideConfig }; + this.clients.set(configKey, new ClientClass(clientConfig)); + } + + return this.clients.get(configKey); + } + + /** + * Send a command to an AWS service + * @param {string} serviceName - Name of the AWS service + * @param {Object} command - AWS SDK v3 command instance + * @param {Object} clientConfig - Optional client configuration override + * @returns {Promise} Result of the AWS API call + */ + async send(serviceName, command, clientConfig = {}) { + const client = this.getClient(serviceName, clientConfig); + return client.send(command); + } + + /** + * Clear the client cache + */ + clearCache() { + this.clients.clear(); + } + + /** + * Update the base configuration for all future clients + * @param {Object} newConfig - New base configuration + */ + updateBaseConfig(newConfig) { + this.baseConfig = { ...this.baseConfig, ...newConfig }; + // Clear cache to force recreation with new config + this.clearCache(); + } + + /** + * Get list of supported AWS services + * @returns {Array} Array of supported service names + */ + getSupportedServices() { + return Object.keys(CLIENT_MAP); + } +} + +module.exports = AWSClientFactory; \ No newline at end of file diff --git a/lib/aws/commands.js b/lib/aws/commands.js new file mode 100644 index 0000000000..8dca7f185a --- /dev/null +++ b/lib/aws/commands.js @@ -0,0 +1,264 @@ +'use strict'; + +// API Gateway Commands +const { + GetAccountCommand, + UpdateAccountCommand, + GetApiKeyCommand, + CreateStageCommand, + GetUsagePlansCommand, + UpdateUsagePlanCommand, + TagResourceCommand, + UntagResourceCommand, + UpdateStageCommand, +} = require('@aws-sdk/client-api-gateway'); + +// API Gateway V2 Commands +const { + GetApiCommand, +} = require('@aws-sdk/client-apigatewayv2'); + +// CloudFormation Commands +const { + CreateStackCommand, + CreateChangeSetCommand, + DeleteChangeSetCommand, + ExecuteChangeSetCommand, + UpdateStackCommand, + DeleteStackCommand, + DescribeStacksCommand, + ValidateTemplateCommand, + SetStackPolicyCommand, + GetTemplateCommand, + ListStackResourcesCommand, + DescribeStackResourceCommand, +} = require('@aws-sdk/client-cloudformation'); + +// CloudWatch Commands +const { + GetMetricStatisticsCommand, +} = require('@aws-sdk/client-cloudwatch'); + +// CloudWatch Logs Commands +const { + DescribeLogStreamsCommand, + FilterLogEventsCommand, + DeleteSubscriptionFilterCommand, +} = require('@aws-sdk/client-cloudwatch-logs'); + +// Cognito Identity Provider Commands +const { + ListUserPoolsCommand, + DescribeUserPoolCommand, + UpdateUserPoolCommand, +} = require('@aws-sdk/client-cognito-identity-provider'); + +// ECR Commands +const { + DeleteRepositoryCommand, + DescribeRepositoriesCommand, +} = require('@aws-sdk/client-ecr'); + +// EventBridge Commands +const { + CreateEventBusCommand, + DeleteEventBusCommand, + PutRuleCommand, + DeleteRuleCommand, + PutTargetsCommand, + RemoveTargetsCommand, +} = require('@aws-sdk/client-eventbridge'); + +// IAM Commands +const { + GetRoleCommand, + ListAttachedRolePoliciesCommand, + CreateRoleCommand, + AttachRolePolicyCommand, +} = require('@aws-sdk/client-iam'); + +// Lambda Commands +const { + GetFunctionCommand, + UpdateFunctionConfigurationCommand, + UpdateFunctionCodeCommand, + InvokeCommand, + ListVersionsByFunctionCommand, + GetLayerVersionCommand, + AddPermissionCommand, + RemovePermissionCommand, +} = require('@aws-sdk/client-lambda'); + +// S3 Commands +const { + ListObjectsV2Command, + ListObjectVersionsCommand, + DeleteObjectsCommand, + HeadObjectCommand, + PutObjectCommand, + GetObjectCommand, + GetBucketLocationCommand, + HeadBucketCommand, + GetBucketNotificationConfigurationCommand, + PutBucketNotificationConfigurationCommand, +} = require('@aws-sdk/client-s3'); + +// SSM Commands +const { + GetParameterCommand, +} = require('@aws-sdk/client-ssm'); + +/** + * Map v2 method names to v3 command classes + * Format: { ServiceName: { methodName: CommandClass } } + */ +const COMMAND_MAP = { + APIGateway: { + getAccount: GetAccountCommand, + updateAccount: UpdateAccountCommand, + getApiKey: GetApiKeyCommand, + createStage: CreateStageCommand, + getUsagePlans: GetUsagePlansCommand, + updateUsagePlan: UpdateUsagePlanCommand, + tagResource: TagResourceCommand, + untagResource: UntagResourceCommand, + updateStage: UpdateStageCommand, + }, + + ApiGatewayV2: { + getApi: GetApiCommand, + }, + + CloudFormation: { + createStack: CreateStackCommand, + createChangeSet: CreateChangeSetCommand, + deleteChangeSet: DeleteChangeSetCommand, + executeChangeSet: ExecuteChangeSetCommand, + updateStack: UpdateStackCommand, + deleteStack: DeleteStackCommand, + describeStacks: DescribeStacksCommand, + validateTemplate: ValidateTemplateCommand, + setStackPolicy: SetStackPolicyCommand, + getTemplate: GetTemplateCommand, + listStackResources: ListStackResourcesCommand, + describeStackResource: DescribeStackResourceCommand, + }, + + CloudWatch: { + getMetricStatistics: GetMetricStatisticsCommand, + }, + + CloudWatchLogs: { + describeLogStreams: DescribeLogStreamsCommand, + filterLogEvents: FilterLogEventsCommand, + deleteSubscriptionFilter: DeleteSubscriptionFilterCommand, + }, + + CognitoIdentityProvider: { + listUserPools: ListUserPoolsCommand, + describeUserPool: DescribeUserPoolCommand, + updateUserPool: UpdateUserPoolCommand, + }, + + ECR: { + deleteRepository: DeleteRepositoryCommand, + describeRepositories: DescribeRepositoriesCommand, + }, + + EventBridge: { + createEventBus: CreateEventBusCommand, + deleteEventBus: DeleteEventBusCommand, + putRule: PutRuleCommand, + deleteRule: DeleteRuleCommand, + putTargets: PutTargetsCommand, + removeTargets: RemoveTargetsCommand, + }, + + IAM: { + getRole: GetRoleCommand, + listAttachedRolePolicies: ListAttachedRolePoliciesCommand, + createRole: CreateRoleCommand, + attachRolePolicy: AttachRolePolicyCommand, + }, + + Lambda: { + getFunction: GetFunctionCommand, + updateFunctionConfiguration: UpdateFunctionConfigurationCommand, + updateFunctionCode: UpdateFunctionCodeCommand, + invoke: InvokeCommand, + listVersionsByFunction: ListVersionsByFunctionCommand, + getLayerVersion: GetLayerVersionCommand, + addPermission: AddPermissionCommand, + removePermission: RemovePermissionCommand, + }, + + S3: { + listObjectsV2: ListObjectsV2Command, + listObjectVersions: ListObjectVersionsCommand, + deleteObjects: DeleteObjectsCommand, + headObject: HeadObjectCommand, + putObject: PutObjectCommand, + getObject: GetObjectCommand, + getBucketLocation: GetBucketLocationCommand, + headBucket: HeadBucketCommand, + getBucketNotificationConfiguration: GetBucketNotificationConfigurationCommand, + putBucketNotificationConfiguration: PutBucketNotificationConfigurationCommand, + // Note: upload is handled separately as it's not a direct API call + }, + + SSM: { + getParameter: GetParameterCommand, + }, +}; + +/** + * Get command class for a service method + * @param {string} serviceName - AWS service name + * @param {string} methodName - Method name from v2 SDK + * @returns {Function} Command class constructor + */ +function getCommand(serviceName, methodName) { + const serviceCommands = COMMAND_MAP[serviceName]; + if (!serviceCommands) { + throw new Error(`Unknown AWS service: ${serviceName}`); + } + + const CommandClass = serviceCommands[methodName]; + if (!CommandClass) { + throw new Error(`Unknown method '${methodName}' for service '${serviceName}'`); + } + + return CommandClass; +} + +/** + * Create a command instance for a service method + * @param {string} serviceName - AWS service name + * @param {string} methodName - Method name from v2 SDK + * @param {Object} params - Parameters for the command + * @returns {Object} Command instance + */ +function createCommand(serviceName, methodName, params = {}) { + const CommandClass = getCommand(serviceName, methodName); + return new CommandClass(params); +} + +/** + * Get all available methods for a service + * @param {string} serviceName - AWS service name + * @returns {Array} Array of method names + */ +function getServiceMethods(serviceName) { + const serviceCommands = COMMAND_MAP[serviceName]; + if (!serviceCommands) { + throw new Error(`Unknown AWS service: ${serviceName}`); + } + return Object.keys(serviceCommands); +} + +module.exports = { + COMMAND_MAP, + getCommand, + createCommand, + getServiceMethods, +}; \ No newline at end of file diff --git a/lib/aws/config.js b/lib/aws/config.js new file mode 100644 index 0000000000..2a72ac4725 --- /dev/null +++ b/lib/aws/config.js @@ -0,0 +1,182 @@ +'use strict'; + +const HttpsProxyAgent = require('https-proxy-agent'); +const url = require('url'); +const https = require('https'); +const fs = require('fs'); + +/** + * Build AWS SDK v3 client configuration from environment and options + * @param {Object} options - Configuration options + * @param {string} options.region - AWS region + * @param {Object} options.credentials - AWS credentials + * @param {number} options.maxAttempts - Maximum retry attempts + * @param {string} options.retryMode - Retry mode ('legacy', 'standard', 'adaptive') + * @returns {Object} AWS SDK v3 client configuration + */ +function buildClientConfig(options = {}) { + const config = { + region: options.region || process.env.AWS_REGION || process.env.AWS_DEFAULT_REGION || 'us-east-1', + maxAttempts: options.maxAttempts || getMaxRetries(), + retryMode: options.retryMode || 'standard', + }; + + // Add credentials if provided + if (options.credentials) { + config.credentials = options.credentials; + } + + // Configure HTTP options (proxy, timeout, certificates) + const httpOptions = buildHttpOptions(); + if (httpOptions) { + config.requestHandler = httpOptions; + } + + return config; +} + +/** + * Get maximum retry attempts from environment + * @returns {number} Maximum retry attempts + */ +function getMaxRetries() { + const userValue = Number(process.env.SLS_AWS_REQUEST_MAX_RETRIES); + return userValue >= 0 ? userValue : 4; +} + +/** + * Build HTTP options for AWS SDK v3 clients + * @returns {Object|null} HTTP configuration or null if no special config needed + */ +function buildHttpOptions() { + const httpOptions = {}; + let needsCustomAgent = false; + + // Configure timeout + const timeout = process.env.AWS_CLIENT_TIMEOUT || process.env.aws_client_timeout; + if (timeout) { + httpOptions.requestTimeout = parseInt(timeout, 10); + } + + // Configure proxy + const proxy = getProxyUrl(); + if (proxy) { + httpOptions.httpsAgent = new HttpsProxyAgent(buildProxyOptions(proxy)); + needsCustomAgent = true; + } + + // Configure custom CA certificates + const caCerts = getCACertificates(); + if (caCerts.length > 0) { + const agentOptions = { + rejectUnauthorized: true, + ca: caCerts, + }; + + if (proxy) { + // Merge with existing proxy agent options + Object.assign(httpOptions.httpsAgent.options, agentOptions); + } else { + httpOptions.httpsAgent = new https.Agent(agentOptions); + needsCustomAgent = true; + } + } + + return needsCustomAgent || httpOptions.requestTimeout ? httpOptions : null; +} + +/** + * Get proxy URL from environment variables + * @returns {string|null} Proxy URL or null if not configured + */ +function getProxyUrl() { + return ( + process.env.proxy || + process.env.HTTP_PROXY || + process.env.http_proxy || + process.env.HTTPS_PROXY || + process.env.https_proxy || + null + ); +} + +/** + * Build proxy options for HttpsProxyAgent + * @param {string} proxyUrl - Proxy URL + * @returns {Object} Proxy configuration options + */ +function buildProxyOptions(proxyUrl) { + // not relying on recommended WHATWG URL + // due to missing support for it in https-proxy-agent + // https://github.com/TooTallNate/node-https-proxy-agent/issues/117 + return url.parse(proxyUrl); +} + +/** + * Get CA certificates from environment variables and files + * @returns {Array} Array of CA certificates + */ +function getCACertificates() { + let caCerts = []; + + // Get certificates from environment variable + const ca = process.env.ca || process.env.HTTPS_CA || process.env.https_ca; + 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'))); + } + + // Get certificates from files + 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()))); + } + + return caCerts; +} + +/** + * Get S3-specific client configuration + * @param {Object} baseConfig - Base client configuration + * @param {boolean} useAcceleration - Whether to use S3 transfer acceleration + * @returns {Object} S3 client configuration + */ +function buildS3Config(baseConfig = {}, useAcceleration = false) { + const s3Config = { ...baseConfig }; + + if (useAcceleration) { + s3Config.useAccelerateEndpoint = true; + } + + return s3Config; +} + +/** + * Check if S3 transfer acceleration should be used + * @param {string} method - S3 method name + * @param {Object} params - Request parameters + * @returns {boolean} Whether to use acceleration + */ +function shouldUseS3Acceleration(method, params) { + const accelerationCompatibleMethods = new Set(['upload', 'putObject']); + + return ( + accelerationCompatibleMethods.has(method) && + params && + params.isS3TransferAccelerationEnabled + ); +} + +module.exports = { + buildClientConfig, + buildHttpOptions, + buildS3Config, + shouldUseS3Acceleration, + getMaxRetries, + getProxyUrl, + getCACertificates, +}; \ No newline at end of file diff --git a/lib/aws/error-utils.js b/lib/aws/error-utils.js new file mode 100644 index 0000000000..447410681d --- /dev/null +++ b/lib/aws/error-utils.js @@ -0,0 +1,219 @@ +'use strict'; + +/** + * Error code mappings from v2 to v3 + * v2 errors used 'code' property, v3 uses 'name' property + */ +const ERROR_CODE_MAPPINGS = { + // CloudFormation errors + 'ValidationError': 'ValidationException', + 'AlreadyExistsException': 'AlreadyExistsException', + 'LimitExceededException': 'LimitExceededException', + 'InsufficientCapabilitiesException': 'InsufficientCapabilitiesException', + + // S3 errors + 'NoSuchBucket': 'NoSuchBucket', + 'NoSuchKey': 'NoSuchKey', + 'BucketAlreadyExists': 'BucketAlreadyExists', + 'BucketAlreadyOwnedByYou': 'BucketAlreadyOwnedByYou', + + // Lambda errors + 'ResourceNotFoundException': 'ResourceNotFoundException', + 'ResourceConflictException': 'ResourceConflictException', + 'InvalidParameterValueException': 'InvalidParameterValueException', + 'TooManyRequestsException': 'TooManyRequestsException', + + // IAM errors + 'NoSuchEntity': 'NoSuchEntityException', + 'EntityAlreadyExists': 'EntityAlreadyExistsException', + 'MalformedPolicyDocument': 'MalformedPolicyDocumentException', + + // General AWS errors + 'AccessDenied': 'AccessDeniedException', + 'UnauthorizedOperation': 'UnauthorizedException', + 'Throttling': 'ThrottlingException', + 'RequestExpired': 'RequestExpiredException', + 'CredentialsError': 'CredentialsError', + 'ExpiredTokenException': 'ExpiredTokenException', +}; + +/** + * Transform AWS SDK v3 error to be compatible with v2 error handling + * @param {Error} error - AWS SDK v3 error + * @returns {Error} Transformed error with v2-compatible properties + */ +function transformV3Error(error) { + if (!error || typeof error !== 'object') { + return error; + } + + // If it's already a v2-style error, return as-is + if (error.code && !error.name) { + return error; + } + + // Create a new error object with v2-compatible properties + const transformedError = new Error(error.message || 'Unknown AWS error'); + + // Copy all original properties + Object.assign(transformedError, error); + + // Map v3 'name' to v2 'code' for backward compatibility + if (error.name && !error.code) { + transformedError.code = error.name; + } + + // Map some common v3 properties to v2 equivalents + if (error.$metadata) { + transformedError.statusCode = error.$metadata.httpStatusCode; + transformedError.retryable = isRetryableError(error); + transformedError.requestId = error.$metadata.requestId; + transformedError.cfId = error.$metadata.cfId; + } + + // Add providerError for compatibility with existing error handling + transformedError.providerError = { + ...error, + code: transformedError.code, + statusCode: transformedError.statusCode, + retryable: transformedError.retryable, + }; + + return transformedError; +} + +/** + * Determine if an error is retryable + * @param {Error} error - AWS SDK error + * @returns {boolean} Whether the error is retryable + */ +function isRetryableError(error) { + if (!error) return false; + + // Check metadata for retry info + if (error.$metadata) { + const statusCode = error.$metadata.httpStatusCode; + + // 5xx errors are generally retryable + if (statusCode >= 500) return true; + + // 429 (Too Many Requests) is retryable + if (statusCode === 429) return true; + + // 403 (Forbidden) is generally not retryable + if (statusCode === 403) return false; + } + + // Check error names/codes for specific retryable errors + const errorCode = error.name || error.code; + const retryableErrors = [ + 'ThrottlingException', + 'Throttling', + 'TooManyRequestsException', + 'RequestTimeout', + 'NetworkingError', + 'TimeoutError', + 'InternalError', + 'ServiceUnavailable', + ]; + + return retryableErrors.includes(errorCode); +} + +/** + * Check if error indicates missing credentials + * @param {Error} error - AWS SDK error + * @returns {boolean} Whether error is due to missing credentials + */ +function isCredentialsError(error) { + if (!error) return false; + + const errorCode = error.name || error.code; + const credentialsErrors = [ + 'CredentialsError', + 'NoCredentialsError', + 'ExpiredTokenException', + 'InvalidUserID.NotFound', + 'SignatureDoesNotMatch', + ]; + + return credentialsErrors.includes(errorCode) || + (error.message && error.message.includes('Missing credentials')); +} + +/** + * Get a user-friendly error message for AWS errors + * @param {Error} error - AWS SDK error + * @returns {string} User-friendly error message + */ +function getFriendlyErrorMessage(error) { + if (!error) return 'Unknown error occurred'; + + const errorCode = error.name || error.code; + + // Specific error message mappings + const friendlyMessages = { + 'ValidationException': 'Invalid parameters provided', + 'AlreadyExistsException': 'Resource already exists', + 'NoSuchBucket': 'S3 bucket does not exist', + 'NoSuchKey': 'S3 object does not exist', + 'ResourceNotFoundException': 'AWS resource not found', + 'AccessDeniedException': 'Access denied - check your permissions', + 'ThrottlingException': 'Request rate exceeded - please retry', + 'CredentialsError': 'Invalid AWS credentials', + 'ExpiredTokenException': 'AWS credentials have expired', + }; + + return friendlyMessages[errorCode] || error.message || `AWS Error: ${errorCode}`; +} + +/** + * Extract the root cause error from a chain of errors + * @param {Error} error - Top-level error + * @returns {Error} Root cause error + */ +function getRootCauseError(error) { + let rootError = error; + + while (rootError && (rootError.originalError || rootError.cause)) { + rootError = rootError.originalError || rootError.cause; + } + + return rootError || error; +} + +/** + * Create a standardized error object from various error types + * @param {Error|string} error - Error object or message + * @param {string} service - AWS service name + * @param {string} operation - AWS operation name + * @returns {Error} Standardized error object + */ +function createStandardError(error, service, operation) { + let standardError; + + if (typeof error === 'string') { + standardError = new Error(error); + } else if (error instanceof Error) { + standardError = error; + } else { + standardError = new Error('Unknown error'); + } + + // Add context information + standardError.service = service; + standardError.operation = operation; + standardError.timestamp = new Date().toISOString(); + + return transformV3Error(standardError); +} + +module.exports = { + ERROR_CODE_MAPPINGS, + transformV3Error, + isRetryableError, + isCredentialsError, + getFriendlyErrorMessage, + getRootCauseError, + createStandardError, +}; \ No newline at end of file diff --git a/lib/plugins/aws/deploy/lib/create-stack.js b/lib/plugins/aws/deploy/lib/create-stack.js index f8a2bcc6e2..8faa4791b4 100644 --- a/lib/plugins/aws/deploy/lib/create-stack.js +++ b/lib/plugins/aws/deploy/lib/create-stack.js @@ -18,7 +18,7 @@ module.exports = { const params = this.getCreateStackParams({ templateBody: this.serverless.service.provider.coreCloudFormationTemplate, }); - monitorCfData = await this.provider.request('CloudFormation', 'createStack', params); + monitorCfData = await this._cloudFormationRequest('createStack', params); } else { // Change-set based deployment const changeSetName = this.provider.naming.getStackChangeSetName(); @@ -31,7 +31,7 @@ module.exports = { // Create new change set log.info('Creating new change set'); - await this.provider.request('CloudFormation', 'createChangeSet', createChangeSetParams); + await this._cloudFormationRequest('createChangeSet', createChangeSetParams); // Wait for changeset to be created log.info('Waiting for new change set to be created'); @@ -41,7 +41,7 @@ module.exports = { if (isChangeSetWithoutChanges(changeSetDescription)) { // Cleanup changeset when it does not include any changes log.info('Created change set does not include any changes, removing it'); - await this.provider.request('CloudFormation', 'deleteChangeSet', { + await this._cloudFormationRequest('deleteChangeSet', { StackName: stackName, ChangeSetName: changeSetName, }); @@ -51,12 +51,24 @@ module.exports = { this.provider.didCreateService = true; log.info('Executing created change set'); - await this.provider.request('CloudFormation', 'executeChangeSet', executeChangeSetParams); + await this._cloudFormationRequest('executeChangeSet', executeChangeSetParams); monitorCfData = changeSetDescription; } await this.monitorStack('create', monitorCfData); }, + /** + * Helper method to route CloudFormation requests through v2 or v3 based on feature flag + * @private + */ + async _cloudFormationRequest(method, params) { + // Use v3 if feature flag is enabled, otherwise fallback to v2 + if (this.provider._v3Enabled) { + return this.provider.requestV3('CloudFormation', method, params); + } + return this.provider.request('CloudFormation', method, params); + }, + async createStack() { const stackName = this.provider.naming.getStackName(); if (/^[^a-zA-Z].+|.*[^a-zA-Z0-9-].*/.test(stackName) || stackName.length > 128) { @@ -72,8 +84,7 @@ module.exports = { return BbPromise.bind(this) .then(() => - this.provider - .request('CloudFormation', 'describeStacks', { StackName: stackName }) + this._cloudFormationRequest('describeStacks', { StackName: stackName }) .then((data) => { const shouldCheckStackOutput = // check stack output only if acceleration is requested diff --git a/lib/plugins/aws/provider.js b/lib/plugins/aws/provider.js index 36ac2f6ec7..5360dd8f7a 100644 --- a/lib/plugins/aws/provider.js +++ b/lib/plugins/aws/provider.js @@ -19,6 +19,12 @@ const reportDeprecatedProperties = require('../../utils/report-deprecated-proper const deepSortObjectByKey = require('../../utils/deep-sort-object-by-key'); const { progress, log } = require('@serverless/utils/log'); +// AWS SDK v3 infrastructure +const AWSClientFactory = require('../../aws/client-factory'); +const { createCommand } = require('../../aws/commands'); +const { buildClientConfig, shouldUseS3Acceleration } = require('../../aws/config'); +const { transformV3Error } = require('../../aws/error-utils'); + 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})$/ @@ -222,6 +228,11 @@ class AwsProvider { // Notice: provider.sdk is used by plugins. Do not remove without deprecating first and // offering a reliable alternative this.sdk = AWS; + + // AWS SDK v3 infrastructure + this.clientFactory = null; // Will be initialized when needed + this._v3Enabled = process.env.SLS_AWS_SDK_V3 === 'true'; // Feature flag for gradual rollout + this.serverless.setProvider(constants.providerName, this); this.hooks = { initialize: () => { @@ -1724,6 +1735,90 @@ class AwsProvider { return (shouldCache ? awsRequest.memoized : awsRequest)(serviceOptions, method, params); } + /** + * AWS SDK v3 request method - new interface for gradual migration + * @param {string} service - AWS service name (e.g., 'CloudFormation', 'S3') + * @param {string} method - Method name from v2 SDK (e.g., 'createStack', 'listObjects') + * @param {Object} params - Parameters for the AWS API call + * @param {Object} options - Additional options + * @returns {Promise} AWS API response + */ + async requestV3(service, method, params = {}, options = {}) { + try { + // Initialize client factory if not already done + if (!this.clientFactory) { + this.clientFactory = new AWSClientFactory(this._getV3BaseConfig()); + } + + // Build client configuration + const clientConfig = this._buildV3ClientConfig(service, method, options); + + // Create command + const command = createCommand(service, method, params); + + // Send request + const result = await this.clientFactory.send(service, command, clientConfig); + + return result; + } catch (error) { + // Transform v3 error to be compatible with existing error handling + throw transformV3Error(error); + } + } + + /** + * Get AWS SDK v3 client for direct use + * @param {string} service - AWS service name + * @param {Object} options - Client configuration options + * @returns {Object} AWS SDK v3 client instance + */ + getV3Client(service, options = {}) { + if (!this.clientFactory) { + this.clientFactory = new AWSClientFactory(this._getV3BaseConfig()); + } + + const clientConfig = this._buildV3ClientConfig(service, null, options); + return this.clientFactory.getClient(service, clientConfig); + } + + /** + * Build base configuration for AWS SDK v3 clients + * @private + */ + _getV3BaseConfig() { + const credentials = this.getCredentials(); + return buildClientConfig({ + region: this.getRegion(), + credentials: credentials.accessKeyId ? credentials : undefined, + }); + } + + /** + * Build client-specific configuration for AWS SDK v3 + * @private + */ + _buildV3ClientConfig(service, method, options) { + const baseConfig = this._getV3BaseConfig(); + const requestOptions = _.isObject(options) ? options : {}; + + // Override region if specified + if (requestOptions.region) { + baseConfig.region = requestOptions.region; + } + + // Handle S3-specific configuration + if (service === 'S3' && method) { + const useAcceleration = shouldUseS3Acceleration(method, { + isS3TransferAccelerationEnabled: this.isS3TransferAccelerationEnabled(), + }); + if (useAcceleration) { + baseConfig.useAccelerateEndpoint = true; + } + } + + return baseConfig; + } + /** * Fetch credentials directly or using a profile from serverless yml configuration or from the * well known environment variables diff --git a/package.json b/package.json index 919f6ad1ad..1fd92a18d4 100644 --- a/package.json +++ b/package.json @@ -22,11 +22,18 @@ }, "dependencies": { "@aws-sdk/client-api-gateway": "^3.588.0", + "@aws-sdk/client-apigatewayv2": "^3.588.0", + "@aws-sdk/client-cloudformation": "^3.588.0", + "@aws-sdk/client-cloudwatch": "^3.588.0", + "@aws-sdk/client-cloudwatch-logs": "^3.588.0", "@aws-sdk/client-cognito-identity-provider": "^3.588.0", + "@aws-sdk/client-ecr": "^3.588.0", "@aws-sdk/client-eventbridge": "^3.588.0", "@aws-sdk/client-iam": "^3.588.0", "@aws-sdk/client-lambda": "^3.588.0", "@aws-sdk/client-s3": "^3.588.0", + "@aws-sdk/client-ssm": "^3.588.0", + "@aws-sdk/credential-providers": "^3.588.0", "@serverless/utils": "^6.13.1", "ajv": "^8.12.0", "ajv-formats": "^2.1.1", From cad0e908cc6ff3c84f6ae8cda47a6aef14597f3a Mon Sep 17 00:00:00 2001 From: Graham Campbell Date: Sun, 8 Jun 2025 19:23:37 +0100 Subject: [PATCH 02/27] Complete migration phase 3 --- .../sources/instance-dependent/get-cf.js | 13 ++++++-- .../sources/instance-dependent/get-s3.js | 8 +++-- .../sources/instance-dependent/get-ssm.js | 14 ++++++++- lib/plugins/aws/deploy-function.js | 22 ++++++++++--- lib/plugins/aws/deploy-list.js | 18 +++++++++-- .../aws/deploy/lib/check-for-changes.js | 22 +++++++++---- .../aws/deploy/lib/cleanup-s3-bucket.js | 18 +++++++++-- .../deploy/lib/ensure-valid-bucket-exists.js | 23 ++++++++++---- .../aws/deploy/lib/upload-artifacts.js | 15 +++++++-- .../aws/deploy/lib/validate-template.js | 14 ++++++++- lib/plugins/aws/info/get-api-key-values.js | 16 ++++++++-- lib/plugins/aws/info/get-resource-count.js | 14 ++++++++- lib/plugins/aws/info/get-stack-info.js | 17 ++++++++-- lib/plugins/aws/invoke-local/index.js | 14 ++++++++- lib/plugins/aws/invoke.js | 14 ++++++++- lib/plugins/aws/lib/check-if-bucket-exists.js | 14 ++++++++- .../aws/lib/check-if-ecr-repository-exists.js | 14 ++++++++- lib/plugins/aws/lib/update-stack.js | 31 +++++++++++++------ lib/plugins/aws/lib/upload-zip-file.js | 14 ++++++++- .../aws/lib/wait-for-change-set-creation.js | 18 ++++++++--- lib/plugins/aws/logs.js | 16 ++++++++-- lib/plugins/aws/metrics.js | 14 ++++++++- .../lib/hack/disassociate-usage-plan.js | 18 +++++++++-- .../api-gateway/lib/hack/update-stage.js | 28 ++++++++++++----- lib/plugins/aws/package/compile/layers.js | 14 ++++++++- lib/plugins/aws/remove/lib/bucket.js | 18 +++++++++-- lib/plugins/aws/remove/lib/ecr.js | 14 ++++++++- lib/plugins/aws/remove/lib/stack.js | 14 ++++++++- lib/plugins/aws/rollback-function.js | 18 +++++++++-- lib/plugins/aws/rollback.js | 16 ++++++++-- 30 files changed, 419 insertions(+), 84 deletions(-) diff --git a/lib/configuration/variables/sources/instance-dependent/get-cf.js b/lib/configuration/variables/sources/instance-dependent/get-cf.js index 26a14dce27..b036b8db4c 100644 --- a/lib/configuration/variables/sources/instance-dependent/get-cf.js +++ b/lib/configuration/variables/sources/instance-dependent/get-cf.js @@ -32,14 +32,21 @@ module.exports = (serverlessInstance) => { const result = await (async () => { try { - return await serverlessInstance - .getProvider('aws') - .request( + const provider = serverlessInstance.getProvider('aws'); + if (provider._v3Enabled) { + return await provider.requestV3( 'CloudFormation', 'describeStacks', { StackName: stackName }, { useCache: true, region: params && params[0] } ); + } + return await provider.request( + 'CloudFormation', + 'describeStacks', + { StackName: stackName }, + { useCache: true, region: params && params[0] } + ); } catch (error) { if ( error.code === 'AWS_CLOUD_FORMATION_DESCRIBE_STACKS_VALIDATION_ERROR' && diff --git a/lib/configuration/variables/sources/instance-dependent/get-s3.js b/lib/configuration/variables/sources/instance-dependent/get-s3.js index 32a1204461..9a3e679760 100644 --- a/lib/configuration/variables/sources/instance-dependent/get-s3.js +++ b/lib/configuration/variables/sources/instance-dependent/get-s3.js @@ -31,9 +31,11 @@ module.exports = (serverlessInstance) => { const result = await (async () => { try { - return await serverlessInstance - .getProvider('aws') - .request('S3', 'getObject', { Bucket: bucketName, Key: key }, { useCache: true }); + const provider = serverlessInstance.getProvider('aws'); + if (provider._v3Enabled) { + return await provider.requestV3('S3', 'getObject', { Bucket: bucketName, Key: key }, { useCache: true }); + } + return await provider.request('S3', 'getObject', { Bucket: bucketName, Key: key }, { useCache: true }); } catch (error) { // Check for normalized error code instead of native one if (error.code === 'AWS_S3_GET_OBJECT_NO_SUCH_KEY') return null; diff --git a/lib/configuration/variables/sources/instance-dependent/get-ssm.js b/lib/configuration/variables/sources/instance-dependent/get-ssm.js index 89cc31dfd3..afcda8effa 100644 --- a/lib/configuration/variables/sources/instance-dependent/get-ssm.js +++ b/lib/configuration/variables/sources/instance-dependent/get-ssm.js @@ -26,7 +26,19 @@ module.exports = (serverlessInstance) => { const result = await (async () => { try { - return await serverlessInstance.getProvider('aws').request( + const provider = serverlessInstance.getProvider('aws'); + if (provider._v3Enabled) { + return await provider.requestV3( + 'SSM', + 'getParameter', + { + Name: address, + WithDecryption: !shouldSkipDecryption, + }, + { useCache: true, region } + ); + } + return await provider.request( 'SSM', 'getParameter', { diff --git a/lib/plugins/aws/deploy-function.js b/lib/plugins/aws/deploy-function.js index 821ac8b8b2..700007f501 100644 --- a/lib/plugins/aws/deploy-function.js +++ b/lib/plugins/aws/deploy-function.js @@ -66,6 +66,18 @@ class AwsDeployFunction { }; } + /** + * Helper method to route AWS requests through v2 or v3 based on feature flag + * @private + */ + async _awsRequest(service, method, params) { + // Use v3 if feature flag is enabled, otherwise fallback to v2 + if (this.provider._v3Enabled) { + return this.provider.requestV3(service, method, params); + } + return this.provider.request(service, method, params); + } + async checkIfFunctionExists() { // check if the function exists in the service this.options.functionObj = this.serverless.service.getFunction(this.options.function); @@ -77,7 +89,7 @@ class AwsDeployFunction { const result = await (async () => { try { - return await this.provider.request('Lambda', 'getFunction', params); + return await this._awsRequest('Lambda', 'getFunction', params); } catch (error) { if (_.get(error, 'providerError.code') === 'ResourceNotFoundException') { const errorMessage = [ @@ -142,7 +154,7 @@ class AwsDeployFunction { return `arn:${result.partition}:iam::${result.accountId}:role${compiledFullRoleName}`; } - const data = await this.provider.request('IAM', 'getRole', { + const data = await this._awsRequest('IAM', 'getRole', { RoleName: role['Fn::GetAtt'][0], }); return data.Arn; @@ -156,7 +168,7 @@ class AwsDeployFunction { const startTime = Date.now(); const callWithRetry = async () => { - const result = await this.provider.request('Lambda', 'getFunction', params); + const result = await this._awsRequest('Lambda', 'getFunction', params); if ( result && result.Configuration.State === 'Active' && @@ -184,7 +196,7 @@ class AwsDeployFunction { const callWithRetry = async () => { try { - await this.provider.request('Lambda', 'updateFunctionConfiguration', params); + await this._awsRequest('Lambda', 'updateFunctionConfiguration', params); } catch (err) { const didOneMinutePass = Date.now() - startTime > 60 * 1000; @@ -541,7 +553,7 @@ class AwsDeployFunction { } mainProgress.notice('Deploying', { isMainEvent: true }); - await this.provider.request('Lambda', 'updateFunctionCode', params); + await this._awsRequest('Lambda', 'updateFunctionCode', params); this.shouldEnsureFunctionState = true; log.notice(); log.notice.success( diff --git a/lib/plugins/aws/deploy-list.js b/lib/plugins/aws/deploy-list.js index 320dfd0e44..0848434609 100644 --- a/lib/plugins/aws/deploy-list.js +++ b/lib/plugins/aws/deploy-list.js @@ -26,6 +26,18 @@ class AwsDeployList { }; } + /** + * Helper method to route AWS requests through v2 or v3 based on feature flag + * @private + */ + async _awsRequest(service, method, params) { + // Use v3 if feature flag is enabled, otherwise fallback to v2 + if (this.provider._v3Enabled) { + return this.provider.requestV3(service, method, params); + } + return this.provider.request(service, method, params); + } + async listDeployments() { const service = this.serverless.service.service; const stage = this.provider.getStage(); @@ -33,7 +45,7 @@ class AwsDeployList { let response; try { - response = await this.provider.request('S3', 'listObjectsV2', { + response = await this._awsRequest('S3', 'listObjectsV2', { Bucket: this.bucketName, Prefix: `${prefix}/${service}/${stage}`, }); @@ -94,7 +106,7 @@ class AwsDeployList { FunctionName: funcName, }; - return this.provider.request('Lambda', 'getFunction', params); + return this._awsRequest('Lambda', 'getFunction', params); }) ); @@ -102,7 +114,7 @@ class AwsDeployList { } async getFunctionPaginatedVersions(params, totalVersions) { - const response = await this.provider.request('Lambda', 'listVersionsByFunction', params); + const response = await this._awsRequest('Lambda', 'listVersionsByFunction', params); const Versions = (totalVersions || []).concat(response.Versions); if (response.NextMarker) { diff --git a/lib/plugins/aws/deploy/lib/check-for-changes.js b/lib/plugins/aws/deploy/lib/check-for-changes.js index 704e85c99f..83db18531d 100644 --- a/lib/plugins/aws/deploy/lib/check-for-changes.js +++ b/lib/plugins/aws/deploy/lib/check-for-changes.js @@ -18,6 +18,17 @@ const isDeploymentDirToken = RegExp.prototype.test.bind( const isOtelExtensionName = RegExp.prototype.test.bind(/^sls-otel\.\d+\.\d+\.\d+\.zip$/); module.exports = { + /** + * Helper method to route AWS requests through v2 or v3 based on feature flag + * @private + */ + async _awsRequest(service, method, params) { + // Use v3 if feature flag is enabled, otherwise fallback to v2 + if (this.provider._v3Enabled) { + return this.provider.requestV3(service, method, params); + } + return this.provider.request(service, method, params); + }, async checkForChanges() { this.serverless.service.provider.shouldNotDeploy = false; if (this.options.force) { @@ -56,7 +67,7 @@ module.exports = { const result = await (async () => { try { - return await this.provider.request('S3', 'listObjectsV2', params); + return await this._awsRequest('S3', 'listObjectsV2', params); } catch (error) { if (!error.message.includes('The specified bucket does not exist')) throw error; const stackName = this.provider.naming.getStackName(); @@ -93,8 +104,7 @@ module.exports = { let couldNotAccessFunction = false; const getFunctionResults = this.serverless.service.getAllFunctions().map((funName) => { const functionObj = this.serverless.service.getFunction(funName); - return this.provider - .request('Lambda', 'getFunction', { + return this._awsRequest('Lambda', 'getFunction', { FunctionName: functionObj.name, }) .then((res) => new Date(res.Configuration.LastModified)) @@ -124,7 +134,7 @@ module.exports = { return Promise.all( objects.map(async (obj) => { try { - const result = await this.provider.request('S3', 'headObject', { + const result = await this._awsRequest('S3', 'headObject', { Bucket: this.bucketName, Key: obj.Key, }); @@ -382,7 +392,7 @@ module.exports = { return Promise.all( notMatchedInternalOldSubscriptionFilters.map((oldSubscriptionFilter) => - this.provider.request('CloudWatchLogs', 'deleteSubscriptionFilter', { + this._awsRequest('CloudWatchLogs', 'deleteSubscriptionFilter', { logGroupName, filterName: oldSubscriptionFilter.filterName, }) @@ -400,7 +410,7 @@ module.exports = { async isInternalSubscriptionFilter(stackName, logicalResourceId, physicalResourceId) { try { - const { StackResourceDetail } = await this.provider.request( + const { StackResourceDetail } = await this._awsRequest( 'CloudFormation', 'describeStackResource', { diff --git a/lib/plugins/aws/deploy/lib/cleanup-s3-bucket.js b/lib/plugins/aws/deploy/lib/cleanup-s3-bucket.js index df5ed15999..c3c6775904 100644 --- a/lib/plugins/aws/deploy/lib/cleanup-s3-bucket.js +++ b/lib/plugins/aws/deploy/lib/cleanup-s3-bucket.js @@ -7,6 +7,18 @@ const ServerlessError = require('../../../../serverless-error'); const { log } = require('@serverless/utils/log'); module.exports = { + /** + * Helper method to route S3 requests through v2 or v3 based on feature flag + * @private + */ + async _s3Request(method, params) { + // Use v3 if feature flag is enabled, otherwise fallback to v2 + if (this.provider._v3Enabled) { + return this.provider.requestV3('S3', method, params); + } + return this.provider.request('S3', method, params); + }, + async getObjectsToRemove() { const stacksToKeepCount = _.get( this.serverless, @@ -20,7 +32,7 @@ module.exports = { let response; try { - response = await this.provider.request('S3', 'listObjectsV2', { + response = await this._s3Request('listObjectsV2', { Bucket: this.bucketName, Prefix: `${prefix}/${service}/${stage}`, }); @@ -41,7 +53,7 @@ module.exports = { async removeObjects(objectsToRemove) { if (!objectsToRemove || !objectsToRemove.length) return; - await this.provider.request('S3', 'deleteObjects', { + await this._s3Request('deleteObjects', { Bucket: this.bucketName, Delete: { Objects: objectsToRemove }, }); @@ -61,7 +73,7 @@ module.exports = { async cleanupArtifactsForEmptyChangeSet() { let response; try { - response = await this.provider.request('S3', 'listObjectsV2', { + response = await this._s3Request('listObjectsV2', { Bucket: this.bucketName, Prefix: this.serverless.service.package.artifactDirectoryName, }); diff --git a/lib/plugins/aws/deploy/lib/ensure-valid-bucket-exists.js b/lib/plugins/aws/deploy/lib/ensure-valid-bucket-exists.js index d41e0d1d0a..afb292a8b9 100644 --- a/lib/plugins/aws/deploy/lib/ensure-valid-bucket-exists.js +++ b/lib/plugins/aws/deploy/lib/ensure-valid-bucket-exists.js @@ -7,6 +7,17 @@ const jsyaml = require('js-yaml'); const mainProgress = progress.get('main'); module.exports = { + /** + * Helper method to route AWS requests through v2 or v3 based on feature flag + * @private + */ + async _awsRequest(service, method, params) { + // Use v3 if feature flag is enabled, otherwise fallback to v2 + if (this.provider._v3Enabled) { + return this.provider.requestV3(service, method, params); + } + return this.provider.request(service, method, params); + }, async ensureValidBucketExists() { // Ensure to set bucket name if it can be resolved // Result of this operation will determine how further validation will be performed @@ -27,7 +38,7 @@ module.exports = { if (this.serverless.service.provider.deploymentBucket) { let result; try { - result = await this.provider.request('S3', 'getBucketLocation', { + result = await this._awsRequest('S3', 'getBucketLocation', { Bucket: this.bucketName, }); } catch (err) { @@ -71,7 +82,7 @@ module.exports = { // It covers the case where someone was using custom deployment bucket // but removed that setting from the configuration mainProgress.notice('Ensuring that deployment bucket exists', { isMainEvent: true }); - const getTemplateResult = await this.provider.request('CloudFormation', 'getTemplate', { + const getTemplateResult = await this._awsRequest('CloudFormation', 'getTemplate', { StackName: stackName, TemplateStage: 'Original', }); @@ -116,7 +127,7 @@ module.exports = { if (this.serverless.service.provider.deploymentMethod === 'direct') { const params = this.getUpdateStackParams({ templateBody }); - monitorCfData = await this.provider.request('CloudFormation', 'updateStack', params); + monitorCfData = await this._awsRequest('CloudFormation', 'updateStack', params); } else { const createChangeSetParams = this.getCreateChangeSetParams({ changeSetType: 'UPDATE', @@ -126,14 +137,14 @@ module.exports = { const executeChangeSetParams = this.getExecuteChangeSetParams(); // Ensure that previous change set has been removed - await this.provider.request('CloudFormation', 'deleteChangeSet', { + await this._awsRequest('CloudFormation', 'deleteChangeSet', { StackName: stackName, ChangeSetName: changeSetName, }); log.info('Creating new change set.'); // Create new change set - const changeSet = await this.provider.request( + const changeSet = await this._awsRequest( 'CloudFormation', 'createChangeSet', createChangeSetParams @@ -147,7 +158,7 @@ module.exports = { // that needs to be created as a part of change set // If that would not be the case, that means we have a bug in the logic above - await this.provider.request('CloudFormation', 'executeChangeSet', executeChangeSetParams); + await this._awsRequest('CloudFormation', 'executeChangeSet', executeChangeSetParams); monitorCfData = changeSet; } await this.monitorStack('update', monitorCfData); diff --git a/lib/plugins/aws/deploy/lib/upload-artifacts.js b/lib/plugins/aws/deploy/lib/upload-artifacts.js index fab5a3ecf9..90e8824e9d 100644 --- a/lib/plugins/aws/deploy/lib/upload-artifacts.js +++ b/lib/plugins/aws/deploy/lib/upload-artifacts.js @@ -16,6 +16,17 @@ const MAX_CONCURRENT_ARTIFACTS_UPLOADS = Number(process.env.SLS_MAX_CONCURRENT_ARTIFACTS_UPLOADS) || 3; module.exports = { + /** + * Helper method to route AWS requests through v2 or v3 based on feature flag + * @private + */ + async _awsRequest(service, method, params) { + // Use v3 if feature flag is enabled, otherwise fallback to v2 + if (this.provider._v3Enabled) { + return this.provider.requestV3(service, method, params); + } + return this.provider.request(service, method, params); + }, async getFileStats(filepath) { try { return await fsp.stat(filepath); @@ -72,7 +83,7 @@ module.exports = { params = setS3UploadEncryptionOptions(params, deploymentBucketObject); } - return this.provider.request('S3', 'upload', params); + return this._awsRequest('S3', 'upload', params); }, async uploadStateFile() { log.info('Uploading State file to S3'); @@ -102,7 +113,7 @@ module.exports = { params = setS3UploadEncryptionOptions(params, deploymentBucketObject); } - return this.provider.request('S3', 'upload', params); + return this._awsRequest('S3', 'upload', params); }, async getFunctionArtifactFilePaths() { diff --git a/lib/plugins/aws/deploy/lib/validate-template.js b/lib/plugins/aws/deploy/lib/validate-template.js index 405f4a9c49..d79e09e4f6 100644 --- a/lib/plugins/aws/deploy/lib/validate-template.js +++ b/lib/plugins/aws/deploy/lib/validate-template.js @@ -4,6 +4,18 @@ const getS3EndpointForRegion = require('../../utils/get-s3-endpoint-for-region') const ServerlessError = require('../../../../serverless-error'); module.exports = { + /** + * Helper method to route CloudFormation requests through v2 or v3 based on feature flag + * @private + */ + async _cloudFormationRequest(method, params) { + // Use v3 if feature flag is enabled, otherwise fallback to v2 + if (this.provider._v3Enabled) { + return this.provider.requestV3('CloudFormation', method, params); + } + return this.provider.request('CloudFormation', method, params); + }, + async validateTemplate() { const bucketName = this.bucketName; const artifactDirectoryName = this.serverless.service.package.artifactDirectoryName; @@ -13,7 +25,7 @@ module.exports = { TemplateURL: `https://${s3Endpoint}/${bucketName}/${artifactDirectoryName}/${compiledTemplateFileName}`, }; - return this.provider.request('CloudFormation', 'validateTemplate', params).catch((error) => { + return this._cloudFormationRequest('validateTemplate', params).catch((error) => { const errorMessage = ['The CloudFormation template is invalid:', ` ${error.message}`].join( '' ); diff --git a/lib/plugins/aws/info/get-api-key-values.js b/lib/plugins/aws/info/get-api-key-values.js index 6b7ac5daf6..e5d3cd5104 100644 --- a/lib/plugins/aws/info/get-api-key-values.js +++ b/lib/plugins/aws/info/get-api-key-values.js @@ -4,6 +4,18 @@ const BbPromise = require('bluebird'); const _ = require('lodash'); module.exports = { + /** + * Helper method to route AWS requests through v2 or v3 based on feature flag + * @private + */ + async _awsRequest(service, method, params) { + // Use v3 if feature flag is enabled, otherwise fallback to v2 + if (this.provider._v3Enabled) { + return this.provider.requestV3(service, method, params); + } + return this.provider.request(service, method, params); + }, + async getApiKeyValues() { const info = this.gatheredData.info; info.apiKeys = []; @@ -32,7 +44,7 @@ module.exports = { if (apiKeyNames.length) { return this.provider - .request('CloudFormation', 'describeStackResources', { + ._awsRequest('CloudFormation', 'describeStackResources', { StackName: this.provider.naming.getStackName(), }) .then((resources) => { @@ -42,7 +54,7 @@ module.exports = { .value(); return Promise.all( apiKeys.map((apiKey) => - this.provider.request('APIGateway', 'getApiKey', { + this._awsRequest('APIGateway', 'getApiKey', { apiKey, includeValue: true, }) diff --git a/lib/plugins/aws/info/get-resource-count.js b/lib/plugins/aws/info/get-resource-count.js index 994469a4aa..ad8293340e 100644 --- a/lib/plugins/aws/info/get-resource-count.js +++ b/lib/plugins/aws/info/get-resource-count.js @@ -3,12 +3,24 @@ const BbPromise = require('bluebird'); module.exports = { + /** + * Helper method to route CloudFormation requests through v2 or v3 based on feature flag + * @private + */ + async _cloudFormationRequest(method, params) { + // Use v3 if feature flag is enabled, otherwise fallback to v2 + if (this.provider._v3Enabled) { + return this.provider.requestV3('CloudFormation', method, params); + } + return this.provider.request('CloudFormation', method, params); + }, + async getResourceCount(nextToken, resourceCount = 0) { const params = { StackName: this.provider.naming.getStackName(), NextToken: nextToken, }; - return this.provider.request('CloudFormation', 'listStackResources', params).then((result) => { + return this._cloudFormationRequest('listStackResources', params).then((result) => { if (Object.keys(result).length) { this.gatheredData.info.resourceCount = resourceCount + result.StackResourceSummaries.length; if (result.NextToken) { diff --git a/lib/plugins/aws/info/get-stack-info.js b/lib/plugins/aws/info/get-stack-info.js index ba411feeda..7234748b57 100644 --- a/lib/plugins/aws/info/get-stack-info.js +++ b/lib/plugins/aws/info/get-stack-info.js @@ -5,6 +5,18 @@ const resolveCfImportValue = require('../utils/resolve-cf-import-value'); const ServerlessError = require('../../../serverless-error'); module.exports = { + /** + * Helper method to route AWS requests through v2 or v3 based on feature flag + * @private + */ + async _awsRequest(service, method, params) { + // Use v3 if feature flag is enabled, otherwise fallback to v2 + if (this.provider._v3Enabled) { + return this.provider.requestV3(service, method, params); + } + return this.provider.request(service, method, params); + }, + async getStackInfo() { // NOTE: this is the global gatheredData object which will be passed around this.gatheredData = { @@ -24,8 +36,7 @@ module.exports = { const stackData = {}; const sdkRequests = [ - this.provider - .request('CloudFormation', 'describeStacks', { StackName: stackName }) + this._awsRequest('CloudFormation', 'describeStacks', { StackName: stackName }) .then((result) => { if (result) stackData.outputs = result.Stacks[0].Outputs; }), @@ -39,7 +50,7 @@ module.exports = { : BbPromise.resolve(httpApiId) ) .then((id) => { - return this.provider.request('ApiGatewayV2', 'getApi', { ApiId: id }); + return this._awsRequest('ApiGatewayV2', 'getApi', { ApiId: id }); }) .then( (result) => { diff --git a/lib/plugins/aws/invoke-local/index.js b/lib/plugins/aws/invoke-local/index.js index a21dfadb9c..7bad5db84a 100644 --- a/lib/plugins/aws/invoke-local/index.js +++ b/lib/plugins/aws/invoke-local/index.js @@ -55,6 +55,18 @@ class AwsInvokeLocal { }; } + /** + * Helper method to route Lambda requests through v2 or v3 based on feature flag + * @private + */ + async _lambdaRequest(method, params) { + // Use v3 if feature flag is enabled, otherwise fallback to v2 + if (this.provider._v3Enabled) { + return this.provider.requestV3('Lambda', method, params); + } + return this.provider.request('Lambda', method, params); + } + getRuntime() { return this.provider.getRuntime(this.options.functionObj.runtime); } @@ -376,7 +388,7 @@ class AwsInvokeLocal { layerProgress.notice(`Downloading layer ${layer}`); await ensureDir(path.join(layerContentsCachePath)); - const layerInfo = await this.provider.request('Lambda', 'getLayerVersion', { + const layerInfo = await this._lambdaRequest('getLayerVersion', { LayerName: layerArn, VersionNumber: layerVersion, }); diff --git a/lib/plugins/aws/invoke.js b/lib/plugins/aws/invoke.js index c82195702f..d0b4272f17 100644 --- a/lib/plugins/aws/invoke.js +++ b/lib/plugins/aws/invoke.js @@ -23,6 +23,18 @@ class AwsInvoke { }; } + /** + * Helper method to route Lambda requests through v2 or v3 based on feature flag + * @private + */ + async _lambdaRequest(method, params) { + // Use v3 if feature flag is enabled, otherwise fallback to v2 + if (this.provider._v3Enabled) { + return this.provider.requestV3('Lambda', method, params); + } + return this.provider.request('Lambda', method, params); + } + async validateFile(key) { const absolutePath = path.resolve(this.serverless.serviceDir, this.options[key]); try { @@ -97,7 +109,7 @@ class AwsInvoke { params.Qualifier = this.options.qualifier; } - return this.provider.request('Lambda', 'invoke', params); + return this._lambdaRequest('invoke', params); } log(invocationReply) { diff --git a/lib/plugins/aws/lib/check-if-bucket-exists.js b/lib/plugins/aws/lib/check-if-bucket-exists.js index 6fc07164b1..180f5a0cd8 100644 --- a/lib/plugins/aws/lib/check-if-bucket-exists.js +++ b/lib/plugins/aws/lib/check-if-bucket-exists.js @@ -3,9 +3,21 @@ const ServerlessError = require('../../../serverless-error'); module.exports = { + /** + * Helper method to route S3 requests through v2 or v3 based on feature flag + * @private + */ + async _s3Request(method, params) { + // Use v3 if feature flag is enabled, otherwise fallback to v2 + if (this.provider._v3Enabled) { + return this.provider.requestV3('S3', method, params); + } + return this.provider.request('S3', method, params); + }, + async checkIfBucketExists(bucketName) { try { - await this.provider.request('S3', 'headBucket', { + await this._s3Request('headBucket', { Bucket: bucketName, }); return true; diff --git a/lib/plugins/aws/lib/check-if-ecr-repository-exists.js b/lib/plugins/aws/lib/check-if-ecr-repository-exists.js index 0253a87984..10e4fdc8e6 100644 --- a/lib/plugins/aws/lib/check-if-ecr-repository-exists.js +++ b/lib/plugins/aws/lib/check-if-ecr-repository-exists.js @@ -3,11 +3,23 @@ const { log } = require('@serverless/utils/log'); module.exports = { + /** + * Helper method to route ECR requests through v2 or v3 based on feature flag + * @private + */ + async _ecrRequest(method, params) { + // Use v3 if feature flag is enabled, otherwise fallback to v2 + if (this.provider._v3Enabled) { + return this.provider.requestV3('ECR', method, params); + } + return this.provider.request('ECR', method, params); + }, + async checkIfEcrRepositoryExists() { const registryId = await this.provider.getAccountId(); const repositoryName = this.provider.naming.getEcrRepositoryName(); try { - await this.provider.request('ECR', 'describeRepositories', { + await this._ecrRequest('describeRepositories', { repositoryNames: [repositoryName], registryId, }); diff --git a/lib/plugins/aws/lib/update-stack.js b/lib/plugins/aws/lib/update-stack.js index c5fcf333e0..71ff340620 100644 --- a/lib/plugins/aws/lib/update-stack.js +++ b/lib/plugins/aws/lib/update-stack.js @@ -8,6 +8,17 @@ const isChangeSetWithoutChanges = require('../utils/is-change-set-without-change const NO_UPDATE_MESSAGE = 'No updates are to be performed.'; module.exports = { + /** + * Helper method to route CloudFormation requests through v2 or v3 based on feature flag + * @private + */ + async _cloudFormationRequest(method, params) { + // Use v3 if feature flag is enabled, otherwise fallback to v2 + if (this.provider._v3Enabled) { + return this.provider.requestV3('CloudFormation', method, params); + } + return this.provider.request('CloudFormation', method, params); + }, async createFallback() { this.createLater = false; progress.get('main').notice('Creating CloudFormation stack', { isMainEvent: true }); @@ -23,7 +34,7 @@ module.exports = { templateUrl, }); - monitorCfData = await this.provider.request('CloudFormation', 'createStack', params); + monitorCfData = await this._cloudFormationRequest('createStack', params); } else { const changeSetName = this.provider.naming.getStackChangeSetName(); @@ -37,7 +48,7 @@ module.exports = { // Create new change set this.provider.didCreateService = true; log.info('Creating new change set'); - await this.provider.request('CloudFormation', 'createChangeSet', createChangeSetParams); + await this._cloudFormationRequest('createChangeSet', createChangeSetParams); // Wait for changeset to be created log.info('Waiting for new change set to be created'); @@ -47,7 +58,7 @@ module.exports = { if (isChangeSetWithoutChanges(changeSetDescription)) { // Cleanup changeset when it does not include any changes log.info('Created change set does not include any changes, removing it'); - await this.provider.request('CloudFormation', 'deleteChangeSet', { + await this._cloudFormationRequest('deleteChangeSet', { StackName: stackName, ChangeSetName: changeSetName, }); @@ -56,7 +67,7 @@ module.exports = { } log.info('Executing created change set'); - await this.provider.request('CloudFormation', 'executeChangeSet', executeChangeSetParams); + await this._cloudFormationRequest('executeChangeSet', executeChangeSetParams); monitorCfData = changeSetDescription; } await this.monitorStack('create', monitorCfData); @@ -75,7 +86,7 @@ module.exports = { const params = this.getUpdateStackParams({ templateUrl }); try { - monitorCfData = await this.provider.request('CloudFormation', 'updateStack', params); + monitorCfData = await this._cloudFormationRequest('updateStack', params); } catch (e) { if (e.message.includes(NO_UPDATE_MESSAGE)) { return false; @@ -93,14 +104,14 @@ module.exports = { const executeChangeSetParams = this.getExecuteChangeSetParams(); // Ensure that previous change set has been removed - await this.provider.request('CloudFormation', 'deleteChangeSet', { + await this._cloudFormationRequest('deleteChangeSet', { StackName: stackName, ChangeSetName: changeSetName, }); // Create new change set log.info('Creating new change set'); - await this.provider.request('CloudFormation', 'createChangeSet', createChangeSetParams); + await this._cloudFormationRequest('createChangeSet', createChangeSetParams); // Wait for changeset to be created log.info('Waiting for new change set to be created'); @@ -110,7 +121,7 @@ module.exports = { if (isChangeSetWithoutChanges(changeSetDescription)) { // Cleanup changeset when it does not include any changes log.info('Created change set does not include any changes, removing it'); - await this.provider.request('CloudFormation', 'deleteChangeSet', { + await this._cloudFormationRequest('deleteChangeSet', { StackName: stackName, ChangeSetName: changeSetName, }); @@ -119,7 +130,7 @@ module.exports = { } log.info('Executing created change set'); - await this.provider.request('CloudFormation', 'executeChangeSet', executeChangeSetParams); + await this._cloudFormationRequest('executeChangeSet', executeChangeSetParams); monitorCfData = changeSetDescription; } @@ -139,7 +150,7 @@ module.exports = { const stackPolicyBody = JSON.stringify({ Statement: this.serverless.service.provider.stackPolicy, }); - await this.provider.request('CloudFormation', 'setStackPolicy', { + await this._cloudFormationRequest('setStackPolicy', { StackName: stackName, StackPolicyBody: stackPolicyBody, }); diff --git a/lib/plugins/aws/lib/upload-zip-file.js b/lib/plugins/aws/lib/upload-zip-file.js index 2445ebc57c..7746d963bd 100644 --- a/lib/plugins/aws/lib/upload-zip-file.js +++ b/lib/plugins/aws/lib/upload-zip-file.js @@ -7,6 +7,18 @@ const log = require('@serverless/utils/log').log.get('deploy:upload'); const setS3UploadEncryptionOptions = require('../../../aws/set-s3-upload-encryption-options'); module.exports = { + /** + * Helper method to route S3 requests through v2 or v3 based on feature flag + * @private + */ + async _s3Request(method, params) { + // Use v3 if feature flag is enabled, otherwise fallback to v2 + if (this.provider._v3Enabled) { + return this.provider.requestV3('S3', method, params); + } + return this.provider.request('S3', method, params); + }, + async uploadZipFile({ filename, s3KeyDirname, basename }) { if (!basename) basename = filename.split(path.sep).pop(); @@ -38,7 +50,7 @@ module.exports = { params = setS3UploadEncryptionOptions(params, deploymentBucketObject); } - const response = await this.provider.request('S3', 'upload', params); + const response = await this._s3Request('upload', params); // Interestingly, if request handling was queued, and stream errored (before being consumed by // AWS SDK) then SDK call succeeds without actually uploading a file to S3 bucket. // Below line ensures that eventual stream error is communicated diff --git a/lib/plugins/aws/lib/wait-for-change-set-creation.js b/lib/plugins/aws/lib/wait-for-change-set-creation.js index 499fe40c2c..4f68968bfa 100644 --- a/lib/plugins/aws/lib/wait-for-change-set-creation.js +++ b/lib/plugins/aws/lib/wait-for-change-set-creation.js @@ -7,6 +7,18 @@ const { log } = require('@serverless/utils/log'); const getMonitoringFrequency = require('../utils/get-monitoring-frequency'); module.exports = { + /** + * Helper method to route CloudFormation requests through v2 or v3 based on feature flag + * @private + */ + async _cloudFormationRequest(method, params) { + // Use v3 if feature flag is enabled, otherwise fallback to v2 + if (this.provider._v3Enabled) { + return this.provider.requestV3('CloudFormation', method, params); + } + return this.provider.request('CloudFormation', method, params); + }, + async waitForChangeSetCreation(changeSetName, stackName) { const params = { ChangeSetName: changeSetName, @@ -14,11 +26,7 @@ module.exports = { }; const callWithRetry = async () => { - const changeSetDescription = await this.provider.request( - 'CloudFormation', - 'describeChangeSet', - params - ); + const changeSetDescription = await this._cloudFormationRequest('describeChangeSet', params); if ( changeSetDescription.Status === 'CREATE_COMPLETE' || isChangeSetWithoutChanges(changeSetDescription) diff --git a/lib/plugins/aws/logs.js b/lib/plugins/aws/logs.js index bd56aacf56..e63f157fae 100644 --- a/lib/plugins/aws/logs.js +++ b/lib/plugins/aws/logs.js @@ -28,6 +28,18 @@ class AwsLogs { }; } + /** + * Helper method to route CloudWatchLogs requests through v2 or v3 based on feature flag + * @private + */ + async _cloudWatchLogsRequest(method, params) { + // Use v3 if feature flag is enabled, otherwise fallback to v2 + if (this.provider._v3Enabled) { + return this.provider.requestV3('CloudWatchLogs', method, params); + } + return this.provider.request('CloudWatchLogs', method, params); + } + extendedValidate() { this.validate(); @@ -46,7 +58,7 @@ class AwsLogs { orderBy: 'LastEventTime', }; - const reply = await this.provider.request('CloudWatchLogs', 'describeLogStreams', params); + const reply = await this._cloudWatchLogsRequest('describeLogStreams', params); if (!reply || reply.logStreams.length === 0) { throw new ServerlessError('No existing streams for the function', 'NO_EXISTING_LOG_STREAMS'); } @@ -92,7 +104,7 @@ class AwsLogs { } } - const results = await this.provider.request('CloudWatchLogs', 'filterLogEvents', params); + const results = await this._cloudWatchLogsRequest('filterLogEvents', params); if (results.events) { results.events.forEach((e) => { if (e.message.includes('SERVERLESS_ENTERPRISE') || e.message.startsWith('END')) { diff --git a/lib/plugins/aws/metrics.js b/lib/plugins/aws/metrics.js index e0ec7a53b1..5abfd06bb1 100644 --- a/lib/plugins/aws/metrics.js +++ b/lib/plugins/aws/metrics.js @@ -27,6 +27,18 @@ class AwsMetrics { }; } + /** + * Helper method to route CloudWatch requests through v2 or v3 based on feature flag + * @private + */ + async _cloudWatchRequest(method, params) { + // Use v3 if feature flag is enabled, otherwise fallback to v2 + if (this.provider._v3Enabled) { + return this.provider.requestV3('CloudWatch', method, params); + } + return this.provider.request('CloudWatch', method, params); + } + extendedValidate() { this.validate(); @@ -86,7 +98,7 @@ class AwsMetrics { }); const getMetrics = (params) => - this.provider.request('CloudWatch', 'getMetricStatistics', params); + this._cloudWatchRequest('getMetricStatistics', params); return BbPromise.all([ getMetrics(invocationsParams), diff --git a/lib/plugins/aws/package/compile/events/api-gateway/lib/hack/disassociate-usage-plan.js b/lib/plugins/aws/package/compile/events/api-gateway/lib/hack/disassociate-usage-plan.js index ce8a250857..7ca261bc58 100644 --- a/lib/plugins/aws/package/compile/events/api-gateway/lib/hack/disassociate-usage-plan.js +++ b/lib/plugins/aws/package/compile/events/api-gateway/lib/hack/disassociate-usage-plan.js @@ -5,6 +5,18 @@ const _ = require('lodash'); const { log } = require('@serverless/utils/log'); module.exports = { + /** + * Helper method to route AWS requests through v2 or v3 based on feature flag + * @private + */ + async _awsRequest(service, method, params) { + // Use v3 if feature flag is enabled, otherwise fallback to v2 + if (this.provider._v3Enabled) { + return this.provider.requestV3(service, method, params); + } + return this.provider.request(service, method, params); + }, + async disassociateUsagePlan() { const apiKeys = _.get(this.serverless.service.provider.apiGateway, 'apiKeys'); @@ -12,11 +24,11 @@ module.exports = { log.info('Removing usage plan association'); const stackName = `${this.provider.naming.getStackName()}`; return BbPromise.all([ - this.provider.request('CloudFormation', 'describeStackResource', { + this._awsRequest('CloudFormation', 'describeStackResource', { StackName: stackName, LogicalResourceId: this.provider.naming.getRestApiLogicalId(), }), - this.provider.request('APIGateway', 'getUsagePlans', {}), + this._awsRequest('APIGateway', 'getUsagePlans', {}), ]) .then((data) => data[1].items.filter((item) => @@ -30,7 +42,7 @@ module.exports = { items .map((item) => item.apiStages.map((apiStage) => - this.provider.request('APIGateway', 'updateUsagePlan', { + this._awsRequest('APIGateway', 'updateUsagePlan', { usagePlanId: item.id, patchOperations: [ { diff --git a/lib/plugins/aws/package/compile/events/api-gateway/lib/hack/update-stage.js b/lib/plugins/aws/package/compile/events/api-gateway/lib/hack/update-stage.js index 6221788f47..7e7c6e5997 100644 --- a/lib/plugins/aws/package/compile/events/api-gateway/lib/hack/update-stage.js +++ b/lib/plugins/aws/package/compile/events/api-gateway/lib/hack/update-stage.js @@ -24,6 +24,18 @@ const defaultApiGatewayLogLevel = 'INFO'; // Stage resource (see https://github.com/serverless/serverless/pull/5692#issuecomment-467849311 for more information). module.exports = { + /** + * Helper method to route AWS requests through v2 or v3 based on feature flag + * @private + */ + async _awsRequest(service, method, params) { + // Use v3 if feature flag is enabled, otherwise fallback to v2 + if (this.provider._v3Enabled) { + return this.provider.requestV3(service, method, params); + } + return this.provider.request(service, method, params); + }, + defaultApiGatewayLogLevel, async updateStage() { return BbPromise.try(() => { @@ -153,7 +165,7 @@ async function resolveRestApiId() { : this.provider.naming.getApiGatewayName(); const resolveFromAws = (position) => this.provider - .request('APIGateway', 'getRestApis', { position, limit: 500 }) + ._awsRequest('APIGateway', 'getRestApis', { position, limit: 500 }) .then((result) => { const restApi = result.items.find((api) => api.name === apiName); if (restApi) return restApi.id; @@ -170,7 +182,7 @@ async function resolveStage() { const restApiId = this.apiGatewayRestApiId; return this.provider - .request('APIGateway', 'getStage', { + ._awsRequest('APIGateway', 'getStage', { restApiId, stageName: this.provider.getApiGatewayStage(), }) @@ -187,7 +199,7 @@ async function resolveDeploymentId() { const restApiId = this.apiGatewayRestApiId; return this.provider - .request('APIGateway', 'getDeployments', { + ._awsRequest('APIGateway', 'getDeployments', { restApiId, limit: 500, }) @@ -212,7 +224,7 @@ async function ensureStage() { const restApiId = this.apiGatewayRestApiId; const deploymentId = this.apiGatewayDeploymentId; - return this.provider.request('APIGateway', 'createStage', { + return this._awsRequest('APIGateway', 'createStage', { deploymentId, restApiId, stageName: this.provider.getApiGatewayStage(), @@ -348,14 +360,14 @@ function handleTags() { async function addTags() { const requests = this.apiGatewayTagResourceParams.map((tagResourceParam) => - this.provider.request('APIGateway', 'tagResource', tagResourceParam) + this._awsRequest('APIGateway', 'tagResource', tagResourceParam) ); return BbPromise.all(requests); } async function removeTags() { const requests = this.apiGatewayUntagResourceParams.map((untagResourceParam) => - this.provider.request('APIGateway', 'untagResource', untagResourceParam) + this._awsRequest('APIGateway', 'untagResource', untagResourceParam) ); return BbPromise.all(requests); } @@ -365,7 +377,7 @@ function applyUpdates() { const patchOperations = this.apiGatewayStagePatchOperations; if (patchOperations.length) { - return this.provider.request('APIGateway', 'updateStage', { + return this._awsRequest('APIGateway', 'updateStage', { restApiId, stageName: this.provider.getApiGatewayStage(), patchOperations, @@ -392,7 +404,7 @@ async function removeAccessLoggingLogGroup() { // log group name issues when logs are enabled again if (!accessLogging) { return this.provider - .request('CloudWatchLogs', 'deleteLogGroup', { + ._awsRequest('CloudWatchLogs', 'deleteLogGroup', { logGroupName, }) .catch(() => { diff --git a/lib/plugins/aws/package/compile/layers.js b/lib/plugins/aws/package/compile/layers.js index 21053b5cef..2263066ab5 100644 --- a/lib/plugins/aws/package/compile/layers.js +++ b/lib/plugins/aws/package/compile/layers.js @@ -23,6 +23,18 @@ class AwsCompileLayers { }; } + /** + * Helper method to route CloudFormation requests through v2 or v3 based on feature flag + * @private + */ + async _cloudFormationRequest(method, params) { + // Use v3 if feature flag is enabled, otherwise fallback to v2 + if (this.provider._v3Enabled) { + return this.provider.requestV3('CloudFormation', method, params); + } + return this.provider.request('CloudFormation', method, params); + } + async compileLayer(layerName) { const newLayer = this.cfLambdaLayerTemplate(); const layerObject = this.serverless.service.getLayer(layerName); @@ -126,7 +138,7 @@ class AwsCompileLayers { const layerHashOutputLogicalId = this.provider.naming.getLambdaLayerHashOutputLogicalId(layerName); - return this.provider.request('CloudFormation', 'describeStacks', { StackName: stackName }).then( + return this._cloudFormationRequest('describeStacks', { StackName: stackName }).then( (data) => { const lastHash = data.Stacks[0].Outputs.find( (output) => output.OutputKey === layerHashOutputLogicalId diff --git a/lib/plugins/aws/remove/lib/bucket.js b/lib/plugins/aws/remove/lib/bucket.js index e6d9a457c4..f92578442f 100644 --- a/lib/plugins/aws/remove/lib/bucket.js +++ b/lib/plugins/aws/remove/lib/bucket.js @@ -4,6 +4,18 @@ const { log } = require('@serverless/utils/log'); const ServerlessError = require('../../../../serverless-error'); module.exports = { + /** + * Helper method to route S3 requests through v2 or v3 based on feature flag + * @private + */ + async _s3Request(method, params) { + // Use v3 if feature flag is enabled, otherwise fallback to v2 + if (this.provider._v3Enabled) { + return this.provider.requestV3('S3', method, params); + } + return this.provider.request('S3', method, params); + }, + async setServerlessDeploymentBucketName() { try { const bucketName = await this.provider.getServerlessDeploymentBucketName(); @@ -27,7 +39,7 @@ module.exports = { let result; try { - result = await this.provider.request('S3', 'listObjectsV2', { + result = await this._s3Request('listObjectsV2', { Bucket: this.bucketName, Prefix: `${this.provider.getDeploymentPrefix()}/${serviceStage}`, }); @@ -55,7 +67,7 @@ module.exports = { const serviceStage = `${this.serverless.service.service}/${this.provider.getStage()}`; - const result = await this.provider.request('S3', 'listObjectVersions', { + const result = await this._s3Request('listObjectVersions', { Bucket: this.bucketName, Prefix: `${this.provider.getDeploymentPrefix()}/${serviceStage}`, }); @@ -90,7 +102,7 @@ module.exports = { async deleteObjects() { if (this.objectsInBucket.length) { - const data = await this.provider.request('S3', 'deleteObjects', { + const data = await this._s3Request('deleteObjects', { Bucket: this.bucketName, Delete: { Objects: this.objectsInBucket, diff --git a/lib/plugins/aws/remove/lib/ecr.js b/lib/plugins/aws/remove/lib/ecr.js index ff44da6381..fe37933c1c 100644 --- a/lib/plugins/aws/remove/lib/ecr.js +++ b/lib/plugins/aws/remove/lib/ecr.js @@ -1,6 +1,18 @@ 'use strict'; module.exports = { + /** + * Helper method to route ECR requests through v2 or v3 based on feature flag + * @private + */ + async _ecrRequest(method, params) { + // Use v3 if feature flag is enabled, otherwise fallback to v2 + if (this.provider._v3Enabled) { + return this.provider.requestV3('ECR', method, params); + } + return this.provider.request('ECR', method, params); + }, + async removeEcrRepository() { const registryId = await this.provider.getAccountId(); const repositoryName = this.provider.naming.getEcrRepositoryName(); @@ -10,6 +22,6 @@ module.exports = { force: true, // To ensure removal of non-empty repository }; - await this.provider.request('ECR', 'deleteRepository', params); + await this._ecrRequest('deleteRepository', params); }, }; diff --git a/lib/plugins/aws/remove/lib/stack.js b/lib/plugins/aws/remove/lib/stack.js index 429801bd45..f420398387 100644 --- a/lib/plugins/aws/remove/lib/stack.js +++ b/lib/plugins/aws/remove/lib/stack.js @@ -1,6 +1,18 @@ 'use strict'; module.exports = { + /** + * Helper method to route CloudFormation requests through v2 or v3 based on feature flag + * @private + */ + async _cloudFormationRequest(method, params) { + // Use v3 if feature flag is enabled, otherwise fallback to v2 + if (this.provider._v3Enabled) { + return this.provider.requestV3('CloudFormation', method, params); + } + return this.provider.request('CloudFormation', method, params); + }, + async remove() { const stackName = this.provider.naming.getStackName(); const params = { @@ -16,7 +28,7 @@ module.exports = { StackId: stackName, }; - return this.provider.request('CloudFormation', 'deleteStack', params).then(() => cfData); + return this._cloudFormationRequest('deleteStack', params).then(() => cfData); }, async removeStack() { diff --git a/lib/plugins/aws/rollback-function.js b/lib/plugins/aws/rollback-function.js index 8040128a59..edaae27231 100644 --- a/lib/plugins/aws/rollback-function.js +++ b/lib/plugins/aws/rollback-function.js @@ -26,6 +26,18 @@ class AwsRollbackFunction { }; } + /** + * Helper method to route Lambda requests through v2 or v3 based on feature flag + * @private + */ + async _lambdaRequest(method, params) { + // Use v3 if feature flag is enabled, otherwise fallback to v2 + if (this.provider._v3Enabled) { + return this.provider.requestV3('Lambda', method, params); + } + return this.provider.request('Lambda', method, params); + } + async getFunctionToBeRestored() { const funcName = this.options.function; let funcVersion = this.options['function-version']; @@ -46,8 +58,8 @@ class AwsRollbackFunction { Qualifier: funcVersion, }; - return this.provider - .request('Lambda', 'getFunction', params) + return this + ._lambdaRequest('getFunction', params) .then((func) => func) .catch((error) => { if (error.message.match(/not found/)) { @@ -82,7 +94,7 @@ class AwsRollbackFunction { ZipFile: zipBuffer, }; - return this.provider.request('Lambda', 'updateFunctionCode', params).then(() => { + return this._lambdaRequest('updateFunctionCode', params).then(() => { log.notice(); log.notice.success( `Successfully rolled back function ${funcName} to version "${ diff --git a/lib/plugins/aws/rollback.js b/lib/plugins/aws/rollback.js index 3e4ca01af4..9fb842335b 100644 --- a/lib/plugins/aws/rollback.js +++ b/lib/plugins/aws/rollback.js @@ -85,6 +85,18 @@ class AwsRollback { }; } + /** + * Helper method to route S3 requests through v2 or v3 based on feature flag + * @private + */ + async _s3Request(method, params) { + // Use v3 if feature flag is enabled, otherwise fallback to v2 + if (this.provider._v3Enabled) { + return this.provider.requestV3('S3', method, params); + } + return this.provider.request('S3', method, params); + } + async setStackToUpdate() { const service = this.serverless.service; const serviceName = this.serverless.service.service; @@ -94,7 +106,7 @@ class AwsRollback { let response; try { - response = await this.provider.request('S3', 'listObjectsV2', { + response = await this._s3Request('listObjectsV2', { Bucket: this.bucketName, Prefix: prefix, }); @@ -142,7 +154,7 @@ class AwsRollback { const stateString = await (async () => { try { return ( - await this.provider.request('S3', 'getObject', { + await this._s3Request('getObject', { Bucket: this.bucketName, Key: `${ service.package.artifactDirectoryName From 0aa768a7b92d6ddf87aa59923c3ee0ddbb8501a1 Mon Sep 17 00:00:00 2001 From: Graham Campbell Date: Sun, 8 Jun 2025 19:35:17 +0100 Subject: [PATCH 03/27] Phase 4 --- lib/aws/client-factory.js | 2 ++ lib/aws/commands.js | 15 +++++++++++++ .../sources/instance-dependent/get-aws.js | 12 +++++++---- lib/plugins/aws/provider.js | 21 ++++++++++++------- package.json | 1 + 5 files changed, 40 insertions(+), 11 deletions(-) diff --git a/lib/aws/client-factory.js b/lib/aws/client-factory.js index c450d0d0fb..88c9134254 100644 --- a/lib/aws/client-factory.js +++ b/lib/aws/client-factory.js @@ -12,6 +12,7 @@ const { IAMClient } = require('@aws-sdk/client-iam'); const { LambdaClient } = require('@aws-sdk/client-lambda'); const { S3Client } = require('@aws-sdk/client-s3'); const { SSMClient } = require('@aws-sdk/client-ssm'); +const { STSClient } = require('@aws-sdk/client-sts'); // Map service names to their client classes const CLIENT_MAP = { @@ -27,6 +28,7 @@ const CLIENT_MAP = { Lambda: LambdaClient, S3: S3Client, SSM: SSMClient, + STS: STSClient, }; class AWSClientFactory { diff --git a/lib/aws/commands.js b/lib/aws/commands.js index 8dca7f185a..6ba9ab8578 100644 --- a/lib/aws/commands.js +++ b/lib/aws/commands.js @@ -57,6 +57,9 @@ const { const { DeleteRepositoryCommand, DescribeRepositoriesCommand, + GetAuthorizationTokenCommand, + CreateRepositoryCommand, + DescribeImagesCommand, } = require('@aws-sdk/client-ecr'); // EventBridge Commands @@ -108,6 +111,11 @@ const { GetParameterCommand, } = require('@aws-sdk/client-ssm'); +// STS Commands +const { + GetCallerIdentityCommand, +} = require('@aws-sdk/client-sts'); + /** * Map v2 method names to v3 command classes * Format: { ServiceName: { methodName: CommandClass } } @@ -163,6 +171,9 @@ const COMMAND_MAP = { ECR: { deleteRepository: DeleteRepositoryCommand, describeRepositories: DescribeRepositoriesCommand, + getAuthorizationToken: GetAuthorizationTokenCommand, + createRepository: CreateRepositoryCommand, + describeImages: DescribeImagesCommand, }, EventBridge: { @@ -209,6 +220,10 @@ const COMMAND_MAP = { SSM: { getParameter: GetParameterCommand, }, + + STS: { + getCallerIdentity: GetCallerIdentityCommand, + }, }; /** diff --git a/lib/configuration/variables/sources/instance-dependent/get-aws.js b/lib/configuration/variables/sources/instance-dependent/get-aws.js index 45b7a021d0..ad9823fc71 100644 --- a/lib/configuration/variables/sources/instance-dependent/get-aws.js +++ b/lib/configuration/variables/sources/instance-dependent/get-aws.js @@ -21,10 +21,14 @@ module.exports = (serverlessInstance) => { switch (address) { case 'accountId': { - const { Account } = await serverlessInstance - .getProvider('aws') - .request('STS', 'getCallerIdentity', {}, { useCache: true }); - return { value: Account }; + const provider = serverlessInstance.getProvider('aws'); + let result; + if (provider._v3Enabled) { + result = await provider.requestV3('STS', 'getCallerIdentity', {}, { useCache: true }); + } else { + result = await provider.request('STS', 'getCallerIdentity', {}, { useCache: true }); + } + return { value: result.Account }; } case 'region': { let region = options.region; diff --git a/lib/plugins/aws/provider.js b/lib/plugins/aws/provider.js index 5360dd8f7a..8a4945a0ba 100644 --- a/lib/plugins/aws/provider.js +++ b/lib/plugins/aws/provider.js @@ -1952,7 +1952,8 @@ class AwsProvider { if (this.serverless.service.provider.deploymentBucket) { return BbPromise.resolve(this.serverless.service.provider.deploymentBucket); } - return this.request('CloudFormation', 'describeStackResource', { + const requestMethod = this._v3Enabled ? 'requestV3' : 'request'; + return this[requestMethod]('CloudFormation', 'describeStackResource', { StackName: this.naming.getStackName(), LogicalResourceId: this.naming.getDeploymentBucketLogicalId(), }).then((result) => result.StackResourceDetail.PhysicalResourceId); @@ -2167,7 +2168,8 @@ class AwsProvider { if (!resources) resources = []; if (next) params.NextToken = next; - return this.request('CloudFormation', 'listStackResources', params).then((res) => { + const requestMethod = this._v3Enabled ? 'requestV3' : 'request'; + return this[requestMethod]('CloudFormation', 'listStackResources', params).then((res) => { const allResources = resources.concat(res.StackResourceSummaries); if (!res.NextToken) { return allResources; @@ -2207,7 +2209,8 @@ Object.defineProperties( memoizeeMethods({ getAccountInfo: d( async function () { - const result = await this.request('STS', 'getCallerIdentity', {}); + const requestMethod = this._v3Enabled ? 'requestV3' : 'request'; + const result = await this[requestMethod]('STS', 'getCallerIdentity', {}); const arn = result.Arn; const accountId = result.Account; const partition = arn.split(':')[1]; // ex: arn:aws:iam:acctId:user/xyz @@ -2243,7 +2246,8 @@ Object.defineProperties( dockerLoginToEcr: d( async function () { const registryId = await this.getAccountId(); - const result = await this.request('ECR', 'getAuthorizationToken', { + const requestMethod = this._v3Enabled ? 'requestV3' : 'request'; + const result = await this[requestMethod]('ECR', 'getAuthorizationToken', { registryIds: [registryId], }); const { authorizationToken, proxyEndpoint } = result.authorizationData[0]; @@ -2281,7 +2285,8 @@ Object.defineProperties( const repositoryName = this.naming.getEcrRepositoryName(); let repositoryUri; try { - const result = await this.request('ECR', 'describeRepositories', { + const requestMethod = this._v3Enabled ? 'requestV3' : 'request'; + const result = await this[requestMethod]('ECR', 'describeRepositories', { repositoryNames: [repositoryName], registryId, }); @@ -2290,7 +2295,8 @@ Object.defineProperties( if (!(err.providerError && err.providerError.code === 'RepositoryNotFoundException')) { throw err; } - const result = await this.request('ECR', 'createRepository', { + const requestMethod = this._v3Enabled ? 'requestV3' : 'request'; + const result = await this[requestMethod]('ECR', 'createRepository', { repositoryName, imageScanningConfiguration: { scanOnPush }, }); @@ -2437,7 +2443,8 @@ Object.defineProperties( 'LAMBDA_ECR_REGION_MISMATCH_ERROR' ); } - const describeImagesResponse = await this.request('ECR', 'describeImages', { + const requestMethod = this._v3Enabled ? 'requestV3' : 'request'; + const describeImagesResponse = await this[requestMethod]('ECR', 'describeImages', { imageIds: [ { imageTag, diff --git a/package.json b/package.json index 1fd92a18d4..742ab4e21f 100644 --- a/package.json +++ b/package.json @@ -33,6 +33,7 @@ "@aws-sdk/client-lambda": "^3.588.0", "@aws-sdk/client-s3": "^3.588.0", "@aws-sdk/client-ssm": "^3.588.0", + "@aws-sdk/client-sts": "^3.588.0", "@aws-sdk/credential-providers": "^3.588.0", "@serverless/utils": "^6.13.1", "ajv": "^8.12.0", From 0f175aa05427dca730ba72f39d99ff3661edcb10 Mon Sep 17 00:00:00 2001 From: Graham Campbell Date: Sun, 8 Jun 2025 19:52:01 +0100 Subject: [PATCH 04/27] Self-review and corrections --- lib/aws/commands.js | 4 ++++ lib/plugins/aws/deploy/lib/check-for-changes.js | 12 +++++------- lib/plugins/aws/lib/monitor-stack.js | 14 ++++++++++++-- lib/plugins/aws/utils/resolve-cf-import-value.js | 3 ++- lib/plugins/aws/utils/resolve-cf-ref-value.js | 12 ++++++------ 5 files changed, 29 insertions(+), 16 deletions(-) diff --git a/lib/aws/commands.js b/lib/aws/commands.js index 6ba9ab8578..907b6acf8f 100644 --- a/lib/aws/commands.js +++ b/lib/aws/commands.js @@ -32,6 +32,8 @@ const { GetTemplateCommand, ListStackResourcesCommand, DescribeStackResourceCommand, + DescribeStackEventsCommand, + ListExportsCommand, } = require('@aws-sdk/client-cloudformation'); // CloudWatch Commands @@ -150,6 +152,8 @@ const COMMAND_MAP = { getTemplate: GetTemplateCommand, listStackResources: ListStackResourcesCommand, describeStackResource: DescribeStackResourceCommand, + describeStackEvents: DescribeStackEventsCommand, + listExports: ListExportsCommand, }, CloudWatch: { diff --git a/lib/plugins/aws/deploy/lib/check-for-changes.js b/lib/plugins/aws/deploy/lib/check-for-changes.js index 83db18531d..dfbc813cda 100644 --- a/lib/plugins/aws/deploy/lib/check-for-changes.js +++ b/lib/plugins/aws/deploy/lib/check-for-changes.js @@ -313,13 +313,11 @@ module.exports = { const cloudwatchLogEvents = params.cloudwatchLogEvents; const CLOUDWATCHLOG_LOG_GROUP_EVENT_PER_FUNCTION_LIMIT = 2; - const response = await this.provider - .request( - 'CloudWatchLogs', - 'describeSubscriptionFilters', - { logGroupName }, - { useCache: true } - ) + const response = await this._awsRequest( + 'CloudWatchLogs', + 'describeSubscriptionFilters', + { logGroupName } + ) .catch(() => ({ subscriptionFilters: [] })); if (response.subscriptionFilters.length === 0) { log.debug('no subscription filters detected'); diff --git a/lib/plugins/aws/lib/monitor-stack.js b/lib/plugins/aws/lib/monitor-stack.js index 336e7fd946..8e036a09cb 100644 --- a/lib/plugins/aws/lib/monitor-stack.js +++ b/lib/plugins/aws/lib/monitor-stack.js @@ -21,6 +21,17 @@ const resourceTypeToErrorCodePostfix = (resourceType) => { }; module.exports = { + /** + * Helper method to route CloudFormation requests through v2 or v3 based on feature flag + * @private + */ + async _cloudFormationRequest(method, params) { + // Use v3 if feature flag is enabled, otherwise fallback to v2 + if (this.provider._v3Enabled) { + return this.provider.requestV3('CloudFormation', method, params); + } + return this.provider.request('CloudFormation', method, params); + }, async checkStackProgress( action, cfData, @@ -36,8 +47,7 @@ module.exports = { ) { return wait(getMonitoringFrequency(options.frequency)) .then(() => - this.provider - .request('CloudFormation', 'describeStackEvents', { StackName: cfData.StackId }) + this._cloudFormationRequest('describeStackEvents', { StackName: cfData.StackId }) .then( ({ StackEvents: stackEvents }) => { if (!stackEvents.length) return; diff --git a/lib/plugins/aws/utils/resolve-cf-import-value.js b/lib/plugins/aws/utils/resolve-cf-import-value.js index 78baba0d19..e318c4d74f 100644 --- a/lib/plugins/aws/utils/resolve-cf-import-value.js +++ b/lib/plugins/aws/utils/resolve-cf-import-value.js @@ -3,7 +3,8 @@ const ServerlessError = require('../../../serverless-error'); async function resolveCfImportValue(provider, name, sdkParams = {}) { - return provider.request('CloudFormation', 'listExports', sdkParams).then((result) => { + const requestMethod = provider._v3Enabled ? 'requestV3' : 'request'; + return provider[requestMethod]('CloudFormation', 'listExports', sdkParams).then((result) => { const targetExportMeta = result.Exports.find((exportMeta) => exportMeta.Name === name); if (targetExportMeta) return targetExportMeta.Value; if (result.NextToken) { diff --git a/lib/plugins/aws/utils/resolve-cf-ref-value.js b/lib/plugins/aws/utils/resolve-cf-ref-value.js index ba3de7a43c..f671ac00dd 100644 --- a/lib/plugins/aws/utils/resolve-cf-ref-value.js +++ b/lib/plugins/aws/utils/resolve-cf-ref-value.js @@ -3,12 +3,12 @@ const ServerlessError = require('../../../serverless-error'); async function resolveCfRefValue(provider, resourceLogicalId, sdkParams = {}) { - return provider - .request( - 'CloudFormation', - 'listStackResources', - Object.assign(sdkParams, { StackName: provider.naming.getStackName() }) - ) + const requestMethod = provider._v3Enabled ? 'requestV3' : 'request'; + return provider[requestMethod]( + 'CloudFormation', + 'listStackResources', + Object.assign(sdkParams, { StackName: provider.naming.getStackName() }) + ) .then((result) => { const targetStackResource = result.StackResourceSummaries.find( (stackResource) => stackResource.LogicalResourceId === resourceLogicalId From 03e9b9d52e8266d7c908efc5d2e789fa514aa738 Mon Sep 17 00:00:00 2001 From: Graham Campbell Date: Sun, 8 Jun 2025 20:09:34 +0100 Subject: [PATCH 05/27] Complete phase 5 --- test/utils/api-gateway.js | 43 ++++++++++++++++++++--- test/utils/cloudformation.js | 50 ++++++++++++++++++++++----- test/utils/cognito.js | 67 ++++++++++++++++++++++++++++++------ test/utils/s3.js | 53 ++++++++++++++++++++++++---- 4 files changed, 182 insertions(+), 31 deletions(-) diff --git a/test/utils/api-gateway.js b/test/utils/api-gateway.js index 9dfa58cd45..00939cb3a5 100644 --- a/test/utils/api-gateway.js +++ b/test/utils/api-gateway.js @@ -2,14 +2,47 @@ const _ = require('lodash'); const awsRequest = require('@serverless/test/aws-request'); -const APIGatewayService = require('aws-sdk').APIGateway; + +// Support for both AWS SDK v2 and v3 +const getAPIGatewayClient = () => { + if (process.env.SLS_AWS_SDK_V3 === 'true') { + // AWS SDK v3 + const { APIGatewayClient } = require('@aws-sdk/client-api-gateway'); + const { + CreateRestApiCommand, + DeleteRestApiCommand, + GetResourcesCommand, + GetRestApisCommand + } = require('@aws-sdk/client-api-gateway'); + + const client = new APIGatewayClient({ region: 'us-east-1' }); + + return { + createRestApi: (params) => client.send(new CreateRestApiCommand(params)), + deleteRestApi: (params) => client.send(new DeleteRestApiCommand(params)), + getResources: (params) => client.send(new GetResourcesCommand(params)), + getRestApis: (params) => client.send(new GetRestApisCommand(params)), + }; + } else { + // AWS SDK v2 + const APIGatewayService = require('aws-sdk').APIGateway; + return { + createRestApi: (params) => awsRequest(APIGatewayService, 'createRestApi', params), + deleteRestApi: (params) => awsRequest(APIGatewayService, 'deleteRestApi', params), + getResources: (params) => awsRequest(APIGatewayService, 'getResources', params), + getRestApis: (params) => awsRequest(APIGatewayService, 'getRestApis', params), + }; + } +}; + +const apiGateway = getAPIGatewayClient(); async function createRestApi(name) { const params = { name, }; - return awsRequest(APIGatewayService, 'createRestApi', params); + return apiGateway.createRestApi(params); } async function deleteRestApi(restApiId) { @@ -17,7 +50,7 @@ async function deleteRestApi(restApiId) { restApiId, }; - return awsRequest(APIGatewayService, 'deleteRestApi', params); + return apiGateway.deleteRestApi(params); } async function getResources(restApiId) { @@ -25,7 +58,7 @@ async function getResources(restApiId) { restApiId, }; - return awsRequest(APIGatewayService, 'getResources', params).then((data) => data.items); + return apiGateway.getResources(params).then((data) => data.items); } async function findRestApis(name) { @@ -35,7 +68,7 @@ async function findRestApis(name) { async function recursiveFind(found, position) { if (position) params.position = position; - return awsRequest(APIGatewayService, 'getRestApis', params).then((result) => { + return apiGateway.getRestApis(params).then((result) => { const matches = result.items.filter((restApi) => restApi.name.match(name)); if (matches.length) { _.merge(found, matches); diff --git a/test/utils/cloudformation.js b/test/utils/cloudformation.js index 8852ef8d35..bedafcaad5 100644 --- a/test/utils/cloudformation.js +++ b/test/utils/cloudformation.js @@ -1,7 +1,40 @@ 'use strict'; const awsRequest = require('@serverless/test/aws-request'); -const CloudFormationService = require('aws-sdk').CloudFormation; + +// Support for both AWS SDK v2 and v3 +const getCloudFormationClient = () => { + if (process.env.SLS_AWS_SDK_V3 === 'true') { + // AWS SDK v3 + const { CloudFormationClient } = require('@aws-sdk/client-cloudformation'); + const { + ListStacksCommand, + DeleteStackCommand, + ListStackResourcesCommand, + DescribeStacksCommand + } = require('@aws-sdk/client-cloudformation'); + + const client = new CloudFormationClient({ region: 'us-east-1' }); + + return { + listStacks: (params) => client.send(new ListStacksCommand(params)), + deleteStack: (params) => client.send(new DeleteStackCommand(params)), + listStackResources: (params) => client.send(new ListStackResourcesCommand(params)), + describeStacks: (params) => client.send(new DescribeStacksCommand(params)), + }; + } else { + // AWS SDK v2 + const CloudFormationService = require('aws-sdk').CloudFormation; + return { + listStacks: (params) => awsRequest(CloudFormationService, 'listStacks', params), + deleteStack: (params) => awsRequest(CloudFormationService, 'deleteStack', params), + listStackResources: (params) => awsRequest(CloudFormationService, 'listStackResources', params), + describeStacks: (params) => awsRequest(CloudFormationService, 'describeStacks', params), + }; + } +}; + +const cf = getCloudFormationClient(); const SHARED_INFRA_TESTS_CLOUDFORMATION_STACK = 'integration-tests-deps-stack'; const SHARED_INFRA_TESTS_ACTIVE_MQ_CREDENTIALS_NAME = @@ -17,7 +50,7 @@ async function findStacks(name, status) { async function recursiveFind(found, token) { if (token) params.NextToken = token; - return awsRequest(CloudFormationService, 'listStacks', params).then((result) => { + return cf.listStacks(params).then((result) => { const matches = result.StackSummaries.filter((stack) => stack.StackName.match(name)); if (matches.length) { found.push(...matches); @@ -35,7 +68,7 @@ async function deleteStack(stack) { StackName: stack, }; - return awsRequest(CloudFormationService, 'deleteStack', params); + return cf.deleteStack(params); } async function listStackResources(stack) { @@ -45,7 +78,7 @@ async function listStackResources(stack) { async function recursiveFind(resources, token) { if (token) params.NextToken = token; - return awsRequest(CloudFormationService, 'listStackResources', params).then((result) => { + return cf.listStackResources(params).then((result) => { resources.push(...result.StackResourceSummaries); if (result.NextToken) return recursiveFind(resources, result.NextToken); return resources; @@ -61,11 +94,11 @@ async function listStacks(status) { params.StackStatusFilter = status; } - return awsRequest(CloudFormationService, 'listStacks', params); + return cf.listStacks(params); } async function getStackOutputMap(name) { - const describeStackResponse = await awsRequest(CloudFormationService, 'describeStacks', { + const describeStackResponse = await cf.describeStacks({ StackName: name, }); @@ -80,7 +113,7 @@ async function isDependencyStackAvailable() { const validStatuses = ['CREATE_COMPLETE', 'UPDATE_COMPLETE']; try { - const describeStacksResponse = await awsRequest(CloudFormationService, 'describeStacks', { + const describeStacksResponse = await cf.describeStacks({ StackName: SHARED_INFRA_TESTS_CLOUDFORMATION_STACK, }); if (validStatuses.includes(describeStacksResponse.Stacks[0].StackStatus)) { @@ -88,7 +121,8 @@ async function isDependencyStackAvailable() { } return false; } catch (e) { - if (e.code === 'ValidationError') { + // Handle both v2 and v3 error patterns + if (e.code === 'ValidationError' || e.name === 'ValidationException') { return false; } throw e; diff --git a/test/utils/cognito.js b/test/utils/cognito.js index c6684bf611..039e6455f0 100644 --- a/test/utils/cognito.js +++ b/test/utils/cognito.js @@ -2,11 +2,56 @@ const awsLog = require('log').get('aws'); const awsRequest = require('@serverless/test/aws-request'); -const CognitoIdentityServiceProviderService = require('aws-sdk').CognitoIdentityServiceProvider; + +// Support for both AWS SDK v2 and v3 +const getCognitoClient = () => { + if (process.env.SLS_AWS_SDK_V3 === 'true') { + // AWS SDK v3 + const { CognitoIdentityProviderClient } = require('@aws-sdk/client-cognito-identity-provider'); + const { + CreateUserPoolCommand, + CreateUserPoolClientCommand, + DeleteUserPoolCommand, + ListUserPoolsCommand, + DescribeUserPoolCommand, + AdminCreateUserCommand, + AdminSetUserPasswordCommand, + InitiateAuthCommand + } = require('@aws-sdk/client-cognito-identity-provider'); + + const client = new CognitoIdentityProviderClient({ region: 'us-east-1' }); + + return { + createUserPool: (params) => client.send(new CreateUserPoolCommand(params)), + createUserPoolClient: (params) => client.send(new CreateUserPoolClientCommand(params)), + deleteUserPool: (params) => client.send(new DeleteUserPoolCommand(params)), + listUserPools: (params) => client.send(new ListUserPoolsCommand(params)), + describeUserPool: (params) => client.send(new DescribeUserPoolCommand(params)), + adminCreateUser: (params) => client.send(new AdminCreateUserCommand(params)), + adminSetUserPassword: (params) => client.send(new AdminSetUserPasswordCommand(params)), + initiateAuth: (params) => client.send(new InitiateAuthCommand(params)), + }; + } else { + // AWS SDK v2 + const CognitoIdentityServiceProviderService = require('aws-sdk').CognitoIdentityServiceProvider; + return { + createUserPool: (params) => awsRequest(CognitoIdentityServiceProviderService, 'createUserPool', params), + createUserPoolClient: (params) => awsRequest(CognitoIdentityServiceProviderService, 'createUserPoolClient', params), + deleteUserPool: (params) => awsRequest(CognitoIdentityServiceProviderService, 'deleteUserPool', params), + listUserPools: (params) => awsRequest(CognitoIdentityServiceProviderService, 'listUserPools', params), + describeUserPool: (params) => awsRequest(CognitoIdentityServiceProviderService, 'describeUserPool', params), + adminCreateUser: (params) => awsRequest(CognitoIdentityServiceProviderService, 'adminCreateUser', params), + adminSetUserPassword: (params) => awsRequest(CognitoIdentityServiceProviderService, 'adminSetUserPassword', params), + initiateAuth: (params) => awsRequest(CognitoIdentityServiceProviderService, 'initiateAuth', params), + }; + } +}; + +const cognito = getCognitoClient(); async function createUserPool(name, config = {}) { const params = Object.assign({}, { PoolName: name }, config); - return awsRequest(CognitoIdentityServiceProviderService, 'createUserPool', params); + return cognito.createUserPool(params); } async function createUserPoolClient(name, userPoolId) { @@ -15,17 +60,17 @@ async function createUserPoolClient(name, userPoolId) { UserPoolId: userPoolId, ExplicitAuthFlows: ['USER_PASSWORD_AUTH'], }; - return awsRequest(CognitoIdentityServiceProviderService, 'createUserPoolClient', params); + return cognito.createUserPoolClient(params); } async function deleteUserPool(name) { return findUserPoolByName(name).then((pool) => - awsRequest(CognitoIdentityServiceProviderService, 'deleteUserPool', { UserPoolId: pool.Id }) + cognito.deleteUserPool({ UserPoolId: pool.Id }) ); } async function deleteUserPoolById(poolId) { - return awsRequest(CognitoIdentityServiceProviderService, 'deleteUserPool', { + return cognito.deleteUserPool({ UserPoolId: poolId, }); } @@ -40,7 +85,7 @@ async function findUserPoolByName(name) { const pools = []; async function recursiveFind(nextToken) { if (nextToken) params.NextToken = nextToken; - return awsRequest(CognitoIdentityServiceProviderService, 'listUserPools', params).then( + return cognito.listUserPools(params).then( (result) => { pools.push(...result.UserPools.filter((pool) => pool.Name === name)); if (result.NextToken) return recursiveFind(result.NextToken); @@ -65,7 +110,7 @@ async function findUserPools() { const pools = []; async function recursiveFind(nextToken) { if (nextToken) params.NextToken = nextToken; - return awsRequest(CognitoIdentityServiceProviderService, 'listUserPools', params).then( + return cognito.listUserPools(params).then( (result) => { pools.push(...result.UserPools.filter((pool) => pool.Name.includes(' CUP '))); if (result.NextToken) return recursiveFind(result.NextToken); @@ -78,7 +123,7 @@ async function findUserPools() { } async function describeUserPool(userPoolId) { - return awsRequest(CognitoIdentityServiceProviderService, 'describeUserPool', { + return cognito.describeUserPool({ UserPoolId: userPoolId, }).then((result) => { awsLog.debug('cognito.describeUserPool %s %j', userPoolId, result); @@ -92,7 +137,7 @@ async function createUser(userPoolId, username, password) { Username: username, TemporaryPassword: password, }; - return awsRequest(CognitoIdentityServiceProviderService, 'adminCreateUser', params); + return cognito.adminCreateUser(params); } async function setUserPassword(userPoolId, username, password) { @@ -102,7 +147,7 @@ async function setUserPassword(userPoolId, username, password) { Password: password, Permanent: true, }; - return awsRequest(CognitoIdentityServiceProviderService, 'adminSetUserPassword', params); + return cognito.adminSetUserPassword(params); } async function initiateAuth(clientId, username, password) { @@ -114,7 +159,7 @@ async function initiateAuth(clientId, username, password) { PASSWORD: password, }, }; - return awsRequest(CognitoIdentityServiceProviderService, 'initiateAuth', params); + return cognito.initiateAuth(params); } module.exports = { diff --git a/test/utils/s3.js b/test/utils/s3.js index 812fde9c0f..715943e50f 100644 --- a/test/utils/s3.js +++ b/test/utils/s3.js @@ -1,10 +1,49 @@ 'use strict'; const awsRequest = require('@serverless/test/aws-request'); -const S3Service = require('aws-sdk').S3; + +// Support for both AWS SDK v2 and v3 +const getS3Client = () => { + if (process.env.SLS_AWS_SDK_V3 === 'true') { + // AWS SDK v3 + const { S3Client } = require('@aws-sdk/client-s3'); + const { + CreateBucketCommand, + PutObjectCommand, + DeleteObjectCommand, + ListObjectsCommand, + DeleteObjectsCommand, + DeleteBucketCommand + } = require('@aws-sdk/client-s3'); + + const client = new S3Client({ region: 'us-east-1' }); + + return { + createBucket: (params) => client.send(new CreateBucketCommand(params)), + putObject: (params) => client.send(new PutObjectCommand(params)), + deleteObject: (params) => client.send(new DeleteObjectCommand(params)), + listObjects: (params) => client.send(new ListObjectsCommand(params)), + deleteObjects: (params) => client.send(new DeleteObjectsCommand(params)), + deleteBucket: (params) => client.send(new DeleteBucketCommand(params)), + }; + } else { + // AWS SDK v2 + const S3Service = require('aws-sdk').S3; + return { + createBucket: (params) => awsRequest(S3Service, 'createBucket', params), + putObject: (params) => awsRequest(S3Service, 'putObject', params), + deleteObject: (params) => awsRequest(S3Service, 'deleteObject', params), + listObjects: (params) => awsRequest(S3Service, 'listObjects', params), + deleteObjects: (params) => awsRequest(S3Service, 'deleteObjects', params), + deleteBucket: (params) => awsRequest(S3Service, 'deleteBucket', params), + }; + } +}; + +const s3 = getS3Client(); async function createBucket(bucket) { - return awsRequest(S3Service, 'createBucket', { Bucket: bucket }); + return s3.createBucket({ Bucket: bucket }); } async function createAndRemoveInBucket(bucket, opts = {}) { @@ -18,19 +57,19 @@ async function createAndRemoveInBucket(bucket, opts = {}) { Body: 'hello world', }; - return awsRequest(S3Service, 'putObject', params).then(() => { + return s3.putObject(params).then(() => { delete params.Body; - return awsRequest(S3Service, 'deleteObject', params); + return s3.deleteObject(params); }); } async function emptyBucket(bucket) { - return awsRequest(S3Service, 'listObjects', { Bucket: bucket }).then((data) => { + return s3.listObjects({ Bucket: bucket }).then((data) => { const items = data.Contents; const numItems = items.length; if (numItems) { const keys = items.map((item) => Object.assign({}, { Key: item.Key })); - return awsRequest(S3Service, 'deleteObjects', { + return s3.deleteObjects({ Bucket: bucket, Delete: { Objects: keys, @@ -42,7 +81,7 @@ async function emptyBucket(bucket) { } async function deleteBucket(bucket) { - return emptyBucket(bucket).then(() => awsRequest(S3Service, 'deleteBucket', { Bucket: bucket })); + return emptyBucket(bucket).then(() => s3.deleteBucket({ Bucket: bucket })); } module.exports = { From 63ab7df338971bde96641e53cf2f803f12f175ec Mon Sep 17 00:00:00 2001 From: Graham Campbell Date: Sun, 8 Jun 2025 22:16:20 +0100 Subject: [PATCH 06/27] Self-review on stage 5 --- package.json | 7 ++++++ test/utils/dynamodb.js | 27 +++++++++++++++++++-- test/utils/event-bridge.js | 43 ++++++++++++++++++++++++++++++---- test/utils/iot.js | 47 +++++++++++++++++++++++++++++++++---- test/utils/kinesis.js | 43 ++++++++++++++++++++++++++++++---- test/utils/misc.js | 26 +++++++++++++++++++-- test/utils/sns.js | 43 ++++++++++++++++++++++++++++++---- test/utils/sqs.js | 45 ++++++++++++++++++++++++++++++----- test/utils/websocket.js | 48 +++++++++++++++++++++++++++++++++----- 9 files changed, 294 insertions(+), 35 deletions(-) diff --git a/package.json b/package.json index 742ab4e21f..28ce05f69d 100644 --- a/package.json +++ b/package.json @@ -27,13 +27,20 @@ "@aws-sdk/client-cloudwatch": "^3.588.0", "@aws-sdk/client-cloudwatch-logs": "^3.588.0", "@aws-sdk/client-cognito-identity-provider": "^3.588.0", + "@aws-sdk/client-dynamodb": "^3.588.0", "@aws-sdk/client-ecr": "^3.588.0", "@aws-sdk/client-eventbridge": "^3.588.0", "@aws-sdk/client-iam": "^3.588.0", + "@aws-sdk/client-iot": "^3.588.0", + "@aws-sdk/client-iot-data-plane": "^3.588.0", + "@aws-sdk/client-kinesis": "^3.588.0", "@aws-sdk/client-lambda": "^3.588.0", "@aws-sdk/client-s3": "^3.588.0", + "@aws-sdk/client-sns": "^3.588.0", + "@aws-sdk/client-sqs": "^3.588.0", "@aws-sdk/client-ssm": "^3.588.0", "@aws-sdk/client-sts": "^3.588.0", + "@aws-sdk/lib-dynamodb": "^3.588.0", "@aws-sdk/credential-providers": "^3.588.0", "@serverless/utils": "^6.13.1", "ajv": "^8.12.0", diff --git a/test/utils/dynamodb.js b/test/utils/dynamodb.js index bcd300aa22..ccea3abc99 100644 --- a/test/utils/dynamodb.js +++ b/test/utils/dynamodb.js @@ -1,7 +1,30 @@ 'use strict'; const awsRequest = require('@serverless/test/aws-request'); -const DDBDocumentClient = require('aws-sdk').DynamoDB.DocumentClient; + +// Support for both AWS SDK v2 and v3 +const getDynamoDBClient = () => { + if (process.env.SLS_AWS_SDK_V3 === 'true') { + // AWS SDK v3 - using DynamoDBDocumentClient from lib-dynamodb + const { DynamoDBClient } = require('@aws-sdk/client-dynamodb'); + const { DynamoDBDocumentClient, PutCommand } = require('@aws-sdk/lib-dynamodb'); + + const client = new DynamoDBClient({ region: 'us-east-1' }); + const docClient = DynamoDBDocumentClient.from(client); + + return { + put: (params) => docClient.send(new PutCommand(params)), + }; + } else { + // AWS SDK v2 + const DDBDocumentClient = require('aws-sdk').DynamoDB.DocumentClient; + return { + put: (params) => awsRequest(DDBDocumentClient, 'put', params), + }; + } +}; + +const dynamodb = getDynamoDBClient(); async function putDynamoDbItem(tableName, item) { const params = { @@ -9,7 +32,7 @@ async function putDynamoDbItem(tableName, item) { Item: item, }; - return awsRequest(DDBDocumentClient, 'put', params); + return dynamodb.put(params); } module.exports = { diff --git a/test/utils/event-bridge.js b/test/utils/event-bridge.js index b44dc8c8a8..eb4a5a1f73 100644 --- a/test/utils/event-bridge.js +++ b/test/utils/event-bridge.js @@ -1,18 +1,51 @@ 'use strict'; const awsRequest = require('@serverless/test/aws-request'); -const EventBridgeService = require('aws-sdk').EventBridge; + +// Support for both AWS SDK v2 and v3 +const getEventBridgeClient = () => { + if (process.env.SLS_AWS_SDK_V3 === 'true') { + // AWS SDK v3 + const { EventBridgeClient } = require('@aws-sdk/client-eventbridge'); + const { + CreateEventBusCommand, + DeleteEventBusCommand, + DescribeEventBusCommand, + PutEventsCommand + } = require('@aws-sdk/client-eventbridge'); + + const client = new EventBridgeClient({ region: 'us-east-1' }); + + return { + createEventBus: (params) => client.send(new CreateEventBusCommand(params)), + deleteEventBus: (params) => client.send(new DeleteEventBusCommand(params)), + describeEventBus: (params) => client.send(new DescribeEventBusCommand(params)), + putEvents: (params) => client.send(new PutEventsCommand(params)), + }; + } else { + // AWS SDK v2 + const EventBridgeService = require('aws-sdk').EventBridge; + return { + createEventBus: (params) => awsRequest(EventBridgeService, 'createEventBus', params), + deleteEventBus: (params) => awsRequest(EventBridgeService, 'deleteEventBus', params), + describeEventBus: (params) => awsRequest(EventBridgeService, 'describeEventBus', params), + putEvents: (params) => awsRequest(EventBridgeService, 'putEvents', params), + }; + } +}; + +const eventBridge = getEventBridgeClient(); async function createEventBus(name) { - return awsRequest(EventBridgeService, 'createEventBus', { Name: name }); + return eventBridge.createEventBus({ Name: name }); } async function deleteEventBus(name) { - return awsRequest(EventBridgeService, 'deleteEventBus', { Name: name }); + return eventBridge.deleteEventBus({ Name: name }); } async function describeEventBus(name) { - return awsRequest(EventBridgeService, 'describeEventBus', { Name: name }); + return eventBridge.describeEventBus({ Name: name }); } async function putEvents(EventBusName, Entries) { @@ -20,7 +53,7 @@ async function putEvents(EventBusName, Entries) { const params = { Entries, }; - return awsRequest(EventBridgeService, 'putEvents', params); + return eventBridge.putEvents(params); } module.exports = { diff --git a/test/utils/iot.js b/test/utils/iot.js index d8bb877f99..076d381f2c 100644 --- a/test/utils/iot.js +++ b/test/utils/iot.js @@ -1,11 +1,49 @@ 'use strict'; const awsRequest = require('@serverless/test/aws-request'); -const IotService = require('aws-sdk').Iot; -const IotDataService = require('aws-sdk').IotData; + +// Support for both AWS SDK v2 and v3 +const getIoTClients = () => { + if (process.env.SLS_AWS_SDK_V3 === 'true') { + // AWS SDK v3 - dual service pattern (IoT + IoTDataPlane) + const { IoTClient, DescribeEndpointCommand } = require('@aws-sdk/client-iot'); + const { IoTDataPlaneClient, PublishCommand } = require('@aws-sdk/client-iot-data-plane'); + + const iotClient = new IoTClient({ region: 'us-east-1' }); + + return { + iot: { + describeEndpoint: (params) => iotClient.send(new DescribeEndpointCommand(params)), + }, + createIoTDataClient: (endpoint) => { + const iotDataClient = new IoTDataPlaneClient({ + region: 'us-east-1', + endpoint: `https://${endpoint}` + }); + return { + publish: (params) => iotDataClient.send(new PublishCommand(params)), + }; + } + }; + } else { + // AWS SDK v2 + const IotService = require('aws-sdk').Iot; + const IotDataService = require('aws-sdk').IotData; + return { + iot: { + describeEndpoint: (params) => awsRequest(IotService, 'describeEndpoint', params), + }, + createIoTDataClient: (endpoint) => ({ + publish: (params) => awsRequest({ client: IotDataService, params: { endpoint } }, 'publish', params), + }) + }; + } +}; + +const { iot, createIoTDataClient } = getIoTClients(); async function resolveIotEndpoint() { - return awsRequest(IotService, 'describeEndpoint', { endpointType: 'iot:Data-ATS' }).then( + return iot.describeEndpoint({ endpointType: 'iot:Data-ATS' }).then( (data) => { return data.endpointAddress; } @@ -19,7 +57,8 @@ async function publishIotData(topic, message) { payload: Buffer.from(message), }; - return awsRequest({ client: IotDataService, params: { endpoint } }, 'publish', params); + const iotDataClient = createIoTDataClient(endpoint); + return iotDataClient.publish(params); }); } diff --git a/test/utils/kinesis.js b/test/utils/kinesis.js index 39d19803a7..d3a8101372 100644 --- a/test/utils/kinesis.js +++ b/test/utils/kinesis.js @@ -1,7 +1,40 @@ 'use strict'; const awsRequest = require('@serverless/test/aws-request'); -const KinesisService = require('aws-sdk').Kinesis; + +// Support for both AWS SDK v2 and v3 +const getKinesisClient = () => { + if (process.env.SLS_AWS_SDK_V3 === 'true') { + // AWS SDK v3 + const { KinesisClient } = require('@aws-sdk/client-kinesis'); + const { + CreateStreamCommand, + DeleteStreamCommand, + DescribeStreamCommand, + PutRecordCommand + } = require('@aws-sdk/client-kinesis'); + + const client = new KinesisClient({ region: 'us-east-1' }); + + return { + createStream: (params) => client.send(new CreateStreamCommand(params)), + deleteStream: (params) => client.send(new DeleteStreamCommand(params)), + describeStream: (params) => client.send(new DescribeStreamCommand(params)), + putRecord: (params) => client.send(new PutRecordCommand(params)), + }; + } else { + // AWS SDK v2 + const KinesisService = require('aws-sdk').Kinesis; + return { + createStream: (params) => awsRequest(KinesisService, 'createStream', params), + deleteStream: (params) => awsRequest(KinesisService, 'deleteStream', params), + describeStream: (params) => awsRequest(KinesisService, 'describeStream', params), + putRecord: (params) => awsRequest(KinesisService, 'putRecord', params), + }; + } +}; + +const kinesis = getKinesisClient(); async function waitForKinesisStream(streamName) { const params = { @@ -9,7 +42,7 @@ async function waitForKinesisStream(streamName) { }; return new Promise((resolve) => { const interval = setInterval(() => { - awsRequest(KinesisService, 'describeStream', params).then((data) => { + kinesis.describeStream(params).then((data) => { const status = data.StreamDescription.StreamStatus; if (status === 'ACTIVE') { clearInterval(interval); @@ -27,7 +60,7 @@ async function createKinesisStream(streamName) { StreamName: streamName, }; - return awsRequest(KinesisService, 'createStream', params).then(() => + return kinesis.createStream(params).then(() => waitForKinesisStream(streamName) ); } @@ -37,7 +70,7 @@ async function deleteKinesisStream(streamName) { StreamName: streamName, }; - return awsRequest(KinesisService, 'deleteStream', params); + return kinesis.deleteStream(params); } async function putKinesisRecord(streamName, message) { @@ -47,7 +80,7 @@ async function putKinesisRecord(streamName, message) { PartitionKey: streamName, // test streams are single shards }; - return awsRequest(KinesisService, 'putRecord', params); + return kinesis.putRecord(params); } module.exports = { diff --git a/test/utils/misc.js b/test/utils/misc.js index 9aa0457f0c..4653c2eec3 100644 --- a/test/utils/misc.js +++ b/test/utils/misc.js @@ -1,9 +1,31 @@ 'use strict'; const awsRequest = require('@serverless/test/aws-request'); -const CloudWatchLogsService = require('aws-sdk').CloudWatchLogs; const wait = require('timers-ext/promise/sleep'); +// Support for both AWS SDK v2 and v3 +const getCloudWatchLogsClient = () => { + if (process.env.SLS_AWS_SDK_V3 === 'true') { + // AWS SDK v3 + const { CloudWatchLogsClient } = require('@aws-sdk/client-cloudwatch-logs'); + const { FilterLogEventsCommand } = require('@aws-sdk/client-cloudwatch-logs'); + + const client = new CloudWatchLogsClient({ region: 'us-east-1' }); + + return { + filterLogEvents: (params) => client.send(new FilterLogEventsCommand(params)), + }; + } else { + // AWS SDK v2 + const CloudWatchLogsService = require('aws-sdk').CloudWatchLogs; + return { + filterLogEvents: (params) => awsRequest(CloudWatchLogsService, 'filterLogEvents', params), + }; + } +}; + +const cloudWatchLogs = getCloudWatchLogsClient(); + const logger = console; function replaceEnv(values) { @@ -33,7 +55,7 @@ async function confirmCloudWatchLogs(logGroupName, trigger, options = {}) { const timeout = options.timeout || 3 * 60 * 1000; return trigger() .then(() => wait(1000)) - .then(() => awsRequest(CloudWatchLogsService, 'filterLogEvents', { logGroupName })) + .then(() => cloudWatchLogs.filterLogEvents({ logGroupName })) .then(({ events }) => { if (events.length) { if (options.checkIsComplete) { diff --git a/test/utils/sns.js b/test/utils/sns.js index 30263a3645..c871a25dcd 100644 --- a/test/utils/sns.js +++ b/test/utils/sns.js @@ -1,18 +1,51 @@ 'use strict'; const awsRequest = require('@serverless/test/aws-request'); -const SNSService = require('aws-sdk').SNS; + +// Support for both AWS SDK v2 and v3 +const getSNSClient = () => { + if (process.env.SLS_AWS_SDK_V3 === 'true') { + // AWS SDK v3 + const { SNSClient } = require('@aws-sdk/client-sns'); + const { + CreateTopicCommand, + DeleteTopicCommand, + ListTopicsCommand, + PublishCommand + } = require('@aws-sdk/client-sns'); + + const client = new SNSClient({ region: 'us-east-1' }); + + return { + createTopic: (params) => client.send(new CreateTopicCommand(params)), + deleteTopic: (params) => client.send(new DeleteTopicCommand(params)), + listTopics: (params) => client.send(new ListTopicsCommand(params)), + publish: (params) => client.send(new PublishCommand(params)), + }; + } else { + // AWS SDK v2 + const SNSService = require('aws-sdk').SNS; + return { + createTopic: (params) => awsRequest(SNSService, 'createTopic', params), + deleteTopic: (params) => awsRequest(SNSService, 'deleteTopic', params), + listTopics: (params) => awsRequest(SNSService, 'listTopics', params), + publish: (params) => awsRequest(SNSService, 'publish', params), + }; + } +}; + +const sns = getSNSClient(); async function createSnsTopic(topicName) { const params = { Name: topicName, }; - return awsRequest(SNSService, 'createTopic', params); + return sns.createTopic(params); } async function resolveTopicArn(topicName, nextToken = null) { - return awsRequest(SNSService, 'listTopics', { NextToken: nextToken }).then((data) => { + return sns.listTopics({ NextToken: nextToken }).then((data) => { const targetTopic = data.Topics.find((topic) => RegExp(topicName, 'g').test(topic.TopicArn)); if (targetTopic) return targetTopic.TopicArn; @@ -28,7 +61,7 @@ async function removeSnsTopic(topicName) { TopicArn: topicArn, }; - return awsRequest(SNSService, 'deleteTopic', params); + return sns.deleteTopic(params); }); } @@ -42,7 +75,7 @@ async function publishSnsMessage(topicName, message, messageAttributes = null) { params.MessageAttributes = messageAttributes; } - return awsRequest(SNSService, 'publish', params); + return sns.publish(params); }); } diff --git a/test/utils/sqs.js b/test/utils/sqs.js index d5a54638cb..48b083ecfa 100644 --- a/test/utils/sqs.js +++ b/test/utils/sqs.js @@ -1,32 +1,65 @@ 'use strict'; const awsRequest = require('@serverless/test/aws-request'); -const SQSService = require('aws-sdk').SQS; + +// Support for both AWS SDK v2 and v3 +const getSQSClient = () => { + if (process.env.SLS_AWS_SDK_V3 === 'true') { + // AWS SDK v3 + const { SQSClient } = require('@aws-sdk/client-sqs'); + const { + CreateQueueCommand, + DeleteQueueCommand, + GetQueueUrlCommand, + SendMessageCommand + } = require('@aws-sdk/client-sqs'); + + const client = new SQSClient({ region: 'us-east-1' }); + + return { + createQueue: (params) => client.send(new CreateQueueCommand(params)), + deleteQueue: (params) => client.send(new DeleteQueueCommand(params)), + getQueueUrl: (params) => client.send(new GetQueueUrlCommand(params)), + sendMessage: (params) => client.send(new SendMessageCommand(params)), + }; + } else { + // AWS SDK v2 + const SQSService = require('aws-sdk').SQS; + return { + createQueue: (params) => awsRequest(SQSService, 'createQueue', params), + deleteQueue: (params) => awsRequest(SQSService, 'deleteQueue', params), + getQueueUrl: (params) => awsRequest(SQSService, 'getQueueUrl', params), + sendMessage: (params) => awsRequest(SQSService, 'sendMessage', params), + }; + } +}; + +const sqs = getSQSClient(); async function createSqsQueue(queueName) { const params = { QueueName: queueName, }; - return awsRequest(SQSService, 'createQueue', params); + return sqs.createQueue(params); } async function deleteSqsQueue(queueName) { - return awsRequest(SQSService, 'getQueueUrl', { QueueName: queueName }).then((data) => { + return sqs.getQueueUrl({ QueueName: queueName }).then((data) => { const params = { QueueUrl: data.QueueUrl, }; - return awsRequest(SQSService, 'deleteQueue', params); + return sqs.deleteQueue(params); }); } async function sendSqsMessage(queueName, message) { - return awsRequest(SQSService, 'getQueueUrl', { QueueName: queueName }).then((data) => { + return sqs.getQueueUrl({ QueueName: queueName }).then((data) => { const params = { QueueUrl: data.QueueUrl, MessageBody: message, }; - return awsRequest(SQSService, 'sendMessage', params); + return sqs.sendMessage(params); }); } diff --git a/test/utils/websocket.js b/test/utils/websocket.js index f630e5f7ea..1d8bae9891 100644 --- a/test/utils/websocket.js +++ b/test/utils/websocket.js @@ -1,10 +1,46 @@ 'use strict'; const awsRequest = require('@serverless/test/aws-request'); -const ApiGatewayV2Service = require('aws-sdk').ApiGatewayV2; + +// Support for both AWS SDK v2 and v3 +const getApiGatewayV2Client = () => { + if (process.env.SLS_AWS_SDK_V3 === 'true') { + // AWS SDK v3 + const { ApiGatewayV2Client } = require('@aws-sdk/client-apigatewayv2'); + const { + CreateApiCommand, + DeleteApiCommand, + CreateStageCommand, + DeleteStageCommand, + GetRoutesCommand + } = require('@aws-sdk/client-apigatewayv2'); + + const client = new ApiGatewayV2Client({ region: 'us-east-1' }); + + return { + createApi: (params) => client.send(new CreateApiCommand(params)), + deleteApi: (params) => client.send(new DeleteApiCommand(params)), + createStage: (params) => client.send(new CreateStageCommand(params)), + deleteStage: (params) => client.send(new DeleteStageCommand(params)), + getRoutes: (params) => client.send(new GetRoutesCommand(params)), + }; + } else { + // AWS SDK v2 + const ApiGatewayV2Service = require('aws-sdk').ApiGatewayV2; + return { + createApi: (params) => awsRequest(ApiGatewayV2Service, 'createApi', params), + deleteApi: (params) => awsRequest(ApiGatewayV2Service, 'deleteApi', params), + createStage: (params) => awsRequest(ApiGatewayV2Service, 'createStage', params), + deleteStage: (params) => awsRequest(ApiGatewayV2Service, 'deleteStage', params), + getRoutes: (params) => awsRequest(ApiGatewayV2Service, 'getRoutes', params), + }; + } +}; + +const apiGatewayV2 = getApiGatewayV2Client(); async function createApi(name) { - return awsRequest(ApiGatewayV2Service, 'createApi', { + return apiGatewayV2.createApi({ Name: name, ProtocolType: 'WEBSOCKET', RouteSelectionExpression: '$request.body.action', @@ -16,11 +52,11 @@ async function createStage(apiId, stageName) { ApiId: apiId, StageName: stageName, }; - return awsRequest(ApiGatewayV2Service, 'createStage', params); + return apiGatewayV2.createStage(params); } async function deleteApi(id) { - return awsRequest(ApiGatewayV2Service, 'deleteApi', { + return apiGatewayV2.deleteApi({ ApiId: id, }); } @@ -30,11 +66,11 @@ async function deleteStage(apiId, stageName) { ApiId: apiId, StageName: stageName, }; - return awsRequest(ApiGatewayV2Service, 'deleteStage', params); + return apiGatewayV2.deleteStage(params); } async function getRoutes(apiId) { - return awsRequest(ApiGatewayV2Service, 'getRoutes', { ApiId: apiId }).then((data) => data.Items); + return apiGatewayV2.getRoutes({ ApiId: apiId }).then((data) => data.Items); } module.exports = { From 19213fc99afbf374df47e2fdd6c2dd03ac150ccb Mon Sep 17 00:00:00 2001 From: Graham Campbell Date: Sun, 8 Jun 2025 22:34:12 +0100 Subject: [PATCH 07/27] Self-review --- lib/plugins/aws/info/get-api-key-values.js | 3 +-- .../events/api-gateway/lib/hack/update-stage.js | 12 ++++-------- 2 files changed, 5 insertions(+), 10 deletions(-) diff --git a/lib/plugins/aws/info/get-api-key-values.js b/lib/plugins/aws/info/get-api-key-values.js index e5d3cd5104..b6db471c24 100644 --- a/lib/plugins/aws/info/get-api-key-values.js +++ b/lib/plugins/aws/info/get-api-key-values.js @@ -43,8 +43,7 @@ module.exports = { } if (apiKeyNames.length) { - return this.provider - ._awsRequest('CloudFormation', 'describeStackResources', { + return this._awsRequest('CloudFormation', 'describeStackResources', { StackName: this.provider.naming.getStackName(), }) .then((resources) => { diff --git a/lib/plugins/aws/package/compile/events/api-gateway/lib/hack/update-stage.js b/lib/plugins/aws/package/compile/events/api-gateway/lib/hack/update-stage.js index 7e7c6e5997..a640c1a05d 100644 --- a/lib/plugins/aws/package/compile/events/api-gateway/lib/hack/update-stage.js +++ b/lib/plugins/aws/package/compile/events/api-gateway/lib/hack/update-stage.js @@ -164,8 +164,7 @@ async function resolveRestApiId() { ? apiGatewayResource.Properties.Name : this.provider.naming.getApiGatewayName(); const resolveFromAws = (position) => - this.provider - ._awsRequest('APIGateway', 'getRestApis', { position, limit: 500 }) + this._awsRequest('APIGateway', 'getRestApis', { position, limit: 500 }) .then((result) => { const restApi = result.items.find((api) => api.name === apiName); if (restApi) return restApi.id; @@ -181,8 +180,7 @@ async function resolveRestApiId() { async function resolveStage() { const restApiId = this.apiGatewayRestApiId; - return this.provider - ._awsRequest('APIGateway', 'getStage', { + return this._awsRequest('APIGateway', 'getStage', { restApiId, stageName: this.provider.getApiGatewayStage(), }) @@ -198,8 +196,7 @@ async function resolveDeploymentId() { if (!Object.keys(this.apiGatewayStageState).length) { const restApiId = this.apiGatewayRestApiId; - return this.provider - ._awsRequest('APIGateway', 'getDeployments', { + return this._awsRequest('APIGateway', 'getDeployments', { restApiId, limit: 500, }) @@ -403,8 +400,7 @@ async function removeAccessLoggingLogGroup() { // ensure that the log group is removed. Otherwise we'll run into duplicate // log group name issues when logs are enabled again if (!accessLogging) { - return this.provider - ._awsRequest('CloudWatchLogs', 'deleteLogGroup', { + return this._awsRequest('CloudWatchLogs', 'deleteLogGroup', { logGroupName, }) .catch(() => { From 84185c829629d5cc4edf0916f27f1a80a76a44ca Mon Sep 17 00:00:00 2001 From: Graham Campbell Date: Sun, 8 Jun 2025 22:46:11 +0100 Subject: [PATCH 08/27] Self-review --- lib/aws/client-factory.js | 4 +- lib/aws/commands.js | 18 +- lib/aws/config.js | 15 +- lib/aws/error-utils.js | 102 ++++---- .../sources/instance-dependent/get-s3.js | 14 +- .../aws/deploy/lib/check-for-changes.js | 13 +- lib/plugins/aws/deploy/lib/create-stack.js | 55 +++-- lib/plugins/aws/info/get-api-key-values.js | 17 +- lib/plugins/aws/info/get-stack-info.js | 7 +- lib/plugins/aws/lib/monitor-stack.js | 220 +++++++++--------- lib/plugins/aws/metrics.js | 3 +- .../api-gateway/lib/hack/update-stage.js | 32 +-- lib/plugins/aws/provider.js | 28 ++- lib/plugins/aws/rollback-function.js | 3 +- lib/plugins/aws/utils/resolve-cf-ref-value.js | 27 ++- test/utils/api-gateway.js | 8 +- test/utils/cloudformation.js | 13 +- test/utils/cognito.js | 86 +++---- test/utils/dynamodb.js | 4 +- test/utils/event-bridge.js | 8 +- test/utils/iot.js | 23 +- test/utils/kinesis.js | 12 +- test/utils/misc.js | 4 +- test/utils/s3.js | 8 +- test/utils/sns.js | 8 +- test/utils/sqs.js | 8 +- test/utils/websocket.js | 8 +- 27 files changed, 369 insertions(+), 379 deletions(-) diff --git a/lib/aws/client-factory.js b/lib/aws/client-factory.js index 88c9134254..915966f7f0 100644 --- a/lib/aws/client-factory.js +++ b/lib/aws/client-factory.js @@ -51,7 +51,7 @@ class AWSClientFactory { // Create a cache key based on service and config const configKey = JSON.stringify({ serviceName, ...this.baseConfig, ...overrideConfig }); - + if (!this.clients.has(configKey)) { const clientConfig = { ...this.baseConfig, ...overrideConfig }; this.clients.set(configKey, new ClientClass(clientConfig)); @@ -98,4 +98,4 @@ class AWSClientFactory { } } -module.exports = AWSClientFactory; \ No newline at end of file +module.exports = AWSClientFactory; diff --git a/lib/aws/commands.js b/lib/aws/commands.js index 907b6acf8f..62031f5d32 100644 --- a/lib/aws/commands.js +++ b/lib/aws/commands.js @@ -14,9 +14,7 @@ const { } = require('@aws-sdk/client-api-gateway'); // API Gateway V2 Commands -const { - GetApiCommand, -} = require('@aws-sdk/client-apigatewayv2'); +const { GetApiCommand } = require('@aws-sdk/client-apigatewayv2'); // CloudFormation Commands const { @@ -37,9 +35,7 @@ const { } = require('@aws-sdk/client-cloudformation'); // CloudWatch Commands -const { - GetMetricStatisticsCommand, -} = require('@aws-sdk/client-cloudwatch'); +const { GetMetricStatisticsCommand } = require('@aws-sdk/client-cloudwatch'); // CloudWatch Logs Commands const { @@ -109,14 +105,10 @@ const { } = require('@aws-sdk/client-s3'); // SSM Commands -const { - GetParameterCommand, -} = require('@aws-sdk/client-ssm'); +const { GetParameterCommand } = require('@aws-sdk/client-ssm'); // STS Commands -const { - GetCallerIdentityCommand, -} = require('@aws-sdk/client-sts'); +const { GetCallerIdentityCommand } = require('@aws-sdk/client-sts'); /** * Map v2 method names to v3 command classes @@ -280,4 +272,4 @@ module.exports = { getCommand, createCommand, getServiceMethods, -}; \ No newline at end of file +}; diff --git a/lib/aws/config.js b/lib/aws/config.js index 2a72ac4725..ff07a716ec 100644 --- a/lib/aws/config.js +++ b/lib/aws/config.js @@ -16,7 +16,8 @@ const fs = require('fs'); */ function buildClientConfig(options = {}) { const config = { - region: options.region || process.env.AWS_REGION || process.env.AWS_DEFAULT_REGION || 'us-east-1', + region: + options.region || process.env.AWS_REGION || process.env.AWS_DEFAULT_REGION || 'us-east-1', maxAttempts: options.maxAttempts || getMaxRetries(), retryMode: options.retryMode || 'standard', }; @@ -72,7 +73,7 @@ function buildHttpOptions() { rejectUnauthorized: true, ca: caCerts, }; - + if (proxy) { // Merge with existing proxy agent options Object.assign(httpOptions.httpsAgent.options, agentOptions); @@ -147,7 +148,7 @@ function getCACertificates() { */ function buildS3Config(baseConfig = {}, useAcceleration = false) { const s3Config = { ...baseConfig }; - + if (useAcceleration) { s3Config.useAccelerateEndpoint = true; } @@ -163,11 +164,9 @@ function buildS3Config(baseConfig = {}, useAcceleration = false) { */ function shouldUseS3Acceleration(method, params) { const accelerationCompatibleMethods = new Set(['upload', 'putObject']); - + return ( - accelerationCompatibleMethods.has(method) && - params && - params.isS3TransferAccelerationEnabled + accelerationCompatibleMethods.has(method) && params && params.isS3TransferAccelerationEnabled ); } @@ -179,4 +178,4 @@ module.exports = { getMaxRetries, getProxyUrl, getCACertificates, -}; \ No newline at end of file +}; diff --git a/lib/aws/error-utils.js b/lib/aws/error-utils.js index 447410681d..ba616efe97 100644 --- a/lib/aws/error-utils.js +++ b/lib/aws/error-utils.js @@ -6,35 +6,35 @@ */ const ERROR_CODE_MAPPINGS = { // CloudFormation errors - 'ValidationError': 'ValidationException', - 'AlreadyExistsException': 'AlreadyExistsException', - 'LimitExceededException': 'LimitExceededException', - 'InsufficientCapabilitiesException': 'InsufficientCapabilitiesException', - + ValidationError: 'ValidationException', + AlreadyExistsException: 'AlreadyExistsException', + LimitExceededException: 'LimitExceededException', + InsufficientCapabilitiesException: 'InsufficientCapabilitiesException', + // S3 errors - 'NoSuchBucket': 'NoSuchBucket', - 'NoSuchKey': 'NoSuchKey', - 'BucketAlreadyExists': 'BucketAlreadyExists', - 'BucketAlreadyOwnedByYou': 'BucketAlreadyOwnedByYou', - + NoSuchBucket: 'NoSuchBucket', + NoSuchKey: 'NoSuchKey', + BucketAlreadyExists: 'BucketAlreadyExists', + BucketAlreadyOwnedByYou: 'BucketAlreadyOwnedByYou', + // Lambda errors - 'ResourceNotFoundException': 'ResourceNotFoundException', - 'ResourceConflictException': 'ResourceConflictException', - 'InvalidParameterValueException': 'InvalidParameterValueException', - 'TooManyRequestsException': 'TooManyRequestsException', - + ResourceNotFoundException: 'ResourceNotFoundException', + ResourceConflictException: 'ResourceConflictException', + InvalidParameterValueException: 'InvalidParameterValueException', + TooManyRequestsException: 'TooManyRequestsException', + // IAM errors - 'NoSuchEntity': 'NoSuchEntityException', - 'EntityAlreadyExists': 'EntityAlreadyExistsException', - 'MalformedPolicyDocument': 'MalformedPolicyDocumentException', - + NoSuchEntity: 'NoSuchEntityException', + EntityAlreadyExists: 'EntityAlreadyExistsException', + MalformedPolicyDocument: 'MalformedPolicyDocumentException', + // General AWS errors - 'AccessDenied': 'AccessDeniedException', - 'UnauthorizedOperation': 'UnauthorizedException', - 'Throttling': 'ThrottlingException', - 'RequestExpired': 'RequestExpiredException', - 'CredentialsError': 'CredentialsError', - 'ExpiredTokenException': 'ExpiredTokenException', + AccessDenied: 'AccessDeniedException', + UnauthorizedOperation: 'UnauthorizedException', + Throttling: 'ThrottlingException', + RequestExpired: 'RequestExpiredException', + CredentialsError: 'CredentialsError', + ExpiredTokenException: 'ExpiredTokenException', }; /** @@ -54,15 +54,15 @@ function transformV3Error(error) { // Create a new error object with v2-compatible properties const transformedError = new Error(error.message || 'Unknown AWS error'); - + // Copy all original properties Object.assign(transformedError, error); - + // Map v3 'name' to v2 'code' for backward compatibility if (error.name && !error.code) { transformedError.code = error.name; } - + // Map some common v3 properties to v2 equivalents if (error.$metadata) { transformedError.statusCode = error.$metadata.httpStatusCode; @@ -93,13 +93,13 @@ function isRetryableError(error) { // Check metadata for retry info if (error.$metadata) { const statusCode = error.$metadata.httpStatusCode; - + // 5xx errors are generally retryable if (statusCode >= 500) return true; - + // 429 (Too Many Requests) is retryable if (statusCode === 429) return true; - + // 403 (Forbidden) is generally not retryable if (statusCode === 403) return false; } @@ -127,7 +127,7 @@ function isRetryableError(error) { */ function isCredentialsError(error) { if (!error) return false; - + const errorCode = error.name || error.code; const credentialsErrors = [ 'CredentialsError', @@ -137,8 +137,10 @@ function isCredentialsError(error) { 'SignatureDoesNotMatch', ]; - return credentialsErrors.includes(errorCode) || - (error.message && error.message.includes('Missing credentials')); + return ( + credentialsErrors.includes(errorCode) || + (error.message && error.message.includes('Missing credentials')) + ); } /** @@ -150,18 +152,18 @@ function getFriendlyErrorMessage(error) { if (!error) return 'Unknown error occurred'; const errorCode = error.name || error.code; - + // Specific error message mappings const friendlyMessages = { - 'ValidationException': 'Invalid parameters provided', - 'AlreadyExistsException': 'Resource already exists', - 'NoSuchBucket': 'S3 bucket does not exist', - 'NoSuchKey': 'S3 object does not exist', - 'ResourceNotFoundException': 'AWS resource not found', - 'AccessDeniedException': 'Access denied - check your permissions', - 'ThrottlingException': 'Request rate exceeded - please retry', - 'CredentialsError': 'Invalid AWS credentials', - 'ExpiredTokenException': 'AWS credentials have expired', + ValidationException: 'Invalid parameters provided', + AlreadyExistsException: 'Resource already exists', + NoSuchBucket: 'S3 bucket does not exist', + NoSuchKey: 'S3 object does not exist', + ResourceNotFoundException: 'AWS resource not found', + AccessDeniedException: 'Access denied - check your permissions', + ThrottlingException: 'Request rate exceeded - please retry', + CredentialsError: 'Invalid AWS credentials', + ExpiredTokenException: 'AWS credentials have expired', }; return friendlyMessages[errorCode] || error.message || `AWS Error: ${errorCode}`; @@ -174,11 +176,11 @@ function getFriendlyErrorMessage(error) { */ function getRootCauseError(error) { let rootError = error; - + while (rootError && (rootError.originalError || rootError.cause)) { rootError = rootError.originalError || rootError.cause; } - + return rootError || error; } @@ -191,7 +193,7 @@ function getRootCauseError(error) { */ function createStandardError(error, service, operation) { let standardError; - + if (typeof error === 'string') { standardError = new Error(error); } else if (error instanceof Error) { @@ -199,12 +201,12 @@ function createStandardError(error, service, operation) { } else { standardError = new Error('Unknown error'); } - + // Add context information standardError.service = service; standardError.operation = operation; standardError.timestamp = new Date().toISOString(); - + return transformV3Error(standardError); } @@ -216,4 +218,4 @@ module.exports = { getFriendlyErrorMessage, getRootCauseError, createStandardError, -}; \ No newline at end of file +}; diff --git a/lib/configuration/variables/sources/instance-dependent/get-s3.js b/lib/configuration/variables/sources/instance-dependent/get-s3.js index 9a3e679760..e22f2f9a4f 100644 --- a/lib/configuration/variables/sources/instance-dependent/get-s3.js +++ b/lib/configuration/variables/sources/instance-dependent/get-s3.js @@ -33,9 +33,19 @@ module.exports = (serverlessInstance) => { try { const provider = serverlessInstance.getProvider('aws'); if (provider._v3Enabled) { - return await provider.requestV3('S3', 'getObject', { Bucket: bucketName, Key: key }, { useCache: true }); + return await provider.requestV3( + 'S3', + 'getObject', + { Bucket: bucketName, Key: key }, + { useCache: true } + ); } - return await provider.request('S3', 'getObject', { Bucket: bucketName, Key: key }, { useCache: true }); + return await provider.request( + 'S3', + 'getObject', + { Bucket: bucketName, Key: key }, + { useCache: true } + ); } catch (error) { // Check for normalized error code instead of native one if (error.code === 'AWS_S3_GET_OBJECT_NO_SUCH_KEY') return null; diff --git a/lib/plugins/aws/deploy/lib/check-for-changes.js b/lib/plugins/aws/deploy/lib/check-for-changes.js index dfbc813cda..9bd79b4b77 100644 --- a/lib/plugins/aws/deploy/lib/check-for-changes.js +++ b/lib/plugins/aws/deploy/lib/check-for-changes.js @@ -105,8 +105,8 @@ module.exports = { const getFunctionResults = this.serverless.service.getAllFunctions().map((funName) => { const functionObj = this.serverless.service.getFunction(funName); return this._awsRequest('Lambda', 'getFunction', { - FunctionName: functionObj.name, - }) + FunctionName: functionObj.name, + }) .then((res) => new Date(res.Configuration.LastModified)) .catch((err) => { if (err.providerError && err.providerError.statusCode === 403) { @@ -313,12 +313,9 @@ module.exports = { const cloudwatchLogEvents = params.cloudwatchLogEvents; const CLOUDWATCHLOG_LOG_GROUP_EVENT_PER_FUNCTION_LIMIT = 2; - const response = await this._awsRequest( - 'CloudWatchLogs', - 'describeSubscriptionFilters', - { logGroupName } - ) - .catch(() => ({ subscriptionFilters: [] })); + const response = await this._awsRequest('CloudWatchLogs', 'describeSubscriptionFilters', { + logGroupName, + }).catch(() => ({ subscriptionFilters: [] })); if (response.subscriptionFilters.length === 0) { log.debug('no subscription filters detected'); return false; diff --git a/lib/plugins/aws/deploy/lib/create-stack.js b/lib/plugins/aws/deploy/lib/create-stack.js index 8faa4791b4..4cfc081fdd 100644 --- a/lib/plugins/aws/deploy/lib/create-stack.js +++ b/lib/plugins/aws/deploy/lib/create-stack.js @@ -84,37 +84,36 @@ module.exports = { return BbPromise.bind(this) .then(() => - this._cloudFormationRequest('describeStacks', { StackName: stackName }) - .then((data) => { - const shouldCheckStackOutput = - // check stack output only if acceleration is requested - this.provider.isS3TransferAccelerationEnabled() && - // custom deployment bucket won't generate any output (no check) - !this.serverless.service.provider.deploymentBucket; - if (shouldCheckStackOutput) { - const isAlreadyAccelerated = data.Stacks[0].Outputs.some( - (output) => output.OutputKey === 'ServerlessDeploymentBucketAccelerated' - ); + this._cloudFormationRequest('describeStacks', { StackName: stackName }).then((data) => { + const shouldCheckStackOutput = + // check stack output only if acceleration is requested + this.provider.isS3TransferAccelerationEnabled() && + // custom deployment bucket won't generate any output (no check) + !this.serverless.service.provider.deploymentBucket; + if (shouldCheckStackOutput) { + const isAlreadyAccelerated = data.Stacks[0].Outputs.some( + (output) => output.OutputKey === 'ServerlessDeploymentBucketAccelerated' + ); - if (!isAlreadyAccelerated) { - log.info('Not using S3 Transfer Acceleration (1st deploy)'); - this.provider.disableTransferAccelerationForCurrentDeploy(); - } + if (!isAlreadyAccelerated) { + log.info('Not using S3 Transfer Acceleration (1st deploy)'); + this.provider.disableTransferAccelerationForCurrentDeploy(); } + } - const stackStatus = data.Stacks[0].StackStatus; - if (inactiveStateNames.has(stackStatus)) { - const errorMessage = [ - 'Service cannot be deployed as the CloudFormation stack ', - `is in the '${stackStatus}' state. `, - 'This may signal either that stack is currently deployed by a different entity, ', - 'or that the previous deployment failed and was left in an abnormal state, ', - "in which case you can mitigate the issue by running 'sls remove' command", - ].join(''); - throw new ServerlessError(errorMessage, 'AWS_CLOUDFORMATION_INACTIVE_STACK'); - } - return BbPromise.resolve('alreadyCreated'); - }) + const stackStatus = data.Stacks[0].StackStatus; + if (inactiveStateNames.has(stackStatus)) { + const errorMessage = [ + 'Service cannot be deployed as the CloudFormation stack ', + `is in the '${stackStatus}' state. `, + 'This may signal either that stack is currently deployed by a different entity, ', + 'or that the previous deployment failed and was left in an abnormal state, ', + "in which case you can mitigate the issue by running 'sls remove' command", + ].join(''); + throw new ServerlessError(errorMessage, 'AWS_CLOUDFORMATION_INACTIVE_STACK'); + } + return BbPromise.resolve('alreadyCreated'); + }) ) .catch((e) => { if (e.message.indexOf('does not exist') > -1) { diff --git a/lib/plugins/aws/info/get-api-key-values.js b/lib/plugins/aws/info/get-api-key-values.js index b6db471c24..c086723887 100644 --- a/lib/plugins/aws/info/get-api-key-values.js +++ b/lib/plugins/aws/info/get-api-key-values.js @@ -4,18 +4,6 @@ const BbPromise = require('bluebird'); const _ = require('lodash'); module.exports = { - /** - * Helper method to route AWS requests through v2 or v3 based on feature flag - * @private - */ - async _awsRequest(service, method, params) { - // Use v3 if feature flag is enabled, otherwise fallback to v2 - if (this.provider._v3Enabled) { - return this.provider.requestV3(service, method, params); - } - return this.provider.request(service, method, params); - }, - async getApiKeyValues() { const info = this.gatheredData.info; info.apiKeys = []; @@ -43,7 +31,8 @@ module.exports = { } if (apiKeyNames.length) { - return this._awsRequest('CloudFormation', 'describeStackResources', { + return this.provider + ._awsRequest('CloudFormation', 'describeStackResources', { StackName: this.provider.naming.getStackName(), }) .then((resources) => { @@ -53,7 +42,7 @@ module.exports = { .value(); return Promise.all( apiKeys.map((apiKey) => - this._awsRequest('APIGateway', 'getApiKey', { + this.provider._awsRequest('APIGateway', 'getApiKey', { apiKey, includeValue: true, }) diff --git a/lib/plugins/aws/info/get-stack-info.js b/lib/plugins/aws/info/get-stack-info.js index 7234748b57..3a9c4b9cfa 100644 --- a/lib/plugins/aws/info/get-stack-info.js +++ b/lib/plugins/aws/info/get-stack-info.js @@ -36,10 +36,11 @@ module.exports = { const stackData = {}; const sdkRequests = [ - this._awsRequest('CloudFormation', 'describeStacks', { StackName: stackName }) - .then((result) => { + this._awsRequest('CloudFormation', 'describeStacks', { StackName: stackName }).then( + (result) => { if (result) stackData.outputs = result.Stacks[0].Outputs; - }), + } + ), ]; const httpApiId = this.serverless.service.provider.httpApi && this.serverless.service.provider.httpApi.id; diff --git a/lib/plugins/aws/lib/monitor-stack.js b/lib/plugins/aws/lib/monitor-stack.js index 8e036a09cb..93e8e56b07 100644 --- a/lib/plugins/aws/lib/monitor-stack.js +++ b/lib/plugins/aws/lib/monitor-stack.js @@ -47,128 +47,122 @@ module.exports = { ) { return wait(getMonitoringFrequency(options.frequency)) .then(() => - this._cloudFormationRequest('describeStackEvents', { StackName: cfData.StackId }) - .then( - ({ StackEvents: stackEvents }) => { - if (!stackEvents.length) return; + this._cloudFormationRequest('describeStackEvents', { StackName: cfData.StackId }).then( + ({ StackEvents: stackEvents }) => { + if (!stackEvents.length) return; - // Resolve only events applicable to current deployment - stackEvents.some((event, index) => { - if (firstEventId) { - if (event.EventId !== firstEventId) return false; - } else { - if (event.ResourceType !== 'AWS::CloudFormation::Stack') return false; - if (event.ResourceStatus !== `${action.toUpperCase()}_IN_PROGRESS`) return false; - firstEventId = event.EventId; - } - stackEvents = stackEvents.slice(0, index + 1); - return true; - }); - stackEvents.reverse(); - - // Loop through stack events - stackEvents.forEach((event) => { - if (loggedEventIds.has(event.EventId)) return; - const eventStatus = event.ResourceStatus || null; - // Keep track of stack status - if ( - event.ResourceType === 'AWS::CloudFormation::Stack' && - event.StackName === event.LogicalResourceId - ) { - stackStatus = eventStatus; - } - // Keep track of first failed event - if ( - eventStatus && - (eventStatus.endsWith('FAILED') || - eventStatus === 'UPDATE_ROLLBACK_IN_PROGRESS') && - stackLatestError === null - ) { - stackLatestError = event; - } - // Log stack events - log.info( - style.aside( - ` ${eventStatus} - ${event.ResourceType} - ${event.LogicalResourceId}` - ) - ); - - if ( - event.ResourceType !== 'AWS::CloudFormation::Stack' && - eventStatus && - eventStatus.endsWith('COMPLETE') - ) { - completedResources.add(event.LogicalResourceId); - } + // Resolve only events applicable to current deployment + stackEvents.some((event, index) => { + if (firstEventId) { + if (event.EventId !== firstEventId) return false; + } else { + if (event.ResourceType !== 'AWS::CloudFormation::Stack') return false; + if (event.ResourceStatus !== `${action.toUpperCase()}_IN_PROGRESS`) return false; + firstEventId = event.EventId; + } + stackEvents = stackEvents.slice(0, index + 1); + return true; + }); + stackEvents.reverse(); - if (action !== 'delete' && cfData.Changes) { - const progressMessagePrefix = (() => { - if (action === 'create') return 'Creating'; - if (action === 'update') return 'Updating'; - throw new Error(`Unrecgonized action: ${action}`); - })(); - mainProgress.notice( - `${progressMessagePrefix} CloudFormation stack (${completedResources.size}/${cfData.Changes.length})` - ); - } + // Loop through stack events + stackEvents.forEach((event) => { + if (loggedEventIds.has(event.EventId)) return; + const eventStatus = event.ResourceStatus || null; + // Keep track of stack status + if ( + event.ResourceType === 'AWS::CloudFormation::Stack' && + event.StackName === event.LogicalResourceId + ) { + stackStatus = eventStatus; + } + // Keep track of first failed event + if ( + eventStatus && + (eventStatus.endsWith('FAILED') || eventStatus === 'UPDATE_ROLLBACK_IN_PROGRESS') && + stackLatestError === null + ) { + stackLatestError = event; + } + // Log stack events + log.info( + style.aside(` ${eventStatus} - ${event.ResourceType} - ${event.LogicalResourceId}`) + ); - // Prepare for next monitoring action - loggedEventIds.add(event.EventId); - }); - // Handle stack create/update/delete failures if ( - stackLatestError && - (!this.options.verbose || - (stackStatus && - (stackStatus.endsWith('ROLLBACK_COMPLETE') || - ['DELETE_FAILED', 'DELETE_COMPLETE'].includes(stackStatus)))) + event.ResourceType !== 'AWS::CloudFormation::Stack' && + eventStatus && + eventStatus.endsWith('COMPLETE') ) { - const decoratedErrorMessage = `${stackLatestError.ResourceStatus}: ${ - stackLatestError.LogicalResourceId - } ${style.aside(`(${stackLatestError.ResourceType})`)}\n${ - stackLatestError.ResourceStatusReason - }\n\n${style.aside(`View the full error: ${style.link(stackUrl)}`)}`; + completedResources.add(event.LogicalResourceId); + } - let errorMessage = 'An error occurred: '; - errorMessage += `${stackLatestError.LogicalResourceId} - `; - errorMessage += `${ - stackLatestError.ResourceStatusReason || stackLatestError.ResourceStatus - }.`; - const errorCode = (() => { - if (stackLatestError.ResourceStatusReason) { - if ( - stackLatestError.ResourceStatusReason.startsWith( - 'Properties validation failed' - ) - ) { - return `AWS_CLOUD_FORMATION_${action.toUpperCase()}_STACK_INTERNAL_VALIDATION_ERROR`; - } - if ( - stackLatestError.ResourceStatusReason.includes('is not authorized to perform') - ) { - return `AWS_CLOUD_FORMATION_${action.toUpperCase()}_STACK_INTERNAL_INSUFFICIENT_PERMISSIONS`; - } - } - return ( - `AWS_CLOUD_FORMATION_${action.toUpperCase()}_STACK_INTERNAL` + - `${resourceTypeToErrorCodePostfix(stackLatestError.ResourceType)}_${ - stackLatestError.ResourceStatus - }` - ); + if (action !== 'delete' && cfData.Changes) { + const progressMessagePrefix = (() => { + if (action === 'create') return 'Creating'; + if (action === 'update') return 'Updating'; + throw new Error(`Unrecgonized action: ${action}`); })(); - throw new ServerlessError(errorMessage, errorCode, { - decoratedMessage: decoratedErrorMessage, - }); - } - }, - (e) => { - if (action === 'delete' && e.message.includes('does not exist')) { - stackStatus = 'DELETE_COMPLETE'; - return; + mainProgress.notice( + `${progressMessagePrefix} CloudFormation stack (${completedResources.size}/${cfData.Changes.length})` + ); } - throw e; + + // Prepare for next monitoring action + loggedEventIds.add(event.EventId); + }); + // Handle stack create/update/delete failures + if ( + stackLatestError && + (!this.options.verbose || + (stackStatus && + (stackStatus.endsWith('ROLLBACK_COMPLETE') || + ['DELETE_FAILED', 'DELETE_COMPLETE'].includes(stackStatus)))) + ) { + const decoratedErrorMessage = `${stackLatestError.ResourceStatus}: ${ + stackLatestError.LogicalResourceId + } ${style.aside(`(${stackLatestError.ResourceType})`)}\n${ + stackLatestError.ResourceStatusReason + }\n\n${style.aside(`View the full error: ${style.link(stackUrl)}`)}`; + + let errorMessage = 'An error occurred: '; + errorMessage += `${stackLatestError.LogicalResourceId} - `; + errorMessage += `${ + stackLatestError.ResourceStatusReason || stackLatestError.ResourceStatus + }.`; + const errorCode = (() => { + if (stackLatestError.ResourceStatusReason) { + if ( + stackLatestError.ResourceStatusReason.startsWith('Properties validation failed') + ) { + return `AWS_CLOUD_FORMATION_${action.toUpperCase()}_STACK_INTERNAL_VALIDATION_ERROR`; + } + if ( + stackLatestError.ResourceStatusReason.includes('is not authorized to perform') + ) { + return `AWS_CLOUD_FORMATION_${action.toUpperCase()}_STACK_INTERNAL_INSUFFICIENT_PERMISSIONS`; + } + } + return ( + `AWS_CLOUD_FORMATION_${action.toUpperCase()}_STACK_INTERNAL` + + `${resourceTypeToErrorCodePostfix(stackLatestError.ResourceType)}_${ + stackLatestError.ResourceStatus + }` + ); + })(); + throw new ServerlessError(errorMessage, errorCode, { + decoratedMessage: decoratedErrorMessage, + }); + } + }, + (e) => { + if (action === 'delete' && e.message.includes('does not exist')) { + stackStatus = 'DELETE_COMPLETE'; + return; } - ) + throw e; + } + ) ) .then(() => { if (validStatuses.has(stackStatus)) return stackStatus; diff --git a/lib/plugins/aws/metrics.js b/lib/plugins/aws/metrics.js index 5abfd06bb1..0af2c0a0d1 100644 --- a/lib/plugins/aws/metrics.js +++ b/lib/plugins/aws/metrics.js @@ -97,8 +97,7 @@ class AwsMetrics { Unit: 'Milliseconds', }); - const getMetrics = (params) => - this._cloudWatchRequest('getMetricStatistics', params); + const getMetrics = (params) => this._cloudWatchRequest('getMetricStatistics', params); return BbPromise.all([ getMetrics(invocationsParams), diff --git a/lib/plugins/aws/package/compile/events/api-gateway/lib/hack/update-stage.js b/lib/plugins/aws/package/compile/events/api-gateway/lib/hack/update-stage.js index a640c1a05d..bbfc726a5d 100644 --- a/lib/plugins/aws/package/compile/events/api-gateway/lib/hack/update-stage.js +++ b/lib/plugins/aws/package/compile/events/api-gateway/lib/hack/update-stage.js @@ -24,18 +24,6 @@ const defaultApiGatewayLogLevel = 'INFO'; // Stage resource (see https://github.com/serverless/serverless/pull/5692#issuecomment-467849311 for more information). module.exports = { - /** - * Helper method to route AWS requests through v2 or v3 based on feature flag - * @private - */ - async _awsRequest(service, method, params) { - // Use v3 if feature flag is enabled, otherwise fallback to v2 - if (this.provider._v3Enabled) { - return this.provider.requestV3(service, method, params); - } - return this.provider.request(service, method, params); - }, - defaultApiGatewayLogLevel, async updateStage() { return BbPromise.try(() => { @@ -164,7 +152,8 @@ async function resolveRestApiId() { ? apiGatewayResource.Properties.Name : this.provider.naming.getApiGatewayName(); const resolveFromAws = (position) => - this._awsRequest('APIGateway', 'getRestApis', { position, limit: 500 }) + this.provider + ._awsRequest('APIGateway', 'getRestApis', { position, limit: 500 }) .then((result) => { const restApi = result.items.find((api) => api.name === apiName); if (restApi) return restApi.id; @@ -180,7 +169,8 @@ async function resolveRestApiId() { async function resolveStage() { const restApiId = this.apiGatewayRestApiId; - return this._awsRequest('APIGateway', 'getStage', { + return this.provider + ._awsRequest('APIGateway', 'getStage', { restApiId, stageName: this.provider.getApiGatewayStage(), }) @@ -196,7 +186,8 @@ async function resolveDeploymentId() { if (!Object.keys(this.apiGatewayStageState).length) { const restApiId = this.apiGatewayRestApiId; - return this._awsRequest('APIGateway', 'getDeployments', { + return this.provider + ._awsRequest('APIGateway', 'getDeployments', { restApiId, limit: 500, }) @@ -221,7 +212,7 @@ async function ensureStage() { const restApiId = this.apiGatewayRestApiId; const deploymentId = this.apiGatewayDeploymentId; - return this._awsRequest('APIGateway', 'createStage', { + return this.provider._awsRequest('APIGateway', 'createStage', { deploymentId, restApiId, stageName: this.provider.getApiGatewayStage(), @@ -357,14 +348,14 @@ function handleTags() { async function addTags() { const requests = this.apiGatewayTagResourceParams.map((tagResourceParam) => - this._awsRequest('APIGateway', 'tagResource', tagResourceParam) + this.provider._awsRequest('APIGateway', 'tagResource', tagResourceParam) ); return BbPromise.all(requests); } async function removeTags() { const requests = this.apiGatewayUntagResourceParams.map((untagResourceParam) => - this._awsRequest('APIGateway', 'untagResource', untagResourceParam) + this.provider._awsRequest('APIGateway', 'untagResource', untagResourceParam) ); return BbPromise.all(requests); } @@ -374,7 +365,7 @@ function applyUpdates() { const patchOperations = this.apiGatewayStagePatchOperations; if (patchOperations.length) { - return this._awsRequest('APIGateway', 'updateStage', { + return this.provider._awsRequest('APIGateway', 'updateStage', { restApiId, stageName: this.provider.getApiGatewayStage(), patchOperations, @@ -400,7 +391,8 @@ async function removeAccessLoggingLogGroup() { // ensure that the log group is removed. Otherwise we'll run into duplicate // log group name issues when logs are enabled again if (!accessLogging) { - return this._awsRequest('CloudWatchLogs', 'deleteLogGroup', { + return this.provider + ._awsRequest('CloudWatchLogs', 'deleteLogGroup', { logGroupName, }) .catch(() => { diff --git a/lib/plugins/aws/provider.js b/lib/plugins/aws/provider.js index 8a4945a0ba..54f6e24b34 100644 --- a/lib/plugins/aws/provider.js +++ b/lib/plugins/aws/provider.js @@ -228,11 +228,11 @@ class AwsProvider { // Notice: provider.sdk is used by plugins. Do not remove without deprecating first and // offering a reliable alternative this.sdk = AWS; - + // AWS SDK v3 infrastructure this.clientFactory = null; // Will be initialized when needed this._v3Enabled = process.env.SLS_AWS_SDK_V3 === 'true'; // Feature flag for gradual rollout - + this.serverless.setProvider(constants.providerName, this); this.hooks = { initialize: () => { @@ -1752,13 +1752,13 @@ class AwsProvider { // Build client configuration const clientConfig = this._buildV3ClientConfig(service, method, options); - + // Create command const command = createCommand(service, method, params); - + // Send request const result = await this.clientFactory.send(service, command, clientConfig); - + return result; } catch (error) { // Transform v3 error to be compatible with existing error handling @@ -1766,6 +1766,22 @@ class AwsProvider { } } + /** + * Helper method to route AWS requests through v2 or v3 based on feature flag + * @param {string} service - Service name + * @param {string} method - Method name + * @param {Object} params - Parameters + * @param {Object} options - Options + * @returns {Promise} AWS API response + */ + async _awsRequest(service, method, params, options) { + // Use v3 if feature flag is enabled, otherwise fallback to v2 + if (this._v3Enabled) { + return this.requestV3(service, method, params, options); + } + return this.request(service, method, params, options); + } + /** * Get AWS SDK v3 client for direct use * @param {string} service - AWS service name @@ -1800,7 +1816,7 @@ class AwsProvider { _buildV3ClientConfig(service, method, options) { const baseConfig = this._getV3BaseConfig(); const requestOptions = _.isObject(options) ? options : {}; - + // Override region if specified if (requestOptions.region) { baseConfig.region = requestOptions.region; diff --git a/lib/plugins/aws/rollback-function.js b/lib/plugins/aws/rollback-function.js index edaae27231..23993500bb 100644 --- a/lib/plugins/aws/rollback-function.js +++ b/lib/plugins/aws/rollback-function.js @@ -58,8 +58,7 @@ class AwsRollbackFunction { Qualifier: funcVersion, }; - return this - ._lambdaRequest('getFunction', params) + return this._lambdaRequest('getFunction', params) .then((func) => func) .catch((error) => { if (error.message.match(/not found/)) { diff --git a/lib/plugins/aws/utils/resolve-cf-ref-value.js b/lib/plugins/aws/utils/resolve-cf-ref-value.js index f671ac00dd..1c22c651ce 100644 --- a/lib/plugins/aws/utils/resolve-cf-ref-value.js +++ b/lib/plugins/aws/utils/resolve-cf-ref-value.js @@ -8,21 +8,20 @@ async function resolveCfRefValue(provider, resourceLogicalId, sdkParams = {}) { 'CloudFormation', 'listStackResources', Object.assign(sdkParams, { StackName: provider.naming.getStackName() }) - ) - .then((result) => { - const targetStackResource = result.StackResourceSummaries.find( - (stackResource) => stackResource.LogicalResourceId === resourceLogicalId - ); - if (targetStackResource) return targetStackResource.PhysicalResourceId; - if (result.NextToken) { - return resolveCfRefValue(provider, resourceLogicalId, { NextToken: result.NextToken }); - } + ).then((result) => { + const targetStackResource = result.StackResourceSummaries.find( + (stackResource) => stackResource.LogicalResourceId === resourceLogicalId + ); + if (targetStackResource) return targetStackResource.PhysicalResourceId; + if (result.NextToken) { + return resolveCfRefValue(provider, resourceLogicalId, { NextToken: result.NextToken }); + } - throw new ServerlessError( - `Could not resolve Ref with name ${resourceLogicalId}. Are you sure this value matches one resource logical ID ?`, - 'CF_REF_RESOLUTION' - ); - }); + throw new ServerlessError( + `Could not resolve Ref with name ${resourceLogicalId}. Are you sure this value matches one resource logical ID ?`, + 'CF_REF_RESOLUTION' + ); + }); } module.exports = resolveCfRefValue; diff --git a/test/utils/api-gateway.js b/test/utils/api-gateway.js index 00939cb3a5..f409f1f0b1 100644 --- a/test/utils/api-gateway.js +++ b/test/utils/api-gateway.js @@ -8,15 +8,15 @@ const getAPIGatewayClient = () => { if (process.env.SLS_AWS_SDK_V3 === 'true') { // AWS SDK v3 const { APIGatewayClient } = require('@aws-sdk/client-api-gateway'); - const { + const { CreateRestApiCommand, DeleteRestApiCommand, GetResourcesCommand, - GetRestApisCommand + GetRestApisCommand, } = require('@aws-sdk/client-api-gateway'); - + const client = new APIGatewayClient({ region: 'us-east-1' }); - + return { createRestApi: (params) => client.send(new CreateRestApiCommand(params)), deleteRestApi: (params) => client.send(new DeleteRestApiCommand(params)), diff --git a/test/utils/cloudformation.js b/test/utils/cloudformation.js index bedafcaad5..6621c91f38 100644 --- a/test/utils/cloudformation.js +++ b/test/utils/cloudformation.js @@ -7,15 +7,15 @@ const getCloudFormationClient = () => { if (process.env.SLS_AWS_SDK_V3 === 'true') { // AWS SDK v3 const { CloudFormationClient } = require('@aws-sdk/client-cloudformation'); - const { + const { ListStacksCommand, - DeleteStackCommand, + DeleteStackCommand, ListStackResourcesCommand, - DescribeStacksCommand + DescribeStacksCommand, } = require('@aws-sdk/client-cloudformation'); - + const client = new CloudFormationClient({ region: 'us-east-1' }); - + return { listStacks: (params) => client.send(new ListStacksCommand(params)), deleteStack: (params) => client.send(new DeleteStackCommand(params)), @@ -28,7 +28,8 @@ const getCloudFormationClient = () => { return { listStacks: (params) => awsRequest(CloudFormationService, 'listStacks', params), deleteStack: (params) => awsRequest(CloudFormationService, 'deleteStack', params), - listStackResources: (params) => awsRequest(CloudFormationService, 'listStackResources', params), + listStackResources: (params) => + awsRequest(CloudFormationService, 'listStackResources', params), describeStacks: (params) => awsRequest(CloudFormationService, 'describeStacks', params), }; } diff --git a/test/utils/cognito.js b/test/utils/cognito.js index 039e6455f0..89a38f482d 100644 --- a/test/utils/cognito.js +++ b/test/utils/cognito.js @@ -8,7 +8,7 @@ const getCognitoClient = () => { if (process.env.SLS_AWS_SDK_V3 === 'true') { // AWS SDK v3 const { CognitoIdentityProviderClient } = require('@aws-sdk/client-cognito-identity-provider'); - const { + const { CreateUserPoolCommand, CreateUserPoolClientCommand, DeleteUserPoolCommand, @@ -16,11 +16,11 @@ const getCognitoClient = () => { DescribeUserPoolCommand, AdminCreateUserCommand, AdminSetUserPasswordCommand, - InitiateAuthCommand + InitiateAuthCommand, } = require('@aws-sdk/client-cognito-identity-provider'); - + const client = new CognitoIdentityProviderClient({ region: 'us-east-1' }); - + return { createUserPool: (params) => client.send(new CreateUserPoolCommand(params)), createUserPoolClient: (params) => client.send(new CreateUserPoolClientCommand(params)), @@ -35,14 +35,22 @@ const getCognitoClient = () => { // AWS SDK v2 const CognitoIdentityServiceProviderService = require('aws-sdk').CognitoIdentityServiceProvider; return { - createUserPool: (params) => awsRequest(CognitoIdentityServiceProviderService, 'createUserPool', params), - createUserPoolClient: (params) => awsRequest(CognitoIdentityServiceProviderService, 'createUserPoolClient', params), - deleteUserPool: (params) => awsRequest(CognitoIdentityServiceProviderService, 'deleteUserPool', params), - listUserPools: (params) => awsRequest(CognitoIdentityServiceProviderService, 'listUserPools', params), - describeUserPool: (params) => awsRequest(CognitoIdentityServiceProviderService, 'describeUserPool', params), - adminCreateUser: (params) => awsRequest(CognitoIdentityServiceProviderService, 'adminCreateUser', params), - adminSetUserPassword: (params) => awsRequest(CognitoIdentityServiceProviderService, 'adminSetUserPassword', params), - initiateAuth: (params) => awsRequest(CognitoIdentityServiceProviderService, 'initiateAuth', params), + createUserPool: (params) => + awsRequest(CognitoIdentityServiceProviderService, 'createUserPool', params), + createUserPoolClient: (params) => + awsRequest(CognitoIdentityServiceProviderService, 'createUserPoolClient', params), + deleteUserPool: (params) => + awsRequest(CognitoIdentityServiceProviderService, 'deleteUserPool', params), + listUserPools: (params) => + awsRequest(CognitoIdentityServiceProviderService, 'listUserPools', params), + describeUserPool: (params) => + awsRequest(CognitoIdentityServiceProviderService, 'describeUserPool', params), + adminCreateUser: (params) => + awsRequest(CognitoIdentityServiceProviderService, 'adminCreateUser', params), + adminSetUserPassword: (params) => + awsRequest(CognitoIdentityServiceProviderService, 'adminSetUserPassword', params), + initiateAuth: (params) => + awsRequest(CognitoIdentityServiceProviderService, 'initiateAuth', params), }; } }; @@ -64,9 +72,7 @@ async function createUserPoolClient(name, userPoolId) { } async function deleteUserPool(name) { - return findUserPoolByName(name).then((pool) => - cognito.deleteUserPool({ UserPoolId: pool.Id }) - ); + return findUserPoolByName(name).then((pool) => cognito.deleteUserPool({ UserPoolId: pool.Id })); } async function deleteUserPoolById(poolId) { @@ -85,20 +91,18 @@ async function findUserPoolByName(name) { const pools = []; async function recursiveFind(nextToken) { if (nextToken) params.NextToken = nextToken; - return cognito.listUserPools(params).then( - (result) => { - pools.push(...result.UserPools.filter((pool) => pool.Name === name)); - if (result.NextToken) return recursiveFind(result.NextToken); - switch (pools.length) { - case 0: - return null; - case 1: - return pools[0]; - default: - throw new Error(`Found more than one pool named '${name}'`); - } + return cognito.listUserPools(params).then((result) => { + pools.push(...result.UserPools.filter((pool) => pool.Name === name)); + if (result.NextToken) return recursiveFind(result.NextToken); + switch (pools.length) { + case 0: + return null; + case 1: + return pools[0]; + default: + throw new Error(`Found more than one pool named '${name}'`); } - ); + }); } return recursiveFind(); @@ -110,25 +114,25 @@ async function findUserPools() { const pools = []; async function recursiveFind(nextToken) { if (nextToken) params.NextToken = nextToken; - return cognito.listUserPools(params).then( - (result) => { - pools.push(...result.UserPools.filter((pool) => pool.Name.includes(' CUP '))); - if (result.NextToken) return recursiveFind(result.NextToken); - return null; - } - ); + return cognito.listUserPools(params).then((result) => { + pools.push(...result.UserPools.filter((pool) => pool.Name.includes(' CUP '))); + if (result.NextToken) return recursiveFind(result.NextToken); + return null; + }); } return recursiveFind().then(() => pools); } async function describeUserPool(userPoolId) { - return cognito.describeUserPool({ - UserPoolId: userPoolId, - }).then((result) => { - awsLog.debug('cognito.describeUserPool %s %j', userPoolId, result); - return result; - }); + return cognito + .describeUserPool({ + UserPoolId: userPoolId, + }) + .then((result) => { + awsLog.debug('cognito.describeUserPool %s %j', userPoolId, result); + return result; + }); } async function createUser(userPoolId, username, password) { diff --git a/test/utils/dynamodb.js b/test/utils/dynamodb.js index ccea3abc99..cece2d37fb 100644 --- a/test/utils/dynamodb.js +++ b/test/utils/dynamodb.js @@ -8,10 +8,10 @@ const getDynamoDBClient = () => { // AWS SDK v3 - using DynamoDBDocumentClient from lib-dynamodb const { DynamoDBClient } = require('@aws-sdk/client-dynamodb'); const { DynamoDBDocumentClient, PutCommand } = require('@aws-sdk/lib-dynamodb'); - + const client = new DynamoDBClient({ region: 'us-east-1' }); const docClient = DynamoDBDocumentClient.from(client); - + return { put: (params) => docClient.send(new PutCommand(params)), }; diff --git a/test/utils/event-bridge.js b/test/utils/event-bridge.js index eb4a5a1f73..4f65a7ac02 100644 --- a/test/utils/event-bridge.js +++ b/test/utils/event-bridge.js @@ -7,15 +7,15 @@ const getEventBridgeClient = () => { if (process.env.SLS_AWS_SDK_V3 === 'true') { // AWS SDK v3 const { EventBridgeClient } = require('@aws-sdk/client-eventbridge'); - const { + const { CreateEventBusCommand, DeleteEventBusCommand, DescribeEventBusCommand, - PutEventsCommand + PutEventsCommand, } = require('@aws-sdk/client-eventbridge'); - + const client = new EventBridgeClient({ region: 'us-east-1' }); - + return { createEventBus: (params) => client.send(new CreateEventBusCommand(params)), deleteEventBus: (params) => client.send(new DeleteEventBusCommand(params)), diff --git a/test/utils/iot.js b/test/utils/iot.js index 076d381f2c..63d6cede3f 100644 --- a/test/utils/iot.js +++ b/test/utils/iot.js @@ -8,22 +8,22 @@ const getIoTClients = () => { // AWS SDK v3 - dual service pattern (IoT + IoTDataPlane) const { IoTClient, DescribeEndpointCommand } = require('@aws-sdk/client-iot'); const { IoTDataPlaneClient, PublishCommand } = require('@aws-sdk/client-iot-data-plane'); - + const iotClient = new IoTClient({ region: 'us-east-1' }); - + return { iot: { describeEndpoint: (params) => iotClient.send(new DescribeEndpointCommand(params)), }, createIoTDataClient: (endpoint) => { - const iotDataClient = new IoTDataPlaneClient({ + const iotDataClient = new IoTDataPlaneClient({ region: 'us-east-1', - endpoint: `https://${endpoint}` + endpoint: `https://${endpoint}`, }); return { publish: (params) => iotDataClient.send(new PublishCommand(params)), }; - } + }, }; } else { // AWS SDK v2 @@ -34,8 +34,9 @@ const getIoTClients = () => { describeEndpoint: (params) => awsRequest(IotService, 'describeEndpoint', params), }, createIoTDataClient: (endpoint) => ({ - publish: (params) => awsRequest({ client: IotDataService, params: { endpoint } }, 'publish', params), - }) + publish: (params) => + awsRequest({ client: IotDataService, params: { endpoint } }, 'publish', params), + }), }; } }; @@ -43,11 +44,9 @@ const getIoTClients = () => { const { iot, createIoTDataClient } = getIoTClients(); async function resolveIotEndpoint() { - return iot.describeEndpoint({ endpointType: 'iot:Data-ATS' }).then( - (data) => { - return data.endpointAddress; - } - ); + return iot.describeEndpoint({ endpointType: 'iot:Data-ATS' }).then((data) => { + return data.endpointAddress; + }); } async function publishIotData(topic, message) { diff --git a/test/utils/kinesis.js b/test/utils/kinesis.js index d3a8101372..16261da008 100644 --- a/test/utils/kinesis.js +++ b/test/utils/kinesis.js @@ -7,15 +7,15 @@ const getKinesisClient = () => { if (process.env.SLS_AWS_SDK_V3 === 'true') { // AWS SDK v3 const { KinesisClient } = require('@aws-sdk/client-kinesis'); - const { + const { CreateStreamCommand, DeleteStreamCommand, DescribeStreamCommand, - PutRecordCommand + PutRecordCommand, } = require('@aws-sdk/client-kinesis'); - + const client = new KinesisClient({ region: 'us-east-1' }); - + return { createStream: (params) => client.send(new CreateStreamCommand(params)), deleteStream: (params) => client.send(new DeleteStreamCommand(params)), @@ -60,9 +60,7 @@ async function createKinesisStream(streamName) { StreamName: streamName, }; - return kinesis.createStream(params).then(() => - waitForKinesisStream(streamName) - ); + return kinesis.createStream(params).then(() => waitForKinesisStream(streamName)); } async function deleteKinesisStream(streamName) { diff --git a/test/utils/misc.js b/test/utils/misc.js index 4653c2eec3..6cbb756d5d 100644 --- a/test/utils/misc.js +++ b/test/utils/misc.js @@ -9,9 +9,9 @@ const getCloudWatchLogsClient = () => { // AWS SDK v3 const { CloudWatchLogsClient } = require('@aws-sdk/client-cloudwatch-logs'); const { FilterLogEventsCommand } = require('@aws-sdk/client-cloudwatch-logs'); - + const client = new CloudWatchLogsClient({ region: 'us-east-1' }); - + return { filterLogEvents: (params) => client.send(new FilterLogEventsCommand(params)), }; diff --git a/test/utils/s3.js b/test/utils/s3.js index 715943e50f..31d04100ed 100644 --- a/test/utils/s3.js +++ b/test/utils/s3.js @@ -7,17 +7,17 @@ const getS3Client = () => { if (process.env.SLS_AWS_SDK_V3 === 'true') { // AWS SDK v3 const { S3Client } = require('@aws-sdk/client-s3'); - const { + const { CreateBucketCommand, PutObjectCommand, DeleteObjectCommand, ListObjectsCommand, DeleteObjectsCommand, - DeleteBucketCommand + DeleteBucketCommand, } = require('@aws-sdk/client-s3'); - + const client = new S3Client({ region: 'us-east-1' }); - + return { createBucket: (params) => client.send(new CreateBucketCommand(params)), putObject: (params) => client.send(new PutObjectCommand(params)), diff --git a/test/utils/sns.js b/test/utils/sns.js index c871a25dcd..31965175cd 100644 --- a/test/utils/sns.js +++ b/test/utils/sns.js @@ -7,15 +7,15 @@ const getSNSClient = () => { if (process.env.SLS_AWS_SDK_V3 === 'true') { // AWS SDK v3 const { SNSClient } = require('@aws-sdk/client-sns'); - const { + const { CreateTopicCommand, DeleteTopicCommand, ListTopicsCommand, - PublishCommand + PublishCommand, } = require('@aws-sdk/client-sns'); - + const client = new SNSClient({ region: 'us-east-1' }); - + return { createTopic: (params) => client.send(new CreateTopicCommand(params)), deleteTopic: (params) => client.send(new DeleteTopicCommand(params)), diff --git a/test/utils/sqs.js b/test/utils/sqs.js index 48b083ecfa..a6223553c9 100644 --- a/test/utils/sqs.js +++ b/test/utils/sqs.js @@ -7,15 +7,15 @@ const getSQSClient = () => { if (process.env.SLS_AWS_SDK_V3 === 'true') { // AWS SDK v3 const { SQSClient } = require('@aws-sdk/client-sqs'); - const { + const { CreateQueueCommand, DeleteQueueCommand, GetQueueUrlCommand, - SendMessageCommand + SendMessageCommand, } = require('@aws-sdk/client-sqs'); - + const client = new SQSClient({ region: 'us-east-1' }); - + return { createQueue: (params) => client.send(new CreateQueueCommand(params)), deleteQueue: (params) => client.send(new DeleteQueueCommand(params)), diff --git a/test/utils/websocket.js b/test/utils/websocket.js index 1d8bae9891..f7565983bb 100644 --- a/test/utils/websocket.js +++ b/test/utils/websocket.js @@ -7,16 +7,16 @@ const getApiGatewayV2Client = () => { if (process.env.SLS_AWS_SDK_V3 === 'true') { // AWS SDK v3 const { ApiGatewayV2Client } = require('@aws-sdk/client-apigatewayv2'); - const { + const { CreateApiCommand, DeleteApiCommand, CreateStageCommand, DeleteStageCommand, - GetRoutesCommand + GetRoutesCommand, } = require('@aws-sdk/client-apigatewayv2'); - + const client = new ApiGatewayV2Client({ region: 'us-east-1' }); - + return { createApi: (params) => client.send(new CreateApiCommand(params)), deleteApi: (params) => client.send(new DeleteApiCommand(params)), From 27660654be4931c4f87a6d1c98616839e7a45e1a Mon Sep 17 00:00:00 2001 From: Graham Campbell Date: Sun, 8 Jun 2025 22:53:33 +0100 Subject: [PATCH 09/27] Self-review --- .../api-gateway-cloud-watch-role/handler.js | 20 ++++++++++---- .../cognito-user-pool/lib/permissions.js | 11 +++++--- .../cognito-user-pool/lib/user-pool.js | 19 +++++++------- .../event-bridge/lib/event-bridge.js | 26 +++++++++---------- .../resources/event-bridge/lib/permissions.js | 13 ++++++---- .../resources/s3/lib/bucket.js | 16 +++++++----- .../resources/s3/lib/permissions.js | 13 ++++++---- 7 files changed, 70 insertions(+), 48 deletions(-) diff --git a/lib/plugins/aws/custom-resources/resources/api-gateway-cloud-watch-role/handler.js b/lib/plugins/aws/custom-resources/resources/api-gateway-cloud-watch-role/handler.js index 58a59def90..806037b990 100644 --- a/lib/plugins/aws/custom-resources/resources/api-gateway-cloud-watch-role/handler.js +++ b/lib/plugins/aws/custom-resources/resources/api-gateway-cloud-watch-role/handler.js @@ -14,8 +14,19 @@ const { AttachRolePolicyCommand, } = require('@aws-sdk/client-iam'); -const apiGateway = new APIGatewayClient({ maxAttempts: MAX_AWS_REQUEST_TRY }); -const iam = new IAMClient({ maxAttempts: MAX_AWS_REQUEST_TRY }); +function getAPIGatewayClient(region) { + return new APIGatewayClient({ + region, + maxAttempts: MAX_AWS_REQUEST_TRY, + }); +} + +function getIAMClient(region) { + return new IAMClient({ + region, + maxAttempts: MAX_AWS_REQUEST_TRY, + }); +} async function handler(event, context) { if (event.RequestType === 'Create') { @@ -31,9 +42,8 @@ async function handler(event, context) { async function create(event, context) { const { RoleArn } = event.ResourceProperties; const { Partition: partition, AccountId: accountId, Region: region } = getEnvironment(context); - - apiGateway.config.region = () => region; - iam.config.region = () => region; + const apiGateway = getAPIGatewayClient(region); + const iam = getIAMClient(region); const assignedRoleArn = (await apiGateway.send(new GetAccountCommand({}))).cloudwatchRoleArn; diff --git a/lib/plugins/aws/custom-resources/resources/cognito-user-pool/lib/permissions.js b/lib/plugins/aws/custom-resources/resources/cognito-user-pool/lib/permissions.js index e9c817dce1..fda423d1d5 100644 --- a/lib/plugins/aws/custom-resources/resources/cognito-user-pool/lib/permissions.js +++ b/lib/plugins/aws/custom-resources/resources/cognito-user-pool/lib/permissions.js @@ -7,7 +7,12 @@ const { RemovePermissionCommand, } = require('@aws-sdk/client-lambda'); -const lambda = new LambdaClient({ maxAttempts: MAX_AWS_REQUEST_TRY }); +function getLambdaClient(region) { + return new LambdaClient({ + region, + maxAttempts: MAX_AWS_REQUEST_TRY, + }); +} function getStatementId(functionName, userPoolName) { const normalizedUserPoolName = userPoolName.toLowerCase().replace(/[.:*\s]/g, ''); @@ -20,7 +25,7 @@ function getStatementId(functionName, userPoolName) { async function addPermission(config) { const { functionName, userPoolName, partition, region, accountId, userPoolId } = config; - lambda.config.region = () => region; + const lambda = getLambdaClient(region); const payload = { Action: 'lambda:InvokeFunction', @@ -34,7 +39,7 @@ async function addPermission(config) { async function removePermission(config) { const { functionName, userPoolName, region } = config; - lambda.config.region = () => region; + const lambda = getLambdaClient(region); const payload = { FunctionName: functionName, StatementId: getStatementId(functionName, userPoolName), diff --git a/lib/plugins/aws/custom-resources/resources/cognito-user-pool/lib/user-pool.js b/lib/plugins/aws/custom-resources/resources/cognito-user-pool/lib/user-pool.js index 724911a15d..31c85b24f4 100644 --- a/lib/plugins/aws/custom-resources/resources/cognito-user-pool/lib/user-pool.js +++ b/lib/plugins/aws/custom-resources/resources/cognito-user-pool/lib/user-pool.js @@ -8,7 +8,12 @@ const { UpdateUserPoolCommand, } = require('@aws-sdk/client-cognito-identity-provider'); -const cognito = new CognitoIdentityProviderClient({ maxAttempts: MAX_AWS_REQUEST_TRY }); +function getCognitoClient(region) { + return new CognitoIdentityProviderClient({ + region, + maxAttempts: MAX_AWS_REQUEST_TRY, + }); +} const customSenderSources = ['CustomSMSSender', 'CustomEmailSender']; @@ -40,13 +45,12 @@ function getUpdateConfigFromCurrentSetup(currentSetup) { async function findUserPoolByName(config) { const { userPoolName, region } = config; + const cognito = getCognitoClient(region); const payload = { MaxResults: 60, }; - cognito.config.region = () => region; - async function recursiveFind(nextToken) { if (nextToken) payload.NextToken = nextToken; return cognito.send(new ListUserPoolsCommand(payload)).then((result) => { @@ -64,8 +68,7 @@ async function findUserPoolByName(config) { async function getConfiguration(config) { const { region } = config; - - cognito.config.region = () => region; + const cognito = getCognitoClient(region); return findUserPoolByName(config).then((userPool) => cognito.send(new DescribeUserPoolCommand({ UserPoolId: userPool.Id })) @@ -74,8 +77,7 @@ async function getConfiguration(config) { async function updateConfiguration(config) { const { lambdaArn, userPoolConfigs, region } = config; - - cognito.config.region = () => region; + const cognito = getCognitoClient(region); return getConfiguration(config).then((res) => { const UserPoolId = res.UserPool.Id; @@ -108,8 +110,7 @@ async function updateConfiguration(config) { async function removeConfiguration(config) { const { lambdaArn, region } = config; - - cognito.config.region = () => region; + const cognito = getCognitoClient(region); return getConfiguration(config).then((res) => { const UserPoolId = res.UserPool.Id; diff --git a/lib/plugins/aws/custom-resources/resources/event-bridge/lib/event-bridge.js b/lib/plugins/aws/custom-resources/resources/event-bridge/lib/event-bridge.js index 4b47777fc1..5e2c2e2807 100644 --- a/lib/plugins/aws/custom-resources/resources/event-bridge/lib/event-bridge.js +++ b/lib/plugins/aws/custom-resources/resources/event-bridge/lib/event-bridge.js @@ -12,12 +12,16 @@ const { RemoveTargetsCommand, } = require('@aws-sdk/client-eventbridge'); -const eventBridge = new EventBridgeClient({ maxAttempts: MAX_AWS_REQUEST_TRY }); +function getEventBridgeClient(region) { + return new EventBridgeClient({ + region, + maxAttempts: MAX_AWS_REQUEST_TRY, + }); +} async function createEventBus(config) { const { eventBus, region } = config; - - eventBridge.config.region = () => region; + const eventBridge = getEventBridgeClient(region); if (eventBus) { if (eventBus.startsWith('arn')) { @@ -34,8 +38,7 @@ async function createEventBus(config) { async function deleteEventBus(config) { const { eventBus, region } = config; - - eventBridge.config.region = () => region; + const eventBridge = getEventBridgeClient(region); if (eventBus) { if (eventBus.startsWith('arn')) { @@ -53,8 +56,7 @@ async function deleteEventBus(config) { async function updateRuleConfiguration(config) { const { ruleName, eventBus, pattern, schedule, region, state } = config; - - eventBridge.config.region = () => region; + const eventBridge = getEventBridgeClient(region); const EventBusName = getEventBusName(eventBus); @@ -71,8 +73,7 @@ async function updateRuleConfiguration(config) { async function removeRuleConfiguration(config) { const { ruleName, eventBus, region } = config; - - eventBridge.config.region = () => region; + const eventBridge = getEventBridgeClient(region); const EventBusName = getEventBusName(eventBus); @@ -86,8 +87,7 @@ async function removeRuleConfiguration(config) { async function updateTargetConfiguration(config) { const { lambdaArn, ruleName, eventBus, input, inputPath, inputTransformer, region } = config; - - eventBridge.config.region = () => region; + const eventBridge = getEventBridgeClient(region); const EventBusName = getEventBusName(eventBus); @@ -117,11 +117,9 @@ async function updateTargetConfiguration(config) { async function removeTargetConfiguration(config) { const { ruleName, eventBus, region } = config; - + const eventBridge = getEventBridgeClient(region); const EventBusName = getEventBusName(eventBus); - eventBridge.config.region = () => region; - return eventBridge.send( new RemoveTargetsCommand({ Ids: [getEventBusTargetId(ruleName)], diff --git a/lib/plugins/aws/custom-resources/resources/event-bridge/lib/permissions.js b/lib/plugins/aws/custom-resources/resources/event-bridge/lib/permissions.js index 6e8432facf..2463a40eba 100644 --- a/lib/plugins/aws/custom-resources/resources/event-bridge/lib/permissions.js +++ b/lib/plugins/aws/custom-resources/resources/event-bridge/lib/permissions.js @@ -8,7 +8,12 @@ const { RemovePermissionCommand, } = require('@aws-sdk/client-lambda'); -const lambda = new LambdaClient({ maxAttempts: MAX_AWS_REQUEST_TRY }); +function getLambdaClient(region) { + return new LambdaClient({ + region, + maxAttempts: MAX_AWS_REQUEST_TRY, + }); +} function getStatementId(functionName, ruleName) { const normalizedRuleName = ruleName.toLowerCase().replace(/[.:*]/g, ''); @@ -21,8 +26,7 @@ function getStatementId(functionName, ruleName) { async function addPermission(config) { const { functionName, partition, region, accountId, eventBus, ruleName } = config; - - lambda.config.region = () => region; + const lambda = getLambdaClient(region); let SourceArn = `arn:${partition}:events:${region}:${accountId}:rule/${ruleName}`; if (eventBus) { @@ -42,8 +46,7 @@ async function addPermission(config) { async function removePermission(config) { const { functionName, region, ruleName } = config; - - lambda.config.region = () => region; + const lambda = getLambdaClient(region); const payload = { FunctionName: functionName, diff --git a/lib/plugins/aws/custom-resources/resources/s3/lib/bucket.js b/lib/plugins/aws/custom-resources/resources/s3/lib/bucket.js index 6349816cb4..2ed4dbdff0 100644 --- a/lib/plugins/aws/custom-resources/resources/s3/lib/bucket.js +++ b/lib/plugins/aws/custom-resources/resources/s3/lib/bucket.js @@ -8,7 +8,12 @@ const { PutBucketNotificationConfigurationCommand, } = require('@aws-sdk/client-s3'); -const s3 = new S3Client({ maxAttempts: MAX_AWS_REQUEST_TRY }); +function getS3Client(region) { + return new S3Client({ + region, + maxAttempts: MAX_AWS_REQUEST_TRY, + }); +} function generateId(functionName, bucketConfig) { const md5 = crypto.createHash('md5').update(JSON.stringify(bucketConfig)).digest('hex'); @@ -38,8 +43,7 @@ function createFilter(config) { async function getConfiguration(config) { const { bucketName, region } = config; - - s3.config.region = () => region; + const s3 = getS3Client(region); const Bucket = bucketName; const payload = { @@ -51,8 +55,7 @@ async function getConfiguration(config) { async function updateConfiguration(config) { const { lambdaArn, functionName, bucketName, bucketConfigs, region } = config; - - s3.config.region = () => region; + const s3 = getS3Client(region); const Bucket = bucketName; @@ -103,8 +106,7 @@ async function updateConfiguration(config) { async function removeConfiguration(config) { const { functionName, bucketName, region } = config; - - s3.config.region = () => region; + const s3 = getS3Client(region); const Bucket = bucketName; diff --git a/lib/plugins/aws/custom-resources/resources/s3/lib/permissions.js b/lib/plugins/aws/custom-resources/resources/s3/lib/permissions.js index 0689bbd3b4..65649211b6 100644 --- a/lib/plugins/aws/custom-resources/resources/s3/lib/permissions.js +++ b/lib/plugins/aws/custom-resources/resources/s3/lib/permissions.js @@ -7,7 +7,12 @@ const { RemovePermissionCommand, } = require('@aws-sdk/client-lambda'); -const lambda = new LambdaClient({ maxAttempts: MAX_AWS_REQUEST_TRY }); +function getLambdaClient(region) { + return new LambdaClient({ + region, + maxAttempts: MAX_AWS_REQUEST_TRY, + }); +} function getStatementId(functionName, bucketName) { const normalizedBucketName = bucketName.replace(/[.:*]/g, ''); @@ -20,8 +25,7 @@ function getStatementId(functionName, bucketName) { async function addPermission(config) { const { functionName, bucketName, partition, region, accountId } = config; - - lambda.config.region = () => region; + const lambda = getLambdaClient(region); const payload = { Action: 'lambda:InvokeFunction', @@ -37,8 +41,7 @@ async function addPermission(config) { async function removePermission(config) { const { functionName, bucketName, region } = config; - - lambda.config.region = () => region; + const lambda = getLambdaClient(region); const payload = { FunctionName: functionName, From 2fc0cdad81dc11329115405c72a84a7e0095ae74 Mon Sep 17 00:00:00 2001 From: Graham Campbell Date: Sun, 8 Jun 2025 23:14:22 +0100 Subject: [PATCH 10/27] Cleanup --- lib/plugins/aws/deploy-function.js | 22 +++------- lib/plugins/aws/deploy-list.js | 18 ++------ .../aws/deploy/lib/check-for-changes.js | 34 ++++++--------- lib/plugins/aws/deploy/lib/create-stack.js | 6 +-- .../deploy/lib/ensure-valid-bucket-exists.js | 23 +++-------- .../aws/deploy/lib/upload-artifacts.js | 15 +------ .../aws/deploy/lib/validate-template.js | 6 +-- lib/plugins/aws/info/get-stack-info.js | 22 +++------- lib/plugins/aws/invoke.js | 6 +-- lib/plugins/aws/lib/check-if-bucket-exists.js | 6 +-- lib/plugins/aws/lib/monitor-stack.js | 6 +-- lib/plugins/aws/logs.js | 6 +-- lib/plugins/aws/metrics.js | 6 +-- .../lib/hack/disassociate-usage-plan.js | 18 ++------ lib/plugins/aws/provider.js | 21 ++++------ lib/plugins/aws/remove/lib/stack.js | 6 +-- lib/plugins/aws/rollback-function.js | 6 +-- lib/plugins/aws/rollback.js | 6 +-- .../aws/utils/resolve-cf-import-value.js | 3 +- lib/plugins/aws/utils/resolve-cf-ref-value.js | 37 +++++++++-------- test/utils/api-gateway.js | 17 ++++---- test/utils/cloudformation.js | 18 ++++---- test/utils/cognito.js | 41 +++++++++---------- test/utils/dynamodb.js | 11 +++-- test/utils/event-bridge.js | 17 ++++---- test/utils/iot.js | 25 ++++++----- test/utils/kinesis.js | 17 ++++---- test/utils/misc.js | 11 +++-- test/utils/s3.js | 21 +++++----- test/utils/sns.js | 17 ++++---- test/utils/sqs.js | 17 ++++---- test/utils/websocket.js | 19 ++++----- 32 files changed, 183 insertions(+), 321 deletions(-) diff --git a/lib/plugins/aws/deploy-function.js b/lib/plugins/aws/deploy-function.js index 700007f501..a3702877fa 100644 --- a/lib/plugins/aws/deploy-function.js +++ b/lib/plugins/aws/deploy-function.js @@ -66,18 +66,6 @@ class AwsDeployFunction { }; } - /** - * Helper method to route AWS requests through v2 or v3 based on feature flag - * @private - */ - async _awsRequest(service, method, params) { - // Use v3 if feature flag is enabled, otherwise fallback to v2 - if (this.provider._v3Enabled) { - return this.provider.requestV3(service, method, params); - } - return this.provider.request(service, method, params); - } - async checkIfFunctionExists() { // check if the function exists in the service this.options.functionObj = this.serverless.service.getFunction(this.options.function); @@ -89,7 +77,7 @@ class AwsDeployFunction { const result = await (async () => { try { - return await this._awsRequest('Lambda', 'getFunction', params); + return await this.provider._awsRequest('Lambda', 'getFunction', params); } catch (error) { if (_.get(error, 'providerError.code') === 'ResourceNotFoundException') { const errorMessage = [ @@ -154,7 +142,7 @@ class AwsDeployFunction { return `arn:${result.partition}:iam::${result.accountId}:role${compiledFullRoleName}`; } - const data = await this._awsRequest('IAM', 'getRole', { + const data = await this.provider._awsRequest('IAM', 'getRole', { RoleName: role['Fn::GetAtt'][0], }); return data.Arn; @@ -168,7 +156,7 @@ class AwsDeployFunction { const startTime = Date.now(); const callWithRetry = async () => { - const result = await this._awsRequest('Lambda', 'getFunction', params); + const result = await this.provider._awsRequest('Lambda', 'getFunction', params); if ( result && result.Configuration.State === 'Active' && @@ -196,7 +184,7 @@ class AwsDeployFunction { const callWithRetry = async () => { try { - await this._awsRequest('Lambda', 'updateFunctionConfiguration', params); + await this.provider._awsRequest('Lambda', 'updateFunctionConfiguration', params); } catch (err) { const didOneMinutePass = Date.now() - startTime > 60 * 1000; @@ -553,7 +541,7 @@ class AwsDeployFunction { } mainProgress.notice('Deploying', { isMainEvent: true }); - await this._awsRequest('Lambda', 'updateFunctionCode', params); + await this.provider._awsRequest('Lambda', 'updateFunctionCode', params); this.shouldEnsureFunctionState = true; log.notice(); log.notice.success( diff --git a/lib/plugins/aws/deploy-list.js b/lib/plugins/aws/deploy-list.js index 0848434609..d7fc5fa1c2 100644 --- a/lib/plugins/aws/deploy-list.js +++ b/lib/plugins/aws/deploy-list.js @@ -26,18 +26,6 @@ class AwsDeployList { }; } - /** - * Helper method to route AWS requests through v2 or v3 based on feature flag - * @private - */ - async _awsRequest(service, method, params) { - // Use v3 if feature flag is enabled, otherwise fallback to v2 - if (this.provider._v3Enabled) { - return this.provider.requestV3(service, method, params); - } - return this.provider.request(service, method, params); - } - async listDeployments() { const service = this.serverless.service.service; const stage = this.provider.getStage(); @@ -45,7 +33,7 @@ class AwsDeployList { let response; try { - response = await this._awsRequest('S3', 'listObjectsV2', { + response = await this.provider._awsRequest('S3', 'listObjectsV2', { Bucket: this.bucketName, Prefix: `${prefix}/${service}/${stage}`, }); @@ -106,7 +94,7 @@ class AwsDeployList { FunctionName: funcName, }; - return this._awsRequest('Lambda', 'getFunction', params); + return this.provider._awsRequest('Lambda', 'getFunction', params); }) ); @@ -114,7 +102,7 @@ class AwsDeployList { } async getFunctionPaginatedVersions(params, totalVersions) { - const response = await this._awsRequest('Lambda', 'listVersionsByFunction', params); + const response = await this.provider._awsRequest('Lambda', 'listVersionsByFunction', params); const Versions = (totalVersions || []).concat(response.Versions); if (response.NextMarker) { diff --git a/lib/plugins/aws/deploy/lib/check-for-changes.js b/lib/plugins/aws/deploy/lib/check-for-changes.js index 9bd79b4b77..6ededb6ee3 100644 --- a/lib/plugins/aws/deploy/lib/check-for-changes.js +++ b/lib/plugins/aws/deploy/lib/check-for-changes.js @@ -18,17 +18,6 @@ const isDeploymentDirToken = RegExp.prototype.test.bind( const isOtelExtensionName = RegExp.prototype.test.bind(/^sls-otel\.\d+\.\d+\.\d+\.zip$/); module.exports = { - /** - * Helper method to route AWS requests through v2 or v3 based on feature flag - * @private - */ - async _awsRequest(service, method, params) { - // Use v3 if feature flag is enabled, otherwise fallback to v2 - if (this.provider._v3Enabled) { - return this.provider.requestV3(service, method, params); - } - return this.provider.request(service, method, params); - }, async checkForChanges() { this.serverless.service.provider.shouldNotDeploy = false; if (this.options.force) { @@ -67,7 +56,7 @@ module.exports = { const result = await (async () => { try { - return await this._awsRequest('S3', 'listObjectsV2', params); + return await this.provider._awsRequest('S3', 'listObjectsV2', params); } catch (error) { if (!error.message.includes('The specified bucket does not exist')) throw error; const stackName = this.provider.naming.getStackName(); @@ -104,9 +93,10 @@ module.exports = { let couldNotAccessFunction = false; const getFunctionResults = this.serverless.service.getAllFunctions().map((funName) => { const functionObj = this.serverless.service.getFunction(funName); - return this._awsRequest('Lambda', 'getFunction', { - FunctionName: functionObj.name, - }) + return this.provider + ._awsRequest('Lambda', 'getFunction', { + FunctionName: functionObj.name, + }) .then((res) => new Date(res.Configuration.LastModified)) .catch((err) => { if (err.providerError && err.providerError.statusCode === 403) { @@ -134,7 +124,7 @@ module.exports = { return Promise.all( objects.map(async (obj) => { try { - const result = await this._awsRequest('S3', 'headObject', { + const result = await this.provider._awsRequest('S3', 'headObject', { Bucket: this.bucketName, Key: obj.Key, }); @@ -313,9 +303,11 @@ module.exports = { const cloudwatchLogEvents = params.cloudwatchLogEvents; const CLOUDWATCHLOG_LOG_GROUP_EVENT_PER_FUNCTION_LIMIT = 2; - const response = await this._awsRequest('CloudWatchLogs', 'describeSubscriptionFilters', { - logGroupName, - }).catch(() => ({ subscriptionFilters: [] })); + const response = await this.provider + ._awsRequest('CloudWatchLogs', 'describeSubscriptionFilters', { + logGroupName, + }) + .catch(() => ({ subscriptionFilters: [] })); if (response.subscriptionFilters.length === 0) { log.debug('no subscription filters detected'); return false; @@ -387,7 +379,7 @@ module.exports = { return Promise.all( notMatchedInternalOldSubscriptionFilters.map((oldSubscriptionFilter) => - this._awsRequest('CloudWatchLogs', 'deleteSubscriptionFilter', { + this.provider._awsRequest('CloudWatchLogs', 'deleteSubscriptionFilter', { logGroupName, filterName: oldSubscriptionFilter.filterName, }) @@ -405,7 +397,7 @@ module.exports = { async isInternalSubscriptionFilter(stackName, logicalResourceId, physicalResourceId) { try { - const { StackResourceDetail } = await this._awsRequest( + const { StackResourceDetail } = await this.provider._awsRequest( 'CloudFormation', 'describeStackResource', { diff --git a/lib/plugins/aws/deploy/lib/create-stack.js b/lib/plugins/aws/deploy/lib/create-stack.js index 4cfc081fdd..33c8207c30 100644 --- a/lib/plugins/aws/deploy/lib/create-stack.js +++ b/lib/plugins/aws/deploy/lib/create-stack.js @@ -62,11 +62,7 @@ module.exports = { * @private */ async _cloudFormationRequest(method, params) { - // Use v3 if feature flag is enabled, otherwise fallback to v2 - if (this.provider._v3Enabled) { - return this.provider.requestV3('CloudFormation', method, params); - } - return this.provider.request('CloudFormation', method, params); + return this.provider._awsRequest('CloudFormation', method, params); }, async createStack() { diff --git a/lib/plugins/aws/deploy/lib/ensure-valid-bucket-exists.js b/lib/plugins/aws/deploy/lib/ensure-valid-bucket-exists.js index afb292a8b9..39aac99918 100644 --- a/lib/plugins/aws/deploy/lib/ensure-valid-bucket-exists.js +++ b/lib/plugins/aws/deploy/lib/ensure-valid-bucket-exists.js @@ -7,17 +7,6 @@ const jsyaml = require('js-yaml'); const mainProgress = progress.get('main'); module.exports = { - /** - * Helper method to route AWS requests through v2 or v3 based on feature flag - * @private - */ - async _awsRequest(service, method, params) { - // Use v3 if feature flag is enabled, otherwise fallback to v2 - if (this.provider._v3Enabled) { - return this.provider.requestV3(service, method, params); - } - return this.provider.request(service, method, params); - }, async ensureValidBucketExists() { // Ensure to set bucket name if it can be resolved // Result of this operation will determine how further validation will be performed @@ -38,7 +27,7 @@ module.exports = { if (this.serverless.service.provider.deploymentBucket) { let result; try { - result = await this._awsRequest('S3', 'getBucketLocation', { + result = await this.provider._awsRequest('S3', 'getBucketLocation', { Bucket: this.bucketName, }); } catch (err) { @@ -82,7 +71,7 @@ module.exports = { // It covers the case where someone was using custom deployment bucket // but removed that setting from the configuration mainProgress.notice('Ensuring that deployment bucket exists', { isMainEvent: true }); - const getTemplateResult = await this._awsRequest('CloudFormation', 'getTemplate', { + const getTemplateResult = await this.provider._awsRequest('CloudFormation', 'getTemplate', { StackName: stackName, TemplateStage: 'Original', }); @@ -127,7 +116,7 @@ module.exports = { if (this.serverless.service.provider.deploymentMethod === 'direct') { const params = this.getUpdateStackParams({ templateBody }); - monitorCfData = await this._awsRequest('CloudFormation', 'updateStack', params); + monitorCfData = await this.provider._awsRequest('CloudFormation', 'updateStack', params); } else { const createChangeSetParams = this.getCreateChangeSetParams({ changeSetType: 'UPDATE', @@ -137,14 +126,14 @@ module.exports = { const executeChangeSetParams = this.getExecuteChangeSetParams(); // Ensure that previous change set has been removed - await this._awsRequest('CloudFormation', 'deleteChangeSet', { + await this.provider._awsRequest('CloudFormation', 'deleteChangeSet', { StackName: stackName, ChangeSetName: changeSetName, }); log.info('Creating new change set.'); // Create new change set - const changeSet = await this._awsRequest( + const changeSet = await this.provider._awsRequest( 'CloudFormation', 'createChangeSet', createChangeSetParams @@ -158,7 +147,7 @@ module.exports = { // that needs to be created as a part of change set // If that would not be the case, that means we have a bug in the logic above - await this._awsRequest('CloudFormation', 'executeChangeSet', executeChangeSetParams); + await this.provider._awsRequest('CloudFormation', 'executeChangeSet', executeChangeSetParams); monitorCfData = changeSet; } await this.monitorStack('update', monitorCfData); diff --git a/lib/plugins/aws/deploy/lib/upload-artifacts.js b/lib/plugins/aws/deploy/lib/upload-artifacts.js index 90e8824e9d..ed6e002ecd 100644 --- a/lib/plugins/aws/deploy/lib/upload-artifacts.js +++ b/lib/plugins/aws/deploy/lib/upload-artifacts.js @@ -16,17 +16,6 @@ const MAX_CONCURRENT_ARTIFACTS_UPLOADS = Number(process.env.SLS_MAX_CONCURRENT_ARTIFACTS_UPLOADS) || 3; module.exports = { - /** - * Helper method to route AWS requests through v2 or v3 based on feature flag - * @private - */ - async _awsRequest(service, method, params) { - // Use v3 if feature flag is enabled, otherwise fallback to v2 - if (this.provider._v3Enabled) { - return this.provider.requestV3(service, method, params); - } - return this.provider.request(service, method, params); - }, async getFileStats(filepath) { try { return await fsp.stat(filepath); @@ -83,7 +72,7 @@ module.exports = { params = setS3UploadEncryptionOptions(params, deploymentBucketObject); } - return this._awsRequest('S3', 'upload', params); + return this.provider._awsRequest('S3', 'upload', params); }, async uploadStateFile() { log.info('Uploading State file to S3'); @@ -113,7 +102,7 @@ module.exports = { params = setS3UploadEncryptionOptions(params, deploymentBucketObject); } - return this._awsRequest('S3', 'upload', params); + return this.provider._awsRequest('S3', 'upload', params); }, async getFunctionArtifactFilePaths() { diff --git a/lib/plugins/aws/deploy/lib/validate-template.js b/lib/plugins/aws/deploy/lib/validate-template.js index d79e09e4f6..907bfa9f5d 100644 --- a/lib/plugins/aws/deploy/lib/validate-template.js +++ b/lib/plugins/aws/deploy/lib/validate-template.js @@ -9,11 +9,7 @@ module.exports = { * @private */ async _cloudFormationRequest(method, params) { - // Use v3 if feature flag is enabled, otherwise fallback to v2 - if (this.provider._v3Enabled) { - return this.provider.requestV3('CloudFormation', method, params); - } - return this.provider.request('CloudFormation', method, params); + return this.provider._awsRequest('CloudFormation', method, params); }, async validateTemplate() { diff --git a/lib/plugins/aws/info/get-stack-info.js b/lib/plugins/aws/info/get-stack-info.js index 3a9c4b9cfa..c9d34c05be 100644 --- a/lib/plugins/aws/info/get-stack-info.js +++ b/lib/plugins/aws/info/get-stack-info.js @@ -5,18 +5,6 @@ const resolveCfImportValue = require('../utils/resolve-cf-import-value'); const ServerlessError = require('../../../serverless-error'); module.exports = { - /** - * Helper method to route AWS requests through v2 or v3 based on feature flag - * @private - */ - async _awsRequest(service, method, params) { - // Use v3 if feature flag is enabled, otherwise fallback to v2 - if (this.provider._v3Enabled) { - return this.provider.requestV3(service, method, params); - } - return this.provider.request(service, method, params); - }, - async getStackInfo() { // NOTE: this is the global gatheredData object which will be passed around this.gatheredData = { @@ -36,11 +24,11 @@ module.exports = { const stackData = {}; const sdkRequests = [ - this._awsRequest('CloudFormation', 'describeStacks', { StackName: stackName }).then( - (result) => { + this.provider + ._awsRequest('CloudFormation', 'describeStacks', { StackName: stackName }) + .then((result) => { if (result) stackData.outputs = result.Stacks[0].Outputs; - } - ), + }), ]; const httpApiId = this.serverless.service.provider.httpApi && this.serverless.service.provider.httpApi.id; @@ -51,7 +39,7 @@ module.exports = { : BbPromise.resolve(httpApiId) ) .then((id) => { - return this._awsRequest('ApiGatewayV2', 'getApi', { ApiId: id }); + return this.provider._awsRequest('ApiGatewayV2', 'getApi', { ApiId: id }); }) .then( (result) => { diff --git a/lib/plugins/aws/invoke.js b/lib/plugins/aws/invoke.js index d0b4272f17..b6c279b86c 100644 --- a/lib/plugins/aws/invoke.js +++ b/lib/plugins/aws/invoke.js @@ -28,11 +28,7 @@ class AwsInvoke { * @private */ async _lambdaRequest(method, params) { - // Use v3 if feature flag is enabled, otherwise fallback to v2 - if (this.provider._v3Enabled) { - return this.provider.requestV3('Lambda', method, params); - } - return this.provider.request('Lambda', method, params); + return this.provider._awsRequest('Lambda', method, params); } async validateFile(key) { diff --git a/lib/plugins/aws/lib/check-if-bucket-exists.js b/lib/plugins/aws/lib/check-if-bucket-exists.js index 180f5a0cd8..439ba9d31f 100644 --- a/lib/plugins/aws/lib/check-if-bucket-exists.js +++ b/lib/plugins/aws/lib/check-if-bucket-exists.js @@ -8,11 +8,7 @@ module.exports = { * @private */ async _s3Request(method, params) { - // Use v3 if feature flag is enabled, otherwise fallback to v2 - if (this.provider._v3Enabled) { - return this.provider.requestV3('S3', method, params); - } - return this.provider.request('S3', method, params); + return this.provider._awsRequest('S3', method, params); }, async checkIfBucketExists(bucketName) { diff --git a/lib/plugins/aws/lib/monitor-stack.js b/lib/plugins/aws/lib/monitor-stack.js index 93e8e56b07..5163c1aef3 100644 --- a/lib/plugins/aws/lib/monitor-stack.js +++ b/lib/plugins/aws/lib/monitor-stack.js @@ -26,11 +26,7 @@ module.exports = { * @private */ async _cloudFormationRequest(method, params) { - // Use v3 if feature flag is enabled, otherwise fallback to v2 - if (this.provider._v3Enabled) { - return this.provider.requestV3('CloudFormation', method, params); - } - return this.provider.request('CloudFormation', method, params); + return this.provider._awsRequest('CloudFormation', method, params); }, async checkStackProgress( action, diff --git a/lib/plugins/aws/logs.js b/lib/plugins/aws/logs.js index e63f157fae..70c084ef43 100644 --- a/lib/plugins/aws/logs.js +++ b/lib/plugins/aws/logs.js @@ -33,11 +33,7 @@ class AwsLogs { * @private */ async _cloudWatchLogsRequest(method, params) { - // Use v3 if feature flag is enabled, otherwise fallback to v2 - if (this.provider._v3Enabled) { - return this.provider.requestV3('CloudWatchLogs', method, params); - } - return this.provider.request('CloudWatchLogs', method, params); + return this.provider._awsRequest('CloudWatchLogs', method, params); } extendedValidate() { diff --git a/lib/plugins/aws/metrics.js b/lib/plugins/aws/metrics.js index 0af2c0a0d1..7ed0fd234a 100644 --- a/lib/plugins/aws/metrics.js +++ b/lib/plugins/aws/metrics.js @@ -32,11 +32,7 @@ class AwsMetrics { * @private */ async _cloudWatchRequest(method, params) { - // Use v3 if feature flag is enabled, otherwise fallback to v2 - if (this.provider._v3Enabled) { - return this.provider.requestV3('CloudWatch', method, params); - } - return this.provider.request('CloudWatch', method, params); + return this.provider._awsRequest('CloudWatch', method, params); } extendedValidate() { diff --git a/lib/plugins/aws/package/compile/events/api-gateway/lib/hack/disassociate-usage-plan.js b/lib/plugins/aws/package/compile/events/api-gateway/lib/hack/disassociate-usage-plan.js index 7ca261bc58..5d50872e2e 100644 --- a/lib/plugins/aws/package/compile/events/api-gateway/lib/hack/disassociate-usage-plan.js +++ b/lib/plugins/aws/package/compile/events/api-gateway/lib/hack/disassociate-usage-plan.js @@ -5,18 +5,6 @@ const _ = require('lodash'); const { log } = require('@serverless/utils/log'); module.exports = { - /** - * Helper method to route AWS requests through v2 or v3 based on feature flag - * @private - */ - async _awsRequest(service, method, params) { - // Use v3 if feature flag is enabled, otherwise fallback to v2 - if (this.provider._v3Enabled) { - return this.provider.requestV3(service, method, params); - } - return this.provider.request(service, method, params); - }, - async disassociateUsagePlan() { const apiKeys = _.get(this.serverless.service.provider.apiGateway, 'apiKeys'); @@ -24,11 +12,11 @@ module.exports = { log.info('Removing usage plan association'); const stackName = `${this.provider.naming.getStackName()}`; return BbPromise.all([ - this._awsRequest('CloudFormation', 'describeStackResource', { + this.provider._awsRequest('CloudFormation', 'describeStackResource', { StackName: stackName, LogicalResourceId: this.provider.naming.getRestApiLogicalId(), }), - this._awsRequest('APIGateway', 'getUsagePlans', {}), + this.provider._awsRequest('APIGateway', 'getUsagePlans', {}), ]) .then((data) => data[1].items.filter((item) => @@ -42,7 +30,7 @@ module.exports = { items .map((item) => item.apiStages.map((apiStage) => - this._awsRequest('APIGateway', 'updateUsagePlan', { + this.provider._awsRequest('APIGateway', 'updateUsagePlan', { usagePlanId: item.id, patchOperations: [ { diff --git a/lib/plugins/aws/provider.js b/lib/plugins/aws/provider.js index 54f6e24b34..64566c1640 100644 --- a/lib/plugins/aws/provider.js +++ b/lib/plugins/aws/provider.js @@ -1968,8 +1968,7 @@ class AwsProvider { if (this.serverless.service.provider.deploymentBucket) { return BbPromise.resolve(this.serverless.service.provider.deploymentBucket); } - const requestMethod = this._v3Enabled ? 'requestV3' : 'request'; - return this[requestMethod]('CloudFormation', 'describeStackResource', { + return this._awsRequest('CloudFormation', 'describeStackResource', { StackName: this.naming.getStackName(), LogicalResourceId: this.naming.getDeploymentBucketLogicalId(), }).then((result) => result.StackResourceDetail.PhysicalResourceId); @@ -2184,8 +2183,7 @@ class AwsProvider { if (!resources) resources = []; if (next) params.NextToken = next; - const requestMethod = this._v3Enabled ? 'requestV3' : 'request'; - return this[requestMethod]('CloudFormation', 'listStackResources', params).then((res) => { + return this._awsRequest('CloudFormation', 'listStackResources', params).then((res) => { const allResources = resources.concat(res.StackResourceSummaries); if (!res.NextToken) { return allResources; @@ -2225,8 +2223,7 @@ Object.defineProperties( memoizeeMethods({ getAccountInfo: d( async function () { - const requestMethod = this._v3Enabled ? 'requestV3' : 'request'; - const result = await this[requestMethod]('STS', 'getCallerIdentity', {}); + const result = await this._awsRequest('STS', 'getCallerIdentity', {}); const arn = result.Arn; const accountId = result.Account; const partition = arn.split(':')[1]; // ex: arn:aws:iam:acctId:user/xyz @@ -2262,8 +2259,7 @@ Object.defineProperties( dockerLoginToEcr: d( async function () { const registryId = await this.getAccountId(); - const requestMethod = this._v3Enabled ? 'requestV3' : 'request'; - const result = await this[requestMethod]('ECR', 'getAuthorizationToken', { + const result = await this._awsRequest('ECR', 'getAuthorizationToken', { registryIds: [registryId], }); const { authorizationToken, proxyEndpoint } = result.authorizationData[0]; @@ -2301,8 +2297,7 @@ Object.defineProperties( const repositoryName = this.naming.getEcrRepositoryName(); let repositoryUri; try { - const requestMethod = this._v3Enabled ? 'requestV3' : 'request'; - const result = await this[requestMethod]('ECR', 'describeRepositories', { + const result = await this._awsRequest('ECR', 'describeRepositories', { repositoryNames: [repositoryName], registryId, }); @@ -2311,8 +2306,7 @@ Object.defineProperties( if (!(err.providerError && err.providerError.code === 'RepositoryNotFoundException')) { throw err; } - const requestMethod = this._v3Enabled ? 'requestV3' : 'request'; - const result = await this[requestMethod]('ECR', 'createRepository', { + const result = await this._awsRequest('ECR', 'createRepository', { repositoryName, imageScanningConfiguration: { scanOnPush }, }); @@ -2459,8 +2453,7 @@ Object.defineProperties( 'LAMBDA_ECR_REGION_MISMATCH_ERROR' ); } - const requestMethod = this._v3Enabled ? 'requestV3' : 'request'; - const describeImagesResponse = await this[requestMethod]('ECR', 'describeImages', { + const describeImagesResponse = await this._awsRequest('ECR', 'describeImages', { imageIds: [ { imageTag, diff --git a/lib/plugins/aws/remove/lib/stack.js b/lib/plugins/aws/remove/lib/stack.js index f420398387..cd19cd7fe7 100644 --- a/lib/plugins/aws/remove/lib/stack.js +++ b/lib/plugins/aws/remove/lib/stack.js @@ -6,11 +6,7 @@ module.exports = { * @private */ async _cloudFormationRequest(method, params) { - // Use v3 if feature flag is enabled, otherwise fallback to v2 - if (this.provider._v3Enabled) { - return this.provider.requestV3('CloudFormation', method, params); - } - return this.provider.request('CloudFormation', method, params); + return this.provider._awsRequest('CloudFormation', method, params); }, async remove() { diff --git a/lib/plugins/aws/rollback-function.js b/lib/plugins/aws/rollback-function.js index 23993500bb..20e5a9c1aa 100644 --- a/lib/plugins/aws/rollback-function.js +++ b/lib/plugins/aws/rollback-function.js @@ -31,11 +31,7 @@ class AwsRollbackFunction { * @private */ async _lambdaRequest(method, params) { - // Use v3 if feature flag is enabled, otherwise fallback to v2 - if (this.provider._v3Enabled) { - return this.provider.requestV3('Lambda', method, params); - } - return this.provider.request('Lambda', method, params); + return this.provider._awsRequest('Lambda', method, params); } async getFunctionToBeRestored() { diff --git a/lib/plugins/aws/rollback.js b/lib/plugins/aws/rollback.js index 9fb842335b..63c2dffe89 100644 --- a/lib/plugins/aws/rollback.js +++ b/lib/plugins/aws/rollback.js @@ -90,11 +90,7 @@ class AwsRollback { * @private */ async _s3Request(method, params) { - // Use v3 if feature flag is enabled, otherwise fallback to v2 - if (this.provider._v3Enabled) { - return this.provider.requestV3('S3', method, params); - } - return this.provider.request('S3', method, params); + return this.provider._awsRequest('S3', method, params); } async setStackToUpdate() { diff --git a/lib/plugins/aws/utils/resolve-cf-import-value.js b/lib/plugins/aws/utils/resolve-cf-import-value.js index e318c4d74f..27aa620f57 100644 --- a/lib/plugins/aws/utils/resolve-cf-import-value.js +++ b/lib/plugins/aws/utils/resolve-cf-import-value.js @@ -3,8 +3,7 @@ const ServerlessError = require('../../../serverless-error'); async function resolveCfImportValue(provider, name, sdkParams = {}) { - const requestMethod = provider._v3Enabled ? 'requestV3' : 'request'; - return provider[requestMethod]('CloudFormation', 'listExports', sdkParams).then((result) => { + return provider._awsRequest('CloudFormation', 'listExports', sdkParams).then((result) => { const targetExportMeta = result.Exports.find((exportMeta) => exportMeta.Name === name); if (targetExportMeta) return targetExportMeta.Value; if (result.NextToken) { diff --git a/lib/plugins/aws/utils/resolve-cf-ref-value.js b/lib/plugins/aws/utils/resolve-cf-ref-value.js index 1c22c651ce..9a01e5932a 100644 --- a/lib/plugins/aws/utils/resolve-cf-ref-value.js +++ b/lib/plugins/aws/utils/resolve-cf-ref-value.js @@ -3,25 +3,26 @@ const ServerlessError = require('../../../serverless-error'); async function resolveCfRefValue(provider, resourceLogicalId, sdkParams = {}) { - const requestMethod = provider._v3Enabled ? 'requestV3' : 'request'; - return provider[requestMethod]( - 'CloudFormation', - 'listStackResources', - Object.assign(sdkParams, { StackName: provider.naming.getStackName() }) - ).then((result) => { - const targetStackResource = result.StackResourceSummaries.find( - (stackResource) => stackResource.LogicalResourceId === resourceLogicalId - ); - if (targetStackResource) return targetStackResource.PhysicalResourceId; - if (result.NextToken) { - return resolveCfRefValue(provider, resourceLogicalId, { NextToken: result.NextToken }); - } + return provider + ._awsRequest( + 'CloudFormation', + 'listStackResources', + Object.assign(sdkParams, { StackName: provider.naming.getStackName() }) + ) + .then((result) => { + const targetStackResource = result.StackResourceSummaries.find( + (stackResource) => stackResource.LogicalResourceId === resourceLogicalId + ); + if (targetStackResource) return targetStackResource.PhysicalResourceId; + if (result.NextToken) { + return resolveCfRefValue(provider, resourceLogicalId, { NextToken: result.NextToken }); + } - throw new ServerlessError( - `Could not resolve Ref with name ${resourceLogicalId}. Are you sure this value matches one resource logical ID ?`, - 'CF_REF_RESOLUTION' - ); - }); + throw new ServerlessError( + `Could not resolve Ref with name ${resourceLogicalId}. Are you sure this value matches one resource logical ID ?`, + 'CF_REF_RESOLUTION' + ); + }); } module.exports = resolveCfRefValue; diff --git a/test/utils/api-gateway.js b/test/utils/api-gateway.js index f409f1f0b1..bbacc91e5b 100644 --- a/test/utils/api-gateway.js +++ b/test/utils/api-gateway.js @@ -23,16 +23,15 @@ const getAPIGatewayClient = () => { getResources: (params) => client.send(new GetResourcesCommand(params)), getRestApis: (params) => client.send(new GetRestApisCommand(params)), }; - } else { - // AWS SDK v2 - const APIGatewayService = require('aws-sdk').APIGateway; - return { - createRestApi: (params) => awsRequest(APIGatewayService, 'createRestApi', params), - deleteRestApi: (params) => awsRequest(APIGatewayService, 'deleteRestApi', params), - getResources: (params) => awsRequest(APIGatewayService, 'getResources', params), - getRestApis: (params) => awsRequest(APIGatewayService, 'getRestApis', params), - }; } + // AWS SDK v2 + const APIGatewayService = require('aws-sdk').APIGateway; + return { + createRestApi: (params) => awsRequest(APIGatewayService, 'createRestApi', params), + deleteRestApi: (params) => awsRequest(APIGatewayService, 'deleteRestApi', params), + getResources: (params) => awsRequest(APIGatewayService, 'getResources', params), + getRestApis: (params) => awsRequest(APIGatewayService, 'getRestApis', params), + }; }; const apiGateway = getAPIGatewayClient(); diff --git a/test/utils/cloudformation.js b/test/utils/cloudformation.js index 6621c91f38..e82d94c6bb 100644 --- a/test/utils/cloudformation.js +++ b/test/utils/cloudformation.js @@ -22,17 +22,15 @@ const getCloudFormationClient = () => { listStackResources: (params) => client.send(new ListStackResourcesCommand(params)), describeStacks: (params) => client.send(new DescribeStacksCommand(params)), }; - } else { - // AWS SDK v2 - const CloudFormationService = require('aws-sdk').CloudFormation; - return { - listStacks: (params) => awsRequest(CloudFormationService, 'listStacks', params), - deleteStack: (params) => awsRequest(CloudFormationService, 'deleteStack', params), - listStackResources: (params) => - awsRequest(CloudFormationService, 'listStackResources', params), - describeStacks: (params) => awsRequest(CloudFormationService, 'describeStacks', params), - }; } + // AWS SDK v2 + const CloudFormationService = require('aws-sdk').CloudFormation; + return { + listStacks: (params) => awsRequest(CloudFormationService, 'listStacks', params), + deleteStack: (params) => awsRequest(CloudFormationService, 'deleteStack', params), + listStackResources: (params) => awsRequest(CloudFormationService, 'listStackResources', params), + describeStacks: (params) => awsRequest(CloudFormationService, 'describeStacks', params), + }; }; const cf = getCloudFormationClient(); diff --git a/test/utils/cognito.js b/test/utils/cognito.js index 89a38f482d..ec04ff363e 100644 --- a/test/utils/cognito.js +++ b/test/utils/cognito.js @@ -31,28 +31,27 @@ const getCognitoClient = () => { adminSetUserPassword: (params) => client.send(new AdminSetUserPasswordCommand(params)), initiateAuth: (params) => client.send(new InitiateAuthCommand(params)), }; - } else { - // AWS SDK v2 - const CognitoIdentityServiceProviderService = require('aws-sdk').CognitoIdentityServiceProvider; - return { - createUserPool: (params) => - awsRequest(CognitoIdentityServiceProviderService, 'createUserPool', params), - createUserPoolClient: (params) => - awsRequest(CognitoIdentityServiceProviderService, 'createUserPoolClient', params), - deleteUserPool: (params) => - awsRequest(CognitoIdentityServiceProviderService, 'deleteUserPool', params), - listUserPools: (params) => - awsRequest(CognitoIdentityServiceProviderService, 'listUserPools', params), - describeUserPool: (params) => - awsRequest(CognitoIdentityServiceProviderService, 'describeUserPool', params), - adminCreateUser: (params) => - awsRequest(CognitoIdentityServiceProviderService, 'adminCreateUser', params), - adminSetUserPassword: (params) => - awsRequest(CognitoIdentityServiceProviderService, 'adminSetUserPassword', params), - initiateAuth: (params) => - awsRequest(CognitoIdentityServiceProviderService, 'initiateAuth', params), - }; } + // AWS SDK v2 + const CognitoIdentityServiceProviderService = require('aws-sdk').CognitoIdentityServiceProvider; + return { + createUserPool: (params) => + awsRequest(CognitoIdentityServiceProviderService, 'createUserPool', params), + createUserPoolClient: (params) => + awsRequest(CognitoIdentityServiceProviderService, 'createUserPoolClient', params), + deleteUserPool: (params) => + awsRequest(CognitoIdentityServiceProviderService, 'deleteUserPool', params), + listUserPools: (params) => + awsRequest(CognitoIdentityServiceProviderService, 'listUserPools', params), + describeUserPool: (params) => + awsRequest(CognitoIdentityServiceProviderService, 'describeUserPool', params), + adminCreateUser: (params) => + awsRequest(CognitoIdentityServiceProviderService, 'adminCreateUser', params), + adminSetUserPassword: (params) => + awsRequest(CognitoIdentityServiceProviderService, 'adminSetUserPassword', params), + initiateAuth: (params) => + awsRequest(CognitoIdentityServiceProviderService, 'initiateAuth', params), + }; }; const cognito = getCognitoClient(); diff --git a/test/utils/dynamodb.js b/test/utils/dynamodb.js index cece2d37fb..6bdf44f88c 100644 --- a/test/utils/dynamodb.js +++ b/test/utils/dynamodb.js @@ -15,13 +15,12 @@ const getDynamoDBClient = () => { return { put: (params) => docClient.send(new PutCommand(params)), }; - } else { - // AWS SDK v2 - const DDBDocumentClient = require('aws-sdk').DynamoDB.DocumentClient; - return { - put: (params) => awsRequest(DDBDocumentClient, 'put', params), - }; } + // AWS SDK v2 + const DDBDocumentClient = require('aws-sdk').DynamoDB.DocumentClient; + return { + put: (params) => awsRequest(DDBDocumentClient, 'put', params), + }; }; const dynamodb = getDynamoDBClient(); diff --git a/test/utils/event-bridge.js b/test/utils/event-bridge.js index 4f65a7ac02..b67fcb5aa8 100644 --- a/test/utils/event-bridge.js +++ b/test/utils/event-bridge.js @@ -22,16 +22,15 @@ const getEventBridgeClient = () => { describeEventBus: (params) => client.send(new DescribeEventBusCommand(params)), putEvents: (params) => client.send(new PutEventsCommand(params)), }; - } else { - // AWS SDK v2 - const EventBridgeService = require('aws-sdk').EventBridge; - return { - createEventBus: (params) => awsRequest(EventBridgeService, 'createEventBus', params), - deleteEventBus: (params) => awsRequest(EventBridgeService, 'deleteEventBus', params), - describeEventBus: (params) => awsRequest(EventBridgeService, 'describeEventBus', params), - putEvents: (params) => awsRequest(EventBridgeService, 'putEvents', params), - }; } + // AWS SDK v2 + const EventBridgeService = require('aws-sdk').EventBridge; + return { + createEventBus: (params) => awsRequest(EventBridgeService, 'createEventBus', params), + deleteEventBus: (params) => awsRequest(EventBridgeService, 'deleteEventBus', params), + describeEventBus: (params) => awsRequest(EventBridgeService, 'describeEventBus', params), + putEvents: (params) => awsRequest(EventBridgeService, 'putEvents', params), + }; }; const eventBridge = getEventBridgeClient(); diff --git a/test/utils/iot.js b/test/utils/iot.js index 63d6cede3f..d7a6962fd6 100644 --- a/test/utils/iot.js +++ b/test/utils/iot.js @@ -25,20 +25,19 @@ const getIoTClients = () => { }; }, }; - } else { - // AWS SDK v2 - const IotService = require('aws-sdk').Iot; - const IotDataService = require('aws-sdk').IotData; - return { - iot: { - describeEndpoint: (params) => awsRequest(IotService, 'describeEndpoint', params), - }, - createIoTDataClient: (endpoint) => ({ - publish: (params) => - awsRequest({ client: IotDataService, params: { endpoint } }, 'publish', params), - }), - }; } + // AWS SDK v2 + const IotService = require('aws-sdk').Iot; + const IotDataService = require('aws-sdk').IotData; + return { + iot: { + describeEndpoint: (params) => awsRequest(IotService, 'describeEndpoint', params), + }, + createIoTDataClient: (endpoint) => ({ + publish: (params) => + awsRequest({ client: IotDataService, params: { endpoint } }, 'publish', params), + }), + }; }; const { iot, createIoTDataClient } = getIoTClients(); diff --git a/test/utils/kinesis.js b/test/utils/kinesis.js index 16261da008..0187711801 100644 --- a/test/utils/kinesis.js +++ b/test/utils/kinesis.js @@ -22,16 +22,15 @@ const getKinesisClient = () => { describeStream: (params) => client.send(new DescribeStreamCommand(params)), putRecord: (params) => client.send(new PutRecordCommand(params)), }; - } else { - // AWS SDK v2 - const KinesisService = require('aws-sdk').Kinesis; - return { - createStream: (params) => awsRequest(KinesisService, 'createStream', params), - deleteStream: (params) => awsRequest(KinesisService, 'deleteStream', params), - describeStream: (params) => awsRequest(KinesisService, 'describeStream', params), - putRecord: (params) => awsRequest(KinesisService, 'putRecord', params), - }; } + // AWS SDK v2 + const KinesisService = require('aws-sdk').Kinesis; + return { + createStream: (params) => awsRequest(KinesisService, 'createStream', params), + deleteStream: (params) => awsRequest(KinesisService, 'deleteStream', params), + describeStream: (params) => awsRequest(KinesisService, 'describeStream', params), + putRecord: (params) => awsRequest(KinesisService, 'putRecord', params), + }; }; const kinesis = getKinesisClient(); diff --git a/test/utils/misc.js b/test/utils/misc.js index 6cbb756d5d..70c718cb3f 100644 --- a/test/utils/misc.js +++ b/test/utils/misc.js @@ -15,13 +15,12 @@ const getCloudWatchLogsClient = () => { return { filterLogEvents: (params) => client.send(new FilterLogEventsCommand(params)), }; - } else { - // AWS SDK v2 - const CloudWatchLogsService = require('aws-sdk').CloudWatchLogs; - return { - filterLogEvents: (params) => awsRequest(CloudWatchLogsService, 'filterLogEvents', params), - }; } + // AWS SDK v2 + const CloudWatchLogsService = require('aws-sdk').CloudWatchLogs; + return { + filterLogEvents: (params) => awsRequest(CloudWatchLogsService, 'filterLogEvents', params), + }; }; const cloudWatchLogs = getCloudWatchLogsClient(); diff --git a/test/utils/s3.js b/test/utils/s3.js index 31d04100ed..8f6e445a52 100644 --- a/test/utils/s3.js +++ b/test/utils/s3.js @@ -26,18 +26,17 @@ const getS3Client = () => { deleteObjects: (params) => client.send(new DeleteObjectsCommand(params)), deleteBucket: (params) => client.send(new DeleteBucketCommand(params)), }; - } else { - // AWS SDK v2 - const S3Service = require('aws-sdk').S3; - return { - createBucket: (params) => awsRequest(S3Service, 'createBucket', params), - putObject: (params) => awsRequest(S3Service, 'putObject', params), - deleteObject: (params) => awsRequest(S3Service, 'deleteObject', params), - listObjects: (params) => awsRequest(S3Service, 'listObjects', params), - deleteObjects: (params) => awsRequest(S3Service, 'deleteObjects', params), - deleteBucket: (params) => awsRequest(S3Service, 'deleteBucket', params), - }; } + // AWS SDK v2 + const S3Service = require('aws-sdk').S3; + return { + createBucket: (params) => awsRequest(S3Service, 'createBucket', params), + putObject: (params) => awsRequest(S3Service, 'putObject', params), + deleteObject: (params) => awsRequest(S3Service, 'deleteObject', params), + listObjects: (params) => awsRequest(S3Service, 'listObjects', params), + deleteObjects: (params) => awsRequest(S3Service, 'deleteObjects', params), + deleteBucket: (params) => awsRequest(S3Service, 'deleteBucket', params), + }; }; const s3 = getS3Client(); diff --git a/test/utils/sns.js b/test/utils/sns.js index 31965175cd..f3ac22de3b 100644 --- a/test/utils/sns.js +++ b/test/utils/sns.js @@ -22,16 +22,15 @@ const getSNSClient = () => { listTopics: (params) => client.send(new ListTopicsCommand(params)), publish: (params) => client.send(new PublishCommand(params)), }; - } else { - // AWS SDK v2 - const SNSService = require('aws-sdk').SNS; - return { - createTopic: (params) => awsRequest(SNSService, 'createTopic', params), - deleteTopic: (params) => awsRequest(SNSService, 'deleteTopic', params), - listTopics: (params) => awsRequest(SNSService, 'listTopics', params), - publish: (params) => awsRequest(SNSService, 'publish', params), - }; } + // AWS SDK v2 + const SNSService = require('aws-sdk').SNS; + return { + createTopic: (params) => awsRequest(SNSService, 'createTopic', params), + deleteTopic: (params) => awsRequest(SNSService, 'deleteTopic', params), + listTopics: (params) => awsRequest(SNSService, 'listTopics', params), + publish: (params) => awsRequest(SNSService, 'publish', params), + }; }; const sns = getSNSClient(); diff --git a/test/utils/sqs.js b/test/utils/sqs.js index a6223553c9..5fecb168ba 100644 --- a/test/utils/sqs.js +++ b/test/utils/sqs.js @@ -22,16 +22,15 @@ const getSQSClient = () => { getQueueUrl: (params) => client.send(new GetQueueUrlCommand(params)), sendMessage: (params) => client.send(new SendMessageCommand(params)), }; - } else { - // AWS SDK v2 - const SQSService = require('aws-sdk').SQS; - return { - createQueue: (params) => awsRequest(SQSService, 'createQueue', params), - deleteQueue: (params) => awsRequest(SQSService, 'deleteQueue', params), - getQueueUrl: (params) => awsRequest(SQSService, 'getQueueUrl', params), - sendMessage: (params) => awsRequest(SQSService, 'sendMessage', params), - }; } + // AWS SDK v2 + const SQSService = require('aws-sdk').SQS; + return { + createQueue: (params) => awsRequest(SQSService, 'createQueue', params), + deleteQueue: (params) => awsRequest(SQSService, 'deleteQueue', params), + getQueueUrl: (params) => awsRequest(SQSService, 'getQueueUrl', params), + sendMessage: (params) => awsRequest(SQSService, 'sendMessage', params), + }; }; const sqs = getSQSClient(); diff --git a/test/utils/websocket.js b/test/utils/websocket.js index f7565983bb..5505864b10 100644 --- a/test/utils/websocket.js +++ b/test/utils/websocket.js @@ -24,17 +24,16 @@ const getApiGatewayV2Client = () => { deleteStage: (params) => client.send(new DeleteStageCommand(params)), getRoutes: (params) => client.send(new GetRoutesCommand(params)), }; - } else { - // AWS SDK v2 - const ApiGatewayV2Service = require('aws-sdk').ApiGatewayV2; - return { - createApi: (params) => awsRequest(ApiGatewayV2Service, 'createApi', params), - deleteApi: (params) => awsRequest(ApiGatewayV2Service, 'deleteApi', params), - createStage: (params) => awsRequest(ApiGatewayV2Service, 'createStage', params), - deleteStage: (params) => awsRequest(ApiGatewayV2Service, 'deleteStage', params), - getRoutes: (params) => awsRequest(ApiGatewayV2Service, 'getRoutes', params), - }; } + // AWS SDK v2 + const ApiGatewayV2Service = require('aws-sdk').ApiGatewayV2; + return { + createApi: (params) => awsRequest(ApiGatewayV2Service, 'createApi', params), + deleteApi: (params) => awsRequest(ApiGatewayV2Service, 'deleteApi', params), + createStage: (params) => awsRequest(ApiGatewayV2Service, 'createStage', params), + deleteStage: (params) => awsRequest(ApiGatewayV2Service, 'deleteStage', params), + getRoutes: (params) => awsRequest(ApiGatewayV2Service, 'getRoutes', params), + }; }; const apiGatewayV2 = getApiGatewayV2Client(); From 69870c2fdcd7ec90d9bee208fa40b5165a70c0f4 Mon Sep 17 00:00:00 2001 From: Graham Campbell Date: Sun, 8 Jun 2025 23:35:51 +0100 Subject: [PATCH 11/27] Further cleanup --- .../sources/instance-dependent/get-aws.js | 7 +---- .../sources/instance-dependent/get-cf.js | 10 +------ .../sources/instance-dependent/get-s3.js | 10 +------ .../sources/instance-dependent/get-ssm.js | 13 +-------- .../aws/deploy/lib/cleanup-s3-bucket.js | 13 +-------- lib/plugins/aws/info/get-resource-count.js | 13 +-------- lib/plugins/aws/invoke-local/index.js | 13 +-------- .../aws/lib/check-if-ecr-repository-exists.js | 14 +-------- lib/plugins/aws/lib/update-stack.js | 29 ++++++------------- lib/plugins/aws/lib/upload-zip-file.js | 13 +-------- .../aws/lib/wait-for-change-set-creation.js | 13 +-------- lib/plugins/aws/package/compile/layers.js | 13 +-------- lib/plugins/aws/remove/lib/bucket.js | 17 ++--------- lib/plugins/aws/remove/lib/ecr.js | 14 +-------- 14 files changed, 24 insertions(+), 168 deletions(-) diff --git a/lib/configuration/variables/sources/instance-dependent/get-aws.js b/lib/configuration/variables/sources/instance-dependent/get-aws.js index ad9823fc71..e7a1bc9b70 100644 --- a/lib/configuration/variables/sources/instance-dependent/get-aws.js +++ b/lib/configuration/variables/sources/instance-dependent/get-aws.js @@ -22,12 +22,7 @@ module.exports = (serverlessInstance) => { switch (address) { case 'accountId': { const provider = serverlessInstance.getProvider('aws'); - let result; - if (provider._v3Enabled) { - result = await provider.requestV3('STS', 'getCallerIdentity', {}, { useCache: true }); - } else { - result = await provider.request('STS', 'getCallerIdentity', {}, { useCache: true }); - } + const result = await provider._awsRequest('STS', 'getCallerIdentity', {}, { useCache: true }); return { value: result.Account }; } case 'region': { diff --git a/lib/configuration/variables/sources/instance-dependent/get-cf.js b/lib/configuration/variables/sources/instance-dependent/get-cf.js index b036b8db4c..64098feccb 100644 --- a/lib/configuration/variables/sources/instance-dependent/get-cf.js +++ b/lib/configuration/variables/sources/instance-dependent/get-cf.js @@ -33,15 +33,7 @@ module.exports = (serverlessInstance) => { const result = await (async () => { try { const provider = serverlessInstance.getProvider('aws'); - if (provider._v3Enabled) { - return await provider.requestV3( - 'CloudFormation', - 'describeStacks', - { StackName: stackName }, - { useCache: true, region: params && params[0] } - ); - } - return await provider.request( + return await provider._awsRequest( 'CloudFormation', 'describeStacks', { StackName: stackName }, diff --git a/lib/configuration/variables/sources/instance-dependent/get-s3.js b/lib/configuration/variables/sources/instance-dependent/get-s3.js index e22f2f9a4f..79ff594c44 100644 --- a/lib/configuration/variables/sources/instance-dependent/get-s3.js +++ b/lib/configuration/variables/sources/instance-dependent/get-s3.js @@ -32,15 +32,7 @@ module.exports = (serverlessInstance) => { const result = await (async () => { try { const provider = serverlessInstance.getProvider('aws'); - if (provider._v3Enabled) { - return await provider.requestV3( - 'S3', - 'getObject', - { Bucket: bucketName, Key: key }, - { useCache: true } - ); - } - return await provider.request( + return await provider._awsRequest( 'S3', 'getObject', { Bucket: bucketName, Key: key }, diff --git a/lib/configuration/variables/sources/instance-dependent/get-ssm.js b/lib/configuration/variables/sources/instance-dependent/get-ssm.js index afcda8effa..13e156739e 100644 --- a/lib/configuration/variables/sources/instance-dependent/get-ssm.js +++ b/lib/configuration/variables/sources/instance-dependent/get-ssm.js @@ -27,18 +27,7 @@ module.exports = (serverlessInstance) => { const result = await (async () => { try { const provider = serverlessInstance.getProvider('aws'); - if (provider._v3Enabled) { - return await provider.requestV3( - 'SSM', - 'getParameter', - { - Name: address, - WithDecryption: !shouldSkipDecryption, - }, - { useCache: true, region } - ); - } - return await provider.request( + return await provider._awsRequest( 'SSM', 'getParameter', { diff --git a/lib/plugins/aws/deploy/lib/cleanup-s3-bucket.js b/lib/plugins/aws/deploy/lib/cleanup-s3-bucket.js index c3c6775904..d1f2f494e8 100644 --- a/lib/plugins/aws/deploy/lib/cleanup-s3-bucket.js +++ b/lib/plugins/aws/deploy/lib/cleanup-s3-bucket.js @@ -7,17 +7,6 @@ const ServerlessError = require('../../../../serverless-error'); const { log } = require('@serverless/utils/log'); module.exports = { - /** - * Helper method to route S3 requests through v2 or v3 based on feature flag - * @private - */ - async _s3Request(method, params) { - // Use v3 if feature flag is enabled, otherwise fallback to v2 - if (this.provider._v3Enabled) { - return this.provider.requestV3('S3', method, params); - } - return this.provider.request('S3', method, params); - }, async getObjectsToRemove() { const stacksToKeepCount = _.get( @@ -32,7 +21,7 @@ module.exports = { let response; try { - response = await this._s3Request('listObjectsV2', { + response = await this.provider._awsRequest('S3', 'listObjectsV2', { Bucket: this.bucketName, Prefix: `${prefix}/${service}/${stage}`, }); diff --git a/lib/plugins/aws/info/get-resource-count.js b/lib/plugins/aws/info/get-resource-count.js index ad8293340e..771f8f56c3 100644 --- a/lib/plugins/aws/info/get-resource-count.js +++ b/lib/plugins/aws/info/get-resource-count.js @@ -3,24 +3,13 @@ const BbPromise = require('bluebird'); module.exports = { - /** - * Helper method to route CloudFormation requests through v2 or v3 based on feature flag - * @private - */ - async _cloudFormationRequest(method, params) { - // Use v3 if feature flag is enabled, otherwise fallback to v2 - if (this.provider._v3Enabled) { - return this.provider.requestV3('CloudFormation', method, params); - } - return this.provider.request('CloudFormation', method, params); - }, async getResourceCount(nextToken, resourceCount = 0) { const params = { StackName: this.provider.naming.getStackName(), NextToken: nextToken, }; - return this._cloudFormationRequest('listStackResources', params).then((result) => { + return this.provider._awsRequest('CloudFormation', 'listStackResources', params).then((result) => { if (Object.keys(result).length) { this.gatheredData.info.resourceCount = resourceCount + result.StackResourceSummaries.length; if (result.NextToken) { diff --git a/lib/plugins/aws/invoke-local/index.js b/lib/plugins/aws/invoke-local/index.js index 7bad5db84a..3cfd0f1e2c 100644 --- a/lib/plugins/aws/invoke-local/index.js +++ b/lib/plugins/aws/invoke-local/index.js @@ -55,17 +55,6 @@ class AwsInvokeLocal { }; } - /** - * Helper method to route Lambda requests through v2 or v3 based on feature flag - * @private - */ - async _lambdaRequest(method, params) { - // Use v3 if feature flag is enabled, otherwise fallback to v2 - if (this.provider._v3Enabled) { - return this.provider.requestV3('Lambda', method, params); - } - return this.provider.request('Lambda', method, params); - } getRuntime() { return this.provider.getRuntime(this.options.functionObj.runtime); @@ -388,7 +377,7 @@ class AwsInvokeLocal { layerProgress.notice(`Downloading layer ${layer}`); await ensureDir(path.join(layerContentsCachePath)); - const layerInfo = await this._lambdaRequest('getLayerVersion', { + const layerInfo = await this.provider._awsRequest('Lambda', 'getLayerVersion', { LayerName: layerArn, VersionNumber: layerVersion, }); diff --git a/lib/plugins/aws/lib/check-if-ecr-repository-exists.js b/lib/plugins/aws/lib/check-if-ecr-repository-exists.js index 10e4fdc8e6..90032e8d63 100644 --- a/lib/plugins/aws/lib/check-if-ecr-repository-exists.js +++ b/lib/plugins/aws/lib/check-if-ecr-repository-exists.js @@ -3,23 +3,11 @@ const { log } = require('@serverless/utils/log'); module.exports = { - /** - * Helper method to route ECR requests through v2 or v3 based on feature flag - * @private - */ - async _ecrRequest(method, params) { - // Use v3 if feature flag is enabled, otherwise fallback to v2 - if (this.provider._v3Enabled) { - return this.provider.requestV3('ECR', method, params); - } - return this.provider.request('ECR', method, params); - }, - async checkIfEcrRepositoryExists() { const registryId = await this.provider.getAccountId(); const repositoryName = this.provider.naming.getEcrRepositoryName(); try { - await this._ecrRequest('describeRepositories', { + await this.provider._awsRequest('ECR', 'describeRepositories', { repositoryNames: [repositoryName], registryId, }); diff --git a/lib/plugins/aws/lib/update-stack.js b/lib/plugins/aws/lib/update-stack.js index 71ff340620..ca4797abe9 100644 --- a/lib/plugins/aws/lib/update-stack.js +++ b/lib/plugins/aws/lib/update-stack.js @@ -8,17 +8,6 @@ const isChangeSetWithoutChanges = require('../utils/is-change-set-without-change const NO_UPDATE_MESSAGE = 'No updates are to be performed.'; module.exports = { - /** - * Helper method to route CloudFormation requests through v2 or v3 based on feature flag - * @private - */ - async _cloudFormationRequest(method, params) { - // Use v3 if feature flag is enabled, otherwise fallback to v2 - if (this.provider._v3Enabled) { - return this.provider.requestV3('CloudFormation', method, params); - } - return this.provider.request('CloudFormation', method, params); - }, async createFallback() { this.createLater = false; progress.get('main').notice('Creating CloudFormation stack', { isMainEvent: true }); @@ -34,7 +23,7 @@ module.exports = { templateUrl, }); - monitorCfData = await this._cloudFormationRequest('createStack', params); + monitorCfData = await this.provider._awsRequest('CloudFormation', 'createStack', params); } else { const changeSetName = this.provider.naming.getStackChangeSetName(); @@ -48,7 +37,7 @@ module.exports = { // Create new change set this.provider.didCreateService = true; log.info('Creating new change set'); - await this._cloudFormationRequest('createChangeSet', createChangeSetParams); + await this.provider._awsRequest('CloudFormation', 'createChangeSet', createChangeSetParams); // Wait for changeset to be created log.info('Waiting for new change set to be created'); @@ -58,7 +47,7 @@ module.exports = { if (isChangeSetWithoutChanges(changeSetDescription)) { // Cleanup changeset when it does not include any changes log.info('Created change set does not include any changes, removing it'); - await this._cloudFormationRequest('deleteChangeSet', { + await this.provider._awsRequest('CloudFormation', 'deleteChangeSet', { StackName: stackName, ChangeSetName: changeSetName, }); @@ -67,7 +56,7 @@ module.exports = { } log.info('Executing created change set'); - await this._cloudFormationRequest('executeChangeSet', executeChangeSetParams); + await this.provider._awsRequest('CloudFormation', 'executeChangeSet', executeChangeSetParams); monitorCfData = changeSetDescription; } await this.monitorStack('create', monitorCfData); @@ -86,7 +75,7 @@ module.exports = { const params = this.getUpdateStackParams({ templateUrl }); try { - monitorCfData = await this._cloudFormationRequest('updateStack', params); + monitorCfData = await this.provider._awsRequest('CloudFormation', 'updateStack', params); } catch (e) { if (e.message.includes(NO_UPDATE_MESSAGE)) { return false; @@ -111,7 +100,7 @@ module.exports = { // Create new change set log.info('Creating new change set'); - await this._cloudFormationRequest('createChangeSet', createChangeSetParams); + await this.provider._awsRequest('CloudFormation', 'createChangeSet', createChangeSetParams); // Wait for changeset to be created log.info('Waiting for new change set to be created'); @@ -121,7 +110,7 @@ module.exports = { if (isChangeSetWithoutChanges(changeSetDescription)) { // Cleanup changeset when it does not include any changes log.info('Created change set does not include any changes, removing it'); - await this._cloudFormationRequest('deleteChangeSet', { + await this.provider._awsRequest('CloudFormation', 'deleteChangeSet', { StackName: stackName, ChangeSetName: changeSetName, }); @@ -130,7 +119,7 @@ module.exports = { } log.info('Executing created change set'); - await this._cloudFormationRequest('executeChangeSet', executeChangeSetParams); + await this.provider._awsRequest('CloudFormation', 'executeChangeSet', executeChangeSetParams); monitorCfData = changeSetDescription; } @@ -150,7 +139,7 @@ module.exports = { const stackPolicyBody = JSON.stringify({ Statement: this.serverless.service.provider.stackPolicy, }); - await this._cloudFormationRequest('setStackPolicy', { + await this.provider._awsRequest('CloudFormation', 'setStackPolicy', { StackName: stackName, StackPolicyBody: stackPolicyBody, }); diff --git a/lib/plugins/aws/lib/upload-zip-file.js b/lib/plugins/aws/lib/upload-zip-file.js index 7746d963bd..7c56f51451 100644 --- a/lib/plugins/aws/lib/upload-zip-file.js +++ b/lib/plugins/aws/lib/upload-zip-file.js @@ -7,17 +7,6 @@ const log = require('@serverless/utils/log').log.get('deploy:upload'); const setS3UploadEncryptionOptions = require('../../../aws/set-s3-upload-encryption-options'); module.exports = { - /** - * Helper method to route S3 requests through v2 or v3 based on feature flag - * @private - */ - async _s3Request(method, params) { - // Use v3 if feature flag is enabled, otherwise fallback to v2 - if (this.provider._v3Enabled) { - return this.provider.requestV3('S3', method, params); - } - return this.provider.request('S3', method, params); - }, async uploadZipFile({ filename, s3KeyDirname, basename }) { if (!basename) basename = filename.split(path.sep).pop(); @@ -50,7 +39,7 @@ module.exports = { params = setS3UploadEncryptionOptions(params, deploymentBucketObject); } - const response = await this._s3Request('upload', params); + const response = await this.provider._awsRequest('S3', 'upload', params); // Interestingly, if request handling was queued, and stream errored (before being consumed by // AWS SDK) then SDK call succeeds without actually uploading a file to S3 bucket. // Below line ensures that eventual stream error is communicated diff --git a/lib/plugins/aws/lib/wait-for-change-set-creation.js b/lib/plugins/aws/lib/wait-for-change-set-creation.js index 4f68968bfa..979eadec58 100644 --- a/lib/plugins/aws/lib/wait-for-change-set-creation.js +++ b/lib/plugins/aws/lib/wait-for-change-set-creation.js @@ -7,17 +7,6 @@ const { log } = require('@serverless/utils/log'); const getMonitoringFrequency = require('../utils/get-monitoring-frequency'); module.exports = { - /** - * Helper method to route CloudFormation requests through v2 or v3 based on feature flag - * @private - */ - async _cloudFormationRequest(method, params) { - // Use v3 if feature flag is enabled, otherwise fallback to v2 - if (this.provider._v3Enabled) { - return this.provider.requestV3('CloudFormation', method, params); - } - return this.provider.request('CloudFormation', method, params); - }, async waitForChangeSetCreation(changeSetName, stackName) { const params = { @@ -26,7 +15,7 @@ module.exports = { }; const callWithRetry = async () => { - const changeSetDescription = await this._cloudFormationRequest('describeChangeSet', params); + const changeSetDescription = await this.provider._awsRequest('CloudFormation', 'describeChangeSet', params); if ( changeSetDescription.Status === 'CREATE_COMPLETE' || isChangeSetWithoutChanges(changeSetDescription) diff --git a/lib/plugins/aws/package/compile/layers.js b/lib/plugins/aws/package/compile/layers.js index 2263066ab5..848ca482e3 100644 --- a/lib/plugins/aws/package/compile/layers.js +++ b/lib/plugins/aws/package/compile/layers.js @@ -23,17 +23,6 @@ class AwsCompileLayers { }; } - /** - * Helper method to route CloudFormation requests through v2 or v3 based on feature flag - * @private - */ - async _cloudFormationRequest(method, params) { - // Use v3 if feature flag is enabled, otherwise fallback to v2 - if (this.provider._v3Enabled) { - return this.provider.requestV3('CloudFormation', method, params); - } - return this.provider.request('CloudFormation', method, params); - } async compileLayer(layerName) { const newLayer = this.cfLambdaLayerTemplate(); @@ -138,7 +127,7 @@ class AwsCompileLayers { const layerHashOutputLogicalId = this.provider.naming.getLambdaLayerHashOutputLogicalId(layerName); - return this._cloudFormationRequest('describeStacks', { StackName: stackName }).then( + return this.provider._awsRequest('CloudFormation', 'describeStacks', { StackName: stackName }).then( (data) => { const lastHash = data.Stacks[0].Outputs.find( (output) => output.OutputKey === layerHashOutputLogicalId diff --git a/lib/plugins/aws/remove/lib/bucket.js b/lib/plugins/aws/remove/lib/bucket.js index f92578442f..fc4236f7cc 100644 --- a/lib/plugins/aws/remove/lib/bucket.js +++ b/lib/plugins/aws/remove/lib/bucket.js @@ -4,17 +4,6 @@ const { log } = require('@serverless/utils/log'); const ServerlessError = require('../../../../serverless-error'); module.exports = { - /** - * Helper method to route S3 requests through v2 or v3 based on feature flag - * @private - */ - async _s3Request(method, params) { - // Use v3 if feature flag is enabled, otherwise fallback to v2 - if (this.provider._v3Enabled) { - return this.provider.requestV3('S3', method, params); - } - return this.provider.request('S3', method, params); - }, async setServerlessDeploymentBucketName() { try { @@ -39,7 +28,7 @@ module.exports = { let result; try { - result = await this._s3Request('listObjectsV2', { + result = await this.provider._awsRequest('S3', 'listObjectsV2', { Bucket: this.bucketName, Prefix: `${this.provider.getDeploymentPrefix()}/${serviceStage}`, }); @@ -67,7 +56,7 @@ module.exports = { const serviceStage = `${this.serverless.service.service}/${this.provider.getStage()}`; - const result = await this._s3Request('listObjectVersions', { + const result = await this.provider._awsRequest('S3', 'listObjectVersions', { Bucket: this.bucketName, Prefix: `${this.provider.getDeploymentPrefix()}/${serviceStage}`, }); @@ -102,7 +91,7 @@ module.exports = { async deleteObjects() { if (this.objectsInBucket.length) { - const data = await this._s3Request('deleteObjects', { + const data = await this.provider._awsRequest('S3', 'deleteObjects', { Bucket: this.bucketName, Delete: { Objects: this.objectsInBucket, diff --git a/lib/plugins/aws/remove/lib/ecr.js b/lib/plugins/aws/remove/lib/ecr.js index fe37933c1c..1f9feb1add 100644 --- a/lib/plugins/aws/remove/lib/ecr.js +++ b/lib/plugins/aws/remove/lib/ecr.js @@ -1,18 +1,6 @@ 'use strict'; module.exports = { - /** - * Helper method to route ECR requests through v2 or v3 based on feature flag - * @private - */ - async _ecrRequest(method, params) { - // Use v3 if feature flag is enabled, otherwise fallback to v2 - if (this.provider._v3Enabled) { - return this.provider.requestV3('ECR', method, params); - } - return this.provider.request('ECR', method, params); - }, - async removeEcrRepository() { const registryId = await this.provider.getAccountId(); const repositoryName = this.provider.naming.getEcrRepositoryName(); @@ -22,6 +10,6 @@ module.exports = { force: true, // To ensure removal of non-empty repository }; - await this._ecrRequest('deleteRepository', params); + await this.provider._awsRequest('ECR', 'deleteRepository', params); }, }; From 1c122f518a3a44514e08a2c6fc3279b8a80f93c7 Mon Sep 17 00:00:00 2001 From: Graham Campbell Date: Sun, 8 Jun 2025 23:50:06 +0100 Subject: [PATCH 12/27] Refactor to a cleaner approach --- .../sources/instance-dependent/get-aws.js | 2 +- .../sources/instance-dependent/get-cf.js | 2 +- .../sources/instance-dependent/get-s3.js | 2 +- .../sources/instance-dependent/get-ssm.js | 2 +- lib/plugins/aws/deploy-function.js | 10 +- lib/plugins/aws/deploy-list.js | 6 +- .../aws/deploy/lib/check-for-changes.js | 12 +- .../aws/deploy/lib/cleanup-s3-bucket.js | 7 +- lib/plugins/aws/deploy/lib/create-stack.js | 72 +++--- .../deploy/lib/ensure-valid-bucket-exists.js | 12 +- .../aws/deploy/lib/upload-artifacts.js | 4 +- .../aws/deploy/lib/validate-template.js | 10 +- lib/plugins/aws/info/get-api-key-values.js | 4 +- lib/plugins/aws/info/get-resource-count.js | 3 +- lib/plugins/aws/info/get-stack-info.js | 4 +- lib/plugins/aws/invoke-local/index.js | 3 +- lib/plugins/aws/invoke.js | 10 +- lib/plugins/aws/lib/check-if-bucket-exists.js | 10 +- .../aws/lib/check-if-ecr-repository-exists.js | 2 +- lib/plugins/aws/lib/monitor-stack.js | 228 +++++++++--------- lib/plugins/aws/lib/update-stack.js | 20 +- lib/plugins/aws/lib/upload-zip-file.js | 3 +- .../aws/lib/wait-for-change-set-creation.js | 7 +- lib/plugins/aws/logs.js | 12 +- lib/plugins/aws/metrics.js | 11 +- .../lib/hack/disassociate-usage-plan.js | 6 +- .../api-gateway/lib/hack/update-stage.js | 16 +- lib/plugins/aws/package/compile/layers.js | 3 +- lib/plugins/aws/provider.js | 50 ++-- lib/plugins/aws/remove/lib/bucket.js | 7 +- lib/plugins/aws/remove/lib/ecr.js | 2 +- lib/plugins/aws/remove/lib/stack.js | 10 +- lib/plugins/aws/rollback-function.js | 13 +- lib/plugins/aws/rollback.js | 12 +- .../aws/utils/resolve-cf-import-value.js | 2 +- lib/plugins/aws/utils/resolve-cf-ref-value.js | 2 +- 36 files changed, 255 insertions(+), 326 deletions(-) diff --git a/lib/configuration/variables/sources/instance-dependent/get-aws.js b/lib/configuration/variables/sources/instance-dependent/get-aws.js index e7a1bc9b70..b3eee17f7f 100644 --- a/lib/configuration/variables/sources/instance-dependent/get-aws.js +++ b/lib/configuration/variables/sources/instance-dependent/get-aws.js @@ -22,7 +22,7 @@ module.exports = (serverlessInstance) => { switch (address) { case 'accountId': { const provider = serverlessInstance.getProvider('aws'); - const result = await provider._awsRequest('STS', 'getCallerIdentity', {}, { useCache: true }); + const result = await provider.request('STS', 'getCallerIdentity', {}, { useCache: true }); return { value: result.Account }; } case 'region': { diff --git a/lib/configuration/variables/sources/instance-dependent/get-cf.js b/lib/configuration/variables/sources/instance-dependent/get-cf.js index 64098feccb..aa53ae32cb 100644 --- a/lib/configuration/variables/sources/instance-dependent/get-cf.js +++ b/lib/configuration/variables/sources/instance-dependent/get-cf.js @@ -33,7 +33,7 @@ module.exports = (serverlessInstance) => { const result = await (async () => { try { const provider = serverlessInstance.getProvider('aws'); - return await provider._awsRequest( + return await provider.request( 'CloudFormation', 'describeStacks', { StackName: stackName }, diff --git a/lib/configuration/variables/sources/instance-dependent/get-s3.js b/lib/configuration/variables/sources/instance-dependent/get-s3.js index 79ff594c44..0913adeed5 100644 --- a/lib/configuration/variables/sources/instance-dependent/get-s3.js +++ b/lib/configuration/variables/sources/instance-dependent/get-s3.js @@ -32,7 +32,7 @@ module.exports = (serverlessInstance) => { const result = await (async () => { try { const provider = serverlessInstance.getProvider('aws'); - return await provider._awsRequest( + return await provider.request( 'S3', 'getObject', { Bucket: bucketName, Key: key }, diff --git a/lib/configuration/variables/sources/instance-dependent/get-ssm.js b/lib/configuration/variables/sources/instance-dependent/get-ssm.js index 13e156739e..1a34554d8c 100644 --- a/lib/configuration/variables/sources/instance-dependent/get-ssm.js +++ b/lib/configuration/variables/sources/instance-dependent/get-ssm.js @@ -27,7 +27,7 @@ module.exports = (serverlessInstance) => { const result = await (async () => { try { const provider = serverlessInstance.getProvider('aws'); - return await provider._awsRequest( + return await provider.request( 'SSM', 'getParameter', { diff --git a/lib/plugins/aws/deploy-function.js b/lib/plugins/aws/deploy-function.js index a3702877fa..821ac8b8b2 100644 --- a/lib/plugins/aws/deploy-function.js +++ b/lib/plugins/aws/deploy-function.js @@ -77,7 +77,7 @@ class AwsDeployFunction { const result = await (async () => { try { - return await this.provider._awsRequest('Lambda', 'getFunction', params); + return await this.provider.request('Lambda', 'getFunction', params); } catch (error) { if (_.get(error, 'providerError.code') === 'ResourceNotFoundException') { const errorMessage = [ @@ -142,7 +142,7 @@ class AwsDeployFunction { return `arn:${result.partition}:iam::${result.accountId}:role${compiledFullRoleName}`; } - const data = await this.provider._awsRequest('IAM', 'getRole', { + const data = await this.provider.request('IAM', 'getRole', { RoleName: role['Fn::GetAtt'][0], }); return data.Arn; @@ -156,7 +156,7 @@ class AwsDeployFunction { const startTime = Date.now(); const callWithRetry = async () => { - const result = await this.provider._awsRequest('Lambda', 'getFunction', params); + const result = await this.provider.request('Lambda', 'getFunction', params); if ( result && result.Configuration.State === 'Active' && @@ -184,7 +184,7 @@ class AwsDeployFunction { const callWithRetry = async () => { try { - await this.provider._awsRequest('Lambda', 'updateFunctionConfiguration', params); + await this.provider.request('Lambda', 'updateFunctionConfiguration', params); } catch (err) { const didOneMinutePass = Date.now() - startTime > 60 * 1000; @@ -541,7 +541,7 @@ class AwsDeployFunction { } mainProgress.notice('Deploying', { isMainEvent: true }); - await this.provider._awsRequest('Lambda', 'updateFunctionCode', params); + await this.provider.request('Lambda', 'updateFunctionCode', params); this.shouldEnsureFunctionState = true; log.notice(); log.notice.success( diff --git a/lib/plugins/aws/deploy-list.js b/lib/plugins/aws/deploy-list.js index d7fc5fa1c2..320dfd0e44 100644 --- a/lib/plugins/aws/deploy-list.js +++ b/lib/plugins/aws/deploy-list.js @@ -33,7 +33,7 @@ class AwsDeployList { let response; try { - response = await this.provider._awsRequest('S3', 'listObjectsV2', { + response = await this.provider.request('S3', 'listObjectsV2', { Bucket: this.bucketName, Prefix: `${prefix}/${service}/${stage}`, }); @@ -94,7 +94,7 @@ class AwsDeployList { FunctionName: funcName, }; - return this.provider._awsRequest('Lambda', 'getFunction', params); + return this.provider.request('Lambda', 'getFunction', params); }) ); @@ -102,7 +102,7 @@ class AwsDeployList { } async getFunctionPaginatedVersions(params, totalVersions) { - const response = await this.provider._awsRequest('Lambda', 'listVersionsByFunction', params); + const response = await this.provider.request('Lambda', 'listVersionsByFunction', params); const Versions = (totalVersions || []).concat(response.Versions); if (response.NextMarker) { diff --git a/lib/plugins/aws/deploy/lib/check-for-changes.js b/lib/plugins/aws/deploy/lib/check-for-changes.js index 6ededb6ee3..f2603dbff6 100644 --- a/lib/plugins/aws/deploy/lib/check-for-changes.js +++ b/lib/plugins/aws/deploy/lib/check-for-changes.js @@ -56,7 +56,7 @@ module.exports = { const result = await (async () => { try { - return await this.provider._awsRequest('S3', 'listObjectsV2', params); + return await this.provider.request('S3', 'listObjectsV2', params); } catch (error) { if (!error.message.includes('The specified bucket does not exist')) throw error; const stackName = this.provider.naming.getStackName(); @@ -94,7 +94,7 @@ module.exports = { const getFunctionResults = this.serverless.service.getAllFunctions().map((funName) => { const functionObj = this.serverless.service.getFunction(funName); return this.provider - ._awsRequest('Lambda', 'getFunction', { + .request('Lambda', 'getFunction', { FunctionName: functionObj.name, }) .then((res) => new Date(res.Configuration.LastModified)) @@ -124,7 +124,7 @@ module.exports = { return Promise.all( objects.map(async (obj) => { try { - const result = await this.provider._awsRequest('S3', 'headObject', { + const result = await this.provider.request('S3', 'headObject', { Bucket: this.bucketName, Key: obj.Key, }); @@ -304,7 +304,7 @@ module.exports = { const CLOUDWATCHLOG_LOG_GROUP_EVENT_PER_FUNCTION_LIMIT = 2; const response = await this.provider - ._awsRequest('CloudWatchLogs', 'describeSubscriptionFilters', { + .request('CloudWatchLogs', 'describeSubscriptionFilters', { logGroupName, }) .catch(() => ({ subscriptionFilters: [] })); @@ -379,7 +379,7 @@ module.exports = { return Promise.all( notMatchedInternalOldSubscriptionFilters.map((oldSubscriptionFilter) => - this.provider._awsRequest('CloudWatchLogs', 'deleteSubscriptionFilter', { + this.provider.request('CloudWatchLogs', 'deleteSubscriptionFilter', { logGroupName, filterName: oldSubscriptionFilter.filterName, }) @@ -397,7 +397,7 @@ module.exports = { async isInternalSubscriptionFilter(stackName, logicalResourceId, physicalResourceId) { try { - const { StackResourceDetail } = await this.provider._awsRequest( + const { StackResourceDetail } = await this.provider.request( 'CloudFormation', 'describeStackResource', { diff --git a/lib/plugins/aws/deploy/lib/cleanup-s3-bucket.js b/lib/plugins/aws/deploy/lib/cleanup-s3-bucket.js index d1f2f494e8..df5ed15999 100644 --- a/lib/plugins/aws/deploy/lib/cleanup-s3-bucket.js +++ b/lib/plugins/aws/deploy/lib/cleanup-s3-bucket.js @@ -7,7 +7,6 @@ const ServerlessError = require('../../../../serverless-error'); const { log } = require('@serverless/utils/log'); module.exports = { - async getObjectsToRemove() { const stacksToKeepCount = _.get( this.serverless, @@ -21,7 +20,7 @@ module.exports = { let response; try { - response = await this.provider._awsRequest('S3', 'listObjectsV2', { + response = await this.provider.request('S3', 'listObjectsV2', { Bucket: this.bucketName, Prefix: `${prefix}/${service}/${stage}`, }); @@ -42,7 +41,7 @@ module.exports = { async removeObjects(objectsToRemove) { if (!objectsToRemove || !objectsToRemove.length) return; - await this._s3Request('deleteObjects', { + await this.provider.request('S3', 'deleteObjects', { Bucket: this.bucketName, Delete: { Objects: objectsToRemove }, }); @@ -62,7 +61,7 @@ module.exports = { async cleanupArtifactsForEmptyChangeSet() { let response; try { - response = await this._s3Request('listObjectsV2', { + response = await this.provider.request('S3', 'listObjectsV2', { Bucket: this.bucketName, Prefix: this.serverless.service.package.artifactDirectoryName, }); diff --git a/lib/plugins/aws/deploy/lib/create-stack.js b/lib/plugins/aws/deploy/lib/create-stack.js index 33c8207c30..f8a2bcc6e2 100644 --- a/lib/plugins/aws/deploy/lib/create-stack.js +++ b/lib/plugins/aws/deploy/lib/create-stack.js @@ -18,7 +18,7 @@ module.exports = { const params = this.getCreateStackParams({ templateBody: this.serverless.service.provider.coreCloudFormationTemplate, }); - monitorCfData = await this._cloudFormationRequest('createStack', params); + monitorCfData = await this.provider.request('CloudFormation', 'createStack', params); } else { // Change-set based deployment const changeSetName = this.provider.naming.getStackChangeSetName(); @@ -31,7 +31,7 @@ module.exports = { // Create new change set log.info('Creating new change set'); - await this._cloudFormationRequest('createChangeSet', createChangeSetParams); + await this.provider.request('CloudFormation', 'createChangeSet', createChangeSetParams); // Wait for changeset to be created log.info('Waiting for new change set to be created'); @@ -41,7 +41,7 @@ module.exports = { if (isChangeSetWithoutChanges(changeSetDescription)) { // Cleanup changeset when it does not include any changes log.info('Created change set does not include any changes, removing it'); - await this._cloudFormationRequest('deleteChangeSet', { + await this.provider.request('CloudFormation', 'deleteChangeSet', { StackName: stackName, ChangeSetName: changeSetName, }); @@ -51,20 +51,12 @@ module.exports = { this.provider.didCreateService = true; log.info('Executing created change set'); - await this._cloudFormationRequest('executeChangeSet', executeChangeSetParams); + await this.provider.request('CloudFormation', 'executeChangeSet', executeChangeSetParams); monitorCfData = changeSetDescription; } await this.monitorStack('create', monitorCfData); }, - /** - * Helper method to route CloudFormation requests through v2 or v3 based on feature flag - * @private - */ - async _cloudFormationRequest(method, params) { - return this.provider._awsRequest('CloudFormation', method, params); - }, - async createStack() { const stackName = this.provider.naming.getStackName(); if (/^[^a-zA-Z].+|.*[^a-zA-Z0-9-].*/.test(stackName) || stackName.length > 128) { @@ -80,36 +72,38 @@ module.exports = { return BbPromise.bind(this) .then(() => - this._cloudFormationRequest('describeStacks', { StackName: stackName }).then((data) => { - const shouldCheckStackOutput = - // check stack output only if acceleration is requested - this.provider.isS3TransferAccelerationEnabled() && - // custom deployment bucket won't generate any output (no check) - !this.serverless.service.provider.deploymentBucket; - if (shouldCheckStackOutput) { - const isAlreadyAccelerated = data.Stacks[0].Outputs.some( - (output) => output.OutputKey === 'ServerlessDeploymentBucketAccelerated' - ); + this.provider + .request('CloudFormation', 'describeStacks', { StackName: stackName }) + .then((data) => { + const shouldCheckStackOutput = + // check stack output only if acceleration is requested + this.provider.isS3TransferAccelerationEnabled() && + // custom deployment bucket won't generate any output (no check) + !this.serverless.service.provider.deploymentBucket; + if (shouldCheckStackOutput) { + const isAlreadyAccelerated = data.Stacks[0].Outputs.some( + (output) => output.OutputKey === 'ServerlessDeploymentBucketAccelerated' + ); - if (!isAlreadyAccelerated) { - log.info('Not using S3 Transfer Acceleration (1st deploy)'); - this.provider.disableTransferAccelerationForCurrentDeploy(); + if (!isAlreadyAccelerated) { + log.info('Not using S3 Transfer Acceleration (1st deploy)'); + this.provider.disableTransferAccelerationForCurrentDeploy(); + } } - } - const stackStatus = data.Stacks[0].StackStatus; - if (inactiveStateNames.has(stackStatus)) { - const errorMessage = [ - 'Service cannot be deployed as the CloudFormation stack ', - `is in the '${stackStatus}' state. `, - 'This may signal either that stack is currently deployed by a different entity, ', - 'or that the previous deployment failed and was left in an abnormal state, ', - "in which case you can mitigate the issue by running 'sls remove' command", - ].join(''); - throw new ServerlessError(errorMessage, 'AWS_CLOUDFORMATION_INACTIVE_STACK'); - } - return BbPromise.resolve('alreadyCreated'); - }) + const stackStatus = data.Stacks[0].StackStatus; + if (inactiveStateNames.has(stackStatus)) { + const errorMessage = [ + 'Service cannot be deployed as the CloudFormation stack ', + `is in the '${stackStatus}' state. `, + 'This may signal either that stack is currently deployed by a different entity, ', + 'or that the previous deployment failed and was left in an abnormal state, ', + "in which case you can mitigate the issue by running 'sls remove' command", + ].join(''); + throw new ServerlessError(errorMessage, 'AWS_CLOUDFORMATION_INACTIVE_STACK'); + } + return BbPromise.resolve('alreadyCreated'); + }) ) .catch((e) => { if (e.message.indexOf('does not exist') > -1) { diff --git a/lib/plugins/aws/deploy/lib/ensure-valid-bucket-exists.js b/lib/plugins/aws/deploy/lib/ensure-valid-bucket-exists.js index 39aac99918..d41e0d1d0a 100644 --- a/lib/plugins/aws/deploy/lib/ensure-valid-bucket-exists.js +++ b/lib/plugins/aws/deploy/lib/ensure-valid-bucket-exists.js @@ -27,7 +27,7 @@ module.exports = { if (this.serverless.service.provider.deploymentBucket) { let result; try { - result = await this.provider._awsRequest('S3', 'getBucketLocation', { + result = await this.provider.request('S3', 'getBucketLocation', { Bucket: this.bucketName, }); } catch (err) { @@ -71,7 +71,7 @@ module.exports = { // It covers the case where someone was using custom deployment bucket // but removed that setting from the configuration mainProgress.notice('Ensuring that deployment bucket exists', { isMainEvent: true }); - const getTemplateResult = await this.provider._awsRequest('CloudFormation', 'getTemplate', { + const getTemplateResult = await this.provider.request('CloudFormation', 'getTemplate', { StackName: stackName, TemplateStage: 'Original', }); @@ -116,7 +116,7 @@ module.exports = { if (this.serverless.service.provider.deploymentMethod === 'direct') { const params = this.getUpdateStackParams({ templateBody }); - monitorCfData = await this.provider._awsRequest('CloudFormation', 'updateStack', params); + monitorCfData = await this.provider.request('CloudFormation', 'updateStack', params); } else { const createChangeSetParams = this.getCreateChangeSetParams({ changeSetType: 'UPDATE', @@ -126,14 +126,14 @@ module.exports = { const executeChangeSetParams = this.getExecuteChangeSetParams(); // Ensure that previous change set has been removed - await this.provider._awsRequest('CloudFormation', 'deleteChangeSet', { + await this.provider.request('CloudFormation', 'deleteChangeSet', { StackName: stackName, ChangeSetName: changeSetName, }); log.info('Creating new change set.'); // Create new change set - const changeSet = await this.provider._awsRequest( + const changeSet = await this.provider.request( 'CloudFormation', 'createChangeSet', createChangeSetParams @@ -147,7 +147,7 @@ module.exports = { // that needs to be created as a part of change set // If that would not be the case, that means we have a bug in the logic above - await this.provider._awsRequest('CloudFormation', 'executeChangeSet', executeChangeSetParams); + await this.provider.request('CloudFormation', 'executeChangeSet', executeChangeSetParams); monitorCfData = changeSet; } await this.monitorStack('update', monitorCfData); diff --git a/lib/plugins/aws/deploy/lib/upload-artifacts.js b/lib/plugins/aws/deploy/lib/upload-artifacts.js index ed6e002ecd..fab5a3ecf9 100644 --- a/lib/plugins/aws/deploy/lib/upload-artifacts.js +++ b/lib/plugins/aws/deploy/lib/upload-artifacts.js @@ -72,7 +72,7 @@ module.exports = { params = setS3UploadEncryptionOptions(params, deploymentBucketObject); } - return this.provider._awsRequest('S3', 'upload', params); + return this.provider.request('S3', 'upload', params); }, async uploadStateFile() { log.info('Uploading State file to S3'); @@ -102,7 +102,7 @@ module.exports = { params = setS3UploadEncryptionOptions(params, deploymentBucketObject); } - return this.provider._awsRequest('S3', 'upload', params); + return this.provider.request('S3', 'upload', params); }, async getFunctionArtifactFilePaths() { diff --git a/lib/plugins/aws/deploy/lib/validate-template.js b/lib/plugins/aws/deploy/lib/validate-template.js index 907bfa9f5d..405f4a9c49 100644 --- a/lib/plugins/aws/deploy/lib/validate-template.js +++ b/lib/plugins/aws/deploy/lib/validate-template.js @@ -4,14 +4,6 @@ const getS3EndpointForRegion = require('../../utils/get-s3-endpoint-for-region') const ServerlessError = require('../../../../serverless-error'); module.exports = { - /** - * Helper method to route CloudFormation requests through v2 or v3 based on feature flag - * @private - */ - async _cloudFormationRequest(method, params) { - return this.provider._awsRequest('CloudFormation', method, params); - }, - async validateTemplate() { const bucketName = this.bucketName; const artifactDirectoryName = this.serverless.service.package.artifactDirectoryName; @@ -21,7 +13,7 @@ module.exports = { TemplateURL: `https://${s3Endpoint}/${bucketName}/${artifactDirectoryName}/${compiledTemplateFileName}`, }; - return this._cloudFormationRequest('validateTemplate', params).catch((error) => { + return this.provider.request('CloudFormation', 'validateTemplate', params).catch((error) => { const errorMessage = ['The CloudFormation template is invalid:', ` ${error.message}`].join( '' ); diff --git a/lib/plugins/aws/info/get-api-key-values.js b/lib/plugins/aws/info/get-api-key-values.js index c086723887..6b7ac5daf6 100644 --- a/lib/plugins/aws/info/get-api-key-values.js +++ b/lib/plugins/aws/info/get-api-key-values.js @@ -32,7 +32,7 @@ module.exports = { if (apiKeyNames.length) { return this.provider - ._awsRequest('CloudFormation', 'describeStackResources', { + .request('CloudFormation', 'describeStackResources', { StackName: this.provider.naming.getStackName(), }) .then((resources) => { @@ -42,7 +42,7 @@ module.exports = { .value(); return Promise.all( apiKeys.map((apiKey) => - this.provider._awsRequest('APIGateway', 'getApiKey', { + this.provider.request('APIGateway', 'getApiKey', { apiKey, includeValue: true, }) diff --git a/lib/plugins/aws/info/get-resource-count.js b/lib/plugins/aws/info/get-resource-count.js index 771f8f56c3..994469a4aa 100644 --- a/lib/plugins/aws/info/get-resource-count.js +++ b/lib/plugins/aws/info/get-resource-count.js @@ -3,13 +3,12 @@ const BbPromise = require('bluebird'); module.exports = { - async getResourceCount(nextToken, resourceCount = 0) { const params = { StackName: this.provider.naming.getStackName(), NextToken: nextToken, }; - return this.provider._awsRequest('CloudFormation', 'listStackResources', params).then((result) => { + return this.provider.request('CloudFormation', 'listStackResources', params).then((result) => { if (Object.keys(result).length) { this.gatheredData.info.resourceCount = resourceCount + result.StackResourceSummaries.length; if (result.NextToken) { diff --git a/lib/plugins/aws/info/get-stack-info.js b/lib/plugins/aws/info/get-stack-info.js index c9d34c05be..ba411feeda 100644 --- a/lib/plugins/aws/info/get-stack-info.js +++ b/lib/plugins/aws/info/get-stack-info.js @@ -25,7 +25,7 @@ module.exports = { const stackData = {}; const sdkRequests = [ this.provider - ._awsRequest('CloudFormation', 'describeStacks', { StackName: stackName }) + .request('CloudFormation', 'describeStacks', { StackName: stackName }) .then((result) => { if (result) stackData.outputs = result.Stacks[0].Outputs; }), @@ -39,7 +39,7 @@ module.exports = { : BbPromise.resolve(httpApiId) ) .then((id) => { - return this.provider._awsRequest('ApiGatewayV2', 'getApi', { ApiId: id }); + return this.provider.request('ApiGatewayV2', 'getApi', { ApiId: id }); }) .then( (result) => { diff --git a/lib/plugins/aws/invoke-local/index.js b/lib/plugins/aws/invoke-local/index.js index 3cfd0f1e2c..a21dfadb9c 100644 --- a/lib/plugins/aws/invoke-local/index.js +++ b/lib/plugins/aws/invoke-local/index.js @@ -55,7 +55,6 @@ class AwsInvokeLocal { }; } - getRuntime() { return this.provider.getRuntime(this.options.functionObj.runtime); } @@ -377,7 +376,7 @@ class AwsInvokeLocal { layerProgress.notice(`Downloading layer ${layer}`); await ensureDir(path.join(layerContentsCachePath)); - const layerInfo = await this.provider._awsRequest('Lambda', 'getLayerVersion', { + const layerInfo = await this.provider.request('Lambda', 'getLayerVersion', { LayerName: layerArn, VersionNumber: layerVersion, }); diff --git a/lib/plugins/aws/invoke.js b/lib/plugins/aws/invoke.js index b6c279b86c..c82195702f 100644 --- a/lib/plugins/aws/invoke.js +++ b/lib/plugins/aws/invoke.js @@ -23,14 +23,6 @@ class AwsInvoke { }; } - /** - * Helper method to route Lambda requests through v2 or v3 based on feature flag - * @private - */ - async _lambdaRequest(method, params) { - return this.provider._awsRequest('Lambda', method, params); - } - async validateFile(key) { const absolutePath = path.resolve(this.serverless.serviceDir, this.options[key]); try { @@ -105,7 +97,7 @@ class AwsInvoke { params.Qualifier = this.options.qualifier; } - return this._lambdaRequest('invoke', params); + return this.provider.request('Lambda', 'invoke', params); } log(invocationReply) { diff --git a/lib/plugins/aws/lib/check-if-bucket-exists.js b/lib/plugins/aws/lib/check-if-bucket-exists.js index 439ba9d31f..6fc07164b1 100644 --- a/lib/plugins/aws/lib/check-if-bucket-exists.js +++ b/lib/plugins/aws/lib/check-if-bucket-exists.js @@ -3,17 +3,9 @@ const ServerlessError = require('../../../serverless-error'); module.exports = { - /** - * Helper method to route S3 requests through v2 or v3 based on feature flag - * @private - */ - async _s3Request(method, params) { - return this.provider._awsRequest('S3', method, params); - }, - async checkIfBucketExists(bucketName) { try { - await this._s3Request('headBucket', { + await this.provider.request('S3', 'headBucket', { Bucket: bucketName, }); return true; diff --git a/lib/plugins/aws/lib/check-if-ecr-repository-exists.js b/lib/plugins/aws/lib/check-if-ecr-repository-exists.js index 90032e8d63..0253a87984 100644 --- a/lib/plugins/aws/lib/check-if-ecr-repository-exists.js +++ b/lib/plugins/aws/lib/check-if-ecr-repository-exists.js @@ -7,7 +7,7 @@ module.exports = { const registryId = await this.provider.getAccountId(); const repositoryName = this.provider.naming.getEcrRepositoryName(); try { - await this.provider._awsRequest('ECR', 'describeRepositories', { + await this.provider.request('ECR', 'describeRepositories', { repositoryNames: [repositoryName], registryId, }); diff --git a/lib/plugins/aws/lib/monitor-stack.js b/lib/plugins/aws/lib/monitor-stack.js index 5163c1aef3..336e7fd946 100644 --- a/lib/plugins/aws/lib/monitor-stack.js +++ b/lib/plugins/aws/lib/monitor-stack.js @@ -21,13 +21,6 @@ const resourceTypeToErrorCodePostfix = (resourceType) => { }; module.exports = { - /** - * Helper method to route CloudFormation requests through v2 or v3 based on feature flag - * @private - */ - async _cloudFormationRequest(method, params) { - return this.provider._awsRequest('CloudFormation', method, params); - }, async checkStackProgress( action, cfData, @@ -43,122 +36,129 @@ module.exports = { ) { return wait(getMonitoringFrequency(options.frequency)) .then(() => - this._cloudFormationRequest('describeStackEvents', { StackName: cfData.StackId }).then( - ({ StackEvents: stackEvents }) => { - if (!stackEvents.length) return; + this.provider + .request('CloudFormation', 'describeStackEvents', { StackName: cfData.StackId }) + .then( + ({ StackEvents: stackEvents }) => { + if (!stackEvents.length) return; - // Resolve only events applicable to current deployment - stackEvents.some((event, index) => { - if (firstEventId) { - if (event.EventId !== firstEventId) return false; - } else { - if (event.ResourceType !== 'AWS::CloudFormation::Stack') return false; - if (event.ResourceStatus !== `${action.toUpperCase()}_IN_PROGRESS`) return false; - firstEventId = event.EventId; - } - stackEvents = stackEvents.slice(0, index + 1); - return true; - }); - stackEvents.reverse(); + // Resolve only events applicable to current deployment + stackEvents.some((event, index) => { + if (firstEventId) { + if (event.EventId !== firstEventId) return false; + } else { + if (event.ResourceType !== 'AWS::CloudFormation::Stack') return false; + if (event.ResourceStatus !== `${action.toUpperCase()}_IN_PROGRESS`) return false; + firstEventId = event.EventId; + } + stackEvents = stackEvents.slice(0, index + 1); + return true; + }); + stackEvents.reverse(); - // Loop through stack events - stackEvents.forEach((event) => { - if (loggedEventIds.has(event.EventId)) return; - const eventStatus = event.ResourceStatus || null; - // Keep track of stack status - if ( - event.ResourceType === 'AWS::CloudFormation::Stack' && - event.StackName === event.LogicalResourceId - ) { - stackStatus = eventStatus; - } - // Keep track of first failed event - if ( - eventStatus && - (eventStatus.endsWith('FAILED') || eventStatus === 'UPDATE_ROLLBACK_IN_PROGRESS') && - stackLatestError === null - ) { - stackLatestError = event; - } - // Log stack events - log.info( - style.aside(` ${eventStatus} - ${event.ResourceType} - ${event.LogicalResourceId}`) - ); + // Loop through stack events + stackEvents.forEach((event) => { + if (loggedEventIds.has(event.EventId)) return; + const eventStatus = event.ResourceStatus || null; + // Keep track of stack status + if ( + event.ResourceType === 'AWS::CloudFormation::Stack' && + event.StackName === event.LogicalResourceId + ) { + stackStatus = eventStatus; + } + // Keep track of first failed event + if ( + eventStatus && + (eventStatus.endsWith('FAILED') || + eventStatus === 'UPDATE_ROLLBACK_IN_PROGRESS') && + stackLatestError === null + ) { + stackLatestError = event; + } + // Log stack events + log.info( + style.aside( + ` ${eventStatus} - ${event.ResourceType} - ${event.LogicalResourceId}` + ) + ); + + if ( + event.ResourceType !== 'AWS::CloudFormation::Stack' && + eventStatus && + eventStatus.endsWith('COMPLETE') + ) { + completedResources.add(event.LogicalResourceId); + } + if (action !== 'delete' && cfData.Changes) { + const progressMessagePrefix = (() => { + if (action === 'create') return 'Creating'; + if (action === 'update') return 'Updating'; + throw new Error(`Unrecgonized action: ${action}`); + })(); + mainProgress.notice( + `${progressMessagePrefix} CloudFormation stack (${completedResources.size}/${cfData.Changes.length})` + ); + } + + // Prepare for next monitoring action + loggedEventIds.add(event.EventId); + }); + // Handle stack create/update/delete failures if ( - event.ResourceType !== 'AWS::CloudFormation::Stack' && - eventStatus && - eventStatus.endsWith('COMPLETE') + stackLatestError && + (!this.options.verbose || + (stackStatus && + (stackStatus.endsWith('ROLLBACK_COMPLETE') || + ['DELETE_FAILED', 'DELETE_COMPLETE'].includes(stackStatus)))) ) { - completedResources.add(event.LogicalResourceId); - } + const decoratedErrorMessage = `${stackLatestError.ResourceStatus}: ${ + stackLatestError.LogicalResourceId + } ${style.aside(`(${stackLatestError.ResourceType})`)}\n${ + stackLatestError.ResourceStatusReason + }\n\n${style.aside(`View the full error: ${style.link(stackUrl)}`)}`; - if (action !== 'delete' && cfData.Changes) { - const progressMessagePrefix = (() => { - if (action === 'create') return 'Creating'; - if (action === 'update') return 'Updating'; - throw new Error(`Unrecgonized action: ${action}`); + let errorMessage = 'An error occurred: '; + errorMessage += `${stackLatestError.LogicalResourceId} - `; + errorMessage += `${ + stackLatestError.ResourceStatusReason || stackLatestError.ResourceStatus + }.`; + const errorCode = (() => { + if (stackLatestError.ResourceStatusReason) { + if ( + stackLatestError.ResourceStatusReason.startsWith( + 'Properties validation failed' + ) + ) { + return `AWS_CLOUD_FORMATION_${action.toUpperCase()}_STACK_INTERNAL_VALIDATION_ERROR`; + } + if ( + stackLatestError.ResourceStatusReason.includes('is not authorized to perform') + ) { + return `AWS_CLOUD_FORMATION_${action.toUpperCase()}_STACK_INTERNAL_INSUFFICIENT_PERMISSIONS`; + } + } + return ( + `AWS_CLOUD_FORMATION_${action.toUpperCase()}_STACK_INTERNAL` + + `${resourceTypeToErrorCodePostfix(stackLatestError.ResourceType)}_${ + stackLatestError.ResourceStatus + }` + ); })(); - mainProgress.notice( - `${progressMessagePrefix} CloudFormation stack (${completedResources.size}/${cfData.Changes.length})` - ); + throw new ServerlessError(errorMessage, errorCode, { + decoratedMessage: decoratedErrorMessage, + }); } - - // Prepare for next monitoring action - loggedEventIds.add(event.EventId); - }); - // Handle stack create/update/delete failures - if ( - stackLatestError && - (!this.options.verbose || - (stackStatus && - (stackStatus.endsWith('ROLLBACK_COMPLETE') || - ['DELETE_FAILED', 'DELETE_COMPLETE'].includes(stackStatus)))) - ) { - const decoratedErrorMessage = `${stackLatestError.ResourceStatus}: ${ - stackLatestError.LogicalResourceId - } ${style.aside(`(${stackLatestError.ResourceType})`)}\n${ - stackLatestError.ResourceStatusReason - }\n\n${style.aside(`View the full error: ${style.link(stackUrl)}`)}`; - - let errorMessage = 'An error occurred: '; - errorMessage += `${stackLatestError.LogicalResourceId} - `; - errorMessage += `${ - stackLatestError.ResourceStatusReason || stackLatestError.ResourceStatus - }.`; - const errorCode = (() => { - if (stackLatestError.ResourceStatusReason) { - if ( - stackLatestError.ResourceStatusReason.startsWith('Properties validation failed') - ) { - return `AWS_CLOUD_FORMATION_${action.toUpperCase()}_STACK_INTERNAL_VALIDATION_ERROR`; - } - if ( - stackLatestError.ResourceStatusReason.includes('is not authorized to perform') - ) { - return `AWS_CLOUD_FORMATION_${action.toUpperCase()}_STACK_INTERNAL_INSUFFICIENT_PERMISSIONS`; - } - } - return ( - `AWS_CLOUD_FORMATION_${action.toUpperCase()}_STACK_INTERNAL` + - `${resourceTypeToErrorCodePostfix(stackLatestError.ResourceType)}_${ - stackLatestError.ResourceStatus - }` - ); - })(); - throw new ServerlessError(errorMessage, errorCode, { - decoratedMessage: decoratedErrorMessage, - }); - } - }, - (e) => { - if (action === 'delete' && e.message.includes('does not exist')) { - stackStatus = 'DELETE_COMPLETE'; - return; + }, + (e) => { + if (action === 'delete' && e.message.includes('does not exist')) { + stackStatus = 'DELETE_COMPLETE'; + return; + } + throw e; } - throw e; - } - ) + ) ) .then(() => { if (validStatuses.has(stackStatus)) return stackStatus; diff --git a/lib/plugins/aws/lib/update-stack.js b/lib/plugins/aws/lib/update-stack.js index ca4797abe9..c5fcf333e0 100644 --- a/lib/plugins/aws/lib/update-stack.js +++ b/lib/plugins/aws/lib/update-stack.js @@ -23,7 +23,7 @@ module.exports = { templateUrl, }); - monitorCfData = await this.provider._awsRequest('CloudFormation', 'createStack', params); + monitorCfData = await this.provider.request('CloudFormation', 'createStack', params); } else { const changeSetName = this.provider.naming.getStackChangeSetName(); @@ -37,7 +37,7 @@ module.exports = { // Create new change set this.provider.didCreateService = true; log.info('Creating new change set'); - await this.provider._awsRequest('CloudFormation', 'createChangeSet', createChangeSetParams); + await this.provider.request('CloudFormation', 'createChangeSet', createChangeSetParams); // Wait for changeset to be created log.info('Waiting for new change set to be created'); @@ -47,7 +47,7 @@ module.exports = { if (isChangeSetWithoutChanges(changeSetDescription)) { // Cleanup changeset when it does not include any changes log.info('Created change set does not include any changes, removing it'); - await this.provider._awsRequest('CloudFormation', 'deleteChangeSet', { + await this.provider.request('CloudFormation', 'deleteChangeSet', { StackName: stackName, ChangeSetName: changeSetName, }); @@ -56,7 +56,7 @@ module.exports = { } log.info('Executing created change set'); - await this.provider._awsRequest('CloudFormation', 'executeChangeSet', executeChangeSetParams); + await this.provider.request('CloudFormation', 'executeChangeSet', executeChangeSetParams); monitorCfData = changeSetDescription; } await this.monitorStack('create', monitorCfData); @@ -75,7 +75,7 @@ module.exports = { const params = this.getUpdateStackParams({ templateUrl }); try { - monitorCfData = await this.provider._awsRequest('CloudFormation', 'updateStack', params); + monitorCfData = await this.provider.request('CloudFormation', 'updateStack', params); } catch (e) { if (e.message.includes(NO_UPDATE_MESSAGE)) { return false; @@ -93,14 +93,14 @@ module.exports = { const executeChangeSetParams = this.getExecuteChangeSetParams(); // Ensure that previous change set has been removed - await this._cloudFormationRequest('deleteChangeSet', { + await this.provider.request('CloudFormation', 'deleteChangeSet', { StackName: stackName, ChangeSetName: changeSetName, }); // Create new change set log.info('Creating new change set'); - await this.provider._awsRequest('CloudFormation', 'createChangeSet', createChangeSetParams); + await this.provider.request('CloudFormation', 'createChangeSet', createChangeSetParams); // Wait for changeset to be created log.info('Waiting for new change set to be created'); @@ -110,7 +110,7 @@ module.exports = { if (isChangeSetWithoutChanges(changeSetDescription)) { // Cleanup changeset when it does not include any changes log.info('Created change set does not include any changes, removing it'); - await this.provider._awsRequest('CloudFormation', 'deleteChangeSet', { + await this.provider.request('CloudFormation', 'deleteChangeSet', { StackName: stackName, ChangeSetName: changeSetName, }); @@ -119,7 +119,7 @@ module.exports = { } log.info('Executing created change set'); - await this.provider._awsRequest('CloudFormation', 'executeChangeSet', executeChangeSetParams); + await this.provider.request('CloudFormation', 'executeChangeSet', executeChangeSetParams); monitorCfData = changeSetDescription; } @@ -139,7 +139,7 @@ module.exports = { const stackPolicyBody = JSON.stringify({ Statement: this.serverless.service.provider.stackPolicy, }); - await this.provider._awsRequest('CloudFormation', 'setStackPolicy', { + await this.provider.request('CloudFormation', 'setStackPolicy', { StackName: stackName, StackPolicyBody: stackPolicyBody, }); diff --git a/lib/plugins/aws/lib/upload-zip-file.js b/lib/plugins/aws/lib/upload-zip-file.js index 7c56f51451..2445ebc57c 100644 --- a/lib/plugins/aws/lib/upload-zip-file.js +++ b/lib/plugins/aws/lib/upload-zip-file.js @@ -7,7 +7,6 @@ const log = require('@serverless/utils/log').log.get('deploy:upload'); const setS3UploadEncryptionOptions = require('../../../aws/set-s3-upload-encryption-options'); module.exports = { - async uploadZipFile({ filename, s3KeyDirname, basename }) { if (!basename) basename = filename.split(path.sep).pop(); @@ -39,7 +38,7 @@ module.exports = { params = setS3UploadEncryptionOptions(params, deploymentBucketObject); } - const response = await this.provider._awsRequest('S3', 'upload', params); + const response = await this.provider.request('S3', 'upload', params); // Interestingly, if request handling was queued, and stream errored (before being consumed by // AWS SDK) then SDK call succeeds without actually uploading a file to S3 bucket. // Below line ensures that eventual stream error is communicated diff --git a/lib/plugins/aws/lib/wait-for-change-set-creation.js b/lib/plugins/aws/lib/wait-for-change-set-creation.js index 979eadec58..499fe40c2c 100644 --- a/lib/plugins/aws/lib/wait-for-change-set-creation.js +++ b/lib/plugins/aws/lib/wait-for-change-set-creation.js @@ -7,7 +7,6 @@ const { log } = require('@serverless/utils/log'); const getMonitoringFrequency = require('../utils/get-monitoring-frequency'); module.exports = { - async waitForChangeSetCreation(changeSetName, stackName) { const params = { ChangeSetName: changeSetName, @@ -15,7 +14,11 @@ module.exports = { }; const callWithRetry = async () => { - const changeSetDescription = await this.provider._awsRequest('CloudFormation', 'describeChangeSet', params); + const changeSetDescription = await this.provider.request( + 'CloudFormation', + 'describeChangeSet', + params + ); if ( changeSetDescription.Status === 'CREATE_COMPLETE' || isChangeSetWithoutChanges(changeSetDescription) diff --git a/lib/plugins/aws/logs.js b/lib/plugins/aws/logs.js index 70c084ef43..bd56aacf56 100644 --- a/lib/plugins/aws/logs.js +++ b/lib/plugins/aws/logs.js @@ -28,14 +28,6 @@ class AwsLogs { }; } - /** - * Helper method to route CloudWatchLogs requests through v2 or v3 based on feature flag - * @private - */ - async _cloudWatchLogsRequest(method, params) { - return this.provider._awsRequest('CloudWatchLogs', method, params); - } - extendedValidate() { this.validate(); @@ -54,7 +46,7 @@ class AwsLogs { orderBy: 'LastEventTime', }; - const reply = await this._cloudWatchLogsRequest('describeLogStreams', params); + const reply = await this.provider.request('CloudWatchLogs', 'describeLogStreams', params); if (!reply || reply.logStreams.length === 0) { throw new ServerlessError('No existing streams for the function', 'NO_EXISTING_LOG_STREAMS'); } @@ -100,7 +92,7 @@ class AwsLogs { } } - const results = await this._cloudWatchLogsRequest('filterLogEvents', params); + const results = await this.provider.request('CloudWatchLogs', 'filterLogEvents', params); if (results.events) { results.events.forEach((e) => { if (e.message.includes('SERVERLESS_ENTERPRISE') || e.message.startsWith('END')) { diff --git a/lib/plugins/aws/metrics.js b/lib/plugins/aws/metrics.js index 7ed0fd234a..e0ec7a53b1 100644 --- a/lib/plugins/aws/metrics.js +++ b/lib/plugins/aws/metrics.js @@ -27,14 +27,6 @@ class AwsMetrics { }; } - /** - * Helper method to route CloudWatch requests through v2 or v3 based on feature flag - * @private - */ - async _cloudWatchRequest(method, params) { - return this.provider._awsRequest('CloudWatch', method, params); - } - extendedValidate() { this.validate(); @@ -93,7 +85,8 @@ class AwsMetrics { Unit: 'Milliseconds', }); - const getMetrics = (params) => this._cloudWatchRequest('getMetricStatistics', params); + const getMetrics = (params) => + this.provider.request('CloudWatch', 'getMetricStatistics', params); return BbPromise.all([ getMetrics(invocationsParams), diff --git a/lib/plugins/aws/package/compile/events/api-gateway/lib/hack/disassociate-usage-plan.js b/lib/plugins/aws/package/compile/events/api-gateway/lib/hack/disassociate-usage-plan.js index 5d50872e2e..ce8a250857 100644 --- a/lib/plugins/aws/package/compile/events/api-gateway/lib/hack/disassociate-usage-plan.js +++ b/lib/plugins/aws/package/compile/events/api-gateway/lib/hack/disassociate-usage-plan.js @@ -12,11 +12,11 @@ module.exports = { log.info('Removing usage plan association'); const stackName = `${this.provider.naming.getStackName()}`; return BbPromise.all([ - this.provider._awsRequest('CloudFormation', 'describeStackResource', { + this.provider.request('CloudFormation', 'describeStackResource', { StackName: stackName, LogicalResourceId: this.provider.naming.getRestApiLogicalId(), }), - this.provider._awsRequest('APIGateway', 'getUsagePlans', {}), + this.provider.request('APIGateway', 'getUsagePlans', {}), ]) .then((data) => data[1].items.filter((item) => @@ -30,7 +30,7 @@ module.exports = { items .map((item) => item.apiStages.map((apiStage) => - this.provider._awsRequest('APIGateway', 'updateUsagePlan', { + this.provider.request('APIGateway', 'updateUsagePlan', { usagePlanId: item.id, patchOperations: [ { diff --git a/lib/plugins/aws/package/compile/events/api-gateway/lib/hack/update-stage.js b/lib/plugins/aws/package/compile/events/api-gateway/lib/hack/update-stage.js index bbfc726a5d..6221788f47 100644 --- a/lib/plugins/aws/package/compile/events/api-gateway/lib/hack/update-stage.js +++ b/lib/plugins/aws/package/compile/events/api-gateway/lib/hack/update-stage.js @@ -153,7 +153,7 @@ async function resolveRestApiId() { : this.provider.naming.getApiGatewayName(); const resolveFromAws = (position) => this.provider - ._awsRequest('APIGateway', 'getRestApis', { position, limit: 500 }) + .request('APIGateway', 'getRestApis', { position, limit: 500 }) .then((result) => { const restApi = result.items.find((api) => api.name === apiName); if (restApi) return restApi.id; @@ -170,7 +170,7 @@ async function resolveStage() { const restApiId = this.apiGatewayRestApiId; return this.provider - ._awsRequest('APIGateway', 'getStage', { + .request('APIGateway', 'getStage', { restApiId, stageName: this.provider.getApiGatewayStage(), }) @@ -187,7 +187,7 @@ async function resolveDeploymentId() { const restApiId = this.apiGatewayRestApiId; return this.provider - ._awsRequest('APIGateway', 'getDeployments', { + .request('APIGateway', 'getDeployments', { restApiId, limit: 500, }) @@ -212,7 +212,7 @@ async function ensureStage() { const restApiId = this.apiGatewayRestApiId; const deploymentId = this.apiGatewayDeploymentId; - return this.provider._awsRequest('APIGateway', 'createStage', { + return this.provider.request('APIGateway', 'createStage', { deploymentId, restApiId, stageName: this.provider.getApiGatewayStage(), @@ -348,14 +348,14 @@ function handleTags() { async function addTags() { const requests = this.apiGatewayTagResourceParams.map((tagResourceParam) => - this.provider._awsRequest('APIGateway', 'tagResource', tagResourceParam) + this.provider.request('APIGateway', 'tagResource', tagResourceParam) ); return BbPromise.all(requests); } async function removeTags() { const requests = this.apiGatewayUntagResourceParams.map((untagResourceParam) => - this.provider._awsRequest('APIGateway', 'untagResource', untagResourceParam) + this.provider.request('APIGateway', 'untagResource', untagResourceParam) ); return BbPromise.all(requests); } @@ -365,7 +365,7 @@ function applyUpdates() { const patchOperations = this.apiGatewayStagePatchOperations; if (patchOperations.length) { - return this.provider._awsRequest('APIGateway', 'updateStage', { + return this.provider.request('APIGateway', 'updateStage', { restApiId, stageName: this.provider.getApiGatewayStage(), patchOperations, @@ -392,7 +392,7 @@ async function removeAccessLoggingLogGroup() { // log group name issues when logs are enabled again if (!accessLogging) { return this.provider - ._awsRequest('CloudWatchLogs', 'deleteLogGroup', { + .request('CloudWatchLogs', 'deleteLogGroup', { logGroupName, }) .catch(() => { diff --git a/lib/plugins/aws/package/compile/layers.js b/lib/plugins/aws/package/compile/layers.js index 848ca482e3..21053b5cef 100644 --- a/lib/plugins/aws/package/compile/layers.js +++ b/lib/plugins/aws/package/compile/layers.js @@ -23,7 +23,6 @@ class AwsCompileLayers { }; } - async compileLayer(layerName) { const newLayer = this.cfLambdaLayerTemplate(); const layerObject = this.serverless.service.getLayer(layerName); @@ -127,7 +126,7 @@ class AwsCompileLayers { const layerHashOutputLogicalId = this.provider.naming.getLambdaLayerHashOutputLogicalId(layerName); - return this.provider._awsRequest('CloudFormation', 'describeStacks', { StackName: stackName }).then( + return this.provider.request('CloudFormation', 'describeStacks', { StackName: stackName }).then( (data) => { const lastHash = data.Stacks[0].Outputs.find( (output) => output.OutputKey === layerHashOutputLogicalId diff --git a/lib/plugins/aws/provider.js b/lib/plugins/aws/provider.js index 64566c1640..9d457a59c8 100644 --- a/lib/plugins/aws/provider.js +++ b/lib/plugins/aws/provider.js @@ -1710,7 +1710,23 @@ class AwsProvider { } /** - * Execute an AWS request by calling the AWS SDK + * Execute an AWS request - routes between v2 and v3 based on feature flag + * @param {string} service - Service name + * @param {string} method - Method name + * @param {Object} params - Parameters + * @param {Object} options - Options + * @returns {Promise} AWS API response + */ + async request(service, method, params, options) { + // Use v3 if feature flag is enabled, otherwise fallback to v2 + if (this._v3Enabled) { + return this.requestV3(service, method, params, options); + } + return this.requestV2(service, method, params, options); + } + + /** + * Execute an AWS request by calling the AWS SDK v2 * @param {string} service - Service name * @param {string} method - Method name * @param {Object} params - Parameters @@ -1718,7 +1734,7 @@ class AwsProvider { * @prop [options.useCache] - Utilize cache to retrieve results * @prop [options.region] - Specify when to request to different region */ - async request(service, method, params, options) { + async requestV2(service, method, params, options) { // TODO: Determine calling module and log that const requestOptions = _.isObject(options) ? options : {}; const shouldCache = _.get(requestOptions, 'useCache', false); @@ -1766,22 +1782,6 @@ class AwsProvider { } } - /** - * Helper method to route AWS requests through v2 or v3 based on feature flag - * @param {string} service - Service name - * @param {string} method - Method name - * @param {Object} params - Parameters - * @param {Object} options - Options - * @returns {Promise} AWS API response - */ - async _awsRequest(service, method, params, options) { - // Use v3 if feature flag is enabled, otherwise fallback to v2 - if (this._v3Enabled) { - return this.requestV3(service, method, params, options); - } - return this.request(service, method, params, options); - } - /** * Get AWS SDK v3 client for direct use * @param {string} service - AWS service name @@ -1968,7 +1968,7 @@ class AwsProvider { if (this.serverless.service.provider.deploymentBucket) { return BbPromise.resolve(this.serverless.service.provider.deploymentBucket); } - return this._awsRequest('CloudFormation', 'describeStackResource', { + return this.request('CloudFormation', 'describeStackResource', { StackName: this.naming.getStackName(), LogicalResourceId: this.naming.getDeploymentBucketLogicalId(), }).then((result) => result.StackResourceDetail.PhysicalResourceId); @@ -2183,7 +2183,7 @@ class AwsProvider { if (!resources) resources = []; if (next) params.NextToken = next; - return this._awsRequest('CloudFormation', 'listStackResources', params).then((res) => { + return this.request('CloudFormation', 'listStackResources', params).then((res) => { const allResources = resources.concat(res.StackResourceSummaries); if (!res.NextToken) { return allResources; @@ -2223,7 +2223,7 @@ Object.defineProperties( memoizeeMethods({ getAccountInfo: d( async function () { - const result = await this._awsRequest('STS', 'getCallerIdentity', {}); + 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 @@ -2259,7 +2259,7 @@ Object.defineProperties( dockerLoginToEcr: d( async function () { const registryId = await this.getAccountId(); - const result = await this._awsRequest('ECR', 'getAuthorizationToken', { + const result = await this.request('ECR', 'getAuthorizationToken', { registryIds: [registryId], }); const { authorizationToken, proxyEndpoint } = result.authorizationData[0]; @@ -2297,7 +2297,7 @@ Object.defineProperties( const repositoryName = this.naming.getEcrRepositoryName(); let repositoryUri; try { - const result = await this._awsRequest('ECR', 'describeRepositories', { + const result = await this.request('ECR', 'describeRepositories', { repositoryNames: [repositoryName], registryId, }); @@ -2306,7 +2306,7 @@ Object.defineProperties( if (!(err.providerError && err.providerError.code === 'RepositoryNotFoundException')) { throw err; } - const result = await this._awsRequest('ECR', 'createRepository', { + const result = await this.request('ECR', 'createRepository', { repositoryName, imageScanningConfiguration: { scanOnPush }, }); @@ -2453,7 +2453,7 @@ Object.defineProperties( 'LAMBDA_ECR_REGION_MISMATCH_ERROR' ); } - const describeImagesResponse = await this._awsRequest('ECR', 'describeImages', { + const describeImagesResponse = await this.request('ECR', 'describeImages', { imageIds: [ { imageTag, diff --git a/lib/plugins/aws/remove/lib/bucket.js b/lib/plugins/aws/remove/lib/bucket.js index fc4236f7cc..e6d9a457c4 100644 --- a/lib/plugins/aws/remove/lib/bucket.js +++ b/lib/plugins/aws/remove/lib/bucket.js @@ -4,7 +4,6 @@ const { log } = require('@serverless/utils/log'); const ServerlessError = require('../../../../serverless-error'); module.exports = { - async setServerlessDeploymentBucketName() { try { const bucketName = await this.provider.getServerlessDeploymentBucketName(); @@ -28,7 +27,7 @@ module.exports = { let result; try { - result = await this.provider._awsRequest('S3', 'listObjectsV2', { + result = await this.provider.request('S3', 'listObjectsV2', { Bucket: this.bucketName, Prefix: `${this.provider.getDeploymentPrefix()}/${serviceStage}`, }); @@ -56,7 +55,7 @@ module.exports = { const serviceStage = `${this.serverless.service.service}/${this.provider.getStage()}`; - const result = await this.provider._awsRequest('S3', 'listObjectVersions', { + const result = await this.provider.request('S3', 'listObjectVersions', { Bucket: this.bucketName, Prefix: `${this.provider.getDeploymentPrefix()}/${serviceStage}`, }); @@ -91,7 +90,7 @@ module.exports = { async deleteObjects() { if (this.objectsInBucket.length) { - const data = await this.provider._awsRequest('S3', 'deleteObjects', { + const data = await this.provider.request('S3', 'deleteObjects', { Bucket: this.bucketName, Delete: { Objects: this.objectsInBucket, diff --git a/lib/plugins/aws/remove/lib/ecr.js b/lib/plugins/aws/remove/lib/ecr.js index 1f9feb1add..ff44da6381 100644 --- a/lib/plugins/aws/remove/lib/ecr.js +++ b/lib/plugins/aws/remove/lib/ecr.js @@ -10,6 +10,6 @@ module.exports = { force: true, // To ensure removal of non-empty repository }; - await this.provider._awsRequest('ECR', 'deleteRepository', params); + await this.provider.request('ECR', 'deleteRepository', params); }, }; diff --git a/lib/plugins/aws/remove/lib/stack.js b/lib/plugins/aws/remove/lib/stack.js index cd19cd7fe7..429801bd45 100644 --- a/lib/plugins/aws/remove/lib/stack.js +++ b/lib/plugins/aws/remove/lib/stack.js @@ -1,14 +1,6 @@ 'use strict'; module.exports = { - /** - * Helper method to route CloudFormation requests through v2 or v3 based on feature flag - * @private - */ - async _cloudFormationRequest(method, params) { - return this.provider._awsRequest('CloudFormation', method, params); - }, - async remove() { const stackName = this.provider.naming.getStackName(); const params = { @@ -24,7 +16,7 @@ module.exports = { StackId: stackName, }; - return this._cloudFormationRequest('deleteStack', params).then(() => cfData); + return this.provider.request('CloudFormation', 'deleteStack', params).then(() => cfData); }, async removeStack() { diff --git a/lib/plugins/aws/rollback-function.js b/lib/plugins/aws/rollback-function.js index 20e5a9c1aa..8040128a59 100644 --- a/lib/plugins/aws/rollback-function.js +++ b/lib/plugins/aws/rollback-function.js @@ -26,14 +26,6 @@ class AwsRollbackFunction { }; } - /** - * Helper method to route Lambda requests through v2 or v3 based on feature flag - * @private - */ - async _lambdaRequest(method, params) { - return this.provider._awsRequest('Lambda', method, params); - } - async getFunctionToBeRestored() { const funcName = this.options.function; let funcVersion = this.options['function-version']; @@ -54,7 +46,8 @@ class AwsRollbackFunction { Qualifier: funcVersion, }; - return this._lambdaRequest('getFunction', params) + return this.provider + .request('Lambda', 'getFunction', params) .then((func) => func) .catch((error) => { if (error.message.match(/not found/)) { @@ -89,7 +82,7 @@ class AwsRollbackFunction { ZipFile: zipBuffer, }; - return this._lambdaRequest('updateFunctionCode', params).then(() => { + return this.provider.request('Lambda', 'updateFunctionCode', params).then(() => { log.notice(); log.notice.success( `Successfully rolled back function ${funcName} to version "${ diff --git a/lib/plugins/aws/rollback.js b/lib/plugins/aws/rollback.js index 63c2dffe89..3e4ca01af4 100644 --- a/lib/plugins/aws/rollback.js +++ b/lib/plugins/aws/rollback.js @@ -85,14 +85,6 @@ class AwsRollback { }; } - /** - * Helper method to route S3 requests through v2 or v3 based on feature flag - * @private - */ - async _s3Request(method, params) { - return this.provider._awsRequest('S3', method, params); - } - async setStackToUpdate() { const service = this.serverless.service; const serviceName = this.serverless.service.service; @@ -102,7 +94,7 @@ class AwsRollback { let response; try { - response = await this._s3Request('listObjectsV2', { + response = await this.provider.request('S3', 'listObjectsV2', { Bucket: this.bucketName, Prefix: prefix, }); @@ -150,7 +142,7 @@ class AwsRollback { const stateString = await (async () => { try { return ( - await this._s3Request('getObject', { + await this.provider.request('S3', 'getObject', { Bucket: this.bucketName, Key: `${ service.package.artifactDirectoryName diff --git a/lib/plugins/aws/utils/resolve-cf-import-value.js b/lib/plugins/aws/utils/resolve-cf-import-value.js index 27aa620f57..78baba0d19 100644 --- a/lib/plugins/aws/utils/resolve-cf-import-value.js +++ b/lib/plugins/aws/utils/resolve-cf-import-value.js @@ -3,7 +3,7 @@ const ServerlessError = require('../../../serverless-error'); async function resolveCfImportValue(provider, name, sdkParams = {}) { - return provider._awsRequest('CloudFormation', 'listExports', sdkParams).then((result) => { + return provider.request('CloudFormation', 'listExports', sdkParams).then((result) => { const targetExportMeta = result.Exports.find((exportMeta) => exportMeta.Name === name); if (targetExportMeta) return targetExportMeta.Value; if (result.NextToken) { diff --git a/lib/plugins/aws/utils/resolve-cf-ref-value.js b/lib/plugins/aws/utils/resolve-cf-ref-value.js index 9a01e5932a..ba3de7a43c 100644 --- a/lib/plugins/aws/utils/resolve-cf-ref-value.js +++ b/lib/plugins/aws/utils/resolve-cf-ref-value.js @@ -4,7 +4,7 @@ const ServerlessError = require('../../../serverless-error'); async function resolveCfRefValue(provider, resourceLogicalId, sdkParams = {}) { return provider - ._awsRequest( + .request( 'CloudFormation', 'listStackResources', Object.assign(sdkParams, { StackName: provider.naming.getStackName() }) From 031b59cb1ecb0a2f102993a34603779f357328a0 Mon Sep 17 00:00:00 2001 From: Graham Campbell Date: Mon, 9 Jun 2025 00:22:34 +0100 Subject: [PATCH 13/27] Make internal request methods private --- lib/plugins/aws/provider.js | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/lib/plugins/aws/provider.js b/lib/plugins/aws/provider.js index 9d457a59c8..d9949907b1 100644 --- a/lib/plugins/aws/provider.js +++ b/lib/plugins/aws/provider.js @@ -1710,7 +1710,7 @@ class AwsProvider { } /** - * Execute an AWS request - routes between v2 and v3 based on feature flag + * Primary AWS request interface - auto-routes to v2/v3 based on SLS_AWS_SDK_V3 flag * @param {string} service - Service name * @param {string} method - Method name * @param {Object} params - Parameters @@ -1720,21 +1720,22 @@ class AwsProvider { async request(service, method, params, options) { // Use v3 if feature flag is enabled, otherwise fallback to v2 if (this._v3Enabled) { - return this.requestV3(service, method, params, options); + return this._requestV3(service, method, params, options); } - return this.requestV2(service, method, params, options); + return this._requestV2(service, method, params, options); } /** - * Execute an AWS request by calling the AWS SDK v2 + * Direct AWS SDK v2 interface (private) * @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 + * @private */ - async requestV2(service, method, params, options) { + async _requestV2(service, method, params, options) { // TODO: Determine calling module and log that const requestOptions = _.isObject(options) ? options : {}; const shouldCache = _.get(requestOptions, 'useCache', false); @@ -1752,14 +1753,15 @@ class AwsProvider { } /** - * AWS SDK v3 request method - new interface for gradual migration + * Direct AWS SDK v3 interface (private) * @param {string} service - AWS service name (e.g., 'CloudFormation', 'S3') * @param {string} method - Method name from v2 SDK (e.g., 'createStack', 'listObjects') * @param {Object} params - Parameters for the AWS API call * @param {Object} options - Additional options * @returns {Promise} AWS API response + * @private */ - async requestV3(service, method, params = {}, options = {}) { + async _requestV3(service, method, params = {}, options = {}) { try { // Initialize client factory if not already done if (!this.clientFactory) { From 79e9aa0226d926b78835ad44c4a6cd8ca7454a2f Mon Sep 17 00:00:00 2001 From: Graham Campbell Date: Mon, 9 Jun 2025 00:22:43 +0100 Subject: [PATCH 14/27] Run CI on both SDK versions --- .github/workflows/integrate.yml | 16 ++++++++++++++-- .github/workflows/validate.yml | 16 ++++++++++++++-- 2 files changed, 28 insertions(+), 4 deletions(-) diff --git a/.github/workflows/integrate.yml b/.github/workflows/integrate.yml index 9649fb7d92..e8a201addd 100644 --- a/.github/workflows/integrate.yml +++ b/.github/workflows/integrate.yml @@ -50,12 +50,24 @@ jobs: run: | npm update --no-save npm update --save-dev --no-save - - name: Unit tests + - name: Unit tests (AWS SDK v2) # Some tests depend on TTY support, which is missing in GA runner # Workaround taken from https://github.com/actions/runner/issues/241#issuecomment-577360161 run: script -e -c "npm test -- -b" - - name: Packaging tests + env: + SLS_AWS_SDK_V3: '0' + - name: Unit tests (AWS SDK v3) + run: script -e -c "npm test -- -b" + env: + SLS_AWS_SDK_V3: '1' + - name: Packaging tests (AWS SDK v2) run: npm run integration-test-run-package + env: + SLS_AWS_SDK_V3: '0' + - name: Packaging tests (AWS SDK v3) + run: npm run integration-test-run-package + env: + SLS_AWS_SDK_V3: '1' windowsNode16: name: '[Windows] Node 16: Unit tests' diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index 87b8369c4a..7729baf84a 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -63,12 +63,24 @@ jobs: then npx dump-release-notes-from-cc-changelog $NEW_VERSION fi - - name: Unit tests + - name: Unit tests (AWS SDK v2) # Some tests depend on TTY support, which is missing in GA runner # Workaround taken from https://github.com/actions/runner/issues/241#issuecomment-577360161 run: script -e -c "npm test -- -b" - - name: Packaging tests + env: + SLS_AWS_SDK_V3: '0' + - name: Unit tests (AWS SDK v3) + run: script -e -c "npm test -- -b" + env: + SLS_AWS_SDK_V3: '1' + - name: Packaging tests (AWS SDK v2) + run: npm run integration-test-run-package + env: + SLS_AWS_SDK_V3: '0' + - name: Packaging tests (AWS SDK v3) run: npm run integration-test-run-package + env: + SLS_AWS_SDK_V3: '1' windowsNode16: name: '[Windows] Node 16: Unit tests' From 132431e0a97610b498b2ebf3fa9e8243df2f478c Mon Sep 17 00:00:00 2001 From: Graham Campbell Date: Mon, 9 Jun 2025 00:39:53 +0100 Subject: [PATCH 15/27] Fix env var checking --- lib/plugins/aws/provider.js | 2 +- test/utils/api-gateway.js | 2 +- test/utils/cloudformation.js | 2 +- test/utils/cognito.js | 2 +- test/utils/dynamodb.js | 2 +- test/utils/event-bridge.js | 2 +- test/utils/iot.js | 2 +- test/utils/kinesis.js | 2 +- test/utils/misc.js | 2 +- test/utils/s3.js | 2 +- test/utils/sns.js | 2 +- test/utils/sqs.js | 2 +- test/utils/websocket.js | 2 +- 13 files changed, 13 insertions(+), 13 deletions(-) diff --git a/lib/plugins/aws/provider.js b/lib/plugins/aws/provider.js index d9949907b1..34dab60083 100644 --- a/lib/plugins/aws/provider.js +++ b/lib/plugins/aws/provider.js @@ -231,7 +231,7 @@ class AwsProvider { // AWS SDK v3 infrastructure this.clientFactory = null; // Will be initialized when needed - this._v3Enabled = process.env.SLS_AWS_SDK_V3 === 'true'; // Feature flag for gradual rollout + this._v3Enabled = process.env.SLS_AWS_SDK_V3 === '1'; // Feature flag for gradual rollout this.serverless.setProvider(constants.providerName, this); this.hooks = { diff --git a/test/utils/api-gateway.js b/test/utils/api-gateway.js index bbacc91e5b..8a0f889053 100644 --- a/test/utils/api-gateway.js +++ b/test/utils/api-gateway.js @@ -5,7 +5,7 @@ const awsRequest = require('@serverless/test/aws-request'); // Support for both AWS SDK v2 and v3 const getAPIGatewayClient = () => { - if (process.env.SLS_AWS_SDK_V3 === 'true') { + if (process.env.SLS_AWS_SDK_V3 === '1') { // AWS SDK v3 const { APIGatewayClient } = require('@aws-sdk/client-api-gateway'); const { diff --git a/test/utils/cloudformation.js b/test/utils/cloudformation.js index e82d94c6bb..fc2bc02b92 100644 --- a/test/utils/cloudformation.js +++ b/test/utils/cloudformation.js @@ -4,7 +4,7 @@ const awsRequest = require('@serverless/test/aws-request'); // Support for both AWS SDK v2 and v3 const getCloudFormationClient = () => { - if (process.env.SLS_AWS_SDK_V3 === 'true') { + if (process.env.SLS_AWS_SDK_V3 === '1') { // AWS SDK v3 const { CloudFormationClient } = require('@aws-sdk/client-cloudformation'); const { diff --git a/test/utils/cognito.js b/test/utils/cognito.js index ec04ff363e..9918a0bfcc 100644 --- a/test/utils/cognito.js +++ b/test/utils/cognito.js @@ -5,7 +5,7 @@ const awsRequest = require('@serverless/test/aws-request'); // Support for both AWS SDK v2 and v3 const getCognitoClient = () => { - if (process.env.SLS_AWS_SDK_V3 === 'true') { + if (process.env.SLS_AWS_SDK_V3 === '1') { // AWS SDK v3 const { CognitoIdentityProviderClient } = require('@aws-sdk/client-cognito-identity-provider'); const { diff --git a/test/utils/dynamodb.js b/test/utils/dynamodb.js index 6bdf44f88c..39c85f5926 100644 --- a/test/utils/dynamodb.js +++ b/test/utils/dynamodb.js @@ -4,7 +4,7 @@ const awsRequest = require('@serverless/test/aws-request'); // Support for both AWS SDK v2 and v3 const getDynamoDBClient = () => { - if (process.env.SLS_AWS_SDK_V3 === 'true') { + if (process.env.SLS_AWS_SDK_V3 === '1') { // AWS SDK v3 - using DynamoDBDocumentClient from lib-dynamodb const { DynamoDBClient } = require('@aws-sdk/client-dynamodb'); const { DynamoDBDocumentClient, PutCommand } = require('@aws-sdk/lib-dynamodb'); diff --git a/test/utils/event-bridge.js b/test/utils/event-bridge.js index b67fcb5aa8..5c2d4e0341 100644 --- a/test/utils/event-bridge.js +++ b/test/utils/event-bridge.js @@ -4,7 +4,7 @@ const awsRequest = require('@serverless/test/aws-request'); // Support for both AWS SDK v2 and v3 const getEventBridgeClient = () => { - if (process.env.SLS_AWS_SDK_V3 === 'true') { + if (process.env.SLS_AWS_SDK_V3 === '1') { // AWS SDK v3 const { EventBridgeClient } = require('@aws-sdk/client-eventbridge'); const { diff --git a/test/utils/iot.js b/test/utils/iot.js index d7a6962fd6..3a128075fc 100644 --- a/test/utils/iot.js +++ b/test/utils/iot.js @@ -4,7 +4,7 @@ const awsRequest = require('@serverless/test/aws-request'); // Support for both AWS SDK v2 and v3 const getIoTClients = () => { - if (process.env.SLS_AWS_SDK_V3 === 'true') { + if (process.env.SLS_AWS_SDK_V3 === '1') { // AWS SDK v3 - dual service pattern (IoT + IoTDataPlane) const { IoTClient, DescribeEndpointCommand } = require('@aws-sdk/client-iot'); const { IoTDataPlaneClient, PublishCommand } = require('@aws-sdk/client-iot-data-plane'); diff --git a/test/utils/kinesis.js b/test/utils/kinesis.js index 0187711801..e11dca4fe3 100644 --- a/test/utils/kinesis.js +++ b/test/utils/kinesis.js @@ -4,7 +4,7 @@ const awsRequest = require('@serverless/test/aws-request'); // Support for both AWS SDK v2 and v3 const getKinesisClient = () => { - if (process.env.SLS_AWS_SDK_V3 === 'true') { + if (process.env.SLS_AWS_SDK_V3 === '1') { // AWS SDK v3 const { KinesisClient } = require('@aws-sdk/client-kinesis'); const { diff --git a/test/utils/misc.js b/test/utils/misc.js index 70c718cb3f..f1d92e475b 100644 --- a/test/utils/misc.js +++ b/test/utils/misc.js @@ -5,7 +5,7 @@ const wait = require('timers-ext/promise/sleep'); // Support for both AWS SDK v2 and v3 const getCloudWatchLogsClient = () => { - if (process.env.SLS_AWS_SDK_V3 === 'true') { + if (process.env.SLS_AWS_SDK_V3 === '1') { // AWS SDK v3 const { CloudWatchLogsClient } = require('@aws-sdk/client-cloudwatch-logs'); const { FilterLogEventsCommand } = require('@aws-sdk/client-cloudwatch-logs'); diff --git a/test/utils/s3.js b/test/utils/s3.js index 8f6e445a52..7fd95bc4aa 100644 --- a/test/utils/s3.js +++ b/test/utils/s3.js @@ -4,7 +4,7 @@ const awsRequest = require('@serverless/test/aws-request'); // Support for both AWS SDK v2 and v3 const getS3Client = () => { - if (process.env.SLS_AWS_SDK_V3 === 'true') { + if (process.env.SLS_AWS_SDK_V3 === '1') { // AWS SDK v3 const { S3Client } = require('@aws-sdk/client-s3'); const { diff --git a/test/utils/sns.js b/test/utils/sns.js index f3ac22de3b..a10bfb45b0 100644 --- a/test/utils/sns.js +++ b/test/utils/sns.js @@ -4,7 +4,7 @@ const awsRequest = require('@serverless/test/aws-request'); // Support for both AWS SDK v2 and v3 const getSNSClient = () => { - if (process.env.SLS_AWS_SDK_V3 === 'true') { + if (process.env.SLS_AWS_SDK_V3 === '1') { // AWS SDK v3 const { SNSClient } = require('@aws-sdk/client-sns'); const { diff --git a/test/utils/sqs.js b/test/utils/sqs.js index 5fecb168ba..7c54f8ed26 100644 --- a/test/utils/sqs.js +++ b/test/utils/sqs.js @@ -4,7 +4,7 @@ const awsRequest = require('@serverless/test/aws-request'); // Support for both AWS SDK v2 and v3 const getSQSClient = () => { - if (process.env.SLS_AWS_SDK_V3 === 'true') { + if (process.env.SLS_AWS_SDK_V3 === '1') { // AWS SDK v3 const { SQSClient } = require('@aws-sdk/client-sqs'); const { diff --git a/test/utils/websocket.js b/test/utils/websocket.js index 5505864b10..260042e033 100644 --- a/test/utils/websocket.js +++ b/test/utils/websocket.js @@ -4,7 +4,7 @@ const awsRequest = require('@serverless/test/aws-request'); // Support for both AWS SDK v2 and v3 const getApiGatewayV2Client = () => { - if (process.env.SLS_AWS_SDK_V3 === 'true') { + if (process.env.SLS_AWS_SDK_V3 === '1') { // AWS SDK v3 const { ApiGatewayV2Client } = require('@aws-sdk/client-apigatewayv2'); const { From 252ea8044ab5f13f47b4fe457586deb2dad84c1b Mon Sep 17 00:00:00 2001 From: Graham Campbell Date: Mon, 9 Jun 2025 01:01:30 +0100 Subject: [PATCH 16/27] Fix v2 client reuse regression, and make it even better than before --- lib/aws/v2-client-factory.js | 136 ++++++++++++++++++ ...client-factory.js => v3-client-factory.js} | 4 +- lib/plugins/aws/provider.js | 118 ++++++++++++--- 3 files changed, 236 insertions(+), 22 deletions(-) create mode 100644 lib/aws/v2-client-factory.js rename lib/aws/{client-factory.js => v3-client-factory.js} (98%) diff --git a/lib/aws/v2-client-factory.js b/lib/aws/v2-client-factory.js new file mode 100644 index 0000000000..99b507725a --- /dev/null +++ b/lib/aws/v2-client-factory.js @@ -0,0 +1,136 @@ +'use strict'; + +const memoize = require('memoizee'); +const sdk = require('./sdk-v2'); +const deepSortObjectByKey = require('../utils/deep-sort-object-by-key'); + +// Map service names to their SDK v2 classes +const SERVICE_MAP = { + APIGateway: sdk.APIGateway, + ApiGatewayV2: sdk.ApiGatewayV2, + CloudFormation: sdk.CloudFormation, + CloudWatch: sdk.CloudWatch, + CloudWatchLogs: sdk.CloudWatchLogs, + CognitoIdentityServiceProvider: sdk.CognitoIdentityServiceProvider, + DynamoDB: sdk.DynamoDB, + ECR: sdk.ECR, + EventBridge: sdk.EventBridge, + IAM: sdk.IAM, + Iot: sdk.Iot, + IotData: sdk.IotData, + Kafka: sdk.Kafka, + Kinesis: sdk.Kinesis, + Lambda: sdk.Lambda, + MQ: sdk.MQ, + S3: sdk.S3, + SecretsManager: sdk.SecretsManager, + SNS: sdk.SNS, + SQS: sdk.SQS, + STS: sdk.STS, +}; + +class AWSV2ClientFactory { + constructor(baseConfig = {}) { + this.baseConfig = baseConfig; + this.clients = new Map(); + + // Create memoized request function for response caching + this.memoizedRequest = memoize(this._executeRequest.bind(this), { + promise: true, + normalizer: ([serviceName, method, params, clientConfig]) => { + return [ + serviceName, + method, + JSON.stringify(deepSortObjectByKey(params)), + JSON.stringify(deepSortObjectByKey(clientConfig)), + ].join('|'); + }, + }); + } + + /** + * Get a configured AWS service client + * @param {string} serviceName - Name of the AWS service (e.g., 'S3', 'CloudFormation') + * @param {Object} overrideConfig - Configuration to override base config + * @returns {Object} AWS SDK v2 service instance + */ + getClient(serviceName, overrideConfig = {}) { + const ServiceClass = SERVICE_MAP[serviceName]; + if (!ServiceClass) { + throw new Error(`Unknown AWS service: ${serviceName}`); + } + + // Create cache key - include service + sorted config for consistent keys + const configKey = JSON.stringify({ + serviceName, + ...deepSortObjectByKey({ ...this.baseConfig, ...overrideConfig }), + }); + + if (!this.clients.has(configKey)) { + const clientConfig = { ...this.baseConfig, ...overrideConfig }; + this.clients.set(configKey, new ServiceClass(clientConfig)); + } + + return this.clients.get(configKey); + } + + /** + * Execute an AWS request with optional response caching + * @param {string} serviceName - Name of the AWS service + * @param {string} method - Method to call on the service + * @param {Object} params - Parameters for the AWS API call + * @param {Object} clientConfig - Client configuration + * @param {Object} options - Request options + * @param {boolean} options.useCache - Whether to cache the response + * @returns {Promise} Result of the AWS API call + */ + async request(serviceName, method, params, clientConfig = {}, options = {}) { + const { useCache = false } = options; + + if (useCache) { + return this.memoizedRequest(serviceName, method, params, clientConfig); + } + + return this._executeRequest(serviceName, method, params, clientConfig); + } + + /** + * Execute the actual AWS request + * @private + */ + async _executeRequest(serviceName, method, params, clientConfig) { + const client = this.getClient(serviceName, clientConfig); + return client[method](params).promise(); + } + + /** + * Clear the client cache + */ + clearCache() { + this.clients.clear(); + // Clear response cache if it exists + if (this.memoizedRequest.clear) { + this.memoizedRequest.clear(); + } + } + + /** + * Update the base configuration for all future clients + * @param {Object} newConfig - New base configuration + */ + updateBaseConfig(newConfig) { + this.baseConfig = { ...this.baseConfig, ...newConfig }; + // Clear cache to force recreation with new config + this.clearCache(); + } + + /** + * Get list of supported AWS services + * @returns {Array} Array of supported service names + */ + getSupportedServices() { + return Object.keys(SERVICE_MAP); + } +} + +module.exports = AWSV2ClientFactory; diff --git a/lib/aws/client-factory.js b/lib/aws/v3-client-factory.js similarity index 98% rename from lib/aws/client-factory.js rename to lib/aws/v3-client-factory.js index 915966f7f0..a876359d7c 100644 --- a/lib/aws/client-factory.js +++ b/lib/aws/v3-client-factory.js @@ -31,7 +31,7 @@ const CLIENT_MAP = { STS: STSClient, }; -class AWSClientFactory { +class AWSV3ClientFactory { constructor(baseConfig = {}) { this.baseConfig = baseConfig; this.clients = new Map(); @@ -98,4 +98,4 @@ class AWSClientFactory { } } -module.exports = AWSClientFactory; +module.exports = AWSV3ClientFactory; diff --git a/lib/plugins/aws/provider.js b/lib/plugins/aws/provider.js index 34dab60083..d131246e70 100644 --- a/lib/plugins/aws/provider.js +++ b/lib/plugins/aws/provider.js @@ -13,14 +13,13 @@ const d = require('d'); const path = require('path'); const spawnExt = require('child-process-ext/spawn'); const ServerlessError = require('../../serverless-error'); -const awsRequest = require('../../aws/request'); const { cfValue } = require('../../utils/aws-schema-get-cf-value'); const reportDeprecatedProperties = require('../../utils/report-deprecated-properties'); const deepSortObjectByKey = require('../../utils/deep-sort-object-by-key'); const { progress, log } = require('@serverless/utils/log'); -// AWS SDK v3 infrastructure -const AWSClientFactory = require('../../aws/client-factory'); +const AWSV2ClientFactory = require('../../aws/v2-client-factory'); +const AWSV3ClientFactory = require('../../aws/v3-client-factory'); const { createCommand } = require('../../aws/commands'); const { buildClientConfig, shouldUseS3Acceleration } = require('../../aws/config'); const { transformV3Error } = require('../../aws/error-utils'); @@ -1736,20 +1735,33 @@ class AwsProvider { * @private */ async _requestV2(service, method, params, options) { - // TODO: Determine calling module and log that const requestOptions = _.isObject(options) ? options : {}; const shouldCache = _.get(requestOptions, 'useCache', false); - // Copy is required as the credentials may be modified during the request - const credentials = Object.assign({}, this.getCredentials()); - const serviceOptions = { - name: service, - params: { - ...credentials, - region: _.get(requestOptions, 'region', this.getRegion()), - isS3TransferAccelerationEnabled: this.isS3TransferAccelerationEnabled(), - }, + const region = _.get(requestOptions, 'region', this.getRegion()); + + // Initialize factory if needed + if (!this.v2ClientFactory) { + this.v2ClientFactory = new AWSV2ClientFactory(this._getV2BaseConfig()); + } + + // Build client config (only configuration that affects client creation) + const clientConfig = { + ...this.getCredentials(), + region, }; - return (shouldCache ? awsRequest.memoized : awsRequest)(serviceOptions, method, params); + + // Handle S3 acceleration + if (service === 'S3') { + const accelerationCompatibleMethods = new Set(['upload', 'putObject']); + if (accelerationCompatibleMethods.has(method) && this.isS3TransferAccelerationEnabled()) { + clientConfig.useAccelerateEndpoint = true; + } + } + + // Use factory's request method which handles both client caching and response caching + return this.v2ClientFactory.request(service, method, params, clientConfig, { + useCache: shouldCache, + }); } /** @@ -1764,8 +1776,8 @@ class AwsProvider { async _requestV3(service, method, params = {}, options = {}) { try { // Initialize client factory if not already done - if (!this.clientFactory) { - this.clientFactory = new AWSClientFactory(this._getV3BaseConfig()); + if (!this.v3ClientFactory) { + this.v3ClientFactory = new AWSV3ClientFactory(this._getV3BaseConfig()); } // Build client configuration @@ -1775,7 +1787,7 @@ class AwsProvider { const command = createCommand(service, method, params); // Send request - const result = await this.clientFactory.send(service, command, clientConfig); + const result = await this.v3ClientFactory.send(service, command, clientConfig); return result; } catch (error) { @@ -1791,12 +1803,12 @@ class AwsProvider { * @returns {Object} AWS SDK v3 client instance */ getV3Client(service, options = {}) { - if (!this.clientFactory) { - this.clientFactory = new AWSClientFactory(this._getV3BaseConfig()); + if (!this.v3ClientFactory) { + this.v3ClientFactory = new AWSV3ClientFactory(this._getV3BaseConfig()); } const clientConfig = this._buildV3ClientConfig(service, null, options); - return this.clientFactory.getClient(service, clientConfig); + return this.v3ClientFactory.getClient(service, clientConfig); } /** @@ -1837,6 +1849,72 @@ class AwsProvider { return baseConfig; } + /** + * Build base configuration for AWS SDK v2 clients + * @private + */ + _getV2BaseConfig() { + const config = {}; + + // Handle proxy configuration + 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, require('url').parse(proxy)); + } + + // Handle CA certificates + const ca = process.env.ca || process.env.HTTPS_CA || process.env.https_ca; + let caCerts = []; + + if (ca) { + const caArr = ca.split(','); + 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) { + const caPathArr = cafile.split(','); + caCerts = caCerts.concat( + caPathArr.map((cafilePath) => require('fs').readFileSync(cafilePath.trim())) + ); + } + + if (caCerts.length > 0) { + Object.assign(proxyOptions, { + rejectUnauthorized: true, + ca: caCerts, + }); + } + + // Set up HTTP options + const httpOptions = {}; + + if (proxy) { + httpOptions.agent = new (require('https-proxy-agent'))(proxyOptions); + } else if (proxyOptions.ca) { + httpOptions.agent = new (require('https').Agent)(proxyOptions); + } + + // Handle timeout + const timeout = process.env.AWS_CLIENT_TIMEOUT || process.env.aws_client_timeout; + if (timeout) { + httpOptions.timeout = parseInt(timeout, 10); + } + + if (Object.keys(httpOptions).length > 0) { + config.httpOptions = httpOptions; + } + + return config; + } + /** * Fetch credentials directly or using a profile from serverless yml configuration or from the * well known environment variables From 9bdcc7280ee43d0bc0c16b58d23ea10ca7c975d8 Mon Sep 17 00:00:00 2001 From: Graham Campbell Date: Mon, 9 Jun 2025 01:13:17 +0100 Subject: [PATCH 17/27] Revert "Fix v2 client reuse regression, and make it even better than before" This reverts commit 252ea8044ab5f13f47b4fe457586deb2dad84c1b. --- ...v3-client-factory.js => client-factory.js} | 4 +- lib/aws/v2-client-factory.js | 136 ------------------ lib/plugins/aws/provider.js | 118 +++------------ 3 files changed, 22 insertions(+), 236 deletions(-) rename lib/aws/{v3-client-factory.js => client-factory.js} (98%) delete mode 100644 lib/aws/v2-client-factory.js diff --git a/lib/aws/v3-client-factory.js b/lib/aws/client-factory.js similarity index 98% rename from lib/aws/v3-client-factory.js rename to lib/aws/client-factory.js index a876359d7c..915966f7f0 100644 --- a/lib/aws/v3-client-factory.js +++ b/lib/aws/client-factory.js @@ -31,7 +31,7 @@ const CLIENT_MAP = { STS: STSClient, }; -class AWSV3ClientFactory { +class AWSClientFactory { constructor(baseConfig = {}) { this.baseConfig = baseConfig; this.clients = new Map(); @@ -98,4 +98,4 @@ class AWSV3ClientFactory { } } -module.exports = AWSV3ClientFactory; +module.exports = AWSClientFactory; diff --git a/lib/aws/v2-client-factory.js b/lib/aws/v2-client-factory.js deleted file mode 100644 index 99b507725a..0000000000 --- a/lib/aws/v2-client-factory.js +++ /dev/null @@ -1,136 +0,0 @@ -'use strict'; - -const memoize = require('memoizee'); -const sdk = require('./sdk-v2'); -const deepSortObjectByKey = require('../utils/deep-sort-object-by-key'); - -// Map service names to their SDK v2 classes -const SERVICE_MAP = { - APIGateway: sdk.APIGateway, - ApiGatewayV2: sdk.ApiGatewayV2, - CloudFormation: sdk.CloudFormation, - CloudWatch: sdk.CloudWatch, - CloudWatchLogs: sdk.CloudWatchLogs, - CognitoIdentityServiceProvider: sdk.CognitoIdentityServiceProvider, - DynamoDB: sdk.DynamoDB, - ECR: sdk.ECR, - EventBridge: sdk.EventBridge, - IAM: sdk.IAM, - Iot: sdk.Iot, - IotData: sdk.IotData, - Kafka: sdk.Kafka, - Kinesis: sdk.Kinesis, - Lambda: sdk.Lambda, - MQ: sdk.MQ, - S3: sdk.S3, - SecretsManager: sdk.SecretsManager, - SNS: sdk.SNS, - SQS: sdk.SQS, - STS: sdk.STS, -}; - -class AWSV2ClientFactory { - constructor(baseConfig = {}) { - this.baseConfig = baseConfig; - this.clients = new Map(); - - // Create memoized request function for response caching - this.memoizedRequest = memoize(this._executeRequest.bind(this), { - promise: true, - normalizer: ([serviceName, method, params, clientConfig]) => { - return [ - serviceName, - method, - JSON.stringify(deepSortObjectByKey(params)), - JSON.stringify(deepSortObjectByKey(clientConfig)), - ].join('|'); - }, - }); - } - - /** - * Get a configured AWS service client - * @param {string} serviceName - Name of the AWS service (e.g., 'S3', 'CloudFormation') - * @param {Object} overrideConfig - Configuration to override base config - * @returns {Object} AWS SDK v2 service instance - */ - getClient(serviceName, overrideConfig = {}) { - const ServiceClass = SERVICE_MAP[serviceName]; - if (!ServiceClass) { - throw new Error(`Unknown AWS service: ${serviceName}`); - } - - // Create cache key - include service + sorted config for consistent keys - const configKey = JSON.stringify({ - serviceName, - ...deepSortObjectByKey({ ...this.baseConfig, ...overrideConfig }), - }); - - if (!this.clients.has(configKey)) { - const clientConfig = { ...this.baseConfig, ...overrideConfig }; - this.clients.set(configKey, new ServiceClass(clientConfig)); - } - - return this.clients.get(configKey); - } - - /** - * Execute an AWS request with optional response caching - * @param {string} serviceName - Name of the AWS service - * @param {string} method - Method to call on the service - * @param {Object} params - Parameters for the AWS API call - * @param {Object} clientConfig - Client configuration - * @param {Object} options - Request options - * @param {boolean} options.useCache - Whether to cache the response - * @returns {Promise} Result of the AWS API call - */ - async request(serviceName, method, params, clientConfig = {}, options = {}) { - const { useCache = false } = options; - - if (useCache) { - return this.memoizedRequest(serviceName, method, params, clientConfig); - } - - return this._executeRequest(serviceName, method, params, clientConfig); - } - - /** - * Execute the actual AWS request - * @private - */ - async _executeRequest(serviceName, method, params, clientConfig) { - const client = this.getClient(serviceName, clientConfig); - return client[method](params).promise(); - } - - /** - * Clear the client cache - */ - clearCache() { - this.clients.clear(); - // Clear response cache if it exists - if (this.memoizedRequest.clear) { - this.memoizedRequest.clear(); - } - } - - /** - * Update the base configuration for all future clients - * @param {Object} newConfig - New base configuration - */ - updateBaseConfig(newConfig) { - this.baseConfig = { ...this.baseConfig, ...newConfig }; - // Clear cache to force recreation with new config - this.clearCache(); - } - - /** - * Get list of supported AWS services - * @returns {Array} Array of supported service names - */ - getSupportedServices() { - return Object.keys(SERVICE_MAP); - } -} - -module.exports = AWSV2ClientFactory; diff --git a/lib/plugins/aws/provider.js b/lib/plugins/aws/provider.js index d131246e70..34dab60083 100644 --- a/lib/plugins/aws/provider.js +++ b/lib/plugins/aws/provider.js @@ -13,13 +13,14 @@ const d = require('d'); const path = require('path'); const spawnExt = require('child-process-ext/spawn'); const ServerlessError = require('../../serverless-error'); +const awsRequest = require('../../aws/request'); const { cfValue } = require('../../utils/aws-schema-get-cf-value'); const reportDeprecatedProperties = require('../../utils/report-deprecated-properties'); const deepSortObjectByKey = require('../../utils/deep-sort-object-by-key'); const { progress, log } = require('@serverless/utils/log'); -const AWSV2ClientFactory = require('../../aws/v2-client-factory'); -const AWSV3ClientFactory = require('../../aws/v3-client-factory'); +// AWS SDK v3 infrastructure +const AWSClientFactory = require('../../aws/client-factory'); const { createCommand } = require('../../aws/commands'); const { buildClientConfig, shouldUseS3Acceleration } = require('../../aws/config'); const { transformV3Error } = require('../../aws/error-utils'); @@ -1735,33 +1736,20 @@ class AwsProvider { * @private */ async _requestV2(service, method, params, options) { + // TODO: Determine calling module and log that const requestOptions = _.isObject(options) ? options : {}; const shouldCache = _.get(requestOptions, 'useCache', false); - const region = _.get(requestOptions, 'region', this.getRegion()); - - // Initialize factory if needed - if (!this.v2ClientFactory) { - this.v2ClientFactory = new AWSV2ClientFactory(this._getV2BaseConfig()); - } - - // Build client config (only configuration that affects client creation) - const clientConfig = { - ...this.getCredentials(), - region, + // Copy is required as the credentials may be modified during the request + const credentials = Object.assign({}, this.getCredentials()); + const serviceOptions = { + name: service, + params: { + ...credentials, + region: _.get(requestOptions, 'region', this.getRegion()), + isS3TransferAccelerationEnabled: this.isS3TransferAccelerationEnabled(), + }, }; - - // Handle S3 acceleration - if (service === 'S3') { - const accelerationCompatibleMethods = new Set(['upload', 'putObject']); - if (accelerationCompatibleMethods.has(method) && this.isS3TransferAccelerationEnabled()) { - clientConfig.useAccelerateEndpoint = true; - } - } - - // Use factory's request method which handles both client caching and response caching - return this.v2ClientFactory.request(service, method, params, clientConfig, { - useCache: shouldCache, - }); + return (shouldCache ? awsRequest.memoized : awsRequest)(serviceOptions, method, params); } /** @@ -1776,8 +1764,8 @@ class AwsProvider { async _requestV3(service, method, params = {}, options = {}) { try { // Initialize client factory if not already done - if (!this.v3ClientFactory) { - this.v3ClientFactory = new AWSV3ClientFactory(this._getV3BaseConfig()); + if (!this.clientFactory) { + this.clientFactory = new AWSClientFactory(this._getV3BaseConfig()); } // Build client configuration @@ -1787,7 +1775,7 @@ class AwsProvider { const command = createCommand(service, method, params); // Send request - const result = await this.v3ClientFactory.send(service, command, clientConfig); + const result = await this.clientFactory.send(service, command, clientConfig); return result; } catch (error) { @@ -1803,12 +1791,12 @@ class AwsProvider { * @returns {Object} AWS SDK v3 client instance */ getV3Client(service, options = {}) { - if (!this.v3ClientFactory) { - this.v3ClientFactory = new AWSV3ClientFactory(this._getV3BaseConfig()); + if (!this.clientFactory) { + this.clientFactory = new AWSClientFactory(this._getV3BaseConfig()); } const clientConfig = this._buildV3ClientConfig(service, null, options); - return this.v3ClientFactory.getClient(service, clientConfig); + return this.clientFactory.getClient(service, clientConfig); } /** @@ -1849,72 +1837,6 @@ class AwsProvider { return baseConfig; } - /** - * Build base configuration for AWS SDK v2 clients - * @private - */ - _getV2BaseConfig() { - const config = {}; - - // Handle proxy configuration - 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, require('url').parse(proxy)); - } - - // Handle CA certificates - const ca = process.env.ca || process.env.HTTPS_CA || process.env.https_ca; - let caCerts = []; - - if (ca) { - const caArr = ca.split(','); - 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) { - const caPathArr = cafile.split(','); - caCerts = caCerts.concat( - caPathArr.map((cafilePath) => require('fs').readFileSync(cafilePath.trim())) - ); - } - - if (caCerts.length > 0) { - Object.assign(proxyOptions, { - rejectUnauthorized: true, - ca: caCerts, - }); - } - - // Set up HTTP options - const httpOptions = {}; - - if (proxy) { - httpOptions.agent = new (require('https-proxy-agent'))(proxyOptions); - } else if (proxyOptions.ca) { - httpOptions.agent = new (require('https').Agent)(proxyOptions); - } - - // Handle timeout - const timeout = process.env.AWS_CLIENT_TIMEOUT || process.env.aws_client_timeout; - if (timeout) { - httpOptions.timeout = parseInt(timeout, 10); - } - - if (Object.keys(httpOptions).length > 0) { - config.httpOptions = httpOptions; - } - - return config; - } - /** * Fetch credentials directly or using a profile from serverless yml configuration or from the * well known environment variables From 985cede289b642fb1018f3780fb1244d26840674 Mon Sep 17 00:00:00 2001 From: Graham Campbell Date: Mon, 9 Jun 2025 01:19:26 +0100 Subject: [PATCH 18/27] Fix test --- lib/aws/error-utils.js | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/lib/aws/error-utils.js b/lib/aws/error-utils.js index ba616efe97..ddfd2e65b2 100644 --- a/lib/aws/error-utils.js +++ b/lib/aws/error-utils.js @@ -52,6 +52,25 @@ function transformV3Error(error) { return error; } + // Check for credentials errors and transform them to match v2 behavior + if ( + error.name === 'CredentialsProviderError' || + (error.message && error.message.includes('Could not load credentials')) + ) { + const ServerlessError = require('../serverless-error'); + const chalk = require('chalk'); + + const errorMessage = [ + 'AWS provider credentials not found.', + ' Learn how to set up AWS provider credentials', + ` in our docs here: <${chalk.green('http://slss.io/aws-creds-setup')}>.`, + ].join(''); + + throw Object.assign(new ServerlessError(errorMessage, 'AWS_CREDENTIALS_NOT_FOUND'), { + providerError: Object.assign({}, error, { retryable: false }), + }); + } + // Create a new error object with v2-compatible properties const transformedError = new Error(error.message || 'Unknown AWS error'); From 4524f0103d20385f3426641e8a366c65b690c4a7 Mon Sep 17 00:00:00 2001 From: Graham Campbell Date: Mon, 9 Jun 2025 01:19:41 +0100 Subject: [PATCH 19/27] Apply the correct fix for client re-use --- .../api-gateway-cloud-watch-role/handler.js | 55 +++++++++---------- .../cognito-user-pool/lib/permissions.js | 20 ++----- .../cognito-user-pool/lib/user-pool.js | 45 +++++++-------- .../event-bridge/lib/event-bridge.js | 51 ++++++++--------- .../resources/event-bridge/lib/permissions.js | 22 ++------ .../resources/s3/lib/bucket.js | 22 +++----- .../resources/s3/lib/permissions.js | 22 ++------ 7 files changed, 100 insertions(+), 137 deletions(-) diff --git a/lib/plugins/aws/custom-resources/resources/api-gateway-cloud-watch-role/handler.js b/lib/plugins/aws/custom-resources/resources/api-gateway-cloud-watch-role/handler.js index 806037b990..9ceb448ad0 100644 --- a/lib/plugins/aws/custom-resources/resources/api-gateway-cloud-watch-role/handler.js +++ b/lib/plugins/aws/custom-resources/resources/api-gateway-cloud-watch-role/handler.js @@ -2,31 +2,15 @@ const { wait, MAX_AWS_REQUEST_TRY } = require('../utils'); const { getEnvironment, handlerWrapper } = require('../utils'); +const { GetAccountCommand, UpdateAccountCommand } = require('@aws-sdk/client-api-gateway'); const { - APIGatewayClient, - GetAccountCommand, - UpdateAccountCommand, -} = require('@aws-sdk/client-api-gateway'); -const { - IAMClient, ListAttachedRolePoliciesCommand, CreateRoleCommand, AttachRolePolicyCommand, } = require('@aws-sdk/client-iam'); +const AWSClientFactory = require('../../../../../aws/client-factory'); -function getAPIGatewayClient(region) { - return new APIGatewayClient({ - region, - maxAttempts: MAX_AWS_REQUEST_TRY, - }); -} - -function getIAMClient(region) { - return new IAMClient({ - region, - maxAttempts: MAX_AWS_REQUEST_TRY, - }); -} +const awsFactory = new AWSClientFactory({ maxAttempts: MAX_AWS_REQUEST_TRY }); async function handler(event, context) { if (event.RequestType === 'Create') { @@ -42,10 +26,10 @@ async function handler(event, context) { async function create(event, context) { const { RoleArn } = event.ResourceProperties; const { Partition: partition, AccountId: accountId, Region: region } = getEnvironment(context); - const apiGateway = getAPIGatewayClient(region); - const iam = getIAMClient(region); - const assignedRoleArn = (await apiGateway.send(new GetAccountCommand({}))).cloudwatchRoleArn; + const assignedRoleArn = ( + await awsFactory.send('APIGateway', new GetAccountCommand({}), { region }) + ).cloudwatchRoleArn; let roleArn = `arn:${partition}:iam::${accountId}:role/serverlessApiGatewayCloudWatchRole`; if (RoleArn) { @@ -59,12 +43,18 @@ async function create(event, context) { const attachedPolicies = await (async () => { try { - return (await iam.send(new ListAttachedRolePoliciesCommand({ RoleName: roleName }))) - .AttachedPolicies; + return ( + await awsFactory.send( + 'IAM', + new ListAttachedRolePoliciesCommand({ RoleName: roleName }), + { region } + ) + ).AttachedPolicies; } catch (error) { if (error.code === 'NoSuchEntity') { // Role doesn't exist yet, create; - await iam.send( + await awsFactory.send( + 'IAM', new CreateRoleCommand({ AssumeRolePolicyDocument: JSON.stringify({ Version: '2012-10-17', @@ -80,7 +70,8 @@ async function create(event, context) { }), Path: '/', RoleName: roleName, - }) + }), + { region } ); return []; } @@ -93,11 +84,13 @@ async function create(event, context) { (policy) => policy.PolicyArn === apiGatewayPushToCloudWatchLogsPolicyArn ) ) { - await iam.send( + await awsFactory.send( + 'IAM', new AttachRolePolicyCommand({ PolicyArn: apiGatewayPushToCloudWatchLogsPolicyArn, RoleName: roleName, - }) + }), + { region } ); } } @@ -107,7 +100,8 @@ async function create(event, context) { const updateAccount = async (counter = 1) => { try { - await apiGateway.send( + await awsFactory.send( + 'APIGateway', new UpdateAccountCommand({ patchOperations: [ { @@ -116,7 +110,8 @@ async function create(event, context) { value: roleArn, }, ], - }) + }), + { region } ); } catch (error) { if (counter < 10) { diff --git a/lib/plugins/aws/custom-resources/resources/cognito-user-pool/lib/permissions.js b/lib/plugins/aws/custom-resources/resources/cognito-user-pool/lib/permissions.js index fda423d1d5..7b9e5c8b0f 100644 --- a/lib/plugins/aws/custom-resources/resources/cognito-user-pool/lib/permissions.js +++ b/lib/plugins/aws/custom-resources/resources/cognito-user-pool/lib/permissions.js @@ -1,18 +1,10 @@ 'use strict'; const { MAX_AWS_REQUEST_TRY } = require('../../utils'); -const { - LambdaClient, - AddPermissionCommand, - RemovePermissionCommand, -} = require('@aws-sdk/client-lambda'); +const { AddPermissionCommand, RemovePermissionCommand } = require('@aws-sdk/client-lambda'); +const AWSClientFactory = require('../../../../../../aws/client-factory'); -function getLambdaClient(region) { - return new LambdaClient({ - region, - maxAttempts: MAX_AWS_REQUEST_TRY, - }); -} +const awsFactory = new AWSClientFactory({ maxAttempts: MAX_AWS_REQUEST_TRY }); function getStatementId(functionName, userPoolName) { const normalizedUserPoolName = userPoolName.toLowerCase().replace(/[.:*\s]/g, ''); @@ -25,7 +17,6 @@ function getStatementId(functionName, userPoolName) { async function addPermission(config) { const { functionName, userPoolName, partition, region, accountId, userPoolId } = config; - const lambda = getLambdaClient(region); const payload = { Action: 'lambda:InvokeFunction', @@ -34,17 +25,16 @@ async function addPermission(config) { StatementId: getStatementId(functionName, userPoolName), SourceArn: `arn:${partition}:cognito-idp:${region}:${accountId}:userpool/${userPoolId}`, }; - return lambda.send(new AddPermissionCommand(payload)); + return awsFactory.send('Lambda', new AddPermissionCommand(payload), { region }); } async function removePermission(config) { const { functionName, userPoolName, region } = config; - const lambda = getLambdaClient(region); const payload = { FunctionName: functionName, StatementId: getStatementId(functionName, userPoolName), }; - return lambda.send(new RemovePermissionCommand(payload)); + return awsFactory.send('Lambda', new RemovePermissionCommand(payload), { region }); } module.exports = { diff --git a/lib/plugins/aws/custom-resources/resources/cognito-user-pool/lib/user-pool.js b/lib/plugins/aws/custom-resources/resources/cognito-user-pool/lib/user-pool.js index 31c85b24f4..4785644a69 100644 --- a/lib/plugins/aws/custom-resources/resources/cognito-user-pool/lib/user-pool.js +++ b/lib/plugins/aws/custom-resources/resources/cognito-user-pool/lib/user-pool.js @@ -2,18 +2,13 @@ const { MAX_AWS_REQUEST_TRY } = require('../../utils'); const { - CognitoIdentityProviderClient, ListUserPoolsCommand, DescribeUserPoolCommand, UpdateUserPoolCommand, } = require('@aws-sdk/client-cognito-identity-provider'); +const AWSClientFactory = require('../../../../../../aws/client-factory'); -function getCognitoClient(region) { - return new CognitoIdentityProviderClient({ - region, - maxAttempts: MAX_AWS_REQUEST_TRY, - }); -} +const awsFactory = new AWSClientFactory({ maxAttempts: MAX_AWS_REQUEST_TRY }); const customSenderSources = ['CustomSMSSender', 'CustomEmailSender']; @@ -45,7 +40,6 @@ function getUpdateConfigFromCurrentSetup(currentSetup) { async function findUserPoolByName(config) { const { userPoolName, region } = config; - const cognito = getCognitoClient(region); const payload = { MaxResults: 60, @@ -53,14 +47,16 @@ async function findUserPoolByName(config) { async function recursiveFind(nextToken) { if (nextToken) payload.NextToken = nextToken; - return cognito.send(new ListUserPoolsCommand(payload)).then((result) => { - const matches = result.UserPools.filter((pool) => pool.Name === userPoolName); - if (matches.length) { - return matches.shift(); - } - if (result.NextToken) return recursiveFind(result.NextToken); - return null; - }); + return awsFactory + .send('CognitoIdentityProvider', new ListUserPoolsCommand(payload), { region }) + .then((result) => { + const matches = result.UserPools.filter((pool) => pool.Name === userPoolName); + if (matches.length) { + return matches.shift(); + } + if (result.NextToken) return recursiveFind(result.NextToken); + return null; + }); } return recursiveFind(); @@ -68,16 +64,18 @@ async function findUserPoolByName(config) { async function getConfiguration(config) { const { region } = config; - const cognito = getCognitoClient(region); return findUserPoolByName(config).then((userPool) => - cognito.send(new DescribeUserPoolCommand({ UserPoolId: userPool.Id })) + awsFactory.send( + 'CognitoIdentityProvider', + new DescribeUserPoolCommand({ UserPoolId: userPool.Id }), + { region } + ) ); } async function updateConfiguration(config) { const { lambdaArn, userPoolConfigs, region } = config; - const cognito = getCognitoClient(region); return getConfiguration(config).then((res) => { const UserPoolId = res.UserPool.Id; @@ -104,13 +102,14 @@ async function updateConfiguration(config) { LambdaConfig, }); - return cognito.send(new UpdateUserPoolCommand(updatedConfig)); + return awsFactory.send('CognitoIdentityProvider', new UpdateUserPoolCommand(updatedConfig), { + region, + }); }); } async function removeConfiguration(config) { const { lambdaArn, region } = config; - const cognito = getCognitoClient(region); return getConfiguration(config).then((res) => { const UserPoolId = res.UserPool.Id; @@ -125,7 +124,9 @@ async function removeConfiguration(config) { LambdaConfig, }); - return cognito.send(new UpdateUserPoolCommand(updatedConfig)); + return awsFactory.send('CognitoIdentityProvider', new UpdateUserPoolCommand(updatedConfig), { + region, + }); }); } diff --git a/lib/plugins/aws/custom-resources/resources/event-bridge/lib/event-bridge.js b/lib/plugins/aws/custom-resources/resources/event-bridge/lib/event-bridge.js index 5e2c2e2807..29f8d4d874 100644 --- a/lib/plugins/aws/custom-resources/resources/event-bridge/lib/event-bridge.js +++ b/lib/plugins/aws/custom-resources/resources/event-bridge/lib/event-bridge.js @@ -3,7 +3,6 @@ const { MAX_AWS_REQUEST_TRY } = require('../../utils'); const { getEventBusName, getEventBusTargetId } = require('./utils'); const { - EventBridgeClient, CreateEventBusCommand, DeleteEventBusCommand, PutRuleCommand, @@ -11,26 +10,23 @@ const { PutTargetsCommand, RemoveTargetsCommand, } = require('@aws-sdk/client-eventbridge'); +const AWSClientFactory = require('../../../../../../aws/client-factory'); -function getEventBridgeClient(region) { - return new EventBridgeClient({ - region, - maxAttempts: MAX_AWS_REQUEST_TRY, - }); -} +const awsFactory = new AWSClientFactory({ maxAttempts: MAX_AWS_REQUEST_TRY }); async function createEventBus(config) { const { eventBus, region } = config; - const eventBridge = getEventBridgeClient(region); if (eventBus) { if (eventBus.startsWith('arn')) { return Promise.resolve(); } - return eventBridge.send( + return awsFactory.send( + 'EventBridge', new CreateEventBusCommand({ Name: eventBus, - }) + }), + { region } ); } return Promise.resolve(); @@ -38,17 +34,18 @@ async function createEventBus(config) { async function deleteEventBus(config) { const { eventBus, region } = config; - const eventBridge = getEventBridgeClient(region); if (eventBus) { if (eventBus.startsWith('arn')) { return Promise.resolve(); } - return eventBridge.send( + return awsFactory.send( + 'EventBridge', new DeleteEventBusCommand({ Name: eventBus, - }) + }), + { region } ); } return Promise.resolve(); @@ -56,38 +53,39 @@ async function deleteEventBus(config) { async function updateRuleConfiguration(config) { const { ruleName, eventBus, pattern, schedule, region, state } = config; - const eventBridge = getEventBridgeClient(region); const EventBusName = getEventBusName(eventBus); - return eventBridge.send( + return awsFactory.send( + 'EventBridge', new PutRuleCommand({ Name: ruleName, EventBusName, EventPattern: JSON.stringify(pattern), ScheduleExpression: schedule, State: state, - }) + }), + { region } ); } async function removeRuleConfiguration(config) { const { ruleName, eventBus, region } = config; - const eventBridge = getEventBridgeClient(region); const EventBusName = getEventBusName(eventBus); - return eventBridge.send( + return awsFactory.send( + 'EventBridge', new DeleteRuleCommand({ Name: ruleName, EventBusName, - }) + }), + { region } ); } async function updateTargetConfiguration(config) { const { lambdaArn, ruleName, eventBus, input, inputPath, inputTransformer, region } = config; - const eventBridge = getEventBridgeClient(region); const EventBusName = getEventBusName(eventBus); @@ -105,27 +103,30 @@ async function updateTargetConfiguration(config) { } return removeTargetConfiguration(config).then(() => - eventBridge.send( + awsFactory.send( + 'EventBridge', new PutTargetsCommand({ Rule: ruleName, EventBusName, Targets: [target], - }) + }), + { region } ) ); } async function removeTargetConfiguration(config) { const { ruleName, eventBus, region } = config; - const eventBridge = getEventBridgeClient(region); const EventBusName = getEventBusName(eventBus); - return eventBridge.send( + return awsFactory.send( + 'EventBridge', new RemoveTargetsCommand({ Ids: [getEventBusTargetId(ruleName)], Rule: ruleName, EventBusName, - }) + }), + { region } ); } diff --git a/lib/plugins/aws/custom-resources/resources/event-bridge/lib/permissions.js b/lib/plugins/aws/custom-resources/resources/event-bridge/lib/permissions.js index 2463a40eba..b89d249cdd 100644 --- a/lib/plugins/aws/custom-resources/resources/event-bridge/lib/permissions.js +++ b/lib/plugins/aws/custom-resources/resources/event-bridge/lib/permissions.js @@ -2,18 +2,10 @@ const { MAX_AWS_REQUEST_TRY } = require('../../utils'); const { getEventBusName } = require('./utils'); -const { - LambdaClient, - AddPermissionCommand, - RemovePermissionCommand, -} = require('@aws-sdk/client-lambda'); - -function getLambdaClient(region) { - return new LambdaClient({ - region, - maxAttempts: MAX_AWS_REQUEST_TRY, - }); -} +const { AddPermissionCommand, RemovePermissionCommand } = require('@aws-sdk/client-lambda'); +const AWSClientFactory = require('../../../../../../aws/client-factory'); + +const awsFactory = new AWSClientFactory({ maxAttempts: MAX_AWS_REQUEST_TRY }); function getStatementId(functionName, ruleName) { const normalizedRuleName = ruleName.toLowerCase().replace(/[.:*]/g, ''); @@ -26,7 +18,6 @@ function getStatementId(functionName, ruleName) { async function addPermission(config) { const { functionName, partition, region, accountId, eventBus, ruleName } = config; - const lambda = getLambdaClient(region); let SourceArn = `arn:${partition}:events:${region}:${accountId}:rule/${ruleName}`; if (eventBus) { @@ -41,19 +32,18 @@ async function addPermission(config) { SourceArn, }; - return lambda.send(new AddPermissionCommand(payload)); + return awsFactory.send('Lambda', new AddPermissionCommand(payload), { region }); } async function removePermission(config) { const { functionName, region, ruleName } = config; - const lambda = getLambdaClient(region); const payload = { FunctionName: functionName, StatementId: getStatementId(functionName, ruleName), }; - return lambda.send(new RemovePermissionCommand(payload)); + return awsFactory.send('Lambda', new RemovePermissionCommand(payload), { region }); } module.exports = { diff --git a/lib/plugins/aws/custom-resources/resources/s3/lib/bucket.js b/lib/plugins/aws/custom-resources/resources/s3/lib/bucket.js index 2ed4dbdff0..be7013f0e2 100644 --- a/lib/plugins/aws/custom-resources/resources/s3/lib/bucket.js +++ b/lib/plugins/aws/custom-resources/resources/s3/lib/bucket.js @@ -3,17 +3,12 @@ const crypto = require('crypto'); const { MAX_AWS_REQUEST_TRY } = require('../../utils'); const { - S3Client, GetBucketNotificationConfigurationCommand, PutBucketNotificationConfigurationCommand, } = require('@aws-sdk/client-s3'); +const AWSClientFactory = require('../../../../../../aws/client-factory'); -function getS3Client(region) { - return new S3Client({ - region, - maxAttempts: MAX_AWS_REQUEST_TRY, - }); -} +const awsFactory = new AWSClientFactory({ maxAttempts: MAX_AWS_REQUEST_TRY }); function generateId(functionName, bucketConfig) { const md5 = crypto.createHash('md5').update(JSON.stringify(bucketConfig)).digest('hex'); @@ -43,19 +38,17 @@ function createFilter(config) { async function getConfiguration(config) { const { bucketName, region } = config; - const s3 = getS3Client(region); const Bucket = bucketName; const payload = { Bucket, }; - return s3.send(new GetBucketNotificationConfigurationCommand(payload)); + return awsFactory.send('S3', new GetBucketNotificationConfigurationCommand(payload), { region }); } async function updateConfiguration(config) { const { lambdaArn, functionName, bucketName, bucketConfigs, region } = config; - const s3 = getS3Client(region); const Bucket = bucketName; @@ -100,13 +93,14 @@ async function updateConfiguration(config) { Bucket, NotificationConfiguration, }; - return s3.send(new PutBucketNotificationConfigurationCommand(payload)); + return awsFactory.send('S3', new PutBucketNotificationConfigurationCommand(payload), { + region, + }); }); } async function removeConfiguration(config) { const { functionName, bucketName, region } = config; - const s3 = getS3Client(region); const Bucket = bucketName; @@ -126,7 +120,9 @@ async function removeConfiguration(config) { Bucket, NotificationConfiguration, }; - return s3.send(new PutBucketNotificationConfigurationCommand(payload)); + return awsFactory.send('S3', new PutBucketNotificationConfigurationCommand(payload), { + region, + }); }); } diff --git a/lib/plugins/aws/custom-resources/resources/s3/lib/permissions.js b/lib/plugins/aws/custom-resources/resources/s3/lib/permissions.js index 65649211b6..6fa9bef927 100644 --- a/lib/plugins/aws/custom-resources/resources/s3/lib/permissions.js +++ b/lib/plugins/aws/custom-resources/resources/s3/lib/permissions.js @@ -1,18 +1,10 @@ 'use strict'; const { MAX_AWS_REQUEST_TRY } = require('../../utils'); -const { - LambdaClient, - AddPermissionCommand, - RemovePermissionCommand, -} = require('@aws-sdk/client-lambda'); - -function getLambdaClient(region) { - return new LambdaClient({ - region, - maxAttempts: MAX_AWS_REQUEST_TRY, - }); -} +const { AddPermissionCommand, RemovePermissionCommand } = require('@aws-sdk/client-lambda'); +const AWSClientFactory = require('../../../../../../aws/client-factory'); + +const awsFactory = new AWSClientFactory({ maxAttempts: MAX_AWS_REQUEST_TRY }); function getStatementId(functionName, bucketName) { const normalizedBucketName = bucketName.replace(/[.:*]/g, ''); @@ -25,7 +17,6 @@ function getStatementId(functionName, bucketName) { async function addPermission(config) { const { functionName, bucketName, partition, region, accountId } = config; - const lambda = getLambdaClient(region); const payload = { Action: 'lambda:InvokeFunction', @@ -36,18 +27,17 @@ async function addPermission(config) { SourceAccount: accountId, }; - return lambda.send(new AddPermissionCommand(payload)); + return awsFactory.send('Lambda', new AddPermissionCommand(payload), { region }); } async function removePermission(config) { const { functionName, bucketName, region } = config; - const lambda = getLambdaClient(region); const payload = { FunctionName: functionName, StatementId: getStatementId(functionName, bucketName), }; - return lambda.send(new RemovePermissionCommand(payload)); + return awsFactory.send('Lambda', new RemovePermissionCommand(payload), { region }); } module.exports = { From 7841f0886bbe9310253a2c6d175a922deb5f3ab7 Mon Sep 17 00:00:00 2001 From: Matthieu Napoli Date: Wed, 30 Jul 2025 13:53:23 +0200 Subject: [PATCH 20/27] Revert formatting changes --- .../sources/instance-dependent/get-aws.js | 7 ++++--- .../sources/instance-dependent/get-cf.js | 15 ++++++++------- .../sources/instance-dependent/get-s3.js | 10 +++------- .../sources/instance-dependent/get-ssm.js | 3 +-- 4 files changed, 16 insertions(+), 19 deletions(-) diff --git a/lib/configuration/variables/sources/instance-dependent/get-aws.js b/lib/configuration/variables/sources/instance-dependent/get-aws.js index b3eee17f7f..45b7a021d0 100644 --- a/lib/configuration/variables/sources/instance-dependent/get-aws.js +++ b/lib/configuration/variables/sources/instance-dependent/get-aws.js @@ -21,9 +21,10 @@ module.exports = (serverlessInstance) => { switch (address) { case 'accountId': { - const provider = serverlessInstance.getProvider('aws'); - const result = await provider.request('STS', 'getCallerIdentity', {}, { useCache: true }); - return { value: result.Account }; + const { Account } = await serverlessInstance + .getProvider('aws') + .request('STS', 'getCallerIdentity', {}, { useCache: true }); + return { value: Account }; } case 'region': { let region = options.region; diff --git a/lib/configuration/variables/sources/instance-dependent/get-cf.js b/lib/configuration/variables/sources/instance-dependent/get-cf.js index aa53ae32cb..26a14dce27 100644 --- a/lib/configuration/variables/sources/instance-dependent/get-cf.js +++ b/lib/configuration/variables/sources/instance-dependent/get-cf.js @@ -32,13 +32,14 @@ module.exports = (serverlessInstance) => { const result = await (async () => { try { - const provider = serverlessInstance.getProvider('aws'); - return await provider.request( - 'CloudFormation', - 'describeStacks', - { StackName: stackName }, - { useCache: true, region: params && params[0] } - ); + return await serverlessInstance + .getProvider('aws') + .request( + 'CloudFormation', + 'describeStacks', + { StackName: stackName }, + { useCache: true, region: params && params[0] } + ); } catch (error) { if ( error.code === 'AWS_CLOUD_FORMATION_DESCRIBE_STACKS_VALIDATION_ERROR' && diff --git a/lib/configuration/variables/sources/instance-dependent/get-s3.js b/lib/configuration/variables/sources/instance-dependent/get-s3.js index 0913adeed5..32a1204461 100644 --- a/lib/configuration/variables/sources/instance-dependent/get-s3.js +++ b/lib/configuration/variables/sources/instance-dependent/get-s3.js @@ -31,13 +31,9 @@ module.exports = (serverlessInstance) => { const result = await (async () => { try { - const provider = serverlessInstance.getProvider('aws'); - return await provider.request( - 'S3', - 'getObject', - { Bucket: bucketName, Key: key }, - { useCache: true } - ); + return await serverlessInstance + .getProvider('aws') + .request('S3', 'getObject', { Bucket: bucketName, Key: key }, { useCache: true }); } catch (error) { // Check for normalized error code instead of native one if (error.code === 'AWS_S3_GET_OBJECT_NO_SUCH_KEY') return null; diff --git a/lib/configuration/variables/sources/instance-dependent/get-ssm.js b/lib/configuration/variables/sources/instance-dependent/get-ssm.js index 1a34554d8c..89cc31dfd3 100644 --- a/lib/configuration/variables/sources/instance-dependent/get-ssm.js +++ b/lib/configuration/variables/sources/instance-dependent/get-ssm.js @@ -26,8 +26,7 @@ module.exports = (serverlessInstance) => { const result = await (async () => { try { - const provider = serverlessInstance.getProvider('aws'); - return await provider.request( + return await serverlessInstance.getProvider('aws').request( 'SSM', 'getParameter', { From dba013f253054efeed8bdeca00f88f24313b8117 Mon Sep 17 00:00:00 2001 From: Matthieu Napoli Date: Wed, 30 Jul 2025 14:11:37 +0200 Subject: [PATCH 21/27] Remove unused functions --- lib/aws/client-factory.js | 25 ------------------------- 1 file changed, 25 deletions(-) diff --git a/lib/aws/client-factory.js b/lib/aws/client-factory.js index 915966f7f0..deec89815d 100644 --- a/lib/aws/client-factory.js +++ b/lib/aws/client-factory.js @@ -71,31 +71,6 @@ class AWSClientFactory { const client = this.getClient(serviceName, clientConfig); return client.send(command); } - - /** - * Clear the client cache - */ - clearCache() { - this.clients.clear(); - } - - /** - * Update the base configuration for all future clients - * @param {Object} newConfig - New base configuration - */ - updateBaseConfig(newConfig) { - this.baseConfig = { ...this.baseConfig, ...newConfig }; - // Clear cache to force recreation with new config - this.clearCache(); - } - - /** - * Get list of supported AWS services - * @returns {Array} Array of supported service names - */ - getSupportedServices() { - return Object.keys(CLIENT_MAP); - } } module.exports = AWSClientFactory; From 5c9f50aee0c55b8b9e3a296fbc8155fb535ea734 Mon Sep 17 00:00:00 2001 From: Matthieu Napoli Date: Wed, 30 Jul 2025 14:18:58 +0200 Subject: [PATCH 22/27] Remove more unused code --- lib/aws/commands.js | 16 ------- lib/aws/config.js | 21 ---------- lib/aws/error-utils.js | 95 ------------------------------------------ 3 files changed, 132 deletions(-) diff --git a/lib/aws/commands.js b/lib/aws/commands.js index 62031f5d32..22c0520f45 100644 --- a/lib/aws/commands.js +++ b/lib/aws/commands.js @@ -254,22 +254,6 @@ function createCommand(serviceName, methodName, params = {}) { return new CommandClass(params); } -/** - * Get all available methods for a service - * @param {string} serviceName - AWS service name - * @returns {Array} Array of method names - */ -function getServiceMethods(serviceName) { - const serviceCommands = COMMAND_MAP[serviceName]; - if (!serviceCommands) { - throw new Error(`Unknown AWS service: ${serviceName}`); - } - return Object.keys(serviceCommands); -} - module.exports = { - COMMAND_MAP, - getCommand, createCommand, - getServiceMethods, }; diff --git a/lib/aws/config.js b/lib/aws/config.js index ff07a716ec..69a351e3b2 100644 --- a/lib/aws/config.js +++ b/lib/aws/config.js @@ -140,22 +140,6 @@ function getCACertificates() { return caCerts; } -/** - * Get S3-specific client configuration - * @param {Object} baseConfig - Base client configuration - * @param {boolean} useAcceleration - Whether to use S3 transfer acceleration - * @returns {Object} S3 client configuration - */ -function buildS3Config(baseConfig = {}, useAcceleration = false) { - const s3Config = { ...baseConfig }; - - if (useAcceleration) { - s3Config.useAccelerateEndpoint = true; - } - - return s3Config; -} - /** * Check if S3 transfer acceleration should be used * @param {string} method - S3 method name @@ -172,10 +156,5 @@ function shouldUseS3Acceleration(method, params) { module.exports = { buildClientConfig, - buildHttpOptions, - buildS3Config, shouldUseS3Acceleration, - getMaxRetries, - getProxyUrl, - getCACertificates, }; diff --git a/lib/aws/error-utils.js b/lib/aws/error-utils.js index ddfd2e65b2..b1cca17af7 100644 --- a/lib/aws/error-utils.js +++ b/lib/aws/error-utils.js @@ -139,102 +139,7 @@ function isRetryableError(error) { return retryableErrors.includes(errorCode); } -/** - * Check if error indicates missing credentials - * @param {Error} error - AWS SDK error - * @returns {boolean} Whether error is due to missing credentials - */ -function isCredentialsError(error) { - if (!error) return false; - - const errorCode = error.name || error.code; - const credentialsErrors = [ - 'CredentialsError', - 'NoCredentialsError', - 'ExpiredTokenException', - 'InvalidUserID.NotFound', - 'SignatureDoesNotMatch', - ]; - - return ( - credentialsErrors.includes(errorCode) || - (error.message && error.message.includes('Missing credentials')) - ); -} - -/** - * Get a user-friendly error message for AWS errors - * @param {Error} error - AWS SDK error - * @returns {string} User-friendly error message - */ -function getFriendlyErrorMessage(error) { - if (!error) return 'Unknown error occurred'; - - const errorCode = error.name || error.code; - - // Specific error message mappings - const friendlyMessages = { - ValidationException: 'Invalid parameters provided', - AlreadyExistsException: 'Resource already exists', - NoSuchBucket: 'S3 bucket does not exist', - NoSuchKey: 'S3 object does not exist', - ResourceNotFoundException: 'AWS resource not found', - AccessDeniedException: 'Access denied - check your permissions', - ThrottlingException: 'Request rate exceeded - please retry', - CredentialsError: 'Invalid AWS credentials', - ExpiredTokenException: 'AWS credentials have expired', - }; - - return friendlyMessages[errorCode] || error.message || `AWS Error: ${errorCode}`; -} - -/** - * Extract the root cause error from a chain of errors - * @param {Error} error - Top-level error - * @returns {Error} Root cause error - */ -function getRootCauseError(error) { - let rootError = error; - - while (rootError && (rootError.originalError || rootError.cause)) { - rootError = rootError.originalError || rootError.cause; - } - - return rootError || error; -} - -/** - * Create a standardized error object from various error types - * @param {Error|string} error - Error object or message - * @param {string} service - AWS service name - * @param {string} operation - AWS operation name - * @returns {Error} Standardized error object - */ -function createStandardError(error, service, operation) { - let standardError; - - if (typeof error === 'string') { - standardError = new Error(error); - } else if (error instanceof Error) { - standardError = error; - } else { - standardError = new Error('Unknown error'); - } - - // Add context information - standardError.service = service; - standardError.operation = operation; - standardError.timestamp = new Date().toISOString(); - - return transformV3Error(standardError); -} - module.exports = { ERROR_CODE_MAPPINGS, transformV3Error, - isRetryableError, - isCredentialsError, - getFriendlyErrorMessage, - getRootCauseError, - createStandardError, }; From 1f828a90891c980a4fbade7f38a6bf1a8674c377 Mon Sep 17 00:00:00 2001 From: Matthieu Napoli Date: Wed, 30 Jul 2025 14:20:15 +0200 Subject: [PATCH 23/27] Fix URL --- lib/aws/error-utils.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/aws/error-utils.js b/lib/aws/error-utils.js index b1cca17af7..5851a08519 100644 --- a/lib/aws/error-utils.js +++ b/lib/aws/error-utils.js @@ -63,7 +63,7 @@ function transformV3Error(error) { const errorMessage = [ 'AWS provider credentials not found.', ' Learn how to set up AWS provider credentials', - ` in our docs here: <${chalk.green('http://slss.io/aws-creds-setup')}>.`, + ` in our docs here: <${chalk.green('https://github.com/oss-serverless/serverless/blob/main/docs/guides/credentials.md')}>.`, ].join(''); throw Object.assign(new ServerlessError(errorMessage, 'AWS_CREDENTIALS_NOT_FOUND'), { From 204393a39a39cf12dce99dfd68a807172dbd3af4 Mon Sep 17 00:00:00 2001 From: Matthieu Napoli Date: Wed, 30 Jul 2025 14:22:59 +0200 Subject: [PATCH 24/27] Remove useless comments --- lib/plugins/aws/provider.js | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/lib/plugins/aws/provider.js b/lib/plugins/aws/provider.js index 34dab60083..16db239733 100644 --- a/lib/plugins/aws/provider.js +++ b/lib/plugins/aws/provider.js @@ -1763,21 +1763,15 @@ class AwsProvider { */ async _requestV3(service, method, params = {}, options = {}) { try { - // Initialize client factory if not already done if (!this.clientFactory) { this.clientFactory = new AWSClientFactory(this._getV3BaseConfig()); } - // Build client configuration const clientConfig = this._buildV3ClientConfig(service, method, options); - // Create command const command = createCommand(service, method, params); - // Send request - const result = await this.clientFactory.send(service, command, clientConfig); - - return result; + return await this.clientFactory.send(service, command, clientConfig); } catch (error) { // Transform v3 error to be compatible with existing error handling throw transformV3Error(error); From 13ebae1d337780698e0f9be6afb22aa1d6c03a4f Mon Sep 17 00:00:00 2001 From: Matthieu Napoli Date: Wed, 30 Jul 2025 14:26:55 +0200 Subject: [PATCH 25/27] Fix formatting --- lib/aws/error-utils.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/aws/error-utils.js b/lib/aws/error-utils.js index 5851a08519..1292d32f0d 100644 --- a/lib/aws/error-utils.js +++ b/lib/aws/error-utils.js @@ -63,7 +63,9 @@ function transformV3Error(error) { const errorMessage = [ 'AWS provider credentials not found.', ' Learn how to set up AWS provider credentials', - ` in our docs here: <${chalk.green('https://github.com/oss-serverless/serverless/blob/main/docs/guides/credentials.md')}>.`, + ` in our docs here: <${chalk.green( + 'https://github.com/oss-serverless/serverless/blob/main/docs/guides/credentials.md' + )}>.`, ].join(''); throw Object.assign(new ServerlessError(errorMessage, 'AWS_CREDENTIALS_NOT_FOUND'), { From 26965b0b1194bcaa521485f9da7096fe7bfe05ae Mon Sep 17 00:00:00 2001 From: Matthieu Napoli Date: Mon, 4 Aug 2025 15:39:37 +0200 Subject: [PATCH 26/27] Revert changes to custom resources --- .../api-gateway-cloud-watch-role/handler.js | 45 +++++++--------- .../cognito-user-pool/lib/permissions.js | 15 ++++-- .../cognito-user-pool/lib/user-pool.js | 44 ++++++++------- .../event-bridge/lib/event-bridge.js | 53 ++++++++++--------- .../resources/event-bridge/lib/permissions.js | 17 ++++-- .../resources/s3/lib/bucket.js | 20 +++---- .../resources/s3/lib/permissions.js | 17 ++++-- 7 files changed, 113 insertions(+), 98 deletions(-) diff --git a/lib/plugins/aws/custom-resources/resources/api-gateway-cloud-watch-role/handler.js b/lib/plugins/aws/custom-resources/resources/api-gateway-cloud-watch-role/handler.js index 9ceb448ad0..58a59def90 100644 --- a/lib/plugins/aws/custom-resources/resources/api-gateway-cloud-watch-role/handler.js +++ b/lib/plugins/aws/custom-resources/resources/api-gateway-cloud-watch-role/handler.js @@ -2,15 +2,20 @@ const { wait, MAX_AWS_REQUEST_TRY } = require('../utils'); const { getEnvironment, handlerWrapper } = require('../utils'); -const { GetAccountCommand, UpdateAccountCommand } = require('@aws-sdk/client-api-gateway'); const { + APIGatewayClient, + GetAccountCommand, + UpdateAccountCommand, +} = require('@aws-sdk/client-api-gateway'); +const { + IAMClient, ListAttachedRolePoliciesCommand, CreateRoleCommand, AttachRolePolicyCommand, } = require('@aws-sdk/client-iam'); -const AWSClientFactory = require('../../../../../aws/client-factory'); -const awsFactory = new AWSClientFactory({ maxAttempts: MAX_AWS_REQUEST_TRY }); +const apiGateway = new APIGatewayClient({ maxAttempts: MAX_AWS_REQUEST_TRY }); +const iam = new IAMClient({ maxAttempts: MAX_AWS_REQUEST_TRY }); async function handler(event, context) { if (event.RequestType === 'Create') { @@ -27,9 +32,10 @@ async function create(event, context) { const { RoleArn } = event.ResourceProperties; const { Partition: partition, AccountId: accountId, Region: region } = getEnvironment(context); - const assignedRoleArn = ( - await awsFactory.send('APIGateway', new GetAccountCommand({}), { region }) - ).cloudwatchRoleArn; + apiGateway.config.region = () => region; + iam.config.region = () => region; + + const assignedRoleArn = (await apiGateway.send(new GetAccountCommand({}))).cloudwatchRoleArn; let roleArn = `arn:${partition}:iam::${accountId}:role/serverlessApiGatewayCloudWatchRole`; if (RoleArn) { @@ -43,18 +49,12 @@ async function create(event, context) { const attachedPolicies = await (async () => { try { - return ( - await awsFactory.send( - 'IAM', - new ListAttachedRolePoliciesCommand({ RoleName: roleName }), - { region } - ) - ).AttachedPolicies; + return (await iam.send(new ListAttachedRolePoliciesCommand({ RoleName: roleName }))) + .AttachedPolicies; } catch (error) { if (error.code === 'NoSuchEntity') { // Role doesn't exist yet, create; - await awsFactory.send( - 'IAM', + await iam.send( new CreateRoleCommand({ AssumeRolePolicyDocument: JSON.stringify({ Version: '2012-10-17', @@ -70,8 +70,7 @@ async function create(event, context) { }), Path: '/', RoleName: roleName, - }), - { region } + }) ); return []; } @@ -84,13 +83,11 @@ async function create(event, context) { (policy) => policy.PolicyArn === apiGatewayPushToCloudWatchLogsPolicyArn ) ) { - await awsFactory.send( - 'IAM', + await iam.send( new AttachRolePolicyCommand({ PolicyArn: apiGatewayPushToCloudWatchLogsPolicyArn, RoleName: roleName, - }), - { region } + }) ); } } @@ -100,8 +97,7 @@ async function create(event, context) { const updateAccount = async (counter = 1) => { try { - await awsFactory.send( - 'APIGateway', + await apiGateway.send( new UpdateAccountCommand({ patchOperations: [ { @@ -110,8 +106,7 @@ async function create(event, context) { value: roleArn, }, ], - }), - { region } + }) ); } catch (error) { if (counter < 10) { diff --git a/lib/plugins/aws/custom-resources/resources/cognito-user-pool/lib/permissions.js b/lib/plugins/aws/custom-resources/resources/cognito-user-pool/lib/permissions.js index 7b9e5c8b0f..e9c817dce1 100644 --- a/lib/plugins/aws/custom-resources/resources/cognito-user-pool/lib/permissions.js +++ b/lib/plugins/aws/custom-resources/resources/cognito-user-pool/lib/permissions.js @@ -1,10 +1,13 @@ 'use strict'; const { MAX_AWS_REQUEST_TRY } = require('../../utils'); -const { AddPermissionCommand, RemovePermissionCommand } = require('@aws-sdk/client-lambda'); -const AWSClientFactory = require('../../../../../../aws/client-factory'); +const { + LambdaClient, + AddPermissionCommand, + RemovePermissionCommand, +} = require('@aws-sdk/client-lambda'); -const awsFactory = new AWSClientFactory({ maxAttempts: MAX_AWS_REQUEST_TRY }); +const lambda = new LambdaClient({ maxAttempts: MAX_AWS_REQUEST_TRY }); function getStatementId(functionName, userPoolName) { const normalizedUserPoolName = userPoolName.toLowerCase().replace(/[.:*\s]/g, ''); @@ -17,6 +20,7 @@ function getStatementId(functionName, userPoolName) { async function addPermission(config) { const { functionName, userPoolName, partition, region, accountId, userPoolId } = config; + lambda.config.region = () => region; const payload = { Action: 'lambda:InvokeFunction', @@ -25,16 +29,17 @@ async function addPermission(config) { StatementId: getStatementId(functionName, userPoolName), SourceArn: `arn:${partition}:cognito-idp:${region}:${accountId}:userpool/${userPoolId}`, }; - return awsFactory.send('Lambda', new AddPermissionCommand(payload), { region }); + return lambda.send(new AddPermissionCommand(payload)); } async function removePermission(config) { const { functionName, userPoolName, region } = config; + lambda.config.region = () => region; const payload = { FunctionName: functionName, StatementId: getStatementId(functionName, userPoolName), }; - return awsFactory.send('Lambda', new RemovePermissionCommand(payload), { region }); + return lambda.send(new RemovePermissionCommand(payload)); } module.exports = { diff --git a/lib/plugins/aws/custom-resources/resources/cognito-user-pool/lib/user-pool.js b/lib/plugins/aws/custom-resources/resources/cognito-user-pool/lib/user-pool.js index 4785644a69..724911a15d 100644 --- a/lib/plugins/aws/custom-resources/resources/cognito-user-pool/lib/user-pool.js +++ b/lib/plugins/aws/custom-resources/resources/cognito-user-pool/lib/user-pool.js @@ -2,13 +2,13 @@ const { MAX_AWS_REQUEST_TRY } = require('../../utils'); const { + CognitoIdentityProviderClient, ListUserPoolsCommand, DescribeUserPoolCommand, UpdateUserPoolCommand, } = require('@aws-sdk/client-cognito-identity-provider'); -const AWSClientFactory = require('../../../../../../aws/client-factory'); -const awsFactory = new AWSClientFactory({ maxAttempts: MAX_AWS_REQUEST_TRY }); +const cognito = new CognitoIdentityProviderClient({ maxAttempts: MAX_AWS_REQUEST_TRY }); const customSenderSources = ['CustomSMSSender', 'CustomEmailSender']; @@ -45,18 +45,18 @@ async function findUserPoolByName(config) { MaxResults: 60, }; + cognito.config.region = () => region; + async function recursiveFind(nextToken) { if (nextToken) payload.NextToken = nextToken; - return awsFactory - .send('CognitoIdentityProvider', new ListUserPoolsCommand(payload), { region }) - .then((result) => { - const matches = result.UserPools.filter((pool) => pool.Name === userPoolName); - if (matches.length) { - return matches.shift(); - } - if (result.NextToken) return recursiveFind(result.NextToken); - return null; - }); + return cognito.send(new ListUserPoolsCommand(payload)).then((result) => { + const matches = result.UserPools.filter((pool) => pool.Name === userPoolName); + if (matches.length) { + return matches.shift(); + } + if (result.NextToken) return recursiveFind(result.NextToken); + return null; + }); } return recursiveFind(); @@ -65,18 +65,18 @@ async function findUserPoolByName(config) { async function getConfiguration(config) { const { region } = config; + cognito.config.region = () => region; + return findUserPoolByName(config).then((userPool) => - awsFactory.send( - 'CognitoIdentityProvider', - new DescribeUserPoolCommand({ UserPoolId: userPool.Id }), - { region } - ) + cognito.send(new DescribeUserPoolCommand({ UserPoolId: userPool.Id })) ); } async function updateConfiguration(config) { const { lambdaArn, userPoolConfigs, region } = config; + cognito.config.region = () => region; + return getConfiguration(config).then((res) => { const UserPoolId = res.UserPool.Id; let { LambdaConfig } = res.UserPool; @@ -102,15 +102,15 @@ async function updateConfiguration(config) { LambdaConfig, }); - return awsFactory.send('CognitoIdentityProvider', new UpdateUserPoolCommand(updatedConfig), { - region, - }); + return cognito.send(new UpdateUserPoolCommand(updatedConfig)); }); } async function removeConfiguration(config) { const { lambdaArn, region } = config; + cognito.config.region = () => region; + return getConfiguration(config).then((res) => { const UserPoolId = res.UserPool.Id; let { LambdaConfig } = res.UserPool; @@ -124,9 +124,7 @@ async function removeConfiguration(config) { LambdaConfig, }); - return awsFactory.send('CognitoIdentityProvider', new UpdateUserPoolCommand(updatedConfig), { - region, - }); + return cognito.send(new UpdateUserPoolCommand(updatedConfig)); }); } diff --git a/lib/plugins/aws/custom-resources/resources/event-bridge/lib/event-bridge.js b/lib/plugins/aws/custom-resources/resources/event-bridge/lib/event-bridge.js index 29f8d4d874..4b47777fc1 100644 --- a/lib/plugins/aws/custom-resources/resources/event-bridge/lib/event-bridge.js +++ b/lib/plugins/aws/custom-resources/resources/event-bridge/lib/event-bridge.js @@ -3,6 +3,7 @@ const { MAX_AWS_REQUEST_TRY } = require('../../utils'); const { getEventBusName, getEventBusTargetId } = require('./utils'); const { + EventBridgeClient, CreateEventBusCommand, DeleteEventBusCommand, PutRuleCommand, @@ -10,23 +11,22 @@ const { PutTargetsCommand, RemoveTargetsCommand, } = require('@aws-sdk/client-eventbridge'); -const AWSClientFactory = require('../../../../../../aws/client-factory'); -const awsFactory = new AWSClientFactory({ maxAttempts: MAX_AWS_REQUEST_TRY }); +const eventBridge = new EventBridgeClient({ maxAttempts: MAX_AWS_REQUEST_TRY }); async function createEventBus(config) { const { eventBus, region } = config; + eventBridge.config.region = () => region; + if (eventBus) { if (eventBus.startsWith('arn')) { return Promise.resolve(); } - return awsFactory.send( - 'EventBridge', + return eventBridge.send( new CreateEventBusCommand({ Name: eventBus, - }), - { region } + }) ); } return Promise.resolve(); @@ -35,17 +35,17 @@ async function createEventBus(config) { async function deleteEventBus(config) { const { eventBus, region } = config; + eventBridge.config.region = () => region; + if (eventBus) { if (eventBus.startsWith('arn')) { return Promise.resolve(); } - return awsFactory.send( - 'EventBridge', + return eventBridge.send( new DeleteEventBusCommand({ Name: eventBus, - }), - { region } + }) ); } return Promise.resolve(); @@ -54,39 +54,41 @@ async function deleteEventBus(config) { async function updateRuleConfiguration(config) { const { ruleName, eventBus, pattern, schedule, region, state } = config; + eventBridge.config.region = () => region; + const EventBusName = getEventBusName(eventBus); - return awsFactory.send( - 'EventBridge', + return eventBridge.send( new PutRuleCommand({ Name: ruleName, EventBusName, EventPattern: JSON.stringify(pattern), ScheduleExpression: schedule, State: state, - }), - { region } + }) ); } async function removeRuleConfiguration(config) { const { ruleName, eventBus, region } = config; + eventBridge.config.region = () => region; + const EventBusName = getEventBusName(eventBus); - return awsFactory.send( - 'EventBridge', + return eventBridge.send( new DeleteRuleCommand({ Name: ruleName, EventBusName, - }), - { region } + }) ); } async function updateTargetConfiguration(config) { const { lambdaArn, ruleName, eventBus, input, inputPath, inputTransformer, region } = config; + eventBridge.config.region = () => region; + const EventBusName = getEventBusName(eventBus); let target = { @@ -103,30 +105,29 @@ async function updateTargetConfiguration(config) { } return removeTargetConfiguration(config).then(() => - awsFactory.send( - 'EventBridge', + eventBridge.send( new PutTargetsCommand({ Rule: ruleName, EventBusName, Targets: [target], - }), - { region } + }) ) ); } async function removeTargetConfiguration(config) { const { ruleName, eventBus, region } = config; + const EventBusName = getEventBusName(eventBus); - return awsFactory.send( - 'EventBridge', + eventBridge.config.region = () => region; + + return eventBridge.send( new RemoveTargetsCommand({ Ids: [getEventBusTargetId(ruleName)], Rule: ruleName, EventBusName, - }), - { region } + }) ); } diff --git a/lib/plugins/aws/custom-resources/resources/event-bridge/lib/permissions.js b/lib/plugins/aws/custom-resources/resources/event-bridge/lib/permissions.js index b89d249cdd..6e8432facf 100644 --- a/lib/plugins/aws/custom-resources/resources/event-bridge/lib/permissions.js +++ b/lib/plugins/aws/custom-resources/resources/event-bridge/lib/permissions.js @@ -2,10 +2,13 @@ const { MAX_AWS_REQUEST_TRY } = require('../../utils'); const { getEventBusName } = require('./utils'); -const { AddPermissionCommand, RemovePermissionCommand } = require('@aws-sdk/client-lambda'); -const AWSClientFactory = require('../../../../../../aws/client-factory'); +const { + LambdaClient, + AddPermissionCommand, + RemovePermissionCommand, +} = require('@aws-sdk/client-lambda'); -const awsFactory = new AWSClientFactory({ maxAttempts: MAX_AWS_REQUEST_TRY }); +const lambda = new LambdaClient({ maxAttempts: MAX_AWS_REQUEST_TRY }); function getStatementId(functionName, ruleName) { const normalizedRuleName = ruleName.toLowerCase().replace(/[.:*]/g, ''); @@ -19,6 +22,8 @@ function getStatementId(functionName, ruleName) { async function addPermission(config) { const { functionName, partition, region, accountId, eventBus, ruleName } = config; + lambda.config.region = () => region; + let SourceArn = `arn:${partition}:events:${region}:${accountId}:rule/${ruleName}`; if (eventBus) { const eventBusName = getEventBusName(eventBus); @@ -32,18 +37,20 @@ async function addPermission(config) { SourceArn, }; - return awsFactory.send('Lambda', new AddPermissionCommand(payload), { region }); + return lambda.send(new AddPermissionCommand(payload)); } async function removePermission(config) { const { functionName, region, ruleName } = config; + lambda.config.region = () => region; + const payload = { FunctionName: functionName, StatementId: getStatementId(functionName, ruleName), }; - return awsFactory.send('Lambda', new RemovePermissionCommand(payload), { region }); + return lambda.send(new RemovePermissionCommand(payload)); } module.exports = { diff --git a/lib/plugins/aws/custom-resources/resources/s3/lib/bucket.js b/lib/plugins/aws/custom-resources/resources/s3/lib/bucket.js index be7013f0e2..6349816cb4 100644 --- a/lib/plugins/aws/custom-resources/resources/s3/lib/bucket.js +++ b/lib/plugins/aws/custom-resources/resources/s3/lib/bucket.js @@ -3,12 +3,12 @@ const crypto = require('crypto'); const { MAX_AWS_REQUEST_TRY } = require('../../utils'); const { + S3Client, GetBucketNotificationConfigurationCommand, PutBucketNotificationConfigurationCommand, } = require('@aws-sdk/client-s3'); -const AWSClientFactory = require('../../../../../../aws/client-factory'); -const awsFactory = new AWSClientFactory({ maxAttempts: MAX_AWS_REQUEST_TRY }); +const s3 = new S3Client({ maxAttempts: MAX_AWS_REQUEST_TRY }); function generateId(functionName, bucketConfig) { const md5 = crypto.createHash('md5').update(JSON.stringify(bucketConfig)).digest('hex'); @@ -39,17 +39,21 @@ function createFilter(config) { async function getConfiguration(config) { const { bucketName, region } = config; + s3.config.region = () => region; + const Bucket = bucketName; const payload = { Bucket, }; - return awsFactory.send('S3', new GetBucketNotificationConfigurationCommand(payload), { region }); + return s3.send(new GetBucketNotificationConfigurationCommand(payload)); } async function updateConfiguration(config) { const { lambdaArn, functionName, bucketName, bucketConfigs, region } = config; + s3.config.region = () => region; + const Bucket = bucketName; return getConfiguration(config).then((NotificationConfiguration) => { @@ -93,15 +97,15 @@ async function updateConfiguration(config) { Bucket, NotificationConfiguration, }; - return awsFactory.send('S3', new PutBucketNotificationConfigurationCommand(payload), { - region, - }); + return s3.send(new PutBucketNotificationConfigurationCommand(payload)); }); } async function removeConfiguration(config) { const { functionName, bucketName, region } = config; + s3.config.region = () => region; + const Bucket = bucketName; return getConfiguration(config).then((NotificationConfiguration) => { @@ -120,9 +124,7 @@ async function removeConfiguration(config) { Bucket, NotificationConfiguration, }; - return awsFactory.send('S3', new PutBucketNotificationConfigurationCommand(payload), { - region, - }); + return s3.send(new PutBucketNotificationConfigurationCommand(payload)); }); } diff --git a/lib/plugins/aws/custom-resources/resources/s3/lib/permissions.js b/lib/plugins/aws/custom-resources/resources/s3/lib/permissions.js index 6fa9bef927..0689bbd3b4 100644 --- a/lib/plugins/aws/custom-resources/resources/s3/lib/permissions.js +++ b/lib/plugins/aws/custom-resources/resources/s3/lib/permissions.js @@ -1,10 +1,13 @@ 'use strict'; const { MAX_AWS_REQUEST_TRY } = require('../../utils'); -const { AddPermissionCommand, RemovePermissionCommand } = require('@aws-sdk/client-lambda'); -const AWSClientFactory = require('../../../../../../aws/client-factory'); +const { + LambdaClient, + AddPermissionCommand, + RemovePermissionCommand, +} = require('@aws-sdk/client-lambda'); -const awsFactory = new AWSClientFactory({ maxAttempts: MAX_AWS_REQUEST_TRY }); +const lambda = new LambdaClient({ maxAttempts: MAX_AWS_REQUEST_TRY }); function getStatementId(functionName, bucketName) { const normalizedBucketName = bucketName.replace(/[.:*]/g, ''); @@ -18,6 +21,8 @@ function getStatementId(functionName, bucketName) { async function addPermission(config) { const { functionName, bucketName, partition, region, accountId } = config; + lambda.config.region = () => region; + const payload = { Action: 'lambda:InvokeFunction', FunctionName: functionName, @@ -27,17 +32,19 @@ async function addPermission(config) { SourceAccount: accountId, }; - return awsFactory.send('Lambda', new AddPermissionCommand(payload), { region }); + return lambda.send(new AddPermissionCommand(payload)); } async function removePermission(config) { const { functionName, bucketName, region } = config; + lambda.config.region = () => region; + const payload = { FunctionName: functionName, StatementId: getStatementId(functionName, bucketName), }; - return awsFactory.send('Lambda', new RemovePermissionCommand(payload), { region }); + return lambda.send(new RemovePermissionCommand(payload)); } module.exports = { From 57ef13acf50628e9d3656beeffe94dac7870df51 Mon Sep 17 00:00:00 2001 From: Matthieu Napoli Date: Mon, 4 Aug 2025 15:44:24 +0200 Subject: [PATCH 27/27] Fix workflow config --- .github/workflows/integrate.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/integrate.yml b/.github/workflows/integrate.yml index e8a201addd..2c73af0e43 100644 --- a/.github/workflows/integrate.yml +++ b/.github/workflows/integrate.yml @@ -134,7 +134,7 @@ jobs: integrate: name: Integrate runs-on: ubuntu-latest - needs: [linuxNode22, windowsNode22, linuxNode16] + needs: [linuxNode22, windowsNode16, linuxNode16] timeout-minutes: 30 # Default is 360 env: AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}