@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,121 @@
|
|
|
1
|
+
import { describe, test, expect } from "bun:test";
|
|
2
|
+
import { K8sGenerator } from "./generator";
|
|
3
|
+
import type { TemplateIR, ResourceIR } from "@intentius/chant/import/parser";
|
|
4
|
+
|
|
5
|
+
const generator = new K8sGenerator();
|
|
6
|
+
|
|
7
|
+
function makeIR(resources: ResourceIR[]): TemplateIR {
|
|
8
|
+
return { resources, parameters: [] };
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
describe("K8sGenerator", () => {
|
|
12
|
+
test("generates valid TypeScript from IR", () => {
|
|
13
|
+
const ir = makeIR([
|
|
14
|
+
{
|
|
15
|
+
logicalId: "deploymentApp",
|
|
16
|
+
type: "K8s::Apps::Deployment",
|
|
17
|
+
properties: {
|
|
18
|
+
metadata: { name: "app" },
|
|
19
|
+
spec: { replicas: 2 },
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
]);
|
|
23
|
+
const files = generator.generate(ir);
|
|
24
|
+
expect(files.length).toBe(1);
|
|
25
|
+
expect(files[0].path).toBe("main.ts");
|
|
26
|
+
expect(files[0].content).toContain("import");
|
|
27
|
+
expect(files[0].content).toContain("Deployment");
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test("imports correct classes", () => {
|
|
31
|
+
const ir = makeIR([
|
|
32
|
+
{
|
|
33
|
+
logicalId: "svc",
|
|
34
|
+
type: "K8s::Core::Service",
|
|
35
|
+
properties: { metadata: { name: "svc" } },
|
|
36
|
+
},
|
|
37
|
+
]);
|
|
38
|
+
const files = generator.generate(ir);
|
|
39
|
+
expect(files[0].content).toContain(
|
|
40
|
+
'from "@intentius/chant-lexicon-k8s"',
|
|
41
|
+
);
|
|
42
|
+
expect(files[0].content).toContain("Service");
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test("uses new Constructor() syntax", () => {
|
|
46
|
+
const ir = makeIR([
|
|
47
|
+
{
|
|
48
|
+
logicalId: "deploy",
|
|
49
|
+
type: "K8s::Apps::Deployment",
|
|
50
|
+
properties: {
|
|
51
|
+
metadata: { name: "app" },
|
|
52
|
+
spec: { replicas: 1 },
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
]);
|
|
56
|
+
const files = generator.generate(ir);
|
|
57
|
+
expect(files[0].content).toContain("new Deployment(");
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test("handles array properties (containers, env)", () => {
|
|
61
|
+
const ir = makeIR([
|
|
62
|
+
{
|
|
63
|
+
logicalId: "pod",
|
|
64
|
+
type: "K8s::Core::Pod",
|
|
65
|
+
properties: {
|
|
66
|
+
metadata: { name: "pod" },
|
|
67
|
+
spec: {
|
|
68
|
+
containers: [
|
|
69
|
+
{ name: "app", image: "nginx:1.0" },
|
|
70
|
+
],
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
]);
|
|
75
|
+
const files = generator.generate(ir);
|
|
76
|
+
expect(files[0].content).toContain("Container");
|
|
77
|
+
expect(files[0].content).toContain("nginx:1.0");
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test("generates exports for each resource", () => {
|
|
81
|
+
const ir = makeIR([
|
|
82
|
+
{
|
|
83
|
+
logicalId: "deploy",
|
|
84
|
+
type: "K8s::Apps::Deployment",
|
|
85
|
+
properties: { metadata: { name: "app" } },
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
logicalId: "svc",
|
|
89
|
+
type: "K8s::Core::Service",
|
|
90
|
+
properties: { metadata: { name: "svc" } },
|
|
91
|
+
},
|
|
92
|
+
]);
|
|
93
|
+
const files = generator.generate(ir);
|
|
94
|
+
expect(files[0].content).toContain("export const deploy");
|
|
95
|
+
expect(files[0].content).toContain("export const svc");
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test("Service ports use ServicePort constructor", () => {
|
|
99
|
+
const ir = makeIR([
|
|
100
|
+
{
|
|
101
|
+
logicalId: "svc",
|
|
102
|
+
type: "K8s::Core::Service",
|
|
103
|
+
properties: {
|
|
104
|
+
metadata: { name: "svc" },
|
|
105
|
+
spec: {
|
|
106
|
+
ports: [{ port: 80, targetPort: 8080 }],
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
},
|
|
110
|
+
]);
|
|
111
|
+
const files = generator.generate(ir);
|
|
112
|
+
expect(files[0].content).toContain("ServicePort");
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
test("handles empty IR", () => {
|
|
116
|
+
const ir = makeIR([]);
|
|
117
|
+
const files = generator.generate(ir);
|
|
118
|
+
expect(files.length).toBe(1);
|
|
119
|
+
// Should still have an import line (even if no resources)
|
|
120
|
+
});
|
|
121
|
+
});
|
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TypeScript code generator for Kubernetes import.
|
|
3
|
+
*
|
|
4
|
+
* Converts a TemplateIR (from parsed K8s manifests) into TypeScript
|
|
5
|
+
* source code using the @intentius/chant-lexicon-k8s constructors.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { TypeScriptGenerator, GeneratedFile } from "@intentius/chant/import/generator";
|
|
9
|
+
import type { TemplateIR, ResourceIR } from "@intentius/chant/import/parser";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Map K8s entity types to their constructor class names.
|
|
13
|
+
*/
|
|
14
|
+
const TYPE_TO_CLASS: Record<string, string> = {
|
|
15
|
+
// Core
|
|
16
|
+
"K8s::Core::Pod": "Pod",
|
|
17
|
+
"K8s::Core::Service": "Service",
|
|
18
|
+
"K8s::Core::ConfigMap": "ConfigMap",
|
|
19
|
+
"K8s::Core::Secret": "Secret",
|
|
20
|
+
"K8s::Core::Namespace": "Namespace",
|
|
21
|
+
"K8s::Core::ServiceAccount": "ServiceAccount",
|
|
22
|
+
"K8s::Core::PersistentVolume": "PersistentVolume",
|
|
23
|
+
"K8s::Core::PersistentVolumeClaim": "PersistentVolumeClaim",
|
|
24
|
+
"K8s::Core::Endpoints": "Endpoints",
|
|
25
|
+
"K8s::Core::LimitRange": "LimitRange",
|
|
26
|
+
"K8s::Core::ResourceQuota": "ResourceQuota",
|
|
27
|
+
|
|
28
|
+
// Apps
|
|
29
|
+
"K8s::Apps::Deployment": "Deployment",
|
|
30
|
+
"K8s::Apps::StatefulSet": "StatefulSet",
|
|
31
|
+
"K8s::Apps::DaemonSet": "DaemonSet",
|
|
32
|
+
"K8s::Apps::ReplicaSet": "ReplicaSet",
|
|
33
|
+
|
|
34
|
+
// Batch
|
|
35
|
+
"K8s::Batch::Job": "Job",
|
|
36
|
+
"K8s::Batch::CronJob": "CronJob",
|
|
37
|
+
|
|
38
|
+
// Networking
|
|
39
|
+
"K8s::Networking::Ingress": "Ingress",
|
|
40
|
+
"K8s::Networking::NetworkPolicy": "NetworkPolicy",
|
|
41
|
+
"K8s::Networking::IngressClass": "IngressClass",
|
|
42
|
+
|
|
43
|
+
// RBAC
|
|
44
|
+
"K8s::Rbac::Role": "Role",
|
|
45
|
+
"K8s::Rbac::ClusterRole": "ClusterRole",
|
|
46
|
+
"K8s::Rbac::RoleBinding": "RoleBinding",
|
|
47
|
+
"K8s::Rbac::ClusterRoleBinding": "ClusterRoleBinding",
|
|
48
|
+
|
|
49
|
+
// Autoscaling
|
|
50
|
+
"K8s::Autoscaling::HorizontalPodAutoscaler": "HorizontalPodAutoscaler",
|
|
51
|
+
|
|
52
|
+
// Policy
|
|
53
|
+
"K8s::Policy::PodDisruptionBudget": "PodDisruptionBudget",
|
|
54
|
+
|
|
55
|
+
// Storage
|
|
56
|
+
"K8s::Storage::StorageClass": "StorageClass",
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Properties that reference known property entity constructors.
|
|
61
|
+
* Key: property name found in K8s specs → Value: constructor class name.
|
|
62
|
+
*/
|
|
63
|
+
const PROPERTY_CONSTRUCTORS: Record<string, string> = {
|
|
64
|
+
containers: "Container",
|
|
65
|
+
initContainers: "Container",
|
|
66
|
+
ephemeralContainers: "EphemeralContainer",
|
|
67
|
+
volumes: "Volume",
|
|
68
|
+
volumeMounts: "VolumeMount",
|
|
69
|
+
ports: "ContainerPort",
|
|
70
|
+
env: "EnvVar",
|
|
71
|
+
envFrom: "EnvFromSource",
|
|
72
|
+
resources: "ResourceRequirements",
|
|
73
|
+
securityContext: "SecurityContext",
|
|
74
|
+
podSecurityContext: "PodSecurityContext",
|
|
75
|
+
livenessProbe: "Probe",
|
|
76
|
+
readinessProbe: "Probe",
|
|
77
|
+
startupProbe: "Probe",
|
|
78
|
+
selector: "LabelSelector",
|
|
79
|
+
template: "PodTemplateSpec",
|
|
80
|
+
strategy: "DeploymentStrategy",
|
|
81
|
+
affinity: "Affinity",
|
|
82
|
+
tolerations: "Toleration",
|
|
83
|
+
topologySpreadConstraints: "TopologySpreadConstraint",
|
|
84
|
+
rules: "IngressRule",
|
|
85
|
+
tls: "IngressTLS",
|
|
86
|
+
ingress: "NetworkPolicyIngressRule",
|
|
87
|
+
egress: "NetworkPolicyEgressRule",
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Service port properties need distinct handling since K8s uses "ports"
|
|
92
|
+
* for both ContainerPort and ServicePort depending on the parent resource.
|
|
93
|
+
*/
|
|
94
|
+
const SERVICE_PORT_TYPES = new Set([
|
|
95
|
+
"K8s::Core::Service",
|
|
96
|
+
]);
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Generate TypeScript source code from a Kubernetes IR.
|
|
100
|
+
*/
|
|
101
|
+
export class K8sGenerator implements TypeScriptGenerator {
|
|
102
|
+
generate(ir: TemplateIR): GeneratedFile[] {
|
|
103
|
+
const lines: string[] = [];
|
|
104
|
+
|
|
105
|
+
// Collect which constructors are needed
|
|
106
|
+
const usedConstructors = new Set<string>();
|
|
107
|
+
for (const resource of ir.resources) {
|
|
108
|
+
const cls = this.resolveClass(resource.type);
|
|
109
|
+
if (cls) usedConstructors.add(cls);
|
|
110
|
+
|
|
111
|
+
// Check properties for nested constructors
|
|
112
|
+
this.collectNestedConstructors(resource.properties, usedConstructors, resource.type);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Import statement
|
|
116
|
+
const imports = [...usedConstructors].sort().join(", ");
|
|
117
|
+
lines.push(`import { ${imports} } from "@intentius/chant-lexicon-k8s";`);
|
|
118
|
+
lines.push("");
|
|
119
|
+
|
|
120
|
+
// Emit namespaces as comments
|
|
121
|
+
if (ir.metadata?.namespaces && Array.isArray(ir.metadata.namespaces)) {
|
|
122
|
+
lines.push(`// Namespaces: ${(ir.metadata.namespaces as string[]).join(", ")}`);
|
|
123
|
+
lines.push("");
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Emit resources
|
|
127
|
+
for (const resource of ir.resources) {
|
|
128
|
+
const cls = this.resolveClass(resource.type);
|
|
129
|
+
if (!cls) {
|
|
130
|
+
lines.push(`// TODO: unsupported type ${resource.type} (${resource.logicalId})`);
|
|
131
|
+
lines.push("");
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const varName = resource.logicalId;
|
|
136
|
+
const propsStr = this.emitProps(resource.properties, 1, resource.type);
|
|
137
|
+
|
|
138
|
+
lines.push(`export const ${varName} = new ${cls}(${propsStr});`);
|
|
139
|
+
lines.push("");
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return [{ path: "main.ts", content: lines.join("\n") }];
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Resolve a K8s type name to a constructor class name.
|
|
147
|
+
* Falls back to extracting the last segment of the type.
|
|
148
|
+
*/
|
|
149
|
+
private resolveClass(type: string): string | undefined {
|
|
150
|
+
if (TYPE_TO_CLASS[type]) return TYPE_TO_CLASS[type];
|
|
151
|
+
|
|
152
|
+
// Fallback: extract Kind from "K8s::Group::Kind"
|
|
153
|
+
const parts = type.split("::");
|
|
154
|
+
if (parts.length === 3 && parts[0] === "K8s") return parts[2];
|
|
155
|
+
|
|
156
|
+
return undefined;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
private collectNestedConstructors(
|
|
160
|
+
props: Record<string, unknown>,
|
|
161
|
+
used: Set<string>,
|
|
162
|
+
parentType: string,
|
|
163
|
+
): void {
|
|
164
|
+
for (const [key, value] of Object.entries(props)) {
|
|
165
|
+
if (value === undefined || value === null) continue;
|
|
166
|
+
|
|
167
|
+
// Special case: "ports" on Service resources → ServicePort
|
|
168
|
+
if (key === "ports" && SERVICE_PORT_TYPES.has(parentType)) {
|
|
169
|
+
if (Array.isArray(value)) used.add("ServicePort");
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const constructor = PROPERTY_CONSTRUCTORS[key];
|
|
174
|
+
if (!constructor) continue;
|
|
175
|
+
|
|
176
|
+
if (Array.isArray(value)) {
|
|
177
|
+
used.add(constructor);
|
|
178
|
+
} else if (typeof value === "object") {
|
|
179
|
+
used.add(constructor);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Recurse into nested specs (e.g., template.spec.containers)
|
|
183
|
+
if (typeof value === "object" && !Array.isArray(value)) {
|
|
184
|
+
this.collectNestedConstructors(value as Record<string, unknown>, used, parentType);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Recurse into spec if present
|
|
189
|
+
if (props.spec && typeof props.spec === "object" && !Array.isArray(props.spec)) {
|
|
190
|
+
this.collectNestedConstructors(props.spec as Record<string, unknown>, used, parentType);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
private emitProps(props: Record<string, unknown>, depth: number, parentType: string): string {
|
|
195
|
+
const indent = " ".repeat(depth);
|
|
196
|
+
const innerIndent = " ".repeat(depth + 1);
|
|
197
|
+
const entries: string[] = [];
|
|
198
|
+
|
|
199
|
+
for (const [key, value] of Object.entries(props)) {
|
|
200
|
+
if (value === undefined || value === null) continue;
|
|
201
|
+
const emitted = this.emitValue(key, value, depth + 1, parentType);
|
|
202
|
+
entries.push(`${innerIndent}${key}: ${emitted},`);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (entries.length === 0) return "{}";
|
|
206
|
+
return `{\n${entries.join("\n")}\n${indent}}`;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
private emitValue(key: string, value: unknown, depth: number, parentType: string): string {
|
|
210
|
+
if (value === null || value === undefined) return "undefined";
|
|
211
|
+
|
|
212
|
+
// Special case: "ports" on Service → ServicePort constructor
|
|
213
|
+
if (key === "ports" && SERVICE_PORT_TYPES.has(parentType) && Array.isArray(value)) {
|
|
214
|
+
return this.emitConstructorArray("ServicePort", value, depth, parentType);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Check if this key maps to a property constructor
|
|
218
|
+
const constructor = PROPERTY_CONSTRUCTORS[key];
|
|
219
|
+
if (constructor) {
|
|
220
|
+
// Array of constructors (containers, volumes, ports, env, etc.)
|
|
221
|
+
if (Array.isArray(value)) {
|
|
222
|
+
return this.emitConstructorArray(constructor, value, depth, parentType);
|
|
223
|
+
}
|
|
224
|
+
// Single constructor (securityContext, resources, selector, template, strategy, etc.)
|
|
225
|
+
if (typeof value === "object") {
|
|
226
|
+
const propsStr = this.emitProps(value as Record<string, unknown>, depth, parentType);
|
|
227
|
+
return `new ${constructor}(${propsStr})`;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
return this.emitLiteral(value, depth, parentType);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
private emitConstructorArray(
|
|
235
|
+
constructor: string,
|
|
236
|
+
items: unknown[],
|
|
237
|
+
depth: number,
|
|
238
|
+
parentType: string,
|
|
239
|
+
): string {
|
|
240
|
+
const indent = " ".repeat(depth);
|
|
241
|
+
const innerIndent = " ".repeat(depth + 1);
|
|
242
|
+
|
|
243
|
+
const emitted = items.map((item) => {
|
|
244
|
+
if (typeof item === "object" && item !== null && !Array.isArray(item)) {
|
|
245
|
+
const propsStr = this.emitProps(item as Record<string, unknown>, depth + 1, parentType);
|
|
246
|
+
return `new ${constructor}(${propsStr})`;
|
|
247
|
+
}
|
|
248
|
+
return JSON.stringify(item);
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
if (emitted.length === 1 && emitted[0].length < 60) {
|
|
252
|
+
return `[${emitted[0]}]`;
|
|
253
|
+
}
|
|
254
|
+
return `[\n${emitted.map((e) => `${innerIndent}${e},`).join("\n")}\n${indent}]`;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
private emitLiteral(value: unknown, depth: number, parentType: string): string {
|
|
258
|
+
if (value === null || value === undefined) return "undefined";
|
|
259
|
+
if (typeof value === "string") return JSON.stringify(value);
|
|
260
|
+
if (typeof value === "number" || typeof value === "boolean") return String(value);
|
|
261
|
+
|
|
262
|
+
if (Array.isArray(value)) {
|
|
263
|
+
if (value.length === 0) return "[]";
|
|
264
|
+
const items = value.map((item) => this.emitLiteral(item, depth + 1, parentType));
|
|
265
|
+
const oneLine = `[${items.join(", ")}]`;
|
|
266
|
+
if (oneLine.length < 80) return oneLine;
|
|
267
|
+
const indent = " ".repeat(depth);
|
|
268
|
+
const innerIndent = " ".repeat(depth + 1);
|
|
269
|
+
return `[\n${items.map((i) => `${innerIndent}${i},`).join("\n")}\n${indent}]`;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
if (typeof value === "object") {
|
|
273
|
+
const entries = Object.entries(value as Record<string, unknown>);
|
|
274
|
+
if (entries.length === 0) return "{}";
|
|
275
|
+
const indent = " ".repeat(depth);
|
|
276
|
+
const innerIndent = " ".repeat(depth + 1);
|
|
277
|
+
const items = entries.map(
|
|
278
|
+
([k, v]) => `${innerIndent}${k}: ${this.emitLiteral(v, depth + 1, parentType)},`,
|
|
279
|
+
);
|
|
280
|
+
return `{\n${items.join("\n")}\n${indent}}`;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
return String(value);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { describe, test, expect } from "bun:test";
|
|
2
|
+
import { K8sParser } from "./parser";
|
|
3
|
+
|
|
4
|
+
const parser = new K8sParser();
|
|
5
|
+
|
|
6
|
+
describe("K8sParser", () => {
|
|
7
|
+
test("empty YAML returns empty resources", () => {
|
|
8
|
+
const ir = parser.parse("");
|
|
9
|
+
expect(ir.resources).toEqual([]);
|
|
10
|
+
expect(ir.parameters).toEqual([]);
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
test("single Deployment parses correctly", () => {
|
|
14
|
+
const yaml = `
|
|
15
|
+
apiVersion: apps/v1
|
|
16
|
+
kind: Deployment
|
|
17
|
+
metadata:
|
|
18
|
+
name: my-app
|
|
19
|
+
spec:
|
|
20
|
+
replicas: 2
|
|
21
|
+
`;
|
|
22
|
+
const ir = parser.parse(yaml);
|
|
23
|
+
expect(ir.resources.length).toBe(1);
|
|
24
|
+
const r = ir.resources[0];
|
|
25
|
+
expect(r.type).toBe("K8s::Apps::Deployment");
|
|
26
|
+
expect(r.logicalId).toBe("deploymentMyApp");
|
|
27
|
+
expect(r.properties.spec).toEqual({ replicas: 2 });
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test("multi-doc YAML produces multiple resources", () => {
|
|
31
|
+
const yaml = `
|
|
32
|
+
apiVersion: apps/v1
|
|
33
|
+
kind: Deployment
|
|
34
|
+
metadata:
|
|
35
|
+
name: app
|
|
36
|
+
---
|
|
37
|
+
apiVersion: v1
|
|
38
|
+
kind: Service
|
|
39
|
+
metadata:
|
|
40
|
+
name: svc
|
|
41
|
+
`;
|
|
42
|
+
const ir = parser.parse(yaml);
|
|
43
|
+
expect(ir.resources.length).toBe(2);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test("apiVersion: apps/v1, kind: Deployment → K8s::Apps::Deployment", () => {
|
|
47
|
+
const yaml = `
|
|
48
|
+
apiVersion: apps/v1
|
|
49
|
+
kind: Deployment
|
|
50
|
+
metadata:
|
|
51
|
+
name: test
|
|
52
|
+
`;
|
|
53
|
+
const ir = parser.parse(yaml);
|
|
54
|
+
expect(ir.resources[0].type).toBe("K8s::Apps::Deployment");
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test("apiVersion: v1, kind: Service → K8s::Core::Service", () => {
|
|
58
|
+
const yaml = `
|
|
59
|
+
apiVersion: v1
|
|
60
|
+
kind: Service
|
|
61
|
+
metadata:
|
|
62
|
+
name: test
|
|
63
|
+
`;
|
|
64
|
+
const ir = parser.parse(yaml);
|
|
65
|
+
expect(ir.resources[0].type).toBe("K8s::Core::Service");
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test("v1/ConfigMap maps correctly", () => {
|
|
69
|
+
const yaml = `
|
|
70
|
+
apiVersion: v1
|
|
71
|
+
kind: ConfigMap
|
|
72
|
+
metadata:
|
|
73
|
+
name: config
|
|
74
|
+
data:
|
|
75
|
+
key: value
|
|
76
|
+
`;
|
|
77
|
+
const ir = parser.parse(yaml);
|
|
78
|
+
expect(ir.resources[0].type).toBe("K8s::Core::ConfigMap");
|
|
79
|
+
expect(ir.resources[0].properties.data).toEqual({ key: "value" });
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test("unknown apiVersion/kind falls back to constructed type name", () => {
|
|
83
|
+
const yaml = `
|
|
84
|
+
apiVersion: custom.io/v1
|
|
85
|
+
kind: Widget
|
|
86
|
+
metadata:
|
|
87
|
+
name: w
|
|
88
|
+
`;
|
|
89
|
+
const ir = parser.parse(yaml);
|
|
90
|
+
expect(ir.resources[0].type).toMatch(/^K8s::\w+::Widget$/);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test("metadata.name extracted as logical ID component", () => {
|
|
94
|
+
const yaml = `
|
|
95
|
+
apiVersion: v1
|
|
96
|
+
kind: Pod
|
|
97
|
+
metadata:
|
|
98
|
+
name: my-pod
|
|
99
|
+
`;
|
|
100
|
+
const ir = parser.parse(yaml);
|
|
101
|
+
expect(ir.resources[0].logicalId).toBe("podMyPod");
|
|
102
|
+
expect(ir.resources[0].metadata!.originalName).toBe("my-pod");
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
test("metadata includes apiVersion and kind", () => {
|
|
106
|
+
const yaml = `
|
|
107
|
+
apiVersion: batch/v1
|
|
108
|
+
kind: Job
|
|
109
|
+
metadata:
|
|
110
|
+
name: my-job
|
|
111
|
+
`;
|
|
112
|
+
const ir = parser.parse(yaml);
|
|
113
|
+
expect(ir.resources[0].metadata!.apiVersion).toBe("batch/v1");
|
|
114
|
+
expect(ir.resources[0].metadata!.kind).toBe("Job");
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
test("properties exclude apiVersion and kind", () => {
|
|
118
|
+
const yaml = `
|
|
119
|
+
apiVersion: v1
|
|
120
|
+
kind: Pod
|
|
121
|
+
metadata:
|
|
122
|
+
name: test
|
|
123
|
+
spec:
|
|
124
|
+
containers: []
|
|
125
|
+
`;
|
|
126
|
+
const ir = parser.parse(yaml);
|
|
127
|
+
const props = ir.resources[0].properties;
|
|
128
|
+
expect(props.apiVersion).toBeUndefined();
|
|
129
|
+
expect(props.kind).toBeUndefined();
|
|
130
|
+
expect(props.metadata).toBeDefined();
|
|
131
|
+
expect(props.spec).toBeDefined();
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
test("parameters are empty (K8s has no parameters)", () => {
|
|
135
|
+
const yaml = `
|
|
136
|
+
apiVersion: v1
|
|
137
|
+
kind: Pod
|
|
138
|
+
metadata:
|
|
139
|
+
name: test
|
|
140
|
+
`;
|
|
141
|
+
const ir = parser.parse(yaml);
|
|
142
|
+
expect(ir.parameters).toEqual([]);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
test("tracks namespaces in metadata", () => {
|
|
146
|
+
const yaml = `
|
|
147
|
+
apiVersion: v1
|
|
148
|
+
kind: Pod
|
|
149
|
+
metadata:
|
|
150
|
+
name: test
|
|
151
|
+
namespace: production
|
|
152
|
+
`;
|
|
153
|
+
const ir = parser.parse(yaml);
|
|
154
|
+
expect(ir.metadata?.namespaces).toContain("production");
|
|
155
|
+
});
|
|
156
|
+
});
|