@go-to-k/cdkd 0.147.1 → 0.148.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 +2 -0
- package/dist/cli.js +242 -29
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -306,6 +306,8 @@ cdkd state info --state-bucket my-bucket # explicit bucket; reports Source: --s
|
|
|
306
306
|
cdkd state list
|
|
307
307
|
cdkd state ls --long # include resource count, last-modified, lock status
|
|
308
308
|
cdkd state list --json # JSON output (alone, or combined with --long)
|
|
309
|
+
cdkd state list --tree # parent → child stack tree (nested stacks; #555 A3)
|
|
310
|
+
cdkd state list --tree --json # tree as nested JSON
|
|
309
311
|
|
|
310
312
|
# List resources of a single stack from state
|
|
311
313
|
cdkd state resources MyStack # aligned columns: LogicalID, Type, PhysicalID
|
package/dist/cli.js
CHANGED
|
@@ -34897,6 +34897,95 @@ function createStateMigrateCommand() {
|
|
|
34897
34897
|
return cmd;
|
|
34898
34898
|
}
|
|
34899
34899
|
|
|
34900
|
+
//#endregion
|
|
34901
|
+
//#region src/cli/commands/state-list-tree.ts
|
|
34902
|
+
/**
|
|
34903
|
+
* Build a parent → child tree from the flat list of state records.
|
|
34904
|
+
*
|
|
34905
|
+
* Children are linked to their parent by `(parentStack, parentRegion)`
|
|
34906
|
+
* matching another entry's `(stackName, region)`. Children whose parent
|
|
34907
|
+
* isn't present in the input (orphans — parent state was hand-deleted,
|
|
34908
|
+
* or destroyed out-of-band) are reported at the root level so they stay
|
|
34909
|
+
* visible to `cdkd state list` rather than vanishing silently.
|
|
34910
|
+
*
|
|
34911
|
+
* A self-link (parent equals self) is treated as a missing parent — the
|
|
34912
|
+
* node lands at the root rather than building an infinite tree.
|
|
34913
|
+
*
|
|
34914
|
+
* The roots and every child list are sorted alphabetically by `stackName`,
|
|
34915
|
+
* then by `region` (legacy `undefined` last), so output is stable across
|
|
34916
|
+
* runs.
|
|
34917
|
+
*/
|
|
34918
|
+
function buildStackTree(entries) {
|
|
34919
|
+
const refKey = (stackName, region) => `${stackName}\0${region ?? ""}`;
|
|
34920
|
+
const byKey = /* @__PURE__ */ new Map();
|
|
34921
|
+
for (const entry of entries) byKey.set(refKey(entry.stackName, entry.region), {
|
|
34922
|
+
...entry,
|
|
34923
|
+
children: []
|
|
34924
|
+
});
|
|
34925
|
+
const roots = [];
|
|
34926
|
+
for (const node of byKey.values()) {
|
|
34927
|
+
if (node.parentStack !== void 0) {
|
|
34928
|
+
const parent = byKey.get(refKey(node.parentStack, node.parentRegion));
|
|
34929
|
+
if (parent && parent !== node) {
|
|
34930
|
+
parent.children.push(node);
|
|
34931
|
+
continue;
|
|
34932
|
+
}
|
|
34933
|
+
}
|
|
34934
|
+
roots.push(node);
|
|
34935
|
+
}
|
|
34936
|
+
const cmp = (a, b) => {
|
|
34937
|
+
if (a.stackName < b.stackName) return -1;
|
|
34938
|
+
if (a.stackName > b.stackName) return 1;
|
|
34939
|
+
const ar = a.region ?? "";
|
|
34940
|
+
const br = b.region ?? "";
|
|
34941
|
+
if (ar < br) return -1;
|
|
34942
|
+
if (ar > br) return 1;
|
|
34943
|
+
return 0;
|
|
34944
|
+
};
|
|
34945
|
+
const sortRecursive = (list) => {
|
|
34946
|
+
list.sort(cmp);
|
|
34947
|
+
for (const node of list) sortRecursive(node.children);
|
|
34948
|
+
};
|
|
34949
|
+
sortRecursive(roots);
|
|
34950
|
+
return roots;
|
|
34951
|
+
}
|
|
34952
|
+
/**
|
|
34953
|
+
* Render the tree using `tree(1)`-style box-drawing prefixes.
|
|
34954
|
+
*
|
|
34955
|
+
* Each line is built by `formatLine(node)`. The caller picks the
|
|
34956
|
+
* human-readable shape (e.g. `Stack (region)`); this helper only owns the
|
|
34957
|
+
* indentation.
|
|
34958
|
+
*/
|
|
34959
|
+
function renderStackTreeAscii(roots, formatLine) {
|
|
34960
|
+
const lines = [];
|
|
34961
|
+
for (const root of roots) {
|
|
34962
|
+
lines.push(formatLine(root));
|
|
34963
|
+
renderChildren(root.children, "", lines, formatLine);
|
|
34964
|
+
}
|
|
34965
|
+
return lines.join("\n");
|
|
34966
|
+
}
|
|
34967
|
+
function renderChildren(children, prefix, out, formatLine) {
|
|
34968
|
+
const lastIdx = children.length - 1;
|
|
34969
|
+
for (let i = 0; i < children.length; i++) {
|
|
34970
|
+
const child = children[i];
|
|
34971
|
+
const isLast = i === lastIdx;
|
|
34972
|
+
const branch = isLast ? "└── " : "├── ";
|
|
34973
|
+
const childPrefix = isLast ? " " : "│ ";
|
|
34974
|
+
out.push(`${prefix}${branch}${formatLine(child)}`);
|
|
34975
|
+
renderChildren(child.children, prefix + childPrefix, out, formatLine);
|
|
34976
|
+
}
|
|
34977
|
+
}
|
|
34978
|
+
function stackTreeToJson(roots) {
|
|
34979
|
+
return roots.map((node) => ({
|
|
34980
|
+
stackName: node.stackName,
|
|
34981
|
+
region: node.region ?? null,
|
|
34982
|
+
parentStack: node.parentStack ?? null,
|
|
34983
|
+
parentLogicalId: node.parentLogicalId ?? null,
|
|
34984
|
+
parentRegion: node.parentRegion ?? null,
|
|
34985
|
+
children: stackTreeToJson(node.children)
|
|
34986
|
+
}));
|
|
34987
|
+
}
|
|
34988
|
+
|
|
34900
34989
|
//#endregion
|
|
34901
34990
|
//#region src/cli/commands/state.ts
|
|
34902
34991
|
/**
|
|
@@ -35001,6 +35090,10 @@ function sortRefs(refs) {
|
|
|
35001
35090
|
* `version: 1` records (no region) appear as plain `Stack` rows.
|
|
35002
35091
|
* - `--long`/`-l`: include resource count, last-modified time, and lock status.
|
|
35003
35092
|
* - `--json`: emit a JSON array (alongside or instead of the long form).
|
|
35093
|
+
* - `--tree`: render parent → child stack tree (issue #555 A3). Loads each
|
|
35094
|
+
* state record to read the v6 `parentStack` / `parentRegion` fields, then
|
|
35095
|
+
* reconstructs the hierarchy. Flat default is preserved so tooling that
|
|
35096
|
+
* greps the existing one-per-line shape keeps working.
|
|
35004
35097
|
*/
|
|
35005
35098
|
async function stateListCommand(options) {
|
|
35006
35099
|
const logger = getLogger();
|
|
@@ -35008,6 +35101,10 @@ async function stateListCommand(options) {
|
|
|
35008
35101
|
const setup = await setupStateBackend(options);
|
|
35009
35102
|
try {
|
|
35010
35103
|
const refs = sortRefs(await setup.stateBackend.listStacks());
|
|
35104
|
+
if (options.tree) {
|
|
35105
|
+
await renderTreeMode(refs, setup.stateBackend, options.json);
|
|
35106
|
+
return;
|
|
35107
|
+
}
|
|
35011
35108
|
if (!options.long && !options.json) {
|
|
35012
35109
|
for (const ref of refs) process.stdout.write(`${formatStackRef(ref)}\n`);
|
|
35013
35110
|
return;
|
|
@@ -35057,10 +35154,56 @@ async function stateListCommand(options) {
|
|
|
35057
35154
|
}
|
|
35058
35155
|
}
|
|
35059
35156
|
/**
|
|
35157
|
+
* Render the parent → child stack tree for `cdkd state list --tree`.
|
|
35158
|
+
*
|
|
35159
|
+
* Loads each state record in parallel to read the v6 `parentStack` /
|
|
35160
|
+
* `parentLogicalId` / `parentRegion` fields, then hands the enriched flat
|
|
35161
|
+
* list to {@link buildStackTree}. A missing state record (NoSuchKey returns
|
|
35162
|
+
* `null` from getState) OR an unreadable one (transient S3 503 / throttle /
|
|
35163
|
+
* per-stack IAM hiccup throws) degrades to a top-level entry (no parent
|
|
35164
|
+
* link) — the row still appears in the tree at the root level rather than
|
|
35165
|
+
* vanishing, and one bad record never kills the whole view.
|
|
35166
|
+
*
|
|
35167
|
+
* `tree --json` emits the nested {@link import('./state-list-tree.js').StackTreeJson}
|
|
35168
|
+
* shape; plain `tree` renders `tree(1)`-style box-drawing.
|
|
35169
|
+
*/
|
|
35170
|
+
async function renderTreeMode(refs, stateBackend, asJson) {
|
|
35171
|
+
const roots = buildStackTree(await Promise.all(refs.map(async (ref) => {
|
|
35172
|
+
if (!ref.region) return { stackName: ref.stackName };
|
|
35173
|
+
let state;
|
|
35174
|
+
try {
|
|
35175
|
+
state = (await stateBackend.getState(ref.stackName, ref.region))?.state;
|
|
35176
|
+
} catch {
|
|
35177
|
+
return {
|
|
35178
|
+
stackName: ref.stackName,
|
|
35179
|
+
region: ref.region
|
|
35180
|
+
};
|
|
35181
|
+
}
|
|
35182
|
+
return {
|
|
35183
|
+
stackName: ref.stackName,
|
|
35184
|
+
region: ref.region,
|
|
35185
|
+
...state?.parentStack !== void 0 && { parentStack: state.parentStack },
|
|
35186
|
+
...state?.parentLogicalId !== void 0 && { parentLogicalId: state.parentLogicalId },
|
|
35187
|
+
...state?.parentRegion !== void 0 && { parentRegion: state.parentRegion }
|
|
35188
|
+
};
|
|
35189
|
+
})));
|
|
35190
|
+
if (asJson) {
|
|
35191
|
+
process.stdout.write(`${JSON.stringify(stackTreeToJson(roots), null, 2)}\n`);
|
|
35192
|
+
return;
|
|
35193
|
+
}
|
|
35194
|
+
if (roots.length === 0) return;
|
|
35195
|
+
const rendered = renderStackTreeAscii(roots, (node) => formatStackRef({
|
|
35196
|
+
stackName: node.stackName,
|
|
35197
|
+
...node.region ? { region: node.region } : {}
|
|
35198
|
+
}));
|
|
35199
|
+
process.stdout.write(`${rendered}\n`);
|
|
35200
|
+
}
|
|
35201
|
+
/**
|
|
35060
35202
|
* Create the `state list` subcommand.
|
|
35061
35203
|
*/
|
|
35062
35204
|
function createStateListCommand() {
|
|
35063
|
-
const
|
|
35205
|
+
const treeOption = new Option("--tree", "Render parent → child stack tree (loads each state record to read the v6 parent link)").default(false).conflicts("long");
|
|
35206
|
+
const cmd = new Command("list").alias("ls").description("List stacks registered in the cdkd state bucket").option("-l, --long", "Show resource count, last-modified time, and lock status", false).option("--json", "Output as JSON", false).addOption(treeOption).action(withErrorHandling(stateListCommand));
|
|
35064
35207
|
[...commonOptions, ...stateOptions].forEach((opt) => cmd.addOption(opt));
|
|
35065
35208
|
cmd.addOption(deprecatedRegionOption);
|
|
35066
35209
|
return cmd;
|
|
@@ -36647,9 +36790,14 @@ async function injectRetainPoliciesRecursiveInternal(templateBody, cfnStackName,
|
|
|
36647
36790
|
};
|
|
36648
36791
|
}
|
|
36649
36792
|
/**
|
|
36650
|
-
*
|
|
36651
|
-
* the full
|
|
36652
|
-
* `
|
|
36793
|
+
* Walk a CloudFormation stack and every nested child recursively, returning
|
|
36794
|
+
* the full {@link CfnStackResourceTree} rooted at `rootStackName`. Used by
|
|
36795
|
+
* `cdkd import --migrate-from-cloudformation` to drive BOTH per-child state
|
|
36796
|
+
* writes AND the recursive `injectRetainPoliciesRecursive` walk — both
|
|
36797
|
+
* consumers share this single `DescribeStackResources` tree so an
|
|
36798
|
+
* arbitrarily-deep nesting only costs one round-trip per stack.
|
|
36799
|
+
*
|
|
36800
|
+
* For each `AWS::CloudFormation::Stack` row in the root's resources, recursively
|
|
36653
36801
|
* calls `DescribeStackResources(<child ARN>)` (AWS accepts the ARN as
|
|
36654
36802
|
* `StackName`) to populate the child node, and so on to arbitrary depth.
|
|
36655
36803
|
*
|
|
@@ -39498,6 +39646,7 @@ function extractTaskDefinitionProperties(stack, logicalId, resource, context) {
|
|
|
39498
39646
|
if (rawNetworkMode === "bridge" || rawNetworkMode === "awsvpc" || rawNetworkMode === "host" || rawNetworkMode === "none") networkMode = rawNetworkMode;
|
|
39499
39647
|
else throw new EcsTaskResolutionError(`Task definition '${logicalId}' has unsupported NetworkMode '${rawNetworkMode}'. Supported values: bridge / awsvpc / host / none.`);
|
|
39500
39648
|
if (networkMode === "awsvpc") warnings.push(`NetworkMode 'awsvpc' on '${logicalId}' is mapped to docker bridge locally — docker cannot emulate ENI-per-task. AWS SDK calls still reach public endpoints via the developer network.`);
|
|
39649
|
+
if (props["ProxyConfiguration"]) warnings.push(`Task definition '${logicalId}' declares 'ProxyConfiguration' (custom Envoy / App Mesh bootstrap), which is NOT honored by 'cdkd local start-service' / 'cdkd local run-task' in v1. Local execution will run without the configured proxy. See design doc § 2 for the rationale.`);
|
|
39501
39650
|
const resources = stack.template.Resources ?? {};
|
|
39502
39651
|
const subContext = buildSubstitutionContextFromImageContext(context);
|
|
39503
39652
|
const taskRoleArn = resolveRoleArn(props["TaskRoleArn"], resources, subContext);
|
|
@@ -52066,7 +52215,7 @@ function extractServiceProperties(stack, serviceLogicalId, resource, stacks, con
|
|
|
52066
52215
|
desiredCount,
|
|
52067
52216
|
healthCheckGracePeriodSeconds,
|
|
52068
52217
|
task,
|
|
52069
|
-
serviceRegistries: extractServiceRegistries(props["ServiceRegistries"]),
|
|
52218
|
+
serviceRegistries: extractServiceRegistries(props["ServiceRegistries"], serviceLogicalId, warnings),
|
|
52070
52219
|
warnings
|
|
52071
52220
|
};
|
|
52072
52221
|
if (serviceConnect) out.serviceConnect = serviceConnect;
|
|
@@ -52139,8 +52288,16 @@ function extractServiceConnect(raw, task) {
|
|
|
52139
52288
|
* canonical `Fn::GetAtt: [<CloudMapServiceLogicalId>, 'Arn']` shape;
|
|
52140
52289
|
* cdkd surfaces the logical id (the AWS-side ARN is irrelevant
|
|
52141
52290
|
* locally — the registry is in-process).
|
|
52291
|
+
*
|
|
52292
|
+
* Issue #544 — entries with a literal-string `RegistryArn` (rare
|
|
52293
|
+
* locally — would imply the user bound to an existing Cloud Map
|
|
52294
|
+
* service deployed out-of-band) are skipped with a warning, since the
|
|
52295
|
+
* in-process registry cannot resolve an external Cloud Map service
|
|
52296
|
+
* back to its `(namespace, name)` pair. Pre-fix this was a silent
|
|
52297
|
+
* `continue` and the user got no feedback about why the registration
|
|
52298
|
+
* didn't show up.
|
|
52142
52299
|
*/
|
|
52143
|
-
function extractServiceRegistries(raw) {
|
|
52300
|
+
function extractServiceRegistries(raw, serviceLogicalId, warnings) {
|
|
52144
52301
|
if (!Array.isArray(raw)) return [];
|
|
52145
52302
|
const out = [];
|
|
52146
52303
|
for (const entry of raw) {
|
|
@@ -52148,7 +52305,10 @@ function extractServiceRegistries(raw) {
|
|
|
52148
52305
|
const e = entry;
|
|
52149
52306
|
const registryArn = e["RegistryArn"];
|
|
52150
52307
|
let cloudMapServiceLogicalId;
|
|
52151
|
-
if (typeof registryArn === "string")
|
|
52308
|
+
if (typeof registryArn === "string") {
|
|
52309
|
+
warnings.push(`ECS Service '${serviceLogicalId}' ServiceRegistries[] entry has a literal-string RegistryArn ('${registryArn}'); cdkd cannot resolve external Cloud Map services locally. Skipping this registration; peer services will not discover this endpoint through the in-process registry. Use Fn::GetAtt: [<CloudMapServiceLogicalId>, "Arn"] instead so cdkd can resolve the namespace + service name from the synthesized template.`);
|
|
52310
|
+
continue;
|
|
52311
|
+
}
|
|
52152
52312
|
if (registryArn && typeof registryArn === "object" && !Array.isArray(registryArn)) {
|
|
52153
52313
|
const getAtt = registryArn["Fn::GetAtt"];
|
|
52154
52314
|
if (Array.isArray(getAtt) && typeof getAtt[0] === "string") cloudMapServiceLogicalId = getAtt[0];
|
|
@@ -52329,6 +52489,38 @@ function backoffDelayMs(restartCount) {
|
|
|
52329
52489
|
return Math.min(1e3 * Math.pow(2, Math.min(restartCount, 10)), 3e4);
|
|
52330
52490
|
}
|
|
52331
52491
|
/**
|
|
52492
|
+
* Maximum number of replica indices the per-replica subnet allocator
|
|
52493
|
+
* can serve without modulo-wrap collision. The allocator below walks
|
|
52494
|
+
* the link-local /24 range `169.254.170.0..169.254.253.0` (84 octets)
|
|
52495
|
+
* and **skips 171** because that octet is owned by the shared-service
|
|
52496
|
+
* network in design § 5 Option A (see `SHARED_SVC_SUBNET_OCTET`), so
|
|
52497
|
+
* the usable count is 83. The CLI's `--max-tasks` parser enforces this
|
|
52498
|
+
* cap before any boot work fires.
|
|
52499
|
+
*/
|
|
52500
|
+
const SUBNET_ALLOCATOR_RANGE = 83;
|
|
52501
|
+
/**
|
|
52502
|
+
* Defensive per-replica subnet octet allocator (Issue #544). Only used
|
|
52503
|
+
* when callers bypass the CLI's `sharedNetwork` construction — i.e.
|
|
52504
|
+
* test paths that hand-build `ServiceRunnerOptions.discovery` without
|
|
52505
|
+
* `sharedNetwork`, or the bare `cdkd local run-task`-shaped path that
|
|
52506
|
+
* runs one network per task. Production `cdkd local start-service`
|
|
52507
|
+
* runs always go through the shared network (design § 5 Option A) so
|
|
52508
|
+
* this allocator is unreachable in the standard path.
|
|
52509
|
+
*
|
|
52510
|
+
* Returns the second-from-last octet of the per-replica /24 (170 →
|
|
52511
|
+
* `169.254.170.0/24`). Walks the 83-slot output range
|
|
52512
|
+
* `[170, 172, 173, ..., 253]` — 171 is intentionally **skipped**
|
|
52513
|
+
* because it's reserved for the shared-service network sidecar
|
|
52514
|
+
* (`SHARED_SVC_SUBNET_OCTET`), and assigning a per-replica network
|
|
52515
|
+
* the same /24 would have docker reject the duplicate-subnet
|
|
52516
|
+
* `network create` with the cryptic "Pool overlaps with other one on
|
|
52517
|
+
* this address space" error.
|
|
52518
|
+
*/
|
|
52519
|
+
function pickSubnetOctet(index) {
|
|
52520
|
+
const candidate = 170 + (index % 83 + 83) % 83;
|
|
52521
|
+
return candidate < 171 ? candidate : candidate + 1;
|
|
52522
|
+
}
|
|
52523
|
+
/**
|
|
52332
52524
|
* Decide whether a replica that just exited should restart. Pure-
|
|
52333
52525
|
* functional so the watcher loop's policy is easy to unit-test.
|
|
52334
52526
|
*/
|
|
@@ -52507,7 +52699,7 @@ async function bootReplica(service, options, instance) {
|
|
|
52507
52699
|
...options.taskOptions,
|
|
52508
52700
|
cluster: perReplicaCluster,
|
|
52509
52701
|
detach: true,
|
|
52510
|
-
...sharedNetwork ? { existingNetwork: sharedNetwork } : { subnetOctet:
|
|
52702
|
+
...sharedNetwork ? { existingNetwork: sharedNetwork } : { subnetOctet: pickSubnetOctet(instance.index) },
|
|
52511
52703
|
...addHostFlags.length > 0 ? { addHostFlags } : {},
|
|
52512
52704
|
...networkAliasesByContainer.size > 0 ? { networkAliasesByContainer } : {}
|
|
52513
52705
|
};
|
|
@@ -52709,8 +52901,13 @@ var CloudMapRegistry = class {
|
|
|
52709
52901
|
/**
|
|
52710
52902
|
* Map<alias, fqdn> — secondary index for ClientAlias short forms.
|
|
52711
52903
|
* Multiple aliases can point at the same fqdn; one alias only points
|
|
52712
|
-
* at one fqdn
|
|
52713
|
-
*
|
|
52904
|
+
* at one fqdn. **First-wins on collision** — consistent with design
|
|
52905
|
+
* §O6's namespace-level first-wins rule for `PrivateDnsNamespace`. A
|
|
52906
|
+
* collision (two services declaring the same `ClientAlias.DnsName`)
|
|
52907
|
+
* emits a `logger.warn` so users can debug "why does
|
|
52908
|
+
* `wget http://api/` reach service B instead of service A" without
|
|
52909
|
+
* silently shadowing the prior binding. Idempotent re-register of
|
|
52910
|
+
* the same `(alias, targetFqdn)` pair is a no-op and does NOT warn.
|
|
52714
52911
|
*/
|
|
52715
52912
|
aliasIndex = /* @__PURE__ */ new Map();
|
|
52716
52913
|
/**
|
|
@@ -52746,8 +52943,13 @@ var CloudMapRegistry = class {
|
|
|
52746
52943
|
* suffix). Cloud Map / Service Connect does NOT auto-create such
|
|
52747
52944
|
* aliases — they're populated by `ClientAliases[].DnsName` entries in
|
|
52748
52945
|
* the consumer service's `ServiceConnectConfiguration`. Aliases are
|
|
52749
|
-
* scoped per-CLI-invocation and
|
|
52750
|
-
*
|
|
52946
|
+
* scoped per-CLI-invocation and **first-wins on collision** —
|
|
52947
|
+
* consistent with design §O6's namespace-level first-wins rule. The
|
|
52948
|
+
* first registration sticks; later attempts to bind the same alias
|
|
52949
|
+
* to a different fqdn are ignored and a `logger.warn` is emitted so
|
|
52950
|
+
* users can debug "why does `wget http://api/` reach service B
|
|
52951
|
+
* instead of service A". Re-registering the same `(alias,
|
|
52952
|
+
* targetFqdn)` pair is idempotent and does NOT warn.
|
|
52751
52953
|
*
|
|
52752
52954
|
* @param alias The bare discovery name (e.g. `orders` for an alias to
|
|
52753
52955
|
* `orders.cdkd-local.local`).
|
|
@@ -52757,6 +52959,12 @@ var CloudMapRegistry = class {
|
|
|
52757
52959
|
registerAlias(alias, targetFqdn) {
|
|
52758
52960
|
if (!alias) throw new Error("CloudMapRegistry.registerAlias: alias must be a non-empty string.");
|
|
52759
52961
|
if (!targetFqdn) throw new Error("CloudMapRegistry.registerAlias: targetFqdn must be a non-empty string.");
|
|
52962
|
+
const existing = this.aliasIndex.get(alias);
|
|
52963
|
+
if (existing !== void 0) {
|
|
52964
|
+
if (existing === targetFqdn) return;
|
|
52965
|
+
getLogger().child("cloud-map-registry").warn(`ClientAlias DnsName collision: '${alias}' was already mapped to '${existing}'; keeping first-wins binding and ignoring new mapping to '${targetFqdn}'. Likely cause: two Service Connect services declared the same ClientAlias.DnsName.`);
|
|
52966
|
+
return;
|
|
52967
|
+
}
|
|
52760
52968
|
this.aliasIndex.set(alias, targetFqdn);
|
|
52761
52969
|
}
|
|
52762
52970
|
/**
|
|
@@ -53002,6 +53210,7 @@ async function localStartServiceCommand(targets, options) {
|
|
|
53002
53210
|
const logger = getLogger();
|
|
53003
53211
|
if (options.verbose) logger.setLevel("debug");
|
|
53004
53212
|
warnIfDeprecatedRegion(options);
|
|
53213
|
+
const skipPull = options.pull === false;
|
|
53005
53214
|
if (!targets || targets.length === 0) throw new LocalStartServiceError("cdkd local start-service requires at least one <target>. Pass one or more service paths like 'Stack/Orders' 'Stack/Frontend'.");
|
|
53006
53215
|
const perTarget = targets.map((t) => ({
|
|
53007
53216
|
target: t,
|
|
@@ -53058,7 +53267,7 @@ async function localStartServiceCommand(targets, options) {
|
|
|
53058
53267
|
try {
|
|
53059
53268
|
sharedNetwork = await createSharedSvcNetwork({
|
|
53060
53269
|
prefix: options.cluster,
|
|
53061
|
-
skipPull
|
|
53270
|
+
skipPull,
|
|
53062
53271
|
cluster: options.cluster
|
|
53063
53272
|
});
|
|
53064
53273
|
} catch (err) {
|
|
@@ -53080,7 +53289,7 @@ async function localStartServiceCommand(targets, options) {
|
|
|
53080
53289
|
};
|
|
53081
53290
|
process.on("SIGINT", sigintHandler);
|
|
53082
53291
|
process.on("SIGTERM", sigintHandler);
|
|
53083
|
-
for (const pt of perTarget) pt.controller = await bootOneTarget(pt.target, pt.runState, stacks, options, discovery);
|
|
53292
|
+
for (const pt of perTarget) pt.controller = await bootOneTarget(pt.target, pt.runState, stacks, options, discovery, skipPull);
|
|
53084
53293
|
const summary = perTarget.map((pt) => `${pt.controller.service.serviceName} (${pt.controller.activeReplicaCount()} replica(s))`).join(", ");
|
|
53085
53294
|
logger.info(`Service(s) running: ${summary}. Press ^C to shut down.`);
|
|
53086
53295
|
await Promise.all(perTarget.map((pt) => pt.controller.waitForShutdown()));
|
|
@@ -53098,7 +53307,7 @@ async function localStartServiceCommand(targets, options) {
|
|
|
53098
53307
|
* options) is scoped locally. Returns the started controller for the
|
|
53099
53308
|
* outer code to wait + tear down.
|
|
53100
53309
|
*/
|
|
53101
|
-
async function bootOneTarget(target, runState, stacks, options, discovery) {
|
|
53310
|
+
async function bootOneTarget(target, runState, stacks, options, discovery, skipPull) {
|
|
53102
53311
|
const logger = getLogger();
|
|
53103
53312
|
const imageContext = await buildEcsImageResolutionContext(target, stacks, options);
|
|
53104
53313
|
const service = resolveEcsServiceTarget(target, stacks, imageContext);
|
|
@@ -53141,7 +53350,7 @@ async function bootOneTarget(target, runState, stacks, options, discovery) {
|
|
|
53141
53350
|
const taskOpts = {
|
|
53142
53351
|
cluster: options.cluster,
|
|
53143
53352
|
containerHost: options.containerHost,
|
|
53144
|
-
skipPull
|
|
53353
|
+
skipPull,
|
|
53145
53354
|
keepRunning: false,
|
|
53146
53355
|
detach: true
|
|
53147
53356
|
};
|
|
@@ -53277,22 +53486,26 @@ function parsePositiveInt(raw, flagName) {
|
|
|
53277
53486
|
}
|
|
53278
53487
|
/**
|
|
53279
53488
|
* Hard cap on `--max-tasks` driven by the per-replica subnet allocator
|
|
53280
|
-
* in `ecs-service-runner.ts:
|
|
53281
|
-
*
|
|
53282
|
-
*
|
|
53283
|
-
*
|
|
53284
|
-
*
|
|
53285
|
-
*
|
|
53489
|
+
* in `ecs-service-runner.ts:pickSubnetOctet`. The allocator walks the
|
|
53490
|
+
* link-local /24 range `169.254.170.0..169.254.253.0` and **skips 171**
|
|
53491
|
+
* because that octet is owned by the shared-service network
|
|
53492
|
+
* (`SHARED_SVC_SUBNET_OCTET`) — assigning a per-replica network the
|
|
53493
|
+
* same /24 would have docker reject the duplicate-subnet `network
|
|
53494
|
+
* create`. The usable count is therefore 83 (Issue #544); beyond
|
|
53495
|
+
* that, the modulo-wrap collapses replica N's `/24` onto replica 0's
|
|
53496
|
+
* allocation and docker rejects the duplicate-subnet network creation
|
|
53497
|
+
* with a cryptic "Pool overlaps with other one on this address space"
|
|
53498
|
+
* error 30s into the boot — by which time some early replicas may
|
|
53499
|
+
* have spent docker-run budget. Reject at parse time so the user
|
|
53286
53500
|
* gets an actionable error before any boot work fires.
|
|
53287
53501
|
*
|
|
53288
|
-
*
|
|
53289
|
-
*
|
|
53290
|
-
* this requires extending the allocator to walk a different IP range.
|
|
53502
|
+
* Raising this requires extending the allocator to walk a different
|
|
53503
|
+
* IP range.
|
|
53291
53504
|
*/
|
|
53292
|
-
const MAX_TASKS_SUBNET_RANGE_CAP =
|
|
53505
|
+
const MAX_TASKS_SUBNET_RANGE_CAP = 83;
|
|
53293
53506
|
function parseMaxTasks(raw) {
|
|
53294
53507
|
const parsed = parsePositiveInt(raw, "--max-tasks");
|
|
53295
|
-
if (parsed >
|
|
53508
|
+
if (parsed > 83) throw new LocalStartServiceError(`--max-tasks ${parsed} exceeds the per-replica link-local /24 subnet allocator's range (${83}). The allocator in ecs-service-runner.ts assigns each replica its own 169.254.x.0/24 from the range 169.254.170.0..169.254.253.0; replica indices >= ${83} would collide with earlier replicas via modulo wrap. Lower --max-tasks to <= ${83}, or accept reduced local concurrency for high-DesiredCount services.`);
|
|
53296
53509
|
return parsed;
|
|
53297
53510
|
}
|
|
53298
53511
|
function parseRestartPolicy(raw) {
|
|
@@ -53300,7 +53513,7 @@ function parseRestartPolicy(raw) {
|
|
|
53300
53513
|
throw new LocalStartServiceError(`--restart-policy must be one of 'on-failure', 'always', or 'none' (got '${raw}').`);
|
|
53301
53514
|
}
|
|
53302
53515
|
function createLocalStartServiceCommand() {
|
|
53303
|
-
const cmd = new Command("start-service").description("Run one or more AWS::ECS::Service resources locally as a long-running emulator. Spins up DesiredCount task replicas per service (clamped by --max-tasks) using the same per-task docker network + metadata sidecar pattern as `cdkd local run-task`, then keeps each replica running and restarts it on exit per --restart-policy. ^C tears every replica + sidecar + network down. Each <target> accepts a CDK display path (MyStack/MyService) or stack-qualified logical ID (MyStack:MyServiceXYZ); single-stack apps may omit the stack prefix. When two or more <target>s are supplied, every service is booted into a shared Cloud Map / Service Connect registry so peer services discover each other via docker --add-host overlay (Issue #460).").argument("<targets...>", "One or more CDK display paths or stack-qualified logical IDs of the AWS::ECS::Service resources to run").addOption(new Option("--cluster <name>", "Cluster name surfaced to ECS_CONTAINER_METADATA_URI_V4 and used as the docker network prefix").default("cdkd-local")).addOption(new Option("--env-vars <file>", "JSON env-var overrides (SAM-compatible: {\"ContainerName\":{\"KEY\":\"VALUE\"}, \"Parameters\":{}})")).addOption(new Option("--container-host <ip>", "Host IP to bind published container ports to. Must be a numeric IP (Docker rejects hostnames here)").default("127.0.0.1")).addOption(new Option("--assume-task-role [arn]", "Assume the task definition's TaskRoleArn (or the supplied ARN) and forward STS-issued temp credentials via the metadata sidecar so containers run with the deployed task role. Bare flag uses the template's TaskRoleArn; pass an explicit ARN to override.")).addOption(new Option("--no-pull", "Skip docker pull for every container image and the metadata sidecar")).addOption(new Option("--ecr-role-arn <arn>", "Role ARN to assume before authenticating against ECR for cross-account / centralized registries.")).addOption(new Option("--platform <platform>", "Force docker --platform (linux/amd64 or linux/arm64). Default: inferred from task RuntimePlatform.CpuArchitecture")).addOption(new Option("--max-tasks <n>", `Hard cap on local replica count. Caps the template DesiredCount so local dev machines don't run an unbounded number of containers. Cannot exceed ${
|
|
53516
|
+
const cmd = new Command("start-service").description("Run one or more AWS::ECS::Service resources locally as a long-running emulator. Spins up DesiredCount task replicas per service (clamped by --max-tasks) using the same per-task docker network + metadata sidecar pattern as `cdkd local run-task`, then keeps each replica running and restarts it on exit per --restart-policy. ^C tears every replica + sidecar + network down. Each <target> accepts a CDK display path (MyStack/MyService) or stack-qualified logical ID (MyStack:MyServiceXYZ); single-stack apps may omit the stack prefix. When two or more <target>s are supplied, every service is booted into a shared Cloud Map / Service Connect registry so peer services discover each other via docker --add-host overlay (Issue #460).").argument("<targets...>", "One or more CDK display paths or stack-qualified logical IDs of the AWS::ECS::Service resources to run").addOption(new Option("--cluster <name>", "Cluster name surfaced to ECS_CONTAINER_METADATA_URI_V4 and used as the docker network prefix").default("cdkd-local")).addOption(new Option("--env-vars <file>", "JSON env-var overrides (SAM-compatible: {\"ContainerName\":{\"KEY\":\"VALUE\"}, \"Parameters\":{}})")).addOption(new Option("--container-host <ip>", "Host IP to bind published container ports to. Must be a numeric IP (Docker rejects hostnames here)").default("127.0.0.1")).addOption(new Option("--assume-task-role [arn]", "Assume the task definition's TaskRoleArn (or the supplied ARN) and forward STS-issued temp credentials via the metadata sidecar so containers run with the deployed task role. Bare flag uses the template's TaskRoleArn; pass an explicit ARN to override.")).addOption(new Option("--no-pull", "Skip docker pull for every container image and the metadata sidecar")).addOption(new Option("--ecr-role-arn <arn>", "Role ARN to assume before authenticating against ECR for cross-account / centralized registries.")).addOption(new Option("--platform <platform>", "Force docker --platform (linux/amd64 or linux/arm64). Default: inferred from task RuntimePlatform.CpuArchitecture")).addOption(new Option("--max-tasks <n>", `Hard cap on local replica count. Caps the template DesiredCount so local dev machines don't run an unbounded number of containers. Cannot exceed ${83} due to the per-replica link-local /24 subnet allocator's range.`).default(3).argParser(parseMaxTasks)).addOption(new Option("--restart-policy <policy>", "How to react when an essential container exits. 'on-failure' (default) restarts only on non-zero exit; 'always' restarts on every exit; 'none' shuts the replica down and runs the service degraded.").default("on-failure").argParser(parseRestartPolicy)).addOption(new Option("--from-state", "Read cdkd S3 state for the target stack and substitute Fn::Sub / Fn::GetAtt / Fn::ImportValue / Fn::GetStackOutput intrinsics in container images, environment variables, secrets, role ARNs, and volumes.").default(false)).addOption(new Option("--stack-region <region>", "Region of the cdkd state record to read (used with --from-state when the same stack name has state in multiple regions).")).action(withErrorHandling(localStartServiceCommand));
|
|
53304
53517
|
[
|
|
53305
53518
|
...commonOptions,
|
|
53306
53519
|
...appOptions,
|
|
@@ -56614,7 +56827,7 @@ function reorderArgs(argv) {
|
|
|
56614
56827
|
*/
|
|
56615
56828
|
async function main() {
|
|
56616
56829
|
const program = new Command();
|
|
56617
|
-
program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.
|
|
56830
|
+
program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.148.0");
|
|
56618
56831
|
program.addCommand(createBootstrapCommand());
|
|
56619
56832
|
program.addCommand(createSynthCommand());
|
|
56620
56833
|
program.addCommand(createListCommand());
|