@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.
- package/README.md +63 -0
- package/bin/run +29 -0
- package/bin/run.cmd +3 -0
- package/changes.json +138 -0
- package/console-reporter.js +1 -0
- package/lib/artillery-global.js +33 -0
- package/lib/cli/banner.js +8 -0
- package/lib/cli/common-flags.js +80 -0
- package/lib/cli/hooks/version.js +20 -0
- package/lib/cmds/dino.js +109 -0
- package/lib/cmds/quick.js +122 -0
- package/lib/cmds/report.js +34 -0
- package/lib/cmds/run-aci.js +91 -0
- package/lib/cmds/run-fargate.js +192 -0
- package/lib/cmds/run-lambda.js +96 -0
- package/lib/cmds/run.js +671 -0
- package/lib/console-capture.js +92 -0
- package/lib/console-reporter.js +438 -0
- package/lib/create-bom/built-in-plugins.js +12 -0
- package/lib/create-bom/create-bom.js +301 -0
- package/lib/dispatcher.js +9 -0
- package/lib/dist.js +222 -0
- package/lib/index.js +5 -0
- package/lib/launch-platform.js +439 -0
- package/lib/load-plugins.js +113 -0
- package/lib/platform/aws/aws-cloudwatch.js +106 -0
- package/lib/platform/aws/aws-create-sqs-queue.js +58 -0
- package/lib/platform/aws/aws-ensure-s3-bucket-exists.js +78 -0
- package/lib/platform/aws/aws-get-account-id.js +26 -0
- package/lib/platform/aws/aws-get-bucket-region.js +18 -0
- package/lib/platform/aws/aws-get-credentials.js +28 -0
- package/lib/platform/aws/aws-get-default-region.js +26 -0
- package/lib/platform/aws/aws-whoami.js +15 -0
- package/lib/platform/aws/constants.js +7 -0
- package/lib/platform/aws/iam-cf-templates/aws-iam-fargate-cf-template.yml +219 -0
- package/lib/platform/aws/iam-cf-templates/aws-iam-lambda-cf-template.yml +125 -0
- package/lib/platform/aws/iam-cf-templates/gh-oidc-fargate.yml +241 -0
- package/lib/platform/aws/iam-cf-templates/gh-oidc-lambda.yml +153 -0
- package/lib/platform/aws-ecs/ecs.js +247 -0
- package/lib/platform/aws-ecs/legacy/aws-util.js +134 -0
- package/lib/platform/aws-ecs/legacy/bom.js +528 -0
- package/lib/platform/aws-ecs/legacy/constants.js +27 -0
- package/lib/platform/aws-ecs/legacy/create-s3-client.js +24 -0
- package/lib/platform/aws-ecs/legacy/create-test.js +247 -0
- package/lib/platform/aws-ecs/legacy/errors.js +34 -0
- package/lib/platform/aws-ecs/legacy/find-public-subnets.js +149 -0
- package/lib/platform/aws-ecs/legacy/plugins/artillery-plugin-inspect-script/index.js +27 -0
- package/lib/platform/aws-ecs/legacy/plugins/artillery-plugin-sqs-reporter/azure-aqs.js +80 -0
- package/lib/platform/aws-ecs/legacy/plugins/artillery-plugin-sqs-reporter/index.js +202 -0
- package/lib/platform/aws-ecs/legacy/plugins.js +16 -0
- package/lib/platform/aws-ecs/legacy/run-cluster.js +1994 -0
- package/lib/platform/aws-ecs/legacy/sqs-reporter.js +401 -0
- package/lib/platform/aws-ecs/legacy/tags.js +22 -0
- package/lib/platform/aws-ecs/legacy/test-run-status.js +9 -0
- package/lib/platform/aws-ecs/legacy/time.js +67 -0
- package/lib/platform/aws-ecs/legacy/util.js +97 -0
- package/lib/platform/aws-ecs/worker/Dockerfile +64 -0
- package/lib/platform/aws-ecs/worker/helpers.sh +80 -0
- package/lib/platform/aws-ecs/worker/loadgen-worker +656 -0
- package/lib/platform/aws-lambda/dependencies.js +130 -0
- package/lib/platform/aws-lambda/index.js +734 -0
- package/lib/platform/aws-lambda/lambda-handler/a9-handler-dependencies.js +73 -0
- package/lib/platform/aws-lambda/lambda-handler/a9-handler-helpers.js +43 -0
- package/lib/platform/aws-lambda/lambda-handler/a9-handler-index.js +235 -0
- package/lib/platform/aws-lambda/lambda-handler/package.json +15 -0
- package/lib/platform/aws-lambda/prices.js +29 -0
- package/lib/platform/az/aci.js +694 -0
- package/lib/platform/az/aqs-queue-consumer.js +88 -0
- package/lib/platform/az/regions.js +52 -0
- package/lib/platform/cloud/api.js +72 -0
- package/lib/platform/cloud/cloud.js +448 -0
- package/lib/platform/cloud/http-client.js +19 -0
- package/lib/platform/local/artillery-worker-local.js +154 -0
- package/lib/platform/local/index.js +174 -0
- package/lib/platform/local/worker.js +261 -0
- package/lib/platform/worker-states.js +13 -0
- package/lib/queue-consumer/index.js +56 -0
- package/lib/stash.js +41 -0
- package/lib/telemetry.js +78 -0
- package/lib/util/await-on-ee.js +24 -0
- package/lib/util/generate-id.js +9 -0
- package/lib/util/parse-tag-string.js +21 -0
- package/lib/util/prepare-test-execution-plan.js +216 -0
- package/lib/util/sleep.js +7 -0
- package/lib/util/validate-script.js +132 -0
- package/lib/util.js +294 -0
- package/lib/utils-config.js +31 -0
- package/package.json +323 -0
- package/types.d.ts +317 -0
- 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;
|