cdk-local 0.68.0 → 0.70.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +55 -2
- package/dist/cli.js +2 -2
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/internal.d.ts +25 -25
- package/dist/internal.d.ts.map +1 -1
- package/dist/internal.js +2 -2
- package/dist/{local-list-BFtuOXLq.js → local-list-C-wXKogn.js} +644 -46
- package/dist/local-list-C-wXKogn.js.map +1 -0
- package/dist/{local-list-9jAE7ClA.d.ts → local-list-D6VYjdj1.d.ts} +22 -2
- package/dist/local-list-D6VYjdj1.d.ts.map +1 -0
- package/package.json +1 -1
- package/dist/local-list-9jAE7ClA.d.ts.map +0 -1
- package/dist/local-list-BFtuOXLq.js.map +0 -1
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { a as runDockerStreaming, c as getEmbedConfig, i as runDockerForeground, n as formatDockerLoginError, o as spawnStreaming, r as getDockerCmd, s as getLogger, u as setEmbedConfig } from "./docker-cmd-voNPrcRh.js";
|
|
2
2
|
import { cpSync, createWriteStream, existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, statSync, unlinkSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { homedir, tmpdir } from "node:os";
|
|
4
|
-
import * as path from "node:path";
|
|
5
|
-
import { dirname, isAbsolute, join, normalize, resolve, sep } from "node:path";
|
|
4
|
+
import * as path$1 from "node:path";
|
|
5
|
+
import path, { dirname, isAbsolute, join, normalize, resolve, sep } from "node:path";
|
|
6
6
|
import { Command, Option } from "commander";
|
|
7
7
|
import { AssumeRoleCommand, GetCallerIdentityCommand, STSClient } from "@aws-sdk/client-sts";
|
|
8
8
|
import { MultiSelectPrompt } from "@clack/core";
|
|
@@ -7834,8 +7834,8 @@ function getContainerAwsCredentialsPath() {
|
|
|
7834
7834
|
async function writeProfileCredentialsFile(profileName, creds) {
|
|
7835
7835
|
if (profileName === "") throw new Error("writeProfileCredentialsFile: profile name must not be empty.");
|
|
7836
7836
|
if (/[\r\n[\]]/.test(profileName)) throw new Error(`writeProfileCredentialsFile: profile name '${profileName}' contains a forbidden character (any of CR, LF, '[', ']' would corrupt the INI file or the docker -e env var).`);
|
|
7837
|
-
const dir = await mkdtemp(path.join(tmpdir(), `${getEmbedConfig().productName}-profile-creds-`));
|
|
7838
|
-
const hostPath = path.join(dir, "credentials");
|
|
7837
|
+
const dir = await mkdtemp(path$1.join(tmpdir(), `${getEmbedConfig().productName}-profile-creds-`));
|
|
7838
|
+
const hostPath = path$1.join(dir, "credentials");
|
|
7839
7839
|
const lines = [
|
|
7840
7840
|
`[${profileName}]`,
|
|
7841
7841
|
`aws_access_key_id = ${creds.accessKeyId}`,
|
|
@@ -8177,7 +8177,7 @@ function materializeLambdaLayers$1(layers) {
|
|
|
8177
8177
|
containerPath: "/opt",
|
|
8178
8178
|
readOnly: true
|
|
8179
8179
|
} };
|
|
8180
|
-
const tmpDir = mkdtempSync(path.join(tmpdir(), `${getEmbedConfig().resourceNamePrefix}-invoke-layers-`));
|
|
8180
|
+
const tmpDir = mkdtempSync(path$1.join(tmpdir(), `${getEmbedConfig().resourceNamePrefix}-invoke-layers-`));
|
|
8181
8181
|
for (const layer of layers) cpSync(layer.assetPath, tmpDir, {
|
|
8182
8182
|
recursive: true,
|
|
8183
8183
|
force: true
|
|
@@ -8385,9 +8385,9 @@ function materializeInlineCode$2(handler, source, fileExtension) {
|
|
|
8385
8385
|
const lastDot = handler.lastIndexOf(".");
|
|
8386
8386
|
if (lastDot <= 0) throw new Error(`Handler '${handler}' is malformed: expected '<modulePath>.<exportName>'.`);
|
|
8387
8387
|
const modulePath = handler.substring(0, lastDot);
|
|
8388
|
-
const dir = mkdtempSync(path.join(tmpdir(), `${getEmbedConfig().resourceNamePrefix}-invoke-`));
|
|
8389
|
-
const filePath = path.join(dir, `${modulePath}${fileExtension}`);
|
|
8390
|
-
mkdirSync(path.dirname(filePath), { recursive: true });
|
|
8388
|
+
const dir = mkdtempSync(path$1.join(tmpdir(), `${getEmbedConfig().resourceNamePrefix}-invoke-`));
|
|
8389
|
+
const filePath = path$1.join(dir, `${modulePath}${fileExtension}`);
|
|
8390
|
+
mkdirSync(path$1.dirname(filePath), { recursive: true });
|
|
8391
8391
|
writeFileSync(filePath, source, "utf-8");
|
|
8392
8392
|
return dir;
|
|
8393
8393
|
}
|
|
@@ -16510,15 +16510,19 @@ function createFileWatcher(options) {
|
|
|
16510
16510
|
...options.ignored && { ignored: options.ignored }
|
|
16511
16511
|
});
|
|
16512
16512
|
let timer = null;
|
|
16513
|
+
let pending = /* @__PURE__ */ new Set();
|
|
16513
16514
|
let closed = false;
|
|
16514
|
-
const fire = () => {
|
|
16515
|
+
const fire = (path) => {
|
|
16515
16516
|
if (closed) return;
|
|
16517
|
+
pending.add(path);
|
|
16516
16518
|
if (timer) clearTimeout(timer);
|
|
16517
16519
|
timer = setTimeout(() => {
|
|
16518
16520
|
timer = null;
|
|
16519
16521
|
if (closed) return;
|
|
16522
|
+
const changed = Array.from(pending);
|
|
16523
|
+
pending = /* @__PURE__ */ new Set();
|
|
16520
16524
|
try {
|
|
16521
|
-
options.onChange();
|
|
16525
|
+
options.onChange(changed);
|
|
16522
16526
|
} catch (err) {
|
|
16523
16527
|
logger.warn(`onChange callback threw: ${err instanceof Error ? err.message : String(err)}`);
|
|
16524
16528
|
}
|
|
@@ -16527,7 +16531,7 @@ function createFileWatcher(options) {
|
|
|
16527
16531
|
};
|
|
16528
16532
|
const onEvent = (path) => {
|
|
16529
16533
|
if (options.shouldTrigger && !options.shouldTrigger(path)) return;
|
|
16530
|
-
fire();
|
|
16534
|
+
fire(path);
|
|
16531
16535
|
};
|
|
16532
16536
|
watcher.on("add", onEvent);
|
|
16533
16537
|
watcher.on("change", onEvent);
|
|
@@ -17040,8 +17044,8 @@ async function localStartApiCommand(targets, options, extraStateProviders) {
|
|
|
17040
17044
|
*/
|
|
17041
17045
|
function createWatchPredicates(args) {
|
|
17042
17046
|
const { watchRoot, output, watchConfig } = args;
|
|
17043
|
-
const toRel = (abs) => path.relative(watchRoot, abs).split(path.sep).join("/");
|
|
17044
|
-
const outputRel = toRel(path.resolve(watchRoot, output));
|
|
17047
|
+
const toRel = (abs) => path$1.relative(watchRoot, abs).split(path$1.sep).join("/");
|
|
17048
|
+
const outputRel = toRel(path$1.resolve(watchRoot, output));
|
|
17045
17049
|
const excludePatterns = [
|
|
17046
17050
|
...outputRel !== "" && !outputRel.startsWith("..") ? [outputRel] : [],
|
|
17047
17051
|
"node_modules",
|
|
@@ -17546,7 +17550,7 @@ async function resolveContainerImageForStartApi(lambda, skipPull, profile) {
|
|
|
17546
17550
|
async function resolveLocalBuildPlan(lambda) {
|
|
17547
17551
|
const manifestPath = lambda.stack.assetManifestPath;
|
|
17548
17552
|
if (!manifestPath) return void 0;
|
|
17549
|
-
const cdkOutDir = path.dirname(manifestPath);
|
|
17553
|
+
const cdkOutDir = path$1.dirname(manifestPath);
|
|
17550
17554
|
const manifest = await new AssetManifestLoader().loadManifest(cdkOutDir, lambda.stack.stackName);
|
|
17551
17555
|
if (!manifest) return void 0;
|
|
17552
17556
|
const entry = getDockerImageBySourceHash(manifest, lambda.imageUri);
|
|
@@ -17603,7 +17607,7 @@ async function materializeLambdaLayers(layers, layerTmpDirs, layerRoleArn) {
|
|
|
17603
17607
|
});
|
|
17604
17608
|
}
|
|
17605
17609
|
if (flat.length === 1) return flat[0].assetPath;
|
|
17606
|
-
const dir = mkdtempSync(path.join(tmpdir(), `${getEmbedConfig().resourceNamePrefix}-start-api-layers-`));
|
|
17610
|
+
const dir = mkdtempSync(path$1.join(tmpdir(), `${getEmbedConfig().resourceNamePrefix}-start-api-layers-`));
|
|
17607
17611
|
for (const layer of flat) cpSync(layer.assetPath, dir, {
|
|
17608
17612
|
recursive: true,
|
|
17609
17613
|
force: true
|
|
@@ -17742,8 +17746,8 @@ function resolveImageLambda(args) {
|
|
|
17742
17746
|
function resolveAssetCodePath(stack, logicalId, resource) {
|
|
17743
17747
|
const assetPath = resource.Metadata?.["aws:asset:path"];
|
|
17744
17748
|
if (typeof assetPath !== "string" || assetPath.length === 0) throw new Error(`Lambda '${logicalId}' has no Metadata['aws:asset:path']. ${getEmbedConfig().cliName} start-api needs this hint to find the local asset directory. Re-synthesize the app and retry.`);
|
|
17745
|
-
const cdkOutDir = stack.assetManifestPath ? path.dirname(stack.assetManifestPath) : process.cwd();
|
|
17746
|
-
return path.isAbsolute(assetPath) ? assetPath : path.resolve(cdkOutDir, assetPath);
|
|
17749
|
+
const cdkOutDir = stack.assetManifestPath ? path$1.dirname(stack.assetManifestPath) : process.cwd();
|
|
17750
|
+
return path$1.isAbsolute(assetPath) ? assetPath : path$1.resolve(cdkOutDir, assetPath);
|
|
17747
17751
|
}
|
|
17748
17752
|
/**
|
|
17749
17753
|
* Print the discovered route table to stdout. Format mirrors the spec
|
|
@@ -17798,10 +17802,10 @@ function materializeInlineCode$1(handler, source, fileExtension, tmpDirsOut) {
|
|
|
17798
17802
|
const lastDot = handler.lastIndexOf(".");
|
|
17799
17803
|
if (lastDot <= 0) throw new Error(`Handler '${handler}' is malformed: expected '<modulePath>.<exportName>'.`);
|
|
17800
17804
|
const modulePath = handler.substring(0, lastDot);
|
|
17801
|
-
const dir = mkdtempSync(path.join(tmpdir(), `${getEmbedConfig().resourceNamePrefix}-start-api-`));
|
|
17805
|
+
const dir = mkdtempSync(path$1.join(tmpdir(), `${getEmbedConfig().resourceNamePrefix}-start-api-`));
|
|
17802
17806
|
tmpDirsOut.add(dir);
|
|
17803
|
-
const filePath = path.join(dir, `${modulePath}${fileExtension}`);
|
|
17804
|
-
mkdirSync(path.dirname(filePath), { recursive: true });
|
|
17807
|
+
const filePath = path$1.join(dir, `${modulePath}${fileExtension}`);
|
|
17808
|
+
mkdirSync(path$1.dirname(filePath), { recursive: true });
|
|
17805
17809
|
writeFileSync(filePath, source, "utf-8");
|
|
17806
17810
|
return dir;
|
|
17807
17811
|
}
|
|
@@ -20883,6 +20887,22 @@ var EcsServiceRunnerError = class EcsServiceRunnerError extends Error {
|
|
|
20883
20887
|
Object.setPrototypeOf(this, EcsServiceRunnerError.prototype);
|
|
20884
20888
|
}
|
|
20885
20889
|
};
|
|
20890
|
+
/**
|
|
20891
|
+
* Phase 4 (#214) — completion-log suffix the soft-reload primitive
|
|
20892
|
+
* emits AFTER `Soft-reloaded replica r<i> (gen <g>): ` to confirm
|
|
20893
|
+
* the docker restart + TCP-ready probe + Cloud Map / front-door
|
|
20894
|
+
* re-publish round trip is done.
|
|
20895
|
+
*
|
|
20896
|
+
* Exported so integ fixtures + unit tests can grep against the
|
|
20897
|
+
* canonical text instead of hand-copying the wording — a future
|
|
20898
|
+
* refactor that rewords this line stays detectable via the symbol
|
|
20899
|
+
* import instead of silently breaking every test's regex.
|
|
20900
|
+
*
|
|
20901
|
+
* Per-repo memory (#218 / test reviewer N4): log-line text is part
|
|
20902
|
+
* of the public contract for `--watch` integ scripts, so it earns a
|
|
20903
|
+
* constant.
|
|
20904
|
+
*/
|
|
20905
|
+
const SOFT_RELOAD_COMPLETION_LOG_SUFFIX = "restart + TCP-ready probe complete; Cloud Map + front-door re-published.";
|
|
20886
20906
|
function createServiceRunState() {
|
|
20887
20907
|
return {
|
|
20888
20908
|
replicas: [],
|
|
@@ -21137,6 +21157,27 @@ async function bootReplica(service, options, instance) {
|
|
|
21137
21157
|
await runEcsTask(service.task, perReplicaTaskOptions, instance.state);
|
|
21138
21158
|
if (options.discovery) await publishReplicaToCloudMap(service, instance, options.discovery, ownerKeyPrefix);
|
|
21139
21159
|
if (options.frontDoor) await publishReplicaToFrontDoor(service, instance, options.frontDoor, options.taskOptions.containerHost, ownerKeyPrefix);
|
|
21160
|
+
instance.lastDeployedAssetHash = pickEssentialAssetHash(service);
|
|
21161
|
+
}
|
|
21162
|
+
/**
|
|
21163
|
+
* Phase 4 follow-up (#218) — extract the CDK asset hash from a
|
|
21164
|
+
* resolved service's first essential container (with the same
|
|
21165
|
+
* fallback the watcher uses: first essential, else first container).
|
|
21166
|
+
* Returns `undefined` when the image isn't a CDK asset OR carries
|
|
21167
|
+
* no hash. Pure helper so the boot + rolling + soft-reload paths
|
|
21168
|
+
* share one source of truth for "what's running right now".
|
|
21169
|
+
*
|
|
21170
|
+
* Exported for unit tests; not part of the semver-covered public
|
|
21171
|
+
* surface.
|
|
21172
|
+
*
|
|
21173
|
+
* @internal
|
|
21174
|
+
*/
|
|
21175
|
+
function pickEssentialAssetHash(service) {
|
|
21176
|
+
const essential = service.task.containers.find((c) => c.essential) ?? service.task.containers[0];
|
|
21177
|
+
if (!essential) return void 0;
|
|
21178
|
+
const image = essential.image;
|
|
21179
|
+
if (image?.kind !== "cdk-asset") return void 0;
|
|
21180
|
+
return image.assetHash;
|
|
21140
21181
|
}
|
|
21141
21182
|
/**
|
|
21142
21183
|
* After the replica's main container is up, discover its docker
|
|
@@ -21404,6 +21445,202 @@ async function rollServiceReplica(args) {
|
|
|
21404
21445
|
logger.info(`Rolling replica ${shadow.index} (gen ${shadow.generation}): swap complete; old retired.`);
|
|
21405
21446
|
}
|
|
21406
21447
|
/**
|
|
21448
|
+
* Phase 4 of issue #214 — bind-mount source fast path. `docker cp` the
|
|
21449
|
+
* post-synth asset source directory into each essential container of
|
|
21450
|
+
* the live replica, then `docker restart` it. Skips `docker build`,
|
|
21451
|
+
* skips a shadow boot, and keeps the container's network IP / Cloud
|
|
21452
|
+
* Map / front-door pool registrations intact (the registrations key
|
|
21453
|
+
* off the docker-assigned IP and the published host port; `docker
|
|
21454
|
+
* restart` preserves both via the container's stable network
|
|
21455
|
+
* namespace), so NO registry swap is needed.
|
|
21456
|
+
*
|
|
21457
|
+
* Sequence (per replica, sequenced by the rolling loop one at a time
|
|
21458
|
+
* so peer services + the ALB front-door always have at least N-1 live
|
|
21459
|
+
* endpoints across the reload — same zero-connection-refusal guarantee
|
|
21460
|
+
* the Phase 2/3 rebuild pathway makes):
|
|
21461
|
+
* 1. Locate the live replica by {@link oldReplicaIndex}; reject when
|
|
21462
|
+
* shutting down (the next save can roll a clean boot instead).
|
|
21463
|
+
* 2. Pre-restart DRAIN: drop the replica's Cloud Map handles + every
|
|
21464
|
+
* front-door pool entry under its owner key. Both registries are
|
|
21465
|
+
* synchronous Map mutations; once these complete, peer wget +
|
|
21466
|
+
* front-door `next()` calls route to the surviving replicas. The
|
|
21467
|
+
* handle / owner-key snapshots are kept on `instance.*` so the
|
|
21468
|
+
* symmetric re-register step can pick them up (Cloud Map handles
|
|
21469
|
+
* are rebuilt fresh via `publishReplicaToCloudMap`; the docker
|
|
21470
|
+
* network IP is preserved across `docker restart` so the new
|
|
21471
|
+
* handles point at the SAME endpoint).
|
|
21472
|
+
* 3. Set {@link ServiceReplicaInstance.softReloadInProgress} = true
|
|
21473
|
+
* so the watcher's `waitForExitImpl` post-exit branch defers to
|
|
21474
|
+
* the in-flight restart instead of re-bootstrapping the replica
|
|
21475
|
+
* from scratch.
|
|
21476
|
+
* 4. For each essential container in the replica's started set:
|
|
21477
|
+
* a. Resolve the container's image WORKDIR via `docker inspect`
|
|
21478
|
+
* (default `/` when unset — matches Docker's runtime default
|
|
21479
|
+
* for a Dockerfile with no `WORKDIR`).
|
|
21480
|
+
* b. `docker cp <sourceDirToCopy>/. <containerId>:<workdir>/`
|
|
21481
|
+
* — copy the synthesized asset directory's contents into the
|
|
21482
|
+
* container at the WORKDIR. Trailing `/.` is critical: it
|
|
21483
|
+
* copies the SOURCE DIRECTORY'S CONTENTS, not the directory
|
|
21484
|
+
* itself, mirroring `cp -r src/. dst/`.
|
|
21485
|
+
* c. `docker restart <containerId>` — cycle PID 1. Image,
|
|
21486
|
+
* network namespace, and host-port publish are preserved.
|
|
21487
|
+
* 5. {@link waitForReplicaTcpReady} confirms the essential
|
|
21488
|
+
* container's first port accepts a TCP connection.
|
|
21489
|
+
* 6. Post-TCP-ready RE-REGISTER: re-publish Cloud Map handles +
|
|
21490
|
+
* front-door pool entry under the SAME per-replica owner key
|
|
21491
|
+
* prefix used at initial boot, so the registrations remain
|
|
21492
|
+
* idempotent across multiple `--watch` reloads. After this
|
|
21493
|
+
* step, peers + the front-door route to the replica again.
|
|
21494
|
+
* 7. Clear `softReloadInProgress` in a `finally` so the watcher
|
|
21495
|
+
* always exits its defer-loop, even on a docker error.
|
|
21496
|
+
*
|
|
21497
|
+
* Failure modes:
|
|
21498
|
+
* - `docker inspect` / `docker cp` / `docker restart` errors:
|
|
21499
|
+
* surfaced to the caller via a throw. The replica may be in an
|
|
21500
|
+
* inconsistent state (drained from registries + partial cp + a
|
|
21501
|
+
* possibly-crashed PID 1). The caller (`reloadAllServices`) logs
|
|
21502
|
+
* the failure and continues with the remaining replicas; the
|
|
21503
|
+
* drained state is intentionally NOT re-registered on error so
|
|
21504
|
+
* peers + the front-door stop routing to a broken replica until
|
|
21505
|
+
* the next clean save (or `^C` and re-run).
|
|
21506
|
+
* - TCP probe timeout: best-effort warn (mirrors
|
|
21507
|
+
* {@link rollServiceReplica}); the registrations are re-published
|
|
21508
|
+
* anyway because the container IS up — just slow to bind. The
|
|
21509
|
+
* dying-old-handles-AND-fresh-app-not-yet-listening worst case
|
|
21510
|
+
* would otherwise leave the replica drained forever.
|
|
21511
|
+
*
|
|
21512
|
+
* Out of scope for the v1 primitive (deferred follow-ups):
|
|
21513
|
+
* - Per-container WORKDIR caching across multiple essential
|
|
21514
|
+
* containers in the same task. The `docker inspect` call is
|
|
21515
|
+
* ~10ms; not worth the cache invalidation surface for a path
|
|
21516
|
+
* fired ~once per save.
|
|
21517
|
+
* - SIGHUP / `docker exec`-driven in-process reload. Image-specific
|
|
21518
|
+
* (uvicorn / nodemon / etc.). `docker restart` is the universal
|
|
21519
|
+
* primitive; signal-based reload is a future opt-in if the
|
|
21520
|
+
* per-restart latency proves prohibitive.
|
|
21521
|
+
*
|
|
21522
|
+
* @internal — wired only by the emulator's reload pathway.
|
|
21523
|
+
*/
|
|
21524
|
+
async function softReloadReplica(args) {
|
|
21525
|
+
const { controller, oldReplicaIndex, newService, sourceDirToCopy } = args;
|
|
21526
|
+
const logger = getLogger().child("ecs-service");
|
|
21527
|
+
const instance = controller.runState.replicas[oldReplicaIndex];
|
|
21528
|
+
if (!instance) throw new EcsServiceRunnerError(`softReloadReplica: no replica at index ${oldReplicaIndex} (replicas=${controller.runState.replicas.length}).`);
|
|
21529
|
+
if (instance.shuttingDown) {
|
|
21530
|
+
logger.warn(`Soft-reload of replica r${instance.index} (gen ${instance.generation}): retired by its own watcher mid-reload. Skipping; save again to re-boot it from scratch.`);
|
|
21531
|
+
return;
|
|
21532
|
+
}
|
|
21533
|
+
const essentialContainers = newService.task.containers.filter((c) => c.essential);
|
|
21534
|
+
const containersToCycle = essentialContainers.length > 0 ? essentialContainers : newService.task.containers.length > 0 ? [newService.task.containers[0]] : [];
|
|
21535
|
+
if (containersToCycle.length === 0) throw new EcsServiceRunnerError(`softReloadReplica: service '${newService.serviceLogicalId}' has no containers to cycle.`);
|
|
21536
|
+
const startedById = new Map(instance.state.startedContainers.map((c) => [c.name, c.id]));
|
|
21537
|
+
const targets = [];
|
|
21538
|
+
for (const container of containersToCycle) {
|
|
21539
|
+
const id = startedById.get(container.name);
|
|
21540
|
+
if (!id) throw new EcsServiceRunnerError(`softReloadReplica: replica r${instance.index} has no started container named '${container.name}' (started: ${[...startedById.keys()].join(", ") || "none"}).`);
|
|
21541
|
+
targets.push({
|
|
21542
|
+
name: container.name,
|
|
21543
|
+
id
|
|
21544
|
+
});
|
|
21545
|
+
}
|
|
21546
|
+
const controllerOptions = controller.options;
|
|
21547
|
+
if (controllerOptions.discovery) {
|
|
21548
|
+
for (const handle of instance.cloudMapHandles) try {
|
|
21549
|
+
controllerOptions.discovery.registry.unregister(handle);
|
|
21550
|
+
} catch {}
|
|
21551
|
+
instance.cloudMapHandles = [];
|
|
21552
|
+
}
|
|
21553
|
+
unregisterReplicaFromFrontDoor(instance, controllerOptions.frontDoor);
|
|
21554
|
+
instance.softReloadInProgress = true;
|
|
21555
|
+
instance.softReloadGeneration = (instance.softReloadGeneration ?? 0) + 1;
|
|
21556
|
+
try {
|
|
21557
|
+
logger.info(`Soft-reloading replica r${instance.index} (gen ${instance.generation}): docker cp ${sourceDirToCopy} -> ${targets.length} essential container(s); restart.`);
|
|
21558
|
+
for (const target of targets) {
|
|
21559
|
+
let workdir;
|
|
21560
|
+
try {
|
|
21561
|
+
workdir = await dockerInspectWorkdirImpl(target.id) || "/";
|
|
21562
|
+
} catch (err) {
|
|
21563
|
+
throw new EcsServiceRunnerError(`softReloadReplica: docker inspect of container '${target.name}' (${target.id}) failed: ${err instanceof Error ? err.message : String(err)}. This replica is unregistered from Cloud Map + the front-door pool until the next save triggers another reload.`);
|
|
21564
|
+
}
|
|
21565
|
+
const workdirDest = workdir.endsWith("/") ? workdir : `${workdir}/`;
|
|
21566
|
+
try {
|
|
21567
|
+
await dockerCpImpl(`${sourceDirToCopy}/.`, `${target.id}:${workdirDest}`);
|
|
21568
|
+
} catch (err) {
|
|
21569
|
+
throw new EcsServiceRunnerError(`softReloadReplica: docker cp into '${target.name}' (${target.id}:${workdir}) failed: ${err instanceof Error ? err.message : String(err)}. This replica is unregistered from Cloud Map + the front-door pool until the next save triggers another reload.`);
|
|
21570
|
+
}
|
|
21571
|
+
try {
|
|
21572
|
+
await dockerRestartImpl(target.id);
|
|
21573
|
+
} catch (err) {
|
|
21574
|
+
throw new EcsServiceRunnerError(`softReloadReplica: docker restart of '${target.name}' (${target.id}) failed: ${err instanceof Error ? err.message : String(err)}. This replica is unregistered from Cloud Map + the front-door pool until the next save triggers another reload.`);
|
|
21575
|
+
}
|
|
21576
|
+
}
|
|
21577
|
+
await waitForReplicaTcpReady(newService, instance, {
|
|
21578
|
+
timeoutMs: shadowReadyTimeoutMs,
|
|
21579
|
+
intervalMs: shadowReadyIntervalMs,
|
|
21580
|
+
label: `Soft-reloaded replica r${instance.index} (gen ${instance.generation})`
|
|
21581
|
+
});
|
|
21582
|
+
const ownerKeyGenSuffix = instance.generation > 0 ? `:g${instance.generation}` : "";
|
|
21583
|
+
const ownerKeyPrefix = `${newService.serviceLogicalId}:r${instance.index}${ownerKeyGenSuffix}`;
|
|
21584
|
+
if (controllerOptions.discovery) await publishReplicaToCloudMap(newService, instance, controllerOptions.discovery, ownerKeyPrefix);
|
|
21585
|
+
if (controllerOptions.frontDoor) await publishReplicaToFrontDoor(newService, instance, controllerOptions.frontDoor, controllerOptions.taskOptions.containerHost, ownerKeyPrefix);
|
|
21586
|
+
instance.lastDeployedAssetHash = pickEssentialAssetHash(newService);
|
|
21587
|
+
logger.info(`Soft-reloaded replica r${instance.index} (gen ${instance.generation}): ${SOFT_RELOAD_COMPLETION_LOG_SUFFIX}`);
|
|
21588
|
+
} finally {
|
|
21589
|
+
instance.softReloadInProgress = false;
|
|
21590
|
+
}
|
|
21591
|
+
}
|
|
21592
|
+
/**
|
|
21593
|
+
* Production `docker inspect --format {{.Config.WorkingDir}} <id>`
|
|
21594
|
+
* impl. Returns the container's runtime WORKDIR; empty string when
|
|
21595
|
+
* the Dockerfile didn't set one (caller treats empty as `/`, matching
|
|
21596
|
+
* Docker's runtime default).
|
|
21597
|
+
*
|
|
21598
|
+
* Extracted as a test-overridable function so the soft-reload
|
|
21599
|
+
* primitive's unit tests can assert the WORKDIR resolution branch
|
|
21600
|
+
* without standing up a real container.
|
|
21601
|
+
*/
|
|
21602
|
+
const defaultDockerInspectWorkdirImpl = async (containerId) => {
|
|
21603
|
+
const { execFile } = await import("node:child_process");
|
|
21604
|
+
const { promisify } = await import("node:util");
|
|
21605
|
+
const { getDockerCmd } = await import("./docker-cmd-voNPrcRh.js").then((n) => n.t);
|
|
21606
|
+
const { stdout } = await promisify(execFile)(getDockerCmd(), [
|
|
21607
|
+
"inspect",
|
|
21608
|
+
"--format",
|
|
21609
|
+
"{{.Config.WorkingDir}}",
|
|
21610
|
+
containerId
|
|
21611
|
+
]);
|
|
21612
|
+
return stdout.trim();
|
|
21613
|
+
};
|
|
21614
|
+
let dockerInspectWorkdirImpl = defaultDockerInspectWorkdirImpl;
|
|
21615
|
+
/**
|
|
21616
|
+
* Production `docker cp <src> <containerId>:<dst>` impl. The source
|
|
21617
|
+
* path's trailing `/.` ensures CONTENTS of the directory are copied
|
|
21618
|
+
* (not the directory itself); the caller is responsible for that
|
|
21619
|
+
* convention.
|
|
21620
|
+
*/
|
|
21621
|
+
const defaultDockerCpImpl = async (src, dst) => {
|
|
21622
|
+
const { execFile } = await import("node:child_process");
|
|
21623
|
+
const { promisify } = await import("node:util");
|
|
21624
|
+
const { getDockerCmd } = await import("./docker-cmd-voNPrcRh.js").then((n) => n.t);
|
|
21625
|
+
await promisify(execFile)(getDockerCmd(), [
|
|
21626
|
+
"cp",
|
|
21627
|
+
src,
|
|
21628
|
+
dst
|
|
21629
|
+
], { maxBuffer: 64 * 1024 * 1024 });
|
|
21630
|
+
};
|
|
21631
|
+
let dockerCpImpl = defaultDockerCpImpl;
|
|
21632
|
+
/**
|
|
21633
|
+
* Production `docker restart <id>` impl. Synchronous in docker's
|
|
21634
|
+
* sense — blocks until the container is up again (or fails).
|
|
21635
|
+
*/
|
|
21636
|
+
const defaultDockerRestartImpl = async (containerId) => {
|
|
21637
|
+
const { execFile } = await import("node:child_process");
|
|
21638
|
+
const { promisify } = await import("node:util");
|
|
21639
|
+
const { getDockerCmd } = await import("./docker-cmd-voNPrcRh.js").then((n) => n.t);
|
|
21640
|
+
await promisify(execFile)(getDockerCmd(), ["restart", containerId]);
|
|
21641
|
+
};
|
|
21642
|
+
let dockerRestartImpl = defaultDockerRestartImpl;
|
|
21643
|
+
/**
|
|
21407
21644
|
* Phase 2 of issue #214 — disconnect every container of the dying
|
|
21408
21645
|
* replica from the shared service network BEFORE `cleanupEcsRun`'s
|
|
21409
21646
|
* `docker stop → docker rm` sequence. Docker's embedded DNS strips an
|
|
@@ -21470,13 +21707,14 @@ let dockerNetworkDisconnectImpl = defaultDockerNetworkDisconnectImpl;
|
|
|
21470
21707
|
* injectable via {@link __setTcpProbeImpl} so the rolling-primitive
|
|
21471
21708
|
* unit test can avoid any real TCP socket.
|
|
21472
21709
|
*/
|
|
21473
|
-
async function waitForReplicaTcpReady(service,
|
|
21710
|
+
async function waitForReplicaTcpReady(service, replica, opts) {
|
|
21474
21711
|
const logger = getLogger().child("ecs-service");
|
|
21475
|
-
const
|
|
21712
|
+
const label = opts.label ?? `Shadow replica r${replica.index} (gen ${replica.generation})`;
|
|
21713
|
+
const networkName = replica.state.network?.networkName;
|
|
21476
21714
|
if (!networkName) return;
|
|
21477
21715
|
const essential = service.task.containers.find((c) => c.essential) ?? service.task.containers[0];
|
|
21478
21716
|
if (!essential || essential.portMappings.length === 0) return;
|
|
21479
|
-
const started =
|
|
21717
|
+
const started = replica.state.startedContainers.find((c) => c.name === essential.name);
|
|
21480
21718
|
if (!started) return;
|
|
21481
21719
|
let ip;
|
|
21482
21720
|
try {
|
|
@@ -21484,7 +21722,7 @@ async function waitForReplicaTcpReady(service, shadow, opts) {
|
|
|
21484
21722
|
if (!resolved) return;
|
|
21485
21723
|
ip = resolved;
|
|
21486
21724
|
} catch (err) {
|
|
21487
|
-
logger.warn(
|
|
21725
|
+
logger.warn(`${label}: TCP-ready probe could not resolve docker IP: ${err instanceof Error ? err.message : String(err)}. Proceeding.`);
|
|
21488
21726
|
return;
|
|
21489
21727
|
}
|
|
21490
21728
|
const port = essential.portMappings[0].containerPort;
|
|
@@ -21493,14 +21731,14 @@ async function waitForReplicaTcpReady(service, shadow, opts) {
|
|
|
21493
21731
|
while (Date.now() < deadline) {
|
|
21494
21732
|
try {
|
|
21495
21733
|
await tcpProbeImpl(ip, port);
|
|
21496
|
-
logger.debug(
|
|
21734
|
+
logger.debug(`${label}: TCP probe ${ip}:${port} accepted.`);
|
|
21497
21735
|
return;
|
|
21498
21736
|
} catch (err) {
|
|
21499
21737
|
lastErr = err instanceof Error ? err.message : String(err);
|
|
21500
21738
|
}
|
|
21501
21739
|
await sleep(opts.intervalMs);
|
|
21502
21740
|
}
|
|
21503
|
-
logger.warn(
|
|
21741
|
+
logger.warn(`${label}: TCP probe ${ip}:${port} did not accept within ${opts.timeoutMs}ms (last: ${lastErr ?? "n/a"}). Proceeding anyway — the new source is the user intent. Initial requests may 502 until the app finishes binding.`);
|
|
21504
21742
|
}
|
|
21505
21743
|
/**
|
|
21506
21744
|
* Default TCP-connect probe used by {@link waitForReplicaTcpReady}.
|
|
@@ -21541,6 +21779,7 @@ async function watchReplica(service, options, instance, runState) {
|
|
|
21541
21779
|
await sleep(500);
|
|
21542
21780
|
continue;
|
|
21543
21781
|
}
|
|
21782
|
+
const softReloadGenBeforeWait = instance.softReloadGeneration ?? 0;
|
|
21544
21783
|
let exitCode;
|
|
21545
21784
|
try {
|
|
21546
21785
|
exitCode = await waitForExitImpl(essentialId);
|
|
@@ -21549,6 +21788,12 @@ async function watchReplica(service, options, instance, runState) {
|
|
|
21549
21788
|
exitCode = -1;
|
|
21550
21789
|
}
|
|
21551
21790
|
if (instance.shuttingDown || runState.shuttingDown) return;
|
|
21791
|
+
const softReloadHappenedMidWait = (instance.softReloadGeneration ?? 0) !== softReloadGenBeforeWait;
|
|
21792
|
+
if (instance.softReloadInProgress || softReloadHappenedMidWait) {
|
|
21793
|
+
while (instance.softReloadInProgress && !instance.shuttingDown && !runState.shuttingDown) await sleep(100);
|
|
21794
|
+
if (instance.shuttingDown || runState.shuttingDown) return;
|
|
21795
|
+
continue;
|
|
21796
|
+
}
|
|
21552
21797
|
logger.warn(`Replica ${instance.index} essential container exited with code ${exitCode} (restartCount=${instance.restartCount}).`);
|
|
21553
21798
|
const willRestart = shouldRestart(exitCode, options.restartPolicy);
|
|
21554
21799
|
if (!willRestart || instance.restartCount === 0) await printExitedContainerLogs(instance.index, essentialId, logger);
|
|
@@ -21664,6 +21909,152 @@ function sleep(ms) {
|
|
|
21664
21909
|
return sleepImpl(ms);
|
|
21665
21910
|
}
|
|
21666
21911
|
|
|
21912
|
+
//#endregion
|
|
21913
|
+
//#region src/local/source-change-classifier.ts
|
|
21914
|
+
/**
|
|
21915
|
+
* Dependency-manifest basenames recognized by the classifier. A change
|
|
21916
|
+
* to any of these forces a rebuild because the running container's
|
|
21917
|
+
* pre-built dependency layer is no longer in sync with the source.
|
|
21918
|
+
*
|
|
21919
|
+
* Coverage: the package managers commonly used inside a Lambda /
|
|
21920
|
+
* container image — Node (pnpm / npm / yarn), Python (pip / poetry /
|
|
21921
|
+
* pipenv), Ruby (bundler), Go (modules), Rust (cargo), Java / Kotlin
|
|
21922
|
+
* (Maven, Gradle). Adding a new ecosystem? Append its lockfile +
|
|
21923
|
+
* manifest here and add a classifier test row.
|
|
21924
|
+
*/
|
|
21925
|
+
const REBUILD_TRIGGER_BASENAMES = new Set([
|
|
21926
|
+
"package.json",
|
|
21927
|
+
"package-lock.json",
|
|
21928
|
+
"pnpm-lock.yaml",
|
|
21929
|
+
"yarn.lock",
|
|
21930
|
+
"npm-shrinkwrap.json",
|
|
21931
|
+
"requirements.txt",
|
|
21932
|
+
"requirements-dev.txt",
|
|
21933
|
+
"pyproject.toml",
|
|
21934
|
+
"poetry.lock",
|
|
21935
|
+
"Pipfile",
|
|
21936
|
+
"Pipfile.lock",
|
|
21937
|
+
"uv.lock",
|
|
21938
|
+
"Gemfile",
|
|
21939
|
+
"Gemfile.lock",
|
|
21940
|
+
"go.mod",
|
|
21941
|
+
"go.sum",
|
|
21942
|
+
"Cargo.toml",
|
|
21943
|
+
"Cargo.lock",
|
|
21944
|
+
"pom.xml",
|
|
21945
|
+
"build.gradle",
|
|
21946
|
+
"build.gradle.kts",
|
|
21947
|
+
"settings.gradle",
|
|
21948
|
+
"settings.gradle.kts",
|
|
21949
|
+
"Makefile",
|
|
21950
|
+
"CMakeLists.txt"
|
|
21951
|
+
]);
|
|
21952
|
+
/**
|
|
21953
|
+
* Compiled-language source extensions that require a build step
|
|
21954
|
+
* inside `docker build` (typically a `RUN go build` / `cargo build` /
|
|
21955
|
+
* `mvn package` etc.). A copy of the source alone would leave the
|
|
21956
|
+
* running binary stale, so the user's intent must be a rebuild.
|
|
21957
|
+
*
|
|
21958
|
+
* Interpreted-language runtimes (Node — `.js` / `.mjs` / `.cjs` /
|
|
21959
|
+
* `.ts` when transpiled at runtime, Python — `.py`, Ruby — `.rb`,
|
|
21960
|
+
* shell — `.sh`) read source at process start, so a `docker cp` +
|
|
21961
|
+
* `docker restart` cycle picks them up. Those extensions are
|
|
21962
|
+
* NOT in this set.
|
|
21963
|
+
*/
|
|
21964
|
+
const COMPILED_LANGUAGE_EXTENSIONS = new Set([
|
|
21965
|
+
".go",
|
|
21966
|
+
".rs",
|
|
21967
|
+
".java",
|
|
21968
|
+
".kt",
|
|
21969
|
+
".kts",
|
|
21970
|
+
".scala",
|
|
21971
|
+
".cs",
|
|
21972
|
+
".swift",
|
|
21973
|
+
".fs",
|
|
21974
|
+
".fsx",
|
|
21975
|
+
".c",
|
|
21976
|
+
".cc",
|
|
21977
|
+
".cpp",
|
|
21978
|
+
".cxx",
|
|
21979
|
+
".h",
|
|
21980
|
+
".hpp",
|
|
21981
|
+
".zig",
|
|
21982
|
+
".ml",
|
|
21983
|
+
".mli",
|
|
21984
|
+
".elm",
|
|
21985
|
+
".hs",
|
|
21986
|
+
".dart"
|
|
21987
|
+
]);
|
|
21988
|
+
/**
|
|
21989
|
+
* Classify a single watcher firing into rebuild vs soft-reload. Pure
|
|
21990
|
+
* + synchronous. The caller (emulator's reload pathway) invokes this
|
|
21991
|
+
* once per target per firing AFTER `cdk synth` has run and the new
|
|
21992
|
+
* asset manifest is on disk.
|
|
21993
|
+
*
|
|
21994
|
+
* Branching:
|
|
21995
|
+
* 1. No asset context (image isn't a CDK asset, or asset lookup
|
|
21996
|
+
* failed) → `rebuild`.
|
|
21997
|
+
* 2. The asset hash didn't change between old and new synths
|
|
21998
|
+
* (`oldAssetHash === newAssetHash`, or `oldAssetHash` missing)
|
|
21999
|
+
* → `rebuild`. Load-bearing guard for "user edited a CDK
|
|
22000
|
+
* construct file (e.g. `lib/stack.ts`) that flipped the task
|
|
22001
|
+
* spec but didn't touch the asset content". Soft-reload would
|
|
22002
|
+
* `docker cp` identical files and `docker restart` the
|
|
22003
|
+
* container with the OLD task spec (env / memory / mounts /
|
|
22004
|
+
* added sidecars are set at `docker create` time, not on
|
|
22005
|
+
* restart) — the user's intent would silently NOT apply.
|
|
22006
|
+
* Forcing rebuild keeps Phase 1-3 semantics exactly for this
|
|
22007
|
+
* case: the rolling primitive boots a shadow with the new task
|
|
22008
|
+
* spec, the user sees their construct edit take effect.
|
|
22009
|
+
* 3. No changed paths (the watcher fired on a debounce flush with
|
|
22010
|
+
* an empty pending set — shouldn't happen in practice, but
|
|
22011
|
+
* defensive) → `rebuild`.
|
|
22012
|
+
* 4. Any changed path's basename matches the Dockerfile or a
|
|
22013
|
+
* dependency manifest → `rebuild`.
|
|
22014
|
+
* 5. Any changed path's extension is a compiled-language source →
|
|
22015
|
+
* `rebuild`.
|
|
22016
|
+
* 6. Else → `soft-reload`.
|
|
22017
|
+
*/
|
|
22018
|
+
function classifySourceChange(changedPaths, ctx) {
|
|
22019
|
+
if (!ctx) return {
|
|
22020
|
+
kind: "rebuild",
|
|
22021
|
+
reason: "target image is not a CDK docker-image asset"
|
|
22022
|
+
};
|
|
22023
|
+
if (!ctx.oldAssetHash || ctx.oldAssetHash === ctx.newAssetHash) return {
|
|
22024
|
+
kind: "rebuild",
|
|
22025
|
+
reason: "asset hash unchanged across the synth (CDK construct edit or unrelated file) — task-spec changes need a fresh `docker create`, which only the rebuild path runs"
|
|
22026
|
+
};
|
|
22027
|
+
if (changedPaths.length === 0) return {
|
|
22028
|
+
kind: "rebuild",
|
|
22029
|
+
reason: "no changed paths reported (defensive default)"
|
|
22030
|
+
};
|
|
22031
|
+
for (const p of changedPaths) {
|
|
22032
|
+
const basename = path.basename(p);
|
|
22033
|
+
if (basename === ctx.dockerFile) return {
|
|
22034
|
+
kind: "rebuild",
|
|
22035
|
+
reason: `Dockerfile edit (${basename})`
|
|
22036
|
+
};
|
|
22037
|
+
if (basename.startsWith("Dockerfile.")) return {
|
|
22038
|
+
kind: "rebuild",
|
|
22039
|
+
reason: `Dockerfile.* edit (${basename})`
|
|
22040
|
+
};
|
|
22041
|
+
if (REBUILD_TRIGGER_BASENAMES.has(basename)) return {
|
|
22042
|
+
kind: "rebuild",
|
|
22043
|
+
reason: `dependency manifest edit (${basename})`
|
|
22044
|
+
};
|
|
22045
|
+
const ext = path.extname(p).toLowerCase();
|
|
22046
|
+
if (COMPILED_LANGUAGE_EXTENSIONS.has(ext)) return {
|
|
22047
|
+
kind: "rebuild",
|
|
22048
|
+
reason: `compiled-language source edit (${basename}) — soft-reload would leave the built binary stale`
|
|
22049
|
+
};
|
|
22050
|
+
}
|
|
22051
|
+
return {
|
|
22052
|
+
kind: "soft-reload",
|
|
22053
|
+
reason: `${changedPaths.length} source-only path(s) — skipping rebuild`,
|
|
22054
|
+
newAssetSourceDir: ctx.newAssetSourceDir
|
|
22055
|
+
};
|
|
22056
|
+
}
|
|
22057
|
+
|
|
21667
22058
|
//#endregion
|
|
21668
22059
|
//#region src/local/cloud-map-registry.ts
|
|
21669
22060
|
/**
|
|
@@ -22379,9 +22770,51 @@ function escapeRealmQuotes(realm) {
|
|
|
22379
22770
|
}
|
|
22380
22771
|
/** Reply 404 — an ALB listener with no matching rule and no default action. */
|
|
22381
22772
|
function reply404(req, res, opts) {
|
|
22382
|
-
writeError(res, 404,
|
|
22773
|
+
writeError(res, 404, buildNoRuleMatched404Body(req, opts));
|
|
22383
22774
|
return Promise.resolve();
|
|
22384
22775
|
}
|
|
22776
|
+
/**
|
|
22777
|
+
* Build the no-rule-matched 404 body. When the call site supplied a
|
|
22778
|
+
* {@link StartFrontDoorServerOptions.rulesSummary}, the body lists every
|
|
22779
|
+
* ALB condition field that WAS evaluated (method, host, path) plus every
|
|
22780
|
+
* configured rule's priority + conditions + action target — so a user
|
|
22781
|
+
* whose request missed on, say, the Host header can spot the mismatch
|
|
22782
|
+
* without inspecting the synthesized template. Header conditions are
|
|
22783
|
+
* NOT spelled out in the evaluated section (too noisy) but ARE listed
|
|
22784
|
+
* in each rule's condition row when the rule constrains them. Without a
|
|
22785
|
+
* summary the body falls back to the original path-only shape (preserves
|
|
22786
|
+
* the behavior for direct callers that wire the proxy with just a
|
|
22787
|
+
* `selectPool` / `selectTarget`).
|
|
22788
|
+
*/
|
|
22789
|
+
function buildNoRuleMatched404Body(req, opts) {
|
|
22790
|
+
const requestPath = req.url ?? "/";
|
|
22791
|
+
const summary = opts.rulesSummary;
|
|
22792
|
+
if (!summary) return `No listener rule matched '${requestPath}' on ${opts.label}, and the listener has no default action forwarding to a local target.`;
|
|
22793
|
+
const rawHost = req.headers.host;
|
|
22794
|
+
const hostValue = Array.isArray(rawHost) ? rawHost[0] : rawHost;
|
|
22795
|
+
const lines = [];
|
|
22796
|
+
lines.push(`No listener rule matched the request on ${opts.label}, and the listener has no default action forwarding to a local target.`);
|
|
22797
|
+
lines.push("");
|
|
22798
|
+
lines.push(" Evaluated:");
|
|
22799
|
+
lines.push(` Method: ${req.method ?? "(unknown)"}`);
|
|
22800
|
+
lines.push(` Host: ${hostValue ?? "(no Host header)"}`);
|
|
22801
|
+
lines.push(` Path: ${requestPath}`);
|
|
22802
|
+
lines.push("");
|
|
22803
|
+
if (summary.length === 0) lines.push(" Listener has 0 rule(s).");
|
|
22804
|
+
else {
|
|
22805
|
+
lines.push(` Listener has ${summary.length} rule(s):`);
|
|
22806
|
+
const ordered = [...summary].sort((a, b) => a.priority - b.priority);
|
|
22807
|
+
for (const rule of ordered) {
|
|
22808
|
+
const conditions = rule.conditions.length === 0 ? "(no condition)" : rule.conditions.map(formatRuleConditionSummary).join(" AND ");
|
|
22809
|
+
lines.push(` [priority=${rule.priority}] ${conditions} -> ${rule.action}`);
|
|
22810
|
+
}
|
|
22811
|
+
}
|
|
22812
|
+
return lines.join("\n");
|
|
22813
|
+
}
|
|
22814
|
+
/** Format one condition row of a {@link FrontDoorRuleSummary} for the 404 body. */
|
|
22815
|
+
function formatRuleConditionSummary(c) {
|
|
22816
|
+
return `${c.field} in [${c.values.join(", ")}]`;
|
|
22817
|
+
}
|
|
22385
22818
|
function handlePoolRequest(req, res, pool, opts) {
|
|
22386
22819
|
return new Promise((resolve) => {
|
|
22387
22820
|
const endpoint = pool.next();
|
|
@@ -23319,9 +23752,9 @@ function materializeInlineCode(handler, source, fileExtension) {
|
|
|
23319
23752
|
const lastDot = handler.lastIndexOf(".");
|
|
23320
23753
|
if (lastDot <= 0) throw new Error(`Handler '${handler}' is malformed: expected '<modulePath>.<exportName>'.`);
|
|
23321
23754
|
const modulePath = handler.substring(0, lastDot);
|
|
23322
|
-
const dir = mkdtempSync(path.join(tmpdir(), `${getEmbedConfig().resourceNamePrefix}-alb-lambda-`));
|
|
23323
|
-
const filePath = path.join(dir, `${modulePath}${fileExtension}`);
|
|
23324
|
-
mkdirSync(path.dirname(filePath), { recursive: true });
|
|
23755
|
+
const dir = mkdtempSync(path$1.join(tmpdir(), `${getEmbedConfig().resourceNamePrefix}-alb-lambda-`));
|
|
23756
|
+
const filePath = path$1.join(dir, `${modulePath}${fileExtension}`);
|
|
23757
|
+
mkdirSync(path$1.dirname(filePath), { recursive: true });
|
|
23325
23758
|
writeFileSync(filePath, source, "utf-8");
|
|
23326
23759
|
return dir;
|
|
23327
23760
|
}
|
|
@@ -23353,7 +23786,7 @@ async function resolveContainerImagePlan(lambda, opts) {
|
|
|
23353
23786
|
let imageRef;
|
|
23354
23787
|
let localBuilt = false;
|
|
23355
23788
|
if (manifestPath) {
|
|
23356
|
-
const cdkOutDir = path.dirname(manifestPath);
|
|
23789
|
+
const cdkOutDir = path$1.dirname(manifestPath);
|
|
23357
23790
|
const manifest = await new AssetManifestLoader().loadManifest(cdkOutDir, lambda.stack.stackName);
|
|
23358
23791
|
const entry = manifest ? getDockerImageBySourceHash(manifest, lambda.imageUri) : void 0;
|
|
23359
23792
|
if (entry) {
|
|
@@ -23719,8 +24152,8 @@ async function runEcsServiceEmulator(targets, options, strategy, extraStateProvi
|
|
|
23719
24152
|
paths: [watchRoot],
|
|
23720
24153
|
ignored,
|
|
23721
24154
|
shouldTrigger,
|
|
23722
|
-
onChange: () => {
|
|
23723
|
-
logger.info(
|
|
24155
|
+
onChange: (changedPaths) => {
|
|
24156
|
+
logger.info(`Detected source change (${changedPaths.length} path(s)); reloading service(s)...`);
|
|
23724
24157
|
reloadChain = reloadChain.then(() => reloadAllServices({
|
|
23725
24158
|
perTarget,
|
|
23726
24159
|
synthesizer,
|
|
@@ -23734,6 +24167,7 @@ async function runEcsServiceEmulator(targets, options, strategy, extraStateProvi
|
|
|
23734
24167
|
extraStateProviders,
|
|
23735
24168
|
profileCredsFile,
|
|
23736
24169
|
frontDoorByService,
|
|
24170
|
+
changedPaths,
|
|
23737
24171
|
logger
|
|
23738
24172
|
})).catch((err) => {
|
|
23739
24173
|
logger.error(`reloadAllServices threw: ${err instanceof Error ? err.message : String(err)}`);
|
|
@@ -23752,11 +24186,22 @@ async function runEcsServiceEmulator(targets, options, strategy, extraStateProvi
|
|
|
23752
24186
|
}
|
|
23753
24187
|
}
|
|
23754
24188
|
/**
|
|
23755
|
-
* Phase 2 of issue #214 — multi-replica
|
|
23756
|
-
* `cdkl start-service --watch
|
|
23757
|
-
*
|
|
23758
|
-
*
|
|
23759
|
-
*
|
|
24189
|
+
* Phase 2 + Phase 4 of issue #214 — multi-replica reload cycle for
|
|
24190
|
+
* `cdkl start-service --watch` (Phase 2) and `cdkl start-alb --watch`
|
|
24191
|
+
* (Phase 3 wires the same loop). Mirrors start-api's `reloadAllServers`
|
|
24192
|
+
* shape but per-ECS-service. Per-target verdict from
|
|
24193
|
+
* {@link classifySourceChange} (Phase 4) picks the per-replica action:
|
|
24194
|
+
*
|
|
24195
|
+
* - `'soft-reload'` → {@link softReloadReplica} runs `docker cp`
|
|
24196
|
+
* + `docker restart` against the live replica. No `docker build`,
|
|
24197
|
+
* no shadow boot, no Cloud Map / front-door pool swap — the
|
|
24198
|
+
* container's IP + host port are preserved across the restart, so
|
|
24199
|
+
* existing registrations stay valid. Fast path (~sub-second per
|
|
24200
|
+
* replica for typical interpreted-language handlers).
|
|
24201
|
+
* - `'rebuild'` → {@link rollServiceReplica}, the Phase 1-3 path,
|
|
24202
|
+
* replacing Phase 1's "tear single replica down, boot fresh"
|
|
24203
|
+
* sequence with a per-replica rolling loop so the service stays
|
|
24204
|
+
* available end-to-end:
|
|
23760
24205
|
*
|
|
23761
24206
|
* 1. Re-runs `synthesizer.synthesize(synthOpts)` once (failure → warn
|
|
23762
24207
|
* + keep every replica serving).
|
|
@@ -23791,7 +24236,7 @@ async function runEcsServiceEmulator(targets, options, strategy, extraStateProvi
|
|
|
23791
24236
|
* via the logger so the user can fix the source + save again.
|
|
23792
24237
|
*/
|
|
23793
24238
|
async function reloadAllServices(args) {
|
|
23794
|
-
const { perTarget, synthesizer, synthOpts, strategy, resolvedTargets, cloudMapIndexByStack, options, discovery, skipPull, extraStateProviders, profileCredsFile, frontDoorByService, logger } = args;
|
|
24239
|
+
const { perTarget, synthesizer, synthOpts, strategy, resolvedTargets, cloudMapIndexByStack, options, discovery, skipPull, extraStateProviders, profileCredsFile, frontDoorByService, changedPaths, logger } = args;
|
|
23795
24240
|
let stacks;
|
|
23796
24241
|
try {
|
|
23797
24242
|
({stacks} = await synthesizer.synthesize(synthOpts));
|
|
@@ -23808,6 +24253,8 @@ async function reloadAllServices(args) {
|
|
|
23808
24253
|
cloudMapIndexByStack.set(stack.stackName, index);
|
|
23809
24254
|
for (const w of index.warnings) logger.warn(w);
|
|
23810
24255
|
}
|
|
24256
|
+
const cdkOutDir = options.output;
|
|
24257
|
+
const assetLoader = new AssetManifestLoader();
|
|
23811
24258
|
for (const pt of perTarget) {
|
|
23812
24259
|
const newBoot = newBootByTarget.get(pt.boot.target);
|
|
23813
24260
|
if (!newBoot) {
|
|
@@ -23819,6 +24266,27 @@ async function reloadAllServices(args) {
|
|
|
23819
24266
|
logger.warn(`Reload: target '${pt.boot.target}' has no live controller (previous boot likely failed); skipping roll. \`^C\` and re-run start-service to recover.`);
|
|
23820
24267
|
continue;
|
|
23821
24268
|
}
|
|
24269
|
+
let verdict = {
|
|
24270
|
+
kind: "rebuild",
|
|
24271
|
+
reason: "classifier not consulted"
|
|
24272
|
+
};
|
|
24273
|
+
try {
|
|
24274
|
+
verdict = classifySourceChange(changedPaths, await loadAssetContextForTarget({
|
|
24275
|
+
target: newBoot.target,
|
|
24276
|
+
controller,
|
|
24277
|
+
stacks,
|
|
24278
|
+
cdkOutDir,
|
|
24279
|
+
assetLoader,
|
|
24280
|
+
logger
|
|
24281
|
+
}));
|
|
24282
|
+
logger.info(`Reload of '${newBoot.target}': verdict=${verdict.kind} (${verdict.reason}).`);
|
|
24283
|
+
} catch (err) {
|
|
24284
|
+
logger.warn(`Reload of '${newBoot.target}': classifier context unavailable (${err instanceof Error ? err.message : String(err)}); falling back to rebuild.`);
|
|
24285
|
+
verdict = {
|
|
24286
|
+
kind: "rebuild",
|
|
24287
|
+
reason: "classifier context unavailable; falling back to rebuild"
|
|
24288
|
+
};
|
|
24289
|
+
}
|
|
23822
24290
|
await rollOneTarget({
|
|
23823
24291
|
controller,
|
|
23824
24292
|
newBoot,
|
|
@@ -23830,12 +24298,73 @@ async function reloadAllServices(args) {
|
|
|
23830
24298
|
profileCredsFile,
|
|
23831
24299
|
frontDoorPools: frontDoorByService.get(newBoot.target),
|
|
23832
24300
|
suppressLoadBalancerWarning: strategy.suppressLoadBalancerWarning === true,
|
|
24301
|
+
verdict,
|
|
23833
24302
|
logger
|
|
23834
24303
|
});
|
|
23835
24304
|
}
|
|
23836
24305
|
logger.info("Reload complete.");
|
|
23837
24306
|
}
|
|
23838
24307
|
/**
|
|
24308
|
+
* Phase 4 of issue #214 — load the per-target asset context the
|
|
24309
|
+
* source-change classifier consumes. Resolves the target's docker-image
|
|
24310
|
+
* asset hash via the freshly-synthed `<stackName>.assets.json` and
|
|
24311
|
+
* derives the staged source directory + Dockerfile basename.
|
|
24312
|
+
*
|
|
24313
|
+
* Returns `undefined` (and logs at debug) when:
|
|
24314
|
+
* - The target's image isn't a CDK docker-image asset (ECR / public
|
|
24315
|
+
* registry pin). The classifier treats `undefined` as `rebuild`
|
|
24316
|
+
* because there's no local source tree to copy.
|
|
24317
|
+
* - The asset manifest can't be loaded (stack not synthed yet, file
|
|
24318
|
+
* missing). Same treatment — defensive default to `rebuild`.
|
|
24319
|
+
* - The asset hash isn't in the manifest's `dockerImages`. Same.
|
|
24320
|
+
*
|
|
24321
|
+
* Throws on a malformed manifest (parse failure surfaced via
|
|
24322
|
+
* {@link AssetManifestLoader}) so the caller can fall back to rebuild
|
|
24323
|
+
* with a warn line that explains why the classifier couldn't run.
|
|
24324
|
+
*/
|
|
24325
|
+
/**
|
|
24326
|
+
* @internal — exported for unit tests of the fall-through branches
|
|
24327
|
+
* (the 6 `return undefined` paths + the catch arm on
|
|
24328
|
+
* `resolveEcsServiceTarget` throw). Not part of the semver-covered
|
|
24329
|
+
* public surface; the only legitimate caller is `reloadAllServices`
|
|
24330
|
+
* inside this file.
|
|
24331
|
+
*/
|
|
24332
|
+
async function loadAssetContextForTarget(args) {
|
|
24333
|
+
const { target, controller, stacks, cdkOutDir, assetLoader, logger } = args;
|
|
24334
|
+
const candidate = pickCandidateStack(parseEcsTarget(target).stackPattern, stacks);
|
|
24335
|
+
if (!candidate) return void 0;
|
|
24336
|
+
let newService;
|
|
24337
|
+
try {
|
|
24338
|
+
newService = resolveEcsServiceTarget(target, stacks, void 0, { suppressLoadBalancerWarning: true });
|
|
24339
|
+
} catch (err) {
|
|
24340
|
+
logger.debug(`loadAssetContextForTarget: target '${target}' could not be re-resolved against the new stacks: ${err instanceof Error ? err.message : String(err)}. Classifier will see no asset context (rebuild).`);
|
|
24341
|
+
return;
|
|
24342
|
+
}
|
|
24343
|
+
const essential = newService.task.containers.find((c) => c.essential) ?? newService.task.containers[0];
|
|
24344
|
+
if (!essential) return void 0;
|
|
24345
|
+
if (essential.image.kind !== "cdk-asset" || !essential.image.assetHash) return;
|
|
24346
|
+
const newAssetHash = essential.image.assetHash;
|
|
24347
|
+
const manifest = await assetLoader.loadManifest(cdkOutDir, candidate.stackName);
|
|
24348
|
+
if (!manifest) return void 0;
|
|
24349
|
+
const newDockerImage = manifest.dockerImages?.[newAssetHash];
|
|
24350
|
+
if (!newDockerImage) return void 0;
|
|
24351
|
+
if (!newDockerImage.source.directory) return;
|
|
24352
|
+
const newAssetSourceDir = path.resolve(cdkOutDir, newDockerImage.source.directory);
|
|
24353
|
+
let oldAssetHash;
|
|
24354
|
+
const liveReplica = controller.runState.replicas.find((r) => !r.shuttingDown);
|
|
24355
|
+
if (liveReplica?.lastDeployedAssetHash !== void 0) oldAssetHash = liveReplica.lastDeployedAssetHash;
|
|
24356
|
+
else {
|
|
24357
|
+
const oldEssential = controller.service.task.containers.find((c) => c.essential) ?? controller.service.task.containers[0];
|
|
24358
|
+
if (oldEssential?.image.kind === "cdk-asset") oldAssetHash = oldEssential.image.assetHash;
|
|
24359
|
+
}
|
|
24360
|
+
return {
|
|
24361
|
+
...oldAssetHash !== void 0 && { oldAssetHash },
|
|
24362
|
+
newAssetHash,
|
|
24363
|
+
newAssetSourceDir,
|
|
24364
|
+
dockerFile: path.basename(newDockerImage.source.dockerFile ?? "Dockerfile")
|
|
24365
|
+
};
|
|
24366
|
+
}
|
|
24367
|
+
/**
|
|
23839
24368
|
* Phase 2 of issue #214 — roll every replica of one target through the
|
|
23840
24369
|
* new task descriptor sequentially. Extracted from {@link reloadAllServices}
|
|
23841
24370
|
* so the per-target try/catch logic (synth-failure / resolve-failure /
|
|
@@ -23846,7 +24375,7 @@ async function reloadAllServices(args) {
|
|
|
23846
24375
|
* even when the resolve / roll throws.
|
|
23847
24376
|
*/
|
|
23848
24377
|
async function rollOneTarget(args) {
|
|
23849
|
-
const { controller, newBoot, stacks, options, discovery, skipPull, extraStateProviders, profileCredsFile, frontDoorPools, suppressLoadBalancerWarning, logger } = args;
|
|
24378
|
+
const { controller, newBoot, stacks, options, discovery, skipPull, extraStateProviders, profileCredsFile, frontDoorPools, suppressLoadBalancerWarning, verdict, logger } = args;
|
|
23850
24379
|
const candidate = pickCandidateStack(parseEcsTarget(newBoot.target).stackPattern, stacks);
|
|
23851
24380
|
const stateProvider = createLocalStateProvider(options, candidate?.stackName ?? "", await resolveCfnFallbackRegion(options, candidate?.region), extraStateProviders);
|
|
23852
24381
|
try {
|
|
@@ -23865,7 +24394,8 @@ async function rollOneTarget(args) {
|
|
|
23865
24394
|
return;
|
|
23866
24395
|
}
|
|
23867
24396
|
if (newService.desiredCount !== oldReplicas.length) logger.warn(`Reload of '${newBoot.target}': service DesiredCount=${newService.desiredCount} does not match the ${oldReplicas.length} live replica(s); rolling existing replicas only — scale changes during --watch are not yet supported. \`^C\` and re-run start-service to apply the new replica count.`);
|
|
23868
|
-
logger.info(`Reload of '${newBoot.target}':
|
|
24397
|
+
if (verdict.kind === "soft-reload") logger.info(`Reload of '${newBoot.target}': soft-reloading ${oldReplicas.length} replica(s) one at a time (docker cp source → docker restart → TCP-ready probe; no rebuild).`);
|
|
24398
|
+
else logger.info(`Reload of '${newBoot.target}': rolling ${oldReplicas.length} replica(s) one at a time (start new shadow → swap registrations → stop old).`);
|
|
23869
24399
|
for (const oldInstance of oldReplicas) {
|
|
23870
24400
|
const idx = controller.runState.replicas.indexOf(oldInstance);
|
|
23871
24401
|
if (idx === -1) {
|
|
@@ -23873,7 +24403,13 @@ async function rollOneTarget(args) {
|
|
|
23873
24403
|
continue;
|
|
23874
24404
|
}
|
|
23875
24405
|
try {
|
|
23876
|
-
await
|
|
24406
|
+
if (verdict.kind === "soft-reload") await softReloadReplica({
|
|
24407
|
+
controller,
|
|
24408
|
+
oldReplicaIndex: idx,
|
|
24409
|
+
newService,
|
|
24410
|
+
sourceDirToCopy: verdict.newAssetSourceDir
|
|
24411
|
+
});
|
|
24412
|
+
else await rollServiceReplica({
|
|
23877
24413
|
controller,
|
|
23878
24414
|
oldReplicaIndex: idx,
|
|
23879
24415
|
newService,
|
|
@@ -24116,6 +24652,7 @@ async function buildFrontDoor(plan, options, logger) {
|
|
|
24116
24652
|
const tls = listener.protocol === "HTTPS" && wantTls ? tlsMaterials : void 0;
|
|
24117
24653
|
const forwardedProto = listener.protocol === "HTTPS" ? "https" : "http";
|
|
24118
24654
|
const degradedHttps = listener.protocol === "HTTPS" && !wantTls;
|
|
24655
|
+
const rulesSummary = listener.rules.map(buildRuleSummary);
|
|
24119
24656
|
const server = await startFrontDoorServer({
|
|
24120
24657
|
route,
|
|
24121
24658
|
port: listener.hostPort,
|
|
@@ -24123,6 +24660,7 @@ async function buildFrontDoor(plan, options, logger) {
|
|
|
24123
24660
|
listenerPort: listener.listenerPort,
|
|
24124
24661
|
label: `listener port ${listener.listenerPort}`,
|
|
24125
24662
|
forwardedProto,
|
|
24663
|
+
rulesSummary,
|
|
24126
24664
|
...tls ? { tls } : {}
|
|
24127
24665
|
});
|
|
24128
24666
|
servers.push(server);
|
|
@@ -24187,6 +24725,66 @@ function describeTarget(t) {
|
|
|
24187
24725
|
function describeTargetShort(t) {
|
|
24188
24726
|
return t.kind === "lambda" ? `Lambda ${t.lambda.logicalId}` : t.serviceTarget;
|
|
24189
24727
|
}
|
|
24728
|
+
/**
|
|
24729
|
+
* Build the per-rule summary the front-door surfaces in its no-rule-matched
|
|
24730
|
+
* 404 body (issue #228). One condition row per constrained ALB field, plus a
|
|
24731
|
+
* pre-formatted action target. Distinct from {@link describeConditions} /
|
|
24732
|
+
* {@link describeAction} (which produce a single-line boot-banner string) —
|
|
24733
|
+
* the 404 body needs the rule decomposed into its constituent fields so the
|
|
24734
|
+
* formatter can render `field in [values]` rows.
|
|
24735
|
+
*/
|
|
24736
|
+
function buildRuleSummary(rule) {
|
|
24737
|
+
const conditions = [];
|
|
24738
|
+
if (rule.pathPatterns.length > 0) conditions.push({
|
|
24739
|
+
field: "path-pattern",
|
|
24740
|
+
values: rule.pathPatterns
|
|
24741
|
+
});
|
|
24742
|
+
if (rule.hostPatterns.length > 0) conditions.push({
|
|
24743
|
+
field: "host-header",
|
|
24744
|
+
values: rule.hostPatterns
|
|
24745
|
+
});
|
|
24746
|
+
for (const h of rule.httpHeaderConditions) conditions.push({
|
|
24747
|
+
field: "http-header",
|
|
24748
|
+
values: [`${h.name}: ${h.values.join(", ")}`]
|
|
24749
|
+
});
|
|
24750
|
+
if (rule.httpRequestMethods.length > 0) conditions.push({
|
|
24751
|
+
field: "http-request-method",
|
|
24752
|
+
values: rule.httpRequestMethods
|
|
24753
|
+
});
|
|
24754
|
+
if (rule.queryStringConditions.length > 0) conditions.push({
|
|
24755
|
+
field: "query-string",
|
|
24756
|
+
values: rule.queryStringConditions.map(describeQueryStringCondition)
|
|
24757
|
+
});
|
|
24758
|
+
if (rule.sourceIpCidrs.length > 0) conditions.push({
|
|
24759
|
+
field: "source-ip",
|
|
24760
|
+
values: rule.sourceIpCidrs
|
|
24761
|
+
});
|
|
24762
|
+
return {
|
|
24763
|
+
priority: rule.priority,
|
|
24764
|
+
conditions,
|
|
24765
|
+
action: describeRuleActionForSummary(rule.action)
|
|
24766
|
+
};
|
|
24767
|
+
}
|
|
24768
|
+
/**
|
|
24769
|
+
* Describe a planned action for the no-rule-matched 404 body (issue #228).
|
|
24770
|
+
* Uses the `<ECS: ...>` / `<Lambda: ...>` shape the issue body proposes so a
|
|
24771
|
+
* user can read each rule's target at a glance.
|
|
24772
|
+
*/
|
|
24773
|
+
function describeRuleActionForSummary(action) {
|
|
24774
|
+
if (action.kind === "redirect") return `redirect ${action.statusCode}`;
|
|
24775
|
+
if (action.kind === "fixed-response") return `fixed-response ${action.statusCode}`;
|
|
24776
|
+
if (action.targets.length === 1) return `forward to ${describeForwardTargetForSummary(action.targets[0])}`;
|
|
24777
|
+
return `forward weighted [${action.targets.map((t) => `${describeForwardTargetForSummary(t)}@${t.weight}`).join(", ")}]`;
|
|
24778
|
+
}
|
|
24779
|
+
/**
|
|
24780
|
+
* One forward target named the way the 404 body shows it: `<ECS: Service>` or
|
|
24781
|
+
* `<Lambda: LogicalId>` — distinct from the boot-banner format (which also
|
|
24782
|
+
* prints the container / port / round-robin hint) so the 404 body stays
|
|
24783
|
+
* scannable.
|
|
24784
|
+
*/
|
|
24785
|
+
function describeForwardTargetForSummary(t) {
|
|
24786
|
+
return t.kind === "lambda" ? `<Lambda: ${t.lambda.logicalId}>` : `<ECS: ${t.serviceTarget}>`;
|
|
24787
|
+
}
|
|
24190
24788
|
async function resolvePlaceholderAccount(arn, region) {
|
|
24191
24789
|
if (!arn.includes("${AWS::AccountId}")) return arn;
|
|
24192
24790
|
const { STSClient, GetCallerIdentityCommand } = await import("@aws-sdk/client-sts");
|
|
@@ -24458,7 +25056,7 @@ function createLocalStartServiceCommand(opts = {}) {
|
|
|
24458
25056
|
* for that contract to live.
|
|
24459
25057
|
*/
|
|
24460
25058
|
function addStartServiceSpecificOptions(cmd) {
|
|
24461
|
-
return cmd.addOption(new Option("--host-port <containerPort=hostPort...>", "Publish a container port on a specific host port (e.g. 80=8080); repeatable. Default: host port == container port. Use this on macOS to map a privileged container port (< 1024) to a non-privileged host port and avoid the Docker Desktop admin-password prompt. (Single-replica services only — multi-replica services do not publish host ports.)")).addOption(new Option("--watch", "Hot-reload: re-synth + per-replica
|
|
25059
|
+
return cmd.addOption(new Option("--host-port <containerPort=hostPort...>", "Publish a container port on a specific host port (e.g. 80=8080); repeatable. Default: host port == container port. Use this on macOS to map a privileged container port (< 1024) to a non-privileged host port and avoid the Docker Desktop admin-password prompt. (Single-replica services only — multi-replica services do not publish host ports.)")).addOption(new Option("--watch", "Hot-reload: re-synth + per-replica reload when the CDK source changes (honors cdk.json watch.include/exclude; cdk.out, node_modules, .git are always excluded). A per-firing classifier picks the per-replica primitive: source-only edits on interpreted-language handlers (Node/Python/Ruby/shell) take a bind-mount FAST PATH (`docker cp` the new source into each replica + `docker restart`; no rebuild). Dockerfile / dependency manifest / compiled-language source / ambiguous edits fall through to the rebuild rolling primitive — boot a shadow under a bumped generation suffix, wait for its container port to accept a TCP connection, atomically swap Service-Connect / Cloud Map registrations, then retire the old container. Either path rolls one replica at a time, so peer services see zero connection refusals across the reload even on multi-replica services. Off by default; existing replica(s) keep serving when synth fails mid-reload.").default(false));
|
|
24462
25060
|
}
|
|
24463
25061
|
|
|
24464
25062
|
//#endregion
|
|
@@ -25311,7 +25909,7 @@ function createLocalStartAlbCommand(opts = {}) {
|
|
|
25311
25909
|
* clusters. Chainable: returns `cmd`.
|
|
25312
25910
|
*/
|
|
25313
25911
|
function addAlbSpecificOptions(cmd) {
|
|
25314
|
-
return cmd.addOption(new Option("--lb-port <listenerPort=hostPort...>", "Bind the local front-door on a specific host port (e.g. 80=8080); repeatable. Default: host port == ALB listener port. Use this on macOS to remap a privileged listener port (< 1024) to a non-privileged host port.")).addOption(new Option("--tls", "Terminate TLS locally for cloud-HTTPS listeners. Default: a cloud-HTTPS listener is served over plain HTTP locally (X-Forwarded-Proto: https is preserved so the upstream app still sees the deployed listener protocol). Implied by --tls-cert / --tls-key. Use this when local-dev cookies need Secure / SameSite=None, when the upstream app inspects TLS metadata, or for mTLS / SNI testing — otherwise plain HTTP is friendlier (no self-signed cert warnings in curl / browser).")).addOption(new Option("--tls-cert <path>", "PEM-encoded server certificate for HTTPS front-door listeners. Implies --tls. Must be set together with --tls-key. Pass --tls alone (without --tls-cert / --tls-key) to auto-generate a self-signed cert (cached under $XDG_CACHE_HOME/cdk-local/alb-https/, default ~/.cache/cdk-local/alb-https/); requires openssl on PATH. The deployed Listener Certificates[] are NOT fetched (ACM private keys are not retrievable by design). The auto-generated cert lists DNS:localhost,IP:127.0.0.1 as SubjectAltName, so a client validating a non-loopback --container-host will fail the SAN check — pass --tls-cert / --tls-key with a SAN covering that host instead.")).addOption(new Option("--tls-key <path>", "PEM-encoded server private key matching --tls-cert. Implies --tls. Must be set together with --tls-cert.")).addOption(new Option("--no-verify-auth", "Disable local enforcement of authenticate-cognito / authenticate-oidc actions. Every request is served as if the auth check passed. Useful for local dev where you do not want to mint a Bearer token at all.")).addOption(new Option("--bearer-token <jwt>", "Default Bearer JWT injected as Authorization: Bearer <jwt> when the inbound request has none. Verified against the same JWKS / OIDC discovery URL the deployed ALB would (signature + iss + aud + exp). Local-dev convenience; cookie pass-through (AWSELBAuthSessionCookie-*) also works.")).addOption(new Option("--watch", "Hot-reload: re-synth + per-replica
|
|
25912
|
+
return cmd.addOption(new Option("--lb-port <listenerPort=hostPort...>", "Bind the local front-door on a specific host port (e.g. 80=8080); repeatable. Default: host port == ALB listener port. Use this on macOS to remap a privileged listener port (< 1024) to a non-privileged host port.")).addOption(new Option("--tls", "Terminate TLS locally for cloud-HTTPS listeners. Default: a cloud-HTTPS listener is served over plain HTTP locally (X-Forwarded-Proto: https is preserved so the upstream app still sees the deployed listener protocol). Implied by --tls-cert / --tls-key. Use this when local-dev cookies need Secure / SameSite=None, when the upstream app inspects TLS metadata, or for mTLS / SNI testing — otherwise plain HTTP is friendlier (no self-signed cert warnings in curl / browser).")).addOption(new Option("--tls-cert <path>", "PEM-encoded server certificate for HTTPS front-door listeners. Implies --tls. Must be set together with --tls-key. Pass --tls alone (without --tls-cert / --tls-key) to auto-generate a self-signed cert (cached under $XDG_CACHE_HOME/cdk-local/alb-https/, default ~/.cache/cdk-local/alb-https/); requires openssl on PATH. The deployed Listener Certificates[] are NOT fetched (ACM private keys are not retrievable by design). The auto-generated cert lists DNS:localhost,IP:127.0.0.1 as SubjectAltName, so a client validating a non-loopback --container-host will fail the SAN check — pass --tls-cert / --tls-key with a SAN covering that host instead.")).addOption(new Option("--tls-key <path>", "PEM-encoded server private key matching --tls-cert. Implies --tls. Must be set together with --tls-cert.")).addOption(new Option("--no-verify-auth", "Disable local enforcement of authenticate-cognito / authenticate-oidc actions. Every request is served as if the auth check passed. Useful for local dev where you do not want to mint a Bearer token at all.")).addOption(new Option("--bearer-token <jwt>", "Default Bearer JWT injected as Authorization: Bearer <jwt> when the inbound request has none. Verified against the same JWKS / OIDC discovery URL the deployed ALB would (signature + iss + aud + exp). Local-dev convenience; cookie pass-through (AWSELBAuthSessionCookie-*) also works.")).addOption(new Option("--watch", "Hot-reload: re-synth + per-replica reload of every ECS service behind the ALB when the CDK source changes (honors cdk.json watch.include/exclude; cdk.out, node_modules, .git are always excluded). A per-firing classifier picks the per-replica primitive: source-only edits on interpreted-language handlers (Node/Python/Ruby/shell) take a bind-mount FAST PATH (`docker cp` the new source into each replica + `docker restart`; no rebuild, front-door pool entry unchanged since the IP/port are preserved). Dockerfile / dependency manifest / compiled-language source / ambiguous edits fall through to the rebuild rolling primitive — boot a shadow under a bumped generation suffix, wait for its container port to accept a TCP connection, atomically register it in the front-door pool, then drop the old entry and retire the old container. Either path rolls one replica at a time, so a continuous external request stream against the listener port sees zero connection refusals across the reload. The host front-door (TLS, JWKS cache, Lambda-target containers, listener sockets) stays up across the reload. Lambda target groups behind the ALB are a no-op on reload (the warm RIE container keeps its boot-time image). Off by default; existing replica(s) keep serving when synth fails mid-reload.").default(false));
|
|
25315
25913
|
}
|
|
25316
25914
|
|
|
25317
25915
|
//#endregion
|
|
@@ -25407,5 +26005,5 @@ function addListSpecificOptions(cmd) {
|
|
|
25407
26005
|
}
|
|
25408
26006
|
|
|
25409
26007
|
//#endregion
|
|
25410
|
-
export {
|
|
25411
|
-
//# sourceMappingURL=local-list-
|
|
26008
|
+
export { translateLambdaResponse as $, buildContainerImage as $t, addStartApiSpecificOptions as A, AGENTCORE_A2A_PROTOCOL as An, invokeAgentCoreWs as At, filterRoutesByApiIdentifiers as B, substituteImagePlaceholders as Bn, signAgentCoreInvocation as Bt, classifySourceChange as C, listTargets as Cn, verifyJwtViaDiscovery as Ct, createLocalRunTaskCommand as D, discoverRoutes as Dn, buildCorsConfigFromCloudFrontChain as Dt, addRunTaskSpecificOptions as E, parseSelectionExpressionPath as En, buildCorsConfigByApiId as Et, createFileWatcher as F, AgentCoreResolutionError as Fn, MCP_PATH as Ft, resolveServiceIntegrationParameters as G, SUPPORTED_CODE_RUNTIMES as Gt, readMtlsMaterialsFromDisk as H, LocalInvokeBuildError as Hn, invokeAgentCore as Ht, attachStageContext as I, pickAgentCoreCandidateStack as In, MCP_PROTOCOL_VERSION as It, computeRequestIdentityHash as J, renderCodeDockerfile as Jt, defaultCredentialsLoader as K, buildAgentCoreCodeImage as Kt, buildStageMap as L, resolveAgentCoreTarget as Ln, mcpInvokeOnce as Lt, createWatchPredicates as M, AGENTCORE_HTTP_PROTOCOL as Mn, A2A_PATH as Mt, resolveApiTargetSubset as N, AGENTCORE_MCP_PROTOCOL as Nn, a2aInvokeOnce as Nt, addInvokeAgentCoreSpecificOptions as O, pickRefLogicalId as On, isFunctionUrlOacFronted as Ot, createAuthorizerCache as P, AGENTCORE_RUNTIME_TYPE as Pn, MCP_CONTAINER_PORT as Pt, matchRoute as Q, architectureToPlatform as Qt, availableApiIdentifiers as R, derivePseudoParametersFromRegion as Rn, parseSseForJsonRpc as Rt, CloudMapRegistry as S, countTargets as Sn, verifyJwtAuthorizer as St, getContainerNetworkIp as T, discoverWebSocketApisOrThrow as Tn, applyCorsResponseHeaders as Tt, startApiServer as U, waitForAgentCorePing as Ut, groupRoutesByServer as V, tryResolveImageFnJoin as Vn, AGENTCORE_SESSION_ID_HEADER as Vt, resolveSelectionExpression as W, downloadAndExtractS3Bundle as Wt, invokeRequestAuthorizer as X, addInvokeSpecificOptions as Xt, evaluateCachedLambdaPolicy as Y, toCmdArgv as Yt, invokeTokenAuthorizer as Z, createLocalInvokeCommand as Zt, parseMaxTasks as _, CfnLocalStateProvider as _n, buildMessageEvent as _t, albStrategy as a, substituteAgainstStateAsync as an, selectIntegrationResponse as at, runEcsServiceEmulator as b, resolveWatchConfig as bn, createJwksCache as bt, resolveAlbTarget as c, resolveEnvVars as cn, HOST_GATEWAY_MIN_VERSION as ct, addStartServiceSpecificOptions as d, createLocalStateProvider as dn, ConnectionRegistry as dt, resolveRuntimeCodeMountPath as en, applyAuthorizerOverlay as et, createLocalStartServiceCommand as f, isCfnFlagPresent as fn, buildMgmtEndpointEnvUrl as ft, buildEcsImageResolutionContext as g, resolveCfnStackName as gn, buildDisconnectEvent as gt, addCommonEcsServiceOptions as h, resolveCfnRegion as hn, buildConnectEvent as ht, addAlbSpecificOptions as i, substituteAgainstState as in, pickResponseTemplate as it, createLocalStartApiCommand as j, AGENTCORE_AGUI_PROTOCOL as jn, A2A_CONTAINER_PORT as jt, createLocalInvokeAgentCoreCommand as k, resolveLambdaArnIntrinsic as kn, matchPreflight as kt, isApplicationLoadBalancer as l, materializeLayerFromArn as ln, probeHostGatewaySupport as lt, MAX_TASKS_SUBNET_RANGE_CAP as m, resolveCfnFallbackRegion as mn, parseConnectionsPath as mt, createLocalListCommand as n, resolveRuntimeImage as nn, buildRestV1Event as nt, createLocalStartAlbCommand as o, substituteEnvVarsFromState as on, tryParseStatus as ot, serviceStrategy as p, rejectExplicitCfnStackWithMultipleStacks as pn, handleConnectionsRequest as pt, buildMethodArn as q, computeCodeImageTag as qt, formatTargetListing as r, EcsTaskResolutionError as rn, evaluateResponseParameters as rt, parseLbPortOverrides as s, substituteEnvVarsFromStateAsync as sn, VtlEvaluationError as st, addListSpecificOptions as t, resolveRuntimeFileExtension as tn, buildHttpApiV2Event as tt, resolveAlbFrontDoor as u, LocalStateSourceError as un, bufferToBody as ut, parseRestartPolicy as v, collectSsmParameterRefs as vn, buildCognitoJwksUrl as vt, SOFT_RELOAD_COMPLETION_LOG_SUFFIX as w, discoverWebSocketApis as wn, attachAuthorizers as wt, buildCloudMapIndex as x, resolveSingleTarget as xn, verifyCognitoJwt as xt, resolveSharedSidecarCredentials as y, resolveSsmParameters as yn, buildJwksUrlFromIssuer as yt, filterRoutesByApiIdentifier as z, formatStateRemedy as zn, AGENTCORE_SIGV4_SERVICE as zt };
|
|
26009
|
+
//# sourceMappingURL=local-list-C-wXKogn.js.map
|