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

feat: use sdkv3 #47

Merged
merged 5 commits into from
Aug 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
48 changes: 42 additions & 6 deletions .projen/deps.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions .projen/tasks.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

13 changes: 11 additions & 2 deletions .projenrc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,21 +27,30 @@ const project = new typescript.TypeScriptProject({
'@aws-cdk/cloud-assembly-schema',
'@aws-cdk/cx-api',
'archiver',
'aws-sdk',
'@aws-sdk/client-ecr',
'@aws-sdk/client-s3',
'@aws-sdk/client-secrets-manager',
'@aws-sdk/client-sts',
'@aws-sdk/credential-providers',
'@aws-sdk/lib-storage',
'glob',
'mime',
'yargs',
],
description: 'CDK Asset Publishing Tool',
devDeps: [
'@smithy/config-resolver',
'@smithy/node-config-provider',
'@smithy/types',
'@types/archiver',
'@types/glob',
'@types/mime',
'@types/yargs',
'aws-sdk-client-mock',
'aws-sdk-client-mock-jest',
'fs-extra',
'graceful-fs',
'jszip',
'mock-fs',
],
packageName: 'cdk-assets',
eslintOptions: {
Expand Down
157 changes: 80 additions & 77 deletions lib/aws.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,13 @@
import * as os from 'os';
import { ECRClient } from '@aws-sdk/client-ecr';
import { CompleteMultipartUploadCommandOutput, PutObjectCommandInput, S3Client } from '@aws-sdk/client-s3';
import { SecretsManagerClient } from '@aws-sdk/client-secrets-manager';
import { GetCallerIdentityCommand, STSClient, STSClientConfig } from '@aws-sdk/client-sts';
import { fromNodeProviderChain, fromTemporaryCredentials } from '@aws-sdk/credential-providers';
import { Upload } from '@aws-sdk/lib-storage';
import { NODE_REGION_CONFIG_FILE_OPTIONS, NODE_REGION_CONFIG_OPTIONS } from '@smithy/config-resolver';
import { loadConfig } from '@smithy/node-config-provider';
import { AwsCredentialIdentityProvider } from '@smithy/types';

/**
* AWS SDK operations required by Asset Publishing
Expand All @@ -9,9 +18,10 @@ export interface IAws {
discoverCurrentAccount(): Promise<Account>;

discoverTargetAccount(options: ClientOptions): Promise<Account>;
s3Client(options: ClientOptions): Promise<AWS.S3>;
ecrClient(options: ClientOptions): Promise<AWS.ECR>;
secretsManagerClient(options: ClientOptions): Promise<AWS.SecretsManager>;
s3Client(options: ClientOptions): Promise<S3Client>;
ecrClient(options: ClientOptions): Promise<ECRClient>;
secretsManagerClient(options: ClientOptions): Promise<SecretsManagerClient>;
upload(params: PutObjectCommandInput, options?: ClientOptions): Promise<CompleteMultipartUploadCommandOutput>;
}

export interface ClientOptions {
Expand All @@ -21,6 +31,14 @@ export interface ClientOptions {
quiet?: boolean;
}

const USER_AGENT = 'cdk-assets';

interface Configuration {
clientConfig: STSClientConfig;
region?: string;
credentials: AwsCredentialIdentityProvider;
}

/**
* An AWS account
*
Expand All @@ -43,65 +61,75 @@ export interface Account {
* AWS client using the AWS SDK for JS with no special configuration
*/
export class DefaultAwsClient implements IAws {
private readonly AWS: typeof import('aws-sdk');
private account?: Account;
private config: Configuration;

constructor(profile?: string) {
// Force AWS SDK to look in ~/.aws/credentials and potentially use the configured profile.
process.env.AWS_SDK_LOAD_CONFIG = '1';
process.env.AWS_STS_REGIONAL_ENDPOINTS = 'regional';
process.env.AWS_NODEJS_CONNECTION_REUSE_ENABLED = '1';
if (profile) {
process.env.AWS_PROFILE = profile;
constructor(private readonly profile?: string) {
process.env.AWS_PROFILE = profile;
const clientConfig: STSClientConfig = {
customUserAgent: USER_AGENT,
}
// Stop SDKv2 from displaying a warning for now. We are aware and will migrate at some point,
// our customer don't need to be bothered with this.
process.env.AWS_SDK_JS_SUPPRESS_MAINTENANCE_MODE_MESSAGE = '1';
this.config = {
clientConfig,
credentials: fromNodeProviderChain({
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice, this is a big improvement over random env vars

profile: this.profile,
clientConfig,
}),
};
}

// We need to set the environment before we load this library for the first time.
// eslint-disable-next-line @typescript-eslint/no-require-imports
this.AWS = require('aws-sdk');
public async s3Client(options: ClientOptions): Promise<S3Client> {
return new S3Client(await this.awsOptions(options));
}

public async s3Client(options: ClientOptions) {
return new this.AWS.S3(await this.awsOptions(options));
public async upload(params: PutObjectCommandInput, options: ClientOptions = {}): Promise<CompleteMultipartUploadCommandOutput> {
try {
const upload = new Upload({
client: await this.s3Client(options),
params,
});

return upload.done();
} catch (e) {
// TODO: add something more useful here
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

todo?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll add something here before I remove the release candidate tag. I need to actually test the failure modes in the CLI to figure out what will be useful information here.

console.log(e);
throw e;
}
}

public async ecrClient(options: ClientOptions) {
return new this.AWS.ECR(await this.awsOptions(options));
public async ecrClient(options: ClientOptions): Promise<ECRClient> {
return new ECRClient(await this.awsOptions(options));
}

public async secretsManagerClient(options: ClientOptions) {
return new this.AWS.SecretsManager(await this.awsOptions(options));
public async secretsManagerClient(options: ClientOptions): Promise<SecretsManagerClient> {
return new SecretsManagerClient(await this.awsOptions(options));
}

public async discoverPartition(): Promise<string> {
return (await this.discoverCurrentAccount()).partition;
}

public async discoverDefaultRegion(): Promise<string> {
return this.AWS.config.region || 'us-east-1';
return loadConfig(NODE_REGION_CONFIG_OPTIONS, NODE_REGION_CONFIG_FILE_OPTIONS)() || 'us-east-1';
}

public async discoverCurrentAccount(): Promise<Account> {
if (this.account === undefined) {
const sts = new this.AWS.STS();
const response = await sts.getCallerIdentity().promise();
if (!response.Account || !response.Arn) {
throw new Error(`Unrecognized response from STS: '${JSON.stringify(response)}'`);
}
this.account = {
accountId: response.Account!,
partition: response.Arn!.split(':')[1],
};
this.account = await this.getAccount();
}

return this.account;
}

public async discoverTargetAccount(options: ClientOptions): Promise<Account> {
const sts = new this.AWS.STS(await this.awsOptions(options));
const response = await sts.getCallerIdentity().promise();
return this.getAccount(await this.awsOptions(options));
}

private async getAccount(options?: ClientOptions): Promise<Account> {
this.config.clientConfig = options ?? this.config.clientConfig;
const stsClient = new STSClient(await this.awsOptions(options));

const command = new GetCallerIdentityCommand();
const response = await stsClient.send(command);
if (!response.Account || !response.Arn) {
throw new Error(`Unrecognized response from STS: '${JSON.stringify(response)}'`);
}
Expand All @@ -111,48 +139,23 @@ export class DefaultAwsClient implements IAws {
};
}

private async awsOptions(options: ClientOptions) {
let credentials;

if (options.assumeRoleArn) {
credentials = await this.assumeRole(
options.region,
options.assumeRoleArn,
options.assumeRoleExternalId
);
private async awsOptions(options?: ClientOptions) {
const config = this.config;
config.region = options?.region;
if (options) {
config.region = options.region;
if (options.assumeRoleArn) {
config.credentials = fromTemporaryCredentials({
params: {
RoleArn: options.assumeRoleArn,
ExternalId: options.assumeRoleExternalId,
RoleSessionName: `${USER_AGENT}-${safeUsername()}`,
},
clientConfig: this.config.clientConfig,
});
}
}

return {
region: options.region,
customUserAgent: 'cdk-assets',
credentials,
};
}

/**
* Explicit manual AssumeRole call
*
* Necessary since I can't seem to get the built-in support for ChainableTemporaryCredentials to work.
*
* It needs an explicit configuration of `masterCredentials`, we need to put
* a `DefaultCredentialProverChain()` in there but that is not possible.
*/
private async assumeRole(
region: string | undefined,
roleArn: string,
externalId?: string
): Promise<AWS.Credentials> {
return new this.AWS.ChainableTemporaryCredentials({
params: {
RoleArn: roleArn,
ExternalId: externalId,
RoleSessionName: `cdk-assets-${safeUsername()}`,
},
stsConfig: {
region,
customUserAgent: 'cdk-assets',
},
});
return config;
}
}

Expand Down
15 changes: 10 additions & 5 deletions lib/private/docker-credentials.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import * as os from 'os';
import * as path from 'path';
import { Logger } from './shell';
import { IAws } from '../aws';
import { GetSecretValueCommand } from '@aws-sdk/client-secrets-manager';
import { ECRClient, GetAuthorizationTokenCommand } from '@aws-sdk/client-ecr';

export interface DockerCredentials {
readonly Username: string;
Expand Down Expand Up @@ -69,9 +71,11 @@ export async function fetchDockerLoginCredentials(

if (domainConfig.secretsManagerSecretId) {
const sm = await aws.secretsManagerClient({ assumeRoleArn: domainConfig.assumeRoleArn });
const secretValue = await sm
.getSecretValue({ SecretId: domainConfig.secretsManagerSecretId })
.promise();
const command = new GetSecretValueCommand({
SecretId: domainConfig.secretsManagerSecretId,
});

const secretValue = await sm.send(command);
if (!secretValue.SecretString) {
throw new Error(
`unable to fetch SecretString from secret: ${domainConfig.secretsManagerSecretId}`
Expand Down Expand Up @@ -99,11 +103,12 @@ export async function fetchDockerLoginCredentials(
}
}

export async function obtainEcrCredentials(ecr: AWS.ECR, logger?: Logger) {
export async function obtainEcrCredentials(ecr: ECRClient, logger?: Logger) {
if (logger) {
logger('Fetching ECR authorization token');
}
const authData = (await ecr.getAuthorizationToken({}).promise()).authorizationData || [];

const authData = (await ecr.send(new GetAuthorizationTokenCommand({}))).authorizationData || [];
if (authData.length === 0) {
throw new Error('No authorization data received from ECR');
}
Expand Down
Loading