@intentius/chant-lexicon-aws 0.28.0 → 0.30.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/src/plugin.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { createRequire } from "module";
2
2
  import { detectTemplate } from "./detect";
3
- import type { LexiconPlugin, IntrinsicDef, ResourceMetadata, ExportedTemplate, ResourceSelector, InitTemplateSet, StackStatusObservation } from "@intentius/chant/lexicon";
3
+ import type { LexiconPlugin, IntrinsicDef, ObservationResult, DeepObservationResult, ResourceMetadata, ExportedTemplate, ResourceSelector, InitTemplateSet, StackStatusObservation } from "@intentius/chant/lexicon";
4
4
  const require = createRequire(import.meta.url);
5
5
  import type { LintRule } from "@intentius/chant/lint/rule";
6
6
  import type { TemplateParser } from "@intentius/chant/import/parser";
@@ -16,6 +16,8 @@ import { fileURLToPath } from "url";
16
16
  import { awsSerializer } from "./serializer";
17
17
  import { FLOCI_EMULATOR } from "./op/activities/floci";
18
18
  import { applyAwsEndpointArgv } from "./components/cloud-executor";
19
+ import { stackDoesNotExist } from "./stack-errors";
20
+ import { awsDeepNormalizationHooks, observeResourcesDeepAws } from "./deep-observe";
19
21
  import { awsReferenceCatalog } from "./reference-catalog";
20
22
  import { resolveTemplateAttrs } from "./live-attrs";
21
23
  import { CFParser } from "./import/parser";
@@ -24,12 +26,9 @@ import { parseStackTemplate } from "./import/live-export";
24
26
  import { awsCompletions } from "./lsp/completions";
25
27
  import { awsHover } from "./lsp/hover";
26
28
 
27
- /** True when a CloudFormation CLI error means the stack simply isn't there yet
28
- * (`ValidationError does not exist`) the pre-first-apply state, which live
29
- * queries should treat as "nothing deployed", not a failure. Exported for testing. */
30
- export function stackDoesNotExist(stderr: string): boolean {
31
- return /does not exist/i.test(stderr);
32
- }
29
+ /** Re-exported from ./stack-errors so the long-standing import path (and its
30
+ * tests) keep working now that the deep reader shares the classifier. */
31
+ export { stackDoesNotExist } from "./stack-errors";
33
32
 
