Skip to content

Commit

Permalink
Merge pull request #2297 from artilleryio/feat/platform-fargate
Browse files Browse the repository at this point in the history
refactor: import @artilleryio/platform-fargate
  • Loading branch information
hassy authored Nov 17, 2023
2 parents fef0907 + 6118b17 commit 18ad4ac
Show file tree
Hide file tree
Showing 21 changed files with 4,910 additions and 1,596 deletions.
2,786 changes: 1,266 additions & 1,520 deletions package-lock.json

Large diffs are not rendered by default.

111 changes: 103 additions & 8 deletions packages/artillery/lib/cmds/run-fargate.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,7 @@ const { Command, Flags, Args } = require('@oclif/core');
const telemetry = require('../telemetry').init();
const { Plugin: CloudPlugin } = require('../platform/cloud/cloud');

const tryRequire = require('try-require');
const PlatformFargateLegacy = tryRequire('@artilleryio/platform-fargate');
const runCluster = require('../platform/aws-ecs/legacy/run-cluster');
const PlatformECS = require('../platform/aws-ecs/ecs');
const { ECS_WORKER_ROLE_NAME } = require('../platform/aws/constants');

Expand All @@ -18,6 +17,11 @@ class RunCommand extends Command {

async run() {
const { flags, _argv, args } = await this.parse(RunCommand);

flags['platform-opt'] = [`region=${flags.region}`];

flags.platform = 'aws:ecs';

new CloudPlugin(null, null, { flags });

const ECS = new PlatformECS(null, null, {}, { testRunId: 'foo' });
Expand All @@ -34,14 +38,105 @@ class RunCommand extends Command {
});

// Delegate the rest to existing implementation:
PlatformFargateLegacy.commands.runCluster(args.script, flags);
runCluster(args.script, flags);
}
}

if (PlatformFargateLegacy) {
RunCommand.description = PlatformFargateLegacy.oclif.runTest.description;
RunCommand.flags = PlatformFargateLegacy.oclif.runTest.flags;
RunCommand.args = PlatformFargateLegacy.oclif.runTest.args;
}
const runTestDescriptions = {
count: 'Number of load generator workers to launch',
cluster: 'Name of the Fargate/ECS cluster to run the test on',
region: 'The AWS region to run in',
packages:
'Path to package.json file which lists dependencies for the test script',
maxDuration: 'Maximum duration of the test run',
dotenv: 'Path to a .env file to load environment variables from'
};

RunCommand.description = `launch a test using AWS ECS/Fargate
Examples:
To launch a test with 10 load generating workers using AWS Fargate in us-east-1:
$ artillery run:fargate --count 10 --region us-east-1 my-test.yml
`;

RunCommand.flags = {
count: Flags.integer({
description: runTestDescriptions.count
}),
cluster: Flags.string({
description: runTestDescriptions.cluster
}),
region: Flags.string({
char: 'r',
description: runTestDescriptions.region
}),
secret: Flags.string({
multiple: true
}),
// TODO: Descriptions
'launch-type': Flags.string({}),
'launch-config': Flags.string({}),
'subnet-ids': Flags.string({}),
'security-group-ids': Flags.string({}),
'task-role-name': Flags.string({}),
target: Flags.string({
char: 't',
description:
'Set target endpoint. Overrides the target already set in the test script'
}),
output: Flags.string({
char: 'o',
description: 'Write a JSON report to file'
}),
insecure: Flags.boolean({
char: 'k',
description: 'Allow insecure TLS connections; do not use in production'
}),
environment: Flags.string({
char: 'e',
description: 'Use one of the environments specified in config.environments'
}),
config: Flags.string({
description: 'Read configuration for the test from the specified file'
}),
'scenario-name': Flags.string({
description: 'Name of the specific scenario to run'
}),
overrides: Flags.string({
description: 'Dynamically override values in the test script; a JSON object'
}),
input: Flags.string({
char: 'i',
description: 'Input script file',
multiple: true,
hidden: true
}),
tags: Flags.string({
description:
'Comma-separated list of tags in key:value format to tag the test run, for example: --tags team:sre,service:foo'
}),
note: Flags.string({}), // TODO: description
packages: Flags.string({
description: runTestDescriptions.packages
}),
'max-duration': Flags.string({
description: runTestDescriptions.maxDuration
}),
dotenv: Flags.string({
description: runTestDescriptions.dotenv
}),
record: Flags.boolean({
description: 'Record test run to Artillery Cloud'
}),
key: Flags.string({
description: 'API key for Artillery Cloud'
})
};

