@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,102 @@
|
|
|
1
|
+
import { describe, test, expect } from "bun:test";
|
|
2
|
+
import {
|
|
3
|
+
gvkToTypeName,
|
|
4
|
+
gvkToApiVersion,
|
|
5
|
+
k8sShortName,
|
|
6
|
+
k8sServiceName,
|
|
7
|
+
} from "./parse";
|
|
8
|
+
|
|
9
|
+
describe("gvkToTypeName", () => {
|
|
10
|
+
test("core group maps to Core", () => {
|
|
11
|
+
expect(gvkToTypeName({ group: "", version: "v1", kind: "Pod" })).toBe(
|
|
12
|
+
"K8s::Core::Pod",
|
|
13
|
+
);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
test("apps group", () => {
|
|
17
|
+
expect(
|
|
18
|
+
gvkToTypeName({ group: "apps", version: "v1", kind: "Deployment" }),
|
|
19
|
+
).toBe("K8s::Apps::Deployment");
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
test("batch group", () => {
|
|
23
|
+
expect(
|
|
24
|
+
gvkToTypeName({ group: "batch", version: "v1", kind: "Job" }),
|
|
25
|
+
).toBe("K8s::Batch::Job");
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test("networking.k8s.io group", () => {
|
|
29
|
+
expect(
|
|
30
|
+
gvkToTypeName({
|
|
31
|
+
group: "networking.k8s.io",
|
|
32
|
+
version: "v1",
|
|
33
|
+
kind: "Ingress",
|
|
34
|
+
}),
|
|
35
|
+
).toBe("K8s::Networking::Ingress");
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test("rbac group normalised to Rbac", () => {
|
|
39
|
+
expect(
|
|
40
|
+
gvkToTypeName({
|
|
41
|
+
group: "rbac.authorization.k8s.io",
|
|
42
|
+
version: "v1",
|
|
43
|
+
kind: "Role",
|
|
44
|
+
}),
|
|
45
|
+
).toBe("K8s::Rbac::Role");
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test("autoscaling group", () => {
|
|
49
|
+
expect(
|
|
50
|
+
gvkToTypeName({
|
|
51
|
+
group: "autoscaling",
|
|
52
|
+
version: "v2",
|
|
53
|
+
kind: "HorizontalPodAutoscaler",
|
|
54
|
+
}),
|
|
55
|
+
).toBe("K8s::Autoscaling::HorizontalPodAutoscaler");
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
describe("gvkToApiVersion", () => {
|
|
60
|
+
test("core group returns version only", () => {
|
|
61
|
+
expect(gvkToApiVersion({ group: "", version: "v1", kind: "Pod" })).toBe(
|
|
62
|
+
"v1",
|
|
63
|
+
);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test("empty string group returns version only", () => {
|
|
67
|
+
expect(
|
|
68
|
+
gvkToApiVersion({ group: "", version: "v1", kind: "Service" }),
|
|
69
|
+
).toBe("v1");
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test("non-core group returns group/version", () => {
|
|
73
|
+
expect(
|
|
74
|
+
gvkToApiVersion({ group: "apps", version: "v1", kind: "Deployment" }),
|
|
75
|
+
).toBe("apps/v1");
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test("networking group", () => {
|
|
79
|
+
expect(
|
|
80
|
+
gvkToApiVersion({
|
|
81
|
+
group: "networking.k8s.io",
|
|
82
|
+
version: "v1",
|
|
83
|
+
kind: "Ingress",
|
|
84
|
+
}),
|
|
85
|
+
).toBe("networking.k8s.io/v1");
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
describe("k8sShortName", () => {
|
|
90
|
+
test("returns short name for known types", () => {
|
|
91
|
+
// k8sShortName should map well-known types to their abbreviations
|
|
92
|
+
const name = k8sShortName("Deployment");
|
|
93
|
+
expect(typeof name).toBe("string");
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
describe("k8sServiceName", () => {
|
|
98
|
+
test("returns service name for known types", () => {
|
|
99
|
+
const name = k8sServiceName("Deployment");
|
|
100
|
+
expect(typeof name).toBe("string");
|
|
101
|
+
});
|
|
102
|
+
});
|
|
@@ -0,0 +1,477 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Kubernetes OpenAPI Swagger 2.0 parser.
|
|
3
|
+
*
|
|
4
|
+
* Parses the single swagger.json into multiple K8sParseResult entries —
|
|
5
|
+
* one per resource identified by `x-kubernetes-group-version-kind`.
|
|
6
|
+
* Property types (Container, PodSpec, Volume, etc.) are extracted as
|
|
7
|
+
* nested property types within their parent resources.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { PropertyConstraints } from "@intentius/chant/codegen/json-schema";
|
|
11
|
+
import {
|
|
12
|
+
extractConstraints as coreExtractConstraints,
|
|
13
|
+
primaryType,
|
|
14
|
+
type JsonSchemaProperty,
|
|
15
|
+
} from "@intentius/chant/codegen/json-schema";
|
|
16
|
+
|
|
17
|
+
// ── Types ──────────────────────────────────────────────────────────
|
|
18
|
+
|
|
19
|
+
export type { PropertyConstraints };
|
|
20
|
+
|
|
21
|
+
export interface ParsedProperty {
|
|
22
|
+
name: string;
|
|
23
|
+
tsType: string;
|
|
24
|
+
required: boolean;
|
|
25
|
+
description?: string;
|
|
26
|
+
enum?: string[];
|
|
27
|
+
constraints: PropertyConstraints;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface ParsedPropertyType {
|
|
31
|
+
name: string;
|
|
32
|
+
/** The definition key in the original schema */
|
|
33
|
+
defType: string;
|
|
34
|
+
properties: ParsedProperty[];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface ParsedEnum {
|
|
38
|
+
name: string;
|
|
39
|
+
values: string[];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface ParsedResource {
|
|
43
|
+
typeName: string;
|
|
44
|
+
description?: string;
|
|
45
|
+
properties: ParsedProperty[];
|
|
46
|
+
attributes: Array<{ name: string; tsType: string }>;
|
|
47
|
+
deprecatedProperties: string[];
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface GroupVersionKind {
|
|
51
|
+
group: string;
|
|
52
|
+
version: string;
|
|
53
|
+
kind: string;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface K8sParseResult {
|
|
57
|
+
resource: ParsedResource;
|
|
58
|
+
propertyTypes: ParsedPropertyType[];
|
|
59
|
+
enums: ParsedEnum[];
|
|
60
|
+
gvk: GroupVersionKind;
|
|
61
|
+
/** Whether this entity is a property type (nested inside resources) */
|
|
62
|
+
isProperty?: boolean;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// ── Swagger types ──────────────────────────────────────────────────
|
|
66
|
+
|
|
67
|
+
interface SwaggerDefinition {
|
|
68
|
+
type?: string | string[];
|
|
69
|
+
description?: string;
|
|
70
|
+
properties?: Record<string, SwaggerProperty>;
|
|
71
|
+
required?: string[];
|
|
72
|
+
enum?: string[];
|
|
73
|
+
$ref?: string;
|
|
74
|
+
items?: SwaggerProperty;
|
|
75
|
+
additionalProperties?: boolean | SwaggerProperty;
|
|
76
|
+
format?: string;
|
|
77
|
+
minimum?: number;
|
|
78
|
+
maximum?: number;
|
|
79
|
+
minLength?: number;
|
|
80
|
+
maxLength?: number;
|
|
81
|
+
pattern?: string;
|
|
82
|
+
"x-kubernetes-group-version-kind"?: GroupVersionKind[];
|
|
83
|
+
"x-kubernetes-int-or-string"?: boolean;
|
|
84
|
+
"x-kubernetes-preserve-unknown-fields"?: boolean;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
interface SwaggerProperty extends SwaggerDefinition {
|
|
88
|
+
// Same shape as definition
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
interface SwaggerSpec {
|
|
92
|
+
definitions?: Record<string, SwaggerDefinition>;
|
|
93
|
+
[key: string]: unknown;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// ── Well-known property type definitions ───────────────────────────
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Definitions that should be extracted as standalone property types
|
|
100
|
+
* even though they don't have GVK. Mapped to friendly names.
|
|
101
|
+
*/
|
|
102
|
+
const PROPERTY_TYPE_DEFS: Record<string, { typeName: string; description: string }> = {
|
|
103
|
+
"io.k8s.api.core.v1.Container": { typeName: "K8s::Core::Container", description: "A container definition for a pod" },
|
|
104
|
+
"io.k8s.api.core.v1.ContainerPort": { typeName: "K8s::Core::ContainerPort", description: "A port to expose from a container" },
|
|
105
|
+
"io.k8s.api.core.v1.EnvVar": { typeName: "K8s::Core::EnvVar", description: "An environment variable for a container" },
|
|
106
|
+
"io.k8s.api.core.v1.EnvFromSource": { typeName: "K8s::Core::EnvFromSource", description: "Source for environment variables" },
|
|
107
|
+
"io.k8s.api.core.v1.Volume": { typeName: "K8s::Core::Volume", description: "A volume that can be mounted by containers" },
|
|
108
|
+
"io.k8s.api.core.v1.VolumeMount": { typeName: "K8s::Core::VolumeMount", description: "A volume mount in a container" },
|
|
109
|
+
"io.k8s.api.core.v1.PodSpec": { typeName: "K8s::Core::PodSpec", description: "Specification of a pod" },
|
|
110
|
+
"io.k8s.api.core.v1.PodTemplateSpec": { typeName: "K8s::Core::PodTemplateSpec", description: "Pod template specification" },
|
|
111
|
+
"io.k8s.api.core.v1.ServicePort": { typeName: "K8s::Core::ServicePort", description: "A port exposed by a service" },
|
|
112
|
+
"io.k8s.api.core.v1.Probe": { typeName: "K8s::Core::Probe", description: "A health check probe" },
|
|
113
|
+
"io.k8s.api.core.v1.ResourceRequirements": { typeName: "K8s::Core::ResourceRequirements", description: "CPU and memory resource requirements" },
|
|
114
|
+
"io.k8s.api.core.v1.SecurityContext": { typeName: "K8s::Core::SecurityContext", description: "Security options for a container" },
|
|
115
|
+
"io.k8s.api.core.v1.PodSecurityContext": { typeName: "K8s::Core::PodSecurityContext", description: "Security options for a pod" },
|
|
116
|
+
"io.k8s.api.core.v1.Capabilities": { typeName: "K8s::Core::Capabilities", description: "Linux capabilities to add or drop" },
|
|
117
|
+
"io.k8s.api.core.v1.ConfigMapKeySelector": { typeName: "K8s::Core::ConfigMapKeySelector", description: "Reference to a key in a ConfigMap" },
|
|
118
|
+
"io.k8s.api.core.v1.SecretKeySelector": { typeName: "K8s::Core::SecretKeySelector", description: "Reference to a key in a Secret" },
|
|
119
|
+
"io.k8s.api.core.v1.EnvVarSource": { typeName: "K8s::Core::EnvVarSource", description: "Source for an environment variable value" },
|
|
120
|
+
"io.k8s.api.core.v1.ObjectReference": { typeName: "K8s::Core::ObjectReference", description: "Reference to another Kubernetes object" },
|
|
121
|
+
"io.k8s.api.core.v1.LocalObjectReference": { typeName: "K8s::Core::LocalObjectReference", description: "Reference to a local object" },
|
|
122
|
+
"io.k8s.api.core.v1.Toleration": { typeName: "K8s::Core::Toleration", description: "A toleration for pod scheduling" },
|
|
123
|
+
"io.k8s.api.core.v1.Affinity": { typeName: "K8s::Core::Affinity", description: "Scheduling affinity rules" },
|
|
124
|
+
"io.k8s.api.core.v1.TopologySpreadConstraint": { typeName: "K8s::Core::TopologySpreadConstraint", description: "Pod topology spread constraint" },
|
|
125
|
+
"io.k8s.api.core.v1.PersistentVolumeClaimSpec": { typeName: "K8s::Core::PersistentVolumeClaimSpec", description: "PVC spec for StatefulSet volume templates" },
|
|
126
|
+
"io.k8s.api.core.v1.HTTPGetAction": { typeName: "K8s::Core::HTTPGetAction", description: "HTTP GET probe action" },
|
|
127
|
+
"io.k8s.api.core.v1.TCPSocketAction": { typeName: "K8s::Core::TCPSocketAction", description: "TCP socket probe action" },
|
|
128
|
+
"io.k8s.api.core.v1.ExecAction": { typeName: "K8s::Core::ExecAction", description: "Exec probe action" },
|
|
129
|
+
"io.k8s.api.core.v1.HostAlias": { typeName: "K8s::Core::HostAlias", description: "Host alias entry for /etc/hosts" },
|
|
130
|
+
"io.k8s.api.core.v1.EphemeralContainer": { typeName: "K8s::Core::EphemeralContainer", description: "An ephemeral container for debugging" },
|
|
131
|
+
"io.k8s.api.core.v1.KeyToPath": { typeName: "K8s::Core::KeyToPath", description: "Maps a key to a file path" },
|
|
132
|
+
"io.k8s.api.apps.v1.DeploymentStrategy": { typeName: "K8s::Apps::DeploymentStrategy", description: "Deployment rolling update strategy" },
|
|
133
|
+
"io.k8s.api.apps.v1.RollingUpdateDeployment": { typeName: "K8s::Apps::RollingUpdateDeployment", description: "Rolling update parameters" },
|
|
134
|
+
"io.k8s.api.networking.v1.IngressRule": { typeName: "K8s::Networking::IngressRule", description: "Ingress routing rule" },
|
|
135
|
+
"io.k8s.api.networking.v1.IngressTLS": { typeName: "K8s::Networking::IngressTLS", description: "Ingress TLS configuration" },
|
|
136
|
+
"io.k8s.api.networking.v1.HTTPIngressPath": { typeName: "K8s::Networking::HTTPIngressPath", description: "HTTP Ingress path" },
|
|
137
|
+
"io.k8s.api.networking.v1.IngressBackend": { typeName: "K8s::Networking::IngressBackend", description: "Ingress backend reference" },
|
|
138
|
+
"io.k8s.api.networking.v1.IngressServiceBackend": { typeName: "K8s::Networking::IngressServiceBackend", description: "Ingress service backend" },
|
|
139
|
+
"io.k8s.api.networking.v1.ServiceBackendPort": { typeName: "K8s::Networking::ServiceBackendPort", description: "Service port reference" },
|
|
140
|
+
"io.k8s.api.networking.v1.NetworkPolicyIngressRule": { typeName: "K8s::Networking::NetworkPolicyIngressRule", description: "NetworkPolicy ingress rule" },
|
|
141
|
+
"io.k8s.api.networking.v1.NetworkPolicyEgressRule": { typeName: "K8s::Networking::NetworkPolicyEgressRule", description: "NetworkPolicy egress rule" },
|
|
142
|
+
"io.k8s.api.networking.v1.NetworkPolicyPeer": { typeName: "K8s::Networking::NetworkPolicyPeer", description: "NetworkPolicy peer selector" },
|
|
143
|
+
"io.k8s.api.networking.v1.NetworkPolicyPort": { typeName: "K8s::Networking::NetworkPolicyPort", description: "NetworkPolicy port" },
|
|
144
|
+
"io.k8s.api.rbac.v1.PolicyRule": { typeName: "K8s::Rbac::PolicyRule", description: "RBAC policy rule" },
|
|
145
|
+
"io.k8s.api.rbac.v1.RoleRef": { typeName: "K8s::Rbac::RoleRef", description: "RBAC role reference" },
|
|
146
|
+
"io.k8s.api.rbac.v1.Subject": { typeName: "K8s::Rbac::Subject", description: "RBAC subject" },
|
|
147
|
+
"io.k8s.api.autoscaling.v2.MetricSpec": { typeName: "K8s::Autoscaling::MetricSpec", description: "HPA metric specification" },
|
|
148
|
+
"io.k8s.api.autoscaling.v2.HorizontalPodAutoscalerBehavior": { typeName: "K8s::Autoscaling::HorizontalPodAutoscalerBehavior", description: "HPA scaling behavior" },
|
|
149
|
+
"io.k8s.api.policy.v1.PodDisruptionBudgetSpec": { typeName: "K8s::Policy::PodDisruptionBudgetSpec", description: "PDB specification" },
|
|
150
|
+
"io.k8s.apimachinery.pkg.apis.meta.v1.ObjectMeta": { typeName: "K8s::Meta::ObjectMeta", description: "Standard object metadata" },
|
|
151
|
+
"io.k8s.apimachinery.pkg.apis.meta.v1.LabelSelector": { typeName: "K8s::Meta::LabelSelector", description: "Label selector" },
|
|
152
|
+
"io.k8s.apimachinery.pkg.apis.meta.v1.LabelSelectorRequirement": { typeName: "K8s::Meta::LabelSelectorRequirement", description: "Label selector requirement" },
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
// ── Parser ─────────────────────────────────────────────────────────
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Parse the Kubernetes OpenAPI swagger.json into multiple resource results.
|
|
159
|
+
* Returns one result per top-level resource identified by x-kubernetes-group-version-kind.
|
|
160
|
+
*/
|
|
161
|
+
export function parseK8sSwagger(data: string | Buffer): K8sParseResult[] {
|
|
162
|
+
const spec: SwaggerSpec = JSON.parse(typeof data === "string" ? data : data.toString("utf-8"));
|
|
163
|
+
const definitions = spec.definitions ?? {};
|
|
164
|
+
const results: K8sParseResult[] = [];
|
|
165
|
+
|
|
166
|
+
// Phase 1: Extract top-level resources (definitions with GVK)
|
|
167
|
+
for (const [defKey, def] of Object.entries(definitions)) {
|
|
168
|
+
const gvks = def["x-kubernetes-group-version-kind"];
|
|
169
|
+
if (!gvks || gvks.length === 0) continue;
|
|
170
|
+
|
|
171
|
+
// Take the first GVK (most definitions have exactly one)
|
|
172
|
+
const gvk = gvks[0];
|
|
173
|
+
|
|
174
|
+
// Skip internal/legacy API versions — only take the preferred version
|
|
175
|
+
if (!isPreferredVersion(defKey, gvk, definitions)) continue;
|
|
176
|
+
|
|
177
|
+
const typeName = gvkToTypeName(gvk);
|
|
178
|
+
const result = extractResource(defKey, def, typeName, gvk, definitions);
|
|
179
|
+
if (result) results.push(result);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Phase 2: Extract well-known property types
|
|
183
|
+
for (const [defKey, config] of Object.entries(PROPERTY_TYPE_DEFS)) {
|
|
184
|
+
const def = definitions[defKey];
|
|
185
|
+
if (!def) continue;
|
|
186
|
+
|
|
187
|
+
const result = extractPropertyType(defKey, def, config.typeName, config.description, definitions);
|
|
188
|
+
if (result) results.push(result);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return results;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Convert GVK to our type name convention: K8s::{Group}::{Kind}
|
|
196
|
+
*/
|
|
197
|
+
export function gvkToTypeName(gvk: GroupVersionKind): string {
|
|
198
|
+
const group = normalizeGroup(gvk.group);
|
|
199
|
+
return `K8s::${group}::${gvk.kind}`;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Convert GVK to apiVersion string for serialization.
|
|
204
|
+
*/
|
|
205
|
+
export function gvkToApiVersion(gvk: GroupVersionKind): string {
|
|
206
|
+
if (!gvk.group || gvk.group === "") {
|
|
207
|
+
return gvk.version; // core group: "v1"
|
|
208
|
+
}
|
|
209
|
+
return `${gvk.group}/${gvk.version}`; // e.g. "apps/v1"
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Normalize API group to a PascalCase segment.
|
|
214
|
+
* Empty group (core API) → "Core"
|
|
215
|
+
* "apps" → "Apps"
|
|
216
|
+
* "batch" → "Batch"
|
|
217
|
+
* "rbac.authorization.k8s.io" → "Rbac"
|
|
218
|
+
* "networking.k8s.io" → "Networking"
|
|
219
|
+
*/
|
|
220
|
+
function normalizeGroup(group: string): string {
|
|
221
|
+
if (!group || group === "") return "Core";
|
|
222
|
+
|
|
223
|
+
// Take the first segment before any dots
|
|
224
|
+
const firstSegment = group.split(".")[0];
|
|
225
|
+
// Special-case RBAC
|
|
226
|
+
if (firstSegment === "rbac") return "Rbac";
|
|
227
|
+
// PascalCase the first segment
|
|
228
|
+
return firstSegment.charAt(0).toUpperCase() + firstSegment.slice(1);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Check if this definition key represents the preferred API version.
|
|
233
|
+
* Prefer stable (v1) over beta/alpha, and prefer the highest stable version.
|
|
234
|
+
*/
|
|
235
|
+
function isPreferredVersion(defKey: string, gvk: GroupVersionKind, definitions: Record<string, SwaggerDefinition>): boolean {
|
|
236
|
+
// Skip alpha versions
|
|
237
|
+
if (gvk.version.includes("alpha")) return false;
|
|
238
|
+
|
|
239
|
+
// Find all definitions with the same GVK kind+group
|
|
240
|
+
const sameName: Array<{ key: string; version: string }> = [];
|
|
241
|
+
for (const [key, def] of Object.entries(definitions)) {
|
|
242
|
+
const gvks = def["x-kubernetes-group-version-kind"];
|
|
243
|
+
if (!gvks) continue;
|
|
244
|
+
for (const g of gvks) {
|
|
245
|
+
if (g.kind === gvk.kind && g.group === gvk.group) {
|
|
246
|
+
sameName.push({ key, version: g.version });
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
if (sameName.length <= 1) return true;
|
|
252
|
+
|
|
253
|
+
// Prefer stable (v1, v2) over beta
|
|
254
|
+
const stable = sameName.filter((s) => !s.version.includes("beta") && !s.version.includes("alpha"));
|
|
255
|
+
if (stable.length > 0) {
|
|
256
|
+
// Among stable, pick the highest version
|
|
257
|
+
stable.sort((a, b) => b.version.localeCompare(a.version));
|
|
258
|
+
return defKey === stable[0].key;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// All beta — pick highest
|
|
262
|
+
sameName.sort((a, b) => b.version.localeCompare(a.version));
|
|
263
|
+
return defKey === sameName[0].key;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Extract a top-level resource from a swagger definition.
|
|
268
|
+
*/
|
|
269
|
+
function extractResource(
|
|
270
|
+
defKey: string,
|
|
271
|
+
def: SwaggerDefinition,
|
|
272
|
+
typeName: string,
|
|
273
|
+
gvk: GroupVersionKind,
|
|
274
|
+
definitions: Record<string, SwaggerDefinition>,
|
|
275
|
+
): K8sParseResult | null {
|
|
276
|
+
if (!def.properties) return null;
|
|
277
|
+
|
|
278
|
+
const requiredSet = new Set<string>(def.required ?? []);
|
|
279
|
+
|
|
280
|
+
// K8s resources have standard fields (apiVersion, kind, metadata, spec, status)
|
|
281
|
+
// We only expose user-configurable properties — skip apiVersion, kind, status
|
|
282
|
+
const skipProps = new Set(["apiVersion", "kind", "status"]);
|
|
283
|
+
const filteredProps: Record<string, SwaggerProperty> = {};
|
|
284
|
+
for (const [name, prop] of Object.entries(def.properties)) {
|
|
285
|
+
if (!skipProps.has(name)) {
|
|
286
|
+
filteredProps[name] = prop;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
const properties = parseProperties(filteredProps, requiredSet, definitions);
|
|
291
|
+
|
|
292
|
+
return {
|
|
293
|
+
resource: {
|
|
294
|
+
typeName,
|
|
295
|
+
description: def.description,
|
|
296
|
+
properties,
|
|
297
|
+
attributes: [
|
|
298
|
+
{ name: "name", tsType: "string" },
|
|
299
|
+
{ name: "namespace", tsType: "string" },
|
|
300
|
+
{ name: "uid", tsType: "string" },
|
|
301
|
+
],
|
|
302
|
+
deprecatedProperties: [],
|
|
303
|
+
},
|
|
304
|
+
propertyTypes: [],
|
|
305
|
+
enums: [],
|
|
306
|
+
gvk,
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Extract a property type definition (not a top-level resource).
|
|
312
|
+
*/
|
|
313
|
+
function extractPropertyType(
|
|
314
|
+
defKey: string,
|
|
315
|
+
def: SwaggerDefinition,
|
|
316
|
+
typeName: string,
|
|
317
|
+
description: string,
|
|
318
|
+
definitions: Record<string, SwaggerDefinition>,
|
|
319
|
+
): K8sParseResult | null {
|
|
320
|
+
if (!def.properties) return null;
|
|
321
|
+
|
|
322
|
+
const requiredSet = new Set<string>(def.required ?? []);
|
|
323
|
+
const properties = parseProperties(def.properties, requiredSet, definitions);
|
|
324
|
+
|
|
325
|
+
const gvkParts = typeName.split("::");
|
|
326
|
+
return {
|
|
327
|
+
resource: {
|
|
328
|
+
typeName,
|
|
329
|
+
description,
|
|
330
|
+
properties,
|
|
331
|
+
attributes: [],
|
|
332
|
+
deprecatedProperties: [],
|
|
333
|
+
},
|
|
334
|
+
propertyTypes: [],
|
|
335
|
+
enums: [],
|
|
336
|
+
gvk: { group: gvkParts[1]?.toLowerCase() ?? "", version: "v1", kind: gvkParts[2] ?? "" },
|
|
337
|
+
isProperty: true,
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* Parse properties from a swagger definition into ParsedProperty[].
|
|
343
|
+
*/
|
|
344
|
+
function parseProperties(
|
|
345
|
+
properties: Record<string, SwaggerProperty>,
|
|
346
|
+
requiredSet: Set<string>,
|
|
347
|
+
definitions: Record<string, SwaggerDefinition>,
|
|
348
|
+
): ParsedProperty[] {
|
|
349
|
+
const result: ParsedProperty[] = [];
|
|
350
|
+
|
|
351
|
+
for (const [name, prop] of Object.entries(properties)) {
|
|
352
|
+
const tsType = resolvePropertyType(prop, definitions);
|
|
353
|
+
result.push({
|
|
354
|
+
name,
|
|
355
|
+
tsType,
|
|
356
|
+
required: requiredSet.has(name),
|
|
357
|
+
description: prop.description,
|
|
358
|
+
enum: prop.enum,
|
|
359
|
+
constraints: coreExtractConstraints(prop as JsonSchemaProperty),
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
return result;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* Resolve a swagger property to its TypeScript type string.
|
|
368
|
+
*/
|
|
369
|
+
function resolvePropertyType(prop: SwaggerProperty, definitions: Record<string, SwaggerDefinition>): string {
|
|
370
|
+
if (!prop) return "any";
|
|
371
|
+
|
|
372
|
+
// Handle $ref
|
|
373
|
+
if (prop.$ref) {
|
|
374
|
+
return resolveRefType(prop.$ref, definitions);
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// x-kubernetes-int-or-string
|
|
378
|
+
if (prop["x-kubernetes-int-or-string"]) {
|
|
379
|
+
return "string | number";
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// Inline enum
|
|
383
|
+
if (prop.enum && prop.enum.length > 0) {
|
|
384
|
+
return prop.enum.map((v) => JSON.stringify(v)).join(" | ");
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
const pt = primaryType(prop.type);
|
|
388
|
+
switch (pt) {
|
|
389
|
+
case "string":
|
|
390
|
+
return "string";
|
|
391
|
+
case "integer":
|
|
392
|
+
case "number":
|
|
393
|
+
return "number";
|
|
394
|
+
case "boolean":
|
|
395
|
+
return "boolean";
|
|
396
|
+
case "array":
|
|
397
|
+
if (prop.items) {
|
|
398
|
+
const itemType = resolvePropertyType(prop.items, definitions);
|
|
399
|
+
if (itemType.includes(" | ")) return `(${itemType})[]`;
|
|
400
|
+
return `${itemType}[]`;
|
|
401
|
+
}
|
|
402
|
+
return "any[]";
|
|
403
|
+
case "object":
|
|
404
|
+
if (prop.additionalProperties && typeof prop.additionalProperties === "object") {
|
|
405
|
+
const valueType = resolvePropertyType(prop.additionalProperties, definitions);
|
|
406
|
+
return `Record<string, ${valueType}>`;
|
|
407
|
+
}
|
|
408
|
+
return "Record<string, any>";
|
|
409
|
+
default:
|
|
410
|
+
return "any";
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/**
|
|
415
|
+
* Resolve a $ref to a TypeScript type.
|
|
416
|
+
*/
|
|
417
|
+
function resolveRefType(ref: string, definitions: Record<string, SwaggerDefinition>): string {
|
|
418
|
+
const prefix = "#/definitions/";
|
|
419
|
+
if (!ref.startsWith(prefix)) return "any";
|
|
420
|
+
|
|
421
|
+
const defKey = ref.slice(prefix.length);
|
|
422
|
+
|
|
423
|
+
// Well-known special types
|
|
424
|
+
if (defKey === "io.k8s.apimachinery.pkg.util.intstr.IntOrString") return "string | number";
|
|
425
|
+
if (defKey === "io.k8s.apimachinery.pkg.api.resource.Quantity") return "string";
|
|
426
|
+
if (defKey === "io.k8s.apimachinery.pkg.apis.meta.v1.Time") return "string";
|
|
427
|
+
if (defKey === "io.k8s.apimachinery.pkg.apis.meta.v1.MicroTime") return "string";
|
|
428
|
+
if (defKey === "io.k8s.apimachinery.pkg.runtime.RawExtension") return "Record<string, any>";
|
|
429
|
+
|
|
430
|
+
// Check if it's a known property type
|
|
431
|
+
const ptConfig = PROPERTY_TYPE_DEFS[defKey];
|
|
432
|
+
if (ptConfig) {
|
|
433
|
+
return k8sShortName(ptConfig.typeName);
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
// Resolve the definition
|
|
437
|
+
const def = definitions[defKey];
|
|
438
|
+
if (!def) return "any";
|
|
439
|
+
|
|
440
|
+
// Enum
|
|
441
|
+
if (def.enum && def.enum.length > 0 && !def.properties) {
|
|
442
|
+
return def.enum.map((v) => JSON.stringify(v)).join(" | ");
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
// Primitive type
|
|
446
|
+
if (def.type && !def.properties) {
|
|
447
|
+
const pt = primaryType(def.type);
|
|
448
|
+
switch (pt) {
|
|
449
|
+
case "string": return "string";
|
|
450
|
+
case "integer":
|
|
451
|
+
case "number": return "number";
|
|
452
|
+
case "boolean": return "boolean";
|
|
453
|
+
default: return "any";
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
// Object with properties — use Record
|
|
458
|
+
if (def.properties) return "Record<string, any>";
|
|
459
|
+
|
|
460
|
+
return "any";
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
/**
|
|
464
|
+
* Extract short name: "K8s::Apps::Deployment" → "Deployment"
|
|
465
|
+
*/
|
|
466
|
+
export function k8sShortName(typeName: string): string {
|
|
467
|
+
const parts = typeName.split("::");
|
|
468
|
+
return parts[parts.length - 1];
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
/**
|
|
472
|
+
* Extract service/group name: "K8s::Apps::Deployment" → "Apps"
|
|
473
|
+
*/
|
|
474
|
+
export function k8sServiceName(typeName: string): string {
|
|
475
|
+
const parts = typeName.split("::");
|
|
476
|
+
return parts.length >= 2 ? parts[1] : "Core";
|
|
477
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
apiVersion: apps/v1
|
|
2
|
+
kind: Deployment
|
|
3
|
+
metadata:
|
|
4
|
+
name: nginx
|
|
5
|
+
labels:
|
|
6
|
+
app.kubernetes.io/name: nginx
|
|
7
|
+
spec:
|
|
8
|
+
replicas: 2
|
|
9
|
+
selector:
|
|
10
|
+
matchLabels:
|
|
11
|
+
app.kubernetes.io/name: nginx
|
|
12
|
+
template:
|
|
13
|
+
metadata:
|
|
14
|
+
labels:
|
|
15
|
+
app.kubernetes.io/name: nginx
|
|
16
|
+
spec:
|
|
17
|
+
containers:
|
|
18
|
+
- name: nginx
|
|
19
|
+
image: nginx:1.25
|
|
20
|
+
ports:
|
|
21
|
+
- containerPort: 80
|
|
22
|
+
name: http
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
apiVersion: apps/v1
|
|
2
|
+
kind: Deployment
|
|
3
|
+
metadata:
|
|
4
|
+
name: web-app
|
|
5
|
+
labels:
|
|
6
|
+
app.kubernetes.io/name: web-app
|
|
7
|
+
spec:
|
|
8
|
+
replicas: 3
|
|
9
|
+
selector:
|
|
10
|
+
matchLabels:
|
|
11
|
+
app.kubernetes.io/name: web-app
|
|
12
|
+
template:
|
|
13
|
+
metadata:
|
|
14
|
+
labels:
|
|
15
|
+
app.kubernetes.io/name: web-app
|
|
16
|
+
spec:
|
|
17
|
+
containers:
|
|
18
|
+
- name: web
|
|
19
|
+
image: web-app:1.0
|
|
20
|
+
ports:
|
|
21
|
+
- containerPort: 8080
|
|
22
|
+
name: http
|
|
23
|
+
env:
|
|
24
|
+
- name: DB_HOST
|
|
25
|
+
value: postgres
|
|
26
|
+
---
|
|
27
|
+
apiVersion: v1
|
|
28
|
+
kind: Service
|
|
29
|
+
metadata:
|
|
30
|
+
name: web-app
|
|
31
|
+
spec:
|
|
32
|
+
selector:
|
|
33
|
+
app.kubernetes.io/name: web-app
|
|
34
|
+
ports:
|
|
35
|
+
- port: 80
|
|
36
|
+
targetPort: 8080
|
|
37
|
+
name: http
|
|
38
|
+
---
|
|
39
|
+
apiVersion: networking.k8s.io/v1
|
|
40
|
+
kind: Ingress
|
|
41
|
+
metadata:
|
|
42
|
+
name: web-app
|
|
43
|
+
spec:
|
|
44
|
+
rules:
|
|
45
|
+
- host: web-app.example.com
|
|
46
|
+
http:
|
|
47
|
+
paths:
|
|
48
|
+
- path: /
|
|
49
|
+
pathType: Prefix
|
|
50
|
+
backend:
|
|
51
|
+
service:
|
|
52
|
+
name: web-app
|
|
53
|
+
port:
|
|
54
|
+
number: 80
|
|
55
|
+
---
|
|
56
|
+
apiVersion: v1
|
|
57
|
+
kind: ConfigMap
|
|
58
|
+
metadata:
|
|
59
|
+
name: web-app-config
|
|
60
|
+
data:
|
|
61
|
+
APP_ENV: production
|