@atom8n/n8n-benchmark 2.0.0
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/.turbo/turbo-build.log +4 -0
- package/Dockerfile +63 -0
- package/README.md +122 -0
- package/bin/n8n-benchmark +13 -0
- package/biome.jsonc +7 -0
- package/dist/build.tsbuildinfo +1 -0
- package/dist/commands/list.d.ts +8 -0
- package/dist/commands/list.js +23 -0
- package/dist/commands/list.js.map +1 -0
- package/dist/commands/run.d.ts +24 -0
- package/dist/commands/run.js +128 -0
- package/dist/commands/run.js.map +1 -0
- package/dist/config/common-flags.d.ts +1 -0
- package/dist/config/common-flags.js +9 -0
- package/dist/config/common-flags.js.map +1 -0
- package/dist/n8n-api-client/authenticated-n8n-api-client.d.ts +15 -0
- package/dist/n8n-api-client/authenticated-n8n-api-client.js +67 -0
- package/dist/n8n-api-client/authenticated-n8n-api-client.js.map +1 -0
- package/dist/n8n-api-client/credentials-api-client.d.ts +9 -0
- package/dist/n8n-api-client/credentials-api-client.js +24 -0
- package/dist/n8n-api-client/credentials-api-client.js.map +1 -0
- package/dist/n8n-api-client/data-table-api-client.d.ts +9 -0
- package/dist/n8n-api-client/data-table-api-client.js +23 -0
- package/dist/n8n-api-client/data-table-api-client.js.map +1 -0
- package/dist/n8n-api-client/n8n-api-client.d.ts +13 -0
- package/dist/n8n-api-client/n8n-api-client.js +82 -0
- package/dist/n8n-api-client/n8n-api-client.js.map +1 -0
- package/dist/n8n-api-client/n8n-api-client.types.d.ts +21 -0
- package/dist/n8n-api-client/n8n-api-client.types.js +3 -0
- package/dist/n8n-api-client/n8n-api-client.types.js.map +1 -0
- package/dist/n8n-api-client/project-api-client.d.ts +6 -0
- package/dist/n8n-api-client/project-api-client.js +14 -0
- package/dist/n8n-api-client/project-api-client.js.map +1 -0
- package/dist/n8n-api-client/workflows-api-client.d.ts +11 -0
- package/dist/n8n-api-client/workflows-api-client.js +30 -0
- package/dist/n8n-api-client/workflows-api-client.js.map +1 -0
- package/dist/scenario/scenario-data-loader.d.ts +13 -0
- package/dist/scenario/scenario-data-loader.js +84 -0
- package/dist/scenario/scenario-data-loader.js.map +1 -0
- package/dist/scenario/scenario-loader.d.ts +7 -0
- package/dist/scenario/scenario-loader.js +101 -0
- package/dist/scenario/scenario-loader.js.map +1 -0
- package/dist/test-execution/app-metrics-poller.d.ts +13 -0
- package/dist/test-execution/app-metrics-poller.js +54 -0
- package/dist/test-execution/app-metrics-poller.js.map +1 -0
- package/dist/test-execution/k6-executor.d.ts +33 -0
- package/dist/test-execution/k6-executor.js +120 -0
- package/dist/test-execution/k6-executor.js.map +1 -0
- package/dist/test-execution/k6-summary.d.ts +82 -0
- package/dist/test-execution/k6-summary.js +2 -0
- package/dist/test-execution/k6-summary.js.map +1 -0
- package/dist/test-execution/prometheus-metrics-parser.d.ts +9 -0
- package/dist/test-execution/prometheus-metrics-parser.js +44 -0
- package/dist/test-execution/prometheus-metrics-parser.js.map +1 -0
- package/dist/test-execution/scenario-data-importer.d.ts +21 -0
- package/dist/test-execution/scenario-data-importer.js +108 -0
- package/dist/test-execution/scenario-data-importer.js.map +1 -0
- package/dist/test-execution/scenario-runner.d.ts +18 -0
- package/dist/test-execution/scenario-runner.js +46 -0
- package/dist/test-execution/scenario-runner.js.map +1 -0
- package/dist/test-execution/test-report.d.ts +56 -0
- package/dist/test-execution/test-report.js +65 -0
- package/dist/test-execution/test-report.js.map +1 -0
- package/dist/types/scenario.d.ts +16 -0
- package/dist/types/scenario.js +3 -0
- package/dist/types/scenario.js.map +1 -0
- package/eslint.config.mjs +22 -0
- package/infra/.terraform.lock.hcl +60 -0
- package/infra/benchmark-env.tf +54 -0
- package/infra/modules/benchmark-vm/output.tf +11 -0
- package/infra/modules/benchmark-vm/vars.tf +29 -0
- package/infra/modules/benchmark-vm/vm.tf +126 -0
- package/infra/output.tf +16 -0
- package/infra/providers.tf +23 -0
- package/infra/vars.tf +34 -0
- package/package.json +55 -0
- package/scenarios/binary-data/binary-data.json +67 -0
- package/scenarios/binary-data/binary-data.manifest.json +7 -0
- package/scenarios/binary-data/binary-data.script.js +29 -0
- package/scenarios/credential-http-node/credential-bearer.json +8 -0
- package/scenarios/credential-http-node/credential-http-node.json +241 -0
- package/scenarios/credential-http-node/credential-http-node.manifest.json +10 -0
- package/scenarios/credential-http-node/credential-http-node.script.js +30 -0
- package/scenarios/data-table-node/data-table-node.json +168 -0
- package/scenarios/data-table-node/data-table-node.manifest.json +10 -0
- package/scenarios/data-table-node/data-table-node.script.js +38 -0
- package/scenarios/data-table-node/data-table.json +25 -0
- package/scenarios/http-node/http-node.json +213 -0
- package/scenarios/http-node/http-node.manifest.json +7 -0
- package/scenarios/http-node/http-node.script.js +30 -0
- package/scenarios/js-code-node/js-code-node.json +96 -0
- package/scenarios/js-code-node/js-code-node.manifest.json +7 -0
- package/scenarios/js-code-node/js-code-node.script.js +29 -0
- package/scenarios/multiple-webhooks/multiple-webhooks.manifest.json +20 -0
- package/scenarios/multiple-webhooks/multiple-webhooks.script.js +19 -0
- package/scenarios/multiple-webhooks/multiple-webhooks1.json +25 -0
- package/scenarios/multiple-webhooks/multiple-webhooks10.json +25 -0
- package/scenarios/multiple-webhooks/multiple-webhooks2.json +25 -0
- package/scenarios/multiple-webhooks/multiple-webhooks3.json +25 -0
- package/scenarios/multiple-webhooks/multiple-webhooks4.json +25 -0
- package/scenarios/multiple-webhooks/multiple-webhooks5.json +25 -0
- package/scenarios/multiple-webhooks/multiple-webhooks6.json +25 -0
- package/scenarios/multiple-webhooks/multiple-webhooks7.json +25 -0
- package/scenarios/multiple-webhooks/multiple-webhooks8.json +25 -0
- package/scenarios/multiple-webhooks/multiple-webhooks9.json +25 -0
- package/scenarios/py-code-node/py-code-node.json +98 -0
- package/scenarios/py-code-node/py-code-node.manifest.json +7 -0
- package/scenarios/py-code-node/py-code-node.script.js +29 -0
- package/scenarios/scenario.schema.json +51 -0
- package/scenarios/set-node-expressions/set-node-expressions.json +91 -0
- package/scenarios/set-node-expressions/set-node-expressions.manifest.json +7 -0
- package/scenarios/set-node-expressions/set-node-expressions.script.js +18 -0
- package/scenarios/single-webhook/single-webhook.json +25 -0
- package/scenarios/single-webhook/single-webhook.manifest.json +7 -0
- package/scenarios/single-webhook/single-webhook.script.js +18 -0
- package/scripts/bootstrap.sh +63 -0
- package/scripts/clients/docker-compose-client.mjs +45 -0
- package/scripts/clients/ssh-client.mjs +37 -0
- package/scripts/clients/terraform-client.mjs +71 -0
- package/scripts/destroy-cloud-env.mjs +86 -0
- package/scripts/mock-api/mappings/mockApiData.json +92110 -0
- package/scripts/n8n-setups/postgres/docker-compose.yml +76 -0
- package/scripts/n8n-setups/postgres/setup.mjs +15 -0
- package/scripts/n8n-setups/scaling-multi-main/docker-compose.yml +230 -0
- package/scripts/n8n-setups/scaling-multi-main/nginx.conf +24 -0
- package/scripts/n8n-setups/scaling-multi-main/setup.mjs +15 -0
- package/scripts/n8n-setups/scaling-single-main/docker-compose.yml +174 -0
- package/scripts/n8n-setups/scaling-single-main/setup.mjs +15 -0
- package/scripts/n8n-setups/sqlite/docker-compose.yml +55 -0
- package/scripts/n8n-setups/sqlite/setup.mjs +15 -0
- package/scripts/provision-cloud-env.mjs +36 -0
- package/scripts/run-for-n8n-setup.mjs +175 -0
- package/scripts/run-in-cloud.mjs +167 -0
- package/scripts/run-locally.mjs +73 -0
- package/scripts/run.mjs +192 -0
- package/scripts/utils/flags.mjs +20 -0
- package/src/commands/list.ts +26 -0
- package/src/commands/run.ts +140 -0
- package/src/config/common-flags.ts +6 -0
- package/src/n8n-api-client/authenticated-n8n-api-client.ts +88 -0
- package/src/n8n-api-client/credentials-api-client.ts +28 -0
- package/src/n8n-api-client/data-table-api-client.ts +30 -0
- package/src/n8n-api-client/n8n-api-client.ts +85 -0
- package/src/n8n-api-client/n8n-api-client.types.ts +27 -0
- package/src/n8n-api-client/project-api-client.ts +11 -0
- package/src/n8n-api-client/workflows-api-client.ts +38 -0
- package/src/scenario/scenario-data-loader.ts +75 -0
- package/src/scenario/scenario-loader.ts +90 -0
- package/src/test-execution/app-metrics-poller.ts +81 -0
- package/src/test-execution/k6-executor.ts +192 -0
- package/src/test-execution/k6-summary.ts +255 -0
- package/src/test-execution/prometheus-metrics-parser.ts +63 -0
- package/src/test-execution/scenario-data-importer.ts +165 -0
- package/src/test-execution/scenario-runner.ts +76 -0
- package/src/test-execution/test-report.ts +152 -0
- package/src/types/scenario.ts +33 -0
- package/tsconfig.build.json +9 -0
- package/tsconfig.json +14 -0
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
#!/usr/bin/env zx
|
|
2
|
+
/**
|
|
3
|
+
* This script runs the benchmarks for the given n8n setup.
|
|
4
|
+
*/
|
|
5
|
+
// @ts-check
|
|
6
|
+
import path from 'path';
|
|
7
|
+
import { $, argv, fs } from 'zx';
|
|
8
|
+
import { DockerComposeClient } from './clients/docker-compose-client.mjs';
|
|
9
|
+
import { flagsObjectToCliArgs } from './utils/flags.mjs';
|
|
10
|
+
import { EOL } from 'os';
|
|
11
|
+
|
|
12
|
+
const paths = {
|
|
13
|
+
n8nSetupsDir: path.join(__dirname, 'n8n-setups'),
|
|
14
|
+
mockApiDataPath: path.join(__dirname, 'mock-api'),
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const N8N_ENCRYPTION_KEY = 'very-secret-encryption-key';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Discovers runner services in the docker-compose setup, where the name matches exactly `runners` or ends with `_runners`
|
|
21
|
+
*/
|
|
22
|
+
async function discoverRunnerServices(dockerComposeClient) {
|
|
23
|
+
const result = await dockerComposeClient.$('config', '--services');
|
|
24
|
+
|
|
25
|
+
return result.stdout
|
|
26
|
+
.trim()
|
|
27
|
+
.split(EOL)
|
|
28
|
+
.filter((service) => service === 'runners' || service.endsWith('_runners'));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async function main() {
|
|
32
|
+
const [n8nSetupToUse] = argv._;
|
|
33
|
+
validateN8nSetup(n8nSetupToUse);
|
|
34
|
+
|
|
35
|
+
const composeFilePath = path.join(paths.n8nSetupsDir, n8nSetupToUse);
|
|
36
|
+
const setupScriptPath = path.join(paths.n8nSetupsDir, n8nSetupToUse, 'setup.mjs');
|
|
37
|
+
const n8nTag = argv.n8nDockerTag || process.env.N8N_DOCKER_TAG || 'latest';
|
|
38
|
+
const benchmarkTag = argv.benchmarkDockerTag || process.env.BENCHMARK_DOCKER_TAG || 'latest';
|
|
39
|
+
const k6ApiToken = argv.k6ApiToken || process.env.K6_API_TOKEN || undefined;
|
|
40
|
+
const resultWebhookUrl =
|
|
41
|
+
argv.resultWebhookUrl || process.env.BENCHMARK_RESULT_WEBHOOK_URL || undefined;
|
|
42
|
+
const resultWebhookAuthHeader =
|
|
43
|
+
argv.resultWebhookAuthHeader || process.env.BENCHMARK_RESULT_WEBHOOK_AUTH_HEADER || undefined;
|
|
44
|
+
const baseRunDir = argv.runDir || process.env.RUN_DIR || '/n8n';
|
|
45
|
+
const n8nLicenseCert = argv.n8nLicenseCert || process.env.N8N_LICENSE_CERT || undefined;
|
|
46
|
+
const n8nLicenseActivationKey = process.env.N8N_LICENSE_ACTIVATION_KEY || undefined;
|
|
47
|
+
const n8nLicenseTenantId = argv.n8nLicenseTenantId || process.env.N8N_LICENSE_TENANT_ID || '1';
|
|
48
|
+
const envTag = argv.env || 'local';
|
|
49
|
+
const vus = argv.vus;
|
|
50
|
+
const duration = argv.duration;
|
|
51
|
+
const scenarioFilter = argv.scenarioFilter;
|
|
52
|
+
|
|
53
|
+
const hasN8nLicense = !!n8nLicenseCert || !!n8nLicenseActivationKey;
|
|
54
|
+
if (n8nSetupToUse === 'scaling-multi-main' && !hasN8nLicense) {
|
|
55
|
+
console.error(
|
|
56
|
+
'n8n license is required to run the multi-main scaling setup. Please provide N8N_LICENSE_CERT or N8N_LICENSE_ACTIVATION_KEY (and N8N_LICENSE_TENANT_ID if needed)',
|
|
57
|
+
);
|
|
58
|
+
process.exit(1);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (!fs.existsSync(baseRunDir)) {
|
|
62
|
+
console.error(
|
|
63
|
+
`The run directory "${baseRunDir}" does not exist. Please specify a valid directory using --runDir`,
|
|
64
|
+
);
|
|
65
|
+
process.exit(1);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const runDir = path.join(baseRunDir, n8nSetupToUse);
|
|
69
|
+
fs.emptyDirSync(runDir);
|
|
70
|
+
|
|
71
|
+
const dockerComposeClient = new DockerComposeClient({
|
|
72
|
+
$: $({
|
|
73
|
+
cwd: composeFilePath,
|
|
74
|
+
verbose: true,
|
|
75
|
+
env: {
|
|
76
|
+
PATH: process.env.PATH,
|
|
77
|
+
N8N_VERSION: n8nTag,
|
|
78
|
+
N8N_LICENSE_CERT: n8nLicenseCert,
|
|
79
|
+
N8N_LICENSE_ACTIVATION_KEY: n8nLicenseActivationKey,
|
|
80
|
+
N8N_LICENSE_TENANT_ID: n8nLicenseTenantId,
|
|
81
|
+
N8N_ENCRYPTION_KEY,
|
|
82
|
+
BENCHMARK_VERSION: benchmarkTag,
|
|
83
|
+
K6_API_TOKEN: k6ApiToken,
|
|
84
|
+
BENCHMARK_RESULT_WEBHOOK_URL: resultWebhookUrl,
|
|
85
|
+
BENCHMARK_RESULT_WEBHOOK_AUTH_HEADER: resultWebhookAuthHeader,
|
|
86
|
+
RUN_DIR: runDir,
|
|
87
|
+
MOCK_API_DATA_PATH: paths.mockApiDataPath,
|
|
88
|
+
},
|
|
89
|
+
}),
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
// Run the setup script if it exists
|
|
93
|
+
if (fs.existsSync(setupScriptPath)) {
|
|
94
|
+
const setupScript = await import(setupScriptPath);
|
|
95
|
+
await setupScript.setup({ runDir });
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
try {
|
|
99
|
+
const runnerServices = await discoverRunnerServices(dockerComposeClient);
|
|
100
|
+
await dockerComposeClient.$('up', '-d', '--remove-orphans', 'n8n', ...runnerServices);
|
|
101
|
+
|
|
102
|
+
const tags = Object.entries({
|
|
103
|
+
Env: envTag,
|
|
104
|
+
N8nVersion: n8nTag,
|
|
105
|
+
N8nSetup: n8nSetupToUse,
|
|
106
|
+
})
|
|
107
|
+
.map(([key, value]) => `${key}=${value}`)
|
|
108
|
+
.join(',');
|
|
109
|
+
|
|
110
|
+
const cliArgs = flagsObjectToCliArgs({
|
|
111
|
+
scenarioNamePrefix: n8nSetupToUse,
|
|
112
|
+
scenarioFilter,
|
|
113
|
+
vus,
|
|
114
|
+
duration,
|
|
115
|
+
tags,
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
await dockerComposeClient.$('run', 'benchmark', 'run', ...cliArgs);
|
|
119
|
+
} catch (error) {
|
|
120
|
+
console.error('An error occurred while running the benchmarks:');
|
|
121
|
+
console.error(error.message);
|
|
122
|
+
console.error('');
|
|
123
|
+
await printContainerStatus(dockerComposeClient);
|
|
124
|
+
throw error;
|
|
125
|
+
} finally {
|
|
126
|
+
await dumpLogs(dockerComposeClient);
|
|
127
|
+
await dockerComposeClient.$('down');
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
async function printContainerStatus(dockerComposeClient) {
|
|
132
|
+
console.error('Container statuses:');
|
|
133
|
+
await dockerComposeClient.$('ps', '-a');
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
async function dumpLogs(dockerComposeClient) {
|
|
137
|
+
console.info('Container logs:');
|
|
138
|
+
await dockerComposeClient.$('logs');
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function printUsage() {
|
|
142
|
+
const availableSetups = getAllN8nSetups();
|
|
143
|
+
console.log('Usage: zx runForN8nSetup.mjs --runDir /path/for/n8n/data <n8n setup to use>');
|
|
144
|
+
console.log(` eg: zx runForN8nSetup.mjs --runDir /path/for/n8n/data ${availableSetups[0]}`);
|
|
145
|
+
console.log('');
|
|
146
|
+
console.log('Flags:');
|
|
147
|
+
console.log(
|
|
148
|
+
' --runDir <path> Directory to share with the n8n container for storing data. Default is /n8n',
|
|
149
|
+
);
|
|
150
|
+
console.log(' --n8nDockerTag <tag> Docker tag for n8n image. Default is latest');
|
|
151
|
+
console.log(
|
|
152
|
+
' --benchmarkDockerTag <tag> Docker tag for benchmark cli image. Default is latest',
|
|
153
|
+
);
|
|
154
|
+
console.log(' --k6ApiToken <token> K6 API token to upload the results');
|
|
155
|
+
console.log('');
|
|
156
|
+
console.log('Available setups:');
|
|
157
|
+
console.log(availableSetups.join(', '));
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* @returns {string[]}
|
|
162
|
+
*/
|
|
163
|
+
function getAllN8nSetups() {
|
|
164
|
+
return fs.readdirSync(paths.n8nSetupsDir);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function validateN8nSetup(givenSetup) {
|
|
168
|
+
const availableSetups = getAllN8nSetups();
|
|
169
|
+
if (!availableSetups.includes(givenSetup)) {
|
|
170
|
+
printUsage();
|
|
171
|
+
process.exit(1);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
main();
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
#!/usr/bin/env zx
|
|
2
|
+
/**
|
|
3
|
+
* Script to run benchmarks on the cloud benchmark environment.
|
|
4
|
+
* This script will:
|
|
5
|
+
* 1. Provision a benchmark environment using Terraform.
|
|
6
|
+
* 2. Run the benchmarks on the VM.
|
|
7
|
+
* 3. Destroy the cloud environment.
|
|
8
|
+
*
|
|
9
|
+
* NOTE: Must be run in the root of the package.
|
|
10
|
+
*/
|
|
11
|
+
// @ts-check
|
|
12
|
+
import { sleep, which, $, tmpdir } from 'zx';
|
|
13
|
+
import path from 'path';
|
|
14
|
+
import { SshClient } from './clients/ssh-client.mjs';
|
|
15
|
+
import { TerraformClient } from './clients/terraform-client.mjs';
|
|
16
|
+
import { flagsObjectToCliArgs } from './utils/flags.mjs';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* @typedef {Object} BenchmarkEnv
|
|
20
|
+
* @property {string} vmName
|
|
21
|
+
* @property {string} ip
|
|
22
|
+
* @property {string} sshUsername
|
|
23
|
+
* @property {string} sshPrivateKeyPath
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* @typedef {Object} Config
|
|
28
|
+
* @property {boolean} isVerbose
|
|
29
|
+
* @property {string[]} n8nSetupsToUse
|
|
30
|
+
* @property {string} n8nTag
|
|
31
|
+
* @property {string} benchmarkTag
|
|
32
|
+
* @property {string} [k6ApiToken]
|
|
33
|
+
* @property {string} [resultWebhookUrl]
|
|
34
|
+
* @property {string} [resultWebhookAuthHeader]
|
|
35
|
+
* @property {string} [n8nLicenseCert]
|
|
36
|
+
* @property {string} [vus]
|
|
37
|
+
* @property {string} [duration]
|
|
38
|
+
* @property {string} [scenarioFilter]
|
|
39
|
+
*
|
|
40
|
+
* @param {Config} config
|
|
41
|
+
*/
|
|
42
|
+
export async function runInCloud(config) {
|
|
43
|
+
await ensureDependencies();
|
|
44
|
+
|
|
45
|
+
const terraformClient = new TerraformClient({
|
|
46
|
+
isVerbose: config.isVerbose,
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
const benchmarkEnv = await terraformClient.getTerraformOutputs();
|
|
50
|
+
|
|
51
|
+
await runBenchmarksOnVm(config, benchmarkEnv);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async function ensureDependencies() {
|
|
55
|
+
await which('terraform');
|
|
56
|
+
await which('az');
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* @param {Config} config
|
|
61
|
+
* @param {BenchmarkEnv} benchmarkEnv
|
|
62
|
+
*/
|
|
63
|
+
async function runBenchmarksOnVm(config, benchmarkEnv) {
|
|
64
|
+
console.log(`Setting up the environment...`);
|
|
65
|
+
|
|
66
|
+
const sshClient = new SshClient({
|
|
67
|
+
ip: benchmarkEnv.ip,
|
|
68
|
+
username: benchmarkEnv.sshUsername,
|
|
69
|
+
privateKeyPath: benchmarkEnv.sshPrivateKeyPath,
|
|
70
|
+
verbose: config.isVerbose,
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
await ensureVmIsReachable(sshClient);
|
|
74
|
+
|
|
75
|
+
const scriptsDir = await transferScriptsToVm(sshClient, config);
|
|
76
|
+
|
|
77
|
+
// Bootstrap the environment with dependencies
|
|
78
|
+
console.log('Running bootstrap script...');
|
|
79
|
+
const bootstrapScriptPath = path.join(scriptsDir, 'bootstrap.sh');
|
|
80
|
+
await sshClient.ssh(`chmod a+x ${bootstrapScriptPath} && ${bootstrapScriptPath}`);
|
|
81
|
+
|
|
82
|
+
// Give some time for the VM to be ready
|
|
83
|
+
await sleep(1000);
|
|
84
|
+
|
|
85
|
+
const failures = [];
|
|
86
|
+
|
|
87
|
+
for (const n8nSetup of config.n8nSetupsToUse) {
|
|
88
|
+
try {
|
|
89
|
+
await runBenchmarkForN8nSetup({
|
|
90
|
+
config,
|
|
91
|
+
sshClient,
|
|
92
|
+
scriptsDir,
|
|
93
|
+
n8nSetup,
|
|
94
|
+
});
|
|
95
|
+
} catch (error) {
|
|
96
|
+
console.error(`Benchmark failed for ${n8nSetup}:`, error.message);
|
|
97
|
+
failures.push(n8nSetup);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (failures.length > 0) {
|
|
102
|
+
throw new Error(`Benchmarks failed for setups: ${failures.join(', ')}`);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* @param {{ config: Config; sshClient: any; scriptsDir: string; n8nSetup: string; }} opts
|
|
108
|
+
*/
|
|
109
|
+
async function runBenchmarkForN8nSetup({ config, sshClient, scriptsDir, n8nSetup }) {
|
|
110
|
+
console.log(`Running benchmarks for ${n8nSetup}...`);
|
|
111
|
+
const runScriptPath = path.join(scriptsDir, 'run-for-n8n-setup.mjs');
|
|
112
|
+
|
|
113
|
+
const cliArgs = flagsObjectToCliArgs({
|
|
114
|
+
n8nDockerTag: config.n8nTag,
|
|
115
|
+
benchmarkDockerTag: config.benchmarkTag,
|
|
116
|
+
k6ApiToken: config.k6ApiToken,
|
|
117
|
+
resultWebhookUrl: config.resultWebhookUrl,
|
|
118
|
+
resultWebhookAuthHeader: config.resultWebhookAuthHeader,
|
|
119
|
+
n8nLicenseCert: config.n8nLicenseCert,
|
|
120
|
+
scenarioFilter: config.scenarioFilter,
|
|
121
|
+
vus: config.vus,
|
|
122
|
+
duration: config.duration,
|
|
123
|
+
env: 'cloud',
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
const flagsString = cliArgs.join(' ');
|
|
127
|
+
|
|
128
|
+
await sshClient.ssh(`npx zx ${runScriptPath} ${flagsString} ${n8nSetup}`, {
|
|
129
|
+
// Test run should always log its output
|
|
130
|
+
verbose: true,
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
async function ensureVmIsReachable(sshClient) {
|
|
135
|
+
try {
|
|
136
|
+
await sshClient.ssh('echo "VM is reachable"');
|
|
137
|
+
} catch (error) {
|
|
138
|
+
console.error(`VM is not reachable: ${error.message}`);
|
|
139
|
+
console.error(
|
|
140
|
+
`Did you provision the cloud environment first with 'pnpm provision-cloud-env'? You can also run the benchmarks locally with 'pnpm run benchmark-locally'.`,
|
|
141
|
+
);
|
|
142
|
+
process.exit(1);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* @returns Path where the scripts are located on the VM
|
|
148
|
+
*/
|
|
149
|
+
async function transferScriptsToVm(sshClient, config) {
|
|
150
|
+
const cwd = process.cwd();
|
|
151
|
+
const scriptsDir = path.resolve(cwd, './scripts');
|
|
152
|
+
const tarFilename = 'scripts.tar.gz';
|
|
153
|
+
const scriptsTarPath = path.join(tmpdir('n8n-benchmark'), tarFilename);
|
|
154
|
+
|
|
155
|
+
const $$ = $({ verbose: config.isVerbose });
|
|
156
|
+
|
|
157
|
+
// Compress the scripts folder
|
|
158
|
+
await $$`tar -czf ${scriptsTarPath} ${scriptsDir} -C ${cwd} ./scripts`;
|
|
159
|
+
|
|
160
|
+
// Transfer the scripts to the VM
|
|
161
|
+
await sshClient.scp(scriptsTarPath, `~/${tarFilename}`);
|
|
162
|
+
|
|
163
|
+
// Extract the scripts on the VM
|
|
164
|
+
await sshClient.ssh(`tar -xzf ~/${tarFilename}`);
|
|
165
|
+
|
|
166
|
+
return '~/scripts';
|
|
167
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
#!/usr/bin/env zx
|
|
2
|
+
/**
|
|
3
|
+
* Script to run benchmarks on the cloud benchmark environment.
|
|
4
|
+
* This script will:
|
|
5
|
+
* 1. Provision a benchmark environment using Terraform.
|
|
6
|
+
* 2. Run the benchmarks on the VM.
|
|
7
|
+
* 3. Destroy the cloud environment.
|
|
8
|
+
*
|
|
9
|
+
* NOTE: Must be run in the root of the package.
|
|
10
|
+
*/
|
|
11
|
+
// @ts-check
|
|
12
|
+
import { $ } from 'zx';
|
|
13
|
+
import path from 'path';
|
|
14
|
+
import { flagsObjectToCliArgs } from './utils/flags.mjs';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* @typedef {Object} BenchmarkEnv
|
|
18
|
+
* @property {string} vmName
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
const paths = {
|
|
22
|
+
scriptsDir: path.join(path.resolve('scripts')),
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* @typedef {Object} Config
|
|
27
|
+
* @property {boolean} isVerbose
|
|
28
|
+
* @property {string[]} n8nSetupsToUse
|
|
29
|
+
* @property {string} n8nTag
|
|
30
|
+
* @property {string} benchmarkTag
|
|
31
|
+
* @property {string} [runDir]
|
|
32
|
+
* @property {string} [k6ApiToken]
|
|
33
|
+
* @property {string} [resultWebhookUrl]
|
|
34
|
+
* @property {string} [resultWebhookAuthHeader]
|
|
35
|
+
* @property {string} [n8nLicenseCert]
|
|
36
|
+
* @property {string} [vus]
|
|
37
|
+
* @property {string} [duration]
|
|
38
|
+
* @property {string} [scenarioFilter]
|
|
39
|
+
*
|
|
40
|
+
* @param {Config} config
|
|
41
|
+
*/
|
|
42
|
+
export async function runLocally(config) {
|
|
43
|
+
const runScriptPath = path.join(paths.scriptsDir, 'run-for-n8n-setup.mjs');
|
|
44
|
+
|
|
45
|
+
const cliArgs = flagsObjectToCliArgs({
|
|
46
|
+
n8nDockerTag: config.n8nTag,
|
|
47
|
+
benchmarkDockerTag: config.benchmarkTag,
|
|
48
|
+
runDir: config.runDir,
|
|
49
|
+
vus: config.vus,
|
|
50
|
+
duration: config.duration,
|
|
51
|
+
scenarioFilter: config.scenarioFilter,
|
|
52
|
+
env: 'local',
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
try {
|
|
56
|
+
for (const n8nSetup of config.n8nSetupsToUse) {
|
|
57
|
+
console.log(`Running benchmarks for n8n setup: ${n8nSetup}`);
|
|
58
|
+
|
|
59
|
+
await $({
|
|
60
|
+
env: {
|
|
61
|
+
...process.env,
|
|
62
|
+
K6_API_TOKEN: config.k6ApiToken,
|
|
63
|
+
BENCHMARK_RESULT_WEBHOOK_URL: config.resultWebhookUrl,
|
|
64
|
+
BENCHMARK_RESULT_WEBHOOK_AUTH_HEADER: config.resultWebhookAuthHeader,
|
|
65
|
+
N8N_LICENSE_CERT: config.n8nLicenseCert,
|
|
66
|
+
},
|
|
67
|
+
})`npx ${runScriptPath} ${cliArgs} ${n8nSetup}`;
|
|
68
|
+
}
|
|
69
|
+
} catch (error) {
|
|
70
|
+
console.error('An error occurred while running the benchmarks:');
|
|
71
|
+
console.error(error);
|
|
72
|
+
}
|
|
73
|
+
}
|
package/scripts/run.mjs
ADDED
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
#!/usr/bin/env zx
|
|
2
|
+
/**
|
|
3
|
+
* Script to run benchmarks either on the cloud benchmark environment or locally.
|
|
4
|
+
* The cloud environment needs to be provisioned using Terraform before running the benchmarks.
|
|
5
|
+
*
|
|
6
|
+
* NOTE: Must be run in the root of the package.
|
|
7
|
+
*/
|
|
8
|
+
// @ts-check
|
|
9
|
+
import fs from 'fs';
|
|
10
|
+
import minimist from 'minimist';
|
|
11
|
+
import path from 'path';
|
|
12
|
+
import { runInCloud } from './run-in-cloud.mjs';
|
|
13
|
+
import { runLocally } from './run-locally.mjs';
|
|
14
|
+
|
|
15
|
+
const paths = {
|
|
16
|
+
n8nSetupsDir: path.join(path.resolve('scripts'), 'n8n-setups'),
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
async function main() {
|
|
20
|
+
const config = await parseAndValidateConfig();
|
|
21
|
+
|
|
22
|
+
const n8nSetupsToUse =
|
|
23
|
+
config.n8nSetupToUse === 'all' ? readAvailableN8nSetups() : [config.n8nSetupToUse];
|
|
24
|
+
|
|
25
|
+
console.log('Using n8n tag', config.n8nTag);
|
|
26
|
+
console.log('Using benchmark cli tag', config.benchmarkTag);
|
|
27
|
+
console.log('Using environment', config.env);
|
|
28
|
+
console.log('Using n8n setups', n8nSetupsToUse.join(', '));
|
|
29
|
+
console.log('');
|
|
30
|
+
|
|
31
|
+
if (config.env === 'cloud') {
|
|
32
|
+
await runInCloud({
|
|
33
|
+
benchmarkTag: config.benchmarkTag,
|
|
34
|
+
isVerbose: config.isVerbose,
|
|
35
|
+
k6ApiToken: config.k6ApiToken,
|
|
36
|
+
resultWebhookUrl: config.resultWebhookUrl,
|
|
37
|
+
resultWebhookAuthHeader: config.resultWebhookAuthHeader,
|
|
38
|
+
n8nLicenseCert: config.n8nLicenseCert,
|
|
39
|
+
n8nTag: config.n8nTag,
|
|
40
|
+
n8nSetupsToUse,
|
|
41
|
+
vus: config.vus,
|
|
42
|
+
duration: config.duration,
|
|
43
|
+
scenarioFilter: config.scenarioFilter,
|
|
44
|
+
});
|
|
45
|
+
} else if (config.env === 'local') {
|
|
46
|
+
await runLocally({
|
|
47
|
+
benchmarkTag: config.benchmarkTag,
|
|
48
|
+
isVerbose: config.isVerbose,
|
|
49
|
+
k6ApiToken: config.k6ApiToken,
|
|
50
|
+
resultWebhookUrl: config.resultWebhookUrl,
|
|
51
|
+
resultWebhookAuthHeader: config.resultWebhookAuthHeader,
|
|
52
|
+
n8nLicenseCert: config.n8nLicenseCert,
|
|
53
|
+
n8nTag: config.n8nTag,
|
|
54
|
+
runDir: config.runDir,
|
|
55
|
+
n8nSetupsToUse,
|
|
56
|
+
vus: config.vus,
|
|
57
|
+
duration: config.duration,
|
|
58
|
+
scenarioFilter: config.scenarioFilter,
|
|
59
|
+
});
|
|
60
|
+
} else {
|
|
61
|
+
console.error('Invalid env:', config.env);
|
|
62
|
+
printUsage();
|
|
63
|
+
process.exit(1);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function readAvailableN8nSetups() {
|
|
68
|
+
const setups = fs.readdirSync(paths.n8nSetupsDir);
|
|
69
|
+
|
|
70
|
+
return setups;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* @typedef {Object} Config
|
|
75
|
+
* @property {boolean} isVerbose
|
|
76
|
+
* @property {'cloud' | 'local'} env
|
|
77
|
+
* @property {string} n8nSetupToUse
|
|
78
|
+
* @property {string} n8nTag
|
|
79
|
+
* @property {string} benchmarkTag
|
|
80
|
+
* @property {string} [k6ApiToken]
|
|
81
|
+
* @property {string} [resultWebhookUrl]
|
|
82
|
+
* @property {string} [resultWebhookAuthHeader]
|
|
83
|
+
* @property {string} [n8nLicenseCert]
|
|
84
|
+
* @property {string} [runDir]
|
|
85
|
+
* @property {string} [vus]
|
|
86
|
+
* @property {string} [duration]
|
|
87
|
+
* @property {string} [scenarioFilter]
|
|
88
|
+
*
|
|
89
|
+
* @returns {Promise<Config>}
|
|
90
|
+
*/
|
|
91
|
+
async function parseAndValidateConfig() {
|
|
92
|
+
const args = minimist(process.argv.slice(3), {
|
|
93
|
+
boolean: ['debug', 'help'],
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
if (args.help) {
|
|
97
|
+
printUsage();
|
|
98
|
+
process.exit(0);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const n8nSetupToUse = await getAndValidateN8nSetup(args);
|
|
102
|
+
const isVerbose = args.debug || false;
|
|
103
|
+
const n8nTag = args.n8nTag || process.env.N8N_DOCKER_TAG || 'latest';
|
|
104
|
+
const benchmarkTag = args.benchmarkTag || process.env.BENCHMARK_DOCKER_TAG || 'latest';
|
|
105
|
+
const k6ApiToken = args.k6ApiToken || process.env.K6_API_TOKEN || undefined;
|
|
106
|
+
const resultWebhookUrl =
|
|
107
|
+
args.resultWebhookUrl || process.env.BENCHMARK_RESULT_WEBHOOK_URL || undefined;
|
|
108
|
+
const resultWebhookAuthHeader =
|
|
109
|
+
args.resultWebhookAuthHeader || process.env.BENCHMARK_RESULT_WEBHOOK_AUTH_HEADER || undefined;
|
|
110
|
+
const n8nLicenseCert = args.n8nLicenseCert || process.env.N8N_LICENSE_CERT || undefined;
|
|
111
|
+
const runDir = args.runDir || undefined;
|
|
112
|
+
const env = args.env || 'local';
|
|
113
|
+
const vus = args.vus;
|
|
114
|
+
const duration = args.duration;
|
|
115
|
+
const scenarioFilter = args.scenarioFilter;
|
|
116
|
+
|
|
117
|
+
if (!env) {
|
|
118
|
+
printUsage();
|
|
119
|
+
process.exit(1);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return {
|
|
123
|
+
isVerbose,
|
|
124
|
+
env,
|
|
125
|
+
n8nSetupToUse,
|
|
126
|
+
n8nTag,
|
|
127
|
+
benchmarkTag,
|
|
128
|
+
k6ApiToken,
|
|
129
|
+
resultWebhookUrl,
|
|
130
|
+
resultWebhookAuthHeader,
|
|
131
|
+
n8nLicenseCert,
|
|
132
|
+
runDir,
|
|
133
|
+
vus,
|
|
134
|
+
duration,
|
|
135
|
+
scenarioFilter,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* @param {minimist.ParsedArgs} args
|
|
141
|
+
*/
|
|
142
|
+
async function getAndValidateN8nSetup(args) {
|
|
143
|
+
// Last parameter is the n8n setup to use
|
|
144
|
+
const n8nSetupToUse = args._[args._.length - 1];
|
|
145
|
+
if (!n8nSetupToUse || n8nSetupToUse === 'all') {
|
|
146
|
+
return 'all';
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const availableSetups = readAvailableN8nSetups();
|
|
150
|
+
|
|
151
|
+
if (!availableSetups.includes(n8nSetupToUse)) {
|
|
152
|
+
printUsage();
|
|
153
|
+
process.exit(1);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return n8nSetupToUse;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function printUsage() {
|
|
160
|
+
const availableSetups = readAvailableN8nSetups();
|
|
161
|
+
|
|
162
|
+
console.log(`Usage: zx scripts/${path.basename(__filename)} [n8n setup name]`);
|
|
163
|
+
console.log(` eg: zx scripts/${path.basename(__filename)}`);
|
|
164
|
+
console.log('');
|
|
165
|
+
console.log('Options:');
|
|
166
|
+
console.log(
|
|
167
|
+
` [n8n setup name] Against which n8n setup to run the benchmarks. One of: ${['all', ...availableSetups].join(', ')}. Default is all`,
|
|
168
|
+
);
|
|
169
|
+
console.log(
|
|
170
|
+
' --env Env where to run the benchmarks. Either cloud or local. Default is local.',
|
|
171
|
+
);
|
|
172
|
+
console.log(' --debug Enable verbose output');
|
|
173
|
+
console.log(' --n8nTag Docker tag for n8n image. Default is latest');
|
|
174
|
+
console.log(' --benchmarkTag Docker tag for benchmark cli image. Default is latest');
|
|
175
|
+
console.log(' --scenarioFilter Filter scenarios by name (case-insensitive)');
|
|
176
|
+
console.log(' --vus How many concurrent requests to make');
|
|
177
|
+
console.log(' --duration Test duration, e.g. 1m or 30s');
|
|
178
|
+
console.log(
|
|
179
|
+
' --k6ApiToken API token for k6 cloud. Default is read from K6_API_TOKEN env var. If omitted, k6 cloud will not be used',
|
|
180
|
+
);
|
|
181
|
+
console.log(
|
|
182
|
+
' --runDir Directory to share with the n8n container for storing data. Needed only for local runs.',
|
|
183
|
+
);
|
|
184
|
+
console.log('');
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
main().catch((error) => {
|
|
188
|
+
console.error('An error occurred while running the benchmarks:');
|
|
189
|
+
console.error(error);
|
|
190
|
+
|
|
191
|
+
process.exit(1);
|
|
192
|
+
});
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Converts an object of flags to an array of CLI arguments.
|
|
5
|
+
*
|
|
6
|
+
* @param {Record<string, string | undefined>} flags
|
|
7
|
+
*
|
|
8
|
+
* @returns {string[]}
|
|
9
|
+
*/
|
|
10
|
+
export function flagsObjectToCliArgs(flags) {
|
|
11
|
+
return Object.entries(flags)
|
|
12
|
+
.filter(([, value]) => value !== undefined)
|
|
13
|
+
.map(([key, value]) => {
|
|
14
|
+
if (typeof value === 'string' && value.includes(' ')) {
|
|
15
|
+
return `--${key}="${value}"`;
|
|
16
|
+
} else {
|
|
17
|
+
return `--${key}=${value}`;
|
|
18
|
+
}
|
|
19
|
+
});
|
|
20
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { Command } from '@oclif/core';
|
|
2
|
+
|
|
3
|
+
import { testScenariosPath } from '@/config/common-flags';
|
|
4
|
+
import { ScenarioLoader } from '@/scenario/scenario-loader';
|
|
5
|
+
|
|
6
|
+
export default class ListCommand extends Command {
|
|
7
|
+
static description = 'List all available scenarios';
|
|
8
|
+
|
|
9
|
+
static flags = {
|
|
10
|
+
testScenariosPath,
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
async run() {
|
|
14
|
+
const { flags } = await this.parse(ListCommand);
|
|
15
|
+
const scenarioLoader = new ScenarioLoader();
|
|
16
|
+
|
|
17
|
+
const allScenarios = scenarioLoader.loadAll(flags.testScenariosPath);
|
|
18
|
+
|
|
19
|
+
console.log('Available test scenarios:');
|
|
20
|
+
console.log('');
|
|
21
|
+
|
|
22
|
+
for (const scenario of allScenarios) {
|
|
23
|
+
console.log('\t', scenario.name, ':', scenario.description);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|