-
Notifications
You must be signed in to change notification settings - Fork 2
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
feat: use sdkv3 #47
Changes from all commits
f57e9af
da43342
004e854
463f0c5
1aa3adf
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
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 | ||
|
@@ -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 { | ||
|
@@ -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 | ||
* | ||
|
@@ -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({ | ||
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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. todo? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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)}'`); | ||
} | ||
|
@@ -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; | ||
} | ||
} | ||
|
||
|
There was a problem hiding this comment.
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