@r8s/chromadb 0.2.0 → 0.3.0

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,108 @@
1
+ export interface ChromaDbProps {
2
+ /** Resource name (defaults to 'chromadb') */
3
+ name?: string;
4
+ /** Kubernetes namespace (defaults to 'default') */
5
+ namespace?: string;
6
+ /** Container image tag (defaults to 'latest' — pin a version in production) */
7
+ version?: string;
8
+ /** Public hostname for the vector API (required) */
9
+ host: string;
10
+ /** Port Chroma listens on (defaults to 8000) */
11
+ port?: number;
12
+ /**
13
+ * Number of replicas (defaults to 1). The embedded data PVC is
14
+ * ReadWriteOnce — extra replicas on other nodes cause Multi-Attach
15
+ * errors; scale out only with a ReadWriteMany StorageClass via
16
+ * `storageClassName` (required for `autoscaling`).
17
+ */
18
+ replicas?: number;
19
+ /** Persistent storage size for the embedded data volume (defaults to '50Gi') */
20
+ storage?: string;
21
+ /** StorageClass for the data PersistentVolumeClaim (optional — cluster default) */
22
+ storageClassName?: string;
23
+ /**
24
+ * HTTP path for liveness/readiness probes (defaults to
25
+ * '/api/v2/heartbeat' — the current image API). Set '/api/v1/heartbeat'
26
+ * for older images still serving the v1 API.
27
+ */
28
+ probePath?: string;
29
+ /** Require token authentication for the server (defaults to false) */
30
+ auth?: boolean;
31
+ /**
32
+ * Name of an existing Secret holding key `token` — the Chroma server
33
+ * auth credential. Required when `auth` is true, unless a secrets
34
+ * backend (openbao/vault) is configured on the surrounding Platform —
35
+ * the backend then provisions the token at path
36
+ * `<path>/<name>/auth-token` (key: token). Plaintext credentials are
37
+ * not supported.
38
+ */
39
+ authTokenSecretName?: string;
40
+ /** Autoscale the Deployment via a CPU-based HorizontalPodAutoscaler (defaults to false) */
41
+ autoscaling?: boolean;
42
+ /** Provision a CNPG Postgres cluster as the metadata store (defaults to false) */
43
+ pg?: boolean;
44
+ /** Requested resources */
45
+ resources?: {
46
+ requests?: {
47
+ cpu?: string;
48
+ memory?: string;
49
+ };
50
+ limits?: {
51
+ cpu?: string;
52
+ memory?: string;
53
+ };
54
+ };
55
+ /** TLS configuration (defaults to letsencrypt-prod cluster issuer) */
56
+ tls?: {
57
+ secretName: string;
58
+ clusterIssuer: string;
59
+ };
60
+ }
61
+ /**
62
+ * ChromaDB — open-source vector database for AI embeddings.
63
+ *
64
+ * @title ChromaDb
65
+ * @category AI & Embeddings
66
+ *
67
+ * Composes:
68
+ * - PersistentVolumeClaim (`<name>-data`) mounted at /data for embedded
69
+ * persistence (Chroma's default persist directory in the official image)
70
+ * - Chroma Deployment + Service (port 8000, heartbeat health checks,
71
+ * /api/v2/heartbeat by default — override with `probePath` for older
72
+ * v1-API images)
73
+ * - Endpoint (nginx Ingress or Envoy Gateway depending on RoutingContext)
74
+ * - Optional CNPG Postgres cluster for the metadata store (`pg`)
75
+ * - Optional CPU-based HorizontalPodAutoscaler (`autoscaling`) — requires
76
+ * a ReadWriteMany StorageClass (`storageClassName` ending in 'rwx')
77
+ * - Optional token auth (`auth`): provisioned at path
78
+ * `<path>/<name>/auth-token` through the Platform secrets backend
79
+ * (openbao / vault), or referenced from an existing Secret (key: token)
80
+ *
81
+ * ChromaDB has no dedicated operator — it runs as a plain Deployment.
82
+ *
83
+ * @example
84
+ * import { ChromaDb } from '@r8s/chromadb'
85
+ *
86
+ * export default <ChromaDb name="vectors" host="vectors.example.com" />
87
+ *
88
+ * @example
89
+ * import { Platform } from '@r8s/recipes'
90
+ * import { ChromaDb } from '@r8s/chromadb'
91
+ *
92
+ * export default (
93
+ * <Platform secrets={{ backend: 'openbao', mount: 'kv', path: 'ai' }}>
94
+ * <ChromaDb
95
+ * name="vectors"
96
+ * host="vectors.example.com"
97
+ * storage="50Gi"
98
+ * replicas={2}
99
+ * storageClassName="nfs-rwx"
100
+ * pg
101
+ * autoscaling
102
+ * auth
103
+ * />
104
+ * </Platform>
105
+ * )
106
+ */
107
+ export declare function ChromaDb(props: ChromaDbProps): import("@r8s/core").r8sElement;
108
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.tsx"],"names":[],"mappings":"AAIA,MAAM,WAAW,aAAa;IAC5B,6CAA6C;IAC7C,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,mDAAmD;IACnD,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,+EAA+E;IAC/E,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,oDAAoD;IACpD,IAAI,EAAE,MAAM,CAAA;IACZ,gDAAgD;IAChD,IAAI,CAAC,EAAE,MAAM,CAAA;IACb;;;;;OAKG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,gFAAgF;IAChF,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,mFAAmF;IACnF,gBAAgB,CAAC,EAAE,MAAM,CAAA;IACzB;;;;OAIG;IACH,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,sEAAsE;IACtE,IAAI,CAAC,EAAE,OAAO,CAAA;IACd;;;;;;;OAOG;IACH,mBAAmB,CAAC,EAAE,MAAM,CAAA;IAC5B,2FAA2F;IAC3F,WAAW,CAAC,EAAE,OAAO,CAAA;IACrB,kFAAkF;IAClF,EAAE,CAAC,EAAE,OAAO,CAAA;IACZ,0BAA0B;IAC1B,SAAS,CAAC,EAAE;QACV,QAAQ,CAAC,EAAE;YAAE,GAAG,CAAC,EAAE,MAAM,CAAC;YAAC,MAAM,CAAC,EAAE,MAAM,CAAA;SAAE,CAAA;QAC5C,MAAM,CAAC,EAAE;YAAE,GAAG,CAAC,EAAE,MAAM,CAAC;YAAC,MAAM,CAAC,EAAE,MAAM,CAAA;SAAE,CAAA;KAC3C,CAAA;IACD,sEAAsE;IACtE,GAAG,CAAC,EAAE;QACJ,UAAU,EAAE,MAAM,CAAA;QAClB,aAAa,EAAE,MAAM,CAAA;KACtB,CAAA;CACF;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6CG;AACH,wBAAgB,QAAQ,CAAC,KAAK,EAAE,aAAa,kCAuQ5C"}
package/dist/index.js ADDED
@@ -0,0 +1,248 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ChromaDb = ChromaDb;
4
+ const jsx_runtime_1 = require("@r8s/core/jsx-runtime");
5
+ const core_1 = require("@r8s/core");
6
+ const defaults_1 = require("@r8s/core/defaults");
7
+ const recipes_1 = require("@r8s/recipes");
8
+ /**
9
+ * ChromaDB — open-source vector database for AI embeddings.
10
+ *
11
+ * @title ChromaDb
12
+ * @category AI & Embeddings
13
+ *
14
+ * Composes:
15
+ * - PersistentVolumeClaim (`<name>-data`) mounted at /data for embedded
16
+ * persistence (Chroma's default persist directory in the official image)
17
+ * - Chroma Deployment + Service (port 8000, heartbeat health checks,
18
+ * /api/v2/heartbeat by default — override with `probePath` for older
19
+ * v1-API images)
20
+ * - Endpoint (nginx Ingress or Envoy Gateway depending on RoutingContext)
21
+ * - Optional CNPG Postgres cluster for the metadata store (`pg`)
22
+ * - Optional CPU-based HorizontalPodAutoscaler (`autoscaling`) — requires
23
+ * a ReadWriteMany StorageClass (`storageClassName` ending in 'rwx')
24
+ * - Optional token auth (`auth`): provisioned at path
25
+ * `<path>/<name>/auth-token` through the Platform secrets backend
26
+ * (openbao / vault), or referenced from an existing Secret (key: token)
27
+ *
28
+ * ChromaDB has no dedicated operator — it runs as a plain Deployment.
29
+ *
30
+ * @example
31
+ * import { ChromaDb } from '@r8s/chromadb'
32
+ *
33
+ * export default <ChromaDb name="vectors" host="vectors.example.com" />
34
+ *
35
+ * @example
36
+ * import { Platform } from '@r8s/recipes'
37
+ * import { ChromaDb } from '@r8s/chromadb'
38
+ *
39
+ * export default (
40
+ * <Platform secrets={{ backend: 'openbao', mount: 'kv', path: 'ai' }}>
41
+ * <ChromaDb
42
+ * name="vectors"
43
+ * host="vectors.example.com"
44
+ * storage="50Gi"
45
+ * replicas={2}
46
+ * storageClassName="nfs-rwx"
47
+ * pg
48
+ * autoscaling
49
+ * auth
50
+ * />
51
+ * </Platform>
52
+ * )
53
+ */
54
+ function ChromaDb(props) {
55
+ const { name = 'chromadb', namespace = 'default', version = 'latest', host, port = 8000, replicas = 1, storage = '50Gi', storageClassName, probePath = '/api/v2/heartbeat', auth = false, authTokenSecretName, autoscaling = false, pg = false, resources = {
56
+ requests: { memory: '512Mi', cpu: '250m' },
57
+ limits: { memory: '2Gi', cpu: '1000m' },
58
+ }, tls = { secretName: `${name}-tls`, clusterIssuer: 'letsencrypt-prod' }, } = props;
59
+ const secretProvider = (0, core_1.useContext)(defaults_1.SecretContext);
60
+ const resources_ = [];
61
+ const tokenSecretName = authTokenSecretName ?? `${name}-auth-token`;
62
+ // --- Autoscaling coherence --------------------------------------------------
63
+ // The data PVC is ReadWriteOnce: a HorizontalPodAutoscaler scaling to
64
+ // replicas on other nodes triggers Multi-Attach errors. Scaling out is
65
+ // only safe on a ReadWriteMany StorageClass.
66
+ if (autoscaling && !(storageClassName && storageClassName.toLowerCase().endsWith('rwx'))) {
67
+ throw new Error(`ChromaDb "${name}" cannot autoscale on ReadWriteOnce storage.\n` +
68
+ `\n` +
69
+ `The data PVC uses ReadWriteOnce — scaling to replicas beyond one node ` +
70
+ `triggers Kubernetes Multi-Attach errors. A HorizontalPodAutoscaler ` +
71
+ `needs a ReadWriteMany (RWX) StorageClass.\n` +
72
+ `\n` +
73
+ `Fix: point storageClassName at a ReadWriteMany StorageClass ` +
74
+ `(its name must end with 'rwx'):\n` +
75
+ ` <ChromaDb name="${name}" host="${host}" storageClassName="nfs-rwx" autoscaling />`);
76
+ }
77
+ // --- Auth token — provisioned through the backend or referenced -------------
78
+ // The token authenticates every request to the vector store — it must
79
+ // never be rendered as plaintext.
80
+ if (auth && !authTokenSecretName) {
81
+ if (!secretProvider ||
82
+ (secretProvider.backend !== 'vault' && secretProvider.backend !== 'openbao')) {
83
+ throw new Error(`ChromaDb "${name}" requires an auth token (key: token).\n` +
84
+ `\n` +
85
+ `The CHROMA_SERVER_AUTH_CREDENTIALS token authenticates every request ` +
86
+ `to the vector store — it must not be rendered as plaintext.\n` +
87
+ `\n` +
88
+ `Fix: configure a secrets backend on the Platform holding the token ` +
89
+ `at path <mount-path>/${name}/auth-token:\n` +
90
+ ` <Platform secrets={{ backend: 'openbao', mount: 'kv', path: 'ai' }}>\n` +
91
+ ` <ChromaDb name="${name}" host="${host}" auth={true} />\n` +
92
+ ` </Platform>\n` +
93
+ `\n` +
94
+ `Or reference a pre-created Secret (key: token):\n` +
95
+ ` <ChromaDb name="${name}" host="${host}" auth={true} authTokenSecretName="${name}-auth-token" />`);
96
+ }
97
+ const spec = {
98
+ ...(secretProvider.backend === 'vault'
99
+ ? { vaultAuthRef: secretProvider.authRef }
100
+ : { openbaoAuthRef: secretProvider.authRef }),
101
+ mount: secretProvider.mount,
102
+ type: 'kv-v2',
103
+ path: `${secretProvider.path ?? name}/${name}/auth-token`,
104
+ destination: { create: true, name: tokenSecretName },
105
+ };
106
+ resources_.push(secretProvider.backend === 'vault'
107
+ ? (0, core_1.jsx)('VaultStaticSecret', {
108
+ apiVersion: 'secrets.hashicorp.com/v1beta1',
109
+ kind: 'VaultStaticSecret',
110
+ metadata: { name: `${name}-auth-token`, namespace },
111
+ spec,
112
+ })
113
+ : (0, core_1.jsx)('OpenBaoStaticSecret', {
114
+ apiVersion: 'secrets.openbao.org/v1beta1',
115
+ kind: 'OpenBaoStaticSecret',
116
+ metadata: { name: `${name}-auth-token`, namespace },
117
+ spec,
118
+ }));
119
+ }
120
+ // --- Env wiring -------------------------------------------------------------
121
+ // Only CHROMA_* env vars — every credential arrives via secretKeyRef at
122
+ // runtime (pushed first so dependent $(VAR) expansion resolves).
123
+ const envVars = [];
124
+ // The listener port must track the `port` prop or the server ignores it
125
+ // and binds the image default (8000), breaking the Service and probes.
126
+ envVars.push({ name: 'CHROMA_SERVER_HTTP_PORT', value: String(port) });
127
+ if (pg) {
128
+ // Metadata store: CNPG cluster provisioned as `${name}-meta`. Its
129
+ // connection info follows the Database recipe convention.
130
+ const dbHost = `${name}-meta-rw`;
131
+ envVars.push({ name: 'CHROMA_POSTGRES_HOST', value: dbHost }, { name: 'CHROMA_POSTGRES_PORT', value: '5432' }, { name: 'CHROMA_POSTGRES_DATABASE', value: `${name}-meta` }, { name: 'CHROMA_POSTGRES_USER', value: `${name}-meta` }, {
132
+ name: 'CHROMA_POSTGRES_PASSWORD',
133
+ valueFrom: {
134
+ secretKeyRef: { name: `${name}-meta-db-credentials`, key: 'password' },
135
+ },
136
+ });
137
+ }
138
+ if (auth) {
139
+ envVars.push({
140
+ name: 'CHROMA_SERVER_AUTH_CREDENTIALS',
141
+ valueFrom: { secretKeyRef: { name: tokenSecretName, key: 'token' } },
142
+ }, {
143
+ name: 'CHROMA_SERVER_AUTH_PROVIDER',
144
+ value: 'chromadb.auth.token.TokenAuthenticationServerProvider',
145
+ });
146
+ }
147
+ // --- Data-plane resources ----------------------------------------------------
148
+ // WebService cannot mount volumes, so the app runs as a raw Deployment
149
+ // with heartbeat probes. Chroma persists to /data by default in the
150
+ // official image.
151
+ const dataPlane = [];
152
+ dataPlane.push((0, core_1.jsx)('PersistentVolumeClaim', {
153
+ apiVersion: 'v1',
154
+ kind: 'PersistentVolumeClaim',
155
+ metadata: { name: `${name}-data`, namespace },
156
+ spec: {
157
+ accessModes: ['ReadWriteOnce'],
158
+ ...(storageClassName && { storageClassName }),
159
+ resources: { requests: { storage } },
160
+ },
161
+ }));
162
+ // --- Deployment + Service (raw jsx — WebService lacks volume mounts) --------
163
+ dataPlane.push((0, core_1.jsx)('Deployment', {
164
+ apiVersion: 'apps/v1',
165
+ kind: 'Deployment',
166
+ metadata: { name, namespace, labels: { app: name } },
167
+ spec: {
168
+ replicas,
169
+ selector: { matchLabels: { app: name } },
170
+ template: {
171
+ metadata: { labels: { app: name } },
172
+ spec: {
173
+ containers: [
174
+ {
175
+ name: 'chroma',
176
+ image: `ghcr.io/chroma-core/chroma:${version}`,
177
+ imagePullPolicy: 'Always',
178
+ ports: [{ containerPort: port }],
179
+ env: envVars,
180
+ ...(resources && { resources }),
181
+ volumeMounts: [{ name: 'data', mountPath: '/data' }],
182
+ livenessProbe: {
183
+ httpGet: { path: probePath, port },
184
+ initialDelaySeconds: 10,
185
+ periodSeconds: 10,
186
+ },
187
+ readinessProbe: {
188
+ httpGet: { path: probePath, port },
189
+ initialDelaySeconds: 5,
190
+ periodSeconds: 5,
191
+ },
192
+ },
193
+ ],
194
+ volumes: [{ name: 'data', persistentVolumeClaim: { claimName: `${name}-data` } }],
195
+ },
196
+ },
197
+ },
198
+ }), (0, core_1.jsx)('Service', {
199
+ apiVersion: 'v1',
200
+ kind: 'Service',
201
+ metadata: { name, namespace },
202
+ spec: {
203
+ type: 'ClusterIP',
204
+ selector: { app: name },
205
+ ports: [{ name: 'http', port, targetPort: port, protocol: 'TCP' }],
206
+ },
207
+ }));
208
+ // --- Metadata store (CNPG) — wraps the data-plane resources so connection
209
+ // info stays consistent with the Database recipe convention -----------------
210
+ if (pg) {
211
+ resources_.push((0, core_1.jsx)(recipes_1.Database, {
212
+ backup: false,
213
+ name: `${name}-meta`,
214
+ namespace,
215
+ storage: '10Gi',
216
+ children: (0, core_1.jsx)(core_1.Fragment, { children: dataPlane }),
217
+ }));
218
+ }
219
+ else {
220
+ resources_.push(...dataPlane);
221
+ }
222
+ // --- Autoscaling --------------------------------------------------------------
223
+ if (autoscaling) {
224
+ resources_.push((0, core_1.jsx)('HorizontalPodAutoscaler', {
225
+ apiVersion: 'autoscaling/v2',
226
+ kind: 'HorizontalPodAutoscaler',
227
+ metadata: { name: `${name}-hpa`, namespace },
228
+ spec: {
229
+ scaleTargetRef: { apiVersion: 'apps/v1', kind: 'Deployment', name },
230
+ minReplicas: replicas,
231
+ maxReplicas: replicas * 3,
232
+ metrics: [
233
+ {
234
+ type: 'Resource',
235
+ resource: {
236
+ name: 'cpu',
237
+ target: { type: 'Utilization', averageUtilization: 70 },
238
+ },
239
+ },
240
+ ],
241
+ },
242
+ }));
243
+ }
244
+ // --- Endpoint ------------------------------------------------------------------
245
+ resources_.push((0, jsx_runtime_1.jsx)(recipes_1.Endpoint, { name: `${name}-endpoint`, namespace: namespace, host: host, serviceName: name, servicePort: port, tls: tls }));
246
+ return (0, core_1.jsx)(core_1.Fragment, { children: resources_ });
247
+ }
248
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.tsx"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@r8s/chromadb",
3
- "version": "0.2.0",
4
- "description": "ChromaDB vector database persistent storage, optional Postgres metadata backend, token auth, CPU autoscaling",
3
+ "version": "0.3.0",
4
+ "description": "ChromaDB vector database \u2014 persistent storage, optional Postgres metadata backend, token auth, CPU autoscaling",
5
5
  "license": "MIT",
