cdk-local 0.67.0 → 0.69.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 +1 -1
- package/dist/cli.js +2 -2
- package/dist/index.js +1 -1
- package/dist/internal.d.ts +19 -2
- package/dist/internal.d.ts.map +1 -1
- package/dist/internal.js +2 -2
- package/dist/local-list-9jAE7ClA.d.ts.map +1 -1
- package/dist/{local-list-faPgnDlc.js → local-list-l2_7oGHF.js} +498 -52
- package/dist/local-list-l2_7oGHF.js.map +1 -0
- package/package.json +1 -1
- package/dist/local-list-faPgnDlc.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
|
}
|
|
@@ -21404,6 +21408,200 @@ async function rollServiceReplica(args) {
|
|
|
21404
21408
|
logger.info(`Rolling replica ${shadow.index} (gen ${shadow.generation}): swap complete; old retired.`);
|
|
21405
21409
|
}
|
|
21406
21410
|
/**
|
|
21411
|
+
* Phase 4 of issue #214 — bind-mount source fast path. `docker cp` the
|
|
21412
|
+
* post-synth asset source directory into each essential container of
|
|
21413
|
+
* the live replica, then `docker restart` it. Skips `docker build`,
|
|
21414
|
+
* skips a shadow boot, and keeps the container's network IP / Cloud
|
|
21415
|
+
* Map / front-door pool registrations intact (the registrations key
|
|
21416
|
+
* off the docker-assigned IP and the published host port; `docker
|
|
21417
|
+
* restart` preserves both via the container's stable network
|
|
21418
|
+
* namespace), so NO registry swap is needed.
|
|
21419
|
+
*
|
|
21420
|
+
* Sequence (per replica, sequenced by the rolling loop one at a time
|
|
21421
|
+
* so peer services + the ALB front-door always have at least N-1 live
|
|
21422
|
+
* endpoints across the reload — same zero-connection-refusal guarantee
|
|
21423
|
+
* the Phase 2/3 rebuild pathway makes):
|
|
21424
|
+
* 1. Locate the live replica by {@link oldReplicaIndex}; reject when
|
|
21425
|
+
* shutting down (the next save can roll a clean boot instead).
|
|
21426
|
+
* 2. Pre-restart DRAIN: drop the replica's Cloud Map handles + every
|
|
21427
|
+
* front-door pool entry under its owner key. Both registries are
|
|
21428
|
+
* synchronous Map mutations; once these complete, peer wget +
|
|
21429
|
+
* front-door `next()` calls route to the surviving replicas. The
|
|
21430
|
+
* handle / owner-key snapshots are kept on `instance.*` so the
|
|
21431
|
+
* symmetric re-register step can pick them up (Cloud Map handles
|
|
21432
|
+
* are rebuilt fresh via `publishReplicaToCloudMap`; the docker
|
|
21433
|
+
* network IP is preserved across `docker restart` so the new
|
|
21434
|
+
* handles point at the SAME endpoint).
|
|
21435
|
+
* 3. Set {@link ServiceReplicaInstance.softReloadInProgress} = true
|
|
21436
|
+
* so the watcher's `waitForExitImpl` post-exit branch defers to
|
|
21437
|
+
* the in-flight restart instead of re-bootstrapping the replica
|
|
21438
|
+
* from scratch.
|
|
21439
|
+
* 4. For each essential container in the replica's started set:
|
|
21440
|
+
* a. Resolve the container's image WORKDIR via `docker inspect`
|
|
21441
|
+
* (default `/` when unset — matches Docker's runtime default
|
|
21442
|
+
* for a Dockerfile with no `WORKDIR`).
|
|
21443
|
+
* b. `docker cp <sourceDirToCopy>/. <containerId>:<workdir>/`
|
|
21444
|
+
* — copy the synthesized asset directory's contents into the
|
|
21445
|
+
* container at the WORKDIR. Trailing `/.` is critical: it
|
|
21446
|
+
* copies the SOURCE DIRECTORY'S CONTENTS, not the directory
|
|
21447
|
+
* itself, mirroring `cp -r src/. dst/`.
|
|
21448
|
+
* c. `docker restart <containerId>` — cycle PID 1. Image,
|
|
21449
|
+
* network namespace, and host-port publish are preserved.
|
|
21450
|
+
* 5. {@link waitForReplicaTcpReady} confirms the essential
|
|
21451
|
+
* container's first port accepts a TCP connection.
|
|
21452
|
+
* 6. Post-TCP-ready RE-REGISTER: re-publish Cloud Map handles +
|
|
21453
|
+
* front-door pool entry under the SAME per-replica owner key
|
|
21454
|
+
* prefix used at initial boot, so the registrations remain
|
|
21455
|
+
* idempotent across multiple `--watch` reloads. After this
|
|
21456
|
+
* step, peers + the front-door route to the replica again.
|
|
21457
|
+
* 7. Clear `softReloadInProgress` in a `finally` so the watcher
|
|
21458
|
+
* always exits its defer-loop, even on a docker error.
|
|
21459
|
+
*
|
|
21460
|
+
* Failure modes:
|
|
21461
|
+
* - `docker inspect` / `docker cp` / `docker restart` errors:
|
|
21462
|
+
* surfaced to the caller via a throw. The replica may be in an
|
|
21463
|
+
* inconsistent state (drained from registries + partial cp + a
|
|
21464
|
+
* possibly-crashed PID 1). The caller (`reloadAllServices`) logs
|
|
21465
|
+
* the failure and continues with the remaining replicas; the
|
|
21466
|
+
* drained state is intentionally NOT re-registered on error so
|
|
21467
|
+
* peers + the front-door stop routing to a broken replica until
|
|
21468
|
+
* the next clean save (or `^C` and re-run).
|
|
21469
|
+
* - TCP probe timeout: best-effort warn (mirrors
|
|
21470
|
+
* {@link rollServiceReplica}); the registrations are re-published
|
|
21471
|
+
* anyway because the container IS up — just slow to bind. The
|
|
21472
|
+
* dying-old-handles-AND-fresh-app-not-yet-listening worst case
|
|
21473
|
+
* would otherwise leave the replica drained forever.
|
|
21474
|
+
*
|
|
21475
|
+
* Out of scope for the v1 primitive (deferred follow-ups):
|
|
21476
|
+
* - Per-container WORKDIR caching across multiple essential
|
|
21477
|
+
* containers in the same task. The `docker inspect` call is
|
|
21478
|
+
* ~10ms; not worth the cache invalidation surface for a path
|
|
21479
|
+
* fired ~once per save.
|
|
21480
|
+
* - SIGHUP / `docker exec`-driven in-process reload. Image-specific
|
|
21481
|
+
* (uvicorn / nodemon / etc.). `docker restart` is the universal
|
|
21482
|
+
* primitive; signal-based reload is a future opt-in if the
|
|
21483
|
+
* per-restart latency proves prohibitive.
|
|
21484
|
+
*
|
|
21485
|
+
* @internal — wired only by the emulator's reload pathway.
|
|
21486
|
+
*/
|
|
21487
|
+
async function softReloadReplica(args) {
|
|
21488
|
+
const { controller, oldReplicaIndex, newService, sourceDirToCopy } = args;
|
|
21489
|
+
const logger = getLogger().child("ecs-service");
|
|
21490
|
+
const instance = controller.runState.replicas[oldReplicaIndex];
|
|
21491
|
+
if (!instance) throw new EcsServiceRunnerError(`softReloadReplica: no replica at index ${oldReplicaIndex} (replicas=${controller.runState.replicas.length}).`);
|
|
21492
|
+
if (instance.shuttingDown) {
|
|
21493
|
+
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.`);
|
|
21494
|
+
return;
|
|
21495
|
+
}
|
|
21496
|
+
const essentialContainers = newService.task.containers.filter((c) => c.essential);
|
|
21497
|
+
const containersToCycle = essentialContainers.length > 0 ? essentialContainers : newService.task.containers.length > 0 ? [newService.task.containers[0]] : [];
|
|
21498
|
+
if (containersToCycle.length === 0) throw new EcsServiceRunnerError(`softReloadReplica: service '${newService.serviceLogicalId}' has no containers to cycle.`);
|
|
21499
|
+
const startedById = new Map(instance.state.startedContainers.map((c) => [c.name, c.id]));
|
|
21500
|
+
const targets = [];
|
|
21501
|
+
for (const container of containersToCycle) {
|
|
21502
|
+
const id = startedById.get(container.name);
|
|
21503
|
+
if (!id) throw new EcsServiceRunnerError(`softReloadReplica: replica r${instance.index} has no started container named '${container.name}' (started: ${[...startedById.keys()].join(", ") || "none"}).`);
|
|
21504
|
+
targets.push({
|
|
21505
|
+
name: container.name,
|
|
21506
|
+
id
|
|
21507
|
+
});
|
|
21508
|
+
}
|
|
21509
|
+
const controllerOptions = controller.options;
|
|
21510
|
+
if (controllerOptions.discovery) {
|
|
21511
|
+
for (const handle of instance.cloudMapHandles) try {
|
|
21512
|
+
controllerOptions.discovery.registry.unregister(handle);
|
|
21513
|
+
} catch {}
|
|
21514
|
+
instance.cloudMapHandles = [];
|
|
21515
|
+
}
|
|
21516
|
+
unregisterReplicaFromFrontDoor(instance, controllerOptions.frontDoor);
|
|
21517
|
+
instance.softReloadInProgress = true;
|
|
21518
|
+
try {
|
|
21519
|
+
logger.info(`Soft-reloading replica r${instance.index} (gen ${instance.generation}): docker cp ${sourceDirToCopy} -> ${targets.length} essential container(s); restart.`);
|
|
21520
|
+
for (const target of targets) {
|
|
21521
|
+
let workdir;
|
|
21522
|
+
try {
|
|
21523
|
+
workdir = await dockerInspectWorkdirImpl(target.id) || "/";
|
|
21524
|
+
} catch (err) {
|
|
21525
|
+
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.`);
|
|
21526
|
+
}
|
|
21527
|
+
const workdirDest = workdir.endsWith("/") ? workdir : `${workdir}/`;
|
|
21528
|
+
try {
|
|
21529
|
+
await dockerCpImpl(`${sourceDirToCopy}/.`, `${target.id}:${workdirDest}`);
|
|
21530
|
+
} catch (err) {
|
|
21531
|
+
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.`);
|
|
21532
|
+
}
|
|
21533
|
+
try {
|
|
21534
|
+
await dockerRestartImpl(target.id);
|
|
21535
|
+
} catch (err) {
|
|
21536
|
+
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.`);
|
|
21537
|
+
}
|
|
21538
|
+
}
|
|
21539
|
+
await waitForReplicaTcpReady(newService, instance, {
|
|
21540
|
+
timeoutMs: shadowReadyTimeoutMs,
|
|
21541
|
+
intervalMs: shadowReadyIntervalMs,
|
|
21542
|
+
label: `Soft-reloaded replica r${instance.index} (gen ${instance.generation})`
|
|
21543
|
+
});
|
|
21544
|
+
const ownerKeyGenSuffix = instance.generation > 0 ? `:g${instance.generation}` : "";
|
|
21545
|
+
const ownerKeyPrefix = `${newService.serviceLogicalId}:r${instance.index}${ownerKeyGenSuffix}`;
|
|
21546
|
+
if (controllerOptions.discovery) await publishReplicaToCloudMap(newService, instance, controllerOptions.discovery, ownerKeyPrefix);
|
|
21547
|
+
if (controllerOptions.frontDoor) await publishReplicaToFrontDoor(newService, instance, controllerOptions.frontDoor, controllerOptions.taskOptions.containerHost, ownerKeyPrefix);
|
|
21548
|
+
logger.info(`Soft-reloaded replica r${instance.index} (gen ${instance.generation}): restart + TCP-ready probe complete; Cloud Map + front-door re-published.`);
|
|
21549
|
+
} finally {
|
|
21550
|
+
instance.softReloadInProgress = false;
|
|
21551
|
+
}
|
|
21552
|
+
}
|
|
21553
|
+
/**
|
|
21554
|
+
* Production `docker inspect --format {{.Config.WorkingDir}} <id>`
|
|
21555
|
+
* impl. Returns the container's runtime WORKDIR; empty string when
|
|
21556
|
+
* the Dockerfile didn't set one (caller treats empty as `/`, matching
|
|
21557
|
+
* Docker's runtime default).
|
|
21558
|
+
*
|
|
21559
|
+
* Extracted as a test-overridable function so the soft-reload
|
|
21560
|
+
* primitive's unit tests can assert the WORKDIR resolution branch
|
|
21561
|
+
* without standing up a real container.
|
|
21562
|
+
*/
|
|
21563
|
+
const defaultDockerInspectWorkdirImpl = async (containerId) => {
|
|
21564
|
+
const { execFile } = await import("node:child_process");
|
|
21565
|
+
const { promisify } = await import("node:util");
|
|
21566
|
+
const { getDockerCmd } = await import("./docker-cmd-voNPrcRh.js").then((n) => n.t);
|
|
21567
|
+
const { stdout } = await promisify(execFile)(getDockerCmd(), [
|
|
21568
|
+
"inspect",
|
|
21569
|
+
"--format",
|
|
21570
|
+
"{{.Config.WorkingDir}}",
|
|
21571
|
+
containerId
|
|
21572
|
+
]);
|
|
21573
|
+
return stdout.trim();
|
|
21574
|
+
};
|
|
21575
|
+
let dockerInspectWorkdirImpl = defaultDockerInspectWorkdirImpl;
|
|
21576
|
+
/**
|
|
21577
|
+
* Production `docker cp <src> <containerId>:<dst>` impl. The source
|
|
21578
|
+
* path's trailing `/.` ensures CONTENTS of the directory are copied
|
|
21579
|
+
* (not the directory itself); the caller is responsible for that
|
|
21580
|
+
* convention.
|
|
21581
|
+
*/
|
|
21582
|
+
const defaultDockerCpImpl = async (src, dst) => {
|
|
21583
|
+
const { execFile } = await import("node:child_process");
|
|
21584
|
+
const { promisify } = await import("node:util");
|
|
21585
|
+
const { getDockerCmd } = await import("./docker-cmd-voNPrcRh.js").then((n) => n.t);
|
|
21586
|
+
await promisify(execFile)(getDockerCmd(), [
|
|
21587
|
+
"cp",
|
|
21588
|
+
src,
|
|
21589
|
+
dst
|
|
21590
|
+
], { maxBuffer: 64 * 1024 * 1024 });
|
|
21591
|
+
};
|
|
21592
|
+
let dockerCpImpl = defaultDockerCpImpl;
|
|
21593
|
+
/**
|
|
21594
|
+
* Production `docker restart <id>` impl. Synchronous in docker's
|
|
21595
|
+
* sense — blocks until the container is up again (or fails).
|
|
21596
|
+
*/
|
|
21597
|
+
const defaultDockerRestartImpl = async (containerId) => {
|
|
21598
|
+
const { execFile } = await import("node:child_process");
|
|
21599
|
+
const { promisify } = await import("node:util");
|
|
21600
|
+
const { getDockerCmd } = await import("./docker-cmd-voNPrcRh.js").then((n) => n.t);
|
|
21601
|
+
await promisify(execFile)(getDockerCmd(), ["restart", containerId]);
|
|
21602
|
+
};
|
|
21603
|
+
let dockerRestartImpl = defaultDockerRestartImpl;
|
|
21604
|
+
/**
|
|
21407
21605
|
* Phase 2 of issue #214 — disconnect every container of the dying
|
|
21408
21606
|
* replica from the shared service network BEFORE `cleanupEcsRun`'s
|
|
21409
21607
|
* `docker stop → docker rm` sequence. Docker's embedded DNS strips an
|
|
@@ -21470,13 +21668,14 @@ let dockerNetworkDisconnectImpl = defaultDockerNetworkDisconnectImpl;
|
|
|
21470
21668
|
* injectable via {@link __setTcpProbeImpl} so the rolling-primitive
|
|
21471
21669
|
* unit test can avoid any real TCP socket.
|
|
21472
21670
|
*/
|
|
21473
|
-
async function waitForReplicaTcpReady(service,
|
|
21671
|
+
async function waitForReplicaTcpReady(service, replica, opts) {
|
|
21474
21672
|
const logger = getLogger().child("ecs-service");
|
|
21475
|
-
const
|
|
21673
|
+
const label = opts.label ?? `Shadow replica r${replica.index} (gen ${replica.generation})`;
|
|
21674
|
+
const networkName = replica.state.network?.networkName;
|
|
21476
21675
|
if (!networkName) return;
|
|
21477
21676
|
const essential = service.task.containers.find((c) => c.essential) ?? service.task.containers[0];
|
|
21478
21677
|
if (!essential || essential.portMappings.length === 0) return;
|
|
21479
|
-
const started =
|
|
21678
|
+
const started = replica.state.startedContainers.find((c) => c.name === essential.name);
|
|
21480
21679
|
if (!started) return;
|
|
21481
21680
|
let ip;
|
|
21482
21681
|
try {
|
|
@@ -21484,7 +21683,7 @@ async function waitForReplicaTcpReady(service, shadow, opts) {
|
|
|
21484
21683
|
if (!resolved) return;
|
|
21485
21684
|
ip = resolved;
|
|
21486
21685
|
} catch (err) {
|
|
21487
|
-
logger.warn(
|
|
21686
|
+
logger.warn(`${label}: TCP-ready probe could not resolve docker IP: ${err instanceof Error ? err.message : String(err)}. Proceeding.`);
|
|
21488
21687
|
return;
|
|
21489
21688
|
}
|
|
21490
21689
|
const port = essential.portMappings[0].containerPort;
|
|
@@ -21493,14 +21692,14 @@ async function waitForReplicaTcpReady(service, shadow, opts) {
|
|
|
21493
21692
|
while (Date.now() < deadline) {
|
|
21494
21693
|
try {
|
|
21495
21694
|
await tcpProbeImpl(ip, port);
|
|
21496
|
-
logger.debug(
|
|
21695
|
+
logger.debug(`${label}: TCP probe ${ip}:${port} accepted.`);
|
|
21497
21696
|
return;
|
|
21498
21697
|
} catch (err) {
|
|
21499
21698
|
lastErr = err instanceof Error ? err.message : String(err);
|
|
21500
21699
|
}
|
|
21501
21700
|
await sleep(opts.intervalMs);
|
|
21502
21701
|
}
|
|
21503
|
-
logger.warn(
|
|
21702
|
+
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
21703
|
}
|
|
21505
21704
|
/**
|
|
21506
21705
|
* Default TCP-connect probe used by {@link waitForReplicaTcpReady}.
|
|
@@ -21549,6 +21748,11 @@ async function watchReplica(service, options, instance, runState) {
|
|
|
21549
21748
|
exitCode = -1;
|
|
21550
21749
|
}
|
|
21551
21750
|
if (instance.shuttingDown || runState.shuttingDown) return;
|
|
21751
|
+
if (instance.softReloadInProgress) {
|
|
21752
|
+
while (instance.softReloadInProgress && !instance.shuttingDown && !runState.shuttingDown) await sleep(100);
|
|
21753
|
+
if (instance.shuttingDown || runState.shuttingDown) return;
|
|
21754
|
+
continue;
|
|
21755
|
+
}
|
|
21552
21756
|
logger.warn(`Replica ${instance.index} essential container exited with code ${exitCode} (restartCount=${instance.restartCount}).`);
|
|
21553
21757
|
const willRestart = shouldRestart(exitCode, options.restartPolicy);
|
|
21554
21758
|
if (!willRestart || instance.restartCount === 0) await printExitedContainerLogs(instance.index, essentialId, logger);
|
|
@@ -21664,6 +21868,152 @@ function sleep(ms) {
|
|
|
21664
21868
|
return sleepImpl(ms);
|
|
21665
21869
|
}
|
|
21666
21870
|
|
|
21871
|
+
//#endregion
|
|
21872
|
+
//#region src/local/source-change-classifier.ts
|
|
21873
|
+
/**
|
|
21874
|
+
* Dependency-manifest basenames recognized by the classifier. A change
|
|
21875
|
+
* to any of these forces a rebuild because the running container's
|
|
21876
|
+
* pre-built dependency layer is no longer in sync with the source.
|
|
21877
|
+
*
|
|
21878
|
+
* Coverage: the package managers commonly used inside a Lambda /
|
|
21879
|
+
* container image — Node (pnpm / npm / yarn), Python (pip / poetry /
|
|
21880
|
+
* pipenv), Ruby (bundler), Go (modules), Rust (cargo), Java / Kotlin
|
|
21881
|
+
* (Maven, Gradle). Adding a new ecosystem? Append its lockfile +
|
|
21882
|
+
* manifest here and add a classifier test row.
|
|
21883
|
+
*/
|
|
21884
|
+
const REBUILD_TRIGGER_BASENAMES = new Set([
|
|
21885
|
+
"package.json",
|
|
21886
|
+
"package-lock.json",
|
|
21887
|
+
"pnpm-lock.yaml",
|
|
21888
|
+
"yarn.lock",
|
|
21889
|
+
"npm-shrinkwrap.json",
|
|
21890
|
+
"requirements.txt",
|
|
21891
|
+
"requirements-dev.txt",
|
|
21892
|
+
"pyproject.toml",
|
|
21893
|
+
"poetry.lock",
|
|
21894
|
+
"Pipfile",
|
|
21895
|
+
"Pipfile.lock",
|
|
21896
|
+
"uv.lock",
|
|
21897
|
+
"Gemfile",
|
|
21898
|
+
"Gemfile.lock",
|
|
21899
|
+
"go.mod",
|
|
21900
|
+
"go.sum",
|
|
21901
|
+
"Cargo.toml",
|
|
21902
|
+
"Cargo.lock",
|
|
21903
|
+
"pom.xml",
|
|
21904
|
+
"build.gradle",
|
|
21905
|
+
"build.gradle.kts",
|
|
21906
|
+
"settings.gradle",
|
|
21907
|
+
"settings.gradle.kts",
|
|
21908
|
+
"Makefile",
|
|
21909
|
+
"CMakeLists.txt"
|
|
21910
|
+
]);
|
|
21911
|
+
/**
|
|
21912
|
+
* Compiled-language source extensions that require a build step
|
|
21913
|
+
* inside `docker build` (typically a `RUN go build` / `cargo build` /
|
|
21914
|
+
* `mvn package` etc.). A copy of the source alone would leave the
|
|
21915
|
+
* running binary stale, so the user's intent must be a rebuild.
|
|
21916
|
+
*
|
|
21917
|
+
* Interpreted-language runtimes (Node — `.js` / `.mjs` / `.cjs` /
|
|
21918
|
+
* `.ts` when transpiled at runtime, Python — `.py`, Ruby — `.rb`,
|
|
21919
|
+
* shell — `.sh`) read source at process start, so a `docker cp` +
|
|
21920
|
+
* `docker restart` cycle picks them up. Those extensions are
|
|
21921
|
+
* NOT in this set.
|
|
21922
|
+
*/
|
|
21923
|
+
const COMPILED_LANGUAGE_EXTENSIONS = new Set([
|
|
21924
|
+
".go",
|
|
21925
|
+
".rs",
|
|
21926
|
+
".java",
|
|
21927
|
+
".kt",
|
|
21928
|
+
".kts",
|
|
21929
|
+
".scala",
|
|
21930
|
+
".cs",
|
|
21931
|
+
".swift",
|
|
21932
|
+
".fs",
|
|
21933
|
+
".fsx",
|
|
21934
|
+
".c",
|
|
21935
|
+
".cc",
|
|
21936
|
+
".cpp",
|
|
21937
|
+
".cxx",
|
|
21938
|
+
".h",
|
|
21939
|
+
".hpp",
|
|
21940
|
+
".zig",
|
|
21941
|
+
".ml",
|
|
21942
|
+
".mli",
|
|
21943
|
+
".elm",
|
|
21944
|
+
".hs",
|
|
21945
|
+
".dart"
|
|
21946
|
+
]);
|
|
21947
|
+
/**
|
|
21948
|
+
* Classify a single watcher firing into rebuild vs soft-reload. Pure
|
|
21949
|
+
* + synchronous. The caller (emulator's reload pathway) invokes this
|
|
21950
|
+
* once per target per firing AFTER `cdk synth` has run and the new
|
|
21951
|
+
* asset manifest is on disk.
|
|
21952
|
+
*
|
|
21953
|
+
* Branching:
|
|
21954
|
+
* 1. No asset context (image isn't a CDK asset, or asset lookup
|
|
21955
|
+
* failed) → `rebuild`.
|
|
21956
|
+
* 2. The asset hash didn't change between old and new synths
|
|
21957
|
+
* (`oldAssetHash === newAssetHash`, or `oldAssetHash` missing)
|
|
21958
|
+
* → `rebuild`. Load-bearing guard for "user edited a CDK
|
|
21959
|
+
* construct file (e.g. `lib/stack.ts`) that flipped the task
|
|
21960
|
+
* spec but didn't touch the asset content". Soft-reload would
|
|
21961
|
+
* `docker cp` identical files and `docker restart` the
|
|
21962
|
+
* container with the OLD task spec (env / memory / mounts /
|
|
21963
|
+
* added sidecars are set at `docker create` time, not on
|
|
21964
|
+
* restart) — the user's intent would silently NOT apply.
|
|
21965
|
+
* Forcing rebuild keeps Phase 1-3 semantics exactly for this
|
|
21966
|
+
* case: the rolling primitive boots a shadow with the new task
|
|
21967
|
+
* spec, the user sees their construct edit take effect.
|
|
21968
|
+
* 3. No changed paths (the watcher fired on a debounce flush with
|
|
21969
|
+
* an empty pending set — shouldn't happen in practice, but
|
|
21970
|
+
* defensive) → `rebuild`.
|
|
21971
|
+
* 4. Any changed path's basename matches the Dockerfile or a
|
|
21972
|
+
* dependency manifest → `rebuild`.
|
|
21973
|
+
* 5. Any changed path's extension is a compiled-language source →
|
|
21974
|
+
* `rebuild`.
|
|
21975
|
+
* 6. Else → `soft-reload`.
|
|
21976
|
+
*/
|
|
21977
|
+
function classifySourceChange(changedPaths, ctx) {
|
|
21978
|
+
if (!ctx) return {
|
|
21979
|
+
kind: "rebuild",
|
|
21980
|
+
reason: "target image is not a CDK docker-image asset"
|
|
21981
|
+
};
|
|
21982
|
+
if (!ctx.oldAssetHash || ctx.oldAssetHash === ctx.newAssetHash) return {
|
|
21983
|
+
kind: "rebuild",
|
|
21984
|
+
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"
|
|
21985
|
+
};
|
|
21986
|
+
if (changedPaths.length === 0) return {
|
|
21987
|
+
kind: "rebuild",
|
|
21988
|
+
reason: "no changed paths reported (defensive default)"
|
|
21989
|
+
};
|
|
21990
|
+
for (const p of changedPaths) {
|
|
21991
|
+
const basename = path.basename(p);
|
|
21992
|
+
if (basename === ctx.dockerFile) return {
|
|
21993
|
+
kind: "rebuild",
|
|
21994
|
+
reason: `Dockerfile edit (${basename})`
|
|
21995
|
+
};
|
|
21996
|
+
if (basename.startsWith("Dockerfile.")) return {
|
|
21997
|
+
kind: "rebuild",
|
|
21998
|
+
reason: `Dockerfile.* edit (${basename})`
|
|
21999
|
+
};
|
|
22000
|
+
if (REBUILD_TRIGGER_BASENAMES.has(basename)) return {
|
|
22001
|
+
kind: "rebuild",
|
|
22002
|
+
reason: `dependency manifest edit (${basename})`
|
|
22003
|
+
};
|
|
22004
|
+
const ext = path.extname(p).toLowerCase();
|
|
22005
|
+
if (COMPILED_LANGUAGE_EXTENSIONS.has(ext)) return {
|
|
22006
|
+
kind: "rebuild",
|
|
22007
|
+
reason: `compiled-language source edit (${basename}) — soft-reload would leave the built binary stale`
|
|
22008
|
+
};
|
|
22009
|
+
}
|
|
22010
|
+
return {
|
|
22011
|
+
kind: "soft-reload",
|
|
22012
|
+
reason: `${changedPaths.length} source-only path(s) — skipping rebuild`,
|
|
22013
|
+
newAssetSourceDir: ctx.newAssetSourceDir
|
|
22014
|
+
};
|
|
22015
|
+
}
|
|
22016
|
+
|
|
21667
22017
|
//#endregion
|
|
21668
22018
|
//#region src/local/cloud-map-registry.ts
|
|
21669
22019
|
/**
|
|
@@ -23319,9 +23669,9 @@ function materializeInlineCode(handler, source, fileExtension) {
|
|
|
23319
23669
|
const lastDot = handler.lastIndexOf(".");
|
|
23320
23670
|
if (lastDot <= 0) throw new Error(`Handler '${handler}' is malformed: expected '<modulePath>.<exportName>'.`);
|
|
23321
23671
|
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 });
|
|
23672
|
+
const dir = mkdtempSync(path$1.join(tmpdir(), `${getEmbedConfig().resourceNamePrefix}-alb-lambda-`));
|
|
23673
|
+
const filePath = path$1.join(dir, `${modulePath}${fileExtension}`);
|
|
23674
|
+
mkdirSync(path$1.dirname(filePath), { recursive: true });
|
|
23325
23675
|
writeFileSync(filePath, source, "utf-8");
|
|
23326
23676
|
return dir;
|
|
23327
23677
|
}
|
|
@@ -23353,7 +23703,7 @@ async function resolveContainerImagePlan(lambda, opts) {
|
|
|
23353
23703
|
let imageRef;
|
|
23354
23704
|
let localBuilt = false;
|
|
23355
23705
|
if (manifestPath) {
|
|
23356
|
-
const cdkOutDir = path.dirname(manifestPath);
|
|
23706
|
+
const cdkOutDir = path$1.dirname(manifestPath);
|
|
23357
23707
|
const manifest = await new AssetManifestLoader().loadManifest(cdkOutDir, lambda.stack.stackName);
|
|
23358
23708
|
const entry = manifest ? getDockerImageBySourceHash(manifest, lambda.imageUri) : void 0;
|
|
23359
23709
|
if (entry) {
|
|
@@ -23719,8 +24069,8 @@ async function runEcsServiceEmulator(targets, options, strategy, extraStateProvi
|
|
|
23719
24069
|
paths: [watchRoot],
|
|
23720
24070
|
ignored,
|
|
23721
24071
|
shouldTrigger,
|
|
23722
|
-
onChange: () => {
|
|
23723
|
-
logger.info(
|
|
24072
|
+
onChange: (changedPaths) => {
|
|
24073
|
+
logger.info(`Detected source change (${changedPaths.length} path(s)); reloading service(s)...`);
|
|
23724
24074
|
reloadChain = reloadChain.then(() => reloadAllServices({
|
|
23725
24075
|
perTarget,
|
|
23726
24076
|
synthesizer,
|
|
@@ -23734,6 +24084,7 @@ async function runEcsServiceEmulator(targets, options, strategy, extraStateProvi
|
|
|
23734
24084
|
extraStateProviders,
|
|
23735
24085
|
profileCredsFile,
|
|
23736
24086
|
frontDoorByService,
|
|
24087
|
+
changedPaths,
|
|
23737
24088
|
logger
|
|
23738
24089
|
})).catch((err) => {
|
|
23739
24090
|
logger.error(`reloadAllServices threw: ${err instanceof Error ? err.message : String(err)}`);
|
|
@@ -23752,11 +24103,22 @@ async function runEcsServiceEmulator(targets, options, strategy, extraStateProvi
|
|
|
23752
24103
|
}
|
|
23753
24104
|
}
|
|
23754
24105
|
/**
|
|
23755
|
-
* Phase 2 of issue #214 — multi-replica
|
|
23756
|
-
* `cdkl start-service --watch
|
|
23757
|
-
*
|
|
23758
|
-
*
|
|
23759
|
-
*
|
|
24106
|
+
* Phase 2 + Phase 4 of issue #214 — multi-replica reload cycle for
|
|
24107
|
+
* `cdkl start-service --watch` (Phase 2) and `cdkl start-alb --watch`
|
|
24108
|
+
* (Phase 3 wires the same loop). Mirrors start-api's `reloadAllServers`
|
|
24109
|
+
* shape but per-ECS-service. Per-target verdict from
|
|
24110
|
+
* {@link classifySourceChange} (Phase 4) picks the per-replica action:
|
|
24111
|
+
*
|
|
24112
|
+
* - `'soft-reload'` → {@link softReloadReplica} runs `docker cp`
|
|
24113
|
+
* + `docker restart` against the live replica. No `docker build`,
|
|
24114
|
+
* no shadow boot, no Cloud Map / front-door pool swap — the
|
|
24115
|
+
* container's IP + host port are preserved across the restart, so
|
|
24116
|
+
* existing registrations stay valid. Fast path (~sub-second per
|
|
24117
|
+
* replica for typical interpreted-language handlers).
|
|
24118
|
+
* - `'rebuild'` → {@link rollServiceReplica}, the Phase 1-3 path,
|
|
24119
|
+
* replacing Phase 1's "tear single replica down, boot fresh"
|
|
24120
|
+
* sequence with a per-replica rolling loop so the service stays
|
|
24121
|
+
* available end-to-end:
|
|
23760
24122
|
*
|
|
23761
24123
|
* 1. Re-runs `synthesizer.synthesize(synthOpts)` once (failure → warn
|
|
23762
24124
|
* + keep every replica serving).
|
|
@@ -23791,7 +24153,7 @@ async function runEcsServiceEmulator(targets, options, strategy, extraStateProvi
|
|
|
23791
24153
|
* via the logger so the user can fix the source + save again.
|
|
23792
24154
|
*/
|
|
23793
24155
|
async function reloadAllServices(args) {
|
|
23794
|
-
const { perTarget, synthesizer, synthOpts, strategy, resolvedTargets, cloudMapIndexByStack, options, discovery, skipPull, extraStateProviders, profileCredsFile, frontDoorByService, logger } = args;
|
|
24156
|
+
const { perTarget, synthesizer, synthOpts, strategy, resolvedTargets, cloudMapIndexByStack, options, discovery, skipPull, extraStateProviders, profileCredsFile, frontDoorByService, changedPaths, logger } = args;
|
|
23795
24157
|
let stacks;
|
|
23796
24158
|
try {
|
|
23797
24159
|
({stacks} = await synthesizer.synthesize(synthOpts));
|
|
@@ -23808,6 +24170,8 @@ async function reloadAllServices(args) {
|
|
|
23808
24170
|
cloudMapIndexByStack.set(stack.stackName, index);
|
|
23809
24171
|
for (const w of index.warnings) logger.warn(w);
|
|
23810
24172
|
}
|
|
24173
|
+
const cdkOutDir = options.output;
|
|
24174
|
+
const assetLoader = new AssetManifestLoader();
|
|
23811
24175
|
for (const pt of perTarget) {
|
|
23812
24176
|
const newBoot = newBootByTarget.get(pt.boot.target);
|
|
23813
24177
|
if (!newBoot) {
|
|
@@ -23819,6 +24183,27 @@ async function reloadAllServices(args) {
|
|
|
23819
24183
|
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
24184
|
continue;
|
|
23821
24185
|
}
|
|
24186
|
+
let verdict = {
|
|
24187
|
+
kind: "rebuild",
|
|
24188
|
+
reason: "classifier not consulted"
|
|
24189
|
+
};
|
|
24190
|
+
try {
|
|
24191
|
+
verdict = classifySourceChange(changedPaths, await loadAssetContextForTarget({
|
|
24192
|
+
target: newBoot.target,
|
|
24193
|
+
controller,
|
|
24194
|
+
stacks,
|
|
24195
|
+
cdkOutDir,
|
|
24196
|
+
assetLoader,
|
|
24197
|
+
logger
|
|
24198
|
+
}));
|
|
24199
|
+
logger.info(`Reload of '${newBoot.target}': verdict=${verdict.kind} (${verdict.reason}).`);
|
|
24200
|
+
} catch (err) {
|
|
24201
|
+
logger.warn(`Reload of '${newBoot.target}': classifier context unavailable (${err instanceof Error ? err.message : String(err)}); falling back to rebuild.`);
|
|
24202
|
+
verdict = {
|
|
24203
|
+
kind: "rebuild",
|
|
24204
|
+
reason: "classifier context unavailable; falling back to rebuild"
|
|
24205
|
+
};
|
|
24206
|
+
}
|
|
23822
24207
|
await rollOneTarget({
|
|
23823
24208
|
controller,
|
|
23824
24209
|
newBoot,
|
|
@@ -23830,12 +24215,62 @@ async function reloadAllServices(args) {
|
|
|
23830
24215
|
profileCredsFile,
|
|
23831
24216
|
frontDoorPools: frontDoorByService.get(newBoot.target),
|
|
23832
24217
|
suppressLoadBalancerWarning: strategy.suppressLoadBalancerWarning === true,
|
|
24218
|
+
verdict,
|
|
23833
24219
|
logger
|
|
23834
24220
|
});
|
|
23835
24221
|
}
|
|
23836
24222
|
logger.info("Reload complete.");
|
|
23837
24223
|
}
|
|
23838
24224
|
/**
|
|
24225
|
+
* Phase 4 of issue #214 — load the per-target asset context the
|
|
24226
|
+
* source-change classifier consumes. Resolves the target's docker-image
|
|
24227
|
+
* asset hash via the freshly-synthed `<stackName>.assets.json` and
|
|
24228
|
+
* derives the staged source directory + Dockerfile basename.
|
|
24229
|
+
*
|
|
24230
|
+
* Returns `undefined` (and logs at debug) when:
|
|
24231
|
+
* - The target's image isn't a CDK docker-image asset (ECR / public
|
|
24232
|
+
* registry pin). The classifier treats `undefined` as `rebuild`
|
|
24233
|
+
* because there's no local source tree to copy.
|
|
24234
|
+
* - The asset manifest can't be loaded (stack not synthed yet, file
|
|
24235
|
+
* missing). Same treatment — defensive default to `rebuild`.
|
|
24236
|
+
* - The asset hash isn't in the manifest's `dockerImages`. Same.
|
|
24237
|
+
*
|
|
24238
|
+
* Throws on a malformed manifest (parse failure surfaced via
|
|
24239
|
+
* {@link AssetManifestLoader}) so the caller can fall back to rebuild
|
|
24240
|
+
* with a warn line that explains why the classifier couldn't run.
|
|
24241
|
+
*/
|
|
24242
|
+
async function loadAssetContextForTarget(args) {
|
|
24243
|
+
const { target, controller, stacks, cdkOutDir, assetLoader, logger } = args;
|
|
24244
|
+
const candidate = pickCandidateStack(parseEcsTarget(target).stackPattern, stacks);
|
|
24245
|
+
if (!candidate) return void 0;
|
|
24246
|
+
let newService;
|
|
24247
|
+
try {
|
|
24248
|
+
newService = resolveEcsServiceTarget(target, stacks, void 0, { suppressLoadBalancerWarning: true });
|
|
24249
|
+
} catch (err) {
|
|
24250
|
+
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).`);
|
|
24251
|
+
return;
|
|
24252
|
+
}
|
|
24253
|
+
const essential = newService.task.containers.find((c) => c.essential) ?? newService.task.containers[0];
|
|
24254
|
+
if (!essential) return void 0;
|
|
24255
|
+
if (essential.image.kind !== "cdk-asset" || !essential.image.assetHash) return;
|
|
24256
|
+
const newAssetHash = essential.image.assetHash;
|
|
24257
|
+
const manifest = await assetLoader.loadManifest(cdkOutDir, candidate.stackName);
|
|
24258
|
+
if (!manifest) return void 0;
|
|
24259
|
+
const newDockerImage = manifest.dockerImages?.[newAssetHash];
|
|
24260
|
+
if (!newDockerImage) return void 0;
|
|
24261
|
+
if (!newDockerImage.source.directory) return;
|
|
24262
|
+
const newAssetSourceDir = path.resolve(cdkOutDir, newDockerImage.source.directory);
|
|
24263
|
+
let oldAssetHash;
|
|
24264
|
+
const oldEssential = controller.service.task.containers.find((c) => c.essential) ?? controller.service.task.containers[0];
|
|
24265
|
+
if (oldEssential?.image.kind === "cdk-asset") oldAssetHash = oldEssential.image.assetHash;
|
|
24266
|
+
return {
|
|
24267
|
+
...oldAssetHash !== void 0 && { oldAssetHash },
|
|
24268
|
+
newAssetHash,
|
|
24269
|
+
newAssetSourceDir,
|
|
24270
|
+
dockerFile: path.basename(newDockerImage.source.dockerFile ?? "Dockerfile")
|
|
24271
|
+
};
|
|
24272
|
+
}
|
|
24273
|
+
/**
|
|
23839
24274
|
* Phase 2 of issue #214 — roll every replica of one target through the
|
|
23840
24275
|
* new task descriptor sequentially. Extracted from {@link reloadAllServices}
|
|
23841
24276
|
* so the per-target try/catch logic (synth-failure / resolve-failure /
|
|
@@ -23846,7 +24281,7 @@ async function reloadAllServices(args) {
|
|
|
23846
24281
|
* even when the resolve / roll throws.
|
|
23847
24282
|
*/
|
|
23848
24283
|
async function rollOneTarget(args) {
|
|
23849
|
-
const { controller, newBoot, stacks, options, discovery, skipPull, extraStateProviders, profileCredsFile, frontDoorPools, suppressLoadBalancerWarning, logger } = args;
|
|
24284
|
+
const { controller, newBoot, stacks, options, discovery, skipPull, extraStateProviders, profileCredsFile, frontDoorPools, suppressLoadBalancerWarning, verdict, logger } = args;
|
|
23850
24285
|
const candidate = pickCandidateStack(parseEcsTarget(newBoot.target).stackPattern, stacks);
|
|
23851
24286
|
const stateProvider = createLocalStateProvider(options, candidate?.stackName ?? "", await resolveCfnFallbackRegion(options, candidate?.region), extraStateProviders);
|
|
23852
24287
|
try {
|
|
@@ -23865,7 +24300,8 @@ async function rollOneTarget(args) {
|
|
|
23865
24300
|
return;
|
|
23866
24301
|
}
|
|
23867
24302
|
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}':
|
|
24303
|
+
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).`);
|
|
24304
|
+
else logger.info(`Reload of '${newBoot.target}': rolling ${oldReplicas.length} replica(s) one at a time (start new shadow → swap registrations → stop old).`);
|
|
23869
24305
|
for (const oldInstance of oldReplicas) {
|
|
23870
24306
|
const idx = controller.runState.replicas.indexOf(oldInstance);
|
|
23871
24307
|
if (idx === -1) {
|
|
@@ -23873,7 +24309,13 @@ async function rollOneTarget(args) {
|
|
|
23873
24309
|
continue;
|
|
23874
24310
|
}
|
|
23875
24311
|
try {
|
|
23876
|
-
await
|
|
24312
|
+
if (verdict.kind === "soft-reload") await softReloadReplica({
|
|
24313
|
+
controller,
|
|
24314
|
+
oldReplicaIndex: idx,
|
|
24315
|
+
newService,
|
|
24316
|
+
sourceDirToCopy: verdict.newAssetSourceDir
|
|
24317
|
+
});
|
|
24318
|
+
else await rollServiceReplica({
|
|
23877
24319
|
controller,
|
|
23878
24320
|
oldReplicaIndex: idx,
|
|
23879
24321
|
newService,
|
|
@@ -24400,9 +24842,9 @@ function addCommonEcsServiceOptions(cmd) {
|
|
|
24400
24842
|
* `supportsWatch: true` opts this strategy into the emulator's `--watch`
|
|
24401
24843
|
* reload pathway (Phase 1 + Phase 2 of issue #214 — per-replica rolling
|
|
24402
24844
|
* deploy: shadow boot under a bumped generation suffix, TCP-ready probe,
|
|
24403
|
-
* atomic Cloud Map / front-door swap, retire old).
|
|
24404
|
-
*
|
|
24405
|
-
* the
|
|
24845
|
+
* atomic Cloud Map / front-door swap, retire old). The ALB strategy
|
|
24846
|
+
* (`albStrategy()`) opts in symmetrically via Phase 3 of the same issue;
|
|
24847
|
+
* the same rolling primitive serves both paths.
|
|
24406
24848
|
*/
|
|
24407
24849
|
function serviceStrategy() {
|
|
24408
24850
|
return {
|
|
@@ -24450,12 +24892,15 @@ function createLocalStartServiceCommand(opts = {}) {
|
|
|
24450
24892
|
* `--help` clusters. Chainable: returns `cmd`.
|
|
24451
24893
|
*
|
|
24452
24894
|
* `--watch` is intentionally NOT in the shared
|
|
24453
|
-
* {@link addCommonEcsServiceOptions} block
|
|
24454
|
-
*
|
|
24455
|
-
*
|
|
24895
|
+
* {@link addCommonEcsServiceOptions} block even though both consumers
|
|
24896
|
+
* (`start-service`, `start-alb`) now honor it (Phase 1-3 of issue
|
|
24897
|
+
* #214): each command's `--watch` help string describes its own
|
|
24898
|
+
* rolling-deploy contract (Service Connect / Cloud Map swap vs ALB
|
|
24899
|
+
* front-door pool swap), and the per-command block is the right place
|
|
24900
|
+
* for that contract to live.
|
|
24456
24901
|
*/
|
|
24457
24902
|
function addStartServiceSpecificOptions(cmd) {
|
|
24458
|
-
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
|
|
24903
|
+
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));
|
|
24459
24904
|
}
|
|
24460
24905
|
|
|
24461
24906
|
//#endregion
|
|
@@ -25273,7 +25718,8 @@ function albStrategy(options) {
|
|
|
25273
25718
|
};
|
|
25274
25719
|
},
|
|
25275
25720
|
lbPortOverrides,
|
|
25276
|
-
suppressLoadBalancerWarning: true
|
|
25721
|
+
suppressLoadBalancerWarning: true,
|
|
25722
|
+
supportsWatch: true
|
|
25277
25723
|
};
|
|
25278
25724
|
}
|
|
25279
25725
|
/**
|
|
@@ -25307,7 +25753,7 @@ function createLocalStartAlbCommand(opts = {}) {
|
|
|
25307
25753
|
* clusters. Chainable: returns `cmd`.
|
|
25308
25754
|
*/
|
|
25309
25755
|
function addAlbSpecificOptions(cmd) {
|
|
25310
|
-
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."));
|
|
25756
|
+
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));
|
|
25311
25757
|
}
|
|
25312
25758
|
|
|
25313
25759
|
//#endregion
|
|
@@ -25403,5 +25849,5 @@ function addListSpecificOptions(cmd) {
|
|
|
25403
25849
|
}
|
|
25404
25850
|
|
|
25405
25851
|
//#endregion
|
|
25406
|
-
export {
|
|
25407
|
-
//# sourceMappingURL=local-list-
|
|
25852
|
+
export { applyAuthorizerOverlay as $, resolveRuntimeCodeMountPath as $t, createLocalStartApiCommand as A, AGENTCORE_AGUI_PROTOCOL as An, A2A_CONTAINER_PORT as At, groupRoutesByServer as B, tryResolveImageFnJoin as Bn, AGENTCORE_SESSION_ID_HEADER as Bt, classifySourceChange as C, discoverWebSocketApis as Cn, attachAuthorizers as Ct, addInvokeAgentCoreSpecificOptions as D, pickRefLogicalId as Dn, isFunctionUrlOacFronted as Dt, createLocalRunTaskCommand as E, discoverRoutes as En, buildCorsConfigFromCloudFrontChain as Et, attachStageContext as F, pickAgentCoreCandidateStack as Fn, MCP_PROTOCOL_VERSION as Ft, defaultCredentialsLoader as G, buildAgentCoreCodeImage as Gt, startApiServer as H, waitForAgentCorePing as Ht, buildStageMap as I, resolveAgentCoreTarget as In, mcpInvokeOnce as It, evaluateCachedLambdaPolicy as J, toCmdArgv as Jt, buildMethodArn as K, computeCodeImageTag as Kt, availableApiIdentifiers as L, derivePseudoParametersFromRegion as Ln, parseSseForJsonRpc as Lt, resolveApiTargetSubset as M, AGENTCORE_MCP_PROTOCOL as Mn, a2aInvokeOnce as Mt, createAuthorizerCache as N, AGENTCORE_RUNTIME_TYPE as Nn, MCP_CONTAINER_PORT as Nt, createLocalInvokeAgentCoreCommand as O, resolveLambdaArnIntrinsic as On, matchPreflight as Ot, createFileWatcher as P, AgentCoreResolutionError as Pn, MCP_PATH as Pt, translateLambdaResponse as Q, buildContainerImage as Qt, filterRoutesByApiIdentifier as R, formatStateRemedy as Rn, AGENTCORE_SIGV4_SERVICE as Rt, CloudMapRegistry as S, listTargets as Sn, verifyJwtViaDiscovery as St, addRunTaskSpecificOptions as T, parseSelectionExpressionPath as Tn, buildCorsConfigByApiId as Tt, resolveSelectionExpression as U, downloadAndExtractS3Bundle as Ut, readMtlsMaterialsFromDisk as V, LocalInvokeBuildError as Vn, invokeAgentCore as Vt, resolveServiceIntegrationParameters as W, SUPPORTED_CODE_RUNTIMES as Wt, invokeTokenAuthorizer as X, createLocalInvokeCommand as Xt, invokeRequestAuthorizer as Y, addInvokeSpecificOptions as Yt, matchRoute as Z, architectureToPlatform as Zt, parseMaxTasks as _, collectSsmParameterRefs as _n, buildCognitoJwksUrl as _t, albStrategy as a, substituteEnvVarsFromState as an, tryParseStatus as at, runEcsServiceEmulator as b, resolveSingleTarget as bn, verifyCognitoJwt as bt, resolveAlbTarget as c, materializeLayerFromArn as cn, probeHostGatewaySupport as ct, addStartServiceSpecificOptions as d, isCfnFlagPresent as dn, buildMgmtEndpointEnvUrl as dt, resolveRuntimeFileExtension as en, buildHttpApiV2Event as et, createLocalStartServiceCommand as f, rejectExplicitCfnStackWithMultipleStacks as fn, handleConnectionsRequest as ft, buildEcsImageResolutionContext as g, CfnLocalStateProvider as gn, buildMessageEvent as gt, addCommonEcsServiceOptions as h, resolveCfnStackName as hn, buildDisconnectEvent as ht, addAlbSpecificOptions as i, substituteAgainstStateAsync as in, selectIntegrationResponse as it, createWatchPredicates as j, AGENTCORE_HTTP_PROTOCOL as jn, A2A_PATH as jt, addStartApiSpecificOptions as k, AGENTCORE_A2A_PROTOCOL as kn, invokeAgentCoreWs as kt, isApplicationLoadBalancer as l, LocalStateSourceError as ln, bufferToBody as lt, MAX_TASKS_SUBNET_RANGE_CAP as m, resolveCfnRegion as mn, buildConnectEvent as mt, createLocalListCommand as n, EcsTaskResolutionError as nn, evaluateResponseParameters as nt, createLocalStartAlbCommand as o, substituteEnvVarsFromStateAsync as on, VtlEvaluationError as ot, serviceStrategy as p, resolveCfnFallbackRegion as pn, parseConnectionsPath as pt, computeRequestIdentityHash as q, renderCodeDockerfile as qt, formatTargetListing as r, substituteAgainstState as rn, pickResponseTemplate as rt, parseLbPortOverrides as s, resolveEnvVars as sn, HOST_GATEWAY_MIN_VERSION as st, addListSpecificOptions as t, resolveRuntimeImage as tn, buildRestV1Event as tt, resolveAlbFrontDoor as u, createLocalStateProvider as un, ConnectionRegistry as ut, parseRestartPolicy as v, resolveSsmParameters as vn, buildJwksUrlFromIssuer as vt, getContainerNetworkIp as w, discoverWebSocketApisOrThrow as wn, applyCorsResponseHeaders as wt, buildCloudMapIndex as x, countTargets as xn, verifyJwtAuthorizer as xt, resolveSharedSidecarCredentials as y, resolveWatchConfig as yn, createJwksCache as yt, filterRoutesByApiIdentifiers as z, substituteImagePlaceholders as zn, signAgentCoreInvocation as zt };
|
|
25853
|
+
//# sourceMappingURL=local-list-l2_7oGHF.js.map
|