@r8s/open-webui 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.
- package/dist/index.d.ts +144 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +304 -0
- package/dist/index.js.map +1 -0
- package/package.json +14 -7
- package/__tests__/open-webui.test.ts +0 -457
- package/examples/basic.tsx +0 -8
- package/src/index.tsx +0 -426
- package/tsconfig.json +0 -15
|
@@ -1,457 +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
|
-
// Open WebUI recipe tests:
|
|
9
|
-
// 1. Operator declarations (cnpg via Database, redis, deduped via OperatorContext)
|
|
10
|
-
// 2. Rendering: defaults incl. uploads PVC, all props, gateway/ingress adaptation
|
|
11
|
-
// 3. Env wiring: upstream env names, no duplicate env names, probes, offline mode
|
|
12
|
-
// 4. Security: no plaintext credentials in rendered output
|
|
13
|
-
import { OpenWebui } from '../src/index'
|
|
14
|
-
|
|
15
|
-
/** Render OpenWebui inside a Platform-like secrets backend (OpenBao). */
|
|
16
|
-
function renderOpenWebui(props: Record<string, unknown>): ReturnType<typeof render> {
|
|
17
|
-
return render(
|
|
18
|
-
jsx(SecretContext.Provider, {
|
|
19
|
-
value: { backend: 'openbao', mount: 'kv', path: 'test' },
|
|
20
|
-
children: jsx(OpenWebui, props as never),
|
|
21
|
-
})
|
|
22
|
-
)
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
/** Render OpenWebui wrapped only in an OperatorContext (no secrets backend). */
|
|
26
|
-
function renderOpenWebuiWithContext(operators_: any[], props: Record<string, unknown>): r8sElement {
|
|
27
|
-
return jsx(OperatorContext.Provider, {
|
|
28
|
-
value: operators_,
|
|
29
|
-
children: jsx(OpenWebui, props as never),
|
|
30
|
-
})
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* k8s rejects duplicate env names in one container — assert every env
|
|
35
|
-
* list we render is unique.
|
|
36
|
-
*/
|
|
37
|
-
function assertUniqueEnvNames(env: Array<{ name: string }>): void {
|
|
38
|
-
const names = env.map((e) => e.name)
|
|
39
|
-
const dupes = names.filter((n, i) => names.indexOf(n) !== i)
|
|
40
|
-
expect(dupes).toEqual([])
|
|
41
|
-
expect(new Set(names).size).toBe(names.length)
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
const openbao = { backend: 'openbao', mount: 'kv', path: 'test' }
|
|
45
|
-
|
|
46
|
-
describe('operator declarations', () => {
|
|
47
|
-
it('declares the cnpg operator via the Database recipe', () => {
|
|
48
|
-
const result = renderOpenWebui({ host: 'chat.example.com' })
|
|
49
|
-
expect(result.operators.some((op) => op.name === 'cnpg')).toBe(true)
|
|
50
|
-
})
|
|
51
|
-
|
|
52
|
-
it('declares the redis operator when cache is enabled', () => {
|
|
53
|
-
const result = renderOpenWebui({ host: 'chat.example.com', cache: true })
|
|
54
|
-
expect(result.operators.some((op) => op.name === 'redis-operator')).toBe(true)
|
|
55
|
-
})
|
|
56
|
-
|
|
57
|
-
it('does not declare operators when cache is off (default)', () => {
|
|
58
|
-
const result = render(
|
|
59
|
-
jsx(OpenWebui, { host: 'chat.example.com', secretsName: 'existing-secrets' })
|
|
60
|
-
)
|
|
61
|
-
const names = result.operators.map((op) => op.name)
|
|
62
|
-
expect(names).toContain('cnpg')
|
|
63
|
-
expect(names).not.toContain('redis-operator')
|
|
64
|
-
})
|
|
65
|
-
|
|
66
|
-
it('deduplicates operators provided via context', () => {
|
|
67
|
-
const result = render(
|
|
68
|
-
renderOpenWebuiWithContext([operators['redis-operator'](), operators['cnpg']()], {
|
|
69
|
-
host: 'chat.example.com',
|
|
70
|
-
cache: true,
|
|
71
|
-
secretsName: 'existing-secrets',
|
|
72
|
-
})
|
|
73
|
-
)
|
|
74
|
-
const names = result.operators.map((op) => op.name)
|
|
75
|
-
expect(names.filter((n) => n === 'redis-operator')).toHaveLength(1)
|
|
76
|
-
expect(names.filter((n) => n === 'cnpg')).toHaveLength(1)
|
|
77
|
-
})
|
|
78
|
-
|
|
79
|
-
it('allows version overrides through context operators', () => {
|
|
80
|
-
const result = render(
|
|
81
|
-
renderOpenWebuiWithContext([operators['redis-operator']('1.0.0')], {
|
|
82
|
-
host: 'chat.example.com',
|
|
83
|
-
cache: true,
|
|
84
|
-
secretsName: 'existing-secrets',
|
|
85
|
-
})
|
|
86
|
-
)
|
|
87
|
-
const redis = result.operators.filter((op) => op.name === 'redis-operator')
|
|
88
|
-
expect(redis).toHaveLength(1)
|
|
89
|
-
expect(redis[0].version).toBe('1.0.0')
|
|
90
|
-
})
|
|
91
|
-
})
|
|
92
|
-
|
|
93
|
-
describe('rendering defaults', () => {
|
|
94
|
-
it('renders deployment, service, uploads PVC, database and endpoint', () => {
|
|
95
|
-
const result = renderOpenWebui({ host: 'chat.example.com', storage: '10Gi' })
|
|
96
|
-
const kinds = result.resources.map((r) => r.kind)
|
|
97
|
-
expect(kinds).toContain('Deployment')
|
|
98
|
-
expect(kinds).toContain('Service')
|
|
99
|
-
expect(kinds).toContain('Ingress')
|
|
100
|
-
expect(kinds).toContain('Cluster')
|
|
101
|
-
expect(kinds).toContain('PersistentVolumeClaim')
|
|
102
|
-
|
|
103
|
-
const pvc = result.resources.find(
|
|
104
|
-
(r) => r.kind === 'PersistentVolumeClaim' && (r as any).metadata.name === 'open-webui-uploads'
|
|
105
|
-
) as any
|
|
106
|
-
expect(pvc.spec.accessModes).toEqual(['ReadWriteOnce'])
|
|
107
|
-
expect(pvc.spec.resources.requests.storage).toBe('10Gi')
|
|
108
|
-
})
|
|
109
|
-
|
|
110
|
-
it('wires the deployment image, port, probes and PVC mount', () => {
|
|
111
|
-
const result = renderOpenWebui({ host: 'chat.example.com', storage: '10Gi' })
|
|
112
|
-
const app = result.resources.find(
|
|
113
|
-
(r) => r.kind === 'Deployment' && (r as any).metadata.name === 'open-webui'
|
|
114
|
-
) as any
|
|
115
|
-
const container = app.spec.template.spec.containers[0]
|
|
116
|
-
expect(container.image).toBe('ghcr.io/open-webui/open-webui:latest')
|
|
117
|
-
expect(container.ports[0].containerPort).toBe(8080)
|
|
118
|
-
expect(container.livenessProbe.httpGet).toEqual({ path: '/health', port: 8080 })
|
|
119
|
-
expect(container.readinessProbe.httpGet).toEqual({ path: '/health', port: 8080 })
|
|
120
|
-
// First boot runs Alembic migrations — probes give the app room
|
|
121
|
-
expect(container.livenessProbe.initialDelaySeconds).toBe(30)
|
|
122
|
-
expect(container.livenessProbe.failureThreshold).toBe(6)
|
|
123
|
-
expect(container.readinessProbe.initialDelaySeconds).toBe(30)
|
|
124
|
-
expect(container.readinessProbe.failureThreshold).toBe(6)
|
|
125
|
-
expect(container.volumeMounts).toEqual([{ name: 'uploads', mountPath: '/app/backend/data' }])
|
|
126
|
-
expect(app.spec.template.spec.volumes).toEqual([
|
|
127
|
-
{ name: 'uploads', persistentVolumeClaim: { claimName: 'open-webui-uploads' } },
|
|
128
|
-
])
|
|
129
|
-
})
|
|
130
|
-
|
|
131
|
-
it('renders no PVC or volumes when storage is not set', () => {
|
|
132
|
-
const result = renderOpenWebui({ host: 'chat.example.com' })
|
|
133
|
-
const kinds = result.resources.map((r) => r.kind)
|
|
134
|
-
expect(kinds).not.toContain('PersistentVolumeClaim')
|
|
135
|
-
const app = result.resources.find(
|
|
136
|
-
(r) => r.kind === 'Deployment' && (r as any).metadata.name === 'open-webui'
|
|
137
|
-
) as any
|
|
138
|
-
const container = app.spec.template.spec.containers[0]
|
|
139
|
-
expect(container.volumeMounts).toBeUndefined()
|
|
140
|
-
expect(app.spec.template.spec.volumes).toBeUndefined()
|
|
141
|
-
})
|
|
142
|
-
|
|
143
|
-
it('provisions Redis replication and REDIS_URL against the master service', () => {
|
|
144
|
-
const result = renderOpenWebui({ host: 'chat.example.com', cache: true })
|
|
145
|
-
const kinds = result.resources.map((r) => r.kind)
|
|
146
|
-
expect(kinds).toContain('RedisReplication')
|
|
147
|
-
expect(kinds).not.toContain('RedisCluster')
|
|
148
|
-
|
|
149
|
-
const redis = result.resources.find((r) => r.kind === 'RedisReplication') as any
|
|
150
|
-
expect(redis.metadata.name).toBe('open-webui-redis')
|
|
151
|
-
expect(redis.spec.clusterSize).toBe(3)
|
|
152
|
-
expect(redis.spec.kubernetesConfig.image).toBe('redis:7.2-alpine')
|
|
153
|
-
|
|
154
|
-
const app = result.resources.find(
|
|
155
|
-
(r) => r.kind === 'Deployment' && (r as any).metadata.name === 'open-webui'
|
|
156
|
-
) as any
|
|
157
|
-
const redisUrl = app.spec.template.spec.containers[0].env.find(
|
|
158
|
-
(e: any) => e.name === 'REDIS_URL'
|
|
159
|
-
)
|
|
160
|
-
expect(redisUrl.value).toBe('redis://open-webui-redis:6379')
|
|
161
|
-
})
|
|
162
|
-
|
|
163
|
-
it('defaults ENABLE_OLLAMA_API to false', () => {
|
|
164
|
-
const result = renderOpenWebui({ host: 'chat.example.com' })
|
|
165
|
-
const app = result.resources.find(
|
|
166
|
-
(r) => r.kind === 'Deployment' && (r as any).metadata.name === 'open-webui'
|
|
167
|
-
) as any
|
|
168
|
-
const env = app.spec.template.spec.containers[0].env
|
|
169
|
-
expect(env.find((e: any) => e.name === 'ENABLE_OLLAMA_API').value).toBe('false')
|
|
170
|
-
assertUniqueEnvNames(env)
|
|
171
|
-
})
|
|
172
|
-
|
|
173
|
-
it('renders gateway resources when platform uses gateway routing', () => {
|
|
174
|
-
const result = render(
|
|
175
|
-
jsx(RoutingContext.Provider, {
|
|
176
|
-
value: { mode: 'gateway', gatewayClassName: 'eg' },
|
|
177
|
-
children: jsx(SecretContext.Provider, {
|
|
178
|
-
value: openbao as never,
|
|
179
|
-
children: jsx(OpenWebui, { host: 'chat.example.com' }),
|
|
180
|
-
}),
|
|
181
|
-
})
|
|
182
|
-
)
|
|
183
|
-
const kinds = result.resources.map((r) => r.kind)
|
|
184
|
-
expect(kinds).toContain('HTTPRoute')
|
|
185
|
-
})
|
|
186
|
-
|
|
187
|
-
it('renders a valid Ingress when platform uses ingress routing', () => {
|
|
188
|
-
const result = render(
|
|
189
|
-
jsx(RoutingContext.Provider, {
|
|
190
|
-
value: { mode: 'ingress' },
|
|
191
|
-
children: jsx(SecretContext.Provider, {
|
|
192
|
-
value: openbao as never,
|
|
193
|
-
children: jsx(OpenWebui, { host: 'chat.example.com' }),
|
|
194
|
-
}),
|
|
195
|
-
})
|
|
196
|
-
)
|
|
197
|
-
const ingress = result.resources.find((r) => r.kind === 'Ingress') as any
|
|
198
|
-
expect(ingress).toBeDefined()
|
|
199
|
-
expect(ingress.spec.rules[0].host).toBe('chat.example.com')
|
|
200
|
-
})
|
|
201
|
-
|
|
202
|
-
it('points the Ingress at the service on port 8080', () => {
|
|
203
|
-
const result = renderOpenWebui({ host: 'chat.example.com' })
|
|
204
|
-
const ingress = result.resources.find((r) => r.kind === 'Ingress') as any
|
|
205
|
-
expect(ingress.spec.rules[0].http.paths[0].backend.service.name).toBe('open-webui')
|
|
206
|
-
expect(ingress.spec.rules[0].http.paths[0].backend.service.port.number).toBe(8080)
|
|
207
|
-
})
|
|
208
|
-
|
|
209
|
-
it('passes resource validation', () => {
|
|
210
|
-
const result = renderOpenWebui({
|
|
211
|
-
host: 'chat.example.com',
|
|
212
|
-
storage: '10Gi',
|
|
213
|
-
cache: true,
|
|
214
|
-
sso: {
|
|
215
|
-
issuer: 'https://keycloak.example.com/realms/platform',
|
|
216
|
-
clientId: 'open-webui',
|
|
217
|
-
clientSecretRef: { secret: 'open-webui-sso', key: 'clientSecret' },
|
|
218
|
-
},
|
|
219
|
-
})
|
|
220
|
-
for (const resource of result.resources) {
|
|
221
|
-
expect(validateResource(resource)).toEqual([])
|
|
222
|
-
}
|
|
223
|
-
})
|
|
224
|
-
})
|
|
225
|
-
|
|
226
|
-
describe('rendering with all props', () => {
|
|
227
|
-
it('accepts the full prop surface', () => {
|
|
228
|
-
const result = renderOpenWebui({
|
|
229
|
-
name: 'chat',
|
|
230
|
-
namespace: 'chat',
|
|
231
|
-
version: 'v0.6.5',
|
|
232
|
-
host: 'chat.example.com',
|
|
233
|
-
replicas: 3,
|
|
234
|
-
backend: 'https://api.example.com/v1',
|
|
235
|
-
cache: true,
|
|
236
|
-
sso: {
|
|
237
|
-
issuer: 'https://keycloak.example.com/realms/platform',
|
|
238
|
-
clientId: 'chat',
|
|
239
|
-
clientSecretRef: { secret: 'chat-sso', key: 'clientSecret' },
|
|
240
|
-
scopes: 'openid email profile groups',
|
|
241
|
-
},
|
|
242
|
-
resources: {
|
|
243
|
-
requests: { memory: '1Gi', cpu: '500m' },
|
|
244
|
-
limits: { memory: '4Gi', cpu: '2000m' },
|
|
245
|
-
},
|
|
246
|
-
tls: { secretName: 'chat-tls', clusterIssuer: 'letsencrypt-prod' },
|
|
247
|
-
})
|
|
248
|
-
expect(result.resources.length).toBeGreaterThan(0)
|
|
249
|
-
|
|
250
|
-
const app = result.resources.find(
|
|
251
|
-
(r) => r.kind === 'Deployment' && (r as any).metadata.name === 'chat'
|
|
252
|
-
) as any
|
|
253
|
-
const container = app.spec.template.spec.containers[0]
|
|
254
|
-
expect(container.image).toBe('ghcr.io/open-webui/open-webui:v0.6.5')
|
|
255
|
-
expect(app.spec.replicas).toBe(3)
|
|
256
|
-
expect(container.resources.limits.memory).toBe('4Gi')
|
|
257
|
-
|
|
258
|
-
const backendUrl = container.env.find((e: any) => e.name === 'OPENAI_API_BASE_URL')
|
|
259
|
-
expect(backendUrl.value).toBe('https://api.example.com/v1')
|
|
260
|
-
|
|
261
|
-
const kinds = result.resources.map((r) => r.kind)
|
|
262
|
-
expect(kinds).toContain('RedisReplication')
|
|
263
|
-
expect(kinds).toContain('Ingress')
|
|
264
|
-
expect(kinds).toContain('Cluster')
|
|
265
|
-
assertUniqueEnvNames(container.env)
|
|
266
|
-
})
|
|
267
|
-
})
|
|
268
|
-
|
|
269
|
-
describe('offline mode', () => {
|
|
270
|
-
it('sets OFFLINE_MODE, disables update checks and keeps Ollama off', () => {
|
|
271
|
-
const result = renderOpenWebui({ host: 'chat.example.com', offline: true })
|
|
272
|
-
const app = result.resources.find(
|
|
273
|
-
(r) => r.kind === 'Deployment' && (r as any).metadata.name === 'open-webui'
|
|
274
|
-
) as any
|
|
275
|
-
const env = app.spec.template.spec.containers[0].env
|
|
276
|
-
expect(env.find((e: any) => e.name === 'OFFLINE_MODE').value).toBe('true')
|
|
277
|
-
expect(env.find((e: any) => e.name === 'ENABLE_UPDATE_CHECK').value).toBe('false')
|
|
278
|
-
expect(env.find((e: any) => e.name === 'ENABLE_OLLAMA_API').value).toBe('false')
|
|
279
|
-
assertUniqueEnvNames(env)
|
|
280
|
-
})
|
|
281
|
-
|
|
282
|
-
it('leaves runtime networking untouched when offline is not set', () => {
|
|
283
|
-
const result = renderOpenWebui({ host: 'chat.example.com' })
|
|
284
|
-
const app = result.resources.find(
|
|
285
|
-
(r) => r.kind === 'Deployment' && (r as any).metadata.name === 'open-webui'
|
|
286
|
-
) as any
|
|
287
|
-
const env = app.spec.template.spec.containers[0].env
|
|
288
|
-
expect(env.find((e: any) => e.name === 'OFFLINE_MODE')).toBeUndefined()
|
|
289
|
-
expect(env.find((e: any) => e.name === 'ENABLE_UPDATE_CHECK')).toBeUndefined()
|
|
290
|
-
})
|
|
291
|
-
})
|
|
292
|
-
|
|
293
|
-
describe('secrets handling', () => {
|
|
294
|
-
it('provisions the model API key and secret key through OpenBao', () => {
|
|
295
|
-
const result = renderOpenWebui({ host: 'chat.example.com' })
|
|
296
|
-
const kind = result.resources.find((r) => r.kind === 'OpenBaoStaticSecret') as any
|
|
297
|
-
expect(kind).toBeDefined()
|
|
298
|
-
expect(kind.metadata.name).toBe('open-webui-secrets')
|
|
299
|
-
expect(kind.spec.path).toBe('test/open-webui/secrets')
|
|
300
|
-
expect(kind.spec.destination).toEqual({ create: true, name: 'open-webui-secrets' })
|
|
301
|
-
})
|
|
302
|
-
|
|
303
|
-
it('provisions the keys through Vault', () => {
|
|
304
|
-
const result = render(
|
|
305
|
-
jsx(SecretContext.Provider, {
|
|
306
|
-
value: { backend: 'vault', mount: 'kv', path: 'apps' },
|
|
307
|
-
children: jsx(OpenWebui, { host: 'chat.example.com' }),
|
|
308
|
-
})
|
|
309
|
-
)
|
|
310
|
-
const kinds = result.resources.map((r) => r.kind)
|
|
311
|
-
expect(kinds).toContain('VaultStaticSecret')
|
|
312
|
-
expect(kinds).not.toContain('OpenBaoStaticSecret')
|
|
313
|
-
})
|
|
314
|
-
|
|
315
|
-
it('accepts an explicit secretsName without a backend', () => {
|
|
316
|
-
expect(() =>
|
|
317
|
-
render(jsx(OpenWebui, { host: 'chat.example.com', secretsName: 'existing-secrets' }))
|
|
318
|
-
).not.toThrow()
|
|
319
|
-
const result = render(
|
|
320
|
-
jsx(OpenWebui, { host: 'chat.example.com', secretsName: 'existing-secrets' })
|
|
321
|
-
)
|
|
322
|
-
const kinds = result.resources.map((r) => r.kind)
|
|
323
|
-
expect(kinds).not.toContain('OpenBaoStaticSecret')
|
|
324
|
-
expect(kinds).not.toContain('VaultStaticSecret')
|
|
325
|
-
})
|
|
326
|
-
|
|
327
|
-
it('wires credentials via secretKeyRef (never plaintext env)', () => {
|
|
328
|
-
const result = renderOpenWebui({ host: 'chat.example.com', secretsName: 'existing-secrets' })
|
|
329
|
-
const app = result.resources.find(
|
|
330
|
-
(r) => r.kind === 'Deployment' && (r as any).metadata.name === 'open-webui'
|
|
331
|
-
) as any
|
|
332
|
-
const env = app.spec.template.spec.containers[0].env
|
|
333
|
-
const modelKey = env.find((e: any) => e.name === 'OPENAI_API_KEY')
|
|
334
|
-
const webuiSecret = env.find((e: any) => e.name === 'WEBUI_SECRET_KEY')
|
|
335
|
-
const dbPassword = env.find((e: any) => e.name === 'PGPASSWORD')
|
|
336
|
-
expect(modelKey.valueFrom.secretKeyRef.name).toBe('existing-secrets')
|
|
337
|
-
expect(modelKey.valueFrom.secretKeyRef.key).toBe('modelApiKey')
|
|
338
|
-
expect(webuiSecret.valueFrom.secretKeyRef.name).toBe('existing-secrets')
|
|
339
|
-
expect(webuiSecret.valueFrom.secretKeyRef.key).toBe('secretKey')
|
|
340
|
-
expect(dbPassword.valueFrom.secretKeyRef.name).toBe('open-webui-db-credentials')
|
|
341
|
-
expect(dbPassword.valueFrom.secretKeyRef.key).toBe('password')
|
|
342
|
-
expect(modelKey.value).toBeUndefined()
|
|
343
|
-
expect(webuiSecret.value).toBeUndefined()
|
|
344
|
-
expect(dbPassword.value).toBeUndefined()
|
|
345
|
-
})
|
|
346
|
-
|
|
347
|
-
it('declares secretKeyRef env entries before dependent $(VAR) expansion', () => {
|
|
348
|
-
const result = renderOpenWebui({ host: 'chat.example.com' })
|
|
349
|
-
const app = result.resources.find(
|
|
350
|
-
(r) => r.kind === 'Deployment' && (r as any).metadata.name === 'open-webui'
|
|
351
|
-
) as any
|
|
352
|
-
const env = app.spec.template.spec.containers[0].env
|
|
353
|
-
const names = env.map((e: any) => e.name)
|
|
354
|
-
assertUniqueEnvNames(env)
|
|
355
|
-
expect(names.indexOf('PGPASSWORD')).toBeLessThan(names.indexOf('DATABASE_URL'))
|
|
356
|
-
const databaseUrl = env.find((e: any) => e.name === 'DATABASE_URL')
|
|
357
|
-
expect(databaseUrl.value).toBe(
|
|
358
|
-
'postgresql://open-webui:$(PGPASSWORD)@open-webui-rw:5432/open-webui'
|
|
359
|
-
)
|
|
360
|
-
const modelKey = env.find((e: any) => e.name === 'OPENAI_API_KEY')
|
|
361
|
-
const backendUrl = env.find((e: any) => e.name === 'OPENAI_API_BASE_URL')
|
|
362
|
-
expect(names.indexOf('OPENAI_API_KEY')).toBeLessThan(names.indexOf('OPENAI_API_BASE_URL'))
|
|
363
|
-
expect(modelKey.valueFrom.secretKeyRef.name).toBe('open-webui-secrets')
|
|
364
|
-
expect(backendUrl.value).toBe('https://api.berget.ai/v1')
|
|
365
|
-
})
|
|
366
|
-
|
|
367
|
-
it('renders no plaintext credentials anywhere', () => {
|
|
368
|
-
const result = renderOpenWebui({
|
|
369
|
-
host: 'chat.example.com',
|
|
370
|
-
storage: '10Gi',
|
|
371
|
-
cache: true,
|
|
372
|
-
sso: {
|
|
373
|
-
issuer: 'https://keycloak.example.com/realms/platform',
|
|
374
|
-
clientId: 'open-webui',
|
|
375
|
-
clientSecretRef: { secret: 'open-webui-sso', key: 'clientSecret' },
|
|
376
|
-
},
|
|
377
|
-
})
|
|
378
|
-
const { passed, errors } = runGuardrails(result.resources as any[], [noPlaintextSecrets])
|
|
379
|
-
if (!passed) {
|
|
380
|
-
console.error('Plaintext credential violations:', errors)
|
|
381
|
-
}
|
|
382
|
-
expect(passed).toBe(true)
|
|
383
|
-
})
|
|
384
|
-
})
|
|
385
|
-
|
|
386
|
-
describe('sso wiring', () => {
|
|
387
|
-
it('wires upstream OAUTH env names and the client secret via secretKeyRef', () => {
|
|
388
|
-
const result = renderOpenWebui({
|
|
389
|
-
host: 'chat.example.com',
|
|
390
|
-
sso: {
|
|
391
|
-
issuer: 'https://keycloak.example.com/realms/platform',
|
|
392
|
-
clientId: 'open-webui',
|
|
393
|
-
clientSecretRef: { secret: 'open-webui-sso', key: 'clientSecret' },
|
|
394
|
-
scopes: 'openid email profile',
|
|
395
|
-
},
|
|
396
|
-
})
|
|
397
|
-
const app = result.resources.find(
|
|
398
|
-
(r) => r.kind === 'Deployment' && (r as any).metadata.name === 'open-webui'
|
|
399
|
-
) as any
|
|
400
|
-
const env = app.spec.template.spec.containers[0].env
|
|
401
|
-
const names = env.map((e: any) => e.name)
|
|
402
|
-
assertUniqueEnvNames(env)
|
|
403
|
-
|
|
404
|
-
const clientId = env.find((e: any) => e.name === 'OAUTH_CLIENT_ID')
|
|
405
|
-
const clientSecret = env.find((e: any) => e.name === 'OAUTH_CLIENT_SECRET')
|
|
406
|
-
const providerUrl = env.find((e: any) => e.name === 'OPENID_PROVIDER_URL')
|
|
407
|
-
const scopes = env.find((e: any) => e.name === 'OAUTH_SCOPES')
|
|
408
|
-
const redirectUri = env.find((e: any) => e.name === 'OPENID_REDIRECT_URI')
|
|
409
|
-
expect(clientId.value).toBe('open-webui')
|
|
410
|
-
expect(clientSecret.valueFrom.secretKeyRef.name).toBe('open-webui-sso')
|
|
411
|
-
expect(clientSecret.valueFrom.secretKeyRef.key).toBe('clientSecret')
|
|
412
|
-
expect(clientSecret.value).toBeUndefined()
|
|
413
|
-
// OPENID_PROVIDER_URL is the issuer — Open WebUI appends the
|
|
414
|
-
// /.well-known/openid-configuration discovery path itself
|
|
415
|
-
expect(providerUrl.value).toBe('https://keycloak.example.com/realms/platform')
|
|
416
|
-
expect(scopes.value).toBe('openid email profile')
|
|
417
|
-
expect(redirectUri.value).toBe('https://chat.example.com/oauth/oidc/callback')
|
|
418
|
-
expect(env.find((e: any) => e.name === 'ENABLE_OAUTH_SIGNUP').value).toBe('true')
|
|
419
|
-
// legacy env names must not leak
|
|
420
|
-
expect(names).not.toContain('OPENID_CLIENT_ID')
|
|
421
|
-
expect(names).not.toContain('OPENID_CLIENT_SECRET')
|
|
422
|
-
expect(names).not.toContain('OPENID_SCOPES')
|
|
423
|
-
expect(names).not.toContain('ENABLE_OPENID_SIGNUP')
|
|
424
|
-
})
|
|
425
|
-
})
|
|
426
|
-
|
|
427
|
-
describe('validation errors', () => {
|
|
428
|
-
it('throws when no secrets backend and no existing Secret ref', () => {
|
|
429
|
-
expect(() => render(jsx(OpenWebui, { host: 'chat.example.com' }))).toThrow(
|
|
430
|
-
/application secrets/
|
|
431
|
-
)
|
|
432
|
-
})
|
|
433
|
-
|
|
434
|
-
it('throws for unknown secrets backends', () => {
|
|
435
|
-
expect(() =>
|
|
436
|
-
render(
|
|
437
|
-
jsx(SecretContext.Provider, {
|
|
438
|
-
value: { backend: 'unknown' as never },
|
|
439
|
-
children: jsx(OpenWebui, { host: 'chat.example.com' }),
|
|
440
|
-
})
|
|
441
|
-
)
|
|
442
|
-
).toThrow(/application secrets/)
|
|
443
|
-
})
|
|
444
|
-
|
|
445
|
-
it('throws when storage is combined with multiple replicas (RWO single attach)', () => {
|
|
446
|
-
expect(() =>
|
|
447
|
-
renderOpenWebui({ host: 'chat.example.com', storage: '10Gi', replicas: 2 })
|
|
448
|
-
).toThrow(/storage with replicas/)
|
|
449
|
-
})
|
|
450
|
-
|
|
451
|
-
it('allows single-replica storage and multi-replica without storage', () => {
|
|
452
|
-
expect(() =>
|
|
453
|
-
renderOpenWebui({ host: 'chat.example.com', storage: '10Gi', replicas: 1 })
|
|
454
|
-
).not.toThrow()
|
|
455
|
-
expect(() => renderOpenWebui({ host: 'chat.example.com', replicas: 3 })).not.toThrow()
|
|
456
|
-
})
|
|
457
|
-
})
|
package/examples/basic.tsx
DELETED
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
import { Platform } from '@r8s/recipes'
|
|
2
|
-
import { OpenWebui } from '@r8s/open-webui'
|
|
3
|
-
|
|
4
|
-
export default (
|
|
5
|
-
<Platform secrets={{ backend: 'openbao', mount: 'kv', path: 'apps' }}>
|
|
6
|
-
<OpenWebui name="chat" host="chat.example.com" version="v0.6.5" storage="10Gi" />
|
|
7
|
-
</Platform>
|
|
8
|
-
)
|