@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.
- package/dist/integrity.json +32 -0
- package/dist/manifest.json +8 -0
- package/dist/meta.json +1413 -0
- package/dist/rules/hardcoded-namespace.ts +56 -0
- package/dist/rules/k8s-helpers.ts +149 -0
- package/dist/rules/wk8005.ts +59 -0
- package/dist/rules/wk8006.ts +68 -0
- package/dist/rules/wk8041.ts +73 -0
- package/dist/rules/wk8042.ts +48 -0
- package/dist/rules/wk8101.ts +65 -0
- package/dist/rules/wk8102.ts +42 -0
- package/dist/rules/wk8103.ts +45 -0
- package/dist/rules/wk8104.ts +69 -0
- package/dist/rules/wk8105.ts +45 -0
- package/dist/rules/wk8201.ts +55 -0
- package/dist/rules/wk8202.ts +46 -0
- package/dist/rules/wk8203.ts +46 -0
- package/dist/rules/wk8204.ts +54 -0
- package/dist/rules/wk8205.ts +56 -0
- package/dist/rules/wk8207.ts +45 -0
- package/dist/rules/wk8208.ts +45 -0
- package/dist/rules/wk8209.ts +45 -0
- package/dist/rules/wk8301.ts +51 -0
- package/dist/rules/wk8302.ts +46 -0
- package/dist/rules/wk8303.ts +96 -0
- package/dist/skills/chant-k8s.md +433 -0
- package/dist/types/index.d.ts +2934 -0
- package/package.json +30 -0
- package/src/actions/actions.test.ts +83 -0
- package/src/actions/apps.ts +23 -0
- package/src/actions/batch.ts +9 -0
- package/src/actions/core.ts +62 -0
- package/src/actions/index.ts +50 -0
- package/src/actions/networking.ts +15 -0
- package/src/actions/rbac.ts +13 -0
- package/src/codegen/docs-cli.ts +3 -0
- package/src/codegen/docs.ts +1147 -0
- package/src/codegen/generate-cli.ts +41 -0
- package/src/codegen/generate-lexicon.ts +69 -0
- package/src/codegen/generate-typescript.ts +97 -0
- package/src/codegen/generate.ts +144 -0
- package/src/codegen/naming.test.ts +63 -0
- package/src/codegen/naming.ts +187 -0
- package/src/codegen/package.ts +56 -0
- package/src/codegen/patches.ts +108 -0
- package/src/codegen/snapshot.test.ts +95 -0
- package/src/codegen/typecheck.test.ts +24 -0
- package/src/codegen/typecheck.ts +4 -0
- package/src/codegen/versions.ts +43 -0
- package/src/composites/autoscaled-service.ts +236 -0
- package/src/composites/composites.test.ts +1109 -0
- package/src/composites/cron-workload.ts +167 -0
- package/src/composites/index.ts +14 -0
- package/src/composites/namespace-env.ts +163 -0
- package/src/composites/node-agent.ts +224 -0
- package/src/composites/stateful-app.ts +134 -0
- package/src/composites/web-app.ts +180 -0
- package/src/composites/worker-pool.ts +230 -0
- package/src/coverage.test.ts +27 -0
- package/src/coverage.ts +35 -0
- package/src/crd/loader.ts +112 -0
- package/src/crd/parser.test.ts +217 -0
- package/src/crd/parser.ts +279 -0
- package/src/crd/types.ts +54 -0
- package/src/default-labels.test.ts +111 -0
- package/src/default-labels.ts +122 -0
- package/src/generated/index.d.ts +2934 -0
- package/src/generated/index.ts +203 -0
- package/src/generated/lexicon-k8s.json +1413 -0
- package/src/generated/runtime.ts +4 -0
- package/src/import/generator.test.ts +121 -0
- package/src/import/generator.ts +285 -0
- package/src/import/parser.test.ts +156 -0
- package/src/import/parser.ts +204 -0
- package/src/import/roundtrip.test.ts +86 -0
- package/src/index.ts +38 -0
- package/src/lint/post-synth/k8s-helpers.test.ts +219 -0
- package/src/lint/post-synth/k8s-helpers.ts +149 -0
- package/src/lint/post-synth/post-synth.test.ts +969 -0
- package/src/lint/post-synth/wk8005.ts +59 -0
- package/src/lint/post-synth/wk8006.ts +68 -0
- package/src/lint/post-synth/wk8041.ts +73 -0
- package/src/lint/post-synth/wk8042.ts +48 -0
- package/src/lint/post-synth/wk8101.ts +65 -0
- package/src/lint/post-synth/wk8102.ts +42 -0
- package/src/lint/post-synth/wk8103.ts +45 -0
- package/src/lint/post-synth/wk8104.ts +69 -0
- package/src/lint/post-synth/wk8105.ts +45 -0
- package/src/lint/post-synth/wk8201.ts +55 -0
- package/src/lint/post-synth/wk8202.ts +46 -0
- package/src/lint/post-synth/wk8203.ts +46 -0
- package/src/lint/post-synth/wk8204.ts +54 -0
- package/src/lint/post-synth/wk8205.ts +56 -0
- package/src/lint/post-synth/wk8207.ts +45 -0
- package/src/lint/post-synth/wk8208.ts +45 -0
- package/src/lint/post-synth/wk8209.ts +45 -0
- package/src/lint/post-synth/wk8301.ts +51 -0
- package/src/lint/post-synth/wk8302.ts +46 -0
- package/src/lint/post-synth/wk8303.ts +96 -0
- package/src/lint/rules/hardcoded-namespace.ts +56 -0
- package/src/lint/rules/rules.test.ts +69 -0
- package/src/lsp/completions.test.ts +64 -0
- package/src/lsp/completions.ts +20 -0
- package/src/lsp/hover.test.ts +69 -0
- package/src/lsp/hover.ts +68 -0
- package/src/package-cli.ts +28 -0
- package/src/plugin.test.ts +209 -0
- package/src/plugin.ts +915 -0
- package/src/serializer.test.ts +275 -0
- package/src/serializer.ts +278 -0
- package/src/spec/fetch.test.ts +24 -0
- package/src/spec/fetch.ts +68 -0
- package/src/spec/parse.test.ts +102 -0
- package/src/spec/parse.ts +477 -0
- package/src/testdata/manifests/configmap.yaml +7 -0
- package/src/testdata/manifests/deployment.yaml +22 -0
- package/src/testdata/manifests/full-app.yaml +61 -0
- package/src/testdata/manifests/secret.yaml +7 -0
- package/src/testdata/manifests/service.yaml +15 -0
- package/src/validate-cli.ts +21 -0
- package/src/validate.test.ts +29 -0
- package/src/validate.ts +46 -0
- 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
|
+
}
|
package/src/crd/types.ts
ADDED
|
@@ -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
|
+
});
|