@depup/artillery 2.0.30-depup.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 (90) hide show
  1. package/README.md +63 -0
  2. package/bin/run +29 -0
  3. package/bin/run.cmd +3 -0
  4. package/changes.json +138 -0
  5. package/console-reporter.js +1 -0
  6. package/lib/artillery-global.js +33 -0
  7. package/lib/cli/banner.js +8 -0
  8. package/lib/cli/common-flags.js +80 -0
  9. package/lib/cli/hooks/version.js +20 -0
  10. package/lib/cmds/dino.js +109 -0
  11. package/lib/cmds/quick.js +122 -0
  12. package/lib/cmds/report.js +34 -0
  13. package/lib/cmds/run-aci.js +91 -0
  14. package/lib/cmds/run-fargate.js +192 -0
  15. package/lib/cmds/run-lambda.js +96 -0
  16. package/lib/cmds/run.js +671 -0
  17. package/lib/console-capture.js +92 -0
  18. package/lib/console-reporter.js +438 -0
  19. package/lib/create-bom/built-in-plugins.js +12 -0
  20. package/lib/create-bom/create-bom.js +301 -0
  21. package/lib/dispatcher.js +9 -0
  22. package/lib/dist.js +222 -0
  23. package/lib/index.js +5 -0
  24. package/lib/launch-platform.js +439 -0
  25. package/lib/load-plugins.js +113 -0
  26. package/lib/platform/aws/aws-cloudwatch.js +106 -0
  27. package/lib/platform/aws/aws-create-sqs-queue.js +58 -0
  28. package/lib/platform/aws/aws-ensure-s3-bucket-exists.js +78 -0
  29. package/lib/platform/aws/aws-get-account-id.js +26 -0
  30. package/lib/platform/aws/aws-get-bucket-region.js +18 -0
  31. package/lib/platform/aws/aws-get-credentials.js +28 -0
  32. package/lib/platform/aws/aws-get-default-region.js +26 -0
  33. package/lib/platform/aws/aws-whoami.js +15 -0
  34. package/lib/platform/aws/constants.js +7 -0
  35. package/lib/platform/aws/iam-cf-templates/aws-iam-fargate-cf-template.yml +219 -0
  36. package/lib/platform/aws/iam-cf-templates/aws-iam-lambda-cf-template.yml +125 -0
  37. package/lib/platform/aws/iam-cf-templates/gh-oidc-fargate.yml +241 -0
  38. package/lib/platform/aws/iam-cf-templates/gh-oidc-lambda.yml +153 -0
  39. package/lib/platform/aws-ecs/ecs.js +247 -0
  40. package/lib/platform/aws-ecs/legacy/aws-util.js +134 -0
  41. package/lib/platform/aws-ecs/legacy/bom.js +528 -0
  42. package/lib/platform/aws-ecs/legacy/constants.js +27 -0
  43. package/lib/platform/aws-ecs/legacy/create-s3-client.js +24 -0
  44. package/lib/platform/aws-ecs/legacy/create-test.js +247 -0
  45. package/lib/platform/aws-ecs/legacy/errors.js +34 -0
  46. package/lib/platform/aws-ecs/legacy/find-public-subnets.js +149 -0
  47. package/lib/platform/aws-ecs/legacy/plugins/artillery-plugin-inspect-script/index.js +27 -0
  48. package/lib/platform/aws-ecs/legacy/plugins/artillery-plugin-sqs-reporter/azure-aqs.js +80 -0
  49. package/lib/platform/aws-ecs/legacy/plugins/artillery-plugin-sqs-reporter/index.js +202 -0
  50. package/lib/platform/aws-ecs/legacy/plugins.js +16 -0
  51. package/lib/platform/aws-ecs/legacy/run-cluster.js +1994 -0
  52. package/lib/platform/aws-ecs/legacy/sqs-reporter.js +401 -0
  53. package/lib/platform/aws-ecs/legacy/tags.js +22 -0
  54. package/lib/platform/aws-ecs/legacy/test-run-status.js +9 -0
  55. package/lib/platform/aws-ecs/legacy/time.js +67 -0
  56. package/lib/platform/aws-ecs/legacy/util.js +97 -0
  57. package/lib/platform/aws-ecs/worker/Dockerfile +64 -0
  58. package/lib/platform/aws-ecs/worker/helpers.sh +80 -0
  59. package/lib/platform/aws-ecs/worker/loadgen-worker +656 -0
  60. package/lib/platform/aws-lambda/dependencies.js +130 -0
  61. package/lib/platform/aws-lambda/index.js +734 -0
  62. package/lib/platform/aws-lambda/lambda-handler/a9-handler-dependencies.js +73 -0
  63. package/lib/platform/aws-lambda/lambda-handler/a9-handler-helpers.js +43 -0
  64. package/lib/platform/aws-lambda/lambda-handler/a9-handler-index.js +235 -0
  65. package/lib/platform/aws-lambda/lambda-handler/package.json +15 -0
  66. package/lib/platform/aws-lambda/prices.js +29 -0
  67. package/lib/platform/az/aci.js +694 -0
  68. package/lib/platform/az/aqs-queue-consumer.js +88 -0
  69. package/lib/platform/az/regions.js +52 -0
  70. package/lib/platform/cloud/api.js +72 -0
  71. package/lib/platform/cloud/cloud.js +448 -0
  72. package/lib/platform/cloud/http-client.js +19 -0
  73. package/lib/platform/local/artillery-worker-local.js +154 -0
  74. package/lib/platform/local/index.js +174 -0
  75. package/lib/platform/local/worker.js +261 -0
  76. package/lib/platform/worker-states.js +13 -0
  77. package/lib/queue-consumer/index.js +56 -0
  78. package/lib/stash.js +41 -0
  79. package/lib/telemetry.js +78 -0
  80. package/lib/util/await-on-ee.js +24 -0
  81. package/lib/util/generate-id.js +9 -0
  82. package/lib/util/parse-tag-string.js +21 -0
  83. package/lib/util/prepare-test-execution-plan.js +216 -0
  84. package/lib/util/sleep.js +7 -0
  85. package/lib/util/validate-script.js +132 -0
  86. package/lib/util.js +294 -0
  87. package/lib/utils-config.js +31 -0
  88. package/package.json +323 -0
  89. package/types.d.ts +317 -0
  90. package/util.js +1 -0
