@r8s/n8n 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 +129 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +202 -0
- package/dist/index.js.map +1 -0
- package/package.json +5 -2
- package/__tests__/n8n.test.ts +0 -267
- package/examples/basic.tsx +0 -8
- package/src/index.tsx +0 -284
- package/tsconfig.json +0 -15
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { type DatabaseProps } from '@r8s/recipes';
|
|
2
|
+
export interface N8nProps {
|
|
3
|
+
/** Resource name (defaults to 'n8n') */
|
|
4
|
+
name?: string;
|
|
5
|
+
/** Kubernetes namespace (inherited from Platform context when omitted) */
|
|
6
|
+
namespace?: string;
|
|
7
|
+
/**
|
|
8
|
+
* Container image tag (defaults to a pinned release — see
|
|
9
|
+
* https://github.com/n8n-io/n8n/releases when upgrading). Use 'latest'
|
|
10
|
+
* only for throwaway environments.
|
|
11
|
+
*/
|
|
12
|
+
version?: string;
|
|
13
|
+
/** Public hostname for the editor and webhooks (required) */
|
|
14
|
+
host: string;
|
|
15
|
+
/** Number of editor replicas when not running in queue mode (defaults to 1) */
|
|
16
|
+
replicas?: number;
|
|
17
|
+
/**
|
|
18
|
+
* Redis-backed queue mode. Adds a Redis master/replica set and a worker
|
|
19
|
+
* Deployment so webhook ingestion and heavy executions scale independently.
|
|
20
|
+
*/
|
|
21
|
+
queueMode?: boolean;
|
|
22
|
+
/** Queue worker replicas (defaults to 2, only used with queueMode) */
|
|
23
|
+
workers?: number;
|
|
24
|
+
/** Storage request for the CNPG Postgres cluster (defaults to '10Gi') */
|
|
25
|
+
storage?: string;
|
|
26
|
+
/**
|
|
27
|
+
* Persistent volume for `/home/node/.n8n` (workflow-local state).
|
|
28
|
+
* Pass a size string ('5Gi', the facit default) or an object for more
|
|
29
|
+
* control. When set: a RWO PVC is created, an initContainer fixes
|
|
30
|
+
* ownership (1000:1000), the rollout strategy becomes Recreate, and
|
|
31
|
+
* binary data is stored on the filesystem instead of Postgres.
|
|
32
|
+
*/
|
|
33
|
+
dataStorage?: string | {
|
|
34
|
+
size?: string;
|
|
35
|
+
storageClass?: string;
|
|
36
|
+
};
|
|
37
|
+
/**
|
|
38
|
+
* CNPG backup configuration passed through to the Database recipe
|
|
39
|
+
* (continuous WAL archiving + scheduled base backups).
|
|
40
|
+
*/
|
|
41
|
+
backup?: DatabaseProps['backup'];
|
|
42
|
+
/**
|
|
43
|
+
* Number of trusted proxy hops for client IP / rate limiting
|
|
44
|
+
* (N8N_PROXY_HOPS). Defaults to 1 — trust the ingress one hop.
|
|
45
|
+
*/
|
|
46
|
+
proxyHops?: number;
|
|
47
|
+
/**
|
|
48
|
+
* Execution-data pruning (EXECUTIONS_DATA_PRUNE /
|
|
49
|
+
* EXECUTIONS_DATA_MAX_AGE). Defaults to pruning data older than
|
|
50
|
+
* 168 h (7 days). Set to false to disable.
|
|
51
|
+
*/
|
|
52
|
+
pruning?: {
|
|
53
|
+
maxAgeHours?: number;
|
|
54
|
+
} | false;
|
|
55
|
+
/**
|
|
56
|
+
* Opt-in value for NODE_FUNCTION_ALLOW_BUILTIN (e.g. '*'). Deliberately
|
|
57
|
+
* unset by default: granting workflows access to every builtin Node.js
|
|
58
|
+
* module is a security-sensitive choice.
|
|
59
|
+
*/
|
|
60
|
+
allowBuiltinNodeFunctions?: string;
|
|
61
|
+
/**
|
|
62
|
+
* Tune the backend-provisioned encryption secret: vault path, key name,
|
|
63
|
+
* and refresh/restart behaviour (default: refreshAfter '1h', restarts
|
|
64
|
+
* the editor + workers on rotation).
|
|
65
|
+
*/
|
|
66
|
+
encryptionSecret?: {
|
|
67
|
+
path?: string;
|
|
68
|
+
key?: string;
|
|
69
|
+
refreshAfter?: string;
|
|
70
|
+
rolloutRestartTargets?: string[];
|
|
71
|
+
};
|
|
72
|
+
/** Extra annotations merged onto the Endpoint's IngressRoute/Ingress */
|
|
73
|
+
endpointAnnotations?: Record<string, string>;
|
|
74
|
+
/**
|
|
75
|
+
* Name of an existing Secret containing key `encryptionKey`. n8n
|
|
76
|
+
* encrypts all workflow credentials with this key — lose it and every
|
|
77
|
+
* stored credential is unreadable.
|
|
78
|
+
*
|
|
79
|
+
* Required unless a secrets backend (openbao/vault) is configured on
|
|
80
|
+
* the surrounding Platform — the backend then provisions the key
|
|
81
|
+
* automatically. Plaintext keys are not supported.
|
|
82
|
+
*/
|
|
83
|
+
encryptionKeySecretName?: string;
|
|
84
|
+
/** Requested editor resources */
|
|
85
|
+
resources?: {
|
|
86
|
+
requests?: {
|
|
87
|
+
cpu?: string;
|
|
88
|
+
memory?: string;
|
|
89
|
+
};
|
|
90
|
+
limits?: {
|
|
91
|
+
cpu?: string;
|
|
92
|
+
memory?: string;
|
|
93
|
+
};
|
|
94
|
+
};
|
|
95
|
+
/** TLS configuration (defaults to letsencrypt-prod cluster issuer) */
|
|
96
|
+
tls?: {
|
|
97
|
+
secretName: string;
|
|
98
|
+
clusterIssuer: string;
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* n8n — fair-code workflow automation.
|
|
103
|
+
*
|
|
104
|
+
* @title N8n
|
|
105
|
+
* @category Automation
|
|
106
|
+
*
|
|
107
|
+
* Composes:
|
|
108
|
+
* - CNPG Postgres cluster (workflows, executions, stored credentials)
|
|
109
|
+
* - n8n editor Deployment + Service + Endpoint (webhooks share the host)
|
|
110
|
+
* - Redis cluster + worker Deployment in queue mode
|
|
111
|
+
* - Encryption key provisioned through the Platform secrets backend
|
|
112
|
+
* (openbao / vault), or referenced from an existing Secret
|
|
113
|
+
*
|
|
114
|
+
* Wrap the component in `<Platform secrets={{ backend: 'openbao' }}>` and
|
|
115
|
+
* the N8N_ENCRYPTION_KEY is provisioned for you. Without a backend you
|
|
116
|
+
* must point `encryptionKeySecretName` at a pre-created Secret.
|
|
117
|
+
*
|
|
118
|
+
* @example
|
|
119
|
+
* import { Platform } from '@r8s/recipes'
|
|
120
|
+
* import { N8n } from '@r8s/n8n'
|
|
121
|
+
*
|
|
122
|
+
* export default (
|
|
123
|
+
* <Platform secrets={{ backend: 'openbao', mount: 'kv', path: 'apps' }}>
|
|
124
|
+
* <N8n name="n8n" host="n8n.example.com" queueMode workers={3} />
|
|
125
|
+
* </Platform>
|
|
126
|
+
* )
|
|
127
|
+
*/
|
|
128
|
+
export declare function N8n(props: N8nProps): import("@r8s/core").r8sElement;
|
|
129
|
+
//# 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;AAGrB,MAAM,WAAW,QAAQ;IACvB,wCAAwC;IACxC,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,0EAA0E;IAC1E,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB;;;;OAIG;IACH,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,6DAA6D;IAC7D,IAAI,EAAE,MAAM,CAAA;IACZ,+EAA+E;IAC/E,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB;;;OAGG;IACH,SAAS,CAAC,EAAE,OAAO,CAAA;IACnB,sEAAsE;IACtE,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,yEAAyE;IACzE,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB;;;;;;OAMG;IACH,WAAW,CAAC,EAAE,MAAM,GAAG;QAAE,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,YAAY,CAAC,EAAE,MAAM,CAAA;KAAE,CAAA;IAC/D;;;OAGG;IACH,MAAM,CAAC,EAAE,aAAa,CAAC,QAAQ,CAAC,CAAA;IAChC;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB;;;;OAIG;IACH,OAAO,CAAC,EAAE;QAAE,WAAW,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,KAAK,CAAA;IAC1C;;;;OAIG;IACH,yBAAyB,CAAC,EAAE,MAAM,CAAA;IAClC;;;;OAIG;IACH,gBAAgB,CAAC,EAAE;QACjB,IAAI,CAAC,EAAE,MAAM,CAAA;QACb,GAAG,CAAC,EAAE,MAAM,CAAA;QACZ,YAAY,CAAC,EAAE,MAAM,CAAA;QACrB,qBAAqB,CAAC,EAAE,MAAM,EAAE,CAAA;KACjC,CAAA;IACD,wEAAwE;IACxE,mBAAmB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAC5C;;;;;;;;OAQG;IACH,uBAAuB,CAAC,EAAE,MAAM,CAAA;IAChC,iCAAiC;IACjC,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,sEAAsE;IACtE,GAAG,CAAC,EAAE;QACJ,UAAU,EAAE,MAAM,CAAA;QAClB,aAAa,EAAE,MAAM,CAAA;KACtB,CAAA;CACF;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,wBAAgB,GAAG,CAAC,KAAK,EAAE,QAAQ,kCAkQlC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.N8n = N8n;
|
|
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
|
+
const redis_1 = require("@r8s/crds/redis");
|
|
9
|
+
/**
|
|
10
|
+
* n8n — fair-code workflow automation.
|
|
11
|
+
*
|
|
12
|
+
* @title N8n
|
|
13
|
+
* @category Automation
|
|
14
|
+
*
|
|
15
|
+
* Composes:
|
|
16
|
+
* - CNPG Postgres cluster (workflows, executions, stored credentials)
|
|
17
|
+
* - n8n editor Deployment + Service + Endpoint (webhooks share the host)
|
|
18
|
+
* - Redis cluster + worker Deployment in queue mode
|
|
19
|
+
* - Encryption key provisioned through the Platform secrets backend
|
|
20
|
+
* (openbao / vault), or referenced from an existing Secret
|
|
21
|
+
*
|
|
22
|
+
* Wrap the component in `<Platform secrets={{ backend: 'openbao' }}>` and
|
|
23
|
+
* the N8N_ENCRYPTION_KEY is provisioned for you. Without a backend you
|
|
24
|
+
* must point `encryptionKeySecretName` at a pre-created Secret.
|
|
25
|
+
*
|
|
26
|
+
* @example
|
|
27
|
+
* import { Platform } from '@r8s/recipes'
|
|
28
|
+
* import { N8n } from '@r8s/n8n'
|
|
29
|
+
*
|
|
30
|
+
* export default (
|
|
31
|
+
* <Platform secrets={{ backend: 'openbao', mount: 'kv', path: 'apps' }}>
|
|
32
|
+
* <N8n name="n8n" host="n8n.example.com" queueMode workers={3} />
|
|
33
|
+
* </Platform>
|
|
34
|
+
* )
|
|
35
|
+
*/
|
|
36
|
+
function N8n(props) {
|
|
37
|
+
const { name = 'n8n', namespace: namespaceProp, version = '2.35.5', host, replicas = 1, queueMode = false, workers = 2, storage = '10Gi', dataStorage, backup, proxyHops = 1, pruning = { maxAgeHours: 168 }, allowBuiltinNodeFunctions, encryptionSecret, endpointAnnotations = {}, encryptionKeySecretName, resources = {
|
|
38
|
+
requests: { memory: '512Mi', cpu: '100m' },
|
|
39
|
+
limits: { memory: '1Gi', cpu: '1000m' },
|
|
40
|
+
}, tls = { secretName: `${name}-tls`, clusterIssuer: 'letsencrypt-prod' }, } = props;
|
|
41
|
+
const sharedOperators = (0, recipes_1.useOperators)();
|
|
42
|
+
const secretProvider = (0, core_1.useContext)(defaults_1.SecretContext);
|
|
43
|
+
const namespace = (0, defaults_1.useNamespace)(namespaceProp);
|
|
44
|
+
const resources_ = [];
|
|
45
|
+
const dbCredentialsName = `${name}-db-credentials`;
|
|
46
|
+
const encryptionSecretName = encryptionKeySecretName ?? `${name}-encryption-key`;
|
|
47
|
+
const encryptionKeyDestKey = encryptionSecret?.key ?? 'encryptionKey';
|
|
48
|
+
// --- Secret provisioning -------------------------------------------------
|
|
49
|
+
// The encryption key is the crown jewel of an n8n install — never render
|
|
50
|
+
// it as plaintext. With a secrets backend it is provisioned through the
|
|
51
|
+
// backend (key `encryptionKey`); otherwise reference a pre-created Secret.
|
|
52
|
+
if (!encryptionKeySecretName) {
|
|
53
|
+
if (!(0, recipes_1.canProvisionSecrets)(secretProvider)) {
|
|
54
|
+
throw (0, recipes_1.secretsRequiredError)('N8n', name, 'an encryption key — n8n encrypts all workflow credentials with N8N_ENCRYPTION_KEY', {
|
|
55
|
+
propName: 'encryptionKeySecretName',
|
|
56
|
+
exampleValue: `${name}-encryption-key`,
|
|
57
|
+
keys: ['encryptionKey'],
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
resources_.push((0, core_1.jsx)(recipes_1.StaticSecret, {
|
|
61
|
+
name: `${name}-encryption`,
|
|
62
|
+
namespace,
|
|
63
|
+
path: encryptionSecret?.path ?? `${secretProvider.path ?? name}/${name}/encryption`,
|
|
64
|
+
secretName: encryptionSecretName,
|
|
65
|
+
refreshAfter: encryptionSecret?.refreshAfter,
|
|
66
|
+
keys: { [encryptionKeyDestKey]: encryptionKeyDestKey },
|
|
67
|
+
// Rotate-in-place: restart pods on key changes so editors re-read
|
|
68
|
+
// the key (stale keys break credential decryption silently)
|
|
69
|
+
restart: encryptionSecret?.rolloutRestartTargets ?? [
|
|
70
|
+
{ kind: 'Deployment', name },
|
|
71
|
+
...(queueMode ? [{ kind: 'Deployment', name: `${name}-worker` }] : []),
|
|
72
|
+
],
|
|
73
|
+
}));
|
|
74
|
+
}
|
|
75
|
+
// --- Operators ------------------------------------------------------------
|
|
76
|
+
if (queueMode) {
|
|
77
|
+
resources_.push(...(0, recipes_1.maybeOperator)('redis-operator', sharedOperators));
|
|
78
|
+
}
|
|
79
|
+
// --- Database (CNPG) — workflows, executions, stored credentials ----------
|
|
80
|
+
const dbHost = `${name}-rw`;
|
|
81
|
+
const dataVolumeName = `${name}-data`;
|
|
82
|
+
const dataVolumeDef = dataStorage
|
|
83
|
+
? [{ name: dataVolumeName, persistentVolumeClaim: { claimName: dataVolumeName } }]
|
|
84
|
+
: [];
|
|
85
|
+
const dataInitContainer = {
|
|
86
|
+
name: 'fix-data-permissions',
|
|
87
|
+
image: 'busybox:1.36',
|
|
88
|
+
command: ['sh', '-c', 'chown -R 1000:1000 /data'],
|
|
89
|
+
volumeMounts: [{ name: dataVolumeName, mountPath: '/data' }],
|
|
90
|
+
};
|
|
91
|
+
if (dataStorage) {
|
|
92
|
+
const size = typeof dataStorage === 'string' ? dataStorage : (dataStorage.size ?? '5Gi');
|
|
93
|
+
const storageClass = typeof dataStorage === 'object' ? dataStorage.storageClass : undefined;
|
|
94
|
+
resources_.push((0, core_1.jsx)('PersistentVolumeClaim', {
|
|
95
|
+
apiVersion: 'v1',
|
|
96
|
+
kind: 'PersistentVolumeClaim',
|
|
97
|
+
metadata: { name: dataVolumeName, namespace },
|
|
98
|
+
spec: {
|
|
99
|
+
accessModes: ['ReadWriteOnce'],
|
|
100
|
+
...(storageClass ? { storageClassName: storageClass } : {}),
|
|
101
|
+
resources: { requests: { storage: size } },
|
|
102
|
+
},
|
|
103
|
+
}));
|
|
104
|
+
}
|
|
105
|
+
const sharedEnv = {
|
|
106
|
+
DB_TYPE: 'postgresdb',
|
|
107
|
+
DB_POSTGRESDB_HOST: dbHost,
|
|
108
|
+
DB_POSTGRESDB_PORT: '5432',
|
|
109
|
+
DB_POSTGRESDB_DATABASE: name,
|
|
110
|
+
DB_POSTGRESDB_USER: name,
|
|
111
|
+
N8N_HOST: host,
|
|
112
|
+
N8N_PORT: '5678',
|
|
113
|
+
N8N_PROTOCOL: 'https',
|
|
114
|
+
WEBHOOK_URL: `https://${host}/`,
|
|
115
|
+
// Trust the ingress (Endpoint/TLS) for client IPs/rate limiting
|
|
116
|
+
N8N_PROXY_HOPS: String(proxyHops),
|
|
117
|
+
// Binary data in Postgres for queue mode (shared across editor+workers);
|
|
118
|
+
// filesystem when a data volume exists; Postgres otherwise (no volume
|
|
119
|
+
// means /home/node/.n8n is ephemeral)
|
|
120
|
+
N8N_DEFAULT_BINARY_DATA_MODE: queueMode ? 'default' : dataStorage ? 'filesystem' : 'default',
|
|
121
|
+
...(pruning
|
|
122
|
+
? {
|
|
123
|
+
EXECUTIONS_DATA_PRUNE: 'true',
|
|
124
|
+
EXECUTIONS_DATA_MAX_AGE: String(pruning.maxAgeHours ?? 168),
|
|
125
|
+
}
|
|
126
|
+
: {}),
|
|
127
|
+
...(allowBuiltinNodeFunctions
|
|
128
|
+
? { NODE_FUNCTION_ALLOW_BUILTIN: allowBuiltinNodeFunctions }
|
|
129
|
+
: {}),
|
|
130
|
+
};
|
|
131
|
+
const sharedSecrets = {
|
|
132
|
+
DB_POSTGRESDB_PASSWORD: { secret: dbCredentialsName, key: 'password' },
|
|
133
|
+
N8N_ENCRYPTION_KEY: { secret: encryptionSecretName, key: encryptionKeyDestKey },
|
|
134
|
+
};
|
|
135
|
+
if (queueMode) {
|
|
136
|
+
Object.assign(sharedEnv, {
|
|
137
|
+
EXECUTIONS_MODE: 'queue',
|
|
138
|
+
QUEUE_BULL_REDIS_HOST: `${name}-redis`,
|
|
139
|
+
QUEUE_BULL_REDIS_PORT: '6379',
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
if (!queueMode && replicas > 1) {
|
|
143
|
+
throw new Error(`N8n "${name}" requested replicas={${replicas}} without queue mode.\n` +
|
|
144
|
+
`\n` +
|
|
145
|
+
`Multiple main instances without Redis queue mode run every trigger ` +
|
|
146
|
+
`execution once per replica (duplicated webhooks and schedules).\n` +
|
|
147
|
+
`\n` +
|
|
148
|
+
`Fix: enable queue mode:\n` +
|
|
149
|
+
` <N8n name="${name}" host="${host}" queueMode workers={${replicas}} />\n` +
|
|
150
|
+
`\n` +
|
|
151
|
+
`Or keep a single main instance (workers handle executions in queue mode).`);
|
|
152
|
+
}
|
|
153
|
+
// Database wraps the editor so credentials stay consistent with the
|
|
154
|
+
// r8s Database recipe (CNPG dedicated cluster provisions the secret).
|
|
155
|
+
resources_.push((0, core_1.jsx)(recipes_1.Database, {
|
|
156
|
+
name,
|
|
157
|
+
namespace,
|
|
158
|
+
storage,
|
|
159
|
+
...(backup ? { backup } : {}),
|
|
160
|
+
children: ((0, jsx_runtime_1.jsx)(recipes_1.WebService, { name: name, namespace: namespace, image: `docker.n8n.io/n8nio/n8n:${version}`,
|
|
161
|
+
// Pinned tags are immutable — no need to re-pull on every start
|
|
162
|
+
imagePullPolicy: version === 'latest' ? 'Always' : 'IfNotPresent', port: 5678, replicas: queueMode ? 1 : replicas, resources: resources,
|
|
163
|
+
// RWO volume + in-place DB migrations on boot roll → Recreate
|
|
164
|
+
strategy: dataStorage ? 'Recreate' : undefined, volumes: dataVolumeDef.length > 0 ? dataVolumeDef : undefined, volumeMounts: dataVolumeDef.length > 0
|
|
165
|
+
? [{ name: dataVolumeName, mountPath: '/home/node/.n8n' }]
|
|
166
|
+
: undefined, initContainers: dataVolumeDef.length > 0 ? [dataInitContainer] : undefined, probes: {
|
|
167
|
+
// n8n boots slowly after code upgrades (DB migrations); facit
|
|
168
|
+
// probe timing keeps the pod alive through them
|
|
169
|
+
liveness: { path: '/healthz', initialDelaySeconds: 60, periodSeconds: 30 },
|
|
170
|
+
readiness: { path: '/healthz', initialDelaySeconds: 15, failureThreshold: 6 },
|
|
171
|
+
}, env: { ...sharedEnv, ...(queueMode ? {} : { N8N_CONCURRENCY_PRODUCTION_LIMIT: '10' }) }, secrets: sharedSecrets })),
|
|
172
|
+
}));
|
|
173
|
+
// --- Queue mode: Redis + workers ------------------------------------------
|
|
174
|
+
if (queueMode) {
|
|
175
|
+
// Bull (n8n's queue backend) speaks plain Redis protocol — use a
|
|
176
|
+
// master/replica topology, not a Redis cluster (no MOVED-safe client)
|
|
177
|
+
resources_.push((0, redis_1.RedisReplicationComponent)({
|
|
178
|
+
metadata: { name: `${name}-redis`, namespace },
|
|
179
|
+
spec: {
|
|
180
|
+
clusterSize: 3,
|
|
181
|
+
kubernetesConfig: { image: 'redis:7.2-alpine' },
|
|
182
|
+
},
|
|
183
|
+
}));
|
|
184
|
+
resources_.push((0, jsx_runtime_1.jsx)(recipes_1.WebService, { name: `${name}-worker`, namespace: namespace, image: `docker.n8n.io/n8nio/n8n:${version}`, imagePullPolicy: version === 'latest' ? 'Always' : 'IfNotPresent', port: 5678, replicas: workers, command: ['n8n', 'worker', '--concurrency=10'], resources: {
|
|
185
|
+
requests: { memory: '512Mi', cpu: '250m' },
|
|
186
|
+
limits: { memory: '2Gi', cpu: '2000m' },
|
|
187
|
+
}, probes: {
|
|
188
|
+
liveness: { path: '/healthz' },
|
|
189
|
+
readiness: { path: '/healthz', initialDelaySeconds: 20 },
|
|
190
|
+
}, env: { ...sharedEnv, QUEUE_HEALTH_CHECK_ACTIVE: 'true' }, secrets: sharedSecrets }));
|
|
191
|
+
}
|
|
192
|
+
// --- Endpoint — webhooks share the editor host -----------------------------
|
|
193
|
+
resources_.push((0, jsx_runtime_1.jsx)(recipes_1.Endpoint, { name: `${name}-endpoint`, namespace: namespace, host: host, serviceName: name, servicePort: 5678, tls: tls, annotations: {
|
|
194
|
+
// Long-lived webhook/queue connections (editor SSE, waiting webhooks)
|
|
195
|
+
// die at the default 60 s proxy timeout — facit sets one hour
|
|
196
|
+
'nginx.ingress.kubernetes.io/proxy-read-timeout': '3600',
|
|
197
|
+
'nginx.ingress.kubernetes.io/proxy-send-timeout': '3600',
|
|
198
|
+
...endpointAnnotations,
|
|
199
|
+
} }));
|
|
200
|
+
return (0, core_1.jsx)(core_1.Fragment, { children: resources_ });
|
|
201
|
+
}
|
|
202
|
+
//# 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/n8n",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "n8n workflow automation — editor, Postgres persistence, Redis queue mode, webhook endpoints",
|
|
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
|
}
|
package/__tests__/n8n.test.ts
DELETED
|
@@ -1,267 +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
|
-
// N8n 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 { N8n } from '../src/index'
|
|
13
|
-
|
|
14
|
-
/** Render N8n inside a Platform-like secrets backend (OpenBao). */
|
|
15
|
-
function renderN8n(props: Record<string, unknown>): ReturnType<typeof render> {
|
|
16
|
-
return render(
|
|
17
|
-
jsx(SecretContext.Provider, {
|
|
18
|
-
value: { backend: 'openbao', mount: 'kv', path: 'test' },
|
|
19
|
-
children: jsx(N8n, props as never),
|
|
20
|
-
})
|
|
21
|
-
)
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
/** Render N8n wrapped only in an OperatorContext (no secrets backend). */
|
|
25
|
-
function renderN8nWithContext(operators_: any[], props: Record<string, unknown>): r8sElement {
|
|
26
|
-
return jsx(OperatorContext.Provider, {
|
|
27
|
-
value: operators_,
|
|
28
|
-
children: jsx(N8n, props as never),
|
|
29
|
-
})
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
const openbao = { backend: 'openbao', mount: 'kv', path: 'test' }
|
|
33
|
-
|
|
34
|
-
describe('operator declarations', () => {
|
|
35
|
-
it('declares the redis operator when queue mode is enabled', () => {
|
|
36
|
-
const result = renderN8n({
|
|
37
|
-
host: 'n8n.example.com',
|
|
38
|
-
queueMode: true,
|
|
39
|
-
encryptionKeySecretName: 'enc',
|
|
40
|
-
})
|
|
41
|
-
expect(result.operators.some((op) => op.name === 'redis-operator')).toBe(true)
|
|
42
|
-
})
|
|
43
|
-
|
|
44
|
-
it('does not declare the redis operator without queue mode', () => {
|
|
45
|
-
const result = renderN8n({ host: 'n8n.example.com' })
|
|
46
|
-
expect(result.operators.some((op) => op.name === 'redis-operator')).toBe(false)
|
|
47
|
-
})
|
|
48
|
-
|
|
49
|
-
it('declares the cnpg operator via the Database recipe', () => {
|
|
50
|
-
const result = renderN8n({ host: 'n8n.example.com' })
|
|
51
|
-
expect(result.operators.some((op) => op.name === 'cnpg')).toBe(true)
|
|
52
|
-
})
|
|
53
|
-
|
|
54
|
-
it('deduplicates operators provided via context', () => {
|
|
55
|
-
const result = render(
|
|
56
|
-
renderN8nWithContext([operators['redis-operator'](), operators['cnpg']()], {
|
|
57
|
-
host: 'n8n.example.com',
|
|
58
|
-
encryptionKeySecretName: 'existing-encryption',
|
|
59
|
-
})
|
|
60
|
-
)
|
|
61
|
-
const names = result.operators.map((op) => op.name)
|
|
62
|
-
expect(names.filter((n) => n === 'redis-operator')).toHaveLength(1)
|
|
63
|
-
expect(names.filter((n) => n === 'cnpg')).toHaveLength(1)
|
|
64
|
-
})
|
|
65
|
-
|
|
66
|
-
it('allows version overrides through context operators', () => {
|
|
67
|
-
const result = render(
|
|
68
|
-
renderN8nWithContext([operators['redis-operator']('1.0.0')], {
|
|
69
|
-
host: 'n8n.example.com',
|
|
70
|
-
queueMode: true,
|
|
71
|
-
encryptionKeySecretName: 'existing-encryption',
|
|
72
|
-
})
|
|
73
|
-
)
|
|
74
|
-
const redis = result.operators.filter((op) => op.name === 'redis-operator')
|
|
75
|
-
expect(redis).toHaveLength(1)
|
|
76
|
-
expect(redis[0].version).toBe('1.0.0')
|
|
77
|
-
})
|
|
78
|
-
})
|
|
79
|
-
|
|
80
|
-
describe('rendering defaults', () => {
|
|
81
|
-
it('renders editor deployment, service, database and endpoint', () => {
|
|
82
|
-
const result = renderN8n({ host: 'n8n.example.com' })
|
|
83
|
-
const kinds = result.resources.map((r) => r.kind)
|
|
84
|
-
expect(kinds).toContain('Deployment')
|
|
85
|
-
expect(kinds).toContain('Service')
|
|
86
|
-
expect(kinds).toContain('Ingress')
|
|
87
|
-
expect(kinds).toContain('Cluster')
|
|
88
|
-
})
|
|
89
|
-
|
|
90
|
-
it('renders queue mode with Redis and workers', () => {
|
|
91
|
-
const result = renderN8n({ host: 'n8n.example.com', queueMode: true, workers: 3 })
|
|
92
|
-
const kinds = result.resources.map((r) => r.kind)
|
|
93
|
-
expect(kinds).toContain('RedisReplication')
|
|
94
|
-
const deployments = result.resources.filter((r) => r.kind === 'Deployment')
|
|
95
|
-
expect(deployments.map((d: any) => d.metadata.name)).toContain('n8n-worker')
|
|
96
|
-
expect(deployments.map((d: any) => d.metadata.name)).toContain('n8n')
|
|
97
|
-
})
|
|
98
|
-
|
|
99
|
-
it('wires the worker to the RedisReplication service and health checks', () => {
|
|
100
|
-
const result = renderN8n({ host: 'n8n.example.com', queueMode: true })
|
|
101
|
-
const worker = result.resources.find(
|
|
102
|
-
(r: any) => r.kind === 'Deployment' && r.metadata.name === 'n8n-worker'
|
|
103
|
-
) as any
|
|
104
|
-
const env = worker.spec.template.spec.containers[0].env
|
|
105
|
-
expect(env.find((e: any) => e.name === 'QUEUE_BULL_REDIS_HOST').value).toBe('n8n-redis')
|
|
106
|
-
expect(env.find((e: any) => e.name === 'QUEUE_HEALTH_CHECK_ACTIVE').value).toBe('true')
|
|
107
|
-
const probes = worker.spec.template.spec.containers[0]
|
|
108
|
-
expect(probes.livenessProbe.httpGet.path).toBe('/healthz')
|
|
109
|
-
})
|
|
110
|
-
|
|
111
|
-
it('throws on multiple editor replicas without queue mode', () => {
|
|
112
|
-
expect(() => renderN8n({ host: 'n8n.example.com', replicas: 3 })).toThrow(/queue mode/)
|
|
113
|
-
})
|
|
114
|
-
|
|
115
|
-
it('propagates the storage prop to the CNPG cluster', () => {
|
|
116
|
-
const result = renderN8n({ host: 'n8n.example.com', storage: '30Gi' })
|
|
117
|
-
const cluster = result.resources.find((r: any) => r.kind === 'Cluster') as any
|
|
118
|
-
expect(cluster.spec.storage.size).toBe('30Gi')
|
|
119
|
-
})
|
|
120
|
-
|
|
121
|
-
it('renders gateway resources when platform uses gateway routing', () => {
|
|
122
|
-
const result = render(
|
|
123
|
-
jsx(RoutingContext.Provider, {
|
|
124
|
-
value: { mode: 'gateway', gatewayClassName: 'eg' },
|
|
125
|
-
children: jsx(SecretContext.Provider, {
|
|
126
|
-
value: openbao as never,
|
|
127
|
-
children: jsx(N8n, { host: 'n8n.example.com' }),
|
|
128
|
-
}),
|
|
129
|
-
})
|
|
130
|
-
)
|
|
131
|
-
const kinds = result.resources.map((r) => r.kind)
|
|
132
|
-
expect(kinds).toContain('HTTPRoute')
|
|
133
|
-
})
|
|
134
|
-
|
|
135
|
-
it('renders a valid Ingress when platform uses ingress routing', () => {
|
|
136
|
-
const result = render(
|
|
137
|
-
jsx(RoutingContext.Provider, {
|
|
138
|
-
value: { mode: 'ingress' },
|
|
139
|
-
children: jsx(SecretContext.Provider, {
|
|
140
|
-
value: openbao as never,
|
|
141
|
-
children: jsx(N8n, { host: 'n8n.example.com' }),
|
|
142
|
-
}),
|
|
143
|
-
})
|
|
144
|
-
)
|
|
145
|
-
const ingress = result.resources.find((r) => r.kind === 'Ingress') as any
|
|
146
|
-
expect(ingress).toBeDefined()
|
|
147
|
-
expect(ingress.spec.rules[0].host).toBe('n8n.example.com')
|
|
148
|
-
})
|
|
149
|
-
|
|
150
|
-
it('passes resource validation', () => {
|
|
151
|
-
const result = renderN8n({ host: 'n8n.example.com', queueMode: true })
|
|
152
|
-
for (const resource of result.resources) {
|
|
153
|
-
expect(validateResource(resource)).toEqual([])
|
|
154
|
-
}
|
|
155
|
-
})
|
|
156
|
-
})
|
|
157
|
-
|
|
158
|
-
describe('rendering with all props', () => {
|
|
159
|
-
it('accepts the full prop surface', () => {
|
|
160
|
-
const result = renderN8n({
|
|
161
|
-
name: 'automation',
|
|
162
|
-
namespace: 'automation',
|
|
163
|
-
version: '1.60.0',
|
|
164
|
-
host: 'automation.example.com',
|
|
165
|
-
replicas: 2,
|
|
166
|
-
queueMode: true,
|
|
167
|
-
workers: 4,
|
|
168
|
-
resources: {
|
|
169
|
-
requests: { memory: '1Gi', cpu: '500m' },
|
|
170
|
-
limits: { memory: '4Gi', cpu: '2000m' },
|
|
171
|
-
},
|
|
172
|
-
tls: { secretName: 'automation-tls', clusterIssuer: 'letsencrypt-prod' },
|
|
173
|
-
})
|
|
174
|
-
expect(result.resources.length).toBeGreaterThan(0)
|
|
175
|
-
const editor = result.resources.find(
|
|
176
|
-
(r: any) => r.kind === 'Deployment' && r.metadata.name === 'automation'
|
|
177
|
-
) as any
|
|
178
|
-
expect(editor.spec.template.spec.containers[0].image).toContain('1.60.0')
|
|
179
|
-
expect(editor.spec.template.spec.containers[0].resources.limits.memory).toBe('4Gi')
|
|
180
|
-
})
|
|
181
|
-
})
|
|
182
|
-
|
|
183
|
-
describe('secrets handling', () => {
|
|
184
|
-
it('accepts an explicit encryptionKeySecretName without a backend', () => {
|
|
185
|
-
expect(() =>
|
|
186
|
-
render(jsx(N8n, { host: 'n8n.example.com', encryptionKeySecretName: 'existing-encryption' }))
|
|
187
|
-
).not.toThrow()
|
|
188
|
-
})
|
|
189
|
-
|
|
190
|
-
it('provisions the encryption key through a secrets backend', () => {
|
|
191
|
-
const result = renderN8n({ host: 'n8n.example.com' })
|
|
192
|
-
const kinds = result.resources.map((r) => r.kind)
|
|
193
|
-
expect(kinds).toContain('OpenBaoStaticSecret')
|
|
194
|
-
})
|
|
195
|
-
|
|
196
|
-
it('provisions the encryption key through Vault', () => {
|
|
197
|
-
const result = render(
|
|
198
|
-
jsx(SecretContext.Provider, {
|
|
199
|
-
value: { backend: 'vault', mount: 'kv', path: 'apps' },
|
|
200
|
-
children: jsx(N8n, { host: 'n8n.example.com' }),
|
|
201
|
-
})
|
|
202
|
-
)
|
|
203
|
-
const kinds = result.resources.map((r) => r.kind)
|
|
204
|
-
expect(kinds).toContain('VaultStaticSecret')
|
|
205
|
-
})
|
|
206
|
-
|
|
207
|
-
it('wires credentials via secretKeyRef (never plaintext env)', () => {
|
|
208
|
-
const result = renderN8n({
|
|
209
|
-
host: 'n8n.example.com',
|
|
210
|
-
encryptionKeySecretName: 'existing-encryption',
|
|
211
|
-
})
|
|
212
|
-
const editor = result.resources.find(
|
|
213
|
-
(r: any) => r.kind === 'Deployment' && r.metadata.name === 'n8n'
|
|
214
|
-
) as any
|
|
215
|
-
const env = editor.spec.template.spec.containers[0].env
|
|
216
|
-
const encryption = env.find((e: any) => e.name === 'N8N_ENCRYPTION_KEY')
|
|
217
|
-
const dbPassword = env.find((e: any) => e.name === 'DB_POSTGRESDB_PASSWORD')
|
|
218
|
-
expect(encryption.valueFrom.secretKeyRef.name).toBe('existing-encryption')
|
|
219
|
-
expect(dbPassword.valueFrom.secretKeyRef.name).toBe('n8n-db-credentials')
|
|
220
|
-
expect(encryption.value).toBeUndefined()
|
|
221
|
-
expect(dbPassword.value).toBeUndefined()
|
|
222
|
-
})
|
|
223
|
-
|
|
224
|
-
it('renders no plaintext credentials anywhere', () => {
|
|
225
|
-
const result = renderN8n({ host: 'n8n.example.com', queueMode: true })
|
|
226
|
-
const { passed, errors } = runGuardrails(result.resources as any[], [noPlaintextSecrets])
|
|
227
|
-
if (!passed) {
|
|
228
|
-
console.error('Plaintext credential violations:', errors)
|
|
229
|
-
}
|
|
230
|
-
expect(passed).toBe(true)
|
|
231
|
-
})
|
|
232
|
-
})
|
|
233
|
-
|
|
234
|
-
describe('validation errors', () => {
|
|
235
|
-
it('throws when no secrets backend and no encryption key secret', () => {
|
|
236
|
-
expect(() => render(jsx(N8n, { host: 'n8n.example.com' }))).toThrow(/encryption key/)
|
|
237
|
-
})
|
|
238
|
-
|
|
239
|
-
it('throws for unknown secrets backends', () => {
|
|
240
|
-
expect(() =>
|
|
241
|
-
render(
|
|
242
|
-
jsx(SecretContext.Provider, {
|
|
243
|
-
value: { backend: 'unknown' as never },
|
|
244
|
-
children: jsx(N8n, { host: 'n8n.example.com' }),
|
|
245
|
-
})
|
|
246
|
-
)
|
|
247
|
-
).toThrow(/encryption key/)
|
|
248
|
-
})
|
|
249
|
-
})
|
|
250
|
-
|
|
251
|
-
describe('secretKeyRef wiring in queue mode', () => {
|
|
252
|
-
it('worker deployment uses the same credentials secret', () => {
|
|
253
|
-
const result = renderN8n({
|
|
254
|
-
host: 'n8n.example.com',
|
|
255
|
-
queueMode: true,
|
|
256
|
-
encryptionKeySecretName: 'existing-encryption',
|
|
257
|
-
})
|
|
258
|
-
const worker = result.resources.find(
|
|
259
|
-
(r: any) => r.kind === 'Deployment' && r.metadata.name === 'n8n-worker'
|
|
260
|
-
) as any
|
|
261
|
-
expect(worker).toBeDefined()
|
|
262
|
-
const env = worker.spec.template.spec.containers[0].env
|
|
263
|
-
const dbPassword = env.find((e: any) => e.name === 'DB_POSTGRESDB_PASSWORD')
|
|
264
|
-
expect(dbPassword.valueFrom.secretKeyRef.name).toBe('n8n-db-credentials')
|
|
265
|
-
expect(dbPassword.value).toBeUndefined()
|
|
266
|
-
})
|
|
267
|
-
})
|
package/examples/basic.tsx
DELETED
package/src/index.tsx
DELETED
|
@@ -1,284 +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
|
-
|
|
7
|
-
export interface N8nProps {
|
|
8
|
-
/** Resource name (defaults to 'n8n') */
|
|
9
|
-
name?: string
|
|
10
|
-
/** Kubernetes namespace (inherited from Platform context when omitted) */
|
|
11
|
-
namespace?: string
|
|
12
|
-
/** Container image tag (defaults to 'latest' — pin a version in production) */
|
|
13
|
-
version?: string
|
|
14
|
-
/** Public hostname for the editor and webhooks (required) */
|
|
15
|
-
host: string
|
|
16
|
-
/** Number of editor replicas when not running in queue mode (defaults to 1) */
|
|
17
|
-
replicas?: number
|
|
18
|
-
/**
|
|
19
|
-
* Redis-backed queue mode. Adds a Redis master/replica set and a worker
|
|
20
|
-
* Deployment so webhook ingestion and heavy executions scale independently.
|
|
21
|
-
*/
|
|
22
|
-
queueMode?: boolean
|
|
23
|
-
/** Queue worker replicas (defaults to 2, only used with queueMode) */
|
|
24
|
-
workers?: number
|
|
25
|
-
/** Storage request for the CNPG Postgres cluster (defaults to '10Gi') */
|
|
26
|
-
storage?: string
|
|
27
|
-
/**
|
|
28
|
-
* Name of an existing Secret containing key `encryptionKey`. n8n
|
|
29
|
-
* encrypts all workflow credentials with this key — lose it and every
|
|
30
|
-
* stored credential is unreadable.
|
|
31
|
-
*
|
|
32
|
-
* Required unless a secrets backend (openbao/vault) is configured on
|
|
33
|
-
* the surrounding Platform — the backend then provisions the key
|
|
34
|
-
* automatically. Plaintext keys are not supported.
|
|
35
|
-
*/
|
|
36
|
-
encryptionKeySecretName?: string
|
|
37
|
-
/** Requested editor resources */
|
|
38
|
-
resources?: {
|
|
39
|
-
requests?: { cpu?: string; memory?: string }
|
|
40
|
-
limits?: { cpu?: string; memory?: string }
|
|
41
|
-
}
|
|
42
|
-
/** TLS configuration (defaults to letsencrypt-prod cluster issuer) */
|
|
43
|
-
tls?: {
|
|
44
|
-
secretName: string
|
|
45
|
-
clusterIssuer: string
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
const OPERATOR_REDIS = 'redis-operator'
|
|
50
|
-
|
|
51
|
-
/**
|
|
52
|
-
* n8n — fair-code workflow automation.
|
|
53
|
-
*
|
|
54
|
-
* @title N8n
|
|
55
|
-
* @category Automation
|
|
56
|
-
*
|
|
57
|
-
* Composes:
|
|
58
|
-
* - CNPG Postgres cluster (workflows, executions, stored credentials)
|
|
59
|
-
* - n8n editor Deployment + Service + Endpoint (webhooks share the host)
|
|
60
|
-
* - Redis cluster + worker Deployment in queue mode
|
|
61
|
-
* - Encryption key provisioned through the Platform secrets backend
|
|
62
|
-
* (openbao / vault), or referenced from an existing Secret
|
|
63
|
-
*
|
|
64
|
-
* Wrap the component in `<Platform secrets={{ backend: 'openbao' }}>` and
|
|
65
|
-
* the N8N_ENCRYPTION_KEY is provisioned for you. Without a backend you
|
|
66
|
-
* must point `encryptionKeySecretName` at a pre-created Secret.
|
|
67
|
-
*
|
|
68
|
-
* @example
|
|
69
|
-
* import { Platform } from '@r8s/recipes'
|
|
70
|
-
* import { N8n } from '@r8s/n8n'
|
|
71
|
-
*
|
|
72
|
-
* export default (
|
|
73
|
-
* <Platform secrets={{ backend: 'openbao', mount: 'kv', path: 'apps' }}>
|
|
74
|
-
* <N8n name="n8n" host="n8n.example.com" queueMode workers={3} />
|
|
75
|
-
* </Platform>
|
|
76
|
-
* )
|
|
77
|
-
*/
|
|
78
|
-
export function N8n(props: N8nProps) {
|
|
79
|
-
const {
|
|
80
|
-
name = 'n8n',
|
|
81
|
-
namespace: namespaceProp,
|
|
82
|
-
version = 'latest',
|
|
83
|
-
host,
|
|
84
|
-
replicas = 1,
|
|
85
|
-
queueMode = false,
|
|
86
|
-
workers = 2,
|
|
87
|
-
storage = '10Gi',
|
|
88
|
-
encryptionKeySecretName,
|
|
89
|
-
resources = {
|
|
90
|
-
requests: { memory: '512Mi', cpu: '250m' },
|
|
91
|
-
limits: { memory: '2Gi', cpu: '1000m' },
|
|
92
|
-
},
|
|
93
|
-
tls = { secretName: `${name}-tls`, clusterIssuer: 'letsencrypt-prod' },
|
|
94
|
-
} = props
|
|
95
|
-
|
|
96
|
-
const sharedOperators = useContext(OperatorContext)
|
|
97
|
-
const secretProvider = useContext(SecretContext)
|
|
98
|
-
// Inherit namespace from <Platform> context — mirrors recipes/database.tsx
|
|
99
|
-
const contextNamespace = useContext(Namespace)
|
|
100
|
-
const namespace =
|
|
101
|
-
namespaceProp ?? (contextNamespace !== 'default' ? contextNamespace : undefined) ?? 'default'
|
|
102
|
-
const resources_: ReturnType<typeof jsx>[] = []
|
|
103
|
-
|
|
104
|
-
const dbCredentialsName = `${name}-db-credentials`
|
|
105
|
-
const encryptionSecretName = encryptionKeySecretName ?? `${name}-encryption-key`
|
|
106
|
-
|
|
107
|
-
// --- Secret provisioning -------------------------------------------------
|
|
108
|
-
// The encryption key is the crown jewel of an n8n install — never render
|
|
109
|
-
// it as plaintext. With a secrets backend it is provisioned through the
|
|
110
|
-
// backend (key `encryptionKey`); otherwise reference a pre-created Secret.
|
|
111
|
-
if (!encryptionKeySecretName) {
|
|
112
|
-
if (
|
|
113
|
-
!secretProvider ||
|
|
114
|
-
(secretProvider.backend !== 'vault' && secretProvider.backend !== 'openbao')
|
|
115
|
-
) {
|
|
116
|
-
throw new Error(
|
|
117
|
-
`N8n "${name}" requires an encryption key.\n` +
|
|
118
|
-
`\n` +
|
|
119
|
-
`n8n encrypts all workflow credentials with N8N_ENCRYPTION_KEY — ` +
|
|
120
|
-
`it must not be rendered as plaintext.\n` +
|
|
121
|
-
`\n` +
|
|
122
|
-
`Fix: configure a secrets backend on the Platform:\n` +
|
|
123
|
-
` <Platform secrets={{ backend: 'openbao', mount: 'kv', path: 'n8n' }}>\n` +
|
|
124
|
-
` <N8n name="${name}" host="${host}" />\n` +
|
|
125
|
-
` </Platform>\n` +
|
|
126
|
-
`\n` +
|
|
127
|
-
`Or reference a pre-created Secret (key: encryptionKey):\n` +
|
|
128
|
-
` <N8n name="${name}" host="${host}" encryptionKeySecretName="${name}-encryption-key" />`
|
|
129
|
-
)
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
const spec = {
|
|
133
|
-
...(secretProvider.backend === 'vault'
|
|
134
|
-
? { vaultAuthRef: secretProvider.authRef }
|
|
135
|
-
: { openbaoAuthRef: secretProvider.authRef }),
|
|
136
|
-
mount: secretProvider.mount,
|
|
137
|
-
type: 'kv-v2' as const,
|
|
138
|
-
path: `${secretProvider.path ?? name}/${name}/encryption`,
|
|
139
|
-
destination: { create: true, name: encryptionSecretName },
|
|
140
|
-
}
|
|
141
|
-
resources_.push(
|
|
142
|
-
secretProvider.backend === 'vault'
|
|
143
|
-
? jsx('VaultStaticSecret', {
|
|
144
|
-
apiVersion: 'secrets.hashicorp.com/v1beta1',
|
|
145
|
-
kind: 'VaultStaticSecret',
|
|
146
|
-
metadata: { name: `${name}-encryption`, namespace },
|
|
147
|
-
spec,
|
|
148
|
-
})
|
|
149
|
-
: jsx('OpenBaoStaticSecret', {
|
|
150
|
-
apiVersion: 'secrets.openbao.org/v1beta1',
|
|
151
|
-
kind: 'OpenBaoStaticSecret',
|
|
152
|
-
metadata: { name: `${name}-encryption`, namespace },
|
|
153
|
-
spec,
|
|
154
|
-
})
|
|
155
|
-
)
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
// --- Operators ------------------------------------------------------------
|
|
159
|
-
if (queueMode && !sharedOperators.some((op) => op.name === OPERATOR_REDIS)) {
|
|
160
|
-
resources_.push(declareOperator(operators[OPERATOR_REDIS]()))
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
// --- Database (CNPG) — workflows, executions, stored credentials ----------
|
|
164
|
-
const dbHost = `${name}-rw`
|
|
165
|
-
|
|
166
|
-
const sharedEnv = {
|
|
167
|
-
DB_TYPE: 'postgresdb',
|
|
168
|
-
DB_POSTGRESDB_HOST: dbHost,
|
|
169
|
-
DB_POSTGRESDB_PORT: '5432',
|
|
170
|
-
DB_POSTGRESDB_DATABASE: name,
|
|
171
|
-
DB_POSTGRESDB_USER: name,
|
|
172
|
-
N8N_HOST: host,
|
|
173
|
-
N8N_PORT: '5678',
|
|
174
|
-
N8N_PROTOCOL: 'https',
|
|
175
|
-
WEBHOOK_URL: `https://${host}/`,
|
|
176
|
-
NODE_FUNCTION_ALLOW_BUILTIN: '*',
|
|
177
|
-
// Binary data must live in Postgres in queue mode — the filesystem
|
|
178
|
-
// backend is not shared across editors/workers and is unsupported.
|
|
179
|
-
N8N_DEFAULT_BINARY_DATA_MODE: 'default',
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
const sharedSecrets = {
|
|
183
|
-
DB_POSTGRESDB_PASSWORD: { secret: dbCredentialsName, key: 'password' },
|
|
184
|
-
N8N_ENCRYPTION_KEY: { secret: encryptionSecretName, key: 'encryptionKey' },
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
if (queueMode) {
|
|
188
|
-
Object.assign(sharedEnv, {
|
|
189
|
-
EXECUTIONS_MODE: 'queue',
|
|
190
|
-
QUEUE_BULL_REDIS_HOST: `${name}-redis`,
|
|
191
|
-
QUEUE_BULL_REDIS_PORT: '6379',
|
|
192
|
-
})
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
if (!queueMode && replicas > 1) {
|
|
196
|
-
throw new Error(
|
|
197
|
-
`N8n "${name}" requested replicas={${replicas}} without queue mode.\n` +
|
|
198
|
-
`\n` +
|
|
199
|
-
`Multiple main instances without Redis queue mode run every trigger ` +
|
|
200
|
-
`execution once per replica (duplicated webhooks and schedules).\n` +
|
|
201
|
-
`\n` +
|
|
202
|
-
`Fix: enable queue mode:\n` +
|
|
203
|
-
` <N8n name="${name}" host="${host}" queueMode workers={${replicas}} />\n` +
|
|
204
|
-
`\n` +
|
|
205
|
-
`Or keep a single main instance (workers handle executions in queue mode).`
|
|
206
|
-
)
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
// Database wraps the editor so credentials stay consistent with the
|
|
210
|
-
// r8s Database recipe (CNPG dedicated cluster provisions the secret).
|
|
211
|
-
resources_.push(
|
|
212
|
-
jsx(Database, {
|
|
213
|
-
name,
|
|
214
|
-
namespace,
|
|
215
|
-
storage,
|
|
216
|
-
children: (
|
|
217
|
-
<WebService
|
|
218
|
-
name={name}
|
|
219
|
-
namespace={namespace}
|
|
220
|
-
image={`docker.n8n.io/n8nio/n8n:${version}`}
|
|
221
|
-
port={5678}
|
|
222
|
-
replicas={queueMode ? 1 : replicas}
|
|
223
|
-
resources={resources}
|
|
224
|
-
probes={{
|
|
225
|
-
liveness: { path: '/healthz' },
|
|
226
|
-
readiness: { path: '/healthz', initialDelaySeconds: 15 },
|
|
227
|
-
}}
|
|
228
|
-
env={{ ...sharedEnv, ...(queueMode ? {} : { N8N_CONCURRENCY_PRODUCTION_LIMIT: '10' }) }}
|
|
229
|
-
secrets={sharedSecrets}
|
|
230
|
-
/>
|
|
231
|
-
),
|
|
232
|
-
})
|
|
233
|
-
)
|
|
234
|
-
|
|
235
|
-
// --- Queue mode: Redis + workers ------------------------------------------
|
|
236
|
-
if (queueMode) {
|
|
237
|
-
// Bull (n8n's queue backend) speaks plain Redis protocol — use a
|
|
238
|
-
// master/replica topology, not a Redis cluster (no MOVED-safe client)
|
|
239
|
-
resources_.push(
|
|
240
|
-
RedisReplicationComponent({
|
|
241
|
-
metadata: { name: `${name}-redis`, namespace },
|
|
242
|
-
spec: {
|
|
243
|
-
clusterSize: 3,
|
|
244
|
-
kubernetesConfig: { image: 'redis:7.2-alpine' },
|
|
245
|
-
},
|
|
246
|
-
})
|
|
247
|
-
)
|
|
248
|
-
|
|
249
|
-
resources_.push(
|
|
250
|
-
<WebService
|
|
251
|
-
name={`${name}-worker`}
|
|
252
|
-
namespace={namespace}
|
|
253
|
-
image={`docker.n8n.io/n8nio/n8n:${version}`}
|
|
254
|
-
port={5678}
|
|
255
|
-
replicas={workers}
|
|
256
|
-
command={['n8n', 'worker', '--concurrency=10']}
|
|
257
|
-
resources={{
|
|
258
|
-
requests: { memory: '512Mi', cpu: '250m' },
|
|
259
|
-
limits: { memory: '2Gi', cpu: '2000m' },
|
|
260
|
-
}}
|
|
261
|
-
probes={{
|
|
262
|
-
liveness: { path: '/healthz' },
|
|
263
|
-
readiness: { path: '/healthz', initialDelaySeconds: 20 },
|
|
264
|
-
}}
|
|
265
|
-
env={{ ...sharedEnv, QUEUE_HEALTH_CHECK_ACTIVE: 'true' }}
|
|
266
|
-
secrets={sharedSecrets}
|
|
267
|
-
/>
|
|
268
|
-
)
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
// --- Endpoint — webhooks share the editor host -----------------------------
|
|
272
|
-
resources_.push(
|
|
273
|
-
<Endpoint
|
|
274
|
-
name={`${name}-endpoint`}
|
|
275
|
-
namespace={namespace}
|
|
276
|
-
host={host}
|
|
277
|
-
serviceName={name}
|
|
278
|
-
servicePort={5678}
|
|
279
|
-
tls={tls}
|
|
280
|
-
/>
|
|
281
|
-
)
|
|
282
|
-
|
|
283
|
-
return jsx(Fragment, { children: resources_ })
|
|
284
|
-
}
|
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
|
-
}
|