RunCommand.args = {
script: Args.string()
};

module.exports = RunCommand;
126 changes: 126 additions & 0 deletions packages/artillery/lib/platform/aws-ecs/legacy/aws-util.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
const AWS = require('aws-sdk');
const debug = require('debug')('util');

module.exports = {
// ECS:
ecsDescribeTasks,

// AWS SSM:
ensureParameterExists,
parameterExists,
putParameter,
getParameter
};

// Wraps ecs.describeTasks to support more than 100 task ARNs in params.tasks
async function ecsDescribeTasks(params, ecs) {
const taskArnChunks = splitIntoSublists(params.tasks, 100);
const results = { tasks: [], failures: [] };
for (let i = 0; i < taskArnChunks.length; i++) {
const params2 = Object.assign({}, params, { tasks: taskArnChunks[i] });
try {
const ecsData = await ecs.describeTasks(params2).promise();
results.tasks = results.tasks.concat(ecsData.tasks);
results.failures = results.failures.concat(ecsData.failures);
} catch (err) {
throw err;
}
}
return results;
}

// Slice input list into several lists, where each list has no more than maxGroupSize elements
function splitIntoSublists(list, maxGroupSize) {
const result = [];
const numGroups = Math.ceil(list.length / maxGroupSize);
for (let i = 0; i < numGroups; i++) {
result.push(list.slice(i * maxGroupSize, i * maxGroupSize + maxGroupSize));
}
return result;
}

// ********************
// AWS SSM helpers
// In future these will be parameter-store agnostic, and work with Kubernetes
// ConfigMaps or Azure/GCP native equivalents.
// ********************

// If parameter exists, do nothing; otherwise set the value
async function ensureParameterExists(ssmPath, defaultValue, type, region) {
if (region) AWS.config.update({ region });

try {
const exists = await parameterExists(ssmPath);
if (exists) {
return Promise.resolve();
}
return putParameter(ssmPath, defaultValue, type);
} catch (err) {
return Promise.reject(err);
}
}

async function parameterExists(path, region) {
if (region) AWS.config.update({ region });
const ssm = new AWS.SSM({ apiVersion: '2014-11-06' });
const getParams = {
Name: path,
WithDecryption: true
};

try {
const ssmResponse = await ssm.getParameter(getParams).promise();
return Promise.resolve(true);
} catch (ssmErr) {
if (ssmErr.code === 'ParameterNotFound') {
return Promise.resolve(false);
} else {
return Promise.reject(ssmErr);
}
}
}

async function putParameter(path, value, type, region) {
if (region) AWS.config.update({ region });
const ssm = new AWS.SSM({ apiVersion: '2014-11-06' });

const putParams = {
Name: path,
Type: type,
Value: value,
Overwrite: true
};

try {
const ssmResponse = await ssm.putParameter(putParams).promise();
return Promise.resolve();
} catch (ssmErr) {
return Promise.reject(ssmErr);
}
}

async function getParameter(path, region) {
if (region) {
AWS.config.update({ region });
}

const ssm = new AWS.SSM({ apiVersion: '2014-11-06' });

try {
const ssmResponse = await ssm
.getParameter({
Name: path,
WithDecryption: true
})
.promise();

debug({ ssmResponse });
return ssmResponse.Parameter && ssmResponse.Parameter.Value;
} catch (ssmErr) {
if (ssmErr.code === 'ParameterNotFound') {
return false;
} else {
throw ssmErr;
}
}
}
Loading

0 comments on commit 18ad4ac

Please sign in to comment.