@intentius/chant-lexicon-k8s 0.0.12

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 (123) hide show
  1. package/dist/integrity.json +32 -0
  2. package/dist/manifest.json +8 -0
  3. package/dist/meta.json +1413 -0
  4. package/dist/rules/hardcoded-namespace.ts +56 -0
  5. package/dist/rules/k8s-helpers.ts +149 -0
  6. package/dist/rules/wk8005.ts +59 -0
  7. package/dist/rules/wk8006.ts +68 -0
  8. package/dist/rules/wk8041.ts +73 -0
  9. package/dist/rules/wk8042.ts +48 -0
  10. package/dist/rules/wk8101.ts +65 -0
  11. package/dist/rules/wk8102.ts +42 -0
  12. package/dist/rules/wk8103.ts +45 -0
  13. package/dist/rules/wk8104.ts +69 -0
  14. package/dist/rules/wk8105.ts +45 -0
  15. package/dist/rules/wk8201.ts +55 -0
  16. package/dist/rules/wk8202.ts +46 -0
  17. package/dist/rules/wk8203.ts +46 -0
  18. package/dist/rules/wk8204.ts +54 -0
  19. package/dist/rules/wk8205.ts +56 -0
  20. package/dist/rules/wk8207.ts +45 -0
  21. package/dist/rules/wk8208.ts +45 -0
  22. package/dist/rules/wk8209.ts +45 -0
  23. package/dist/rules/wk8301.ts +51 -0
  24. package/dist/rules/wk8302.ts +46 -0
  25. package/dist/rules/wk8303.ts +96 -0
  26. package/dist/skills/chant-k8s.md +433 -0
  27. package/dist/types/index.d.ts +2934 -0
  28. package/package.json +30 -0
  29. package/src/actions/actions.test.ts +83 -0
  30. package/src/actions/apps.ts +23 -0
  31. package/src/actions/batch.ts +9 -0
  32. package/src/actions/core.ts +62 -0
  33. package/src/actions/index.ts +50 -0
  34. package/src/actions/networking.ts +15 -0
  35. package/src/actions/rbac.ts +13 -0
  36. package/src/codegen/docs-cli.ts +3 -0
  37. package/src/codegen/docs.ts +1147 -0
  38. package/src/codegen/generate-cli.ts +41 -0
  39. package/src/codegen/generate-lexicon.ts +69 -0
  40. package/src/codegen/generate-typescript.ts +97 -0
  41. package/src/codegen/generate.ts +144 -0
  42. package/src/codegen/naming.test.ts +63 -0
  43. package/src/codegen/naming.ts +187 -0
  44. package/src/codegen/package.ts +56 -0
  45. package/src/codegen/patches.ts +108 -0
  46. package/src/codegen/snapshot.test.ts +95 -0
  47. package/src/codegen/typecheck.test.ts +24 -0
  48. package/src/codegen/typecheck.ts +4 -0
  49. package/src/codegen/versions.ts +43 -0
  50. package/src/composites/autoscaled-service.ts +236 -0
  51. package/src/composites/composites.test.ts +1109 -0
  52. package/src/composites/cron-workload.ts +167 -0
  53. package/src/composites/index.ts +14 -0
  54. package/src/composites/namespace-env.ts +163 -0
  55. package/src/composites/node-agent.ts +224 -0
  56. package/src/composites/stateful-app.ts +134 -0
  57. package/src/composites/web-app.ts +180 -0
  58. package/src/composites/worker-pool.ts +230 -0
  59. package/src/coverage.test.ts +27 -0
  60. package/src/coverage.ts +35 -0
  61. package/src/crd/loader.ts +112 -0
  62. package/src/crd/parser.test.ts +217 -0
  63. package/src/crd/parser.ts +279 -0
  64. package/src/crd/types.ts +54 -0
  65. package/src/default-labels.test.ts +111 -0
  66. package/src/default-labels.ts +122 -0
  67. package/src/generated/index.d.ts +2934 -0
  68. package/src/generated/index.ts +203 -0
  69. package/src/generated/lexicon-k8s.json +1413 -0
  70. package/src/generated/runtime.ts +4 -0
  71. package/src/import/generator.test.ts +121 -0
  72. package/src/import/generator.ts +285 -0
  73. package/src/import/parser.test.ts +156 -0
  74. package/src/import/parser.ts +204 -0
  75. package/src/import/roundtrip.test.ts +86 -0
  76. package/src/index.ts +38 -0
  77. package/src/lint/post-synth/k8s-helpers.test.ts +219 -0
  78. package/src/lint/post-synth/k8s-helpers.ts +149 -0
  79. package/src/lint/post-synth/post-synth.test.ts +969 -0
  80. package/src/lint/post-synth/wk8005.ts +59 -0
  81. package/src/lint/post-synth/wk8006.ts +68 -0
  82. package/src/lint/post-synth/wk8041.ts +73 -0
  83. package/src/lint/post-synth/wk8042.ts +48 -0
  84. package/src/lint/post-synth/wk8101.ts +65 -0
  85. package/src/lint/post-synth/wk8102.ts +42 -0
  86. package/src/lint/post-synth/wk8103.ts +45 -0
  87. package/src/lint/post-synth/wk8104.ts +69 -0
  88. package/src/lint/post-synth/wk8105.ts +45 -0
  89. package/src/lint/post-synth/wk8201.ts +55 -0
  90. package/src/lint/post-synth/wk8202.ts +46 -0
  91. package/src/lint/post-synth/wk8203.ts +46 -0
  92. package/src/lint/post-synth/wk8204.ts +54 -0
  93. package/src/lint/post-synth/wk8205.ts +56 -0
  94. package/src/lint/post-synth/wk8207.ts +45 -0
  95. package/src/lint/post-synth/wk8208.ts +45 -0
  96. package/src/lint/post-synth/wk8209.ts +45 -0
  97. package/src/lint/post-synth/wk8301.ts +51 -0
  98. package/src/lint/post-synth/wk8302.ts +46 -0
  99. package/src/lint/post-synth/wk8303.ts +96 -0
  100. package/src/lint/rules/hardcoded-namespace.ts +56 -0
  101. package/src/lint/rules/rules.test.ts +69 -0
  102. package/src/lsp/completions.test.ts +64 -0
  103. package/src/lsp/completions.ts +20 -0
  104. package/src/lsp/hover.test.ts +69 -0
  105. package/src/lsp/hover.ts +68 -0
  106. package/src/package-cli.ts +28 -0
  107. package/src/plugin.test.ts +209 -0
  108. package/src/plugin.ts +915 -0
  109. package/src/serializer.test.ts +275 -0
  110. package/src/serializer.ts +278 -0
  111. package/src/spec/fetch.test.ts +24 -0
  112. package/src/spec/fetch.ts +68 -0
  113. package/src/spec/parse.test.ts +102 -0
  114. package/src/spec/parse.ts +477 -0
  115. package/src/testdata/manifests/configmap.yaml +7 -0
  116. package/src/testdata/manifests/deployment.yaml +22 -0
  117. package/src/testdata/manifests/full-app.yaml +61 -0
  118. package/src/testdata/manifests/secret.yaml +7 -0
  119. package/src/testdata/manifests/service.yaml +15 -0
  120. package/src/validate-cli.ts +21 -0
  121. package/src/validate.test.ts +29 -0
  122. package/src/validate.ts +46 -0
  123. package/src/variables.ts +36 -0
