diff --git a/packages/aws-cdk-lib/aws-rds/README.md b/packages/aws-cdk-lib/aws-rds/README.md index fd0a8a4ee66e1..2e5040e9920f3 100644 --- a/packages/aws-cdk-lib/aws-rds/README.md +++ b/packages/aws-cdk-lib/aws-rds/README.md @@ -1422,6 +1422,23 @@ new rds.DatabaseCluster(this, 'Cluster', { }); ``` +## Importing existing DatabaseInstance + +### Lookup DatabaseInstance by instanceIdentifier + +You can lookup an existing DatabaseInstance by its instanceIdentifier using `DatabaseInstance.fromLookup()`. This method returns an `IDatabaseInstance`. + +Here's how `DatabaseInstance.fromLookup()` can be used: + +```ts +const instance = rds.DatabaseInstance.fromLookup(stack, 'MyInstance', { + instanceIdentifier: 'instance-1', +}); + +// Add the new security group to the existing security groups of the RDS instance +instance.connections.addSecurityGroup(myNewSecurityGroup); +``` + ## Limitless Database Cluster Amazon Aurora [PostgreSQL Limitless Database](https://docs.aws.amazon.com/AmazonRDS/latest/AuroraUserGuide/limitless.html) provides automated horizontal scaling to process millions of write transactions per second and manages petabytes of data while maintaining the simplicity of operating inside a single database. diff --git a/packages/aws-cdk-lib/aws-rds/lib/instance.ts b/packages/aws-cdk-lib/aws-rds/lib/instance.ts index cfd41de9e680c..5d560bc4cb869 100644 --- a/packages/aws-cdk-lib/aws-rds/lib/instance.ts +++ b/packages/aws-cdk-lib/aws-rds/lib/instance.ts @@ -17,7 +17,8 @@ import * as kms from '../../aws-kms'; import * as logs from '../../aws-logs'; import * as s3 from '../../aws-s3'; import * as secretsmanager from '../../aws-secretsmanager'; -import { ArnComponents, ArnFormat, Duration, FeatureFlags, IResource, Lazy, RemovalPolicy, Resource, Stack, Token, Tokenization } from '../../core'; +import * as cxschema from '../../cloud-assembly-schema'; +import { ArnComponents, ArnFormat, ContextProvider, Duration, FeatureFlags, IResource, Lazy, RemovalPolicy, Resource, Stack, Token, Tokenization } from '../../core'; import * as cxapi from '../../cx-api'; /** @@ -132,6 +133,49 @@ export interface DatabaseInstanceAttributes { * A new or imported database instance. */ export abstract class DatabaseInstanceBase extends Resource implements IDatabaseInstance { + /** + * Lookup an existing DatabaseInstance using instanceIdentifier. + */ + public static fromLookup(scope: Construct, id: string, options: DatabaseInstanceLookupOptions): IDatabaseInstance { + const response: {[key: string]: any} = ContextProvider.getValue(scope, { + provider: cxschema.ContextProvider.CC_API_PROVIDER, + props: { + typeName: 'AWS::RDS::DBInstance', + exactIdentifier: options.instanceIdentifier, + propertiesToReturn: [ + 'DBInstanceArn', + 'Endpoint.Address', + 'Endpoint.Port', + 'DbiResourceId', + 'DBSecurityGroups', + ], + } as cxschema.CcApiContextQuery, + dummyValue: {}, + }).value; + + const instance = response[options.instanceIdentifier]; + + // Get ISecurityGroup from securityGroupId + const securityGroups: ec2.ISecurityGroup[] = []; + const dbsg: [string] = instance.DBSecurityGroups; + dbsg.forEach(securityGroupId => { + const securityGroup = ec2.SecurityGroup.fromSecurityGroupId( + scope, + `LSG-${securityGroupId}`, + securityGroupId, + ); + securityGroups.push(securityGroup); + }); + + return this.fromDatabaseInstanceAttributes(scope, id, { + instanceEndpointAddress: instance['Endpoint.Address'], + port: instance['Endpoint.Port'], + instanceIdentifier: options.instanceIdentifier, + securityGroups: securityGroups, + instanceResourceId: instance.DbiResourceId, + }); + } + /** * Import an existing database instance. */ @@ -1125,6 +1169,16 @@ abstract class DatabaseInstanceSource extends DatabaseInstanceNew implements IDa } } +/** + * Properties for looking up an existing DatabaseInstance. + */ +export interface DatabaseInstanceLookupOptions { + /** + * The instance identifier of the DatabaseInstance + */ + readonly instanceIdentifier: string; +} + /** * Construction properties for a DatabaseInstance. */ diff --git a/packages/aws-cdk-lib/aws-rds/test/instance.from-lookup.test.ts b/packages/aws-cdk-lib/aws-rds/test/instance.from-lookup.test.ts new file mode 100644 index 0000000000000..3d84da31e53f7 --- /dev/null +++ b/packages/aws-cdk-lib/aws-rds/test/instance.from-lookup.test.ts @@ -0,0 +1,89 @@ +import { Construct } from 'constructs'; +import * as cxschema from '../../cloud-assembly-schema'; +import { ContextProvider, GetContextValueOptions, GetContextValueResult, Lazy, Stack } from '../../core'; +import * as rds from '../lib'; + +describe('DatabaseInstanceBase from lookup', () => { + test('return correct instance info', () => { + const dataObj = {}; + Object.assign(dataObj, { ['DBInstanceArn']: 'arn:aws:rds:us-east-1:123456789012:db:instance-1' }); + Object.assign(dataObj, { ['Endpoint.Address']: 'instance-1.testserver.us-east-1.rds.amazonaws.com' }); + Object.assign(dataObj, { ['Endpoint.Port']: '5432' }); + Object.assign(dataObj, { ['DbiResourceId']: 'db-ABCDEFGHI' }); + Object.assign(dataObj, { ['DBSecurityGroups']: [] }); + const resultObj = {}; + Object.assign(resultObj, { ['instance-1']: dataObj }); + + const previous = mockDbInstanceContextProviderWith(resultObj, options => { + expect(options.exactIdentifier).toEqual('instance-1'); + }); + + const stack = new Stack(undefined, undefined, { env: { region: 'us-east-1', account: '123456789012' } }); + const instance = rds.DatabaseInstance.fromLookup(stack, 'MyInstance', { + instanceIdentifier: 'instance-1', + }); + + expect(instance.instanceIdentifier).toEqual('instance-1'); + expect(instance.dbInstanceEndpointAddress).toEqual('instance-1.testserver.us-east-1.rds.amazonaws.com'); + expect(instance.dbInstanceEndpointPort).toEqual('5432'); + expect(instance.instanceResourceId).toEqual('db-ABCDEFGHI'); + + restoreContextProvider(previous); + }); +}); + +describe('DatabaseInstanceBase from lookup with DBSG', () => { + test('return correct instance info', () => { + const dataObj = {}; + Object.assign(dataObj, { ['DBInstanceArn']: 'arn:aws:rds:us-east-1:123456789012:db:instance-1' }); + Object.assign(dataObj, { ['Endpoint.Address']: 'instance-1.testserver.us-east-1.rds.amazonaws.com' }); + Object.assign(dataObj, { ['Endpoint.Port']: '5432' }); + Object.assign(dataObj, { ['DbiResourceId']: 'db-ABCDEFGHI' }); + Object.assign(dataObj, { ['DBSecurityGroups']: ['dbsg-1', 'dbsg-2'] }); + const resultObj = {}; + Object.assign(resultObj, { ['instance-1']: dataObj }); + + const previous = mockDbInstanceContextProviderWith(resultObj, options => { + expect(options.exactIdentifier).toEqual('instance-1'); + }); + + const stack = new Stack(undefined, undefined, { env: { region: 'us-east-1', account: '123456789012' } }); + const instance = rds.DatabaseInstance.fromLookup(stack, 'MyInstance', { + instanceIdentifier: 'instance-1', + }); + + expect(instance.instanceIdentifier).toEqual('instance-1'); + expect(instance.dbInstanceEndpointAddress).toEqual('instance-1.testserver.us-east-1.rds.amazonaws.com'); + expect(instance.dbInstanceEndpointPort).toEqual('5432'); + expect(instance.instanceResourceId).toEqual('db-ABCDEFGHI'); + expect(instance.connections.securityGroups.length).toEqual(2); + + restoreContextProvider(previous); + }); +}); + +function mockDbInstanceContextProviderWith( + response: Object, + paramValidator?: (options: cxschema.CcApiContextQuery) => void) { + + const previous = ContextProvider.getValue; + ContextProvider.getValue = (_scope: Construct, options: GetContextValueOptions) => { + // do some basic sanity checks + expect(options.provider).toEqual(cxschema.ContextProvider.CC_API_PROVIDER); + + if (paramValidator) { + paramValidator(options.props as any); + } + + return { + value: { + ...response, + } as Map>, + }; + }; + return previous; +} + +function restoreContextProvider(previous: (scope: any, options: GetContextValueOptions) => GetContextValueResult): void { + ContextProvider.getValue = previous; +} diff --git a/packages/aws-cdk/lib/api/aws-auth/sdk.ts b/packages/aws-cdk/lib/api/aws-auth/sdk.ts index 27c98d4cdbb51..27fa792d07a71 100644 --- a/packages/aws-cdk/lib/api/aws-auth/sdk.ts +++ b/packages/aws-cdk/lib/api/aws-auth/sdk.ts @@ -19,6 +19,15 @@ import { type UpdateResolverCommandInput, type UpdateResolverCommandOutput, } from '@aws-sdk/client-appsync'; +import { + CloudControlClient, + GetResourceCommand, + GetResourceCommandInput, + GetResourceCommandOutput, + ListResourcesCommand, + ListResourcesCommandInput, + ListResourcesCommandOutput, +} from '@aws-sdk/client-cloudcontrol'; import { CloudFormationClient, ContinueUpdateRollbackCommand, @@ -371,6 +380,11 @@ export interface IAppSyncClient { listFunctions(input: ListFunctionsCommandInput): Promise; } +export interface ICloudControlClient{ + listResources(input: ListResourcesCommandInput): Promise; + getResource(input: GetResourceCommandInput): Promise; +} + export interface ICloudFormationClient { continueUpdateRollback(input: ContinueUpdateRollbackCommandInput): Promise; createChangeSet(input: CreateChangeSetCommandInput): Promise; @@ -600,6 +614,16 @@ export class SDK { }; } + public cloudControl(): ICloudControlClient { + const client = new CloudControlClient(this.config); + return { + listResources: (input: ListResourcesCommandInput): Promise => + client.send(new ListResourcesCommand(input)), + getResource: (input: GetResourceCommandInput): Promise => + client.send(new GetResourceCommand(input)), + }; + } + public cloudFormation(): ICloudFormationClient { const client = new CloudFormationClient({ ...this.config, diff --git a/packages/aws-cdk/lib/context-providers/cc-api-provider.ts b/packages/aws-cdk/lib/context-providers/cc-api-provider.ts new file mode 100644 index 0000000000000..c5b2def44261a --- /dev/null +++ b/packages/aws-cdk/lib/context-providers/cc-api-provider.ts @@ -0,0 +1,171 @@ +import type { CcApiContextQuery } from '@aws-cdk/cloud-assembly-schema'; +import { ICloudControlClient } from '../api'; +import { type SdkProvider, initContextProviderSdk } from '../api/aws-auth/sdk-provider'; +import { ContextProviderPlugin } from '../api/plugin'; +import { error } from '../logging'; + +/** + * This gets the values of the jsonObject at the paths specified in propertiesToReturn. + * + * For example, jsonObject = { + * key1: 'abc', + * key2: { + * foo: 'qwerty', + * bar: 'data', + * } + * } + * + * propertiesToReturn = ['key1', 'key2.foo']; + * + * The returned object is: + * { + * key1: 'abc', + * 'key2.foo': 'qwerty', + * } + * @param propsObject + * @param propertiesToReturn + * @returns + */ +export function toResultObj(jsonObject: any, propertiesToReturn: string[]): {[key: string]: any} { + const propsObj = {}; + propertiesToReturn.forEach((propName) => { + Object.assign(propsObj, { [propName]: findJsonValue(jsonObject, propName) }); + }); + return propsObj; +} + +/** + * This finds the value of the jsonObject at the path. Path is delimited by '.'. + * + * For example, jsonObject = { + * key1: 'abc', + * key2: { + * foo: 'qwerty', + * bar: 'data', + * } + * } + * + * If path is 'key1', then it will return 'abc'. + * If path is 'key2.foo', then it will return 'qwerty'. + * If path is 'key2', then it will return the object: + * { + * foo: 'qwerty', + * bar: 'data', + * } + * + * @param jsonObject + * @param path + */ +export function findJsonValue(jsonObject: any, path: string): any { + return path.split('.').reduce((r, k) => r[k], jsonObject); +} + +export class CcApiContextProviderPlugin implements ContextProviderPlugin { + constructor(private readonly aws: SdkProvider) {} + + /** + * This returns a data object with the value from CloudControl API result. + * args.typeName - see https://docs.aws.amazon.com/cloudcontrolapi/latest/userguide/supported-resources.html + * args.exactIdentifier - use CC API getResource. + * args.propertyMatch - use CCP API listResources to get resources and propertyMatch to search through the list. + * args.propertiesToReturn - Properties from CC API to return. + * + * @param args + * @returns + */ + public async getValue(args: CcApiContextQuery) { + const cloudControl = (await initContextProviderSdk(this.aws, args)).cloudControl(); + + const result = await this.findResources(cloudControl, args); + return result; + } + + private async findResources(cc: ICloudControlClient, args: CcApiContextQuery): Promise<{[key: string]: any}> { + if (args.exactIdentifier) { + // use getResource to get the exact indentifier + return this.getResource(cc, args); + } else { + // use listResource + return this.listResources(cc, args); + } + } + + /** + * Calls getResource from CC API to get the resource. + * See https://docs.aws.amazon.com/cli/latest/reference/cloudcontrol/get-resource.html + * + * If the exactIdentifier is not found, then an empty map is returned. + * If the resource is found, then a map of the identifier to a map of property values is returned. + * + * @param cc - CC API client + * @param args - This contains TypeName, exactIdentifier, and propertiesToReturn + * @returns + */ + private async getResource(cc: ICloudControlClient, args: CcApiContextQuery): Promise<{[key: string]: any}> { + const resultObj: {[key: string]: any} = {}; + try { + const result = await cc.getResource({ + TypeName: args.typeName, + Identifier: args.exactIdentifier, + }); + const id = result.ResourceDescription?.Identifier ?? ''; + if (id !== '') { + const propsObject = JSON.parse(result.ResourceDescription?.Properties ?? ''); + const propsObj = toResultObj(propsObject, args.propertiesToReturn); + resultObj[id] = propsObj; + } + } catch (err) { + error(`Could not get resource ${args.exactIdentifier}. Error: ${err}`); + } + return resultObj; + } + + /** + * Calls listResources from CC API to get the resources and apply args.propertyMatch to find the resources. + * See https://docs.aws.amazon.com/cli/latest/reference/cloudcontrol/list-resources.html + * + * Since exactIdentifier is not specified, propertyMatch must be specified. + * This returns an object where the ids are object keys and values are objects with keys of args.propertiesToReturn. + * + * @param cc + * @param args + * @returns + */ + private async listResources(cc: ICloudControlClient, args: CcApiContextQuery): Promise<{[key: string]: any}> { + const resultObj: {[key: string]: any} = {}; + + if (!args.propertyMatch) { + error('Neither exactIdentifier nor propertyMatch are specified.'); + return resultObj; + } + + try { + const result = await cc.listResources({ + TypeName: args.typeName, + }); + result.ResourceDescriptions?.forEach((resource) => { + const id = resource.Identifier ?? ''; + if (id !== '') { + const propsObject = JSON.parse(resource.Properties ?? ''); + const matchKey = Object.keys(args.propertyMatch!); + + let misMatch = false; + matchKey.forEach((key) => { + const value = findJsonValue(propsObject, key); + if (value !== args.propertyMatch![key]) { + misMatch = true; + } + }); + + if (!misMatch) { + const propsObj = toResultObj(propsObject, args.propertiesToReturn); + resultObj[id] = propsObj; + } + } + }); + } catch (err) { + error(`Could not get resources ${args.propertyMatch}. Error: ${err}`); + } + return resultObj; + } +} diff --git a/packages/aws-cdk/package.json b/packages/aws-cdk/package.json index e77fbb90d7ea2..b58e8a6dddcab 100644 --- a/packages/aws-cdk/package.json +++ b/packages/aws-cdk/package.json @@ -98,8 +98,8 @@ "nock": "^13.5.5", "sinon": "^9.2.4", "ts-jest": "^29.2.5", - "ts-node": "^10.9.2", "ts-mock-imports": "^1.3.16", + "ts-node": "^10.9.2", "xml-js": "^1.6.11" }, "dependencies": { @@ -107,32 +107,33 @@ "@aws-cdk/cloudformation-diff": "0.0.0", "@aws-cdk/cx-api": "0.0.0", "@aws-cdk/region-info": "0.0.0", - "@aws-sdk/client-appsync": "3.699.0", - "@aws-sdk/client-cloudformation": "3.699.0", - "@aws-sdk/client-cloudwatch-logs": "3.699.0", - "@aws-sdk/client-codebuild": "3.699.0", - "@aws-sdk/client-ec2": "3.699.0", - "@aws-sdk/client-ecr": "3.699.0", - "@aws-sdk/client-ecs": "3.699.0", - "@aws-sdk/client-elastic-load-balancing-v2": "3.699.0", - "@aws-sdk/client-iam": "3.699.0", - "@aws-sdk/client-kms": "3.699.0", - "@aws-sdk/client-lambda": "3.699.0", - "@aws-sdk/client-route-53": "3.699.0", - "@aws-sdk/client-s3": "3.699.0", - "@aws-sdk/client-secrets-manager": "3.699.0", - "@aws-sdk/client-sfn": "3.699.0", - "@aws-sdk/client-ssm": "3.699.0", - "@aws-sdk/client-sts": "3.699.0", - "@aws-sdk/credential-providers": "3.699.0", - "@aws-sdk/ec2-metadata-service": "3.699.0", - "@aws-sdk/lib-storage": "3.699.0", + "@aws-sdk/client-appsync": "3.723.0", + "@aws-sdk/client-cloudcontrol": "3.723.0", + "@aws-sdk/client-cloudformation": "3.723.0", + "@aws-sdk/client-cloudwatch-logs": "3.723.0", + "@aws-sdk/client-codebuild": "3.723.0", + "@aws-sdk/client-ec2": "3.723.0", + "@aws-sdk/client-ecr": "3.723.0", + "@aws-sdk/client-ecs": "3.723.0", + "@aws-sdk/client-elastic-load-balancing-v2": "3.723.0", + "@aws-sdk/client-iam": "3.723.0", + "@aws-sdk/client-kms": "3.723.0", + "@aws-sdk/client-lambda": "3.723.0", + "@aws-sdk/client-route-53": "3.723.0", + "@aws-sdk/client-s3": "3.723.0", + "@aws-sdk/client-secrets-manager": "3.723.0", + "@aws-sdk/client-sfn": "3.723.0", + "@aws-sdk/client-ssm": "3.723.0", + "@aws-sdk/client-sts": "3.723.0", + "@aws-sdk/credential-providers": "3.723.0", + "@aws-sdk/ec2-metadata-service": "3.723.0", + "@aws-sdk/lib-storage": "3.723.0", "@jsii/check-node": "1.104.0", - "@smithy/middleware-endpoint": "3.1.4", + "@smithy/middleware-endpoint": "4.0.0", "@smithy/node-http-handler": "3.2.4", "@smithy/property-provider": "3.1.10", "@smithy/shared-ini-file-loader": "3.1.8", - "@smithy/types": "3.5.0", + "@smithy/types": "4.0.0", "@smithy/util-retry": "3.0.7", "@smithy/util-stream": "3.1.9", "@smithy/util-waiter": "3.1.6", diff --git a/packages/aws-cdk/test/context-providers/cc-api-provider.test.ts b/packages/aws-cdk/test/context-providers/cc-api-provider.test.ts new file mode 100644 index 0000000000000..aab180cff4b3f --- /dev/null +++ b/packages/aws-cdk/test/context-providers/cc-api-provider.test.ts @@ -0,0 +1,189 @@ +import { GetResourceCommand, ListResourcesCommand } from '@aws-sdk/client-cloudcontrol'; +import { CcApiContextProviderPlugin, findJsonValue, toResultObj } from '../../lib/context-providers/cc-api-provider'; +import { mockCloudControlClient, MockSdkProvider, restoreSdkMocksToDefault } from '../util/mock-sdk'; + +let provider: CcApiContextProviderPlugin; + +beforeEach(() => { + provider = new CcApiContextProviderPlugin(new MockSdkProvider()); + restoreSdkMocksToDefault(); +}); + +test('looks up RDS instance using CC API getResource', async () => { + // GIVEN + mockCloudControlClient.on(GetResourceCommand).resolves({ + TypeName: 'AWS::RDS::DBInstance', + ResourceDescription: { + Identifier: 'my-db-instance-1', + Properties: '{"DBInstanceArn":"arn:aws:rds:us-east-1:123456789012:db:test-instance-1","StorageEncrypted":"true"}', + }, + }); + + // WHEN + const result = await provider.getValue({ + account: '123456789012', + region: 'us-east-1', + typeName: 'AWS::RDS::DBInstance', + exactIdentifier: 'my-db-instance-1', + propertiesToReturn: ['DBInstanceArn', 'StorageEncrypted'], + }); + + // THEN + const propsObj = result['my-db-instance-1']; + expect(propsObj.DBInstanceArn).toEqual('arn:aws:rds:us-east-1:123456789012:db:test-instance-1'); + expect(propsObj.StorageEncrypted).toEqual('true'); +}); + +test('looks up RDS instance using CC API getResource - not found', async () => { + // GIVEN + mockCloudControlClient.on(GetResourceCommand).resolves({ + }); + + // WHEN + const result = await provider.getValue({ + account: '123456789012', + region: 'us-east-1', + typeName: 'AWS::RDS::DBInstance', + exactIdentifier: 'bad-identifier', + propertiesToReturn: ['DBInstanceArn', 'StorageEncrypted'], + }); + + // THEN + expect(result).toEqual({}); +}); + +test('looks up RDS instance using CC API getResource - error in CC API', async () => { + // GIVEN + mockCloudControlClient.on(GetResourceCommand).rejects('No data found'); + + // WHEN + const result = await provider.getValue({ + account: '123456789012', + region: 'us-east-1', + typeName: 'AWS::RDS::DBInstance', + exactIdentifier: 'bad-identifier', + propertiesToReturn: ['DBInstanceArn', 'StorageEncrypted'], + }); + + // THEN + expect(result).toEqual({}); +}); + +test('looks up RDS instance using CC API listResources', async () => { + // GIVEN + mockCloudControlClient.on(ListResourcesCommand).resolves({ + ResourceDescriptions: [ + { + Identifier: 'my-db-instance-1', + Properties: '{"DBInstanceArn":"arn:aws:rds:us-east-1:123456789012:db:test-instance-1","StorageEncrypted":"true","Endpoint":{"Address":"address1.amazonaws.com","Port":"5432"}}', + }, + { + Identifier: 'my-db-instance-2', + Properties: '{"DBInstanceArn":"arn:aws:rds:us-east-1:123456789012:db:test-instance-2","StorageEncrypted":"false","Endpoint":{"Address":"address2.amazonaws.com","Port":"5432"}}', + }, + { + Identifier: 'my-db-instance-3', + Properties: '{"DBInstanceArn":"arn:aws:rds:us-east-1:123456789012:db:test-instance-3","StorageEncrypted":"true","Endpoint":{"Address":"address3.amazonaws.com","Port":"6000"}}', + }, + ], + }); + + // WHEN + const result = await provider.getValue({ + account: '123456789012', + region: 'us-east-1', + typeName: 'AWS::RDS::DBInstance', + propertyMatch: { + StorageEncrypted: 'true', + }, + propertiesToReturn: ['DBInstanceArn', 'StorageEncrypted', 'Endpoint.Port'], + }); + + // THEN + let propsObj = result['my-db-instance-1']; + expect(propsObj.DBInstanceArn).toEqual('arn:aws:rds:us-east-1:123456789012:db:test-instance-1'); + expect(propsObj.StorageEncrypted).toEqual('true'); + expect(propsObj['Endpoint.Port']).toEqual('5432'); + + propsObj = result['my-db-instance-3']; + expect(propsObj.DBInstanceArn).toEqual('arn:aws:rds:us-east-1:123456789012:db:test-instance-3'); + expect(propsObj.StorageEncrypted).toEqual('true'); + expect(propsObj['Endpoint.Port']).toEqual('6000'); + + propsObj = result['my-db-instance-2']; + expect(propsObj).toEqual(undefined); +}); + +test('looks up RDS instance using CC API listResources - nested prop', async () => { + // GIVEN + mockCloudControlClient.on(ListResourcesCommand).resolves({ + ResourceDescriptions: [ + { + Identifier: 'my-db-instance-1', + Properties: '{"DBInstanceArn":"arn:aws:rds:us-east-1:123456789012:db:test-instance-1","StorageEncrypted":"true","Endpoint":{"Address":"address1.amazonaws.com","Port":"5432"}}', + }, + { + Identifier: 'my-db-instance-2', + Properties: '{"DBInstanceArn":"arn:aws:rds:us-east-1:123456789012:db:test-instance-2","StorageEncrypted":"false","Endpoint":{"Address":"address2.amazonaws.com","Port":"5432"}}', + }, + { + Identifier: 'my-db-instance-3', + Properties: '{"DBInstanceArn":"arn:aws:rds:us-east-1:123456789012:db:test-instance-3","StorageEncrypted":"true","Endpoint":{"Address":"address3.amazonaws.com","Port":"6000"}}', + }, + ], + }); + + // WHEN + const result = await provider.getValue({ + account: '123456789012', + region: 'us-east-1', + typeName: 'AWS::RDS::DBInstance', + propertyMatch: { + 'StorageEncrypted': 'true', + 'Endpoint.Port': '5432', + }, + propertiesToReturn: ['DBInstanceArn', 'StorageEncrypted', 'Endpoint.Port'], + }); + + // THEN + let propsObj = result['my-db-instance-1']; + expect(propsObj.DBInstanceArn).toEqual('arn:aws:rds:us-east-1:123456789012:db:test-instance-1'); + expect(propsObj.StorageEncrypted).toEqual('true'); + expect(propsObj['Endpoint.Port']).toEqual('5432'); + + propsObj = result['my-db-instance-3']; + expect(propsObj).toEqual(undefined); + + propsObj = result['my-db-instance-2']; + expect(propsObj).toEqual(undefined); +}); + +test('findJsonValue for paths', async () => { + const jsonString = '{"DBInstanceArn":"arn:aws:rds:us-east-1:123456789012:db:test-instance-1","StorageEncrypted":"true","Endpoint":{"Address":"address1.amazonaws.com","Port":"5432"}}'; + const jsonObj = JSON.parse(jsonString); + + expect(findJsonValue(jsonObj, 'DBInstanceArn')).toEqual('arn:aws:rds:us-east-1:123456789012:db:test-instance-1'); + expect(findJsonValue(jsonObj, 'Endpoint.Address')).toEqual('address1.amazonaws.com'); + + const answer = { + Address: 'address1.amazonaws.com', + Port: '5432', + }; + expect(findJsonValue(jsonObj, 'Endpoint')).toEqual(answer); +}); + +test('toResultObj returns correct objects', async () => { + const jsonString = '{"DBInstanceArn":"arn:aws:rds:us-east-1:123456789012:db:test-instance-1","StorageEncrypted":"true","Endpoint":{"Address":"address1.amazonaws.com","Port":"5432"}}'; + const jsonObj = JSON.parse(jsonString); + const propertiesToReturn = ['DBInstanceArn', 'Endpoint.Port', 'Endpoint']; + + const result = toResultObj(jsonObj, propertiesToReturn); + expect(result.DBInstanceArn).toEqual('arn:aws:rds:us-east-1:123456789012:db:test-instance-1'); + expect(result['Endpoint.Port']).toEqual('5432'); + + const answer = { + Address: 'address1.amazonaws.com', + Port: '5432', + }; + expect(result.Endpoint).toEqual(answer); +}); diff --git a/packages/aws-cdk/test/util/mock-sdk.ts b/packages/aws-cdk/test/util/mock-sdk.ts index 414fc525e08d5..7894dbd7c0638 100644 --- a/packages/aws-cdk/test/util/mock-sdk.ts +++ b/packages/aws-cdk/test/util/mock-sdk.ts @@ -1,6 +1,7 @@ import 'aws-sdk-client-mock-jest'; import { Environment } from '@aws-cdk/cx-api'; import { AppSyncClient } from '@aws-sdk/client-appsync'; +import { CloudControlClient } from '@aws-sdk/client-cloudcontrol'; import { CloudFormationClient, Stack, StackStatus } from '@aws-sdk/client-cloudformation'; import { CloudWatchLogsClient } from '@aws-sdk/client-cloudwatch-logs'; import { CodeBuildClient } from '@aws-sdk/client-codebuild'; @@ -34,6 +35,7 @@ export const FAKE_CREDENTIAL_CHAIN = createCredentialChain(() => Promise.resolve // Default implementations export const mockAppSyncClient = mockClient(AppSyncClient); +export const mockCloudControlClient = mockClient(CloudControlClient); export const mockCloudFormationClient = mockClient(CloudFormationClient); export const mockCloudWatchClient = mockClient(CloudWatchLogsClient); export const mockCodeBuildClient = mockClient(CodeBuildClient); @@ -58,6 +60,8 @@ export const mockSTSClient = mockClient(STSClient); export const restoreSdkMocksToDefault = () => { mockAppSyncClient.reset(); mockAppSyncClient.onAnyCommand().resolves({}); + mockCloudControlClient.reset(); + mockCloudControlClient.onAnyCommand().resolves({}); mockCloudFormationClient.reset(); mockCloudFormationClient.onAnyCommand().resolves({}); mockCloudWatchClient.reset();