Files
n8n-enterprise-unlocked/packages/testing/containers/n8n-start-stack.ts
2025-09-08 09:48:58 +01:00

272 lines
8.1 KiB
TypeScript
Executable File
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env tsx
import { parseArgs } from 'node:util';
import { DockerImageNotFoundError } from './docker-image-not-found-error';
import type { N8NConfig, N8NStack } from './n8n-test-container-creation';
import { createN8NStack } from './n8n-test-container-creation';
import { BASE_PERFORMANCE_PLANS, isValidPerformancePlan } from './performance-plans';
// ANSI colors for terminal output
const colors = {
reset: '\x1b[0m',
bright: '\x1b[1m',
green: '\x1b[32m',
yellow: '\x1b[33m',
blue: '\x1b[34m',
red: '\x1b[31m',
cyan: '\x1b[36m',
};
const log = {
info: (msg: string) => console.log(`${colors.blue}${colors.reset} ${msg}`),
success: (msg: string) => console.log(`${colors.green}${colors.reset} ${msg}`),
error: (msg: string) => console.error(`${colors.red}${colors.reset} ${msg}`),
warn: (msg: string) => console.warn(`${colors.yellow}${colors.reset} ${msg}`),
header: (msg: string) => console.log(`\n${colors.bright}${colors.cyan}${msg}${colors.reset}\n`),
};
function showHelp() {
console.log(`
${colors.bright}n8n Stack Manager${colors.reset}
Start n8n containers for development and testing.
${colors.yellow}Usage:${colors.reset}
npm run stack [options]
${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
--env KEY=VALUE Set environment variables
--plan <plan> Use performance plan preset (${Object.keys(BASE_PERFORMANCE_PLANS).join(', ')})
--help, -h Show this help
${colors.yellow}Performance Plans:${colors.reset}
${Object.entries(BASE_PERFORMANCE_PLANS)
.map(
([name, plan]) =>
` ${name.padEnd(12)} ${plan.memory}GB RAM, ${plan.cpu} CPU cores - SQLite only`,
)
.join('\n')}
${colors.yellow}Environment Variables:${colors.reset}
• N8N_DOCKER_IMAGE=<image> Use a custom Docker image (default: n8nio/n8n:local)
${colors.yellow}Examples:${colors.reset}
${colors.bright}# Simple SQLite instance${colors.reset}
npm run stack
${colors.bright}# PostgreSQL database${colors.reset}
npm run stack --postgres
${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
${colors.bright}# With environment variables${colors.reset}
npm run stack --postgres --env N8N_LOG_LEVEL=info --env N8N_ENABLED_MODULES=insights
${colors.bright}# Performance plan presets${colors.reset}
${Object.keys(BASE_PERFORMANCE_PLANS)
.map((name) => ` npm run stack --plan ${name}`)
.join('\n')}
${colors.bright}# Parallel instances${colors.reset}
npm run stack --name test-1
npm run stack --name test-2
${colors.yellow}Notes:${colors.reset}
• SQLite is the default database (no external dependencies)
• Queue mode requires PostgreSQL and enables horizontal scaling
• Use --name for running multiple instances in parallel
• Performance plans simulate cloud constraints (SQLite only, resource-limited)
• Press Ctrl+C to stop all containers
`);
}
async function main() {
const { values } = parseArgs({
args: process.argv.slice(2),
options: {
help: { type: 'boolean', short: 'h' },
postgres: { type: 'boolean' },
queue: { type: 'boolean' },
'task-runner': { type: 'boolean' },
mains: { type: 'string' },
workers: { type: 'string' },
name: { type: 'string' },
env: { type: 'string', multiple: true },
plan: { type: 'string' },
},
allowPositionals: false,
});
// Show help if requested
if (values.help) {
showHelp();
process.exit(0);
}
// 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)}`,
};
// Handle queue mode
if (values.queue ?? values.mains ?? values.workers) {
const mains = parseInt(values.mains ?? '1', 10);
const workers = parseInt(values.workers ?? '1', 10);
if (isNaN(mains) || isNaN(workers) || mains < 1 || workers < 0) {
log.error('Invalid mains or workers count');
process.exit(1);
}
config.queueMode = { mains, workers };
if (!values.queue && (values.mains ?? values.workers)) {
log.warn('--mains and --workers imply queue mode');
}
}
if (values.plan) {
const planName = values.plan;
if (!isValidPerformancePlan(planName)) {
log.error(`Invalid performance plan: ${values.plan}`);
log.error(`Available plans: ${Object.keys(BASE_PERFORMANCE_PLANS).join(', ')}`);
process.exit(1);
}
const plan = BASE_PERFORMANCE_PLANS[planName];
if (values.postgres) {
log.warn('Performance plans use SQLite only. PostgreSQL option ignored.');
}
if (values.queue || values.mains || values.workers) {
log.warn('Performance plans use SQLite only. Queue mode ignored.');
}
config.resourceQuota = plan;
config.postgres = false; // Force SQLite for performance plans
config.queueMode = false; // Force single instance for performance plans
log.info(
`Using ${planName} performance plan: ${plan.memory}GB RAM, ${plan.cpu} CPU cores (SQLite only)`,
);
}
// Parse environment variables
if (values.env && values.env.length > 0) {
config.env = {};
for (const envStr of values.env) {
const [key, ...valueParts] = envStr.split('=');
const value = valueParts.join('='); // Handle values with = in them
if (key && value) {
config.env[key] = value;
} else {
log.warn(`Invalid env format: ${envStr} (expected KEY=VALUE)`);
}
}
}
log.header('Starting n8n Stack');
log.info(`Project name: ${config.projectName}`);
displayConfig(config);
let stack: N8NStack;
try {
log.info('Starting containers...');
try {
stack = await createN8NStack(config);
} catch (error) {
if (error instanceof DockerImageNotFoundError) {
log.error(error.message);
process.exit(1);
}
throw error;
}
log.success('All containers started successfully!');
console.log('');
log.info(`n8n URL: ${colors.bright}${colors.green}${stack.baseUrl}${colors.reset}`);
} catch (error) {
log.error(`Failed to start: ${error as string}`);
process.exit(1);
}
}
function displayConfig(config: N8NConfig) {
const dockerImage = process.env.N8N_DOCKER_IMAGE ?? 'n8nio/n8n:local';
log.info(`Docker image: ${dockerImage}`);
// Determine actual database
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
const usePostgres = config.postgres || config.queueMode;
log.info(`Database: ${usePostgres ? 'PostgreSQL' : 'SQLite'}`);
if (config.queueMode) {
const qm = typeof config.queueMode === 'boolean' ? { mains: 1, workers: 1 } : config.queueMode;
log.info(`Queue mode: ${qm.mains} main(s), ${qm.workers} worker(s)`);
if (!config.postgres) {
log.info('(PostgreSQL automatically enabled for queue mode)');
}
if (qm.mains && qm.mains > 1) {
log.info('(load balancer will be configured)');
}
} else {
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`,
);
}
if (config.env) {
const envCount = Object.keys(config.env).length;
if (envCount > 0) {
log.info(`Environment variables: ${envCount} custom variable(s)`);
Object.entries(config.env).forEach(([key, value]) => {
console.log(` ${key}=${value}`);
});
}
}
if (process.env.TESTCONTAINERS_REUSE_ENABLE === 'true') {
log.info('Container reuse: enabled (containers will persist)');
}
}
// Run if executed directly
if (require.main === module) {
main().catch((error) => {
log.error(`Unexpected error: ${error}`);
process.exit(1);
});
}