@@ -0,0 +1,180 @@
1
+ /**
2
+ * WebApp composite — Deployment + Service + optional Ingress.
3
+ *
4
+ * A higher-level construct for deploying stateless web applications
5
+ * with common defaults (health probes, resource limits, labels).
6
+ */
7
+
8
+ export interface WebAppProps {
9
+ /** Application name — used in metadata and labels. */
10
+ name: string;
11
+ /** Container image (e.g., "nginx:1.25"). */
12
+ image: string;
13
+ /** Container port (default: 80). */
14
+ port?: number;
15
+ /** Number of replicas (default: 2). */
16
+ replicas?: number;
17
+ /** Ingress hostname — if set, creates an Ingress resource. */
18
+ ingressHost?: string;
19
+ /** Ingress TLS secret name — if set, enables TLS on the Ingress. */
20
+ ingressTlsSecret?: string;
21
+ /** Additional labels to apply to all resources. */
22
+ labels?: Record<string, string>;
23
+ /** CPU limit (e.g., "500m"). */
24
+ cpuLimit?: string;
25
+ /** Memory limit (e.g., "256Mi"). */
26
+ memoryLimit?: string;
27
+ /** CPU request (e.g., "100m"). */
28
+ cpuRequest?: string;
29
+ /** Memory request (e.g., "128Mi"). */
30
+ memoryRequest?: string;
31
+ /** Namespace for all resources. */
32
+ namespace?: string;
33
+ /** Environment variables for the container. */
34
+ env?: Array<{ name: string; value: string }>;
35
+ }
36
+
37
+ export interface WebAppResult {
38
+ deployment: Record<string, unknown>;
39
+ service: Record<string, unknown>;
40
+ ingress?: Record<string, unknown>;
41
+ }
42
+
43
+ /**
44
+ * Create a WebApp composite — returns declarable instances for
45
+ * a Deployment, Service, and optional Ingress.
46
+ *
47
+ * @example
48
+ * ```ts
49
+ * import { WebApp } from "@intentius/chant-lexicon-k8s";
50
+ *
51
+ * const { deployment, service, ingress } = WebApp({
52
+ * name: "my-app",
53
+ * image: "my-app:1.0",
54
+ * port: 8080,
55
+ * replicas: 3,
56
+ * ingressHost: "my-app.example.com",
57
+ * });
58
+ *
59
+ * export { deployment, service, ingress };
60
+ * ```
61
+ */
62
+ export function WebApp(props: WebAppProps): WebAppResult {
63
+ const {
64
+ name,
65
+ image,
66
+ port = 80,
67
+ replicas = 2,
68
+ labels: extraLabels = {},
69
+ cpuLimit = "500m",
70
+ memoryLimit = "256Mi",
71
+ cpuRequest = "100m",
72
+ memoryRequest = "128Mi",
73
+ namespace,
74
+ env,
75
+ } = props;
76
+
77
+ const commonLabels: Record<string, string> = {
78
+ "app.kubernetes.io/name": name,
79
+ "app.kubernetes.io/managed-by": "chant",
80
+ ...extraLabels,
81
+ };
82
+
83
+ // We return plain objects that users pass to constructors.
84
+ // The actual resource instantiation happens in user code with the generated classes.
85
+ const deploymentProps: Record<string, unknown> = {
86
+ metadata: {
87
+ name,
88
+ ...(namespace && { namespace }),
89
+ labels: { ...commonLabels, "app.kubernetes.io/component": "server" },
90
+ },
91
+ spec: {
92
+ replicas,
93
+ selector: { matchLabels: { "app.kubernetes.io/name": name } },
94
+ template: {
95
+ metadata: { labels: { "app.kubernetes.io/name": name, ...extraLabels } },
96
+ spec: {
97
+ containers: [
98
+ {
99
+ name,
100
+ image,
101
+ ports: [{ containerPort: port, name: "http" }],
102
+ resources: {
103
+ limits: { cpu: cpuLimit, memory: memoryLimit },
104
+ requests: { cpu: cpuRequest, memory: memoryRequest },
105
+ },
106
+ livenessProbe: {
107
+ httpGet: { path: "/", port },
108
+ initialDelaySeconds: 10,
109
+ periodSeconds: 10,
110
+ },
111
+ readinessProbe: {
112
+ httpGet: { path: "/", port },
113
+ initialDelaySeconds: 5,
114
+ periodSeconds: 5,
115
+ },
116
+ ...(env && { env }),
117
+ },
118
+ ],
119
+ },
120
+ },
121
+ },
122
+ };
123
+
124
+ const serviceProps: Record<string, unknown> = {
125
+ metadata: {
126
+ name,
127
+ ...(namespace && { namespace }),
128
+ labels: { ...commonLabels, "app.kubernetes.io/component": "server" },
129
+ },
130
+ spec: {
131
+ selector: { "app.kubernetes.io/name": name },
132
+ ports: [{ port: 80, targetPort: port, protocol: "TCP", name: "http" }],
133
+ type: "ClusterIP",
134
+ },
135
+ };
136
+
137
+ const result: WebAppResult = {
138
+ deployment: deploymentProps,
139
+ service: serviceProps,
140
+ };
141
+
142
+ if (props.ingressHost) {
143
+ const ingressProps: Record<string, unknown> = {
144
+ metadata: {
145
+ name,
146
+ ...(namespace && { namespace }),
147
+ labels: { ...commonLabels, "app.kubernetes.io/component": "ingress" },
148
+ },
149
+ spec: {
150
+ rules: [
151
+ {
152
+ host: props.ingressHost,
153
+ http: {
154
+ paths: [
155
+ {
156
+ path: "/",
157
+ pathType: "Prefix",
158
+ backend: {
159
+ service: { name, port: { number: 80 } },
160
+ },
161
+ },
162
+ ],
163
+ },
164
+ },
165
+ ],
166
+ ...(props.ingressTlsSecret && {
167
+ tls: [
168
+ {
169
+ hosts: [props.ingressHost],
170
+ secretName: props.ingressTlsSecret,
171
+ },
172
+ ],
173
+ }),
174
+ },
175
+ };
176
+ result.ingress = ingressProps;
177
+ }
178
+
179
+ return result;
180
+ }
@@ -0,0 +1,230 @@
1
+ /**
2
+ * WorkerPool composite — Deployment + ServiceAccount + Role + RoleBinding + optional ConfigMap + optional HPA.
3
+ *
4
+ * A higher-level construct for background queue workers (Sidekiq, Celery, Bull)
5
+ * that need RBAC for secrets/configmaps and optional autoscaling, but no Service.
6
+ */
7
+
8
+ export interface WorkerPoolProps {
9
+ /** Worker name — used in metadata and labels. */
10
+ name: string;
11
+ /** Container image. */
12
+ image: string;
13
+ /** Command to run in the container. */
14
+ command?: string[];
15
+ /** Arguments to the command. */
16
+ args?: string[];
17
+ /** Number of replicas (default: 1, ignored if autoscaling). */
18
+ replicas?: number;
19
+ /** Config data — creates a ConfigMap and injects via envFrom. */
20
+ config?: Record<string, string>;
21
+ /** RBAC rules for the service account. */
22
+ rbacRules?: Array<{
23
+ apiGroups: string[];
24
+ resources: string[];
25
+ verbs: string[];
26
+ }>;
27
+ /** Optional autoscaling — creates an HPA when provided. */
28
+ autoscaling?: {
29
+ minReplicas: number;
30
+ maxReplicas: number;
31
+ targetCPUPercent?: number;
32
+ };
33
+ /** CPU request (default: "100m"). */
34
+ cpuRequest?: string;
35
+ /** Memory request (default: "128Mi"). */
36
+ memoryRequest?: string;
37
+ /** CPU limit (default: "500m"). */
38
+ cpuLimit?: string;
39
+ /** Memory limit (default: "256Mi"). */
40
+ memoryLimit?: string;
41
+ /** Additional labels to apply to all resources. */
42
+ labels?: Record<string, string>;
43
+ /** Namespace for all resources. */
44
+ namespace?: string;
45
+ /** Environment variables for the container. */
46
+ env?: Array<{ name: string; value: string }>;
47
+ }
48
+
49
+ export interface WorkerPoolResult {
50
+ deployment: Record<string, unknown>;
51
+ serviceAccount?: Record<string, unknown>;
52
+ role?: Record<string, unknown>;
53
+ roleBinding?: Record<string, unknown>;
54
+ configMap?: Record<string, unknown>;
55
+ hpa?: Record<string, unknown>;
56
+ }
57
+
58
+ /**
59
+ * Create a WorkerPool composite — returns prop objects for
60
+ * a Deployment, ServiceAccount, Role, RoleBinding, optional ConfigMap, and optional HPA.
61
+ *
62
+ * @example
63
+ * ```ts
64
+ * import { WorkerPool } from "@intentius/chant-lexicon-k8s";
65
+ *
66
+ * const { deployment, serviceAccount, role, roleBinding } = WorkerPool({
67
+ * name: "email-worker",
68
+ * image: "worker:1.0",
69
+ * command: ["bundle", "exec", "sidekiq"],
70
+ * config: { REDIS_URL: "redis://redis:6379" },
71
+ * });
72
+ * ```
73
+ */
74
+ export function WorkerPool(props: WorkerPoolProps): WorkerPoolResult {
75
+ const {
76
+ name,
77
+ image,
78
+ command,
79
+ args,
80
+ replicas = 1,
81
+ config,
82
+ rbacRules,
83
+ autoscaling,
84
+ cpuRequest = "100m",
85
+ memoryRequest = "128Mi",
86
+ cpuLimit = "500m",
87
+ memoryLimit = "256Mi",
88
+ labels: extraLabels = {},
89
+ namespace,
90
+ env,
91
+ } = props;
92
+
93
+ const saName = `${name}-sa`;
94
+ const roleName = `${name}-role`;
95
+ const bindingName = `${name}-binding`;
96
+ const configMapName = `${name}-config`;
97
+
98
+ const commonLabels: Record<string, string> = {
99
+ "app.kubernetes.io/name": name,
100
+ "app.kubernetes.io/managed-by": "chant",
101
+ ...extraLabels,
102
+ };
103
+
104
+ // undefined → default RBAC rules; explicit [] → no RBAC resources
105
+ const createRbac = rbacRules === undefined || rbacRules.length > 0;
106
+ const effectiveRbacRules = rbacRules === undefined
107
+ ? [{ apiGroups: [""], resources: ["secrets", "configmaps"], verbs: ["get"] }]
108
+ : rbacRules;
109
+
110
+ const effectiveReplicas = autoscaling ? autoscaling.minReplicas : replicas;
111
+
112
+ const container: Record<string, unknown> = {
113
+ name,
114
+ image,
115
+ ...(command && { command }),
116
+ ...(args && { args }),
117
+ resources: {
118
+ limits: { cpu: cpuLimit, memory: memoryLimit },
119
+ requests: { cpu: cpuRequest, memory: memoryRequest },
120
+ },
121
+ ...(env && { env }),
122
+ ...(config && {
123
+ envFrom: [{ configMapRef: { name: configMapName } }],
124
+ }),
125
+ };
126
+
127
+ const deploymentProps: Record<string, unknown> = {
128
+ metadata: {
129
+ name,
130
+ ...(namespace && { namespace }),
131
+ labels: { ...commonLabels, "app.kubernetes.io/component": "worker" },
132
+ },
133
+ spec: {
134
+ replicas: effectiveReplicas,
135
+ selector: { matchLabels: { "app.kubernetes.io/name": name } },
136
+ template: {
137
+ metadata: { labels: { "app.kubernetes.io/name": name, ...extraLabels } },
138
+ spec: {
139
+ ...(createRbac && { serviceAccountName: saName }),
140
+ containers: [container],
141
+ },
142
+ },
143
+ },
144
+ };
145
+
146
+ const result: WorkerPoolResult = {
147
+ deployment: deploymentProps,
148
+ };
149
+
150
+ if (createRbac) {
151
+ result.serviceAccount = {
152
+ metadata: {
153
+ name: saName,
154
+ ...(namespace && { namespace }),
155
+ labels: { ...commonLabels, "app.kubernetes.io/component": "worker" },
156
+ },
157
+ };
158
+
159
+ result.role = {
160
+ metadata: {
161
+ name: roleName,
162
+ ...(namespace && { namespace }),
163
+ labels: { ...commonLabels, "app.kubernetes.io/component": "rbac" },
164
+ },
165
+ rules: effectiveRbacRules,
166
+ };
167
+
168
+ result.roleBinding = {
169
+ metadata: {
170
+ name: bindingName,
171
+ ...(namespace && { namespace }),
172
+ labels: { ...commonLabels, "app.kubernetes.io/component": "rbac" },
173
+ },
174
+ roleRef: {
175
+ apiGroup: "rbac.authorization.k8s.io",
176
+ kind: "Role",
177
+ name: roleName,
178
+ },
179
+ subjects: [
180
+ {
181
+ kind: "ServiceAccount",
182
+ name: saName,
183
+ ...(namespace && { namespace }),
184
+ },
185
+ ],
186
+ };
187
+ }
188
+
189
+ if (config) {
190
+ result.configMap = {
191
+ metadata: {
192
+ name: configMapName,
193
+ ...(namespace && { namespace }),
194
+ labels: { ...commonLabels, "app.kubernetes.io/component": "config" },
195
+ },
196
+ data: config,
197
+ };
198
+ }
199
+
200
+ if (autoscaling) {
201
+ const targetCPUPercent = autoscaling.targetCPUPercent ?? 70;
202
+ result.hpa = {
203
+ metadata: {
204
+ name,
205
+ ...(namespace && { namespace }),
206
+ labels: { ...commonLabels, "app.kubernetes.io/component": "autoscaler" },
207
+ },
208
+ spec: {
209
+ scaleTargetRef: {
210
+ apiVersion: "apps/v1",
211
+ kind: "Deployment",
212
+ name,
213
+ },
214
+ minReplicas: autoscaling.minReplicas,
215
+ maxReplicas: autoscaling.maxReplicas,
216
+ metrics: [
217
+ {
218
+ type: "Resource",
219
+ resource: {
220
+ name: "cpu",
221
+ target: { type: "Utilization", averageUtilization: targetCPUPercent },
222
+ },
223
+ },
224
+ ],
225
+ },
226
+ };
227
+ }
228
+
229
+ return result;
230
+ }
@@ -0,0 +1,27 @@
1
+ import { describe, test, expect } from "bun:test";
2
+ import { existsSync } from "fs";
3
+ import { join, dirname } from "path";
4
+ import { fileURLToPath } from "url";
5
+
6
+ const pkgDir = dirname(dirname(fileURLToPath(import.meta.url)));
7
+ const generatedDir = join(pkgDir, "src", "generated");
8
+ const hasGenerated = existsSync(join(generatedDir, "lexicon-k8s.json"));
9
+
10
+ describe("coverage", () => {
11
+ test.skipIf(!hasGenerated)("analyzeK8sCoverage function exists", async () => {
12
+ const { analyzeK8sCoverage } = await import("./coverage");
13
+ expect(typeof analyzeK8sCoverage).toBe("function");
14
+ });
15
+
16
+ test("handles missing generated files gracefully", async () => {
17
+ const { analyzeK8sCoverage } = await import("./coverage");
18
+ // If generated files don't exist, this should throw/exit rather than crash
19
+ if (!hasGenerated) {
20
+ try {
21
+ await analyzeK8sCoverage();
22
+ } catch {
23
+ // Expected — no generated files
24
+ }
25
+ }
26
+ });
27
+ });
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Coverage analysis for the Kubernetes lexicon.
3
+ *
4
+ * Analyzes generated resources for coverage of property constraints,
5
+ * lifecycle attributes, and other dimensions.
6
+ */
7
+
8
+ import { readFileSync } from "fs";
9
+ import { join, dirname } from "path";
10
+ import { fileURLToPath } from "url";
11
+ import { computeCoverage, overallPct, type CoverageReport } from "@intentius/chant/codegen/coverage";
12
+
13
+ const pkgDir = dirname(dirname(fileURLToPath(import.meta.url)));
14
+
15
+ /**
16
+ * Analyze coverage of the K8s lexicon.
17
+ */
18
+ export async function analyzeK8sCoverage(opts?: {
19
+ verbose?: boolean;
20
+ minOverall?: number;
21
+ }): Promise<void> {
22
+ const generatedDir = join(pkgDir, "src", "generated");
23
+ const lexiconJSON = readFileSync(join(generatedDir, "lexicon-k8s.json"), "utf-8");
24
+ const report = computeCoverage(lexiconJSON);
25
+
26
+ const pct = overallPct(report);
27
+ console.error(`K8s lexicon coverage: ${pct.toFixed(1)}%`);
28
+ console.error(` Resources: ${report.resourceCount}`);
29
+ console.error(` Property constraints: ${report.propertyPct.toFixed(1)}%`);
30
+
31
+ if (opts?.minOverall && pct < opts.minOverall) {
32
+ console.error(`Coverage ${pct.toFixed(1)}% is below minimum ${opts.minOverall}%`);
33
+ process.exit(1);
34
+ }
35
+ }
@@ -0,0 +1,112 @@
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
+
8
+ import type { CRDSource, CRDSpec } from "./types";
9
+ import type { K8sParseResult } from "../spec/parse";
10
+ import { parseCRD } from "./parser";
11
+
12
+ /**
13
+ * Load CRDs from a source and return parsed K8sParseResult entries.
14
+ */
15
+ export async function loadCRDs(source: CRDSource): Promise<K8sParseResult[]> {
16
+ const content = await fetchCRDContent(source);
17
+ return parseCRD(content);
18
+ }
19
+
20
+ /**
21
+ * Load CRDs from multiple sources and merge results.
22
+ */
23
+ export async function loadMultipleCRDs(sources: CRDSource[]): Promise<K8sParseResult[]> {
24
+ const results: K8sParseResult[] = [];
25
+ for (const source of sources) {
26
+ const parsed = await loadCRDs(source);
27
+ results.push(...parsed);
28
+ }
29
+ return results;
30
+ }
31
+
32
+ /**
33
+ * Fetch raw CRD YAML content from a source.
34
+ */
35
+ async function fetchCRDContent(source: CRDSource): Promise<string> {
36
+ switch (source.type) {
37
+ case "file":
38
+ return loadFromFile(source);
39
+ case "url":
40
+ return loadFromURL(source);
41
+ case "cluster":
42
+ return loadFromCluster(source);
43
+ default:
44
+ throw new Error(`Unsupported CRD source type: ${(source as CRDSource).type}`);
45
+ }
46
+ }
47
+
48
+ /**
49
+ * Load CRD YAML from a local file.
50
+ */
51
+ async function loadFromFile(source: CRDSource): Promise<string> {
52
+ if (!source.path) {
53
+ throw new Error("CRD source type 'file' requires a 'path' property");
54
+ }
55
+
56
+ const file = Bun.file(source.path);
57
+ const exists = await file.exists();
58
+ if (!exists) {
59
+ throw new Error(`CRD file not found: ${source.path}`);
60
+ }
61
+
62
+ return file.text();
63
+ }
64
+
65
+ /**
66
+ * Load CRD YAML from a remote URL.
67
+ */
68
+ async function loadFromURL(source: CRDSource): Promise<string> {
69
+ if (!source.url) {
70
+ throw new Error("CRD source type 'url' requires a 'url' property");
71
+ }
72
+
73
+ const response = await fetch(source.url);
74
+ if (!response.ok) {
75
+ throw new Error(`Failed to fetch CRD from ${source.url}: ${response.status} ${response.statusText}`);
76
+ }
77
+
78
+ return response.text();
79
+ }
80
+
81
+ /**
82
+ * Load CRDs from a live Kubernetes cluster via kubectl.
83
+ *
84
+ * This is a placeholder implementation. Full cluster introspection
85
+ * requires kubectl access and proper authentication.
86
+ */
87
+ async function loadFromCluster(source: CRDSource): Promise<string> {
88
+ const contextArg = source.context ? `--context=${source.context}` : "";
89
+ const nsArg = source.namespace ? `--namespace=${source.namespace}` : "";
90
+
91
+ const args = ["kubectl", "get", "crds", "-o", "yaml"];
92
+ if (contextArg) args.push(contextArg);
93
+ if (nsArg) args.push(nsArg);
94
+
95
+ const proc = Bun.spawn(args, {
96
+ stdout: "pipe",
97
+ stderr: "pipe",
98
+ });
99
+
100
+ const stdout = await new Response(proc.stdout).text();
101
+ const stderr = await new Response(proc.stderr).text();
102
+ const exitCode = await proc.exited;
103
+
104
+ if (exitCode !== 0) {
105
+ throw new Error(
106
+ `kubectl failed (exit ${exitCode}): ${stderr.trim() || "unknown error"}. ` +
107
+ "Ensure kubectl is installed and configured with access to the target cluster.",
108
+ );
109
+ }
110
+
111
+ return stdout;
112
+ }