@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,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"createdAt": "2024-08-06T12:19:51.268Z",
|
|
3
|
+
"updatedAt": "2024-08-06T12:20:45.000Z",
|
|
4
|
+
"name": "Multiple Webhook 9",
|
|
5
|
+
"active": true,
|
|
6
|
+
"nodes": [
|
|
7
|
+
{
|
|
8
|
+
"parameters": { "path": "multiple-webhook9", "options": {} },
|
|
9
|
+
"id": "34ac4500-9a29-4f4f-a604-134aa2cb2889",
|
|
10
|
+
"name": "Webhook",
|
|
11
|
+
"type": "n8n-nodes-base.webhook",
|
|
12
|
+
"typeVersion": 2,
|
|
13
|
+
"position": [760, 400],
|
|
14
|
+
"webhookId": "47fe25ac-1376-4ee7-b9de-3fff1d49281c"
|
|
15
|
+
}
|
|
16
|
+
],
|
|
17
|
+
"connections": {},
|
|
18
|
+
"settings": { "executionOrder": "v1" },
|
|
19
|
+
"staticData": null,
|
|
20
|
+
"meta": { "templateCredsSetupCompleted": true, "responseMode": "lastNode", "options": {} },
|
|
21
|
+
"pinData": {},
|
|
22
|
+
"versionId": "106d4d2c-49c0-45b8-8421-99cc0c8b1589",
|
|
23
|
+
"triggerCount": 1,
|
|
24
|
+
"tags": []
|
|
25
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
{
|
|
2
|
+
"createdAt": "2024-08-06T12:19:51.268Z",
|
|
3
|
+
"updatedAt": "2024-08-06T12:20:45.000Z",
|
|
4
|
+
"name": "Python Code Node",
|
|
5
|
+
"active": true,
|
|
6
|
+
"nodes": [
|
|
7
|
+
{
|
|
8
|
+
"parameters": {
|
|
9
|
+
"respondWith": "allIncomingItems",
|
|
10
|
+
"options": {}
|
|
11
|
+
},
|
|
12
|
+
"type": "n8n-nodes-base.respondToWebhook",
|
|
13
|
+
"typeVersion": 1.1,
|
|
14
|
+
"position": [1280, 460],
|
|
15
|
+
"id": "0067e317-09b8-478a-8c50-e19b4c9e294c",
|
|
16
|
+
"name": "Respond to Webhook"
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
"parameters": {
|
|
20
|
+
"language": "pythonNative",
|
|
21
|
+
"mode": "runOnceForEachItem",
|
|
22
|
+
"pythonCode": "def pseudo_random(seed_str, max_val):\n return hash(seed_str) % max_val\n\n# Add new field\n_item['json']['age'] = 10 + pseudo_random(str(_item['json']['email']), 30)\n\n# Mutate existing field\n_item['json']['password'] = '*' * len(_item['json']['password'])\n\n# Remove field\nif 'lastname' in _item['json']:\n del _item['json']['lastname']\n\n# New object field\nemail_parts = _item['json']['email'].split('@')\n_item['json']['emailData'] = {\n 'user': email_parts[0],\n 'domain': email_parts[1]\n}\n\nreturn _item"
|
|
23
|
+
},
|
|
24
|
+
"type": "n8n-nodes-base.code",
|
|
25
|
+
"typeVersion": 2,
|
|
26
|
+
"position": [1040, 460],
|
|
27
|
+
"id": "56d751c0-0d30-43c3-89fa-bebf3a9d436f",
|
|
28
|
+
"name": "OnceForEachItemPythonCode"
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
"parameters": {
|
|
32
|
+
"httpMethod": "POST",
|
|
33
|
+
"path": "py-code-node-benchmark",
|
|
34
|
+
"responseMode": "responseNode",
|
|
35
|
+
"options": {}
|
|
36
|
+
},
|
|
37
|
+
"type": "n8n-nodes-base.webhook",
|
|
38
|
+
"typeVersion": 2,
|
|
39
|
+
"position": [580, 460],
|
|
40
|
+
"id": "417d749d-156c-4ffe-86ea-336f702dc5da",
|
|
41
|
+
"name": "Webhook",
|
|
42
|
+
"webhookId": "34ca1895-ccf4-4a4a-8bb8-a042f5edb567"
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
"parameters": {
|
|
46
|
+
"language": "pythonNative",
|
|
47
|
+
"pythonCode": "def pseudo_random_string(length):\n characters = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'\n result = ''\n seed = hash(str(length)) % 1000\n for i in range(length):\n index = (hash(str(seed + i)) % len(characters))\n result += characters[index]\n seed = (seed * 31 + i) % 1000\n return result\n\ndef random_uid():\n lengths = [8, 4, 4, 4, 8]\n parts = [pseudo_random_string(length) for length in lengths]\n return '-'.join(parts)\n\ndef random_email():\n return f\"{pseudo_random_string(8)}@{pseudo_random_string(10)}.com\"\n\ndef random_person():\n return {\n 'uid': random_uid(),\n 'email': random_email(),\n 'firstname': pseudo_random_string(5),\n 'lastname': pseudo_random_string(12),\n 'password': pseudo_random_string(10)\n }\n\nreturn [{'json': random_person()} for _ in range(100)]"
|
|
48
|
+
},
|
|
49
|
+
"id": "c30db155-73ca-48b9-8860-c3fe7a0926fb",
|
|
50
|
+
"name": "Code",
|
|
51
|
+
"type": "n8n-nodes-base.code",
|
|
52
|
+
"typeVersion": 2,
|
|
53
|
+
"position": [820, 460]
|
|
54
|
+
}
|
|
55
|
+
],
|
|
56
|
+
"connections": {
|
|
57
|
+
"OnceForEachItemPythonCode": {
|
|
58
|
+
"main": [
|
|
59
|
+
[
|
|
60
|
+
{
|
|
61
|
+
"node": "Respond to Webhook",
|
|
62
|
+
"type": "main",
|
|
63
|
+
"index": 0
|
|
64
|
+
}
|
|
65
|
+
]
|
|
66
|
+
]
|
|
67
|
+
},
|
|
68
|
+
"Webhook": {
|
|
69
|
+
"main": [
|
|
70
|
+
[
|
|
71
|
+
{
|
|
72
|
+
"node": "Code",
|
|
73
|
+
"type": "main",
|
|
74
|
+
"index": 0
|
|
75
|
+
}
|
|
76
|
+
]
|
|
77
|
+
]
|
|
78
|
+
},
|
|
79
|
+
"Code": {
|
|
80
|
+
"main": [
|
|
81
|
+
[
|
|
82
|
+
{
|
|
83
|
+
"node": "OnceForEachItemPythonCode",
|
|
84
|
+
"type": "main",
|
|
85
|
+
"index": 0
|
|
86
|
+
}
|
|
87
|
+
]
|
|
88
|
+
]
|
|
89
|
+
}
|
|
90
|
+
},
|
|
91
|
+
"settings": { "executionOrder": "v1" },
|
|
92
|
+
"staticData": null,
|
|
93
|
+
"meta": { "templateCredsSetupCompleted": true, "responseMode": "lastNode", "options": {} },
|
|
94
|
+
"pinData": {},
|
|
95
|
+
"versionId": "840a38a1-ba37-433d-9f20-de73f5131a2b",
|
|
96
|
+
"triggerCount": 1,
|
|
97
|
+
"tags": []
|
|
98
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "../scenario.schema.json",
|
|
3
|
+
"name": "CodeNodePython",
|
|
4
|
+
"description": "A Python Code Node that first generates 100 items and then runs once for each item and adds, modifies and removes properties. The data is returned with RespondToWebhook Node.",
|
|
5
|
+
"scenarioData": { "workflowFiles": ["py-code-node.json"] },
|
|
6
|
+
"scriptPath": "py-code-node.script.js"
|
|
7
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import http from 'k6/http';
|
|
2
|
+
import { check } from 'k6';
|
|
3
|
+
|
|
4
|
+
const apiBaseUrl = __ENV.API_BASE_URL;
|
|
5
|
+
|
|
6
|
+
export default function () {
|
|
7
|
+
const res = http.post(`${apiBaseUrl}/webhook/py-code-node-benchmark`, {});
|
|
8
|
+
|
|
9
|
+
if (res.status !== 200) {
|
|
10
|
+
console.error(
|
|
11
|
+
`Invalid response. Received status ${res.status}. Body: ${JSON.stringify(res.body)}`,
|
|
12
|
+
);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
check(res, {
|
|
16
|
+
'is status 200': (r) => r.status === 200,
|
|
17
|
+
'has items in response': (r) => {
|
|
18
|
+
if (r.status !== 200) return false;
|
|
19
|
+
|
|
20
|
+
try {
|
|
21
|
+
const body = JSON.parse(r.body);
|
|
22
|
+
return Array.isArray(body) ? body.length === 100 : false;
|
|
23
|
+
} catch (error) {
|
|
24
|
+
console.error('Error parsing response body: ', error);
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
});
|
|
29
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
{
|
|
2
|
+
"definitions": {
|
|
3
|
+
"ScenarioData": {
|
|
4
|
+
"type": "object",
|
|
5
|
+
"properties": {
|
|
6
|
+
"workflowFiles": {
|
|
7
|
+
"type": "array",
|
|
8
|
+
"items": {
|
|
9
|
+
"type": "string"
|
|
10
|
+
}
|
|
11
|
+
},
|
|
12
|
+
"credentialFiles": {
|
|
13
|
+
"type": "array",
|
|
14
|
+
"items": {
|
|
15
|
+
"type": "string"
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
"dataTableFile": {
|
|
19
|
+
"type": "string"
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
"required": [],
|
|
23
|
+
"additionalProperties": false
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
"type": "object",
|
|
27
|
+
"properties": {
|
|
28
|
+
"$schema": {
|
|
29
|
+
"type": "string",
|
|
30
|
+
"description": "The JSON schema to validate this file"
|
|
31
|
+
},
|
|
32
|
+
"name": {
|
|
33
|
+
"type": "string",
|
|
34
|
+
"description": "The name of the scenario"
|
|
35
|
+
},
|
|
36
|
+
"description": {
|
|
37
|
+
"type": "string",
|
|
38
|
+
"description": "A longer description of the scenario"
|
|
39
|
+
},
|
|
40
|
+
"scriptPath": {
|
|
41
|
+
"type": "string",
|
|
42
|
+
"description": "Relative path to the k6 test script"
|
|
43
|
+
},
|
|
44
|
+
"scenarioData": {
|
|
45
|
+
"$ref": "#/definitions/ScenarioData",
|
|
46
|
+
"description": "Data to import before running the scenario"
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
"required": ["name", "description", "scriptPath", "scenarioData"],
|
|
50
|
+
"additionalProperties": false
|
|
51
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
{
|
|
2
|
+
"createdAt": "2024-09-03T11:30:26.333Z",
|
|
3
|
+
"updatedAt": "2024-09-03T11:42:52.000Z",
|
|
4
|
+
"name": "Set Node Expressions",
|
|
5
|
+
"active": false,
|
|
6
|
+
"nodes": [
|
|
7
|
+
{
|
|
8
|
+
"parameters": {
|
|
9
|
+
"httpMethod": "POST",
|
|
10
|
+
"path": "set-expressions-benchmark",
|
|
11
|
+
"responseMode": "responseNode",
|
|
12
|
+
"options": {}
|
|
13
|
+
},
|
|
14
|
+
"type": "n8n-nodes-base.webhook",
|
|
15
|
+
"typeVersion": 2,
|
|
16
|
+
"position": [40, 0],
|
|
17
|
+
"id": "5babc228-2b89-48cb-8337-28416e867874",
|
|
18
|
+
"name": "Webhook",
|
|
19
|
+
"webhookId": "f6f1750d-b734-496f-afe8-26e8e393ca87"
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
"parameters": { "respondWith": "allIncomingItems", "options": {} },
|
|
23
|
+
"type": "n8n-nodes-base.respondToWebhook",
|
|
24
|
+
"typeVersion": 1.1,
|
|
25
|
+
"position": [640, 0],
|
|
26
|
+
"id": "4146a3fb-403c-4cfc-9d38-8af4d16a8440",
|
|
27
|
+
"name": "Respond to Webhook"
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
"parameters": {
|
|
31
|
+
"assignments": {
|
|
32
|
+
"assignments": [
|
|
33
|
+
{
|
|
34
|
+
"id": "48c46098-f411-41f7-8f0a-1da372340a4e",
|
|
35
|
+
"name": "oneToOneCopy",
|
|
36
|
+
"value": "={{ $json.headers.host }}",
|
|
37
|
+
"type": "string"
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
"id": "5d90808b-1c1a-4065-ac51-6d61bd03e564",
|
|
41
|
+
"name": "={{ $json.headers['user-agent'].slice(0, 4) }}",
|
|
42
|
+
"value": "Set key with expression",
|
|
43
|
+
"type": "string"
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
"id": "8a74ac24-1f43-43ba-969d-87bfd2f401ce",
|
|
47
|
+
"name": "Multiple variables",
|
|
48
|
+
"value": "={{ $json.executionMode + ' ' + $json.webhookUrl }}",
|
|
49
|
+
"type": "string"
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
"id": "93eba201-79d9-4305-a246-f9c8ec50ebab",
|
|
53
|
+
"name": "Static value",
|
|
54
|
+
"value": 42,
|
|
55
|
+
"type": "number"
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
"id": "0470a712-c795-44ab-9dcc-05a3f67698bb",
|
|
59
|
+
"name": "Object",
|
|
60
|
+
"value": "={{ $json.headers }}",
|
|
61
|
+
"type": "object"
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
"id": "eb671167-da14-4b55-8eea-31ab7bedae10",
|
|
65
|
+
"name": "Array",
|
|
66
|
+
"value": "={{ Object.values($json.headers) }}",
|
|
67
|
+
"type": "array"
|
|
68
|
+
}
|
|
69
|
+
]
|
|
70
|
+
},
|
|
71
|
+
"options": {}
|
|
72
|
+
},
|
|
73
|
+
"type": "n8n-nodes-base.set",
|
|
74
|
+
"typeVersion": 3.4,
|
|
75
|
+
"position": [360, 0],
|
|
76
|
+
"id": "0cb5e82d-f61e-4d91-8fa9-365e382a4d75",
|
|
77
|
+
"name": "Edit Fields"
|
|
78
|
+
}
|
|
79
|
+
],
|
|
80
|
+
"connections": {
|
|
81
|
+
"Webhook": { "main": [[{ "node": "Edit Fields", "type": "main", "index": 0 }]] },
|
|
82
|
+
"Edit Fields": { "main": [[{ "node": "Respond to Webhook", "type": "main", "index": 0 }]] }
|
|
83
|
+
},
|
|
84
|
+
"settings": { "executionOrder": "v1" },
|
|
85
|
+
"staticData": null,
|
|
86
|
+
"meta": null,
|
|
87
|
+
"pinData": {},
|
|
88
|
+
"versionId": "04fd543e-3923-4092-8c2b-2b4262ccbb38",
|
|
89
|
+
"triggerCount": 0,
|
|
90
|
+
"tags": []
|
|
91
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import http from 'k6/http';
|
|
2
|
+
import { check } from 'k6';
|
|
3
|
+
|
|
4
|
+
const apiBaseUrl = __ENV.API_BASE_URL;
|
|
5
|
+
|
|
6
|
+
export default function () {
|
|
7
|
+
const res = http.post(`${apiBaseUrl}/webhook/set-expressions-benchmark`, {});
|
|
8
|
+
|
|
9
|
+
if (res.status !== 200) {
|
|
10
|
+
console.error(
|
|
11
|
+
`Invalid response. Received status ${res.status}. Body: ${JSON.stringify(res.body)}`,
|
|
12
|
+
);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
check(res, {
|
|
16
|
+
'is status 200': (r) => r.status === 200,
|
|
17
|
+
});
|
|
18
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"createdAt": "2024-08-06T12:19:51.268Z",
|
|
3
|
+
"updatedAt": "2024-08-06T12:20:45.000Z",
|
|
4
|
+
"name": "Single Webhook",
|
|
5
|
+
"active": true,
|
|
6
|
+
"nodes": [
|
|
7
|
+
{
|
|
8
|
+
"parameters": { "path": "single-webhook", "options": {} },
|
|
9
|
+
"id": "7587ab0e-cc15-424f-83c0-c887a0eb97fb",
|
|
10
|
+
"name": "Webhook",
|
|
11
|
+
"type": "n8n-nodes-base.webhook",
|
|
12
|
+
"typeVersion": 2,
|
|
13
|
+
"position": [760, 400],
|
|
14
|
+
"webhookId": "fa563fc2-c73f-4631-99a1-39c16f1f858f"
|
|
15
|
+
}
|
|
16
|
+
],
|
|
17
|
+
"connections": {},
|
|
18
|
+
"settings": { "executionOrder": "v1" },
|
|
19
|
+
"staticData": null,
|
|
20
|
+
"meta": { "templateCredsSetupCompleted": true, "responseMode": "lastNode", "options": {} },
|
|
21
|
+
"pinData": {},
|
|
22
|
+
"versionId": "840a38a1-ba37-433d-9f20-de73f5131a2b",
|
|
23
|
+
"triggerCount": 1,
|
|
24
|
+
"tags": []
|
|
25
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import http from 'k6/http';
|
|
2
|
+
import { check } from 'k6';
|
|
3
|
+
|
|
4
|
+
const apiBaseUrl = __ENV.API_BASE_URL;
|
|
5
|
+
|
|
6
|
+
export default function () {
|
|
7
|
+
const res = http.get(`${apiBaseUrl}/webhook/single-webhook`);
|
|
8
|
+
|
|
9
|
+
if (res.status !== 200) {
|
|
10
|
+
console.error(
|
|
11
|
+
`Invalid response. Received status ${res.status}. Body: ${JSON.stringify(res.body)}`,
|
|
12
|
+
);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
check(res, {
|
|
16
|
+
'is status 200': (r) => r.status === 200,
|
|
17
|
+
});
|
|
18
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
#
|
|
3
|
+
# Script to initialize the benchmark environment on a VM
|
|
4
|
+
#
|
|
5
|
+
|
|
6
|
+
set -euo pipefail;
|
|
7
|
+
|
|
8
|
+
CURRENT_USER=$(whoami)
|
|
9
|
+
|
|
10
|
+
# Mount the data disk
|
|
11
|
+
# First wait for the disk to become available
|
|
12
|
+
WAIT_TIME=0
|
|
13
|
+
MAX_WAIT_TIME=60
|
|
14
|
+
|
|
15
|
+
while [ ! -e /dev/sdc ]; do
|
|
16
|
+
if [ $WAIT_TIME -ge $MAX_WAIT_TIME ]; then
|
|
17
|
+
echo "Error: /dev/sdc did not become available within $MAX_WAIT_TIME seconds."
|
|
18
|
+
exit 1
|
|
19
|
+
fi
|
|
20
|
+
|
|
21
|
+
echo "Waiting for /dev/sdc to be available... ($WAIT_TIME/$MAX_WAIT_TIME)"
|
|
22
|
+
sleep 1
|
|
23
|
+
WAIT_TIME=$((WAIT_TIME + 1))
|
|
24
|
+
done
|
|
25
|
+
|
|
26
|
+
# Then mount it
|
|
27
|
+
if [ -d "/n8n" ]; then
|
|
28
|
+
echo "Data disk already mounted. Clearing it..."
|
|
29
|
+
sudo rm -rf /n8n/*
|
|
30
|
+
sudo rm -rf /n8n/.[!.]*
|
|
31
|
+
else
|
|
32
|
+
sudo mkdir -p /n8n
|
|
33
|
+
sudo parted /dev/sdc --script mklabel gpt mkpart xfspart xfs 0% 100%
|
|
34
|
+
sudo mkfs.xfs /dev/sdc1
|
|
35
|
+
sudo partprobe /dev/sdc1
|
|
36
|
+
sudo mount /dev/sdc1 /n8n
|
|
37
|
+
sudo chown -R "$CURRENT_USER":"$CURRENT_USER" /n8n
|
|
38
|
+
fi
|
|
39
|
+
|
|
40
|
+
### Remove unneeded dependencies
|
|
41
|
+
# TTY
|
|
42
|
+
sudo systemctl disable getty@tty1.service
|
|
43
|
+
sudo systemctl disable serial-getty@ttyS0.service
|
|
44
|
+
# Snap
|
|
45
|
+
sudo systemctl disable snapd.service
|
|
46
|
+
# Unattended upgrades
|
|
47
|
+
sudo systemctl disable unattended-upgrades.service
|
|
48
|
+
# Cron
|
|
49
|
+
sudo systemctl disable cron.service
|
|
50
|
+
|
|
51
|
+
# Include nodejs v20 repository
|
|
52
|
+
curl -fsSL https://deb.nodesource.com/setup_20.x -o nodesource_setup.sh
|
|
53
|
+
sudo -E bash nodesource_setup.sh
|
|
54
|
+
|
|
55
|
+
# Install docker, docker compose, nodejs
|
|
56
|
+
sudo DEBIAN_FRONTEND=noninteractive apt-get update -yq
|
|
57
|
+
sudo DEBIAN_FRONTEND=noninteractive apt-get install -yq docker.io docker-compose nodejs
|
|
58
|
+
|
|
59
|
+
# Add the current user to the docker group
|
|
60
|
+
sudo usermod -aG docker "$CURRENT_USER"
|
|
61
|
+
|
|
62
|
+
# Install zx
|
|
63
|
+
npm install zx
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { which } from 'zx';
|
|
2
|
+
|
|
3
|
+
export class DockerComposeClient {
|
|
4
|
+
/**
|
|
5
|
+
*
|
|
6
|
+
* @param {{ $: Shell; verbose?: boolean }} opts
|
|
7
|
+
*/
|
|
8
|
+
constructor({ $ }) {
|
|
9
|
+
this.$$ = $;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
async $(...args) {
|
|
13
|
+
await this.resolveExecutableIfNeeded();
|
|
14
|
+
|
|
15
|
+
if (this.isCompose) {
|
|
16
|
+
return await this.$$`docker-compose ${args}`;
|
|
17
|
+
} else {
|
|
18
|
+
return await this.$$`docker compose ${args}`;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async resolveExecutableIfNeeded() {
|
|
23
|
+
if (this.isResolved) {
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// The VM deployment doesn't have `docker compose` available,
|
|
28
|
+
// so try to resolve the `docker-compose` first
|
|
29
|
+
const compose = await which('docker-compose', { nothrow: true });
|
|
30
|
+
if (compose) {
|
|
31
|
+
this.isResolved = true;
|
|
32
|
+
this.isCompose = true;
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const docker = await which('docker', { nothrow: true });
|
|
37
|
+
if (docker) {
|
|
38
|
+
this.isResolved = true;
|
|
39
|
+
this.isCompose = false;
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
throw new Error('Could not resolve docker-compose or docker');
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
import { $ } from 'zx';
|
|
3
|
+
|
|
4
|
+
export class SshClient {
|
|
5
|
+
/**
|
|
6
|
+
*
|
|
7
|
+
* @param {{ privateKeyPath: string; ip: string; username: string; verbose?: boolean }} param0
|
|
8
|
+
*/
|
|
9
|
+
constructor({ privateKeyPath, ip, username, verbose = false }) {
|
|
10
|
+
this.verbose = verbose;
|
|
11
|
+
this.privateKeyPath = privateKeyPath;
|
|
12
|
+
this.ip = ip;
|
|
13
|
+
this.username = username;
|
|
14
|
+
|
|
15
|
+
this.$$ = $({
|
|
16
|
+
verbose,
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* @param {string} command
|
|
22
|
+
* @param {{ verbose?: boolean }} [options]
|
|
23
|
+
*/
|
|
24
|
+
async ssh(command, options = {}) {
|
|
25
|
+
const $$ = options?.verbose ? $({ verbose: true }) : this.$$;
|
|
26
|
+
|
|
27
|
+
const target = `${this.username}@${this.ip}`;
|
|
28
|
+
|
|
29
|
+
await $$`ssh -i ${this.privateKeyPath} -o StrictHostKeyChecking=accept-new ${target} ${command}`;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async scp(source, destination) {
|
|
33
|
+
const target = `${this.username}@${this.ip}:${destination}`;
|
|
34
|
+
await this
|
|
35
|
+
.$$`scp -i ${this.privateKeyPath} -o StrictHostKeyChecking=accept-new ${source} ${target}`;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { $, fs } from 'zx';
|
|
5
|
+
|
|
6
|
+
const paths = {
|
|
7
|
+
infraCodeDir: path.resolve('infra'),
|
|
8
|
+
terraformStateFile: path.join(path.resolve('infra'), 'terraform.tfstate'),
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export class TerraformClient {
|
|
12
|
+
constructor({ isVerbose = false }) {
|
|
13
|
+
this.isVerbose = isVerbose;
|
|
14
|
+
this.$$ = $({
|
|
15
|
+
cwd: paths.infraCodeDir,
|
|
16
|
+
verbose: isVerbose,
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Provisions the environment
|
|
22
|
+
*/
|
|
23
|
+
async provisionEnvironment() {
|
|
24
|
+
console.log('Provisioning cloud environment...');
|
|
25
|
+
|
|
26
|
+
await this.$$`terraform init`;
|
|
27
|
+
await this.$$`terraform apply -input=false -auto-approve`;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* @typedef {Object} BenchmarkEnv
|
|
32
|
+
* @property {string} vmName
|
|
33
|
+
* @property {string} ip
|
|
34
|
+
* @property {string} sshUsername
|
|
35
|
+
* @property {string} sshPrivateKeyPath
|
|
36
|
+
*
|
|
37
|
+
* @returns {Promise<BenchmarkEnv>}
|
|
38
|
+
*/
|
|
39
|
+
async getTerraformOutputs() {
|
|
40
|
+
const privateKeyName = await this.extractPrivateKey();
|
|
41
|
+
|
|
42
|
+
return {
|
|
43
|
+
ip: await this.getTerraformOutput('ip'),
|
|
44
|
+
sshUsername: await this.getTerraformOutput('ssh_username'),
|
|
45
|
+
sshPrivateKeyPath: path.join(paths.infraCodeDir, privateKeyName),
|
|
46
|
+
vmName: await this.getTerraformOutput('vm_name'),
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
hasTerraformState() {
|
|
51
|
+
return fs.existsSync(paths.terraformStateFile);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async destroyEnvironment() {
|
|
55
|
+
console.log('Destroying cloud environment...');
|
|
56
|
+
|
|
57
|
+
await this.$$`terraform destroy -input=false -auto-approve`;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async getTerraformOutput(key) {
|
|
61
|
+
const output = await this.$$`terraform output -raw ${key}`;
|
|
62
|
+
return output.stdout.trim();
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async extractPrivateKey() {
|
|
66
|
+
await this.$$`terraform output -raw ssh_private_key > privatekey.pem`;
|
|
67
|
+
await this.$$`chmod 600 privatekey.pem`;
|
|
68
|
+
|
|
69
|
+
return 'privatekey.pem';
|
|
70
|
+
}
|
|
71
|
+
}
|