|
1 | 1 | /** |
2 | 2 | * Shared Knative Service helpers — used by both the seed script |
3 | 3 | * and the function:sync-resources queue handler. |
| 4 | + * |
| 5 | + * Spec shape mirrors constructive-cloud's Go operator |
| 6 | + * (operator/internal/resources/knative.go) so the two systems |
| 7 | + * produce functionally identical Knative Services. |
4 | 8 | */ |
5 | 9 |
|
6 | 10 | import type { Pool } from 'pg'; |
7 | 11 |
|
| 12 | +// ── Constants ──────────────────────────────────────────────────────────────── |
| 13 | + |
| 14 | +const DEFAULT_PORT = 8080; |
| 15 | +const DEFAULT_TIMEOUT = 300; |
| 16 | +const DEFAULT_SCALE_TARGET = 50; |
| 17 | +const MANAGED_BY = 'provisioning-handlers'; |
| 18 | + |
| 19 | +// ── Types ──────────────────────────────────────────────────────────────────── |
| 20 | + |
| 21 | +export interface KnativeServiceSpec { |
| 22 | + apiVersion: 'serving.knative.dev/v1'; |
| 23 | + kind: 'Service'; |
| 24 | + metadata: { |
| 25 | + name: string; |
| 26 | + namespace: string; |
| 27 | + labels: Record<string, string>; |
| 28 | + annotations: Record<string, string>; |
| 29 | + resourceVersion?: string; |
| 30 | + }; |
| 31 | + spec: { |
| 32 | + template: { |
| 33 | + metadata: { |
| 34 | + labels: Record<string, string>; |
| 35 | + annotations?: Record<string, string>; |
| 36 | + }; |
| 37 | + spec: { |
| 38 | + containerConcurrency?: number; |
| 39 | + timeoutSeconds: number; |
| 40 | + containers: Array<{ |
| 41 | + image: string; |
| 42 | + ports: Array<{ containerPort: number }>; |
| 43 | + envFrom: Array<{ secretRef: { name: string } }>; |
| 44 | + resources?: Record<string, unknown>; |
| 45 | + volumeMounts: Array<{ name: string; mountPath: string }>; |
| 46 | + }>; |
| 47 | + volumes: Array<{ name: string; emptyDir: Record<string, never> }>; |
| 48 | + }; |
| 49 | + }; |
| 50 | + }; |
| 51 | +} |
| 52 | + |
| 53 | +// ── Labels (matches operator/internal/util/labels.go) ──────────────────────── |
| 54 | + |
| 55 | +function componentLabels(namespaceName: string, fnName: string): Record<string, string> { |
| 56 | + return { |
| 57 | + 'app.kubernetes.io/managed-by': MANAGED_BY, |
| 58 | + 'app.kubernetes.io/part-of': namespaceName, |
| 59 | + 'app.kubernetes.io/component': 'function', |
| 60 | + 'app.kubernetes.io/name': fnName, |
| 61 | + 'app.kubernetes.io/instance': `${namespaceName}-${fnName}`, |
| 62 | + 'networking.knative.dev/visibility': 'cluster-local', |
| 63 | + }; |
| 64 | +} |
| 65 | + |
| 66 | +// ── Builder ────────────────────────────────────────────────────────────────── |
| 67 | + |
8 | 68 | /** |
9 | | - * Build the Knative Service spec from a function definition row. |
| 69 | + * Build a Knative Service spec from a function definition row. |
| 70 | + * |
| 71 | + * Mirrors the operator's `BuildKnativeService()`: |
| 72 | + * - Standard k8s labels + cluster-local visibility |
| 73 | + * - Autoscaling annotations (minScale, maxScale, target) |
| 74 | + * - Explicit containerPort |
| 75 | + * - /tmp emptyDir volume (writable scratch space) |
| 76 | + * - envFrom bulk secret ref |
10 | 77 | */ |
11 | 78 | export function buildKnativeServiceSpec( |
12 | 79 | fnRow: Record<string, unknown>, |
13 | 80 | namespaceName: string |
14 | | -) { |
| 81 | +): KnativeServiceSpec { |
| 82 | + const fnName = fnRow.name as string; |
15 | 83 | const image = fnRow.image as string; |
16 | 84 | const concurrency = (fnRow.concurrency as number) ?? 0; |
17 | 85 | const scaleMin = (fnRow.scale_min as number) ?? 0; |
18 | 86 | const scaleMax = (fnRow.scale_max as number) ?? 0; |
19 | | - const timeoutSeconds = (fnRow.timeout_seconds as number) ?? 300; |
| 87 | + const scaleTarget = (fnRow.scale_target as number) ?? 0; |
| 88 | + const timeoutSeconds = (fnRow.timeout_seconds as number) ?? DEFAULT_TIMEOUT; |
20 | 89 | const resources = (fnRow.resources as Record<string, unknown>) ?? {}; |
21 | | - const fnName = fnRow.name as string; |
22 | 90 |
|
23 | | - const annotations: Record<string, string> = {}; |
24 | | - if (scaleMin > 0) annotations['autoscaling.knative.dev/minScale'] = String(scaleMin); |
25 | | - if (scaleMax > 0) annotations['autoscaling.knative.dev/maxScale'] = String(scaleMax); |
| 91 | + // Template-level autoscaling annotations |
| 92 | + const templateAnnotations: Record<string, string> = {}; |
| 93 | + if (scaleMin > 0) templateAnnotations['autoscaling.knative.dev/minScale'] = String(scaleMin); |
| 94 | + if (scaleMax > 0) templateAnnotations['autoscaling.knative.dev/maxScale'] = String(scaleMax); |
| 95 | + if (scaleTarget > 0 || (scaleMin > 0 && scaleMax > 0)) { |
| 96 | + templateAnnotations['autoscaling.knative.dev/target'] = String(scaleTarget || DEFAULT_SCALE_TARGET); |
| 97 | + } |
| 98 | + |
| 99 | + const labels = componentLabels(namespaceName, fnName); |
| 100 | + |
| 101 | + const container: KnativeServiceSpec['spec']['template']['spec']['containers'][0] = { |
| 102 | + image, |
| 103 | + ports: [{ containerPort: DEFAULT_PORT }], |
| 104 | + envFrom: [{ secretRef: { name: `${namespaceName}-secrets` } }], |
| 105 | + volumeMounts: [{ name: 'tmp', mountPath: '/tmp' }], |
| 106 | + }; |
| 107 | + |
| 108 | + if (Object.keys(resources).length > 0) { |
| 109 | + container.resources = resources; |
| 110 | + } |
26 | 111 |
|
27 | 112 | return { |
28 | 113 | apiVersion: 'serving.knative.dev/v1', |
29 | 114 | kind: 'Service', |
30 | | - metadata: { name: fnName, namespace: namespaceName } as { |
31 | | - name: string; |
32 | | - namespace: string; |
33 | | - resourceVersion?: string; |
34 | | - annotations?: Record<string, string>; |
35 | | - labels?: Record<string, string>; |
| 115 | + metadata: { |
| 116 | + name: fnName, |
| 117 | + namespace: namespaceName, |
| 118 | + labels: { ...labels }, |
| 119 | + annotations: {}, |
36 | 120 | }, |
37 | 121 | spec: { |
38 | 122 | template: { |
39 | 123 | metadata: { |
40 | | - annotations: Object.keys(annotations).length > 0 ? annotations : undefined, |
| 124 | + labels: { ...labels }, |
| 125 | + annotations: Object.keys(templateAnnotations).length > 0 ? templateAnnotations : undefined, |
41 | 126 | }, |
42 | 127 | spec: { |
43 | 128 | containerConcurrency: concurrency || undefined, |
44 | 129 | timeoutSeconds, |
45 | | - containers: [ |
46 | | - { |
47 | | - image, |
48 | | - envFrom: [{ secretRef: { name: `${namespaceName}-secrets` } }], |
49 | | - ...(Object.keys(resources).length > 0 ? { resources } : {}), |
50 | | - }, |
51 | | - ], |
| 130 | + containers: [container], |
| 131 | + volumes: [{ name: 'tmp', emptyDir: {} }], |
52 | 132 | }, |
53 | 133 | }, |
54 | 134 | }, |
55 | 135 | }; |
56 | 136 | } |
57 | 137 |
|
| 138 | +// ── Namespace resolver ─────────────────────────────────────────────────────── |
| 139 | + |
58 | 140 | /** |
59 | 141 | * Resolve a namespace name from a namespace_id. Returns 'default' if null. |
60 | 142 | */ |
|
0 commit comments