Skip to content

Commit 4767724

Browse files
authored
refactor(cli): create ProxyAgent in the CLI in preparation of proxy-agent removal from toolkit-lib (#532)
Updates the `CdkToolkit` and `Notices` APIs to take a Node https.Agent instead of options. This gives more flexibility to integrators while retaining proxy support. We can make the changes to `Notices` since it is not a public API. The CLI changed need to be split out from the toolkit-lib changes, so the latter can be safely marked as breaking without touching CLI files. The respective `toolkit-lib` changes are in #533 Relates to #398 --- By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license
1 parent 09c8402 commit 4767724

File tree

6 files changed

+153
-29
lines changed

6 files changed

+153
-29
lines changed

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

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import type { Agent } from 'node:https';
12
import { format } from 'node:util';
23
import type { SDKv3CompatibleCredentialProvider } from '@aws-cdk/cli-plugin-contract';
34
import { createCredentialChain, fromEnv, fromIni, fromNodeProviderChain } from '@aws-sdk/credential-providers';
@@ -270,13 +271,16 @@ export interface CredentialChainOptions {
270271
readonly logger?: ISdkLogger;
271272
}
272273

273-
export async function makeRequestHandler(ioHelper: IoHelper, options: SdkHttpOptions = {}): Promise<NodeHttpHandlerOptions> {
274-
const agent = await new ProxyAgentProvider(ioHelper).create(options);
275-
274+
export function sdkRequestHandler(agent?: Agent): NodeHttpHandlerOptions {
276275
return {
277276
connectionTimeout: DEFAULT_CONNECTION_TIMEOUT,
278277
requestTimeout: DEFAULT_TIMEOUT,
279278
httpsAgent: agent,
280279
httpAgent: agent,
281280
};
282281
}
282+
283+
export async function makeRequestHandler(ioHelper: IoHelper, options: SdkHttpOptions = {}): Promise<NodeHttpHandlerOptions> {
284+
const agent = await new ProxyAgentProvider(ioHelper).create(options);
285+
return sdkRequestHandler(agent);
286+
}

packages/@aws-cdk/toolkit-lib/lib/api/notices/notices.ts

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1+
import type { Agent } from 'https';
12
import * as path from 'path';
23
import { cdkCacheDir } from '../../util';
3-
import type { SdkHttpOptions } from '../aws-auth';
44
import type { Context } from '../context';
55
import type { IIoHost } from '../io';
66
import { CachedDataSource } from './cached-data-source';
@@ -13,6 +13,20 @@ import { IO, asIoHelper } from '../io/private';
1313

1414
const CACHE_FILE_PATH = path.join(cdkCacheDir(), 'notices.json');
1515

16+
/**
17+
* Options for the HTTPS requests made by Notices
18+
*/
19+
export interface NoticesHttpOptions {
20+
/**
21+
* The agent responsible for making the network requests.
22+
*
23+
* Use this so set up a proxy connection.
24+
*
25+
* @default - uses the shared global node agent
26+
*/
27+
readonly agent?: Agent;
28+
}
29+
1630
export interface NoticesProps {
1731
/**
1832
* CDK context
@@ -27,9 +41,9 @@ export interface NoticesProps {
2741
readonly output?: string;
2842

2943
/**
30-
* Options for the HTTP request
44+
* Options for the HTTPS requests made by Notices
3145
*/
32-
readonly httpOptions?: SdkHttpOptions;
46+
readonly httpOptions?: NoticesHttpOptions;
3347

3448
/**
3549
* Where messages are going to be sent
@@ -100,7 +114,7 @@ export class Notices {
100114
private readonly context: Context;
101115
private readonly output: string;
102116
private readonly acknowledgedIssueNumbers: Set<Number>;
103-
private readonly httpOptions: SdkHttpOptions;
117+
private readonly httpOptions: NoticesHttpOptions;
104118
private readonly ioHelper: IoHelper;
105119
private readonly cliVersion: string;
106120

@@ -148,7 +162,7 @@ export class Notices {
148162
}
149163

150164
/**
151-
* Filter the data sourece for relevant notices
165+
* Filter the data source for relevant notices
152166
*/
153167
public filter(options: NoticesDisplayOptions = {}): Promise<FilteredNotice[]> {
154168
return new NoticesFilter(this.ioHelper).filter({

packages/@aws-cdk/toolkit-lib/lib/api/notices/web-data-source.ts

Lines changed: 37 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,53 @@
1-
import type { ClientRequest } from 'http';
2-
import type { RequestOptions } from 'https';
1+
import type { ClientRequest } from 'node:http';
2+
import type { RequestOptions } from 'node:https';
33
import * as https from 'node:https';
4-
import type { SdkHttpOptions } from '../aws-auth';
54
import type { Notice, NoticeDataSource } from './types';
65
import { ToolkitError } from '../../toolkit/toolkit-error';
76
import { formatErrorMessage, humanHttpStatusError, humanNetworkError } from '../../util';
8-
import { ProxyAgentProvider } from '../aws-auth/private';
97
import type { IoHelper } from '../io/private';
108

9+
/**
10+
* A data source that fetches notices from the CDK notices data source
11+
*/
12+
export class WebsiteNoticeDataSourceProps {
13+
/**
14+
* The URL to load notices from.
15+
*
16+
* Note this must be a valid JSON document in the CDK notices data schema.
17+
*
18+
* @see https://github.com/cdklabs/aws-cdk-notices
19+
*
20+
* @default - official CDK notices
21+
*/
22+
readonly url?: string | URL;
23+
/**
24+
* The agent responsible for making the network requests.
25+
*
26+
* Use this so set up a proxy connection.
27+
*
28+
* @default - uses the shared global node agent
29+
*/
30+
readonly agent?: https.Agent;
31+
}
32+
1133
export class WebsiteNoticeDataSource implements NoticeDataSource {
12-
private readonly options: SdkHttpOptions;
34+
/**
35+
* The URL notices are loaded from.
36+
*/
37+
public readonly url: any;
38+
39+
private readonly agent?: https.Agent;
1340

14-
constructor(private readonly ioHelper: IoHelper, options: SdkHttpOptions = {}) {
15-
this.options = options;
41+
constructor(private readonly ioHelper: IoHelper, props: WebsiteNoticeDataSourceProps = {}) {
42+
this.agent = props.agent;
43+
this.url = props.url ?? 'https://cli.cdk.dev-tools.aws.dev/notices.json';
1644
}
1745

1846
async fetch(): Promise<Notice[]> {
1947
const timeout = 3000;
2048

2149
const options: RequestOptions = {
22-
agent: await new ProxyAgentProvider(this.ioHelper).create(this.options),
50+
agent: this.agent,
2351
};
2452

2553
const notices = await new Promise<Notice[]>((resolve, reject) => {
@@ -34,7 +62,7 @@ export class WebsiteNoticeDataSource implements NoticeDataSource {
3462
timer.unref();
3563

3664
try {
37-
req = https.get('https://cli.cdk.dev-tools.aws.dev/notices.json',
65+
req = https.get(this.url,
3866
options,
3967
res => {
4068
if (res.statusCode === 200) {

packages/aws-cdk/lib/cli/cdk-toolkit.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -136,9 +136,15 @@ export enum AssetBuildTime {
136136
JUST_IN_TIME = 'just-in-time',
137137
}
138138

139+
/**
140+
* Custom implementation of the public Toolkit to integrate with the legacy CdkToolkit
141+
*
142+
* This overwrites how an sdkProvider is acquired
143+
* in favor of the one provided directly to CdkToolkit.
144+
*/
139145
class InternalToolkit extends Toolkit {
140146
private readonly _sdkProvider: SdkProvider;
141-
public constructor(sdkProvider: SdkProvider, options: ToolkitOptions) {
147+
public constructor(sdkProvider: SdkProvider, options: Omit<ToolkitOptions, 'sdkConfig'>) {
142148
super(options);
143149
this._sdkProvider = sdkProvider;
144150
}
@@ -172,10 +178,8 @@ export class CdkToolkit {
172178
color: true,
173179
emojis: true,
174180
ioHost: this.ioHost,
175-
sdkConfig: {},
176181
toolkitStackName: this.toolkitStackName,
177182
});
178-
this.toolkit; // aritifical use of this.toolkit to satisfy TS, we want to prepare usage of the new toolkit without using it just yet
179183
}
180184

181185
public async metadata(stackName: string, json: boolean) {

packages/aws-cdk/lib/cli/cli.ts

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import * as version from './version';
1616
import { asIoHelper } from '../../lib/api-private';
1717
import type { IReadLock } from '../api';
1818
import { ToolkitInfo, Notices } from '../api';
19-
import { SdkProvider, IoHostSdkLogger, setSdkTracing, makeRequestHandler } from '../api/aws-auth';
19+
import { SdkProvider, IoHostSdkLogger, setSdkTracing, sdkRequestHandler } from '../api/aws-auth';
2020
import type { BootstrapSource } from '../api/bootstrap';
2121
import { Bootstrapper } from '../api/bootstrap';
2222
import { Deployments } from '../api/deployments';
@@ -29,6 +29,7 @@ import { cliInit, printAvailableTemplates } from '../commands/init';
2929
import { getMigrateScanType } from '../commands/migrate';
3030
import { execProgram, CloudExecutable } from '../cxapp';
3131
import type { StackSelector, Synthesizer } from '../cxapp';
32+
import { ProxyAgentProvider } from './proxy-agent';
3233

3334
if (!process.stdout.isTTY) {
3435
// Disable chalk color highlighting
@@ -89,17 +90,20 @@ export async function exec(args: string[], synthesizer?: Synthesizer): Promise<n
8990

9091
const ioHelper = asIoHelper(ioHost, ioHost.currentAction as any);
9192

93+
// Always create and use ProxyAgent to support configuration via env vars
94+
const proxyAgent = await new ProxyAgentProvider(ioHelper).create({
95+
proxyAddress: configuration.settings.get(['proxy']),
96+
caBundlePath: configuration.settings.get(['caBundlePath']),
97+
});
98+
9299
const shouldDisplayNotices = configuration.settings.get(['notices']);
93100
// Notices either go to stderr, or nowhere
94101
ioHost.noticesDestination = shouldDisplayNotices ? 'stderr' : 'drop';
95102
const notices = Notices.create({
96103
ioHost,
97104
context: configuration.context,
98105
output: configuration.settings.get(['outdir']),
99-
httpOptions: {
100-
proxyAddress: configuration.settings.get(['proxy']),
101-
caBundlePath: configuration.settings.get(['caBundlePath']),
102-
},
106+
httpOptions: { agent: proxyAgent },
103107
cliVersion: version.versionNumber(),
104108
});
105109
const refreshNotices = (async () => {
@@ -116,10 +120,7 @@ export async function exec(args: string[], synthesizer?: Synthesizer): Promise<n
116120
const sdkProvider = await SdkProvider.withAwsCliCompatibleDefaults({
117121
ioHelper,
118122
profile: configuration.settings.get(['profile']),
119-
requestHandler: await makeRequestHandler(ioHelper, {
120-
proxyAddress: argv.proxy,
121-
caBundlePath: argv['ca-bundle-path'],
122-
}),
123+
requestHandler: sdkRequestHandler(proxyAgent),
123124
logger: new IoHostSdkLogger(asIoHelper(ioHost, ioHost.currentAction as any)),
124125
pluginHost: GLOBAL_PLUGIN_HOST,
125126
});
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import * as fs from 'fs-extra';
2+
import { ProxyAgent } from 'proxy-agent';
3+
import type { IoHelper } from '../api-private';
4+
5+
/**
6+
* Options for proxy-agent SDKs
7+
*/
8+
interface ProxyAgentOptions {
9+
/**
10+
* Proxy address to use
11+
*
12+
* @default No proxy
13+
*/
14+
readonly proxyAddress?: string;
15+
16+
/**
17+
* A path to a certificate bundle that contains a cert to be trusted.
18+
*
19+
* @default No certificate bundle
20+
*/
21+
readonly caBundlePath?: string;
22+
}
23+
24+
export class ProxyAgentProvider {
25+
private readonly ioHelper: IoHelper;
26+
27+
public constructor(ioHelper: IoHelper) {
28+
this.ioHelper = ioHelper;
29+
}
30+
31+
public async create(options: ProxyAgentOptions) {
32+
// Force it to use the proxy provided through the command line.
33+
// Otherwise, let the ProxyAgent auto-detect the proxy using environment variables.
34+
const getProxyForUrl = options.proxyAddress != null
35+
? () => Promise.resolve(options.proxyAddress!)
36+
: undefined;
37+
38+
return new ProxyAgent({
39+
ca: await this.tryGetCACert(options.caBundlePath),
40+
getProxyForUrl,
41+
});
42+
}
43+
44+
private async tryGetCACert(bundlePath?: string) {
45+
const path = bundlePath || this.caBundlePathFromEnvironment();
46+
if (path) {
47+
await this.ioHelper.defaults.debug(`Using CA bundle path: ${path}`);
48+
try {
49+
if (!fs.pathExistsSync(path)) {
50+
return undefined;
51+
}
52+
return fs.readFileSync(path, { encoding: 'utf-8' });
53+
} catch (e: any) {
54+
await this.ioHelper.defaults.debug(String(e));
55+
return undefined;
56+
}
57+
}
58+
return undefined;
59+
}
60+
61+
/**
62+
* Find and return a CA certificate bundle path to be passed into the SDK.
63+
*/
64+
private caBundlePathFromEnvironment(): string | undefined {
65+
if (process.env.aws_ca_bundle) {
66+
return process.env.aws_ca_bundle;
67+
}
68+
if (process.env.AWS_CA_BUNDLE) {
69+
return process.env.AWS_CA_BUNDLE;
70+
}
71+
return undefined;
72+
}
73+
}

0 commit comments

Comments
 (0)