@intentius/chant-lexicon-k8s 0.124.31 → 0.124.33

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,181 @@
1
+ /**
2
+ * CRD loader — loads Custom Resource Definitions from various sources.
3
+ *
4
+ * Supports loading from local files, remote URLs, and (placeholder)
5
+ * live cluster introspection via kubectl.
6
+ */
7
+ import { existsSync, readFileSync } from "fs";
8
+ import { fetchWithRetry } from "@intentius/chant/codegen/fetch";
9
+ import { parseCRD } from "./parser.js";
10
+ import { chartYamlFiles, pullOciChart, readTarGz } from "./oci-chart.js";
11
+ /**
12
+ * Load CRDs from a source and return parsed K8sParseResult entries.
13
+ */
14
+ export async function loadCRDs(source) {
15
+ const content = await fetchCRDContent(source);
16
+ const parsed = parseCRD(content);
17
+ if (source.kinds && source.kinds.length > 0) {
18
+ const allow = new Set(source.kinds);
19
+ return parsed.filter((r) => allow.has(r.gvk.kind));
20
+ }
21
+ return parsed;
22
+ }
23
+ /**
24
+ * Load CRDs from multiple sources and merge results.
25
+ */
26
+ export async function loadMultipleCRDs(sources) {
27
+ const results = [];
28
+ for (const source of sources) {
29
+ const parsed = await loadCRDs(source);
30
+ results.push(...parsed);
31
+ }
32
+ return results;
33
+ }
34
+ /**
35
+ * Fetch raw CRD YAML content from a source.
36
+ */
37
+ export async function fetchCRDContent(source) {
38
+ switch (source.type) {
39
+ case "file":
40
+ return loadFromFile(source);
41
+ case "url":
42
+ return loadFromURL(source);
43
+ case "cluster":
44
+ return loadFromCluster(source);
45
+ case "helm":
46
+ return loadFromHelmChart(source);
47
+ default:
48
+ throw new Error(`Unsupported CRD source type: ${source.type}`);
49
+ }
50
+ }
51
+ /**
52
+ * Load CRD YAML from a local file.
53
+ */
54
+ async function loadFromFile(source) {
55
+ if (!source.path) {
56
+ throw new Error("CRD source type 'file' requires a 'path' property");
57
+ }
58
+ if (!existsSync(source.path)) {
59
+ throw new Error(`CRD file not found: ${source.path}`);
60
+ }
61
+ return readFileSync(source.path, "utf8");
62
+ }
63
+ /**
64
+ * Load CRD YAML from a remote URL.
65
+ */
66
+ async function loadFromURL(source) {
67
+ if (!source.url) {
68
+ throw new Error("CRD source type 'url' requires a 'url' property");
69
+ }
70
+ // fetchWithRetry retries transient failures and throws on a permanent
71
+ // status (or after exhausting retries); the returned response is `ok`.
72
+ const response = await fetchWithRetry(source.url);
73
+ return response.text();
74
+ }
75
+ /**
76
+ * Load CRDs out of a Helm chart, by pulling and unpacking it.
77
+ *
78
+ * Helm's own convention is that CRDs live in a chart's `crds/` directory and
79
+ * are applied before templates, so that is the default place to look. Every
80
+ * YAML document found there is concatenated into one multi-doc string, which
81
+ * the parser already handles, and the existing `kinds` allowlist then applies
82
+ * unchanged — a chart carrying more CRDs than a consumer wants is the same
83
+ * situation as Flux's install bundle.
84
+ *
85
+ * An `oci://` chart is pulled over the registry's HTTP API and unpacked in
86
+ * process, so generation does not depend on `helm` being on PATH. Before,
87
+ * a host without helm dropped the chart's kinds with only a warning and the
88
+ * generated surface changed with the machine. Only a classic `https://` chart
89
+ * repository still shells out to `helm pull`.
90
+ */
91
+ async function loadFromHelmChart(source) {
92
+ if (!source.chart) {
93
+ throw new Error("CRD source type 'helm' requires a 'chart' property");
94
+ }
95
+ if (!source.version) {
96
+ throw new Error(`CRD source type 'helm' requires a 'version' property (chart: ${source.chart}). ` +
97
+ "An unpinned chart makes generated output depend on when it was generated.");
98
+ }
99
+ const subdir = source.chartSubdir ?? "crds";
100
+ if (source.chart.startsWith("oci://")) {
101
+ let tgz;
102
+ try {
103
+ tgz = await pullOciChart(source.chart, source.version, source.digest);
104
+ }
105
+ catch (err) {
106
+ const msg = err instanceof Error ? err.message : String(err);
107
+ throw new Error(`could not pull chart ${source.chart} ${source.version}: ${msg}`);
108
+ }
109
+ const files = chartYamlFiles(readTarGz(tgz), subdir);
110
+ if (files.length === 0) {
111
+ throw new Error(`chart ${source.chart} ${source.version} has no CRD YAML in '${subdir}'. ` +
112
+ "Charts that template their CRDs instead of shipping them in crds/ need 'chartSubdir'.");
113
+ }
114
+ return files.map(([, text]) => text).join("\n---\n");
115
+ }
116
+ const { execFile } = await import("child_process");
117
+ const { promisify } = await import("util");
118
+ const { mkdtempSync, rmSync, readdirSync, readFileSync: read } = await import("fs");
119
+ const { join } = await import("path");
120
+ const { tmpdir } = await import("os");
121
+ const execFileAsync = promisify(execFile);
122
+ const workdir = mkdtempSync(join(tmpdir(), "chant-crd-helm-"));
123
+ try {
124
+ await execFileAsync("helm", [
125
+ "pull", source.chart,
126
+ "--version", source.version,
127
+ "--untar",
128
+ "--untardir", workdir,
129
+ ]).catch((err) => {
130
+ if (err.code === "ENOENT") {
131
+ throw new Error(`helm not found on PATH, needed to read CRDs from ${source.chart}. ` +
132
+ "Install helm, or vendor the CRDs and use a 'file' source.");
133
+ }
134
+ throw new Error(`helm pull failed for ${source.chart} ${source.version}: ${err.stderr?.trim() || err.message}`);
135
+ });
136
+ // --untar writes a single directory named after the chart, which need not
137
+ // match the last segment of the reference, so read it rather than guess.
138
+ const unpacked = readdirSync(workdir, { withFileTypes: true }).filter((e) => e.isDirectory());
139
+ if (unpacked.length !== 1) {
140
+ throw new Error(`expected one unpacked chart directory in ${workdir}, found ${unpacked.length}`);
141
+ }
142
+ const crdDir = join(workdir, unpacked[0].name, subdir);
143
+ let entries;
144
+ try {
145
+ entries = readdirSync(crdDir).filter((f) => f.endsWith(".yaml") || f.endsWith(".yml")).sort();
146
+ }
147
+ catch {
148
+ throw new Error(`chart ${source.chart} ${source.version} has no '${subdir}' directory. ` +
149
+ "Charts that template their CRDs instead of shipping them in crds/ need 'chartSubdir'.");
150
+ }
151
+ if (entries.length === 0) {
152
+ throw new Error(`chart ${source.chart} ${source.version} has no CRD YAML in '${subdir}'`);
153
+ }
154
+ return entries.map((f) => read(join(crdDir, f), "utf8")).join("\n---\n");
155
+ }
156
+ finally {
157
+ rmSync(workdir, { recursive: true, force: true });
158
+ }
159
+ }
160
+ /**
161
+ * Load CRDs from a live Kubernetes cluster via kubectl.
162
+ *
163
+ * This is a placeholder implementation. Full cluster introspection
164
+ * requires kubectl access and proper authentication.
165
+ */
166
+ async function loadFromCluster(source) {
167
+ const { execFile } = await import("child_process");
168
+ const { promisify } = await import("util");
169
+ const execFileAsync = promisify(execFile);
170
+ const args = ["get", "crds", "-o", "yaml"];
171
+ if (source.context)
172
+ args.push(`--context=${source.context}`);
173
+ if (source.namespace)
174
+ args.push(`--namespace=${source.namespace}`);
175
+ const { stdout, stderr } = await execFileAsync("kubectl", args).catch((err) => {
176
+ throw new Error(`kubectl failed: ${err.stderr?.trim() || err.message}. ` +
177
+ "Ensure kubectl is installed and configured with access to the target cluster.");
178
+ });
179
+ void stderr;
180
+ return stdout;
181
+ }
@@ -0,0 +1,144 @@
1
+ /**
2
+ * Pull a Helm chart out of an OCI registry over plain HTTP, and read its files.
3
+ *
4
+ * Generation used to shell out to `helm pull` for these. A machine without
5
+ * helm on PATH (a fresh CI runner, a container) then dropped the chart's kinds
6
+ * with only a warning, and the generated surface depended on which tools the
7
+ * host happened to have. The registry API is three requests, and a chart is a
8
+ * small gzipped tarball, so neither needs the binary.
9
+ */
10
+ import { createHash } from "crypto";
11
+ import { gunzipSync } from "zlib";
12
+ import { fetchWithRetry } from "@intentius/chant/codegen/fetch";
13
+ /** The layer media type Helm pushes a chart's content under. */
14
+ export const HELM_CHART_LAYER = "application/vnd.cncf.helm.chart.content.v1.tar+gzip";
15
+ const MANIFEST_ACCEPT = [
16
+ "application/vnd.oci.image.manifest.v1+json",
17
+ "application/vnd.docker.distribution.manifest.v2+json",
18
+ ].join(", ");
19
+ const defaultHttp = {
20
+ probe: (url, init) => fetch(url, init),
21
+ get: (url, init) => fetchWithRetry(url, undefined, undefined, init),
22
+ };
23
+ /** Split an `oci://host/repo/path` chart reference. */
24
+ export function parseOciRef(chart) {
25
+ const m = /^oci:\/\/([^/]+)\/(.+)$/.exec(chart);
26
+ if (!m)
27
+ throw new Error(`not an oci:// chart reference: ${chart}`);
28
+ return { host: m[1], repository: m[2].replace(/\/+$/, "") };
29
+ }
30
+ /**
31
+ * Get an anonymous pull token, following the registry's own challenge
32
+ * (`WWW-Authenticate: Bearer realm=...,service=...`). Returns undefined when
33
+ * the registry does not ask for one.
34
+ */
35
+ async function anonymousToken(ref, http) {
36
+ const probe = await http.probe(`https://${ref.host}/v2/`);
37
+ if (probe.status !== 401)
38
+ return undefined;
39
+ const challenge = probe.headers.get("www-authenticate") ?? "";
40
+ const params = Object.fromEntries([...challenge.matchAll(/(\w+)="([^"]*)"/g)].map((m) => [m[1], m[2]]));
41
+ if (!/^bearer/i.test(challenge) || !params.realm) {
42
+ throw new Error(`registry ${ref.host} asks for auth this loader does not speak: ${challenge || "(no challenge)"}`);
43
+ }
44
+ const url = new URL(params.realm);
45
+ if (params.service)
46
+ url.searchParams.set("service", params.service);
47
+ url.searchParams.set("scope", `repository:${ref.repository}:pull`);
48
+ const res = await http.get(url.toString());
49
+ const body = (await res.json());
50
+ const token = body.token ?? body.access_token;
51
+ if (!token)
52
+ throw new Error(`registry ${ref.host} returned no token for ${ref.repository}`);
53
+ return token;
54
+ }
55
+ /**
56
+ * Download a chart's content layer (the .tgz `helm pull` would fetch).
57
+ *
58
+ * @param digest When given ("sha256:..."), the layer must have this digest.
59
+ * Pinning it makes the source content-addressed: a re-pushed
60
+ * tag fails loudly instead of changing generated output.
61
+ */
62
+ export async function pullOciChart(chart, version, digest, http = defaultHttp) {
63
+ const ref = parseOciRef(chart);
64
+ const token = await anonymousToken(ref, http);
65
+ const auth = token ? { Authorization: `Bearer ${token}` } : {};
66
+ const base = `https://${ref.host}/v2/${ref.repository}`;
67
+ const manifestRes = await http.get(`${base}/manifests/${version}`, {
68
+ headers: { ...auth, Accept: MANIFEST_ACCEPT },
69
+ });
70
+ const manifest = (await manifestRes.json());
71
+ const layer = manifest.layers?.find((l) => l.mediaType === HELM_CHART_LAYER);
72
+ if (!layer) {
73
+ throw new Error(`${chart}:${version} has no layer of type ${HELM_CHART_LAYER}; is it a Helm chart?`);
74
+ }
75
+ if (digest && layer.digest !== digest) {
76
+ throw new Error(`${chart}:${version} chart layer is ${layer.digest}, but the source pins ${digest}. ` +
77
+ "The tag was re-pushed; review the new chart and update the pin.");
78
+ }
79
+ const blobRes = await http.get(`${base}/blobs/${layer.digest}`, { headers: auth });
80
+ const blob = Buffer.from(await blobRes.arrayBuffer());
81
+ const actual = `sha256:${createHash("sha256").update(blob).digest("hex")}`;
82
+ if (actual !== layer.digest) {
83
+ throw new Error(`${chart}:${version} chart layer downloaded as ${actual}, expected ${layer.digest}`);
84
+ }
85
+ return blob;
86
+ }
87
+ /**
88
+ * Read the regular files out of a gzipped tarball, as path to content.
89
+ * Handles ustar, plus the pax (`x`) and GNU (`L`) long-name records Helm's
90
+ * Go archiver may write.
91
+ */
92
+ export function readTarGz(tgz) {
93
+ const tar = gunzipSync(tgz);
94
+ const files = new Map();
95
+ let offset = 0;
96
+ let longName;
97
+ const field = (start, len) => {
98
+ const raw = tar.subarray(offset + start, offset + start + len);
99
+ const nul = raw.indexOf(0);
100
+ return raw.subarray(0, nul === -1 ? raw.length : nul).toString("utf8");
101
+ };
102
+ while (offset + 512 <= tar.length) {
103
+ // Two zero blocks end the archive; one is enough to stop reading.
104
+ if (tar.subarray(offset, offset + 512).every((b) => b === 0))
105
+ break;
106
+ const size = parseInt(field(124, 12).trim() || "0", 8);
107
+ const type = field(156, 1) || "0";
108
+ const prefix = field(345, 155);
109
+ const name = longName ?? (prefix ? `${prefix}/${field(0, 100)}` : field(0, 100));
110
+ const data = tar.subarray(offset + 512, offset + 512 + size);
111
+ longName = undefined;
112
+ if (type === "x") {
113
+ const path = /(?:^|\n)\d+ path=([^\n]*)\n/.exec(data.toString("utf8"));
114
+ if (path)
115
+ longName = path[1];
116
+ }
117
+ else if (type === "L") {
118
+ longName = data.toString("utf8").replace(/\0+$/, "");
119
+ }
120
+ else if (type === "0" || type === "\0") {
121
+ files.set(name, Buffer.from(data));
122
+ }
123
+ offset += 512 + Math.ceil(size / 512) * 512;
124
+ }
125
+ return files;
126
+ }
127
+ /**
128
+ * The YAML files directly inside `<chart dir>/<subdir>/`, sorted by path so
129
+ * the concatenation does not depend on archive order.
130
+ */
131
+ export function chartYamlFiles(files, subdir) {
132
+ const dir = subdir.replace(/^\/+|\/+$/g, "");
133
+ return [...files.entries()]
134
+ .filter(([path]) => {
135
+ // Strip the single top-level chart directory, whatever it is named.
136
+ const rest = path.split("/").slice(1).join("/");
137
+ if (!rest.startsWith(`${dir}/`))
138
+ return false;
139
+ const leaf = rest.slice(dir.length + 1);
140
+ return !leaf.includes("/") && (leaf.endsWith(".yaml") || leaf.endsWith(".yml"));
141
+ })
142
+ .sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))
143
+ .map(([path, buf]) => [path, buf.toString("utf8")]);
144
+ }