@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,217 @@
1
+ import { describe, test, expect } from "bun:test";
2
+ import { parseCRD, parseCRDSpec } from "./parser";
3
+
4
+ describe("parseCRD", () => {
5
+ test("parses valid CRD YAML", () => {
6
+ const crd = `
7
+ apiVersion: apiextensions.k8s.io/v1
8
+ kind: CustomResourceDefinition
9
+ metadata:
10
+ name: certificates.cert-manager.io
11
+ spec:
12
+ group: cert-manager.io
13
+ names:
14
+ kind: Certificate
15
+ plural: certificates
16
+ scope: Namespaced
17
+ versions:
18
+ - name: v1
19
+ served: true
20
+ storage: true
21
+ schema:
22
+ openAPIV3Schema:
23
+ type: object
24
+ properties:
25
+ spec:
26
+ type: object
27
+ properties:
28
+ secretName:
29
+ type: string
30
+ issuerRef:
31
+ type: object
32
+ properties:
33
+ name:
34
+ type: string
35
+ kind:
36
+ type: string
37
+ `;
38
+ const results = parseCRD(crd);
39
+ expect(results.length).toBe(1);
40
+ expect(results[0].resource.typeName).toBe("K8s::CertManager::Certificate");
41
+ expect(results[0].gvk.group).toBe("cert-manager.io");
42
+ expect(results[0].gvk.version).toBe("v1");
43
+ expect(results[0].gvk.kind).toBe("Certificate");
44
+ });
45
+
46
+ test("parses multi-doc CRD bundle", () => {
47
+ const bundle = `
48
+ apiVersion: apiextensions.k8s.io/v1
49
+ kind: CustomResourceDefinition
50
+ metadata:
51
+ name: certificates.cert-manager.io
52
+ spec:
53
+ group: cert-manager.io
54
+ names:
55
+ kind: Certificate
56
+ plural: certificates
57
+ scope: Namespaced
58
+ versions:
59
+ - name: v1
60
+ served: true
61
+ storage: true
62
+ schema:
63
+ openAPIV3Schema:
64
+ type: object
65
+ ---
66
+ apiVersion: apiextensions.k8s.io/v1
67
+ kind: CustomResourceDefinition
68
+ metadata:
69
+ name: issuers.cert-manager.io
70
+ spec:
71
+ group: cert-manager.io
72
+ names:
73
+ kind: Issuer
74
+ plural: issuers
75
+ scope: Namespaced
76
+ versions:
77
+ - name: v1
78
+ served: true
79
+ storage: true
80
+ schema:
81
+ openAPIV3Schema:
82
+ type: object
83
+ `;
84
+ const results = parseCRD(bundle);
85
+ expect(results.length).toBe(2);
86
+ });
87
+
88
+ test("skips non-CRD documents", () => {
89
+ const mixed = `
90
+ apiVersion: v1
91
+ kind: ConfigMap
92
+ metadata:
93
+ name: something
94
+ data:
95
+ key: value
96
+ ---
97
+ apiVersion: apiextensions.k8s.io/v1
98
+ kind: CustomResourceDefinition
99
+ metadata:
100
+ name: foos.example.com
101
+ spec:
102
+ group: example.com
103
+ names:
104
+ kind: Foo
105
+ plural: foos
106
+ scope: Namespaced
107
+ versions:
108
+ - name: v1
109
+ served: true
110
+ storage: true
111
+ schema:
112
+ openAPIV3Schema:
113
+ type: object
114
+ `;
115
+ const results = parseCRD(mixed);
116
+ expect(results.length).toBe(1);
117
+ expect(results[0].resource.typeName).toBe("K8s::Example::Foo");
118
+ });
119
+
120
+ test("handles CRD without schema (empty properties)", () => {
121
+ const crd = `
122
+ apiVersion: apiextensions.k8s.io/v1
123
+ kind: CustomResourceDefinition
124
+ metadata:
125
+ name: bars.example.com
126
+ spec:
127
+ group: example.com
128
+ names:
129
+ kind: Bar
130
+ plural: bars
131
+ scope: Namespaced
132
+ versions:
133
+ - name: v1
134
+ served: true
135
+ storage: true
136
+ `;
137
+ const results = parseCRD(crd);
138
+ expect(results.length).toBe(1);
139
+ expect(results[0].resource.properties).toEqual([]);
140
+ });
141
+
142
+ test("type name follows K8s::{GroupNs}::{Kind} convention", () => {
143
+ const crd = `
144
+ apiVersion: apiextensions.k8s.io/v1
145
+ kind: CustomResourceDefinition
146
+ metadata:
147
+ name: widgets.my-company.io
148
+ spec:
149
+ group: my-company.io
150
+ names:
151
+ kind: Widget
152
+ plural: widgets
153
+ scope: Namespaced
154
+ versions:
155
+ - name: v1alpha1
156
+ served: true
157
+ storage: true
158
+ schema:
159
+ openAPIV3Schema:
160
+ type: object
161
+ `;
162
+ const results = parseCRD(crd);
163
+ expect(results[0].resource.typeName).toMatch(/^K8s::\w+::\w+$/);
164
+ });
165
+ });
166
+
167
+ describe("parseCRDSpec", () => {
168
+ test("extracts properties from openAPIV3Schema", () => {
169
+ const spec = {
170
+ group: "example.com",
171
+ names: { kind: "Foo", plural: "foos" },
172
+ scope: "Namespaced" as const,
173
+ versions: [
174
+ {
175
+ name: "v1",
176
+ served: true,
177
+ storage: true,
178
+ schema: {
179
+ openAPIV3Schema: {
180
+ type: "object",
181
+ properties: {
182
+ spec: {
183
+ type: "object",
184
+ properties: {
185
+ count: { type: "integer" },
186
+ name: { type: "string" },
187
+ },
188
+ },
189
+ metadata: { type: "object" },
190
+ },
191
+ },
192
+ },
193
+ },
194
+ ],
195
+ };
196
+
197
+ const results = parseCRDSpec(spec);
198
+ expect(results.length).toBe(1);
199
+ const props = results[0].resource.properties;
200
+ expect(props.some((p) => p.name === "spec")).toBe(true);
201
+ expect(props.some((p) => p.name === "metadata")).toBe(true);
202
+ });
203
+
204
+ test("normalizeGroupName converts cert-manager.io to CertManager", () => {
205
+ const spec = {
206
+ group: "cert-manager.io",
207
+ names: { kind: "Certificate", plural: "certificates" },
208
+ scope: "Namespaced" as const,
209
+ versions: [
210
+ { name: "v1", served: true, storage: true },
211
+ ],
212
+ };
213
+
214
+ const results = parseCRDSpec(spec);
215
+ expect(results[0].resource.typeName).toBe("K8s::CertManager::Certificate");
216
+ });
217
+ });
@@ -0,0 +1,279 @@
1
+ /**
2
+ * CRD parser — converts CRD YAML into K8sParseResult entries.
3
+ *
4
+ * Parses the openAPIV3Schema from a CRD's versions to extract resource
5
+ * properties, building the same K8sParseResult structures used by the
6
+ * main K8s swagger parser. This enables CRD-based resources to integrate
7
+ * with the full codegen pipeline.
8
+ */
9
+
10
+ import type { K8sParseResult, ParsedProperty, ParsedPropertyType, GroupVersionKind } from "../spec/parse";
11
+ import type { CRDSpec } from "./types";
12
+ import type { PropertyConstraints } from "@intentius/chant/codegen/json-schema";
13
+ import { parseYAML } from "@intentius/chant/yaml";
14
+
15
+ /**
16
+ * Normalize a CRD group to a PascalCase namespace segment.
17
+ * "cert-manager.io" → "CertManager"
18
+ * "monitoring.coreos.com" → "Monitoring"
19
+ */
20
+ function normalizeGroupName(group: string): string {
21
+ // Take the first segment before the first dot
22
+ const firstSegment = group.split(".")[0];
23
+ // Convert kebab-case to PascalCase
24
+ return firstSegment
25
+ .split("-")
26
+ .map((s) => s.charAt(0).toUpperCase() + s.slice(1))
27
+ .join("");
28
+ }
29
+
30
+ /**
31
+ * Parse a CRD YAML document string into K8sParseResult entries.
32
+ * Returns one result per served version in the CRD.
33
+ */
34
+ export function parseCRD(content: string): K8sParseResult[] {
35
+ const results: K8sParseResult[] = [];
36
+
37
+ // Support multi-document YAML for CRD bundles
38
+ const documents = content
39
+ .split(/^---\s*$/m)
40
+ .map((d) => d.trim())
41
+ .filter((d) => d.length > 0);
42
+
43
+ for (const docStr of documents) {
44
+ const doc = parseYAML(docStr) as Record<string, unknown>;
45
+ if (!doc || doc.kind !== "CustomResourceDefinition") continue;
46
+
47
+ const spec = doc.spec as CRDSpec | undefined;
48
+ if (!spec?.group || !spec?.names?.kind || !spec?.versions) continue;
49
+
50
+ const crdResults = parseCRDSpec(spec);
51
+ results.push(...crdResults);
52
+ }
53
+
54
+ return results;
55
+ }
56
+
57
+ /**
58
+ * Parse a CRD spec into K8sParseResult entries.
59
+ * Extracts one result per served version with storage version preferred.
60
+ */
61
+ export function parseCRDSpec(spec: CRDSpec): K8sParseResult[] {
62
+ const results: K8sParseResult[] = [];
63
+ const groupNs = normalizeGroupName(spec.group);
64
+
65
+ // Find the storage version (the canonical version)
66
+ const storageVersion = spec.versions.find((v) => v.storage && v.served);
67
+ // Fall back to any served version
68
+ const targetVersion = storageVersion ?? spec.versions.find((v) => v.served);
69
+
70
+ if (!targetVersion) return results;
71
+
72
+ const typeName = `K8s::${groupNs}::${spec.names.kind}`;
73
+ const gvk: GroupVersionKind = {
74
+ group: spec.group,
75
+ version: targetVersion.name,
76
+ kind: spec.names.kind,
77
+ };
78
+
79
+ const schema = targetVersion.schema?.openAPIV3Schema as OpenAPISchema | undefined;
80
+ const properties = schema ? extractProperties(schema) : [];
81
+ const propertyTypes = schema ? extractPropertyTypes(schema, typeName) : [];
82
+
83
+ results.push({
84
+ resource: {
85
+ typeName,
86
+ description: `Custom resource: ${spec.names.kind} (${spec.group})`,
87
+ properties,
88
+ attributes: [
89
+ { name: "name", tsType: "string" },
90
+ { name: "namespace", tsType: "string" },
91
+ { name: "uid", tsType: "string" },
92
+ ],
93
+ deprecatedProperties: [],
94
+ },
95
+ propertyTypes,
96
+ enums: [],
97
+ gvk,
98
+ });
99
+
100
+ return results;
101
+ }
102
+
103
+ // ── OpenAPI schema types ────────────────────────────────────────────
104
+
105
+ interface OpenAPISchema {
106
+ type?: string;
107
+ description?: string;
108
+ properties?: Record<string, OpenAPISchema>;
109
+ required?: string[];
110
+ items?: OpenAPISchema;
111
+ additionalProperties?: boolean | OpenAPISchema;
112
+ enum?: string[];
113
+ format?: string;
114
+ minimum?: number;
115
+ maximum?: number;
116
+ minLength?: number;
117
+ maxLength?: number;
118
+ pattern?: string;
119
+ default?: unknown;
120
+ "x-kubernetes-preserve-unknown-fields"?: boolean;
121
+ "x-kubernetes-int-or-string"?: boolean;
122
+ }
123
+
124
+ // ── Property extraction ─────────────────────────────────────────────
125
+
126
+ /**
127
+ * Extract top-level properties from a CRD's openAPIV3Schema.
128
+ * Focuses on the "spec" sub-object and metadata, skipping status.
129
+ */
130
+ function extractProperties(schema: OpenAPISchema): ParsedProperty[] {
131
+ const result: ParsedProperty[] = [];
132
+ const topProps = schema.properties ?? {};
133
+ const topRequired = new Set<string>(schema.required ?? []);
134
+
135
+ // Skip apiVersion, kind, status — same pattern as core parser
136
+ const skipProps = new Set(["apiVersion", "kind", "status"]);
137
+
138
+ for (const [name, prop] of Object.entries(topProps)) {
139
+ if (skipProps.has(name)) continue;
140
+
141
+ result.push({
142
+ name,
143
+ tsType: resolveSchemaType(prop),
144
+ required: topRequired.has(name),
145
+ description: prop.description,
146
+ enum: prop.enum,
147
+ constraints: extractConstraints(prop),
148
+ });
149
+ }
150
+
151
+ return result;
152
+ }
153
+
154
+ /**
155
+ * Extract nested object types as ParsedPropertyType entries.
156
+ * Walks the spec's properties looking for inline object definitions.
157
+ */
158
+ function extractPropertyTypes(schema: OpenAPISchema, parentTypeName: string): ParsedPropertyType[] {
159
+ const results: ParsedPropertyType[] = [];
160
+ const specSchema = schema.properties?.spec;
161
+ if (!specSchema?.properties) return results;
162
+
163
+ for (const [name, prop] of Object.entries(specSchema.properties)) {
164
+ // Extract inline object definitions as property types
165
+ if (prop.type === "object" && prop.properties) {
166
+ const ptName = `${parentTypeName}::${pascalCase(name)}`;
167
+ const requiredSet = new Set<string>(prop.required ?? []);
168
+
169
+ results.push({
170
+ name: ptName,
171
+ defType: name,
172
+ properties: Object.entries(prop.properties).map(([pName, pSchema]) => ({
173
+ name: pName,
174
+ tsType: resolveSchemaType(pSchema),
175
+ required: requiredSet.has(pName),
176
+ description: pSchema.description,
177
+ enum: pSchema.enum,
178
+ constraints: extractConstraints(pSchema),
179
+ })),
180
+ });
181
+ }
182
+
183
+ // Array of objects
184
+ if (prop.type === "array" && prop.items?.type === "object" && prop.items.properties) {
185
+ const itemSchema = prop.items;
186
+ const itemProps = itemSchema.properties!;
187
+ const ptName = `${parentTypeName}::${pascalCase(singularize(name))}`;
188
+ const requiredSet = new Set<string>(itemSchema.required ?? []);
189
+
190
+ results.push({
191
+ name: ptName,
192
+ defType: name,
193
+ properties: Object.entries(itemProps).map(([pName, pSchema]) => ({
194
+ name: pName,
195
+ tsType: resolveSchemaType(pSchema),
196
+ required: requiredSet.has(pName),
197
+ description: pSchema.description,
198
+ enum: pSchema.enum,
199
+ constraints: extractConstraints(pSchema),
200
+ })),
201
+ });
202
+ }
203
+ }
204
+
205
+ return results;
206
+ }
207
+
208
+ /**
209
+ * Resolve an OpenAPI schema node to a TypeScript type string.
210
+ */
211
+ function resolveSchemaType(schema: OpenAPISchema): string {
212
+ if (!schema) return "any";
213
+
214
+ if (schema["x-kubernetes-int-or-string"]) return "string | number";
215
+ if (schema["x-kubernetes-preserve-unknown-fields"]) return "Record<string, any>";
216
+
217
+ if (schema.enum && schema.enum.length > 0) {
218
+ return schema.enum.map((v) => JSON.stringify(v)).join(" | ");
219
+ }
220
+
221
+ switch (schema.type) {
222
+ case "string":
223
+ return "string";
224
+ case "integer":
225
+ case "number":
226
+ return "number";
227
+ case "boolean":
228
+ return "boolean";
229
+ case "array":
230
+ if (schema.items) {
231
+ const itemType = resolveSchemaType(schema.items);
232
+ if (itemType.includes(" | ")) return `(${itemType})[]`;
233
+ return `${itemType}[]`;
234
+ }
235
+ return "any[]";
236
+ case "object":
237
+ if (schema.additionalProperties && typeof schema.additionalProperties === "object") {
238
+ const valueType = resolveSchemaType(schema.additionalProperties);
239
+ return `Record<string, ${valueType}>`;
240
+ }
241
+ if (schema.properties) return "Record<string, any>"; // will be a property type
242
+ return "Record<string, any>";
243
+ default:
244
+ return "any";
245
+ }
246
+ }
247
+
248
+ /**
249
+ * Extract property constraints from an OpenAPI schema node.
250
+ */
251
+ function extractConstraints(schema: OpenAPISchema): PropertyConstraints {
252
+ const constraints: PropertyConstraints = {};
253
+ if (schema.minimum !== undefined) constraints.minimum = schema.minimum;
254
+ if (schema.maximum !== undefined) constraints.maximum = schema.maximum;
255
+ if (schema.minLength !== undefined) constraints.minLength = schema.minLength;
256
+ if (schema.maxLength !== undefined) constraints.maxLength = schema.maxLength;
257
+ if (schema.pattern !== undefined) constraints.pattern = schema.pattern;
258
+ return constraints;
259
+ }
260
+
261
+ /**
262
+ * Convert a string to PascalCase.
263
+ */
264
+ function pascalCase(str: string): string {
265
+ return str
266
+ .split(/[-_.]/)
267
+ .map((s) => s.charAt(0).toUpperCase() + s.slice(1))
268
+ .join("");
269
+ }
270
+
271
+ /**
272
+ * Naive singularize — removes trailing "s" for property type naming.
273
+ */
274
+ function singularize(str: string): string {
275
+ if (str.endsWith("ies")) return str.slice(0, -3) + "y";
276
+ if (str.endsWith("ses")) return str.slice(0, -2);
277
+ if (str.endsWith("s") && !str.endsWith("ss")) return str.slice(0, -1);
278
+ return str;
279
+ }
@@ -0,0 +1,54 @@
1
+ /**
2
+ * CRD (Custom Resource Definition) framework types.
3
+ *
4
+ * Defines the data structures used to load, parse, and process
5
+ * Kubernetes CRDs for code generation and lexicon extension.
6
+ */
7
+
8
+ /**
9
+ * Source from which to load CRD definitions.
10
+ */
11
+ export interface CRDSource {
12
+ /** How to fetch the CRD */
13
+ type: "file" | "url" | "cluster";
14
+ /** File path for type="file" */
15
+ path?: string;
16
+ /** URL for type="url" */
17
+ url?: string;
18
+ /** Kubectl context for type="cluster" */
19
+ context?: string;
20
+ /** Namespace to scope the CRD lookup for type="cluster" */
21
+ namespace?: string;
22
+ }
23
+
24
+ /**
25
+ * Parsed representation of a CRD's spec section.
26
+ */
27
+ export interface CRDSpec {
28
+ /** API group (e.g. "cert-manager.io") */
29
+ group: string;
30
+ /** Name variants for the CRD */
31
+ names: {
32
+ kind: string;
33
+ plural: string;
34
+ singular?: string;
35
+ shortNames?: string[];
36
+ };
37
+ /** API versions served by this CRD */
38
+ versions: Array<{
39
+ name: string;
40
+ served: boolean;
41
+ storage: boolean;
42
+ schema?: Record<string, unknown>;
43
+ }>;
44
+ }
45
+
46
+ /**
47
+ * Full CRD document as parsed from YAML.
48
+ */
49
+ export interface CRDDocument {
50
+ apiVersion: string;
51
+ kind: "CustomResourceDefinition";
52
+ metadata: { name: string; [key: string]: unknown };
53
+ spec: CRDSpec;
54
+ }
@@ -0,0 +1,111 @@
1
+ import { describe, test, expect } from "bun:test";
2
+ import {
3
+ defaultLabels,
4
+ defaultAnnotations,
5
+ isDefaultLabels,
6
+ isDefaultAnnotations,
7
+ DEFAULT_LABELS_MARKER,
8
+ DEFAULT_ANNOTATIONS_MARKER,
9
+ } from "./default-labels";
10
+ import { DECLARABLE_MARKER } from "@intentius/chant/declarable";
11
+
12
+ describe("defaultLabels", () => {
13
+ test("returns object with correct markers", () => {
14
+ const dl = defaultLabels({ env: "prod" });
15
+ expect(dl[DEFAULT_LABELS_MARKER]).toBe(true);
16
+ expect(dl[DECLARABLE_MARKER]).toBe(true);
17
+ });
18
+
19
+ test("has lexicon k8s", () => {
20
+ const dl = defaultLabels({ env: "prod" });
21
+ expect(dl.lexicon).toBe("k8s");
22
+ });
23
+
24
+ test("has correct entityType", () => {
25
+ const dl = defaultLabels({ env: "prod" });
26
+ expect(dl.entityType).toBe("chant:k8s:defaultLabels");
27
+ });
28
+
29
+ test("labels are accessible", () => {
30
+ const dl = defaultLabels({ env: "prod", team: "backend" });
31
+ expect(dl.labels.env).toBe("prod");
32
+ expect(dl.labels.team).toBe("backend");
33
+ });
34
+
35
+ test("empty labels allowed", () => {
36
+ const dl = defaultLabels({});
37
+ expect(dl.labels).toEqual({});
38
+ });
39
+ });
40
+
41
+ describe("defaultAnnotations", () => {
42
+ test("returns object with correct markers", () => {
43
+ const da = defaultAnnotations({ note: "hello" });
44
+ expect(da[DEFAULT_ANNOTATIONS_MARKER]).toBe(true);
45
+ expect(da[DECLARABLE_MARKER]).toBe(true);
46
+ });
47
+
48
+ test("has lexicon k8s", () => {
49
+ const da = defaultAnnotations({ note: "hello" });
50
+ expect(da.lexicon).toBe("k8s");
51
+ });
52
+
53
+ test("has correct entityType", () => {
54
+ const da = defaultAnnotations({ note: "hello" });
55
+ expect(da.entityType).toBe("chant:k8s:defaultAnnotations");
56
+ });
57
+
58
+ test("annotations are accessible", () => {
59
+ const da = defaultAnnotations({ "kubernetes.io/description": "test" });
60
+ expect(da.annotations["kubernetes.io/description"]).toBe("test");
61
+ });
62
+
63
+ test("empty annotations allowed", () => {
64
+ const da = defaultAnnotations({});
65
+ expect(da.annotations).toEqual({});
66
+ });
67
+ });
68
+
69
+ describe("isDefaultLabels", () => {
70
+ test("returns true for defaultLabels result", () => {
71
+ expect(isDefaultLabels(defaultLabels({ env: "prod" }))).toBe(true);
72
+ });
73
+
74
+ test("returns false for null", () => {
75
+ expect(isDefaultLabels(null)).toBe(false);
76
+ });
77
+
78
+ test("returns false for undefined", () => {
79
+ expect(isDefaultLabels(undefined)).toBe(false);
80
+ });
81
+
82
+ test("returns false for regular objects", () => {
83
+ expect(isDefaultLabels({ labels: { env: "prod" } })).toBe(false);
84
+ });
85
+
86
+ test("returns false for defaultAnnotations", () => {
87
+ expect(isDefaultLabels(defaultAnnotations({ note: "hi" }))).toBe(false);
88
+ });
89
+ });
90
+
91
+ describe("isDefaultAnnotations", () => {
92
+ test("returns true for defaultAnnotations result", () => {
93
+ expect(isDefaultAnnotations(defaultAnnotations({ note: "hi" }))).toBe(true);
94
+ });
95
+
96
+ test("returns false for null", () => {
97
+ expect(isDefaultAnnotations(null)).toBe(false);
98
+ });
99
+
100
+ test("returns false for undefined", () => {
101
+ expect(isDefaultAnnotations(undefined)).toBe(false);
102
+ });
103
+
104
+ test("returns false for regular objects", () => {
105
+ expect(isDefaultAnnotations({ annotations: {} })).toBe(false);
106
+ });
107
+
108
+ test("returns false for defaultLabels", () => {
109
+ expect(isDefaultAnnotations(defaultLabels({ env: "prod" }))).toBe(false);
110
+ });
111
+ });