@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.
Files changed (158) hide show
  1. package/.turbo/turbo-build.log +4 -0
  2. package/Dockerfile +63 -0
  3. package/README.md +122 -0
  4. package/bin/n8n-benchmark +13 -0
  5. package/biome.jsonc +7 -0
  6. package/dist/build.tsbuildinfo +1 -0
  7. package/dist/commands/list.d.ts +8 -0
  8. package/dist/commands/list.js +23 -0
  9. package/dist/commands/list.js.map +1 -0
  10. package/dist/commands/run.d.ts +24 -0
  11. package/dist/commands/run.js +128 -0
  12. package/dist/commands/run.js.map +1 -0
  13. package/dist/config/common-flags.d.ts +1 -0
  14. package/dist/config/common-flags.js +9 -0
  15. package/dist/config/common-flags.js.map +1 -0
  16. package/dist/n8n-api-client/authenticated-n8n-api-client.d.ts +15 -0
  17. package/dist/n8n-api-client/authenticated-n8n-api-client.js +67 -0
  18. package/dist/n8n-api-client/authenticated-n8n-api-client.js.map +1 -0
  19. package/dist/n8n-api-client/credentials-api-client.d.ts +9 -0
  20. package/dist/n8n-api-client/credentials-api-client.js +24 -0
  21. package/dist/n8n-api-client/credentials-api-client.js.map +1 -0
  22. package/dist/n8n-api-client/data-table-api-client.d.ts +9 -0
  23. package/dist/n8n-api-client/data-table-api-client.js +23 -0
  24. package/dist/n8n-api-client/data-table-api-client.js.map +1 -0
  25. package/dist/n8n-api-client/n8n-api-client.d.ts +13 -0
  26. package/dist/n8n-api-client/n8n-api-client.js +82 -0
  27. package/dist/n8n-api-client/n8n-api-client.js.map +1 -0
  28. package/dist/n8n-api-client/n8n-api-client.types.d.ts +21 -0
  29. package/dist/n8n-api-client/n8n-api-client.types.js +3 -0
  30. package/dist/n8n-api-client/n8n-api-client.types.js.map +1 -0
  31. package/dist/n8n-api-client/project-api-client.d.ts +6 -0
  32. package/dist/n8n-api-client/project-api-client.js +14 -0
  33. package/dist/n8n-api-client/project-api-client.js.map +1 -0
  34. package/dist/n8n-api-client/workflows-api-client.d.ts +11 -0
  35. package/dist/n8n-api-client/workflows-api-client.js +30 -0
  36. package/dist/n8n-api-client/workflows-api-client.js.map +1 -0
  37. package/dist/scenario/scenario-data-loader.d.ts +13 -0
  38. package/dist/scenario/scenario-data-loader.js +84 -0
  39. package/dist/scenario/scenario-data-loader.js.map +1 -0
  40. package/dist/scenario/scenario-loader.d.ts +7 -0
  41. package/dist/scenario/scenario-loader.js +101 -0
  42. package/dist/scenario/scenario-loader.js.map +1 -0
  43. package/dist/test-execution/app-metrics-poller.d.ts +13 -0
  44. package/dist/test-execution/app-metrics-poller.js +54 -0
  45. package/dist/test-execution/app-metrics-poller.js.map +1 -0
  46. package/dist/test-execution/k6-executor.d.ts +33 -0
  47. package/dist/test-execution/k6-executor.js +120 -0
  48. package/dist/test-execution/k6-executor.js.map +1 -0
  49. package/dist/test-execution/k6-summary.d.ts +82 -0
  50. package/dist/test-execution/k6-summary.js +2 -0
  51. package/dist/test-execution/k6-summary.js.map +1 -0
  52. package/dist/test-execution/prometheus-metrics-parser.d.ts +9 -0
  53. package/dist/test-execution/prometheus-metrics-parser.js +44 -0
  54. package/dist/test-execution/prometheus-metrics-parser.js.map +1 -0
  55. package/dist/test-execution/scenario-data-importer.d.ts +21 -0
  56. package/dist/test-execution/scenario-data-importer.js +108 -0
  57. package/dist/test-execution/scenario-data-importer.js.map +1 -0
  58. package/dist/test-execution/scenario-runner.d.ts +18 -0
  59. package/dist/test-execution/scenario-runner.js +46 -0
  60. package/dist/test-execution/scenario-runner.js.map +1 -0
  61. package/dist/test-execution/test-report.d.ts +56 -0
  62. package/dist/test-execution/test-report.js +65 -0
  63. package/dist/test-execution/test-report.js.map +1 -0
  64. package/dist/types/scenario.d.ts +16 -0
  65. package/dist/types/scenario.js +3 -0
  66. package/dist/types/scenario.js.map +1 -0
  67. package/eslint.config.mjs +22 -0
  68. package/infra/.terraform.lock.hcl +60 -0
  69. package/infra/benchmark-env.tf +54 -0
  70. package/infra/modules/benchmark-vm/output.tf +11 -0
  71. package/infra/modules/benchmark-vm/vars.tf +29 -0
  72. package/infra/modules/benchmark-vm/vm.tf +126 -0
  73. package/infra/output.tf +16 -0
  74. package/infra/providers.tf +23 -0
  75. package/infra/vars.tf +34 -0
  76. package/package.json +55 -0
  77. package/scenarios/binary-data/binary-data.json +67 -0
  78. package/scenarios/binary-data/binary-data.manifest.json +7 -0
  79. package/scenarios/binary-data/binary-data.script.js +29 -0
  80. package/scenarios/credential-http-node/credential-bearer.json +8 -0
  81. package/scenarios/credential-http-node/credential-http-node.json +241 -0
  82. package/scenarios/credential-http-node/credential-http-node.manifest.json +10 -0
  83. package/scenarios/credential-http-node/credential-http-node.script.js +30 -0
  84. package/scenarios/data-table-node/data-table-node.json +168 -0
  85. package/scenarios/data-table-node/data-table-node.manifest.json +10 -0
  86. package/scenarios/data-table-node/data-table-node.script.js +38 -0
  87. package/scenarios/data-table-node/data-table.json +25 -0
  88. package/scenarios/http-node/http-node.json +213 -0
  89. package/scenarios/http-node/http-node.manifest.json +7 -0
  90. package/scenarios/http-node/http-node.script.js +30 -0
  91. package/scenarios/js-code-node/js-code-node.json +96 -0
  92. package/scenarios/js-code-node/js-code-node.manifest.json +7 -0
  93. package/scenarios/js-code-node/js-code-node.script.js +29 -0
  94. package/scenarios/multiple-webhooks/multiple-webhooks.manifest.json +20 -0
  95. package/scenarios/multiple-webhooks/multiple-webhooks.script.js +19 -0
  96. package/scenarios/multiple-webhooks/multiple-webhooks1.json +25 -0
  97. package/scenarios/multiple-webhooks/multiple-webhooks10.json +25 -0
  98. package/scenarios/multiple-webhooks/multiple-webhooks2.json +25 -0
  99. package/scenarios/multiple-webhooks/multiple-webhooks3.json +25 -0
  100. package/scenarios/multiple-webhooks/multiple-webhooks4.json +25 -0
  101. package/scenarios/multiple-webhooks/multiple-webhooks5.json +25 -0
  102. package/scenarios/multiple-webhooks/multiple-webhooks6.json +25 -0
  103. package/scenarios/multiple-webhooks/multiple-webhooks7.json +25 -0
  104. package/scenarios/multiple-webhooks/multiple-webhooks8.json +25 -0
  105. package/scenarios/multiple-webhooks/multiple-webhooks9.json +25 -0
  106. package/scenarios/py-code-node/py-code-node.json +98 -0
  107. package/scenarios/py-code-node/py-code-node.manifest.json +7 -0
  108. package/scenarios/py-code-node/py-code-node.script.js +29 -0
  109. package/scenarios/scenario.schema.json +51 -0
  110. package/scenarios/set-node-expressions/set-node-expressions.json +91 -0
  111. package/scenarios/set-node-expressions/set-node-expressions.manifest.json +7 -0
  112. package/scenarios/set-node-expressions/set-node-expressions.script.js +18 -0
  113. package/scenarios/single-webhook/single-webhook.json +25 -0
  114. package/scenarios/single-webhook/single-webhook.manifest.json +7 -0
  115. package/scenarios/single-webhook/single-webhook.script.js +18 -0
  116. package/scripts/bootstrap.sh +63 -0
  117. package/scripts/clients/docker-compose-client.mjs +45 -0
  118. package/scripts/clients/ssh-client.mjs +37 -0
  119. package/scripts/clients/terraform-client.mjs +71 -0
  120. package/scripts/destroy-cloud-env.mjs +86 -0
  121. package/scripts/mock-api/mappings/mockApiData.json +92110 -0
  122. package/scripts/n8n-setups/postgres/docker-compose.yml +76 -0
  123. package/scripts/n8n-setups/postgres/setup.mjs +15 -0
  124. package/scripts/n8n-setups/scaling-multi-main/docker-compose.yml +230 -0
  125. package/scripts/n8n-setups/scaling-multi-main/nginx.conf +24 -0
  126. package/scripts/n8n-setups/scaling-multi-main/setup.mjs +15 -0
  127. package/scripts/n8n-setups/scaling-single-main/docker-compose.yml +174 -0
  128. package/scripts/n8n-setups/scaling-single-main/setup.mjs +15 -0
  129. package/scripts/n8n-setups/sqlite/docker-compose.yml +55 -0
  130. package/scripts/n8n-setups/sqlite/setup.mjs +15 -0
  131. package/scripts/provision-cloud-env.mjs +36 -0
  132. package/scripts/run-for-n8n-setup.mjs +175 -0
  133. package/scripts/run-in-cloud.mjs +167 -0
  134. package/scripts/run-locally.mjs +73 -0
  135. package/scripts/run.mjs +192 -0
  136. package/scripts/utils/flags.mjs +20 -0
  137. package/src/commands/list.ts +26 -0
  138. package/src/commands/run.ts +140 -0
  139. package/src/config/common-flags.ts +6 -0
  140. package/src/n8n-api-client/authenticated-n8n-api-client.ts +88 -0
  141. package/src/n8n-api-client/credentials-api-client.ts +28 -0
  142. package/src/n8n-api-client/data-table-api-client.ts +30 -0
  143. package/src/n8n-api-client/n8n-api-client.ts +85 -0
  144. package/src/n8n-api-client/n8n-api-client.types.ts +27 -0
  145. package/src/n8n-api-client/project-api-client.ts +11 -0
  146. package/src/n8n-api-client/workflows-api-client.ts +38 -0
  147. package/src/scenario/scenario-data-loader.ts +75 -0
  148. package/src/scenario/scenario-loader.ts +90 -0
  149. package/src/test-execution/app-metrics-poller.ts +81 -0
  150. package/src/test-execution/k6-executor.ts +192 -0
  151. package/src/test-execution/k6-summary.ts +255 -0
  152. package/src/test-execution/prometheus-metrics-parser.ts +63 -0
  153. package/src/test-execution/scenario-data-importer.ts +165 -0
  154. package/src/test-execution/scenario-runner.ts +76 -0
  155. package/src/test-execution/test-report.ts +152 -0
  156. package/src/types/scenario.ts +33 -0
  157. package/tsconfig.build.json +9 -0
  158. 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
+ }
@@ -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
+ }