@intentius/chant-lexicon-k8s 0.0.15 → 0.0.18

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.
@@ -0,0 +1,421 @@
1
+ /**
2
+ * CockroachDbCluster composite — StatefulSet + Services + RBAC + PDB + Jobs.
3
+ *
4
+ * Deploys a CockroachDB cluster on Kubernetes with TLS support via self-signed
5
+ * certificates. Produces all K8s resources needed for a single cloud's slice of
6
+ * a CockroachDB cluster (typically 3 nodes). Multi-cloud deployments use one
7
+ * CockroachDbCluster per cloud, sharing joinAddresses across clouds.
8
+ */
9
+
10
+ export interface CockroachDbClusterProps {
11
+ /** Cluster name — used in metadata, labels, and service names. */
12
+ name: string;
13
+ /** Namespace for all namespaced resources. */
14
+ namespace?: string;
15
+ /** Number of StatefulSet replicas (default: 3). */
16
+ replicas?: number;
17
+ /** CockroachDB container image (default: "cockroachdb/cockroach:v24.3.0"). */
18
+ image?: string;
19
+ /** PVC storage size per node (default: "100Gi"). */
20
+ storageSize?: string;
21
+ /** StorageClass name for PVCs. */
22
+ storageClassName?: string;
23
+ /** CPU limit per pod (default: "2"). */
24
+ cpuLimit?: string;
25
+ /** Memory limit per pod (default: "8Gi"). */
26
+ memoryLimit?: string;
27
+ /** Fraction of container memory for CockroachDB cache (default: ".25"). */
28
+ cachePercent?: string;
29
+ /** Fraction of container memory for SQL temp storage (default: ".25"). */
30
+ sqlMemoryPercent?: string;
31
+ /** CockroachDB locality flag (e.g., "cloud=aws,region=us-east-1"). */
32
+ locality?: string;
33
+ /** All node DNS names for --join (cross-cloud cluster membership). */
34
+ joinAddresses?: string[];
35
+ /** Enable TLS via self-signed CA certs (default: true). */
36
+ secure?: boolean;
37
+ /** Additional labels to apply to all resources. */
38
+ labels?: Record<string, string>;
39
+ }
40
+
41
+ export interface CockroachDbClusterResult {
42
+ serviceAccount: Record<string, unknown>;
43
+ role: Record<string, unknown>;
44
+ roleBinding: Record<string, unknown>;
45
+ clusterRole: Record<string, unknown>;
46
+ clusterRoleBinding: Record<string, unknown>;
47
+ /** Client-facing service (ClusterIP, ports 26257+8080). */
48
+ publicService: Record<string, unknown>;
49
+ /** Pod discovery service (headless, publishNotReadyAddresses). */
50
+ headlessService: Record<string, unknown>;
51
+ pdb: Record<string, unknown>;
52
+ statefulSet: Record<string, unknown>;
53
+ /** One-shot cockroach init job. */
54
+ initJob: Record<string, unknown>;
55
+ /** Generates self-signed CA + node certs, stores in Secrets. */
56
+ certGenJob: Record<string, unknown>;
57
+ }
58
+
59
+ /**
60
+ * Create a CockroachDbCluster composite — returns prop objects for a full
61
+ * CockroachDB StatefulSet deployment including RBAC, Services, PDB, and Jobs.
62
+ *
63
+ * @example
64
+ * ```ts
65
+ * import { CockroachDbCluster } from "@intentius/chant-lexicon-k8s";
66
+ *
67
+ * const crdb = CockroachDbCluster({
68
+ * name: "cockroachdb",
69
+ * namespace: "crdb",
70
+ * replicas: 3,
71
+ * locality: "cloud=aws,region=us-east-1",
72
+ * joinAddresses: [
73
+ * "cockroachdb-0.cockroachdb.crdb.svc.cluster.local",
74
+ * "cockroachdb-1.cockroachdb.crdb.svc.cluster.local",
75
+ * "cockroachdb-2.cockroachdb.crdb.svc.cluster.local",
76
+ * ],
77
+ * });
78
+ * ```
79
+ */
80
+ export function CockroachDbCluster(props: CockroachDbClusterProps): CockroachDbClusterResult {
81
+ const {
82
+ name,
83
+ namespace,
84
+ replicas = 3,
85
+ image = "cockroachdb/cockroach:v24.3.0",
86
+ storageSize = "100Gi",
87
+ storageClassName,
88
+ cpuLimit = "2",
89
+ memoryLimit = "8Gi",
90
+ cachePercent = ".25",
91
+ sqlMemoryPercent = ".25",
92
+ locality,
93
+ joinAddresses = [],
94
+ secure = true,
95
+ labels: extraLabels = {},
96
+ } = props;
97
+
98
+ const saName = name;
99
+ const certsDir = "/cockroach/cockroach-certs";
100
+ const dataDir = "/cockroach/cockroach-data";
101
+
102
+ const commonLabels: Record<string, string> = {
103
+ "app.kubernetes.io/name": name,
104
+ "app.kubernetes.io/managed-by": "chant",
105
+ ...extraLabels,
106
+ };
107
+
108
+ // ── RBAC ─────────────────────────────────────────────────────────
109
+
110
+ const serviceAccount: Record<string, unknown> = {
111
+ metadata: {
112
+ name: saName,
113
+ ...(namespace && { namespace }),
114
+ labels: { ...commonLabels, "app.kubernetes.io/component": "database" },
115
+ },
116
+ };
117
+
118
+ const role: Record<string, unknown> = {
119
+ metadata: {
120
+ name,
121
+ ...(namespace && { namespace }),
122
+ labels: { ...commonLabels, "app.kubernetes.io/component": "rbac" },
123
+ },
124
+ rules: [
125
+ { apiGroups: [""], resources: ["secrets"], verbs: ["get", "create", "patch"] },
126
+ ],
127
+ };
128
+
129
+ const roleBinding: Record<string, unknown> = {
130
+ metadata: {
131
+ name,
132
+ ...(namespace && { namespace }),
133
+ labels: { ...commonLabels, "app.kubernetes.io/component": "rbac" },
134
+ },
135
+ roleRef: {
136
+ apiGroup: "rbac.authorization.k8s.io",
137
+ kind: "Role",
138
+ name,
139
+ },
140
+ subjects: [
141
+ { kind: "ServiceAccount", name: saName, ...(namespace && { namespace }) },
142
+ ],
143
+ };
144
+
145
+ const clusterRole: Record<string, unknown> = {
146
+ metadata: {
147
+ name,
148
+ labels: { ...commonLabels, "app.kubernetes.io/component": "rbac" },
149
+ },
150
+ rules: [
151
+ { apiGroups: ["certificates.k8s.io"], resources: ["certificatesigningrequests"], verbs: ["get", "create", "watch"] },
152
+ ],
153
+ };
154
+
155
+ const clusterRoleBinding: Record<string, unknown> = {
156
+ metadata: {
157
+ name,
158
+ labels: { ...commonLabels, "app.kubernetes.io/component": "rbac" },
159
+ },
160
+ roleRef: {
161
+ apiGroup: "rbac.authorization.k8s.io",
162
+ kind: "ClusterRole",
163
+ name,
164
+ },
165
+ subjects: [
166
+ { kind: "ServiceAccount", name: saName, ...(namespace && { namespace }) },
167
+ ],
168
+ };
169
+
170
+ // ── Services ────────────────────────────────────────────────────
171
+
172
+ const publicService: Record<string, unknown> = {
173
+ metadata: {
174
+ name: `${name}-public`,
175
+ ...(namespace && { namespace }),
176
+ labels: { ...commonLabels, "app.kubernetes.io/component": "database" },
177
+ },
178
+ spec: {
179
+ selector: { "app.kubernetes.io/name": name },
180
+ ports: [
181
+ { port: 26257, targetPort: 26257, protocol: "TCP", name: "grpc" },
182
+ { port: 8080, targetPort: 8080, protocol: "TCP", name: "http" },
183
+ ],
184
+ type: "ClusterIP",
185
+ },
186
+ };
187
+
188
+ const headlessService: Record<string, unknown> = {
189
+ metadata: {
190
+ name,
191
+ ...(namespace && { namespace }),
192
+ labels: { ...commonLabels, "app.kubernetes.io/component": "database" },
193
+ annotations: {
194
+ "service.alpha.kubernetes.io/tolerate-unready-endpoints": "true",
195
+ },
196
+ },
197
+ spec: {
198
+ selector: { "app.kubernetes.io/name": name },
199
+ ports: [
200
+ { port: 26257, targetPort: 26257, protocol: "TCP", name: "grpc" },
201
+ { port: 8080, targetPort: 8080, protocol: "TCP", name: "http" },
202
+ ],
203
+ clusterIP: "None",
204
+ publishNotReadyAddresses: true,
205
+ },
206
+ };
207
+
208
+ // ── PodDisruptionBudget ─────────────────────────────────────────
209
+
210
+ const pdb: Record<string, unknown> = {
211
+ metadata: {
212
+ name,
213
+ ...(namespace && { namespace }),
214
+ labels: { ...commonLabels, "app.kubernetes.io/component": "disruption-budget" },
215
+ },
216
+ spec: {
217
+ maxUnavailable: 1,
218
+ selector: { matchLabels: { "app.kubernetes.io/name": name } },
219
+ },
220
+ };
221
+
222
+ // ── StatefulSet ─────────────────────────────────────────────────
223
+
224
+ const cockroachArgs = [
225
+ "start",
226
+ `--logtostderr=WARNING`,
227
+ `--certs-dir=${secure ? certsDir : ""}`,
228
+ ...(secure ? [] : ["--insecure"]),
229
+ `--advertise-host=$(hostname -f)`,
230
+ `--http-addr=0.0.0.0`,
231
+ `--cache=${cachePercent}`,
232
+ `--max-sql-memory=${sqlMemoryPercent}`,
233
+ ...(joinAddresses.length > 0 ? [`--join=${joinAddresses.join(",")}`] : []),
234
+ ...(locality ? [`--locality=${locality}`] : []),
235
+ ];
236
+
237
+ const volumes: Record<string, unknown>[] = [];
238
+ const volumeMounts: Record<string, unknown>[] = [
239
+ { name: "datadir", mountPath: dataDir },
240
+ ];
241
+
242
+ if (secure) {
243
+ volumes.push({ name: "certs", secret: { secretName: `${name}-node-certs`, defaultMode: 0o400 } });
244
+ volumeMounts.push({ name: "certs", mountPath: certsDir });
245
+ }
246
+
247
+ const container: Record<string, unknown> = {
248
+ name,
249
+ image,
250
+ ports: [
251
+ { containerPort: 26257, name: "grpc" },
252
+ { containerPort: 8080, name: "http" },
253
+ ],
254
+ command: ["/cockroach/cockroach"],
255
+ args: cockroachArgs,
256
+ resources: {
257
+ limits: { cpu: cpuLimit, memory: memoryLimit },
258
+ requests: { cpu: cpuLimit, memory: memoryLimit },
259
+ },
260
+ volumeMounts,
261
+ env: [
262
+ { name: "COCKROACH_CHANNEL", value: "kubernetes-multiregion" },
263
+ ],
264
+ readinessProbe: {
265
+ httpGet: { path: "/health?ready=1", port: 8080, ...(secure && { scheme: "HTTPS" }) },
266
+ initialDelaySeconds: 10,
267
+ periodSeconds: 5,
268
+ failureThreshold: 2,
269
+ },
270
+ livenessProbe: {
271
+ httpGet: { path: "/health", port: 8080, ...(secure && { scheme: "HTTPS" }) },
272
+ initialDelaySeconds: 30,
273
+ periodSeconds: 5,
274
+ },
275
+ };
276
+
277
+ const statefulSet: Record<string, unknown> = {
278
+ metadata: {
279
+ name,
280
+ ...(namespace && { namespace }),
281
+ labels: { ...commonLabels, "app.kubernetes.io/component": "database" },
282
+ },
283
+ spec: {
284
+ serviceName: name,
285
+ replicas,
286
+ podManagementPolicy: "Parallel",
287
+ selector: { matchLabels: { "app.kubernetes.io/name": name } },
288
+ template: {
289
+ metadata: { labels: { "app.kubernetes.io/name": name, ...extraLabels } },
290
+ spec: {
291
+ serviceAccountName: saName,
292
+ terminationGracePeriodSeconds: 60,
293
+ containers: [container],
294
+ ...(volumes.length > 0 && { volumes }),
295
+ affinity: {
296
+ podAntiAffinity: {
297
+ preferredDuringSchedulingIgnoredDuringExecution: [
298
+ {
299
+ weight: 100,
300
+ podAffinityTerm: {
301
+ labelSelector: { matchLabels: { "app.kubernetes.io/name": name } },
302
+ topologyKey: "kubernetes.io/hostname",
303
+ },
304
+ },
305
+ ],
306
+ },
307
+ },
308
+ },
309
+ },
310
+ volumeClaimTemplates: [
311
+ {
312
+ metadata: { name: "datadir" },
313
+ spec: {
314
+ accessModes: ["ReadWriteOnce"],
315
+ ...(storageClassName && { storageClassName }),
316
+ resources: { requests: { storage: storageSize } },
317
+ },
318
+ },
319
+ ],
320
+ },
321
+ };
322
+
323
+ // ── cert-gen Job ─────────────────────────────────────────────────
324
+
325
+ // Generates self-signed CA and node certs, stores them in K8s Secrets.
326
+ // Each node's cert includes the pod DNS names (pod-N.svc.namespace.svc.cluster.local).
327
+ const nodeNames = Array.from({ length: replicas }, (_, i) => `${name}-${i}.${name}`);
328
+ const nodeAddresses = namespace
329
+ ? nodeNames.map((n) => `${n}.${namespace}.svc.cluster.local`)
330
+ : nodeNames.map((n) => `${n}.default.svc.cluster.local`);
331
+
332
+ const certGenScript = [
333
+ "set -ex",
334
+ "cd /cockroach",
335
+ "cockroach cert create-ca --certs-dir=certs --ca-key=certs/ca.key",
336
+ `cockroach cert create-node ${nodeAddresses.join(" ")} localhost 127.0.0.1 --certs-dir=certs --ca-key=certs/ca.key`,
337
+ "cockroach cert create-client root --certs-dir=certs --ca-key=certs/ca.key",
338
+ ].join(" && ");
339
+
340
+ const certGenJob: Record<string, unknown> = {
341
+ metadata: {
342
+ name: `${name}-cert-gen`,
343
+ ...(namespace && { namespace }),
344
+ labels: { ...commonLabels, "app.kubernetes.io/component": "cert-gen" },
345
+ },
346
+ spec: {
347
+ backoffLimit: 3,
348
+ ttlSecondsAfterFinished: 3600,
349
+ template: {
350
+ metadata: { labels: { "app.kubernetes.io/name": name, ...extraLabels } },
351
+ spec: {
352
+ serviceAccountName: saName,
353
+ restartPolicy: "OnFailure",
354
+ containers: [
355
+ {
356
+ name: "cert-gen",
357
+ image,
358
+ command: ["bash", "-c", certGenScript],
359
+ },
360
+ ],
361
+ },
362
+ },
363
+ },
364
+ };
365
+
366
+ // ── init Job ────────────────────────────────────────────────────
367
+
368
+ const initArgs = secure
369
+ ? [`--certs-dir=${certsDir}`, `--host=${name}-0.${name}`]
370
+ : ["--insecure", `--host=${name}-0.${name}`];
371
+
372
+ const initVolumes: Record<string, unknown>[] = [];
373
+ const initVolumeMounts: Record<string, unknown>[] = [];
374
+ if (secure) {
375
+ initVolumes.push({ name: "client-certs", secret: { secretName: `${name}-node-certs`, defaultMode: 0o400 } });
376
+ initVolumeMounts.push({ name: "client-certs", mountPath: certsDir });
377
+ }
378
+
379
+ const initJob: Record<string, unknown> = {
380
+ metadata: {
381
+ name: `${name}-init`,
382
+ ...(namespace && { namespace }),
383
+ labels: { ...commonLabels, "app.kubernetes.io/component": "init" },
384
+ },
385
+ spec: {
386
+ backoffLimit: 6,
387
+ ttlSecondsAfterFinished: 3600,
388
+ template: {
389
+ metadata: { labels: { "app.kubernetes.io/name": name, ...extraLabels } },
390
+ spec: {
391
+ serviceAccountName: saName,
392
+ restartPolicy: "OnFailure",
393
+ containers: [
394
+ {
395
+ name: "cluster-init",
396
+ image,
397
+ command: ["/cockroach/cockroach"],
398
+ args: ["init", ...initArgs],
399
+ ...(initVolumeMounts.length > 0 && { volumeMounts: initVolumeMounts }),
400
+ },
401
+ ],
402
+ ...(initVolumes.length > 0 && { volumes: initVolumes }),
403
+ },
404
+ },
405
+ },
406
+ };
407
+
408
+ return {
409
+ serviceAccount,
410
+ role,
411
+ roleBinding,
412
+ clusterRole,
413
+ clusterRoleBinding,
414
+ publicService,
415
+ headlessService,
416
+ pdb,
417
+ statefulSet,
418
+ initJob,
419
+ certGenJob,
420
+ };
421
+ }