Skip to content

Commit 7c3c94e

Browse files
authored
feat(toolkit-lib): make base credentials configurable (#388)
Previously, the Toolkit would always use AWS CLI-compatible base credentials. This is now configurable: ```ts const toolkit = new Toolkit({ sdkConfig: { baseCredentials: BaseCredentials.custom(...), }, }); ``` ## Design The `BaseCredentials` (abstract) class is responsible for producing the following 2 bits of information: - A credential provider - A default region These will be used to initialize an `SdkProvider`, which will then proceed to use that information: - To inform the CDK app about the desired target environment - After synthesis and during lookup: - use those credentials directly; or - use those credentials to assume roles; or - use available plugins to obtain credentials `requestHandler` (the proxy agent plus some SDK settings) used to be produced by the `SdkProvider` itself, but it is now produced by the `Toolkit` and part of the "services" that get passed in. That way, it is both available to the `AwsCliCompatible` base credentials to initialize the STS client, as well as to the `SdkProvider` class that now also gets instantiated by the `Toolkit`. ![image](https://github.com/user-attachments/assets/4ee1656b-d54f-4d1d-8ac7-77cd75c145b2) ## Supporting changes - Many of the supporting positional arguments to `SdkProvider` have been grouped into `SdkProviderServices`. - Change how we mock the SDK in a number of tests (instead of mocking the SDK Provider, mock the SDK) --- By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license
1 parent 59526e8 commit 7c3c94e

File tree

16 files changed

+336
-94
lines changed

16 files changed

+336
-94
lines changed

packages/@aws-cdk/tmp-toolkit-helpers/src/api/aws-auth/awscli-compatible.ts

+26-14
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,22 @@ const DEFAULT_TIMEOUT = 300000;
2424
*/
2525
export class AwsCliCompatible {
2626
private readonly ioHelper: IoHelper;
27+
private readonly requestHandler: NodeHttpHandlerOptions;
28+
private readonly logger?: Logger;
2729

28-
public constructor(ioHelper: IoHelper) {
30+
public constructor(ioHelper: IoHelper, requestHandler: NodeHttpHandlerOptions, logger?: Logger) {
2931
this.ioHelper = ioHelper;
32+
this.requestHandler = requestHandler;
33+
this.logger = logger;
34+
}
35+
36+
public async baseConfig(profile?: string): Promise<{ credentialProvider: AwsCredentialIdentityProvider; defaultRegion: string }> {
37+
const credentialProvider = await this.credentialChainBuilder({
38+
profile,
39+
logger: this.logger,
40+
});
41+
const defaultRegion = await this.region(profile);
42+
return { credentialProvider, defaultRegion };
3043
}
3144

3245
/**
@@ -38,7 +51,7 @@ export class AwsCliCompatible {
3851
options: CredentialChainOptions = {},
3952
): Promise<AwsCredentialIdentityProvider> {
4053
const clientConfig = {
41-
requestHandler: await this.requestHandlerBuilder(options.httpOptions),
54+
requestHandler: this.requestHandler,
4255
customUserAgent: 'aws-cdk',
4356
logger: options.logger,
4457
};
@@ -115,17 +128,6 @@ export class AwsCliCompatible {
115128
: nodeProviderChain;
116129
}
117130

118-
public async requestHandlerBuilder(options: SdkHttpOptions = {}): Promise<NodeHttpHandlerOptions> {
119-
const agent = await new ProxyAgentProvider(this.ioHelper).create(options);
120-
121-
return {
122-
connectionTimeout: DEFAULT_CONNECTION_TIMEOUT,
123-
requestTimeout: DEFAULT_TIMEOUT,
124-
httpsAgent: agent,
125-
httpAgent: agent,
126-
};
127-
}
128-
129131
/**
130132
* Attempts to get the region from a number of sources and falls back to us-east-1 if no region can be found,
131133
* as is done in the AWS CLI.
@@ -265,6 +267,16 @@ function shouldPrioritizeEnv() {
265267

266268
export interface CredentialChainOptions {
267269
readonly profile?: string;
268-
readonly httpOptions?: SdkHttpOptions;
269270
readonly logger?: Logger;
270271
}
272+
273+
export async function makeRequestHandler(ioHelper: IoHelper, options: SdkHttpOptions = {}): Promise<NodeHttpHandlerOptions> {
274+
const agent = await new ProxyAgentProvider(ioHelper).create(options);
275+
276+
return {
277+
connectionTimeout: DEFAULT_CONNECTION_TIMEOUT,
278+
requestTimeout: DEFAULT_TIMEOUT,
279+
httpsAgent: agent,
280+
httpAgent: agent,
281+
};
282+
}

packages/@aws-cdk/tmp-toolkit-helpers/src/api/aws-auth/sdk-provider.ts

+39-43
Original file line numberDiff line numberDiff line change
@@ -22,35 +22,13 @@ export type AssumeRoleAdditionalOptions = Partial<Omit<AssumeRoleCommandInput, '
2222
/**
2323
* Options for the default SDK provider
2424
*/
25-
export interface SdkProviderOptions {
26-
/**
27-
* IoHelper for messaging
28-
*/
29-
readonly ioHelper: IoHelper;
30-
25+
export interface SdkProviderOptions extends SdkProviderServices {
3126
/**
3227
* Profile to read from ~/.aws
3328
*
3429
* @default - No profile
3530
*/
3631
readonly profile?: string;
37-
38-
/**
39-
* HTTP options for SDK
40-
*/
41-
readonly httpOptions?: SdkHttpOptions;
42-
43-
/**
44-
* The logger for sdk calls.
45-
*/
46-
readonly logger?: Logger;
47-
48-
/**
49-
* The plugin host to use
50-
*
51-
* @default - an empty plugin host
52-
*/
53-
readonly pluginHost?: PluginHost;
5432
}
5533

5634
/**
@@ -133,33 +111,29 @@ export class SdkProvider {
133111
* class `AwsCliCompatible` for the details.
134112
*/
135113
public static async withAwsCliCompatibleDefaults(options: SdkProviderOptions) {
136-
const builder = new AwsCliCompatible(options.ioHelper);
137114
callTrace(SdkProvider.withAwsCliCompatibleDefaults.name, SdkProvider.constructor.name, options.logger);
138-
const credentialProvider = await builder.credentialChainBuilder({
139-
profile: options.profile,
140-
httpOptions: options.httpOptions,
141-
logger: options.logger,
142-
});
143-
144-
const region = await builder.region(options.profile);
145-
const requestHandler = await builder.requestHandlerBuilder(options.httpOptions);
146-
return new SdkProvider(credentialProvider, region, requestHandler, options.pluginHost ?? new PluginHost(), options.ioHelper, options.logger);
115+
const config = await new AwsCliCompatible(options.ioHelper, options.requestHandler ?? {}, options.logger).baseConfig(options.profile);
116+
return new SdkProvider(config.credentialProvider, config.defaultRegion, options);
147117
}
148118

119+
public readonly defaultRegion: string;
120+
private readonly defaultCredentialProvider: AwsCredentialIdentityProvider;
149121
private readonly plugins;
122+
private readonly requestHandler: NodeHttpHandlerOptions;
123+
private readonly ioHelper: IoHelper;
124+
private readonly logger?: Logger;
150125

151126
public constructor(
152-
private readonly defaultCredentialProvider: AwsCredentialIdentityProvider,
153-
/**
154-
* Default region
155-
*/
156-
public readonly defaultRegion: string,
157-
private readonly requestHandler: NodeHttpHandlerOptions = {},
158-
pluginHost: PluginHost,
159-
private readonly ioHelper: IoHelper,
160-
private readonly logger?: Logger,
127+
defaultCredentialProvider: AwsCredentialIdentityProvider,
128+
defaultRegion: string | undefined,
129+
services: SdkProviderServices,
161130
) {
162-
this.plugins = new CredentialPlugins(pluginHost, ioHelper);
131+
this.defaultCredentialProvider = defaultCredentialProvider;
132+
this.defaultRegion = defaultRegion ?? 'us-east-1';
133+
this.requestHandler = services.requestHandler ?? {};
134+
this.ioHelper = services.ioHelper;
135+
this.logger = services.logger;
136+
this.plugins = new CredentialPlugins(services.pluginHost ?? new PluginHost(), this.ioHelper);
163137
}
164138

165139
/**
@@ -572,3 +546,25 @@ export async function initContextProviderSdk(aws: SdkProvider, options: ContextL
572546

573547
return (await aws.forEnvironment(EnvironmentUtils.make(account, region), Mode.ForReading, creds)).sdk;
574548
}
549+
550+
export interface SdkProviderServices {
551+
/**
552+
* An IO helper for emitting messages
553+
*/
554+
readonly ioHelper: IoHelper;
555+
556+
/**
557+
* The request handler settings
558+
*/
559+
readonly requestHandler?: NodeHttpHandlerOptions;
560+
561+
/**
562+
* A plugin host
563+
*/
564+
readonly pluginHost?: PluginHost;
565+
566+
/**
567+
* An SDK logger
568+
*/
569+
readonly logger?: Logger;
570+
}

packages/@aws-cdk/toolkit-lib/lib/api/aws-auth/types.ts

+132-1
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,23 @@
1+
import type { AwsCredentialIdentityProvider } from '@smithy/types';
2+
import type { SdkProviderServices } from '../shared-private';
3+
import { AwsCliCompatible } from '../shared-private';
14

25
/**
36
* Options for the default SDK provider
47
*/
58
export interface SdkConfig {
69
/**
7-
* Profile to read from ~/.aws
10+
* The base credentials and region used to seed the Toolkit with
11+
*
12+
* @default BaseCredentials.awsCliCompatible()
13+
*/
14+
readonly baseCredentials?: BaseCredentials;
15+
16+
/**
17+
* Profile to read from ~/.aws for base credentials
818
*
919
* @default - No profile
20+
* @deprecated Use `baseCredentials` instead
1021
*/
1122
readonly profile?: string;
1223

@@ -34,3 +45,123 @@ export interface SdkHttpOptions {
3445
*/
3546
readonly caBundlePath?: string;
3647
}
48+
49+
export abstract class BaseCredentials {
50+
/**
51+
* Use no base credentials
52+
*
53+
* There will be no current account and no current region during synthesis. To
54+
* successfully deploy with this set of base credentials:
55+
*
56+
* - The CDK app must provide concrete accounts and regions during synthesis
57+
* - Credential plugins must be installed to provide credentials for those
58+
* accounts.
59+
*/
60+
public static none(): BaseCredentials {
61+
return new class extends BaseCredentials {
62+
public async makeSdkConfig(): Promise<SdkBaseConfig> {
63+
return {
64+
credentialProvider: () => {
65+
// eslint-disable-next-line @cdklabs/no-throw-default-error
66+
throw new Error('No credentials available due to BaseCredentials.none()');
67+
},
68+
};
69+
}
70+
71+
public toString() {
72+
return 'BaseCredentials.none()';
73+
}
74+
};
75+
}
76+
77+
/**
78+
* Obtain base credentials and base region the same way the AWS CLI would
79+
*
80+
* Credentials and region will be read from the environment first, falling back
81+
* to INI files or other sources if available.
82+
*
83+
* The profile name is configurable.
84+
*/
85+
public static awsCliCompatible(options: AwsCliCompatibleOptions = {}): BaseCredentials {
86+
return new class extends BaseCredentials {
87+
public makeSdkConfig(services: SdkProviderServices): Promise<SdkBaseConfig> {
88+
const awsCli = new AwsCliCompatible(services.ioHelper, services.requestHandler ?? {}, services.logger);
89+
return awsCli.baseConfig(options.profile);
90+
}
91+
92+
public toString() {
93+
return `BaseCredentials.awsCliCompatible(${JSON.stringify(options)})`;
94+
}
95+
};
96+
}
97+
98+
/**
99+
* Use a custom SDK identity provider for the base credentials
100+
*
101+
* If your provider uses STS calls to obtain base credentials, you must make
102+
* sure to also configure the necessary HTTP options (like proxy and user
103+
* agent) and the region on the STS client directly; the toolkit code cannot
104+
* do this for you.
105+
*/
106+
public static custom(options: CustomBaseCredentialsOption): BaseCredentials {
107+
return new class extends BaseCredentials {
108+
public makeSdkConfig(): Promise<SdkBaseConfig> {
109+
return Promise.resolve({
110+
credentialProvider: options.provider,
111+
defaultRegion: options.region,
112+
});
113+
}
114+
115+
public toString() {
116+
return `BaseCredentials.custom(${JSON.stringify({
117+
...options,
118+
provider: '...',
119+
})})`;
120+
}
121+
};
122+
}
123+
124+
/**
125+
* Make SDK config from the BaseCredentials settings
126+
*/
127+
public abstract makeSdkConfig(services: SdkProviderServices): Promise<SdkBaseConfig>;
128+
}
129+
130+
export interface AwsCliCompatibleOptions {
131+
/**
132+
* The profile to read from `~/.aws/credentials`.
133+
*
134+
* If not supplied the environment variable AWS_PROFILE will be used.
135+
*
136+
* @default - Use environment variable if set.
137+
*/
138+
readonly profile?: string;
139+
}
140+
141+
export interface CustomBaseCredentialsOption {
142+
/**
143+
* The credentials provider to use to obtain base credentials
144+
*
145+
* If your provider uses STS calls to obtain base credentials, you must make
146+
* sure to also configure the necessary HTTP options (like proxy and user
147+
* agent) on the STS client directly; the toolkit code cannot do this for you.
148+
*/
149+
readonly provider: AwsCredentialIdentityProvider;
150+
151+
/**
152+
* The default region to synthesize for
153+
*
154+
* CDK applications can override this region. NOTE: this region will *not*
155+
* affect any STS calls made by the given provider, if any. You need to configure
156+
* your credential provider separately.
157+
*
158+
* @default 'us-east-1'
159+
*/
160+
readonly region?: string;
161+
}
162+
163+
export interface SdkBaseConfig {
164+
readonly credentialProvider: AwsCredentialIdentityProvider;
165+
166+
readonly defaultRegion?: string;
167+
}

packages/@aws-cdk/toolkit-lib/lib/api/shared-private.ts

+1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ export * from '../../../tmp-toolkit-helpers/src/api/io/private';
44
export * from '../../../tmp-toolkit-helpers/src/private';
55
export * from '../../../tmp-toolkit-helpers/src/api';
66
export * as cfnApi from '../../../tmp-toolkit-helpers/src/api/deployments/cfn-api';
7+
export { makeRequestHandler } from '../../../tmp-toolkit-helpers/src/api/aws-auth/awscli-compatible';
78

89
// Context Providers
910
export * as contextproviders from '../../../tmp-toolkit-helpers/src/context-providers';

0 commit comments

Comments
 (0)