@r8s/nextcloud 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 +132 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +328 -0
- package/dist/index.js.map +1 -0
- package/package.json +14 -7
- package/__tests__/nextcloud.test.ts +0 -409
- package/examples/basic.tsx +0 -16
- package/src/index.tsx +0 -447
- package/tsconfig.json +0 -15
|
@@ -1,409 +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
|
-
// Nextcloud recipe tests:
|
|
9
|
-
// 1. Operator declarations (deduped via OperatorContext)
|
|
10
|
-
// 2. Rendering: defaults, all props, gateway/ingress adaptation, cron
|
|
11
|
-
// 3. Persistence: /var/www/html claim shared by app + cron
|
|
12
|
-
// 4. Security: no plaintext credentials in rendered output
|
|
13
|
-
import { Nextcloud } from '../src/index'
|
|
14
|
-
|
|
15
|
-
const openbao = { backend: 'openbao', mount: 'kv', path: 'test' }
|
|
16
|
-
|
|
17
|
-
/** Render Nextcloud inside a Platform-like secrets backend (OpenBao). */
|
|
18
|
-
function renderNextcloud(props: Record<string, unknown>): ReturnType<typeof render> {
|
|
19
|
-
return render(
|
|
20
|
-
jsx(SecretContext.Provider, {
|
|
21
|
-
value: openbao as never,
|
|
22
|
-
children: jsx(Nextcloud, props as never),
|
|
23
|
-
})
|
|
24
|
-
)
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
/** Wrap Nextcloud in an OperatorContext (no secrets backend). */
|
|
28
|
-
function elementWithContext(ops: any[], props: Record<string, unknown>): r8sElement {
|
|
29
|
-
return jsx(OperatorContext.Provider, {
|
|
30
|
-
value: ops,
|
|
31
|
-
children: jsx(Nextcloud, props as never),
|
|
32
|
-
})
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
const objectStorage = {
|
|
36
|
-
endpoint: 's3.internal.example.com',
|
|
37
|
-
bucket: 'cloud-files',
|
|
38
|
-
credentialsSecret: 'cloud-files-credentials',
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
function findAppDeployment(result: ReturnType<typeof render>, name = 'nextcloud'): any {
|
|
42
|
-
return result.resources.find(
|
|
43
|
-
(r: any) => r.kind === 'Deployment' && r.metadata.name === name
|
|
44
|
-
) as any
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
function findCron(result: ReturnType<typeof render>, name = 'nextcloud-cron'): any {
|
|
48
|
-
return result.resources.find((r: any) => r.kind === 'CronJob' && r.metadata.name === name) as any
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
describe('operator declarations', () => {
|
|
52
|
-
it('declares the redis operator when cache is enabled', () => {
|
|
53
|
-
const result = renderNextcloud({ host: 'cloud.example.com' })
|
|
54
|
-
expect(result.operators.some((op) => op.name === 'redis-operator')).toBe(true)
|
|
55
|
-
})
|
|
56
|
-
|
|
57
|
-
it('declares the cnpg operator via the Database recipe', () => {
|
|
58
|
-
const result = renderNextcloud({ host: 'cloud.example.com' })
|
|
59
|
-
expect(result.operators.some((op) => op.name === 'cnpg')).toBe(true)
|
|
60
|
-
})
|
|
61
|
-
|
|
62
|
-
it('skips the redis operator when cache is disabled', () => {
|
|
63
|
-
const result = renderNextcloud({ host: 'cloud.example.com', cache: false })
|
|
64
|
-
expect(result.operators.some((op) => op.name === 'redis-operator')).toBe(false)
|
|
65
|
-
})
|
|
66
|
-
|
|
67
|
-
it('deduplicates operators provided via context', () => {
|
|
68
|
-
const result = render(
|
|
69
|
-
elementWithContext([operators['redis-operator'](), operators['cnpg']()], {
|
|
70
|
-
host: 'cloud.example.com',
|
|
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
|
-
|
|
80
|
-
describe('rendering defaults', () => {
|
|
81
|
-
it('renders app deployment, service, ingress, database cluster, cron job, redis and html claim', () => {
|
|
82
|
-
const result = renderNextcloud({ host: 'cloud.example.com' })
|
|
83
|
-
const kinds = result.resources.map((r) => r.kind)
|
|
84
|
-
expect(kinds).toContain('Deployment')
|
|
85
|
-
expect(kinds).toContain('Service')
|
|
86
|
-
expect(kinds).toContain('Ingress')
|
|
87
|
-
expect(kinds).toContain('Cluster')
|
|
88
|
-
expect(kinds).toContain('CronJob')
|
|
89
|
-
expect(kinds).toContain('RedisReplication')
|
|
90
|
-
expect(kinds).toContain('PersistentVolumeClaim')
|
|
91
|
-
})
|
|
92
|
-
|
|
93
|
-
it('renders the app on nextcloud:31-apache, port 80, as a raw deployment', () => {
|
|
94
|
-
const result = renderNextcloud({ host: 'cloud.example.com' })
|
|
95
|
-
const app = findAppDeployment(result)
|
|
96
|
-
expect(app).toBeDefined()
|
|
97
|
-
const container = app.spec.template.spec.containers[0]
|
|
98
|
-
expect(container.image).toBe('nextcloud:31-apache')
|
|
99
|
-
expect(container.ports[0].containerPort).toBe(80)
|
|
100
|
-
expect(container.name).toBe('app')
|
|
101
|
-
})
|
|
102
|
-
|
|
103
|
-
it('probes /status.php on port 80 for liveness and readiness', () => {
|
|
104
|
-
const result = renderNextcloud({ host: 'cloud.example.com' })
|
|
105
|
-
const container = findAppDeployment(result).spec.template.spec.containers[0]
|
|
106
|
-
expect(container.livenessProbe.httpGet).toEqual({ path: '/status.php', port: 80 })
|
|
107
|
-
expect(container.livenessProbe.initialDelaySeconds).toBe(30)
|
|
108
|
-
expect(container.readinessProbe.httpGet).toEqual({ path: '/status.php', port: 80 })
|
|
109
|
-
expect(container.readinessProbe.initialDelaySeconds).toBe(20)
|
|
110
|
-
expect(container.readinessProbe.failureThreshold).toBe(5)
|
|
111
|
-
})
|
|
112
|
-
|
|
113
|
-
it('runs cron.php every 5 minutes with the same image, no busybox', () => {
|
|
114
|
-
const result = renderNextcloud({ host: 'cloud.example.com' })
|
|
115
|
-
const cron = findCron(result)
|
|
116
|
-
expect(cron).toBeDefined()
|
|
117
|
-
expect(cron.apiVersion).toBe('batch/v1')
|
|
118
|
-
expect(cron.spec.schedule).toBe('*/5 * * * *')
|
|
119
|
-
const container = cron.spec.jobTemplate.spec.template.spec.containers[0]
|
|
120
|
-
expect(container.image).toBe('nextcloud:31-apache')
|
|
121
|
-
expect(container.command).toEqual(['php', '-f', '/var/www/html/cron.php'])
|
|
122
|
-
expect(container.env.find((e: any) => e.name === 'POSTGRES_HOST').value).toBe('nextcloud-rw')
|
|
123
|
-
expect(container.env.find((e: any) => e.name === 'POSTGRES_USER').value).toBe('nextcloud')
|
|
124
|
-
})
|
|
125
|
-
|
|
126
|
-
it('renders a ReadWriteMany html claim with the default 10Gi size', () => {
|
|
127
|
-
const result = renderNextcloud({ host: 'cloud.example.com' })
|
|
128
|
-
const pvc = result.resources.find(
|
|
129
|
-
(r: any) => r.kind === 'PersistentVolumeClaim' && r.metadata.name === 'nextcloud-html'
|
|
130
|
-
) as any
|
|
131
|
-
expect(pvc).toBeDefined()
|
|
132
|
-
expect(pvc.spec.accessModes).toEqual(['ReadWriteMany'])
|
|
133
|
-
expect(pvc.spec.resources.requests.storage).toBe('10Gi')
|
|
134
|
-
expect(pvc.spec.storageClassName).toBeUndefined()
|
|
135
|
-
})
|
|
136
|
-
|
|
137
|
-
it('honours storage and storageClassName props on the html claim', () => {
|
|
138
|
-
const result = renderNextcloud({
|
|
139
|
-
host: 'cloud.example.com',
|
|
140
|
-
storage: '25Gi',
|
|
141
|
-
storageClassName: 'nfs-client',
|
|
142
|
-
})
|
|
143
|
-
const pvc = result.resources.find(
|
|
144
|
-
(r: any) => r.kind === 'PersistentVolumeClaim' && r.metadata.name === 'nextcloud-html'
|
|
145
|
-
) as any
|
|
146
|
-
expect(pvc.spec.resources.requests.storage).toBe('25Gi')
|
|
147
|
-
expect(pvc.spec.storageClassName).toBe('nfs-client')
|
|
148
|
-
})
|
|
149
|
-
|
|
150
|
-
it('mounts the html claim in both the app deployment and the cron job', () => {
|
|
151
|
-
const result = renderNextcloud({ host: 'cloud.example.com' })
|
|
152
|
-
const appContainer = findAppDeployment(result).spec.template.spec.containers[0]
|
|
153
|
-
expect(appContainer.volumeMounts).toEqual([{ name: 'html', mountPath: '/var/www/html' }])
|
|
154
|
-
expect(findAppDeployment(result).spec.template.spec.volumes).toEqual([
|
|
155
|
-
{ name: 'html', persistentVolumeClaim: { claimName: 'nextcloud-html' } },
|
|
156
|
-
])
|
|
157
|
-
const cronPodSpec = findCron(result).spec.jobTemplate.spec.template.spec
|
|
158
|
-
expect(cronPodSpec.containers[0].volumeMounts).toEqual([
|
|
159
|
-
{ name: 'html', mountPath: '/var/www/html' },
|
|
160
|
-
])
|
|
161
|
-
expect(cronPodSpec.volumes).toEqual([
|
|
162
|
-
{ name: 'html', persistentVolumeClaim: { claimName: 'nextcloud-html' } },
|
|
163
|
-
])
|
|
164
|
-
})
|
|
165
|
-
|
|
166
|
-
it('forbids cron concurrency and never restarts jobs past the backoff limit', () => {
|
|
167
|
-
const result = renderNextcloud({ host: 'cloud.example.com' })
|
|
168
|
-
const cron = findCron(result)
|
|
169
|
-
expect(cron.spec.concurrencyPolicy).toBe('Forbid')
|
|
170
|
-
const jobSpec = cron.spec.jobTemplate.spec
|
|
171
|
-
expect(jobSpec.backoffLimit).toBe(2)
|
|
172
|
-
expect(jobSpec.template.spec.restartPolicy).toBe('OnFailure')
|
|
173
|
-
})
|
|
174
|
-
|
|
175
|
-
it('renders a ClusterIP service that fronts the app on port 80', () => {
|
|
176
|
-
const result = renderNextcloud({ host: 'cloud.example.com' })
|
|
177
|
-
const service = result.resources.find(
|
|
178
|
-
(r: any) => r.kind === 'Service' && r.metadata.name === 'nextcloud'
|
|
179
|
-
) as any
|
|
180
|
-
expect(service).toBeDefined()
|
|
181
|
-
expect(service.spec.type).toBe('ClusterIP')
|
|
182
|
-
expect(service.spec.ports[0].port).toBe(80)
|
|
183
|
-
})
|
|
184
|
-
|
|
185
|
-
it('skips redis resources when cache is disabled', () => {
|
|
186
|
-
const result = renderNextcloud({ host: 'cloud.example.com', cache: false })
|
|
187
|
-
const kinds = result.resources.map((r) => r.kind)
|
|
188
|
-
expect(kinds).not.toContain('RedisReplication')
|
|
189
|
-
})
|
|
190
|
-
|
|
191
|
-
it('renders gateway resources when platform uses gateway routing', () => {
|
|
192
|
-
const result = render(
|
|
193
|
-
jsx(RoutingContext.Provider, {
|
|
194
|
-
value: { mode: 'gateway', gatewayClassName: 'eg' },
|
|
195
|
-
children: jsx(SecretContext.Provider, {
|
|
196
|
-
value: openbao as never,
|
|
197
|
-
children: jsx(Nextcloud, { host: 'cloud.example.com' }),
|
|
198
|
-
}),
|
|
199
|
-
})
|
|
200
|
-
)
|
|
201
|
-
const kinds = result.resources.map((r) => r.kind)
|
|
202
|
-
expect(kinds).toContain('HTTPRoute')
|
|
203
|
-
})
|
|
204
|
-
|
|
205
|
-
it('renders a valid Ingress when platform uses ingress routing', () => {
|
|
206
|
-
const result = render(
|
|
207
|
-
jsx(RoutingContext.Provider, {
|
|
208
|
-
value: { mode: 'ingress' },
|
|
209
|
-
children: jsx(SecretContext.Provider, {
|
|
210
|
-
value: openbao as never,
|
|
211
|
-
children: jsx(Nextcloud, { host: 'cloud.example.com' }),
|
|
212
|
-
}),
|
|
213
|
-
})
|
|
214
|
-
)
|
|
215
|
-
const ingress = result.resources.find((r) => r.kind === 'Ingress') as any
|
|
216
|
-
expect(ingress).toBeDefined()
|
|
217
|
-
expect(ingress.spec.rules[0].host).toBe('cloud.example.com')
|
|
218
|
-
})
|
|
219
|
-
|
|
220
|
-
it('passes resource validation', () => {
|
|
221
|
-
const result = renderNextcloud({
|
|
222
|
-
host: 'cloud.example.com',
|
|
223
|
-
objectStorage,
|
|
224
|
-
})
|
|
225
|
-
for (const resource of result.resources) {
|
|
226
|
-
expect(validateResource(resource)).toEqual([])
|
|
227
|
-
}
|
|
228
|
-
})
|
|
229
|
-
})
|
|
230
|
-
|
|
231
|
-
describe('rendering with all props', () => {
|
|
232
|
-
it('accepts the full prop surface', () => {
|
|
233
|
-
const result = renderNextcloud({
|
|
234
|
-
name: 'cloud',
|
|
235
|
-
namespace: 'collab',
|
|
236
|
-
version: '31.0.5-apache',
|
|
237
|
-
host: 'cloud.example.com',
|
|
238
|
-
replicas: 3,
|
|
239
|
-
storage: '50Gi',
|
|
240
|
-
storageClassName: 'nfs-client',
|
|
241
|
-
objectStorage: {
|
|
242
|
-
...objectStorage,
|
|
243
|
-
bucket: 'shared',
|
|
244
|
-
region: 'eu-north-1',
|
|
245
|
-
port: 9000,
|
|
246
|
-
ssl: false,
|
|
247
|
-
},
|
|
248
|
-
resources: {
|
|
249
|
-
requests: { memory: '1Gi', cpu: '500m' },
|
|
250
|
-
limits: { memory: '4Gi', cpu: '2000m' },
|
|
251
|
-
},
|
|
252
|
-
tls: { secretName: 'cloud-tls', clusterIssuer: 'letsencrypt-prod' },
|
|
253
|
-
})
|
|
254
|
-
|
|
255
|
-
const app = findAppDeployment(result, 'cloud')
|
|
256
|
-
expect(app).toBeDefined()
|
|
257
|
-
expect(app.spec.replicas).toBe(3)
|
|
258
|
-
expect(app.spec.template.spec.containers[0].image).toContain('31.0.5-apache')
|
|
259
|
-
expect(app.spec.template.spec.containers[0].resources.limits.memory).toBe('4Gi')
|
|
260
|
-
|
|
261
|
-
const env = app.spec.template.spec.containers[0].env
|
|
262
|
-
expect(env.find((e: any) => e.name === 'OBJECTSTORE_S3_BUCKET').value).toBe('shared')
|
|
263
|
-
expect(env.find((e: any) => e.name === 'OBJECTSTORE_S3_REGION').value).toBe('eu-north-1')
|
|
264
|
-
expect(env.find((e: any) => e.name === 'OBJECTSTORE_S3_PORT').value).toBe('9000')
|
|
265
|
-
expect(env.find((e: any) => e.name === 'OBJECTSTORE_S3_SSL').value).toBe('false')
|
|
266
|
-
expect(env.find((e: any) => e.name === 'OBJECTSTORE_S3_HOST').value).toBe(
|
|
267
|
-
's3.internal.example.com'
|
|
268
|
-
)
|
|
269
|
-
expect(env.find((e: any) => e.name === 'NEXTCLOUD_TRUSTED_DOMAINS').value).toBe(
|
|
270
|
-
'cloud.example.com'
|
|
271
|
-
)
|
|
272
|
-
|
|
273
|
-
const pvc = result.resources.find(
|
|
274
|
-
(r: any) => r.kind === 'PersistentVolumeClaim' && r.metadata.name === 'cloud-html'
|
|
275
|
-
) as any
|
|
276
|
-
expect(pvc.spec.resources.requests.storage).toBe('50Gi')
|
|
277
|
-
expect(pvc.spec.storageClassName).toBe('nfs-client')
|
|
278
|
-
})
|
|
279
|
-
|
|
280
|
-
it('defaults OBJECTSTORE_S3_SSL to true and omits the port when unset', () => {
|
|
281
|
-
const result = renderNextcloud({ host: 'cloud.example.com', objectStorage })
|
|
282
|
-
const env = findAppDeployment(result).spec.template.spec.containers[0].env
|
|
283
|
-
expect(env.find((e: any) => e.name === 'OBJECTSTORE_S3_SSL').value).toBe('true')
|
|
284
|
-
expect(env.find((e: any) => e.name === 'OBJECTSTORE_S3_PORT')).toBeUndefined()
|
|
285
|
-
})
|
|
286
|
-
|
|
287
|
-
it('wires the app and cron to redis and database by naming convention', () => {
|
|
288
|
-
const result = renderNextcloud({ host: 'cloud.example.com' })
|
|
289
|
-
const env = findAppDeployment(result).spec.template.spec.containers[0].env
|
|
290
|
-
expect(env.find((e: any) => e.name === 'REDIS_HOST').value).toBe('nextcloud-redis')
|
|
291
|
-
expect(env.find((e: any) => e.name === 'REDIS_HOST_PORT').value).toBe('6379')
|
|
292
|
-
expect(env.find((e: any) => e.name === 'POSTGRES_HOST').value).toBe('nextcloud-rw')
|
|
293
|
-
const cronEnv = findCron(result).spec.jobTemplate.spec.template.spec.containers[0].env
|
|
294
|
-
expect(cronEnv.find((e: any) => e.name === 'REDIS_HOST').value).toBe('nextcloud-redis')
|
|
295
|
-
})
|
|
296
|
-
|
|
297
|
-
it('emits the redis replication component with 3 nodes', () => {
|
|
298
|
-
const result = renderNextcloud({ host: 'cloud.example.com' })
|
|
299
|
-
const redis = result.resources.find(
|
|
300
|
-
(r: any) => r.kind === 'RedisReplication' && r.metadata.name === 'nextcloud-redis'
|
|
301
|
-
) as any
|
|
302
|
-
expect(redis).toBeDefined()
|
|
303
|
-
expect(redis.spec.clusterSize).toBe(3)
|
|
304
|
-
expect(redis.spec.kubernetesConfig.image).toBe('redis:7.2-alpine')
|
|
305
|
-
})
|
|
306
|
-
|
|
307
|
-
it('keeps only POSTGRES_* database wiring (no DATABASE_URL)', () => {
|
|
308
|
-
const result = renderNextcloud({ host: 'cloud.example.com' })
|
|
309
|
-
const env = findAppDeployment(result).spec.template.spec.containers[0].env
|
|
310
|
-
expect(env.find((e: any) => e.name === 'DATABASE_URL')).toBeUndefined()
|
|
311
|
-
expect(env.find((e: any) => e.name === 'POSTGRES_HOST').value).toBe('nextcloud-rw')
|
|
312
|
-
expect(env.find((e: any) => e.name === 'POSTGRES_PORT').value).toBe('5432')
|
|
313
|
-
expect(env.find((e: any) => e.name === 'POSTGRES_DB').value).toBe('nextcloud')
|
|
314
|
-
expect(env.find((e: any) => e.name === 'POSTGRES_USER').value).toBe('nextcloud')
|
|
315
|
-
expect(env.find((e: any) => e.name === 'POSTGRES_PASSWORD').value).toBe('$(PGPASSWORD)')
|
|
316
|
-
})
|
|
317
|
-
})
|
|
318
|
-
|
|
319
|
-
describe('secrets handling', () => {
|
|
320
|
-
it('provisions app secrets through a secrets backend', () => {
|
|
321
|
-
const result = renderNextcloud({ host: 'cloud.example.com' })
|
|
322
|
-
const kinds = result.resources.map((r) => r.kind)
|
|
323
|
-
expect(kinds).toContain('OpenBaoStaticSecret')
|
|
324
|
-
const appSecret = result.resources.find(
|
|
325
|
-
(r: any) =>
|
|
326
|
-
r.kind === 'OpenBaoStaticSecret' && r.spec.destination.name === 'nextcloud-app-secrets'
|
|
327
|
-
) as any
|
|
328
|
-
expect(appSecret).toBeDefined()
|
|
329
|
-
expect(appSecret.spec.path).toBe('test/nextcloud/secrets')
|
|
330
|
-
})
|
|
331
|
-
|
|
332
|
-
it('provisions app secrets through Vault', () => {
|
|
333
|
-
const result = render(
|
|
334
|
-
jsx(SecretContext.Provider, {
|
|
335
|
-
value: { backend: 'vault', mount: 'kv', path: 'apps' },
|
|
336
|
-
children: jsx(Nextcloud, { host: 'cloud.example.com' }),
|
|
337
|
-
})
|
|
338
|
-
)
|
|
339
|
-
const kinds = result.resources.map((r) => r.kind)
|
|
340
|
-
expect(kinds).toContain('VaultStaticSecret')
|
|
341
|
-
})
|
|
342
|
-
|
|
343
|
-
it('throws when no secrets backend and no secretsName', () => {
|
|
344
|
-
expect(() => render(jsx(Nextcloud, { host: 'cloud.example.com' }))).toThrow(
|
|
345
|
-
/application secrets/
|
|
346
|
-
)
|
|
347
|
-
})
|
|
348
|
-
|
|
349
|
-
it('accepts an existing secretsName without a backend', () => {
|
|
350
|
-
expect(() =>
|
|
351
|
-
render(jsx(Nextcloud, { host: 'cloud.example.com', secretsName: 'existing-secrets' }))
|
|
352
|
-
).not.toThrow()
|
|
353
|
-
})
|
|
354
|
-
|
|
355
|
-
it('wires credentials via secretKeyRef (never plaintext env)', () => {
|
|
356
|
-
const result = renderNextcloud({ host: 'cloud.example.com', secretsName: 'existing-secrets' })
|
|
357
|
-
const env = findAppDeployment(result).spec.template.spec.containers[0].env
|
|
358
|
-
const adminPassword = env.find((e: any) => e.name === 'NEXTCLOUD_ADMIN_PASSWORD')
|
|
359
|
-
const pgPassword = env.find((e: any) => e.name === 'PGPASSWORD')
|
|
360
|
-
expect(adminPassword.valueFrom.secretKeyRef.name).toBe('existing-secrets')
|
|
361
|
-
expect(adminPassword.valueFrom.secretKeyRef.key).toBe('adminPassword')
|
|
362
|
-
expect(pgPassword.valueFrom.secretKeyRef.name).toBe('nextcloud-db-credentials')
|
|
363
|
-
expect(adminPassword.value).toBeUndefined()
|
|
364
|
-
expect(pgPassword.value).toBeUndefined()
|
|
365
|
-
})
|
|
366
|
-
|
|
367
|
-
it('wires object storage credentials to the referenced bucket secret', () => {
|
|
368
|
-
const result = renderNextcloud({ host: 'cloud.example.com', objectStorage })
|
|
369
|
-
const env = findAppDeployment(result).spec.template.spec.containers[0].env
|
|
370
|
-
const accessKey = env.find((e: any) => e.name === 'AWS_ACCESS_KEY_ID')
|
|
371
|
-
const secretAccessKey = env.find((e: any) => e.name === 'AWS_SECRET_ACCESS_KEY')
|
|
372
|
-
expect(accessKey.valueFrom.secretKeyRef).toEqual({
|
|
373
|
-
name: 'cloud-files-credentials',
|
|
374
|
-
key: 'accessKey',
|
|
375
|
-
})
|
|
376
|
-
expect(secretAccessKey.valueFrom.secretKeyRef).toEqual({
|
|
377
|
-
name: 'cloud-files-credentials',
|
|
378
|
-
key: 'secretKey',
|
|
379
|
-
})
|
|
380
|
-
expect(accessKey.value).toBeUndefined()
|
|
381
|
-
expect(secretAccessKey.value).toBeUndefined()
|
|
382
|
-
// Nextcloud-native envs are $(...) references to the secret-backed vars
|
|
383
|
-
expect(env.find((e: any) => e.name === 'OBJECTSTORE_S3_KEY').value).toBe('$(AWS_ACCESS_KEY_ID)')
|
|
384
|
-
expect(env.find((e: any) => e.name === 'OBJECTSTORE_S3_SECRET').value).toBe(
|
|
385
|
-
'$(AWS_SECRET_ACCESS_KEY)'
|
|
386
|
-
)
|
|
387
|
-
})
|
|
388
|
-
|
|
389
|
-
it('cron job reads the database password via secretKeyRef', () => {
|
|
390
|
-
const result = renderNextcloud({ host: 'cloud.example.com' })
|
|
391
|
-
const env = findCron(result).spec.jobTemplate.spec.template.spec.containers[0].env
|
|
392
|
-
const pgPassword = env.find((e: any) => e.name === 'PGPASSWORD')
|
|
393
|
-
expect(pgPassword.valueFrom.secretKeyRef.name).toBe('nextcloud-db-credentials')
|
|
394
|
-
expect(pgPassword.valueFrom.secretKeyRef.key).toBe('password')
|
|
395
|
-
expect(pgPassword.value).toBeUndefined()
|
|
396
|
-
})
|
|
397
|
-
|
|
398
|
-
it('renders no plaintext credentials anywhere', () => {
|
|
399
|
-
const result = renderNextcloud({
|
|
400
|
-
host: 'cloud.example.com',
|
|
401
|
-
objectStorage,
|
|
402
|
-
})
|
|
403
|
-
const { passed, errors } = runGuardrails(result.resources as any[], [noPlaintextSecrets])
|
|
404
|
-
if (!passed) {
|
|
405
|
-
console.error('Plaintext credential violations:', errors)
|
|
406
|
-
}
|
|
407
|
-
expect(passed).toBe(true)
|
|
408
|
-
})
|
|
409
|
-
})
|
package/examples/basic.tsx
DELETED
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
import { Platform } from '@r8s/recipes'
|
|
2
|
-
import { Nextcloud } from '@r8s/nextcloud'
|
|
3
|
-
|
|
4
|
-
export default (
|
|
5
|
-
<Platform secrets={{ backend: 'openbao', mount: 'kv', path: 'apps' }}>
|
|
6
|
-
<Nextcloud
|
|
7
|
-
name="cloud"
|
|
8
|
-
host="cloud.example.com"
|
|
9
|
-
objectStorage={{
|
|
10
|
-
endpoint: 's3.internal.example.com',
|
|
11
|
-
bucket: 'cloud-files',
|
|
12
|
-
credentialsSecret: 'cloud-files-credentials',
|
|
13
|
-
}}
|
|
14
|
-
/>
|
|
15
|
-
</Platform>
|
|
16
|
-
)
|