@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/src/index.tsx DELETED
@@ -1,447 +0,0 @@
1
- import { jsx, Fragment, useContext, declareOperator } from '@r8s/core'
2
- import type { Deployment, EnvVar, PersistentVolumeClaim, Service } from '@r8s/k8s-types'
3
- import { OperatorContext, SecretContext } from '@r8s/core/defaults'
4
- import { operators } from '@r8s/crds'
5
- import { Database, Endpoint } from '@r8s/recipes'
6
- import { RedisReplicationComponent } from '@r8s/crds/redis'
7
- import type { SecretRef } from '@r8s/recipes'
8
-
9
- export interface NextcloudProps {
10
- /** Resource name (defaults to 'nextcloud') */
11
- name?: string
12
- /** Kubernetes namespace (defaults to 'default') */
13
- namespace?: string
14
- /** Container image tag (defaults to '31-apache' — pin a version in production) */
15
- version?: string
16
- /** Public hostname for the web UI and WebDAV (required) */
17
- host: string
18
- /**
19
- * Number of replicas. Safe to scale beyond 1 when `objectStorage` is
20
- * configured (file blobs live in S3) and `cache` is enabled — Nextcloud
21
- * becomes effectively stateless. Requires a StorageClass with
22
- * ReadWriteMany support for the /var/www/html claim.
23
- */
24
- replicas?: number
25
- /** Provision a Redis replication set for file locking and caching (default: true) */
26
- cache?: boolean
27
- /**
28
- * Size of the PersistentVolumeClaim backing /var/www/html (defaults to
29
- * '10Gi'). Apps, config and the data directory all live in this tree.
30
- */
31
- storage?: string
32
- /**
33
- * StorageClass for the /var/www/html PersistentVolumeClaim. Must provide
34
- * ReadWriteMany when `replicas` > 1 (e.g. NFS or EFS). Defaults to the
35
- * cluster default StorageClass when omitted.
36
- */
37
- storageClassName?: string
38
- /**
39
- * S3-compatible object storage used as primary storage for files
40
- * (RustFS in the platform). Reference a bucket whose credentials live
41
- * in a Secret provisioned by the secrets backend (keys: accessKey,
42
- * secretKey) — never plaintext.
43
- */
44
- objectStorage?: {
45
- /** S3 host WITHOUT protocol/scheme, e.g. s3.internal.example.com */
46
- endpoint: string
47
- /** Bucket used for user files */
48
- bucket: string
49
- /** Name of the Secret holding accessKey / secretKey */
50
- credentialsSecret: string
51
- /** Region string for the S3 client (defaults to 'us-east-1') */
52
- region?: string
53
- /** TCP port of the S3 endpoint (defaults to the provider default, typically 443) */
54
- port?: number
55
- /** Use TLS against the S3 endpoint (default: true) */
56
- ssl?: boolean
57
- }
58
- /**
59
- * Name of an existing Secret holding the Nextcloud app secrets
60
- * (key: adminPassword). Required unless a secrets backend
61
- * (openbao/vault) is configured on the surrounding Platform — the
62
- * backend then provisions them automatically. Plaintext admin
63
- * passwords are not supported.
64
- */
65
- secretsName?: string
66
- /** Requested resources */
67
- resources?: {
68
- requests?: { cpu?: string; memory?: string }
69
- limits?: { cpu?: string; memory?: string }
70
- }
71
- /** TLS configuration (defaults to letsencrypt-prod cluster issuer) */
72
- tls?: {
73
- secretName: string
74
- clusterIssuer: string
75
- }
76
- }
77
-
78
- const OPERATOR_REDIS = 'redis-operator'
79
- const HTML_MOUNT = '/var/www/html'
80
- const STATUS_PATH = '/status.php'
81
-
82
- /**
83
- * Nextcloud — self-hosted file cloud with Postgres, Redis, S3 primary
84
- * storage and background cron jobs.
85
- *
86
- * @title Nextcloud
87
- * @category File & Collaboration
88
- *
89
- * Composes:
90
- * - CNPG Postgres cluster (file index, shares, user data)
91
- * - Redis replication set for file locking and caching (default on)
92
- * - PersistentVolumeClaim backing /var/www/html (ReadWriteMany)
93
- * - Nextcloud Deployment + Service + Endpoint (nextcloud:31-apache,
94
- * probed via /status.php)
95
- * - CronJob running `php -f /var/www/html/cron.php` every 5 minutes on
96
- * the same claim (concurrency forbidden)
97
- * - S3/RustFS bucket reference for primary file storage
98
- * - Admin bootstrap password provisioned by the Platform secrets
99
- * backend (openbao / vault), or referenced from an existing Secret
100
- *
101
- * The main app is a raw Deployment (not WebService) because Nextcloud
102
- * needs the /var/www/html claim volume-mounted; the CronJob shares the
103
- * same claim so background jobs operate on the live data tree.
104
- *
105
- * @example
106
- * import { Platform } from '@r8s/recipes'
107
- * import { Nextcloud } from '@r8s/nextcloud'
108
- *
109
- * export default (
110
- * <Platform secrets={{ backend: 'openbao', mount: 'kv', path: 'apps' }}>
111
- * <Nextcloud
112
- * name="cloud"
113
- * host="cloud.example.com"
114
- * objectStorage={{
115
- * endpoint: 's3.internal.example.com',
116
- * bucket: 'cloud-files',
117
- * credentialsSecret: 'cloud-files-credentials',
118
- * }}
119
- * />
120
- * </Platform>
121
- * )
122
- * @example
123
- * import { Nextcloud } from '@r8s/nextcloud'
124
- *
125
- * export default (
126
- * <Nextcloud
127
- * name="cloud"
128
- * host="${env:CLOUD_HOST}"
129
- * secretsName="cloud-app-secrets"
130
- * objectStorage={{
131
- * endpoint: '${env:S3_ENDPOINT}',
132
- * bucket: 'cloud-files',
133
- * credentialsSecret: 'cloud-files-credentials',
134
- * }}
135
- * />
136
- * )
137
- */
138
- export function Nextcloud(props: NextcloudProps) {
139
- const {
140
- name = 'nextcloud',
141
- namespace = 'default',
142
- version = '31-apache',
143
- host,
144
- replicas = 1,
145
- cache = true,
146
- storage = '10Gi',
147
- storageClassName,
148
- objectStorage,
149
- secretsName,
150
- resources = {
151
- requests: { memory: '512Mi', cpu: '250m' },
152
- limits: { memory: '2Gi', cpu: '1000m' },
153
- },
154
- tls = { secretName: `${name}-tls`, clusterIssuer: 'letsencrypt-prod' },
155
- } = props
156
-
157
- const sharedOperators = useContext(OperatorContext)
158
- const secretProvider = useContext(SecretContext)
159
- const resources_: ReturnType<typeof jsx>[] = []
160
-
161
- const dbHost = `${name}-rw`
162
- const dbCredentialsName = `${name}-db-credentials`
163
- const appSecretsName = secretsName ?? `${name}-app-secrets`
164
- const htmlClaim = `${name}-html`
165
- const image = `nextcloud:${version}`
166
- const cronSchedule = '*/5 * * * *'
167
-
168
- // --- App secrets provisioning ---------------------------------------------
169
- // The admin bootstrap password must never be rendered as plaintext. With a
170
- // secrets backend it is provisioned through the backend (key `adminPassword`);
171
- // otherwise reference a pre-created Secret.
172
- if (!secretsName) {
173
- if (
174
- !secretProvider ||
175
- (secretProvider.backend !== 'vault' && secretProvider.backend !== 'openbao')
176
- ) {
177
- throw new Error(
178
- `Nextcloud "${name}" requires application secrets (NEXTCLOUD_ADMIN_PASSWORD).\n` +
179
- `\n` +
180
- `Nextcloud bootstraps its admin account from this password — it must not ` +
181
- `be rendered as plaintext.\n` +
182
- `\n` +
183
- `Fix: configure a secrets backend on the Platform:\n` +
184
- ` <Platform secrets={{ backend: 'openbao', mount: 'kv', path: 'apps' }}>\n` +
185
- ` <Nextcloud name="${name}" host="${host}" />\n` +
186
- ` </Platform>\n` +
187
- `\n` +
188
- `Or reference a pre-created Secret (key: adminPassword):\n` +
189
- ` <Nextcloud name="${name}" host="${host}" secretsName="${name}-app-secrets" />`
190
- )
191
- }
192
-
193
- const spec = {
194
- ...(secretProvider.backend === 'vault'
195
- ? { vaultAuthRef: secretProvider.authRef }
196
- : { openbaoAuthRef: secretProvider.authRef }),
197
- mount: secretProvider.mount,
198
- type: 'kv-v2' as const,
199
- path: `${secretProvider.path ?? name}/${name}/secrets`,
200
- destination: { create: true, name: appSecretsName },
201
- }
202
- resources_.push(
203
- secretProvider.backend === 'vault'
204
- ? jsx('VaultStaticSecret', {
205
- apiVersion: 'secrets.hashicorp.com/v1beta1',
206
- kind: 'VaultStaticSecret',
207
- metadata: { name: `${name}-secrets`, namespace },
208
- spec,
209
- })
210
- : jsx('OpenBaoStaticSecret', {
211
- apiVersion: 'secrets.openbao.org/v1beta1',
212
- kind: 'OpenBaoStaticSecret',
213
- metadata: { name: `${name}-secrets`, namespace },
214
- spec,
215
- })
216
- )
217
- }
218
-
219
- // --- Operators -------------------------------------------------------------
220
- if (cache && !sharedOperators.some((op) => op.name === OPERATOR_REDIS)) {
221
- resources_.push(declareOperator(operators[OPERATOR_REDIS]()))
222
- }
223
-
224
- // Redis — file locking + caching (OT-Container-Kit operator). A
225
- // RedisReplication (master/replica) is what Nextcloud's locking needs;
226
- // cluster mode is overkill and the `${name}-redis` service fronts it.
227
- if (cache) {
228
- resources_.push(
229
- RedisReplicationComponent({
230
- metadata: { name: `${name}-redis`, namespace },
231
- spec: {
232
- clusterSize: 3,
233
- kubernetesConfig: { image: 'redis:7.2-alpine' },
234
- },
235
- })
236
- )
237
- }
238
-
239
- // --- /var/www/html persistence ----------------------------------------------
240
- // Apps, config and the data directory live under /var/www/html and must
241
- // survive pod restarts. ReadWriteMany lets the cron job share the same
242
- // tree and keeps replicas > 1 viable (requires an RWX StorageClass).
243
- const htmlPvc: PersistentVolumeClaim = {
244
- apiVersion: 'v1',
245
- kind: 'PersistentVolumeClaim',
246
- metadata: { name: htmlClaim, namespace, labels: { app: name } },
247
- spec: {
248
- accessModes: ['ReadWriteMany'],
249
- resources: { requests: { storage } },
250
- ...(storageClassName && { storageClassName }),
251
- },
252
- }
253
- resources_.push(jsx('PersistentVolumeClaim', htmlPvc))
254
-
255
- // --- Env wiring --------------------------------------------------------------
256
- // Every credential is referenced with $(VAR) expansion or secretKeyRef —
257
- // no plaintext in the manifest. Secret-backed vars are declared BEFORE
258
- // plain env vars so dependent $(VAR) expansion resolves. Nextcloud
259
- // natively reads POSTGRES_* and OBJECTSTORE_S3_*; the S3 keys/secrets are
260
- // expanded from the AWS_* secret-backed vars. (Nextcloud ignores
261
- // DATABASE_URL — do not add it back.)
262
- const secrets: Record<string, SecretRef | string> = {
263
- PGPASSWORD: { secret: dbCredentialsName, key: 'password' },
264
- NEXTCLOUD_ADMIN_PASSWORD: { secret: appSecretsName, key: 'adminPassword' },
265
- ...(objectStorage
266
- ? {
267
- AWS_ACCESS_KEY_ID: { secret: objectStorage.credentialsSecret, key: 'accessKey' },
268
- AWS_SECRET_ACCESS_KEY: { secret: objectStorage.credentialsSecret, key: 'secretKey' },
269
- }
270
- : {}),
271
- }
272
-
273
- const env: Record<string, string> = {
274
- POSTGRES_HOST: dbHost,
275
- POSTGRES_PORT: '5432',
276
- POSTGRES_DB: name,
277
- POSTGRES_USER: name,
278
- POSTGRES_PASSWORD: '$(PGPASSWORD)',
279
- NEXTCLOUD_ADMIN_USER: 'admin',
280
- NEXTCLOUD_TRUSTED_DOMAINS: host,
281
- OVERWRITEHOST: host,
282
- OVERWRITEPROTOCOL: 'https',
283
- OVERWRITECLIURL: `https://${host}`,
284
- ...(cache ? { REDIS_HOST: `${name}-redis`, REDIS_HOST_PORT: '6379' } : {}),
285
- ...(objectStorage
286
- ? {
287
- OBJECTSTORE_S3_HOST: objectStorage.endpoint,
288
- OBJECTSTORE_S3_BUCKET: objectStorage.bucket,
289
- OBJECTSTORE_S3_REGION: objectStorage.region ?? 'us-east-1',
290
- OBJECTSTORE_S3_SSL: (objectStorage.ssl ?? true) ? 'true' : 'false',
291
- ...(objectStorage.port !== undefined && {
292
- OBJECTSTORE_S3_PORT: String(objectStorage.port),
293
- }),
294
- OBJECTSTORE_S3_USEPATH_STYLE: 'true',
295
- OBJECTSTORE_S3_AUTOCREATE: 'true',
296
- OBJECTSTORE_S3_KEY: '$(AWS_ACCESS_KEY_ID)',
297
- OBJECTSTORE_S3_SECRET: '$(AWS_SECRET_ACCESS_KEY)',
298
- }
299
- : {}),
300
- }
301
-
302
- const envVars: EnvVar[] = Object.entries(secrets).map(([envName, ref]) => ({
303
- name: envName,
304
- valueFrom: {
305
- secretKeyRef:
306
- typeof ref === 'string'
307
- ? { name: ref, key: envName }
308
- : { name: ref.secret, key: ref.key || envName },
309
- },
310
- }))
311
- envVars.push(...Object.entries(env).map(([envName, value]) => ({ name: envName, value })))
312
-
313
- // --- Database (CNPG) + app + endpoint ------------------------------------------
314
- // <Database> is the parent wrapper so the CNPG operator and the Cluster
315
- // piggyback on the r8s Database recipe (connect info stays by convention:
316
- // host `${name}-rw`, secret `${name}-db-credentials` key `password`). The
317
- // app is a raw Deployment because WebService cannot mount volumes.
318
- const app: Deployment = {
319
- apiVersion: 'apps/v1',
320
- kind: 'Deployment',
321
- metadata: { name, namespace, labels: { app: name } },
322
- spec: {
323
- replicas,
324
- selector: { matchLabels: { app: name } },
325
- template: {
326
- metadata: { labels: { app: name } },
327
- spec: {
328
- containers: [
329
- {
330
- name: 'app',
331
- image,
332
- imagePullPolicy: 'Always',
333
- ports: [{ containerPort: 80 }],
334
- env: envVars,
335
- resources,
336
- livenessProbe: {
337
- httpGet: { path: STATUS_PATH, port: 80 },
338
- initialDelaySeconds: 30,
339
- periodSeconds: 10,
340
- },
341
- readinessProbe: {
342
- httpGet: { path: STATUS_PATH, port: 80 },
343
- initialDelaySeconds: 20,
344
- periodSeconds: 10,
345
- failureThreshold: 5,
346
- },
347
- volumeMounts: [{ name: 'html', mountPath: HTML_MOUNT }],
348
- },
349
- ],
350
- volumes: [{ name: 'html', persistentVolumeClaim: { claimName: htmlClaim } }],
351
- },
352
- },
353
- },
354
- }
355
-
356
- const appService: Service = {
357
- apiVersion: 'v1',
358
- kind: 'Service',
359
- metadata: { name, namespace },
360
- spec: {
361
- type: 'ClusterIP',
362
- selector: { app: name },
363
- ports: [{ port: 80, targetPort: 80 }],
364
- },
365
- }
366
-
367
- resources_.push(
368
- jsx(Database, {
369
- name,
370
- namespace,
371
- storage: '10Gi',
372
- children: jsx(Fragment, {
373
- children: [jsx('Deployment', app), jsx('Service', appService)],
374
- }),
375
- })
376
- )
377
-
378
- // --- CronJob — background jobs (php -f cron.php) --------------------------------
379
- // WebService cannot express a separate scheduled workload, so the cron job
380
- // is a raw CronJob that mounts the SAME `${name}-html` claim and runs the
381
- // image's own php. It cannot see DatabaseContext: host/user are hardcoded
382
- // by convention and the DB password arrives via secretKeyRef.
383
- resources_.push(
384
- jsx('CronJob', {
385
- apiVersion: 'batch/v1',
386
- kind: 'CronJob',
387
- metadata: { name: `${name}-cron`, namespace },
388
- spec: {
389
- schedule: cronSchedule,
390
- concurrencyPolicy: 'Forbid',
391
- successfulJobsHistoryLimit: 1,
392
- failedJobsHistoryLimit: 1,
393
- jobTemplate: {
394
- spec: {
395
- backoffLimit: 2,
396
- template: {
397
- metadata: { labels: { app: `${name}-cron` } },
398
- spec: {
399
- restartPolicy: 'OnFailure',
400
- containers: [
401
- {
402
- name: 'nextcloud-cron',
403
- image,
404
- command: ['php', '-f', '/var/www/html/cron.php'],
405
- env: [
406
- { name: 'POSTGRES_HOST', value: dbHost },
407
- { name: 'POSTGRES_PORT', value: '5432' },
408
- { name: 'POSTGRES_DB', value: name },
409
- { name: 'POSTGRES_USER', value: name },
410
- {
411
- name: 'PGPASSWORD',
412
- valueFrom: {
413
- secretKeyRef: { name: dbCredentialsName, key: 'password' },
414
- },
415
- },
416
- ...(cache ? [{ name: 'REDIS_HOST', value: `${name}-redis` }] : []),
417
- ],
418
- resources: {
419
- requests: { memory: '128Mi', cpu: '100m' },
420
- limits: { memory: '512Mi', cpu: '500m' },
421
- },
422
- volumeMounts: [{ name: 'html', mountPath: HTML_MOUNT }],
423
- },
424
- ],
425
- volumes: [{ name: 'html', persistentVolumeClaim: { claimName: htmlClaim } }],
426
- },
427
- },
428
- },
429
- },
430
- },
431
- })
432
- )
433
-
434
- // --- Endpoint — web UI and WebDAV share the host --------------------------------
435
- resources_.push(
436
- <Endpoint
437
- name={`${name}-endpoint`}
438
- namespace={namespace}
439
- host={host}
440
- serviceName={name}
441
- servicePort={80}
442
- tls={tls}
443
- />
444
- )
445
-
446
- return jsx(Fragment, { children: resources_ })
447
- }
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
- }