@intentius/chant-lexicon-k8s 0.124.30 → 0.124.32

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.
Files changed (270) hide show
  1. package/dist/actions/apps.js +18 -0
  2. package/dist/actions/batch.js +6 -0
  3. package/dist/actions/core.js +48 -0
  4. package/dist/actions/index.js +13 -0
  5. package/dist/actions/networking.js +11 -0
  6. package/dist/actions/rbac.js +8 -0
  7. package/dist/api/classify.js +123 -0
  8. package/dist/api/connect.js +62 -0
  9. package/dist/api/fake-cluster.js +175 -0
  10. package/dist/api/operation-surface.d.ts.map +1 -1
  11. package/dist/api/operation-surface.js +109 -0
  12. package/dist/api/owner-chain.js +100 -0
  13. package/dist/api/sweep-types.js +35 -0
  14. package/dist/behaviour-kinds.js +82 -0
  15. package/dist/codegen/docs-cli.js +2 -0
  16. package/dist/codegen/docs.js +146 -0
  17. package/dist/codegen/generate-cli.js +30 -0
  18. package/dist/codegen/generate-lexicon.js +74 -0
  19. package/dist/codegen/generate-list-map-keys.js +88 -0
  20. package/dist/codegen/generate-operations.js +53 -0
  21. package/dist/codegen/generate-typescript.js +124 -0
  22. package/dist/codegen/generate.js +173 -0
  23. package/dist/codegen/naming.js +174 -0
  24. package/dist/codegen/package.js +36 -0
  25. package/dist/codegen/patches.js +88 -0
  26. package/dist/codegen/typecheck.js +4 -0
  27. package/dist/codegen/versions.js +41 -0
  28. package/dist/components/argo-app.js +51 -0
  29. package/dist/components/builders.js +22 -0
  30. package/dist/components/capability-plugin.js +31 -0
  31. package/dist/components/flux-reconcile.js +61 -0
  32. package/dist/components/index.d.ts +1 -1
  33. package/dist/components/index.d.ts.map +1 -1
  34. package/dist/components/index.js +18 -0
  35. package/dist/components/kubectl-apply.d.ts +83 -1
  36. package/dist/components/kubectl-apply.d.ts.map +1 -1
  37. package/dist/components/kubectl-apply.js +274 -0
  38. package/dist/components/kustomize-apply.js +25 -0
  39. package/dist/composites/adot-collector.js +184 -0
  40. package/dist/composites/agic-ingress.js +85 -0
  41. package/dist/composites/aks-external-dns-agent.js +146 -0
  42. package/dist/composites/alb-ingress.js +86 -0
  43. package/dist/composites/argo-app.js +199 -0
  44. package/dist/composites/autoscaled-service.js +174 -0
  45. package/dist/composites/azure-disk-storage-class.js +47 -0
  46. package/dist/composites/azure-file-storage-class.js +48 -0
  47. package/dist/composites/azure-monitor-collector.js +176 -0
  48. package/dist/composites/batch-job.js +139 -0
  49. package/dist/composites/catalog.js +4110 -0
  50. package/dist/composites/cockroachdb-cluster.js +371 -0
  51. package/dist/composites/cockroachdb-region-stack.js +369 -0
  52. package/dist/composites/config-connector-context.js +40 -0
  53. package/dist/composites/configured-app.js +135 -0
  54. package/dist/composites/cron-schedule.js +38 -0
  55. package/dist/composites/cron-workload.js +109 -0
  56. package/dist/composites/ebs-storage-class.js +54 -0
  57. package/dist/composites/efs-storage-class.js +46 -0
  58. package/dist/composites/external-dns-agent.js +122 -0
  59. package/dist/composites/filestore-storage-class.js +49 -0
  60. package/dist/composites/fluent-bit-agent.js +157 -0
  61. package/dist/composites/flux-app.js +156 -0
  62. package/dist/composites/gce-ingress.js +83 -0
  63. package/dist/composites/gce-pd-storage-class.js +49 -0
  64. package/dist/composites/gke-external-dns-agent.js +129 -0
  65. package/dist/composites/gke-fluent-bit-agent.js +165 -0
  66. package/dist/composites/gke-gateway.js +91 -0
  67. package/dist/composites/gke-otel-collector.js +94 -0
  68. package/dist/composites/index.js +51 -0
  69. package/dist/composites/inference-service.js +76 -0
  70. package/dist/composites/irsa-service-account.js +77 -0
  71. package/dist/composites/member-defaults.js +14 -0
  72. package/dist/composites/metrics-server.js +181 -0
  73. package/dist/composites/model.js +92 -0
  74. package/dist/composites/monitored-service.js +152 -0
  75. package/dist/composites/namespace-env.js +114 -0
  76. package/dist/composites/network-isolated-app.js +124 -0
  77. package/dist/composites/node-agent.js +149 -0
  78. package/dist/composites/operator-stack.js +268 -0
  79. package/dist/composites/otel-collector-agent.js +142 -0
  80. package/dist/composites/otel-collector-gateway.js +216 -0
  81. package/dist/composites/otel-collector-node.js +176 -0
  82. package/dist/composites/otel-collector-rbac.js +115 -0
  83. package/dist/composites/otel-collector-shape.js +128 -0
  84. package/dist/composites/otel-collector.js +115 -0
  85. package/dist/composites/otel-operator-collector.js +123 -0
  86. package/dist/composites/ray-cluster.js +378 -0
  87. package/dist/composites/ray-job.js +136 -0
  88. package/dist/composites/ray-service.js +178 -0
  89. package/dist/composites/secure-ingress.js +100 -0
  90. package/dist/composites/security-context.js +1 -0
  91. package/dist/composites/sidecar-app.js +105 -0
  92. package/dist/composites/stateful-app.js +111 -0
  93. package/dist/composites/vllm-serving-runtime.js +114 -0
  94. package/dist/composites/web-app.js +157 -0
  95. package/dist/composites/worker-pool.js +167 -0
  96. package/dist/composites/workload-identity-sa.js +81 -0
  97. package/dist/composites/workload-identity-service-account.js +77 -0
  98. package/dist/config-schema.js +72 -0
  99. package/dist/config.js +59 -0
  100. package/dist/coverage.js +27 -0
  101. package/dist/crd/crd-sources.js +615 -0
  102. package/dist/crd/loader.js +181 -0
  103. package/dist/crd/oci-chart.js +144 -0
  104. package/dist/crd/parser.js +382 -0
  105. package/dist/crd/project-codegen.js +273 -0
  106. package/dist/crd/types.js +7 -0
  107. package/dist/deep-observe-hooks.js +306 -0
  108. package/dist/deep-observe.js +234 -0
  109. package/dist/default-labels.js +87 -0
  110. package/dist/describe-resources.js +699 -0
  111. package/dist/describe-stack-status.js +99 -0
  112. package/dist/detect.js +11 -0
  113. package/dist/effect-receipt-row.js +205 -0
  114. package/dist/export-resources.js +39 -0
  115. package/dist/generated/index.js +1092 -0
  116. package/dist/generated/runtime.js +4 -0
  117. package/dist/group-namespace.js +96 -0
  118. package/dist/import/embedded.js +197 -0
  119. package/dist/import/generator.js +288 -0
  120. package/dist/import/live-export.js +85 -0
  121. package/dist/import/parser.js +204 -0
  122. package/dist/import/testdata/embedded/fixtures.js +60 -0
  123. package/dist/include/entity.js +78 -0
  124. package/dist/include/resolve.js +170 -0
  125. package/dist/index.js +59 -0
  126. package/dist/integrity.json +3 -3
  127. package/dist/kube/apply.js +90 -0
  128. package/dist/kube/connect.js +46 -0
  129. package/dist/kube/delete.js +138 -0
  130. package/dist/kube/describe.js +169 -0
  131. package/dist/kube/events.js +132 -0
  132. package/dist/kube/flags.js +91 -0
  133. package/dist/kube/get.js +159 -0
  134. package/dist/kube/group.js +123 -0
  135. package/dist/kube/logs.js +82 -0
  136. package/dist/kube/project.js +94 -0
  137. package/dist/kube/render.js +227 -0
  138. package/dist/kube/source.js +122 -0
  139. package/dist/kube/target.js +63 -0
  140. package/dist/kube/testing.js +28 -0
  141. package/dist/kube/top.js +99 -0
  142. package/dist/kube/verdict.js +72 -0
  143. package/dist/kube/wait.js +129 -0
  144. package/dist/kustomize/render.js +73 -0
  145. package/dist/kustomize/rendered-entity.js +67 -0
  146. package/dist/kustomize/root.js +68 -0
  147. package/dist/lint/audit-catalog.js +61 -0
  148. package/dist/lint/audit-lineage.js +118 -0
  149. package/dist/lint/post-synth/argo-helpers.js +36 -0
  150. package/dist/lint/post-synth/argo002.js +31 -0
  151. package/dist/lint/post-synth/argo003.js +53 -0
  152. package/dist/lint/post-synth/argo005.js +54 -0
  153. package/dist/lint/post-synth/crd-schema-helpers.d.ts.map +1 -1
  154. package/dist/lint/post-synth/crd-schema-helpers.js +214 -0
  155. package/dist/lint/post-synth/flux002.js +56 -0
  156. package/dist/lint/post-synth/flux003.js +50 -0
  157. package/dist/lint/post-synth/index.js +104 -0
  158. package/dist/lint/post-synth/k8s-helpers.js +186 -0
  159. package/dist/lint/post-synth/monitoring-selection-helpers.js +163 -0
  160. package/dist/lint/post-synth/otel-placement-helpers.js +293 -0
  161. package/dist/lint/post-synth/sops-helpers.js +43 -0
  162. package/dist/lint/post-synth/wk8005.js +44 -0
  163. package/dist/lint/post-synth/wk8006.js +56 -0
  164. package/dist/lint/post-synth/wk8041.js +60 -0
  165. package/dist/lint/post-synth/wk8042.js +36 -0
  166. package/dist/lint/post-synth/wk8101.js +52 -0
  167. package/dist/lint/post-synth/wk8102.js +30 -0
  168. package/dist/lint/post-synth/wk8103.js +33 -0
  169. package/dist/lint/post-synth/wk8104.js +58 -0
  170. package/dist/lint/post-synth/wk8105.js +33 -0
  171. package/dist/lint/post-synth/wk8201.js +44 -0
  172. package/dist/lint/post-synth/wk8202.js +34 -0
  173. package/dist/lint/post-synth/wk8203.js +34 -0
  174. package/dist/lint/post-synth/wk8204.js +71 -0
  175. package/dist/lint/post-synth/wk8205.js +38 -0
  176. package/dist/lint/post-synth/wk8207.js +33 -0
  177. package/dist/lint/post-synth/wk8208.js +33 -0
  178. package/dist/lint/post-synth/wk8209.js +33 -0
  179. package/dist/lint/post-synth/wk8301.js +49 -0
  180. package/dist/lint/post-synth/wk8302.js +34 -0
  181. package/dist/lint/post-synth/wk8303.js +68 -0
  182. package/dist/lint/post-synth/wk8304.js +58 -0
  183. package/dist/lint/post-synth/wk8305.js +96 -0
  184. package/dist/lint/post-synth/wk8306.js +38 -0
  185. package/dist/lint/post-synth/wk8401.js +86 -0
  186. package/dist/lint/post-synth/wk8402.js +31 -0
  187. package/dist/lint/post-synth/wk8403.js +49 -0
  188. package/dist/lint/post-synth/wk8404.js +36 -0
  189. package/dist/lint/post-synth/wk8405.js +65 -0
  190. package/dist/lint/post-synth/wk8406.js +50 -0
  191. package/dist/lint/post-synth/wk8407.js +58 -0
  192. package/dist/lint/post-synth/wk8501.js +36 -0
  193. package/dist/lint/post-synth/wk8502.js +35 -0
  194. package/dist/lint/post-synth/wk8503.js +164 -0
  195. package/dist/lint/post-synth/wk8504.js +20 -0
  196. package/dist/lint/post-synth/wk8505.js +31 -0
  197. package/dist/lint/post-synth/wk8601.js +40 -0
  198. package/dist/lint/post-synth/wk8602.js +120 -0
  199. package/dist/lint/post-synth/wk8603.js +63 -0
  200. package/dist/lint/post-synth/wk8604.js +31 -0
  201. package/dist/lint/post-synth/wk8605.js +124 -0
  202. package/dist/lint/post-synth/wk8606.js +22 -0
  203. package/dist/lint/post-synth/wk8701.js +31 -0
  204. package/dist/lint/post-synth/wk8702.js +37 -0
  205. package/dist/lint/post-synth/wk8703.js +31 -0
  206. package/dist/lint/post-synth/wk8704.js +135 -0
  207. package/dist/lint/post-synth/wk8705.js +38 -0
  208. package/dist/lint/rules/argo-appset-single-project.js +56 -0
  209. package/dist/lint/rules/argo-ast.js +94 -0
  210. package/dist/lint/rules/argo-automated-prune.js +59 -0
  211. package/dist/lint/rules/flux-source-ref-pin.js +44 -0
  212. package/dist/lint/rules/hardcoded-namespace.js +57 -0
  213. package/dist/lint/rules/latest-image-tag.js +103 -0
  214. package/dist/lint/rules/missing-resource-limits.js +85 -0
  215. package/dist/lsp/completions.d.ts.map +1 -1
  216. package/dist/lsp/completions.js +18 -0
  217. package/dist/lsp/hover.d.ts.map +1 -1
  218. package/dist/lsp/hover.js +57 -0
  219. package/dist/manifest-entity.js +71 -0
  220. package/dist/manifest.json +1 -1
  221. package/dist/micro-time.js +49 -0
  222. package/dist/op/activities/apply-diff.d.ts +31 -0
  223. package/dist/op/activities/apply-diff.d.ts.map +1 -0
  224. package/dist/op/activities/apply-diff.js +79 -0
  225. package/dist/op/activities/argo.js +98 -0
  226. package/dist/op/activities/ensure-secret.js +38 -0
  227. package/dist/op/activities/index.js +51 -0
  228. package/dist/op/activities/kubectl.d.ts +64 -0
  229. package/dist/op/activities/kubectl.d.ts.map +1 -1
  230. package/dist/op/activities/kubectl.js +517 -0
  231. package/dist/op/activities/wait-for-ready.js +229 -0
  232. package/dist/op/builders.js +68 -0
  233. package/dist/package-cli.js +21 -0
  234. package/dist/patch/json-patch.js +187 -0
  235. package/dist/patch/rendered-patch.js +94 -0
  236. package/dist/plugin.js +741 -0
  237. package/dist/project-kinds.js +56 -0
  238. package/dist/receipt-store.js +216 -0
  239. package/dist/rules/crd-schema-helpers.ts +2 -1
  240. package/dist/secret-labels.js +33 -0
  241. package/dist/secret-store.js +116 -0
  242. package/dist/serializer.d.ts.map +1 -1
  243. package/dist/serializer.js +578 -0
  244. package/dist/sops/encrypted-secret-file.js +194 -0
  245. package/dist/sops/entity.js +32 -0
  246. package/dist/spec/fetch.js +58 -0
  247. package/dist/spec/parse.js +622 -0
  248. package/dist/subscribe-changes.js +168 -0
  249. package/dist/teardown.js +197 -0
  250. package/dist/telemetry-release.js +86 -0
  251. package/dist/validate-cli.js +17 -0
  252. package/dist/validate.js +47 -0
  253. package/dist/variables.js +34 -0
  254. package/dist/whoami.js +109 -0
  255. package/package.json +12 -12
  256. package/src/api/operation-surface.ts +2 -1
  257. package/src/codegen/package.ts +2 -2
  258. package/src/components/index.ts +3 -0
  259. package/src/components/kubectl-apply-plan.k3d.e2e.test.ts +127 -0
  260. package/src/components/kubectl-apply.test.ts +262 -0
  261. package/src/components/kubectl-apply.ts +341 -12
  262. package/src/deep-observe-hooks.ts +2 -2
  263. package/src/import/roundtrip-build.test.ts +46 -0
  264. package/src/lint/post-synth/crd-schema-helpers.ts +2 -1
  265. package/src/lsp/completions.ts +2 -1
  266. package/src/lsp/hover.ts +2 -1
  267. package/src/op/activities/apply-diff.ts +87 -0
  268. package/src/op/activities/kubectl-plan.test.ts +224 -0
  269. package/src/op/activities/kubectl.ts +204 -16
  270. package/src/serializer.ts +2 -1
