Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore: migrate @aws-cdk-testing/cli-integ from sdk v2 to sdk v3 #30046

Closed
wants to merge 26 commits into from
Closed
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
6686358
chore: migrate @aws-cdk-testing/cli-integ from sdk v2 to sdk v3
vinayak-kukreja Apr 29, 2024
2cc10e5
build and unit test pass
vinayak-kukreja Apr 30, 2024
32cbbc8
make updates
vinayak-kukreja May 2, 2024
18ba00d
updated test and package build works
vinayak-kukreja May 2, 2024
b1611bf
Merge branch 'main' into vkukreja/sdkv3-cli
vinayak-kukreja May 2, 2024
cff868c
make updates
vinayak-kukreja May 3, 2024
af5f46d
make updates
vinayak-kukreja May 3, 2024
411e581
revert .md files
vinayak-kukreja May 3, 2024
c43039b
revert yarn.lock
vinayak-kukreja May 3, 2024
14acc6b
fresh yarn install
vinayak-kukreja May 3, 2024
63bc772
global sdk version mismatch for ecs client
vinayak-kukreja May 3, 2024
af488e5
Merge branch 'main' into vkukreja/sdkv3-cli
vinayak-kukreja May 6, 2024
3b8cf77
Merge branch 'main' into vkukreja/sdkv3-cli
vinayak-kukreja May 9, 2024
44c929f
Merge branch 'main' into vkukreja/sdkv3-cli
vinayak-kukreja May 10, 2024
06b9534
testing
vinayak-kukreja May 10, 2024
95af578
testing
vinayak-kukreja May 10, 2024
b1b93ca
lets see if this works
vinayak-kukreja May 10, 2024
1800024
fix test: deploy and test stack with lambda asset
vinayak-kukreja May 13, 2024
f7c6fce
cleanup
vinayak-kukreja May 13, 2024
3f8745a
Merge branch 'main' into vkukreja/sdkv3-cli
vinayak-kukreja May 13, 2024
0f99d65
add ssm client
vinayak-kukreja May 13, 2024
1517157
Merge branch 'main' into vkukreja/sdkv3-cli
vinayak-kukreja May 13, 2024
2c609ef
Merge branch 'main' into vkukreja/sdkv3-cli
vinayak-kukreja May 14, 2024
7f7b528
Merge branch 'main' into vkukreja/sdkv3-cli
vinayak-kukreja May 14, 2024
e290cae
Merge branch 'main' into vkukreja/sdkv3-cli
TheRealAmazonKendra Jul 19, 2024
2c16959
Merge branch 'main' into vkukreja/sdkv3-cli
mergify[bot] Aug 26, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
196 changes: 79 additions & 117 deletions packages/@aws-cdk-testing/cli-integ/lib/aws.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,15 @@
import * as AWS from 'aws-sdk';

// eslint-disable-next-line @typescript-eslint/no-require-imports
require('aws-sdk/lib/maintenance_mode_message').suppress = true;
import { readFileSync } from 'fs';
import { CloudFormationClient, DeleteStackCommand, DescribeStacksCommand, Stack as CFNStack, UpdateTerminationProtectionCommand } from '@aws-sdk/client-cloudformation';
import { DeleteRepositoryCommand, ECRClient } from '@aws-sdk/client-ecr';
import { ECSClient } from '@aws-sdk/client-ecs';
import { IAMClient } from '@aws-sdk/client-iam';
import { LambdaClient } from '@aws-sdk/client-lambda';
import { S3Client, DeleteObjectsCommand, ListObjectVersionsCommand, ObjectIdentifier, DeleteBucketCommand } from '@aws-sdk/client-s3';
import { SNSClient } from '@aws-sdk/client-sns';
import { SSOClient } from '@aws-sdk/client-sso';
import { STSClient, GetCallerIdentityCommand } from '@aws-sdk/client-sts';
import { fromContainerMetadata, fromTemporaryCredentials } from '@aws-sdk/credential-providers';
import { parse } from 'ini';