34
33
  /**
35
34
  * AWS CloudFormation lexicon plugin.
@@ -534,8 +533,9 @@ aws cloudformation wait stack-update-complete --stack-name my-app-prod`,
534
533
  entityNames: string[];
535
534
  stack?: string;
536
535
  owned?: boolean;
537
- }): Promise<Record<string, ResourceMetadata>> {
536
+ }): Promise<ObservationResult> {
538
537
  const { getRuntime } = await import("@intentius/chant/runtime-adapter");
538
+ const { observation, unobservedAll } = await import("@intentius/chant/observation");
539
539
  const rt = getRuntime();
540
540
  const resources: Record<string, ResourceMetadata> = {};
541
541
 
@@ -544,7 +544,7 @@ aws cloudformation wait stack-update-complete --stack-name my-app-prod`,
544
544
  // determined here. Degrade to detect-only rather than silently filtering.
545
545
  // eslint-disable-next-line no-console
546
546
  console.warn(
547
- "[aws] ownership filter unavailable on describeResources (no tags from describe-stack-resources) — returning all; use `chant import --from <env> --owned` for ownership-filtered export",
547
+ "[aws] 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",
548
548
  );
549
549
  }
550
550
 
@@ -565,12 +565,26 @@ aws cloudformation wait stack-update-complete --stack-name my-app-prod`,
565
565
  if (listResult.exitCode !== 0) {
566
566
  // A stack that doesn't exist yet is the pre-first-apply state: nothing is
567
567
  // deployed for this env, so there are no live resources (every declared
568
- // resource is "pending") — not an error. Returning empty lets `lifecycle
569
- // diff --live` / the overlay show pending nodes instead of failing hard.
568
+ // resource is "pending") — not an error. That is a real absence, so the
569
+ // empty result is the honest one and `create` is the right proposal.
570
570
  if (stackDoesNotExist(listResult.stderr)) {
571
- return resources;
571
+ return observation(resources);
572
572
  }
573
- throw new Error(`Failed to describe stack "${stackName}": ${listResult.stderr}`);
573
+ // Any other failure (credentials, throttling, a region that can't be
574
+ // reached) establishes nothing about what is deployed. Reporting every
575
+ // declared entity as NOT-OBSERVED (#1089) is what keeps a broken read
576
+ // from arriving downstream as "none of this exists".
577
+ const reason = /credential|token|expired|AccessDenied|not authorized|UnauthorizedOperation/i.test(listResult.stderr)
578
+ ? "no-credentials"
579
+ : "read-failed";
580
+ return observation(
581
+ {},
582
+ unobservedAll(
583
+ options.entityNames,
584
+ reason,
585
+ `describe-stack-resources failed for stack "${stackName}": ${listResult.stderr.trim().split("\n")[0] ?? ""}`,
586
+ ),
587
+ );
574
588
  }
575
589
 
576
590
  const data = JSON.parse(listResult.stdout) as {
@@ -627,13 +641,46 @@ aws cloudformation wait stack-update-complete --stack-name my-app-prod`,
627
641
  physicalId: stackResource.PhysicalResourceId,
628
642
  status: stackResource.ResourceStatus,
629
643
  lastUpdated: stackResource.Timestamp,
644
+ // Total verdict (#1089): describe-stack-resources returns no tags, so
645
+ // this path cannot read the ownership marker. Say `unknown` explicitly
646
+ // rather than leaving the field off and letting each consumer guess —
647
+ // the change set never escalates `unknown` to a delete.
648
+ ownership: "unknown",
630
649
  attributes: Object.keys(attributes).length > 0 ? attributes : undefined,
631
650
  };
632
651
  }
633
652
 
634
- return resources;
653
+ // Every entity the stack answered for was answered for: an entity the
654
+ // template doesn't carry is genuinely not in this stack, which is an
655
+ // absence, not a hole.
656
+ return observation(resources);
635
657
  },
636
658
 
659
+ /**
660
+ * Property-level live read (#1015) via the Cloud Control API — past
661
+ * CloudFormation's view of the world, into the resource as the service
662
+ * actually holds it. Implementation in ./deep-observe.ts.
663
+ */
664
+ async observeResourcesDeep(options: {
665
+ environment: string;
666
+ buildOutput: string;
667
+ entityNames: string[];
668
+ entities: Map<string, { entityType: string; props: Record<string, unknown> }>;
669
+ stack?: string;
670
+ owned?: boolean;
671
+ }): Promise<DeepObservationResult> {
672
+ return observeResourcesDeepAws({
673
+ environment: options.environment,
674
+ entityNames: options.entityNames,
675
+ entities: options.entities,
676
+ stack: options.stack,
677
+ owned: options.owned,
678
+ });
679
+ },
680
+
681
+ /** The noise rules the deep pass applies to both the live and declared trees. */
682
+ deepNormalizationHooks: awsDeepNormalizationHooks,
683
+
637
684
  async describeStackStatus(options: { environment: string; stack: string }): Promise<StackStatusObservation | null> {
638
685
  const { getRuntime } = await import("@intentius/chant/runtime-adapter");
639
686
  const rt = getRuntime();
@@ -0,0 +1,15 @@
1
+ /**
2
+ * CloudFormation CLI error classification, shared by every live read path.
3
+ *
4
+ * Lives in its own module (rather than in ./plugin.ts, where it started) so the
5
+ * deep reader (#1015) can classify the same failure the same way without
6
+ * importing the plugin that imports it. ./plugin.ts re-exports it, so the
7
+ * original import path is unchanged.
8
+ */
9
+
10
+ /** True when a CloudFormation CLI error means the stack simply isn't there yet
11
+ * (`ValidationError … does not exist`) — the pre-first-apply state, which live
12
+ * queries should treat as "nothing deployed", not a failure. */
13
+ export function stackDoesNotExist(stderr: string): boolean {
14
+ return /does not exist/i.test(stderr);
15
+ }