@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,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,7 @@
1
+ {
2
+ "$schema": "../scenario.schema.json",
3
+ "name": "SetNodeExpressions",
4
+ "description": "Expressions in a Set node",
5
+ "scenarioData": { "workflowFiles": ["set-node-expressions.json"] },
6
+ "scriptPath": "set-node-expressions.script.js"
7
+ }
@@ -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,7 @@
1
+ {
2
+ "$schema": "../scenario.schema.json",
3
+ "name": "SingleWebhook",
4
+ "description": "A single webhook trigger that responds with a 200 status code",
5
+ "scenarioData": { "workflowFiles": ["single-webhook.json"] },
6
+ "scriptPath": "single-webhook.script.js"
7
+ }
@@ -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
+ }