@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,204 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Kubernetes YAML parser for `chant import`.
|
|
3
|
+
*
|
|
4
|
+
* Parses Kubernetes manifests (single or multi-document YAML) into the core
|
|
5
|
+
* TemplateIR format, mapping K8s resources to the K8s::{Group}::{Kind} type
|
|
6
|
+
* convention.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { TemplateParser, TemplateIR, ResourceIR } from "@intentius/chant/import/parser";
|
|
10
|
+
import { parseYAML } from "@intentius/chant/yaml";
|
|
11
|
+
|
|
12
|
+
// ── GVK to type name mapping ───────────────────────────────────────
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Well-known kind+apiVersion pairs mapped to K8s type names.
|
|
16
|
+
* Key format: "apiVersion/kind" (core group uses just "kind").
|
|
17
|
+
*/
|
|
18
|
+
const GVK_TYPE_MAP: Record<string, string> = {
|
|
19
|
+
// Core (v1)
|
|
20
|
+
"v1/Pod": "K8s::Core::Pod",
|
|
21
|
+
"v1/Service": "K8s::Core::Service",
|
|
22
|
+
"v1/ConfigMap": "K8s::Core::ConfigMap",
|
|
23
|
+
"v1/Secret": "K8s::Core::Secret",
|
|
24
|
+
"v1/Namespace": "K8s::Core::Namespace",
|
|
25
|
+
"v1/ServiceAccount": "K8s::Core::ServiceAccount",
|
|
26
|
+
"v1/PersistentVolume": "K8s::Core::PersistentVolume",
|
|
27
|
+
"v1/PersistentVolumeClaim": "K8s::Core::PersistentVolumeClaim",
|
|
28
|
+
"v1/Endpoints": "K8s::Core::Endpoints",
|
|
29
|
+
"v1/LimitRange": "K8s::Core::LimitRange",
|
|
30
|
+
"v1/ResourceQuota": "K8s::Core::ResourceQuota",
|
|
31
|
+
|
|
32
|
+
// Apps (apps/v1)
|
|
33
|
+
"apps/v1/Deployment": "K8s::Apps::Deployment",
|
|
34
|
+
"apps/v1/StatefulSet": "K8s::Apps::StatefulSet",
|
|
35
|
+
"apps/v1/DaemonSet": "K8s::Apps::DaemonSet",
|
|
36
|
+
"apps/v1/ReplicaSet": "K8s::Apps::ReplicaSet",
|
|
37
|
+
|
|
38
|
+
// Batch (batch/v1)
|
|
39
|
+
"batch/v1/Job": "K8s::Batch::Job",
|
|
40
|
+
"batch/v1/CronJob": "K8s::Batch::CronJob",
|
|
41
|
+
|
|
42
|
+
// Networking (networking.k8s.io/v1)
|
|
43
|
+
"networking.k8s.io/v1/Ingress": "K8s::Networking::Ingress",
|
|
44
|
+
"networking.k8s.io/v1/NetworkPolicy": "K8s::Networking::NetworkPolicy",
|
|
45
|
+
"networking.k8s.io/v1/IngressClass": "K8s::Networking::IngressClass",
|
|
46
|
+
|
|
47
|
+
// RBAC (rbac.authorization.k8s.io/v1)
|
|
48
|
+
"rbac.authorization.k8s.io/v1/Role": "K8s::Rbac::Role",
|
|
49
|
+
"rbac.authorization.k8s.io/v1/ClusterRole": "K8s::Rbac::ClusterRole",
|
|
50
|
+
"rbac.authorization.k8s.io/v1/RoleBinding": "K8s::Rbac::RoleBinding",
|
|
51
|
+
"rbac.authorization.k8s.io/v1/ClusterRoleBinding": "K8s::Rbac::ClusterRoleBinding",
|
|
52
|
+
|
|
53
|
+
// Autoscaling (autoscaling/v2)
|
|
54
|
+
"autoscaling/v2/HorizontalPodAutoscaler": "K8s::Autoscaling::HorizontalPodAutoscaler",
|
|
55
|
+
"autoscaling/v1/HorizontalPodAutoscaler": "K8s::Autoscaling::HorizontalPodAutoscaler",
|
|
56
|
+
|
|
57
|
+
// Policy (policy/v1)
|
|
58
|
+
"policy/v1/PodDisruptionBudget": "K8s::Policy::PodDisruptionBudget",
|
|
59
|
+
|
|
60
|
+
// Storage (storage.k8s.io/v1)
|
|
61
|
+
"storage.k8s.io/v1/StorageClass": "K8s::Storage::StorageClass",
|
|
62
|
+
|
|
63
|
+
// Certificates (certificates.k8s.io/v1)
|
|
64
|
+
"certificates.k8s.io/v1/CertificateSigningRequest": "K8s::Certificates::CertificateSigningRequest",
|
|
65
|
+
|
|
66
|
+
// Coordination (coordination.k8s.io/v1)
|
|
67
|
+
"coordination.k8s.io/v1/Lease": "K8s::Coordination::Lease",
|
|
68
|
+
|
|
69
|
+
// Discovery (discovery.k8s.io/v1)
|
|
70
|
+
"discovery.k8s.io/v1/EndpointSlice": "K8s::Discovery::EndpointSlice",
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Resolve apiVersion + kind to a K8s type name.
|
|
75
|
+
* Falls back to constructing from the apiVersion group.
|
|
76
|
+
*/
|
|
77
|
+
function resolveTypeName(apiVersion: string, kind: string): string {
|
|
78
|
+
// Try exact match
|
|
79
|
+
const key = `${apiVersion}/${kind}`;
|
|
80
|
+
if (GVK_TYPE_MAP[key]) return GVK_TYPE_MAP[key];
|
|
81
|
+
|
|
82
|
+
// Core group uses just kind
|
|
83
|
+
if (apiVersion === "v1") {
|
|
84
|
+
const coreKey = `v1/${kind}`;
|
|
85
|
+
if (GVK_TYPE_MAP[coreKey]) return GVK_TYPE_MAP[coreKey];
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Construct from apiVersion group
|
|
89
|
+
const group = apiVersionToGroup(apiVersion);
|
|
90
|
+
return `K8s::${group}::${kind}`;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Extract PascalCase group name from an apiVersion string.
|
|
95
|
+
* "apps/v1" → "Apps"
|
|
96
|
+
* "v1" → "Core"
|
|
97
|
+
* "networking.k8s.io/v1" → "Networking"
|
|
98
|
+
* "rbac.authorization.k8s.io/v1" → "Rbac"
|
|
99
|
+
*/
|
|
100
|
+
function apiVersionToGroup(apiVersion: string): string {
|
|
101
|
+
const slashIdx = apiVersion.indexOf("/");
|
|
102
|
+
if (slashIdx === -1) return "Core"; // core group (v1)
|
|
103
|
+
|
|
104
|
+
const groupStr = apiVersion.slice(0, slashIdx);
|
|
105
|
+
const firstSegment = groupStr.split(".")[0];
|
|
106
|
+
if (firstSegment === "rbac") return "Rbac";
|
|
107
|
+
return firstSegment.charAt(0).toUpperCase() + firstSegment.slice(1);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Convert a K8s object name to a camelCase logical identifier.
|
|
112
|
+
* Combines kind (lowercased first char) with the object name (PascalCased).
|
|
113
|
+
*
|
|
114
|
+
* E.g., kind="Deployment", name="my-app" → "deploymentMyApp"
|
|
115
|
+
*/
|
|
116
|
+
function toLogicalId(kind: string, name: string | undefined): string {
|
|
117
|
+
const prefix = kind.charAt(0).toLowerCase() + kind.slice(1);
|
|
118
|
+
if (!name) return prefix;
|
|
119
|
+
|
|
120
|
+
const pascalName = name
|
|
121
|
+
.split(/[-_.]/)
|
|
122
|
+
.map((seg) => seg.charAt(0).toUpperCase() + seg.slice(1))
|
|
123
|
+
.join("");
|
|
124
|
+
return `${prefix}${pascalName}`;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// ── Parser ──────────────────────────────────────────────────────────
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Kubernetes YAML parser implementation.
|
|
131
|
+
*
|
|
132
|
+
* Supports multi-document YAML (split on `---`). Each document is expected
|
|
133
|
+
* to be a standard Kubernetes object with apiVersion, kind, and metadata.
|
|
134
|
+
*/
|
|
135
|
+
export class K8sParser implements TemplateParser {
|
|
136
|
+
parse(content: string): TemplateIR {
|
|
137
|
+
const resources: ResourceIR[] = [];
|
|
138
|
+
const namespaces = new Set<string>();
|
|
139
|
+
|
|
140
|
+
// Split on YAML document separator, filter blanks
|
|
141
|
+
const documents = content
|
|
142
|
+
.split(/^---\s*$/m)
|
|
143
|
+
.map((d) => d.trim())
|
|
144
|
+
.filter((d) => d.length > 0 && !/^[\s#]*$/.test(d.replace(/#[^\n]*/g, "")));
|
|
145
|
+
|
|
146
|
+
for (const docStr of documents) {
|
|
147
|
+
const doc = parseYAML(docStr);
|
|
148
|
+
if (!doc || typeof doc !== "object") continue;
|
|
149
|
+
|
|
150
|
+
const resource = this.parseDocument(doc as Record<string, unknown>);
|
|
151
|
+
if (resource) {
|
|
152
|
+
resources.push(resource);
|
|
153
|
+
|
|
154
|
+
// Track namespaces
|
|
155
|
+
const ns = (doc as Record<string, unknown>).metadata as Record<string, unknown> | undefined;
|
|
156
|
+
if (ns?.namespace && typeof ns.namespace === "string") {
|
|
157
|
+
namespaces.add(ns.namespace);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const metadata: Record<string, unknown> = {};
|
|
163
|
+
if (namespaces.size > 0) {
|
|
164
|
+
metadata.namespaces = [...namespaces];
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return {
|
|
168
|
+
resources,
|
|
169
|
+
parameters: [],
|
|
170
|
+
metadata: Object.keys(metadata).length > 0 ? metadata : undefined,
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
private parseDocument(doc: Record<string, unknown>): ResourceIR | null {
|
|
175
|
+
const apiVersion = doc.apiVersion as string | undefined;
|
|
176
|
+
const kind = doc.kind as string | undefined;
|
|
177
|
+
|
|
178
|
+
if (!apiVersion || !kind) return null;
|
|
179
|
+
|
|
180
|
+
const metadata = (doc.metadata ?? {}) as Record<string, unknown>;
|
|
181
|
+
const name = metadata.name as string | undefined;
|
|
182
|
+
const type = resolveTypeName(apiVersion, kind);
|
|
183
|
+
const logicalId = toLogicalId(kind, name);
|
|
184
|
+
|
|
185
|
+
// Extract the user-configurable properties (skip apiVersion, kind)
|
|
186
|
+
const properties: Record<string, unknown> = {};
|
|
187
|
+
for (const [key, value] of Object.entries(doc)) {
|
|
188
|
+
if (key === "apiVersion" || key === "kind") continue;
|
|
189
|
+
properties[key] = value;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return {
|
|
193
|
+
logicalId,
|
|
194
|
+
type,
|
|
195
|
+
properties,
|
|
196
|
+
metadata: {
|
|
197
|
+
originalName: name,
|
|
198
|
+
apiVersion,
|
|
199
|
+
kind,
|
|
200
|
+
...(metadata.namespace ? { namespace: metadata.namespace as string } : {}),
|
|
201
|
+
},
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { describe, test, expect } from "bun:test";
|
|
2
|
+
import { readFileSync } from "fs";
|
|
3
|
+
import { join, dirname } from "path";
|
|
4
|
+
import { fileURLToPath } from "url";
|
|
5
|
+
import { K8sParser } from "./parser";
|
|
6
|
+
import { K8sGenerator } from "./generator";
|
|
7
|
+
|
|
8
|
+
const testdataDir = join(
|
|
9
|
+
dirname(dirname(fileURLToPath(import.meta.url))),
|
|
10
|
+
"testdata",
|
|
11
|
+
"manifests",
|
|
12
|
+
);
|
|
13
|
+
|
|
14
|
+
const parser = new K8sParser();
|
|
15
|
+
const generator = new K8sGenerator();
|
|
16
|
+
|
|
17
|
+
describe("roundtrip: parse YAML → generate TypeScript", () => {
|
|
18
|
+
test("Deployment roundtrip", () => {
|
|
19
|
+
const yaml = readFileSync(join(testdataDir, "deployment.yaml"), "utf-8");
|
|
20
|
+
const ir = parser.parse(yaml);
|
|
21
|
+
const files = generator.generate(ir);
|
|
22
|
+
|
|
23
|
+
expect(files.length).toBe(1);
|
|
24
|
+
expect(files[0].content).toContain("new Deployment");
|
|
25
|
+
expect(files[0].content).toContain("nginx");
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test("multi-doc full-app roundtrip", () => {
|
|
29
|
+
const yaml = readFileSync(join(testdataDir, "full-app.yaml"), "utf-8");
|
|
30
|
+
const ir = parser.parse(yaml);
|
|
31
|
+
expect(ir.resources.length).toBe(4); // Deployment + Service + Ingress + ConfigMap
|
|
32
|
+
|
|
33
|
+
const files = generator.generate(ir);
|
|
34
|
+
expect(files[0].content).toContain("Deployment");
|
|
35
|
+
expect(files[0].content).toContain("Service");
|
|
36
|
+
expect(files[0].content).toContain("Ingress");
|
|
37
|
+
expect(files[0].content).toContain("ConfigMap");
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test("Service roundtrip preserves port info", () => {
|
|
41
|
+
const yaml = readFileSync(join(testdataDir, "service.yaml"), "utf-8");
|
|
42
|
+
const ir = parser.parse(yaml);
|
|
43
|
+
const files = generator.generate(ir);
|
|
44
|
+
|
|
45
|
+
expect(files[0].content).toContain("new Service");
|
|
46
|
+
expect(files[0].content).toContain("80");
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test("ConfigMap roundtrip preserves data", () => {
|
|
50
|
+
const yaml = readFileSync(join(testdataDir, "configmap.yaml"), "utf-8");
|
|
51
|
+
const ir = parser.parse(yaml);
|
|
52
|
+
const files = generator.generate(ir);
|
|
53
|
+
|
|
54
|
+
expect(files[0].content).toContain("ConfigMap");
|
|
55
|
+
expect(files[0].content).toContain("DATABASE_URL");
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test("inline YAML roundtrip", () => {
|
|
59
|
+
const yaml = `
|
|
60
|
+
apiVersion: apps/v1
|
|
61
|
+
kind: Deployment
|
|
62
|
+
metadata:
|
|
63
|
+
name: test-app
|
|
64
|
+
spec:
|
|
65
|
+
replicas: 1
|
|
66
|
+
selector:
|
|
67
|
+
matchLabels:
|
|
68
|
+
app: test
|
|
69
|
+
template:
|
|
70
|
+
metadata:
|
|
71
|
+
labels:
|
|
72
|
+
app: test
|
|
73
|
+
spec:
|
|
74
|
+
containers:
|
|
75
|
+
- name: test
|
|
76
|
+
image: test:1.0
|
|
77
|
+
ports:
|
|
78
|
+
- containerPort: 8080
|
|
79
|
+
`;
|
|
80
|
+
const ir = parser.parse(yaml);
|
|
81
|
+
const files = generator.generate(ir);
|
|
82
|
+
|
|
83
|
+
expect(files[0].content).toContain("new Deployment");
|
|
84
|
+
expect(files[0].content).toContain("export const");
|
|
85
|
+
});
|
|
86
|
+
});
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// Serializer
|
|
2
|
+
export { k8sSerializer } from "./serializer";
|
|
3
|
+
|
|
4
|
+
// Plugin
|
|
5
|
+
export { k8sPlugin } from "./plugin";
|
|
6
|
+
|
|
7
|
+
// Default labels & annotations
|
|
8
|
+
export { defaultLabels, defaultAnnotations, isDefaultLabels, isDefaultAnnotations } from "./default-labels";
|
|
9
|
+
export { DEFAULT_LABELS_MARKER, DEFAULT_ANNOTATIONS_MARKER } from "./default-labels";
|
|
10
|
+
|
|
11
|
+
// Variables / label constants
|
|
12
|
+
export { K8sLabels, K8sAnnotations } from "./variables";
|
|
13
|
+
|
|
14
|
+
// Generated entities — export everything from generated index
|
|
15
|
+
// After running `chant generate`, this re-exports all K8s resource classes
|
|
16
|
+
export * from "./generated/index";
|
|
17
|
+
|
|
18
|
+
// Composites
|
|
19
|
+
export { WebApp, StatefulApp, CronWorkload, AutoscaledService, WorkerPool, NamespaceEnv, NodeAgent } from "./composites/index";
|
|
20
|
+
export type { WebAppProps, WebAppResult, StatefulAppProps, StatefulAppResult, CronWorkloadProps, CronWorkloadResult, AutoscaledServiceProps, AutoscaledServiceResult, WorkerPoolProps, WorkerPoolResult, NamespaceEnvProps, NamespaceEnvResult, NodeAgentProps, NodeAgentResult } from "./composites/index";
|
|
21
|
+
|
|
22
|
+
// RBAC verb constants
|
|
23
|
+
export * from "./actions/index";
|
|
24
|
+
|
|
25
|
+
// Spec utilities (for tooling)
|
|
26
|
+
export { fetchK8sSchema, fetchSchemas, K8S_SCHEMA_VERSION } from "./spec/fetch";
|
|
27
|
+
export { parseK8sSwagger, k8sShortName, k8sServiceName, gvkToTypeName, gvkToApiVersion } from "./spec/parse";
|
|
28
|
+
export type { K8sParseResult, ParsedResource, ParsedProperty, ParsedPropertyType, ParsedEnum, GroupVersionKind } from "./spec/parse";
|
|
29
|
+
|
|
30
|
+
// Code generation pipeline
|
|
31
|
+
export { generate, writeGeneratedFiles } from "./codegen/generate";
|
|
32
|
+
export { packageLexicon } from "./codegen/package";
|
|
33
|
+
export type { PackageOptions, PackageResult } from "./codegen/package";
|
|
34
|
+
|
|
35
|
+
// CRD framework
|
|
36
|
+
export type { CRDSource, CRDSpec } from "./crd/types";
|
|
37
|
+
export { parseCRD, parseCRDSpec } from "./crd/parser";
|
|
38
|
+
export { loadCRDs, loadMultipleCRDs } from "./crd/loader";
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
import { describe, test, expect } from "bun:test";
|
|
2
|
+
import {
|
|
3
|
+
parseK8sManifests,
|
|
4
|
+
extractContainers,
|
|
5
|
+
extractPodSpec,
|
|
6
|
+
WORKLOAD_KINDS,
|
|
7
|
+
} from "./k8s-helpers";
|
|
8
|
+
|
|
9
|
+
describe("parseK8sManifests", () => {
|
|
10
|
+
test("splits multi-doc YAML", () => {
|
|
11
|
+
const yaml = `
|
|
12
|
+
apiVersion: v1
|
|
13
|
+
kind: Service
|
|
14
|
+
metadata:
|
|
15
|
+
name: svc
|
|
16
|
+
---
|
|
17
|
+
apiVersion: apps/v1
|
|
18
|
+
kind: Deployment
|
|
19
|
+
metadata:
|
|
20
|
+
name: deploy
|
|
21
|
+
`;
|
|
22
|
+
const manifests = parseK8sManifests(yaml);
|
|
23
|
+
expect(manifests.length).toBe(2);
|
|
24
|
+
expect(manifests[0].kind).toBe("Service");
|
|
25
|
+
expect(manifests[1].kind).toBe("Deployment");
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test("handles single document", () => {
|
|
29
|
+
const yaml = `
|
|
30
|
+
apiVersion: v1
|
|
31
|
+
kind: ConfigMap
|
|
32
|
+
metadata:
|
|
33
|
+
name: config
|
|
34
|
+
data:
|
|
35
|
+
key: value
|
|
36
|
+
`;
|
|
37
|
+
const manifests = parseK8sManifests(yaml);
|
|
38
|
+
expect(manifests.length).toBe(1);
|
|
39
|
+
expect(manifests[0].kind).toBe("ConfigMap");
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test("handles empty/invalid YAML gracefully", () => {
|
|
43
|
+
expect(parseK8sManifests("")).toEqual([]);
|
|
44
|
+
expect(parseK8sManifests("---")).toEqual([]);
|
|
45
|
+
// "---\n---" doesn't split on /\n---\n/ — the full string is parsed as empty object
|
|
46
|
+
expect(parseK8sManifests("---\n---\n")).toEqual([]);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test("skips blank documents between separators", () => {
|
|
50
|
+
const yaml = `
|
|
51
|
+
apiVersion: v1
|
|
52
|
+
kind: Pod
|
|
53
|
+
metadata:
|
|
54
|
+
name: p
|
|
55
|
+
---
|
|
56
|
+
|
|
57
|
+
---
|
|
58
|
+
apiVersion: v1
|
|
59
|
+
kind: Service
|
|
60
|
+
metadata:
|
|
61
|
+
name: s
|
|
62
|
+
`;
|
|
63
|
+
const manifests = parseK8sManifests(yaml);
|
|
64
|
+
expect(manifests.length).toBe(2);
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
describe("extractContainers", () => {
|
|
69
|
+
test("extracts from Deployment", () => {
|
|
70
|
+
const manifest = {
|
|
71
|
+
kind: "Deployment",
|
|
72
|
+
spec: {
|
|
73
|
+
template: {
|
|
74
|
+
spec: {
|
|
75
|
+
containers: [{ name: "app", image: "nginx" }],
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
const containers = extractContainers(manifest);
|
|
81
|
+
expect(containers.length).toBe(1);
|
|
82
|
+
expect(containers[0].name).toBe("app");
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test("extracts from Pod", () => {
|
|
86
|
+
const manifest = {
|
|
87
|
+
kind: "Pod",
|
|
88
|
+
spec: {
|
|
89
|
+
containers: [{ name: "app", image: "nginx" }],
|
|
90
|
+
},
|
|
91
|
+
};
|
|
92
|
+
const containers = extractContainers(manifest);
|
|
93
|
+
expect(containers.length).toBe(1);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
test("extracts from StatefulSet", () => {
|
|
97
|
+
const manifest = {
|
|
98
|
+
kind: "StatefulSet",
|
|
99
|
+
spec: {
|
|
100
|
+
template: {
|
|
101
|
+
spec: {
|
|
102
|
+
containers: [{ name: "db", image: "postgres" }],
|
|
103
|
+
},
|
|
104
|
+
},
|
|
105
|
+
},
|
|
106
|
+
};
|
|
107
|
+
const containers = extractContainers(manifest);
|
|
108
|
+
expect(containers.length).toBe(1);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
test("extracts from DaemonSet", () => {
|
|
112
|
+
const manifest = {
|
|
113
|
+
kind: "DaemonSet",
|
|
114
|
+
spec: {
|
|
115
|
+
template: {
|
|
116
|
+
spec: {
|
|
117
|
+
containers: [{ name: "agent", image: "fluentd" }],
|
|
118
|
+
},
|
|
119
|
+
},
|
|
120
|
+
},
|
|
121
|
+
};
|
|
122
|
+
const containers = extractContainers(manifest);
|
|
123
|
+
expect(containers.length).toBe(1);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
test("extracts from Job", () => {
|
|
127
|
+
const manifest = {
|
|
128
|
+
kind: "Job",
|
|
129
|
+
spec: {
|
|
130
|
+
template: {
|
|
131
|
+
spec: {
|
|
132
|
+
containers: [{ name: "worker", image: "job:1.0" }],
|
|
133
|
+
},
|
|
134
|
+
},
|
|
135
|
+
},
|
|
136
|
+
};
|
|
137
|
+
const containers = extractContainers(manifest);
|
|
138
|
+
expect(containers.length).toBe(1);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
test("extracts from CronJob", () => {
|
|
142
|
+
const manifest = {
|
|
143
|
+
kind: "CronJob",
|
|
144
|
+
spec: {
|
|
145
|
+
jobTemplate: {
|
|
146
|
+
spec: {
|
|
147
|
+
template: {
|
|
148
|
+
spec: {
|
|
149
|
+
containers: [{ name: "cron", image: "cron:1.0" }],
|
|
150
|
+
},
|
|
151
|
+
},
|
|
152
|
+
},
|
|
153
|
+
},
|
|
154
|
+
},
|
|
155
|
+
};
|
|
156
|
+
const containers = extractContainers(manifest);
|
|
157
|
+
expect(containers.length).toBe(1);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
test("includes init containers", () => {
|
|
161
|
+
const manifest = {
|
|
162
|
+
kind: "Deployment",
|
|
163
|
+
spec: {
|
|
164
|
+
template: {
|
|
165
|
+
spec: {
|
|
166
|
+
containers: [{ name: "app", image: "nginx" }],
|
|
167
|
+
initContainers: [{ name: "init", image: "busybox" }],
|
|
168
|
+
},
|
|
169
|
+
},
|
|
170
|
+
},
|
|
171
|
+
};
|
|
172
|
+
const containers = extractContainers(manifest);
|
|
173
|
+
expect(containers.length).toBe(2);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
test("returns empty for non-workload types", () => {
|
|
177
|
+
const manifest = { kind: "ConfigMap", data: {} };
|
|
178
|
+
const containers = extractContainers(manifest);
|
|
179
|
+
expect(containers.length).toBe(0);
|
|
180
|
+
});
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
describe("extractPodSpec", () => {
|
|
184
|
+
test("returns spec for Pod", () => {
|
|
185
|
+
const manifest = {
|
|
186
|
+
kind: "Pod",
|
|
187
|
+
spec: { containers: [{ name: "app" }] },
|
|
188
|
+
};
|
|
189
|
+
const podSpec = extractPodSpec(manifest);
|
|
190
|
+
expect(podSpec).toBeDefined();
|
|
191
|
+
expect(podSpec!.containers).toBeDefined();
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
test("returns null for ConfigMap", () => {
|
|
195
|
+
const manifest = { kind: "ConfigMap", data: {} };
|
|
196
|
+
expect(extractPodSpec(manifest)).toBeNull();
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
test("returns null when no spec", () => {
|
|
200
|
+
const manifest = { kind: "Deployment" };
|
|
201
|
+
expect(extractPodSpec(manifest)).toBeNull();
|
|
202
|
+
});
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
describe("WORKLOAD_KINDS", () => {
|
|
206
|
+
test("includes all expected kinds", () => {
|
|
207
|
+
expect(WORKLOAD_KINDS.has("Pod")).toBe(true);
|
|
208
|
+
expect(WORKLOAD_KINDS.has("Deployment")).toBe(true);
|
|
209
|
+
expect(WORKLOAD_KINDS.has("StatefulSet")).toBe(true);
|
|
210
|
+
expect(WORKLOAD_KINDS.has("DaemonSet")).toBe(true);
|
|
211
|
+
expect(WORKLOAD_KINDS.has("Job")).toBe(true);
|
|
212
|
+
expect(WORKLOAD_KINDS.has("CronJob")).toBe(true);
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
test("does not include non-workload kinds", () => {
|
|
216
|
+
expect(WORKLOAD_KINDS.has("Service")).toBe(false);
|
|
217
|
+
expect(WORKLOAD_KINDS.has("ConfigMap")).toBe(false);
|
|
218
|
+
});
|
|
219
|
+
});
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared helpers for Kubernetes post-synthesis lint rules.
|
|
3
|
+
*
|
|
4
|
+
* Provides YAML parsing for multi-document K8s manifests and container
|
|
5
|
+
* extraction logic that handles all common workload types.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { parseYAML } from "@intentius/chant/yaml";
|
|
9
|
+
export { getPrimaryOutput } from "@intentius/chant/lint/post-synth";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* A parsed Kubernetes manifest (loosely typed).
|
|
13
|
+
*/
|
|
14
|
+
export interface K8sManifest {
|
|
15
|
+
apiVersion?: string;
|
|
16
|
+
kind?: string;
|
|
17
|
+
metadata?: {
|
|
18
|
+
name?: string;
|
|
19
|
+
namespace?: string;
|
|
20
|
+
labels?: Record<string, string>;
|
|
21
|
+
[key: string]: unknown;
|
|
22
|
+
};
|
|
23
|
+
spec?: Record<string, unknown>;
|
|
24
|
+
data?: Record<string, unknown>;
|
|
25
|
+
[key: string]: unknown;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* A Kubernetes container spec (loosely typed).
|
|
30
|
+
*/
|
|
31
|
+
export interface K8sContainer {
|
|
32
|
+
name?: string;
|
|
33
|
+
image?: string;
|
|
34
|
+
env?: Array<{ name?: string; value?: unknown; valueFrom?: unknown }>;
|
|
35
|
+
ports?: Array<{ name?: string; containerPort?: number; [key: string]: unknown }>;
|
|
36
|
+
resources?: {
|
|
37
|
+
limits?: Record<string, unknown>;
|
|
38
|
+
requests?: Record<string, unknown>;
|
|
39
|
+
};
|
|
40
|
+
securityContext?: Record<string, unknown>;
|
|
41
|
+
livenessProbe?: unknown;
|
|
42
|
+
readinessProbe?: unknown;
|
|
43
|
+
imagePullPolicy?: string;
|
|
44
|
+
[key: string]: unknown;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Split a multi-document YAML string on `---` boundaries and parse each
|
|
49
|
+
* document into a K8sManifest.
|
|
50
|
+
*/
|
|
51
|
+
export function parseK8sManifests(yaml: string): K8sManifest[] {
|
|
52
|
+
const documents = yaml.split(/\n---\n/);
|
|
53
|
+
const manifests: K8sManifest[] = [];
|
|
54
|
+
|
|
55
|
+
for (const doc of documents) {
|
|
56
|
+
const trimmed = doc.trim();
|
|
57
|
+
if (trimmed === "" || trimmed === "---") continue;
|
|
58
|
+
try {
|
|
59
|
+
const parsed = parseYAML(trimmed);
|
|
60
|
+
if (typeof parsed === "object" && parsed !== null) {
|
|
61
|
+
manifests.push(parsed as K8sManifest);
|
|
62
|
+
}
|
|
63
|
+
} catch {
|
|
64
|
+
// Skip unparseable documents
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return manifests;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Extract the pod spec from any workload manifest.
|
|
73
|
+
*
|
|
74
|
+
* Handles:
|
|
75
|
+
* - Pod: spec directly
|
|
76
|
+
* - Deployment, StatefulSet, DaemonSet: spec.template.spec
|
|
77
|
+
* - Job: spec.template.spec
|
|
78
|
+
* - CronJob: spec.jobTemplate.spec.template.spec
|
|
79
|
+
*/
|
|
80
|
+
export function extractPodSpec(
|
|
81
|
+
manifest: K8sManifest,
|
|
82
|
+
): Record<string, unknown> | null {
|
|
83
|
+
const kind = manifest.kind;
|
|
84
|
+
const spec = manifest.spec;
|
|
85
|
+
if (!spec) return null;
|
|
86
|
+
|
|
87
|
+
switch (kind) {
|
|
88
|
+
case "Pod":
|
|
89
|
+
return spec as Record<string, unknown>;
|
|
90
|
+
|
|
91
|
+
case "Deployment":
|
|
92
|
+
case "StatefulSet":
|
|
93
|
+
case "DaemonSet":
|
|
94
|
+
case "Job": {
|
|
95
|
+
const template = spec.template as Record<string, unknown> | undefined;
|
|
96
|
+
return (template?.spec as Record<string, unknown>) ?? null;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
case "CronJob": {
|
|
100
|
+
const jobTemplate = spec.jobTemplate as Record<string, unknown> | undefined;
|
|
101
|
+
const jobSpec = jobTemplate?.spec as Record<string, unknown> | undefined;
|
|
102
|
+
const template = jobSpec?.template as Record<string, unknown> | undefined;
|
|
103
|
+
return (template?.spec as Record<string, unknown>) ?? null;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
default:
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Extract all containers (including init containers) from a workload manifest.
|
|
113
|
+
*/
|
|
114
|
+
export function extractContainers(manifest: K8sManifest): K8sContainer[] {
|
|
115
|
+
const podSpec = extractPodSpec(manifest);
|
|
116
|
+
if (!podSpec) return [];
|
|
117
|
+
|
|
118
|
+
const containers: K8sContainer[] = [];
|
|
119
|
+
|
|
120
|
+
if (Array.isArray(podSpec.containers)) {
|
|
121
|
+
for (const c of podSpec.containers) {
|
|
122
|
+
if (typeof c === "object" && c !== null) {
|
|
123
|
+
containers.push(c as K8sContainer);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (Array.isArray(podSpec.initContainers)) {
|
|
129
|
+
for (const c of podSpec.initContainers) {
|
|
130
|
+
if (typeof c === "object" && c !== null) {
|
|
131
|
+
containers.push(c as K8sContainer);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return containers;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Workload kinds that contain pod templates.
|
|
141
|
+
*/
|
|
142
|
+
export const WORKLOAD_KINDS = new Set([
|
|
143
|
+
"Pod",
|
|
144
|
+
"Deployment",
|
|
145
|
+
"StatefulSet",
|
|
146
|
+
"DaemonSet",
|
|
147
|
+
"Job",
|
|
148
|
+
"CronJob",
|
|
149
|
+
]);
|