@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.
- package/dist/index.d.ts +147 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +249 -0
- package/dist/index.js.map +1 -0
- package/package.json +5 -2
- package/__tests__/outline.test.ts +0 -213
- package/examples/basic.tsx +0 -21
- package/src/index.tsx +0 -289
- package/tsconfig.json +0 -15
package/dist/index.d.ts
ADDED
|
@@ -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.
|
|
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
|
-
})
|
package/examples/basic.tsx
DELETED
|
@@ -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
|
-
}
|