export class AwsClients {
public static async default(output: NodeJS.WritableStream) {
Expand All @@ -13,40 +21,46 @@ export class AwsClients {
return new AwsClients(region, output);
}

// TODO Can this be type safe instead on any with sdk v3
private readonly config: any;

public readonly cloudFormation: AwsCaller<AWS.CloudFormation>;
public readonly s3: AwsCaller<AWS.S3>;
public readonly ecr: AwsCaller<AWS.ECR>;
public readonly ecs: AwsCaller<AWS.ECS>;
public readonly sso: AwsCaller<AWS.SSO>;
public readonly sns: AwsCaller<AWS.SNS>;
public readonly iam: AwsCaller<AWS.IAM>;
public readonly lambda: AwsCaller<AWS.Lambda>;
public readonly sts: AwsCaller<AWS.STS>;
public readonly cloudFormation: CloudFormationClient;
public readonly s3: S3Client;
public readonly ecr: ECRClient;
public readonly ecs: ECSClient;
public readonly sso: SSOClient;
public readonly sns: SNSClient;
public readonly iam: IAMClient;
public readonly lambda: LambdaClient;
public readonly sts: STSClient;

constructor(public readonly region: string, private readonly output: NodeJS.WritableStream) {
this.config = {
credentials: chainableCredentials(this.region),
region: this.region,
maxRetries: 8,
retryDelayOptions: { base: 500 },
stsRegionalEndpoints: 'regional',
maxAttempts: 9, // maxAttempts = 1 + maxRetries
};
this.cloudFormation = makeAwsCaller(AWS.CloudFormation, this.config);
this.s3 = makeAwsCaller(AWS.S3, this.config);
this.ecr = makeAwsCaller(AWS.ECR, this.config);
this.ecs = makeAwsCaller(AWS.ECS, this.config);
this.sso = makeAwsCaller(AWS.SSO, this.config);
this.sns = makeAwsCaller(AWS.SNS, this.config);
this.iam = makeAwsCaller(AWS.IAM, this.config);
this.lambda = makeAwsCaller(AWS.Lambda, this.config);
this.sts = makeAwsCaller(AWS.STS, this.config);

this.cloudFormation = new CloudFormationClient(this.config);
this.s3 = new S3Client(this.config);
this.ecr = new ECRClient(this.config);
this.ecs = new ECSClient(this.config);
this.sso = new SSOClient(this.config);
this.sns = new SNSClient(this.config);
this.iam = new IAMClient(this.config);
this.lambda = new LambdaClient(this.config);
this.sts = new STSClient(this.config);
}

public async account(): Promise<string> {
// Reduce # of retries, we use this as a circuit breaker for detecting no-config
return (await new AWS.STS({ ...this.config, maxRetries: 1 }).getCallerIdentity().promise()).Account!;
const client = new STSClient({
...this.config,
maxAttempts: 2,
});
const command = new GetCallerIdentityCommand({});

return (await client.send(command)).Account!;
}

public async deleteStacks(...stackNames: string[]) {
Expand All @@ -55,14 +69,15 @@ export class AwsClients {
// We purposely do all stacks serially, because they've been ordered
// to do the bootstrap stack last.
for (const stackName of stackNames) {
await this.cloudFormation('updateTerminationProtection', {
await this.cloudFormation.send(new UpdateTerminationProtectionCommand({
EnableTerminationProtection: false,
StackName: stackName,
});
await this.cloudFormation('deleteStack', {
}));
await this.cloudFormation.send(new DeleteStackCommand({
StackName: stackName,
});
}));

// TODO confirm at end that all is good
await retry(this.output, `Deleting ${stackName}`, retry.forSeconds(600), async () => {
const status = await this.stackStatus(stackName);
if (status !== undefined && status.endsWith('_FAILED')) {
Expand All @@ -77,15 +92,20 @@ export class AwsClients {

public async stackStatus(stackName: string): Promise<string | undefined> {
try {
return (await this.cloudFormation('describeStacks', { StackName: stackName })).Stacks?.[0].StackStatus;
} catch (e: any) {
return (await this.cloudFormation.send(new DescribeStacksCommand({
StackName: stackName,
}))).Stacks?.[0].StackStatus;
} catch (e: any) { // TODO confirm these errors make sense in sdk v3
if (isStackMissingError(e)) { return undefined; }
throw e;
}
}

public async emptyBucket(bucketName: string) {
const objects = await this.s3('listObjectVersions', { Bucket: bucketName });
const objects = await this.s3.send(new ListObjectVersionsCommand({
Bucket: bucketName,
}));;

const deletes = [...objects.Versions || [], ...objects.DeleteMarkers || []]
.reduce((acc, obj) => {
if (typeof obj.VersionId !== 'undefined' && typeof obj.Key !== 'undefined') {
Expand All @@ -94,107 +114,49 @@ export class AwsClients {
acc.push({ Key: obj.Key });
}
return acc;
}, [] as AWS.S3.ObjectIdentifierList);
}, [] as ObjectIdentifier[]);

if (deletes.length === 0) {
return Promise.resolve();
}
return this.s3('deleteObjects', {

return this.s3.send(new DeleteObjectsCommand({
Bucket: bucketName,
Delete: {
Objects: deletes,
Quiet: false,
},
});
}));
}

public async deleteImageRepository(repositoryName: string) {
await this.ecr('deleteRepository', { repositoryName, force: true });
await this.ecr.send(new DeleteRepositoryCommand({
repositoryName: repositoryName,
force: true,
}));
}

public async deleteBucket(bucketName: string) {
try {
await this.emptyBucket(bucketName);
await this.s3('deleteBucket', {

await this.s3.send(new DeleteBucketCommand({
Bucket: bucketName,
});
}));
} catch (e: any) {
// TODO make sure this error makes sense with sdk v3
if (isBucketMissingError(e)) { return; }
throw e;
}
}
}

/**
* Perform an AWS call from nothing
*
* Create the correct client, do the call and resole the promise().
*/
async function awsCall<
Svc extends AWS.Service,
Calls extends ServiceCalls<Svc>,
Call extends keyof Calls,
// eslint-disable-next-line @typescript-eslint/no-shadow
>(ctor: new (config: any) => Svc, config: any, call: Call, request: First<Calls[Call]>): Promise<Second<Calls[Call]>> {
const cfn = new ctor(config);
const response = ((cfn as any)[call] as any)(request);
try {
return response.promise();
} catch (e: any) {
const newErr = new Error(`${String(call)}(${JSON.stringify(request)}): ${e.message}`);
(newErr as any).code = e.code;
throw newErr;
}
}

type AwsCaller<A> = <B extends keyof ServiceCalls<A>>(call: B, request: First<ServiceCalls<A>[B]>) => Promise<Second<ServiceCalls<A>[B]>>;

/**
* Factory function to invoke 'awsCall' for specific services.
*
* Not strictly necessary but calling this replaces a whole bunch of annoying generics you otherwise have to type:
*
* ```ts
* export function cloudFormation<
* C extends keyof ServiceCalls<AWS.CloudFormation>,
* >(call: C, request: First<ServiceCalls<AWS.CloudFormation>[C]>): Promise<Second<ServiceCalls<AWS.CloudFormation>[C]>> {
* return awsCall(AWS.CloudFormation, call, request);
* }
* ```
*/
// eslint-disable-next-line @typescript-eslint/no-shadow
function makeAwsCaller<A extends AWS.Service>(ctor: new (config: any) => A, config: any): AwsCaller<A> {
return <B extends keyof ServiceCalls<A>>(call: B, request: First<ServiceCalls<A>[B]>): Promise<Second<ServiceCalls<A>[B]>> => {
return awsCall(ctor, config, call, request);
};
}

type ServiceCalls<T> = NoNayNever<SimplifiedService<T>>;
// Map ever member in the type to the important AWS call overload, or to 'never'
type SimplifiedService<T> = {[k in keyof T]: AwsCallIO<T[k]>};
// Remove all 'never' types from an object type
type NoNayNever<T> = Pick<T, {[k in keyof T]: T[k] extends never ? never : k }[keyof T]>;

// Because of the overloads an AWS handler type looks like this:
//
// {
// (params: INPUTSTRUCT, callback?: ((err: AWSError, data: {}) => void) | undefined): Request<OUTPUT, ...>;
// (callback?: ((err: AWS.AWSError, data: {}) => void) | undefined): AWS.Request<...>;
// }
//
// Get the first overload and extract the input and output struct types
type AwsCallIO<T> =
T extends {
(args: infer INPUT, callback?: ((err: AWS.AWSError, data: any) => void) | undefined): AWS.Request<infer OUTPUT, AWS.AWSError>;
(callback?: ((err: AWS.AWSError, data: {}) => void) | undefined): AWS.Request<any, any>;
} ? [INPUT, OUTPUT] : never;

type First<T> = T extends [any, any] ? T[0] : never;
type Second<T> = T extends [any, any] ? T[1] : never;

// TODO do these align with new errors in sdk v3?
export function isStackMissingError(e: Error) {
return e.message.indexOf('does not exist') > -1;
}

// TODO do these align with new errors in sdk v3?
export function isBucketMissingError(e: Error) {
return e.message.indexOf('does not exist') > -1;
}
Expand Down Expand Up @@ -241,29 +203,27 @@ retry.abort = (e: Error): Error => {
return e;
};

export function outputFromStack(key: string, stack: AWS.CloudFormation.Stack): string | undefined {
export function outputFromStack(key: string, stack: CFNStack): string | undefined {
return (stack.Outputs ?? []).find(o => o.OutputKey === key)?.OutputValue;
}

export async function sleep(ms: number) {
return new Promise(ok => setTimeout(ok, ms));
}

function chainableCredentials(region: string): AWS.Credentials | undefined {
function chainableCredentials(region: string) {

const profileName = process.env.AWS_PROFILE;
if (process.env.CODEBUILD_BUILD_ARN && profileName) {

if (process.env.CODEBUILD_BUILD_ARN && profileName) {
// in codebuild we must assume the role that the cdk uses
// otherwise credentials will just be picked up by the normal sdk
// heuristics and expire after an hour.

// can't use '~' since the SDK doesn't seem to expand it...?
const configPath = `${process.env.HOME}/.aws/config`;
const ini = new AWS.IniLoader().loadFrom({
filename: configPath,
isConfig: true,
});

const ini = parse(readFileSync(configPath, 'utf-8'));

const profile = ini[profileName];

Expand All @@ -282,18 +242,20 @@ function chainableCredentials(region: string): AWS.Credentials | undefined {
throw new Error(`external_id does not exist in profile ${externalId}`);
}

return new AWS.ChainableTemporaryCredentials({
return fromTemporaryCredentials({
params: {
RoleArn: arn,
ExternalId: externalId,
RoleSessionName: 'integ-tests',
},
stsConfig: {
region,
clientConfig: {
region: region,
},
masterCredentials: new AWS.ECSCredentials(),
masterCredentials: fromContainerMetadata(),
});
}

return undefined;
}

// TODO confirm return types of functions and add them if not explicitly mentioned
Loading
Loading