Skip to content

Commit ccf0c47

Browse files
authored
Merge branch 'main' into conroy/teleminteg
2 parents ec49986 + 29a6178 commit ccf0c47

File tree

20 files changed

+381
-16
lines changed

20 files changed

+381
-16
lines changed

package.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-testing/cli-integ/tests/cli-integ-tests/proxy/cdk-requests-go-through-a-proxy-when-configured.integtest.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@ integTest('requests go through a proxy when configured',
1414
// Delete notices cache if it exists
1515
await fs.rm(path.join(cdkCacheDir, 'notices.json'), { force: true });
1616

17+
// Delete connection cache if it exists
18+
await fs.rm(path.join(cdkCacheDir, 'connection.json'), { force: true });
19+
1720
await fixture.cdkDeploy('test-2', {
1821
captureStderr: true,
1922
options: [
@@ -26,7 +29,6 @@ integTest('requests go through a proxy when configured',
2629
});
2730

2831
const requests = await proxyServer.getSeenRequests();
29-
3032
expect(requests.map(req => req.url))
3133
.toContain('https://cli.cdk.dev-tools.aws.dev/notices.json');
3234

packages/@aws-cdk/cdk-assets-lib/package.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/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ export * from './garbage-collection';
1010
export * from './hotswap';
1111
export * from './io';
1212
export * from './logs-monitor';
13+
export * from './network-detector';
1314
export * from './notices';
1415
export * from './plugin';
1516
export * from './refactoring';
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from './network-detector';
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
import * as https from 'node:https';
2+
import type { RequestOptions } from 'node:https';
3+
import * as path from 'path';
4+
import * as fs from 'fs-extra';
5+
import { cdkCacheDir } from '../../util';
6+
7+
interface CachedConnectivity {
8+
expiration: number;
9+
hasConnectivity: boolean;
10+
}
11+
12+
const TIME_TO_LIVE_SUCCESS = 60 * 60 * 1000; // 1 hour
13+
const CACHE_FILE_PATH = path.join(cdkCacheDir(), 'connection.json');
14+
15+
/**
16+
* Detects internet connectivity by making a lightweight request to the notices endpoint
17+
*/
18+
export class NetworkDetector {
19+
/**
20+
* Check if internet connectivity is available
21+
*/
22+
public static async hasConnectivity(agent?: https.Agent): Promise<boolean> {
23+
const cachedData = await this.load();
24+
const expiration = cachedData.expiration ?? 0;
25+
26+
if (Date.now() > expiration) {
27+
try {
28+
const connected = await this.ping(agent);
29+
const updatedData = {
30+
expiration: Date.now() + TIME_TO_LIVE_SUCCESS,
31+
hasConnectivity: connected,
32+
};
33+
await this.save(updatedData);
34+
return connected;
35+
} catch {
36+
return false;
37+
}
38+
} else {
39+
return cachedData.hasConnectivity;
40+
}
41+
}
42+
43+
// We are observing lots of timeouts when running in a massively parallel
44+
// integration test environment, so wait for a longer timeout there.
45+
//
46+
// In production, have a short timeout to not hold up the user experience.
47+
private static readonly TIMEOUT = process.env.TESTING_CDK ? 30_000 : 3_000;
48+
private static readonly URL = 'https://cli.cdk.dev-tools.aws.dev/notices.json';
49+
50+
private static async load(): Promise<CachedConnectivity> {
51+
const defaultValue = {
52+
expiration: 0,
53+
hasConnectivity: false,
54+
};
55+
56+
try {
57+
return fs.existsSync(CACHE_FILE_PATH)
58+
? await fs.readJSON(CACHE_FILE_PATH) as CachedConnectivity
59+
: defaultValue;
60+
} catch {
61+
return defaultValue;
62+
}
63+
}
64+
65+
private static async save(cached: CachedConnectivity): Promise<void> {
66+
try {
67+
await fs.ensureFile(CACHE_FILE_PATH);
68+
await fs.writeJSON(CACHE_FILE_PATH, cached);
69+
} catch {
70+
// Silently ignore cache save errors
71+
}
72+
}
73+
74+
private static ping(agent?: https.Agent): Promise<boolean> {
75+
const options: RequestOptions = {
76+
method: 'HEAD',
77+
agent: agent,
78+
timeout: this.TIMEOUT,
79+
};
80+
81+
return new Promise((resolve) => {
82+
const req = https.request(
83+
NetworkDetector.URL,
84+
options,
85+
(res) => {
86+
resolve(res.statusCode !== undefined && res.statusCode < 500);
87+
},
88+
);
89+
req.on('error', () => resolve(false));
90+
req.on('timeout', () => {
91+
req.destroy();
92+
resolve(false);
93+
});
94+
95+
req.end();
96+
});
97+
}
98+
}

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

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import type { Notice, NoticeDataSource } from './types';
55
import { ToolkitError } from '../../toolkit/toolkit-error';
66
import { formatErrorMessage, humanHttpStatusError, humanNetworkError } from '../../util';
77
import type { IoHelper } from '../io/private';
8+
import { NetworkDetector } from '../network-detector/network-detector';
89

910
/**
1011
* A data source that fetches notices from the CDK notices data source
@@ -20,6 +21,7 @@ export class WebsiteNoticeDataSourceProps {
2021
* @default - Official CDK notices
2122
*/
2223
readonly url?: string | URL;
24+
2325
/**
2426
* The agent responsible for making the network requests.
2527
*
@@ -44,6 +46,12 @@ export class WebsiteNoticeDataSource implements NoticeDataSource {
4446
}
4547

4648
async fetch(): Promise<Notice[]> {
49+
// Check connectivity before attempting network request
50+
const hasConnectivity = await NetworkDetector.hasConnectivity(this.agent);
51+
if (!hasConnectivity) {
52+
throw new ToolkitError('No internet connectivity detected');
53+
}
54+
4755
// We are observing lots of timeouts when running in a massively parallel
4856
// integration test environment, so wait for a longer timeout there.
4957
//
@@ -66,7 +74,8 @@ export class WebsiteNoticeDataSource implements NoticeDataSource {
6674
timer.unref();
6775

6876
try {
69-
req = https.get(this.url,
77+
req = https.get(
78+
this.url,
7079
options,
7180
res => {
7281
if (res.statusCode === 200) {
@@ -92,7 +101,8 @@ export class WebsiteNoticeDataSource implements NoticeDataSource {
92101
} else {
93102
reject(new ToolkitError(`${humanHttpStatusError(res.statusCode!)} (Status code: ${res.statusCode})`));
94103
}
95-
});
104+
},
105+
);
96106
req.on('error', e => {
97107
reject(ToolkitError.withCause(humanNetworkError(e), e));
98108
});

packages/@aws-cdk/toolkit-lib/package.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/test/api/notices.test.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import * as fs from 'fs-extra';
55
import * as nock from 'nock';
66
import { Context } from '../../lib/api/context';
77
import { asIoHelper } from '../../lib/api/io/private';
8+
import { NetworkDetector } from '../../lib/api/network-detector/network-detector';
89
import { Notices } from '../../lib/api/notices';
910
import { CachedDataSource } from '../../lib/api/notices/cached-data-source';
1011
import { FilteredNotice, NoticesFilter } from '../../lib/api/notices/filter';
@@ -540,6 +541,24 @@ function parseTestComponent(x: string): Component {
540541
describe(WebsiteNoticeDataSource, () => {
541542
const dataSource = new WebsiteNoticeDataSource(ioHelper);
542543

544+
beforeEach(() => {
545+
// Mock NetworkDetector to return true by default for existing tests
546+
jest.spyOn(NetworkDetector, 'hasConnectivity').mockResolvedValue(true);
547+
});
548+
549+
afterEach(() => {
550+
jest.restoreAllMocks();
551+
});
552+
553+
test('throws error when no connectivity detected', async () => {
554+
const mockHasConnectivity = jest.spyOn(NetworkDetector, 'hasConnectivity').mockResolvedValue(false);
555+
556+
await expect(dataSource.fetch()).rejects.toThrow('No internet connectivity detected');
557+
expect(mockHasConnectivity).toHaveBeenCalledWith(undefined);
558+
559+
mockHasConnectivity.mockRestore();
560+
});
561+
543562
test('returns data when download succeeds', async () => {
544563
const result = await mockCall(200, {
545564
notices: [BASIC_NOTICE, MULTIPLE_AFFECTED_VERSIONS_NOTICE],

0 commit comments

Comments
 (0)