-
Notifications
You must be signed in to change notification settings - Fork 17
/
Copy pathcdklocal
executable file
·497 lines (438 loc) · 17.4 KB
/
cdklocal
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
#!/usr/bin/env node
"use strict"
const fs = require("fs");
const diff = require("diff");
const path = require("path");
const http = require("http");
const https = require("https");
const crypto = require("crypto");
const net = require('net');
const { isEnvTrue, EDGE_PORT, PROTOCOL, configureEnvironment } = require("../src");
// constants and custom config values
const DEFAULT_HOSTNAME = "localhost";
const LAMBDA_MOUNT_CODE = isEnvTrue("LAMBDA_MOUNT_CODE");
const AWS_ENVAR_ALLOWLIST = process.env.AWS_ENVAR_ALLOWLIST || "";
//----------------
// UTIL FUNCTIONS
//----------------
const getLocalEndpoint = async () => process.env.AWS_ENDPOINT_URL || `${PROTOCOL}://${await getLocalHost()}`;
var resolvedHostname = undefined;
const runAsyncFunctionAsSync = (asyncFn) => {
return (...args) => {
asyncFn(...args).catch((e) => {
console.error(e);
});
};
};
const getLocalHost = async () => {
if (resolvedHostname) {
// early exit to not resolve again
return `${resolvedHostname}:${EDGE_PORT}`;
}
var hostname = process.env.LOCALSTACK_HOSTNAME || DEFAULT_HOSTNAME;
// Fall back to using local IPv4 address if connection to localhost fails.
// This workaround transparently handles systems (e.g., macOS) where
// localhost resolves to IPv6 when using Nodejs >=v17. See discussion:
// https://github.com/localstack/aws-cdk-local/issues/76#issuecomment-1412590519
// Issue: https://github.com/localstack/aws-cdk-local/issues/78
if (hostname === "localhost") {
try {
const options = {host: hostname, port: EDGE_PORT};
await checkTCPConnection(options);
} catch (e) {
hostname = "127.0.0.1";
}
}
resolvedHostname = hostname;
return `${hostname}:${EDGE_PORT}`;
};
/**
* Checks whether a TCP connection to the given "options" can be established.
* @param {object} options connection options of net.socket.connect()
* https://nodejs.org/api/net.html#socketconnectoptions-connectlistener
* Example: { host: "localhost", port: 4566 }
* @returns {Promise} A fulfilled empty promise on successful connection and
* a rejected promise on any connection error.
*/
const checkTCPConnection = async (options) => {
return new Promise((resolve, reject) => {
const socket = new net.Socket();
const client = socket.connect(options, () => {
client.end();
resolve();
});
client.setTimeout(500); // milliseconds
client.on("timeout", err => {
client.destroy();
reject(err);
});
client.on("error", err => {
client.destroy();
reject(err);
});
});
}
const useLocal = () => {
// TODO make configurable..?
return true;
};
const md5 = s => crypto.createHash("md5").update(s).digest("hex");
const getMethods = (obj) => {
const properties = new Set();
let currentObj = obj;
do {
Object.getOwnPropertyNames(currentObj).map((item) => properties.add(item));
} while ((currentObj = Object.getPrototypeOf(currentObj)));
const excluded = [
"caller", "callee", "arguments", "constructor", "isPrototypeOf",
"hasOwnProperty", "valueOf", "toString", "toLocaleString", "propertyIsEnumerable"
];
const props = [...properties.keys()].filter((pr) => !excluded.includes(pr) && !pr.startsWith("__"));
return props.filter((item) => typeof obj[item] === "function");
};
// simple helper function to fetch an HTTP URL in a promisified way
const fetchURLAsync = (url) => new Promise((resolve, reject) => {
const httpClient = url.includes("https://") ? https : http;
const req = httpClient.get(url, {"rejectUnauthorized": false}, (res) => {
let responseBody = "";
res.on("data", (chunk) => {
responseBody += chunk;
});
res.on("end", () => {
resolve(responseBody || "{}");
});
});
req.on("error", (err) => {
reject(err);
});
req.end();
});
const getTemplateBody = (params) => {
if (params.TemplateBody) {
return params.TemplateBody;
}
return fetchURLAsync(params.TemplateURL);
};
// small import util function
const modulePrefix = "aws-cdk/node_modules";
const importLib = function importLib(libPath) {
try {
return require(path.join(modulePrefix, libPath));
} catch (exc) {
return require(path.join(libPath));
}
};
// this isn't doing anything for current versions (e.g. 2.167.1)
const setSdkOptions = async (options, setHttpOptions) => {
if (!useLocal(options)) {
return;
}
if (setHttpOptions) {
options = options.httpOptions = options.httpOptions || {};
}
options.endpoint = await getLocalEndpoint();
options.s3ForcePathStyle = true;
options.accessKeyId = "test";
options.secretAccessKey = "test";
};
const patchProviderCredentials = (provider) => {
const origConstr = provider.SdkProvider.withAwsCliCompatibleDefaults;
provider.SdkProvider.withAwsCliCompatibleDefaults = async (options = {}) => {
const localEndpoint = await getLocalEndpoint();
await setSdkOptions(options, true); // legacy
const result = await origConstr(options);
result.sdkOptions = result.sdkOptions || {}; // legacy
await setSdkOptions(result.sdkOptions); // legacy
// >= 2.167.0
if (result.requestHandler) {
result.requestHandler.endpoint = localEndpoint;
result.requestHandler.forcePathStyle = true;
}
return result;
};
provider.SdkProvider.prototype.defaultCredentials = () => ({
"accessKeyId": process.env.AWS_ACCESS_KEY_ID || "test",
"secretAccessKey": process.env.AWS_SECRET_ACCESS_KEY || "test"
});
};
const patchCdkToolkit = (CdkToolkit) => {
const CdkToolkitClass = CdkToolkit.ToolkitInfo || CdkToolkit;
getMethods(CdkToolkitClass.prototype).forEach((meth) => {
const original = CdkToolkitClass.prototype[meth];
CdkToolkitClass.prototype[meth] = async function methFunc(...args) {
await setSdkOptions(this.props.sdkProvider.sdkOptions);
return original.bind(this).apply(this, args);
};
});
};
const patchCurrentAccount = (SDK) => {
const currentAccountOrig = SDK.prototype.currentAccount;
SDK.prototype.currentAccount = async function currentAccount() {
const {config} = this;
await setSdkOptions(config);
return currentAccountOrig.bind(this)();
};
const forceCredentialRetrievalOrig = SDK.prototype.forceCredentialRetrieval;
SDK.prototype.forceCredentialRetrieval = function forceCredentialRetrieval() {
if (!this._credentials.getPromise) {
this._credentials.getPromise = () => this._credentials;
}
return forceCredentialRetrievalOrig.bind(this)();
};
};
const patchToolkitInfo = (ToolkitInfo) => {
const setBucketUrl = function setBucketUrl(object, bucket, domain) {
const newBucketUrl = `https://${domain.replace(`${bucket}.`, "")}:${EDGE_PORT}/${bucket}`;
Object.defineProperty(object, "bucketUrl", {
get() {
return newBucketUrl;
}
});
};
// Pre-fetch the necessary values for the bucket URL
const prefetchBucketUrl = async (object) => {
// Has been observed that the object is not always an instance of ToolkitInfo
if (object && Object.prototype.hasOwnProperty.call(object, "bucketName") && Object.prototype.hasOwnProperty.call(object, "bucketUrl")) {
try {
const bucket = object.bucketName;
const domain = object.bucketUrl.replace("https://", "") || await getLocalHost();
// When object is ExistingToolkitInfo & the bucketName/bucketUrl attributes are non-null
setBucketUrl(object, bucket, domain);
} catch (e) {
// ToolkitInfo: bucketName/bucketUrl attributes don't exist or if implemented, they throw exceptions
// so the exceptions have to be ignored.
//
// The following is an example of how the bucketName/bucketUrl attributes are implemented in the BootstrapStackNotFoundInfos class:
// I.e.: https://github.com/aws/aws-cdk/blob/87e21d625af86873716734dd5568940d41096c45/packages/aws-cdk/lib/api/toolkit-info.ts#L190-L196
//
// The following is an example of how the bucketName/bucketUrl attributes are implemented in the ExistingToolkitInfo class:
// I.e.: https://github.com/aws/aws-cdk/blob/87e21d625af86873716734dd5568940d41096c45/packages/aws-cdk/lib/api/toolkit-info.ts#L124-L130
}
}
};
// for compatibility with with older versions of CDK
runAsyncFunctionAsSync(prefetchBucketUrl(ToolkitInfo.prototype));
const cdkLookupFn = ToolkitInfo.lookup;
ToolkitInfo.lookup = async (...args) => {
const toolkitInfoObject = await cdkLookupFn(...args);
await prefetchBucketUrl(toolkitInfoObject);
return toolkitInfoObject;
};
const fromStackFn = ToolkitInfo.fromStack;
ToolkitInfo.fromStack = (...args) => {
const toolkitInfoObject = fromStackFn(...args);
runAsyncFunctionAsSync(prefetchBucketUrl(toolkitInfoObject));
return toolkitInfoObject;
};
};
const patchLambdaMounting = (CdkToolkit) => {
const {deserializeStructure} = require("aws-cdk/lib/serialize");
const deployStackMod = require("aws-cdk/lib/api/deploy-stack");
// modify asset paths to enable local Lambda code mounting
const lookupLambdaForAsset = (template, paramName) => {
const result = Object.keys(template.Resources).map((key) => template.Resources[key]).filter((res) => res.Type === "AWS::Lambda::Function").filter((res) => JSON.stringify(res.Properties.Code.S3Key).includes(paramName));
const props = result[0].Properties;
const funcName = props.FunctionName;
if (funcName) {
return funcName;
}
const attributes = ["Handler", "Runtime", "Description", "Timeout", "MemorySize", "Environment"];
const valueToHash = attributes.map((attr) => props[attr]).map((val) => typeof val === "object" ? JSON.stringify(diff.canonicalize(val)) : val ? val : "").join("|");
return md5(valueToHash);
};
const symlinkLambdaAssets = (template, parameters) => {
const params = parameters || template.Parameters || {};
Object.keys(params).forEach((key) => {
const item = params[key];
const paramKey = item.ParameterKey || key;
// TODO: create a more resilient lookup mechanism (not based on "S3Bucket" param key) below!
if (item.ParameterKey && item.ParameterKey.includes("S3Bucket")) {
// TODO: change the default BUCKET_MARKER_LOCAL to 'hot-reload'
item.ParameterValue = process.env.BUCKET_MARKER_LOCAL || '__local__'; // for now, the default is still __local__
}
if (!paramKey.includes("S3VersionKey")) {
return;
}
let assetId = "";
if (item.ParameterValue) {
const parts = item.ParameterValue.split("||");
if (item.ParameterValue.endsWith(".zip") && parts.length > 1) {
assetId = parts[1].replace(".zip", "");
}
}
if (!assetId) {
[assetId] = paramKey.replace("AssetParameters", "").split("S3VersionKey");
}
const funcName = lookupLambdaForAsset(template, paramKey);
const lambdaAsset = `cdk.out/asset.lambda.${funcName}`;
if (fs.existsSync(lambdaAsset)) {
// delete any existing symlinks
fs.unlinkSync(lambdaAsset);
}
if (fs.existsSync(`cdk.out/asset.${assetId}`)) {
fs.symlinkSync(`asset.${assetId}`, lambdaAsset);
}
item.ParameterValue = `${process.cwd()}/||${lambdaAsset}`;
});
};
// symlink local Lambda assets if "cdklocal deploy" is called with LAMBDA_MOUNT_CODE=1
const deployStackOrig = deployStackMod.deployStack;
deployStackMod.deployStack = function deployStack(options) {
options.sdk.cloudFormationOrig = options.sdk.cloudFormationOrig || options.sdk.cloudFormation;
const state = {};
options.sdk.cloudFormation = () => state.instance;
const cfn = state.instance = options.sdk.cloudFormationOrig();
cfn.createChangeSetOrig = cfn.createChangeSetOrig || cfn.createChangeSet;
const createChangeSetAsync = async function createChangeSetAsync(params) {
if (LAMBDA_MOUNT_CODE) {
const template = deserializeStructure(await getTemplateBody(params));
symlinkLambdaAssets(template, params.Parameters);
}
return cfn.createChangeSetOrig(params).promise();
};
cfn.createChangeSet = (params) => ({"promise": () => createChangeSetAsync(params)});
const result = deployStackOrig(options);
return result;
};
// skip uploading Lambda assets if LAMBDA_MOUNT_CODE=1
const {FileAssetHandler} = importLib("cdk-assets/lib/private/handlers/files");
const handlerPublish = FileAssetHandler.prototype.publish;
FileAssetHandler.prototype.publish = function publish() {
if (LAMBDA_MOUNT_CODE && this.asset.destination && this.asset.source) {
if (this.asset.source.packaging === "zip") {
// skip uploading this asset - should get mounted via `__file__` into the Lambda container later on
return null;
}
}
return handlerPublish.bind(this)();
};
// symlink local Lambda assets if "cdklocal synth" is called with LAMBDA_MOUNT_CODE=1
const assemblyOrig = CdkToolkit.prototype.assembly;
CdkToolkit.prototype.assembly = async function assembly() {
const result = await assemblyOrig.bind(this)();
if (LAMBDA_MOUNT_CODE) {
result.assembly.artifacts.forEach((art) => {
symlinkLambdaAssets(art._template || {});
});
}
return result;
};
};
const isEsbuildBundle = () => {
// simple heuristic to determine whether this is a new esbuild bundle (CDK v2.14.0+),
// based on this change which replaced `__dirname` with `rootDir()`:
// https://github.com/aws/aws-cdk/pull/18667/files#diff-6902a5fbdd9dfe9dc5563fe7d7d156e4fd99f945ac3977390d6aaacdd0370f82
try {
const directories = require("aws-cdk/lib/util/directories");
return directories && directories.rootDir;
} catch (exc) {
return false;
}
};
const patchSdk = (SDK, localEndpoint) => {
getMethods(SDK.prototype).forEach((methodName) => {
if (typeof SDK.prototype[methodName] === 'function') {
const original = SDK.prototype[methodName];
SDK.prototype[methodName] = function methFunc(...args) {
this.config.endpoint = localEndpoint;
this.config.forcePathStyle = true;
return original.apply(this, args);
};
}
});
};
let sdkOverwritten = false;
const patchSdkProvider = (provider, SDK) => {
getMethods(provider.SdkProvider.prototype).forEach((methodName) => {
if (typeof provider.SdkProvider.prototype[methodName] === 'function') {
const original = provider.SdkProvider.prototype[methodName];
provider.SdkProvider.prototype[methodName] = async function methFunc(...args) {
const localEndpoint = await getLocalEndpoint();
// patch for >= 2.167.0
if (!sdkOverwritten && this.requestHandler) {
// the goal is to support `SdkProvider.withAssumedRole`
// since it instantiates a different client (i.e. not from the SDK class)
this.requestHandler.endpoint = localEndpoint;
this.requestHandler.forcePathStyle = true;
// patch SDK class methods (mostly clients) to make sure the config that is created in the constructor
// is updated with the correct configuration
patchSdk(SDK, localEndpoint);
sdkOverwritten = true;
}
return await original.apply(this, args);
};
}
}
);
};
const applyPatches = (provider, CdkToolkit, SDK, ToolkitInfo, patchAssets = true) => {
patchSdkProvider(provider, SDK);
// TODO: a lot of the patches are not really needed for newer versions
patchProviderCredentials(provider);
patchCdkToolkit(CdkToolkit);
patchCurrentAccount(SDK);
patchToolkitInfo(ToolkitInfo);
// Patch asset handling for Lambda code mounting - TODO currently failing for CDK v2.14.0+
if (patchAssets) {
patchLambdaMounting(CdkToolkit);
}
};
const patchPre_2_14 = () => {
var provider = null;
try {
provider = require("aws-cdk/lib/api/aws-auth");
} catch (e) {
if (e.code == "MODULE_NOT_FOUND") {
console.log(e);
console.error("`aws-cdk` module NOT found! Have you tried adding it to your `NODE_PATH`?");
throw e;
}
}
const {CdkToolkit} = require("aws-cdk/lib/cdk-toolkit");
const {SDK} = require("aws-cdk/lib/api/aws-auth/sdk");
const {ToolkitInfo} = require("aws-cdk/lib/api");
applyPatches(provider, CdkToolkit, SDK, ToolkitInfo);
};
const patchPost_2_14 = () => {
var lib = null;
try {
lib = require("aws-cdk/lib");
} catch (e) {
if (e.code == "MODULE_NOT_FOUND") {
console.log(e);
console.log("`aws-cdk` module NOT found! Have you tried to add it to your `NODE_PATH`?");
process.exit(1);
}
}
// detect if we are using version 2.177.0 or later. This version has reorganised the package
// structure so that we cannot import and patch the aws-cdk package as we did for versions
// <2.177.0. We use the specific error raised by the node require system to determine if we are
// using pre or post 2.177.0.
try {
require("aws-cdk/lib/legacy-exports");
} catch (e) {
switch (e.code) {
case "MODULE_NOT_FOUND":
// pre 2.177
applyPatches(lib, lib, lib.SDK, lib.ToolkitInfo, false);
break;
case "ERR_PACKAGE_PATH_NOT_EXPORTED":
// post 2.177
configureEnvironment(process.env, AWS_ENVAR_ALLOWLIST);
break;
default:
// a different error
throw e
}
}
};
if (isEsbuildBundle()) {
// load for CDK version 2.14.0 and above
// (v2.14.0+ uses a self-contained bundle, see https://github.com/aws/aws-cdk/pull/18667)
patchPost_2_14();
} else {
// fall back to loading for CDK version 2.13.0 and below
patchPre_2_14();
}
// load main CLI script
require("aws-cdk/bin/cdk");