@intentius/chant 0.44.14 → 0.46.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/dist/audit/discover.d.ts +26 -2
- package/dist/audit/discover.d.ts.map +1 -1
- package/dist/audit/report-model.d.ts +10 -0
- package/dist/audit/report-model.d.ts.map +1 -1
- package/dist/audit/rules-doc.d.ts.map +1 -1
- package/dist/cli/build-params-cli.d.ts +16 -7
- package/dist/cli/build-params-cli.d.ts.map +1 -1
- package/dist/cli/commands/audit.d.ts +20 -0
- package/dist/cli/commands/audit.d.ts.map +1 -1
- package/dist/cli/commands/build.d.ts +9 -0
- package/dist/cli/commands/build.d.ts.map +1 -1
- package/dist/cli/commands/check-lexicon-docs.d.ts +13 -0
- package/dist/cli/commands/check-lexicon-docs.d.ts.map +1 -1
- package/dist/cli/commands/check-lexicon-examples.d.ts +24 -14
- package/dist/cli/commands/check-lexicon-examples.d.ts.map +1 -1
- package/dist/cli/commands/check-lexicon.d.ts +14 -0
- package/dist/cli/commands/check-lexicon.d.ts.map +1 -1
- package/dist/cli/commands/init-lexicon/templates/codegen.d.ts.map +1 -1
- package/dist/cli/commands/init-lexicon/templates/docs.d.ts +5 -0
- package/dist/cli/commands/init-lexicon/templates/docs.d.ts.map +1 -1
- package/dist/cli/commands/init-lexicon.d.ts.map +1 -1
- package/dist/cli/commands/lexicon-surface-diff.d.ts +6 -0
- package/dist/cli/commands/lexicon-surface-diff.d.ts.map +1 -1
- package/dist/cli/commands/lint.d.ts.map +1 -1
- package/dist/cli/commands/onboard.d.ts +28 -0
- package/dist/cli/commands/onboard.d.ts.map +1 -1
- package/dist/cli/handlers/build.d.ts.map +1 -1
- package/dist/cli/handlers/graph.d.ts.map +1 -1
- package/dist/cli/handlers/lifecycle.d.ts +13 -0
- package/dist/cli/handlers/lifecycle.d.ts.map +1 -1
- package/dist/cli/handlers/run.d.ts.map +1 -1
- package/dist/cli/handlers/search.d.ts +27 -0
- package/dist/cli/handlers/search.d.ts.map +1 -1
- package/dist/cli/main.d.ts.map +1 -1
- package/dist/cli/registry.d.ts +7 -0
- package/dist/cli/registry.d.ts.map +1 -1
- package/dist/codegen/docs-pages.d.ts +39 -0
- package/dist/codegen/docs-pages.d.ts.map +1 -0
- package/dist/codegen/docs-sections.d.ts.map +1 -1
- package/dist/codegen/docs-sidebar.d.ts +13 -2
- package/dist/codegen/docs-sidebar.d.ts.map +1 -1
- package/dist/codegen/docs-types.d.ts +30 -10
- package/dist/codegen/docs-types.d.ts.map +1 -1
- package/dist/codegen/docs.d.ts +14 -2
- package/dist/codegen/docs.d.ts.map +1 -1
- package/dist/codegen/lexicon-regen.d.ts +11 -0
- package/dist/codegen/lexicon-regen.d.ts.map +1 -1
- package/dist/codegen/surface-snapshot.d.ts +27 -3
- package/dist/codegen/surface-snapshot.d.ts.map +1 -1
- package/dist/codegen/validate.d.ts +21 -3
- package/dist/codegen/validate.d.ts.map +1 -1
- package/dist/components/capability-plugin.d.ts +13 -2
- package/dist/components/capability-plugin.d.ts.map +1 -1
- package/dist/components/driver.d.ts +38 -0
- package/dist/components/driver.d.ts.map +1 -1
- package/dist/components/index.d.ts +1 -1
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components/starter-plugin.d.ts.map +1 -1
- package/dist/config.d.ts +85 -5
- package/dist/config.d.ts.map +1 -1
- package/dist/discovery/fold-import.d.ts.map +1 -1
- package/dist/env.d.ts +12 -1
- package/dist/env.d.ts.map +1 -1
- package/dist/fold/fold.d.ts +100 -1
- package/dist/fold/fold.d.ts.map +1 -1
- package/dist/graph-ir.d.ts +12 -0
- package/dist/graph-ir.d.ts.map +1 -1
- package/dist/graph-ops.d.ts +17 -0
- package/dist/graph-ops.d.ts.map +1 -0
- package/dist/lexicon.d.ts +182 -2
- package/dist/lexicon.d.ts.map +1 -1
- package/dist/lifecycle/change-set.d.ts +23 -2
- package/dist/lifecycle/change-set.d.ts.map +1 -1
- package/dist/lifecycle/deep-observe.d.ts.map +1 -1
- package/dist/lifecycle/index.d.ts +1 -0
- package/dist/lifecycle/index.d.ts.map +1 -1
- package/dist/lifecycle/observe.d.ts +8 -0
- package/dist/lifecycle/observe.d.ts.map +1 -1
- package/dist/lifecycle/replay.d.ts.map +1 -1
- package/dist/lifecycle/snapshot.d.ts.map +1 -1
- package/dist/lifecycle/teardown.d.ts +130 -0
- package/dist/lifecycle/teardown.d.ts.map +1 -0
- package/dist/lifecycle/types.d.ts +7 -0
- package/dist/lifecycle/types.d.ts.map +1 -1
- package/dist/lint/engine.d.ts +6 -2
- package/dist/lint/engine.d.ts.map +1 -1
- package/dist/lint/policy.d.ts.map +1 -1
- package/dist/lint/rule.d.ts +31 -0
- package/dist/lint/rule.d.ts.map +1 -1
- package/dist/lint/rules/cor021-env-literal-name.d.ts +3 -0
- package/dist/lint/rules/cor021-env-literal-name.d.ts.map +1 -0
- package/dist/lint/rules/index.d.ts +2 -1
- package/dist/lint/rules/index.d.ts.map +1 -1
- package/dist/managed-fields.d.ts +33 -21
- package/dist/managed-fields.d.ts.map +1 -1
- package/dist/observation.d.ts +23 -1
- package/dist/observation.d.ts.map +1 -1
- package/dist/op/builders.d.ts +36 -7
- package/dist/op/builders.d.ts.map +1 -1
- package/dist/op/index.d.ts +1 -1
- package/dist/op/index.d.ts.map +1 -1
- package/dist/testing.d.ts +136 -0
- package/dist/testing.d.ts.map +1 -0
- package/dist/yaml.d.ts +0 -8
- package/dist/yaml.d.ts.map +1 -1
- package/package.json +6 -1
- package/src/__snapshots__/okf.test.ts.snap +20 -0
- package/src/audit/discover.test.ts +55 -1
- package/src/audit/discover.ts +70 -2
- package/src/audit/edge-init-safety.test.ts +149 -18
- package/src/audit/report-model.ts +12 -1
- package/src/audit/rules-doc.ts +1 -0
- package/src/build-params.test.ts +52 -0
- package/src/cli/build-params-cli.test.ts +25 -8
- package/src/cli/build-params-cli.ts +37 -23
- package/src/cli/commands/__fixtures__/audit-coverage/.github/workflows/ci.yml +5 -0
- package/src/cli/commands/__fixtures__/audit-coverage/Dockerfile +2 -0
- package/src/cli/commands/__fixtures__/audit-coverage/infra/main.tf +3 -0
- package/src/cli/commands/__fixtures__/audit-coverage/infra/stack.json +6 -0
- package/src/cli/commands/__fixtures__/audit-coverage/k8s/deploy.yaml +12 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/docs/pages/getting-started.mdx +16 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/docs/src/content.config.ts +10 -2
- package/src/cli/commands/__fixtures__/init-lexicon-output/src/codegen/generate-cli.ts +3 -1
- package/src/cli/commands/__fixtures__/init-lexicon-output/src/codegen/generate.ts +2 -1
- package/src/cli/commands/__snapshots__/init-lexicon.test.ts.snap +2 -1
- package/src/cli/commands/audit.test.ts +83 -1
- package/src/cli/commands/audit.ts +121 -28
- package/src/cli/commands/build.test.ts +275 -5
- package/src/cli/commands/build.ts +86 -14
- package/src/cli/commands/check-lexicon-docs.ts +30 -0
- package/src/cli/commands/check-lexicon-examples.test.ts +73 -7
- package/src/cli/commands/check-lexicon-examples.ts +66 -17
- package/src/cli/commands/check-lexicon.test.ts +46 -2
- package/src/cli/commands/check-lexicon.ts +64 -4
- package/src/cli/commands/init-lexicon/templates/codegen.ts +5 -2
- package/src/cli/commands/init-lexicon/templates/docs.ts +35 -2
- package/src/cli/commands/init-lexicon.test.ts +42 -1
- package/src/cli/commands/init-lexicon.ts +3 -1
- package/src/cli/commands/lexicon-rolling-upgrade.ts +1 -1
- package/src/cli/commands/lexicon-surface-diff.test.ts +1 -1
- package/src/cli/commands/lexicon-surface-diff.ts +10 -1
- package/src/cli/commands/lexicon-surface-diff.update.test.ts +112 -0
- package/src/cli/commands/lint.ts +19 -6
- package/src/cli/commands/onboard.test.ts +319 -225
- package/src/cli/commands/onboard.ts +144 -58
- package/src/cli/handlers/build.test.ts +3 -3
- package/src/cli/handlers/build.ts +2 -0
- package/src/cli/handlers/components.ts +2 -2
- package/src/cli/handlers/graph.test.ts +44 -0
- package/src/cli/handlers/graph.ts +24 -4
- package/src/cli/handlers/lifecycle.test.ts +348 -1
- package/src/cli/handlers/lifecycle.ts +235 -3
- package/src/cli/handlers/run.test.ts +5 -5
- package/src/cli/handlers/run.ts +1 -0
- package/src/cli/handlers/search-live.test.ts +217 -0
- package/src/cli/handlers/search.test.ts +41 -1
- package/src/cli/handlers/search.ts +60 -8
- package/src/cli/main.ts +17 -4
- package/src/cli/registry.ts +7 -0
- package/src/cli/security-boundary.test.ts +2 -1
- package/src/codegen/docs-pages.test.ts +194 -0
- package/src/codegen/docs-pages.ts +138 -0
- package/src/codegen/docs-sections.ts +0 -8
- package/src/codegen/docs-sidebar.ts +50 -37
- package/src/codegen/docs-types.ts +32 -4
- package/src/codegen/docs.ts +78 -40
- package/src/codegen/lexicon-regen.ts +22 -2
- package/src/codegen/rolling-upgrade.test.ts +1 -1
- package/src/codegen/rolling-upgrade.ts +1 -1
- package/src/codegen/surface-snapshot.test.ts +139 -0
- package/src/codegen/surface-snapshot.ts +297 -122
- package/src/codegen/validate.test.ts +49 -3
- package/src/codegen/validate.ts +36 -7
- package/src/components/auto-release.ts +1 -1
- package/src/components/capability-plugin.ts +23 -2
- package/src/components/driver.test.ts +45 -0
- package/src/components/driver.ts +64 -27
- package/src/components/index.ts +2 -0
- package/src/components/starter-plugin.ts +5 -2
- package/src/config.test.ts +40 -0
- package/src/config.ts +159 -8
- package/src/discovery/fold-import.test.ts +427 -2
- package/src/discovery/fold-import.ts +163 -7
- package/src/discovery/sandbox/driver.test.ts +11 -0
- package/src/env.test.ts +35 -1
- package/src/env.ts +17 -3
- package/src/fold/fold.test.ts +41 -0
- package/src/fold/fold.ts +372 -2
- package/src/graph-ir-live.test.ts +42 -0
- package/src/graph-ir.ts +42 -2
- package/src/graph-ops.test.ts +69 -0
- package/src/graph-ops.ts +42 -0
- package/src/lexicon.ts +182 -2
- package/src/lifecycle/change-set.test.ts +41 -0
- package/src/lifecycle/change-set.ts +30 -2
- package/src/lifecycle/deep-observe.ts +4 -0
- package/src/lifecycle/index.ts +1 -0
- package/src/lifecycle/observe.ts +29 -2
- package/src/lifecycle/replay.ts +3 -0
- package/src/lifecycle/snapshot.ts +3 -0
- package/src/lifecycle/teardown.test.ts +537 -0
- package/src/lifecycle/teardown.ts +357 -0
- package/src/lifecycle/types.ts +7 -0
- package/src/lint/engine.ts +7 -1
- package/src/lint/policy.ts +14 -3
- package/src/lint/rule.ts +23 -0
- package/src/lint/rules/cor021-env-literal-name.test.ts +128 -0
- package/src/lint/rules/cor021-env-literal-name.ts +114 -0
- package/src/lint/rules/index.ts +4 -1
- package/src/managed-fields.test.ts +15 -42
- package/src/managed-fields.ts +43 -26
- package/src/observation.test.ts +24 -8
- package/src/observation.ts +48 -4
- package/src/op/builders.ts +40 -7
- package/src/op/index.ts +1 -1
- package/src/ownership.test.ts +63 -1
- package/src/testing.test.ts +261 -0
- package/src/testing.ts +338 -0
- package/src/yaml.test.ts +15 -0
- package/src/yaml.ts +19 -2
|
@@ -30,7 +30,9 @@ import { discoverComponents } from "../../components/discover";
|
|
|
30
30
|
import { cfnDeployStacks } from "./components";
|
|
31
31
|
import { affectedStacks } from "../../lifecycle/affected";
|
|
32
32
|
import { rollbackToRevision } from "../../lifecycle/rollback";
|
|
33
|
-
import { loadChantConfig, environmentNames } from "../../config";
|
|
33
|
+
import { loadChantConfig, environmentNames, matchesDeclaredEnvironment, resolveOwnershipStack } from "../../config";
|
|
34
|
+
import { unknownEnvError, isProdLikeEnvironment } from "../../env";
|
|
35
|
+
import { planTeardown, executeTeardown, type TeardownPlan, type TeardownReport } from "../../lifecycle/teardown";
|
|
34
36
|
import { collectBuildRootContributors } from "../plugins";
|
|
35
37
|
import { applyLiveEndpoint } from "../../live-endpoint";
|
|
36
38
|
import { isResourceDeclarable } from "../../declarable";
|
|
@@ -109,7 +111,7 @@ export async function runLifecycleSnapshot(ctx: CommandContext): Promise<number>
|
|
|
109
111
|
const declaredParams = await commandBuildParams(config.buildParams, args);
|
|
110
112
|
if (!declaredParams) return 1;
|
|
111
113
|
const declaredEnvNames = environmentNames(config.environments);
|
|
112
|
-
if (declaredEnvNames && !
|
|
114
|
+
if (declaredEnvNames && !matchesDeclaredEnvironment(config.environments, environment)) {
|
|
113
115
|
console.error(formatError({
|
|
114
116
|
message: `Unknown environment "${environment}"`,
|
|
115
117
|
hint: `Defined environments: ${declaredEnvNames.join(", ")}`,
|
|
@@ -416,6 +418,7 @@ export async function runLifecycleDiff(ctx: CommandContext): Promise<number> {
|
|
|
416
418
|
buildResult,
|
|
417
419
|
json,
|
|
418
420
|
stack: target.stack,
|
|
421
|
+
region: target.region,
|
|
419
422
|
componentStacks,
|
|
420
423
|
baseline,
|
|
421
424
|
updateBaseline: args.updateBaseline,
|
|
@@ -650,6 +653,11 @@ interface LiveDiffArgs {
|
|
|
650
653
|
/** Deployed stack name for a multi-stack project (#932); scopes the live
|
|
651
654
|
* observation (which CloudFormation stack to query) and the snapshot read. */
|
|
652
655
|
stack?: string;
|
|
656
|
+
/** Region that stack is deployed in, from `stacks[].region` (#1264). Same
|
|
657
|
+
* contract as snapshot (#1261): without it every stack is read against the
|
|
658
|
+
* ambient region, so a multi-region estate reports its out-of-region stacks
|
|
659
|
+
* as absent rather than unobserved. */
|
|
660
|
+
region?: string;
|
|
653
661
|
/** Component projects deploy one CFN stack per component; observe them all and
|
|
654
662
|
* union (the same fix graph/plan use), else every deployed resource reads as
|
|
655
663
|
* "missing". Empty → the single-stack observe path. */
|
|
@@ -710,6 +718,9 @@ async function observeLexicon(
|
|
|
710
718
|
declared: Set<string>;
|
|
711
719
|
entities: Map<string, { entityType: string; props: Record<string, unknown> }>;
|
|
712
720
|
stack?: string;
|
|
721
|
+
/** Region the stack is deployed in (#1264); the read targets it instead of
|
|
722
|
+
* the ambient region. */
|
|
723
|
+
region?: string;
|
|
713
724
|
componentStacks?: string[];
|
|
714
725
|
owned?: boolean;
|
|
715
726
|
/** `--namespace <ns>` (#1629): where to read an entity that declares no
|
|
@@ -723,6 +734,7 @@ async function observeLexicon(
|
|
|
723
734
|
buildOutput: opts.buildOutput,
|
|
724
735
|
entityNames,
|
|
725
736
|
entities: opts.entities,
|
|
737
|
+
...(opts.region ? { region: opts.region } : {}),
|
|
726
738
|
...(opts.owned !== undefined ? { owned: opts.owned } : {}),
|
|
727
739
|
...(opts.namespace ? { namespace: opts.namespace } : {}),
|
|
728
740
|
};
|
|
@@ -746,6 +758,7 @@ async function observeLexicon(
|
|
|
746
758
|
resources: {},
|
|
747
759
|
unobserved: unobservedAll(entityNames, "read-failed", message, opts.entities),
|
|
748
760
|
queried: {},
|
|
761
|
+
notes: [],
|
|
749
762
|
};
|
|
750
763
|
}
|
|
751
764
|
}
|
|
@@ -811,6 +824,7 @@ async function runLifecycleDiffLive(args: LiveDiffArgs): Promise<LiveDiffOutcome
|
|
|
811
824
|
declared,
|
|
812
825
|
entities,
|
|
813
826
|
stack: args.stack,
|
|
827
|
+
region: args.region,
|
|
814
828
|
componentStacks: args.componentStacks,
|
|
815
829
|
...(args.namespace ? { namespace: args.namespace } : {}),
|
|
816
830
|
});
|
|
@@ -839,6 +853,7 @@ async function runLifecycleDiffLive(args: LiveDiffArgs): Promise<LiveDiffOutcome
|
|
|
839
853
|
buildOutput,
|
|
840
854
|
entities,
|
|
841
855
|
stack: args.stack,
|
|
856
|
+
region: args.region,
|
|
842
857
|
componentStacks: args.componentStacks,
|
|
843
858
|
baseline: baselineForLexicon(args.baseline, lexiconName),
|
|
844
859
|
});
|
|
@@ -1181,6 +1196,9 @@ export async function runLifecyclePlan(ctx: CommandContext): Promise<number> {
|
|
|
1181
1196
|
// The resolved read address rides every entry, not just unobserved
|
|
1182
1197
|
// ones — the diff path already passes it (#1620); plan lost it.
|
|
1183
1198
|
queried: observed.queried,
|
|
1199
|
+
}, {
|
|
1200
|
+
// Attribution survives the flat merge below (#1674).
|
|
1201
|
+
lexicon: lexiconName,
|
|
1184
1202
|
});
|
|
1185
1203
|
merged.entries.push(...cs.entries);
|
|
1186
1204
|
checked++;
|
|
@@ -1224,6 +1242,220 @@ export async function runLifecyclePlan(ctx: CommandContext): Promise<number> {
|
|
|
1224
1242
|
return 0;
|
|
1225
1243
|
}
|
|
1226
1244
|
|
|
1245
|
+
/**
|
|
1246
|
+
* chant lifecycle teardown <environment> (#1222).
|
|
1247
|
+
*
|
|
1248
|
+
* Enumerates what deleting the environment would remove: every live resource
|
|
1249
|
+
* carrying this project's ownership marker (managed-by + `ownership.stack`)
|
|
1250
|
+
* with the requested env identity. Stateless — live markers only, no build, no
|
|
1251
|
+
* snapshot. Without `--yes` nothing is deleted; with it the planned set is
|
|
1252
|
+
* executed per lexicon with one bounded retry pass over failures, and every
|
|
1253
|
+
* candidate's outcome is reported (deleted / failed / not-prunable / skipped —
|
|
1254
|
+
* never silence). A production-like environment name additionally requires an
|
|
1255
|
+
* interactive confirmation, or `--confirm-prod` non-interactively.
|
|
1256
|
+
*/
|
|
1257
|
+
export async function runLifecycleTeardown(ctx: CommandContext): Promise<number> {
|
|
1258
|
+
const { args, plugins } = ctx;
|
|
1259
|
+
const environment = args.extraPositional;
|
|
1260
|
+
|
|
1261
|
+
if (!environment) {
|
|
1262
|
+
console.error(formatError({ message: "Environment is required: chant lifecycle teardown <environment>" }));
|
|
1263
|
+
return 1;
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1266
|
+
const { config } = await loadChantConfig(resolve("."));
|
|
1267
|
+
|
|
1268
|
+
// Refuse an env the project does not declare — a typo here is the difference
|
|
1269
|
+
// between tearing down `dev` and tearing down `prod`. Literal `environments`
|
|
1270
|
+
// entries only (#1221's pattern entries would extend this same check).
|
|
1271
|
+
const envErr = unknownEnvError(environment, config.environments);
|
|
1272
|
+
if (envErr) {
|
|
1273
|
+
console.error(formatError({ message: envErr }));
|
|
1274
|
+
return 1;
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1277
|
+
// Teardown selects on the ownership marker; a project that stamps none has
|
|
1278
|
+
// nothing to key on, and "delete what looks like mine" is not a fallback.
|
|
1279
|
+
const stack = resolveOwnershipStack(config);
|
|
1280
|
+
if (stack === undefined) {
|
|
1281
|
+
console.error(formatError({
|
|
1282
|
+
message: "This project declares no ownership.stack — teardown is marker-scoped and has nothing to select on",
|
|
1283
|
+
hint: 'Set `ownership: { stack: "<name>" }` in chant.config.ts and deploy, so resources carry the marker teardown keys on.',
|
|
1284
|
+
}));
|
|
1285
|
+
return 1;
|
|
1286
|
+
}
|
|
1287
|
+
|
|
1288
|
+
// The prod guard (#1222): a production-like name never falls to `--yes`
|
|
1289
|
+
// alone. Interactive runs re-type the environment name; non-interactive
|
|
1290
|
+
// runs say `--confirm-prod` explicitly. Checked before any live read so a
|
|
1291
|
+
// refused teardown touches nothing at all.
|
|
1292
|
+
if (args.yes && isProdLikeEnvironment(environment) && !args.confirmProd) {
|
|
1293
|
+
if (!process.stdin.isTTY) {
|
|
1294
|
+
console.error(formatError({
|
|
1295
|
+
message: `"${environment}" looks like a production environment — --yes alone is not enough`,
|
|
1296
|
+
hint: "Re-run with --yes --confirm-prod to tear it down non-interactively.",
|
|
1297
|
+
}));
|
|
1298
|
+
return 1;
|
|
1299
|
+
}
|
|
1300
|
+
const confirmed = await promptProdTeardown(environment);
|
|
1301
|
+
if (!confirmed) {
|
|
1302
|
+
console.error(formatError({ message: "Confirmation did not match — nothing was deleted." }));
|
|
1303
|
+
return 1;
|
|
1304
|
+
}
|
|
1305
|
+
}
|
|
1306
|
+
|
|
1307
|
+
// #1166 — teardown is always a live read (and with --yes, a live write), so
|
|
1308
|
+
// an environment's declared endpoint applies here too, unless the ambient
|
|
1309
|
+
// shell already set it.
|
|
1310
|
+
const readingPlugins = plugins.filter((p) => p.teardownOwned || p.describeResources || p.executeTeardown);
|
|
1311
|
+
const endpointResult = applyLiveEndpoint(config.environments, environment, readingPlugins);
|
|
1312
|
+
if (endpointResult.notice) console.error(formatWarning({ message: endpointResult.notice }));
|
|
1313
|
+
|
|
1314
|
+
let plan: TeardownPlan;
|
|
1315
|
+
let report: TeardownReport | undefined;
|
|
1316
|
+
try {
|
|
1317
|
+
// A multi-stack project's declared stacks, for stack-shaped teardowns
|
|
1318
|
+
// (aws enumerates and deletes whole CloudFormation stacks). A single-stack
|
|
1319
|
+
// project passes nothing and the env-named default convention applies.
|
|
1320
|
+
const deployedStacks = (config.stacks ?? []).map((s) => ({
|
|
1321
|
+
name: s.name,
|
|
1322
|
+
...(s.region ? { region: s.region } : {}),
|
|
1323
|
+
}));
|
|
1324
|
+
plan = await planTeardown({
|
|
1325
|
+
environment,
|
|
1326
|
+
stack,
|
|
1327
|
+
plugins,
|
|
1328
|
+
...(deployedStacks.length > 0 ? { deployedStacks } : {}),
|
|
1329
|
+
});
|
|
1330
|
+
if (args.yes) {
|
|
1331
|
+
report = await executeTeardown({
|
|
1332
|
+
environment,
|
|
1333
|
+
stack,
|
|
1334
|
+
plugins,
|
|
1335
|
+
plan,
|
|
1336
|
+
...(deployedStacks.length > 0 ? { deployedStacks } : {}),
|
|
1337
|
+
});
|
|
1338
|
+
}
|
|
1339
|
+
} finally {
|
|
1340
|
+
endpointResult.restore();
|
|
1341
|
+
}
|
|
1342
|
+
|
|
1343
|
+
if (args.json) {
|
|
1344
|
+
console.log(JSON.stringify(report ?? plan, null, 2));
|
|
1345
|
+
return report && report.outcomes.some((o) => o.outcome === "failed") ? 1 : 0;
|
|
1346
|
+
}
|
|
1347
|
+
|
|
1348
|
+
console.log(formatBold(
|
|
1349
|
+
args.yes
|
|
1350
|
+
? `Teardown — environment: ${environment}, stack: ${stack}`
|
|
1351
|
+
: `Teardown plan — environment: ${environment}, stack: ${stack} (plan only — nothing is deleted)`,
|
|
1352
|
+
));
|
|
1353
|
+
|
|
1354
|
+
if (plan.entries.length === 0) {
|
|
1355
|
+
console.error(formatWarning({
|
|
1356
|
+
message: `No live resources carry the marker stack "${stack}" + env "${environment}" — nothing would be deleted.` +
|
|
1357
|
+
(plan.holes.length > 0
|
|
1358
|
+
? " But this plan has holes (below) — parts of the estate could not be read, so \"nothing\" is a claim about what was readable, not about the environment."
|
|
1359
|
+
: " If this environment is deployed, check that its resources were stamped (ownership marking on, and the env identity resolved at build time)."),
|
|
1360
|
+
}));
|
|
1361
|
+
} else {
|
|
1362
|
+
console.log(`\n${plan.entries.length} resource(s) would be deleted:`);
|
|
1363
|
+
console.log("RESOURCE".padEnd(28) + "TYPE".padEnd(32) + "MARKER".padEnd(24) + "LEXICON");
|
|
1364
|
+
console.log("-".repeat(96));
|
|
1365
|
+
for (const entry of plan.entries) {
|
|
1366
|
+
console.log(
|
|
1367
|
+
entry.name.padEnd(28) +
|
|
1368
|
+
entry.type.padEnd(32) +
|
|
1369
|
+
`${entry.marker.stack}/${entry.marker.env}`.padEnd(24) +
|
|
1370
|
+
entry.lexicon,
|
|
1371
|
+
);
|
|
1372
|
+
}
|
|
1373
|
+
}
|
|
1374
|
+
|
|
1375
|
+
// Holes are loud (#1089): an unreadable kind is unknown, not absent, and the
|
|
1376
|
+
// execution half must not treat this plan as the whole delete set.
|
|
1377
|
+
if (plan.holes.length > 0) {
|
|
1378
|
+
console.error(formatWarning({
|
|
1379
|
+
message: `${plan.holes.length} hole(s) — resources chant may own but could not read. This plan is incomplete, not clean.`,
|
|
1380
|
+
}));
|
|
1381
|
+
console.log(formatBold("\nHOLES (stamped kinds the read could not cover):"));
|
|
1382
|
+
for (const hole of plan.holes) {
|
|
1383
|
+
console.log(` ? ${hole.lexicon}: ${hole.name}${hole.type ? ` (${hole.type})` : ""} — ${hole.reason}${hole.detail ? `: ${hole.detail}` : ""}`);
|
|
1384
|
+
}
|
|
1385
|
+
}
|
|
1386
|
+
|
|
1387
|
+
if (plan.skipped.length > 0) {
|
|
1388
|
+
console.error(formatWarning({
|
|
1389
|
+
message: `Skipped (no teardown enumeration and no describeResources): ${plan.skipped.join(", ")} — those lexicons' resources are not in this plan.`,
|
|
1390
|
+
}));
|
|
1391
|
+
}
|
|
1392
|
+
|
|
1393
|
+
if (!report) {
|
|
1394
|
+
console.error(formatWarning({ message: "Plan only — re-run with --yes to execute." }));
|
|
1395
|
+
return 0;
|
|
1396
|
+
}
|
|
1397
|
+
|
|
1398
|
+
if (report.outcomes.length > 0) {
|
|
1399
|
+
console.log(formatBold("\nOutcomes:"));
|
|
1400
|
+
console.log("RESOURCE".padEnd(28) + "OUTCOME".padEnd(14) + "LEXICON".padEnd(12) + "DETAIL");
|
|
1401
|
+
console.log("-".repeat(96));
|
|
1402
|
+
for (const o of report.outcomes) {
|
|
1403
|
+
console.log(
|
|
1404
|
+
o.name.padEnd(28) +
|
|
1405
|
+
o.outcome.padEnd(14) +
|
|
1406
|
+
o.lexicon.padEnd(12) +
|
|
1407
|
+
(o.detail ?? "") +
|
|
1408
|
+
(o.retried ? " (after retry)" : ""),
|
|
1409
|
+
);
|
|
1410
|
+
}
|
|
1411
|
+
}
|
|
1412
|
+
|
|
1413
|
+
const counts = { deleted: 0, failed: 0, "not-prunable": 0, skipped: 0 };
|
|
1414
|
+
for (const o of report.outcomes) counts[o.outcome]++;
|
|
1415
|
+
console.log(
|
|
1416
|
+
`\n${counts.deleted} deleted, ${counts.failed} failed, ${counts["not-prunable"]} not prunable, ${counts.skipped} skipped`,
|
|
1417
|
+
);
|
|
1418
|
+
|
|
1419
|
+
if (report.unimplemented.length > 0) {
|
|
1420
|
+
console.error(formatWarning({
|
|
1421
|
+
message: `Not executed (no teardown execution in these lexicons yet): ${report.unimplemented.join(", ")} — their candidates are reported as skipped, not deleted.`,
|
|
1422
|
+
}));
|
|
1423
|
+
}
|
|
1424
|
+
if (plan.holes.length > 0) {
|
|
1425
|
+
console.error(formatWarning({
|
|
1426
|
+
message: "This teardown ran over an incomplete plan (holes above) — the environment cannot be called clean.",
|
|
1427
|
+
}));
|
|
1428
|
+
}
|
|
1429
|
+
if (counts.failed > 0) {
|
|
1430
|
+
console.error(formatError({
|
|
1431
|
+
message: `${counts.failed} candidate(s) failed to delete after the retry pass — see the outcomes above.`,
|
|
1432
|
+
}));
|
|
1433
|
+
return 1;
|
|
1434
|
+
}
|
|
1435
|
+
|
|
1436
|
+
return 0;
|
|
1437
|
+
}
|
|
1438
|
+
|
|
1439
|
+
/**
|
|
1440
|
+
* The interactive half of the prod guard: the operator re-types the
|
|
1441
|
+
* environment name. Anything else — including EOF — refuses.
|
|
1442
|
+
*/
|
|
1443
|
+
async function promptProdTeardown(environment: string): Promise<boolean> {
|
|
1444
|
+
const { createInterface } = await import("node:readline");
|
|
1445
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
1446
|
+
return new Promise((resolvePrompt) => {
|
|
1447
|
+
rl.question(
|
|
1448
|
+
`"${environment}" looks like a production environment. Type the environment name to confirm teardown: `,
|
|
1449
|
+
(answer) => {
|
|
1450
|
+
resolvePrompt(answer.trim() === environment);
|
|
1451
|
+
rl.close();
|
|
1452
|
+
},
|
|
1453
|
+
);
|
|
1454
|
+
// EOF (Ctrl-D) closes the interface without answering — that is a refusal.
|
|
1455
|
+
rl.on("close", () => resolvePrompt(false));
|
|
1456
|
+
});
|
|
1457
|
+
}
|
|
1458
|
+
|
|
1227
1459
|
/**
|
|
1228
1460
|
* chant lifecycle log [environment]
|
|
1229
1461
|
*/
|
|
@@ -1252,7 +1484,7 @@ export async function runLifecycleLog(ctx: CommandContext): Promise<number> {
|
|
|
1252
1484
|
export async function runLifecycleUnknown(ctx: CommandContext): Promise<number> {
|
|
1253
1485
|
console.error(formatError({
|
|
1254
1486
|
message: `Unknown state subcommand: ${ctx.args.extraPositional ?? ctx.args.path}`,
|
|
1255
|
-
hint: "Available: chant lifecycle snapshot, chant lifecycle show, chant lifecycle diff, chant lifecycle plan, chant lifecycle log",
|
|
1487
|
+
hint: "Available: chant lifecycle snapshot, chant lifecycle show, chant lifecycle diff, chant lifecycle plan, chant lifecycle teardown, chant lifecycle log",
|
|
1256
1488
|
}));
|
|
1257
1489
|
return 1;
|
|
1258
1490
|
}
|
|
@@ -101,7 +101,7 @@ function makeOp(name: string, depends: string[] = []): [string, { config: { name
|
|
|
101
101
|
function setupTemporalClient(mock: ReturnType<typeof createMockTemporalClient>) {
|
|
102
102
|
loadTemporalClientMock.mockResolvedValue({
|
|
103
103
|
Connection: { connect: vi.fn(async () => ({})) },
|
|
104
|
-
Client: vi.fn(()
|
|
104
|
+
Client: vi.fn(function () { return mock.client; }) as unknown as new () => unknown,
|
|
105
105
|
});
|
|
106
106
|
loadChantConfigMock.mockResolvedValue({ config: {} });
|
|
107
107
|
resolveProfileMock.mockReturnValue({ address: "localhost:7233", namespace: "default", taskQueue: "q" });
|
|
@@ -951,9 +951,9 @@ describe("runOpComponents", () => {
|
|
|
951
951
|
expect(runComponentsMock).toHaveBeenCalledWith(expect.any(String), "svc", expect.objectContaining({
|
|
952
952
|
buildParams: [{ name: "tier", value: "light", source: "default" }],
|
|
953
953
|
}));
|
|
954
|
-
//
|
|
955
|
-
//
|
|
956
|
-
expect(stderr.join("\n")).toContain("
|
|
954
|
+
// The echo is a one-line count (#1424); the provenance forwarded above
|
|
955
|
+
// is where the value is asserted.
|
|
956
|
+
expect(stderr.join("\n")).toContain("1 build parameter resolved (1 default)");
|
|
957
957
|
});
|
|
958
958
|
|
|
959
959
|
test("--param overrides a declared default and is threaded through to runComponents", async () => {
|
|
@@ -978,7 +978,7 @@ describe("runOpComponents", () => {
|
|
|
978
978
|
expect(runComponentsMock).toHaveBeenCalledWith(expect.any(String), "svc", expect.objectContaining({
|
|
979
979
|
buildParams: [{ name: "tier", value: "production", source: "cli" }],
|
|
980
980
|
}));
|
|
981
|
-
expect(stderr.join("\n")).toContain("
|
|
981
|
+
expect(stderr.join("\n")).toContain("1 build parameter resolved (1 from cli)");
|
|
982
982
|
vi.restoreAllMocks();
|
|
983
983
|
});
|
|
984
984
|
|
package/src/cli/handlers/run.ts
CHANGED
|
@@ -714,6 +714,7 @@ export async function runOpComponents(ctx: CommandContext): Promise<number> {
|
|
|
714
714
|
const paramsResolution = resolveCliBuildParams(config.buildParams, {
|
|
715
715
|
cli: parseParamFlags(ctx.args.param),
|
|
716
716
|
paramsFile: ctx.args.paramsFile,
|
|
717
|
+
verbose: ctx.args.verbose,
|
|
717
718
|
});
|
|
718
719
|
if (!paramsResolution.success) {
|
|
719
720
|
for (const message of paramsResolution.errors) console.error(message);
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import { describe, test, expect, vi, beforeEach } from "vitest";
|
|
2
|
+
import type { ParsedArgs } from "../registry";
|
|
3
|
+
import { DECLARABLE_MARKER, type Declarable } from "../../declarable";
|
|
4
|
+
import { createMockPlugin } from "../../../../test-utils/src/mock-plugin";
|
|
5
|
+
|
|
6
|
+
// #1263 — `chant search --live` against an unreachable endpoint returned the
|
|
7
|
+
// declared rows, exit 0, and a footer naming derived facts it never computed.
|
|
8
|
+
// The observe layer already records a thrown read as NOT-OBSERVED
|
|
9
|
+
// (`read-failed`, #1089); these tests pin that the handler carries it through
|
|
10
|
+
// to the rows, the footer, stderr, and the exit code. `observeResources` is
|
|
11
|
+
// real here so the plumbing from a throwing `describeResources` is exercised.
|
|
12
|
+
|
|
13
|
+
const discoverMock = vi.fn();
|
|
14
|
+
vi.mock("../../discovery/index", () => ({
|
|
15
|
+
discover: (...a: unknown[]) => discoverMock(...a),
|
|
16
|
+
}));
|
|
17
|
+
const loadPluginsMock = vi.fn();
|
|
18
|
+
const resolveLexMock = vi.fn();
|
|
19
|
+
vi.mock("../plugins", async () => {
|
|
20
|
+
const actual = await vi.importActual<typeof import("../plugins")>("../plugins");
|
|
21
|
+
return {
|
|
22
|
+
...actual,
|
|
23
|
+
loadPlugins: (...a: unknown[]) => loadPluginsMock(...a),
|
|
24
|
+
resolveProjectLexicons: (...a: unknown[]) => resolveLexMock(...a),
|
|
25
|
+
};
|
|
26
|
+
});
|
|
27
|
+
const hasSnapshotMock = vi.fn((..._a: unknown[]) => Promise.resolve(false));
|
|
28
|
+
vi.mock("../../lifecycle/replay", () => ({
|
|
29
|
+
replaySnapshots: vi.fn(),
|
|
30
|
+
hasSnapshot: (...a: unknown[]) => hasSnapshotMock(...a),
|
|
31
|
+
}));
|
|
32
|
+
const loadChantConfigMock = vi.fn();
|
|
33
|
+
vi.mock("../../config", async () => {
|
|
34
|
+
const actual = await vi.importActual<typeof import("../../config")>("../../config");
|
|
35
|
+
return { ...actual, loadChantConfig: (...a: unknown[]) => loadChantConfigMock(...a) };
|
|
36
|
+
});
|
|
37
|
+
const buildResultMock = vi.fn();
|
|
38
|
+
vi.mock("../../build", async () => {
|
|
39
|
+
const actual = await vi.importActual<typeof import("../../build")>("../../build");
|
|
40
|
+
return {
|
|
41
|
+
...actual,
|
|
42
|
+
build: (...a: unknown[]) => Promise.resolve(buildResultMock(...a)),
|
|
43
|
+
buildProject: (...a: unknown[]) => Promise.resolve(buildResultMock(...a)),
|
|
44
|
+
};
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
const { runSearch } = await import("./search");
|
|
48
|
+
|
|
49
|
+
function decl<T extends object>(base: T): Declarable & T {
|
|
50
|
+
return { [DECLARABLE_MARKER]: true, ...base } as Declarable & T;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function makeArgs(overrides: Partial<ParsedArgs> = {}): ParsedArgs {
|
|
54
|
+
return {
|
|
55
|
+
command: "search", path: "kind:Instance",
|
|
56
|
+
format: "", fix: false, watch: false, verbose: false, help: false, live: true, env: "dev",
|
|
57
|
+
...overrides,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
describe("search --live when the live read fails (#1263)", () => {
|
|
62
|
+
let out: string[];
|
|
63
|
+
let err: string[];
|
|
64
|
+
const entities = new Map<string, Declarable>([
|
|
65
|
+
["webServer", decl({ lexicon: "aws", entityType: "AWS::EC2::Instance", props: {} })],
|
|
66
|
+
["privateServer", decl({ lexicon: "aws", entityType: "AWS::EC2::Instance", props: {} })],
|
|
67
|
+
]);
|
|
68
|
+
|
|
69
|
+
beforeEach(() => {
|
|
70
|
+
out = [];
|
|
71
|
+
err = [];
|
|
72
|
+
vi.spyOn(console, "log").mockImplementation((s: string) => { out.push(s); });
|
|
73
|
+
vi.spyOn(console, "error").mockImplementation((s: string) => { err.push(s); });
|
|
74
|
+
discoverMock.mockReset();
|
|
75
|
+
discoverMock.mockResolvedValue({ entities, errors: [], sourceFiles: [] });
|
|
76
|
+
buildResultMock.mockReset();
|
|
77
|
+
buildResultMock.mockReturnValue({ errors: [], entities, outputs: new Map([["aws", ""]]), warnings: [] });
|
|
78
|
+
loadChantConfigMock.mockReset();
|
|
79
|
+
loadChantConfigMock.mockResolvedValue({ config: {} });
|
|
80
|
+
resolveLexMock.mockReset();
|
|
81
|
+
resolveLexMock.mockResolvedValue(["aws"]);
|
|
82
|
+
hasSnapshotMock.mockReset();
|
|
83
|
+
hasSnapshotMock.mockResolvedValue(false);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
const deadEndpoint = createMockPlugin({
|
|
87
|
+
name: "aws",
|
|
88
|
+
describeResources: async () => {
|
|
89
|
+
throw new Error("connect ECONNREFUSED 127.0.0.1:9999");
|
|
90
|
+
},
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test("exits non-zero, names the lexicon and cause, and marks every row unobserved", async () => {
|
|
94
|
+
loadPluginsMock.mockResolvedValue([deadEndpoint]);
|
|
95
|
+
const exit = await runSearch({ args: makeArgs(), plugins: [], serializers: [] });
|
|
96
|
+
expect(exit).toBe(1);
|
|
97
|
+
const stderr = err.join("\n");
|
|
98
|
+
expect(stderr).toContain("aws");
|
|
99
|
+
expect(stderr).toContain("ECONNREFUSED 127.0.0.1:9999");
|
|
100
|
+
expect(stderr).toContain("declared-only");
|
|
101
|
+
const stdout = out.join("\n");
|
|
102
|
+
expect(stdout).toContain("webServer AWS::EC2::Instance (unobserved: read-failed)");
|
|
103
|
+
expect(stdout).toContain("privateServer AWS::EC2::Instance (unobserved: read-failed)");
|
|
104
|
+
expect(stdout).toContain("live read failed (aws) · 2/2 rows unobserved");
|
|
105
|
+
// Nothing was read, so no fold over live topology could have run.
|
|
106
|
+
expect(stdout).not.toContain("also derived");
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
test("points at a recorded snapshot when one exists", async () => {
|
|
110
|
+
loadPluginsMock.mockResolvedValue([deadEndpoint]);
|
|
111
|
+
hasSnapshotMock.mockResolvedValue(true);
|
|
112
|
+
const exit = await runSearch({ args: makeArgs(), plugins: [], serializers: [] });
|
|
113
|
+
expect(exit).toBe(1);
|
|
114
|
+
expect(out.join("\n")).toContain("--at latest");
|
|
115
|
+
expect(err.join("\n")).toContain("--at latest");
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
test("a working live read still exits 0 with bound rows", async () => {
|
|
119
|
+
loadPluginsMock.mockResolvedValue([
|
|
120
|
+
createMockPlugin({
|
|
121
|
+
name: "aws",
|
|
122
|
+
describeResources: async () => ({
|
|
123
|
+
webServer: { type: "AWS::EC2::Instance", physicalId: "i-1", status: "OK", owned: true },
|
|
124
|
+
privateServer: { type: "AWS::EC2::Instance", physicalId: "i-2", status: "OK", owned: true },
|
|
125
|
+
}),
|
|
126
|
+
}),
|
|
127
|
+
]);
|
|
128
|
+
const exit = await runSearch({ args: makeArgs(), plugins: [], serializers: [] });
|
|
129
|
+
expect(exit).toBe(0);
|
|
130
|
+
const stdout = out.join("\n");
|
|
131
|
+
expect(stdout).toContain("webServer AWS::EC2::Instance i-1");
|
|
132
|
+
expect(stdout).toContain("observed live · bound 2/2");
|
|
133
|
+
expect(err.join("\n")).not.toContain("live read failed");
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
// #1265 — a lexicon's run-level note ("ownership filter unavailable on this
|
|
138
|
+
// path") printed once per stack, ahead of the rows. It is one fact about the
|
|
139
|
+
// read, so it is said once, and it qualifies the answer, so it follows it.
|
|
140
|
+
describe("search --live run-level notes (#1265)", () => {
|
|
141
|
+
const NOTE =
|
|
142
|
+
"ownership filter unavailable on describeResources (no tags from describe-stack-resources) — returning all, each with an explicit `unknown` verdict; use `chant import --from <env> --owned` for ownership-filtered export";
|
|
143
|
+
let out: string[];
|
|
144
|
+
let err: string[];
|
|
145
|
+
// One log of everything, in the order it was written, so the test can see
|
|
146
|
+
// whether the note came before or after the rows.
|
|
147
|
+
let all: string[];
|
|
148
|
+
const entities = new Map<string, Declarable>([
|
|
149
|
+
["webServer", decl({ lexicon: "aws", entityType: "AWS::EC2::Instance", props: {} })],
|
|
150
|
+
["privateServer", decl({ lexicon: "aws", entityType: "AWS::EC2::Instance", props: {} })],
|
|
151
|
+
]);
|
|
152
|
+
const calls: string[] = [];
|
|
153
|
+
|
|
154
|
+
beforeEach(() => {
|
|
155
|
+
out = [];
|
|
156
|
+
err = [];
|
|
157
|
+
all = [];
|
|
158
|
+
calls.length = 0;
|
|
159
|
+
vi.spyOn(console, "log").mockImplementation((s: string) => { out.push(s); all.push(s); });
|
|
160
|
+
vi.spyOn(console, "error").mockImplementation((s: string) => { err.push(s); all.push(s); });
|
|
161
|
+
vi.spyOn(console, "warn").mockImplementation((s: string) => { all.push(s); });
|
|
162
|
+
discoverMock.mockReset();
|
|
163
|
+
discoverMock.mockResolvedValue({ entities, errors: [], sourceFiles: [] });
|
|
164
|
+
buildResultMock.mockReset();
|
|
165
|
+
buildResultMock.mockReturnValue({ errors: [], entities, outputs: new Map([["aws", ""]]), warnings: [] });
|
|
166
|
+
loadChantConfigMock.mockReset();
|
|
167
|
+
loadChantConfigMock.mockResolvedValue({
|
|
168
|
+
config: { stacks: [{ name: "net" }, { name: "data" }, { name: "app" }, { name: "edge" }] },
|
|
169
|
+
});
|
|
170
|
+
resolveLexMock.mockReset();
|
|
171
|
+
resolveLexMock.mockResolvedValue(["aws"]);
|
|
172
|
+
hasSnapshotMock.mockReset();
|
|
173
|
+
hasSnapshotMock.mockResolvedValue(false);
|
|
174
|
+
loadPluginsMock.mockResolvedValue([
|
|
175
|
+
createMockPlugin({
|
|
176
|
+
name: "aws",
|
|
177
|
+
describeResources: async (opts: { stack?: string }) => {
|
|
178
|
+
calls.push(opts.stack ?? "");
|
|
179
|
+
return {
|
|
180
|
+
observation: "v1" as const,
|
|
181
|
+
resources: {
|
|
182
|
+
webServer: { type: "AWS::EC2::Instance", physicalId: "i-1", status: "OK", ownership: "unknown" as const },
|
|
183
|
+
privateServer: { type: "AWS::EC2::Instance", physicalId: "i-2", status: "OK", ownership: "unknown" as const },
|
|
184
|
+
},
|
|
185
|
+
notes: [NOTE],
|
|
186
|
+
};
|
|
187
|
+
},
|
|
188
|
+
}),
|
|
189
|
+
]);
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
test("four stacks, one note, after the rows and the provenance line", async () => {
|
|
193
|
+
const exit = await runSearch({ args: makeArgs(), plugins: [], serializers: [] });
|
|
194
|
+
expect(exit).toBe(0);
|
|
195
|
+
expect(calls).toEqual(["net", "data", "app", "edge"]);
|
|
196
|
+
const notes = all.filter((line) => line.includes("ownership filter unavailable"));
|
|
197
|
+
expect(notes).toHaveLength(1);
|
|
198
|
+
expect(notes[0]).toContain(`[aws] ${NOTE}`);
|
|
199
|
+
const noteAt = all.findIndex((line) => line.includes("ownership filter unavailable"));
|
|
200
|
+
const lastRow = all.findIndex((line) => line.startsWith("privateServer AWS::EC2::Instance i-2"));
|
|
201
|
+
const footer = all.findIndex((line) => line.includes("observed live · bound 2/2"));
|
|
202
|
+
expect(lastRow).toBeGreaterThanOrEqual(0);
|
|
203
|
+
expect(footer).toBeGreaterThan(lastRow);
|
|
204
|
+
expect(noteAt).toBeGreaterThan(footer);
|
|
205
|
+
// stderr, like every other warning; the rows stay clean on stdout.
|
|
206
|
+
expect(out.join("\n")).not.toContain("ownership filter unavailable");
|
|
207
|
+
expect(err.join("\n")).toContain("ownership filter unavailable");
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
test("a miss still says the note, after the miss", async () => {
|
|
211
|
+
const exit = await runSearch({ args: makeArgs({ path: "kind:Nope" }), plugins: [], serializers: [] });
|
|
212
|
+
expect(exit).toBe(0);
|
|
213
|
+
const notes = all.filter((line) => line.includes("ownership filter unavailable"));
|
|
214
|
+
expect(notes).toHaveLength(1);
|
|
215
|
+
expect(all.indexOf("(no matches)")).toBeLessThan(all.findIndex((l) => l.includes("ownership filter unavailable")));
|
|
216
|
+
});
|
|
217
|
+
});
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { describe, test, expect, vi } from "vitest";
|
|
2
2
|
import { __searchInternals } from "./search";
|
|
3
3
|
|
|
4
|
-
const { parseQuery, matchTerm, formatRow, explain, describeTerm, derivedSurface, availableAttrs, ambientHint, regionSpread, showMiss } = __searchInternals;
|
|
4
|
+
const { parseQuery, matchTerm, formatRow, explain, describeTerm, derivedSurface, availableAttrs, ambientHint, regionSpread, showMiss, provenance } = __searchInternals;
|
|
5
5
|
|
|
6
6
|
function node(id: string, kind: string, attrs: Record<string, unknown> = {}) {
|
|
7
7
|
return { id, kind, lexicon: "aws", attrs } as never;
|
|
@@ -53,6 +53,13 @@ describe("search formatting", () => {
|
|
|
53
53
|
test("compact row with live physical id, skipping object placeholders", () => {
|
|
54
54
|
const live = node("webServer", "AWS::EC2::Instance", { physicalId: "i-abc" });
|
|
55
55
|
expect(formatRow(live, [])).toBe("webServer AWS::EC2::Instance i-abc");
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
// #1263 — a row nobody could read printed blank where the physical id goes,
|
|
59
|
+
// which reads as "declared, not provisioned".
|
|
60
|
+
test("a row the live read could not observe says so in place of the physical id", () => {
|
|
61
|
+
const n = node("webServer", "AWS::EC2::Instance", { _status: "neutral", _unobserved: "read-failed" });
|
|
62
|
+
expect(formatRow(n, [])).toBe("webServer AWS::EC2::Instance (unobserved: read-failed)");
|
|
56
63
|
const src = node("webServer", "AWS::EC2::Instance", { InstanceId: { $ref: "webServer.InstanceId" } });
|
|
57
64
|
expect(formatRow(src, [])).toBe("webServer AWS::EC2::Instance");
|
|
58
65
|
});
|
|
@@ -353,3 +360,36 @@ describe("--show name matching", () => {
|
|
|
353
360
|
expect(capture(() => showMiss([n] as never, ["Region"]))).toBe("");
|
|
354
361
|
});
|
|
355
362
|
});
|
|
363
|
+
|
|
364
|
+
// #1263 — a `--live` read that failed entirely returned the declared rows with
|
|
365
|
+
// the same footer as a working answer and exit 0.
|
|
366
|
+
describe("provenance of a failed live read", () => {
|
|
367
|
+
const capture = (fn: () => void): string => {
|
|
368
|
+
const lines: string[] = [];
|
|
369
|
+
const spy = vi.spyOn(console, "log").mockImplementation((s: string) => { lines.push(s); });
|
|
370
|
+
fn();
|
|
371
|
+
spy.mockRestore();
|
|
372
|
+
return lines.join("\n");
|
|
373
|
+
};
|
|
374
|
+
const rows = [
|
|
375
|
+
node("a", "AWS::EC2::Instance", { _unobserved: "read-failed" }),
|
|
376
|
+
node("b", "AWS::EC2::Instance", { _unobserved: "read-failed" }),
|
|
377
|
+
] as never;
|
|
378
|
+
|
|
379
|
+
test("names the lexicon and counts the unobserved rows", () => {
|
|
380
|
+
const out = capture(() => provenance(rows, { kind: "live" }, undefined, ["aws: connect ECONNREFUSED 127.0.0.1:9999"]));
|
|
381
|
+
expect(out).toContain("live read failed (aws)");
|
|
382
|
+
expect(out).toContain("2/2 rows unobserved");
|
|
383
|
+
expect(out).toContain("physical ids unavailable");
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
test("points at a recorded snapshot when one exists", () => {
|
|
387
|
+
const out = capture(() => provenance(rows, { kind: "live" }, "yes", ["aws: connect ECONNREFUSED"]));
|
|
388
|
+
expect(out).toContain("--at latest");
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
test("a working live read is unchanged", () => {
|
|
392
|
+
const bound = [{ id: "a", kind: "AWS::EC2::Instance", lexicon: "aws", attrs: {}, physicalId: "i-1" }] as never;
|
|
393
|
+
expect(capture(() => provenance(bound, { kind: "live" }))).toBe("— observed live · bound 1/1");
|
|
394
|
+
});
|
|
395
|
+
});
|