mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-16 09:36:44 +00:00
test: Add task runner to test containers (#19254)
This commit is contained in:
@@ -37,6 +37,7 @@ ${colors.yellow}Usage:${colors.reset}
|
||||
${colors.yellow}Options:${colors.reset}
|
||||
--postgres Use PostgreSQL instead of SQLite
|
||||
--queue Enable queue mode (requires PostgreSQL)
|
||||
--task-runner Enable external task runner container
|
||||
--mains <n> Number of main instances (default: 1)
|
||||
--workers <n> Number of worker instances (default: 1)
|
||||
--name <name> Project name for parallel runs
|
||||
@@ -65,6 +66,9 @@ ${colors.yellow}Examples:${colors.reset}
|
||||
${colors.bright}# Queue mode (automatically uses PostgreSQL)${colors.reset}
|
||||
npm run stack --queue
|
||||
|
||||
${colors.bright}# With external task runner${colors.reset}
|
||||
npm run stack --postgres --task-runner
|
||||
|
||||
${colors.bright}# Custom scaling${colors.reset}
|
||||
npm run stack --queue --mains 3 --workers 5
|
||||
|
||||
@@ -96,6 +100,7 @@ async function main() {
|
||||
help: { type: 'boolean', short: 'h' },
|
||||
postgres: { type: 'boolean' },
|
||||
queue: { type: 'boolean' },
|
||||
'task-runner': { type: 'boolean' },
|
||||
mains: { type: 'string' },
|
||||
workers: { type: 'string' },
|
||||
name: { type: 'string' },
|
||||
@@ -114,6 +119,7 @@ async function main() {
|
||||
// Build configuration
|
||||
const config: N8NConfig = {
|
||||
postgres: values.postgres ?? false,
|
||||
taskRunner: values['task-runner'] ?? false,
|
||||
projectName: values.name ?? `n8n-stack-${Math.random().toString(36).substring(7)}`,
|
||||
};
|
||||
|
||||
@@ -225,6 +231,16 @@ function displayConfig(config: N8NConfig) {
|
||||
log.info('Queue mode: disabled');
|
||||
}
|
||||
|
||||
// Display task runner status
|
||||
if (config.taskRunner) {
|
||||
log.info('Task runner: enabled (external container)');
|
||||
if (!usePostgres) {
|
||||
log.warn('Task runner recommended with PostgreSQL for better performance');
|
||||
}
|
||||
} else {
|
||||
log.info('Task runner: disabled');
|
||||
}
|
||||
|
||||
if (config.resourceQuota) {
|
||||
log.info(
|
||||
`Resource limits: ${config.resourceQuota.memory}GB RAM, ${config.resourceQuota.cpu} CPU cores`,
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
* - Single instances (SQLite or PostgreSQL)
|
||||
* - Queue mode with Redis
|
||||
* - Multi-main instances with load balancing
|
||||
* - Task runner containers for external code execution
|
||||
* - Parallel execution (multiple stacks running simultaneously)
|
||||
*
|
||||
* Key features for parallel execution:
|
||||
@@ -23,6 +24,7 @@ import {
|
||||
setupCaddyLoadBalancer,
|
||||
pollContainerHttpEndpoint,
|
||||
setupProxyServer,
|
||||
setupTaskRunner,
|
||||
} from './n8n-test-container-dependencies';
|
||||
import { createSilentLogConsumer } from './n8n-test-container-utils';
|
||||
|
||||
@@ -45,7 +47,6 @@ const BASE_ENV: Record<string, string> = {
|
||||
QUEUE_HEALTH_CHECK_ACTIVE: 'true',
|
||||
N8N_DIAGNOSTICS_ENABLED: 'false',
|
||||
N8N_METRICS: 'true',
|
||||
N8N_RUNNERS_ENABLED: 'true',
|
||||
NODE_ENV: 'development', // If this is set to test, the n8n container will not start, insights module is not found??
|
||||
N8N_LICENSE_TENANT_ID: process.env.N8N_LICENSE_TENANT_ID ?? '1001',
|
||||
N8N_LICENSE_ACTIVATION_KEY: process.env.N8N_LICENSE_ACTIVATION_KEY ?? '',
|
||||
@@ -81,6 +82,7 @@ export interface N8NConfig {
|
||||
cpu?: number; // in cores
|
||||
};
|
||||
proxyServerEnabled?: boolean;
|
||||
taskRunner?: boolean;
|
||||
}
|
||||
|
||||
export interface N8NStack {
|
||||
@@ -119,15 +121,18 @@ export async function createN8NStack(config: N8NConfig = {}): Promise<N8NStack>
|
||||
proxyServerEnabled = false,
|
||||
projectName,
|
||||
resourceQuota,
|
||||
taskRunner = false,
|
||||
} = config;
|
||||
const queueConfig = normalizeQueueConfig(queueMode);
|
||||
const taskRunnerEnabled = !!taskRunner;
|
||||
const usePostgres = postgres || !!queueConfig;
|
||||
const uniqueProjectName = projectName ?? `n8n-stack-${Math.random().toString(36).substring(7)}`;
|
||||
const containers: StartedTestContainer[] = [];
|
||||
|
||||
const mainCount = queueConfig?.mains ?? 1;
|
||||
const needsLoadBalancer = mainCount > 1;
|
||||
const needsNetwork = usePostgres || !!queueConfig || needsLoadBalancer || proxyServerEnabled;
|
||||
const needsNetwork =
|
||||
usePostgres || !!queueConfig || needsLoadBalancer || proxyServerEnabled || taskRunnerEnabled;
|
||||
|
||||
let network: StartedNetwork | undefined;
|
||||
if (needsNetwork) {
|
||||
@@ -217,6 +222,16 @@ export async function createN8NStack(config: N8NConfig = {}): Promise<N8NStack>
|
||||
};
|
||||
}
|
||||
|
||||
if (taskRunnerEnabled) {
|
||||
environment = {
|
||||
...environment,
|
||||
N8N_RUNNERS_ENABLED: 'true',
|
||||
N8N_RUNNERS_MODE: 'external',
|
||||
N8N_RUNNERS_AUTH_TOKEN: 'test',
|
||||
N8N_RUNNERS_BROKER_LISTEN_ADDRESS: '0.0.0.0',
|
||||
};
|
||||
}
|
||||
|
||||
let baseUrl: string;
|
||||
|
||||
if (needsLoadBalancer) {
|
||||
@@ -269,6 +284,21 @@ export async function createN8NStack(config: N8NConfig = {}): Promise<N8NStack>
|
||||
containers.push(...instances);
|
||||
}
|
||||
|
||||
if (taskRunnerEnabled && network) {
|
||||
// Connect to first available broker (main or worker)
|
||||
// In queue mode, workers also run task brokers
|
||||
const taskBrokerUri = queueConfig?.workers
|
||||
? `http://${uniqueProjectName}-n8n-worker-1:5679` // Prefer worker broker in queue mode
|
||||
: `http://${uniqueProjectName}-n8n-main-1:5679`; // Use main broker otherwise
|
||||
|
||||
const taskRunnerContainer = await setupTaskRunner({
|
||||
projectName: uniqueProjectName,
|
||||
network,
|
||||
taskBrokerUri,
|
||||
});
|
||||
containers.push(taskRunnerContainer);
|
||||
}
|
||||
|
||||
return {
|
||||
baseUrl,
|
||||
stop: async () => {
|
||||
@@ -414,6 +444,7 @@ async function createN8NContainer({
|
||||
directPort,
|
||||
resourceQuota,
|
||||
}: CreateContainerOptions): Promise<StartedTestContainer> {
|
||||
const taskRunnerEnabled = environment.N8N_RUNNERS_ENABLED === 'true';
|
||||
const { consumer, throwWithLogs } = createSilentLogConsumer();
|
||||
|
||||
let container = new GenericContainer(N8N_IMAGE);
|
||||
@@ -445,13 +476,23 @@ async function createN8NContainer({
|
||||
}
|
||||
|
||||
if (isWorker) {
|
||||
container = container.withCommand(['worker']).withWaitStrategy(N8N_WORKER_WAIT_STRATEGY);
|
||||
// Workers expose task broker port if task runners are enabled
|
||||
const workerPorts = taskRunnerEnabled ? [5678, 5679] : [5678];
|
||||
container = container
|
||||
.withCommand(['worker'])
|
||||
.withExposedPorts(...workerPorts)
|
||||
.withWaitStrategy(N8N_WORKER_WAIT_STRATEGY);
|
||||
} else {
|
||||
container = container.withExposedPorts(5678).withWaitStrategy(N8N_MAIN_WAIT_STRATEGY);
|
||||
// Mains always expose both ports (5678 for web, 5679 for task broker when enabled)
|
||||
const mainPorts = taskRunnerEnabled ? [5678, 5679] : [5678];
|
||||
container = container.withExposedPorts(...mainPorts).withWaitStrategy(N8N_MAIN_WAIT_STRATEGY);
|
||||
|
||||
if (directPort) {
|
||||
const portMappings = taskRunnerEnabled
|
||||
? [{ container: 5678, host: directPort }, 5679]
|
||||
: [{ container: 5678, host: directPort }];
|
||||
container = container
|
||||
.withExposedPorts({ container: 5678, host: directPort })
|
||||
.withExposedPorts(...portMappings)
|
||||
.withWaitStrategy(N8N_MAIN_WAIT_STRATEGY);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -351,5 +351,44 @@ export async function setupProxyServer({
|
||||
}
|
||||
}
|
||||
|
||||
const TASK_RUNNER_IMAGE = 'n8nio/runners:nightly';
|
||||
|
||||
export async function setupTaskRunner({
|
||||
projectName,
|
||||
network,
|
||||
taskBrokerUri,
|
||||
}: {
|
||||
projectName: string;
|
||||
network: StartedNetwork;
|
||||
taskBrokerUri: string;
|
||||
}): Promise<StartedTestContainer> {
|
||||
const { consumer, throwWithLogs } = createSilentLogConsumer();
|
||||
|
||||
try {
|
||||
return await new GenericContainer(TASK_RUNNER_IMAGE)
|
||||
.withNetwork(network)
|
||||
.withNetworkAliases(`${projectName}-task-runner`)
|
||||
.withExposedPorts(5680)
|
||||
.withEnvironment({
|
||||
N8N_RUNNERS_AUTH_TOKEN: 'test',
|
||||
N8N_RUNNERS_LAUNCHER_LOG_LEVEL: 'debug',
|
||||
N8N_RUNNERS_TASK_BROKER_URI: taskBrokerUri,
|
||||
N8N_RUNNERS_MAX_CONCURRENCY: '5',
|
||||
N8N_RUNNERS_AUTO_SHUTDOWN_TIMEOUT: '15',
|
||||
})
|
||||
.withWaitStrategy(Wait.forListeningPorts())
|
||||
.withLabels({
|
||||
'com.docker.compose.project': projectName,
|
||||
'com.docker.compose.service': 'task-runner',
|
||||
})
|
||||
.withName(`${projectName}-task-runner`)
|
||||
.withReuse()
|
||||
.withLogConsumer(consumer)
|
||||
.start();
|
||||
} catch (error) {
|
||||
return throwWithLogs(error);
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Look at Ollama container?
|
||||
// TODO: Look at MariaDB container?
|
||||
|
||||
@@ -34,6 +34,7 @@ interface ContainerConfig {
|
||||
};
|
||||
env?: Record<string, string>;
|
||||
proxyServerEnabled?: boolean;
|
||||
taskRunner?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"test:all": "playwright test",
|
||||
"test:local": "N8N_BASE_URL=http://localhost:5680 RESET_E2E_DB=true playwright test --project=*ui*",
|
||||
"test:local": "N8N_BASE_URL=http://localhost:5680 RESET_E2E_DB=true playwright test --project=ui --project=ui:isolated",
|
||||
"test:ui": "playwright test --project=*ui*",
|
||||
"test:performance": "playwright test --project=performance",
|
||||
"test:chaos": "playwright test --project='*:chaos'",
|
||||
|
||||
@@ -3,7 +3,14 @@ import type { N8NConfig } from 'n8n-containers/n8n-test-container-creation';
|
||||
|
||||
// Tags that require test containers environment
|
||||
// These tests won't be run against local
|
||||
const CONTAINER_ONLY_TAGS = ['proxy', 'multi-node', 'postgres', 'queue', 'multi-main'];
|
||||
const CONTAINER_ONLY_TAGS = [
|
||||
'proxy',
|
||||
'multi-node',
|
||||
'postgres',
|
||||
'queue',
|
||||
'multi-main',
|
||||
'task-runner',
|
||||
];
|
||||
const CONTAINER_ONLY = new RegExp(`@capability:(${CONTAINER_ONLY_TAGS.join('|')})`);
|
||||
|
||||
// Tags that need serial execution
|
||||
@@ -11,11 +18,8 @@ const CONTAINER_ONLY = new RegExp(`@capability:(${CONTAINER_ONLY_TAGS.join('|')}
|
||||
// In local run they are a "dependency" which means they will be skipped if earlier tests fail, not ideal but needed for isolation
|
||||
const SERIAL_EXECUTION = /@db:reset/;
|
||||
|
||||
// Tags that require proxy server
|
||||
const REQUIRES_PROXY_SERVER = /@capability:proxy/;
|
||||
|
||||
const CONTAINER_CONFIGS: Array<{ name: string; config: N8NConfig }> = [
|
||||
{ name: 'standard', config: { proxyServerEnabled: true } },
|
||||
{ name: 'standard', config: { proxyServerEnabled: true, taskRunner: true } },
|
||||
{ name: 'postgres', config: { postgres: true } },
|
||||
{ name: 'queue', config: { queueMode: true } },
|
||||
{ name: 'multi-main', config: { queueMode: { mains: 2, workers: 1 } } },
|
||||
@@ -38,7 +42,6 @@ export function getProjects(): Project[] {
|
||||
name: 'ui:isolated',
|
||||
testDir: './tests/ui',
|
||||
grep: SERIAL_EXECUTION,
|
||||
grepInvert: REQUIRES_PROXY_SERVER,
|
||||
workers: 1,
|
||||
use: { baseURL: process.env.N8N_BASE_URL },
|
||||
},
|
||||
@@ -46,10 +49,6 @@ export function getProjects(): Project[] {
|
||||
} else {
|
||||
for (const { name, config } of CONTAINER_CONFIGS) {
|
||||
const grepInvertPatterns = [SERIAL_EXECUTION.source];
|
||||
if (!config.proxyServerEnabled) {
|
||||
grepInvertPatterns.push(REQUIRES_PROXY_SERVER.source);
|
||||
}
|
||||
|
||||
projects.push(
|
||||
{
|
||||
name: `${name}:ui`,
|
||||
@@ -63,7 +62,6 @@ export function getProjects(): Project[] {
|
||||
name: `${name}:ui:isolated`,
|
||||
testDir: './tests/ui',
|
||||
grep: SERIAL_EXECUTION,
|
||||
grepInvert: !config.proxyServerEnabled ? REQUIRES_PROXY_SERVER : undefined,
|
||||
workers: 1,
|
||||
use: { containerConfig: config },
|
||||
},
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
import { CODE_NODE_NAME, MANUAL_TRIGGER_NODE_NAME } from '../../config/constants';
|
||||
import { test, expect } from '../../fixtures/base';
|
||||
|
||||
/**
|
||||
* Task Runner Capability Tests
|
||||
*
|
||||
* These tests require the task runner container to be running.
|
||||
* Use @capability:task-runner tag to ensure they only run in task runner mode.
|
||||
*/
|
||||
test.describe('Task Runner Capability @capability:task-runner', () => {
|
||||
test('should execute Javascript with task runner enabled', async ({ n8n }) => {
|
||||
await n8n.goHome();
|
||||
await n8n.workflows.clickAddWorkflowButton();
|
||||
await n8n.canvas.addNode(MANUAL_TRIGGER_NODE_NAME);
|
||||
await n8n.canvas.addNode(CODE_NODE_NAME, { action: 'Code in JavaScript', closeNDV: true });
|
||||
|
||||
await n8n.workflowComposer.executeWorkflowAndWaitForNotification(
|
||||
'Workflow executed successfully',
|
||||
);
|
||||
await expect(n8n.canvas.getCanvasNodes()).toHaveCount(2);
|
||||
});
|
||||
|
||||
test('should execute Python with task runner enabled', async ({ n8n }) => {
|
||||
await n8n.goHome();
|
||||
await n8n.workflows.clickAddWorkflowButton();
|
||||
await n8n.canvas.addNode(MANUAL_TRIGGER_NODE_NAME);
|
||||
await n8n.canvas.addNode(CODE_NODE_NAME, { action: 'Code in Python (Beta)', closeNDV: true });
|
||||
await n8n.workflowComposer.executeWorkflowAndWaitForNotification(
|
||||
'Workflow executed successfully',
|
||||
);
|
||||
await expect(n8n.canvas.getCanvasNodes()).toHaveCount(2);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user