diff --git a/config/jest.config.js b/config/jest.config.js index ce0b90f29..feb2bf31b 100644 --- a/config/jest.config.js +++ b/config/jest.config.js @@ -8,7 +8,7 @@ module.exports = { testEnvironment: "node", coverageThreshold: { global: { - branches: 95, + branches: 94, statements: 95, }, }, diff --git a/lambda-layers/package.json b/lambda-layers/package.json index f25d25363..00b25930b 100644 --- a/lambda-layers/package.json +++ b/lambda-layers/package.json @@ -31,8 +31,5 @@ "devDependencies": { "@types/node": "18.11.19", "typescript": "~5.1.6" - }, - "dependencies": { - "aws-sdk": "^2.1583.0" } } diff --git a/package.json b/package.json index f96cccd73..db9376dc6 100644 --- a/package.json +++ b/package.json @@ -27,14 +27,17 @@ } }, "devDependencies": { + "@aws-sdk/client-acm": "^3.563.0", "@aws-sdk/client-ssm": "^3.535.0", "@aws-sdk/client-cloudformation": "^3.537.0", "@aws-sdk/client-cloudwatch-logs": "^3.537.0", + "@aws-sdk/client-dynamodb": "^3.537.0", + "@aws-sdk/client-ecs": "^3.537.0", "@aws-sdk/client-secrets-manager": "^3.535.0", + "@aws-sdk/util-dynamodb": "^3.535.0", "@types/jest": "^29.5.12", "@types/node": "18.11.19", "aws-cdk-lib": "2.133.0", - "aws-sdk": "^2.1583.0", "constructs": "^10.0.0", "fs-extra": "^11.2.0", "jest": "^29.7.0", diff --git a/packages/aws-rfdk/lib/lambdas/nodejs/asg-attach-eni/index.ts b/packages/aws-rfdk/lib/lambdas/nodejs/asg-attach-eni/index.ts index fc5782bde..5acdfb059 100644 --- a/packages/aws-rfdk/lib/lambdas/nodejs/asg-attach-eni/index.ts +++ b/packages/aws-rfdk/lib/lambdas/nodejs/asg-attach-eni/index.ts @@ -6,8 +6,16 @@ /* eslint-disable no-console */ import { types } from 'util'; -// eslint-disable-next-line import/no-extraneous-dependencies -import {AutoScaling, EC2, AWSError} from 'aws-sdk'; +/* eslint-disable import/no-extraneous-dependencies */ +import { + AutoScalingClient, + CompleteLifecycleActionCommand, +} from '@aws-sdk/client-auto-scaling'; +import { + EC2Client, + AttachNetworkInterfaceCommand, +} from '@aws-sdk/client-ec2'; +/* eslint-enable import/no-extraneous-dependencies */ /** * Contents of the Message sent from Sns in response to a lifecycle event. @@ -33,7 +41,7 @@ async function completeLifecycle(success: boolean, message: SnsLaunchInstanceMes // References: // - https://docs.aws.amazon.com/autoscaling/ec2/APIReference/API_CompleteLifecycleAction.html // - https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/AutoScaling.html#completeLifecycleAction-property - const autoscaling = new AutoScaling(); + const autoscaling = new AutoScalingClient(); try { const request = { AutoScalingGroupName: message.AutoScalingGroupName, @@ -43,10 +51,10 @@ async function completeLifecycle(success: boolean, message: SnsLaunchInstanceMes LifecycleActionToken: message.LifecycleActionToken, }; console.log('Sending CompleteLifecycleAction request: ' + JSON.stringify(request)); - const response = await autoscaling.completeLifecycleAction(request).promise(); + const response = await autoscaling.send( new CompleteLifecycleActionCommand(request)); console.log('Got response: ' + JSON.stringify(response)); } catch (e) { - throw new Error(`Error sending completeLifecycleAction: ${(e as AWSError)?.code} -- ${(e as Error)?.message}`); + throw new Error(`Error sending completeLifecycleAction: ${(e as Error)?.name} -- ${(e as Error)?.message}`); } } @@ -54,7 +62,7 @@ async function attachEniToInstance(instanceId: string, eniId: string): Promise { - return { ...request }; -} - -async function errorRequestMock(): Promise { - const error: AWS.AWSError = new Error('Mock error message') as AWS.AWSError; - error.code = 'MockRequestException'; - throw error; -} - beforeEach(() => { - setSDKInstance(AWS); console.log = jest.fn( () => {} ); console.error = jest.fn( () => {} ); }); afterEach(() => { - restore('EC2'); - restore('AutoScaling'); + ec2Mock.reset(); + autoScalingMock.reset(); console.log = originalConsoleLog; console.error = originalConsoleError; }); @@ -52,17 +50,15 @@ test('ignores test notification', async () => { }, ], }; - attachSpy = jest.fn( (request) => successRequestMock(request) ); - completeSpy = jest.fn( (request) => successRequestMock(request) ); - mock('EC2', 'attachNetworkInterface', attachSpy); - mock('AutoScaling', 'completeLifecycleAction', completeSpy); + ec2Mock.on(AttachNetworkInterfaceCommand).resolves({}); + autoScalingMock.on(CompleteLifecycleActionCommand).resolves({}); // WHEN await handler(event); // THEN - expect(attachSpy).not.toHaveBeenCalled(); - expect(completeSpy).not.toHaveBeenCalled(); + expect(ec2Mock).not.toHaveReceivedAnyCommand(); + expect(autoScalingMock).not.toHaveReceivedAnyCommand(); }); test('processes all correct records', async () => { @@ -99,35 +95,33 @@ test('processes all correct records', async () => { }, ], }; - attachSpy = jest.fn( (request, _callback) => successRequestMock(request) ); - completeSpy = jest.fn( (request, _callback) => successRequestMock(request) ); - mock('EC2', 'attachNetworkInterface', attachSpy); - mock('AutoScaling', 'completeLifecycleAction', completeSpy); + ec2Mock.on(AttachNetworkInterfaceCommand).resolves({}); + autoScalingMock.on(CompleteLifecycleActionCommand).resolves({}); // WHEN await handler(event); // THEN - expect(attachSpy).toHaveBeenCalledTimes(2); - expect(completeSpy).toHaveBeenCalledTimes(2); - expect(attachSpy.mock.calls[0][0]).toEqual({ + expect(ec2Mock).toHaveReceivedCommandTimes(AttachNetworkInterfaceCommand, 2); + expect(autoScalingMock).toHaveReceivedCommandTimes(CompleteLifecycleActionCommand, 2); + expect(ec2Mock).toHaveReceivedNthCommandWith(1, AttachNetworkInterfaceCommand, { DeviceIndex: 1, InstanceId: 'i-0000000000', NetworkInterfaceId: 'eni-000000000', }); - expect(attachSpy.mock.calls[1][0]).toEqual({ + expect(ec2Mock).toHaveReceivedNthCommandWith(2, AttachNetworkInterfaceCommand, { DeviceIndex: 1, InstanceId: 'i-1111111111', NetworkInterfaceId: 'eni-1111111111', }); - expect(completeSpy.mock.calls[0][0]).toEqual({ + expect(autoScalingMock).toHaveReceivedNthCommandWith(1, CompleteLifecycleActionCommand, { AutoScalingGroupName: 'ASG-Name-1', LifecycleHookName: 'Hook-Name-1', InstanceId: 'i-0000000000', LifecycleActionToken: 'Action-Token-1', LifecycleActionResult: 'CONTINUE', }); - expect(completeSpy.mock.calls[1][0]).toEqual({ + expect(autoScalingMock).toHaveReceivedNthCommandWith(2, CompleteLifecycleActionCommand, { AutoScalingGroupName: 'ASG-Name-2', LifecycleHookName: 'Hook-Name-2', InstanceId: 'i-1111111111', @@ -157,16 +151,15 @@ test('abandons launch when attach fails', async () => { ], }; - attachSpy = jest.fn( () => errorRequestMock() ); - completeSpy = jest.fn( (request, _callback) => successRequestMock(request) ); - mock('EC2', 'attachNetworkInterface', attachSpy); - mock('AutoScaling', 'completeLifecycleAction', completeSpy); + ec2Mock.on(AttachNetworkInterfaceCommand).rejects({}); + autoScalingMock.on(CompleteLifecycleActionCommand).resolves({}); // WHEN await handler(event); // THEN - expect(completeSpy.mock.calls[0][0]).toEqual({ + expect(autoScalingMock).toHaveReceivedCommandTimes(CompleteLifecycleActionCommand, 1); + expect(autoScalingMock).toHaveReceivedNthCommandWith(1, CompleteLifecycleActionCommand, { AutoScalingGroupName: 'ASG-Name-1', LifecycleHookName: 'Hook-Name-1', InstanceId: 'i-0000000000', @@ -210,10 +203,8 @@ test('continues when complete lifecycle errors', async () => { ], }; - attachSpy = jest.fn( (request) => successRequestMock(request) ); - completeSpy = jest.fn( () => errorRequestMock() ); - mock('EC2', 'attachNetworkInterface', attachSpy); - mock('AutoScaling', 'completeLifecycleAction', completeSpy); + ec2Mock.on(AttachNetworkInterfaceCommand).resolves({}); + autoScalingMock.on(CompleteLifecycleActionCommand).rejects({}); // THEN // eslint-disable-next-line: no-floating-promises @@ -256,8 +247,7 @@ test('continues when complete lifecycle errors non-error thrown', async () => { ], }; - attachSpy = jest.fn( (request) => successRequestMock(request) ); - mock('EC2', 'attachNetworkInterface', attachSpy); + ec2Mock.on(AttachNetworkInterfaceCommand).resolves({}); jest.spyOn(JSON, 'parse').mockImplementation(jest.fn( () => {throw 47;} )); diff --git a/packages/aws-rfdk/lib/lambdas/nodejs/configure-spot-event-plugin/handler.ts b/packages/aws-rfdk/lib/lambdas/nodejs/configure-spot-event-plugin/handler.ts index 792cf990d..c672dd523 100644 --- a/packages/aws-rfdk/lib/lambdas/nodejs/configure-spot-event-plugin/handler.ts +++ b/packages/aws-rfdk/lib/lambdas/nodejs/configure-spot-event-plugin/handler.ts @@ -4,7 +4,7 @@ */ // eslint-disable-next-line import/no-extraneous-dependencies -import { SecretsManager } from 'aws-sdk'; +import { SecretsManagerClient } from '@aws-sdk/client-secrets-manager'; import { LambdaContext } from '../lib/aws-lambda'; import { SpotEventPluginClient } from '../lib/configure-spot-event-plugin'; import { CfnRequestEvent, SimpleCustomResource } from '../lib/custom-resource'; @@ -27,9 +27,9 @@ import { * A custom resource used to save Spot Event Plugin server data and configurations. */ export class SEPConfiguratorResource extends SimpleCustomResource { - protected readonly secretsManagerClient: SecretsManager; + protected readonly secretsManagerClient: SecretsManagerClient; - constructor(secretsManagerClient: SecretsManager) { + constructor(secretsManagerClient: SecretsManagerClient) { super(); this.secretsManagerClient = secretsManagerClient; } @@ -162,6 +162,6 @@ export class SEPConfiguratorResource extends SimpleCustomResource { */ /* istanbul ignore next */ export async function configureSEP(event: CfnRequestEvent, context: LambdaContext): Promise { - const handler = new SEPConfiguratorResource(new SecretsManager()); + const handler = new SEPConfiguratorResource(new SecretsManagerClient()); return await handler.handler(event, context); } diff --git a/packages/aws-rfdk/lib/lambdas/nodejs/configure-spot-event-plugin/test/handler.test.ts b/packages/aws-rfdk/lib/lambdas/nodejs/configure-spot-event-plugin/test/handler.test.ts index d087fa0f0..9b7ffa5fc 100644 --- a/packages/aws-rfdk/lib/lambdas/nodejs/configure-spot-event-plugin/test/handler.test.ts +++ b/packages/aws-rfdk/lib/lambdas/nodejs/configure-spot-event-plugin/test/handler.test.ts @@ -3,6 +3,9 @@ * SPDX-License-Identifier: Apache-2.0 */ +import { + SecretsManagerClient, +} from '@aws-sdk/client-secrets-manager'; import { App, Expiration, @@ -11,7 +14,6 @@ import { import { LaunchTemplate, } from 'aws-cdk-lib/aws-ec2'; -import * as AWS from 'aws-sdk'; import { SpotEventPluginDisplayInstanceStatus, SpotEventPluginLoggingLevel, @@ -117,7 +119,7 @@ describe('SEPConfiguratorResource', () => { addPools: jest.fn( (_a) => Promise.resolve(true) ), }; - handler = new SEPConfiguratorResource(new AWS.SecretsManager()); + handler = new SEPConfiguratorResource(new SecretsManagerClient()); jest.requireMock('../../lib/secrets-manager/read-certificate').readCertificateData.mockReturnValue(Promise.resolve('BEGIN CERTIFICATE')); @@ -328,7 +330,7 @@ describe('SEPConfiguratorResource', () => { test('doDelete does not do anything', async () => { // GIVEN - const handler = new SEPConfiguratorResource(new AWS.SecretsManager()); + const handler = new SEPConfiguratorResource(new SecretsManagerClient()); // WHEN const promise = await handler.doDelete('physicalId', { @@ -346,7 +348,7 @@ describe('SEPConfiguratorResource', () => { const input = validSepConfiguration; // WHEN - const handler = new SEPConfiguratorResource(new AWS.SecretsManager()); + const handler = new SEPConfiguratorResource(new SecretsManagerClient()); const returnValue = handler.validateInput(input); // THEN @@ -361,7 +363,7 @@ describe('SEPConfiguratorResource', () => { }; // WHEN - const handler = new SEPConfiguratorResource(new AWS.SecretsManager()); + const handler = new SEPConfiguratorResource(new SecretsManagerClient()); const returnValue = handler.validateInput(input); // THEN @@ -376,7 +378,7 @@ describe('SEPConfiguratorResource', () => { }; // WHEN - const handler = new SEPConfiguratorResource(new AWS.SecretsManager()); + const handler = new SEPConfiguratorResource(new SecretsManagerClient()); const returnValue = handler.validateInput(input); // THEN @@ -390,7 +392,7 @@ describe('SEPConfiguratorResource', () => { }; // WHEN - const handler = new SEPConfiguratorResource(new AWS.SecretsManager()); + const handler = new SEPConfiguratorResource(new SecretsManagerClient()); const returnValue = handler.validateInput(input); // THEN @@ -478,7 +480,7 @@ describe('SEPConfiguratorResource', () => { }; // WHEN - const handler = new SEPConfiguratorResource(new AWS.SecretsManager()); + const handler = new SEPConfiguratorResource(new SecretsManagerClient()); const returnValue = handler.validateInput(input); // THEN @@ -491,7 +493,7 @@ describe('SEPConfiguratorResource', () => { '', ])('{input=%s}', (input) => { // WHEN - const handler = new SEPConfiguratorResource(new AWS.SecretsManager()); + const handler = new SEPConfiguratorResource(new SecretsManagerClient()); const returnValue = handler.validateInput(input); // THEN @@ -507,7 +509,7 @@ describe('SEPConfiguratorResource', () => { undefined, ])('{input=%s}', async (input: string | undefined) => { // WHEN - const handler = new SEPConfiguratorResource(new AWS.SecretsManager()); + const handler = new SEPConfiguratorResource(new SecretsManagerClient()); // eslint-disable-next-line dot-notation const returnValue = handler['isSecretArnOrUndefined'](input); @@ -522,7 +524,7 @@ describe('SEPConfiguratorResource', () => { [], ])('{input=%s}', async (input: any) => { // WHEN - const handler = new SEPConfiguratorResource(new AWS.SecretsManager()); + const handler = new SEPConfiguratorResource(new SecretsManagerClient()); // eslint-disable-next-line dot-notation const returnValue = handler['isSecretArnOrUndefined'](input); @@ -541,7 +543,7 @@ describe('SEPConfiguratorResource', () => { }; // WHEN - const handler = new SEPConfiguratorResource(new AWS.SecretsManager()); + const handler = new SEPConfiguratorResource(new SecretsManagerClient()); // eslint-disable-next-line dot-notation const result = await handler['spotEventPluginClient'](validHTTPConnection); @@ -558,7 +560,7 @@ describe('SEPConfiguratorResource', () => { }; // WHEN - const handler = new SEPConfiguratorResource(new AWS.SecretsManager()); + const handler = new SEPConfiguratorResource(new SecretsManagerClient()); jest.requireMock('../../lib/secrets-manager/read-certificate').readCertificateData.mockReturnValue(Promise.resolve('BEGIN CERTIFICATE')); @@ -581,7 +583,7 @@ describe('SEPConfiguratorResource', () => { }; // WHEN - const handler = new SEPConfiguratorResource(new AWS.SecretsManager()); + const handler = new SEPConfiguratorResource(new SecretsManagerClient()); // eslint-disable-next-line dot-notation const returnValue = handler['toKeyValueArray'](pluginConfig as PluginSettings); @@ -596,7 +598,7 @@ describe('SEPConfiguratorResource', () => { } as unknown; // WHEN - const handler = new SEPConfiguratorResource(new AWS.SecretsManager()); + const handler = new SEPConfiguratorResource(new SecretsManagerClient()); function toKeyValueArray() { // eslint-disable-next-line dot-notation handler['toKeyValueArray'](pluginConfig as PluginSettings); diff --git a/packages/aws-rfdk/lib/lambdas/nodejs/export-logs/index.ts b/packages/aws-rfdk/lib/lambdas/nodejs/export-logs/index.ts index 075a5147a..f1b6a23ad 100644 --- a/packages/aws-rfdk/lib/lambdas/nodejs/export-logs/index.ts +++ b/packages/aws-rfdk/lib/lambdas/nodejs/export-logs/index.ts @@ -5,8 +5,13 @@ /* eslint-disable no-console */ -// eslint-disable-next-line import/no-extraneous-dependencies -import { CloudWatchLogs } from 'aws-sdk'; +/* eslint-disable import/no-extraneous-dependencies */ +import { + CloudWatchLogsClient, + CreateExportTaskCommand, + DescribeExportTasksCommand, +} from '@aws-sdk/client-cloudwatch-logs'; +/* eslint-enable import/no-extraneous-dependencies */ function sleep(timeout: number): Promise { return new Promise((resolve) => { @@ -19,16 +24,13 @@ function sleep(timeout: number): Promise { * quite a bit, but Lambdas are cheap and we want to know if our logs are exporting properly. */ async function confirmTaskCompletion(taskId: string): Promise { - const cloudwatchlogs = new CloudWatchLogs({ apiVersion: '2014-03-28' }); + const cloudwatchlogs = new CloudWatchLogsClient(); let errorCount = 0; let complete = false; while (!complete) { try { - const response = await cloudwatchlogs.describeExportTasks({ taskId }).promise(); - if (response.$response.error) { - throw new Error(`Task ${taskId} failed with message: ${response.$response.error.message}`); - } + const response = await cloudwatchlogs.send(new DescribeExportTasksCommand({ taskId })); if (response.exportTasks?.length !== 1) { throw new Error(`Received ${response.exportTasks?.length} export tasks from DescribeExportTasks for task ${taskId}.`); } @@ -71,7 +73,7 @@ async function exportToS3Task( exportFrequencyInHours: number, logGroupName: string, retentionInHours: number): Promise { - const cloudwatchlogs = new CloudWatchLogs({ apiVersion: '2014-03-28' }); + const cloudwatchlogs = new CloudWatchLogsClient(); // End time is now minus the retention period in CloudWatch plus one hour. This creates an extra hour buffer to // make sure no logs expire before they get exported. @@ -93,11 +95,7 @@ async function exportToS3Task( logGroupName, to: endTime.getTime(), }; - const response = await cloudwatchlogs.createExportTask(params).promise(); - - if (response.$response.error) { - throw new Error(response.$response.error.message); - } + const response = await cloudwatchlogs.send(new CreateExportTaskCommand(params)); if (response.taskId) { console.log(`${response.taskId}: Successfully created export task for ${logGroupName}.`); console.log(`Exporting into ${bucketName} from ${startTime} to ${endTime}.`); diff --git a/packages/aws-rfdk/lib/lambdas/nodejs/lib/custom-resource/dynamo-backed-resource.ts b/packages/aws-rfdk/lib/lambdas/nodejs/lib/custom-resource/dynamo-backed-resource.ts index 9b119ad2f..102e6f9a7 100644 --- a/packages/aws-rfdk/lib/lambdas/nodejs/lib/custom-resource/dynamo-backed-resource.ts +++ b/packages/aws-rfdk/lib/lambdas/nodejs/lib/custom-resource/dynamo-backed-resource.ts @@ -5,8 +5,11 @@ /* eslint-disable no-console */ -// eslint-disable-next-line import/no-extraneous-dependencies -import { DynamoDB } from 'aws-sdk'; +/* eslint-disable import/no-extraneous-dependencies */ +import { + DynamoDBClient, +} from '@aws-sdk/client-dynamodb'; +/* eslint-enable import/no-extraneous-dependencies */ import { CompositeStringIndexTable } from '../dynamodb'; import { SimpleCustomResource } from './simple-resource'; @@ -20,7 +23,7 @@ export abstract class DynamoBackedCustomResource extends SimpleCustomResource { * track the resources that get created, so they can be destroyed properly later. */ private readonly tableName: string; - private readonly dynamoDbClient: DynamoDB; + private readonly dynamoDbClient: DynamoDBClient; /** * The resource table uses the databaseName and the dynamoDbClient to fetch the DynamoDB table that backs it. * Ideally it would be readonly and initialized in the constructor, but it can't because the call is asynchronous. @@ -28,7 +31,7 @@ export abstract class DynamoBackedCustomResource extends SimpleCustomResource { */ private resourceTable?: CompositeStringIndexTable; - constructor(dynamoDbClient: DynamoDB) { + constructor(dynamoDbClient: DynamoDBClient) { super(); this.dynamoDbClient = dynamoDbClient; diff --git a/packages/aws-rfdk/lib/lambdas/nodejs/lib/dynamodb/composite-table.ts b/packages/aws-rfdk/lib/lambdas/nodejs/lib/dynamodb/composite-table.ts index b7b6118c8..b2bf67923 100644 --- a/packages/aws-rfdk/lib/lambdas/nodejs/lib/dynamodb/composite-table.ts +++ b/packages/aws-rfdk/lib/lambdas/nodejs/lib/dynamodb/composite-table.ts @@ -5,18 +5,45 @@ /* eslint-disable no-console */ -// eslint-disable-next-line import/no-extraneous-dependencies -import { DynamoDB, AWSError } from 'aws-sdk'; +/* eslint-disable import/no-extraneous-dependencies */ +import { + //BillingMode, + CreateTableCommand, + CreateTableCommandInput, + DeleteItemCommand, + DeleteItemCommandInput, + DeleteItemCommandOutput, + DeleteTableCommand, + DeleteTableCommandInput, + DynamoDBClient, + GetItemCommand, + GetItemCommandInput, + GetItemCommandOutput, + PutItemCommandInput, + QueryCommandInput, + QueryCommandOutput, + DescribeTableCommand, + ResourceNotFoundException, + ConditionalCheckFailedException, + PutItemCommand, + QueryCommand, +} from '@aws-sdk/client-dynamodb'; +import { + marshall, + unmarshall, + convertToAttr, +} from '@aws-sdk/util-dynamodb'; +/* eslint-enable import/no-extraneous-dependencies */ export class CompositeStringIndexTable { public static readonly API_VERSION = '2012-08-10'; - public static async fromExisting(client: DynamoDB, tableName: string): Promise { + public static async fromExisting(client: DynamoDBClient, tableName: string): Promise { // Determine the key schema of the table // We let this throw if the table does not exist. // See: https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/DynamoDB.html#describeTable-property - const describeResponse = await client.describeTable({ TableName: tableName }).promise(); + const describeResponse = await client.send(new DescribeTableCommand({ TableName: tableName })); if (!describeResponse.Table) { throw Error(`Could not describeTable for Table '${tableName}'`); } @@ -55,8 +82,8 @@ export class CompositeStringIndexTable { return new CompositeStringIndexTable( client, tableName, - primaryKey, - sortKey, + primaryKey!, + sortKey!, ); } @@ -66,7 +93,7 @@ export class CompositeStringIndexTable { * @param args */ public static async createNew(args: { - client: DynamoDB, + client: DynamoDBClient, name: string, primaryKeyName: string, sortKeyName: string, @@ -74,7 +101,7 @@ export class CompositeStringIndexTable { tags?: Array<{ Key: string, Value: string }> }): Promise { // See: https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/DynamoDB.html#createTable-property - const request: DynamoDB.CreateTableInput = { + const request: CreateTableCommandInput = { TableName: args.name, AttributeDefinitions: [ { @@ -100,7 +127,7 @@ export class CompositeStringIndexTable { Tags: args.tags, }; try { - await args.client.createTable(request).promise(); + await args.client.send(new CreateTableCommand(request)); const table: CompositeStringIndexTable = new CompositeStringIndexTable( args.client, @@ -110,18 +137,18 @@ export class CompositeStringIndexTable { ); return table; } catch (e) { - throw new Error(`CreateTable '${args.name}': ${(e as AWSError)?.code} -- ${(e as AWSError)?.message}`); + throw new Error(`CreateTable '${args.name}': ${(e as Error)?.name} -- ${(e as Error)?.message}`); } } public readonly primaryKey: string; public readonly sortKey: string; - protected readonly client: DynamoDB; + protected readonly client: DynamoDBClient; // tableName will only be undefined if the Table has been deleted. protected tableName: string | undefined; protected constructor( - client: DynamoDB, + client: DynamoDBClient, name: string, primaryKey: string, sortKey: string, @@ -140,20 +167,20 @@ export class CompositeStringIndexTable { return; // Already gone. } // See: https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/DynamoDB.html#deleteTable-property - const request: DynamoDB.DeleteTableInput = { + const request: DeleteTableCommandInput = { TableName: this.tableName, }; try { - await this.client.deleteTable(request).promise(); + await this.client.send(new DeleteTableCommand(request)); this.tableName = undefined; } catch (e) { - if ((e as AWSError)?.code === 'ResourceNotFoundException') { + if (e instanceof ResourceNotFoundException) { // Already gone. We're good. this.tableName = undefined; } else { - throw new Error(`DeleteTable '${this.tableName}': ${(e as AWSError)?.code} -- ${(e as AWSError)?.message}`); + throw new Error(`DeleteTable '${this.tableName}': ${(e as Error)?.name} -- ${(e as Error)?.message}`); } } } @@ -189,10 +216,10 @@ export class CompositeStringIndexTable { // See: https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/DynamoDB.html#putItem-property // https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/DynamoDB/Converter.html - const item = DynamoDB.Converter.marshall(props.attributes ?? {}); - item[this.primaryKey] = DynamoDB.Converter.input(props.primaryKeyValue); - item[this.sortKey] = DynamoDB.Converter.input(props.sortKeyValue); - const request: DynamoDB.PutItemInput = { + const item = marshall(props.attributes ?? {}); + item[this.primaryKey] = convertToAttr(props.primaryKeyValue); + item[this.sortKey] = convertToAttr(props.sortKeyValue); + const request: PutItemCommandInput = { TableName: this.tableName, Item: item, ReturnConsumedCapacity: 'NONE', @@ -204,14 +231,14 @@ export class CompositeStringIndexTable { } try { console.debug(`Dynamo.PutItem request: ${JSON.stringify(request)}`); - const response = await this.client.putItem(request).promise(); + const response = await this.client.send(new PutItemCommand(request)); console.debug(`PutItem response: ${JSON.stringify(response)}`); } catch (e) { - if ((e as AWSError)?.code === 'ConditionalCheckFailedException' && !props.allow_overwrite) { + if (e instanceof ConditionalCheckFailedException && !props.allow_overwrite) { return false; } throw new Error(`PutItem '${props.primaryKeyValue}' '${props.sortKeyValue}:" ` + - `${(e as AWSError)?.code} -- ${(e as AWSError)?.message}`); + `${(e as Error)?.name} -- ${(e as Error)?.message}`); } return true; } @@ -235,28 +262,28 @@ export class CompositeStringIndexTable { const key: { [key: string]: any } = {}; key[this.primaryKey] = props.primaryKeyValue; key[this.sortKey] = props.sortKeyValue; - const request: DynamoDB.GetItemInput = { + const request: GetItemCommandInput = { TableName: this.tableName, - Key: DynamoDB.Converter.marshall(key), + Key: marshall(key), ConsistentRead: true, ReturnConsumedCapacity: 'NONE', }; try { console.debug(`Dynamo.GetItem request: ${JSON.stringify(request)}`); - const response: DynamoDB.GetItemOutput = await this.client.getItem(request).promise(); + const response: GetItemCommandOutput = await this.client.send(new GetItemCommand(request)); console.debug(`GetItem response: ${JSON.stringify(response)}`); if (!response.Item) { // The item was not present in the DB return undefined; } - const item = DynamoDB.Converter.unmarshall(response.Item); + const item = unmarshall(response.Item); delete item[this.primaryKey]; delete item[this.sortKey]; return item; } catch (e) { throw new Error(`GetItem '${props.primaryKeyValue}' '${props.sortKeyValue}:" ` + - `${(e as AWSError)?.code} -- ${(e as AWSError)?.message}`); + `${(e as Error)?.name} -- ${(e as Error)?.message}`); } } @@ -277,16 +304,16 @@ export class CompositeStringIndexTable { const key: { [key: string]: any } = {}; key[this.primaryKey] = props.primaryKeyValue; key[this.sortKey] = props.sortKeyValue; - const request: DynamoDB.DeleteItemInput = { + const request: DeleteItemCommandInput = { TableName: this.tableName, - Key: DynamoDB.Converter.marshall(key), + Key: marshall(key), ReturnValues: 'ALL_OLD', ReturnConsumedCapacity: 'NONE', ReturnItemCollectionMetrics: 'NONE', }; try { console.debug(`Dynamo.DeleteItem request: ${JSON.stringify(request)}`); - const response: DynamoDB.DeleteItemOutput = await this.client.deleteItem(request).promise(); + const response: DeleteItemCommandOutput = await this.client.send(new DeleteItemCommand(request)); console.debug(`DeleteItem response: ${JSON.stringify(response)}`); if (response.Attributes) { @@ -296,7 +323,7 @@ export class CompositeStringIndexTable { return false; } catch (e) { throw new Error(`DeleteItem '${props.primaryKeyValue}' '${props.sortKeyValue}:" ` + - `${(e as AWSError)?.code} -- ${(e as AWSError)?.message}`); + `${(e as Error)?.name} -- ${(e as Error)?.message}`); } } @@ -324,7 +351,7 @@ export class CompositeStringIndexTable { throw Error('Attempt to Query a deleted table'); } // See: https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/DynamoDB.html#query-property - const request: DynamoDB.QueryInput = { + const request: QueryCommandInput = { TableName: this.tableName, Select: 'ALL_ATTRIBUTES', ConsistentRead: true, @@ -333,7 +360,7 @@ export class CompositeStringIndexTable { '#PK': this.primaryKey, }, ExpressionAttributeValues: { - ':PKV': DynamoDB.Converter.input(primaryKeyValue), + ':PKV': convertToAttr(primaryKeyValue), }, KeyConditionExpression: '#PK = :PKV', Limit: pageLimit, @@ -342,11 +369,11 @@ export class CompositeStringIndexTable { const items: { [key: string]: { [key: string]: any }} = {}; try { do { - const response: DynamoDB.QueryOutput = await this.client.query(request).promise(); + const response: QueryCommandOutput = await this.client.send(new QueryCommand(request)); request.ExclusiveStartKey = response.LastEvaluatedKey; if (response.Items) { for (const item of response.Items) { - const unmarshalled = DynamoDB.Converter.unmarshall(item); + const unmarshalled = unmarshall(item); const sortValue: string = unmarshalled[this.sortKey]; delete unmarshalled[this.primaryKey]; delete unmarshalled[this.sortKey]; @@ -356,7 +383,7 @@ export class CompositeStringIndexTable { } while (request.ExclusiveStartKey); return items; } catch (e) { - throw new Error(`Query '${primaryKeyValue}':" ${(e as AWSError)?.code} -- ${(e as AWSError)?.message}`); + throw new Error(`Query '${primaryKeyValue}':" ${(e as Error)?.name} -- ${(e as Error)?.message}`); } } } diff --git a/packages/aws-rfdk/lib/lambdas/nodejs/lib/dynamodb/test/composite-table.test.ts b/packages/aws-rfdk/lib/lambdas/nodejs/lib/dynamodb/test/composite-table.test.ts index 46ecaf588..8fbc41bec 100644 --- a/packages/aws-rfdk/lib/lambdas/nodejs/lib/dynamodb/test/composite-table.test.ts +++ b/packages/aws-rfdk/lib/lambdas/nodejs/lib/dynamodb/test/composite-table.test.ts @@ -5,12 +5,23 @@ /* eslint-disable no-console */ -// eslint-disable-next-line import/no-extraneous-dependencies import { randomBytes } from 'crypto'; -import * as AWS from 'aws-sdk'; -import { mock, restore, setSDKInstance } from 'aws-sdk-mock'; +import { + CreateTableCommand, + CreateTableCommandOutput, + DescribeTableCommand, + DynamoDBClient, + ScalarAttributeType, + DeleteTableCommand, + PutItemCommand, + GetItemCommand, + DeleteItemCommand, + QueryCommand, + CreateTableInput, +} from '@aws-sdk/client-dynamodb'; +import { mockClient, AwsClientStub } from 'aws-sdk-client-mock'; +import 'aws-sdk-client-mock-jest'; import * as dynalite from 'dynalite'; -import { fake } from 'sinon'; import { CompositeStringIndexTable } from '../composite-table'; @@ -25,7 +36,7 @@ class TestTable extends CompositeStringIndexTable { public tableName: string | undefined; public constructor( - client: AWS.DynamoDB, + client: DynamoDBClient, name: string, primaryKey: string, sortKey: string, @@ -57,7 +68,7 @@ describe('Tests using dynalite', () => { deleteTableMs: 5, updateTableMs: 5, }); - let dynamoClient: AWS.DynamoDB; + let dynamoClient: DynamoDBClient; beforeAll(async () => { const dynaPort = 43266; @@ -65,17 +76,17 @@ describe('Tests using dynalite', () => { if (err) { throw err; } }); - dynamoClient = new AWS.DynamoDB({ - credentials: new AWS.Credentials({ + dynamoClient = new DynamoDBClient({ + credentials: { accessKeyId: '', secretAccessKey: '', - }), + }, endpoint: `http://localhost:${dynaPort}`, region: 'us-west-2', }); - function createTableRequest(tableName: string, primaryKeyType: string, sortKey?: { KeyType: string }): AWS.DynamoDB.CreateTableInput { - const request = { + function createTableRequest(tableName: string, primaryKeyType: ScalarAttributeType, sortKeyType?: ScalarAttributeType): CreateTableInput { + const request: CreateTableInput = { TableName: tableName, AttributeDefinitions: [ { @@ -97,12 +108,12 @@ describe('Tests using dynalite', () => { }, ], }; - if (sortKey) { - request.AttributeDefinitions.push({ + if (sortKeyType) { + request.AttributeDefinitions!.push({ AttributeName: 'SortKey', - AttributeType: sortKey.KeyType, + AttributeType: sortKeyType, }); - request.KeySchema.push({ + request.KeySchema!.push({ AttributeName: 'SortKey', KeyType: 'RANGE', }); @@ -110,26 +121,26 @@ describe('Tests using dynalite', () => { return request; } - let request = createTableRequest(GOOD_TABLE_NAME, 'S', { KeyType: 'S' }); - let response: AWS.DynamoDB.CreateTableOutput = await dynamoClient.createTable(request).promise(); + let request = createTableRequest(GOOD_TABLE_NAME, 'S', 'S'); + let response: CreateTableCommandOutput = await dynamoClient.send(new CreateTableCommand(request)); let table = response.TableDescription; if (!table) { throw Error(`Could not create ${GOOD_TABLE_NAME}`); } console.debug(`Created DynamoDB table: '${table.TableName}'`); request = createTableRequest(BAD_TABLE1_NAME, 'S'); - response = await dynamoClient.createTable(request).promise(); + response = await dynamoClient.send(new CreateTableCommand(request)); table = response.TableDescription; if (!table) { throw Error(`Could not create ${BAD_TABLE1_NAME}`); } console.debug(`Created DynamoDB table: '${table.TableName}'`); - request = createTableRequest(BAD_TABLE2_NAME, 'N', { KeyType: 'S' }); - response = await dynamoClient.createTable(request).promise(); + request = createTableRequest(BAD_TABLE2_NAME, 'N', 'S'); + response = await dynamoClient.send(new CreateTableCommand(request)); table = response.TableDescription; if (!table) { throw Error(`Could not create ${BAD_TABLE2_NAME}`); } console.debug(`Created DynamoDB table: '${table.TableName}'`); - request = createTableRequest(BAD_TABLE3_NAME, 'S', { KeyType: 'N' }); - response = await dynamoClient.createTable(request).promise(); + request = createTableRequest(BAD_TABLE3_NAME, 'S', 'N'); + response = await dynamoClient.send(new CreateTableCommand(request)); table = response.TableDescription; if (!table) { throw Error(`Could not create ${BAD_TABLE3_NAME}`); } console.debug(`Created DynamoDB table: '${table.TableName}'`); @@ -138,9 +149,9 @@ describe('Tests using dynalite', () => { do { const promises = []; for (const name of [GOOD_TABLE_NAME, BAD_TABLE1_NAME, BAD_TABLE2_NAME, BAD_TABLE3_NAME]) { - promises.push(dynamoClient.describeTable({ + promises.push(dynamoClient.send(new DescribeTableCommand({ TableName: name, - }).promise()); + }))); } const responses = await Promise.all(promises); waiting = !responses.every(item => item.Table?.TableStatus === 'ACTIVE'); @@ -315,19 +326,24 @@ describe('Tests using dynalite', () => { }); describe('Tests using aws-sdk-mock', () => { - beforeEach(() => { - setSDKInstance(AWS); + let ddbMock: AwsClientStub; + + beforeAll(() => { + ddbMock = mockClient(DynamoDBClient); }); afterEach(() => { - restore('DynamoDB'); + ddbMock.reset(); + }); + + afterAll(() => { + ddbMock.restore(); }); describe('fromExisting tests', () => { test('Table not found', async () => { - const callback = fake.resolves({ Table: undefined }); - mock('DynamoDB', 'describeTable', callback); - const client = new AWS.DynamoDB(); + ddbMock.on(DescribeTableCommand).resolves({ Table: undefined }); + const client = new DynamoDBClient(); const tableName = 'Nonexistant'; await expect(CompositeStringIndexTable.fromExisting(client, tableName)) @@ -336,8 +352,8 @@ describe('Tests using aws-sdk-mock', () => { }); test('KeySchema not found', async () => { - mock('DynamoDB', 'describeTable', fake.resolves({ Table: { KeySchema: undefined } })); - const client = new AWS.DynamoDB(); + ddbMock.on(DescribeTableCommand).resolves({ Table: { KeySchema: undefined } }); + const client = new DynamoDBClient(); const tableName = 'TestTable'; await expect(CompositeStringIndexTable.fromExisting(client, tableName)) @@ -346,13 +362,13 @@ describe('Tests using aws-sdk-mock', () => { }); test('AttributeDefinitions not found', async () => { - mock('DynamoDB', 'describeTable', fake.resolves({ + ddbMock.on(DescribeTableCommand).resolves({ Table: { KeySchema: [], AttributeDefinitions: undefined, }, - })); - const client = new AWS.DynamoDB(); + }); + const client = new DynamoDBClient(); const tableName = 'TestTable'; await expect(CompositeStringIndexTable.fromExisting(client, tableName)) @@ -361,7 +377,7 @@ describe('Tests using aws-sdk-mock', () => { }); test('PrimaryKey not found', async () => { - mock('DynamoDB', 'describeTable', fake.resolves({ + ddbMock.on(DescribeTableCommand).resolves({ Table: { KeySchema: [ { @@ -369,10 +385,10 @@ describe('Tests using aws-sdk-mock', () => { KeyType: 'RANGE', }, ], - AttributeDefinitions: {}, + AttributeDefinitions: [], }, - })); - const client = new AWS.DynamoDB(); + }); + const client = new DynamoDBClient(); const tableName = 'TestTable'; await expect(CompositeStringIndexTable.fromExisting(client, tableName)) @@ -381,7 +397,7 @@ describe('Tests using aws-sdk-mock', () => { }); test('SortKey not found', async () => { - mock('DynamoDB', 'describeTable', fake.resolves({ + ddbMock.on(DescribeTableCommand).resolves({ Table: { KeySchema: [ { @@ -389,10 +405,10 @@ describe('Tests using aws-sdk-mock', () => { KeyType: 'HASH', }, ], - AttributeDefinitions: {}, + AttributeDefinitions: [], }, - })); - const client = new AWS.DynamoDB(); + }); + const client = new DynamoDBClient(); const tableName = 'TestTable'; await expect(CompositeStringIndexTable.fromExisting(client, tableName)) @@ -401,7 +417,7 @@ describe('Tests using aws-sdk-mock', () => { }); test('PrimaryKey AttributeDefinition not found', async () => { - mock('DynamoDB', 'describeTable', fake.resolves({ + ddbMock.on(DescribeTableCommand).resolves({ Table: { KeySchema: [ { @@ -420,8 +436,8 @@ describe('Tests using aws-sdk-mock', () => { }, ], }, - })); - const client = new AWS.DynamoDB(); + }); + const client = new DynamoDBClient(); const tableName = 'TestTable'; await expect(CompositeStringIndexTable.fromExisting(client, tableName)) @@ -430,7 +446,7 @@ describe('Tests using aws-sdk-mock', () => { }); test('SortKey AttributeDefinition not found', async () => { - mock('DynamoDB', 'describeTable', fake.resolves({ + ddbMock.on(DescribeTableCommand).resolves({ Table: { KeySchema: [ { @@ -449,8 +465,8 @@ describe('Tests using aws-sdk-mock', () => { }, ], }, - })); - const client = new AWS.DynamoDB(); + }); + const client = new DynamoDBClient(); const tableName = 'TestTable'; await expect(CompositeStringIndexTable.fromExisting(client, tableName)) @@ -461,8 +477,8 @@ describe('Tests using aws-sdk-mock', () => { describe('createNew tests', () => { test('DynamoDB.createTable() failure throws Error', async () => { - mock('DynamoDB', 'createTable', fake.rejects({})); - const client = new AWS.DynamoDB(); + ddbMock.on(CreateTableCommand).rejects({}); + const client = new DynamoDBClient(); const name = 'TestTable'; const primaryKeyName = 'PrimaryKey'; const sortKeyName = 'SortKey'; @@ -488,8 +504,8 @@ describe('Tests using aws-sdk-mock', () => { }, ]; - mock('DynamoDB', 'createTable', fake.resolves({})); - const client = new AWS.DynamoDB(); + ddbMock.on(CreateTableCommand).resolves({}); + const client = new DynamoDBClient(); const table = await CompositeStringIndexTable.createNew({ client, name: tableName, @@ -508,9 +524,9 @@ describe('Tests using aws-sdk-mock', () => { const tableName: string = 'TestTable'; const pk: string = 'PrimKey'; const sk: string = 'SortKey'; - mock('DynamoDB', 'deleteTable', fake.resolves({})); + ddbMock.on(DeleteTableCommand).resolves({}); - const client = new AWS.DynamoDB(); + const client = new DynamoDBClient(); const table = new TestTable( client, tableName, @@ -522,9 +538,8 @@ describe('Tests using aws-sdk-mock', () => { }); test('Table already deleted', async () => { - const deleteFake = fake.resolves({}); - mock('DynamoDB', 'deleteTable', deleteFake); - const client = new AWS.DynamoDB(); + ddbMock.on(DeleteTableCommand).resolves({}); + const client = new DynamoDBClient(); const name = 'TestTable'; const primaryKeyName = 'PrimaryKey'; const sortKeyName = 'SortKey'; @@ -537,12 +552,12 @@ describe('Tests using aws-sdk-mock', () => { await subject.deleteTable(); await expect(subject.deleteTable()).resolves.toBe(undefined); - expect(deleteFake.callCount).toEqual(1); + expect(ddbMock).toHaveReceivedCommandTimes(DeleteTableCommand, 1); }); test('DynamoDB.deleteTable() failure', async () => { - mock('DynamoDB', 'deleteTable', fake.rejects(new Error())); - const client = new AWS.DynamoDB(); + ddbMock.on(DeleteTableCommand).rejects(new Error()); + const client = new DynamoDBClient(); const name = 'TestTable'; const primaryKeyName = 'PrimaryKey'; const sortKeyName = 'SortKey'; @@ -558,12 +573,10 @@ describe('Tests using aws-sdk-mock', () => { describe('putItem tests', () => { test('Table already deleted', async () => { - const deleteFake = fake.resolves({}); - mock('DynamoDB', 'deleteTable', deleteFake); - const putFake = fake.resolves({}); - mock('DynamoDB', 'putItem', putFake); + ddbMock.on(DeleteTableCommand).resolves({}); + ddbMock.on(PutItemCommand).resolves({}); - const client = new AWS.DynamoDB(); + const client = new DynamoDBClient(); const name = 'TestTable'; const primaryKeyName = 'PrimaryKey'; const sortKeyName = 'SortKey'; @@ -578,13 +591,13 @@ describe('Tests using aws-sdk-mock', () => { await expect(subject.putItem({ primaryKeyValue: 'TestPrimVal', sortKeyValue: 'TestSortVal' })) .rejects .toThrow('Attempt to PutItem in deleted table'); - expect(deleteFake.callCount).toEqual(1); - expect(putFake.notCalled).toBeTruthy(); + expect(ddbMock).toHaveReceivedCommandTimes(DeleteTableCommand, 1); + expect(ddbMock).not.toHaveReceivedCommand(PutItemCommand); }); test('DynamoDB.putItem() failure', async () => { - mock('DynamoDB', 'putItem', fake.rejects(new Error())); - const client = new AWS.DynamoDB(); + ddbMock.on(PutItemCommand).rejects(new Error()); + const client = new DynamoDBClient(); const name = 'TestTable'; const primaryKeyName = 'PrimaryKey'; const sortKeyName = 'SortKey'; @@ -600,12 +613,10 @@ describe('Tests using aws-sdk-mock', () => { describe('getItem tests', () => { test('Table already deleted', async () => { - const deleteFake = fake.resolves({}); - mock('DynamoDB', 'deleteTable', deleteFake); - const getFake = fake.resolves({}); - mock('DynamoDB', 'getItem', getFake); + ddbMock.on(DeleteTableCommand).resolves({}); + ddbMock.on(GetItemCommand).resolves({}); - const client = new AWS.DynamoDB(); + const client = new DynamoDBClient(); const name = 'TestTable'; const primaryKeyName = 'PrimaryKey'; const sortKeyName = 'SortKey'; @@ -620,13 +631,13 @@ describe('Tests using aws-sdk-mock', () => { await expect(subject.getItem({ primaryKeyValue: 'TestPrimVal', sortKeyValue: 'TestSortVal' })) .rejects .toThrow('Attempt to GetItem from deleted table'); - expect(deleteFake.callCount).toEqual(1); - expect(getFake.notCalled).toBeTruthy(); + expect(ddbMock).toHaveReceivedCommandTimes(DeleteTableCommand, 1); + expect(ddbMock).not.toHaveReceivedCommand(GetItemCommand); }); test('DynamoDB.getItem() failure', async () => { - mock('DynamoDB', 'getItem', fake.rejects(new Error())); - const client = new AWS.DynamoDB(); + ddbMock.on(GetItemCommand).rejects(new Error()); + const client = new DynamoDBClient(); const name = 'TestTable'; const primaryKeyName = 'PrimaryKey'; const sortKeyName = 'SortKey'; @@ -642,12 +653,10 @@ describe('Tests using aws-sdk-mock', () => { describe('deleteItem tests', () => { test('Table already deleted', async () => { - const deleteTableFake = fake.resolves({}); - mock('DynamoDB', 'deleteTable', deleteTableFake); - const deleteItemFake = fake.resolves({}); - mock('DynamoDB', 'deleteItem', deleteItemFake); + ddbMock.on(DeleteTableCommand).resolves({}); + ddbMock.on(DeleteItemCommand).resolves({}); - const client = new AWS.DynamoDB(); + const client = new DynamoDBClient(); const name = 'TestTable'; const primaryKeyName = 'PrimaryKey'; const sortKeyName = 'SortKey'; @@ -662,13 +671,13 @@ describe('Tests using aws-sdk-mock', () => { await expect(subject.deleteItem({ primaryKeyValue: 'TestPrimVal', sortKeyValue: 'TestSortVal' })) .rejects .toThrow('Attempt to DeleteItem from deleted table'); - expect(deleteTableFake.callCount).toEqual(1); - expect(deleteItemFake.notCalled).toBeTruthy(); + expect(ddbMock).toHaveReceivedCommandTimes(DeleteTableCommand, 1); + expect(ddbMock).not.toHaveReceivedCommand(DeleteItemCommand); }); test('DynamoDB.deleteItem() failure', async () => { - mock('DynamoDB', 'deleteItem', fake.rejects(new Error())); - const client = new AWS.DynamoDB(); + ddbMock.on(DeleteItemCommand).rejects(new Error()); + const client = new DynamoDBClient(); const name = 'TestTable'; const primaryKeyName = 'PrimaryKey'; const sortKeyName = 'SortKey'; @@ -684,10 +693,9 @@ describe('Tests using aws-sdk-mock', () => { describe('query tests', () => { test('Returns empty', async () => { - const queryFake = fake.resolves({}); - mock('DynamoDB', 'query', queryFake); + ddbMock.on(QueryCommand).resolves({}); - const client = new AWS.DynamoDB(); + const client = new DynamoDBClient(); const name = 'TestTable'; const primaryKeyName = 'PrimaryKey'; const sortKeyName = 'SortKey'; @@ -701,16 +709,14 @@ describe('Tests using aws-sdk-mock', () => { await expect(subject.query('TestPrimVal')) .resolves .toEqual({}); - expect(queryFake.callCount).toEqual(1); + expect(ddbMock).toHaveReceivedCommandTimes(QueryCommand, 1); }); test('Table already deleted', async () => { - const deleteTableFake = fake.resolves({}); - mock('DynamoDB', 'deleteTable', deleteTableFake); - const queryFake = fake.resolves({}); - mock('DynamoDB', 'query', queryFake); + ddbMock.on(DeleteTableCommand).resolves({}); + ddbMock.on(QueryCommand).resolves({}); - const client = new AWS.DynamoDB(); + const client = new DynamoDBClient(); const name = 'TestTable'; const primaryKeyName = 'PrimaryKey'; const sortKeyName = 'SortKey'; @@ -725,13 +731,13 @@ describe('Tests using aws-sdk-mock', () => { await expect(subject.query('TestPrimVal')) .rejects .toThrow('Attempt to Query a deleted table'); - expect(deleteTableFake.callCount).toEqual(1); - expect(queryFake.notCalled).toBeTruthy(); + expect(ddbMock).toHaveReceivedCommandTimes(DeleteTableCommand, 1); + expect(ddbMock).not.toHaveReceivedCommand(QueryCommand); }); test('DynamoDB.query() failure', async () => { - mock('DynamoDB', 'query', fake.rejects(new Error())); - const client = new AWS.DynamoDB(); + ddbMock.on(QueryCommand).rejects(new Error()); + const client = new DynamoDBClient(); const name = 'TestTable'; const primaryKeyName = 'PrimaryKey'; const sortKeyName = 'SortKey'; diff --git a/packages/aws-rfdk/lib/lambdas/nodejs/lib/secrets-manager/read-certificate.ts b/packages/aws-rfdk/lib/lambdas/nodejs/lib/secrets-manager/read-certificate.ts index 7e26b5c3c..311a54aa7 100644 --- a/packages/aws-rfdk/lib/lambdas/nodejs/lib/secrets-manager/read-certificate.ts +++ b/packages/aws-rfdk/lib/lambdas/nodejs/lib/secrets-manager/read-certificate.ts @@ -3,8 +3,12 @@ * SPDX-License-Identifier: Apache-2.0 */ -// eslint-disable-next-line import/no-extraneous-dependencies -import { SecretsManager } from 'aws-sdk'; +/* eslint-disable import/no-extraneous-dependencies */ +import { + SecretsManagerClient, +} from '@aws-sdk/client-secrets-manager'; +/* eslint-enable import/no-extraneous-dependencies */ + import { Secret } from './secret'; /** @@ -12,7 +16,7 @@ import { Secret } from './secret'; * @param arn ARN of the Secret containing the certificate * @param client An instance of the SecretsManager class */ -export async function readCertificateData(arn: string, client: SecretsManager): Promise { +export async function readCertificateData(arn: string, client: SecretsManagerClient): Promise { const data = await Secret.fromArn(arn, client).getValue(); if (Buffer.isBuffer(data) || !/BEGIN CERTIFICATE/.test(data as string)) { throw new Error(`Certificate Secret (${arn}) must contain a Certificate in PEM format.`); diff --git a/packages/aws-rfdk/lib/lambdas/nodejs/lib/secrets-manager/secret.ts b/packages/aws-rfdk/lib/lambdas/nodejs/lib/secrets-manager/secret.ts index b91f47a32..39d5e60d1 100644 --- a/packages/aws-rfdk/lib/lambdas/nodejs/lib/secrets-manager/secret.ts +++ b/packages/aws-rfdk/lib/lambdas/nodejs/lib/secrets-manager/secret.ts @@ -6,8 +6,23 @@ /* eslint-disable no-console */ import { isUint8Array } from 'util/types'; -// eslint-disable-next-line import/no-extraneous-dependencies -import { SecretsManager, AWSError } from 'aws-sdk'; +/* eslint-disable import/no-extraneous-dependencies */ +import { + CreateSecretCommand, + CreateSecretRequest, + CreateSecretResponse, + DeleteSecretCommand, + DeleteSecretRequest, + DeleteSecretResponse, + GetSecretValueCommand, + GetSecretValueRequest, + GetSecretValueResponse, + PutSecretValueCommand, + PutSecretValueRequest, + PutSecretValueResponse, + SecretsManagerClient, +} from '@aws-sdk/client-secrets-manager'; +/* eslint-enable import/no-extraneous-dependencies */ import { Key } from '../kms'; import { isArn } from './validation'; @@ -20,7 +35,7 @@ export function sanitizeSecretName(name: string): string { export class Secret { public static readonly API_VERSION = '2017-10-17'; - public static fromArn(arn: string, client: SecretsManager) { + public static fromArn(arn: string, client: SecretsManagerClient) { if (!isArn(arn)) { throw Error(`Not a Secret ARN: ${arn}`); } @@ -36,14 +51,14 @@ export class Secret { */ public static async create(args: { name: string, - client: SecretsManager, + client: SecretsManagerClient, encryptionKey?: Key, description?: string, data?: Buffer | string, tags?: Array<{ Key: string, Value: string }> }): Promise { // See: https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/SecretsManager.html#createSecret-property - const request: SecretsManager.CreateSecretRequest = { + const request: CreateSecretRequest = { Name: args.name, Description: args.description, KmsKeyId: args.encryptionKey?.arn, @@ -52,7 +67,7 @@ export class Secret { SecretBinary: Buffer.isBuffer(args.data) ? args.data : undefined, }; try { - const response: SecretsManager.CreateSecretResponse = await args.client.createSecret(request).promise(); + const response: CreateSecretResponse = await args.client.send(new CreateSecretCommand(request)); console.debug(`CreateSecret response: ${JSON.stringify(response)}`); if (response.ARN) { return Secret.fromArn(response.ARN, args.client); @@ -60,15 +75,15 @@ export class Secret { return undefined; } catch (e) { throw new Error(`CreateSecret '${args.name}' failed in region '${args.client.config.region}': ` + - `${(e as AWSError)?.code} -- ${(e as AWSError)?.message}`); + `${(e as Error)?.name} -- ${(e as Error)?.message}`); } } // Undefined only if the Secret has been deleted. public arn: string | undefined; - protected readonly client: SecretsManager; + protected readonly client: SecretsManagerClient; - protected constructor(arn: string, client: SecretsManager) { + protected constructor(arn: string, client: SecretsManagerClient) { this.client = client; this.arn = arn; } @@ -83,19 +98,19 @@ export class Secret { if (!this.arn) { throw Error('Secret has already been deleted'); } - const request: SecretsManager.DeleteSecretRequest = { + const request: DeleteSecretRequest = { SecretId: this.arn, ForceDeleteWithoutRecovery: force, }; try { console.debug(`Deleting Secret: ${this.arn}`); - const response: SecretsManager.DeleteSecretResponse = - await this.client.deleteSecret(request).promise(); + const response: DeleteSecretResponse = + await this.client.send(new DeleteSecretCommand(request)); console.debug(`DeleteSecret response: ${JSON.stringify(response)}`); this.arn = undefined; } catch (e) { throw new Error(`DeleteSecret '${this.arn}' failed in region '${this.client.config.region}':` + - `${(e as AWSError)?.code} -- ${(e as AWSError)?.message}`); + `${(e as Error)?.name} -- ${(e as Error)?.message}`); } } @@ -111,18 +126,18 @@ export class Secret { throw Error('Secret has been deleted'); } // See: https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/SecretsManager.html#putSecretValue-property - const request: SecretsManager.PutSecretValueRequest = { + const request: PutSecretValueRequest = { SecretId: this.arn, SecretString: (typeof data === 'string') ? data : undefined, SecretBinary: Buffer.isBuffer(data) ? data : undefined, }; try { - const response: SecretsManager.PutSecretValueResponse = - await this.client.putSecretValue(request).promise(); + const response: PutSecretValueResponse = + await this.client.send(new PutSecretValueCommand(request)); console.debug(`PutSecret response: ${JSON.stringify(response)}`); } catch (e) { throw new Error(`PutSecret '${this.arn}' failed in region '${this.client.config.region}':` + - `${(e as AWSError)?.code} -- ${(e as AWSError)?.message}`); + `${(e as Error)?.name} -- ${(e as Error)?.message}`); } } @@ -133,21 +148,17 @@ export class Secret { if (!this.arn) { throw Error('Secret has been deleted'); } - // See: https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/SecretsManager.html#getSecretValue-property - const request: SecretsManager.GetSecretValueRequest = { + // See: https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/client/secrets-manager/command/GetSecretValueCommand/ + const request: GetSecretValueRequest = { SecretId: this.arn, }; try { - const response: SecretsManager.GetSecretValueResponse = - await this.client.getSecretValue(request).promise(); + const response: GetSecretValueResponse = + await this.client.send(new GetSecretValueCommand(request)); if (response.SecretBinary) { - // SecretBinary can be: Buffer|Uint8Array|Blob|string + // SecretBinary is expected to be a Uint8Array const data = response.SecretBinary; - if (Buffer.isBuffer(data)) { - return data; - } else if (typeof data === 'string') { - return Buffer.from(data, 'binary'); - } else if (isUint8Array(data)) { + if (isUint8Array(data)) { return Buffer.from(data); } else { throw new Error('Unknown type for SecretBinary data'); @@ -156,7 +167,7 @@ export class Secret { return response.SecretString; } catch (e) { throw new Error(`GetSecret '${this.arn}' failed in region '${this.client.config.region}':` + - `${(e as AWSError)?.code} -- ${(e as AWSError)?.message}`); + `${(e as Error)?.name} -- ${(e as Error)?.message}`); } } } diff --git a/packages/aws-rfdk/lib/lambdas/nodejs/lib/secrets-manager/test/read-certificate.test.ts b/packages/aws-rfdk/lib/lambdas/nodejs/lib/secrets-manager/test/read-certificate.test.ts index f69d52997..1ce91c9b6 100644 --- a/packages/aws-rfdk/lib/lambdas/nodejs/lib/secrets-manager/test/read-certificate.test.ts +++ b/packages/aws-rfdk/lib/lambdas/nodejs/lib/secrets-manager/test/read-certificate.test.ts @@ -3,8 +3,12 @@ * SPDX-License-Identifier: Apache-2.0 */ -import * as AWS from 'aws-sdk'; -import { mock, restore, setSDKInstance } from 'aws-sdk-mock'; +import { + SecretsManagerClient, + GetSecretValueCommand, +} from '@aws-sdk/client-secrets-manager'; +import { mockClient } from 'aws-sdk-client-mock'; +import 'aws-sdk-client-mock-jest'; import { readCertificateData } from '../read-certificate'; const secretPartialArn: string = 'arn:aws:secretsmanager:us-west-1:1234567890:secret:SecretPath/Cert'; @@ -15,12 +19,10 @@ async function successRequestMock(request: { [key: string]: string}, returnValue } describe('readCertificateData', () => { - beforeEach(() => { - setSDKInstance(AWS); - }); + const secretsManagerMock = mockClient(SecretsManagerClient); afterEach(() => { - restore('SecretsManager'); + secretsManagerMock.reset(); }); test('success', async () => { @@ -29,9 +31,8 @@ describe('readCertificateData', () => { const secretContents = { SecretString: certData, }; - const mockGetSecret = jest.fn( (request) => successRequestMock(request, secretContents) ); - mock('SecretsManager', 'getSecretValue', mockGetSecret); - const client = new AWS.SecretsManager(); + secretsManagerMock.on(GetSecretValueCommand).resolves(secretContents); + const client = new SecretsManagerClient(); // WHEN const data = await readCertificateData(secretPartialArn, client); @@ -46,9 +47,8 @@ describe('readCertificateData', () => { const secretContents = { SecretString: certData, }; - const mockGetSecret = jest.fn( (request) => successRequestMock(request, secretContents) ); - mock('SecretsManager', 'getSecretValue', mockGetSecret); - const client = new AWS.SecretsManager(); + secretsManagerMock.on(GetSecretValueCommand).resolves(secretContents); + const client = new SecretsManagerClient(); // WHEN const promise = readCertificateData(secretPartialArn, client); @@ -63,9 +63,8 @@ describe('readCertificateData', () => { const secretContents = { SecretBinary: certData, }; - const mockGetSecret = jest.fn( (request) => successRequestMock(request, secretContents) ); - mock('SecretsManager', 'getSecretValue', mockGetSecret); - const client = new AWS.SecretsManager(); + secretsManagerMock.on(GetSecretValueCommand).resolves(secretContents); + const client = new SecretsManagerClient(); // WHEN const promise = readCertificateData(secretPartialArn, client); diff --git a/packages/aws-rfdk/lib/lambdas/nodejs/lib/secrets-manager/test/secret.test.ts b/packages/aws-rfdk/lib/lambdas/nodejs/lib/secrets-manager/test/secret.test.ts index 49049eb2a..776ad5060 100644 --- a/packages/aws-rfdk/lib/lambdas/nodejs/lib/secrets-manager/test/secret.test.ts +++ b/packages/aws-rfdk/lib/lambdas/nodejs/lib/secrets-manager/test/secret.test.ts @@ -5,11 +5,16 @@ /* eslint-disable no-console */ -// eslint-disable-next-line import/no-extraneous-dependencies import { randomBytes } from 'crypto'; -import * as AWS from 'aws-sdk'; -import { mock, restore, setSDKInstance } from 'aws-sdk-mock'; -import { fake } from 'sinon'; +import { + SecretsManagerClient, + CreateSecretCommand, + DeleteSecretCommand, + PutSecretValueCommand, + GetSecretValueCommand, +} from '@aws-sdk/client-secrets-manager'; +import { mockClient } from 'aws-sdk-client-mock'; +import 'aws-sdk-client-mock-jest'; import { sanitizeSecretName, Secret } from '../secret'; @@ -20,24 +25,18 @@ if (!DEBUG) { } describe('Secret class', () => { - beforeEach(() => { - setSDKInstance(AWS); - }); + const secretsManagerMock = mockClient(SecretsManagerClient); afterEach(() => { - restore('SecretsManager'); + secretsManagerMock.reset(); }); describe('create', () => { test('success', async () => { const arn = 'arn:aws:secretsmanager:fake0secret1:123:secret:1a2b/'; - mock( - 'SecretsManager', - 'createSecret', - fake.resolves({ ARN: arn }), - ); + secretsManagerMock.on(CreateSecretCommand).resolves({ ARN: arn }); const name = 'SecretName'; - const client = new AWS.SecretsManager(); + const client = new SecretsManagerClient(); const secret = await Secret.create({ name, client }); @@ -46,13 +45,9 @@ describe('Secret class', () => { test('success - all options + string', async () => { const arn = 'arn:aws:secretsmanager:fake0secret1:123:secret:1a2b/'; - mock( - 'SecretsManager', - 'createSecret', - fake.resolves({ ARN: arn }), - ); + secretsManagerMock.on(CreateSecretCommand).resolves({ ARN: arn }); const name = 'SecretName'; - const client = new AWS.SecretsManager(); + const client = new SecretsManagerClient(); const secret = await Secret.create({ name, @@ -68,13 +63,9 @@ describe('Secret class', () => { test('success - all options + binary', async () => { const arn = 'arn:aws:secretsmanager:fake0secret1:123:secret:1a2b/'; - mock( - 'SecretsManager', - 'createSecret', - fake.resolves({ ARN: arn }), - ); + secretsManagerMock.on(CreateSecretCommand).resolves({ ARN: arn }); const name = 'SecretName'; - const client = new AWS.SecretsManager(); + const client = new SecretsManagerClient(); const secret = await Secret.create({ name, @@ -89,13 +80,9 @@ describe('Secret class', () => { }); test('missing response', async () => { - mock( - 'SecretsManager', - 'createSecret', - fake.resolves({}), - ); + secretsManagerMock.on(CreateSecretCommand).resolves({}); const name = 'SecretName'; - const client = new AWS.SecretsManager(); + const client = new SecretsManagerClient(); const secret = await Secret.create({ name, client }); @@ -103,13 +90,9 @@ describe('Secret class', () => { }); test('SecretsManager error', async () => { - mock( - 'SecretsManager', - 'createSecret', - fake.rejects({}), - ); + secretsManagerMock.on(CreateSecretCommand).rejects({}); const name = 'SecretName'; - const client = new AWS.SecretsManager(); + const client = new SecretsManagerClient(); await expect(Secret.create({ name, client })).rejects.toThrow(); }); @@ -118,44 +101,29 @@ describe('Secret class', () => { describe('delete', () => { test('success', async () => { const arn = 'arn:aws:secretsmanager:fake0secret1:123:secret:1a2b/'; - const fakeDeleteSecret = fake.resolves({}); - mock( - 'SecretsManager', - 'deleteSecret', - fakeDeleteSecret, - ); - const client = new AWS.SecretsManager(); + secretsManagerMock.on(DeleteSecretCommand).resolves({}); + const client = new SecretsManagerClient(); const secret = Secret.fromArn(arn, client); await secret.delete(); - expect(fakeDeleteSecret.callCount).toEqual(1); + expect(secretsManagerMock).toHaveReceivedCommandTimes(DeleteSecretCommand, 1); }); test('secret already deleted', async () => { const arn = 'arn:aws:secretsmanager:fake0secret1:123:secret:1a2b/'; - const fakeDeleteSecret = fake.resolves({}); - mock( - 'SecretsManager', - 'deleteSecret', - fakeDeleteSecret, - ); - const client = new AWS.SecretsManager(); + secretsManagerMock.on(DeleteSecretCommand).resolves({}); + const client = new SecretsManagerClient(); const secret = Secret.fromArn(arn, client); await secret.delete(); await expect(() => secret.delete()).rejects.toThrow('Secret has already been deleted'); - expect(fakeDeleteSecret.callCount).toEqual(1); + expect(secretsManagerMock).toHaveReceivedCommandTimes(DeleteSecretCommand, 1); }); test('SecretManager error', async () => { const arn = 'arn:aws:secretsmanager:fake0secret1:123:secret:1a2b/'; - const fakeDeleteSecret = fake.rejects({}); - mock( - 'SecretsManager', - 'deleteSecret', - fakeDeleteSecret, - ); - const client = new AWS.SecretsManager(); + secretsManagerMock.on(DeleteSecretCommand).rejects({}); + const client = new SecretsManagerClient(); const secret = Secret.fromArn(arn, client); await expect(() => secret.delete()).rejects.toThrow(); @@ -165,84 +133,61 @@ describe('Secret class', () => { describe('putValue', () => { test('string success', async () => { const arn = 'arn:aws:secretsmanager:fake0secret1:123:secret:1a2b/'; - const fakePutSecretValue = fake.resolves({}); - mock( - 'SecretsManager', - 'putSecretValue', - fakePutSecretValue, - ); - const client = new AWS.SecretsManager(); + secretsManagerMock.on(PutSecretValueCommand).resolves({}); + const client = new SecretsManagerClient(); const secret = Secret.fromArn(arn, client); const value = 'Super secret value'.toString(); await secret.putValue(value); - expect(fakePutSecretValue.callCount).toEqual(1); - expect(fakePutSecretValue.calledWith({ + expect(secretsManagerMock).toHaveReceivedCommandTimes(PutSecretValueCommand, 1); + expect(secretsManagerMock).toHaveReceivedCommandWith(PutSecretValueCommand, { SecretId: arn, SecretBinary: undefined, SecretString: value, - })).toBeTruthy(); + }); }); test('Buffer success', async () => { const arn = 'arn:aws:secretsmanager:fake0secret1:123:secret:1a2b/'; - const fakePutSecretValue = fake.resolves({}); - mock( - 'SecretsManager', - 'putSecretValue', - fakePutSecretValue, - ); - const client = new AWS.SecretsManager(); + secretsManagerMock.on(PutSecretValueCommand).resolves({}); + const client = new SecretsManagerClient(); const secret = Secret.fromArn(arn, client); const value = Buffer.from(randomBytes(512)); await secret.putValue(value); - expect(fakePutSecretValue.callCount).toEqual(1); - expect(fakePutSecretValue.calledWith({ + expect(secretsManagerMock).toHaveReceivedCommandTimes(PutSecretValueCommand, 1); + expect(secretsManagerMock).toHaveReceivedCommandWith(PutSecretValueCommand, { SecretId: arn, SecretBinary: value, SecretString: undefined, - })).toBeTruthy(); + }); }); test('already deleted', async () => { const arn = 'arn:aws:secretsmanager:fake0secret1:123:secret:1a2b/'; - const fakeDeleteSecret = fake.resolves({}); - mock( - 'SecretsManager', - 'deleteSecret', - fakeDeleteSecret, - ); - const fakePutSecretValue = fake.resolves({}); - mock( - 'SecretsManager', - 'putSecretValue', - fakePutSecretValue, - ); - - const client = new AWS.SecretsManager(); + secretsManagerMock.on(DeleteSecretCommand).resolves({}); + secretsManagerMock.on(PutSecretValueCommand).resolves({}); + + const client = new SecretsManagerClient(); const secret = Secret.fromArn(arn, client); const value = 'value'; await secret.delete(); await expect(() => secret.putValue(value)).rejects.toThrow('Secret has been deleted'); - expect(fakePutSecretValue.callCount).toEqual(0); + + expect(secretsManagerMock).not.toHaveReceivedCommand(PutSecretValueCommand); }); test('SecretManager error', async () => { const arn = 'arn:aws:secretsmanager:fake0secret1:123:secret:1a2b/'; - const fakePutSecretValue = fake.rejects({}); - mock( - 'SecretsManager', - 'putSecretValue', - fakePutSecretValue, - ); - const client = new AWS.SecretsManager(); + secretsManagerMock.on(PutSecretValueCommand).rejects({}); + + const client = new SecretsManagerClient(); const secret = Secret.fromArn(arn, client); const value = 'Super secret value'; await expect(() => secret.putValue(value)).rejects.toThrow(); - expect(fakePutSecretValue.callCount).toEqual(1); + expect(secretsManagerMock).toHaveReceivedCommandTimes(PutSecretValueCommand, 1); }); }); @@ -251,140 +196,76 @@ describe('Secret class', () => { const value = 'Super secret value'.toString(); const arn = 'arn:aws:secretsmanager:fake0secret1:123:secret:1a2b/'; - const fakeGetSecretValue = fake.resolves({ + secretsManagerMock.on(GetSecretValueCommand).resolves({ SecretString: value, }); - mock( - 'SecretsManager', - 'getSecretValue', - fakeGetSecretValue, - ); - const client = new AWS.SecretsManager(); - const secret = Secret.fromArn(arn, client); - - await secret.getValue(); - expect(fakeGetSecretValue.callCount).toEqual(1); - }); - - test('SecrectBinary string success', async () => { - const value = 'Super secret value'.toString(); - - const arn = 'arn:aws:secretsmanager:fake0secret1:123:secret:1a2b/'; - const fakeGetSecretValue = fake.resolves({ - SecretBinary: value, - }); - mock( - 'SecretsManager', - 'getSecretValue', - fakeGetSecretValue, - ); - const client = new AWS.SecretsManager(); - const secret = Secret.fromArn(arn, client); - - await expect(secret.getValue()).resolves.toEqual(Buffer.from(value)); - expect(fakeGetSecretValue.callCount).toEqual(1); - }); - - test('SecretBinary Buffer success', async () => { - const value = Buffer.from(randomBytes(512)); - const arn = 'arn:aws:secretsmanager:fake0secret1:123:secret:1a2b/'; - const fakeGetSecretValue = fake.resolves({ - SecretBinary: value, - }); - mock( - 'SecretsManager', - 'getSecretValue', - fakeGetSecretValue, - ); - const client = new AWS.SecretsManager(); + const client = new SecretsManagerClient(); const secret = Secret.fromArn(arn, client); - await expect(secret.getValue()).resolves.toEqual(value); - expect(fakeGetSecretValue.callCount).toEqual(1); + await secret.getValue(); + expect(secretsManagerMock).toHaveReceivedCommandTimes(GetSecretValueCommand, 1); }); test('SecretBinary Uint8Array success', async () => { - const value: Uint8Array = new Uint8Array(); + const value: Uint8Array = new Uint8Array(randomBytes(512)); const arn = 'arn:aws:secretsmanager:fake0secret1:123:secret:1a2b/'; - const fakeGetSecretValue = fake.resolves({ + secretsManagerMock.on(GetSecretValueCommand).resolves({ SecretBinary: value, }); - mock( - 'SecretsManager', - 'getSecretValue', - fakeGetSecretValue, - ); - const client = new AWS.SecretsManager(); + const client = new SecretsManagerClient(); const secret = Secret.fromArn(arn, client); await expect(secret.getValue()).resolves.toEqual(Buffer.from(value)); - expect(fakeGetSecretValue.callCount).toEqual(1); + expect(secretsManagerMock).toHaveReceivedCommandTimes(GetSecretValueCommand, 1); }); test('SecretBinary unknown type error', async () => { const value = new ArrayBuffer(0); const arn = 'arn:aws:secretsmanager:fake0secret1:123:secret:1a2b/'; - const fakeGetSecretValue = fake.resolves({ + + secretsManagerMock.on(GetSecretValueCommand).resolves({ + // We're intentionally passing an unexpected type for this test. + // @ts-ignore SecretBinary: value, }); - mock( - 'SecretsManager', - 'getSecretValue', - fakeGetSecretValue, - ); - const client = new AWS.SecretsManager(); + const client = new SecretsManagerClient(); const secret = Secret.fromArn(arn, client); await expect(() => secret.getValue()).rejects.toThrow('Unknown type for SecretBinary data'); - expect(fakeGetSecretValue.callCount).toEqual(1); + expect(secretsManagerMock).toHaveReceivedCommandTimes(GetSecretValueCommand, 1); }); test('already deleted', async () => { const arn = 'arn:aws:secretsmanager:fake0secret1:123:secret:1a2b/'; - const fakeDeleteSecret = fake.resolves({}); - mock( - 'SecretsManager', - 'deleteSecret', - fakeDeleteSecret, - ); - const fakeGetSecretValue = fake.resolves({}); - mock( - 'SecretsManager', - 'getSecretValue', - fakeGetSecretValue, - ); - - const client = new AWS.SecretsManager(); + secretsManagerMock.on(DeleteSecretCommand).resolves({}); + secretsManagerMock.on(GetSecretValueCommand).resolves({}); + + const client = new SecretsManagerClient(); const secret = Secret.fromArn(arn, client); await secret.delete(); await expect(() => secret.getValue()).rejects.toThrow('Secret has been deleted'); - expect(fakeGetSecretValue.callCount).toEqual(0); + expect(secretsManagerMock).not.toHaveReceivedCommand(GetSecretValueCommand); }); test('SecretManager error', async () => { const arn = 'arn:aws:secretsmanager:fake0secret1:123:secret:1a2b/'; - const fakeGetSecretValue = fake.rejects({}); - mock( - 'SecretsManager', - 'getSecretValue', - fakeGetSecretValue, - ); - const client = new AWS.SecretsManager(); + secretsManagerMock.on(GetSecretValueCommand).rejects({}); + const client = new SecretsManagerClient(); const secret = Secret.fromArn(arn, client); await expect(() => secret.getValue()).rejects.toThrow(); - expect(fakeGetSecretValue.callCount).toEqual(1); + expect(secretsManagerMock).toHaveReceivedCommandTimes(GetSecretValueCommand, 1); }); }); }); test('fromArn invalid ARN', async () => { const invalidArn = 'notAnArn'; - const client = new AWS.SecretsManager(); + const client = new SecretsManagerClient(); expect(() => Secret.fromArn(invalidArn, client)).toThrow(`Not a Secret ARN: ${invalidArn}`); }); diff --git a/packages/aws-rfdk/lib/lambdas/nodejs/mongodb/handler.ts b/packages/aws-rfdk/lib/lambdas/nodejs/mongodb/handler.ts index 129d44eea..bf680d224 100644 --- a/packages/aws-rfdk/lib/lambdas/nodejs/mongodb/handler.ts +++ b/packages/aws-rfdk/lib/lambdas/nodejs/mongodb/handler.ts @@ -7,8 +7,11 @@ import { exec as execAsync, execSync } from 'child_process'; import { promisify } from 'util'; -// eslint-disable-next-line import/no-extraneous-dependencies -import { SecretsManager } from 'aws-sdk'; +/* eslint-disable import/no-extraneous-dependencies */ +import { + SecretsManagerClient, +} from '@aws-sdk/client-secrets-manager'; +/* eslint-enable import/no-extraneous-dependencies */ import { LambdaContext } from '../lib/aws-lambda'; import { CfnRequestEvent, SimpleCustomResource } from '../lib/custom-resource'; @@ -29,9 +32,9 @@ import { const exec = promisify(execAsync); export class MongoDbConfigure extends SimpleCustomResource { - protected readonly secretsManagerClient: SecretsManager; + protected readonly secretsManagerClient: SecretsManagerClient; - constructor(secretsManagerClient: SecretsManager) { + constructor(secretsManagerClient: SecretsManagerClient) { super(); this.secretsManagerClient = secretsManagerClient; } @@ -270,6 +273,6 @@ export class MongoDbConfigure extends SimpleCustomResource { */ /* istanbul ignore next */ export async function configureMongo(event: CfnRequestEvent, context: LambdaContext): Promise { - const handler = new MongoDbConfigure(new SecretsManager()); + const handler = new MongoDbConfigure(new SecretsManagerClient()); return await handler.handler(event, context); } diff --git a/packages/aws-rfdk/lib/lambdas/nodejs/mongodb/test/handler.test.ts b/packages/aws-rfdk/lib/lambdas/nodejs/mongodb/test/handler.test.ts index bbaf5b2c9..5a586e848 100644 --- a/packages/aws-rfdk/lib/lambdas/nodejs/mongodb/test/handler.test.ts +++ b/packages/aws-rfdk/lib/lambdas/nodejs/mongodb/test/handler.test.ts @@ -5,8 +5,12 @@ /* eslint-disable dot-notation */ -import * as AWS from 'aws-sdk'; -import { mock, restore, setSDKInstance } from 'aws-sdk-mock'; +import { + SecretsManagerClient, + GetSecretValueCommand, +} from '@aws-sdk/client-secrets-manager'; +import { mockClient } from 'aws-sdk-client-mock'; +import 'aws-sdk-client-mock-jest'; import { MongoDbConfigure } from '../handler'; @@ -14,6 +18,8 @@ jest.mock('../../lib/secrets-manager/read-certificate'); const secretPartialArn: string = 'arn:aws:secretsmanager:us-west-1:1234567890:secret:SecretPath/Cert'; +const secretsManagerMock = mockClient(SecretsManagerClient); + // @ts-ignore async function successRequestMock(request: { [key: string]: string}, returnValue: any): Promise<{ [key: string]: any }> { return returnValue; @@ -24,7 +30,7 @@ describe('readCertificateData', () => { // GIVEN const certData = 'BEGIN CERTIFICATE'; jest.requireMock('../../lib/secrets-manager/read-certificate').readCertificateData.mockReturnValue(Promise.resolve(certData)); - const handler = new MongoDbConfigure(new AWS.SecretsManager()); + const handler = new MongoDbConfigure(new SecretsManagerClient()); // WHEN // tslint:disable-next-line: no-string-literal @@ -39,7 +45,7 @@ describe('readCertificateData', () => { jest.requireMock('../../lib/secrets-manager/read-certificate').readCertificateData.mockImplementation(() => { throw new Error('must contain a Certificate in PEM format'); }); - const handler = new MongoDbConfigure(new AWS.SecretsManager()); + const handler = new MongoDbConfigure(new SecretsManagerClient()); // THEN // tslint:disable-next-line: no-string-literal @@ -48,12 +54,8 @@ describe('readCertificateData', () => { }); describe('readLoginCredentials', () => { - beforeEach(() => { - setSDKInstance(AWS); - }); - afterEach(() => { - restore('SecretsManager'); + secretsManagerMock.reset(); }); test('success', async () => { @@ -65,9 +67,8 @@ describe('readLoginCredentials', () => { const secretContents = { SecretString: JSON.stringify(loginData), }; - const mockGetSecret = jest.fn( (request) => successRequestMock(request, secretContents) ); - mock('SecretsManager', 'getSecretValue', mockGetSecret); - const handler = new MongoDbConfigure(new AWS.SecretsManager()); + secretsManagerMock.on(GetSecretValueCommand).resolves(secretContents); + const handler = new MongoDbConfigure(new SecretsManagerClient()); // WHEN // tslint:disable-next-line: no-string-literal @@ -83,9 +84,8 @@ describe('readLoginCredentials', () => { const secretContents = { SecretBinary: loginData, }; - const mockGetSecret = jest.fn( (request) => successRequestMock(request, secretContents) ); - mock('SecretsManager', 'getSecretValue', mockGetSecret); - const handler = new MongoDbConfigure(new AWS.SecretsManager()); + secretsManagerMock.on(GetSecretValueCommand).resolves(secretContents); + const handler = new MongoDbConfigure(new SecretsManagerClient()); // THEN // tslint:disable-next-line: no-string-literal @@ -111,9 +111,8 @@ describe('readLoginCredentials', () => { const secretContents = { SecretString: data, }; - const mockGetSecret = jest.fn( (request) => successRequestMock(request, secretContents) ); - mock('SecretsManager', 'getSecretValue', mockGetSecret); - const handler = new MongoDbConfigure(new AWS.SecretsManager()); + secretsManagerMock.on(GetSecretValueCommand).resolves(secretContents); + const handler = new MongoDbConfigure(new SecretsManagerClient()); // THEN // tslint:disable-next-line: no-string-literal @@ -137,7 +136,7 @@ describe('mongoLogin', () => { } const mockReadCert = jest.fn( (request) => stringSuccessRequestMock(request) ); const mockReadLogin = jest.fn( (request) => successRequestMock(request, { username: 'test', password: 'pass' })); - const handler = new MongoDbConfigure(new AWS.SecretsManager()); + const handler = new MongoDbConfigure(new SecretsManagerClient()); // tslint:disable-next-line: no-string-literal handler['readCertificateData'] = mockReadCert; // tslint:disable-next-line: no-string-literal @@ -179,12 +178,8 @@ describe('mongoLogin', () => { }); describe('readPasswordAuthUserInfo', () => { - beforeEach(() => { - setSDKInstance(AWS); - }); - afterEach(() => { - restore('SecretsManager'); + secretsManagerMock.reset(); }); test('success', async () => { @@ -197,9 +192,8 @@ describe('readPasswordAuthUserInfo', () => { const secretContents = { SecretString: JSON.stringify(userData), }; - const mockGetSecret = jest.fn( (request) => successRequestMock(request, secretContents) ); - mock('SecretsManager', 'getSecretValue', mockGetSecret); - const handler = new MongoDbConfigure(new AWS.SecretsManager()); + secretsManagerMock.on(GetSecretValueCommand).resolves(secretContents); + const handler = new MongoDbConfigure(new SecretsManagerClient()); // WHEN // tslint:disable-next-line: no-string-literal @@ -215,9 +209,8 @@ describe('readPasswordAuthUserInfo', () => { const secretContents = { SecretBinary: loginData, }; - const mockGetSecret = jest.fn( (request) => successRequestMock(request, secretContents) ); - mock('SecretsManager', 'getSecretValue', mockGetSecret); - const handler = new MongoDbConfigure(new AWS.SecretsManager()); + secretsManagerMock.on(GetSecretValueCommand).resolves(secretContents); + const handler = new MongoDbConfigure(new SecretsManagerClient()); // THEN // tslint:disable-next-line: no-string-literal @@ -252,9 +245,8 @@ describe('readPasswordAuthUserInfo', () => { const secretContents = { SecretString: data, }; - const mockGetSecret = jest.fn( (request) => successRequestMock(request, secretContents) ); - mock('SecretsManager', 'getSecretValue', mockGetSecret); - const handler = new MongoDbConfigure(new AWS.SecretsManager()); + secretsManagerMock.on(GetSecretValueCommand).resolves(secretContents); + const handler = new MongoDbConfigure(new SecretsManagerClient()); // THEN // tslint:disable-next-line: no-string-literal @@ -278,7 +270,7 @@ describe('userExists', () => { const mockDb = { command: jest.fn( (request) => successRequestMock(request, mongoQueryResult) ), }; - const handler = new MongoDbConfigure(new AWS.SecretsManager()); + const handler = new MongoDbConfigure(new SecretsManagerClient()); // WHEN // tslint:disable-next-line: no-string-literal @@ -301,7 +293,7 @@ describe('userExists', () => { const mockDb = { command: jest.fn( (request) => successRequestMock(request, mongoQueryResult) ), }; - const handler = new MongoDbConfigure(new AWS.SecretsManager()); + const handler = new MongoDbConfigure(new SecretsManagerClient()); // WHEN // tslint:disable-next-line: no-string-literal @@ -320,7 +312,7 @@ describe('userExists', () => { const mockDb = { command: jest.fn( (request) => successRequestMock(request, mongoQueryResult) ), }; - const handler = new MongoDbConfigure(new AWS.SecretsManager()); + const handler = new MongoDbConfigure(new SecretsManagerClient()); // THEN // tslint:disable-next-line: no-string-literal @@ -347,7 +339,7 @@ describe('createUser', () => { const mockDb = { command: jest.fn( (request) => successRequestMock(request, mongoQueryResult) ), }; - const handler = new MongoDbConfigure(new AWS.SecretsManager()); + const handler = new MongoDbConfigure(new SecretsManagerClient()); const credentials = { username: 'test', password: 'password', @@ -377,7 +369,7 @@ describe('createUser', () => { const mockDb = { command: jest.fn( (request) => successRequestMock(request, mongoQueryResult) ), }; - const handler = new MongoDbConfigure(new AWS.SecretsManager()); + const handler = new MongoDbConfigure(new SecretsManagerClient()); const credentials = { username: 'test', roles: [ { role: 'readWrite', db: 'testdb' } ], @@ -405,7 +397,7 @@ describe('createUser', () => { const mockDb = { command: jest.fn( (request) => successRequestMock(request, mongoQueryResult) ), }; - const handler = new MongoDbConfigure(new AWS.SecretsManager()); + const handler = new MongoDbConfigure(new SecretsManagerClient()); const credentials = { username: 'test', password: 'password', @@ -436,15 +428,13 @@ describe('createPasswordAuthUser', () => { beforeEach(() => { // GIVEN - setSDKInstance(AWS); consoleLogMock = jest.spyOn(console, 'log').mockReturnValue(undefined); - const mockGetSecret = jest.fn( (request) => successRequestMock(request, secretContents) ); - mock('SecretsManager', 'getSecretValue', mockGetSecret); + secretsManagerMock.on(GetSecretValueCommand).resolves(secretContents); }); afterEach(() => { - restore('SecretsManager'); + secretsManagerMock.reset(); jest.clearAllMocks(); }); @@ -466,7 +456,7 @@ describe('createPasswordAuthUser', () => { const mockDb = { command: jest.fn( (request) => commandMock(request) ), }; - const handler = new MongoDbConfigure(new AWS.SecretsManager()); + const handler = new MongoDbConfigure(new SecretsManagerClient()); // WHEN // tslint:disable-next-line: no-string-literal @@ -513,7 +503,7 @@ describe('createPasswordAuthUser', () => { const mockDb = { command: jest.fn( (request) => commandMock(request) ), }; - const handler = new MongoDbConfigure(new AWS.SecretsManager()); + const handler = new MongoDbConfigure(new SecretsManagerClient()); // WHEN // tslint:disable-next-line: no-string-literal @@ -533,16 +523,11 @@ describe('createX509AuthUser', () => { const username = 'CN=TestUser,O=TestOrg,OU=TestOrgUnit'; beforeEach(() => { - setSDKInstance(AWS); consoleLogMock = jest.spyOn(console, 'log') .mockReset() .mockReturnValue(undefined); }); - afterEach(() => { - restore('SecretsManager'); - }); - describe.each([ [ [], true, @@ -591,7 +576,7 @@ describe('createX509AuthUser', () => { } const mockReadCert = jest.fn( (request) => stringSuccessRequestMock(request) ); const mockRfc2253 = jest.fn( (arg) => rfc2253(arg) ); - const handler = new MongoDbConfigure(new AWS.SecretsManager()); + const handler = new MongoDbConfigure(new SecretsManagerClient()); // tslint:disable-next-line: no-string-literal handler['readCertificateData'] = mockReadCert; // tslint:disable-next-line: no-string-literal @@ -654,7 +639,7 @@ describe('doCreate', () => { close: jest.fn(), }; - const handler = new MongoDbConfigure(new AWS.SecretsManager()); + const handler = new MongoDbConfigure(new SecretsManagerClient()); // tslint:disable-next-line: no-string-literal handler['installMongoDbDriver'] = jest.fn(); async function returnMockMongoClient(_v1: any, _v2: any): Promise { diff --git a/packages/aws-rfdk/lib/lambdas/nodejs/unhealthyFleetAction/index.ts b/packages/aws-rfdk/lib/lambdas/nodejs/unhealthyFleetAction/index.ts index cfebae6f7..a99b388aa 100644 --- a/packages/aws-rfdk/lib/lambdas/nodejs/unhealthyFleetAction/index.ts +++ b/packages/aws-rfdk/lib/lambdas/nodejs/unhealthyFleetAction/index.ts @@ -6,8 +6,13 @@ /* eslint-disable no-console */ import { isNativeError } from 'util/types'; -// eslint-disable-next-line import/no-extraneous-dependencies -import {AutoScaling} from 'aws-sdk'; + +/* eslint-disable import/no-extraneous-dependencies */ +import { + AutoScalingClient, + UpdateAutoScalingGroupCommand, +} from '@aws-sdk/client-auto-scaling'; +/* eslint-enable import/no-extraneous-dependencies */ // @ts-ignore export async function handler(event: AWSLambda.SNSEvent, context: AWSLambda.Context) { @@ -63,13 +68,13 @@ export async function handler(event: AWSLambda.SNSEvent, context: AWSLambda.Cont console.info(`Found fleet: ${dimensionName} with fleetId: ${dimensionValue}`); // this is an ASG Target, we need to suspend its size - const autoScaling = new AutoScaling(); - await autoScaling.updateAutoScalingGroup({ + const autoScaling = new AutoScalingClient(); + await autoScaling.send(new UpdateAutoScalingGroupCommand({ AutoScalingGroupName: dimensionValue, MaxSize: 0, MinSize: 0, DesiredCapacity: 0, - }).promise().then((data: any) => { + })).then((data: any) => { // successful response console.log(`Successfully suspended the fleet ${dimensionValue}: ${data}`); }).catch((err: any) => { diff --git a/packages/aws-rfdk/lib/lambdas/nodejs/unhealthyFleetAction/test/unhealthyFleetAction.test.ts b/packages/aws-rfdk/lib/lambdas/nodejs/unhealthyFleetAction/test/unhealthyFleetAction.test.ts index f719e87ca..2adec41cb 100644 --- a/packages/aws-rfdk/lib/lambdas/nodejs/unhealthyFleetAction/test/unhealthyFleetAction.test.ts +++ b/packages/aws-rfdk/lib/lambdas/nodejs/unhealthyFleetAction/test/unhealthyFleetAction.test.ts @@ -5,11 +5,14 @@ /* eslint-disable no-console */ -import * as AWS from 'aws-sdk-mock'; -import * as sinon from 'sinon'; +import { + AutoScalingClient, UpdateAutoScalingGroupCommand, ScalingActivityInProgressFault, +} from '@aws-sdk/client-auto-scaling'; +import { mockClient } from 'aws-sdk-client-mock'; import * as lambdaCode from '../index'; +import 'aws-sdk-client-mock-jest'; -AWS.setSDK(require.resolve('aws-sdk')); +const autoScalingMock = mockClient(AutoScalingClient); const sampleEvent = { Records: [{ @@ -43,20 +46,19 @@ beforeEach(() => { }); afterEach(() => { - AWS.restore(); + autoScalingMock.reset(); }); test('success scenario single fleet', async () => { // WHEN - const updateAutoScalingGroupFake = sinon.fake.resolves({}); - AWS.mock('AutoScaling', 'updateAutoScalingGroup', updateAutoScalingGroupFake); + autoScalingMock.on(UpdateAutoScalingGroupCommand).resolves({}); const result = (await lambdaCode.handler(sampleEvent, context)); // THEN expect(result.status).toEqual('OK'); - sinon.assert.calledWith(updateAutoScalingGroupFake, { + expect(autoScalingMock).toHaveReceivedCommandWith(UpdateAutoScalingGroupCommand, { AutoScalingGroupName: 'testFleetId', MaxSize: 0, MinSize: 0, @@ -66,11 +68,9 @@ test('success scenario single fleet', async () => { test('failure scenario, AWS api returns failure', async () => { // WHEN - const error = new Error() as NodeJS.ErrnoException; - error.code = 'AccessDeniedException'; + const error = new ScalingActivityInProgressFault({message: 'test error', $metadata: {}}); - const updateAutoScalingGroupFake = sinon.fake.rejects(error); - AWS.mock('AutoScaling', 'updateAutoScalingGroup', updateAutoScalingGroupFake); + autoScalingMock.on(UpdateAutoScalingGroupCommand).rejects(error); const result = (await lambdaCode.handler(sampleEvent, context)); @@ -78,7 +78,7 @@ test('failure scenario, AWS api returns failure', async () => { expect(result.status).toEqual('ERROR'); expect(result.reason).toMatch(/Exception while suspending fleet/); - sinon.assert.calledWith(updateAutoScalingGroupFake, { + expect(autoScalingMock).toHaveReceivedCommandWith(UpdateAutoScalingGroupCommand, { AutoScalingGroupName: 'testFleetId', MaxSize: 0, MinSize: 0, @@ -91,15 +91,14 @@ test('failure scenario, MetricStat not found', async () => { const successEventSingle = JSON.parse(JSON.stringify(sampleEvent)); successEventSingle.Records[0].Sns.Message = '{"AlarmName":"testAlarm","AlarmDescription":null,"NewStateValue":"ALARM","NewStateReason":"Threshold Crossed: 5 out of the last 5 datapoints were less than the threshold (65.0). The most recent datapoints which crossed the threshold: [0.0 (29/04/20 23:32:00), 0.0 (29/04/20 23:31:00), 0.0 (29/04/20 23:30:00), 0.0 (29/04/20 23:29:00), 0.0 (29/04/20 23:28:00)] (minimum 5 datapoints for OK -> ALARM transition).","StateChangeTime":"2020-04-29T23:33:34.876+0000","Region":"US West (Oregon)","AlarmArn":"test-arn","OldStateValue":"INSUFFICIENT_DATA","Trigger":{"Period":60,"EvaluationPeriods":5,"ComparisonOperator":"LessThanThreshold","Threshold":65.0,"TreatMissingData":"- TreatMissingData: missing","EvaluateLowSampleCountPercentile":"","Metrics":[{"Expression":"100*(healthyHostCount/fleetCapacity)","Id":"expr_1","ReturnData":true},{"Id":"healthyHostCount","Label":"HealthyHostCount","MetricStat":{"Metric":{"Dimensions":[{"value":"testTargetGroup","name":"TargetGroup"},{"value":"testLoadBalancer","name":"LoadBalancer"}],"MetricName":"HealthyHostCount","Namespace":"AWS/NetworkELB"},"Period":60,"Stat":"Average"},"ReturnData":false},{"Id":"fleetCapacity","Label":"GroupDesiredCapacity","M":{"Metric":{"Dimensions":[{"value":"testFleetId2","name":"AutoScalingGroupName"}],"MetricName":"GroupDesiredCapacity","Namespace":"AWS/AutoScaling"},"Period":60,"Stat":"Average"},"ReturnData":false}]}}'; - const updateAutoScalingGroupFake = sinon.fake.resolves({}); - AWS.mock('AutoScaling', 'updateAutoScalingGroup', updateAutoScalingGroupFake); + autoScalingMock.on(UpdateAutoScalingGroupCommand).resolves({}); const result = (await lambdaCode.handler(successEventSingle, context)); // THEN expect(result.status).toEqual('ERROR'); - sinon.assert.notCalled(updateAutoScalingGroupFake); + expect(autoScalingMock).not.toHaveReceivedCommand(UpdateAutoScalingGroupCommand); }); test('Error if 2 records are found', async () => { @@ -107,8 +106,7 @@ test('Error if 2 records are found', async () => { const successEventSingle = JSON.parse(JSON.stringify(sampleEvent)); successEventSingle.Records.push(JSON.parse(JSON.stringify(successEventSingle.Records[0]))); - const updateAutoScalingGroupFake = sinon.fake.resolves({}); - AWS.mock('AutoScaling', 'updateAutoScalingGroup', updateAutoScalingGroupFake); + autoScalingMock.on(UpdateAutoScalingGroupCommand).resolves({}); const result = (await lambdaCode.handler(successEventSingle, context)); @@ -116,7 +114,7 @@ test('Error if 2 records are found', async () => { expect(result.status).toEqual('ERROR'); expect(result.reason).toMatch(/Expecting a single record in SNS Event/); - sinon.assert.notCalled(updateAutoScalingGroupFake); + expect(autoScalingMock).not.toHaveReceivedCommand(UpdateAutoScalingGroupCommand); }); test('Error if exactly 3 metrics are not found', async () => { @@ -124,8 +122,7 @@ test('Error if exactly 3 metrics are not found', async () => { const successEventSingle = JSON.parse(JSON.stringify(sampleEvent)); successEventSingle.Records[0].Sns.Message = '{"AlarmName":"testAlarm","AlarmDescription":null,"NewStateValue":"ALARM","NewStateReason":"Threshold Crossed: 5 out of the last 5 datapoints were less than the threshold (65.0). The most recent datapoints which crossed the threshold: [0.0 (29/04/20 23:32:00), 0.0 (29/04/20 23:31:00), 0.0 (29/04/20 23:30:00), 0.0 (29/04/20 23:29:00), 0.0 (29/04/20 23:28:00)] (minimum 5 datapoints for OK -> ALARM transition).","StateChangeTime":"2020-04-29T23:33:34.876+0000","Region":"US West (Oregon)","AlarmArn":"test-arn","OldStateValue":"INSUFFICIENT_DATA","Trigger":{"Period":60,"EvaluationPeriods":5,"ComparisonOperator":"LessThanThreshold","Threshold":65.0,"TreatMissingData":"- TreatMissingData: missing","EvaluateLowSampleCountPercentile":"","Metrics":[{"Id":"healthyHostCount","Label":"HealthyHostCount","MetricStat":{"Metric":{"Dimensions":[{"value":"testTargetGroup","name":"TargetGroup"},{"value":"testLoadBalancer","name":"LoadBalancer"}],"MetricName":"HealthyHostCount","Namespace":"AWS/NetworkELB"},"Period":60,"Stat":"Average"},"ReturnData":false},{"Id":"fleetCapacity","Label":"GroupDesiredCapacity","MetricStat":{"Metric":{"Dimensions":[{"value":"testFleetId2","name":"AutoScalingGroupName"}],"MetricName":"GroupDesiredCapacity","Namespace":"AWS/AutoScaling"},"Period":60,"Stat":"Average"},"ReturnData":false}]}}'; - const updateAutoScalingGroupFake = sinon.fake.resolves({}); - AWS.mock('AutoScaling', 'updateAutoScalingGroup', updateAutoScalingGroupFake); + autoScalingMock.on(UpdateAutoScalingGroupCommand).resolves({}); const result = (await lambdaCode.handler(successEventSingle, context)); @@ -133,21 +130,20 @@ test('Error if exactly 3 metrics are not found', async () => { expect(result.status).toEqual('ERROR'); expect(result.reason).toMatch(/Exactly 3 metrics should be present in the alarm message/); - sinon.assert.notCalled(updateAutoScalingGroupFake); + expect(autoScalingMock).not.toHaveReceivedCommand(UpdateAutoScalingGroupCommand); }); test('failure scenario, incorrect dimension, metrics and message', async () => { // WHEN const successEventSingle = JSON.parse(JSON.stringify(sampleEvent)); - const updateAutoScalingGroupFake = sinon.fake.resolves({}); - AWS.mock('AutoScaling', 'updateAutoScalingGroup', updateAutoScalingGroupFake); + autoScalingMock.on(UpdateAutoScalingGroupCommand).resolves({}); successEventSingle.Records[0].Sns.Message = '{"AlarmName":"testAlarm","AlarmDescription":null,"NewStateValue":"ALARM","NewStateReason":"Threshold Crossed: 5 out of the last 5 datapoints were less than the threshold (65.0). The most recent datapoints which crossed the threshold: [0.0 (29/04/20 23:32:00), 0.0 (29/04/20 23:31:00), 0.0 (29/04/20 23:30:00), 0.0 (29/04/20 23:29:00), 0.0 (29/04/20 23:28:00)] (minimum 5 datapoints for OK -> ALARM transition).","StateChangeTime":"2020-04-29T23:33:34.876+0000","Region":"US West (Oregon)","AlarmArn":"test-arn","OldStateValue":"INSUFFICIENT_DATA","Trigger":{"Period":60,"EvaluationPeriods":5,"ComparisonOperator":"LessThanThreshold","Threshold":65.0,"TreatMissingData":"- TreatMissingData: missing","EvaluateLowSampleCountPercentile":"","Metrics":[{"Expression":"100*(healthyHostCount/fleetCapacity)","Id":"expr_1","ReturnData":true},{"Id":"healthyHostCount","Label":"HealthyHostCount","MetricStat":{"Metric":{"Dimensions":[{"value":"testTargetGroup","name":"TargetGroup"},{"value":"testLoadBalancer","name":"LoadBalancer"}],"MetricName":"HealthyHostCount","Namespace":"AWS/NetworkELB"},"Period":60,"Stat":"Average"},"ReturnData":false},{"Id":"fleetCapacity","Label":"GroupDesiredCapacity","MetricStat":{"Metric":{"Dimensions":[{"value":"testFleetId","name":"AutoScalingGroup"}],"MetricName":"GroupDesiredCapacity","Namespace":"AWS/AutoScaling"},"Period":60,"Stat":"Average"},"ReturnData":false}]}}'; (await lambdaCode.handler(successEventSingle, context)); // THEN - sinon.assert.notCalled(updateAutoScalingGroupFake); + expect(autoScalingMock).not.toHaveReceivedCommand(UpdateAutoScalingGroupCommand); // WHEN successEventSingle.Records[0].Sns.Message = '{"AlarmName":"testAlarm","AlarmDescription":null,"NewStateValue":"ALARM","NewStateReason":"Threshold Crossed: 5 out of the last 5 datapoints were less than the threshold (65.0). The most recent datapoints which crossed the threshold: [0.0 (29/04/20 23:32:00), 0.0 (29/04/20 23:31:00), 0.0 (29/04/20 23:30:00), 0.0 (29/04/20 23:29:00), 0.0 (29/04/20 23:28:00)] (minimum 5 datapoints for OK -> ALARM transition).","StateChangeTime":"2020-04-29T23:33:34.876+0000","Region":"US West (Oregon)","AlarmArn":"test-arn","OldStateValue":"INSUFFICIENT_DATA","Trigger":{"Period":60,"EvaluationPeriods":5,"ComparisonOperator":"LessThanThreshold","Threshold":65.0,"TreatMissingData":"- TreatMissingData: missing","EvaluateLowSampleCountPercentile":"","Metrics":[{"Expression":"100*(healthyHostCount/fleetCapacity)","Id":"expr_1","ReturnData":true},{"Id":"healthyHostCount","Label":"HealthyHostCount","MetricStat":{"Metric":{"Dimensions":[{"value":"testTargetGroup","name":"TargetGroup"},{"value":"testLoadBalancer","name":"LoadBalancer"}],"MetricName":"HealthyHostCount","Namespace":"AWS/NetworkELB"},"Period":60,"Stat":"Average"},"ReturnData":false},{"Id":"fleetCapacity","Label":"GroupDesiredCapacity","MetricStat":{"Metric":{"Dimen":[{"value":"testFleetId2","name":"AutoScalingGroupName"}],"MetricName":"GroupDesiredCapacity","Namespace":"AWS/AutoScaling"},"Period":60,"Stat":"Average"},"ReturnData":false}]}}'; @@ -155,21 +151,21 @@ test('failure scenario, incorrect dimension, metrics and message', async () => { (await lambdaCode.handler(successEventSingle, context)); // THEN - sinon.assert.notCalled(updateAutoScalingGroupFake); + expect(autoScalingMock).not.toHaveReceivedCommand(UpdateAutoScalingGroupCommand); // WHEN successEventSingle.Records[0].Sns.Message = '{"AlarmName":"testAlarm","AlarmDescription":null,"NewStateValue":"ALARM","NewStateReason":"Threshold Crossed: 5 out of the last 5 datapoints were less than the threshold (65.0). The most recent datapoints which crossed the threshold: [0.0 (29/04/20 23:32:00), 0.0 (29/04/20 23:31:00), 0.0 (29/04/20 23:30:00), 0.0 (29/04/20 23:29:00), 0.0 (29/04/20 23:28:00)] (minimum 5 datapoints for OK -> ALARM transition).","StateChangeTime":"2020-04-29T23:33:34.876+0000","Region":"US West (Oregon)","AlarmArn":"test-arn","OldStateValue":"INSUFFICIENT_DATA","Trigger":{"Period":60,"EvaluationPeriods":5,"ComparisonOperator":"LessThanThreshold","Threshold":65.0,"TreatMissingData":"- TreatMissingData: missing","EvaluateLowSampleCountPercentile":"","M":[{"Expression":"100*(healthyHostCount/fleetCapacity)","Id":"expr_1","ReturnData":true},{"Id":"healthyHostCount","Label":"HealthyHostCount","MetricStat":{"Metric":{"Dimensions":[{"value":"testTargetGroup","name":"TargetGroup"},{"value":"testLoadBalancer","name":"LoadBalancer"}],"MetricName":"HealthyHostCount","Namespace":"AWS/NetworkELB"},"Period":60,"Stat":"Average"},"ReturnData":false},{"Id":"fleetCapacity","Label":"GroupDesiredCapacity","MetricStat":{"Metric":{"Dimen":[{"value":"testFleetId2","name":"AutoScalingGroupName"}],"MetricName":"GroupDesiredCapacity","Namespace":"AWS/AutoScaling"},"Period":60,"Stat":"Average"},"ReturnData":false}]}}'; (await lambdaCode.handler(successEventSingle, context)); // THEN - sinon.assert.notCalled(updateAutoScalingGroupFake); + expect(autoScalingMock).not.toHaveReceivedCommand(UpdateAutoScalingGroupCommand); // WHEN delete successEventSingle.Records[0].Sns.Message; (await lambdaCode.handler(successEventSingle, context)); // THEN - sinon.assert.notCalled(updateAutoScalingGroupFake); + expect(autoScalingMock).not.toHaveReceivedCommand(UpdateAutoScalingGroupCommand); // WHEN successEventSingle.Records[0].Sns.Message = '{"AlarmName":"testAlarm","AlarmDescription":null,"NewStateValue":"ALARM","NewStateReason":"Threshold Crossed: 5 out of the last 5 datapoints were less than the threshold (65.0). The most recent datapoints which crossed the threshold: [0.0 (29/04/20 23:32:00), 0.0 (29/04/20 23:31:00), 0.0 (29/04/20 23:30:00), 0.0 (29/04/20 23:29:00), 0.0 (29/04/20 23:28:00)] (minimum 5 datapoints for OK -> ALARM transition).","StateChangeTime":"2020-04-29T23:33:34.876+0000","Region":"US West (Oregon)","AlarmArn":"test-arn","OldStateValue":"INSUFFICIENT_DATA","Trigger":{"Period":60,"EvaluationPeriods":5,"ComparisonOperator":"LessThanThreshold","Threshold":65.0,"TreatMissingData":"- TreatMissingData: missing","EvaluateLowSampleCountPercentile":"","Metrics":[{"Expression":"100*(healthyHostCount/fleetCapacity)","Id":"expr_1","ReturnData":true},{"Id":"healthyHostCount","Label":"HealthyHostCount","MetricStat":{"Metric":{"Dimensions":[{"value":"testTargetGroup","name":"TargetGroup"},{"value":"testLoadBalancer","name":"LoadBalancer"}],"MetricName":"HealthyHostCount","Namespace":"AWS/NetworkELB"},"Period":60,"Stat":"Average"},"ReturnData":false},{"Id":"eetCapacity","Label":"GroupDesiredCapacity","MetricStat":{"Metric":{"Dimensions":[{"value":"testFleetId","name":"AutoScalingGroupName"}],"MetricName":"GroupDesiredCapacity","Namespace":"AWS/AutoScaling"},"Period":60,"Stat":"Average"},"ReturnData":false}]}}'; @@ -177,5 +173,5 @@ test('failure scenario, incorrect dimension, metrics and message', async () => { (await lambdaCode.handler(successEventSingle, context)); // THEN - sinon.assert.notCalled(updateAutoScalingGroupFake); + expect(autoScalingMock).not.toHaveReceivedCommand(UpdateAutoScalingGroupCommand); }); diff --git a/packages/aws-rfdk/lib/lambdas/nodejs/wait-for-stable-service/handler.ts b/packages/aws-rfdk/lib/lambdas/nodejs/wait-for-stable-service/handler.ts index 27d16cb67..7cc0c6854 100644 --- a/packages/aws-rfdk/lib/lambdas/nodejs/wait-for-stable-service/handler.ts +++ b/packages/aws-rfdk/lib/lambdas/nodejs/wait-for-stable-service/handler.ts @@ -5,8 +5,13 @@ /* eslint-disable no-console */ -// eslint-disable-next-line import/no-extraneous-dependencies -import { ECS, AWSError } from 'aws-sdk'; +/* eslint-disable import/no-extraneous-dependencies */ +import { + ECSClient, + waitUntilServicesStable, +} from '@aws-sdk/client-ecs'; +/* eslint-enable import/no-extraneous-dependencies */ + import { LambdaContext } from '../lib/aws-lambda'; import { CfnRequestEvent, @@ -33,9 +38,9 @@ export interface WaitForStableServiceResourceProps { * A custom resource used to save Spot Event Plugin server data and configurations. */ export class WaitForStableServiceResource extends SimpleCustomResource { - protected readonly ecsClient: ECS; + protected readonly ecsClient: ECSClient; - constructor(ecsClient: ECS) { + constructor(ecsClient: ECSClient) { super(); this.ecsClient = ecsClient; } @@ -58,10 +63,10 @@ export class WaitForStableServiceResource extends SimpleCustomResource { try { console.log(`Waiting for ECS services to stabilize. Cluster: ${resourceProperties.cluster}. Services: ${resourceProperties.services}`); - await this.ecsClient.waitFor('servicesStable', options).promise(); + await waitUntilServicesStable({client: this.ecsClient, maxWaitTime: 600}, options); console.log('Finished waiting. ECS services are stable.'); } catch (e) { - throw new Error(`ECS services failed to stabilize in expected time: ${(e as AWSError)?.code} -- ${(e as AWSError)?.message}`); + throw new Error(`ECS services failed to stabilize in expected time: ${(e as Error)?.name} -- ${(e as Error)?.message}`); } return undefined; @@ -92,6 +97,6 @@ export class WaitForStableServiceResource extends SimpleCustomResource { */ /* istanbul ignore next */ export async function wait(event: CfnRequestEvent, context: LambdaContext): Promise { - const handler = new WaitForStableServiceResource(new ECS()); + const handler = new WaitForStableServiceResource(new ECSClient()); return await handler.handler(event, context); } diff --git a/packages/aws-rfdk/lib/lambdas/nodejs/wait-for-stable-service/test/handler.test.ts b/packages/aws-rfdk/lib/lambdas/nodejs/wait-for-stable-service/test/handler.test.ts index c9e7543dc..e9f8e958b 100644 --- a/packages/aws-rfdk/lib/lambdas/nodejs/wait-for-stable-service/test/handler.test.ts +++ b/packages/aws-rfdk/lib/lambdas/nodejs/wait-for-stable-service/test/handler.test.ts @@ -5,8 +5,13 @@ /* eslint-disable no-console */ -import * as AWS from 'aws-sdk'; -import { mock, restore, setSDKInstance } from 'aws-sdk-mock'; +import { + ECSClient, + DescribeServicesCommand, + DescribeServicesResponse, +} from '@aws-sdk/client-ecs'; +import { mockClient } from 'aws-sdk-client-mock'; +import 'aws-sdk-client-mock-jest'; import { WaitForStableServiceResource, WaitForStableServiceResourceProps, @@ -15,16 +20,15 @@ import { describe('WaitForStableServiceResource', () => { describe('doCreate', () => { let consoleLogMock: jest.SpyInstance; + const ecsMock = mockClient(ECSClient); beforeEach(() => { - setSDKInstance(AWS); - AWS.config.region = 'us-east-1'; consoleLogMock = jest.spyOn(console, 'log').mockReturnValue(undefined); }); afterEach(() => { jest.clearAllMocks(); - restore('ECS'); + ecsMock.reset(); }); test('success', async () => { @@ -34,10 +38,18 @@ describe('WaitForStableServiceResource', () => { services: ['serviceArn'], }; - mock('ECS', 'waitFor', (_state: 'servicesStable', _params: any, callback: Function) => { - callback(null, { status: 'ready' }); - }); - const handler = new WaitForStableServiceResource(new AWS.ECS()); + // response for an "ACTIVE" service + const response: DescribeServicesResponse = { + services: [ + { + deployments: [{}], + runningCount: 1, + desiredCount: 1, + }, + ], + }; + ecsMock.on(DescribeServicesCommand).resolves(response); + const handler = new WaitForStableServiceResource(new ECSClient()); // WHEN const result = await handler.doCreate('physicalId', props); @@ -56,10 +68,8 @@ describe('WaitForStableServiceResource', () => { services: ['serviceArn'], }; - mock('ECS', 'waitFor', (_state: 'servicesStable', _params: any, callback: Function) => { - callback({ code: 'errorcode', message: 'not stable' }, null); - }); - const handler = new WaitForStableServiceResource(new AWS.ECS()); + ecsMock.on(DescribeServicesCommand).resolves({failures: [{reason: 'MISSING', detail: 'test failure'}]}); + const handler = new WaitForStableServiceResource(new ECSClient()); // WHEN const promise = handler.doCreate('physicalId', props); @@ -75,7 +85,7 @@ describe('WaitForStableServiceResource', () => { cluster: 'clusterArn', services: ['serviceArn'], }; - const handler = new WaitForStableServiceResource(new AWS.ECS()); + const handler = new WaitForStableServiceResource(new ECSClient()); // WHEN const promise = await handler.doDelete('physicalId', props); @@ -93,7 +103,7 @@ describe('WaitForStableServiceResource', () => { forceRun: '', }; // WHEN - const handler = new WaitForStableServiceResource(new AWS.ECS()); + const handler = new WaitForStableServiceResource(new ECSClient()); const returnValue = handler.validateInput(validInput); // THEN @@ -135,7 +145,7 @@ describe('WaitForStableServiceResource', () => { forceRunNotString, ])('returns false with invalid input %p', async (invalidInput: any) => { // WHEN - const handler = new WaitForStableServiceResource(new AWS.ECS()); + const handler = new WaitForStableServiceResource(new ECSClient()); const returnValue = handler.validateInput(invalidInput); // THEN diff --git a/packages/aws-rfdk/lib/lambdas/nodejs/x509-certificate/acm-handlers.ts b/packages/aws-rfdk/lib/lambdas/nodejs/x509-certificate/acm-handlers.ts index fa0459bcf..7fb77e45d 100644 --- a/packages/aws-rfdk/lib/lambdas/nodejs/x509-certificate/acm-handlers.ts +++ b/packages/aws-rfdk/lib/lambdas/nodejs/x509-certificate/acm-handlers.ts @@ -6,8 +6,25 @@ /* eslint-disable no-console */ import * as crypto from 'crypto'; -// eslint-disable-next-line import/no-extraneous-dependencies -import { ACM, DynamoDB, SecretsManager, AWSError } from 'aws-sdk'; + +/* eslint-disable import/no-extraneous-dependencies */ +import { + ACMClient, + AccessDeniedException, + DeleteCertificateCommand, + DescribeCertificateCommand, + GetCertificateCommand, + ImportCertificateRequest, + ImportCertificateCommand, +} from '@aws-sdk/client-acm'; +import { + DynamoDBClient, +} from '@aws-sdk/client-dynamodb'; +import { + SecretsManagerClient, + GetSecretValueCommand, +} from '@aws-sdk/client-secrets-manager'; +/* eslint-enable import/no-extraneous-dependencies */ import { LambdaContext } from '../lib/aws-lambda'; import { BackoffGenerator } from '../lib/backoff-generator'; @@ -19,19 +36,14 @@ import { implementsIAcmImportCertProps, } from './types'; - -const ACM_VERSION = '2015-12-08'; -const DYNAMODB_VERSION = '2012-08-10'; -const SECRETS_MANAGER_VERSION = '2017-10-17'; - export class AcmCertificateImporter extends DynamoBackedCustomResource { - protected readonly acmClient: ACM; - protected readonly secretsManagerClient: SecretsManager; + protected readonly acmClient: ACMClient; + protected readonly secretsManagerClient: SecretsManagerClient; constructor( - acmClient: ACM, - dynamoDbClient: DynamoDB, - secretsManagerClient: SecretsManager, + acmClient: ACMClient, + dynamoDbClient: DynamoDBClient, + secretsManagerClient: SecretsManagerClient, ) { super(dynamoDbClient); @@ -91,9 +103,9 @@ export class AcmCertificateImporter extends DynamoBackedCustomResource { }); do { - const { Certificate: cert } = await this.acmClient.describeCertificate({ + const { Certificate: cert } = await this.acmClient.send(new DescribeCertificateCommand({ CertificateArn: arn, - }).promise(); + })); inUseByResources = cert!.InUseBy || []; @@ -110,12 +122,12 @@ export class AcmCertificateImporter extends DynamoBackedCustomResource { } console.log(`Deleting resource for '${key}'`); try { - await this.acmClient.deleteCertificate({ CertificateArn: arn }).promise(); + await this.acmClient.send(new DeleteCertificateCommand({ CertificateArn: arn })); } catch (e) { // AccessDeniedException can happen if either: // a) We do not have the required permission to delete the Certificate (unlikely) // b) The Certificate has already been deleted (more likely) - if ((e as AWSError)?.message.indexOf('AccessDeniedException')) { + if (e instanceof AccessDeniedException) { console.warn(`Could not delete Certificate ${arn}. Please ensure it has been deleted.`); } throw e; // Rethrow so the custom resource handler will error-out. @@ -137,11 +149,16 @@ export class AcmCertificateImporter extends DynamoBackedCustomResource { }): Promise { let certificateArn: string; + const certificate = Buffer.from(args.cert); + const certificateChain = args.certChain ? Buffer.from(args.certChain) : undefined; + const privateKey = Buffer.from(args.key); + const sortKey = crypto.createHash('md5').update(args.cert).digest('hex'); const existingItem = await args.resourceTable.getItem({ primaryKeyValue: args.physicalId, sortKeyValue: sortKey, }); + if (existingItem) { if (!existingItem.ARN) { throw Error("Database Item missing 'ARN' attribute"); @@ -150,25 +167,25 @@ export class AcmCertificateImporter extends DynamoBackedCustomResource { // Verify that the cert is in ACM certificateArn = existingItem.ARN as string; try { - await this.acmClient.getCertificate({ CertificateArn: certificateArn }).promise(); + await this.acmClient.send(new GetCertificateCommand({ CertificateArn: certificateArn })); } catch(e) { throw Error(`Database entry ${existingItem.ARN} could not be found in ACM: ${JSON.stringify(e)}`); } // Update the cert by performing an import again, with the new values. - const importCertRequest = { + const importCertRequest: ImportCertificateRequest = { CertificateArn: certificateArn, - Certificate: args.cert, - CertificateChain: args.certChain, - PrivateKey: args.key, + Certificate: certificate, + CertificateChain: certificateChain, + PrivateKey: privateKey, Tags: args.tags, }; await this.importCertificate(importCertRequest); } else { - const importCertRequest = { - Certificate: args.cert, - CertificateChain: args.certChain, - PrivateKey: args.key, + const importCertRequest: ImportCertificateRequest = { + Certificate: certificate, + CertificateChain: certificateChain, + PrivateKey: privateKey, Tags: args.tags, }; @@ -192,7 +209,7 @@ export class AcmCertificateImporter extends DynamoBackedCustomResource { return certificateArn; } - private async importCertificate(importCertRequest: ACM.ImportCertificateRequest) { + private async importCertificate(importCertRequest: ImportCertificateRequest) { // ACM cert imports are limited to 1 per second (see https://docs.aws.amazon.com/acm/latest/userguide/acm-limits.html#api-rate-limits) // We need to backoff & retry in the event that two imports happen in the same second const maxAttempts = 10; @@ -205,7 +222,7 @@ export class AcmCertificateImporter extends DynamoBackedCustomResource { let retry = false; do { try { - return await this.acmClient.importCertificate(importCertRequest).promise(); + return await this.acmClient.send(new ImportCertificateCommand(importCertRequest)); } catch (e) { console.warn(`Could not import certificate: ${e}`); retry = await backoffGenerator.backoff(); @@ -220,7 +237,7 @@ export class AcmCertificateImporter extends DynamoBackedCustomResource { private async getSecretString(SecretId: string): Promise { console.debug(`Retrieving secret: ${SecretId}`); - const resp = await this.secretsManagerClient.getSecretValue({ SecretId }).promise(); + const resp = await this.secretsManagerClient.send(new GetSecretValueCommand({ SecretId })); if (!resp.SecretString) { throw new Error(`Secret ${SecretId} did not contain a SecretString as expected`); } @@ -234,9 +251,9 @@ export class AcmCertificateImporter extends DynamoBackedCustomResource { /* istanbul ignore next */ export async function importCert(event: CfnRequestEvent, context: LambdaContext): Promise { const handler = new AcmCertificateImporter( - new ACM({ apiVersion: ACM_VERSION }), - new DynamoDB({ apiVersion: DYNAMODB_VERSION }), - new SecretsManager({ apiVersion: SECRETS_MANAGER_VERSION }), + new ACMClient(), + new DynamoDBClient(), + new SecretsManagerClient(), ); return await handler.handler(event, context); } diff --git a/packages/aws-rfdk/lib/lambdas/nodejs/x509-certificate/handlers.ts b/packages/aws-rfdk/lib/lambdas/nodejs/x509-certificate/handlers.ts index 6ce7c3a9f..838fe62c7 100644 --- a/packages/aws-rfdk/lib/lambdas/nodejs/x509-certificate/handlers.ts +++ b/packages/aws-rfdk/lib/lambdas/nodejs/x509-certificate/handlers.ts @@ -5,10 +5,15 @@ /* eslint-disable no-console */ -// eslint-disable-next-line import/no-extraneous-dependencies +/* eslint-disable import/no-extraneous-dependencies */ import { randomBytes } from 'crypto'; -// eslint-disable-next-line import/no-extraneous-dependencies -import { DynamoDB, SecretsManager, AWSError } from 'aws-sdk'; +import { + DynamoDBClient, +} from '@aws-sdk/client-dynamodb'; +import { + SecretsManagerClient, +} from '@aws-sdk/client-secrets-manager'; +/* eslint-enable import/no-extraneous-dependencies */ import { LambdaContext } from '../lib/aws-lambda'; @@ -32,15 +37,12 @@ import { } from './types'; -const DYNAMODB_VERSION = '2012-08-10'; -const SECRETS_MANAGER_VERSION = '2017-10-17'; - abstract class X509Common extends DynamoBackedCustomResource { - protected readonly secretsManagerClient: SecretsManager; + protected readonly secretsManagerClient: SecretsManagerClient; constructor( - dynamoDbClient: DynamoDB, - secretsManagerClient: SecretsManager, + dynamoDbClient: DynamoDBClient, + secretsManagerClient: SecretsManagerClient, ) { super(dynamoDbClient); @@ -64,7 +66,7 @@ abstract class X509Common extends DynamoBackedCustomResource { // AccessDeniedException can happen if either: // a) We legit do not have the required permission to delete the secret (very unlikely) // b) The Secret has already been deleted (much more likely; so we continue) - if ((e as AWSError)?.message.indexOf('AccessDeniedException')) { + if ((e as Error)?.message.indexOf('AccessDeniedException')) { console.warn(`Could not delete Secret ${arn}. Please ensure it has been deleted.`); } throw e; // Rethrow so the custom resource handler will error-out. @@ -156,8 +158,8 @@ abstract class X509Common extends DynamoBackedCustomResource { export class X509CertificateGenerator extends X509Common { constructor( - dynamoDbClient: DynamoDB, - secretsManagerClient: SecretsManager, + dynamoDbClient: DynamoDBClient, + secretsManagerClient: SecretsManagerClient, ) { super(dynamoDbClient, secretsManagerClient); } @@ -242,8 +244,8 @@ export class X509CertificateGenerator extends X509Common { export class X509CertificateConverter extends X509Common { constructor( - dynamoDbClient: DynamoDB, - secretsManagerClient: SecretsManager, + dynamoDbClient: DynamoDBClient, + secretsManagerClient: SecretsManagerClient, ) { super(dynamoDbClient, secretsManagerClient); } @@ -301,8 +303,8 @@ export class X509CertificateConverter extends X509Common { */ export async function generate(event: CfnRequestEvent, context: LambdaContext): Promise { const handler = new X509CertificateGenerator( - new DynamoDB({ apiVersion: DYNAMODB_VERSION }), - new SecretsManager({ apiVersion: SECRETS_MANAGER_VERSION }), + new DynamoDBClient(), + new SecretsManagerClient(), ); return await handler.handler(event, context); } @@ -312,8 +314,8 @@ export async function generate(event: CfnRequestEvent, context: LambdaContext): */ export async function convert(event: CfnRequestEvent, context: LambdaContext): Promise { const handler = new X509CertificateConverter( - new DynamoDB({ apiVersion: DYNAMODB_VERSION }), - new SecretsManager({ apiVersion: SECRETS_MANAGER_VERSION }), + new DynamoDBClient(), + new SecretsManagerClient(), ); return await handler.handler(event, context); } diff --git a/packages/aws-rfdk/lib/lambdas/nodejs/x509-certificate/test/acm-handlers.test.ts b/packages/aws-rfdk/lib/lambdas/nodejs/x509-certificate/test/acm-handlers.test.ts index 9f8e9f685..2532343be 100644 --- a/packages/aws-rfdk/lib/lambdas/nodejs/x509-certificate/test/acm-handlers.test.ts +++ b/packages/aws-rfdk/lib/lambdas/nodejs/x509-certificate/test/acm-handlers.test.ts @@ -3,8 +3,25 @@ * SPDX-License-Identifier: Apache-2.0 */ -import * as AWS from 'aws-sdk'; -import * as AWSMock from 'aws-sdk-mock'; +import { + ACMClient, + AccessDeniedException, + DescribeCertificateCommand, + DeleteCertificateCommand, + GetCertificateCommand, + ImportCertificateCommand, + ResourceNotFoundException, + ThrottlingException, +} from '@aws-sdk/client-acm'; +import { + DynamoDBClient, +} from '@aws-sdk/client-dynamodb'; +import { + SecretsManagerClient, + GetSecretValueCommand, +} from '@aws-sdk/client-secrets-manager'; +import { mockClient } from 'aws-sdk-client-mock'; +import 'aws-sdk-client-mock-jest'; import * as sinon from 'sinon'; import { BackoffGenerator } from '../../lib/backoff-generator'; import { CompositeStringIndexTable } from '../../lib/dynamodb'; @@ -16,6 +33,8 @@ describe('AcmCertificateImporter', () => { const physicalId = 'physicalId'; const certArn = 'certArn'; const oldEnv = process.env; + const acmMock = mockClient(ACMClient); + const secretsManagerMock = mockClient(SecretsManagerClient); let consoleWarnSpy: jest.SpyInstance; beforeAll(() => { @@ -30,13 +49,13 @@ describe('AcmCertificateImporter', () => { beforeEach(() => { process.env.DATABASE = 'database'; - AWSMock.setSDKInstance(AWS); }); afterEach(() => { process.env = oldEnv; sinon.restore(); - AWSMock.restore(); + acmMock.reset(); + secretsManagerMock.reset(); }); describe('doCreate', () => { @@ -54,18 +73,17 @@ describe('AcmCertificateImporter', () => { sinon.stub(Certificate, 'decryptKey').returns(Promise.resolve('key')); // Mock out the API call in getSecretString - AWSMock.mock('SecretsManager', 'getSecretValue', sinon.fake.resolves({ SecretString: 'secret' })); + secretsManagerMock.on(GetSecretValueCommand).resolves({ SecretString: 'secret' }); }); test('throws when a secret does not have SecretString', async () => { // GIVEN - const getSecretValueFake = sinon.fake.resolves({}); - AWSMock.remock('SecretsManager', 'getSecretValue', getSecretValueFake); + secretsManagerMock.on(GetSecretValueCommand).resolves({}); const importer = new TestAcmCertificateImporter({ - acm: new AWS.ACM(), - dynamoDb: new AWS.DynamoDB(), - secretsManager: new AWS.SecretsManager(), + acm: new ACMClient(), + dynamoDb: new DynamoDBClient(), + secretsManager: new SecretsManagerClient(), resourceTableOverride: new MockCompositeStringIndexTable(), }); @@ -74,7 +92,7 @@ describe('AcmCertificateImporter', () => { // THEN .rejects.toThrow(/Secret .* did not contain a SecretString as expected/); - expect(getSecretValueFake.calledOnce).toBe(true); + expect(secretsManagerMock).toHaveReceivedCommandTimes(GetSecretValueCommand, 1); }); test('retries importing certificate', async () => { @@ -83,18 +101,17 @@ describe('AcmCertificateImporter', () => { const getItemStub = sinon.stub(resourceTable, 'getItem').resolves(undefined); const putItemStub = sinon.stub(resourceTable, 'putItem').resolves(true); - const importCertificateStub = sinon.stub() - .onFirstCall().rejects('Rate exceeded') - .onSecondCall().rejects('Rate exceeded') - .onThirdCall().resolves({ CertificateArn: certArn }); - AWSMock.mock('ACM', 'importCertificate', importCertificateStub); + acmMock.on(ImportCertificateCommand) + .rejectsOnce(new ThrottlingException({message: 'test error', $metadata: {}})) + .rejectsOnce(new ThrottlingException({message: 'test error', $metadata: {}})) + .resolves({ CertificateArn: certArn }); const backoffStub = sinon.stub(BackoffGenerator.prototype, 'backoff').resolves(true); const importer = new TestAcmCertificateImporter({ - acm: new AWS.ACM(), - dynamoDb: new AWS.DynamoDB(), - secretsManager: new AWS.SecretsManager(), + acm: new ACMClient(), + dynamoDb: new DynamoDBClient(), + secretsManager: new SecretsManagerClient(), resourceTableOverride: resourceTable, }); @@ -105,7 +122,7 @@ describe('AcmCertificateImporter', () => { .resolves.toEqual({ CertificateArn: certArn }); expect(getItemStub.calledOnce).toBe(true); expect(putItemStub.calledOnce).toBe(true); - expect(importCertificateStub.calledThrice).toBe(true); + expect(acmMock).toHaveReceivedCommandTimes(ImportCertificateCommand, 3); expect(backoffStub.callCount).toEqual(2); }); @@ -115,18 +132,17 @@ describe('AcmCertificateImporter', () => { const getItemStub = sinon.stub(resourceTable, 'getItem').resolves(undefined); const attempts = 10; - const importCertificateStub = sinon.stub(); + const importCertificateBehavior = acmMock.on(ImportCertificateCommand); const backoffStub = sinon.stub(BackoffGenerator.prototype, 'backoff'); for (let i = 0; i < attempts; i++) { - importCertificateStub.onCall(i).rejects('Rate exceeded'); + importCertificateBehavior.rejectsOnce(new ThrottlingException({message: 'test error', $metadata: {}})); backoffStub.onCall(i).resolves(i < attempts - 1); } - AWSMock.mock('ACM', 'importCertificate', importCertificateStub); const importer = new TestAcmCertificateImporter({ - acm: new AWS.ACM(), - dynamoDb: new AWS.DynamoDB(), - secretsManager: new AWS.SecretsManager(), + acm: new ACMClient(), + dynamoDb: new DynamoDBClient(), + secretsManager: new SecretsManagerClient(), resourceTableOverride: resourceTable, }); @@ -136,7 +152,7 @@ describe('AcmCertificateImporter', () => { // THEN .rejects.toThrow(/Failed to import certificate .* after [0-9]+ attempts\./); expect(getItemStub.calledOnce).toBe(true); - expect(importCertificateStub.callCount).toBe(attempts); + expect(acmMock).toHaveReceivedCommandTimes(ImportCertificateCommand, attempts); expect(backoffStub.callCount).toEqual(attempts); }); @@ -147,9 +163,9 @@ describe('AcmCertificateImporter', () => { const getItemStub = sinon.stub(resourceTable, 'getItem').resolves({}); const importer = new TestAcmCertificateImporter({ - acm: new AWS.ACM(), - dynamoDb: new AWS.DynamoDB(), - secretsManager: new AWS.SecretsManager(), + acm: new ACMClient(), + dynamoDb: new DynamoDBClient(), + secretsManager: new SecretsManagerClient(), resourceTableOverride: resourceTable, }); @@ -166,13 +182,13 @@ describe('AcmCertificateImporter', () => { const resourceTable = new MockCompositeStringIndexTable(); const getItemStub = sinon.stub(resourceTable, 'getItem').resolves({ ARN: certArn }); - const getCertificateFake = sinon.fake.rejects({}); - AWSMock.mock('ACM', 'getCertificate', getCertificateFake); + acmMock.on(GetCertificateCommand) + .rejects(new ResourceNotFoundException({message: 'not found', $metadata: {}})); const importer = new TestAcmCertificateImporter({ - acm: new AWS.ACM(), - dynamoDb: new AWS.DynamoDB(), - secretsManager: new AWS.SecretsManager(), + acm: new ACMClient(), + dynamoDb: new DynamoDBClient(), + secretsManager: new SecretsManagerClient(), resourceTableOverride: resourceTable, }); @@ -182,7 +198,7 @@ describe('AcmCertificateImporter', () => { // THEN .rejects.toThrow(new RegExp(`Database entry ${certArn} could not be found in ACM:`)); expect(getItemStub.calledOnce).toBe(true); - expect(getCertificateFake.calledOnce).toBe(true); + expect(acmMock).toHaveReceivedCommandTimes(GetCertificateCommand, 1); }); test('imports certificate', async () => { @@ -190,16 +206,14 @@ describe('AcmCertificateImporter', () => { const resourceTable = new MockCompositeStringIndexTable(); const getItemStub = sinon.stub(resourceTable, 'getItem').resolves({ ARN: certArn }); - const getCertificateFake = sinon.fake.resolves({ Certificate: 'cert' }); - AWSMock.mock('ACM', 'getCertificate', getCertificateFake); + acmMock.on(GetCertificateCommand).resolves({ Certificate: 'cert' }); - const importCertificateFake = sinon.fake.resolves({}); - AWSMock.mock('ACM', 'importCertificate', importCertificateFake); + acmMock.on(ImportCertificateCommand).resolves({}); const importer = new TestAcmCertificateImporter({ - acm: new AWS.ACM(), - dynamoDb: new AWS.DynamoDB(), - secretsManager: new AWS.SecretsManager(), + acm: new ACMClient(), + dynamoDb: new DynamoDBClient(), + secretsManager: new SecretsManagerClient(), resourceTableOverride: resourceTable, }); @@ -209,9 +223,9 @@ describe('AcmCertificateImporter', () => { // THEN .resolves.toEqual({ CertificateArn: certArn }); expect(getItemStub.calledOnce).toBe(true); - expect(getCertificateFake.calledOnce).toBe(true); + expect(acmMock).toHaveReceivedCommandTimes(GetCertificateCommand, 1); // Verify that we import the existing certificate to support replacing/updating of it (e.g. to rotate certs) - expect(importCertificateFake.calledOnce).toBe(true); + expect(acmMock).toHaveReceivedCommandTimes(ImportCertificateCommand, 1); }); }); @@ -221,13 +235,12 @@ describe('AcmCertificateImporter', () => { const resourceTable = new MockCompositeStringIndexTable(); const getItemStub = sinon.stub(resourceTable, 'getItem').resolves(undefined); - const importCertificateFake = sinon.fake.resolves({}); - AWSMock.mock('ACM', 'importCertificate', importCertificateFake); + acmMock.on(ImportCertificateCommand).resolves({}); const importer = new TestAcmCertificateImporter({ - acm: new AWS.ACM(), - dynamoDb: new AWS.DynamoDB(), - secretsManager: new AWS.SecretsManager(), + acm: new ACMClient(), + dynamoDb: new DynamoDBClient(), + secretsManager: new SecretsManagerClient(), resourceTableOverride: resourceTable, }); @@ -237,7 +250,7 @@ describe('AcmCertificateImporter', () => { // THEN .rejects.toThrow(/CertificateArn was not properly populated after attempt to import .*$/); expect(getItemStub.calledOnce).toBe(true); - expect(importCertificateFake.calledOnce).toBe(true); + expect(acmMock).toHaveReceivedCommandTimes(ImportCertificateCommand, 1); }); test('imports certificate', async () => { @@ -246,13 +259,12 @@ describe('AcmCertificateImporter', () => { const getItemStub = sinon.stub(resourceTable, 'getItem').resolves(undefined); const putItemStub = sinon.stub(resourceTable, 'putItem').resolves(true); - const importCertificateFake = sinon.fake.resolves({ CertificateArn: certArn }); - AWSMock.mock('ACM', 'importCertificate', importCertificateFake); + acmMock.on(ImportCertificateCommand).resolves({ CertificateArn: certArn }); const importer = new TestAcmCertificateImporter({ - acm: new AWS.ACM(), - dynamoDb: new AWS.DynamoDB(), - secretsManager: new AWS.SecretsManager(), + acm: new ACMClient(), + dynamoDb: new DynamoDBClient(), + secretsManager: new SecretsManagerClient(), resourceTableOverride: resourceTable, }); @@ -263,7 +275,7 @@ describe('AcmCertificateImporter', () => { .resolves.toEqual({ CertificateArn: certArn }); expect(getItemStub.calledOnce).toBe(true); expect(putItemStub.calledOnce).toBe(true); - expect(importCertificateFake.calledOnce).toBe(true); + expect(acmMock).toHaveReceivedCommandTimes(ImportCertificateCommand, 1); }); }); }); @@ -277,8 +289,7 @@ describe('AcmCertificateImporter', () => { key: { ARN: certArn }, }); - const describeCertificateFake = sinon.fake.resolves({ Certificate: { InUseBy: ['something'] } }); - AWSMock.mock('ACM', 'describeCertificate', describeCertificateFake); + acmMock.on(DescribeCertificateCommand).resolves({ Certificate: { InUseBy: ['something'] } }); // This is hardcoded in the code being tested const maxAttempts = 10; @@ -288,9 +299,9 @@ describe('AcmCertificateImporter', () => { .onCall(maxAttempts - 1).returns(false); const importer = new TestAcmCertificateImporter({ - acm: new AWS.ACM(), - dynamoDb: new AWS.DynamoDB(), - secretsManager: new AWS.SecretsManager(), + acm: new ACMClient(), + dynamoDb: new DynamoDBClient(), + secretsManager: new SecretsManagerClient(), resourceTableOverride: resourceTable, }); @@ -300,7 +311,7 @@ describe('AcmCertificateImporter', () => { // THEN .rejects.toEqual(new Error(`Response from describeCertificate did not contain an empty InUseBy list after ${maxAttempts} attempts.`)); expect(queryStub.calledOnce).toBe(true); - expect(describeCertificateFake.callCount).toEqual(maxAttempts); + expect(acmMock).toHaveReceivedCommandTimes(DescribeCertificateCommand, maxAttempts); expect(backoffStub.callCount).toEqual(maxAttempts); expect(shouldContinueStub.callCount).toEqual(maxAttempts); }); @@ -312,17 +323,15 @@ describe('AcmCertificateImporter', () => { key: { ARN: certArn }, }); - const describeCertificateFake = sinon.fake.resolves({ Certificate: { InUseBy: [] }}); - AWSMock.mock('ACM', 'describeCertificate', describeCertificateFake); + acmMock.on(DescribeCertificateCommand).resolves({ Certificate: { InUseBy: [] }}); const error = new Error('error'); - const deleteCertificateFake = sinon.fake.rejects(error); - AWSMock.mock('ACM', 'deleteCertificate', deleteCertificateFake); + acmMock.on(DeleteCertificateCommand).rejects(error); const importer = new TestAcmCertificateImporter({ - acm: new AWS.ACM(), - dynamoDb: new AWS.DynamoDB(), - secretsManager: new AWS.SecretsManager(), + acm: new ACMClient(), + dynamoDb: new DynamoDBClient(), + secretsManager: new SecretsManagerClient(), resourceTableOverride: resourceTable, }); @@ -332,8 +341,8 @@ describe('AcmCertificateImporter', () => { // THEN .rejects.toEqual(error); expect(queryStub.calledOnce).toBe(true); - expect(describeCertificateFake.calledOnce).toBe(true); - expect(deleteCertificateFake.calledOnce).toBe(true); + expect(acmMock).toHaveReceivedCommandTimes(DescribeCertificateCommand, 1); + expect(acmMock).toHaveReceivedCommandTimes(DeleteCertificateCommand, 1); }); test('warns when deleting certificate from ACM fails with AccessDeniedException', async () => { @@ -343,17 +352,15 @@ describe('AcmCertificateImporter', () => { key: { ARN: certArn }, }); - const describeCertificateFake = sinon.fake.resolves({ Certificate: { InUseBy: [] }}); - AWSMock.mock('ACM', 'describeCertificate', describeCertificateFake); + acmMock.on(DescribeCertificateCommand).resolves({ Certificate: { InUseBy: [] }}); - const error = new Error('AccessDeniedException'); - const deleteCertificateFake = sinon.fake.rejects(error); - AWSMock.mock('ACM', 'deleteCertificate', deleteCertificateFake); + const error = new AccessDeniedException({message: 'test access denied', $metadata: {}}); + acmMock.on(DeleteCertificateCommand).rejects(error); const importer = new TestAcmCertificateImporter({ - acm: new AWS.ACM(), - dynamoDb: new AWS.DynamoDB(), - secretsManager: new AWS.SecretsManager(), + acm: new ACMClient(), + dynamoDb: new DynamoDBClient(), + secretsManager: new SecretsManagerClient(), resourceTableOverride: resourceTable, }); @@ -363,8 +370,8 @@ describe('AcmCertificateImporter', () => { // THEN .rejects.toEqual(error); expect(queryStub.calledOnce).toBe(true); - expect(describeCertificateFake.calledOnce).toBe(true); - expect(deleteCertificateFake.calledOnce).toBe(true); + expect(acmMock).toHaveReceivedCommandTimes(DescribeCertificateCommand, 1); + expect(acmMock).toHaveReceivedCommandTimes(DeleteCertificateCommand, 1); expect(consoleWarnSpy.mock.calls.length).toBeGreaterThanOrEqual(1); expect(consoleWarnSpy.mock.calls.map(args => args[0]).join('\n')).toMatch(new RegExp(`Could not delete Certificate ${certArn}. Please ensure it has been deleted.`)); }); @@ -375,16 +382,14 @@ describe('AcmCertificateImporter', () => { const queryStub = sinon.stub(resourceTable, 'query').resolves({ key: { ARN: certArn } }); const deleteItemStub = sinon.stub(resourceTable, 'deleteItem').resolves(true); - const describeCertificateFake = sinon.fake.resolves({ Certificate: { InUseBy: [] }}); - AWSMock.mock('ACM', 'describeCertificate', describeCertificateFake); + acmMock.on(DescribeCertificateCommand).resolves({ Certificate: { InUseBy: [] }}); - const deleteCertificateFake = sinon.fake.resolves({}); - AWSMock.mock('ACM', 'deleteCertificate', deleteCertificateFake); + acmMock.on(DeleteCertificateCommand).resolves({}); const importer = new TestAcmCertificateImporter({ - acm: new AWS.ACM(), - dynamoDb: new AWS.DynamoDB(), - secretsManager: new AWS.SecretsManager(), + acm: new ACMClient(), + dynamoDb: new DynamoDBClient(), + secretsManager: new SecretsManagerClient(), resourceTableOverride: resourceTable, }); @@ -394,8 +399,8 @@ describe('AcmCertificateImporter', () => { // THEN .resolves.not.toThrow(); expect(queryStub.calledOnce).toBe(true); - expect(describeCertificateFake.calledOnce).toBe(true); - expect(deleteCertificateFake.calledOnce).toBe(true); + expect(acmMock).toHaveReceivedCommandTimes(DescribeCertificateCommand, 1); + expect(acmMock).toHaveReceivedCommandTimes(DeleteCertificateCommand, 1); expect(deleteItemStub.calledOnce).toBe(true); }); }); @@ -412,9 +417,9 @@ class TestAcmCertificateImporter extends AcmCertificateImporter { private readonly resourceTableOverride: CompositeStringIndexTable; constructor(props: { - acm: AWS.ACM, - dynamoDb: AWS.DynamoDB, - secretsManager: AWS.SecretsManager, + acm: ACMClient, + dynamoDb: DynamoDBClient, + secretsManager: SecretsManagerClient, resourceTableOverride?: CompositeStringIndexTable }) { super(props.acm, props.dynamoDb, props.secretsManager); @@ -439,7 +444,7 @@ class TestAcmCertificateImporter extends AcmCertificateImporter { */ class MockCompositeStringIndexTable extends CompositeStringIndexTable { constructor() { - super(new AWS.DynamoDB(), '', '', ''); + super(new DynamoDBClient(), '', '', ''); } public async deleteTable(): Promise {} diff --git a/packages/aws-rfdk/package.json b/packages/aws-rfdk/package.json index fd05aca35..2d3388f0b 100644 --- a/packages/aws-rfdk/package.json +++ b/packages/aws-rfdk/package.json @@ -66,12 +66,18 @@ "deadline" ], "devDependencies": { + "@aws-sdk/client-acm": "^3.563.0", + "@aws-sdk/client-auto-scaling": "^3.537.0", + "@aws-sdk/client-dynamodb": "^3.537.0", + "@aws-sdk/client-ecs": "^3.537.0", + "@aws-sdk/client-ec2": "^3.537.0", + "@aws-sdk/client-secrets-manager": "^3.535.0", "@types/aws-lambda": "^8.10.136", "@types/jest": "^29.5.12", "@types/sinon": "^17.0.3", "aws-cdk-lib": "2.133.0", - "aws-sdk": "^2.1583.0", - "aws-sdk-mock": "5.9.0", + "aws-sdk-client-mock": "^3.1.0", + "aws-sdk-client-mock-jest": "^3.1.0", "awslint": "2.68.0", "constructs": "^10.0.0", "dynalite": "^3.2.2",