UNPKG

7.98 kBJavaScriptView Raw
1'use strict';
2
3/* This Source Code Form is subject to the terms of the Mozilla Public
4 * License, v. 2.0. If a copy of the MPL was not distributed with this
5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
6
7const aws = require('aws-sdk');
8const debug = require('debug')('engine:lambda');
9const A = require('async');
10const _ = require('lodash');
11const helpers = require('artillery/core/lib/engine_util');
12
13const utils = require('./utils');
14
15function LambdaEngine (script, ee) {
16 this.script = script;
17 this.ee = ee;
18 this.helpers = helpers;
19 this.config = script.config;
20
21 this.config.processor = this.config.processor || {};
22
23 return this;
24}
25
26LambdaEngine.prototype.createScenario = function createScenario (scenarioSpec, ee) {
27
28 // as for http engine we add before and after scenario hook
29 // as normal functions in scenario's steps
30 const beforeScenarioFns = _.map(
31 scenarioSpec.beforeScenario,
32 function(hookFunctionName) {
33 return {'function': hookFunctionName};
34 });
35 const afterScenarioFns = _.map(
36 scenarioSpec.afterScenario,
37 function(hookFunctionName) {
38 return {'function': hookFunctionName};
39 });
40
41 const newFlow = beforeScenarioFns.concat(
42 scenarioSpec.flow.concat(afterScenarioFns));
43
44 scenarioSpec.flow = newFlow;
45
46 const tasks = scenarioSpec.flow.map(rs => this.step(rs, ee, {
47 beforeRequest: scenarioSpec.beforeRequest,
48 afterResponse: scenarioSpec.afterResponse,
49 }));
50
51 return this.compile(tasks, scenarioSpec.flow, ee);
52};
53
54LambdaEngine.prototype.step = function step (rs, ee, opts) {
55 opts = opts || {};
56 let self = this;
57
58 if (rs.loop) {
59 let steps = _.map(rs.loop, function (rs) {
60 return self.step(rs, ee, opts);
61 });
62
63 return this.helpers.createLoopWithCount(
64 rs.count || -1,
65 steps,
66 {
67 loopValue: rs.loopValue || '$loopCount',
68 overValues: rs.over,
69 whileTrue: self.config.processor
70 ? self.config.processor[rs.whileTrue] : undefined
71 });
72 }
73
74 if (rs.log) {
75 return function log (context, callback) {
76 return process.nextTick(function () { callback(null, context); });
77 };
78 }
79
80 if (rs.think) {
81 return this.helpers.createThink(rs, _.get(self.config, 'defaults.think', {}));
82 }
83
84 if (rs.function) {
85 return function (context, callback) {
86 let func = self.config.processor[rs.function];
87 if (!func) {
88 return process.nextTick(function () { callback(null, context); });
89 }
90
91 return func(context, ee, function () {
92 return callback(null, context);
93 });
94 };
95 }
96
97 if (rs.invoke) {
98 return function invoke (context, callback) {
99
100 context.funcs.$increment = self.$increment;
101 context.funcs.$decrement = self.$decrement;
102 context.funcs.$contextUid = function () {
103 return context._uid;
104 };
105
106 const payload = typeof rs.invoke.payload === 'object'
107 ? JSON.stringify(rs.invoke.payload)
108 : String(rs.invoke.payload);
109
110 // see documentation for a description of these fields
111 // https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/Lambda.html#invoke-property
112 var awsParams = {
113 ClientContext: Buffer.from(rs.invoke.clientContext || '{}').toString('base64'),
114 FunctionName: rs.invoke.target || self.script.config.target,
115 InvocationType: rs.invoke.invocationType || 'Event',
116 LogType: rs.invoke.logType || 'Tail',
117 Payload: helpers.template(payload, context),
118 Qualifier: rs.invoke.qualifier || '$LATEST'
119 };
120
121 // build object to pass to hooks
122 // we do not pass only aws params but also additional information
123 // we need to make the engine work with other plugins
124 const params = _.assign({
125 url: context.lambda.endpoint.href,
126 awsParams: awsParams,
127 }, rs.invoke);
128
129
130 const beforeRequestFunctionNames = _.concat(opts.beforeRequest || [], rs.invoke.beforeRequest || []);
131
132 utils.processBeforeRequestFunctions(
133 self.script,
134 beforeRequestFunctionNames,
135 params,
136 context,
137 ee,
138 function done(err) {
139 if (err) {
140 debug(err);
141 return callback(err, context);
142 }
143
144 ee.emit('request');
145 const startedAt = process.hrtime();
146
147 // after running "before request" functions
148 // the context could have changed
149 // we need to rerun template on payload
150 awsParams.Payload = helpers.template(payload, context);
151
152 // invoke lambda function
153 context.lambda.invoke(awsParams, function (err, data) {
154
155 if (err) {
156 debug(err);
157 ee.emit('error', err);
158 return callback(err, context);
159 }
160
161 let code = data.StatusCode || 0;
162 const endedAt = process.hrtime(startedAt);
163 let delta = (endedAt[0] * 1e9) + endedAt[1];
164 ee.emit('response', delta, code, context._uid);
165 debug(data);
166
167 // AWS output is a generic string
168 // we need to guess its content type
169 const payload = utils.tryToParse(data.Payload);
170
171 // we build a fake http response
172 // it is needed to make the lib work with other plugins
173 // such as https://github.com/artilleryio/artillery-plugin-expect
174 const response = {
175 body: payload.body,
176 statusCode: data.StatusCode,
177 headers: {
178 'content-type': payload.contentType
179 },
180 };
181
182 helpers.captureOrMatch(
183 params,
184 response,
185 context,
186 function captured(err, result) {
187 // TODO handle matches
188 let haveFailedCaptures = _.some(result.captures, function(v, k) {
189 return v === '';
190 });
191
192 if (!haveFailedCaptures) {
193 _.each(result.captures, function(v, k) {
194 _.set(context.vars, k, v);
195 });
196 }
197
198 const afterResponseFunctionNames = _.concat(opts.afterResponse || [], rs.invoke.afterResponse || []);
199
200 utils.processAfterResponseFunctions(
201 self.script,
202 afterResponseFunctionNames,
203 params,
204 response,
205 context,
206 ee,
207 function done(err) {
208 if (err) {
209 debug(err);
210 return callback(err, context);
211 }
212
213 return callback(null, context);
214 }
215 );
216 }
217 );
218
219 });
220 }
221 )
222 };
223 }
224
225 return function (context, callback) {
226 return callback(null, context);
227 };
228};
229
230LambdaEngine.prototype.compile = function compile (tasks, scenarioSpec, ee) {
231 const self = this;
232 return function scenario (initialContext, callback) {
233 const init = function init (next) {
234 let opts = {
235 region: self.script.config.lambda.region || 'us-east-1'
236 };
237
238 if (self.script.config.lambda.function) {
239 opts.endpoint = self.script.config.lambda.function;
240 }
241
242 initialContext.lambda = new aws.Lambda(opts);
243 ee.emit('started');
244 return next(null, initialContext);
245 };
246
247 let steps = [init].concat(tasks);
248
249 A.waterfall(
250 steps,
251 function done (err, context) {
252 if (err) {
253 debug(err);
254 }
255
256 return callback(err, context);
257 });
258 };
259};
260
261LambdaEngine.prototype.$increment = function $increment (value) {
262 let result = Number.isInteger(value) ? value += 1 : NaN;
263 return result;
264};
265
266LambdaEngine.prototype.$decrement = function $decrement (value) {
267 let result = Number.isInteger(value) ? value -= 1 : NaN;
268 return result;
269};
270
271module.exports = LambdaEngine;