6
6
  "author": "Berget AI AB",
7
7
  "homepage": "https://r8s.berget.ai",
@@ -29,13 +29,16 @@
29
29
  },
30
30
  "category": "AI & Assistants",
31
31
  "dependencies": {
32
- "@r8s/core": "^0.2.0",
33
- "@r8s/crds": "^0.2.0",
34
- "@r8s/k8s-types": "^0.2.0",
35
- "@r8s/recipes": "^0.2.0"
32
+ "@r8s/core": "^0.3.0",
33
+ "@r8s/crds": "^0.3.0",
34
+ "@r8s/k8s-types": "^0.3.0",
35
+ "@r8s/recipes": "^0.3.0"
36
36
  },
37
37
  "devDependencies": {
38
38
  "typescript": "^5.3.0",
39
39
  "vitest": "^1.0.0"
40
- }
40
+ },
41
+ "files": [
42
+ "dist"
43
+ ]
41
44
  }
@@ -1,374 +0,0 @@
1
- import { describe, it, expect } from 'vitest'
2
- import { render, jsx } from '@r8s/core'
3
- import { OperatorContext, SecretContext, RoutingContext } from '@r8s/core/defaults'
4
- import { runGuardrails, noPlaintextSecrets, validateResource } from '@r8s/core'
5
- import { operators } from '@r8s/crds'
6
- import type { r8sElement } from '@r8s/core'
7
-
8
- // ChromaDb recipe tests:
9
- // 1. Operator declarations (deduped via OperatorContext)
10
- // 2. Rendering: defaults, all props, gateway/ingress adaptation
11
- // 3. Security: no plaintext credentials in rendered output
12
- import { ChromaDb } from '../src/index'
13
-
14
- /** Render ChromaDb inside a Platform-like secrets backend (OpenBao). */
15
- function renderChromaDb(props: Record<string, unknown>): ReturnType<typeof render> {
16
- return render(
17
- jsx(SecretContext.Provider, {
18
- value: { backend: 'openbao', mount: 'kv', path: 'test' },
19
- children: jsx(ChromaDb, props as never),
20
- })
21
- )
22
- }
23
-
24
- /** Render ChromaDb wrapped only in an OperatorContext (no secrets backend). */
25
- function renderChromaDbWithContext(operators_: any[], props: Record<string, unknown>): r8sElement {
26
- return jsx(OperatorContext.Provider, {
27
- value: operators_,
28
- children: jsx(ChromaDb, props as never),
29
- })
30
- }
31
-
32
- const openbao = { backend: 'openbao', mount: 'kv', path: 'test' }
33
-
34
- describe('operator declarations', () => {
35
- it('declares the cnpg operator via the Database recipe when pg is enabled', () => {
36
- const result = renderChromaDb({ host: 'vectors.example.com', pg: true })
37
- expect(result.operators.some((op) => op.name === 'cnpg')).toBe(true)
38
- })
39
-
40
- it('requires no cnpg operator without pg', () => {
41
- const result = renderChromaDb({ host: 'vectors.example.com' })
42
- expect(result.operators.some((op) => op.name === 'cnpg')).toBe(false)
43
- })
44
-
45
- it('deduplicates operators provided via context', () => {
46
- const result = render(
47
- renderChromaDbWithContext([operators['cnpg']()], {
48
- host: 'vectors.example.com',
49
- pg: true,
50
- })
51
- )
52
- const names = result.operators.map((op) => op.name)
53
- expect(names.filter((n) => n === 'cnpg')).toHaveLength(1)
54
- })
55
-
56
- it('allows version overrides through context operators', () => {
57
- const result = render(
58
- renderChromaDbWithContext([operators['cnpg']('1.26.0')], {
59
- host: 'vectors.example.com',
60
- pg: true,
61
- })
62
- )
63
- const cnpg = result.operators.filter((op) => op.name === 'cnpg')
64
- expect(cnpg).toHaveLength(1)
65
- expect(cnpg[0].version).toBe('1.26.0')
66
- })
67
- })
68
-
69
- describe('rendering defaults', () => {
70
- it('renders PVC, deployment, service and endpoint', () => {
71
- const result = renderChromaDb({ host: 'vectors.example.com' })
72
- const kinds = result.resources.map((r) => r.kind)
73
- expect(kinds).toContain('PersistentVolumeClaim')
74
- expect(kinds).toContain('Deployment')
75
- expect(kinds).toContain('Service')
76
- expect(kinds).toContain('Ingress')
77
- expect(kinds).not.toContain('Cluster')
78
- })
79
-
80
- it('defaults to a single replica (RWO-safe)', () => {
81
- const result = renderChromaDb({ host: 'vectors.example.com' })
82
- const deployment = result.resources.find(
83
- (r) => r.kind === 'Deployment' && r.metadata.name === 'chromadb'
84
- ) as any
85
- expect(deployment.spec.replicas).toBe(1)
86
- })
87
-
88
- it('renders the data PVC with the requested storage', () => {
89
- const result = renderChromaDb({ host: 'vectors.example.com' })
90
- const pvc = result.resources.find((r) => r.kind === 'PersistentVolumeClaim') as any
91
- expect(pvc.metadata.name).toBe('chromadb-data')
92
- expect(pvc.spec.resources.requests.storage).toBe('50Gi')
93
- })
94
-
95
- it('wires the v2 heartbeat health checks on port 8000', () => {
96
- const result = renderChromaDb({ host: 'vectors.example.com' })
97
- const deployment = result.resources.find(
98
- (r) => r.kind === 'Deployment' && r.metadata.name === 'chromadb'
99
- ) as any
100
- const container = deployment.spec.template.spec.containers[0]
101
- expect(container.livenessProbe.httpGet.path).toBe('/api/v2/heartbeat')
102
- expect(container.livenessProbe.httpGet.port).toBe(8000)
103
- expect(container.readinessProbe.httpGet.path).toBe('/api/v2/heartbeat')
104
- expect(container.ports[0].containerPort).toBe(8000)
105
- expect(container.volumeMounts).toContainEqual({ name: 'data', mountPath: '/data' })
106
- expect(deployment.spec.template.spec.volumes[0].persistentVolumeClaim.claimName).toBe(
107
- 'chromadb-data'
108
- )
109
- })
110
-
111
- it('supports the probePath prop for older v1-API images', () => {
112
- const result = renderChromaDb({
113
- host: 'vectors.example.com',
114
- probePath: '/api/v1/heartbeat',
115
- })
116
- const deployment = result.resources.find(
117
- (r) => r.kind === 'Deployment' && r.metadata.name === 'chromadb'
118
- ) as any
119
- const container = deployment.spec.template.spec.containers[0]
120
- expect(container.livenessProbe.httpGet.path).toBe('/api/v1/heartbeat')
121
- expect(container.readinessProbe.httpGet.path).toBe('/api/v1/heartbeat')
122
- })
123
-
124
- it('wires CHROMA_SERVER_HTTP_PORT to the port prop on a custom port', () => {
125
- const result = renderChromaDb({ host: 'vectors.example.com', port: 9000 })
126
- const deployment = result.resources.find(
127
- (r) => r.kind === 'Deployment' && r.metadata.name === 'chromadb'
128
- ) as any
129
- const container = deployment.spec.template.spec.containers[0]
130
- const httpPort = container.env.find((e: any) => e.name === 'CHROMA_SERVER_HTTP_PORT')
131
- expect(httpPort.value).toBe('9000')
132
- expect(container.ports[0].containerPort).toBe(9000)
133
- const service = result.resources.find(
134
- (r) => r.kind === 'Service' && r.metadata.name === 'chromadb'
135
- ) as any
136
- expect(service.spec.ports[0].port).toBe(9000)
137
- })
138
-
139
- it('renders gateway resources when platform uses gateway routing', () => {
140
- const result = render(
141
- jsx(RoutingContext.Provider, {
142
- value: { mode: 'gateway', gatewayClassName: 'eg' },
143
- children: jsx(SecretContext.Provider, {
144
- value: openbao as never,
145
- children: jsx(ChromaDb, { host: 'vectors.example.com' }),
146
- }),
147
- })
148
- )
149
- const kinds = result.resources.map((r) => r.kind)
150
- expect(kinds).toContain('HTTPRoute')
151
- expect(kinds).not.toContain('Ingress')
152
- })
153
-
154
- it('renders a valid Ingress when platform uses ingress routing', () => {
155
- const result = render(
156
- jsx(RoutingContext.Provider, {
157
- value: { mode: 'ingress' },
158
- children: jsx(SecretContext.Provider, {
159
- value: openbao as never,
160
- children: jsx(ChromaDb, { host: 'vectors.example.com' }),
161
- }),
162
- })
163
- )
164
- const ingress = result.resources.find((r) => r.kind === 'Ingress') as any
165
- expect(ingress).toBeDefined()
166
- expect(ingress.spec.rules[0].host).toBe('vectors.example.com')
167
- expect(ingress.spec.rules[0].http.paths[0].backend.service.port.number).toBe(8000)
168
- })
169
-
170
- it('passes resource validation', () => {
171
- const result = renderChromaDb({
172
- host: 'vectors.example.com',
173
- pg: true,
174
- autoscaling: true,
175
- storageClassName: 'fast-ssd-rwx',
176
- auth: true,
177
- authTokenSecretName: 'chromadb-auth-token',
178
- })
179
- for (const resource of result.resources) {
180
- expect(validateResource(resource)).toEqual([])
181
- }
182
- })
183
- })
184
-
185
- describe('rendering with all props', () => {
186
- it('accepts the full prop surface', () => {
187
- const result = renderChromaDb({
188
- name: 'vectors',
189
- namespace: 'ai',
190
- version: '0.6.3',
191
- host: 'vectors.example.com',
192
- port: 8000,
193
- replicas: 3,
194
- storage: '100Gi',
195
- storageClassName: 'fast-ssd-rwx',
196
- auth: true,
197
- authTokenSecretName: 'vectors-auth-token',
198
- autoscaling: true,
199
- pg: true,
200
- resources: {
201
- requests: { memory: '1Gi', cpu: '500m' },
202
- limits: { memory: '4Gi', cpu: '2000m' },
203
- },
204
- tls: { secretName: 'vectors-tls', clusterIssuer: 'letsencrypt-prod' },
205
- })
206
- const deployment = result.resources.find(
207
- (r: any) => r.kind === 'Deployment' && r.metadata.name === 'vectors'
208
- ) as any
209
- expect(deployment.spec.template.spec.containers[0].image).toBe(
210
- 'ghcr.io/chroma-core/chroma:0.6.3'
211
- )
212
- expect(deployment.spec.replicas).toBe(3)
213
- expect(deployment.spec.template.spec.containers[0].resources.limits.memory).toBe('4Gi')
214
-
215
- const pvc = result.resources.find((r) => r.kind === 'PersistentVolumeClaim') as any
216
- expect(pvc.metadata.name).toBe('vectors-data')
217
- expect(pvc.spec.resources.requests.storage).toBe('100Gi')
218
- expect(pvc.spec.storageClassName).toBe('fast-ssd-rwx')
219
-
220
- const hpa = result.resources.find((r) => r.kind === 'HorizontalPodAutoscaler') as any
221
- expect(hpa).toBeDefined()
222
- expect(hpa.spec.scaleTargetRef).toMatchObject({ kind: 'Deployment', name: 'vectors' })
223
- expect(hpa.spec.minReplicas).toBe(3)
224
- expect(hpa.spec.maxReplicas).toBe(9)
225
- expect(hpa.spec.metrics[0].resource.target.averageUtilization).toBe(70)
226
-
227
- const cluster = result.resources.find((r) => r.kind === 'Cluster') as any
228
- expect(cluster).toBeDefined()
229
- expect(cluster.metadata.name).toBe('vectors-meta')
230
- })
231
- })
232
-
233
- describe('secrets handling', () => {
234
- it('accepts an explicit authTokenSecretName without a backend', () => {
235
- expect(() =>
236
- render(
237
- jsx(ChromaDb, {
238
- host: 'vectors.example.com',
239
- auth: true,
240
- authTokenSecretName: 'existing-token-secret',
241
- })
242
- )
243
- ).not.toThrow()
244
- })
245
-
246
- it('provisions the auth token through a secrets backend at <path>/<name>/auth-token', () => {
247
- const result = renderChromaDb({ host: 'vectors.example.com', auth: true })
248
- const kinds = result.resources.map((r) => r.kind)
249
- expect(kinds).toContain('OpenBaoStaticSecret')
250
- const secret = result.resources.find((r) => r.kind === 'OpenBaoStaticSecret') as any
251
- expect(secret.metadata.name).toBe('chromadb-auth-token')
252
- expect(secret.spec.path).toBe('test/chromadb/auth-token')
253
- expect(secret.spec.destination.name).toBe('chromadb-auth-token')
254
- })
255
-
256
- it('provisions the auth token through vault', () => {
257
- const result = render(
258
- jsx(SecretContext.Provider, {
259
- value: { backend: 'vault', mount: 'kv', path: 'ai' },
260
- children: jsx(ChromaDb, { name: 'vectors', host: 'vectors.example.com', auth: true }),
261
- })
262
- )
263
- const secret = result.resources.find((r) => r.kind === 'VaultStaticSecret') as any
264
- expect(secret).toBeDefined()
265
- expect(secret.spec.path).toBe('ai/vectors/auth-token')
266
- expect(secret.spec.destination.name).toBe('vectors-auth-token')
267
- })
268
-
269
- it('wires the backend-provisioned auth token via secretKeyRef', () => {
270
- const result = renderChromaDb({ host: 'vectors.example.com', auth: true })
271
- const deployment = result.resources.find(
272
- (r: any) => r.kind === 'Deployment' && r.metadata.name === 'chromadb'
273
- ) as any
274
- const env = deployment.spec.template.spec.containers[0].env
275
- const token = env.find((e: any) => e.name === 'CHROMA_SERVER_AUTH_CREDENTIALS')
276
- expect(token.valueFrom.secretKeyRef.name).toBe('chromadb-auth-token')
277
- expect(token.valueFrom.secretKeyRef.key).toBe('token')
278
- expect(token.value).toBeUndefined()
279
- })
280
-
281
- it('wires the auth token via secretKeyRef (never plaintext env)', () => {
282
- const result = renderChromaDb({
283
- host: 'vectors.example.com',
284
- auth: true,
285
- authTokenSecretName: 'existing-token-secret',
286
- })
287
- const deployment = result.resources.find(
288
- (r: any) => r.kind === 'Deployment' && r.metadata.name === 'chromadb'
289
- ) as any
290
- const env = deployment.spec.template.spec.containers[0].env
291
- const token = env.find((e: any) => e.name === 'CHROMA_SERVER_AUTH_CREDENTIALS')
292
- expect(token.valueFrom.secretKeyRef.name).toBe('existing-token-secret')
293
- expect(token.valueFrom.secretKeyRef.key).toBe('token')
294
- expect(token.value).toBeUndefined()
295
- })
296
-
297
- it('wires Postgres credentials via secretKeyRef when pg is enabled', () => {
298
- const result = renderChromaDb({ host: 'vectors.example.com', pg: true })
299
- const deployment = result.resources.find(
300
- (r: any) => r.kind === 'Deployment' && r.metadata.name === 'chromadb'
301
- ) as any
302
- const env = deployment.spec.template.spec.containers[0].env
303
- const host = env.find((e: any) => e.name === 'CHROMA_POSTGRES_HOST')
304
- const password = env.find((e: any) => e.name === 'CHROMA_POSTGRES_PASSWORD')
305
- expect(host.value).toBe('chromadb-meta-rw')
306
- expect(password.valueFrom.secretKeyRef.name).toBe('chromadb-meta-db-credentials')
307
- expect(password.valueFrom.secretKeyRef.key).toBe('password')
308
- expect(password.value).toBeUndefined()
309
- })
310
-
311
- it('provisions Postgres credentials through the openbao backend when pg is enabled', () => {
312
- const result = renderChromaDb({ host: 'vectors.example.com', pg: true })
313
- const kinds = result.resources.map((r) => r.kind)
314
- expect(kinds).toContain('OpenBaoStaticSecret')
315
- })
316
-
317
- it('provisions Postgres credentials through vault when pg is enabled', () => {
318
- const result = render(
319
- jsx(SecretContext.Provider, {
320
- value: { backend: 'vault', mount: 'kv', path: 'apps' },
321
- children: jsx(ChromaDb, { host: 'vectors.example.com', pg: true }),
322
- })
323
- )
324
- const kinds = result.resources.map((r) => r.kind)
325
- expect(kinds).toContain('VaultStaticSecret')
326
- })
327
-
328
- it('renders no plaintext credentials anywhere', () => {
329
- const result = renderChromaDb({
330
- name: 'vectors',
331
- host: 'vectors.example.com',
332
- replicas: 3,
333
- storageClassName: 'vectors-rwx',
334
- auth: true,
335
- authTokenSecretName: 'vectors-auth-token',
336
- autoscaling: true,
337
- pg: true,
338
- })
339
- const { passed, errors } = runGuardrails(result.resources as any[], [noPlaintextSecrets])
340
- if (!passed) {
341
- console.error('Plaintext credential violations:', errors)
342
- }
343
- expect(passed).toBe(true)
344
- })
345
- })
346
-
347
- describe('validation errors', () => {
348
- it('throws when auth is enabled without an auth token secret or backend', () => {
349
- expect(() => render(jsx(ChromaDb, { host: 'vectors.example.com', auth: true }))).toThrow(
350
- /auth token/
351
- )
352
- })
353
-
354
- it('throws when autoscaling on RWO storage without an RWX StorageClass', () => {
355
- expect(() =>
356
- renderChromaDb({
357
- host: 'vectors.example.com',
358
- autoscaling: true,
359
- storageClassName: 'fast-ssd',
360
- })
361
- ).toThrow(/Multi-Attach/)
362
- expect(() => renderChromaDb({ host: 'vectors.example.com', autoscaling: true })).toThrow(
363
- /ReadWriteMany/
364
- )
365
- // Explicit RWX class names (any case) are allowed
366
- expect(() =>
367
- renderChromaDb({
368
- host: 'vectors.example.com',
369
- autoscaling: true,
370
- storageClassName: 'NFS-RWX',
371
- })
372
- ).not.toThrow()
373
- })
374
- })
@@ -1,3 +0,0 @@
1
- import { ChromaDb } from '@r8s/chromadb'
2
-
3
- export default <ChromaDb name="vectors" host="vectors.example.com" />
package/src/index.tsx DELETED
@@ -1,368 +0,0 @@
1
- import { jsx, Fragment, useContext } from '@r8s/core'
2
- import { SecretContext } from '@r8s/core/defaults'
3
- import { Database, Endpoint } from '@r8s/recipes'
4
-
5
- export interface ChromaDbProps {
6
- /** Resource name (defaults to 'chromadb') */
7
- name?: string
8
- /** Kubernetes namespace (defaults to 'default') */
9
- namespace?: string
10
- /** Container image tag (defaults to 'latest' — pin a version in production) */
11
- version?: string
12
- /** Public hostname for the vector API (required) */
13
- host: string
14
- /** Port Chroma listens on (defaults to 8000) */
15
- port?: number
16
- /**
17
- * Number of replicas (defaults to 1). The embedded data PVC is
18
- * ReadWriteOnce — extra replicas on other nodes cause Multi-Attach
19
- * errors; scale out only with a ReadWriteMany StorageClass via
20
- * `storageClassName` (required for `autoscaling`).
21
- */
22
- replicas?: number
23
- /** Persistent storage size for the embedded data volume (defaults to '50Gi') */
24
- storage?: string
25
- /** StorageClass for the data PersistentVolumeClaim (optional — cluster default) */
26
- storageClassName?: string
27
- /**
28
- * HTTP path for liveness/readiness probes (defaults to
29
- * '/api/v2/heartbeat' — the current image API). Set '/api/v1/heartbeat'
30
- * for older images still serving the v1 API.
31
- */
32
- probePath?: string
33
- /** Require token authentication for the server (defaults to false) */
34
- auth?: boolean
35
- /**
36
- * Name of an existing Secret holding key `token` — the Chroma server
37
- * auth credential. Required when `auth` is true, unless a secrets
38
- * backend (openbao/vault) is configured on the surrounding Platform —
39
- * the backend then provisions the token at path
40
- * `<path>/<name>/auth-token` (key: token). Plaintext credentials are
41
- * not supported.
42
- */
43
- authTokenSecretName?: string
44
- /** Autoscale the Deployment via a CPU-based HorizontalPodAutoscaler (defaults to false) */
45
- autoscaling?: boolean
46
- /** Provision a CNPG Postgres cluster as the metadata store (defaults to false) */
47
- pg?: boolean
48
- /** Requested resources */
49
- resources?: {
50
- requests?: { cpu?: string; memory?: string }
51
- limits?: { cpu?: string; memory?: string }
52
- }
53
- /** TLS configuration (defaults to letsencrypt-prod cluster issuer) */
54
- tls?: {
55
- secretName: string
56
- clusterIssuer: string
57
- }
58
- }
59
-
60
- /**
61
- * ChromaDB — open-source vector database for AI embeddings.
62
- *
63
- * @title ChromaDb
64
- * @category AI & Embeddings
65
- *
66
- * Composes:
67
- * - PersistentVolumeClaim (`<name>-data`) mounted at /data for embedded
68
- * persistence (Chroma's default persist directory in the official image)
69
- * - Chroma Deployment + Service (port 8000, heartbeat health checks,
70
- * /api/v2/heartbeat by default — override with `probePath` for older
71
- * v1-API images)
72
- * - Endpoint (nginx Ingress or Envoy Gateway depending on RoutingContext)
73
- * - Optional CNPG Postgres cluster for the metadata store (`pg`)
74
- * - Optional CPU-based HorizontalPodAutoscaler (`autoscaling`) — requires
75
- * a ReadWriteMany StorageClass (`storageClassName` ending in 'rwx')
76
- * - Optional token auth (`auth`): provisioned at path
77
- * `<path>/<name>/auth-token` through the Platform secrets backend
78
- * (openbao / vault), or referenced from an existing Secret (key: token)
79
- *
80
- * ChromaDB has no dedicated operator — it runs as a plain Deployment.
81
- *
82
- * @example
83
- * import { ChromaDb } from '@r8s/chromadb'
84
- *
85
- * export default <ChromaDb name="vectors" host="vectors.example.com" />
86
- *
87
- * @example
88
- * import { Platform } from '@r8s/recipes'
89
- * import { ChromaDb } from '@r8s/chromadb'
90
- *
91
- * export default (
92
- * <Platform secrets={{ backend: 'openbao', mount: 'kv', path: 'ai' }}>
93
- * <ChromaDb
94
- * name="vectors"
95
- * host="vectors.example.com"
96
- * storage="50Gi"
97
- * replicas={2}
98
- * storageClassName="nfs-rwx"
99
- * pg
100
- * autoscaling
101
- * auth
102
- * />
103
- * </Platform>
104
- * )
105
- */
106
- export function ChromaDb(props: ChromaDbProps) {
107
- const {
108
- name = 'chromadb',
109
- namespace = 'default',
110
- version = 'latest',
111
- host,
112
- port = 8000,
113
- replicas = 1,
114
- storage = '50Gi',
115
- storageClassName,
116
- probePath = '/api/v2/heartbeat',
117
- auth = false,
118
- authTokenSecretName,
119
- autoscaling = false,
120
- pg = false,
121
- resources = {
122
- requests: { memory: '512Mi', cpu: '250m' },
123
- limits: { memory: '2Gi', cpu: '1000m' },
124
- },
125
- tls = { secretName: `${name}-tls`, clusterIssuer: 'letsencrypt-prod' },
126
- } = props
127
-
128
- const secretProvider = useContext(SecretContext)
129
- const resources_: ReturnType<typeof jsx>[] = []
130
-
131
- const tokenSecretName = authTokenSecretName ?? `${name}-auth-token`
132
-
133
- // --- Autoscaling coherence --------------------------------------------------
134
- // The data PVC is ReadWriteOnce: a HorizontalPodAutoscaler scaling to
135
- // replicas on other nodes triggers Multi-Attach errors. Scaling out is
136
- // only safe on a ReadWriteMany StorageClass.
137
- if (autoscaling && !(storageClassName && storageClassName.toLowerCase().endsWith('rwx'))) {
138
- throw new Error(
139
- `ChromaDb "${name}" cannot autoscale on ReadWriteOnce storage.\n` +
140
- `\n` +
141
- `The data PVC uses ReadWriteOnce — scaling to replicas beyond one node ` +
142
- `triggers Kubernetes Multi-Attach errors. A HorizontalPodAutoscaler ` +
143
- `needs a ReadWriteMany (RWX) StorageClass.\n` +
144
- `\n` +
145
- `Fix: point storageClassName at a ReadWriteMany StorageClass ` +
146
- `(its name must end with 'rwx'):\n` +
147
- ` <ChromaDb name="${name}" host="${host}" storageClassName="nfs-rwx" autoscaling />`
148
- )
149
- }
150
-
151
- // --- Auth token — provisioned through the backend or referenced -------------
152
- // The token authenticates every request to the vector store — it must
153
- // never be rendered as plaintext.
154
- if (auth && !authTokenSecretName) {
155
- if (
156
- !secretProvider ||
157
- (secretProvider.backend !== 'vault' && secretProvider.backend !== 'openbao')
158
- ) {
159
- throw new Error(
160
- `ChromaDb "${name}" requires an auth token (key: token).\n` +
161
- `\n` +
162
- `The CHROMA_SERVER_AUTH_CREDENTIALS token authenticates every request ` +
163
- `to the vector store — it must not be rendered as plaintext.\n` +
164
- `\n` +
165
- `Fix: configure a secrets backend on the Platform holding the token ` +
166
- `at path <mount-path>/${name}/auth-token:\n` +
167
- ` <Platform secrets={{ backend: 'openbao', mount: 'kv', path: 'ai' }}>\n` +
168
- ` <ChromaDb name="${name}" host="${host}" auth={true} />\n` +
169
- ` </Platform>\n` +
170
- `\n` +
171
- `Or reference a pre-created Secret (key: token):\n` +
172
- ` <ChromaDb name="${name}" host="${host}" auth={true} authTokenSecretName="${name}-auth-token" />`
173
- )
174
- }
175
-
176
- const spec = {
177
- ...(secretProvider.backend === 'vault'
178
- ? { vaultAuthRef: secretProvider.authRef }
179
- : { openbaoAuthRef: secretProvider.authRef }),
180
- mount: secretProvider.mount,
181
- type: 'kv-v2' as const,
182
- path: `${secretProvider.path ?? name}/${name}/auth-token`,
183
- destination: { create: true, name: tokenSecretName },
184
- }
185
- resources_.push(
186
- secretProvider.backend === 'vault'
187
- ? jsx('VaultStaticSecret', {
188
- apiVersion: 'secrets.hashicorp.com/v1beta1',
189
- kind: 'VaultStaticSecret',
190
- metadata: { name: `${name}-auth-token`, namespace },
191
- spec,
192
- })
193
- : jsx('OpenBaoStaticSecret', {
194
- apiVersion: 'secrets.openbao.org/v1beta1',
195
- kind: 'OpenBaoStaticSecret',
196
- metadata: { name: `${name}-auth-token`, namespace },
197
- spec,
198
- })
199
- )
200
- }
201
-
202
- // --- Env wiring -------------------------------------------------------------
203
- // Only CHROMA_* env vars — every credential arrives via secretKeyRef at
204
- // runtime (pushed first so dependent $(VAR) expansion resolves).
205
- const envVars: {
206
- name: string
207
- value?: string
208
- valueFrom?: { secretKeyRef: { name: string; key: string } }
209
- }[] = []
210
-
211
- // The listener port must track the `port` prop or the server ignores it
212
- // and binds the image default (8000), breaking the Service and probes.
213
- envVars.push({ name: 'CHROMA_SERVER_HTTP_PORT', value: String(port) })
214
-
215
- if (pg) {
216
- // Metadata store: CNPG cluster provisioned as `${name}-meta`. Its
217
- // connection info follows the Database recipe convention.
218
- const dbHost = `${name}-meta-rw`
219
- envVars.push(
220
- { name: 'CHROMA_POSTGRES_HOST', value: dbHost },
221
- { name: 'CHROMA_POSTGRES_PORT', value: '5432' },
222
- { name: 'CHROMA_POSTGRES_DATABASE', value: `${name}-meta` },
223
- { name: 'CHROMA_POSTGRES_USER', value: `${name}-meta` },
224
- {
225
- name: 'CHROMA_POSTGRES_PASSWORD',
226
- valueFrom: {
227
- secretKeyRef: { name: `${name}-meta-db-credentials`, key: 'password' },
228
- },
229
- }
230
- )
231
- }
232
-
233
- if (auth) {
234
- envVars.push(
235
- {
236
- name: 'CHROMA_SERVER_AUTH_CREDENTIALS',
237
- valueFrom: { secretKeyRef: { name: tokenSecretName, key: 'token' } },
238
- },
239
- {
240
- name: 'CHROMA_SERVER_AUTH_PROVIDER',
241
- value: 'chromadb.auth.token.TokenAuthenticationServerProvider',
242
- }
243
- )
244
- }
245
-
246
- // --- Data-plane resources ----------------------------------------------------
247
- // WebService cannot mount volumes, so the app runs as a raw Deployment
248
- // with heartbeat probes. Chroma persists to /data by default in the
249
- // official image.
250
- const dataPlane: ReturnType<typeof jsx>[] = []
251
-
252
- dataPlane.push(
253
- jsx('PersistentVolumeClaim', {
254
- apiVersion: 'v1',
255
- kind: 'PersistentVolumeClaim',
256
- metadata: { name: `${name}-data`, namespace },
257
- spec: {
258
- accessModes: ['ReadWriteOnce'],
259
- ...(storageClassName && { storageClassName }),
260
- resources: { requests: { storage } },
261
- },
262
- })
263
- )
264
-
265
- // --- Deployment + Service (raw jsx — WebService lacks volume mounts) --------
266
- dataPlane.push(
267
- jsx('Deployment', {
268
- apiVersion: 'apps/v1',
269
- kind: 'Deployment',
270
- metadata: { name, namespace, labels: { app: name } },
271
- spec: {
272
- replicas,
273
- selector: { matchLabels: { app: name } },
274
- template: {
275
- metadata: { labels: { app: name } },
276
- spec: {
277
- containers: [
278
- {
279
- name: 'chroma',
280
- image: `ghcr.io/chroma-core/chroma:${version}`,
281
- imagePullPolicy: 'Always',
282
- ports: [{ containerPort: port }],
283
- env: envVars,
284
- ...(resources && { resources }),
285
- volumeMounts: [{ name: 'data', mountPath: '/data' }],
286
- livenessProbe: {
287
- httpGet: { path: probePath, port },
288
- initialDelaySeconds: 10,
289
- periodSeconds: 10,
290
- },
291
- readinessProbe: {
292
- httpGet: { path: probePath, port },
293
- initialDelaySeconds: 5,
294
- periodSeconds: 5,
295
- },
296
- },
297
- ],
298
- volumes: [{ name: 'data', persistentVolumeClaim: { claimName: `${name}-data` } }],
299
- },
300
- },
301
- },
302
- }),
303
- jsx('Service', {
304
- apiVersion: 'v1',
305
- kind: 'Service',
306
- metadata: { name, namespace },
307
- spec: {
308
- type: 'ClusterIP',
309
- selector: { app: name },
310
- ports: [{ name: 'http', port, targetPort: port, protocol: 'TCP' }],
311
- },
312
- })
313
- )
314
-
315
- // --- Metadata store (CNPG) — wraps the data-plane resources so connection
316
- // info stays consistent with the Database recipe convention -----------------
317
- if (pg) {
318
- resources_.push(
319
- jsx(Database, {
320
- name: `${name}-meta`,
321
- namespace,
322
- storage: '10Gi',
323
- children: jsx(Fragment, { children: dataPlane }),
324
- })
325
- )
326
- } else {
327
- resources_.push(...dataPlane)
328
- }
329
-
330
- // --- Autoscaling --------------------------------------------------------------
331
- if (autoscaling) {
332
- resources_.push(
333
- jsx('HorizontalPodAutoscaler', {
334
- apiVersion: 'autoscaling/v2',
335
- kind: 'HorizontalPodAutoscaler',
336
- metadata: { name: `${name}-hpa`, namespace },
337
- spec: {
338
- scaleTargetRef: { apiVersion: 'apps/v1', kind: 'Deployment', name },
339
- minReplicas: replicas,
340
- maxReplicas: replicas * 3,
341
- metrics: [
342
- {
343
- type: 'Resource',
344
- resource: {
345
- name: 'cpu',
346
- target: { type: 'Utilization', averageUtilization: 70 },
347
- },
348
- },
349
- ],
350
- },
351
- })
352
- )
353
- }
354
-
355
- // --- Endpoint ------------------------------------------------------------------
356
- resources_.push(
357
- <Endpoint
358
- name={`${name}-endpoint`}
359
- namespace={namespace}
360
- host={host}
361
- serviceName={name}
362
- servicePort={port}
363
- tls={tls}
364
- />
365
- )
366
-
367
- return jsx(Fragment, { children: resources_ })
368
- }
package/tsconfig.json DELETED
@@ -1,15 +0,0 @@
1
- {
2
- "extends": "../../tsconfig.json",
3
- "compilerOptions": {
4
- "outDir": "./dist",
5
- "rootDir": "./src",
6
- "composite": true
7
- },
8
- "include": ["src/**/*"],
9
- "references": [
10
- { "path": "../core" },
11
- { "path": "../k8s-types" },
12
- { "path": "../crds" },
13
- { "path": "../recipes" }
14
- ]
15
- }