1/**
2 * Internal Kubernetes object model + ordering/path helpers powering
3 * `Kubernetes.Manifest` and the workload platforms (`Deployment`, `Job`).
4 * Not part of the public surface — the public manifest shape is the literal
5 * `KubernetesManifest` on `Kubernetes.Manifest`.
6 */
7
8export interface KubernetesObjectMetadata {
9 name: string;
10 namespace?: string;
11 labels?: Record<string, string>;
12 annotations?: Record<string, string>;
13}
14
15export type KubernetesObjectDefinition = {
16 apiVersion: string;
17 kind: string;
18 metadata: KubernetesObjectMetadata;
19} & Record<string, unknown>;
20
21export interface KubernetesObjectRef {
22 apiVersion: string;
23 kind: string;
24 name: string;
25 namespace?: string;
26}
27
28export interface KubernetesObjectBinding {
29 type: "kubernetes-object";
30 object: KubernetesObjectDefinition;
31}
32
33export type KubernetesObjectScope = "Cluster" | "Namespaced";
34
35export interface KubernetesObjectKindSpec {
36 plural: string;
37 scope: KubernetesObjectScope;
38 applyRank: number;
39}
40
41/** Apply rank for kinds not in the static table (applied last, deleted first). */
42export const DEFAULT_APPLY_RANK = 100;
43
44const supportedKinds: Record<string, KubernetesObjectKindSpec> = {
45 "v1/Namespace": {
46 plural: "namespaces",
47 scope: "Cluster",
48 applyRank: 10,
49 },
50 // CRDs apply right after namespaces so a chart's custom resources
51 // (default rank 100) always find their definition registered.
52 "apiextensions.k8s.io/v1/CustomResourceDefinition": {
53 plural: "customresourcedefinitions",
54 scope: "Cluster",
55 applyRank: 15,
56 },
57 "v1/ServiceAccount": {
58 plural: "serviceaccounts",
59 scope: "Namespaced",
60 applyRank: 20,
61 },
62 "v1/ConfigMap": {
63 plural: "configmaps",
64 scope: "Namespaced",
65 applyRank: 30,
66 },
67 "v1/Secret": {
68 plural: "secrets",
69 scope: "Namespaced",
70 applyRank: 30,
71 },
72 "v1/Service": {
73 plural: "services",
74 scope: "Namespaced",
75 applyRank: 40,
76 },
77 "apps/v1/Deployment": {
78 plural: "deployments",
79 scope: "Namespaced",
80 applyRank: 50,
81 },
82 "apps/v1/StatefulSet": {
83 plural: "statefulsets",
84 scope: "Namespaced",
85 applyRank: 50,
86 },
87 "apps/v1/DaemonSet": {
88 plural: "daemonsets",
89 scope: "Namespaced",
90 applyRank: 50,
91 },
92 "batch/v1/Job": {
93 plural: "jobs",
94 scope: "Namespaced",
95 applyRank: 60,
96 },
97 "batch/v1/CronJob": {
98 plural: "cronjobs",
99 scope: "Namespaced",
100 applyRank: 60,
101 },
102 "v1/Pod": {
103 plural: "pods",
104 scope: "Namespaced",
105 applyRank: 60,
106 },
107};
108
109const objectTypeKey = (
110 input: Pick<KubernetesObjectRef, "apiVersion" | "kind">,
111) => `${input.apiVersion}/${input.kind}`;
112
113/** Look up the static kind table; `undefined` for kinds needing discovery. */
114export const lookupKubernetesKindSpec = (
115 input: Pick<KubernetesObjectRef, "apiVersion" | "kind">,
116): KubernetesObjectKindSpec | undefined => supportedKinds[objectTypeKey(input)];
117
118export const getKubernetesKindSpec = (
119 input: Pick<KubernetesObjectRef, "apiVersion" | "kind">,
120) => {
121 const spec = lookupKubernetesKindSpec(input);
122 if (!spec) {
123 throw new Error(
124 `Unsupported Kubernetes object ${input.apiVersion}/${input.kind}`,
125 );
126 }
127 return spec;
128};
129
130const applyRankOf = (input: Pick<KubernetesObjectRef, "apiVersion" | "kind">) =>
131 lookupKubernetesKindSpec(input)?.applyRank ?? DEFAULT_APPLY_RANK;
132
133export const toKubernetesObjectRef = (
134 object: KubernetesObjectDefinition,
135): KubernetesObjectRef => ({
136 apiVersion: object.apiVersion,
137 kind: object.kind,
138 name: object.metadata.name,
139 namespace: object.metadata.namespace,
140});
141
142export const kubernetesObjectKey = (
143 input: Pick<
144 KubernetesObjectRef,
145 "apiVersion" | "kind" | "name" | "namespace"
146 >,
147) =>
148 [
149 input.apiVersion,
150 input.kind,
151 input.namespace ?? "_cluster",
152 input.name,
153 ].join("/");
154
155const compareRefs = (a: KubernetesObjectRef, b: KubernetesObjectRef) =>
156 kubernetesObjectKey(a).localeCompare(kubernetesObjectKey(b));
157
158export const sortObjectsForApply = (
159 objects: ReadonlyArray<KubernetesObjectDefinition>,
160) =>
161 [...objects].sort(
162 (a, b) =>
163 applyRankOf(a) - applyRankOf(b) ||
164 compareRefs(toKubernetesObjectRef(a), toKubernetesObjectRef(b)),
165 );
166
167export const sortRefsForDelete = (
168 objects: ReadonlyArray<KubernetesObjectRef>,
169) =>
170 [...objects].sort(
171 (a, b) => applyRankOf(b) - applyRankOf(a) || compareRefs(a, b),
172 );
173
174export const chunkByApplyRank = (
175 objects: ReadonlyArray<KubernetesObjectDefinition>,
176) => {
177 const chunks: KubernetesObjectDefinition[][] = [];
178
179 for (const object of sortObjectsForApply(objects)) {
180 const rank = applyRankOf(object);
181 const current = chunks[chunks.length - 1];
182 if (!current) {
183 chunks.push([object]);
184 continue;
185 }
186
187 const currentRank = applyRankOf(current[0]);
188 if (currentRank === rank) {
189 current.push(object);
190 } else {
191 chunks.push([object]);
192 }
193 }
194
195 return chunks;
196};
197
198/**
199 * Build the REST path for an object given its (statically-known or
200 * discovered) kind spec.
201 */
202export const buildKubernetesObjectPathWithSpec = (
203 input: Pick<
204 KubernetesObjectRef,
205 "apiVersion" | "kind" | "name" | "namespace"
206 >,
207 spec: KubernetesObjectKindSpec,
208) => {
209 const [group, version] = input.apiVersion.includes("/")
210 ? input.apiVersion.split("/", 2)
211 : [undefined, input.apiVersion];
212
213 const base = group ? `/apis/${group}/${version}` : `/api/${version}`;
214
215 if (spec.scope === "Namespaced") {
216 if (!input.namespace) {
217 throw new Error(
218 `Kubernetes object ${input.apiVersion}/${input.kind}/${input.name} requires a namespace`,
219 );
220 }
221
222 return `${base}/namespaces/${input.namespace}/${spec.plural}/${input.name}`;
223 }
224
225 return `${base}/${spec.plural}/${input.name}`;
226};
227
228export const buildKubernetesObjectPath = (
229 input: Pick<
230 KubernetesObjectRef,
231 "apiVersion" | "kind" | "name" | "namespace"
232 >,
233) => buildKubernetesObjectPathWithSpec(input, getKubernetesKindSpec(input));