@r8s/outline 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.
@@ -0,0 +1,147 @@
1
+ import { type DatabaseProps } from '@r8s/recipes';
2
+ import type { SecretRef } from '@r8s/recipes';
3
+ export interface OutlineProps {
4
+ /** Resource name (defaults to 'outline') */
5
+ name?: string;
6
+ /** Kubernetes namespace (defaults to 'default') */
7
+ namespace?: string;
8
+ /**
9
+ * Container image tag (defaults to a pinned release — see
10
+ * https://github.com/outline/outline/releases when upgrading).
11
+ */
12
+ version?: string;
13
+ /** Public hostname for the wiki */
14
+ host: string;
15
+ /** Storage request for the CNPG Postgres cluster (defaults to '20Gi') */
16
+ storage?: string;
17
+ /** Number of CNPG instances (defaults to 2 — facit production sizing) */
18
+ instances?: number;
19
+ /**
20
+ * CNPG backup configuration passed through to the Database recipe
21
+ * (continuous WAL archiving + scheduled base backups). Attachments
22
+ * durability belongs to the object store (erasure-coded RustFS),
23
+ * NOT to Velero — CNPG barman is the backup path.
24
+ */
25
+ backup?: DatabaseProps['backup'];
26
+ /**
27
+ * Number of replicas. Defaults to 1 with strategy Recreate: Outline runs
28
+ * DB migrations at boot and two concurrent migrating pods corrupt state.
29
+ * Scale via WEB_CONCURRENCY inside the pod instead.
30
+ */
31
+ replicas?: number;
32
+ /** Provision a standalone Redis (caching + websockets) — default on */
33
+ cache?: boolean;
34
+ /** Redis shape (standalone by facit default; opt out with cache: false) */
35
+ redis?: {
36
+ /** Redis image (default 'redis:7.0.12') */
37
+ image?: string;
38
+ /** PVC size (default '1Gi') */
39
+ storage?: string;
40
+ /** storageClassName (default cluster default) */
41
+ storageClass?: string;
42
+ /** Promote metrics via redis-exporter sidecar (default true) */
43
+ exporter?: boolean;
44
+ };
45
+ /**
46
+ * S3-compatible object storage for attachments (RustFS in the platform).
47
+ * Reference a bucket whose credentials live in a Secret provisioned by
48
+ * the secrets backend (keys: accessKey, secretKey) — never plaintext.
49
+ */
50
+ objectStorage?: {
51
+ /** S3 endpoint URL, e.g. https://s3.internal.example.com */
52
+ endpoint: string;
53
+ /** Bucket name for attachments */
54
+ bucket: string;
55
+ /** Name of the Secret holding accessKey / secretKey */
56
+ credentialsSecret: string;
57
+ /** Region string for Outline's S3 client (defaults to 'us-east-1') */
58
+ region?: string;
59
+ };
60
+ /**
61
+ * OIDC SSO client — register Outline as a client in Keycloak (the
62
+ * Auth recipe). With a secrets backend, client id/secret are taken
63
+ * from the provisioned bundle (keys oidc_client_id/oidc_client_secret)
64
+ * unless clientSecretRef is given.
65
+ */
66
+ sso?: {
67
+ issuer: string;
68
+ /** Optional when the backend bundle carries oidc_client_id */
69
+ clientId?: string;
70
+ /** Optional when the backend bundle carries oidc_client_secret */
71
+ clientSecretRef?: SecretRef;
72
+ scopes?: string;
73
+ /** OIDC_DISPLAY_NAME — login-button label (default 'SSO') */
74
+ displayName?: string;
75
+ /** OIDC_USERNAME_CLAIM (default 'preferred_username') */
76
+ usernameClaim?: string;
77
+ /** OIDC_LOGOUT_URI — optional full logout URL */
78
+ logoutUri?: string;
79
+ };
80
+ /**
81
+ * Name of an existing Secret holding `SECRET_KEY`/`UTILS_SECRET`
82
+ * (and OIDC_* when using bundled SSO). Required unless a secrets
83
+ * backend (openbao/vault) is configured on the surrounding
84
+ * Platform — the backend then provisions them.
85
+ */
86
+ secretsName?: string;
87
+ /** Requested resources */
88
+ resources?: {
89
+ requests?: {
90
+ cpu?: string;
91
+ memory?: string;
92
+ };
93
+ limits?: {
94
+ cpu?: string;
95
+ memory?: string;
96
+ };
97
+ };
98
+ /** Extra env for the app container (merged last — escape hatch) */
99
+ env?: Record<string, string>;
100
+ /** Extra annotations merged onto the Endpoint */
101
+ endpointAnnotations?: Record<string, string>;
102
+ /** TLS configuration (defaults to letsencrypt-prod cluster issuer) */
103
+ tls?: {
104
+ secretName: string;
105
+ clusterIssuer: string;
106
+ };
107
+ }
108
+ /**
109
+ * Outline — team wiki with Postgres, Redis, S3 attachments and OIDC SSO.
110
+ *
111
+ * @title Outline
112
+ * @category Knowledge & Documentation
113
+ *
114
+ * Composes:
115
+ * - CNPG Postgres cluster (documents, revisions, users)
116
+ * - Redis cluster for queue + rate limiting (default on)
117
+ * - Outline Deployment + Service + Endpoint
118
+ * - S3/RustFS bucket reference for attachments
119
+ * - SECRET_KEY / UTILS_SECRET provisioned by the Platform secrets
120
+ * backend (openbao / vault), or referenced from an existing Secret
121
+ * - OIDC SSO against the Keycloak `Auth` recipe
122
+ *
123
+ * @example
124
+ * import { Platform } from '@r8s/recipes'
125
+ * import { Outline } from '@r8s/outline'
126
+ *
127
+ * export default (
128
+ * <Platform secrets={{ backend: 'openbao', mount: 'kv', path: 'apps' }}>
129
+ * <Outline
130
+ * name="wiki"
131
+ * host="wiki.example.com"
132
+ * objectStorage={{
133
+ * endpoint: 'https://s3.internal.example.com',
134
+ * bucket: 'wiki-attachments',
135
+ * credentialsSecret: 'wiki-attachments-credentials',
136
+ * }}
137
+ * sso={{
138
+ * issuer: 'https://keycloak.example.com/realms/platform',
139
+ * clientId: 'outline',
140
+ * clientSecretRef: { secret: 'outline-sso', key: 'clientSecret' },
141
+ * }}
142
+ * />
143
+ * </Platform>
144
+ * )
145
+ */
146
+ export declare function Outline(props: OutlineProps): import("@r8s/core").r8sElement;
147
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.tsx"],"names":[],"mappings":"AAEA,OAAO,EASL,KAAK,aAAa,EACnB,MAAM,cAAc,CAAA;AACrB,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,cAAc,CAAA;AAE7C,MAAM,WAAW,YAAY;IAC3B,4CAA4C;IAC5C,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,mDAAmD;IACnD,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB;;;OAGG;IACH,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,mCAAmC;IACnC,IAAI,EAAE,MAAM,CAAA;IACZ,yEAAyE;IACzE,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,yEAAyE;IACzE,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB;;;;;OAKG;IACH,MAAM,CAAC,EAAE,aAAa,CAAC,QAAQ,CAAC,CAAA;IAChC;;;;OAIG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,uEAAuE;IACvE,KAAK,CAAC,EAAE,OAAO,CAAA;IACf,2EAA2E;IAC3E,KAAK,CAAC,EAAE;QACN,2CAA2C;QAC3C,KAAK,CAAC,EAAE,MAAM,CAAA;QACd,+BAA+B;QAC/B,OAAO,CAAC,EAAE,MAAM,CAAA;QAChB,iDAAiD;QACjD,YAAY,CAAC,EAAE,MAAM,CAAA;QACrB,gEAAgE;QAChE,QAAQ,CAAC,EAAE,OAAO,CAAA;KACnB,CAAA;IACD;;;;OAIG;IACH,aAAa,CAAC,EAAE;QACd,4DAA4D;QAC5D,QAAQ,EAAE,MAAM,CAAA;QAChB,kCAAkC;QAClC,MAAM,EAAE,MAAM,CAAA;QACd,uDAAuD;QACvD,iBAAiB,EAAE,MAAM,CAAA;QACzB,sEAAsE;QACtE,MAAM,CAAC,EAAE,MAAM,CAAA;KAChB,CAAA;IACD;;;;;OAKG;IACH,GAAG,CAAC,EAAE;QACJ,MAAM,EAAE,MAAM,CAAA;QACd,8DAA8D;QAC9D,QAAQ,CAAC,EAAE,MAAM,CAAA;QACjB,kEAAkE;QAClE,eAAe,CAAC,EAAE,SAAS,CAAA;QAC3B,MAAM,CAAC,EAAE,MAAM,CAAA;QACf,6DAA6D;QAC7D,WAAW,CAAC,EAAE,MAAM,CAAA;QACpB,yDAAyD;QACzD,aAAa,CAAC,EAAE,MAAM,CAAA;QACtB,iDAAiD;QACjD,SAAS,CAAC,EAAE,MAAM,CAAA;KACnB,CAAA;IACD;;;;;OAKG;IACH,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,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,mEAAmE;IACnE,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAC5B,iDAAiD;IACjD,mBAAmB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAC5C,sEAAsE;IACtE,GAAG,CAAC,EAAE;QACJ,UAAU,EAAE,MAAM,CAAA;QAClB,aAAa,EAAE,MAAM,CAAA;KACtB,CAAA;CACF;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqCG;AACH,wBAAgB,OAAO,CAAC,KAAK,EAAE,YAAY,kCAwQ1C"}
package/dist/index.js ADDED
@@ -0,0 +1,249 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.Outline = Outline;
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
+ * Outline — team wiki with Postgres, Redis, S3 attachments and OIDC SSO.
10
+ *
11
+ * @title Outline
12
+ * @category Knowledge & Documentation
13
+ *
14
+ * Composes:
15
+ * - CNPG Postgres cluster (documents, revisions, users)
16
+ * - Redis cluster for queue + rate limiting (default on)
17
+ * - Outline Deployment + Service + Endpoint
18
+ * - S3/RustFS bucket reference for attachments
19
+ * - SECRET_KEY / UTILS_SECRET provisioned by the Platform secrets
20
+ * backend (openbao / vault), or referenced from an existing Secret
21
+ * - OIDC SSO against the Keycloak `Auth` recipe
22
+ *
23
+ * @example
24
+ * import { Platform } from '@r8s/recipes'
25
+ * import { Outline } from '@r8s/outline'
26
+ *
27
+ * export default (
28
+ * <Platform secrets={{ backend: 'openbao', mount: 'kv', path: 'apps' }}>
29
+ * <Outline
30
+ * name="wiki"
31
+ * host="wiki.example.com"
32
+ * objectStorage={{
33
+ * endpoint: 'https://s3.internal.example.com',
34
+ * bucket: 'wiki-attachments',
35
+ * credentialsSecret: 'wiki-attachments-credentials',
36
+ * }}
37
+ * sso={{
38
+ * issuer: 'https://keycloak.example.com/realms/platform',
39
+ * clientId: 'outline',
40
+ * clientSecretRef: { secret: 'outline-sso', key: 'clientSecret' },
41
+ * }}
42
+ * />
43
+ * </Platform>
44
+ * )
45
+ */
46
+ function Outline(props) {
47
+ const { name = 'outline', namespace: namespaceProp, version = '1.9.2', host, storage = '20Gi', instances = 2, backup, replicas = 1, cache = true, redis = {}, objectStorage, sso, secretsName, resources = {
48
+ requests: { memory: '512Mi', cpu: '250m' },
49
+ limits: { memory: '2Gi', cpu: '1000m' },
50
+ }, env: extraEnv = {}, endpointAnnotations = {}, tls = { secretName: `${name}-tls`, clusterIssuer: 'letsencrypt-prod' }, } = props;
51
+ const sharedOperators = (0, recipes_1.useOperators)();
52
+ const secretProvider = (0, core_1.useContext)(defaults_1.SecretContext);
53
+ const namespace = (0, defaults_1.useNamespace)(namespaceProp);
54
+ const resources_ = [];
55
+ const dbHost = `${name}-rw`;
56
+ const dbCredentialsName = `${name}-db-credentials`;
57
+ const platformSecretsName = secretsName ?? `${name}-app-secrets`;
58
+ // --- App secrets (SECRET_KEY / UTILS_SECRET) ------------------------------
59
+ if (!secretsName) {
60
+ if (!(0, recipes_1.canProvisionSecrets)(secretProvider)) {
61
+ throw (0, recipes_1.secretsRequiredError)('Outline', name, 'application secrets (SECRET_KEY, UTILS_SECRET)', {
62
+ propName: 'secretsName',
63
+ exampleValue: `${name}-app-secrets`,
64
+ keys: ['SECRET_KEY', 'UTILS_SECRET'],
65
+ });
66
+ }
67
+ // Facit bundle: vault keys are snake_case, the destination carries
68
+ // env-case names so Outline reads SECRET_KEY/UTILS_SECRET/OIDC_* directly.
69
+ resources_.push((0, core_1.jsx)(recipes_1.StaticSecret, {
70
+ name: `${name}-secrets`,
71
+ namespace,
72
+ path: `${secretProvider.path ?? name}/${name}/app`,
73
+ secretName: platformSecretsName,
74
+ // Facit interval; rotation of SECRET_KEY/OIDC invalidates sessions —
75
+ // restart the single Deployment so the pod re-reads fresh values
76
+ refreshAfter: '3600s',
77
+ keys: {
78
+ SECRET_KEY: 'secret_key',
79
+ UTILS_SECRET: 'utils_secret',
80
+ ...(sso
81
+ ? { OIDC_CLIENT_ID: 'oidc_client_id', OIDC_CLIENT_SECRET: 'oidc_client_secret' }
82
+ : {}),
83
+ },
84
+ restart: [{ kind: 'Deployment', name }],
85
+ }));
86
+ }
87
+ // --- Operators -------------------------------------------------------------
88
+ if (cache) {
89
+ resources_.push(...(0, recipes_1.maybeOperator)('redis-operator', sharedOperators));
90
+ }
91
+ // Redis — caching + websockets. Standalone is the facit shape: durable via
92
+ // a 1Gi PVC, observable via the exporter sidecar, sized 100m/256Mi→250m/512Mi.
93
+ // (A Replication topology exists for durable queues; Outline's cache can
94
+ // simply rebuild after pod loss — no need to spend three pods here.)
95
+ if (cache) {
96
+ const exporter = redis.exporter ?? true;
97
+ resources_.push((0, core_1.jsx)('Redis', {
98
+ apiVersion: 'redis.redis.opstreelabs.in/v1beta2',
99
+ kind: 'Redis',
100
+ metadata: {
101
+ name: `${name}-redis`,
102
+ namespace,
103
+ // Scrape annotations only make sense when the exporter sidecar runs
104
+ ...(exporter
105
+ ? {
106
+ annotations: {
107
+ 'prometheus.io/scrape': 'true',
108
+ 'prometheus.io/port': '9121',
109
+ },
110
+ }
111
+ : {}),
112
+ },
113
+ spec: {
114
+ kubernetesConfig: {
115
+ image: redis.image ?? 'redis:7.0.12',
116
+ imagePullPolicy: 'IfNotPresent',
117
+ resources: {
118
+ requests: { cpu: '100m', memory: '256Mi' },
119
+ limits: { cpu: '250m', memory: '512Mi' },
120
+ },
121
+ },
122
+ storage: {
123
+ volumeClaimTemplate: {
124
+ spec: {
125
+ accessModes: ['ReadWriteOnce'],
126
+ resources: { requests: { storage: redis.storage ?? '1Gi' } },
127
+ ...(redis.storageClass ? { storageClassName: redis.storageClass } : {}),
128
+ },
129
+ },
130
+ },
131
+ ...(exporter
132
+ ? {
133
+ redisExporter: {
134
+ enabled: true,
135
+ image: 'quay.io/opstree/redis-exporter:v1.44.0',
136
+ resources: {
137
+ requests: { cpu: '50m', memory: '64Mi' },
138
+ limits: { cpu: '100m', memory: '128Mi' },
139
+ },
140
+ },
141
+ }
142
+ : {}),
143
+ },
144
+ }));
145
+ }
146
+ // --- Env wiring --------------------------------------------------------------
147
+ // Every credential is referenced with $(VAR) expansion or secretKeyRef —
148
+ // no plaintext in the manifest. The WebService declares secret-backed
149
+ // vars before plain env vars, so dependent expansion resolves.
150
+ const env = {
151
+ NODE_ENV: 'production',
152
+ PORT: '3000',
153
+ HOST: '0.0.0.0',
154
+ // One in-process worker: scaling happens via WEB_CONCURRENCY/resources
155
+ // inside the pod, while replicas stays 1 (boot-time DB migrations must
156
+ // never run concurrently — see the Recreate strategy)
157
+ WEB_CONCURRENCY: '1',
158
+ // Deterministic parts inlined; password arrives via PGPASSWORD
159
+ DATABASE_URL: `postgresql://${name}:$(PGPASSWORD)@${dbHost}:5432/${name}`,
160
+ // Outline (Sequelize) cannot do SSL to the in-cluster CNPG service
161
+ PGSSLMODE: 'disable',
162
+ SECRET_KEY: '$(SECRET_KEY)',
163
+ UTILS_SECRET: '$(UTILS_SECRET)',
164
+ URL: `https://${host}`,
165
+ FORCE_HTTPS: 'true',
166
+ RATE_LIMITER_ENABLED: 'true',
167
+ // Self-hosted: no in-app upgrade prompts (image is pinned + deliberate)
168
+ ENABLE_UPDATES: 'false',
169
+ LOG_LEVEL: 'info',
170
+ ...(cache ? { REDIS_URL: `redis://${name}-redis:6379` } : {}),
171
+ ...(objectStorage
172
+ ? {
173
+ FILE_STORAGE: 's3',
174
+ AWS_REGION: objectStorage.region ?? 'us-east-1',
175
+ // Facit semantics: this is the ENDPOINT only, not endpoint/bucket
176
+ AWS_S3_UPLOAD_BUCKET_URL: objectStorage.endpoint,
177
+ AWS_S3_UPLOAD_BUCKET_NAME: objectStorage.bucket,
178
+ AWS_S3_FORCE_PATH_STYLE: 'true',
179
+ AWS_S3_ACL: 'private',
180
+ }
181
+ : {}),
182
+ ...(sso
183
+ ? {
184
+ OIDC_DISPLAY_NAME: sso.displayName ?? 'SSO',
185
+ OIDC_USERNAME_CLAIM: sso.usernameClaim ?? 'preferred_username',
186
+ OIDC_ISSUER: sso.issuer,
187
+ // OIDC_CLIENT_ID/SECRET arrive via secretKeyRef (below) unless a
188
+ // literal clientId is given — no duplicating expansion vars
189
+ ...(sso.clientId ? { OIDC_CLIENT_ID: sso.clientId } : {}),
190
+ OIDC_SCOPES: sso.scopes ?? 'openid email profile',
191
+ OIDC_AUTH_URI: '$(OIDC_ISSUER)/protocol/openid-connect/auth',
192
+ OIDC_TOKEN_URI: '$(OIDC_ISSUER)/protocol/openid-connect/token',
193
+ OIDC_USERINFO_URI: '$(OIDC_ISSUER)/protocol/openid-connect/userinfo',
194
+ ...(sso.logoutUri ? { OIDC_LOGOUT_URI: sso.logoutUri } : {}),
195
+ }
196
+ : {}),
197
+ ...extraEnv,
198
+ };
199
+ // Credentials delivered via secretKeyRef (runtime injection). OIDC reads
200
+ // from the bundle unless an explicit clientSecretRef is given (a
201
+ // user-supplied secretsName Secret is expected to carry the same keys).
202
+ const secrets = {
203
+ SECRET_KEY: { secret: platformSecretsName, key: 'SECRET_KEY' },
204
+ UTILS_SECRET: { secret: platformSecretsName, key: 'UTILS_SECRET' },
205
+ PGPASSWORD: { secret: dbCredentialsName, key: 'password' },
206
+ ...(objectStorage
207
+ ? {
208
+ AWS_ACCESS_KEY_ID: { secret: objectStorage.credentialsSecret, key: 'accessKey' },
209
+ AWS_SECRET_ACCESS_KEY: { secret: objectStorage.credentialsSecret, key: 'secretKey' },
210
+ }
211
+ : {}),
212
+ ...(sso
213
+ ? {
214
+ OIDC_CLIENT_SECRET: sso.clientSecretRef ?? {
215
+ secret: platformSecretsName,
216
+ key: 'OIDC_CLIENT_SECRET',
217
+ },
218
+ ...(!sso.clientId
219
+ ? { OIDC_CLIENT_ID: { secret: platformSecretsName, key: 'OIDC_CLIENT_ID' } }
220
+ : {}),
221
+ }
222
+ : {}),
223
+ };
224
+ // --- Database + app + endpoint ------------------------------------------------
225
+ // Database wraps the app so credentials stay consistent with the r8s
226
+ // Database recipe (CNPG dedicated cluster provisions the secret).
227
+ resources_.push((0, core_1.jsx)(recipes_1.Database, {
228
+ name,
229
+ namespace,
230
+ storage,
231
+ instances,
232
+ ...(backup ? { backup } : {}),
233
+ children: ((0, jsx_runtime_1.jsx)(recipes_1.WebService, { name: name, namespace: namespace, image: `docker.getoutline.com/outlinewiki/outline:${version}`, port: 3000, replicas: replicas,
234
+ // DB migrations run at boot — never run two migrating pods at once
235
+ strategy: "Recreate", resources: resources, probes: {
236
+ // Outline boots slowly (assets + migrations): facit probe timings
237
+ readiness: { tcp: true, initialDelaySeconds: 30, failureThreshold: 6 },
238
+ liveness: { tcp: true, initialDelaySeconds: 90, periodSeconds: 30 },
239
+ }, env: env, secrets: secrets })),
240
+ }));
241
+ resources_.push((0, jsx_runtime_1.jsx)(recipes_1.Endpoint, { name: `${name}-endpoint`, namespace: namespace, host: host, serviceName: name, servicePort: 3000, tls: tls, annotations: {
242
+ // Websockets keep realtime-collab connections open — facit sets 1h
243
+ 'nginx.ingress.kubernetes.io/proxy-read-timeout': '3600',
244
+ 'nginx.ingress.kubernetes.io/proxy-send-timeout': '3600',
245
+ ...endpointAnnotations,
246
+ } }));
247
+ return (0, core_1.jsx)(core_1.Fragment, { children: resources_ });
248
+ }
249
+ //# 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,6 +1,6 @@
1
1
  {
2
2
  "name": "@r8s/outline",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "Outline wiki — Postgres persistence, Redis queue, S3 attachments, OIDC SSO via Keycloak",
5
5
  "license": "MIT",
6
6
  "author": "Berget AI AB",
@@ -37,5 +37,8 @@
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,213 +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
- // Outline 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 { Outline } from '../src/index'
13
-
14
- const openbao = { backend: 'openbao', mount: 'kv', path: 'test' }
15
-
16
- /** Render Outline inside a Platform-like secrets backend (OpenBao). */
17
- function renderOutline(props: Record<string, unknown>): ReturnType<typeof render> {
18
- return render(
19
- jsx(SecretContext.Provider, {
20
- value: openbao as never,
21
- children: jsx(Outline, props as never),
22
- })
23
- )
24
- }
25
-
26
- /** Wrap Outline in an OperatorContext (no secrets backend). */
27
- function elementWithContext(ops: any[], props: Record<string, unknown>): r8sElement {
28
- return jsx(OperatorContext.Provider, {
29
- value: ops,
30
- children: jsx(Outline, props as never),
31
- })
32
- }
33
-
34
- const objectStorage = {
35
- endpoint: 'https://s3.internal.example.com',
36
- bucket: 'wiki-attachments',
37
- credentialsSecret: 'wiki-attachments-credentials',
38
- }
39
-
40
- const sso = {
41
- issuer: 'https://keycloak.example.com/realms/platform',
42
- clientId: 'outline',
43
- clientSecretRef: { secret: 'outline-sso', key: 'clientSecret' },
44
- }
45
-
46
- describe('operator declarations', () => {
47
- it('declares the redis operator when cache is enabled', () => {
48
- const result = renderOutline({ host: 'wiki.example.com' })
49
- expect(result.operators.some((op) => op.name === 'redis-operator')).toBe(true)
50
- })
51
-
52
- it('declares the cnpg operator via the Database recipe', () => {
53
- const result = renderOutline({ host: 'wiki.example.com' })
54
- expect(result.operators.some((op) => op.name === 'cnpg')).toBe(true)
55
- })
56
-
57
- it('skips the redis operator when cache is disabled', () => {
58
- const result = renderOutline({ host: 'wiki.example.com', cache: false })
59
- expect(result.operators.some((op) => op.name === 'redis-operator')).toBe(false)
60
- })
61
-
62
- it('deduplicates operators provided via context', () => {
63
- const result = render(
64
- elementWithContext([operators['redis-operator'](), operators['cnpg']()], {
65
- host: 'wiki.example.com',
66
- secretsName: 'existing-secrets',
67
- })
68
- )
69
- const names = result.operators.map((op) => op.name)
70
- expect(names.filter((n) => n === 'redis-operator')).toHaveLength(1)
71
- expect(names.filter((n) => n === 'cnpg')).toHaveLength(1)
72
- })
73
- })
74
-
75
- describe('rendering defaults', () => {
76
- it('renders deployment, service, ingress, database and redis', () => {
77
- const result = renderOutline({ host: 'wiki.example.com' })
78
- const kinds = result.resources.map((r) => r.kind)
79
- expect(kinds).toContain('Deployment')
80
- expect(kinds).toContain('Service')
81
- expect(kinds).toContain('Ingress')
82
- expect(kinds).toContain('Cluster')
83
- expect(kinds).toContain('RedisReplication')
84
- })
85
-
86
- it('renders gateway resources when platform uses gateway routing', () => {
87
- const result = render(
88
- jsx(RoutingContext.Provider, {
89
- value: { mode: 'gateway', gatewayClassName: 'eg' },
90
- children: jsx(SecretContext.Provider, {
91
- value: openbao as never,
92
- children: jsx(Outline, { host: 'wiki.example.com' }),
93
- }),
94
- })
95
- )
96
- const kinds = result.resources.map((r) => r.kind)
97
- expect(kinds).toContain('HTTPRoute')
98
- })
99
-
100
- it('renders a valid Ingress when platform uses ingress routing', () => {
101
- const result = render(
102
- jsx(RoutingContext.Provider, {
103
- value: { mode: 'ingress' },
104
- children: jsx(SecretContext.Provider, {
105
- value: openbao as never,
106
- children: jsx(Outline, { host: 'wiki.example.com' }),
107
- }),
108
- })
109
- )
110
- const ingress = result.resources.find((r) => r.kind === 'Ingress') as any
111
- expect(ingress).toBeDefined()
112
- expect(ingress.spec.rules[0].host).toBe('wiki.example.com')
113
- })
114
-
115
- it('passes resource validation', () => {
116
- const result = renderOutline({
117
- host: 'wiki.example.com',
118
- objectStorage,
119
- sso,
120
- })
121
- for (const resource of result.resources) {
122
- expect(validateResource(resource)).toEqual([])
123
- }
124
- })
125
- })
126
-
127
- describe('rendering with all props', () => {
128
- it('accepts the full prop surface', () => {
129
- const result = renderOutline({
130
- name: 'wiki',
131
- namespace: 'docs',
132
- version: '0.78.0',
133
- host: 'docs.example.com',
134
- replicas: 3,
135
- objectStorage: { ...objectStorage, bucket: 'docs' },
136
- sso,
137
- resources: {
138
- requests: { memory: '1Gi', cpu: '500m' },
139
- limits: { memory: '4Gi', cpu: '2000m' },
140
- },
141
- tls: { secretName: 'docs-tls', clusterIssuer: 'letsencrypt-prod' },
142
- })
143
-
144
- const app = result.resources.find(
145
- (r: any) => r.kind === 'Deployment' && r.metadata.name === 'wiki'
146
- ) as any
147
- expect(app).toBeDefined()
148
- expect(app.spec.replicas).toBe(3)
149
- expect(app.spec.template.spec.containers[0].image).toContain('0.78.0')
150
- expect(app.spec.template.spec.containers[0].resources.limits.memory).toBe('4Gi')
151
- })
152
-
153
- it('wires S3 and SSO on the app when configured', () => {
154
- const result = renderOutline({
155
- host: 'wiki.example.com',
156
- objectStorage,
157
- sso,
158
- })
159
- const app = result.resources.find(
160
- (r: any) => r.kind === 'Deployment' && r.metadata.name === 'outline'
161
- ) as any
162
- const env = app.spec.template.spec.containers[0].env
163
- expect(env.find((e: any) => e.name === 'AWS_S3_UPLOAD_BUCKET_NAME').value).toBe(
164
- 'wiki-attachments'
165
- )
166
- expect(env.find((e: any) => e.name === 'OIDC_ISSUER').value).toBe(sso.issuer)
167
- })
168
- })
169
-
170
- describe('secrets handling', () => {
171
- it('provisions app secrets through a secrets backend', () => {
172
- const result = renderOutline({ host: 'wiki.example.com' })
173
- const kinds = result.resources.map((r) => r.kind)
174
- expect(kinds).toContain('OpenBaoStaticSecret')
175
- })
176
-
177
- it('throws when no secrets backend and no secretsName', () => {
178
- expect(() => render(jsx(Outline, { host: 'wiki.example.com' }))).toThrow(/application secrets/)
179
- })
180
-
181
- it('accepts an existing secretsName without a backend', () => {
182
- expect(() =>
183
- render(jsx(Outline, { host: 'wiki.example.com', secretsName: 'existing-secrets' }))
184
- ).not.toThrow()
185
- })
186
-
187
- it('wires credentials via secretKeyRef (never plaintext env)', () => {
188
- const result = renderOutline({ host: 'wiki.example.com', secretsName: 'existing-secrets' })
189
- const app = result.resources.find(
190
- (r: any) => r.kind === 'Deployment' && r.metadata.name === 'outline'
191
- ) as any
192
- const env = app.spec.template.spec.containers[0].env
193
- const secretKey = env.find((e: any) => e.name === 'SECRET_KEY')
194
- const pgPassword = env.find((e: any) => e.name === 'PGPASSWORD')
195
- expect(secretKey.valueFrom.secretKeyRef.name).toBe('existing-secrets')
196
- expect(pgPassword.valueFrom.secretKeyRef.name).toBe('outline-db-credentials')
197
- expect(secretKey.value).toBeUndefined()
198
- expect(pgPassword.value).toBeUndefined()
199
- })
200
-
201
- it('renders no plaintext credentials anywhere', () => {
202
- const result = renderOutline({
203
- host: 'wiki.example.com',
204
- objectStorage,
205
- sso,
206
- })
207
- const { passed, errors } = runGuardrails(result.resources as any[], [noPlaintextSecrets])
208
- if (!passed) {
209
- console.error('Plaintext credential violations:', errors)
210
- }
211
- expect(passed).toBe(true)
212
- })
213
- })
@@ -1,21 +0,0 @@
1
- import { Platform } from '@r8s/recipes'
2
- import { Outline } from '@r8s/outline'
3
-
4
- export default (
5
- <Platform secrets={{ backend: 'openbao', mount: 'kv', path: 'apps' }}>
6
- <Outline
7
- name="wiki"
8
- host="wiki.example.com"
9
- objectStorage={{
10
- endpoint: 'https://s3.internal.example.com',
11
- bucket: 'wiki-attachments',
12
- credentialsSecret: 'wiki-attachments-credentials',
13
- }}
14
- sso={{
15
- issuer: 'https://keycloak.example.com/realms/platform',
16
- clientId: 'outline',
17
- clientSecretRef: { secret: 'outline-sso', key: 'clientSecret' },
18
- }}
19
- />
20
- </Platform>
21
- )
package/src/index.tsx DELETED
@@ -1,289 +0,0 @@
1
- import { jsx, Fragment, useContext, declareOperator } from '@r8s/core'
2
- import { Namespace, OperatorContext, SecretContext } from '@r8s/core/defaults'
3
- import { operators } from '@r8s/crds'
4
- import { Database, WebService, Endpoint } from '@r8s/recipes'
5
- import { RedisReplicationComponent } from '@r8s/crds/redis'
6
- import type { SecretRef } from '@r8s/recipes'
7
-
8
- export interface OutlineProps {
9
- /** Resource name (defaults to 'outline') */
10
- name?: string
11
- /** Kubernetes namespace (defaults to 'default') */
12
- namespace?: string
13
- /** Container image tag (defaults to 'latest' — pin a version in production) */
14
- version?: string
15
- /** Public hostname for the wiki */
16
- host: string
17
- /** Storage request for the CNPG Postgres cluster (defaults to '10Gi') */
18
- storage?: string
19
- /** Number of replicas (Outline is stateless — scale freely) */
20
- replicas?: number
21
- /** Provision a Redis cluster for the queue and rate limiting (default: true) */
22
- cache?: boolean
23
- /**
24
- * S3-compatible object storage for attachments (RustFS in the platform).
25
- * Reference a bucket whose credentials live in a Secret provisioned by
26
- * the secrets backend (keys: accessKey, secretKey) — never plaintext.
27
- */
28
- objectStorage?: {
29
- /** S3 endpoint URL, e.g. https://s3.internal.example.com */
30
- endpoint: string
31
- /** Bucket name for attachments */
32
- bucket: string
33
- /** Name of the Secret holding accessKey / secretKey */
34
- credentialsSecret: string
35
- /** Region string for Outline's S3 client (defaults to 'us-east-1') */
36
- region?: string
37
- }
38
- /**
39
- * OIDC SSO client — register Outline as a client in Keycloak (the
40
- * Auth recipe) and reference the client secret through the backend.
41
- */
42
- sso?: {
43
- issuer: string
44
- clientId: string
45
- clientSecretRef: SecretRef
46
- scopes?: string
47
- }
48
- /**
49
- * Name of an existing Secret holding `secretKey` and `utilsSecret`.
50
- * Required unless a secrets backend (openbao/vault) is configured on
51
- * the surrounding Platform — the backend then provisions them.
52
- */
53
- secretsName?: string
54
- /** Requested resources */
55
- resources?: {
56
- requests?: { cpu?: string; memory?: string }
57
- limits?: { cpu?: string; memory?: string }
58
- }
59
- /** TLS configuration (defaults to letsencrypt-prod cluster issuer) */
60
- tls?: {
61
- secretName: string
62
- clusterIssuer: string
63
- }
64
- }
65
-
66
- const OPERATOR_REDIS = 'redis-operator'
67
-
68
- /**
69
- * Outline — team wiki with Postgres, Redis, S3 attachments and OIDC SSO.
70
- *
71
- * @title Outline
72
- * @category Knowledge & Documentation
73
- *
74
- * Composes:
75
- * - CNPG Postgres cluster (documents, revisions, users)
76
- * - Redis cluster for queue + rate limiting (default on)
77
- * - Outline Deployment + Service + Endpoint
78
- * - S3/RustFS bucket reference for attachments
79
- * - SECRET_KEY / UTILS_SECRET provisioned by the Platform secrets
80
- * backend (openbao / vault), or referenced from an existing Secret
81
- * - OIDC SSO against the Keycloak `Auth` recipe
82
- *
83
- * @example
84
- * import { Platform } from '@r8s/recipes'
85
- * import { Outline } from '@r8s/outline'
86
- *
87
- * export default (
88
- * <Platform secrets={{ backend: 'openbao', mount: 'kv', path: 'apps' }}>
89
- * <Outline
90
- * name="wiki"
91
- * host="wiki.example.com"
92
- * objectStorage={{
93
- * endpoint: 'https://s3.internal.example.com',
94
- * bucket: 'wiki-attachments',
95
- * credentialsSecret: 'wiki-attachments-credentials',
96
- * }}
97
- * sso={{
98
- * issuer: 'https://keycloak.example.com/realms/platform',
99
- * clientId: 'outline',
100
- * clientSecretRef: { secret: 'outline-sso', key: 'clientSecret' },
101
- * }}
102
- * />
103
- * </Platform>
104
- * )
105
- */
106
- export function Outline(props: OutlineProps) {
107
- const {
108
- name = 'outline',
109
- namespace: namespaceProp,
110
- version = 'latest',
111
- host,
112
- storage = '10Gi',
113
- replicas = 1,
114
- cache = true,
115
- objectStorage,
116
- sso,
117
- secretsName,
118
- resources = {
119
- requests: { memory: '512Mi', cpu: '250m' },
120
- limits: { memory: '2Gi', cpu: '1000m' },
121
- },
122
- tls = { secretName: `${name}-tls`, clusterIssuer: 'letsencrypt-prod' },
123
- } = props
124
-
125
- const sharedOperators = useContext(OperatorContext)
126
- const secretProvider = useContext(SecretContext)
127
- // Inherit namespace from <Platform> context — mirrors recipes/database.tsx
128
- const contextNamespace = useContext(Namespace)
129
- const namespace =
130
- namespaceProp ?? (contextNamespace !== 'default' ? contextNamespace : undefined) ?? 'default'
131
- const resources_: ReturnType<typeof jsx>[] = []
132
-
133
- const dbHost = `${name}-rw`
134
- const dbCredentialsName = `${name}-db-credentials`
135
- const platformSecretsName = secretsName ?? `${name}-app-secrets`
136
-
137
- // --- App secrets (SECRET_KEY / UTILS_SECRET) ------------------------------
138
- if (!secretsName) {
139
- if (
140
- !secretProvider ||
141
- (secretProvider.backend !== 'vault' && secretProvider.backend !== 'openbao')
142
- ) {
143
- throw new Error(
144
- `Outline "${name}" requires application secrets (SECRET_KEY, UTILS_SECRET).\n` +
145
- `\n` +
146
- `These must not be rendered as plaintext.\n` +
147
- `\n` +
148
- `Fix: configure a secrets backend on the Platform:\n` +
149
- ` <Platform secrets={{ backend: 'openbao', mount: 'kv', path: 'apps' }}>\n` +
150
- ` <Outline name="${name}" host="${host}" />\n` +
151
- ` </Platform>\n` +
152
- `\n` +
153
- `Or reference a pre-created Secret (keys: secretKey, utilsSecret):\n` +
154
- ` <Outline name="${name}" host="${host}" secretsName="${name}-app-secrets" />`
155
- )
156
- }
157
-
158
- const spec = {
159
- ...(secretProvider.backend === 'vault'
160
- ? { vaultAuthRef: secretProvider.authRef }
161
- : { openbaoAuthRef: secretProvider.authRef }),
162
- mount: secretProvider.mount,
163
- type: 'kv-v2' as const,
164
- path: `${secretProvider.path ?? name}/${name}/secrets`,
165
- destination: { create: true, name: platformSecretsName },
166
- }
167
- resources_.push(
168
- secretProvider.backend === 'vault'
169
- ? jsx('VaultStaticSecret', {
170
- apiVersion: 'secrets.hashicorp.com/v1beta1',
171
- kind: 'VaultStaticSecret',
172
- metadata: { name: `${name}-secrets`, namespace },
173
- spec,
174
- })
175
- : jsx('OpenBaoStaticSecret', {
176
- apiVersion: 'secrets.openbao.org/v1beta1',
177
- kind: 'OpenBaoStaticSecret',
178
- metadata: { name: `${name}-secrets`, namespace },
179
- spec,
180
- })
181
- )
182
- }
183
-
184
- // --- Operators -------------------------------------------------------------
185
- if (cache && !sharedOperators.some((op) => op.name === OPERATOR_REDIS)) {
186
- resources_.push(declareOperator(operators[OPERATOR_REDIS]()))
187
- }
188
-
189
- // Redis — queue + rate limiting (OT-Container-Kit operator)
190
- if (cache) {
191
- resources_.push(
192
- RedisReplicationComponent({
193
- metadata: { name: `${name}-redis`, namespace },
194
- spec: {
195
- clusterSize: 3,
196
- kubernetesConfig: { image: 'redis:7.2-alpine' },
197
- },
198
- })
199
- )
200
- }
201
-
202
- // --- Env wiring --------------------------------------------------------------
203
- // Every credential is referenced with $(VAR) expansion or secretKeyRef —
204
- // no plaintext in the manifest. The WebService declares secret-backed
205
- // vars before plain env vars, so dependent expansion resolves.
206
- const env: Record<string, string> = {
207
- PORT: '3000',
208
- // Deterministic parts inlined; password arrives via PGPASSWORD
209
- DATABASE_URL: `postgresql://${name}:$(PGPASSWORD)@${dbHost}:5432/${name}`,
210
- SECRET_KEY: '$(SECRET_KEY)',
211
- UTILS_SECRET: '$(UTILS_SECRET)',
212
- URL: `https://${host}`,
213
- APP_URL: `https://${host}`,
214
- PROXY_HEADERS_TRUSTED: 'true',
215
- ...(cache ? { REDIS_URL: `redis://${name}-redis:6379` } : {}),
216
- ...(objectStorage
217
- ? {
218
- FILE_STORAGE: 's3',
219
- AWS_REGION: objectStorage.region ?? 'us-east-1',
220
- AWS_S3_UPLOAD_BUCKET_URL: `${objectStorage.endpoint}/${objectStorage.bucket}`,
221
- AWS_S3_UPLOAD_BUCKET_NAME: objectStorage.bucket,
222
- AWS_S3_FORCE_PATH_STYLE: 'true',
223
- AWS_S3_ACL: 'private',
224
- }
225
- : {}),
226
- ...(sso
227
- ? {
228
- OIDC_ISSUER: sso.issuer,
229
- OIDC_CLIENT_ID: sso.clientId,
230
- OIDC_CLIENT_SECRET: '$(OIDC_CLIENT_SECRET)',
231
- OIDC_SCOPES: sso.scopes ?? 'openid email profile',
232
- OIDC_AUTH_URI: '$(OIDC_ISSUER)/protocol/openid-connect/auth',
233
- OIDC_TOKEN_URI: '$(OIDC_ISSUER)/protocol/openid-connect/token',
234
- OIDC_USERINFO_URI: '$(OIDC_ISSUER)/protocol/openid-connect/userinfo',
235
- }
236
- : {}),
237
- }
238
-
239
- // Credentials delivered via secretKeyRef (runtime injection)
240
- const secrets: Record<string, SecretRef | string> = {
241
- SECRET_KEY: { secret: platformSecretsName, key: 'secretKey' },
242
- UTILS_SECRET: { secret: platformSecretsName, key: 'utilsSecret' },
243
- PGPASSWORD: { secret: dbCredentialsName, key: 'password' },
244
- ...(objectStorage
245
- ? {
246
- AWS_ACCESS_KEY_ID: { secret: objectStorage.credentialsSecret, key: 'accessKey' },
247
- AWS_SECRET_ACCESS_KEY: { secret: objectStorage.credentialsSecret, key: 'secretKey' },
248
- }
249
- : {}),
250
- ...(sso ? { OIDC_CLIENT_SECRET: sso.clientSecretRef } : {}),
251
- }
252
-
253
- // --- Database + app + endpoint ------------------------------------------------
254
- // Database wraps the app so credentials stay consistent with the r8s
255
- // Database recipe (CNPG dedicated cluster provisions the secret).
256
- resources_.push(
257
- jsx(Database, {
258
- name,
259
- namespace,
260
- storage,
261
- children: (
262
- <WebService
263
- name={name}
264
- namespace={namespace}
265
- image={`outlinewiki/outline:${version}`}
266
- port={3000}
267
- replicas={replicas}
268
- resources={resources}
269
- probes={{ liveness: { tcp: true }, readiness: { tcp: true, initialDelaySeconds: 15 } }}
270
- env={env}
271
- secrets={secrets}
272
- />
273
- ),
274
- })
275
- )
276
-
277
- resources_.push(
278
- <Endpoint
279
- name={`${name}-endpoint`}
280
- namespace={namespace}
281
- host={host}
282
- serviceName={name}
283
- servicePort={3000}
284
- tls={tls}
285
- />
286
- )
287
-
288
- return jsx(Fragment, { children: resources_ })
289
- }
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
- }