@@ -0,0 +1,699 @@
1
+ /**
2
+ * Live introspection of a Kubernetes cluster — implements the
3
+ * LexiconPlugin.describeResources() contract for the k8s lexicon.
4
+ *
5
+ * ## What changed, and why it matters (chant #1074)
6
+ *
7
+ * This used to run `kubectl get <kind> <name> -o json` once per declared
8
+ * entity, serially, resolved through a hardcoded twenty-entry
9
+ * `KUBECTL_RESOURCE` map. Every CRD and every uncommon type fell off the end
10
+ * of that map and came back as a hole. Coverage, concurrency and error quality
11
+ * were all capped by it, and `lifecycle diff --live`, `lifecycle plan` and
12
+ * behold's overlay inherited the cap.
13
+ *
14
+ * Now:
15
+ *
16
+ * - **Coverage.** An entity type becomes an API address through the generated
17
+ * operation surface (`./api/operation-surface.ts`), which the same codegen
18
+ * pass that emits the declarable classes writes — 184 types rather than 20,
19
+ * CRDs included. The address is then confirmed against the cluster's *own*
20
+ * discovery, which is what knows the plural and the scope for the version
21
+ * that cluster actually serves.
22
+ * - **Concurrency.** Reads run through the client's bounded pool. A hundred
23
+ * entities are a hundred concurrent HTTP GETs sharing one connection and one
24
+ * cached credential, not a hundred serial process spawns.
25
+ * - **Error quality.** Failures arrive as typed errors carrying the API
26
+ * server's own `code` and `reason`, so the tri-state verdict is read off a
27
+ * field instead of matched against English on stderr.
28
+ *
29
+ * ## What did not change
30
+ *
31
+ * The observation tri-state (chant #1089) and the cluster binding (chant
32
+ * #1100/#1155) are the same contracts they were. A genuine `NotFound` — and a
33
+ * kind the cluster's discovery does not serve, where no instance can exist —
34
+ * is an absence, and only an absence becomes a `create`. Everything else is
35
+ * NOT-OBSERVED with a reason. A declared `k8s.profiles.<env>.context` that
36
+ * disagrees with the ambient one still refuses before a single resource is
37
+ * touched, via the same `resolveClusterTarget` the GCP lexicon shares.
38
+ *
39
+ * ## Runtime children (chant #1077)
40
+ *
41
+ * A Pod a declared Deployment's controller created is live and undeclared —
42
+ * exactly the shape `lifecycle diff --live` has always classified `orphan`.
43
+ * On Kubernetes that is wrong: it is expected runtime, not drift, and will be
44
+ * recreated the moment it is deleted. After the declared-entity reads above
45
+ * resolve, this sweeps candidate kinds in the namespaces they actually live in
46
+ * and walks each object's `ownerReferences` chain (`./api/owner-chain.ts`)
47
+ * against the entities this very call resolved. A chain reaching one of them
48
+ * is reported with `ownerChain: { root: "declared", ... }`; core's
49
+ * diff/change-set engine reads that as `runtime` instead of `orphan`.
50
+ *
51
+ * Which kinds are candidates is the cluster's own API discovery's answer
52
+ * (#1517): every listable kind it serves, one version per group. The sweep's
53
+ * bound is the namespaces the declared estate touches, never a kind table —
54
+ * see `addRuntimeChildren`.
55
+ */
56
+ import { observation, unobservedAll } from "@intentius/chant/observation";
57
+ import { hasOwnershipMarker, classifyOwnership, readOwnership, LABEL_OWNERSHIP_KEYS } from "@intentius/chant/ownership";
58
+ import { defaultK8sConnector } from "./api/connect.js";
59
+ import { classifyApiFailure, isMissingClientPackage, isWholeLexiconFailure, MISSING_CLIENT_DETAIL, } from "./api/classify.js";
60
+ import { operationFor } from "./api/operation-surface.js";
61
+ import { resolveK8sOwnerChain } from "./api/owner-chain.js";
62
+ import { gvkToTypeName } from "./spec/parse.js";
63
+ import { observeReceiptRows, receiptRowsFor } from "./receipt-store.js";
64
+ function pruneUndefined(obj) {
65
+ const out = {};
66
+ for (const [k, v] of Object.entries(obj)) {
67
+ if (v !== undefined)
68
+ out[k] = v;
69
+ }
70
+ return out;
71
+ }
72
+ /**
73
+ * Collapse a live object to one status word. Unchanged from the kubectl path
74
+ * — the shape of the response is the same JSON either way.
75
+ */
76
+ export function statusFromObject(obj) {
77
+ const status = obj.status;
78
+ // The most specific FAILING signal wins over the outermost one (#1397).
79
+ //
80
+ // A Pod's phase is `Running` from the moment the kubelet admits it and one
81
+ // container starts — so a crashlooping Pod reported `Running`, which every
82
+ // consumer classifying that word read as healthy. The failure lives one level
83
+ // down, in `containerStatuses[].state.waiting.reason`, and never reached the
84
+ // wire, so nothing downstream could recover it.
85
+ const waiting = failingContainerReason(status);
86
+ if (waiting)
87
+ return waiting;
88
+ // Scheduling failure is a condition, not a phase: an unschedulable Pod is
89
+ // `Pending`, which is true and useless next to `Unschedulable`.
90
+ const blocked = failingConditionReason(status);
91
+ if (blocked)
92
+ return blocked;
93
+ const phase = status?.phase;
94
+ if (typeof phase === "string")
95
+ return phase;
96
+ if (status && typeof status.readyReplicas === "number" && typeof status.replicas === "number") {
97
+ return status.readyReplicas === status.replicas
98
+ ? "READY"
99
+ : `PROGRESSING(${status.readyReplicas}/${status.replicas})`;
100
+ }
101
+ // The kstatus rung (#1549): an object with NO phase and NO replica counts —
102
+ // a Flux Kustomization/HelmRelease, cert-manager, any conditions-first CRD —
103
+ // used to fall straight to PRESENT, so a wedged reconciler read exactly like
104
+ // a healthy one. `Ready` is the contract those objects speak: False → its
105
+ // reason (`BuildFailed`, `UpgradeFailed`, …), True → READY. Deliberately
106
+ // BELOW the phase/replica rungs so no Pod or workload word changes — for
107
+ // those objects a False Ready is already visible through the rungs above.
108
+ const ready = readyConditionWord(status);
109
+ if (ready)
110
+ return ready;
111
+ // Argo's Application speaks health/sync, not a Ready condition (the same
112
+ // split the readiness registry encodes) — `Degraded` / `OutOfSync` beats
113
+ // PRESENT for the one object whose whole job is convergence.
114
+ const argo = argoStatusWord(status);
115
+ if (argo)
116
+ return argo;
117
+ return "PRESENT";
118
+ }
119
+ /** The `Ready` condition rendered as a status word: False → its reason (or
120
+ * NOT-READY when the controller gave none), True → READY, absent → nothing. */
121
+ function readyConditionWord(status) {
122
+ const conditions = status?.conditions;
123
+ if (!Array.isArray(conditions))
124
+ return undefined;
125
+ for (const c of conditions) {
126
+ const cond = c;
127
+ if (cond.type !== "Ready")
128
+ continue;
129
+ if (cond.status === "True")
130
+ return "READY";
131
+ if (cond.status === "False") {
132
+ return typeof cond.reason === "string" && cond.reason.length > 0 ? cond.reason : "NOT-READY";
133
+ }
134
+ }
135
+ return undefined;
136
+ }
137
+ /** Argo Application health/sync as a status word: anything but the happy pair
138
+ * surfaces the unhappy half; Healthy+Synced reads as READY. */
139
+ function argoStatusWord(status) {
140
+ const s = status;
141
+ const health = typeof s?.health?.status === "string" ? s.health.status : undefined;
142
+ const sync = typeof s?.sync?.status === "string" ? s.sync.status : undefined;
143
+ if (!health && !sync)
144
+ return undefined;
145
+ if (health && health !== "Healthy")
146
+ return health;
147
+ if (sync && sync !== "Synced")
148
+ return sync;
149
+ return "READY";
150
+ }
151
+ /**
152
+ * The reason a container is stuck, if one is — `CrashLoopBackOff`,
153
+ * `ImagePullBackOff`, `CreateContainerConfigError`.
154
+ *
155
+ * Only a WAITING container counts. A terminated one may have exited cleanly as
156
+ * part of a Job, and a running one is not stuck; neither should outrank the
157
+ * phase.
158
+ */
159
+ function failingContainerReason(status) {
160
+ const containers = status?.containerStatuses;
161
+ if (!Array.isArray(containers))
162
+ return undefined;
163
+ for (const c of containers) {
164
+ const reason = c?.state?.waiting?.reason;
165
+ // `ContainerCreating` and `PodInitializing` are ordinary startup, not
166
+ // failure — they already classify as progressing, and promoting them over
167
+ // the phase would just be noisier.
168
+ if (typeof reason === "string" && reason.length > 0 && reason !== "ContainerCreating" && reason !== "PodInitializing") {
169
+ return reason;
170
+ }
171
+ }
172
+ return undefined;
173
+ }
174
+ /**
175
+ * The reason a blocking condition is False — `Unschedulable` and the like.
176
+ *
177
+ * Only conditions whose truth means "this object is usable"; a False `Ready` on
178
+ * its own is already visible through the phase and the container states above.
179
+ */
180
+ function failingConditionReason(status) {
181
+ const conditions = status?.conditions;
182
+ if (!Array.isArray(conditions))
183
+ return undefined;
184
+ for (const c of conditions) {
185
+ const cond = c;
186
+ if (cond.type !== "PodScheduled")
187
+ continue;
188
+ if (cond.status !== "False")
189
+ continue;
190
+ if (typeof cond.reason === "string" && cond.reason.length > 0)
191
+ return cond.reason;
192
+ }
193
+ return undefined;
194
+ }
195
+ /**
196
+ * The unhappy conditions on a live object, as `Type=Reason: message` (#1401).
197
+ *
198
+ * #1397 made the status WORD honest — a crashlooping Pod reports
199
+ * `CrashLoopBackOff` rather than `Running`. This carries the part that says
200
+ * what to do about it: `Unschedulable` is the reason, and
201
+ * `0/3 nodes are available: 1 node(s) had untolerated taint` is the answer, and
202
+ * only the first reached the wire.
203
+ *
204
+ * Only conditions that are NOT in their happy state are reported. A `Ready=True`
205
+ * says nothing an operator needs and would bury the one line that does — and a
206
+ * condition's happy polarity is per type: `Ready`/`Available`/`PodScheduled`
207
+ * are good when True, while `Unschedulable`-style and the standard
208
+ * `*Pressure`/`NetworkUnavailable` node conditions are good when False.
209
+ *
210
+ * Absent when everything is happy, so a consumer can treat presence as "this
211
+ * object has something to say".
212
+ */
213
+ const CONDITIONS_GOOD_WHEN_FALSE = /pressure$|^networkunavailable$|^unschedulable$|failed/i;
214
+ export function unhappyConditions(obj) {
215
+ const conditions = obj.status?.conditions;
216
+ if (!Array.isArray(conditions))
217
+ return undefined;
218
+ const out = [];
219
+ for (const c of conditions) {
220
+ const cond = c;
221
+ if (typeof cond.type !== "string")
222
+ continue;
223
+ if (typeof cond.status === "string") {
224
+ const goodWhenFalse = CONDITIONS_GOOD_WHEN_FALSE.test(cond.type);
225
+ const happy = goodWhenFalse ? cond.status === "False" : cond.status === "True";
226
+ if (happy)
227
+ continue;
228
+ }
229
+ else if (!(typeof cond.message === "string" && cond.message)) {
230
+ // No status, no message: nothing to say. But a status-less condition
231
+ // WITH a message is an object speaking — Argo's ApplicationCondition is
232
+ // `{type, message, lastTransitionTime}`, written only for problems, and
233
+ // the polarity test above has nothing to read on it (#1644).
234
+ continue;
235
+ }
236
+ const reason = typeof cond.reason === "string" && cond.reason ? `=${cond.reason}` : "";
237
+ const message = typeof cond.message === "string" && cond.message ? `: ${cond.message}` : "";
238
+ out.push(`${cond.type}${reason}${message}`);
239
+ }
240
+ return out.length > 0 ? out : undefined;
241
+ }
242
+ /**
243
+ * The revisions a GitOps controller writes onto its own object (#1632).
244
+ *
245
+ * Conditions lag. A Flux Kustomization mid-rollout still reports the previous
246
+ * reconcile's `Ready=True` for as long as the new one takes, so a consumer
247
+ * reading only conditions calls a half-applied estate converged. The
248
+ * controller's own definition of converged is a revision comparison:
249
+ * `lastAppliedRevision` against `lastAttemptedRevision` on the same object
250
+ * (attempted-but-not-applied is in flight), and a Kustomization's
251
+ * `lastAppliedRevision` against its source's `artifact.revision` (the
252
+ * cross-object half, which a consumer joins once both ends are on the wire).
253
+ * None of the three reached an observed entity's attributes before this.
254
+ *
255
+ * Read by path rather than by kind, which is the module's standing rule
256
+ * everywhere else: `status.lastAppliedRevision` / `status.lastAttemptedRevision`
257
+ * are what kustomize-controller and helm-controller write, and
258
+ * `status.artifact.revision` is what every source kind (GitRepository,
259
+ * OCIRepository, Bucket, HelmChart) writes. An object that has none of them —
260
+ * every non-GitOps kind — contributes nothing, so no kind table has to name
261
+ * which kinds those are, and a CRD speaking the same convention is carried for
262
+ * free. `artifactRevision` flattens the one nested path so every revision on
263
+ * the wire is a plain string next to the others.
264
+ */
265
+ export function revisionAttributes(obj) {
266
+ const status = obj.status;
267
+ const str = (v) => (typeof v === "string" && v.length > 0 ? v : undefined);
268
+ return {
269
+ lastAppliedRevision: str(status?.lastAppliedRevision),
270
+ lastAttemptedRevision: str(status?.lastAttemptedRevision),
271
+ artifactRevision: str(status?.artifact?.revision),
272
+ };
273
+ }
274
+ /**
275
+ * The declared repository protocol of a Flux HelmRepository (behold#298).
276
+ *
277
+ * A HelmRepository with `spec.type: oci` is never reconciled by
278
+ * source-controller — no conditions, no artifact, by design — so a consumer
279
+ * classifying health off conditions reads a healthy OCI repository exactly
280
+ * like one whose controller has not shown up. `spec.type` is the field that
281
+ * separates the two, and no spec field reached the wire before this. It rides
282
+ * the attributes flattened, as `type`, the same way `artifactRevision`
283
+ * flattens `status.artifact.revision`: every scalar on the wire is a plain
284
+ * key next to the others, and the wire stays lean — one field, not the spec.
285
+ *
286
+ * Kind-gated, unlike the module's usual read-by-path rule, because the PATH
287
+ * is not distinctive: a core Service's `spec.type` is
288
+ * `ClusterIP`/`LoadBalancer`, and forwarding that would change the wire shape
289
+ * of a kind this was never about. Only source.toolkit.fluxcd.io's
290
+ * HelmRepository speaks this meaning of the field.
291
+ */
292
+ export function helmRepositoryTypeAttribute(gvk, obj) {
293
+ if (gvk.kind !== "HelmRepository" || !gvk.apiVersion.startsWith("source.toolkit.fluxcd.io/"))
294
+ return undefined;
295
+ const type = obj.spec?.type;
296
+ return typeof type === "string" && type.length > 0 ? type : undefined;
297
+ }
298
+ export async function describeResources(options, connect = defaultK8sConnector) {
299
+ const resources = {};
300
+ const unobserved = {};
301
+ // The resolved query address per declared entity (#1620) — the exact request
302
+ // path each read was issued against, with the namespace the client actually
303
+ // defaulted to when the declaration carries none. This is what lets an
304
+ // absence explain itself: a Flux-deployed object declaring no
305
+ // metadata.namespace reads from `default`, correctly comes back 404, and
306
+ // without the address the report says "not there" when the truth is "not
307
+ // looked for where it lives".
308
+ const queried = {};
309
+ // Effect receipt rows (#2074) are read by their own leg below: the applier
310
+ // never wrote them (#1832), and their declaration carries no props, so the
311
+ // generic sweep has no `metadata.name` to query by and would report a hole
312
+ // where the receipt leg has a real answer. Their addresses come from the
313
+ // build output's receipt comment, which is the serializer's one rendering of
314
+ // the derivation (./effect-receipt-row.ts).
315
+ const receiptRows = receiptRowsFor(options.entityNames, options.buildOutput);
316
+ const declared = [...options.entities]
317
+ .filter(([entityName]) => !receiptRows.has(entityName))
318
+ .map(([entityName, entity]) => ({
319
+ entityName,
320
+ entityType: entity.entityType,
321
+ props: entity.props,
322
+ }));
323
+ // A whole-lexicon failure below is a hole for the receipts too: nobody
324
+ // looked at those either, and a connect that never happened proves nothing
325
+ // about a receipt's presence.
326
+ const everyName = [...declared.map((d) => d.entityName), ...receiptRows.keys()];
327
+ // Connect first. The binding check lives here, so a bound-but-mismatched
328
+ // context throws before any resource is read — core turns that into
329
+ // NOT-OBSERVED for every declared entity rather than an empty result.
330
+ let client;
331
+ try {
332
+ ({ client } = await connect({ environment: options.environment, cwd: options.cwd }));
333
+ }
334
+ catch (err) {
335
+ if (isMissingClientPackage(err)) {
336
+ return observation({}, unobservedAll(everyName, "read-failed", MISSING_CLIENT_DETAIL, options.entities));
337
+ }
338
+ if (isWholeLexiconFailure(err)) {
339
+ const outcome = classifyApiFailure(err);
340
+ return observation({}, unobservedAll(everyName, outcome.kind === "unobserved" ? outcome.reason : "read-failed", outcome.kind === "unobserved" ? outcome.detail : undefined, options.entities));
341
+ }
342
+ // A cluster-binding mismatch (chant #1100) belongs here: it is a loud
343
+ // refusal, and core's whole-lexicon handling is what turns it into an
344
+ // honest hole per entity.
345
+ throw err;
346
+ }
347
+ await client.concurrently(declared, async ({ entityName, entityType, props }) => {
348
+ const operation = operationFor(entityType);
349
+ if (!operation) {
350
+ // chant knows no API address for this type at all — not even its group.
351
+ // The object may well exist, so this is a hole, never an absence.
352
+ unobserved[entityName] = {
353
+ type: entityType,
354
+ reason: "unsupported-kind",
355
+ detail: `no generated operation surface for ${entityType} — run \`chant generate\` in the k8s lexicon, or declare the CRD as a codegen source`,
356
+ };
357
+ return;
358
+ }
359
+ const metadata = props.metadata;
360
+ const name = metadata?.name;
361
+ if (!name) {
362
+ // Nothing to query by. Not an absence — chant never issued a read.
363
+ unobserved[entityName] = {
364
+ type: entityType,
365
+ reason: "read-failed",
366
+ detail: "declared entity has no metadata.name to query by",
367
+ };
368
+ return;
369
+ }
370
+ // The declared namespace wins; the caller's override only fills the hole a
371
+ // declaration left, and only for a kind that HAS a namespace (#1629). With
372
+ // neither, the client defaults to `default` exactly as it did.
373
+ const namespace = metadata?.namespace ?? (operation.scope === "Namespaced" ? options.namespace : undefined);
374
+ const ref = {
375
+ apiVersion: operation.apiVersion,
376
+ kind: operation.kind,
377
+ name,
378
+ ...(namespace ? { namespace } : {}),
379
+ };
380
+ // Resolve the address BEFORE the read, so every verdict below — present,
381
+ // absent, filtered, read-failed — carries the same record of what was
382
+ // asked. Resolution rides the client's cached discovery; a kind discovery
383
+ // does not serve has no path, and a resolution failure classifies through
384
+ // the read below, never here.
385
+ try {
386
+ const address = typeof client.pathFor === "function" ? await client.pathFor(ref) : undefined;
387
+ if (address)
388
+ queried[entityName] = address;
389
+ }
390
+ catch {
391
+ // The read below hits the same resolution and classifies its failure.
392
+ }
393
+ try {
394
+ const obj = await client.read(ref);
395
+ // owned filter: withhold resources not carrying chant's marker label.
396
+ // Withheld is not absent (chant #1089) — this object exists, it just
397
+ // isn't chant's, and dropping it silently is how `--owned` used to turn a
398
+ // declared-but-foreign resource into a proposed `create`.
399
+ if (options.owned && !hasOwnershipMarker(obj.metadata?.labels, LABEL_OWNERSHIP_KEYS)) {
400
+ unobserved[entityName] = {
401
+ type: entityType,
402
+ reason: "filtered",
403
+ detail: "live object carries no chant ownership marker and --owned was requested",
404
+ ...(queried[entityName] ? { queried: queried[entityName] } : {}),
405
+ };
406
+ return;
407
+ }
408
+ resources[entityName] = {
409
+ type: entityType,
410
+ physicalId: obj.metadata?.uid,
411
+ status: statusFromObject(obj),
412
+ lastUpdated: obj.metadata?.creationTimestamp,
413
+ ownership: classifyOwnership(obj.metadata?.labels, LABEL_OWNERSHIP_KEYS),
414
+ // Marker identity (#1222): the stack/env labels stamped at synthesis,
415
+ // read back verbatim. Undefined when the managed-by label is absent.
416
+ marker: readOwnership(obj.metadata?.labels, LABEL_OWNERSHIP_KEYS),
417
+ attributes: pruneUndefined({
418
+ namespace: obj.metadata?.namespace,
419
+ labels: obj.metadata?.labels,
420
+ resourceVersion: obj.metadata?.resourceVersion,
421
+ // What the object's own controller says is wrong with it (#1401).
422
+ conditions: unhappyConditions(obj),
423
+ // What a GitOps controller says it has applied and is applying
424
+ // (#1632) — the convergence signal conditions cannot carry.
425
+ ...revisionAttributes(obj),
426
+ // A HelmRepository's spec.type — `oci` means "no conditions is
427
+ // healthy", and only this field can say so (behold#298).
428
+ type: helmRepositoryTypeAttribute(operation, obj),
429
+ }),
430
+ };
431
+ }
432
+ catch (err) {
433
+ const outcome = classifyApiFailure(err);
434
+ if (outcome.kind === "unobserved") {
435
+ unobserved[entityName] = {
436
+ type: entityType,
437
+ reason: outcome.reason,
438
+ detail: outcome.detail,
439
+ ...(queried[entityName] ? { queried: queried[entityName] } : {}),
440
+ };
441
+ }
442
+ // `absent` deliberately records nothing: in neither map is how the
443
+ // contract spells "asked, and it is not there". Its address stays in
444
+ // `queried` (#1620), which is the only record an absence gets.
445
+ }
446
+ });
447
+ await addRuntimeChildren(client, resources, unobserved, options.owned, declared);
448
+ // The receipt leg last, so its answers are the ones that stand for the
449
+ // receipt entities, because nothing above ever looked at one.
450
+ if (receiptRows.size > 0) {
451
+ const receiptObs = await observeReceiptRows(client, receiptRows);
452
+ Object.assign(resources, receiptObs.resources);
453
+ Object.assign(unobserved, receiptObs.unobserved);
454
+ }
455
+ return observation(resources, unobserved, queried);
456
+ }
457
+ function gitopsIndex(declared) {
458
+ const byRef = new Map();
459
+ const argoByName = new Map();
460
+ const targetNamespaces = new Set();
461
+ let present = false;
462
+ for (const { entityName, entityType, props } of declared) {
463
+ const isArgo = entityType === "K8s::Argo::Application";
464
+ const isFlux = entityType === "K8s::Flux::Kustomization" || entityType === "K8s::Flux::HelmRelease";
465
+ if (!isArgo && !isFlux)
466
+ continue;
467
+ present = true;
468
+ const meta = props.metadata;
469
+ const spec = props.spec;
470
+ const name = meta?.name;
471
+ if (!name)
472
+ continue;
473
+ if (isFlux) {
474
+ byRef.set(`${entityType}\0${meta?.namespace ?? "default"}\0${name}`, entityName);
475
+ if (typeof spec?.targetNamespace === "string")
476
+ targetNamespaces.add(spec.targetNamespace);
477
+ }
478
+ else {
479
+ argoByName.set(name, argoByName.has(name) ? null : entityName);
480
+ if (typeof spec?.destination?.namespace === "string")
481
+ targetNamespaces.add(spec.destination.namespace);
482
+ }
483
+ }
484
+ return { present, byRef, argoByName, targetNamespaces };
485
+ }
486
+ /** The declared GitOps CR a swept object's controller labels resolve to, or
487
+ * undefined. Flux's label pairs are exact; Argo's bare instance label claims
488
+ * only a UNIQUE declared Application. */
489
+ function gitopsOwner(labels, gitops) {
490
+ if (!labels || !gitops.present)
491
+ return undefined;
492
+ const ksName = labels["kustomize.toolkit.fluxcd.io/name"];
493
+ const ksNs = labels["kustomize.toolkit.fluxcd.io/namespace"];
494
+ if (ksName && ksNs) {
495
+ const hit = gitops.byRef.get(`K8s::Flux::Kustomization\0${ksNs}\0${ksName}`);
496
+ if (hit)
497
+ return hit;
498
+ }
499
+ const hrName = labels["helm.toolkit.fluxcd.io/name"];
500
+ const hrNs = labels["helm.toolkit.fluxcd.io/namespace"];
501
+ if (hrName && hrNs) {
502
+ const hit = gitops.byRef.get(`K8s::Flux::HelmRelease\0${hrNs}\0${hrName}`);
503
+ if (hit)
504
+ return hit;
505
+ }
506
+ const instance = labels["app.kubernetes.io/instance"];
507
+ if (instance) {
508
+ const hit = gitops.argoByName.get(instance);
509
+ if (hit)
510
+ return hit; // null (ambiguous) and undefined both decline
511
+ }
512
+ return undefined;
513
+ }
514
+ /**
515
+ * Kinds the sweep skips even though discovery serves them. Not a kind table
516
+ * creeping back in: each entry is a kind that CANNOT attribute to a declared
517
+ * owner — an Event names its subject via `involvedObject`, never
518
+ * `ownerReferences` — and churns by the thousands in any active namespace,
519
+ * so listing it can only cost and never attach.
520
+ */
521
+ function isSweepNoise(info) {
522
+ return info.kind === "Event" && (info.group === "" || info.group === "events.k8s.io");
523
+ }
524
+ async function addRuntimeChildren(client, resources, unobserved, owned, declared = []) {
525
+ const declaredByUid = new Map();
526
+ for (const [entityName, meta] of Object.entries(resources)) {
527
+ if (meta.physicalId)
528
+ declaredByUid.set(meta.physicalId, entityName);
529
+ }
530
+ // GitOps attribution (#1549): Argo and Flux stamp their managed objects with
531
+ // labels, not ownerReferences, so the chain walk never reaches a declared
532
+ // entity for them. The label channel resolves against the DECLARED CRs by
533
+ // (type, namespace, name) — exact for Flux (its labels carry both halves) —
534
+ // and by unique name for Argo (its instance label carries no namespace, and
535
+ // it doubles as a generic Helm label, so anything ambiguous resolves to
536
+ // nothing: the module's standing conservatism).
537
+ const gitops = gitopsIndex(declared);
538
+ const namespaces = new Set();
539
+ let clusterScopedDeclared = false;
540
+ for (const meta of Object.values(resources)) {
541
+ const namespace = meta.attributes?.namespace;
542
+ if (namespace)
543
+ namespaces.add(namespace);
544
+ else
545
+ clusterScopedDeclared = true;
546
+ }
547
+ // The namespaces a declared GitOps CR TARGETS are declared literals too
548
+ // (Argo `spec.destination.namespace`, Flux `spec.targetNamespace`) — the
549
+ // controller puts the workloads there, so the scan must look there, and
550
+ // reading it off the declaration keeps this bounded by the estate, never a
551
+ // cluster-wide sweep.
552
+ for (const ns of gitops.targetNamespaces)
553
+ namespaces.add(ns);
554
+ if (namespaces.size === 0 && !clusterScopedDeclared)
555
+ return;
556
+ // Every kind the cluster serves, from its own discovery (#1517): one
557
+ // groupVersion per group, every listable kind, split by scope. Failing to
558
+ // enumerate is a hole the observation must admit — silently sweeping
559
+ // nothing would read as "no runtime children", which was never established.
560
+ let groupVersions;
561
+ try {
562
+ groupVersions = await client.preferredGroupVersions();
563
+ }
564
+ catch (err) {
565
+ const outcome = classifyApiFailure(err);
566
+ unobserved["runtime-sweep"] = {
567
+ reason: outcome.kind === "unobserved" ? outcome.reason : "read-failed",
568
+ detail: "cluster API discovery failed, so runtime children were not swept" +
569
+ (outcome.kind === "unobserved" ? `: ${outcome.detail}` : ""),
570
+ };
571
+ return;
572
+ }
573
+ const perGroup = await client.concurrently(groupVersions, async (gv) => {
574
+ try {
575
+ return await client.resources(gv);
576
+ }
577
+ catch (err) {
578
+ const outcome = classifyApiFailure(err);
579
+ if (outcome.kind === "unobserved") {
580
+ unobserved[`runtime-sweep:${gv}`] = {
581
+ reason: outcome.reason,
582
+ detail: `discovery for ${gv} failed, so its kinds were not swept: ${outcome.detail}`,
583
+ };
584
+ }
585
+ return [];
586
+ }
587
+ });
588
+ const namespacedKinds = [];
589
+ const clusterKinds = [];
590
+ for (const infos of perGroup) {
591
+ for (const info of infos) {
592
+ if (!info.verbs.includes("list") || isSweepNoise(info))
593
+ continue;
594
+ const swept = {
595
+ apiVersion: info.apiVersion,
596
+ kind: info.kind,
597
+ typeName: gvkToTypeName({ group: info.group, version: info.version, kind: info.kind }),
598
+ };
599
+ (info.namespaced ? namespacedKinds : clusterKinds).push(swept);
600
+ }
601
+ }
602
+ // A failed list is recorded once per kind, naming every scope it failed in,
603
+ // rather than once per (kind, namespace) — one RBAC rule denying a kind
604
+ // across N namespaces is one hole, not N.
605
+ const listFailures = new Map();
606
+ const sweep = async (swept, namespace) => {
607
+ let objects;
608
+ try {
609
+ objects = await client.list({ apiVersion: swept.apiVersion, kind: swept.kind }, namespace ? { namespace } : {});
610
+ }
611
+ catch (err) {
612
+ const outcome = classifyApiFailure(err);
613
+ // `absent` (a list 404: the kind vanished between discovery and now)
614
+ // proves no instance exists — nothing to record.
615
+ if (outcome.kind === "unobserved") {
616
+ const key = `runtime-sweep:${swept.apiVersion}/${swept.kind}`;
617
+ const scope = namespace ? `namespace ${namespace}` : "cluster scope";
618
+ const failure = listFailures.get(key);
619
+ if (failure)
620
+ failure.scopes.push(scope);
621
+ else
622
+ listFailures.set(key, { typeName: swept.typeName, reason: outcome.reason, scopes: [scope], detail: outcome.detail });
623
+ }
624
+ return;
625
+ }
626
+ await client.concurrently(objects, async (obj) => {
627
+ const uid = obj.metadata?.uid;
628
+ const name = obj.metadata?.name;
629
+ if (!uid || !name || declaredByUid.has(uid))
630
+ return; // declared directly, or unaddressable
631
+ // The kind is part of the identity: a Deployment, Service and Endpoints
632
+ // all named `app-a` in namespace `app-a` are three objects, and a
633
+ // kind-less key reported only whichever kind swept first (#1643).
634
+ const key = namespace ? `${swept.kind}/${namespace}/${name}` : `cluster:${swept.kind}/${name}`;
635
+ if (resources[key])
636
+ return; // re-swept object, not a new report
637
+ let ownerChain = await resolveK8sOwnerChain(obj, { declaredByUid, reader: client, namespace });
638
+ // The label channel (#1549) — only when the chain itself said nothing:
639
+ // a verdict the walk DID reach is never overridden.
640
+ if (ownerChain.root !== "declared") {
641
+ const entity = gitopsOwner(obj.metadata?.labels, gitops);
642
+ if (entity)
643
+ ownerChain = { root: "declared", entity };
644
+ }
645
+ // `--owned`: withhold an object that is neither a runtime child of a
646
+ // declared entity nor carrying chant's own marker — the same rule the
647
+ // declared-entity read above applies, extended to the undeclared axis
648
+ // this scan introduces.
649
+ const marker = hasOwnershipMarker(obj.metadata?.labels, LABEL_OWNERSHIP_KEYS);
650
+ if (owned && ownerChain.root !== "declared" && !marker)
651
+ return;
652
+ // An undeclared object with NO owner chain at all is not a runtime
653
+ // child — reporting every loose object the cluster serves would turn
654
+ // the sweep into an inventory. Pods keep their pre-#1517 reporting
655
+ // (their chain verdicts, including foreign, were already part of
656
+ // #1077's contract); every other swept kind only reports when the
657
+ // chain reaches a declared entity.
658
+ if (swept.kind !== "Pod" && ownerChain.root !== "declared")
659
+ return;
660
+ resources[key] = {
661
+ type: swept.typeName,
662
+ physicalId: uid,
663
+ status: statusFromObject(obj),
664
+ lastUpdated: obj.metadata?.creationTimestamp,
665
+ ownership: classifyOwnership(obj.metadata?.labels, LABEL_OWNERSHIP_KEYS),
666
+ // Marker identity (#1222), same rule as the declared-entity read above.
667
+ marker: readOwnership(obj.metadata?.labels, LABEL_OWNERSHIP_KEYS),
668
+ ownerChain,
669
+ attributes: pruneUndefined({
670
+ namespace,
671
+ labels: obj.metadata?.labels,
672
+ resourceVersion: obj.metadata?.resourceVersion,
673
+ conditions: unhappyConditions(obj),
674
+ // The same one spec field the declared read forwards (behold#298):
675
+ // a swept HelmRepository (Flux's own bootstrap manages them through
676
+ // a Kustomization) must not read differently from a declared one.
677
+ type: helmRepositoryTypeAttribute(swept, obj),
678
+ }),
679
+ };
680
+ });
681
+ };
682
+ await client.concurrently([...namespaces], async (namespace) => {
683
+ for (const swept of namespacedKinds)
684
+ await sweep(swept, namespace);
685
+ });
686
+ // Cluster-scoped kinds are swept once (their lists are cluster-wide by
687
+ // nature), and only when a declared cluster-scoped entity exists for a
688
+ // chain to reach — a cluster-scoped object's owners are cluster-scoped.
689
+ if (clusterScopedDeclared) {
690
+ await client.concurrently(clusterKinds, async (swept) => sweep(swept, undefined));
691
+ }
692
+ for (const [key, failure] of listFailures) {
693
+ unobserved[key] = {
694
+ type: failure.typeName,
695
+ reason: failure.reason,
696
+ detail: `the runtime-children sweep could not list this kind in ${failure.scopes.join(", ")}: ${failure.detail}`,
697
+ };
698
+ }
699
+ }