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' diff --git a/lib/aws/client-factory.js b/lib/aws/client-factory.js new file mode 100644 index 0000000000..deec89815d --- /dev/null +++ b/lib/aws/client-factory.js @@ -0,0 +1,76 @@ +'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'); +const { STSClient } = require('@aws-sdk/client-sts'); + +// 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, + STS: STSClient, +}; + +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); + } +} + +module.exports = AWSClientFactory; diff --git a/lib/aws/commands.js b/lib/aws/commands.js new file mode 100644 index 0000000000..22c0520f45 --- /dev/null +++ b/lib/aws/commands.js @@ -0,0 +1,259 @@ +'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, + DescribeStackEventsCommand, + ListExportsCommand, +} = 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, + GetAuthorizationTokenCommand, + CreateRepositoryCommand, + DescribeImagesCommand, +} = 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'); + +// STS Commands +const { GetCallerIdentityCommand } = require('@aws-sdk/client-sts'); + +/** + * 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, + describeStackEvents: DescribeStackEventsCommand, + listExports: ListExportsCommand, + }, + + CloudWatch: { + getMetricStatistics: GetMetricStatisticsCommand, + }, + + CloudWatchLogs: { + describeLogStreams: DescribeLogStreamsCommand, + filterLogEvents: FilterLogEventsCommand, + deleteSubscriptionFilter: DeleteSubscriptionFilterCommand, + }, + + CognitoIdentityProvider: { + listUserPools: ListUserPoolsCommand, + describeUserPool: DescribeUserPoolCommand, + updateUserPool: UpdateUserPoolCommand, + }, + + ECR: { + deleteRepository: DeleteRepositoryCommand, + describeRepositories: DescribeRepositoriesCommand, + getAuthorizationToken: GetAuthorizationTokenCommand, + createRepository: CreateRepositoryCommand, + describeImages: DescribeImagesCommand, + }, + + 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, + }, + + STS: { + getCallerIdentity: GetCallerIdentityCommand, + }, +}; + +/** + * 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); +} + +module.exports = { + createCommand, +}; diff --git a/lib/aws/config.js b/lib/aws/config.js new file mode 100644 index 0000000000..69a351e3b2 --- /dev/null +++ b/lib/aws/config.js @@ -0,0 +1,160 @@ +'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; +} + +/** + * 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, + shouldUseS3Acceleration, +}; diff --git a/lib/aws/error-utils.js b/lib/aws/error-utils.js new file mode 100644 index 0000000000..1292d32f0d --- /dev/null +++ b/lib/aws/error-utils.js @@ -0,0 +1,147 @@ +'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; + } + + // 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( + 'https://github.com/oss-serverless/serverless/blob/main/docs/guides/credentials.md' + )}>.`, + ].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'); + + // 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); +} + +module.exports = { + ERROR_CODE_MAPPINGS, + transformV3Error, +}; 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..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,20 +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'); -const apiGateway = new APIGatewayClient({ maxAttempts: MAX_AWS_REQUEST_TRY }); -const iam = new IAMClient({ maxAttempts: MAX_AWS_REQUEST_TRY }); +const awsFactory = new AWSClientFactory({ maxAttempts: MAX_AWS_REQUEST_TRY }); async function handler(event, context) { if (event.RequestType === 'Create') { @@ -32,10 +27,9 @@ 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 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) { @@ -49,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', @@ -70,7 +70,8 @@ async function create(event, context) { }), Path: '/', RoleName: roleName, - }) + }), + { region } ); return []; } @@ -83,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 } ); } } @@ -97,7 +100,8 @@ async function create(event, context) { const updateAccount = async (counter = 1) => { try { - await apiGateway.send( + await awsFactory.send( + 'APIGateway', new UpdateAccountCommand({ patchOperations: [ { @@ -106,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 e9c817dce1..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,13 +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'); -const lambda = new LambdaClient({ 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, ''); @@ -20,7 +17,6 @@ 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', @@ -29,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; - lambda.config.region = () => 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 724911a15d..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,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 cognito = new CognitoIdentityProviderClient({ maxAttempts: MAX_AWS_REQUEST_TRY }); +const awsFactory = new AWSClientFactory({ 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 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(); @@ -65,18 +65,18 @@ async function findUserPoolByName(config) { async function getConfiguration(config) { const { region } = config; - cognito.config.region = () => 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; - 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 cognito.send(new UpdateUserPoolCommand(updatedConfig)); + return awsFactory.send('CognitoIdentityProvider', new UpdateUserPoolCommand(updatedConfig), { + region, + }); }); } 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,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 4b47777fc1..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,22 +10,23 @@ const { PutTargetsCommand, RemoveTargetsCommand, } = require('@aws-sdk/client-eventbridge'); +const AWSClientFactory = require('../../../../../../aws/client-factory'); -const eventBridge = new EventBridgeClient({ maxAttempts: MAX_AWS_REQUEST_TRY }); +const awsFactory = new AWSClientFactory({ 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 eventBridge.send( + return awsFactory.send( + 'EventBridge', 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 eventBridge.send( + return awsFactory.send( + 'EventBridge', new DeleteEventBusCommand({ Name: eventBus, - }) + }), + { region } ); } return Promise.resolve(); @@ -54,41 +54,39 @@ 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 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; - eventBridge.config.region = () => 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; - eventBridge.config.region = () => region; - const EventBusName = getEventBusName(eventBus); let target = { @@ -105,29 +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 EventBusName = getEventBusName(eventBus); - eventBridge.config.region = () => region; - - 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 6e8432facf..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,13 +2,10 @@ const { MAX_AWS_REQUEST_TRY } = require('../../utils'); const { getEventBusName } = 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'); -const lambda = new LambdaClient({ maxAttempts: MAX_AWS_REQUEST_TRY }); +const awsFactory = new AWSClientFactory({ maxAttempts: MAX_AWS_REQUEST_TRY }); function getStatementId(functionName, ruleName) { const normalizedRuleName = ruleName.toLowerCase().replace(/[.:*]/g, ''); @@ -22,8 +19,6 @@ 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); @@ -37,20 +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; - lambda.config.region = () => 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 6349816cb4..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,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 s3 = new S3Client({ 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'); @@ -39,21 +39,17 @@ function createFilter(config) { async function getConfiguration(config) { const { bucketName, region } = config; - s3.config.region = () => 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; - s3.config.region = () => region; - const Bucket = bucketName; return getConfiguration(config).then((NotificationConfiguration) => { @@ -97,15 +93,15 @@ 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; - s3.config.region = () => region; - const Bucket = bucketName; return getConfiguration(config).then((NotificationConfiguration) => { @@ -124,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 0689bbd3b4..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,13 +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'); -const lambda = new LambdaClient({ maxAttempts: MAX_AWS_REQUEST_TRY }); +const awsFactory = new AWSClientFactory({ maxAttempts: MAX_AWS_REQUEST_TRY }); function getStatementId(functionName, bucketName) { const normalizedBucketName = bucketName.replace(/[.:*]/g, ''); @@ -21,8 +18,6 @@ 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, @@ -32,19 +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; - lambda.config.region = () => 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 = { diff --git a/lib/plugins/aws/deploy/lib/check-for-changes.js b/lib/plugins/aws/deploy/lib/check-for-changes.js index 704e85c99f..f2603dbff6 100644 --- a/lib/plugins/aws/deploy/lib/check-for-changes.js +++ b/lib/plugins/aws/deploy/lib/check-for-changes.js @@ -304,12 +304,9 @@ module.exports = { const CLOUDWATCHLOG_LOG_GROUP_EVENT_PER_FUNCTION_LIMIT = 2; const response = await this.provider - .request( - 'CloudWatchLogs', - 'describeSubscriptionFilters', - { logGroupName }, - { useCache: true } - ) + .request('CloudWatchLogs', 'describeSubscriptionFilters', { + logGroupName, + }) .catch(() => ({ subscriptionFilters: [] })); if (response.subscriptionFilters.length === 0) { log.debug('no subscription filters detected'); diff --git a/lib/plugins/aws/provider.js b/lib/plugins/aws/provider.js index 36ac2f6ec7..16db239733 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 === '1'; // Feature flag for gradual rollout + this.serverless.setProvider(constants.providerName, this); this.hooks = { initialize: () => { @@ -1699,15 +1710,32 @@ class AwsProvider { } /** - * Execute an AWS request by calling the AWS SDK + * 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 + * @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); + } + + /** + * 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 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); @@ -1724,6 +1752,85 @@ class AwsProvider { return (shouldCache ? awsRequest.memoized : awsRequest)(serviceOptions, method, params); } + /** + * 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 = {}) { + try { + if (!this.clientFactory) { + this.clientFactory = new AWSClientFactory(this._getV3BaseConfig()); + } + + const clientConfig = this._buildV3ClientConfig(service, method, options); + + const command = createCommand(service, method, params); + + return await this.clientFactory.send(service, command, clientConfig); + } 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..28ce05f69d 100644 --- a/package.json +++ b/package.json @@ -22,11 +22,26 @@ }, "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-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", "ajv-formats": "^2.1.1", diff --git a/test/utils/api-gateway.js b/test/utils/api-gateway.js index 9dfa58cd45..8a0f889053 100644 --- a/test/utils/api-gateway.js +++ b/test/utils/api-gateway.js @@ -2,14 +2,46 @@ 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 === '1') { + // 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)), + }; + } + // 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 +49,7 @@ async function deleteRestApi(restApiId) { restApiId, }; - return awsRequest(APIGatewayService, 'deleteRestApi', params); + return apiGateway.deleteRestApi(params); } async function getResources(restApiId) { @@ -25,7 +57,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 +67,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..fc2bc02b92 100644 --- a/test/utils/cloudformation.js +++ b/test/utils/cloudformation.js @@ -1,7 +1,39 @@ '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 === '1') { + // 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)), + }; + } + // 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 +49,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 +67,7 @@ async function deleteStack(stack) { StackName: stack, }; - return awsRequest(CloudFormationService, 'deleteStack', params); + return cf.deleteStack(params); } async function listStackResources(stack) { @@ -45,7 +77,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 +93,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 +112,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 +120,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..9918a0bfcc 100644 --- a/test/utils/cognito.js +++ b/test/utils/cognito.js @@ -2,11 +2,63 @@ 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 === '1') { + // 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)), + }; + } + // 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 +67,15 @@ 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 }) - ); + return findUserPoolByName(name).then((pool) => cognito.deleteUserPool({ UserPoolId: pool.Id })); } async function deleteUserPoolById(poolId) { - return awsRequest(CognitoIdentityServiceProviderService, 'deleteUserPool', { + return cognito.deleteUserPool({ UserPoolId: poolId, }); } @@ -40,20 +90,18 @@ async function findUserPoolByName(name) { const pools = []; async function recursiveFind(nextToken) { if (nextToken) params.NextToken = nextToken; - return awsRequest(CognitoIdentityServiceProviderService, '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(); @@ -65,25 +113,25 @@ async function findUserPools() { const pools = []; async function recursiveFind(nextToken) { if (nextToken) params.NextToken = nextToken; - return awsRequest(CognitoIdentityServiceProviderService, '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 awsRequest(CognitoIdentityServiceProviderService, '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) { @@ -92,7 +140,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 +150,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 +162,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/dynamodb.js b/test/utils/dynamodb.js index bcd300aa22..39c85f5926 100644 --- a/test/utils/dynamodb.js +++ b/test/utils/dynamodb.js @@ -1,7 +1,29 @@ '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 === '1') { + // 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)), + }; + } + // 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 +31,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..5c2d4e0341 100644 --- a/test/utils/event-bridge.js +++ b/test/utils/event-bridge.js @@ -1,18 +1,50 @@ '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 === '1') { + // 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)), + }; + } + // 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 +52,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..3a128075fc 100644 --- a/test/utils/iot.js +++ b/test/utils/iot.js @@ -1,15 +1,51 @@ '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 === '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'); + + 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)), + }; + }, + }; + } + // 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( - (data) => { - return data.endpointAddress; - } - ); + return iot.describeEndpoint({ endpointType: 'iot:Data-ATS' }).then((data) => { + return data.endpointAddress; + }); } async function publishIotData(topic, message) { @@ -19,7 +55,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..e11dca4fe3 100644 --- a/test/utils/kinesis.js +++ b/test/utils/kinesis.js @@ -1,7 +1,39 @@ '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 === '1') { + // 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)), + }; + } + // 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 +41,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,9 +59,7 @@ async function createKinesisStream(streamName) { StreamName: streamName, }; - return awsRequest(KinesisService, 'createStream', params).then(() => - waitForKinesisStream(streamName) - ); + return kinesis.createStream(params).then(() => waitForKinesisStream(streamName)); } async function deleteKinesisStream(streamName) { @@ -37,7 +67,7 @@ async function deleteKinesisStream(streamName) { StreamName: streamName, }; - return awsRequest(KinesisService, 'deleteStream', params); + return kinesis.deleteStream(params); } async function putKinesisRecord(streamName, message) { @@ -47,7 +77,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..f1d92e475b 100644 --- a/test/utils/misc.js +++ b/test/utils/misc.js @@ -1,9 +1,30 @@ '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 === '1') { + // 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)), + }; + } + // 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 +54,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/s3.js b/test/utils/s3.js index 812fde9c0f..7fd95bc4aa 100644 --- a/test/utils/s3.js +++ b/test/utils/s3.js @@ -1,10 +1,48 @@ '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 === '1') { + // 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)), + }; + } + // 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 +56,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 +80,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 = { diff --git a/test/utils/sns.js b/test/utils/sns.js index 30263a3645..a10bfb45b0 100644 --- a/test/utils/sns.js +++ b/test/utils/sns.js @@ -1,18 +1,50 @@ '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 === '1') { + // 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)), + }; + } + // 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 +60,7 @@ async function removeSnsTopic(topicName) { TopicArn: topicArn, }; - return awsRequest(SNSService, 'deleteTopic', params); + return sns.deleteTopic(params); }); } @@ -42,7 +74,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..7c54f8ed26 100644 --- a/test/utils/sqs.js +++ b/test/utils/sqs.js @@ -1,32 +1,64 @@ '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 === '1') { + // 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)), + }; + } + // 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..260042e033 100644 --- a/test/utils/websocket.js +++ b/test/utils/websocket.js @@ -1,10 +1,45 @@ '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 === '1') { + // 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)), + }; + } + // 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 +51,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 +65,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 = {