fix(core): Validate task runner mode (#14376)

This commit is contained in:
Iván Ovejero
2025-04-10 13:35:13 +02:00
committed by GitHub
parent 8c417d7b1b
commit 52170f1bbc
5 changed files with 49 additions and 13 deletions

View File

@@ -22,7 +22,8 @@
], ],
"dependencies": { "dependencies": {
"@n8n/di": "workspace:*", "@n8n/di": "workspace:*",
"reflect-metadata": "catalog:" "reflect-metadata": "catalog:",
"zod": "catalog:"
}, },
"devDependencies": { "devDependencies": {
"@n8n/typescript-config": "workspace:*" "@n8n/typescript-config": "workspace:*"

View File

@@ -1,18 +1,21 @@
import { z } from 'zod';
import { Config, Env } from '../decorators'; import { Config, Env } from '../decorators';
/** const runnerModeSchema = z.enum(['internal', 'external']);
* Whether to enable task runners and how to run them
* - internal: Task runners are run as a child process and launched by n8n export type TaskRunnerMode = z.infer<typeof runnerModeSchema>;
* - external: Task runners are run as a separate program not launched by n8n
*/
export type TaskRunnerMode = 'internal' | 'external';
@Config @Config
export class TaskRunnersConfig { export class TaskRunnersConfig {
@Env('N8N_RUNNERS_ENABLED') @Env('N8N_RUNNERS_ENABLED')
enabled: boolean = false; enabled: boolean = false;
@Env('N8N_RUNNERS_MODE') /**
* Whether the task runner should run as a child process spawned by n8n (internal mode)
* or as a separate process launched outside n8n (external mode).
*/
@Env('N8N_RUNNERS_MODE', runnerModeSchema)
mode: TaskRunnerMode = 'internal'; mode: TaskRunnerMode = 'internal';
/** Endpoint which task runners connect to */ /** Endpoint which task runners connect to */

View File

@@ -1,6 +1,7 @@
import 'reflect-metadata'; import 'reflect-metadata';
import { Container, Service } from '@n8n/di'; import { Container, Service } from '@n8n/di';
import { readFileSync } from 'fs'; import { readFileSync } from 'fs';
import { z } from 'zod';
// eslint-disable-next-line @typescript-eslint/ban-types // eslint-disable-next-line @typescript-eslint/ban-types
type Class = Function; type Class = Function;
@@ -10,6 +11,7 @@ type PropertyType = number | boolean | string | Class;
interface PropertyMetadata { interface PropertyMetadata {
type: PropertyType; type: PropertyType;
envName?: string; envName?: string;
schema?: z.ZodType<unknown>;
} }
const globalMetadata = new Map<Class, Map<PropertyKey, PropertyMetadata>>(); const globalMetadata = new Map<Class, Map<PropertyKey, PropertyMetadata>>();
@@ -33,14 +35,22 @@ export const Config: ClassDecorator = (ConfigClass: Class) => {
throw new Error('Invalid config class: ' + ConfigClass.name); throw new Error('Invalid config class: ' + ConfigClass.name);
} }
for (const [key, { type, envName }] of classMetadata) { for (const [key, { type, envName, schema }] of classMetadata) {
if (typeof type === 'function' && globalMetadata.has(type)) { if (typeof type === 'function' && globalMetadata.has(type)) {
config[key] = Container.get(type as Constructable); config[key] = Container.get(type as Constructable);
} else if (envName) { } else if (envName) {
const value = readEnv(envName); const value = readEnv(envName);
if (value === undefined) continue; if (value === undefined) continue;
if (type === Number) { if (schema) {
const result = schema.safeParse(value);
if (result.error) {
console.warn(
`Invalid value for ${envName} - ${result.error.issues[0].message}. Falling back to default value.`,
);
continue;
}
} else if (type === Number) {
const parsed = Number(value); const parsed = Number(value);
if (isNaN(parsed)) { if (isNaN(parsed)) {
console.warn(`Invalid number value for ${envName}: ${value}`); console.warn(`Invalid number value for ${envName}: ${value}`);
@@ -84,18 +94,21 @@ export const Nested: PropertyDecorator = (target: object, key: PropertyKey) => {
}; };
export const Env = export const Env =
(envName: string): PropertyDecorator => (envName: string, schema?: PropertyMetadata['schema']): PropertyDecorator =>
(target: object, key: PropertyKey) => { (target: object, key: PropertyKey) => {
const ConfigClass = target.constructor; const ConfigClass = target.constructor;
const classMetadata = const classMetadata =
globalMetadata.get(ConfigClass) ?? new Map<PropertyKey, PropertyMetadata>(); globalMetadata.get(ConfigClass) ?? new Map<PropertyKey, PropertyMetadata>();
const type = Reflect.getMetadata('design:type', target, key) as PropertyType; const type = Reflect.getMetadata('design:type', target, key) as PropertyType;
if (type === Object) { const isEnum = schema instanceof z.ZodEnum;
if (type === Object && !isEnum) {
// eslint-disable-next-line n8n-local-rules/no-plain-errors // eslint-disable-next-line n8n-local-rules/no-plain-errors
throw new Error( throw new Error(
`Invalid decorator metadata on key "${key as string}" on ${ConfigClass.name}\n Please use explicit typing on all config fields`, `Invalid decorator metadata on key "${key as string}" on ${ConfigClass.name}\n Please use explicit typing on all config fields`,
); );
} }
classMetadata.set(key, { type, envName });
classMetadata.set(key, { type, envName, schema });
globalMetadata.set(ConfigClass, classMetadata); globalMetadata.set(ConfigClass, classMetadata);
}; };

View File

@@ -399,4 +399,20 @@ describe('GlobalConfig', () => {
'Invalid number value for DB_LOGGING_MAX_EXECUTION_TIME: abcd', 'Invalid number value for DB_LOGGING_MAX_EXECUTION_TIME: abcd',
); );
}); });
describe('string unions', () => {
it('on invalid value, should warn and fall back to default value', () => {
process.env = {
N8N_RUNNERS_MODE: 'non-existing-mode',
};
const globalConfig = Container.get(GlobalConfig);
expect(globalConfig.taskRunners.mode).toEqual('internal');
expect(consoleWarnMock).toHaveBeenCalledWith(
expect.stringContaining(
"Invalid value for N8N_RUNNERS_MODE - Invalid enum value. Expected 'internal' | 'external', received 'non-existing-mode'. Falling back to default value.",
),
);
});
});
}); });

3
pnpm-lock.yaml generated
View File

@@ -396,6 +396,9 @@ importers:
reflect-metadata: reflect-metadata:
specifier: 'catalog:' specifier: 'catalog:'
version: 0.2.2 version: 0.2.2
zod:
specifier: 'catalog:'
version: 3.24.1
devDependencies: devDependencies:
'@n8n/typescript-config': '@n8n/typescript-config':
specifier: workspace:* specifier: workspace:*