Skip to content

Commit b04ad30

Browse files
authored
feat(toolkit-lib)!: improved types for BaseCredentials (#542)
This PR refactors the `BaseCredentials` implementation to provide better type safety and a cleaner API surface. By converting from an abstract class to a static factory pattern with proper interfaces, we improve the developer experience and make the code more maintainable. The changes also reduce external dependencies by removing the direct dependency on `@smithy/node-http-handler` and creating our own simplified interface for request handler settings. Reason for this change is that in the previous version `SdkProviderServices` was used but not publicly exported since this interface wasn't meant for public consumption. That however made it difficult to implement custom `BaseCredentials` solutions. Instead of `SdkProviderServices` we have no reduced the public API surface to the exactly needed properties. BREAKING CHANGE: Custom implementations of `BaseCredentials` have changed. If you previously extended the abstract `BaseCredentials` class and implemented the abstract method, you can now pass any object implementing the new `IBaseCredentialsProvider` interface. The method name change to `sdkBaseConfig` and it now receives two parameters: an `ioHost` that can be used to emit messages and a `clientConfig` to should be honored for all SDK requests. However the `clientConfig` may be extended as needed. --- By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license
1 parent 65937a2 commit b04ad30

File tree

19 files changed

+266
-223
lines changed

19 files changed

+266
-223
lines changed

.projenrc.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -738,7 +738,6 @@ const toolkitLib = configureProject(
738738
`@aws-sdk/ec2-metadata-service@${CLI_SDK_V3_RANGE}`,
739739
`@aws-sdk/lib-storage@${CLI_SDK_V3_RANGE}`,
740740
'@smithy/middleware-endpoint',
741-
'@smithy/node-http-handler',
742741
'@smithy/property-provider',
743742
'@smithy/shared-ini-file-loader',
744743
'@smithy/util-retry',
@@ -872,7 +871,7 @@ new pj.JsonFile(toolkitLib, 'api-extractor.json', {
872871
logLevel: 'none',
873872
},
874873
'ae-forgotten-export': {
875-
logLevel: 'warning', // @todo fix issues and change to error
874+
logLevel: 'error',
876875
},
877876
},
878877
tsdocMessageReporting: {

packages/@aws-cdk/toolkit-lib/.projen/deps.json

Lines changed: 0 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/@aws-cdk/toolkit-lib/.projen/tasks.json

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/@aws-cdk/toolkit-lib/api-extractor.json

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

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

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@ import { format } from 'node:util';
33
import type { SDKv3CompatibleCredentialProvider } from '@aws-cdk/cli-plugin-contract';
44
import { createCredentialChain, fromEnv, fromIni, fromNodeProviderChain } from '@aws-sdk/credential-providers';
55
import { MetadataService } from '@aws-sdk/ec2-metadata-service';
6-
import type { NodeHttpHandlerOptions } from '@smithy/node-http-handler';
76
import { loadSharedConfigFiles } from '@smithy/shared-ini-file-loader';
7+
import type { RequestHandlerSettings } from './base-credentials';
88
import { makeCachingProvider } from './provider-caching';
99
import type { ISdkLogger } from './sdk-logger';
1010
import { AuthenticationError } from '../../toolkit/toolkit-error';
@@ -23,10 +23,10 @@ const DEFAULT_TIMEOUT = 300000;
2323
*/
2424
export class AwsCliCompatible {
2525
private readonly ioHelper: IoHelper;
26-
private readonly requestHandler: NodeHttpHandlerOptions;
26+
private readonly requestHandler: RequestHandlerSettings;
2727
private readonly logger?: ISdkLogger;
2828

29-
public constructor(ioHelper: IoHelper, requestHandler: NodeHttpHandlerOptions, logger?: ISdkLogger) {
29+
public constructor(ioHelper: IoHelper, requestHandler: RequestHandlerSettings, logger?: ISdkLogger) {
3030
this.ioHelper = ioHelper;
3131
this.requestHandler = requestHandler;
3232
this.logger = logger;
@@ -269,7 +269,7 @@ export interface CredentialChainOptions {
269269
readonly logger?: ISdkLogger;
270270
}
271271

272-
export function sdkRequestHandler(agent?: Agent): NodeHttpHandlerOptions {
272+
export function sdkRequestHandler(agent?: Agent): RequestHandlerSettings {
273273
return {
274274
connectionTimeout: DEFAULT_CONNECTION_TIMEOUT,
275275
requestTimeout: DEFAULT_TIMEOUT,
Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
import type * as http from 'node:http';
2+
import type * as https from 'node:https';
3+
import type { SDKv3CompatibleCredentialProvider } from '@aws-cdk/cli-plugin-contract';
4+
import { AwsCliCompatible } from './awscli-compatible';
5+
import { AuthenticationError } from '../../toolkit/toolkit-error';
6+
import type { IActionAwareIoHost } from '../io';
7+
import { IoHostSdkLogger } from './sdk-logger';
8+
import { IoHelper } from '../io/private';
9+
10+
/**
11+
* Settings for the request handle
12+
*/
13+
export interface RequestHandlerSettings {
14+
/**
15+
* The maximum time in milliseconds that the connection phase of a request
16+
* may take before the connection attempt is abandoned.
17+
*
18+
* Defaults to 0, which disables the timeout.
19+
*/
20+
connectionTimeout?: number;
21+
/**
22+
* The number of milliseconds a request can take before automatically being terminated.
23+
* Defaults to 0, which disables the timeout.
24+
*/
25+
requestTimeout?: number;
26+
/**
27+
* An http.Agent to be used
28+
*/
29+
httpAgent?: http.Agent;
30+
/**
31+
* An https.Agent to be used
32+
*/
33+
httpsAgent?: https.Agent;
34+
}
35+
36+
/**
37+
* An SDK config that
38+
*/
39+
export interface SdkBaseConfig {
40+
/**
41+
* The credential provider to use for SDK calls.
42+
*/
43+
readonly credentialProvider: SDKv3CompatibleCredentialProvider;
44+
/**
45+
* The default region to use for SDK calls.
46+
*/
47+
readonly defaultRegion?: string;
48+
}
49+
50+
export interface SdkBaseClientConfig {
51+
requestHandler?: RequestHandlerSettings;
52+
}
53+
54+
export interface IBaseCredentialsProvider {
55+
sdkBaseConfig(ioHost: IActionAwareIoHost, clientConfig: SdkBaseClientConfig): Promise<SdkBaseConfig>;
56+
}
57+
58+
export class BaseCredentials {
59+
/**
60+
* Use no base credentials
61+
*
62+
* There will be no current account and no current region during synthesis. To
63+
* successfully deploy with this set of base credentials:
64+
*
65+
* - The CDK app must provide concrete accounts and regions during synthesis
66+
* - Credential plugins must be installed to provide credentials for those
67+
* accounts.
68+
*/
69+
public static none(): IBaseCredentialsProvider {
70+
return new class implements IBaseCredentialsProvider {
71+
public async sdkBaseConfig() {
72+
return {
73+
credentialProvider: () => {
74+
throw new AuthenticationError('No credentials available due to BaseCredentials.none()');
75+
},
76+
};
77+
}
78+
79+
public toString() {
80+
return 'BaseCredentials.none()';
81+
}
82+
};
83+
}
84+
85+
/**
86+
* Obtain base credentials and base region the same way the AWS CLI would
87+
*
88+
* Credentials and region will be read from the environment first, falling back
89+
* to INI files or other sources if available.
90+
*
91+
* The profile name is configurable.
92+
*/
93+
public static awsCliCompatible(options: AwsCliCompatibleOptions = {}): IBaseCredentialsProvider {
94+
return new class implements IBaseCredentialsProvider {
95+
public sdkBaseConfig(ioHost: IActionAwareIoHost, clientConfig: SdkBaseClientConfig) {
96+
const ioHelper = IoHelper.fromActionAwareIoHost(ioHost);
97+
const awsCli = new AwsCliCompatible(ioHelper, clientConfig.requestHandler ?? {}, new IoHostSdkLogger(ioHelper));
98+
return awsCli.baseConfig(options.profile);
99+
}
100+
101+
public toString() {
102+
return `BaseCredentials.awsCliCompatible(${JSON.stringify(options)})`;
103+
}
104+
};
105+
}
106+
107+
/**
108+
* Use a custom SDK identity provider for the base credentials
109+
*
110+
* If your provider uses STS calls to obtain base credentials, you must make
111+
* sure to also configure the necessary HTTP options (like proxy and user
112+
* agent) and the region on the STS client directly; the toolkit code cannot
113+
* do this for you.
114+
*/
115+
public static custom(options: CustomBaseCredentialsOption): IBaseCredentialsProvider {
116+
return new class implements IBaseCredentialsProvider {
117+
public sdkBaseConfig(): Promise<SdkBaseConfig> {
118+
return Promise.resolve({
119+
credentialProvider: options.provider,
120+
defaultRegion: options.region,
121+
});
122+
}
123+
124+
public toString() {
125+
return `BaseCredentials.custom(${JSON.stringify({
126+
...options,
127+
provider: '...',
128+
})})`;
129+
}
130+
};
131+
}
132+
}
133+
134+
export interface AwsCliCompatibleOptions {
135+
/**
136+
* The profile to read from `~/.aws/credentials`.
137+
*
138+
* If not supplied the environment variable AWS_PROFILE will be used.
139+
*
140+
* @default - Use environment variable if set.
141+
*/
142+
readonly profile?: string;
143+
}
144+
145+
export interface CustomBaseCredentialsOption {
146+
/**
147+
* The credentials provider to use to obtain base credentials
148+
*
149+
* If your provider uses STS calls to obtain base credentials, you must make
150+
* sure to also configure the necessary HTTP options (like proxy and user
151+
* agent) on the STS client directly; the toolkit code cannot do this for you.
152+
*/
153+
readonly provider: SDKv3CompatibleCredentialProvider;
154+
155+
/**
156+
* The default region to synthesize for
157+
*
158+
* CDK applications can override this region. NOTE: this region will *not*
159+
* affect any STS calls made by the given provider, if any. You need to configure
160+
* your credential provider separately.
161+
*
162+
* @default 'us-east-1'
163+
*/
164+
readonly region?: string;
165+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
export * from './types';
2+
export * from './base-credentials';

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

Lines changed: 31 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@ import type { Environment } from '@aws-cdk/cx-api';
55
import { EnvironmentUtils, UNKNOWN_ACCOUNT, UNKNOWN_REGION } from '@aws-cdk/cx-api';
66
import type { AssumeRoleCommandInput } from '@aws-sdk/client-sts';
77
import { fromTemporaryCredentials } from '@aws-sdk/credential-providers';
8-
import type { NodeHttpHandlerOptions } from '@smithy/node-http-handler';
98
import { AwsCliCompatible } from './awscli-compatible';
9+
import type { RequestHandlerSettings } from './base-credentials';
1010
import { cached } from './cached';
1111
import { CredentialPlugins } from './credential-plugins';
1212
import { makeCachingProvider } from './provider-caching';
@@ -23,13 +23,26 @@ export type AssumeRoleAdditionalOptions = Partial<Omit<AssumeRoleCommandInput, '
2323
/**
2424
* Options for the default SDK provider
2525
*/
26-
export interface SdkProviderOptions extends SdkProviderServices {
26+
export interface SdkProviderOptions {
2727
/**
28-
* Profile to read from ~/.aws
29-
*
30-
* @default - No profile
28+
* An IO helper for emitting messages
29+
*/
30+
readonly ioHelper: IoHelper;
31+
32+
/**
33+
* The request handler settings
3134
*/
32-
readonly profile?: string;
35+
readonly requestHandler?: RequestHandlerSettings;
36+
37+
/**
38+
* A plugin host
39+
*/
40+
readonly pluginHost?: PluginHost;
41+
42+
/**
43+
* An SDK logger
44+
*/
45+
readonly logger?: ISdkLogger;
3346
}
3447

3548
const CACHED_ACCOUNT = Symbol('cached_account');
@@ -91,31 +104,35 @@ export class SdkProvider {
91104
*
92105
* The AWS SDK for JS behaves slightly differently from the AWS CLI in a number of ways; see the
93106
* class `AwsCliCompatible` for the details.
107+
*
108+
* @param options - Options for the default SDK provider
109+
* @param profile - Profile to read from ~/.aws
110+
* @returns a configured SdkProvider
94111
*/
95-
public static async withAwsCliCompatibleDefaults(options: SdkProviderOptions) {
112+
public static async withAwsCliCompatibleDefaults(options: SdkProviderOptions, profile?: string) {
96113
callTrace(SdkProvider.withAwsCliCompatibleDefaults.name, SdkProvider.constructor.name, options.logger);
97-
const config = await new AwsCliCompatible(options.ioHelper, options.requestHandler ?? {}, options.logger).baseConfig(options.profile);
114+
const config = await new AwsCliCompatible(options.ioHelper, options.requestHandler ?? {}, options.logger).baseConfig(profile);
98115
return new SdkProvider(config.credentialProvider, config.defaultRegion, options);
99116
}
100117

101118
public readonly defaultRegion: string;
102119
private readonly defaultCredentialProvider: SDKv3CompatibleCredentialProvider;
103120
private readonly plugins;
104-
private readonly requestHandler: NodeHttpHandlerOptions;
121+
private readonly requestHandler: RequestHandlerSettings;
105122
private readonly ioHelper: IoHelper;
106123
private readonly logger?: ISdkLogger;
107124

108125
public constructor(
109126
defaultCredentialProvider: SDKv3CompatibleCredentialProvider,
110127
defaultRegion: string | undefined,
111-
services: SdkProviderServices,
128+
options: SdkProviderOptions,
112129
) {
113130
this.defaultCredentialProvider = defaultCredentialProvider;
114131
this.defaultRegion = defaultRegion ?? 'us-east-1';
115-
this.requestHandler = services.requestHandler ?? {};
116-
this.ioHelper = services.ioHelper;
117-
this.logger = services.logger;
118-
this.plugins = new CredentialPlugins(services.pluginHost ?? new PluginHost(), this.ioHelper);
132+
this.requestHandler = options.requestHandler ?? {};
133+
this.ioHelper = options.ioHelper;
134+
this.logger = options.logger;
135+
this.plugins = new CredentialPlugins(options.pluginHost ?? new PluginHost(), this.ioHelper);
119136
}
120137

121138
/**
@@ -528,25 +545,3 @@ export async function initContextProviderSdk(aws: SdkProvider, options: ContextL
528545

529546
return (await aws.forEnvironment(EnvironmentUtils.make(account, region), Mode.ForReading, creds)).sdk;
530547
}
531-
532-
export interface SdkProviderServices {
533-
/**
534-
* An IO helper for emitting messages
535-
*/
536-
readonly ioHelper: IoHelper;
537-
538-
/**
539-
* The request handler settings
540-
*/
541-
readonly requestHandler?: NodeHttpHandlerOptions;
542-
543-
/**
544-
* A plugin host
545-
*/
546-
readonly pluginHost?: PluginHost;
547-
548-
/**
549-
* An SDK logger
550-
*/
551-
readonly logger?: ISdkLogger;
552-
}

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -352,10 +352,10 @@ import {
352352
import { GetCallerIdentityCommand, STSClient } from '@aws-sdk/client-sts';
353353
import { Upload } from '@aws-sdk/lib-storage';
354354
import { getEndpointFromInstructions } from '@smithy/middleware-endpoint';
355-
import type { NodeHttpHandlerOptions } from '@smithy/node-http-handler';
356355
import { ConfiguredRetryStrategy } from '@smithy/util-retry';
357356
import type { WaiterResult } from '@smithy/util-waiter';
358357
import { AccountAccessKeyCache } from './account-cache';
358+
import type { RequestHandlerSettings } from './base-credentials';
359359
import { cachedAsync } from './cached';
360360
import type { ISdkLogger } from './sdk-logger';
361361
import type { Account } from './sdk-provider';
@@ -395,7 +395,7 @@ export interface SdkOptions {
395395
export interface ConfigurationOptions {
396396
region: string;
397397
credentials: SDKv3CompatibleCredentialProvider;
398-
requestHandler: NodeHttpHandlerOptions;
398+
requestHandler: RequestHandlerSettings;
399399
retryStrategy: ConfiguredRetryStrategy;
400400
customUserAgent: string;
401401
logger?: ISdkLogger;
@@ -605,7 +605,7 @@ export class SDK {
605605
constructor(
606606
private readonly credProvider: SDKv3CompatibleCredentialProvider,
607607
region: string,
608-
requestHandler: NodeHttpHandlerOptions,
608+
requestHandler: RequestHandlerSettings,
609609
ioHelper: IoHelper,
610610
logger?: ISdkLogger,
611611
) {

0 commit comments

Comments
 (0)