mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-17 10:02:05 +00:00
ci: Test container enhancements (#17008)
This commit is contained in:
@@ -3,47 +3,48 @@
|
||||
* This file provides a complete n8n container stack for testing with support for:
|
||||
* - Single instances (SQLite or PostgreSQL)
|
||||
* - Queue mode with Redis
|
||||
* - Multi-main instances with nginx load balancing
|
||||
* - Multi-main instances with load balancing
|
||||
* - Parallel execution (multiple stacks running simultaneously)
|
||||
*
|
||||
* Key features for parallel execution:
|
||||
* - Dynamic port allocation to avoid conflicts (handled by testcontainers)
|
||||
* - WebSocket support through nginx load balancer
|
||||
* - Dynamic port allocation to avoid conflicts (handled by testcontainers or get-port)
|
||||
*/
|
||||
|
||||
import getPort from 'get-port';
|
||||
import assert from 'node:assert';
|
||||
import type { StartedNetwork, StartedTestContainer } from 'testcontainers';
|
||||
import { GenericContainer, Network, Wait } from 'testcontainers';
|
||||
|
||||
import {
|
||||
setupNginxLoadBalancer,
|
||||
setupPostgres,
|
||||
setupRedis,
|
||||
} from './n8n-test-container-dependencies';
|
||||
import { DockerImageNotFoundError } from './docker-image-not-found-error';
|
||||
import { N8nImagePullPolicy } from './n8n-image-pull-policy';
|
||||
import {
|
||||
setupPostgres,
|
||||
setupRedis,
|
||||
setupCaddyLoadBalancer,
|
||||
pollContainerHttpEndpoint,
|
||||
} from './n8n-test-container-dependencies';
|
||||
import { createSilentLogConsumer } from './n8n-test-container-utils';
|
||||
|
||||
// --- Constants ---
|
||||
|
||||
const POSTGRES_IMAGE = 'postgres:16-alpine';
|
||||
const REDIS_IMAGE = 'redis:7-alpine';
|
||||
const NGINX_IMAGE = 'nginx:stable';
|
||||
const CADDY_IMAGE = 'caddy:2-alpine';
|
||||
const N8N_E2E_IMAGE = 'n8nio/n8n:local';
|
||||
|
||||
// Default n8n image (can be overridden via N8N_DOCKER_IMAGE env var)
|
||||
const N8N_IMAGE = process.env.N8N_DOCKER_IMAGE || N8N_E2E_IMAGE;
|
||||
const N8N_IMAGE = process.env.N8N_DOCKER_IMAGE ?? N8N_E2E_IMAGE;
|
||||
|
||||
// Base environment for all n8n instances
|
||||
const BASE_ENV: Record<string, string> = {
|
||||
N8N_LOG_LEVEL: 'debug',
|
||||
N8N_ENCRYPTION_KEY: 'test-encryption-key',
|
||||
E2E_TESTS: 'true',
|
||||
E2E_TESTS: 'false',
|
||||
QUEUE_HEALTH_CHECK_ACTIVE: 'true',
|
||||
N8N_DIAGNOSTICS_ENABLED: 'false',
|
||||
N8N_RUNNERS_ENABLED: 'true',
|
||||
NODE_ENV: 'development', // If this is set to test, the n8n container will not start, insights module is not found??
|
||||
};
|
||||
|
||||
const MULTI_MAIN_LICENSE = {
|
||||
N8N_LICENSE_TENANT_ID: '1001',
|
||||
N8N_LICENSE_TENANT_ID: process.env.N8N_LICENSE_TENANT_ID ?? '1001',
|
||||
N8N_LICENSE_ACTIVATION_KEY: process.env.N8N_LICENSE_ACTIVATION_KEY ?? '',
|
||||
};
|
||||
|
||||
@@ -89,7 +90,7 @@ export interface N8NStack {
|
||||
* const stack = await createN8NStack({ queueMode: true });
|
||||
*
|
||||
* @example
|
||||
* // Custom scaling
|
||||
* // Custom scaling (uses load balancer for multiple mains)
|
||||
* const stack = await createN8NStack({
|
||||
* queueMode: { mains: 3, workers: 5 },
|
||||
* env: { N8N_ENABLED_MODULES: 'insights' }
|
||||
@@ -99,22 +100,34 @@ export async function createN8NStack(config: N8NConfig = {}): Promise<N8NStack>
|
||||
const { postgres = false, queueMode = false, env = {}, projectName } = config;
|
||||
const queueConfig = normalizeQueueConfig(queueMode);
|
||||
const usePostgres = postgres || !!queueConfig;
|
||||
const uniqueProjectName = projectName ?? `n8n-${Math.random().toString(36).substring(7)}`;
|
||||
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;
|
||||
|
||||
let network: StartedNetwork | undefined;
|
||||
let nginxContainer: StartedTestContainer | undefined;
|
||||
|
||||
let environment: Record<string, string> = { ...BASE_ENV, ...env };
|
||||
|
||||
if (usePostgres || queueConfig) {
|
||||
if (needsNetwork) {
|
||||
network = await new Network().start();
|
||||
}
|
||||
|
||||
let environment: Record<string, string> = {
|
||||
...BASE_ENV,
|
||||
...env,
|
||||
};
|
||||
|
||||
// Add proxy hops only if using load balancer
|
||||
if (needsLoadBalancer) {
|
||||
environment.N8N_PROXY_HOPS = '1';
|
||||
}
|
||||
|
||||
if (usePostgres) {
|
||||
assert(network, 'Network should be created for postgres');
|
||||
const postgresContainer = await setupPostgres({
|
||||
postgresImage: POSTGRES_IMAGE,
|
||||
projectName: uniqueProjectName,
|
||||
network: network!,
|
||||
network,
|
||||
});
|
||||
containers.push(postgresContainer.container);
|
||||
environment = {
|
||||
@@ -131,10 +144,11 @@ export async function createN8NStack(config: N8NConfig = {}): Promise<N8NStack>
|
||||
}
|
||||
|
||||
if (queueConfig) {
|
||||
assert(network, 'Network should be created for queue mode');
|
||||
const redis = await setupRedis({
|
||||
redisImage: REDIS_IMAGE,
|
||||
projectName: uniqueProjectName,
|
||||
network: network!,
|
||||
network,
|
||||
});
|
||||
containers.push(redis);
|
||||
environment = {
|
||||
@@ -142,6 +156,7 @@ export async function createN8NStack(config: N8NConfig = {}): Promise<N8NStack>
|
||||
EXECUTIONS_MODE: 'queue',
|
||||
QUEUE_BULL_REDIS_HOST: 'redis',
|
||||
QUEUE_BULL_REDIS_PORT: '6379',
|
||||
OFFLOAD_MANUAL_EXECUTIONS_TO_WORKERS: 'true',
|
||||
};
|
||||
|
||||
if (queueConfig.mains > 1) {
|
||||
@@ -150,35 +165,59 @@ export async function createN8NStack(config: N8NConfig = {}): Promise<N8NStack>
|
||||
}
|
||||
environment = {
|
||||
...environment,
|
||||
N8N_PROXY_HOPS: '1',
|
||||
N8N_MULTI_MAIN_SETUP_ENABLED: 'true',
|
||||
...MULTI_MAIN_LICENSE,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
let baseUrl: string;
|
||||
|
||||
const instances = await createN8NInstances({
|
||||
mainCount: queueConfig?.mains ?? 1,
|
||||
workerCount: queueConfig?.workers ?? 0,
|
||||
uniqueProjectName: uniqueProjectName,
|
||||
environment,
|
||||
network,
|
||||
});
|
||||
containers.push(...instances);
|
||||
|
||||
if (queueConfig && queueConfig.mains > 1) {
|
||||
nginxContainer = await setupNginxLoadBalancer({
|
||||
nginxImage: NGINX_IMAGE,
|
||||
if (needsLoadBalancer) {
|
||||
assert(network, 'Network should be created for load balancer');
|
||||
const loadBalancerContainer = await setupCaddyLoadBalancer({
|
||||
caddyImage: CADDY_IMAGE,
|
||||
projectName: uniqueProjectName,
|
||||
mainInstances: instances.slice(0, queueConfig.mains),
|
||||
network: network!,
|
||||
mainCount,
|
||||
network,
|
||||
});
|
||||
containers.push(nginxContainer);
|
||||
baseUrl = `http://localhost:${nginxContainer.getMappedPort(80)}`;
|
||||
containers.push(loadBalancerContainer);
|
||||
|
||||
const loadBalancerPort = loadBalancerContainer.getMappedPort(80);
|
||||
baseUrl = `http://localhost:${loadBalancerPort}`;
|
||||
environment = {
|
||||
...environment,
|
||||
WEBHOOK_URL: baseUrl,
|
||||
};
|
||||
|
||||
const instances = await createN8NInstances({
|
||||
mainCount,
|
||||
workerCount: queueConfig?.workers ?? 0,
|
||||
uniqueProjectName,
|
||||
environment,
|
||||
network,
|
||||
});
|
||||
containers.push(...instances);
|
||||
|
||||
// Wait for all containers to be ready behind the load balancer
|
||||
await pollContainerHttpEndpoint(loadBalancerContainer, '/healthz/readiness');
|
||||
} else {
|
||||
baseUrl = `http://localhost:${instances[0].getMappedPort(5678)}`;
|
||||
const assignedPort = await getPort();
|
||||
baseUrl = `http://localhost:${assignedPort}`;
|
||||
environment = {
|
||||
...environment,
|
||||
WEBHOOK_URL: baseUrl,
|
||||
N8N_PORT: '5678', // Internal port
|
||||
};
|
||||
|
||||
const instances = await createN8NInstances({
|
||||
mainCount: 1,
|
||||
workerCount: queueConfig?.workers ?? 0,
|
||||
uniqueProjectName,
|
||||
environment,
|
||||
network,
|
||||
directPort: assignedPort,
|
||||
});
|
||||
containers.push(...instances);
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -245,6 +284,7 @@ interface CreateInstancesOptions {
|
||||
uniqueProjectName: string;
|
||||
environment: Record<string, string>;
|
||||
network?: StartedNetwork;
|
||||
directPort?: number;
|
||||
}
|
||||
|
||||
async function createN8NInstances({
|
||||
@@ -253,11 +293,15 @@ async function createN8NInstances({
|
||||
uniqueProjectName,
|
||||
environment,
|
||||
network,
|
||||
/** The host port to use for the main instance */
|
||||
directPort,
|
||||
}: CreateInstancesOptions): Promise<StartedTestContainer[]> {
|
||||
const instances: StartedTestContainer[] = [];
|
||||
|
||||
// Create main instances
|
||||
for (let i = 1; i <= mainCount; i++) {
|
||||
const name = mainCount > 1 ? `${uniqueProjectName}-n8n-main-${i}` : `${uniqueProjectName}-n8n`;
|
||||
const networkAlias = mainCount > 1 ? name : `${uniqueProjectName}-n8n-main-1`;
|
||||
const container = await createN8NContainer({
|
||||
name,
|
||||
uniqueProjectName,
|
||||
@@ -265,18 +309,20 @@ async function createN8NInstances({
|
||||
network,
|
||||
isWorker: false,
|
||||
instanceNumber: i,
|
||||
networkAlias: mainCount > 1 ? name : undefined,
|
||||
networkAlias,
|
||||
directPort: i === 1 ? directPort : undefined, // Only first main gets direct port
|
||||
});
|
||||
instances.push(container);
|
||||
}
|
||||
|
||||
// Create worker instances
|
||||
for (let i = 1; i <= workerCount; i++) {
|
||||
const name = `${uniqueProjectName}-n8n-worker-${i}`;
|
||||
const container = await createN8NContainer({
|
||||
name,
|
||||
uniqueProjectName,
|
||||
environment,
|
||||
network: network!,
|
||||
network,
|
||||
isWorker: true,
|
||||
instanceNumber: i,
|
||||
});
|
||||
@@ -294,6 +340,7 @@ interface CreateContainerOptions {
|
||||
isWorker: boolean;
|
||||
instanceNumber: number;
|
||||
networkAlias?: string;
|
||||
directPort?: number;
|
||||
}
|
||||
|
||||
async function createN8NContainer({
|
||||
@@ -304,7 +351,10 @@ async function createN8NContainer({
|
||||
isWorker,
|
||||
instanceNumber,
|
||||
networkAlias,
|
||||
directPort,
|
||||
}: CreateContainerOptions): Promise<StartedTestContainer> {
|
||||
const { consumer, throwWithLogs } = createSilentLogConsumer();
|
||||
|
||||
let container = new GenericContainer(N8N_IMAGE);
|
||||
|
||||
container = container
|
||||
@@ -316,14 +366,10 @@ async function createN8NContainer({
|
||||
})
|
||||
.withPullPolicy(new N8nImagePullPolicy(N8N_IMAGE))
|
||||
.withName(name)
|
||||
.withLogConsumer(consumer)
|
||||
.withName(name)
|
||||
.withReuse();
|
||||
|
||||
if (isWorker) {
|
||||
container = container.withCommand(['worker']);
|
||||
} else {
|
||||
container = container.withExposedPorts(5678).withWaitStrategy(N8N_WAIT_STRATEGY);
|
||||
}
|
||||
|
||||
if (network) {
|
||||
container = container.withNetwork(network);
|
||||
if (networkAlias) {
|
||||
@@ -331,12 +377,30 @@ async function createN8NContainer({
|
||||
}
|
||||
}
|
||||
|
||||
if (isWorker) {
|
||||
container = container.withCommand(['worker']);
|
||||
} else {
|
||||
container = container.withExposedPorts(5678).withWaitStrategy(N8N_WAIT_STRATEGY);
|
||||
|
||||
if (directPort) {
|
||||
container = container.withExposedPorts({ container: 5678, host: directPort });
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
return await container.start();
|
||||
} catch (error) {
|
||||
if (error instanceof Error && 'statusCode' in error && error.statusCode === 404) {
|
||||
} catch (error: unknown) {
|
||||
if (
|
||||
error instanceof Error &&
|
||||
'statusCode' in error &&
|
||||
(error as Error & { statusCode: number }).statusCode === 404
|
||||
) {
|
||||
throw new DockerImageNotFoundError(name, error);
|
||||
}
|
||||
throw error;
|
||||
|
||||
console.error(`Container "${name}" failed to start!`);
|
||||
console.error('Original error:', error instanceof Error ? error.message : String(error));
|
||||
|
||||
return throwWithLogs(error);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user