@r8s/supabase 0.2.0 → 0.2.1
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 +131 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +258 -0
- package/dist/index.js.map +1 -0
- package/package.json +5 -2
- package/__tests__/supabase.test.ts +0 -480
- package/examples/basic.tsx +0 -16
- package/src/index.tsx +0 -454
- package/tsconfig.json +0 -15
|
@@ -1,480 +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
|
-
// Supabase recipe tests:
|
|
9
|
-
// 1. Operator declarations (cnpg via Database, deduped via OperatorContext)
|
|
10
|
-
// 2. Rendering: defaults (>=5 service deployments), all props, gateway/ingress adaptation
|
|
11
|
-
// 3. Security: no plaintext credentials in rendered output
|
|
12
|
-
import { Supabase } from '../src/index'
|
|
13
|
-
|
|
14
|
-
const openbao = { backend: 'openbao', mount: 'kv', path: 'test' }
|
|
15
|
-
|
|
16
|
-
const objectStorage = {
|
|
17
|
-
endpoint: 'https://s3.internal.example.com',
|
|
18
|
-
bucket: 'supabase-uploads',
|
|
19
|
-
credentialsSecret: 'object-store-credentials',
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
/** Render Supabase inside a Platform-like secrets backend (OpenBao). */
|
|
23
|
-
function renderSupabase(props: Record<string, unknown>): ReturnType<typeof render> {
|
|
24
|
-
return render(
|
|
25
|
-
jsx(SecretContext.Provider, {
|
|
26
|
-
value: openbao as never,
|
|
27
|
-
children: jsx(Supabase, props as never),
|
|
28
|
-
})
|
|
29
|
-
)
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
/** Render Supabase wrapped only in an OperatorContext (no secrets backend). */
|
|
33
|
-
function renderSupabaseWithContext(operators_: any[], props: Record<string, unknown>): r8sElement {
|
|
34
|
-
return jsx(OperatorContext.Provider, {
|
|
35
|
-
value: operators_,
|
|
36
|
-
children: jsx(Supabase, props as never),
|
|
37
|
-
})
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
describe('operator declarations', () => {
|
|
41
|
-
it('declares the cnpg operator via the Database recipe', () => {
|
|
42
|
-
const result = renderSupabase({ host: 'backend.example.com', objectStorage })
|
|
43
|
-
expect(result.operators.some((op) => op.name === 'cnpg')).toBe(true)
|
|
44
|
-
})
|
|
45
|
-
|
|
46
|
-
it('deduplicates operators provided via context', () => {
|
|
47
|
-
const result = render(
|
|
48
|
-
renderSupabaseWithContext([operators['cnpg']()], {
|
|
49
|
-
host: 'backend.example.com',
|
|
50
|
-
objectStorage,
|
|
51
|
-
jwtSecretsName: 'existing-jwt',
|
|
52
|
-
})
|
|
53
|
-
)
|
|
54
|
-
const names = result.operators.map((op) => op.name)
|
|
55
|
-
expect(names.filter((n) => n === 'cnpg')).toHaveLength(1)
|
|
56
|
-
})
|
|
57
|
-
|
|
58
|
-
it('allows version overrides through context operators', () => {
|
|
59
|
-
const result = render(
|
|
60
|
-
renderSupabaseWithContext([operators['cnpg']('1.27.0')], {
|
|
61
|
-
host: 'backend.example.com',
|
|
62
|
-
objectStorage,
|
|
63
|
-
jwtSecretsName: 'existing-jwt',
|
|
64
|
-
})
|
|
65
|
-
)
|
|
66
|
-
const cnpg = result.operators.filter((op) => op.name === 'cnpg')
|
|
67
|
-
expect(cnpg).toHaveLength(1)
|
|
68
|
-
expect(cnpg[0].version).toBe('1.27.0')
|
|
69
|
-
})
|
|
70
|
-
})
|
|
71
|
-
|
|
72
|
-
describe('rendering defaults', () => {
|
|
73
|
-
it('renders the service suite, database and endpoint', () => {
|
|
74
|
-
const result = renderSupabase({ host: 'backend.example.com', objectStorage })
|
|
75
|
-
const kinds = result.resources.map((r) => r.kind)
|
|
76
|
-
expect(kinds).toContain('Deployment')
|
|
77
|
-
expect(kinds).toContain('Service')
|
|
78
|
-
expect(kinds).toContain('Ingress')
|
|
79
|
-
expect(kinds).toContain('Cluster')
|
|
80
|
-
const deployments = result.resources.filter((r) => r.kind === 'Deployment')
|
|
81
|
-
expect(deployments.length).toBeGreaterThanOrEqual(5)
|
|
82
|
-
const names = deployments.map((d: any) => d.metadata.name)
|
|
83
|
-
expect(names).toContain('supabase-gotrue')
|
|
84
|
-
expect(names).toContain('supabase-postgrest')
|
|
85
|
-
expect(names).toContain('supabase-realtime')
|
|
86
|
-
expect(names).toContain('supabase-storage-api')
|
|
87
|
-
expect(names).toContain('supabase-imgproxy')
|
|
88
|
-
})
|
|
89
|
-
|
|
90
|
-
it('renders gateway resources when platform uses gateway routing', () => {
|
|
91
|
-
const result = render(
|
|
92
|
-
jsx(RoutingContext.Provider, {
|
|
93
|
-
value: { mode: 'gateway', gatewayClassName: 'eg' },
|
|
94
|
-
children: jsx(SecretContext.Provider, {
|
|
95
|
-
value: openbao as never,
|
|
96
|
-
children: jsx(Supabase, { host: 'backend.example.com', objectStorage }),
|
|
97
|
-
}),
|
|
98
|
-
})
|
|
99
|
-
)
|
|
100
|
-
const kinds = result.resources.map((r) => r.kind)
|
|
101
|
-
expect(kinds).toContain('HTTPRoute')
|
|
102
|
-
})
|
|
103
|
-
|
|
104
|
-
it('renders a valid Ingress when platform uses ingress routing', () => {
|
|
105
|
-
const result = render(
|
|
106
|
-
jsx(RoutingContext.Provider, {
|
|
107
|
-
value: { mode: 'ingress' },
|
|
108
|
-
children: jsx(SecretContext.Provider, {
|
|
109
|
-
value: openbao as never,
|
|
110
|
-
children: jsx(Supabase, { host: 'backend.example.com', objectStorage }),
|
|
111
|
-
}),
|
|
112
|
-
})
|
|
113
|
-
)
|
|
114
|
-
const ingress = result.resources.find((r) => r.kind === 'Ingress') as any
|
|
115
|
-
expect(ingress).toBeDefined()
|
|
116
|
-
expect(ingress.spec.rules[0].host).toBe('backend.example.com')
|
|
117
|
-
})
|
|
118
|
-
|
|
119
|
-
it('passes resource validation', () => {
|
|
120
|
-
const result = renderSupabase({ host: 'backend.example.com', objectStorage })
|
|
121
|
-
for (const resource of result.resources) {
|
|
122
|
-
expect(validateResource(resource)).toEqual([])
|
|
123
|
-
}
|
|
124
|
-
})
|
|
125
|
-
})
|
|
126
|
-
|
|
127
|
-
describe('service probes', () => {
|
|
128
|
-
it('configures per-service probe paths', () => {
|
|
129
|
-
const result = renderSupabase({ host: 'backend.example.com', objectStorage })
|
|
130
|
-
const containerOf = (name: string) =>
|
|
131
|
-
(
|
|
132
|
-
result.resources.find(
|
|
133
|
-
(r: any) => r.kind === 'Deployment' && r.metadata.name === name
|
|
134
|
-
) as any
|
|
135
|
-
).spec.template.spec.containers[0]
|
|
136
|
-
|
|
137
|
-
for (const serviceName of ['supabase-gotrue', 'supabase-realtime', 'supabase-imgproxy']) {
|
|
138
|
-
const container = containerOf(serviceName)
|
|
139
|
-
expect(container.livenessProbe.httpGet.path).toBe('/health')
|
|
140
|
-
expect(container.readinessProbe.httpGet.path).toBe('/health')
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
const postgrest = containerOf('supabase-postgrest')
|
|
144
|
-
expect(postgrest.livenessProbe.httpGet.path).toBe('/')
|
|
145
|
-
expect(postgrest.readinessProbe.httpGet.path).toBe('/')
|
|
146
|
-
|
|
147
|
-
const storage = containerOf('supabase-storage-api')
|
|
148
|
-
expect(storage.livenessProbe.httpGet.path).toBe('/status')
|
|
149
|
-
expect(storage.readinessProbe.httpGet.path).toBe('/status')
|
|
150
|
-
})
|
|
151
|
-
|
|
152
|
-
it('gives GoTrue a longer probe initial delay', () => {
|
|
153
|
-
const result = renderSupabase({ host: 'backend.example.com', objectStorage })
|
|
154
|
-
const gotrue = result.resources.find(
|
|
155
|
-
(r: any) => r.kind === 'Deployment' && r.metadata.name === 'supabase-gotrue'
|
|
156
|
-
) as any
|
|
157
|
-
const container = gotrue.spec.template.spec.containers[0]
|
|
158
|
-
expect(container.livenessProbe.initialDelaySeconds).toBe(15)
|
|
159
|
-
expect(container.readinessProbe.initialDelaySeconds).toBe(15)
|
|
160
|
-
})
|
|
161
|
-
})
|
|
162
|
-
|
|
163
|
-
describe('database bootstrap', () => {
|
|
164
|
-
it('bootstraps extensions, roles and schemas via postInitSQL', () => {
|
|
165
|
-
const result = renderSupabase({ host: 'backend.example.com', objectStorage })
|
|
166
|
-
const cluster = result.resources.find((r) => r.kind === 'Cluster') as any
|
|
167
|
-
const sql = cluster.spec.bootstrap.initdb.postInitApplicationSQL as string[]
|
|
168
|
-
expect(Array.isArray(sql)).toBe(true)
|
|
169
|
-
expect(sql.length).toBeGreaterThan(0)
|
|
170
|
-
for (const statement of sql) {
|
|
171
|
-
expect(typeof statement).toBe('string')
|
|
172
|
-
}
|
|
173
|
-
const joined = sql.join('\n')
|
|
174
|
-
for (const needle of [
|
|
175
|
-
'CREATE EXTENSION IF NOT EXISTS pgcrypto;',
|
|
176
|
-
'CREATE EXTENSION IF NOT EXISTS pgjwt;',
|
|
177
|
-
'CREATE ROLE anon NOLOGIN;',
|
|
178
|
-
'CREATE ROLE authenticated NOLOGIN;',
|
|
179
|
-
'CREATE ROLE service_role NOLOGIN;',
|
|
180
|
-
'CREATE ROLE authenticator NOLOGIN;',
|
|
181
|
-
'CREATE SCHEMA IF NOT EXISTS auth;',
|
|
182
|
-
'CREATE SCHEMA IF NOT EXISTS storage;',
|
|
183
|
-
'CREATE SCHEMA IF NOT EXISTS realtime;',
|
|
184
|
-
'GRANT USAGE ON SCHEMA auth TO anon, authenticated, service_role;',
|
|
185
|
-
'GRANT USAGE ON SCHEMA storage TO anon, authenticated, service_role;',
|
|
186
|
-
'GRANT USAGE ON SCHEMA realtime TO anon, authenticated, service_role;',
|
|
187
|
-
'GRANT anon TO authenticator;',
|
|
188
|
-
'GRANT authenticated TO authenticator;',
|
|
189
|
-
'GRANT service_role TO authenticator;',
|
|
190
|
-
]) {
|
|
191
|
-
expect(joined).toContain(needle)
|
|
192
|
-
}
|
|
193
|
-
})
|
|
194
|
-
})
|
|
195
|
-
|
|
196
|
-
describe('service urls and env', () => {
|
|
197
|
-
it('points IMGPROXY_URL at the imgproxy app port', () => {
|
|
198
|
-
const result = renderSupabase({ host: 'backend.example.com', objectStorage })
|
|
199
|
-
const storage = result.resources.find(
|
|
200
|
-
(r: any) => r.kind === 'Deployment' && r.metadata.name === 'supabase-storage-api'
|
|
201
|
-
) as any
|
|
202
|
-
const env = storage.spec.template.spec.containers[0].env
|
|
203
|
-
const imgproxyUrl = env.find((e: any) => e.name === 'IMGPROXY_URL')
|
|
204
|
-
expect(imgproxyUrl.value).toBe('http://supabase-imgproxy:8080')
|
|
205
|
-
})
|
|
206
|
-
|
|
207
|
-
it('uses Postgres service-internal ports and the storage region', () => {
|
|
208
|
-
const result = renderSupabase({ host: 'backend.example.com', objectStorage })
|
|
209
|
-
for (const serviceName of ['supabase-gotrue', 'supabase-postgrest', 'supabase-storage-api']) {
|
|
210
|
-
const deploy = result.resources.find(
|
|
211
|
-
(r: any) => r.kind === 'Deployment' && r.metadata.name === serviceName
|
|
212
|
-
) as any
|
|
213
|
-
const env = deploy.spec.template.spec.containers[0].env as any[]
|
|
214
|
-
const dbUri = env.find(
|
|
215
|
-
(e: any) =>
|
|
216
|
-
e.name === 'GOTRUE_DB_DATABASE_URL' ||
|
|
217
|
-
e.name === 'PGRST_DB_URI' ||
|
|
218
|
-
e.name === 'DATABASE_URL'
|
|
219
|
-
)
|
|
220
|
-
expect(dbUri.value).toContain('@supabase-rw:5432/supabase')
|
|
221
|
-
}
|
|
222
|
-
const realtime = result.resources.find(
|
|
223
|
-
(r: any) => r.kind === 'Deployment' && r.metadata.name === 'supabase-realtime'
|
|
224
|
-
) as any
|
|
225
|
-
const realtimeEnv = realtime.spec.template.spec.containers[0].env as any[]
|
|
226
|
-
expect(realtimeEnv.find((e: any) => e.name === 'DB_HOST').value).toBe('supabase-rw')
|
|
227
|
-
expect(realtimeEnv.find((e: any) => e.name === 'DB_PORT').value).toBe('5432')
|
|
228
|
-
})
|
|
229
|
-
|
|
230
|
-
it('defaults GLOBAL_S3_REGION to us-east-1 without GOTRUE_URI_ALLOW_LIST', () => {
|
|
231
|
-
const result = renderSupabase({ host: 'backend.example.com', objectStorage })
|
|
232
|
-
const storage = result.resources.find(
|
|
233
|
-
(r: any) => r.kind === 'Deployment' && r.metadata.name === 'supabase-storage-api'
|
|
234
|
-
) as any
|
|
235
|
-
const storageEnv = storage.spec.template.spec.containers[0].env as any[]
|
|
236
|
-
expect(storageEnv.find((e: any) => e.name === 'GLOBAL_S3_REGION').value).toBe('us-east-1')
|
|
237
|
-
const gotrue = result.resources.find(
|
|
238
|
-
(r: any) => r.kind === 'Deployment' && r.metadata.name === 'supabase-gotrue'
|
|
239
|
-
) as any
|
|
240
|
-
const gotrueEnv = gotrue.spec.template.spec.containers[0].env as any[]
|
|
241
|
-
expect(gotrueEnv.find((e: any) => e.name === 'GOTRUE_URI_ALLOW_LIST')).toBeUndefined()
|
|
242
|
-
})
|
|
243
|
-
|
|
244
|
-
it('accepts region and uriAllowList overrides', () => {
|
|
245
|
-
const result = renderSupabase({
|
|
246
|
-
host: 'backend.example.com',
|
|
247
|
-
objectStorage,
|
|
248
|
-
region: 'eu-central-1',
|
|
249
|
-
uriAllowList: ['https://app.example.com', 'https://app2.example.com'],
|
|
250
|
-
})
|
|
251
|
-
const storage = result.resources.find(
|
|
252
|
-
(r: any) => r.kind === 'Deployment' && r.metadata.name === 'supabase-storage-api'
|
|
253
|
-
) as any
|
|
254
|
-
const storageEnv = storage.spec.template.spec.containers[0].env as any[]
|
|
255
|
-
expect(storageEnv.find((e: any) => e.name === 'GLOBAL_S3_REGION').value).toBe('eu-central-1')
|
|
256
|
-
const gotrue = result.resources.find(
|
|
257
|
-
(r: any) => r.kind === 'Deployment' && r.metadata.name === 'supabase-gotrue'
|
|
258
|
-
) as any
|
|
259
|
-
const gotrueEnv = gotrue.spec.template.spec.containers[0].env as any[]
|
|
260
|
-
expect(gotrueEnv.find((e: any) => e.name === 'GOTRUE_URI_ALLOW_LIST').value).toBe(
|
|
261
|
-
'https://app.example.com,https://app2.example.com'
|
|
262
|
-
)
|
|
263
|
-
})
|
|
264
|
-
})
|
|
265
|
-
|
|
266
|
-
describe('path-based routing', () => {
|
|
267
|
-
const routes = [
|
|
268
|
-
{ path: '/auth/v1', service: 'supabase-gotrue', port: 9999 },
|
|
269
|
-
{ path: '/rest/v1', service: 'supabase-postgrest', port: 3000 },
|
|
270
|
-
{ path: '/realtime/v1', service: 'supabase-realtime', port: 4000 },
|
|
271
|
-
{ path: '/storage/v1', service: 'supabase-storage-api', port: 5000 },
|
|
272
|
-
]
|
|
273
|
-
|
|
274
|
-
it('renders path-based Ingresses for auth, rest, realtime and storage on one host', () => {
|
|
275
|
-
const result = renderSupabase({ host: 'backend.example.com', objectStorage })
|
|
276
|
-
const ingresses = result.resources.filter((r) => r.kind === 'Ingress') as any[]
|
|
277
|
-
// Root route (PostgREST) + one per service
|
|
278
|
-
expect(ingresses).toHaveLength(5)
|
|
279
|
-
for (const route of routes) {
|
|
280
|
-
const match = ingresses.find((i) => i.spec.rules[0].http.paths[0].path === route.path) as any
|
|
281
|
-
expect(match, `no Ingress for ${route.path}`).toBeDefined()
|
|
282
|
-
expect(match.spec.rules[0].host).toBe('backend.example.com')
|
|
283
|
-
const backend = match.spec.rules[0].http.paths[0].backend.service
|
|
284
|
-
expect(backend.name).toBe(route.service)
|
|
285
|
-
expect(backend.port.number).toBe(route.port)
|
|
286
|
-
}
|
|
287
|
-
const root = ingresses.find((i) => i.spec.rules[0].http.paths[0].path === '/')
|
|
288
|
-
expect(root).toBeDefined()
|
|
289
|
-
expect(root.spec.rules[0].http.paths[0].backend.service.name).toBe('supabase-postgrest')
|
|
290
|
-
})
|
|
291
|
-
|
|
292
|
-
it('renders HTTPRoute PathPrefix matches in gateway mode', () => {
|
|
293
|
-
const result = render(
|
|
294
|
-
jsx(RoutingContext.Provider, {
|
|
295
|
-
value: { mode: 'gateway', gatewayClassName: 'eg' },
|
|
296
|
-
children: jsx(SecretContext.Provider, {
|
|
297
|
-
value: openbao as never,
|
|
298
|
-
children: jsx(Supabase, { host: 'backend.example.com', objectStorage }),
|
|
299
|
-
}),
|
|
300
|
-
})
|
|
301
|
-
)
|
|
302
|
-
const httpRoutes = result.resources.filter((r) => r.kind === 'HTTPRoute') as any[]
|
|
303
|
-
expect(httpRoutes).toHaveLength(5)
|
|
304
|
-
for (const route of routes) {
|
|
305
|
-
const match = httpRoutes.find(
|
|
306
|
-
(r) => r.spec.rules[0].matches?.[0]?.path?.value === route.path
|
|
307
|
-
) as any
|
|
308
|
-
expect(match, `no HTTPRoute for ${route.path}`).toBeDefined()
|
|
309
|
-
expect(match.spec.hostnames).toContain('backend.example.com')
|
|
310
|
-
expect(match.spec.rules[0].matches[0].path.type).toBe('PathPrefix')
|
|
311
|
-
expect(match.spec.rules[0].backendRefs[0]).toMatchObject({
|
|
312
|
-
name: route.service,
|
|
313
|
-
port: route.port,
|
|
314
|
-
})
|
|
315
|
-
}
|
|
316
|
-
})
|
|
317
|
-
|
|
318
|
-
it('adds websocket-friendly realtime annotations to the realtime route', () => {
|
|
319
|
-
const result = renderSupabase({ host: 'backend.example.com', objectStorage })
|
|
320
|
-
const realtime = result.resources.find(
|
|
321
|
-
(r: any) => r.kind === 'Ingress' && r.metadata.name === 'supabase-realtime-endpoint'
|
|
322
|
-
) as any
|
|
323
|
-
expect(realtime).toBeDefined()
|
|
324
|
-
expect(realtime.metadata.annotations['nginx.ingress.kubernetes.io/proxy-buffering']).toBe('off')
|
|
325
|
-
expect(realtime.metadata.annotations['nginx.ingress.kubernetes.io/proxy-read-timeout']).toBe(
|
|
326
|
-
'3600'
|
|
327
|
-
)
|
|
328
|
-
expect(realtime.metadata.annotations['nginx.ingress.kubernetes.io/proxy-send-timeout']).toBe(
|
|
329
|
-
'3600'
|
|
330
|
-
)
|
|
331
|
-
})
|
|
332
|
-
|
|
333
|
-
it('omits the storage route when storageApi is disabled', () => {
|
|
334
|
-
const result = renderSupabase({ host: 'backend.example.com', objectStorage, storageApi: false })
|
|
335
|
-
const ingresses = result.resources.filter((r) => r.kind === 'Ingress') as any[]
|
|
336
|
-
expect(ingresses).toHaveLength(4)
|
|
337
|
-
const paths = ingresses.map((i) => i.spec.rules[0].http.paths[0].path)
|
|
338
|
-
expect(paths).not.toContain('/storage/v1')
|
|
339
|
-
})
|
|
340
|
-
})
|
|
341
|
-
|
|
342
|
-
describe('rendering with all props', () => {
|
|
343
|
-
it('accepts the full prop surface', () => {
|
|
344
|
-
const result = renderSupabase({
|
|
345
|
-
name: 'platform',
|
|
346
|
-
namespace: 'platform',
|
|
347
|
-
host: 'platform.example.com',
|
|
348
|
-
replicas: 3,
|
|
349
|
-
storage: '50Gi',
|
|
350
|
-
storageApi: true,
|
|
351
|
-
objectStorage,
|
|
352
|
-
jwtSecretsName: 'platform-jwt',
|
|
353
|
-
resources: {
|
|
354
|
-
requests: { memory: '1Gi', cpu: '500m' },
|
|
355
|
-
limits: { memory: '4Gi', cpu: '2000m' },
|
|
356
|
-
},
|
|
357
|
-
tls: { secretName: 'platform-tls', clusterIssuer: 'letsencrypt-prod' },
|
|
358
|
-
})
|
|
359
|
-
expect(result.resources.length).toBeGreaterThan(0)
|
|
360
|
-
const deployments = result.resources.filter((r) => r.kind === 'Deployment')
|
|
361
|
-
expect(deployments.length).toBeGreaterThanOrEqual(5)
|
|
362
|
-
const names = deployments.map((d: any) => d.metadata.name)
|
|
363
|
-
for (const prefix of ['gotrue', 'postgrest', 'realtime', 'storage-api', 'imgproxy']) {
|
|
364
|
-
expect(names).toContain(`platform-${prefix}`)
|
|
365
|
-
}
|
|
366
|
-
const images = Object.fromEntries(
|
|
367
|
-
deployments.map((d: any) => [d.metadata.name, d.spec.template.spec.containers[0].image])
|
|
368
|
-
)
|
|
369
|
-
expect(images['platform-gotrue']).toBe('supabase/gotrue:v2')
|
|
370
|
-
expect(images['platform-postgrest']).toBe('postgrest/postgrest:v12')
|
|
371
|
-
expect(images['platform-realtime']).toBe('supabase/realtime:v2')
|
|
372
|
-
expect(images['platform-storage-api']).toBe('supabase/storage-api:v0')
|
|
373
|
-
expect(images['platform-imgproxy']).toBe('darthsim/imgproxy:v3')
|
|
374
|
-
const gotrue = deployments.find((d: any) => d.metadata.name === 'platform-gotrue') as any
|
|
375
|
-
expect(gotrue.spec.replicas).toBe(3)
|
|
376
|
-
expect(gotrue.spec.template.spec.containers[0].resources.limits.memory).toBe('4Gi')
|
|
377
|
-
const cluster = result.resources.find((r) => r.kind === 'Cluster') as any
|
|
378
|
-
expect(cluster.spec.storage.size).toBe('50Gi')
|
|
379
|
-
})
|
|
380
|
-
})
|
|
381
|
-
|
|
382
|
-
describe('secrets handling', () => {
|
|
383
|
-
it('provisions the JWT bundle through a secrets backend', () => {
|
|
384
|
-
const result = renderSupabase({ host: 'backend.example.com', objectStorage })
|
|
385
|
-
const kinds = result.resources.map((r) => r.kind)
|
|
386
|
-
expect(kinds).toContain('OpenBaoStaticSecret')
|
|
387
|
-
const bundle = result.resources.find((r) => r.kind === 'OpenBaoStaticSecret') as any
|
|
388
|
-
expect(bundle.spec.path).toBe('test/supabase/jwt')
|
|
389
|
-
expect(bundle.spec.destination.name).toBe('supabase-jwt')
|
|
390
|
-
})
|
|
391
|
-
|
|
392
|
-
it('provisions the JWT bundle through Vault', () => {
|
|
393
|
-
const result = render(
|
|
394
|
-
jsx(SecretContext.Provider, {
|
|
395
|
-
value: { backend: 'vault', mount: 'kv', path: 'apps' },
|
|
396
|
-
children: jsx(Supabase, { host: 'backend.example.com', objectStorage }),
|
|
397
|
-
})
|
|
398
|
-
)
|
|
399
|
-
const kinds = result.resources.map((r) => r.kind)
|
|
400
|
-
expect(kinds).toContain('VaultStaticSecret')
|
|
401
|
-
})
|
|
402
|
-
|
|
403
|
-
it('accepts an explicit jwtSecretsName without a backend', () => {
|
|
404
|
-
expect(() =>
|
|
405
|
-
render(
|
|
406
|
-
jsx(Supabase, {
|
|
407
|
-
host: 'backend.example.com',
|
|
408
|
-
objectStorage,
|
|
409
|
-
jwtSecretsName: 'existing-jwt',
|
|
410
|
-
})
|
|
411
|
-
)
|
|
412
|
-
).not.toThrow()
|
|
413
|
-
})
|
|
414
|
-
|
|
415
|
-
it('throws when no secrets backend and no JWT bundle secret', () => {
|
|
416
|
-
expect(() => render(jsx(Supabase, { host: 'backend.example.com', objectStorage }))).toThrow(
|
|
417
|
-
/JWT secret bundle/
|
|
418
|
-
)
|
|
419
|
-
})
|
|
420
|
-
|
|
421
|
-
it('wires credentials via secretKeyRef (never plaintext env)', () => {
|
|
422
|
-
const result = renderSupabase({ host: 'backend.example.com', objectStorage })
|
|
423
|
-
const getDeploy = (name: string) =>
|
|
424
|
-
result.resources.find((r: any) => r.kind === 'Deployment' && r.metadata.name === name) as any
|
|
425
|
-
const envOf = (name: string) => {
|
|
426
|
-
const deploy = getDeploy(name)
|
|
427
|
-
expect(deploy).toBeDefined()
|
|
428
|
-
return deploy.spec.template.spec.containers[0].env as any[]
|
|
429
|
-
}
|
|
430
|
-
const findEnv = (env: any[], name: string) => env.find((e) => e.name === name)
|
|
431
|
-
|
|
432
|
-
const gotrueEnv = envOf('supabase-gotrue')
|
|
433
|
-
const jwtSecret = findEnv(gotrueEnv, 'GOTRUE_JWT_SECRET')
|
|
434
|
-
expect(jwtSecret.valueFrom.secretKeyRef).toEqual({ name: 'supabase-jwt', key: 'jwtSecret' })
|
|
435
|
-
expect(jwtSecret.value).toBeUndefined()
|
|
436
|
-
const pgPassword = findEnv(gotrueEnv, 'PGPASSWORD')
|
|
437
|
-
expect(pgPassword.valueFrom.secretKeyRef).toEqual({
|
|
438
|
-
name: 'supabase-db-credentials',
|
|
439
|
-
key: 'password',
|
|
440
|
-
})
|
|
441
|
-
expect(pgPassword.value).toBeUndefined()
|
|
442
|
-
|
|
443
|
-
const postgrestEnv = envOf('supabase-postgrest')
|
|
444
|
-
expect(findEnv(postgrestEnv, 'PGRST_JWT_SECRET').value).toBeUndefined()
|
|
445
|
-
expect(findEnv(postgrestEnv, 'PGRST_JWT_SECRET').valueFrom.secretKeyRef.name).toBe(
|
|
446
|
-
'supabase-jwt'
|
|
447
|
-
)
|
|
448
|
-
|
|
449
|
-
const realtimeEnv = envOf('supabase-realtime')
|
|
450
|
-
expect(findEnv(realtimeEnv, 'DB_PASSWORD').value).toBeUndefined()
|
|
451
|
-
expect(findEnv(realtimeEnv, 'DB_PASSWORD').valueFrom.secretKeyRef.name).toBe(
|
|
452
|
-
'supabase-db-credentials'
|
|
453
|
-
)
|
|
454
|
-
|
|
455
|
-
const storageEnv = envOf('supabase-storage-api')
|
|
456
|
-
expect(findEnv(storageEnv, 'ANON_KEY').valueFrom.secretKeyRef).toEqual({
|
|
457
|
-
name: 'supabase-jwt',
|
|
458
|
-
key: 'anonKey',
|
|
459
|
-
})
|
|
460
|
-
expect(findEnv(storageEnv, 'SERVICE_KEY').valueFrom.secretKeyRef).toEqual({
|
|
461
|
-
name: 'supabase-jwt',
|
|
462
|
-
key: 'serviceRoleKey',
|
|
463
|
-
})
|
|
464
|
-
expect(findEnv(storageEnv, 'ANON_KEY').value).toBeUndefined()
|
|
465
|
-
for (const envName of ['GLOBAL_S3_ACCESS_KEY', 'GLOBAL_S3_SECRET_KEY']) {
|
|
466
|
-
const envVar = findEnv(storageEnv, envName)
|
|
467
|
-
expect(envVar.value).toBeUndefined()
|
|
468
|
-
expect(envVar.valueFrom.secretKeyRef.name).toBe('object-store-credentials')
|
|
469
|
-
}
|
|
470
|
-
})
|
|
471
|
-
|
|
472
|
-
it('renders no plaintext credentials anywhere', () => {
|
|
473
|
-
const result = renderSupabase({ host: 'backend.example.com', objectStorage })
|
|
474
|
-
const { passed, errors } = runGuardrails(result.resources as any[], [noPlaintextSecrets])
|
|
475
|
-
if (!passed) {
|
|
476
|
-
console.error('Plaintext credential violations:', errors)
|
|
477
|
-
}
|
|
478
|
-
expect(passed).toBe(true)
|
|
479
|
-
})
|
|
480
|
-
})
|
package/examples/basic.tsx
DELETED
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
import { Platform } from '@r8s/recipes'
|
|
2
|
-
import { Supabase } from '@r8s/supabase'
|
|
3
|
-
|
|
4
|
-
export default (
|
|
5
|
-
<Platform secrets={{ backend: 'openbao', mount: 'kv', path: 'apps' }}>
|
|
6
|
-
<Supabase
|
|
7
|
-
name="backend"
|
|
8
|
-
host="backend.example.com"
|
|
9
|
-
objectStorage={{
|
|
10
|
-
endpoint: 'https://s3.internal.example.com',
|
|
11
|
-
bucket: 'backend-uploads',
|
|
12
|
-
credentialsSecret: 'backend-object-store-credentials',
|
|
13
|
-
}}
|
|
14
|
-
/>
|
|
15
|
-
</Platform>
|
|
16
|
-
)
|