@@ -0,0 +1,734 @@
1
+ /* This Source Code Form is subject to the terms of the Mozilla Public
2
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
3
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4
+
5
+ const EventEmitter = require('node:events');
6
+ const debug = require('debug')('platform:aws-lambda');
7
+
8
+ const { randomUUID } = require('node:crypto');
9
+
10
+ const sleep = require('../../util/sleep');
11
+ const path = require('node:path');
12
+ const {
13
+ LambdaClient,
14
+ GetFunctionConfigurationCommand,
15
+ InvokeCommand,
16
+ CreateFunctionCommand,
17
+ DeleteFunctionCommand,
18
+ ResourceConflictException,
19
+ ResourceNotFoundException
20
+ } = require('@aws-sdk/client-lambda');
21
+ const { PutObjectCommand } = require('@aws-sdk/client-s3');
22
+ const { SQSClient, DeleteQueueCommand } = require('@aws-sdk/client-sqs');
23
+ const {
24
+ IAMClient,
25
+ GetRoleCommand,
26
+ CreateRoleCommand,
27
+ AttachRolePolicyCommand,
28
+ CreatePolicyCommand
29
+ } = require('@aws-sdk/client-iam');
30
+
31
+ const createS3Client = require('../aws-ecs/legacy/create-s3-client');
32
+ const { getBucketRegion } = require('../aws/aws-get-bucket-region');
33
+
34
+ const _https = require('node:https');
35
+
36
+ const { QueueConsumer } = require('../../queue-consumer');
37
+
38
+ const telemetry = require('../../telemetry');
39
+ const crypto = require('node:crypto');
40
+
41
+ const prices = require('./prices');
42
+ const _ = require('lodash');
43
+
44
+ const { SQS_QUEUES_NAME_PREFIX } = require('../aws/constants');
45
+ const ensureS3BucketExists = require('../aws/aws-ensure-s3-bucket-exists');
46
+ const getAccountId = require('../aws/aws-get-account-id');
47
+
48
+ const createSQSQueue = require('../aws/aws-create-sqs-queue');
49
+ const { createAndUploadTestDependencies } = require('./dependencies');
50
+ const awsGetDefaultRegion = require('../aws/aws-get-default-region');
51
+ const pkgVersion = require('../../../package.json').version;
52
+
53
+ // https://stackoverflow.com/a/66523153
54
+ function memoryToVCPU(memMB) {
55
+ if (memMB < 832) {
56
+ return 0.5;
57
+ }
58
+
59
+ if (memMB < 3009) {
60
+ return 2;
61
+ }
62
+
63
+ if (memMB < 5308) {
64
+ return 3;
65
+ }
66
+
67
+ if (memMB < 7077) {
68
+ return 4;
69
+ }
70
+
71
+ if (memMB < 8846) {
72
+ return 5;
73
+ }
74
+
75
+ return 6;
76
+ }
77
+
78
+ class PlatformLambda {
79
+ constructor(script, payload, opts, platformOpts) {
80
+ this.workers = {};
81
+
82
+ this.count = 0;
83
+ this.waitingReadyCount = 0;
84
+
85
+ this.script = script;
86
+ this.payload = payload;
87
+ this.opts = opts;
88
+
89
+ this.events = new EventEmitter();
90
+
91
+ const platformConfig = platformOpts.platformConfig;
92
+
93
+ this.currentVersion = process.env.LAMBDA_IMAGE_VERSION || pkgVersion;
94
+ this.ecrImageUrl = process.env.WORKER_IMAGE_URL;
95
+ this.architecture = platformConfig.architecture || 'arm64';
96
+ this.region = platformConfig.region || 'us-east-1';
97
+ this.arnPrefix = this.region.startsWith('cn-') ? 'arn:aws-cn' : 'arn:aws';
98
+
99
+ this.securityGroupIds =
100
+ platformConfig['security-group-ids']?.split(',') || [];
101
+ this.subnetIds = platformConfig['subnet-ids']?.split(',') || [];
102
+
103
+ this.useVPC = this.securityGroupIds.length > 0 && this.subnetIds.length > 0;
104
+
105
+ this.memorySize = platformConfig['memory-size'] || 4096;
106
+
107
+ this.testRunId = platformOpts.testRunId;
108
+ this.lambdaRoleArn =
109
+ platformConfig['lambda-role-arn'] || platformConfig.lambdaRoleArn;
110
+
111
+ this.platformOpts = platformOpts;
112
+
113
+ this.cloudKey =
114
+ this.platformOpts.cliArgs.key || process.env.ARTILLERY_CLOUD_API_KEY;
115
+
116
+ this.s3LifecycleConfigurationRules = [
117
+ {
118
+ Expiration: { Days: 2 },
119
+ Filter: { Prefix: '/lambda' },
120
+ ID: 'RemoveAdHocTestData',
121
+ Status: 'Enabled'
122
+ },
123
+ {
124
+ Expiration: { Days: 7 },
125
+ Filter: { Prefix: '/' },
126
+ ID: 'RemoveTestRunMetadata',
127
+ Status: 'Enabled'
128
+ }
129
+ ];
130
+
131
+ this.artilleryArgs = [];
132
+ }
133
+
134
+ async init() {
135
+ global.artillery.awsRegion = (await awsGetDefaultRegion()) || this.region;
136
+ artillery.log('λ Preparing AWS Lambda function...');
137
+ this.accountId = await getAccountId();
138
+
139
+ const metadata = {
140
+ region: this.region,
141
+ platformConfig: {
142
+ memory: this.memorySize,
143
+ cpu: memoryToVCPU(this.memorySize)
144
+ }
145
+ };
146
+ global.artillery.globalEvents.emit('metadata', metadata);
147
+
148
+ //make sure the bucket exists to send the zip file or the dependencies to
149
+ const bucketName = await ensureS3BucketExists(
150
+ this.region,
151
+ this.s3LifecycleConfigurationRules,
152
+ true
153
+ );
154
+ this.bucketName = bucketName;
155
+
156
+ global.artillery.s3BucketRegion = await getBucketRegion(bucketName);
157
+
158
+ const { bom, s3Path } = await createAndUploadTestDependencies(
159
+ this.bucketName,
160
+ this.testRunId,
161
+ this.opts.absoluteScriptPath,
162
+ this.opts.absoluteConfigPath,
163
+ this.platformOpts.cliArgs
164
+ );
165
+
166
+ this.artilleryArgs.push('run');
167
+
168
+ if (this.platformOpts.cliArgs.environment) {
169
+ this.artilleryArgs.push('-e');
170
+ this.artilleryArgs.push(this.platformOpts.cliArgs.environment);
171
+ }
172
+ if (this.platformOpts.cliArgs.solo) {
173
+ this.artilleryArgs.push('--solo');
174
+ }
175
+
176
+ if (this.platformOpts.cliArgs.target) {
177
+ this.artilleryArgs.push('--target');
178
+ this.artilleryArgs.push(this.platformOpts.cliArgs.target);
179
+ }
180
+
181
+ if (this.platformOpts.cliArgs.variables) {
182
+ this.artilleryArgs.push('-v');
183
+ this.artilleryArgs.push(this.platformOpts.cliArgs.variables);
184
+ }
185
+
186
+ if (this.platformOpts.cliArgs.overrides) {
187
+ this.artilleryArgs.push('--overrides');
188
+ this.artilleryArgs.push(this.platformOpts.cliArgs.overrides);
189
+ }
190
+
191
+ if (this.platformOpts.cliArgs.dotenv) {
192
+ this.artilleryArgs.push('--dotenv');
193
+ this.artilleryArgs.push(path.basename(this.platformOpts.cliArgs.dotenv));
194
+ }
195
+
196
+ if (this.platformOpts.cliArgs['scenario-name']) {
197
+ this.artilleryArgs.push('--scenario-name');
198
+ this.artilleryArgs.push(this.platformOpts.cliArgs['scenario-name']);
199
+ }
200
+
201
+ if (this.platformOpts.cliArgs.config) {
202
+ this.artilleryArgs.push('--config');
203
+ const p = bom.files.filter(
204
+ (x) => x.orig === this.opts.absoluteConfigPath
205
+ )[0];
206
+ this.artilleryArgs.push(p.noPrefixPosix);
207
+ }
208
+
209
+ // This needs to be the last argument for now:
210
+ const p = bom.files.filter(
211
+ (x) => x.orig === this.opts.absoluteScriptPath
212
+ )[0];
213
+ this.artilleryArgs.push(p.noPrefixPosix);
214
+ // 36 is length of a UUUI v4 string
215
+ const queueName = `${SQS_QUEUES_NAME_PREFIX}_${this.testRunId.slice(
216
+ 0,
217
+ 36
218
+ )}.fifo`;
219
+
220
+ const sqsQueueUrl = await createSQSQueue(this.region, queueName);
221
+ this.sqsQueueUrl = sqsQueueUrl;
222
+
223
+ if (typeof this.lambdaRoleArn === 'undefined') {
224
+ const lambdaRoleArn = await this.createLambdaRole();
225
+ this.lambdaRoleArn = lambdaRoleArn;
226
+ } else {
227
+ artillery.log(` - Lambda role ARN: ${this.lambdaRoleArn}`);
228
+ }
229
+
230
+ this.functionName = this.createFunctionNameWithHash();
231
+
232
+ await this.createOrUpdateLambdaFunctionIfNeeded();
233
+
234
+ artillery.log(` - Lambda function: ${this.functionName}`);
235
+ artillery.log(` - Region: ${this.region}`);
236
+ artillery.log(` - AWS account: ${this.accountId}`);
237
+
238
+ debug({ bucketName, s3Path, sqsQueueUrl });
239
+
240
+ const consumer = new QueueConsumer();
241
+ consumer.create(
242
+ {
243
+ poolSize: Math.min(this.platformOpts.count, 100)
244
+ },
245
+ {
246
+ queueUrl: process.env.SQS_QUEUE_URL || this.sqsQueueUrl,
247
+ region: this.region,
248
+ waitTimeSeconds: 10,
249
+ messageAttributeNames: ['testId', 'workerId'],
250
+ visibilityTimeout: 60,
251
+ batchSize: 10,
252
+ handleMessage: async (message) => {
253
+ let body = null;
254
+ try {
255
+ body = JSON.parse(message.Body);
256
+ } catch (err) {
257
+ console.error(err);
258
+ console.log(message.Body);
259
+ }
260
+
261
+ //
262
+ // Ignore any messages that are invalid or not tagged properly.
263
+ //
264
+
265
+ if (process.env.LOG_SQS_MESSAGES) {
266
+ console.log(message);
267
+ }
268
+
269
+ if (!body) {
270
+ throw new Error('SQS message with empty body');
271
+ }
272
+
273
+ const attrs = message.MessageAttributes;
274
+ if (!attrs || !attrs.testId || !attrs.workerId) {
275
+ throw new Error('SQS message with no testId or workerId');
276
+ }
277
+
278
+ if (this.testRunId !== attrs.testId.StringValue) {
279
+ throw new Error('SQS message for an unknown testId');
280
+ }
281
+
282
+ const workerId = attrs.workerId.StringValue;
283
+
284
+ if (body.event === 'workerStats') {
285
+ this.events.emit('stats', workerId, body); // event consumer accesses body.stats
286
+ } else if (body.event === 'artillery.log') {
287
+ console.log(body.log);
288
+ } else if (body.event === 'done') {
289
+ // 'done' handler in Launcher exects the message argument to have an "id" and "report" fields
290
+ body.id = workerId;
291
+ body.report = body.stats; // Launcher expects "report", SQS reporter sends "stats"
292
+ this.events.emit('done', workerId, body);
293
+ } else if (
294
+ body.event === 'phaseStarted' ||
295
+ body.event === 'phaseCompleted'
296
+ ) {
297
+ body.id = workerId;
298
+ this.events.emit(body.event, workerId, { phase: body.phase });
299
+ } else if (body.event === 'workerError') {
300
+ global.artillery.suggestedExitCode = body.exitCode || 1;
301
+
302
+ if (body.exitCode !== 21) {
303
+ this.events.emit(body.event, workerId, {
304
+ id: workerId,
305
+ error: new Error(
306
+ `A Lambda function has exited with an error. Reason: ${body.reason}`
307
+ ),
308
+ level: 'error',
309
+ aggregatable: false,
310
+ logs: body.logs
311
+ });
312
+ }
313
+ } else if (body.event === 'workerReady') {
314
+ this.events.emit(body.event, workerId);
315
+ this.waitingReadyCount++;
316
+
317
+ // TODO: Do this only for batches of workers with "wait" option set
318
+ if (this.waitingReadyCount === this.count) {
319
+ // TODO: Retry
320
+ const s3 = createS3Client();
321
+ await s3.send(
322
+ new PutObjectCommand({
323
+ Body: Buffer.from(''),
324
+ Bucket: this.bucketName,
325
+ Key: `/${this.testRunId}/green`
326
+ })
327
+ );
328
+ }
329
+ } else {
330
+ debug(body);
331
+ }
332
+ }
333
+ }
334
+ );
335
+
336
+ let queueEmpty = 0;
337
+
338
+ consumer.on('error', (err) => {
339
+ artillery.log(err);
340
+ });
341
+ consumer.on('empty', (_err) => {
342
+ debug('queueEmpty:', queueEmpty);
343
+ queueEmpty++;
344
+ });
345
+
346
+ consumer.start();
347
+
348
+ this.sqsConsumer = consumer;
349
+
350
+ // TODO: Start the timer when the first worker is created
351
+ const startedAt = Date.now();
352
+ global.artillery.ext({
353
+ ext: 'beforeExit',
354
+ method: async (event) => {
355
+ try {
356
+ await telemetry.init().capture({
357
+ event: 'ping',
358
+ awsAccountId: crypto
359
+ .createHash('sha1')
360
+ .update(this.accountId)
361
+ .digest('base64')
362
+ });
363
+
364
+ process.nextTick(() => {
365
+ telemetry.shutdown();
366
+ });
367
+ } catch (_err) {}
368
+
369
+ function round(number, decimals) {
370
+ const m = 10 ** decimals;
371
+ return Math.round(number * m) / m;
372
+ }
373
+
374
+ if (event.flags && event.flags.platform === 'aws:lambda') {
375
+ let price = 0;
376
+ if (!prices[this.region]) {
377
+ price = prices.base[this.architecture];
378
+ } else {
379
+ price = prices[this.region][this.architecture];
380
+ }
381
+
382
+ const duration = Math.ceil((Date.now() - startedAt) / 1000);
383
+ const total =
384
+ ((price * this.memorySize) / 1024) *
385
+ this.platformOpts.count *
386
+ duration;
387
+ const cost = round(total / 10e10, 4);
388
+ console.log(`\nEstimated AWS Lambda cost for this test: $${cost}\n`);
389
+ }
390
+ }
391
+ });
392
+ }
393
+
394
+ getDesiredWorkerCount() {
395
+ return this.platformOpts.count;
396
+ }
397
+
398
+ async startJob() {
399
+ await this.init();
400
+
401
+ for (let i = 0; i < this.platformOpts.count; i++) {
402
+ const { workerId } = await this.createWorker();
403
+ this.workers[workerId] = { id: workerId };
404
+ await this.runWorker(workerId);
405
+ }
406
+ }
407
+
408
+ async createWorker() {
409
+ const workerId = randomUUID();
410
+
411
+ return { workerId };
412
+ }
413
+
414
+ async runWorker(workerId) {
415
+ const lambda = new LambdaClient({
416
+ apiVersion: '2015-03-31',
417
+ region: this.region
418
+ });
419
+ const event = {
420
+ SQS_QUEUE_URL: this.sqsQueueUrl,
421
+ SQS_REGION: this.region,
422
+ WORKER_ID: workerId,
423
+ ARTILLERY_ARGS: this.artilleryArgs,
424
+ TEST_RUN_ID: this.testRunId,
425
+ BUCKET: this.bucketName,
426
+ WAIT_FOR_GREEN: true,
427
+ ARTILLERY_CLOUD_API_KEY: this.cloudKey
428
+ };
429
+
430
+ if (process.env.ARTILLERY_CLOUD_ENDPOINT) {
431
+ event.ARTILLERY_CLOUD_ENDPOINT = process.env.ARTILLERY_CLOUD_ENDPOINT;
432
+ }
433
+
434
+ debug('Lambda event payload:');
435
+ debug({ event });
436
+
437
+ const payload = JSON.stringify(event);
438
+
439
+ // Wait for the function to be invocable:
440
+ const timeout = this.useVPC ? 240e3 : 120e3;
441
+ let waited = 0;
442
+ let ok = false;
443
+ let state;
444
+ while (waited < timeout) {
445
+ try {
446
+ state = (
447
+ await lambda.send(
448
+ new GetFunctionConfigurationCommand({
449
+ FunctionName: this.functionName
450
+ })
451
+ )
452
+ ).State;
453
+ if (state === 'Active') {
454
+ debug('Lambda function ready:', this.functionName);
455
+ ok = true;
456
+ break;
457
+ } else {
458
+ await sleep(10 * 1000);
459
+ waited += 10 * 1000;
460
+ }
461
+ } catch (err) {
462
+ debug('Error getting lambda state:', err);
463
+ await sleep(10 * 1000);
464
+ waited += 10 * 1000;
465
+ }
466
+ }
467
+
468
+ if (!ok) {
469
+ debug(
470
+ 'Time out waiting for lamda function to be ready:',
471
+ this.functionName
472
+ );
473
+ throw new Error(
474
+ 'Timeout waiting for lambda function to be ready for invocation'
475
+ );
476
+ }
477
+
478
+ await lambda.send(
479
+ new InvokeCommand({
480
+ FunctionName: this.functionName,
481
+ Payload: payload,
482
+ InvocationType: 'Event'
483
+ })
484
+ );
485
+
486
+ this.count++;
487
+ }
488
+
489
+ async stopWorker(_workerId) {
490
+ // TODO: Send message to that worker and have it exit early
491
+ }
492
+
493
+ async shutdown() {
494
+ if (this.sqsConsumer) {
495
+ this.sqsConsumer.stop();
496
+ }
497
+
498
+ const sqs = new SQSClient({ region: this.region });
499
+ const lambda = new LambdaClient({
500
+ apiVersion: '2015-03-31',
501
+ region: this.region
502
+ });
503
+
504
+ try {
505
+ await sqs.send(
506
+ new DeleteQueueCommand({
507
+ QueueUrl: this.sqsQueueUrl
508
+ })
509
+ );
510
+
511
+ if (process.env.RETAIN_LAMBDA === 'false') {
512
+ await lambda.send(
513
+ new DeleteFunctionCommand({
514
+ FunctionName: this.functionName
515
+ })
516
+ );
517
+ }
518
+ } catch (err) {
519
+ console.error(err);
520
+ }
521
+ }
522
+
523
+ async createLambdaRole() {
524
+ const ROLE_NAME = 'artilleryio-default-lambda-role-20230116';
525
+ const POLICY_NAME = 'artilleryio-lambda-policy-20230116';
526
+
527
+ const iam = new IAMClient({ region: global.artillery.awsRegion });
528
+
529
+ try {
530
+ const res = await iam.send(new GetRoleCommand({ RoleName: ROLE_NAME }));
531
+ return res.Role.Arn;
532
+ } catch (err) {
533
+ debug(err);
534
+ }
535
+
536
+ const principalService = this.region.startsWith('cn-')
537
+ ? 'lambda.amazonaws.com.cn'
538
+ : 'lambda.amazonaws.com';
539
+
540
+ const res = await iam.send(
541
+ new CreateRoleCommand({
542
+ AssumeRolePolicyDocument: `{
543
+ "Version": "2012-10-17",
544
+ "Statement": [
545
+ {
546
+ "Effect": "Allow",
547
+ "Principal": {
548
+ "Service": "${principalService}"
549
+ },
550
+ "Action": "sts:AssumeRole"
551
+ }
552
+ ]
553
+ }`,
554
+ Path: '/',
555
+ RoleName: ROLE_NAME
556
+ })
557
+ );
558
+
559
+ const lambdaRoleArn = res.Role.Arn;
560
+
561
+ await iam.send(
562
+ new AttachRolePolicyCommand({
563
+ PolicyArn: `${this.arnPrefix}:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole`,
564
+ RoleName: ROLE_NAME
565
+ })
566
+ );
567
+
568
+ await iam.send(
569
+ new AttachRolePolicyCommand({
570
+ PolicyArn: `${this.arnPrefix}:iam::aws:policy/service-role/AWSLambdaVPCAccessExecutionRole`,
571
+ RoleName: ROLE_NAME
572
+ })
573
+ );
574
+
575
+ const iamRes = await iam.send(
576
+ new CreatePolicyCommand({
577
+ PolicyDocument: `{
578
+ "Version": "2012-10-17",
579
+ "Statement": [
580
+ {
581
+ "Effect": "Allow",
582
+ "Action": ["sqs:*"],
583
+ "Resource": "${this.arnPrefix}:sqs:*:${this.accountId}:artilleryio*"
584
+ },
585
+ {
586
+ "Effect": "Allow",
587
+ "Action": ["s3:HeadObject", "s3:PutObject", "s3:ListBucket", "s3:GetObject", "s3:GetObjectAttributes"],
588
+ "Resource": [ "${this.arnPrefix}:s3:::artilleryio-test-data*", "${this.arnPrefix}:s3:::artilleryio-test-data*/*" ]
589
+ }
590
+ ]
591
+ }
592
+ `,
593
+ PolicyName: POLICY_NAME,
594
+ Path: '/'
595
+ })
596
+ );
597
+
598
+ await iam.send(
599
+ new AttachRolePolicyCommand({
600
+ PolicyArn: iamRes.Policy.Arn,
601
+ RoleName: ROLE_NAME
602
+ })
603
+ );
604
+
605
+ // See https://stackoverflow.com/a/37438525 for why we need this
606
+ await sleep(10 * 1000);
607
+
608
+ return lambdaRoleArn;
609
+ }
610
+
611
+ async createOrUpdateLambdaFunctionIfNeeded() {
612
+ const existingLambdaConfig = await this.getLambdaFunctionConfiguration();
613
+
614
+ if (existingLambdaConfig) {
615
+ debug(
616
+ 'Lambda function with this configuration already exists. Using existing function.'
617
+ );
618
+ return;
619
+ }
620
+
621
+ try {
622
+ await this.createLambda({
623
+ bucketName: this.bucketName,
624
+ functionName: this.functionName
625
+ });
626
+ return;
627
+ } catch (err) {
628
+ if (err instanceof ResourceConflictException) {
629
+ debug(
630
+ 'Lambda function with this configuration already exists. Using existing function.'
631
+ );
632
+ return;
633
+ }
634
+
635
+ throw new Error(`Failed to create Lambda Function: \n${err}`);
636
+ }
637
+ }
638
+
639
+ async getLambdaFunctionConfiguration() {
640
+ const lambda = new LambdaClient({
641
+ apiVersion: '2015-03-31',
642
+ region: this.region
643
+ });
644
+
645
+ try {
646
+ const res = await lambda.send(
647
+ new GetFunctionConfigurationCommand({
648
+ FunctionName: this.functionName
649
+ })
650
+ );
651
+
652
+ return res;
653
+ } catch (err) {
654
+ if (err instanceof ResourceNotFoundException) {
655
+ return null;
656
+ }
657
+
658
+ throw new Error(`Failed to get Lambda Function: \n${err}`);
659
+ }
660
+ }
661
+
662
+ createFunctionNameWithHash(_lambdaConfig) {
663
+ const changeableConfig = {
664
+ MemorySize: this.memorySize,
665
+ VpcConfig: {
666
+ SecurityGroupIds: this.securityGroupIds,
667
+ SubnetIds: this.subnetIds
668
+ }
669
+ };
670
+
671
+ const configHash = crypto
672
+ .createHash('md5')
673
+ .update(JSON.stringify(changeableConfig))
674
+ .digest('hex');
675
+
676
+ let name = `artilleryio-v${this.currentVersion.replace(/\./g, '-')}-${
677
+ this.architecture
678
+ }-${configHash}`;
679
+
680
+ if (name.length > 64) {
681
+ name = name.slice(0, 64);
682
+ }
683
+
684
+ return name;
685
+ }
686
+
687
+ async createLambda(opts) {
688
+ const { functionName } = opts;
689
+
690
+ const lambda = new LambdaClient({
691
+ apiVersion: '2015-03-31',
692
+ region: this.region
693
+ });
694
+
695
+ const lambdaConfig = {
696
+ PackageType: 'Image',
697
+ Code: {
698
+ ImageUri:
699
+ this.ecrImageUrl ||
700
+ `248481025674.dkr.ecr.${this.region}.amazonaws.com/artillery-worker:${this.currentVersion}-${this.architecture}`
701
+ },
702
+ ImageConfig: {
703
+ Command: ['a9-handler-index.handler'],
704
+ EntryPoint: ['/usr/bin/npx', 'aws-lambda-ric']
705
+ },
706
+ FunctionName: functionName,
707
+ Description: 'Artillery.io test',
708
+ MemorySize: parseInt(this.memorySize, 10),
709
+ Timeout: 900,
710
+ Role: this.lambdaRoleArn,
711
+ //TODO: architecture influences the entrypoint. We should review which architecture to use in the end (may impact Playwright viability)
712
+ Architectures: [this.architecture],
713
+ Environment: {
714
+ Variables: {
715
+ S3_BUCKET_PATH: this.bucketName,
716
+ NPM_CONFIG_CACHE: '/tmp/.npm', //TODO: move this to Dockerfile
717
+ AWS_LAMBDA_LOG_FORMAT: 'JSON', //TODO: review this. we need to find a ways for logs to look better in Cloudwatch
718
+ ARTILLERY_WORKER_PLATFORM: 'aws:lambda'
719
+ }
720
+ }
721
+ };
722
+
723
+ if (this.useVPC) {
724
+ lambdaConfig.VpcConfig = {
725
+ SecurityGroupIds: this.securityGroupIds,
726
+ SubnetIds: this.subnetIds
727
+ };
728
+ }
729
+
730
+ await lambda.send(new CreateFunctionCommand(lambdaConfig));
731
+ }
732
+ }
733
+
734
+ module.exports = PlatformLambda;