@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,1994 @@
|
|
|
1
|
+
const {
|
|
2
|
+
ECSClient,
|
|
3
|
+
CreateClusterCommand,
|
|
4
|
+
DescribeClustersCommand,
|
|
5
|
+
RunTaskCommand,
|
|
6
|
+
StopTaskCommand,
|
|
7
|
+
DescribeTaskDefinitionCommand,
|
|
8
|
+
RegisterTaskDefinitionCommand,
|
|
9
|
+
DeregisterTaskDefinitionCommand,
|
|
10
|
+
InvalidParameterException,
|
|
11
|
+
ThrottlingException
|
|
12
|
+
} = require('@aws-sdk/client-ecs');
|
|
13
|
+
const {
|
|
14
|
+
SQSClient,
|
|
15
|
+
CreateQueueCommand,
|
|
16
|
+
DeleteQueueCommand,
|
|
17
|
+
ListQueuesCommand,
|
|
18
|
+
GetQueueAttributesCommand
|
|
19
|
+
} = require('@aws-sdk/client-sqs');
|
|
20
|
+
const { GetObjectCommand, NoSuchKey } = require('@aws-sdk/client-s3');
|
|
21
|
+
const { IAMClient, GetRoleCommand } = require('@aws-sdk/client-iam');
|
|
22
|
+
// Normal debugging for messages, summaries, and errors:
|
|
23
|
+
const debug = require('debug')('commands:run-test');
|
|
24
|
+
// Verbose debugging for responses from AWS API calls, large objects etc:
|
|
25
|
+
const debugVerbose = require('debug')('commands:run-test:v');
|
|
26
|
+
const debugErr = require('debug')('commands:run-test:errors');
|
|
27
|
+
const _A = require('async');
|
|
28
|
+
const path = require('node:path');
|
|
29
|
+
const fs = require('node:fs');
|
|
30
|
+
const chalk = require('chalk');
|
|
31
|
+
const defaultOptions = require('rc')('artillery');
|
|
32
|
+
const moment = require('moment');
|
|
33
|
+
|
|
34
|
+
const EnsurePlugin = require('artillery-plugin-ensure');
|
|
35
|
+
const SlackPlugin = require('artillery-plugin-slack');
|
|
36
|
+
|
|
37
|
+
const {
|
|
38
|
+
getADOTRelevantReporterConfigs,
|
|
39
|
+
resolveADOTConfigSettings
|
|
40
|
+
} = require('artillery-plugin-publish-metrics');
|
|
41
|
+
|
|
42
|
+
const EventEmitter = require('node:events');
|
|
43
|
+
|
|
44
|
+
const _ = require('lodash');
|
|
45
|
+
|
|
46
|
+
const pkg = require('../../../../package.json');
|
|
47
|
+
const { parseTags } = require('./tags');
|
|
48
|
+
const { Timeout, sleep, timeStringToMs } = require('./time');
|
|
49
|
+
const { SqsReporter } = require('./sqs-reporter');
|
|
50
|
+
|
|
51
|
+
const awaitOnEE = require('../../../../lib/util/await-on-ee');
|
|
52
|
+
|
|
53
|
+
const { VPCSubnetFinder } = require('./find-public-subnets');
|
|
54
|
+
const awsUtil = require('./aws-util');
|
|
55
|
+
const { createTest } = require('./create-test');
|
|
56
|
+
|
|
57
|
+
const createS3Client = require('./create-s3-client');
|
|
58
|
+
const { getBucketName } = require('./util');
|
|
59
|
+
const getAccountId = require('../../aws/aws-get-account-id');
|
|
60
|
+
const { setCloudwatchRetention } = require('../../aws/aws-cloudwatch');
|
|
61
|
+
|
|
62
|
+
const dotenv = require('dotenv');
|
|
63
|
+
|
|
64
|
+
const util = require('./util');
|
|
65
|
+
|
|
66
|
+
module.exports = runCluster;
|
|
67
|
+
|
|
68
|
+
let consoleReporter = {
|
|
69
|
+
toggleSpinner: () => {}
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const {
|
|
73
|
+
TASK_NAME,
|
|
74
|
+
SQS_QUEUES_NAME_PREFIX,
|
|
75
|
+
LOGGROUP_NAME,
|
|
76
|
+
LOGGROUP_RETENTION_DAYS,
|
|
77
|
+
IMAGE_VERSION,
|
|
78
|
+
WAIT_TIMEOUT,
|
|
79
|
+
ARTILLERY_CLUSTER_NAME,
|
|
80
|
+
TEST_RUNS_MAX_TAGS
|
|
81
|
+
} = require('./constants');
|
|
82
|
+
|
|
83
|
+
const {
|
|
84
|
+
TestNotFoundError,
|
|
85
|
+
NoAvailableQueueError,
|
|
86
|
+
ClientServerVersionMismatchError
|
|
87
|
+
} = require('./errors');
|
|
88
|
+
|
|
89
|
+
let IS_FARGATE = false;
|
|
90
|
+
|
|
91
|
+
const TEST_RUN_STATUS = require('./test-run-status');
|
|
92
|
+
const prepareTestExecutionPlan = require('../../../util/prepare-test-execution-plan');
|
|
93
|
+
const { PutObjectCommand } = require('@aws-sdk/client-s3');
|
|
94
|
+
const awsGetDefaultRegion = require('../../aws/aws-get-default-region');
|
|
95
|
+
|
|
96
|
+
function setupConsoleReporter(quiet) {
|
|
97
|
+
const reporterOpts = {
|
|
98
|
+
outputFormat: 'classic',
|
|
99
|
+
printPeriod: false,
|
|
100
|
+
quiet: quiet
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
if (
|
|
104
|
+
global.artillery?.version?.startsWith('2')
|
|
105
|
+
) {
|
|
106
|
+
delete reporterOpts.outputFormat;
|
|
107
|
+
delete reporterOpts.printPeriod;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const reporterEvents = new EventEmitter();
|
|
111
|
+
consoleReporter = global.artillery.__createReporter(
|
|
112
|
+
reporterEvents,
|
|
113
|
+
reporterOpts
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
// // Disable spinner on v1
|
|
117
|
+
if (
|
|
118
|
+
global.artillery?.version &&
|
|
119
|
+
!global.artillery.version.startsWith('2')
|
|
120
|
+
) {
|
|
121
|
+
consoleReporter.spinner.stop();
|
|
122
|
+
consoleReporter.spinner.clear();
|
|
123
|
+
consoleReporter.spinner = {
|
|
124
|
+
start: () => {},
|
|
125
|
+
stop: () => {},
|
|
126
|
+
clear: () => {}
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return {
|
|
131
|
+
reporterEvents
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function runCluster(scriptPath, options) {
|
|
136
|
+
const artilleryReporter = setupConsoleReporter(options.quiet);
|
|
137
|
+
|
|
138
|
+
// camelCase all flag names, e.g. `launch-config` becomes launchConfig
|
|
139
|
+
const options2 = {};
|
|
140
|
+
for (const [k, v] of Object.entries(options)) {
|
|
141
|
+
options2[_.camelCase(k)] = v;
|
|
142
|
+
}
|
|
143
|
+
tryRunCluster(scriptPath, options2, artilleryReporter);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function logProgress(msg, opts = {}) {
|
|
147
|
+
if (typeof opts.showTimestamp === 'undefined') {
|
|
148
|
+
opts.showTimestamp = true;
|
|
149
|
+
}
|
|
150
|
+
if (global.artillery?.log) {
|
|
151
|
+
artillery.logger(opts).log(msg);
|
|
152
|
+
} else {
|
|
153
|
+
consoleReporter.toggleSpinner();
|
|
154
|
+
artillery.log(
|
|
155
|
+
`${msg} ${chalk.gray(`[${moment().format('HH:mm:ss')}]`)}`
|
|
156
|
+
);
|
|
157
|
+
consoleReporter.toggleSpinner();
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
async function tryRunCluster(scriptPath, options, artilleryReporter) {
|
|
162
|
+
global.artillery.awsRegion = (await awsGetDefaultRegion()) || options.region;
|
|
163
|
+
|
|
164
|
+
let context = {};
|
|
165
|
+
const inputFiles = [].concat(scriptPath, options.config || []);
|
|
166
|
+
const runnableScript = await prepareTestExecutionPlan(inputFiles, options);
|
|
167
|
+
|
|
168
|
+
context.runnableScript = runnableScript;
|
|
169
|
+
|
|
170
|
+
let absoluteScriptPath;
|
|
171
|
+
if (typeof scriptPath !== 'undefined') {
|
|
172
|
+
absoluteScriptPath = path.resolve(process.cwd(), scriptPath);
|
|
173
|
+
context.namedTest = false;
|
|
174
|
+
|
|
175
|
+
try {
|
|
176
|
+
fs.statSync(absoluteScriptPath);
|
|
177
|
+
} catch (_statErr) {
|
|
178
|
+
artillery.log('Could not read file:', scriptPath);
|
|
179
|
+
process.exit(1);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (options.dotenv) {
|
|
184
|
+
const dotEnvPath = path.resolve(process.cwd(), options.dotenv);
|
|
185
|
+
const contents = fs.readFileSync(dotEnvPath);
|
|
186
|
+
context.dotenv = dotenv.parse(contents);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const cloudKey = options.key || process.env.ARTILLERY_CLOUD_API_KEY;
|
|
190
|
+
if (cloudKey) {
|
|
191
|
+
const cloudEndpoint = process.env.ARTILLERY_CLOUD_ENDPOINT;
|
|
192
|
+
// Explicitly make Artillery Cloud API key available to workers (if it's set)
|
|
193
|
+
// Relying on the fact that contents of context.dotenv gets passed onto workers
|
|
194
|
+
// for it
|
|
195
|
+
context.dotenv = {
|
|
196
|
+
...context.dotenv,
|
|
197
|
+
ARTILLERY_CLOUD_API_KEY: cloudKey
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
// Explicitly make Artillery Cloud endpoint available to workers (if it's set)
|
|
201
|
+
if (cloudEndpoint) {
|
|
202
|
+
context.dotenv = {
|
|
203
|
+
...context.dotenv,
|
|
204
|
+
ARTILLERY_CLOUD_ENDPOINT: cloudEndpoint
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if (options.bundle) {
|
|
210
|
+
context.namedTest = true;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (options.maxDuration) {
|
|
214
|
+
const maxDurationMs = timeStringToMs(options.maxDuration);
|
|
215
|
+
context.maxDurationMs = maxDurationMs;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
context.tags = parseTags(options.tags);
|
|
219
|
+
|
|
220
|
+
if (context.tags.length > TEST_RUNS_MAX_TAGS) {
|
|
221
|
+
console.error(
|
|
222
|
+
chalk.red(
|
|
223
|
+
`A maximum of ${TEST_RUNS_MAX_TAGS} tags is allowed per test run`
|
|
224
|
+
)
|
|
225
|
+
);
|
|
226
|
+
|
|
227
|
+
process.exit(1);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Set name tag if not already set:
|
|
231
|
+
if (context.tags.filter((t) => t.name === 'name').length === 0) {
|
|
232
|
+
if (typeof scriptPath !== 'undefined') {
|
|
233
|
+
context.tags.push({
|
|
234
|
+
name: 'name',
|
|
235
|
+
value: path.basename(scriptPath)
|
|
236
|
+
});
|
|
237
|
+
} else {
|
|
238
|
+
context.tags.push({
|
|
239
|
+
name: 'name',
|
|
240
|
+
value: options.bundle
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
if (options.name) {
|
|
246
|
+
for (const t of context.tags) {
|
|
247
|
+
if (t.name === 'name') {
|
|
248
|
+
t.value = options.name;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
context.extraSecrets = options.secret || [];
|
|
254
|
+
|
|
255
|
+
context.testId = global.artillery.testRunId;
|
|
256
|
+
|
|
257
|
+
if (context.namedTest) {
|
|
258
|
+
context.s3Prefix = options.bundle;
|
|
259
|
+
debug(`Trying to run a named test: ${context.s3Prefix}`);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
if (!context.namedTest) {
|
|
263
|
+
const contextPath = options.context
|
|
264
|
+
? path.resolve(options.context)
|
|
265
|
+
: path.dirname(absoluteScriptPath);
|
|
266
|
+
|
|
267
|
+
debugVerbose('script:', absoluteScriptPath);
|
|
268
|
+
debugVerbose('root:', contextPath);
|
|
269
|
+
|
|
270
|
+
const containerScriptPath = path.join(
|
|
271
|
+
path.relative(contextPath, path.dirname(absoluteScriptPath)),
|
|
272
|
+
path.basename(absoluteScriptPath)
|
|
273
|
+
);
|
|
274
|
+
|
|
275
|
+
if (containerScriptPath.indexOf('..') !== -1) {
|
|
276
|
+
artillery.log(
|
|
277
|
+
chalk.red(
|
|
278
|
+
'Test script must reside inside the context dir. See Artillery docs for more details.'
|
|
279
|
+
)
|
|
280
|
+
);
|
|
281
|
+
process.exit(1);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// FIXME: These need clearer names. dir vs path and local vs container.
|
|
285
|
+
context.contextDir = contextPath;
|
|
286
|
+
context.newScriptPath = containerScriptPath;
|
|
287
|
+
|
|
288
|
+
debug('container script path:', containerScriptPath);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const count = Number(options.count) || 1;
|
|
292
|
+
|
|
293
|
+
if (typeof options.taskRoleName !== 'undefined') {
|
|
294
|
+
let customRoleName = options.taskRoleName;
|
|
295
|
+
// Allow ARNs for convenience
|
|
296
|
+
// https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_identifiers.html
|
|
297
|
+
// We split by :role because role names may contain slash characters (subpaths)
|
|
298
|
+
if (
|
|
299
|
+
customRoleName.startsWith('arn:aws:iam') ||
|
|
300
|
+
customRoleName.startsWith('arn:aws-cn:iam')
|
|
301
|
+
) {
|
|
302
|
+
customRoleName = customRoleName.split(':role/')[1];
|
|
303
|
+
}
|
|
304
|
+
context.customTaskRoleName = customRoleName;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
const clusterName = options.cluster || ARTILLERY_CLUSTER_NAME;
|
|
308
|
+
if (options.launchConfig) {
|
|
309
|
+
let launchConfig;
|
|
310
|
+
try {
|
|
311
|
+
launchConfig = JSON.parse(options.launchConfig);
|
|
312
|
+
} catch (parseErr) {
|
|
313
|
+
debug(parseErr);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
if (!launchConfig) {
|
|
317
|
+
artillery.log(
|
|
318
|
+
chalk.red(
|
|
319
|
+
"Launch config could not be parsed. Please check that it's valid JSON."
|
|
320
|
+
)
|
|
321
|
+
);
|
|
322
|
+
process.exit(1);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
if (launchConfig.ulimits && !Array.isArray(launchConfig.ulimits)) {
|
|
326
|
+
// TODO: Proper schema validation for the object
|
|
327
|
+
artillery.log(chalk.red('ulimits must be an array of objects'));
|
|
328
|
+
artillery.log(
|
|
329
|
+
'Please see AWS documentation for more information:\nhttps://docs.aws.amazon.com/AmazonECS/latest/APIReference/API_Ulimit.html'
|
|
330
|
+
);
|
|
331
|
+
process.exit(1);
|
|
332
|
+
}
|
|
333
|
+
options.launchConfig = launchConfig;
|
|
334
|
+
} else {
|
|
335
|
+
options.launchConfig = {};
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
if (options.cpu) {
|
|
339
|
+
const n = Number(options.cpu);
|
|
340
|
+
if (Number.isNaN(n)) {
|
|
341
|
+
artillery.log('The value of --cpu must be a number');
|
|
342
|
+
process.exit(1);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// Allow specifying 16 vCPU as either "16" or "16384". The actual value is
|
|
346
|
+
// validated later.
|
|
347
|
+
const MAX_VCPUS = 16;
|
|
348
|
+
if (n <= MAX_VCPUS) {
|
|
349
|
+
options.launchConfig.cpu = n * 1024;
|
|
350
|
+
} else {
|
|
351
|
+
options.launchConfig.cpu = n;
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
if (options.memory) {
|
|
356
|
+
const n = Number(options.memory);
|
|
357
|
+
if (Number.isNaN(n)) {
|
|
358
|
+
artillery.log('The value of --memory must be a number');
|
|
359
|
+
process.exit(1);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
const MAX_MEMORY_IN_GB = 120;
|
|
363
|
+
if (n <= MAX_MEMORY_IN_GB) {
|
|
364
|
+
options.launchConfig.memory = String(parseInt(options.memory, 10) * 1024);
|
|
365
|
+
} else {
|
|
366
|
+
options.launchConfig.memory = options.memory;
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// check launch type is valid:
|
|
371
|
+
if (typeof options.launchType !== 'undefined') {
|
|
372
|
+
if (
|
|
373
|
+
options.launchType !== 'ecs:fargate' &&
|
|
374
|
+
options.launchType !== 'ecs:ec2'
|
|
375
|
+
) {
|
|
376
|
+
artillery.log(
|
|
377
|
+
'Invalid launch type - the value of --launch-type needs to be ecs:fargate or ecs:ec2'
|
|
378
|
+
);
|
|
379
|
+
process.exit(1);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
if (typeof options.fargate !== 'undefined') {
|
|
384
|
+
console.error(
|
|
385
|
+
'The --fargate flag is deprecated, use --launch-type ecs:fargate instead'
|
|
386
|
+
);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
if (options.fargate && options.launchType) {
|
|
390
|
+
console.error(
|
|
391
|
+
'Either --fargate or --launch-type flag should be set, not both'
|
|
392
|
+
);
|
|
393
|
+
process.exit(1);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
if (
|
|
397
|
+
typeof options.fargate === 'undefined' &&
|
|
398
|
+
typeof options.launchType === 'undefined'
|
|
399
|
+
) {
|
|
400
|
+
options.launchType = 'ecs:fargate';
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
IS_FARGATE =
|
|
404
|
+
typeof options.fargate !== 'undefined' || // --fargate set
|
|
405
|
+
typeof options.publicSubnetIds !== 'undefined' || // --public-subnet-ids set
|
|
406
|
+
(typeof options.launchType !== 'undefined' &&
|
|
407
|
+
options.launchType === 'ecs:fargate') || // --launch-type ecs:fargate
|
|
408
|
+
typeof options.launchType === 'undefined';
|
|
409
|
+
|
|
410
|
+
global.artillery.globalEvents.emit('test:init', {
|
|
411
|
+
flags: options,
|
|
412
|
+
testRunId: context.testId,
|
|
413
|
+
tags: context.tags,
|
|
414
|
+
metadata: {
|
|
415
|
+
testId: context.testId,
|
|
416
|
+
startedAt: Date.now(),
|
|
417
|
+
count,
|
|
418
|
+
tags: context.tags,
|
|
419
|
+
launchType: options.launchType
|
|
420
|
+
}
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
let packageJsonPath;
|
|
424
|
+
if (options.packages) {
|
|
425
|
+
packageJsonPath = path.resolve(process.cwd(), options.packages);
|
|
426
|
+
try {
|
|
427
|
+
// TODO: Check that filename is package.json
|
|
428
|
+
|
|
429
|
+
JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
|
|
430
|
+
} catch (err) {
|
|
431
|
+
console.error('Could not load package dependency list');
|
|
432
|
+
console.error('Trying to read from:', packageJsonPath);
|
|
433
|
+
console.error(err);
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
context = Object.assign(context, {
|
|
438
|
+
scriptPath: absoluteScriptPath,
|
|
439
|
+
originalScriptPath: scriptPath,
|
|
440
|
+
count: count,
|
|
441
|
+
region: options.region,
|
|
442
|
+
arnPrefix: options.region.startsWith('cn-') ? 'arn:aws-cn' : 'arn:aws',
|
|
443
|
+
taskName: `${TASK_NAME}_${
|
|
444
|
+
IS_FARGATE ? 'fargate' : ''
|
|
445
|
+
}_${clusterName}_${IMAGE_VERSION.replace(/\./g, '-')}_${Math.floor(
|
|
446
|
+
Math.random() * 1e6
|
|
447
|
+
)}`,
|
|
448
|
+
clusterName: clusterName,
|
|
449
|
+
logGroupName: LOGGROUP_NAME,
|
|
450
|
+
cliOptions: options,
|
|
451
|
+
isFargate: IS_FARGATE,
|
|
452
|
+
isCapacitySpot: typeof options.spot !== 'undefined',
|
|
453
|
+
configTableName: '',
|
|
454
|
+
status: TEST_RUN_STATUS.INITIALIZING,
|
|
455
|
+
packageJsonPath,
|
|
456
|
+
taskArns: []
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
let subnetIds = [];
|
|
460
|
+
if (options.publicSubnetIds) {
|
|
461
|
+
console.error(
|
|
462
|
+
`${chalk.yellow(
|
|
463
|
+
'Warning'
|
|
464
|
+
)}: --public-subnet-ids will be deprecated. Use --subnet-ids instead.`
|
|
465
|
+
);
|
|
466
|
+
|
|
467
|
+
subnetIds = options.publicSubnetIds.split(',');
|
|
468
|
+
}
|
|
469
|
+
if (options.subnetIds) {
|
|
470
|
+
subnetIds = options.subnetIds.split(',');
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
if (IS_FARGATE) {
|
|
474
|
+
context.fargatePublicSubnetIds = subnetIds;
|
|
475
|
+
context.fargateSecurityGroupIds =
|
|
476
|
+
typeof options.securityGroupIds !== 'undefined'
|
|
477
|
+
? options.securityGroupIds.split(',')
|
|
478
|
+
: [];
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
if (global.artillery?.telemetry) {
|
|
482
|
+
global.artillery.telemetry.capture('run-test', {
|
|
483
|
+
version: global.artillery.version,
|
|
484
|
+
proVersion: pkg.version,
|
|
485
|
+
count: count,
|
|
486
|
+
launchPlatform: IS_FARGATE ? 'ecs:fargate' : 'ecs:ec2',
|
|
487
|
+
usesTags: context.tags.length > 0,
|
|
488
|
+
region: context.region,
|
|
489
|
+
crossRegion: context.region !== context.backendRegion
|
|
490
|
+
});
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
async function newWaterfall(artilleryReporter) {
|
|
494
|
+
let testRunCompletedSuccessfully = true;
|
|
495
|
+
|
|
496
|
+
let shuttingDown = false;
|
|
497
|
+
|
|
498
|
+
async function gracefulShutdown(opts = { earlyStop: false, exitCode: 0 }) {
|
|
499
|
+
if (shuttingDown) {
|
|
500
|
+
return;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
shuttingDown = true;
|
|
504
|
+
|
|
505
|
+
if (opts.earlyStop) {
|
|
506
|
+
if (context.status !== TEST_RUN_STATUS.ERROR) {
|
|
507
|
+
// Retain ERROR status if already set elsewhere
|
|
508
|
+
context.status = TEST_RUN_STATUS.EARLY_STOP;
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
await cleanupResources(context);
|
|
512
|
+
|
|
513
|
+
global.artillery.globalEvents.emit('shutdown:start', {
|
|
514
|
+
exitCode: opts.exitCode,
|
|
515
|
+
earlyStop: opts.earlyStop
|
|
516
|
+
});
|
|
517
|
+
|
|
518
|
+
const ps = [];
|
|
519
|
+
for (const e of global.artillery.extensionEvents) {
|
|
520
|
+
const testInfo = { endTime: Date.now() };
|
|
521
|
+
if (e.ext === 'beforeExit') {
|
|
522
|
+
ps.push(
|
|
523
|
+
e.method({
|
|
524
|
+
report: context.aggregateReport,
|
|
525
|
+
flags: context.cliOptions,
|
|
526
|
+
runnerOpts: {
|
|
527
|
+
environment: context.cliOptions?.environment,
|
|
528
|
+
scriptPath: '',
|
|
529
|
+
absoluteScriptPath: ''
|
|
530
|
+
},
|
|
531
|
+
testInfo
|
|
532
|
+
})
|
|
533
|
+
);
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
await Promise.allSettled(ps);
|
|
537
|
+
|
|
538
|
+
const ps2 = [];
|
|
539
|
+
const shutdownOpts = {
|
|
540
|
+
earlyStop: opts.earlyStop,
|
|
541
|
+
exitCode: opts.exitCode
|
|
542
|
+
};
|
|
543
|
+
for (const e of global.artillery.extensionEvents) {
|
|
544
|
+
if (e.ext === 'onShutdown') {
|
|
545
|
+
ps2.push(e.method(shutdownOpts));
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
await Promise.allSettled(ps2);
|
|
549
|
+
|
|
550
|
+
process.exit(global.artillery.suggestedExitCode || opts.exitCode);
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
global.artillery.shutdown = gracefulShutdown;
|
|
554
|
+
|
|
555
|
+
process.on('SIGINT', async () => {
|
|
556
|
+
if (shuttingDown) {
|
|
557
|
+
return;
|
|
558
|
+
}
|
|
559
|
+
console.log('Stopping test run (SIGINT received)...');
|
|
560
|
+
await gracefulShutdown({ exitCode: 1, earlyStop: true });
|
|
561
|
+
});
|
|
562
|
+
process.on('SIGTERM', async () => {
|
|
563
|
+
if (shuttingDown) {
|
|
564
|
+
return;
|
|
565
|
+
}
|
|
566
|
+
console.log('Stopping test run (SIGTERM received)...');
|
|
567
|
+
await gracefulShutdown({ exitCode: 1, earlyStop: true });
|
|
568
|
+
});
|
|
569
|
+
|
|
570
|
+
// Messages from SQS reporter created later will be relayed via this EE
|
|
571
|
+
context.reporterEvents = artilleryReporter.reporterEvents;
|
|
572
|
+
|
|
573
|
+
try {
|
|
574
|
+
logProgress('Checking AWS connectivity...');
|
|
575
|
+
context.accountId = await getAccountId({ region: context.region });
|
|
576
|
+
await Promise.all([
|
|
577
|
+
(async (context) => {
|
|
578
|
+
const bucketName = await getBucketName();
|
|
579
|
+
context.s3Bucket = bucketName;
|
|
580
|
+
return context;
|
|
581
|
+
})(context)
|
|
582
|
+
]);
|
|
583
|
+
|
|
584
|
+
logProgress('Checking cluster...');
|
|
585
|
+
const clusterExists = await checkTargetCluster(context);
|
|
586
|
+
|
|
587
|
+
if (!clusterExists) {
|
|
588
|
+
if (typeof context.cliOptions.cluster === 'undefined') {
|
|
589
|
+
// User did not specify a cluster with --cluster, and ARTILLERY_CLUSTER_NAME
|
|
590
|
+
// does not exist, so create it
|
|
591
|
+
await createArtilleryCluster(context);
|
|
592
|
+
} else {
|
|
593
|
+
// User specified a cluster, but it's not there
|
|
594
|
+
throw new Error(
|
|
595
|
+
`Could not find cluster ${context.clusterName} in ${context.region}`
|
|
596
|
+
);
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
if (context.tags.length > 0) {
|
|
601
|
+
logProgress(
|
|
602
|
+
`Tags: ${context.tags.map((t) => `${t.name}:${t.value}`).join(', ')}`
|
|
603
|
+
);
|
|
604
|
+
}
|
|
605
|
+
logProgress(`Test run ID: ${context.testId}`);
|
|
606
|
+
|
|
607
|
+
logProgress('Preparing launch platform...');
|
|
608
|
+
|
|
609
|
+
await maybeGetSubnetIdsForFargate(context);
|
|
610
|
+
|
|
611
|
+
logProgress(
|
|
612
|
+
`Environment:
|
|
613
|
+
Account: ${context.accountId}
|
|
614
|
+
Region: ${context.region}
|
|
615
|
+
Count: ${context.count}
|
|
616
|
+
Cluster: ${context.clusterName}
|
|
617
|
+
Launch type: ${context.cliOptions.launchType} ${
|
|
618
|
+
context.isFargate && context.isCapacitySpot ? '(Spot)' : '(On-demand)'
|
|
619
|
+
}
|
|
620
|
+
`,
|
|
621
|
+
{ showTimestamp: false }
|
|
622
|
+
);
|
|
623
|
+
|
|
624
|
+
await createQueue(context);
|
|
625
|
+
await checkCustomTaskRole(context);
|
|
626
|
+
logProgress('Preparing test bundle...');
|
|
627
|
+
await createTestBundle(context);
|
|
628
|
+
await createADOTDefinitionIfNeeded(context);
|
|
629
|
+
await ensureTaskExists(context);
|
|
630
|
+
await getManifest(context);
|
|
631
|
+
await generateTaskOverrides(context);
|
|
632
|
+
|
|
633
|
+
logProgress('Launching workers...');
|
|
634
|
+
await setupDefaultECSParams(context);
|
|
635
|
+
|
|
636
|
+
if (
|
|
637
|
+
context.status !== TEST_RUN_STATUS.EARLY_STOP &&
|
|
638
|
+
context.status !== TEST_RUN_STATUS.TERMINATING
|
|
639
|
+
) {
|
|
640
|
+
// Set up SQS listener:
|
|
641
|
+
listen(context, artilleryReporter.reporterEvents);
|
|
642
|
+
await launchLeadTask(context);
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
setCloudwatchRetention(
|
|
646
|
+
`${LOGGROUP_NAME}/${context.clusterName}`,
|
|
647
|
+
LOGGROUP_RETENTION_DAYS,
|
|
648
|
+
context.region,
|
|
649
|
+
{
|
|
650
|
+
maxRetries: 10,
|
|
651
|
+
waitPerRetry: 2 * 1000
|
|
652
|
+
}
|
|
653
|
+
);
|
|
654
|
+
|
|
655
|
+
if (
|
|
656
|
+
context.status !== TEST_RUN_STATUS.EARLY_STOP &&
|
|
657
|
+
context.status !== TEST_RUN_STATUS.TERMINATING
|
|
658
|
+
) {
|
|
659
|
+
logProgress(
|
|
660
|
+
context.isFargate ? 'Waiting for Fargate...' : 'Waiting for ECS...'
|
|
661
|
+
);
|
|
662
|
+
await ecsRunTask(context);
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
if (
|
|
666
|
+
context.status !== TEST_RUN_STATUS.EARLY_STOP &&
|
|
667
|
+
context.status !== TEST_RUN_STATUS.TERMINATING
|
|
668
|
+
) {
|
|
669
|
+
await waitForTasks2(context);
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
if (
|
|
673
|
+
context.status !== TEST_RUN_STATUS.EARLY_STOP &&
|
|
674
|
+
context.status !== TEST_RUN_STATUS.TERMINATING
|
|
675
|
+
) {
|
|
676
|
+
logProgress('Waiting for workers to come online...');
|
|
677
|
+
await waitForWorkerSync(context);
|
|
678
|
+
await sendGoSignal(context);
|
|
679
|
+
logProgress('Workers are running, waiting for reports...');
|
|
680
|
+
|
|
681
|
+
if (context.maxDurationMs && context.maxDurationMs > 0) {
|
|
682
|
+
logProgress(
|
|
683
|
+
`Max duration for test run is set to: ${context.cliOptions.maxDuration}`
|
|
684
|
+
);
|
|
685
|
+
const testDurationTimeout = new Timeout(context.maxDurationMs);
|
|
686
|
+
testDurationTimeout.start();
|
|
687
|
+
testDurationTimeout.on('timeout', async () => {
|
|
688
|
+
artillery.log(
|
|
689
|
+
`Max duration of test run exceeded: ${context.cliOptions.maxDuration}\n`
|
|
690
|
+
);
|
|
691
|
+
await gracefulShutdown({ earlyStop: true });
|
|
692
|
+
});
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
context.status = TEST_RUN_STATUS.RECEIVING_REPORTS;
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
// Need to wait for all reports to be over here, not exit
|
|
699
|
+
const workerState = await awaitOnEE(
|
|
700
|
+
artilleryReporter.reporterEvents,
|
|
701
|
+
'workersDone'
|
|
702
|
+
);
|
|
703
|
+
debug(workerState);
|
|
704
|
+
|
|
705
|
+
logProgress(`Test run completed: ${context.testId}`);
|
|
706
|
+
|
|
707
|
+
context.status = TEST_RUN_STATUS.COMPLETED;
|
|
708
|
+
|
|
709
|
+
let checks = [];
|
|
710
|
+
global.artillery.globalEvents.once('checks', async (results) => {
|
|
711
|
+
checks = results;
|
|
712
|
+
});
|
|
713
|
+
|
|
714
|
+
if (context.ensureSpec) {
|
|
715
|
+
new EnsurePlugin.Plugin({ config: { ensure: context.ensureSpec } });
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
if (context.fullyResolvedConfig?.plugins?.slack) {
|
|
719
|
+
new SlackPlugin.Plugin({
|
|
720
|
+
config: context.fullyResolvedConfig
|
|
721
|
+
});
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
if (context.cliOptions.output) {
|
|
725
|
+
const logfile = getLogFilename(
|
|
726
|
+
context.cliOptions.output,
|
|
727
|
+
defaultOptions.logFilenameFormat
|
|
728
|
+
);
|
|
729
|
+
|
|
730
|
+
for (const ix of context.intermediateReports) {
|
|
731
|
+
delete ix.histograms;
|
|
732
|
+
ix.histograms = ix.summaries;
|
|
733
|
+
}
|
|
734
|
+
delete context.aggregateReport.histograms;
|
|
735
|
+
context.aggregateReport.histograms = context.aggregateReport.summaries;
|
|
736
|
+
|
|
737
|
+
const jsonReport = {
|
|
738
|
+
intermediate: context.intermediateReports,
|
|
739
|
+
aggregate: context.aggregateReport,
|
|
740
|
+
testId: context.testId,
|
|
741
|
+
metadata: {
|
|
742
|
+
tags: context.tags,
|
|
743
|
+
count: context.count,
|
|
744
|
+
region: context.region,
|
|
745
|
+
cluster: context.clusterName,
|
|
746
|
+
artilleryVersion: {
|
|
747
|
+
core: global.artillery.version,
|
|
748
|
+
pro: pkg.version
|
|
749
|
+
}
|
|
750
|
+
},
|
|
751
|
+
ensure: checks.map((c) => {
|
|
752
|
+
return {
|
|
753
|
+
condition: c.original,
|
|
754
|
+
success: c.result === 1,
|
|
755
|
+
strict: c.strict
|
|
756
|
+
};
|
|
757
|
+
})
|
|
758
|
+
};
|
|
759
|
+
|
|
760
|
+
fs.writeFileSync(logfile, JSON.stringify(jsonReport, null, 2), {
|
|
761
|
+
flag: 'w'
|
|
762
|
+
});
|
|
763
|
+
}
|
|
764
|
+
debug(context.testId, 'done');
|
|
765
|
+
} catch (err) {
|
|
766
|
+
debug(err);
|
|
767
|
+
if (err instanceof InvalidParameterException) {
|
|
768
|
+
if (
|
|
769
|
+
err.message
|
|
770
|
+
.toLowerCase()
|
|
771
|
+
.indexOf('no container instances were found') !== -1
|
|
772
|
+
) {
|
|
773
|
+
artillery.log(
|
|
774
|
+
chalk.yellow('The ECS cluster has no active EC2 instances')
|
|
775
|
+
);
|
|
776
|
+
} else {
|
|
777
|
+
artillery.log(err);
|
|
778
|
+
}
|
|
779
|
+
} else if (err instanceof TestNotFoundError) {
|
|
780
|
+
artillery.log(`Test ${context.s3Prefix} not found`);
|
|
781
|
+
} else if (
|
|
782
|
+
err instanceof NoAvailableQueueError ||
|
|
783
|
+
err instanceof ClientServerVersionMismatchError
|
|
784
|
+
) {
|
|
785
|
+
artillery.log(chalk.red('Error:', err.message));
|
|
786
|
+
} else {
|
|
787
|
+
artillery.log(util.formatError(err));
|
|
788
|
+
artillery.log(err);
|
|
789
|
+
artillery.log(err.stack);
|
|
790
|
+
}
|
|
791
|
+
testRunCompletedSuccessfully = false;
|
|
792
|
+
global.artillery.suggestedExitCode = 1;
|
|
793
|
+
} finally {
|
|
794
|
+
if (!testRunCompletedSuccessfully) {
|
|
795
|
+
logProgress('Cleaning up...');
|
|
796
|
+
context.status = TEST_RUN_STATUS.ERROR;
|
|
797
|
+
await gracefulShutdown({ earlyStop: true, exitCode: 1 });
|
|
798
|
+
} else {
|
|
799
|
+
context.status = TEST_RUN_STATUS.COMPLETED;
|
|
800
|
+
await gracefulShutdown({ earlyStop: false, exitCode: 0 });
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
await newWaterfall(artilleryReporter);
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
async function cleanupResources(context) {
|
|
809
|
+
try {
|
|
810
|
+
if (context.sqsReporter) {
|
|
811
|
+
context.sqsReporter.stop();
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
if (context.adot?.SSMParameterPath) {
|
|
815
|
+
await awsUtil.deleteParameter(
|
|
816
|
+
context.adot.SSMParameterPath,
|
|
817
|
+
context.region
|
|
818
|
+
);
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
if (context.taskArns && context.taskArns.length > 0) {
|
|
822
|
+
for (const taskArn of context.taskArns) {
|
|
823
|
+
try {
|
|
824
|
+
const ecs = new ECSClient({
|
|
825
|
+
apiVersion: '2014-11-13',
|
|
826
|
+
region: context.region
|
|
827
|
+
});
|
|
828
|
+
await ecs.send(
|
|
829
|
+
new StopTaskCommand({
|
|
830
|
+
task: taskArn,
|
|
831
|
+
cluster: context.clusterName,
|
|
832
|
+
reason: 'Test cleanup'
|
|
833
|
+
})
|
|
834
|
+
);
|
|
835
|
+
} catch (err) {
|
|
836
|
+
// TODO: Retry if appropriate, give the user more information
|
|
837
|
+
// to be able to fall back to manual intervention if possible.
|
|
838
|
+
// TODO: Consumer has no idea if this succeeded or not
|
|
839
|
+
debug(err);
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
// TODO: Should either retry, or not throw in any of these
|
|
845
|
+
await Promise.all([
|
|
846
|
+
deleteQueue(context),
|
|
847
|
+
deregisterTaskDefinition(context),
|
|
848
|
+
gcQueues(context)
|
|
849
|
+
]);
|
|
850
|
+
} catch (err) {
|
|
851
|
+
artillery.log(err);
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
function checkFargateResourceConfig(cpu, memory) {
|
|
856
|
+
function generateListOfOptionsMiB(minGB, maxGB, incrementGB) {
|
|
857
|
+
const result = [];
|
|
858
|
+
for (let i = 0; i <= (maxGB - minGB) / incrementGB; i++) {
|
|
859
|
+
result.push((minGB + incrementGB * i) * 1024);
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
return result;
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
// Based on https://docs.aws.amazon.com/AmazonECS/latest/developerguide/task-cpu-memory-error.html
|
|
866
|
+
const FARGATE_VALID_CONFIGS = {
|
|
867
|
+
256: [512, 1024, 2048],
|
|
868
|
+
512: [1024, 2048, 3072, 4096],
|
|
869
|
+
1024: [2048, 3072, 4096, 5120, 6144, 7168, 8192],
|
|
870
|
+
2048: generateListOfOptionsMiB(4, 16, 1),
|
|
871
|
+
4096: generateListOfOptionsMiB(8, 30, 1),
|
|
872
|
+
8192: generateListOfOptionsMiB(16, 60, 4),
|
|
873
|
+
16384: generateListOfOptionsMiB(32, 120, 8)
|
|
874
|
+
};
|
|
875
|
+
|
|
876
|
+
if (!FARGATE_VALID_CONFIGS[cpu]) {
|
|
877
|
+
return new Error(
|
|
878
|
+
`Unsupported cpu override for Fargate. Must be one of: ${Object.keys(
|
|
879
|
+
FARGATE_VALID_CONFIGS
|
|
880
|
+
).join(', ')}`
|
|
881
|
+
);
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
if (FARGATE_VALID_CONFIGS[cpu].indexOf(memory) < 0) {
|
|
885
|
+
return new Error(
|
|
886
|
+
`Fargate memory override for cpu = ${cpu} must be one of: ${FARGATE_VALID_CONFIGS[
|
|
887
|
+
cpu
|
|
888
|
+
].join(', ')}`
|
|
889
|
+
);
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
return null;
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
async function createArtilleryCluster(context) {
|
|
896
|
+
const ecs = new ECSClient({
|
|
897
|
+
apiVersion: '2014-11-13',
|
|
898
|
+
region: context.region
|
|
899
|
+
});
|
|
900
|
+
await ecs.send(
|
|
901
|
+
new CreateClusterCommand({
|
|
902
|
+
clusterName: ARTILLERY_CLUSTER_NAME,
|
|
903
|
+
capacityProviders: ['FARGATE_SPOT']
|
|
904
|
+
})
|
|
905
|
+
);
|
|
906
|
+
|
|
907
|
+
let retries = 0;
|
|
908
|
+
while (retries < 12) {
|
|
909
|
+
const clusterActive = await checkTargetCluster(context);
|
|
910
|
+
if (clusterActive) {
|
|
911
|
+
break;
|
|
912
|
+
}
|
|
913
|
+
retries++;
|
|
914
|
+
await sleep(10 * 1000);
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
//
|
|
919
|
+
// Check that ECS cluster exists:
|
|
920
|
+
//
|
|
921
|
+
async function checkTargetCluster(context) {
|
|
922
|
+
const ecs = new ECSClient({
|
|
923
|
+
apiVersion: '2014-11-13',
|
|
924
|
+
region: context.region
|
|
925
|
+
});
|
|
926
|
+
try {
|
|
927
|
+
const response = await ecs.send(
|
|
928
|
+
new DescribeClustersCommand({ clusters: [context.clusterName] })
|
|
929
|
+
);
|
|
930
|
+
debug(response);
|
|
931
|
+
if (response.clusters.length === 0 || response.failures.length > 0) {
|
|
932
|
+
debugVerbose(response);
|
|
933
|
+
return false;
|
|
934
|
+
} else {
|
|
935
|
+
const activeClusters = response.clusters.filter(
|
|
936
|
+
(c) => c.status === 'ACTIVE'
|
|
937
|
+
);
|
|
938
|
+
return activeClusters.length > 0;
|
|
939
|
+
}
|
|
940
|
+
} catch (err) {
|
|
941
|
+
debugVerbose(err);
|
|
942
|
+
return false;
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
async function maybeGetSubnetIdsForFargate(context) {
|
|
947
|
+
if (!context.isFargate) {
|
|
948
|
+
return context;
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
// TODO: Sanity check that subnets actually exist before trying to use them in test definitions
|
|
952
|
+
|
|
953
|
+
if (context.fargatePublicSubnetIds.length > 0) {
|
|
954
|
+
return context;
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
debug('Subnet IDs not provided, looking up default VPC');
|
|
958
|
+
|
|
959
|
+
const f = new VPCSubnetFinder({ region: context.region });
|
|
960
|
+
const publicSubnets = await f.findPublicSubnets();
|
|
961
|
+
|
|
962
|
+
if (publicSubnets.length === 0) {
|
|
963
|
+
throw new Error('Could not find public subnets in default VPC');
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
context.fargatePublicSubnetIds = publicSubnets.map((s) => s.SubnetId);
|
|
967
|
+
|
|
968
|
+
debug('Found public subnets:', context.fargatePublicSubnetIds.join(', '));
|
|
969
|
+
|
|
970
|
+
return context;
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
async function createTestBundle(context) {
|
|
974
|
+
const result = await createTest(context.scriptPath, {
|
|
975
|
+
name: context.testId,
|
|
976
|
+
config: context.cliOptions.config,
|
|
977
|
+
packageJsonPath: context.packageJsonPath,
|
|
978
|
+
flags: context.cliOptions
|
|
979
|
+
});
|
|
980
|
+
|
|
981
|
+
context.fullyResolvedConfig = result.manifest.fullyResolvedConfig;
|
|
982
|
+
|
|
983
|
+
return context;
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
async function createADOTDefinitionIfNeeded(context) {
|
|
987
|
+
const publishMetricsConfig =
|
|
988
|
+
context.fullyResolvedConfig.plugins?.['publish-metrics'];
|
|
989
|
+
if (!publishMetricsConfig) {
|
|
990
|
+
debug('No publish-metrics plugin set, skipping ADOT configuration');
|
|
991
|
+
return context;
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
const adotRelevantConfigs =
|
|
995
|
+
getADOTRelevantReporterConfigs(publishMetricsConfig);
|
|
996
|
+
if (adotRelevantConfigs.length === 0) {
|
|
997
|
+
debug('No ADOT relevant reporter configs set, skipping ADOT configuration');
|
|
998
|
+
return context;
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
try {
|
|
1002
|
+
const { adotEnvVars, adotConfig } = resolveADOTConfigSettings({
|
|
1003
|
+
configList: adotRelevantConfigs,
|
|
1004
|
+
dotenv: { ...context.dotenv }
|
|
1005
|
+
});
|
|
1006
|
+
|
|
1007
|
+
context.dotenv = Object.assign(context.dotenv || {}, adotEnvVars);
|
|
1008
|
+
|
|
1009
|
+
context.adot = {
|
|
1010
|
+
SSMParameterPath: `/artilleryio/OTEL_CONFIG_${context.testId}`
|
|
1011
|
+
};
|
|
1012
|
+
|
|
1013
|
+
await awsUtil.putParameter(
|
|
1014
|
+
context.adot.SSMParameterPath,
|
|
1015
|
+
JSON.stringify(adotConfig),
|
|
1016
|
+
'String',
|
|
1017
|
+
context.region
|
|
1018
|
+
);
|
|
1019
|
+
|
|
1020
|
+
context.adot.taskDefinition = {
|
|
1021
|
+
name: 'adot-collector',
|
|
1022
|
+
image: 'amazon/aws-otel-collector:v0.39.0',
|
|
1023
|
+
command: [
|
|
1024
|
+
'--config=/etc/ecs/container-insights/otel-task-metrics-config.yaml'
|
|
1025
|
+
],
|
|
1026
|
+
secrets: [
|
|
1027
|
+
{
|
|
1028
|
+
name: 'AOT_CONFIG_CONTENT',
|
|
1029
|
+
valueFrom: `${context.arnPrefix}:ssm:${context.region}:${context.accountId}:parameter${context.adot.SSMParameterPath}`
|
|
1030
|
+
}
|
|
1031
|
+
],
|
|
1032
|
+
logConfiguration: {
|
|
1033
|
+
logDriver: 'awslogs',
|
|
1034
|
+
options: {
|
|
1035
|
+
'awslogs-group': `${context.logGroupName}/${context.clusterName}`,
|
|
1036
|
+
'awslogs-region': context.region,
|
|
1037
|
+
'awslogs-stream-prefix': `artilleryio/${context.testId}`,
|
|
1038
|
+
'awslogs-create-group': 'true'
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
1041
|
+
};
|
|
1042
|
+
} catch (err) {
|
|
1043
|
+
throw new Error(err);
|
|
1044
|
+
}
|
|
1045
|
+
return context;
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
async function ensureTaskExists(context) {
|
|
1049
|
+
const ecs = new ECSClient({
|
|
1050
|
+
apiVersion: '2014-11-13',
|
|
1051
|
+
region: context.region
|
|
1052
|
+
});
|
|
1053
|
+
|
|
1054
|
+
// Note: these are integers for container definitions, and strings for task definitions (on Fargate)
|
|
1055
|
+
// Defaults have to be Fargate-compatible
|
|
1056
|
+
// https://docs.aws.amazon.com/AmazonECS/latest/developerguide/task_definition_parameters.html#task_size
|
|
1057
|
+
let cpu = 4096;
|
|
1058
|
+
let memory = 8192;
|
|
1059
|
+
|
|
1060
|
+
const defaultUlimits = {
|
|
1061
|
+
nofile: {
|
|
1062
|
+
softLimit: 8192,
|
|
1063
|
+
hardLimit: 8192
|
|
1064
|
+
}
|
|
1065
|
+
};
|
|
1066
|
+
let ulimits = [];
|
|
1067
|
+
|
|
1068
|
+
if (context.cliOptions.launchConfig) {
|
|
1069
|
+
const lc = context.cliOptions.launchConfig;
|
|
1070
|
+
if (lc.cpu) {
|
|
1071
|
+
cpu = parseInt(lc.cpu, 10);
|
|
1072
|
+
}
|
|
1073
|
+
if (lc.memory) {
|
|
1074
|
+
memory = parseInt(lc.memory, 10);
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
if (lc.ulimits) {
|
|
1078
|
+
lc.ulimits.forEach((u) => {
|
|
1079
|
+
if (!defaultUlimits[u.name]) {
|
|
1080
|
+
defaultUlimits[u.name] = {};
|
|
1081
|
+
}
|
|
1082
|
+
defaultUlimits[u.name] = {
|
|
1083
|
+
softLimit: u.softLimit,
|
|
1084
|
+
hardLimit: typeof u.hardLimit === 'number' ? u.hardLimit : u.softLimit
|
|
1085
|
+
};
|
|
1086
|
+
});
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
// TODO: Check this earlier to return an error faster.
|
|
1090
|
+
if (context.isFargate) {
|
|
1091
|
+
const configErr = checkFargateResourceConfig(cpu, memory);
|
|
1092
|
+
if (configErr) {
|
|
1093
|
+
throw configErr;
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
ulimits = Object.keys(defaultUlimits).map((name) => {
|
|
1099
|
+
return {
|
|
1100
|
+
name: name,
|
|
1101
|
+
softLimit: defaultUlimits[name].softLimit,
|
|
1102
|
+
hardLimit: defaultUlimits[name].hardLimit
|
|
1103
|
+
};
|
|
1104
|
+
});
|
|
1105
|
+
|
|
1106
|
+
const defaultArchitecture = 'x86_64';
|
|
1107
|
+
const imageUrl =
|
|
1108
|
+
process.env.WORKER_IMAGE_URL ||
|
|
1109
|
+
`public.ecr.aws/d8a4z9o5/artillery-worker:${IMAGE_VERSION}-${defaultArchitecture}`;
|
|
1110
|
+
|
|
1111
|
+
const secrets = [
|
|
1112
|
+
'NPM_TOKEN',
|
|
1113
|
+
'NPM_REGISTRY',
|
|
1114
|
+
'NPM_SCOPE',
|
|
1115
|
+
'NPM_SCOPE_REGISTRY',
|
|
1116
|
+
'NPMRC',
|
|
1117
|
+
'ARTIFACTORY_AUTH',
|
|
1118
|
+
'ARTIFACTORY_EMAIL'
|
|
1119
|
+
]
|
|
1120
|
+
.concat(context.extraSecrets)
|
|
1121
|
+
.map((secretName) => {
|
|
1122
|
+
return {
|
|
1123
|
+
name: secretName,
|
|
1124
|
+
valueFrom: `${context.arnPrefix}:ssm:${context.region}:${context.accountId}:parameter/artilleryio/${secretName}`
|
|
1125
|
+
};
|
|
1126
|
+
});
|
|
1127
|
+
|
|
1128
|
+
const artilleryContainerDefinition = {
|
|
1129
|
+
name: 'artillery',
|
|
1130
|
+
image: imageUrl,
|
|
1131
|
+
cpu: cpu,
|
|
1132
|
+
command: [],
|
|
1133
|
+
entryPoint: ['/artillery/loadgen-worker'],
|
|
1134
|
+
memory: memory,
|
|
1135
|
+
secrets: secrets,
|
|
1136
|
+
ulimits: ulimits,
|
|
1137
|
+
essential: true,
|
|
1138
|
+
logConfiguration: {
|
|
1139
|
+
logDriver: 'awslogs',
|
|
1140
|
+
options: {
|
|
1141
|
+
'awslogs-group': `${context.logGroupName}/${context.clusterName}`,
|
|
1142
|
+
'awslogs-region': context.region,
|
|
1143
|
+
'awslogs-stream-prefix': `artilleryio/${context.testId}`,
|
|
1144
|
+
'awslogs-create-group': 'true',
|
|
1145
|
+
mode: 'non-blocking'
|
|
1146
|
+
}
|
|
1147
|
+
}
|
|
1148
|
+
};
|
|
1149
|
+
|
|
1150
|
+
if (context.cliOptions.containerDnsServers) {
|
|
1151
|
+
artilleryContainerDefinition.dnsServers =
|
|
1152
|
+
context.cliOptions.containerDnsServers.split(',');
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
const taskDefinition = {
|
|
1156
|
+
family: context.taskName,
|
|
1157
|
+
containerDefinitions: [artilleryContainerDefinition],
|
|
1158
|
+
executionRoleArn: context.taskRoleArn
|
|
1159
|
+
};
|
|
1160
|
+
|
|
1161
|
+
if (typeof context.adot !== 'undefined') {
|
|
1162
|
+
taskDefinition.containerDefinitions.push(context.adot.taskDefinition);
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
context.taskDefinition = taskDefinition;
|
|
1166
|
+
|
|
1167
|
+
if (!context.isFargate && taskDefinition.containerDefinitions.length > 1) {
|
|
1168
|
+
// Limits for sidecar have to be set explicitly on ECS EC2
|
|
1169
|
+
taskDefinition.containerDefinitions[1].memory = 1024;
|
|
1170
|
+
taskDefinition.containerDefinitions[1].cpu = 1024;
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1173
|
+
if (context.isFargate) {
|
|
1174
|
+
taskDefinition.networkMode = 'awsvpc';
|
|
1175
|
+
taskDefinition.requiresCompatibilities = ['FARGATE'];
|
|
1176
|
+
taskDefinition.cpu = String(cpu);
|
|
1177
|
+
taskDefinition.memory = String(memory);
|
|
1178
|
+
// NOTE: This role must exist.
|
|
1179
|
+
// This value cannot be an override, meaning it's hardcoded into the task definition.
|
|
1180
|
+
// That in turn means that if the role is updated then the task definition needs to be
|
|
1181
|
+
// recreated too
|
|
1182
|
+
taskDefinition.executionRoleArn = context.taskRoleArn; // TODO: A separate role for Fargate
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1185
|
+
const params = {
|
|
1186
|
+
taskDefinition: context.taskName
|
|
1187
|
+
};
|
|
1188
|
+
|
|
1189
|
+
debug('Task definition\n', JSON.stringify(taskDefinition, null, 4));
|
|
1190
|
+
|
|
1191
|
+
try {
|
|
1192
|
+
await ecs.send(new DescribeTaskDefinitionCommand(params));
|
|
1193
|
+
debug('OK: ECS task exists');
|
|
1194
|
+
if (process.env.ECR_IMAGE_VERSION) {
|
|
1195
|
+
debug(
|
|
1196
|
+
'ECR_IMAGE_VERSION is set, but the task definition was already in place.'
|
|
1197
|
+
);
|
|
1198
|
+
}
|
|
1199
|
+
return context;
|
|
1200
|
+
} catch (_err) {
|
|
1201
|
+
try {
|
|
1202
|
+
const response = await ecs.send(
|
|
1203
|
+
new RegisterTaskDefinitionCommand(taskDefinition)
|
|
1204
|
+
);
|
|
1205
|
+
debug('OK: ECS task registered');
|
|
1206
|
+
debugVerbose(JSON.stringify(response, null, 4));
|
|
1207
|
+
context.taskDefinitionArn = response.taskDefinition.taskDefinitionArn;
|
|
1208
|
+
debug(`Task definition ARN: ${context.taskDefinitionArn}`);
|
|
1209
|
+
return context;
|
|
1210
|
+
} catch (registerErr) {
|
|
1211
|
+
artillery.log(registerErr);
|
|
1212
|
+
artillery.log('Could not create ECS task, please try again');
|
|
1213
|
+
throw registerErr;
|
|
1214
|
+
}
|
|
1215
|
+
}
|
|
1216
|
+
}
|
|
1217
|
+
|
|
1218
|
+
async function checkCustomTaskRole(context) {
|
|
1219
|
+
if (!context.customTaskRoleName) {
|
|
1220
|
+
return;
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1223
|
+
const iam = new IAMClient({ region: global.artillery.awsRegion });
|
|
1224
|
+
const roleData = await iam.send(
|
|
1225
|
+
new GetRoleCommand({ RoleName: context.customTaskRoleName })
|
|
1226
|
+
);
|
|
1227
|
+
context.customRoleArn = roleData.Role.Arn;
|
|
1228
|
+
context.taskRoleArn = roleData.Role.Arn;
|
|
1229
|
+
debug(roleData);
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1232
|
+
async function gcQueues(context) {
|
|
1233
|
+
const sqs = new SQSClient({
|
|
1234
|
+
region: context.region
|
|
1235
|
+
});
|
|
1236
|
+
|
|
1237
|
+
let data;
|
|
1238
|
+
try {
|
|
1239
|
+
data = await sqs.send(
|
|
1240
|
+
new ListQueuesCommand({
|
|
1241
|
+
QueueNamePrefix: SQS_QUEUES_NAME_PREFIX,
|
|
1242
|
+
MaxResults: 1000
|
|
1243
|
+
})
|
|
1244
|
+
);
|
|
1245
|
+
} catch (err) {
|
|
1246
|
+
debug(err);
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1249
|
+
if (data?.QueueUrls && data.QueueUrls.length > 0) {
|
|
1250
|
+
for (const qu of data.QueueUrls) {
|
|
1251
|
+
try {
|
|
1252
|
+
const data = await sqs.send(
|
|
1253
|
+
new GetQueueAttributesCommand({
|
|
1254
|
+
QueueUrl: qu,
|
|
1255
|
+
AttributeNames: ['CreatedTimestamp']
|
|
1256
|
+
})
|
|
1257
|
+
);
|
|
1258
|
+
const ts = Number(data.Attributes.CreatedTimestamp) * 1000;
|
|
1259
|
+
// Delete after 96 hours
|
|
1260
|
+
if (Date.now() - ts > 96 * 60 * 60 * 1000) {
|
|
1261
|
+
await sqs.send(new DeleteQueueCommand({ QueueUrl: qu }));
|
|
1262
|
+
}
|
|
1263
|
+
} catch (err) {
|
|
1264
|
+
// TODO: Filter on errors which may be ignored, e.g.:
|
|
1265
|
+
// AWS.SimpleQueueService.NonExistentQueue: The specified queue does not exist
|
|
1266
|
+
// which can happen if another test ends between calls to listQueues and getQueueAttributes.
|
|
1267
|
+
// Sometimes SQS returns recently deleted queues to ListQueues too.
|
|
1268
|
+
debug(err);
|
|
1269
|
+
}
|
|
1270
|
+
}
|
|
1271
|
+
}
|
|
1272
|
+
}
|
|
1273
|
+
|
|
1274
|
+
async function deleteQueue(context) {
|
|
1275
|
+
if (!context.sqsQueueUrl) {
|
|
1276
|
+
return;
|
|
1277
|
+
}
|
|
1278
|
+
|
|
1279
|
+
const sqs = new SQSClient({
|
|
1280
|
+
region: context.region
|
|
1281
|
+
});
|
|
1282
|
+
|
|
1283
|
+
try {
|
|
1284
|
+
await sqs.send(new DeleteQueueCommand({ QueueUrl: context.sqsQueueUrl }));
|
|
1285
|
+
} catch (err) {
|
|
1286
|
+
console.error(`Unable to clean up SQS queue. URL: ${context.sqsQueueUrl}`);
|
|
1287
|
+
debug(err);
|
|
1288
|
+
}
|
|
1289
|
+
}
|
|
1290
|
+
|
|
1291
|
+
async function createQueue(context) {
|
|
1292
|
+
const sqs = new SQSClient({
|
|
1293
|
+
region: context.region
|
|
1294
|
+
});
|
|
1295
|
+
|
|
1296
|
+
const queueName = `${SQS_QUEUES_NAME_PREFIX}_${context.testId.slice(
|
|
1297
|
+
0,
|
|
1298
|
+
30
|
|
1299
|
+
)}.fifo`;
|
|
1300
|
+
const params = {
|
|
1301
|
+
QueueName: queueName,
|
|
1302
|
+
Attributes: {
|
|
1303
|
+
FifoQueue: 'true',
|
|
1304
|
+
ContentBasedDeduplication: 'false',
|
|
1305
|
+
MessageRetentionPeriod: '1800',
|
|
1306
|
+
VisibilityTimeout: '600' // 10 minutes
|
|
1307
|
+
}
|
|
1308
|
+
};
|
|
1309
|
+
const result = await sqs.send(new CreateQueueCommand(params));
|
|
1310
|
+
context.sqsQueueUrl = result.QueueUrl;
|
|
1311
|
+
|
|
1312
|
+
// Wait for the queue to be available:
|
|
1313
|
+
let waited = 0;
|
|
1314
|
+
let ok = false;
|
|
1315
|
+
while (waited < 120 * 1000) {
|
|
1316
|
+
try {
|
|
1317
|
+
const results = await sqs.send(
|
|
1318
|
+
new ListQueuesCommand({ QueueNamePrefix: queueName })
|
|
1319
|
+
);
|
|
1320
|
+
if (results.QueueUrls && results.QueueUrls.length === 1) {
|
|
1321
|
+
debug('SQS queue created:', queueName);
|
|
1322
|
+
ok = true;
|
|
1323
|
+
break;
|
|
1324
|
+
} else {
|
|
1325
|
+
await sleep(10 * 1000);
|
|
1326
|
+
waited += 10 * 1000;
|
|
1327
|
+
}
|
|
1328
|
+
} catch (_err) {
|
|
1329
|
+
await sleep(10 * 1000);
|
|
1330
|
+
waited += 10 * 1000;
|
|
1331
|
+
}
|
|
1332
|
+
}
|
|
1333
|
+
|
|
1334
|
+
if (!ok) {
|
|
1335
|
+
debug('Time out waiting for SQS queue:', queueName);
|
|
1336
|
+
throw new Error('SQS queue could not be created');
|
|
1337
|
+
}
|
|
1338
|
+
}
|
|
1339
|
+
|
|
1340
|
+
async function getManifest(context) {
|
|
1341
|
+
try {
|
|
1342
|
+
const s3 = createS3Client({ region: global.artillery.s3BucketRegion });
|
|
1343
|
+
const params = {
|
|
1344
|
+
Bucket: context.s3Bucket,
|
|
1345
|
+
Key: `tests/${context.testId}/metadata.json`
|
|
1346
|
+
};
|
|
1347
|
+
|
|
1348
|
+
const data = await s3.send(new GetObjectCommand(params));
|
|
1349
|
+
const metadata = JSON.parse(await data.Body.transformToString());
|
|
1350
|
+
context.newScriptPath = metadata.scriptPath;
|
|
1351
|
+
|
|
1352
|
+
if (metadata.configPath) {
|
|
1353
|
+
context.configPath = metadata.configPath;
|
|
1354
|
+
}
|
|
1355
|
+
|
|
1356
|
+
return context;
|
|
1357
|
+
} catch (err) {
|
|
1358
|
+
if (err instanceof NoSuchKey) {
|
|
1359
|
+
throw new TestNotFoundError();
|
|
1360
|
+
} else {
|
|
1361
|
+
throw err;
|
|
1362
|
+
}
|
|
1363
|
+
}
|
|
1364
|
+
}
|
|
1365
|
+
|
|
1366
|
+
async function generateTaskOverrides(context) {
|
|
1367
|
+
const cliArgs = ['run'].concat(
|
|
1368
|
+
context.cliOptions.environment
|
|
1369
|
+
? ['--environment', context.cliOptions.environment]
|
|
1370
|
+
: [],
|
|
1371
|
+
context.cliOptions['scenarioName']
|
|
1372
|
+
? ['--scenario-name', context.cliOptions['scenarioName']]
|
|
1373
|
+
: [],
|
|
1374
|
+
context.cliOptions.insecure ? ['-k'] : [],
|
|
1375
|
+
context.cliOptions.target ? ['-t', context.cliOptions.target] : [],
|
|
1376
|
+
context.cliOptions.overrides
|
|
1377
|
+
? ['--overrides', context.cliOptions.overrides]
|
|
1378
|
+
: [],
|
|
1379
|
+
context.cliOptions.variables
|
|
1380
|
+
? ['--variables', context.cliOptions.variables]
|
|
1381
|
+
: [],
|
|
1382
|
+
context.configPath ? ['--config', context.configPath] : []
|
|
1383
|
+
);
|
|
1384
|
+
// NOTE: This MUST come last:
|
|
1385
|
+
cliArgs.push(context.newScriptPath);
|
|
1386
|
+
|
|
1387
|
+
debug('cliArgs', cliArgs, cliArgs.join(' '));
|
|
1388
|
+
|
|
1389
|
+
const s3path = `s3://${context.s3Bucket}/tests/${
|
|
1390
|
+
context.namedTest ? context.s3Prefix : context.testId
|
|
1391
|
+
}`;
|
|
1392
|
+
const adotOverride = [
|
|
1393
|
+
{
|
|
1394
|
+
name: 'adot-collector',
|
|
1395
|
+
environment: []
|
|
1396
|
+
}
|
|
1397
|
+
];
|
|
1398
|
+
|
|
1399
|
+
const overrides = {
|
|
1400
|
+
containerOverrides: [
|
|
1401
|
+
{
|
|
1402
|
+
name: 'artillery',
|
|
1403
|
+
command: [
|
|
1404
|
+
'-p',
|
|
1405
|
+
s3path,
|
|
1406
|
+
'-a',
|
|
1407
|
+
util.btoa(JSON.stringify(cliArgs)),
|
|
1408
|
+
'-r',
|
|
1409
|
+
context.region,
|
|
1410
|
+
'-q',
|
|
1411
|
+
process.env.SQS_QUEUE_URL || context.sqsQueueUrl,
|
|
1412
|
+
'-i',
|
|
1413
|
+
context.testId,
|
|
1414
|
+
'-d',
|
|
1415
|
+
`s3://${context.s3Bucket}/test-runs`,
|
|
1416
|
+
'-t',
|
|
1417
|
+
String(WAIT_TIMEOUT)
|
|
1418
|
+
],
|
|
1419
|
+
environment: [
|
|
1420
|
+
{
|
|
1421
|
+
name: 'AWS_SDK_JS_SUPPRESS_MAINTENANCE_MODE_MESSAGE',
|
|
1422
|
+
value: '1'
|
|
1423
|
+
},
|
|
1424
|
+
{
|
|
1425
|
+
name: 'ARTILLERY_TEST_RUN_ID',
|
|
1426
|
+
value: global.artillery.testRunId
|
|
1427
|
+
},
|
|
1428
|
+
{
|
|
1429
|
+
name: 'ARTILLERY_S3_BUCKET',
|
|
1430
|
+
value: context.s3Bucket
|
|
1431
|
+
}
|
|
1432
|
+
]
|
|
1433
|
+
},
|
|
1434
|
+
...(context.adot ? adotOverride : [])
|
|
1435
|
+
],
|
|
1436
|
+
taskRoleArn: context.taskRoleArn
|
|
1437
|
+
};
|
|
1438
|
+
|
|
1439
|
+
if (context.customRoleArn) {
|
|
1440
|
+
overrides.taskRoleArn = context.customRoleArn;
|
|
1441
|
+
}
|
|
1442
|
+
|
|
1443
|
+
if (context.cliOptions.taskEphemeralStorage) {
|
|
1444
|
+
overrides.ephemeralStorage = {
|
|
1445
|
+
sizeInGiB: context.cliOptions.taskEphemeralStorage
|
|
1446
|
+
};
|
|
1447
|
+
}
|
|
1448
|
+
|
|
1449
|
+
overrides.containerOverrides[0].environment.push({
|
|
1450
|
+
name: 'USE_V2',
|
|
1451
|
+
value: 'true'
|
|
1452
|
+
});
|
|
1453
|
+
|
|
1454
|
+
if (context.dotenv) {
|
|
1455
|
+
const extraEnv = [];
|
|
1456
|
+
for (const [name, value] of Object.entries(context.dotenv)) {
|
|
1457
|
+
extraEnv.push({ name, value });
|
|
1458
|
+
}
|
|
1459
|
+
overrides.containerOverrides[0].environment =
|
|
1460
|
+
overrides.containerOverrides[0].environment.concat(extraEnv);
|
|
1461
|
+
if (overrides.containerOverrides[1]) {
|
|
1462
|
+
overrides.containerOverrides[1].environment =
|
|
1463
|
+
overrides.containerOverrides[1].environment.concat(extraEnv);
|
|
1464
|
+
}
|
|
1465
|
+
}
|
|
1466
|
+
|
|
1467
|
+
if (context.cliOptions.launchConfig) {
|
|
1468
|
+
const lc = context.cliOptions.launchConfig;
|
|
1469
|
+
if (lc.environment) {
|
|
1470
|
+
overrides.containerOverrides[0].environment =
|
|
1471
|
+
overrides.containerOverrides[0].environment.concat(lc.environment);
|
|
1472
|
+
if (overrides.containerOverrides[1]) {
|
|
1473
|
+
overrides.containerOverrides[1].environment =
|
|
1474
|
+
overrides.containerOverrides[1].environment.concat(lc.environment);
|
|
1475
|
+
}
|
|
1476
|
+
}
|
|
1477
|
+
|
|
1478
|
+
//
|
|
1479
|
+
// Not officially supported:
|
|
1480
|
+
//
|
|
1481
|
+
if (lc.taskRoleArn) {
|
|
1482
|
+
overrides.taskRoleArn = lc.taskRoleArn;
|
|
1483
|
+
}
|
|
1484
|
+
if (lc.command) {
|
|
1485
|
+
overrides.containerOverrides[0].command = lc.command;
|
|
1486
|
+
}
|
|
1487
|
+
}
|
|
1488
|
+
|
|
1489
|
+
debug('OK: Overrides generated');
|
|
1490
|
+
debugVerbose(JSON.stringify(overrides, null, 4));
|
|
1491
|
+
|
|
1492
|
+
context.taskOverrides = overrides;
|
|
1493
|
+
|
|
1494
|
+
return context;
|
|
1495
|
+
}
|
|
1496
|
+
|
|
1497
|
+
async function setupDefaultECSParams(context) {
|
|
1498
|
+
const defaultParams = {
|
|
1499
|
+
taskDefinition: context.taskName,
|
|
1500
|
+
cluster: context.clusterName,
|
|
1501
|
+
overrides: context.taskOverrides
|
|
1502
|
+
};
|
|
1503
|
+
|
|
1504
|
+
if (context.isFargate) {
|
|
1505
|
+
if (context.isCapacitySpot) {
|
|
1506
|
+
defaultParams.capacityProviderStrategy = [
|
|
1507
|
+
{
|
|
1508
|
+
capacityProvider: 'FARGATE_SPOT',
|
|
1509
|
+
weight: 1,
|
|
1510
|
+
base: 0
|
|
1511
|
+
}
|
|
1512
|
+
];
|
|
1513
|
+
} else {
|
|
1514
|
+
// On-demand capacity
|
|
1515
|
+
defaultParams.launchType = 'FARGATE';
|
|
1516
|
+
}
|
|
1517
|
+
// Networking config: private subnets of the VPC that the ECS cluster
|
|
1518
|
+
// is in. Don't need public subnets.
|
|
1519
|
+
defaultParams.networkConfiguration = {
|
|
1520
|
+
awsvpcConfiguration: {
|
|
1521
|
+
// https://github.com/aws/amazon-ecs-agent/issues/1128
|
|
1522
|
+
assignPublicIp: context.cliOptions.noAssignPublicIp
|
|
1523
|
+
? 'DISABLED'
|
|
1524
|
+
: 'ENABLED',
|
|
1525
|
+
securityGroups: context.fargateSecurityGroupIds,
|
|
1526
|
+
subnets: context.fargatePublicSubnetIds
|
|
1527
|
+
}
|
|
1528
|
+
};
|
|
1529
|
+
} else {
|
|
1530
|
+
defaultParams.launchType = 'EC2';
|
|
1531
|
+
}
|
|
1532
|
+
|
|
1533
|
+
context.defaultECSParams = defaultParams;
|
|
1534
|
+
return context;
|
|
1535
|
+
}
|
|
1536
|
+
|
|
1537
|
+
async function launchLeadTask(context) {
|
|
1538
|
+
const metadata = {
|
|
1539
|
+
testId: context.testId,
|
|
1540
|
+
startedAt: Date.now(),
|
|
1541
|
+
cluster: context.clusterName,
|
|
1542
|
+
region: context.region,
|
|
1543
|
+
launchType: context.cliOptions.launchType,
|
|
1544
|
+
isFargateSpot: context.isCapacitySpot,
|
|
1545
|
+
count: context.count,
|
|
1546
|
+
sqsQueueUrl: context.sqsQueueUrl,
|
|
1547
|
+
tags: context.tags,
|
|
1548
|
+
secrets: JSON.stringify(
|
|
1549
|
+
Array.isArray(context.extraSecrets)
|
|
1550
|
+
? context.extraSecrets
|
|
1551
|
+
: [context.extraSecrets]
|
|
1552
|
+
),
|
|
1553
|
+
platformConfig: JSON.stringify({
|
|
1554
|
+
memory: context.taskDefinition.containerDefinitions[0].memory,
|
|
1555
|
+
cpu: context.taskDefinition.containerDefinitions[0].cpu
|
|
1556
|
+
}),
|
|
1557
|
+
artilleryVersion: JSON.stringify({
|
|
1558
|
+
core: global.artillery.version
|
|
1559
|
+
}),
|
|
1560
|
+
// Properties from the runnable script object:
|
|
1561
|
+
testConfig: {
|
|
1562
|
+
target: context.runnableScript.config.target,
|
|
1563
|
+
phases: context.runnableScript.config.phases,
|
|
1564
|
+
plugins: context.runnableScript.config.plugins,
|
|
1565
|
+
environment: context.runnableScript._environment,
|
|
1566
|
+
scriptPath: context.runnableScript._scriptPath,
|
|
1567
|
+
configPath: context.runnableScript._configPath
|
|
1568
|
+
}
|
|
1569
|
+
};
|
|
1570
|
+
|
|
1571
|
+
artillery.globalEvents.emit('metadata', metadata);
|
|
1572
|
+
|
|
1573
|
+
context.status = TEST_RUN_STATUS.LAUNCHING_WORKERS;
|
|
1574
|
+
|
|
1575
|
+
const ecs = new ECSClient({
|
|
1576
|
+
apiVersion: '2014-11-13',
|
|
1577
|
+
region: context.region
|
|
1578
|
+
});
|
|
1579
|
+
|
|
1580
|
+
const leaderParams = Object.assign(
|
|
1581
|
+
{ count: 1 },
|
|
1582
|
+
JSON.parse(JSON.stringify(context.defaultECSParams))
|
|
1583
|
+
);
|
|
1584
|
+
leaderParams.overrides.containerOverrides[0].environment.push({
|
|
1585
|
+
name: 'IS_LEADER',
|
|
1586
|
+
value: 'true'
|
|
1587
|
+
});
|
|
1588
|
+
const runData = await ecs.send(new RunTaskCommand(leaderParams));
|
|
1589
|
+
if (runData.failures.length > 0) {
|
|
1590
|
+
if (runData.failures.length === context.count) {
|
|
1591
|
+
artillery.log('ERROR: Worker start failure');
|
|
1592
|
+
const uniqueReasons = [
|
|
1593
|
+
...new Set(runData.failures.map((f) => f.reason))
|
|
1594
|
+
];
|
|
1595
|
+
artillery.log('Reason:', uniqueReasons);
|
|
1596
|
+
throw new Error('Could not start workers');
|
|
1597
|
+
} else {
|
|
1598
|
+
artillery.log('WARNING: Some workers failed to start');
|
|
1599
|
+
artillery.log(chalk.red(JSON.stringify(runData.failures, null, 4)));
|
|
1600
|
+
throw new Error('Not enough capacity - terminating');
|
|
1601
|
+
}
|
|
1602
|
+
}
|
|
1603
|
+
|
|
1604
|
+
context.taskArns = context.taskArns.concat(
|
|
1605
|
+
runData.tasks.map((task) => task.taskArn)
|
|
1606
|
+
);
|
|
1607
|
+
artillery.globalEvents.emit('metadata', {
|
|
1608
|
+
platformMetadata: { taskArns: context.taskArns }
|
|
1609
|
+
});
|
|
1610
|
+
|
|
1611
|
+
return context;
|
|
1612
|
+
}
|
|
1613
|
+
|
|
1614
|
+
// TODO: When launching >20 containers on Fargate, adjust WAIT_TIMEOUT dynamically to
|
|
1615
|
+
// add extra time spent in waiting between runTask calls: WAIT_TIMEOUT + worker_count.
|
|
1616
|
+
async function ecsRunTask(context) {
|
|
1617
|
+
const ecs = new ECSClient({
|
|
1618
|
+
apiVersion: '2014-11-13',
|
|
1619
|
+
region: context.region
|
|
1620
|
+
});
|
|
1621
|
+
let tasksRemaining = context.count - 1;
|
|
1622
|
+
let retries = 0;
|
|
1623
|
+
|
|
1624
|
+
while (
|
|
1625
|
+
tasksRemaining > 0 &&
|
|
1626
|
+
context.status !== TEST_RUN_STATUS.TERMINATING &&
|
|
1627
|
+
context.status !== TEST_RUN_STATUS.EARLY_STOP
|
|
1628
|
+
) {
|
|
1629
|
+
if (retries >= 10) {
|
|
1630
|
+
artillery.log('Max retries for ECS (10) exceeded');
|
|
1631
|
+
throw new Error('Max retries exceeded');
|
|
1632
|
+
}
|
|
1633
|
+
|
|
1634
|
+
const launchCount = tasksRemaining <= 10 ? tasksRemaining : 10;
|
|
1635
|
+
const params = Object.assign(
|
|
1636
|
+
{ count: launchCount },
|
|
1637
|
+
JSON.parse(JSON.stringify(context.defaultECSParams))
|
|
1638
|
+
);
|
|
1639
|
+
|
|
1640
|
+
params.overrides.containerOverrides[0].environment.push({
|
|
1641
|
+
name: 'IS_LEADER',
|
|
1642
|
+
value: 'false'
|
|
1643
|
+
});
|
|
1644
|
+
|
|
1645
|
+
try {
|
|
1646
|
+
const runData = await ecs.send(new RunTaskCommand(params));
|
|
1647
|
+
|
|
1648
|
+
const launchedTasksCount = runData.tasks?.length || 0;
|
|
1649
|
+
tasksRemaining -= launchedTasksCount;
|
|
1650
|
+
|
|
1651
|
+
if (launchedTasksCount > 0) {
|
|
1652
|
+
const newTaskArns = runData.tasks.map((task) => task.taskArn);
|
|
1653
|
+
context.taskArns = context.taskArns.concat(newTaskArns);
|
|
1654
|
+
artillery.globalEvents.emit('metadata', {
|
|
1655
|
+
platformMetadata: { taskArns: newTaskArns }
|
|
1656
|
+
});
|
|
1657
|
+
debug(`Launched ${launchedTasksCount} tasks`);
|
|
1658
|
+
}
|
|
1659
|
+
|
|
1660
|
+
if (runData.failures.length > 0) {
|
|
1661
|
+
artillery.log('Some workers failed to start');
|
|
1662
|
+
const uniqueReasons = [
|
|
1663
|
+
...new Set(runData.failures.map((f) => f.reason))
|
|
1664
|
+
];
|
|
1665
|
+
artillery.log(chalk.red(uniqueReasons));
|
|
1666
|
+
artillery.log('Retrying...');
|
|
1667
|
+
await sleep(10 * 1000);
|
|
1668
|
+
throw new Error('Not enough ECS capacity');
|
|
1669
|
+
}
|
|
1670
|
+
} catch (runErr) {
|
|
1671
|
+
if (runErr instanceof ThrottlingException) {
|
|
1672
|
+
artillery.log('ThrottlingException returned from ECS, retrying');
|
|
1673
|
+
await sleep(2000 * retries);
|
|
1674
|
+
debug('runTask throttled, retrying');
|
|
1675
|
+
debug(runErr);
|
|
1676
|
+
} else if (runErr.message.match(/Not enough ECS capacity/gi)) {
|
|
1677
|
+
// Do nothing
|
|
1678
|
+
} else {
|
|
1679
|
+
artillery.log(runErr);
|
|
1680
|
+
}
|
|
1681
|
+
|
|
1682
|
+
retries++;
|
|
1683
|
+
if (retries >= 10) {
|
|
1684
|
+
artillery.log('Max retries for ECS (10) exceeded');
|
|
1685
|
+
throw runErr;
|
|
1686
|
+
}
|
|
1687
|
+
}
|
|
1688
|
+
}
|
|
1689
|
+
return context;
|
|
1690
|
+
}
|
|
1691
|
+
|
|
1692
|
+
async function waitForTasks2(context) {
|
|
1693
|
+
const params = {
|
|
1694
|
+
tasks: context.taskArns,
|
|
1695
|
+
cluster: context.clusterName
|
|
1696
|
+
};
|
|
1697
|
+
|
|
1698
|
+
let failedTasks = [];
|
|
1699
|
+
let stoppedTasks = [];
|
|
1700
|
+
let maybeErr = null;
|
|
1701
|
+
|
|
1702
|
+
const silentWaitTimeout = new Timeout(30 * 1000).start(); // wait this long before updating the user
|
|
1703
|
+
const waitTimeout = new Timeout(60 * 1000).start(); // wait for up to 1 minute
|
|
1704
|
+
while (context.status !== TEST_RUN_STATUS.TERMINATING) {
|
|
1705
|
+
let ecsData;
|
|
1706
|
+
try {
|
|
1707
|
+
ecsData = await awsUtil.ecsDescribeTasks(params, context.region);
|
|
1708
|
+
} catch (err) {
|
|
1709
|
+
// TODO: Inspect err for any conditions in which we may want to abort immediately.
|
|
1710
|
+
// Otherwise, let the timeout run to completion.
|
|
1711
|
+
debug(err);
|
|
1712
|
+
await sleep(5000);
|
|
1713
|
+
continue;
|
|
1714
|
+
}
|
|
1715
|
+
|
|
1716
|
+
// All tasks are RUNNING, proceed:
|
|
1717
|
+
if (_.every(ecsData.tasks, (s) => s.lastStatus === 'RUNNING')) {
|
|
1718
|
+
logProgress('All workers started...');
|
|
1719
|
+
debug('All tasks in RUNNING state');
|
|
1720
|
+
break;
|
|
1721
|
+
}
|
|
1722
|
+
|
|
1723
|
+
// If there are STOPPED tasks, we need to stop:
|
|
1724
|
+
stoppedTasks = ecsData.tasks.filter((t) => t.lastStatus === 'STOPPED');
|
|
1725
|
+
if (stoppedTasks.length > 0) {
|
|
1726
|
+
debug('Some tasks in STOPPED state');
|
|
1727
|
+
debugErr(stoppedTasks);
|
|
1728
|
+
// TODO: Stop RUNNING tasks and clean up (release queue lock, deregister task definition)
|
|
1729
|
+
// TODO: Provide more information here, e.g. task ARNs, or CloudWatch log group ID
|
|
1730
|
+
maybeErr = new Error('Worker init failure, aborting test');
|
|
1731
|
+
break;
|
|
1732
|
+
}
|
|
1733
|
+
|
|
1734
|
+
// If some tasks failed to start altogether, abort:
|
|
1735
|
+
if (ecsData.failures.length > 0) {
|
|
1736
|
+
failedTasks = ecsData.failures;
|
|
1737
|
+
debug('Some tasks failed to start');
|
|
1738
|
+
debugErr(ecsData.failures);
|
|
1739
|
+
maybeErr = new Error('Worker start up failure, aborting test');
|
|
1740
|
+
break;
|
|
1741
|
+
}
|
|
1742
|
+
|
|
1743
|
+
// If there are PENDING, update progress bar
|
|
1744
|
+
debug('Waiting on pending tasks');
|
|
1745
|
+
if (silentWaitTimeout.timedout()) {
|
|
1746
|
+
const statusCounts = _.countBy(ecsData.tasks, 'lastStatus');
|
|
1747
|
+
const statusSummary = _.map(statusCounts, (count, status) => {
|
|
1748
|
+
const displayStatus =
|
|
1749
|
+
status === 'RUNNING' ? 'ready' : status.toLowerCase();
|
|
1750
|
+
let displayStatusChalked = displayStatus;
|
|
1751
|
+
if (displayStatus === 'ready') {
|
|
1752
|
+
displayStatusChalked = chalk.green(displayStatus);
|
|
1753
|
+
} else if (displayStatus === 'pending') {
|
|
1754
|
+
displayStatusChalked = chalk.yellow(displayStatus);
|
|
1755
|
+
}
|
|
1756
|
+
|
|
1757
|
+
return `${displayStatusChalked}: ${count}`;
|
|
1758
|
+
}).join(' / ');
|
|
1759
|
+
|
|
1760
|
+
logProgress(`Waiting for workers to start: ${statusSummary}`);
|
|
1761
|
+
}
|
|
1762
|
+
|
|
1763
|
+
if (waitTimeout.timedout()) {
|
|
1764
|
+
// TODO: Clean up RUNNING tasks etc
|
|
1765
|
+
break;
|
|
1766
|
+
}
|
|
1767
|
+
await sleep(10 * 1000);
|
|
1768
|
+
} // while
|
|
1769
|
+
waitTimeout.stop();
|
|
1770
|
+
|
|
1771
|
+
if (maybeErr) {
|
|
1772
|
+
if (stoppedTasks.length > 0) {
|
|
1773
|
+
artillery.log(stoppedTasks);
|
|
1774
|
+
}
|
|
1775
|
+
if (failedTasks.length > 0) {
|
|
1776
|
+
artillery.log(failedTasks);
|
|
1777
|
+
}
|
|
1778
|
+
throw maybeErr;
|
|
1779
|
+
}
|
|
1780
|
+
|
|
1781
|
+
return context;
|
|
1782
|
+
}
|
|
1783
|
+
|
|
1784
|
+
async function waitForWorkerSync(context) {
|
|
1785
|
+
const MAGIC_PREFIX = 'synced_';
|
|
1786
|
+
const prefix = `test-runs/${context.testId}/${MAGIC_PREFIX}`;
|
|
1787
|
+
|
|
1788
|
+
const intervalSec = 10;
|
|
1789
|
+
const times = WAIT_TIMEOUT / intervalSec;
|
|
1790
|
+
let attempts = 0;
|
|
1791
|
+
let synced = false;
|
|
1792
|
+
|
|
1793
|
+
while (attempts < times) {
|
|
1794
|
+
try {
|
|
1795
|
+
const objects = await util.listAllObjectsWithPrefix(
|
|
1796
|
+
context.s3Bucket,
|
|
1797
|
+
prefix
|
|
1798
|
+
);
|
|
1799
|
+
if (objects.length !== context.count) {
|
|
1800
|
+
attempts++;
|
|
1801
|
+
} else {
|
|
1802
|
+
synced = true;
|
|
1803
|
+
break;
|
|
1804
|
+
}
|
|
1805
|
+
} catch (_err) {
|
|
1806
|
+
attempts++;
|
|
1807
|
+
}
|
|
1808
|
+
await sleep(intervalSec * 1000);
|
|
1809
|
+
}
|
|
1810
|
+
|
|
1811
|
+
if (synced) {
|
|
1812
|
+
return context;
|
|
1813
|
+
} else {
|
|
1814
|
+
throw new Error('Timed out waiting for worker sync');
|
|
1815
|
+
}
|
|
1816
|
+
}
|
|
1817
|
+
|
|
1818
|
+
async function sendGoSignal(context) {
|
|
1819
|
+
const s3 = createS3Client();
|
|
1820
|
+
const params = {
|
|
1821
|
+
Body: context.testId,
|
|
1822
|
+
Bucket: context.s3Bucket,
|
|
1823
|
+
Key: `test-runs/${context.testId}/go.json`
|
|
1824
|
+
};
|
|
1825
|
+
await s3.send(new PutObjectCommand(params));
|
|
1826
|
+
return context;
|
|
1827
|
+
}
|
|
1828
|
+
|
|
1829
|
+
async function listen(context, ee) {
|
|
1830
|
+
return new Promise((resolve, _reject) => {
|
|
1831
|
+
context.intermediateReports = [];
|
|
1832
|
+
context.aggregateReport = null;
|
|
1833
|
+
|
|
1834
|
+
const r = new SqsReporter(context);
|
|
1835
|
+
context.sqsReporter = r;
|
|
1836
|
+
r.on('workersDone', (state) => {
|
|
1837
|
+
ee.emit('workersDone', state);
|
|
1838
|
+
return resolve(context);
|
|
1839
|
+
});
|
|
1840
|
+
r.on('done', (stats) => {
|
|
1841
|
+
if (stats.report) {
|
|
1842
|
+
context.aggregateReport = stats.report();
|
|
1843
|
+
} else {
|
|
1844
|
+
context.aggregateReport = stats;
|
|
1845
|
+
}
|
|
1846
|
+
|
|
1847
|
+
global.artillery.globalEvents.emit('done', stats);
|
|
1848
|
+
ee.emit('done', stats);
|
|
1849
|
+
});
|
|
1850
|
+
r.on('error', (err) => {
|
|
1851
|
+
// Ignore SQS errors
|
|
1852
|
+
// ee.emit('error', err);
|
|
1853
|
+
// return reject(err);
|
|
1854
|
+
debug(err);
|
|
1855
|
+
});
|
|
1856
|
+
|
|
1857
|
+
r.on('workerDone', (body, attrs) => {
|
|
1858
|
+
if (process.env.LOG_WORKER_MESSAGES) {
|
|
1859
|
+
artillery.log(
|
|
1860
|
+
chalk.green(
|
|
1861
|
+
`[${attrs.workerId.StringValue} ${JSON.stringify(body, null, 4)}]`
|
|
1862
|
+
)
|
|
1863
|
+
);
|
|
1864
|
+
}
|
|
1865
|
+
});
|
|
1866
|
+
r.on('workerError', (body, attrs) => {
|
|
1867
|
+
if (process.env.LOG_WORKER_MESSAGES) {
|
|
1868
|
+
artillery.log(
|
|
1869
|
+
chalk.red(
|
|
1870
|
+
`[${attrs.workerId.StringValue} ${JSON.stringify(body, null, 4)}]`
|
|
1871
|
+
)
|
|
1872
|
+
);
|
|
1873
|
+
}
|
|
1874
|
+
if (body.exitCode !== 21) {
|
|
1875
|
+
artillery.log(
|
|
1876
|
+
chalk.yellow(
|
|
1877
|
+
`Worker exited with an error, worker ID = ${attrs.workerId.StringValue}`
|
|
1878
|
+
)
|
|
1879
|
+
);
|
|
1880
|
+
}
|
|
1881
|
+
|
|
1882
|
+
// TODO: Copy log over and print path to log file so that user may inspect it - in a temporary location
|
|
1883
|
+
global.artillery.suggestedExitCode = body.exitCode || 1;
|
|
1884
|
+
});
|
|
1885
|
+
|
|
1886
|
+
r.on('workerMessage', (body, attrs) => {
|
|
1887
|
+
if (process.env.LOG_WORKER_MESSAGES) {
|
|
1888
|
+
artillery.log(
|
|
1889
|
+
chalk.yellow(
|
|
1890
|
+
`[${attrs.workerId.StringValue}] ${body.msg} ${body.type}`
|
|
1891
|
+
)
|
|
1892
|
+
);
|
|
1893
|
+
}
|
|
1894
|
+
|
|
1895
|
+
if (body.type === 'stopped') {
|
|
1896
|
+
if (context.status !== TEST_RUN_STATUS.EARLY_STOP) {
|
|
1897
|
+
artillery.log('Test run has been requested to stop');
|
|
1898
|
+
}
|
|
1899
|
+
context.status = TEST_RUN_STATUS.EARLY_STOP;
|
|
1900
|
+
}
|
|
1901
|
+
|
|
1902
|
+
if (body.type === 'ensure') {
|
|
1903
|
+
try {
|
|
1904
|
+
context.ensureSpec = JSON.parse(util.atob(body.msg));
|
|
1905
|
+
} catch (_parseErr) {
|
|
1906
|
+
console.error('Error processing ensure directive');
|
|
1907
|
+
}
|
|
1908
|
+
}
|
|
1909
|
+
|
|
1910
|
+
if (body.type === 'leader' && body.msg === 'prepack_end') {
|
|
1911
|
+
ee.emit('prepack_end');
|
|
1912
|
+
}
|
|
1913
|
+
});
|
|
1914
|
+
|
|
1915
|
+
r.on('stats', async (stats) => {
|
|
1916
|
+
let report;
|
|
1917
|
+
if (stats.report) {
|
|
1918
|
+
report = stats.report();
|
|
1919
|
+
context.intermediateReports.push(report);
|
|
1920
|
+
} else {
|
|
1921
|
+
context.intermediateReports.push(stats);
|
|
1922
|
+
report = stats;
|
|
1923
|
+
}
|
|
1924
|
+
|
|
1925
|
+
global.artillery.globalEvents.emit('stats', stats);
|
|
1926
|
+
ee.emit('stats', stats);
|
|
1927
|
+
});
|
|
1928
|
+
|
|
1929
|
+
r.on('phaseStarted', (phase) => {
|
|
1930
|
+
global.artillery.globalEvents.emit('phaseStarted', phase);
|
|
1931
|
+
});
|
|
1932
|
+
|
|
1933
|
+
r.on('phaseCompleted', (phase) => {
|
|
1934
|
+
global.artillery.globalEvents.emit('phaseCompleted', phase);
|
|
1935
|
+
});
|
|
1936
|
+
|
|
1937
|
+
r.start();
|
|
1938
|
+
});
|
|
1939
|
+
}
|
|
1940
|
+
|
|
1941
|
+
async function deregisterTaskDefinition(context) {
|
|
1942
|
+
if (!context.taskDefinitionArn) {
|
|
1943
|
+
return;
|
|
1944
|
+
}
|
|
1945
|
+
|
|
1946
|
+
const ecs = new ECSClient({
|
|
1947
|
+
apiVersion: '2014-11-13',
|
|
1948
|
+
region: context.region
|
|
1949
|
+
});
|
|
1950
|
+
try {
|
|
1951
|
+
await ecs.send(
|
|
1952
|
+
new DeregisterTaskDefinitionCommand({
|
|
1953
|
+
taskDefinition: context.taskDefinitionArn
|
|
1954
|
+
})
|
|
1955
|
+
);
|
|
1956
|
+
debug(`Deregistered ${context.taskDefinitionArn}`);
|
|
1957
|
+
} catch (err) {
|
|
1958
|
+
artillery.log(err);
|
|
1959
|
+
debug(err);
|
|
1960
|
+
}
|
|
1961
|
+
|
|
1962
|
+
return context;
|
|
1963
|
+
}
|
|
1964
|
+
|
|
1965
|
+
// TODO: Remove - duplicated in run.js
|
|
1966
|
+
function getLogFilename(output, userDefaultFilenameFormat) {
|
|
1967
|
+
let logfile;
|
|
1968
|
+
|
|
1969
|
+
// is the destination a directory that exists?
|
|
1970
|
+
let isDir = false;
|
|
1971
|
+
if (output) {
|
|
1972
|
+
try {
|
|
1973
|
+
isDir = fs.statSync(output).isDirectory();
|
|
1974
|
+
} catch (_err) {
|
|
1975
|
+
// ENOENT, don't need to do anything
|
|
1976
|
+
}
|
|
1977
|
+
}
|
|
1978
|
+
|
|
1979
|
+
const defaultFormat = '[artillery_report_]YMMDD_HHmmSS[.json]';
|
|
1980
|
+
if (!isDir && output) {
|
|
1981
|
+
// -o is set with a filename (existing or not)
|
|
1982
|
+
logfile = output;
|
|
1983
|
+
} else if (!isDir && !output) {
|
|
1984
|
+
// no -o set
|
|
1985
|
+
} else {
|
|
1986
|
+
// -o is set with a directory
|
|
1987
|
+
logfile = path.join(
|
|
1988
|
+
output,
|
|
1989
|
+
moment().format(userDefaultFilenameFormat || defaultFormat)
|
|
1990
|
+
);
|
|
1991
|
+
}
|
|
1992
|
+
|
|
1993
|
+
return logfile;
|
|
1994
|
+
}
|