cdk-local 0.153.1 → 0.153.2
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/dist/cli.js +2 -2
- package/dist/index.js +1 -1
- package/dist/internal.d.ts +7 -2
- package/dist/internal.d.ts.map +1 -1
- package/dist/internal.js +2 -2
- package/dist/{local-studio-tC3I-pTi.js → local-studio-DCTVck5l.js} +554 -106
- package/dist/local-studio-DCTVck5l.js.map +1 -0
- package/dist/local-studio-DDFmguSG.d.ts.map +1 -1
- package/package.json +3 -3
- package/dist/local-studio-tC3I-pTi.js.map +0 -1
|
@@ -74,6 +74,36 @@ function hostFromAuthority(authority) {
|
|
|
74
74
|
return authority.slice(0, firstColon);
|
|
75
75
|
}
|
|
76
76
|
|
|
77
|
+
//#endregion
|
|
78
|
+
//#region src/utils/loopback-host.ts
|
|
79
|
+
function normaliseHostLiteral(hostname) {
|
|
80
|
+
return hostname.trim().replace(/^\[/, "").replace(/\]$/, "").toLowerCase();
|
|
81
|
+
}
|
|
82
|
+
function isLoopbackLiteral(h) {
|
|
83
|
+
if (h === "localhost") return true;
|
|
84
|
+
if (h === "::1" || h === "0:0:0:0:0:0:0:1") return true;
|
|
85
|
+
const mapped = /^::ffff:([0-9a-f]{1,4}):([0-9a-f]{1,4})$/.exec(h);
|
|
86
|
+
if (mapped) return parseInt(mapped[1], 16) >> 8 === 127;
|
|
87
|
+
const dotted = /^(?:::ffff:)?(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/.exec(h);
|
|
88
|
+
if (!dotted) return false;
|
|
89
|
+
const octets = dotted.slice(1).map(Number);
|
|
90
|
+
if (octets.some((o) => o > 255)) return false;
|
|
91
|
+
return octets[0] === 127;
|
|
92
|
+
}
|
|
93
|
+
function isWildcardLiteral(h) {
|
|
94
|
+
return h === "0.0.0.0" || h === "::" || h === "0:0:0:0:0:0:0:0" || h === "::ffff:0.0.0.0" || h === "::ffff:0:0";
|
|
95
|
+
}
|
|
96
|
+
function isLoopbackHostname(hostname) {
|
|
97
|
+
return isLoopbackLiteral(normaliseHostLiteral(hostname));
|
|
98
|
+
}
|
|
99
|
+
function isWildcardHostname(hostname) {
|
|
100
|
+
return isWildcardLiteral(normaliseHostLiteral(hostname));
|
|
101
|
+
}
|
|
102
|
+
function isLoopbackHost(hostname) {
|
|
103
|
+
const h = normaliseHostLiteral(hostname).replace(/\.$/, "");
|
|
104
|
+
return h.endsWith(".localhost") || isLoopbackLiteral(h) || isWildcardLiteral(h);
|
|
105
|
+
}
|
|
106
|
+
|
|
77
107
|
//#endregion
|
|
78
108
|
//#region src/utils/aws-proxy.ts
|
|
79
109
|
const AGENT_OPTS = {
|
|
@@ -81,8 +111,23 @@ const AGENT_OPTS = {
|
|
|
81
111
|
maxSockets: 50
|
|
82
112
|
};
|
|
83
113
|
function isProxyEnvConfigured() {
|
|
84
|
-
|
|
85
|
-
|
|
114
|
+
return PROXY_ENV_VARIABLES.some((key) => Boolean(process.env[key]));
|
|
115
|
+
}
|
|
116
|
+
const PROXY_ENV_VARIABLES = [
|
|
117
|
+
"https_proxy",
|
|
118
|
+
"HTTPS_PROXY",
|
|
119
|
+
"http_proxy",
|
|
120
|
+
"HTTP_PROXY",
|
|
121
|
+
"all_proxy",
|
|
122
|
+
"ALL_PROXY"
|
|
123
|
+
];
|
|
124
|
+
function isSpeakableProxyEnvValue(value) {
|
|
125
|
+
return !value.includes("://") || isSpeakableProxy(value.trim());
|
|
126
|
+
}
|
|
127
|
+
const AWS_ENDPOINT_PROBES = ["https://sts.amazonaws.com", "http://sts.amazonaws.com"];
|
|
128
|
+
function everySetProxyEnvValueIsUnspeakable() {
|
|
129
|
+
const set = PROXY_ENV_VARIABLES.map((key) => process.env[key] ?? "").filter((v) => v !== "");
|
|
130
|
+
return set.length > 0 && !set.some(isSpeakableProxyEnvValue);
|
|
86
131
|
}
|
|
87
132
|
const warnedProxySchemes = /* @__PURE__ */ new Set();
|
|
88
133
|
function proxySchemeOf(proxyUrl) {
|
|
@@ -96,7 +141,7 @@ function warnUnspeakableProxy(proxyUrl) {
|
|
|
96
141
|
const scheme = proxySchemeOf(proxyUrl);
|
|
97
142
|
if (warnedProxySchemes.has(scheme)) return;
|
|
98
143
|
try {
|
|
99
|
-
getLogger().child("aws-proxy").warn(`Unsupported proxy scheme "${scheme}" — cdk-local speaks HTTP CONNECT proxies only, so
|
|
144
|
+
getLogger().child("aws-proxy").warn(`Unsupported proxy scheme "${scheme}" — cdk-local speaks HTTP CONNECT proxies only, so requests it would carry go DIRECT instead of through the proxy. Set HTTPS_PROXY / HTTP_PROXY / ALL_PROXY to an http:// or https:// proxy URL. (The proxy URL is withheld here: it can carry credentials.)`);
|
|
100
145
|
} catch {
|
|
101
146
|
return;
|
|
102
147
|
}
|
|
@@ -147,6 +192,10 @@ function buildProxyRequestHandler() {
|
|
|
147
192
|
}
|
|
148
193
|
function buildProxyClientConfig(opts = {}) {
|
|
149
194
|
if (!isProxyEnvConfigured()) return {};
|
|
195
|
+
if (everySetProxyEnvValueIsUnspeakable()) {
|
|
196
|
+
for (const probe of AWS_ENDPOINT_PROBES) resolveProxyForTarget(probe);
|
|
197
|
+
return {};
|
|
198
|
+
}
|
|
150
199
|
const profile = opts.profile;
|
|
151
200
|
let chain;
|
|
152
201
|
const credentials = async (identityProperties) => {
|
|
@@ -177,19 +226,6 @@ const REQUEST_STALL_TIMEOUT_MS = 3e5;
|
|
|
177
226
|
function isRedirectStatus(status) {
|
|
178
227
|
return status === 301 || status === 302 || status === 303 || status === 307 || status === 308;
|
|
179
228
|
}
|
|
180
|
-
function isLoopbackHost(hostname) {
|
|
181
|
-
const host = hostname.trim().replace(/^\[|\]$/g, "").replace(/\.$/, "").toLowerCase();
|
|
182
|
-
if (host === "localhost" || host.endsWith(".localhost")) return true;
|
|
183
|
-
if (host === "::1" || host === "0:0:0:0:0:0:0:1") return true;
|
|
184
|
-
if (host === "0.0.0.0" || host === "::" || host === "0:0:0:0:0:0:0:0" || host === "::ffff:0.0.0.0" || host === "::ffff:0:0") return true;
|
|
185
|
-
const mapped = /^::ffff:([0-9a-f]{1,4}):([0-9a-f]{1,4})$/.exec(host);
|
|
186
|
-
if (mapped) return parseInt(mapped[1], 16) >> 8 === 127;
|
|
187
|
-
const dotted = /^(?:::ffff:)?(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/.exec(host);
|
|
188
|
-
if (!dotted) return false;
|
|
189
|
-
const octets = dotted.slice(1).map(Number);
|
|
190
|
-
if (octets.some((o) => o > 255)) return false;
|
|
191
|
-
return octets[0] === 127;
|
|
192
|
-
}
|
|
193
229
|
function parseHttpUrl(href) {
|
|
194
230
|
const parsed = new URL(href);
|
|
195
231
|
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") throw new TypeError(`Unsupported protocol for a proxied request: "${parsed.protocol}"`);
|
|
@@ -15905,7 +15941,7 @@ function addInvokeAgentCoreSpecificOptions(cmd) {
|
|
|
15905
15941
|
|
|
15906
15942
|
//#endregion
|
|
15907
15943
|
//#region src/local/container-log-streamer.ts
|
|
15908
|
-
function attachContainerLogStreamer(prefix, containerId) {
|
|
15944
|
+
function attachContainerLogStreamer(prefix, containerId, opts = {}) {
|
|
15909
15945
|
let stopping = false;
|
|
15910
15946
|
let current;
|
|
15911
15947
|
let stdoutBuf = "";
|
|
@@ -15940,23 +15976,35 @@ function attachContainerLogStreamer(prefix, containerId) {
|
|
|
15940
15976
|
return;
|
|
15941
15977
|
}
|
|
15942
15978
|
reattachCount += 1;
|
|
15943
|
-
|
|
15944
|
-
|
|
15945
|
-
|
|
15946
|
-
|
|
15947
|
-
|
|
15948
|
-
|
|
15949
|
-
|
|
15950
|
-
|
|
15951
|
-
|
|
15952
|
-
|
|
15979
|
+
considerRespawn();
|
|
15980
|
+
});
|
|
15981
|
+
};
|
|
15982
|
+
const considerRespawn = () => {
|
|
15983
|
+
execFile(getDockerCmd(), [
|
|
15984
|
+
"inspect",
|
|
15985
|
+
"--format",
|
|
15986
|
+
"{{.State.Status}}",
|
|
15987
|
+
containerId
|
|
15988
|
+
], (err, stdout) => {
|
|
15989
|
+
if (stopping) return;
|
|
15990
|
+
if (!err) {
|
|
15991
|
+
const status = stdout.trim().toLowerCase();
|
|
15992
|
+
if (status === "exited" || status === "dead" || status === "removing") {
|
|
15993
|
+
if (opts.expectRestart?.()) {
|
|
15994
|
+
pendingRespawn = setTimeout(() => {
|
|
15995
|
+
pendingRespawn = void 0;
|
|
15996
|
+
if (!stopping) considerRespawn();
|
|
15997
|
+
}, 200);
|
|
15998
|
+
return;
|
|
15999
|
+
}
|
|
16000
|
+
return;
|
|
15953
16001
|
}
|
|
15954
|
-
|
|
15955
|
-
|
|
15956
|
-
|
|
15957
|
-
|
|
15958
|
-
|
|
15959
|
-
});
|
|
16002
|
+
}
|
|
16003
|
+
pendingRespawn = setTimeout(() => {
|
|
16004
|
+
pendingRespawn = void 0;
|
|
16005
|
+
if (stopping) return;
|
|
16006
|
+
spawnOnce("0s");
|
|
16007
|
+
}, 200);
|
|
15960
16008
|
});
|
|
15961
16009
|
};
|
|
15962
16010
|
spawnOnce(void 0);
|
|
@@ -16442,6 +16490,7 @@ async function runEcsTask(task, options, state) {
|
|
|
16442
16490
|
...options.ephemeralPublishContainerPorts && options.ephemeralPublishContainerPorts.length > 0 ? { ephemeralPublishContainerPorts: options.ephemeralPublishContainerPorts } : {},
|
|
16443
16491
|
...mergedAddHostFlags.length > 0 ? { addHostFlags: mergedAddHostFlags } : {},
|
|
16444
16492
|
...(options.networkAliasesByContainer?.get(container.name)?.length ?? 0) > 0 ? { networkAliases: options.networkAliasesByContainer.get(container.name) } : {},
|
|
16493
|
+
...options.containerNameAliasOmitted?.has(container.name) ? { omitContainerNameAlias: true } : {},
|
|
16445
16494
|
...options.profileCredentialsFile && { profileCredentialsFile: options.profileCredentialsFile }
|
|
16446
16495
|
});
|
|
16447
16496
|
dockerCmds.set(container.name, {
|
|
@@ -16757,7 +16806,7 @@ function buildDockerRunArgs(opts) {
|
|
|
16757
16806
|
const publishedEndpoints = [];
|
|
16758
16807
|
args.push("--name", `${getEmbedConfig().resourceNamePrefix}-${task.family}-${container.name}-${randHex(3)}`);
|
|
16759
16808
|
args.push("--network", network);
|
|
16760
|
-
args.push("--network-alias", container.name);
|
|
16809
|
+
if (!opts.omitContainerNameAlias) args.push("--network-alias", container.name);
|
|
16761
16810
|
if (opts.networkAliases && opts.networkAliases.length > 0) {
|
|
16762
16811
|
const seen = /* @__PURE__ */ new Set([container.name]);
|
|
16763
16812
|
for (const a of opts.networkAliases) if (!seen.has(a)) {
|
|
@@ -17123,6 +17172,7 @@ var EcsServiceRunnerError = class EcsServiceRunnerError extends Error {
|
|
|
17123
17172
|
}
|
|
17124
17173
|
};
|
|
17125
17174
|
const SOFT_RELOAD_COMPLETION_LOG_SUFFIX = "restart + TCP-ready probe complete; Cloud Map + front-door re-published.";
|
|
17175
|
+
const SOFT_RELOAD_PROBE_TIMEOUT_LOG_SUFFIX = "restarted, but the TCP-ready probe timed out; Cloud Map + front-door re-published anyway.";
|
|
17126
17176
|
function createServiceRunState() {
|
|
17127
17177
|
return {
|
|
17128
17178
|
replicas: [],
|
|
@@ -17249,7 +17299,7 @@ function buildNetworkAliasesByContainer(service) {
|
|
|
17249
17299
|
}
|
|
17250
17300
|
return out;
|
|
17251
17301
|
}
|
|
17252
|
-
async function bootReplica(service, options, instance) {
|
|
17302
|
+
async function bootReplica(service, options, instance, bootOpts = {}) {
|
|
17253
17303
|
const logger = getLogger().child("ecs-service");
|
|
17254
17304
|
const gen = instance.generation;
|
|
17255
17305
|
const genSuffix = gen > 0 ? `-g${gen}` : "";
|
|
@@ -17261,6 +17311,18 @@ async function bootReplica(service, options, instance) {
|
|
|
17261
17311
|
const networkAliasesByContainer = buildNetworkAliasesByContainer(service);
|
|
17262
17312
|
const skipHostPortPublish = computeReplicaCount(service.desiredCount, options.maxTasks) > 1;
|
|
17263
17313
|
const ephemeralPublishContainerPorts = options.frontDoor ? [...new Set(options.frontDoor.pools.map((p) => p.targetContainerPort))] : [];
|
|
17314
|
+
const withheldAliasContainers = [];
|
|
17315
|
+
if (bootOpts.deferPublish && sharedNetwork) for (const container of service.task.containers) {
|
|
17316
|
+
if (!networkAliasesByContainer.has(container.name)) continue;
|
|
17317
|
+
if (containerPublishesHostPorts(container, {
|
|
17318
|
+
skipHostPortPublish,
|
|
17319
|
+
ephemeralPublishContainerPorts
|
|
17320
|
+
})) continue;
|
|
17321
|
+
networkAliasesByContainer.delete(container.name);
|
|
17322
|
+
withheldAliasContainers.push(container.name);
|
|
17323
|
+
}
|
|
17324
|
+
const containerNameAliasOmitted = new Set(withheldAliasContainers.filter((name) => (buildNetworkAliasesByContainer(service).get(name) ?? []).includes(name)));
|
|
17325
|
+
instance.withheldAliasContainers = withheldAliasContainers;
|
|
17264
17326
|
const perReplicaTaskOptions = {
|
|
17265
17327
|
...options.taskOptions,
|
|
17266
17328
|
cluster: perReplicaCluster,
|
|
@@ -17269,17 +17331,62 @@ async function bootReplica(service, options, instance) {
|
|
|
17269
17331
|
...sharedNetwork ? { existingNetwork: sharedNetwork } : { subnetOctet: pickSubnetOctet(instance.index) },
|
|
17270
17332
|
...addHostFlags.length > 0 ? { addHostFlags } : {},
|
|
17271
17333
|
...networkAliasesByContainer.size > 0 ? { networkAliasesByContainer } : {},
|
|
17334
|
+
...containerNameAliasOmitted.size > 0 ? { containerNameAliasOmitted } : {},
|
|
17272
17335
|
...ephemeralPublishContainerPorts.length > 0 ? { ephemeralPublishContainerPorts } : {}
|
|
17273
17336
|
};
|
|
17274
17337
|
logger.info(`Booting replica ${instance.index} (${perReplicaCluster})`);
|
|
17275
17338
|
await runEcsTask(service.task, perReplicaTaskOptions, instance.state);
|
|
17276
17339
|
if (options.streamLogs !== false) for (const started of instance.state.startedContainers) {
|
|
17277
17340
|
const prefix = `[svc=${service.serviceDisplayName} r=${instance.index} c=${started.name}] `;
|
|
17278
|
-
instance.state.logStoppers.push(attachContainerLogStreamer(prefix, started.id));
|
|
17341
|
+
instance.state.logStoppers.push(attachContainerLogStreamer(prefix, started.id, { expectRestart: () => instance.softReloadInProgress === true }));
|
|
17279
17342
|
}
|
|
17343
|
+
if (options.discovery && !bootOpts.deferPublish) await publishReplicaToCloudMap(service, instance, options.discovery, ownerKeyPrefix);
|
|
17344
|
+
if (options.frontDoor && !bootOpts.deferPublish) await publishReplicaToFrontDoor(service, instance, options.frontDoor, options.taskOptions.containerHost, ownerKeyPrefix);
|
|
17345
|
+
instance.lastDeployedAssetHash = pickEssentialAssetHash(service);
|
|
17346
|
+
}
|
|
17347
|
+
function containerPublishesHostPorts(container, opts) {
|
|
17348
|
+
if (container.portMappings.length === 0) return false;
|
|
17349
|
+
if (!opts.skipHostPortPublish) return true;
|
|
17350
|
+
return container.portMappings.some((pm) => opts.ephemeralPublishContainerPorts.includes(pm.containerPort));
|
|
17351
|
+
}
|
|
17352
|
+
async function publishGatedReplica(service, options, instance, deadline, isShuttingDown, label = `Shadow replica r${instance.index} (gen ${instance.generation})`) {
|
|
17353
|
+
const network = instance.state.network;
|
|
17354
|
+
const withheld = instance.withheldAliasContainers ?? [];
|
|
17355
|
+
if (network && withheld.length > 0) {
|
|
17356
|
+
const fullAliases = buildNetworkAliasesByContainer(service);
|
|
17357
|
+
for (const name of withheld) {
|
|
17358
|
+
if (isShuttingDown()) return;
|
|
17359
|
+
const container = service.task.containers.find((c) => c.name === name);
|
|
17360
|
+
const started = instance.state.startedContainers.find((c) => c.name === name);
|
|
17361
|
+
if (!container || !started) continue;
|
|
17362
|
+
const port = container.portMappings[0]?.containerPort;
|
|
17363
|
+
if (port !== void 0) {
|
|
17364
|
+
if (await waitForContainerListening(started.id, port, deadline) === "exited") {
|
|
17365
|
+
getLogger().child("ecs-service").debug(`${label}: '${name}' exited before listening; leaving its aliases withheld.`);
|
|
17366
|
+
continue;
|
|
17367
|
+
}
|
|
17368
|
+
}
|
|
17369
|
+
if (isShuttingDown()) return;
|
|
17370
|
+
const ip = await getContainerNetworkIp(started.id, network.networkName);
|
|
17371
|
+
if (!ip) throw new EcsServiceRunnerError(`${label}: no docker IP for '${name}' on ${network.networkName}; cannot attach its Service Connect aliases.`);
|
|
17372
|
+
const aliases = [name];
|
|
17373
|
+
for (const a of fullAliases.get(name) ?? []) if (!aliases.includes(a)) aliases.push(a);
|
|
17374
|
+
await dockerNetworkDisconnectImpl(network.networkName, started.id);
|
|
17375
|
+
await dockerNetworkConnectImpl(network.networkName, started.id, {
|
|
17376
|
+
ip,
|
|
17377
|
+
aliases
|
|
17378
|
+
});
|
|
17379
|
+
}
|
|
17380
|
+
instance.withheldAliasContainers = [];
|
|
17381
|
+
}
|
|
17382
|
+
if (isShuttingDown()) return;
|
|
17383
|
+
const ownerKeyPrefix = replicaOwnerKeyPrefix(service, instance);
|
|
17280
17384
|
if (options.discovery) await publishReplicaToCloudMap(service, instance, options.discovery, ownerKeyPrefix);
|
|
17281
17385
|
if (options.frontDoor) await publishReplicaToFrontDoor(service, instance, options.frontDoor, options.taskOptions.containerHost, ownerKeyPrefix);
|
|
17282
|
-
|
|
17386
|
+
}
|
|
17387
|
+
function replicaOwnerKeyPrefix(service, instance) {
|
|
17388
|
+
const genSuffix = instance.generation > 0 ? `:g${instance.generation}` : "";
|
|
17389
|
+
return `${service.serviceLogicalId}:r${instance.index}${genSuffix}`;
|
|
17283
17390
|
}
|
|
17284
17391
|
function pickEssentialAssetHash(service) {
|
|
17285
17392
|
const essential = service.task.containers.find((c) => c.essential) ?? service.task.containers[0];
|
|
@@ -17433,11 +17540,14 @@ async function rollServiceReplica(args) {
|
|
|
17433
17540
|
if (oldIdx !== -1) controller.runState.replicas.splice(oldIdx, 1);
|
|
17434
17541
|
} else logger.info(`Rolling replica ${shadow.index} (gen ${shadow.generation}): booting shadow before retiring old.`);
|
|
17435
17542
|
const bootPromise = (async () => {
|
|
17436
|
-
await bootReplica(newService, newOptions, shadow);
|
|
17543
|
+
await bootReplica(newService, newOptions, shadow, { deferPublish: !teardownOldFirst });
|
|
17544
|
+
const deadline = Date.now() + shadowReadyTimeoutMs;
|
|
17437
17545
|
await waitForReplicaTcpReady(newService, shadow, {
|
|
17438
|
-
timeoutMs:
|
|
17439
|
-
intervalMs: shadowReadyIntervalMs
|
|
17546
|
+
timeoutMs: Math.max(1, deadline - Date.now()),
|
|
17547
|
+
intervalMs: shadowReadyIntervalMs,
|
|
17548
|
+
isShuttingDown: () => shadow.shuttingDown || controller.runState.shuttingDown
|
|
17440
17549
|
});
|
|
17550
|
+
if (!teardownOldFirst) await publishGatedReplica(newService, newOptions, shadow, deadline, () => shadow.shuttingDown || controller.runState.shuttingDown);
|
|
17441
17551
|
})();
|
|
17442
17552
|
shadow.inFlightBoot = bootPromise;
|
|
17443
17553
|
try {
|
|
@@ -17445,6 +17555,7 @@ async function rollServiceReplica(args) {
|
|
|
17445
17555
|
} catch (err) {
|
|
17446
17556
|
const shadowIdx = controller.runState.replicas.indexOf(shadow);
|
|
17447
17557
|
if (shadowIdx !== -1) controller.runState.replicas.splice(shadowIdx, 1);
|
|
17558
|
+
await disconnectOldFromSharedNetwork(shadow).catch(() => void 0);
|
|
17448
17559
|
try {
|
|
17449
17560
|
await cleanupEcsRun(shadow.state, { keepRunning: false });
|
|
17450
17561
|
} catch {}
|
|
@@ -17465,7 +17576,8 @@ async function rollServiceReplica(args) {
|
|
|
17465
17576
|
oldInstance.cloudMapHandles = [];
|
|
17466
17577
|
}
|
|
17467
17578
|
unregisterReplicaFromFrontDoor(oldInstance, newOptions.frontDoor);
|
|
17468
|
-
|
|
17579
|
+
oldInstance.shuttingDown = true;
|
|
17580
|
+
await disconnectOldFromSharedNetwork(oldInstance, { drain: true }).catch((err) => {
|
|
17469
17581
|
logger.debug(`Rolling replica ${oldInstance.index}: shared-network disconnect of old (gen ${oldInstance.generation}) failed: ${err instanceof Error ? err.message : String(err)}. Proceeding with cleanup (the docker-rm step still tears it down).`);
|
|
17470
17582
|
});
|
|
17471
17583
|
oldInstance.shuttingDown = true;
|
|
@@ -17492,13 +17604,19 @@ async function softReloadReplica(args) {
|
|
|
17492
17604
|
const containersToCycle = essentialContainers.length > 0 ? essentialContainers : newService.task.containers.length > 0 ? [newService.task.containers[0]] : [];
|
|
17493
17605
|
if (containersToCycle.length === 0) throw new EcsServiceRunnerError(`softReloadReplica: service '${newService.serviceLogicalId}' has no containers to cycle.`);
|
|
17494
17606
|
const startedById = new Map(instance.state.startedContainers.map((c) => [c.name, c.id]));
|
|
17607
|
+
const networkAliasesByContainer = buildNetworkAliasesByContainer(newService);
|
|
17495
17608
|
const targets = [];
|
|
17496
17609
|
for (const container of containersToCycle) {
|
|
17497
17610
|
const id = startedById.get(container.name);
|
|
17498
17611
|
if (!id) throw new EcsServiceRunnerError(`softReloadReplica: replica r${instance.index} has no started container named '${container.name}' (started: ${[...startedById.keys()].join(", ") || "none"}).`);
|
|
17612
|
+
const aliases = [container.name];
|
|
17613
|
+
for (const a of networkAliasesByContainer.get(container.name) ?? []) if (!aliases.includes(a)) aliases.push(a);
|
|
17614
|
+
const port = container.portMappings[0]?.containerPort;
|
|
17499
17615
|
targets.push({
|
|
17500
17616
|
name: container.name,
|
|
17501
|
-
id
|
|
17617
|
+
id,
|
|
17618
|
+
aliases,
|
|
17619
|
+
...port !== void 0 ? { port } : {}
|
|
17502
17620
|
});
|
|
17503
17621
|
}
|
|
17504
17622
|
const controllerOptions = controller.options;
|
|
@@ -17511,6 +17629,7 @@ async function softReloadReplica(args) {
|
|
|
17511
17629
|
unregisterReplicaFromFrontDoor(instance, controllerOptions.frontDoor);
|
|
17512
17630
|
instance.softReloadInProgress = true;
|
|
17513
17631
|
instance.softReloadGeneration = (instance.softReloadGeneration ?? 0) + 1;
|
|
17632
|
+
const attachPlans = /* @__PURE__ */ new Map();
|
|
17514
17633
|
try {
|
|
17515
17634
|
logger.info(`Soft-reloading replica r${instance.index} (gen ${instance.generation}): docker cp ${flattenToOneLine(sourceDirToCopy)} -> ${targets.length} essential container(s); restart.`);
|
|
17516
17635
|
for (const target of targets) {
|
|
@@ -17526,23 +17645,72 @@ async function softReloadReplica(args) {
|
|
|
17526
17645
|
} catch (err) {
|
|
17527
17646
|
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.`);
|
|
17528
17647
|
}
|
|
17529
|
-
|
|
17530
|
-
|
|
17531
|
-
|
|
17532
|
-
|
|
17648
|
+
}
|
|
17649
|
+
const sharedNetworkName = instance.state.network?.ownedByCaller ? instance.state.network.networkName : void 0;
|
|
17650
|
+
if (sharedNetworkName) {
|
|
17651
|
+
for (const target of targets) {
|
|
17652
|
+
const plan = await detachFromSharedNetwork(target, sharedNetworkName);
|
|
17653
|
+
if (plan) attachPlans.set(target.id, plan);
|
|
17533
17654
|
}
|
|
17655
|
+
if (await reattachForDrain(attachPlans)) await sleep(reloadDrainMs);
|
|
17656
|
+
}
|
|
17657
|
+
for (const target of targets) try {
|
|
17658
|
+
await dockerStopImpl(target.id);
|
|
17659
|
+
const plan = attachPlans.get(target.id);
|
|
17660
|
+
if (plan && (plan.publishesHostPorts || !plan.attached)) {
|
|
17661
|
+
if (plan.attached) {
|
|
17662
|
+
await dockerNetworkDisconnectImpl(plan.networkName, target.id);
|
|
17663
|
+
plan.attached = false;
|
|
17664
|
+
}
|
|
17665
|
+
await attachToSharedNetwork(target.id, plan, plan.publishesHostPorts);
|
|
17666
|
+
}
|
|
17667
|
+
await dockerStartImpl(target.id);
|
|
17668
|
+
} catch (err) {
|
|
17669
|
+
throw new EcsServiceRunnerError(`softReloadReplica: docker stop / start 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.`);
|
|
17534
17670
|
}
|
|
17535
|
-
|
|
17671
|
+
const aliasDeadline = Date.now() + shadowReadyTimeoutMs;
|
|
17672
|
+
const ready = await waitForReplicaTcpReady(newService, instance, {
|
|
17536
17673
|
timeoutMs: shadowReadyTimeoutMs,
|
|
17537
17674
|
intervalMs: shadowReadyIntervalMs,
|
|
17538
17675
|
label: `Soft-reloaded replica r${instance.index} (gen ${instance.generation})`
|
|
17539
|
-
});
|
|
17540
|
-
const
|
|
17541
|
-
|
|
17676
|
+
}) === "ready";
|
|
17677
|
+
for (const target of targets) {
|
|
17678
|
+
const plan = attachPlans.get(target.id);
|
|
17679
|
+
if (!plan || plan.aliasesAttached) continue;
|
|
17680
|
+
if (target.port !== void 0) await waitForContainerListening(target.id, target.port, aliasDeadline);
|
|
17681
|
+
try {
|
|
17682
|
+
await dockerNetworkDisconnectImpl(plan.networkName, target.id);
|
|
17683
|
+
plan.attached = false;
|
|
17684
|
+
await attachToSharedNetwork(target.id, plan, true);
|
|
17685
|
+
} catch (err) {
|
|
17686
|
+
throw new EcsServiceRunnerError(`softReloadReplica: restoring the network aliases 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.`);
|
|
17687
|
+
}
|
|
17688
|
+
}
|
|
17689
|
+
const ownerKeyPrefix = replicaOwnerKeyPrefix(newService, instance);
|
|
17542
17690
|
if (controllerOptions.discovery) await publishReplicaToCloudMap(newService, instance, controllerOptions.discovery, ownerKeyPrefix);
|
|
17543
17691
|
if (controllerOptions.frontDoor) await publishReplicaToFrontDoor(newService, instance, controllerOptions.frontDoor, controllerOptions.taskOptions.containerHost, ownerKeyPrefix);
|
|
17544
17692
|
instance.lastDeployedAssetHash = pickEssentialAssetHash(newService);
|
|
17545
|
-
logger.info(`Soft-reloaded replica r${instance.index} (gen ${instance.generation}):
|
|
17693
|
+
logger.info(`Soft-reloaded replica r${instance.index} (gen ${instance.generation}): ` + (ready ? SOFT_RELOAD_COMPLETION_LOG_SUFFIX : SOFT_RELOAD_PROBE_TIMEOUT_LOG_SUFFIX));
|
|
17694
|
+
} catch (err) {
|
|
17695
|
+
for (const [id, plan] of attachPlans) {
|
|
17696
|
+
if (plan.aliasesAttached) continue;
|
|
17697
|
+
try {
|
|
17698
|
+
if (plan.attached) {
|
|
17699
|
+
await dockerNetworkDisconnectImpl(plan.networkName, id);
|
|
17700
|
+
plan.attached = false;
|
|
17701
|
+
}
|
|
17702
|
+
try {
|
|
17703
|
+
await attachToSharedNetwork(id, plan, true);
|
|
17704
|
+
} catch {
|
|
17705
|
+
await dockerNetworkConnectImpl(plan.networkName, id, { aliases: plan.aliases });
|
|
17706
|
+
plan.attached = true;
|
|
17707
|
+
plan.aliasesAttached = true;
|
|
17708
|
+
}
|
|
17709
|
+
} catch (restoreErr) {
|
|
17710
|
+
logger.debug(`Soft-reload of replica r${instance.index}: re-attaching container ${id} to ${plan.networkName} failed: ${restoreErr instanceof Error ? restoreErr.message : String(restoreErr)}`);
|
|
17711
|
+
}
|
|
17712
|
+
}
|
|
17713
|
+
throw err;
|
|
17546
17714
|
} finally {
|
|
17547
17715
|
instance.softReloadInProgress = false;
|
|
17548
17716
|
}
|
|
@@ -17571,23 +17739,122 @@ const defaultDockerCpImpl = async (src, dst) => {
|
|
|
17571
17739
|
], { maxBuffer: 64 * 1024 * 1024 });
|
|
17572
17740
|
};
|
|
17573
17741
|
let dockerCpImpl = defaultDockerCpImpl;
|
|
17574
|
-
const
|
|
17742
|
+
const defaultDockerStopImpl = async (containerId) => {
|
|
17575
17743
|
const { execFile } = await import("node:child_process");
|
|
17576
17744
|
const { promisify } = await import("node:util");
|
|
17577
17745
|
const { getDockerCmd } = await import("./docker-cmd-BSdu45mc.js").then((n) => n.t);
|
|
17578
|
-
await promisify(execFile)(getDockerCmd(), ["
|
|
17746
|
+
await promisify(execFile)(getDockerCmd(), ["stop", containerId]);
|
|
17579
17747
|
};
|
|
17580
|
-
|
|
17581
|
-
|
|
17748
|
+
const defaultDockerStartImpl = async (containerId) => {
|
|
17749
|
+
const { execFile } = await import("node:child_process");
|
|
17750
|
+
const { promisify } = await import("node:util");
|
|
17751
|
+
const { getDockerCmd } = await import("./docker-cmd-BSdu45mc.js").then((n) => n.t);
|
|
17752
|
+
await promisify(execFile)(getDockerCmd(), ["start", containerId]);
|
|
17753
|
+
};
|
|
17754
|
+
let dockerStopImpl = defaultDockerStopImpl;
|
|
17755
|
+
let dockerStartImpl = defaultDockerStartImpl;
|
|
17756
|
+
async function detachFromSharedNetwork(target, networkName) {
|
|
17757
|
+
const logger = getLogger().child("ecs-service");
|
|
17758
|
+
let ip;
|
|
17759
|
+
try {
|
|
17760
|
+
ip = await getContainerNetworkIp(target.id, networkName);
|
|
17761
|
+
} catch {
|
|
17762
|
+
ip = void 0;
|
|
17763
|
+
}
|
|
17764
|
+
if (!ip) {
|
|
17765
|
+
logger.debug(`Soft-reload: no IP for '${target.name}' (${target.id}) on ${networkName}; restarting it in place without detaching its network aliases first.`);
|
|
17766
|
+
return;
|
|
17767
|
+
}
|
|
17768
|
+
let publishesHostPorts;
|
|
17769
|
+
try {
|
|
17770
|
+
publishesHostPorts = await dockerInspectPublishesHostPortsImpl(target.id);
|
|
17771
|
+
} catch {
|
|
17772
|
+
publishesHostPorts = true;
|
|
17773
|
+
}
|
|
17774
|
+
try {
|
|
17775
|
+
await dockerNetworkDisconnectImpl(networkName, target.id);
|
|
17776
|
+
} catch (err) {
|
|
17777
|
+
logger.debug(`Soft-reload: detaching '${target.name}' (${target.id}) from ${networkName} failed: ${err instanceof Error ? err.message : String(err)}. Restarting it in place.`);
|
|
17778
|
+
return;
|
|
17779
|
+
}
|
|
17780
|
+
return {
|
|
17781
|
+
networkName,
|
|
17782
|
+
ip,
|
|
17783
|
+
aliases: target.aliases,
|
|
17784
|
+
publishesHostPorts,
|
|
17785
|
+
attached: false,
|
|
17786
|
+
aliasesAttached: false
|
|
17787
|
+
};
|
|
17788
|
+
}
|
|
17789
|
+
const DEFAULT_RELOAD_DRAIN_MS = 1e3;
|
|
17790
|
+
let reloadDrainMs = DEFAULT_RELOAD_DRAIN_MS;
|
|
17791
|
+
async function reattachForDrain(plans) {
|
|
17792
|
+
let any = false;
|
|
17793
|
+
for (const [id, plan] of plans) try {
|
|
17794
|
+
await attachToSharedNetwork(id, plan, false);
|
|
17795
|
+
any = true;
|
|
17796
|
+
} catch (err) {
|
|
17797
|
+
getLogger().child("ecs-service").debug(`Soft-reload: drain re-attach of ${id} to ${plan.networkName} failed: ${err instanceof Error ? err.message : String(err)}. Stopping it without a drain.`);
|
|
17798
|
+
}
|
|
17799
|
+
return any;
|
|
17800
|
+
}
|
|
17801
|
+
async function attachToSharedNetwork(containerId, plan, withAliases) {
|
|
17802
|
+
await dockerNetworkConnectImpl(plan.networkName, containerId, {
|
|
17803
|
+
ip: plan.ip,
|
|
17804
|
+
aliases: withAliases ? plan.aliases : []
|
|
17805
|
+
});
|
|
17806
|
+
plan.attached = true;
|
|
17807
|
+
plan.aliasesAttached = withAliases;
|
|
17808
|
+
}
|
|
17809
|
+
const defaultDockerNetworkConnectImpl = async (networkName, containerId, opts) => {
|
|
17810
|
+
const { execFile } = await import("node:child_process");
|
|
17811
|
+
const { promisify } = await import("node:util");
|
|
17812
|
+
const { getDockerCmd } = await import("./docker-cmd-BSdu45mc.js").then((n) => n.t);
|
|
17813
|
+
const args = ["network", "connect"];
|
|
17814
|
+
if (opts.ip) args.push("--ip", opts.ip);
|
|
17815
|
+
for (const a of opts.aliases) args.push("--alias", a);
|
|
17816
|
+
args.push(networkName, containerId);
|
|
17817
|
+
await promisify(execFile)(getDockerCmd(), args);
|
|
17818
|
+
};
|
|
17819
|
+
let dockerNetworkConnectImpl = defaultDockerNetworkConnectImpl;
|
|
17820
|
+
const defaultDockerInspectPublishesHostPortsImpl = async (containerId) => {
|
|
17821
|
+
const { execFile } = await import("node:child_process");
|
|
17822
|
+
const { promisify } = await import("node:util");
|
|
17823
|
+
const { getDockerCmd } = await import("./docker-cmd-BSdu45mc.js").then((n) => n.t);
|
|
17824
|
+
const { stdout } = await promisify(execFile)(getDockerCmd(), [
|
|
17825
|
+
"inspect",
|
|
17826
|
+
"--format",
|
|
17827
|
+
"{{json .HostConfig.PortBindings}}",
|
|
17828
|
+
containerId
|
|
17829
|
+
]);
|
|
17830
|
+
const parsed = JSON.parse(stdout.trim() || "null");
|
|
17831
|
+
if (!parsed || typeof parsed !== "object") return false;
|
|
17832
|
+
return Object.values(parsed).some((bindings) => Array.isArray(bindings) && bindings.length > 0);
|
|
17833
|
+
};
|
|
17834
|
+
let dockerInspectPublishesHostPortsImpl = defaultDockerInspectPublishesHostPortsImpl;
|
|
17835
|
+
async function disconnectOldFromSharedNetwork(oldInstance, opts = {}) {
|
|
17582
17836
|
const network = oldInstance.state.network;
|
|
17583
17837
|
if (!network || !network.ownedByCaller) return;
|
|
17584
17838
|
const networkName = network.networkName;
|
|
17585
|
-
const
|
|
17586
|
-
if (
|
|
17587
|
-
|
|
17839
|
+
const drainIps = /* @__PURE__ */ new Map();
|
|
17840
|
+
if (opts.drain) for (const c of oldInstance.state.startedContainers) {
|
|
17841
|
+
const ip = await getContainerNetworkIp(c.id, networkName).catch(() => void 0);
|
|
17842
|
+
if (ip) drainIps.set(c.id, ip);
|
|
17843
|
+
}
|
|
17844
|
+
const targets = oldInstance.state.startedContainers.map((c) => c.id);
|
|
17588
17845
|
for (const id of targets) try {
|
|
17589
17846
|
await dockerNetworkDisconnectImpl(networkName, id);
|
|
17590
17847
|
} catch (err) {}
|
|
17848
|
+
if (drainIps.size === 0) return;
|
|
17849
|
+
let reattached = false;
|
|
17850
|
+
for (const [id, ip] of drainIps) try {
|
|
17851
|
+
await dockerNetworkConnectImpl(networkName, id, {
|
|
17852
|
+
ip,
|
|
17853
|
+
aliases: []
|
|
17854
|
+
});
|
|
17855
|
+
reattached = true;
|
|
17856
|
+
} catch {}
|
|
17857
|
+
if (reattached) await sleep(reloadDrainMs);
|
|
17591
17858
|
}
|
|
17592
17859
|
const defaultDockerNetworkDisconnectImpl = async (networkName, containerId) => {
|
|
17593
17860
|
const { execFile } = await import("node:child_process");
|
|
@@ -17606,36 +17873,129 @@ async function waitForReplicaTcpReady(service, replica, opts) {
|
|
|
17606
17873
|
const logger = getLogger().child("ecs-service");
|
|
17607
17874
|
const label = opts.label ?? `Shadow replica r${replica.index} (gen ${replica.generation})`;
|
|
17608
17875
|
const networkName = replica.state.network?.networkName;
|
|
17609
|
-
if (!networkName) return;
|
|
17876
|
+
if (!networkName) return "ready";
|
|
17610
17877
|
const essential = service.task.containers.find((c) => c.essential) ?? service.task.containers[0];
|
|
17611
|
-
if (!essential || essential.portMappings.length === 0) return;
|
|
17878
|
+
if (!essential || essential.portMappings.length === 0) return "ready";
|
|
17612
17879
|
const started = replica.state.startedContainers.find((c) => c.name === essential.name);
|
|
17613
|
-
if (!started) return;
|
|
17614
|
-
let ip;
|
|
17615
|
-
try {
|
|
17616
|
-
const resolved = await getContainerNetworkIp(started.id, networkName);
|
|
17617
|
-
if (!resolved) return;
|
|
17618
|
-
ip = resolved;
|
|
17619
|
-
} catch (err) {
|
|
17620
|
-
logger.warn(`${label}: TCP-ready probe could not resolve docker IP: ${err instanceof Error ? err.message : String(err)}. Proceeding.`);
|
|
17621
|
-
return;
|
|
17622
|
-
}
|
|
17880
|
+
if (!started) return "ready";
|
|
17623
17881
|
const port = essential.portMappings[0].containerPort;
|
|
17624
17882
|
const startedAt = Date.now();
|
|
17625
17883
|
const deadline = startedAt + opts.timeoutMs;
|
|
17626
17884
|
let lastErr;
|
|
17885
|
+
let target = formatAuthority(started.name, port);
|
|
17886
|
+
let inContainer = true;
|
|
17887
|
+
let ip;
|
|
17627
17888
|
while (Date.now() < deadline) {
|
|
17628
|
-
|
|
17629
|
-
|
|
17630
|
-
const
|
|
17631
|
-
|
|
17632
|
-
|
|
17633
|
-
|
|
17634
|
-
|
|
17889
|
+
if (opts.isShuttingDown?.()) return "not-ready";
|
|
17890
|
+
if (inContainer) {
|
|
17891
|
+
const result = await listenProbeImpl(started.id, port, deadline - Date.now());
|
|
17892
|
+
if (result.kind === "listening") {
|
|
17893
|
+
logger.debug(`${label}: port ${port} listening in ${Date.now() - startedAt}ms (in-container check).`);
|
|
17894
|
+
return "ready";
|
|
17895
|
+
}
|
|
17896
|
+
if (result.kind === "unsupported") {
|
|
17897
|
+
logger.debug(`${label}: in-container LISTEN check unavailable (${result.detail}); falling back to a host-side TCP connect.`);
|
|
17898
|
+
inContainer = false;
|
|
17899
|
+
continue;
|
|
17900
|
+
}
|
|
17901
|
+
if (result.kind === "exited") {
|
|
17902
|
+
logger.warn(`${label}: the container exited before port ${port} listened. Proceeding.`);
|
|
17903
|
+
return "exited";
|
|
17904
|
+
}
|
|
17905
|
+
lastErr = result.kind === "not-listening" ? "not listening yet" : result.detail;
|
|
17906
|
+
} else {
|
|
17907
|
+
if (ip === void 0) {
|
|
17908
|
+
try {
|
|
17909
|
+
ip = await getContainerNetworkIp(started.id, networkName);
|
|
17910
|
+
} catch (err) {
|
|
17911
|
+
logger.warn(`${label}: TCP-ready probe could not resolve docker IP: ${err instanceof Error ? err.message : String(err)}. Proceeding.`);
|
|
17912
|
+
return "not-ready";
|
|
17913
|
+
}
|
|
17914
|
+
if (!ip) {
|
|
17915
|
+
logger.warn(`${label}: TCP-ready probe found no docker IP for ${started.name} on ${networkName}. Proceeding.`);
|
|
17916
|
+
return "not-ready";
|
|
17917
|
+
}
|
|
17918
|
+
target = formatAuthority(ip, port);
|
|
17919
|
+
}
|
|
17920
|
+
try {
|
|
17921
|
+
await tcpProbeImpl(ip, port);
|
|
17922
|
+
logger.debug(`${label}: TCP probe ${target} accepted in ${Date.now() - startedAt}ms.`);
|
|
17923
|
+
return "ready";
|
|
17924
|
+
} catch (err) {
|
|
17925
|
+
lastErr = err instanceof Error ? err.message : String(err);
|
|
17926
|
+
}
|
|
17635
17927
|
}
|
|
17636
17928
|
await sleep(opts.intervalMs);
|
|
17637
17929
|
}
|
|
17638
|
-
logger.warn(`${label}: TCP probe ${
|
|
17930
|
+
logger.warn(`${label}: TCP probe ${target} 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.`);
|
|
17931
|
+
return "not-ready";
|
|
17932
|
+
}
|
|
17933
|
+
const LISTEN_PROBE_SCRIPT = [
|
|
17934
|
+
"p=$(printf \"%04X\" \"$1\")",
|
|
17935
|
+
"seen=0",
|
|
17936
|
+
"for f in /proc/net/tcp /proc/net/tcp6; do",
|
|
17937
|
+
" [ -r \"$f\" ] || continue",
|
|
17938
|
+
" seen=1",
|
|
17939
|
+
" while read -r _ l _ s _; do",
|
|
17940
|
+
" [ \"$s\" = 0A ] || continue",
|
|
17941
|
+
" case \"$l\" in *:\"$p\") ;; *) continue ;; esac",
|
|
17942
|
+
" case \"$l\" in ??????7F:*|0000000000000000FFFF0000??????7F:*) continue ;; esac",
|
|
17943
|
+
" case \"$l\" in 00000000000000000000000001000000:*) continue ;; esac",
|
|
17944
|
+
" exit 0",
|
|
17945
|
+
" done < \"$f\"",
|
|
17946
|
+
"done",
|
|
17947
|
+
"[ \"$seen\" = 1 ] && exit 3",
|
|
17948
|
+
"exit 4"
|
|
17949
|
+
].join("\n");
|
|
17950
|
+
async function dockerExecListenProbe(containerId, port, timeoutMs = 1e4) {
|
|
17951
|
+
const { execFile } = await import("node:child_process");
|
|
17952
|
+
const { promisify } = await import("node:util");
|
|
17953
|
+
const { getDockerCmd } = await import("./docker-cmd-BSdu45mc.js").then((n) => n.t);
|
|
17954
|
+
try {
|
|
17955
|
+
await promisify(execFile)(getDockerCmd(), [
|
|
17956
|
+
"exec",
|
|
17957
|
+
containerId,
|
|
17958
|
+
"sh",
|
|
17959
|
+
"-c",
|
|
17960
|
+
LISTEN_PROBE_SCRIPT,
|
|
17961
|
+
"sh",
|
|
17962
|
+
String(port)
|
|
17963
|
+
], { timeout: Math.max(1, Math.min(1e4, timeoutMs)) });
|
|
17964
|
+
return { kind: "listening" };
|
|
17965
|
+
} catch (err) {
|
|
17966
|
+
const e = err;
|
|
17967
|
+
const ran = typeof e.code === "number" || typeof e.signal === "string";
|
|
17968
|
+
const detail = flattenToOneLine(typeof e.stderr === "string" && e.stderr.trim() || (ran ? `exit ${String(e.code ?? e.signal)}` : e.message || String(err))).slice(0, 200);
|
|
17969
|
+
if (e.code === 3) return { kind: "not-listening" };
|
|
17970
|
+
if (e.code === 4) return {
|
|
17971
|
+
kind: "unsupported",
|
|
17972
|
+
detail: "no readable /proc/net/tcp"
|
|
17973
|
+
};
|
|
17974
|
+
const stderr = typeof e.stderr === "string" ? e.stderr : "";
|
|
17975
|
+
if (/is not running|stopped state/i.test(stderr)) return {
|
|
17976
|
+
kind: "exited",
|
|
17977
|
+
detail
|
|
17978
|
+
};
|
|
17979
|
+
if ((e.code === 126 || e.code === 127) && /not found|no such file/i.test(stderr)) return {
|
|
17980
|
+
kind: "unsupported",
|
|
17981
|
+
detail
|
|
17982
|
+
};
|
|
17983
|
+
return {
|
|
17984
|
+
kind: "error",
|
|
17985
|
+
detail
|
|
17986
|
+
};
|
|
17987
|
+
}
|
|
17988
|
+
}
|
|
17989
|
+
let listenProbeImpl = dockerExecListenProbe;
|
|
17990
|
+
async function waitForContainerListening(containerId, port, deadline) {
|
|
17991
|
+
while (Date.now() < deadline) {
|
|
17992
|
+
const result = await listenProbeImpl(containerId, port, deadline - Date.now());
|
|
17993
|
+
if (result.kind === "listening") return "ready";
|
|
17994
|
+
if (result.kind === "exited") return "exited";
|
|
17995
|
+
if (result.kind === "unsupported") return "not-ready";
|
|
17996
|
+
await sleep(shadowReadyIntervalMs);
|
|
17997
|
+
}
|
|
17998
|
+
return "not-ready";
|
|
17639
17999
|
}
|
|
17640
18000
|
const defaultTcpProbeImpl = async (host, port) => {
|
|
17641
18001
|
const { createConnection } = await import("node:net");
|
|
@@ -17705,7 +18065,19 @@ async function watchReplica(service, options, instance, runState) {
|
|
|
17705
18065
|
}
|
|
17706
18066
|
instance.state = createEcsRunState();
|
|
17707
18067
|
instance.restartCount += 1;
|
|
17708
|
-
const
|
|
18068
|
+
const isShuttingDown = () => instance.shuttingDown || runState.shuttingDown;
|
|
18069
|
+
const bootPromise = (async () => {
|
|
18070
|
+
await bootReplica(service, options, instance, { deferPublish: true });
|
|
18071
|
+
const deadline = Date.now() + shadowReadyTimeoutMs;
|
|
18072
|
+
const label = `Restarted replica r${instance.index} (gen ${instance.generation})`;
|
|
18073
|
+
if (await waitForReplicaTcpReady(service, instance, {
|
|
18074
|
+
timeoutMs: shadowReadyTimeoutMs,
|
|
18075
|
+
intervalMs: shadowReadyIntervalMs,
|
|
18076
|
+
label,
|
|
18077
|
+
isShuttingDown
|
|
18078
|
+
}) === "exited") return;
|
|
18079
|
+
await publishGatedReplica(service, options, instance, deadline, isShuttingDown, label);
|
|
18080
|
+
})();
|
|
17709
18081
|
instance.inFlightBoot = bootPromise;
|
|
17710
18082
|
try {
|
|
17711
18083
|
await bootPromise;
|
|
@@ -19344,6 +19716,16 @@ function describePinnedImageUri(service) {
|
|
|
19344
19716
|
if (image.kind === "cdk-asset") return void 0;
|
|
19345
19717
|
return image.uri;
|
|
19346
19718
|
}
|
|
19719
|
+
function imageOverrideKeyAliases(service) {
|
|
19720
|
+
const out = /* @__PURE__ */ new Set([
|
|
19721
|
+
service.serviceLogicalId,
|
|
19722
|
+
`${service.stack.stackName}:${service.serviceLogicalId}`,
|
|
19723
|
+
service.serviceDisplayName,
|
|
19724
|
+
service.serviceName
|
|
19725
|
+
]);
|
|
19726
|
+
out.delete("");
|
|
19727
|
+
return [...out];
|
|
19728
|
+
}
|
|
19347
19729
|
function listPinnedTargets(resolvedServices) {
|
|
19348
19730
|
const out = [];
|
|
19349
19731
|
for (const { target, service } of resolvedServices) {
|
|
@@ -19351,7 +19733,8 @@ function listPinnedTargets(resolvedServices) {
|
|
|
19351
19733
|
const label = describePinnedImageUri(service);
|
|
19352
19734
|
out.push({
|
|
19353
19735
|
target,
|
|
19354
|
-
...label !== void 0 && { label }
|
|
19736
|
+
...label !== void 0 && { label },
|
|
19737
|
+
aliases: imageOverrideKeyAliases(service)
|
|
19355
19738
|
});
|
|
19356
19739
|
}
|
|
19357
19740
|
return out;
|
|
@@ -19480,6 +19863,70 @@ function parseImageOverrideFlags(input) {
|
|
|
19480
19863
|
perService
|
|
19481
19864
|
};
|
|
19482
19865
|
}
|
|
19866
|
+
function canonicalizeImageOverrideKeys(rawFlags, pinnedAliases, knownTargets = []) {
|
|
19867
|
+
const exact = /* @__PURE__ */ new Set([...pinnedAliases.keys(), ...knownTargets]);
|
|
19868
|
+
const targetsByAlias = /* @__PURE__ */ new Map();
|
|
19869
|
+
for (const [target, aliases] of pinnedAliases) for (const alias of aliases) {
|
|
19870
|
+
let set = targetsByAlias.get(alias);
|
|
19871
|
+
if (!set) {
|
|
19872
|
+
set = /* @__PURE__ */ new Set();
|
|
19873
|
+
targetsByAlias.set(alias, set);
|
|
19874
|
+
}
|
|
19875
|
+
set.add(target);
|
|
19876
|
+
}
|
|
19877
|
+
const logger = getLogger();
|
|
19878
|
+
const canonical = (key, flag) => {
|
|
19879
|
+
if (exact.has(key)) return key;
|
|
19880
|
+
const matches = targetsByAlias.get(key);
|
|
19881
|
+
if (!matches) return key;
|
|
19882
|
+
if (matches.size > 1) throw new ImageOverrideError(`${flag} key '${key}' matches more than one pinned target (${[...matches].join(", ")}). Name the target in full instead.`);
|
|
19883
|
+
const target = [...matches][0];
|
|
19884
|
+
logger.debug(`${flag} key '${key}' resolved to pinned target '${target}'.`);
|
|
19885
|
+
return target;
|
|
19886
|
+
};
|
|
19887
|
+
const explicit = /* @__PURE__ */ new Map();
|
|
19888
|
+
const explicitKeyByTarget = /* @__PURE__ */ new Map();
|
|
19889
|
+
for (const [key, dockerfile] of rawFlags.explicit) {
|
|
19890
|
+
const target = canonical(key, "--image-override");
|
|
19891
|
+
const earlier = explicitKeyByTarget.get(target);
|
|
19892
|
+
if (earlier !== void 0) throw new ImageOverrideError(`--image-override keys '${earlier}' and '${key}' both name the pinned target '${target}'. Name each service target at most once.`);
|
|
19893
|
+
explicitKeyByTarget.set(target, key);
|
|
19894
|
+
explicit.set(target, dockerfile);
|
|
19895
|
+
}
|
|
19896
|
+
const perService = /* @__PURE__ */ new Map();
|
|
19897
|
+
for (const [key, overlay] of rawFlags.perService) {
|
|
19898
|
+
const target = canonical(key, "Per-service image-override flag");
|
|
19899
|
+
const into = perService.get(target);
|
|
19900
|
+
if (!into) {
|
|
19901
|
+
perService.set(target, {
|
|
19902
|
+
buildArgs: new Map(overlay.buildArgs),
|
|
19903
|
+
buildSecrets: new Map(overlay.buildSecrets),
|
|
19904
|
+
...overlay.targetStage !== void 0 && { targetStage: overlay.targetStage }
|
|
19905
|
+
});
|
|
19906
|
+
continue;
|
|
19907
|
+
}
|
|
19908
|
+
const conflict = (what) => {
|
|
19909
|
+
throw new ImageOverrideError(`Per-service image-override flags give ${what} two different values for the pinned target '${target}' (one of them keyed '${key}'). Pass it once.`);
|
|
19910
|
+
};
|
|
19911
|
+
for (const [k, v] of overlay.buildArgs) {
|
|
19912
|
+
if (into.buildArgs.has(k) && into.buildArgs.get(k) !== v) conflict(`build arg '${k}'`);
|
|
19913
|
+
into.buildArgs.set(k, v);
|
|
19914
|
+
}
|
|
19915
|
+
for (const [id, src] of overlay.buildSecrets) {
|
|
19916
|
+
if (into.buildSecrets.has(id) && into.buildSecrets.get(id) !== src) conflict(`build secret '${id}'`);
|
|
19917
|
+
into.buildSecrets.set(id, src);
|
|
19918
|
+
}
|
|
19919
|
+
if (overlay.targetStage !== void 0) {
|
|
19920
|
+
if (into.targetStage !== void 0 && into.targetStage !== overlay.targetStage) conflict("the target stage");
|
|
19921
|
+
into.targetStage = overlay.targetStage;
|
|
19922
|
+
}
|
|
19923
|
+
}
|
|
19924
|
+
return {
|
|
19925
|
+
...rawFlags,
|
|
19926
|
+
explicit,
|
|
19927
|
+
perService
|
|
19928
|
+
};
|
|
19929
|
+
}
|
|
19483
19930
|
async function resolveImageOverrides(args) {
|
|
19484
19931
|
const logger = getLogger();
|
|
19485
19932
|
const { rawFlags, pinnedTargets, noInteractive } = args;
|
|
@@ -20129,7 +20576,7 @@ async function rollOneTarget(args) {
|
|
|
20129
20576
|
return;
|
|
20130
20577
|
}
|
|
20131
20578
|
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.`);
|
|
20132
|
-
if (verdict.kind === "soft-reload") logger.info(`Reload of '${newBoot.target}': soft-reloading ${oldReplicas.length} replica(s) one at a time (docker cp source →
|
|
20579
|
+
if (verdict.kind === "soft-reload") logger.info(`Reload of '${newBoot.target}': soft-reloading ${oldReplicas.length} replica(s) one at a time (docker cp source → restart → TCP-ready probe; no rebuild).`);
|
|
20133
20580
|
else logger.info(`Reload of '${newBoot.target}': rolling ${oldReplicas.length} replica(s) one at a time (start new shadow → swap registrations → stop old).`);
|
|
20134
20581
|
for (const oldInstance of oldReplicas) {
|
|
20135
20582
|
const idx = controller.runState.replicas.indexOf(oldInstance);
|
|
@@ -20726,13 +21173,17 @@ async function resolveAndBuildImageOverrides(args) {
|
|
|
20726
21173
|
const pinnedEntries = listPinnedTargets(resolvedForPeek);
|
|
20727
21174
|
const pinnedTargets = pinnedEntries.map((e) => e.target);
|
|
20728
21175
|
const pinnedLabels = /* @__PURE__ */ new Map();
|
|
20729
|
-
|
|
20730
|
-
const
|
|
21176
|
+
const pinnedAliases = /* @__PURE__ */ new Map();
|
|
21177
|
+
for (const entry of pinnedEntries) {
|
|
21178
|
+
if (entry.label !== void 0) pinnedLabels.set(entry.target, entry.label);
|
|
21179
|
+
pinnedAliases.set(entry.target, entry.aliases);
|
|
21180
|
+
}
|
|
21181
|
+
const rawFlags = canonicalizeImageOverrideKeys(parseImageOverrideFlags({
|
|
20731
21182
|
...options.imageOverride && { imageOverride: options.imageOverride },
|
|
20732
21183
|
...options.imageBuildArg && { imageBuildArg: options.imageBuildArg },
|
|
20733
21184
|
...options.imageBuildSecret && { imageBuildSecret: options.imageBuildSecret },
|
|
20734
21185
|
...options.imageTarget && { imageTarget: options.imageTarget }
|
|
20735
|
-
});
|
|
21186
|
+
}), pinnedAliases, perTarget);
|
|
20736
21187
|
if (pinnedTargets.length === 0 && rawFlags.explicit.size === 0 && rawFlags.pickerPaths.length === 0 && rawFlags.perService.size === 0) return {
|
|
20737
21188
|
tags: /* @__PURE__ */ new Map(),
|
|
20738
21189
|
overrides: /* @__PURE__ */ new Map()
|
|
@@ -20745,6 +21196,7 @@ async function resolveAndBuildImageOverrides(args) {
|
|
|
20745
21196
|
noInteractive: options.interactiveOverrides === false
|
|
20746
21197
|
});
|
|
20747
21198
|
enforceImageOverrideOrphans(rawFlags, overrides);
|
|
21199
|
+
enforceStrictOverrides(options.strictOverrides === true, pinnedTargets.filter((t) => !overrides.has(t)));
|
|
20748
21200
|
if (overrides.size === 0) return {
|
|
20749
21201
|
tags: /* @__PURE__ */ new Map(),
|
|
20750
21202
|
overrides: /* @__PURE__ */ new Map()
|
|
@@ -21092,6 +21544,16 @@ function createLocalRunTaskCommand(opts = {}) {
|
|
|
21092
21544
|
cmd.addOption(regionOption);
|
|
21093
21545
|
return cmd;
|
|
21094
21546
|
}
|
|
21547
|
+
function runTaskOverrideKeyAliases(task) {
|
|
21548
|
+
const id = task.taskDefinitionLogicalId;
|
|
21549
|
+
const out = /* @__PURE__ */ new Set([
|
|
21550
|
+
id,
|
|
21551
|
+
`${task.stack.stackName}:${id}`,
|
|
21552
|
+
deriveServiceDisplayName(void 0, id, task.resource.Metadata)
|
|
21553
|
+
]);
|
|
21554
|
+
out.delete("");
|
|
21555
|
+
return [...out];
|
|
21556
|
+
}
|
|
21095
21557
|
async function resolveRunTaskImageOverride(args) {
|
|
21096
21558
|
const { task, target, options, cwd } = args;
|
|
21097
21559
|
const essential = task.containers.find((c) => c.essential) ?? task.containers[0];
|
|
@@ -21099,12 +21561,14 @@ async function resolveRunTaskImageOverride(args) {
|
|
|
21099
21561
|
const pinnedTargets = pinned ? [target] : [];
|
|
21100
21562
|
const pinnedLabels = /* @__PURE__ */ new Map();
|
|
21101
21563
|
if (pinned) pinnedLabels.set(target, `${task.stack.stackName}/${task.taskDefinitionLogicalId}`);
|
|
21102
|
-
const
|
|
21564
|
+
const pinnedAliases = /* @__PURE__ */ new Map();
|
|
21565
|
+
if (pinned) pinnedAliases.set(target, runTaskOverrideKeyAliases(task));
|
|
21566
|
+
const rawFlags = canonicalizeImageOverrideKeys(parseImageOverrideFlags({
|
|
21103
21567
|
...options.imageOverride && { imageOverride: options.imageOverride },
|
|
21104
21568
|
...options.imageBuildArg && { imageBuildArg: options.imageBuildArg },
|
|
21105
21569
|
...options.imageBuildSecret && { imageBuildSecret: options.imageBuildSecret },
|
|
21106
21570
|
...options.imageTarget && { imageTarget: options.imageTarget }
|
|
21107
|
-
});
|
|
21571
|
+
}), pinnedAliases, [target]);
|
|
21108
21572
|
if (pinnedTargets.length === 0 && rawFlags.explicit.size === 0 && rawFlags.pickerPaths.length === 0 && rawFlags.perService.size === 0) return {};
|
|
21109
21573
|
const overrides = await resolveImageOverrides({
|
|
21110
21574
|
rawFlags,
|
|
@@ -21156,7 +21620,7 @@ function createLocalStartServiceCommand(opts = {}) {
|
|
|
21156
21620
|
}
|
|
21157
21621
|
function addStartServiceSpecificOptions(cmd) {
|
|
21158
21622
|
addImageOverrideOptions(cmd);
|
|
21159
|
-
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 +
|
|
21623
|
+
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 + restart it; 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));
|
|
21160
21624
|
}
|
|
21161
21625
|
|
|
21162
21626
|
//#endregion
|
|
@@ -21792,7 +22256,7 @@ function createLocalStartAlbCommand(opts = {}) {
|
|
|
21792
22256
|
}
|
|
21793
22257
|
function addAlbSpecificOptions(cmd) {
|
|
21794
22258
|
addImageOverrideOptions(cmd);
|
|
21795
|
-
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 this command INJECTS only when an inbound request has none (the default-when-missing role) — `cdkl start-alb` is the local ALB front-door RECEIVING outside-in requests, so this token is the fallback the front-door slots in as Authorization: Bearer <jwt> if the caller did not already supply one. Verified against the same JWKS / OIDC discovery URL the deployed ALB would (signature + iss + aud + exp). Cookie pass-through (AWSELBAuthSessionCookie-*) also bypasses the guard. Contrast with `cdkl invoke-agentcore --bearer-token`, where the role is reversed — that command is the outbound client and ALWAYS presents this token (the supplier).")).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 +
|
|
22259
|
+
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 this command INJECTS only when an inbound request has none (the default-when-missing role) — `cdkl start-alb` is the local ALB front-door RECEIVING outside-in requests, so this token is the fallback the front-door slots in as Authorization: Bearer <jwt> if the caller did not already supply one. Verified against the same JWKS / OIDC discovery URL the deployed ALB would (signature + iss + aud + exp). Cookie pass-through (AWSELBAuthSessionCookie-*) also bypasses the guard. Contrast with `cdkl invoke-agentcore --bearer-token`, where the role is reversed — that command is the outbound client and ALWAYS presents this token (the supplier).")).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 + restart it; no rebuild, its front-door pool entry is drained and re-published once it listens again). 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));
|
|
21796
22260
|
}
|
|
21797
22261
|
|
|
21798
22262
|
//#endregion
|
|
@@ -28575,25 +29039,9 @@ async function relayServeRequest(input, fetchFn = fetch, clock = Date.now) {
|
|
|
28575
29039
|
//#endregion
|
|
28576
29040
|
//#region src/local/studio-proxy.ts
|
|
28577
29041
|
let proxyIdCounter = 0;
|
|
28578
|
-
function isLoopbackHostname(hostname) {
|
|
28579
|
-
const h = hostname.trim().replace(/^\[/, "").replace(/\]$/, "").toLowerCase();
|
|
28580
|
-
if (h === "localhost") return true;
|
|
28581
|
-
if (h === "::1" || h === "0:0:0:0:0:0:0:1") return true;
|
|
28582
|
-
const mapped = /^::ffff:([0-9a-f]{1,4}):([0-9a-f]{1,4})$/.exec(h);
|
|
28583
|
-
if (mapped) return parseInt(mapped[1], 16) >> 8 === 127;
|
|
28584
|
-
const dotted = /^(?:::ffff:)?(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/.exec(h);
|
|
28585
|
-
if (!dotted) return false;
|
|
28586
|
-
const octets = dotted.slice(1).map(Number);
|
|
28587
|
-
if (octets.some((o) => o > 255)) return false;
|
|
28588
|
-
return octets[0] === 127;
|
|
28589
|
-
}
|
|
28590
29042
|
function stripHostBrackets(hostname) {
|
|
28591
29043
|
return hostname.replace(/^\[/, "").replace(/\]$/, "");
|
|
28592
29044
|
}
|
|
28593
|
-
function isWildcardHostname(hostname) {
|
|
28594
|
-
const h = hostname.trim().replace(/^\[/, "").replace(/\]$/, "").toLowerCase();
|
|
28595
|
-
return h === "0.0.0.0" || h === "::" || h === "0:0:0:0:0:0:0:0" || h === "::ffff:0.0.0.0" || h === "::ffff:0:0";
|
|
28596
|
-
}
|
|
28597
29045
|
function rewriteWildcardHostToken(upstream) {
|
|
28598
29046
|
const schemeEnd = upstream.indexOf("://");
|
|
28599
29047
|
if (schemeEnd === -1) return void 0;
|
|
@@ -29887,5 +30335,5 @@ function addStudioSpecificOptions(cmd) {
|
|
|
29887
30335
|
}
|
|
29888
30336
|
|
|
29889
30337
|
//#endregion
|
|
29890
|
-
export { applyEdgeResponseResult as $,
|
|
29891
|
-
//# sourceMappingURL=local-studio-
|
|
30338
|
+
export { applyEdgeResponseResult as $, defaultCredentialsLoader as $n, resolveCfnFallbackRegion as $r, isLocalCdkAssetImage as $t, startAgentCoreHttpServer as A, LocalInvokeBuildError as Ai, renderCodeDockerfile as An, resolveHostGatewayExtraHosts as Ar, addRunTaskSpecificOptions as At, idFromArn as B, withoutKeys as Bi, createFileWatcher as Bn, buildContainerImage as Br, resolveEcsAssumeRoleOption as Bt, addListSpecificOptions as C, AgentCoreResolutionError as Ci, invokeAgentCore as Cn, pickResponseTemplate as Cr, parseLbPortOverrides as Ct, createLocalStartAgentCoreCommand as D, formatStateRemedy as Di, SUPPORTED_CODE_RUNTIMES as Dn, HOST_DOCKER_INTERNAL_GATEWAY as Dr, addStartServiceSpecificOptions as Dt, addStartAgentCoreSpecificOptions as E, derivePseudoParametersFromRegion as Ei, downloadAndExtractS3Bundle as En, VtlEvaluationError as Er, resolveAlbFrontDoor as Et, createLocalStartCloudFrontCommand as F, firstUsableRegion as Fi, addStartApiSpecificOptions as Fn, parseConnectionsPath as Fr, addImageOverrideOptions as Ft, classifyS3Error as G, recordAssumedRole as Gi, availableApiIdentifiers as Gn, substituteAgainstState as Gr, canonicalizeImageOverrideKeys as Gt, createDeployedKvsDataSource as H, assumedRoleRecorded as Hi, buildStageMap as Hn, resolveRuntimeFileExtension as Hr, runEcsServiceEmulator as Ht, normalizeKvsFileKeys as I, keysNotFromTemplate as Ii, createLocalStartApiCommand as In, buildConnectEvent as Ir, buildEcsImageResolutionContext$1 as It, startCloudFrontServer as J, workloadStsClientConfig as Ji, groupRoutesByServer as Jn, substituteEnvVarsFromStateAsync as Jr, parseImageOverrideFlags as Jt, createS3OriginReader as K, resetWorkloadIdentity as Ki, filterRoutesByApiIdentifier as Kn, substituteAgainstStateAsync as Kr, enforceImageOverrideOrphans as Kt, parseKvsFileOverrides as L, keysOverriddenBy as Li, createWatchPredicates as Ln, buildDisconnectEvent as Lr, ecsClusterOption as Lt, startAgentCoreWsBridge as M, DynamicReferenceResolver as Mi, classifySourceChange as Mn, ConnectionRegistry as Mr, MAX_TASKS_SUBNET_RANGE_CAP as Mt, LocalStartCloudFrontError as N, containsDynamicReference as Ni, addInvokeSpecificOptions as Nn, buildMgmtEndpointEnvUrl as Nr, addCommonEcsServiceOptions as Nt, buildAgentCoreServeAuthCheck as O, substituteImagePlaceholders as Oi, buildAgentCoreCodeImage as On, HOST_GATEWAY_MIN_VERSION as Or, createLocalStartServiceCommand as Ot, addStartCloudFrontSpecificOptions as P, deployedFillMayBeSecret as Pi, createLocalInvokeCommand as Pn, handleConnectionsRequest as Pr, addEcsAssumeRoleOptions as Pt, applyEdgeRequestResult as Q, isProxyEnvConfigured as Qi, resolveServiceIntegrationParameters as Qn, rejectExplicitCfnStackWithMultipleStacks as Qr, imageOverrideKeyAliases as Qt, parseOriginOverrides as R, parseDynamicReference as Ri, resolveApiTargetSubset as Rn, buildMessageEvent as Rr, parseMaxTasks as Rt, StudioEventBus as S, AGENTCORE_RUNTIME_TYPE as Si, AGENTCORE_SESSION_ID_HEADER as Sn, evaluateResponseParameters as Sr, createLocalStartAlbCommand as St, formatTargetListing as T, resolveAgentCoreTarget as Ti, waitForAgentCorePing as Tn, tryParseStatus as Tr, isApplicationLoadBalancer as Tt, resolveDeployedKvsArnByName as U, deployAccountStsClientConfig as Ui, materializeLayerFromArn as Un, resolveRuntimeImage as Ur, ImageOverrideError as Ut, resolveKvsModulesForDistribution as V, SECOND_ASSUME_MESSAGE as Vi, attachStageContext as Vn, resolveRuntimeCodeMountPath as Vr, resolveSharedSidecarCredentials as Vt, resolveDeployedOriginBucket as W, readEnvCallerCredentials as Wi, resolveEnvVars$1 as Wn, EcsTaskResolutionError as Wr, buildImageOverrideTag as Wt, serveFromStaticOrigin as X, resolveProfileCredentials as Xi, startApiServer as Xn, createLocalStateProvider as Xr, runImageOverrideBuilds as Xt, resolveErrorResponseCandidates as Y, buildStsClientConfig as Yi, readMtlsMaterialsFromDisk as Yn, LocalStateSourceError as Yr, resolveImageOverrides as Yt, serveLambdaUrlOrigin as Z, buildProxyClientConfig as Zi, resolveSelectionExpression as Zn, isCfnFlagPresent as Zr, describePinnedImageUri as Zt, filterStudioTargetGroups as _, resolveLambdaArnIntrinsic as _i, MCP_PROTOCOL_VERSION as _n, matchRoute as _r, createCloudFrontModule as _t, createLocalStudioCommand as a, resolveWatchConfig as ai, setShadowReadyTimeoutMs as an, verifyJwtViaDiscovery as ar, describeS3OriginDomain as at, renderStudioHtml as b, AGENTCORE_HTTP_PROTOCOL as bi, AGENTCORE_SIGV4_SERVICE as bn, buildHttpApiV2Event as br, addAlbSpecificOptions as bt, startStudioProxy as c, listTargets as ci, addInvokeAgentCoreSpecificOptions as cn, evaluateCachedLambdaPolicy as cr, pickFunctionUrlLogicalIdFromOrigin as ct, createStudioDispatcher as d, discoverWebSocketApisOrThrow as di, invokeAgentCoreWs as dn, attachAuthorizers as dr, pickTargetFunctionLogicalId as dt, resolveCfnRegion as ei, listPinnedTargets as en, buildCognitoJwksUrl as er, buildEdgeRequestEvent as et, filterStudioCustomResources as f, filterWebSocketApisByIdentifiers as fi, A2A_CONTAINER_PORT as fn, applyCorsResponseHeaders as fr, resolveCloudFrontDistribution as ft, annotatePinnedEcsTargets as g, pickRefLogicalId as gi, MCP_PATH as gn, matchPreflight as gr, stripCloudFrontImport as gt, annotateEcsTaskPinnedTargets as h, discoverRoutes as hi, MCP_CONTAINER_PORT as hn, isFunctionUrlOacFronted as hr, runViewerResponse as ht, coerceStopRequest as i, resolveSsmParameters as ii, SOFT_RELOAD_COMPLETION_LOG_SUFFIX as in, verifyJwtAuthorizer as ir, CLOUDFRONT_DISTRIBUTION_TYPE as it, attachAgentCoreWsBridge as j, DynamicReferenceResolutionError as ji, toCmdArgv as jn, bufferToBody as jr, createLocalRunTaskCommand as jt, selectServeInboundAuth as k, tryResolveImageFnJoin as ki, computeCodeImageTag as kn, probeHostGatewaySupport as kr, serviceStrategy as kt, relayServeRequest as l, availableWebSocketApiIdentifiers as li, createLocalInvokeAgentCoreCommand as ln, invokeRequestAuthorizer as lr, pickKvsLogicalIdFromArn as lt, annotateAlbPinnedBackingServices as m, webSocketApiMatchesIdentifier as mi, a2aInvokeOnce as mn, buildCorsConfigFromCloudFrontChain as mr, runViewerRequest as mt, coerceRunRequest as n, CfnLocalStateProvider as ni, CloudMapRegistry as nn, createJwksCache as nr, edgeHeadersToHttp as nt, resolveServeBaseUrl as o, resolveSingleTarget as oi, getContainerNetworkIp as on, buildMethodArn as or, extractKvsAssociations as ot, isCustomResourceLambdaTarget as p, parseSelectionExpressionPath as pi, A2A_PATH as pn, buildCorsConfigByApiId as pr, compileCloudFrontFunction as pt, matchBehavior as q, workloadClientConfig as qi, filterRoutesByApiIdentifiers as qn, substituteEnvVarsFromState as qr, mergeForService as qt, coerceServeRequest as r, collectSsmParameterRefs as ri, DEFAULT_SHADOW_READY_TIMEOUT_MS as rn, verifyCognitoJwt as rr, httpHeadersToEdge as rt, createStudioServeManager as s, countTargets as si, attachContainerLogStreamer as sn, computeRequestIdentityHash as sr, isCloudFrontDistribution as st, addStudioSpecificOptions as t, resolveCfnStackName as ti, buildCloudMapIndex as tn, buildJwksUrlFromIssuer as tr, buildEdgeResponseEvent as tt, reinvoke as u, discoverWebSocketApis as ui, bridgeAgentCoreWs as un, invokeTokenAuthorizer as ur, pickLambdaEdgeFunctionLogicalId as ut, startStudioServer as v, AGENTCORE_A2A_PROTOCOL as vi, mcpInvokeOnce as vn, translateLambdaResponse as vr, createLocalFileKvsDataSource as vt, createLocalListCommand as w, pickAgentCoreCandidateStack as wi, waitForAgentCoreHttpReady as wn, selectIntegrationResponse as wr, resolveAlbTarget as wt, createStudioStore as x, AGENTCORE_MCP_PROTOCOL as xi, signAgentCoreInvocation as xn, buildRestV1Event as xr, albStrategy as xt, toStudioTargetGroups as y, AGENTCORE_AGUI_PROTOCOL as yi, parseSseForJsonRpc as yn, applyAuthorizerOverlay as yr, createUnboundCloudFrontModule as yt, resolveCloudFrontTarget as z, resolveDynamicReferencesInEnv as zi, createAuthorizerCache as zn, architectureToPlatform as zr, parseRestartPolicy as zt };
|
|
30339
|
+
//# sourceMappingURL=local-studio-DCTVck5l.js.map
|