UNPKG

serverless-artillery

Version:

A serverless performance testing tool. `serverless` + `artillery` = crush. a.k.a. Orbital Laziers [sic]

838 lines (781 loc) 33.1 kB
const aws = require('aws-sdk') const BbPromise = require('bluebird') const fs = BbPromise.promisifyAll(require('fs')) const os = require('os') const path = require('path') const semver = require('semver') const shortid = require('shortid') const stdin = require('get-stdin') const temp = BbPromise.promisifyAll(require('temp')) const url = require('url') const yaml = require('js-yaml') const npm = require('./npm') const Serverless = require('./serverless-fx') const versioning = require('./versioning')() const platformSettings = require('./lambda/platform-settings.js') const modes = require('./lambda/modes.js') const planning = require('./lambda/planning.js') const constants = { CompatibleServerlessSemver: '^1.0.3', DefaultScriptName: 'script.yml', ServerlessFile: 'serverless.yml', // duplicated below but mentioned here for consistent use ServerlessFiles: [ '.slsart', 'alert.js', 'analysis.js', 'artillery-acceptance.js', 'artillery-monitoring.js', 'artillery-performance.js', 'artillery-task.js', 'handler.js', 'modes.js', 'package.json', 'package-lock.json', 'planning.js', 'platform-settings.js', 'sampling.js', 'serverless.yml', ], TestFunctionName: 'loadGenerator', ScheduleName: '${self:service}-${opt:stage, self:provider.stage}-monitoring', // eslint-disable-line no-template-curly-in-string AlertingName: 'monitoringAlerts', UserConfigPath: `${os.homedir()}/.slsart-config`, } const impl = { // FILE UTILS /** * Helper function that, given a path, returns true if it's a file. * @param {string} filePath location to check if file exists * @returns {boolean} true if file exists, false otherwise */ fileExists: (filePath) => { try { return fs.lstatSync(filePath).isFile() } catch (ex) { return false } }, // SCRIPT UTILS /** * Determine, given the user supplied script option, what script to use. Use a given script if it exists but * otherwise fall back to a local script.yml if it exists and finally fall back to the global script.yml if it * does not. * @param scriptPath An optional path the script to find. If supplied, it will be checked for existence. * @return A string indicating the path that was found. If the user supplied a file path that could not be found * return `null` to indicate an error should be displayed. Otherwise, return a local script.yml file or the global * script.yml file if the prior two conditions do not hold. */ findScriptPath: (scriptPath) => { if (scriptPath) { if (impl.fileExists(scriptPath)) { if (path.isAbsolute(scriptPath)) { return scriptPath } else { return path.join(process.cwd(), scriptPath) } } else { return null // doesn't exist } } else { const localDefaultScript = path.join(process.cwd(), constants.DefaultScriptName) if (impl.fileExists(localDefaultScript)) { return localDefaultScript } else { return path.join(__dirname, 'lambda', constants.DefaultScriptName) } } }, /** * Read the input into a parsible string * @param options The CLI flag settings to identify the input source. * @returns {Promise.<string>} */ getScriptText: (options) => { // Priority: `--si`, `--stdIn`, `-d`, `--data`, `-p`, `--path`, ./script.yml, $(npm root -g)/script.yml if (options.si || options.stdIn) { return stdin() } else if (options.d || options.data) { return BbPromise.resolve(options.d || options.data) } else { const scriptPath = impl.findScriptPath(options.p || options.path) if (scriptPath) { return fs.readFileAsync(scriptPath, 'utf8') } else { return BbPromise.reject(new Error(`${os.EOL}\tScript '${options.script}' could not be found.${os.EOL}`)) } } }, /** * Parse the given input as either YAML or JSON, passing back the parsed object or failing otherwise. * @param input The input to attempt parsing * @returns {*} The parsed artillery script */ parseScript: input => yaml.safeLoad(input), /** * Replace the given input flag with the `-d` flag, providing the stringification of the given script as its value. */ replaceArgv: (script) => { const flags = [ { flag: '-si', bool: true }, { flag: '--stdIn', bool: true }, { flag: '-d', bool: false }, { flag: '--data', bool: false }, { flag: '-p', bool: false }, { flag: '--path', bool: false }, ] for (let i = 0; i < flags.length; i++) { const idx = process.argv.indexOf(flags[i].flag) if (idx !== -1) { process.argv = process.argv.slice(0, idx).concat(process.argv.slice(idx + (flags[i].bool ? 1 : 2))) } } // add function flag process.argv.push('-f') process.argv.push(constants.TestFunctionName) // add data flag process.argv.push('-d') process.argv.push(JSON.stringify(script)) }, /** * Get the allowance for and requirement of the given script with regard to execution time * @param script The script to determine allowance and requirements for * @returns {{allowance: number, required: number}} */ scriptConstraints: (script) => { const networkBuffer = 2 // seconds to allow for network transmission const resultsBuffer = 3 // seconds to allow for processing overhead (validation, decision making, results calculation) const settings = platformSettings.getSettings(script) const durationInSeconds = planning.scriptDurationInSeconds(script) const requestsPerSecond = planning.scriptRequestsPerSecond(script) let httpTimeout = 120 // default AWS setting if ( aws.config && aws.config.httpOptions && 'timeout' in aws.config.httpOptions ) { httpTimeout = Math.floor(aws.config.httpOptions.timeout / 1000) // convert from ms to s } const ret = { allowance: Math.min(httpTimeout, settings.maxChunkDurationInSeconds) - networkBuffer, required: durationInSeconds + resultsBuffer, } if ( durationInSeconds > settings.maxChunkDurationInSeconds || requestsPerSecond > settings.maxChunkRequestsPerSecond ) { // if splitting happens, the time requirement is increased by timeBufferInMilliseconds ret.required += Math.ceil(settings.timeBufferInMilliseconds / 1000) // convert from ms to s } return ret }, generateScriptDefaults: (options) => { const opts = options || {} opts.endpoint = opts.endpoint || 'http://aws.amazon.com' opts.duration = opts.duration || 5 opts.rate = opts.rate || 2 // extract and combine options into generated script opts.urlParts = url.parse(opts.endpoint) return opts }, /** * Generate a script with the given options hive. Return the default script generated with those settings, filling * in default values as appropriate. * @param options The options hive to use in building the default script * @return {string} The string containing a JSON object that comprises the default script. */ generateScript: (options) => { // fallback to defaults const opts = impl.generateScriptDefaults(options) // extract and combine options into generated script return [`# Thank you for trying serverless-artillery! # This default script is intended to get you started quickly. # There is a lot more that Artillery can do. # You can find great documentation of the possibilities at: # https://artillery.io/docs/ config: # this hostname will be used as a prefix for each URI in the flow unless a complete URI is specified target: "${opts.urlParts.protocol}//${opts.urlParts.auth ? `${opts.urlParts.auth}@` : ''}${opts.urlParts.host}" phases: - duration: ${opts.duration} arrivalRate: ${opts.rate}`, opts.rampTo ? ` rampTo: ${opts.rampTo}` : // note that this is a break in the template string (to avoid spurious newline) '', ` scenarios: - flow: - get: url: "${opts.urlParts.path}${opts.urlParts.hash ? opts.urlParts.hash : ''}" `, ].join('') }, // MONITORING UTILS /** * Validate that if the service has an enabled monitoring event schedule, that it also has a valid TOPIC_ARN configured for the purpose of * notifying of any events where observed errors exceeded the error budget. Additionally warns users if they have an enabled monitoring * event schedule and a TOPIC_ARN configuration that references a topic without any subscribers. * @param service The service to validate for deployment preconditions */ validateServiceForDeployment: (service) => { if ( service && service.functions && service.functions[constants.TestFunctionName] && service.functions[constants.TestFunctionName].events ) { const isMonitoringEvent = event => (event && event.schedule && event.schedule.name && event.schedule.name === constants.ScheduleName) const topicArnRegex = /^arn:(?:\*|aws[\w-]*):sns:(?:\*|[\w-]+):\d+:\w+$/ const monitoringEvent = service.functions[constants.TestFunctionName].events.filter(isMonitoringEvent)[0] if ( monitoringEvent && ( !('enabled' in monitoringEvent.schedule) || monitoringEvent.schedule.enabled ) ) { // enabled monitoring event if ( !service.functions[constants.TestFunctionName].environment || !service.functions[constants.TestFunctionName].environment.TOPIC_ARN ) { throw new Error('An environment variable supplying a TOPIC_ARN for notifications must be supplied') } else if ( typeof service.functions[constants.TestFunctionName].environment.TOPIC_ARN === 'string' && !service.functions[constants.TestFunctionName].environment.TOPIC_ARN.match(topicArnRegex) ) { throw new Error(`If service.functions.${constants.TestFunctionName}.environment.TOPIC_ARN is a string, it must contain a valid SNS topic ARN`) } else if ( !(typeof service.functions[constants.TestFunctionName].environment.TOPIC_ARN === 'string') && !service.functions[constants.TestFunctionName].environment.TOPIC_ARN.Ref ) { throw new Error(`The variable service.functions.${constants.TestFunctionName}.environment.TOPIC_ARN must be a Ref to a SNS topic resource or string containing an SNS topic ARN`) } else if ( !(typeof service.functions[constants.TestFunctionName].environment.TOPIC_ARN === 'string') && service.functions[constants.TestFunctionName].environment.TOPIC_ARN.Ref && ( !service.resources.Resources[service.functions[constants.TestFunctionName].environment.TOPIC_ARN.Ref] || service.resources.Resources[service.functions[constants.TestFunctionName].environment.TOPIC_ARN.Ref].Type !== 'AWS::SNS::Topic' ) ) { throw new Error(`If service.functions.${constants.TestFunctionName}.environment.TOPIC_ARN is a Ref, it must reference an SNS topic`) } else if ( !(typeof service.functions[constants.TestFunctionName].environment.TOPIC_ARN === 'string') && service.functions[constants.TestFunctionName].environment.TOPIC_ARN.Ref && !( service.resources.Resources[service.functions[constants.TestFunctionName].environment.TOPIC_ARN.Ref].Properties.Subscription && Array.isArray(service.resources.Resources[service.functions[constants.TestFunctionName].environment.TOPIC_ARN.Ref].Properties.Subscription) && service.resources.Resources[service.functions[constants.TestFunctionName].environment.TOPIC_ARN.Ref].Properties.Subscription.length > 0 ) ) { throw new Error('No subscriptions are supplied for the SNS topic associated with your enabled monitoring event') } } } }, /** * Check the version of the local function assets version, the invocation options * and script mode and if the invocation type is monitoring and the default * version is found, then an error is thrown. * @param options The invoke command line options * @param script Artillery script being run * @param cwd Function to get the current working directory path */ validateServiceForInvocation: (options, script, cwd = process.cwd) => { let toss = false try { const localService = require(path.join(cwd(), 'package.json')) // eslint-disable-line global-require, import/no-dynamic-require toss = !('version' in localService) && (options.monitoring || modes.isMonitoringScript(script)) } catch (ex) { // can't be found. that's okay, we could be using the default service } if (toss) { throw new Error([ '', `Your local function project assets in ${cwd()} implements a service`, 'version that does not support invocation with the monitoring flag.', '', 'To use monitoring mode, please run the "configure" command', 'in a clean directory and then customize your worker\'s assets.', ].join(`${os.EOL}\t`)) } }, // SERVERLESS UTILS /** * Copy artillery lambda files to temp dir. */ populateArtilleryLambda: (tempPath) => { const sourceArtilleryLambdaPath = path.join(__dirname, 'lambda') let anyFailedCopies = false // Copy each of the Serverless files from the default implementation. constants.ServerlessFiles.forEach((file) => { try { const sourceFile = path.join(sourceArtilleryLambdaPath, file) const destFile = path.join(tempPath, file) fs.copyFileSync(sourceFile, destFile) } catch (ex) { anyFailedCopies = true console.error(`Failed to copy ${file} from ${sourceArtilleryLambdaPath} to ${tempPath}`) } }) if (anyFailedCopies) throw new Error(`Failed to populate artillery project in ${tempPath}.`) }, /** * Install artillery lambda dependencies to temp dir. */ installArtilleryLambdaDependencies: (tempPath) => { npm.install(tempPath) }, /** * Get the user's slsart info. */ readUserSAConfig: () => { let infoJSON = null // Read user's existing SA config or provide the (empty) default try { infoJSON = fs.readFileSync(constants.UserConfigPath, 'utf8') } catch (ex) { infoJSON = '{}' } return JSON.parse(infoJSON) }, /** * Save the user's slsart info. */ writeUserSAConfig: config => fs.writeFileSync(constants.UserConfigPath, JSON.stringify(config)), /** * Checks for each of the asset files in the target dir. */ allServerlessFilesExist: (targetPath) => { let slsFilesMissing = false // Check for each of the necessary Artillery Lambda files. constants.ServerlessFiles.forEach((file) => { const fullFilePath = path.join(targetPath, file) if (!fs.existsSync(fullFilePath)) { if (process.env.DEBUG) console.log(`Missing Artillery Lambda file ${file} in ${targetPath}.`) slsFilesMissing = true } }) return !slsFilesMissing }, /** * Check that prerequisite assets exist in temporary working dir. * @return {boolean} True if the temp path exists and has valid Artillery Lambda. */ verifyTempArtilleryLambda: (tempPath) => { if ( tempPath // The temp path is not null, && fs.existsSync(tempPath) // it exists, && impl.allServerlessFilesExist(tempPath) // all assets exist, && fs.existsSync(path.join(tempPath, 'node_modules')) // and dependencies appear installed. ) { return true } const message = tempPath ? `Artillery Lambda at ${tempPath} is not valid.` : 'No temp Artillery Lambda found.' if (process.env.DEBUG) console.log(message) return false }, /** * Reuse or create temporary default Artillery Lambda. * @returns {Promise.string} - path to temp Artillery Lambda assets */ tempArtilleryLambda: () => { const userConfig = impl.readUserSAConfig() // Check for existing temp directory with Artillery Lambda already available. if (impl.verifyTempArtilleryLambda(userConfig.tempPath)) { // Reuse the existing temporary path if (process.env.DEBUG) console.log(`Using existing default Artillery Lambda assets in ${userConfig.tempPath} ...`) return BbPromise.resolve(userConfig.tempPath) } // Create new temp dir for Artillery Lambda. return temp.mkdir('artillery-lambda') .then((tempPath) => { if (process.env.DEBUG) console.log(`Creating new default Artillery Lambda assets in ${tempPath} ...`) // Copy Artillery Lambda implementation to the temp path. impl.populateArtilleryLambda(tempPath) // Run `npm install` to provide dependencies there. impl.installArtilleryLambdaDependencies(tempPath) // Store the location of temp dir for reuse later. userConfig.tempPath = tempPath impl.writeUserSAConfig(userConfig) // Finally, return the location of the project to the caller. return tempPath }) .catch((err) => { console.error(err) throw new Error('Failed to create default Artillery Lambda assets.') }) }, /** * Checks working directory for service config, otherwise uses default. * @returns {string} - path to service config */ findServicePath: () => { const cwd = process.cwd() const localServerlessPath = path.join(cwd, 'serverless.yml') if (impl.fileExists(localServerlessPath)) { // User is running a custom SA Lambda, so use the current working dir. return cwd } else { return impl.tempArtilleryLambda() } }, /** * Invokes the Serverless code to perform a give task. Expects process.argv to * contain CLI parameters to pass to SLS. */ serverlessRunner: (options) => { // pretend that SLS was called. process.argv[1] = Serverless.dirname // proceed with using SLS const servicePath = impl.findServicePath() const serverless = new Serverless({ interactive: false, servicePath, }) if (options.verbose) { console.log(`Serverless version is ${serverless.version}, compatible version is '${constants.CompatibleServerlessSemver}'`) } if (!semver.satisfies(serverless.version, constants.CompatibleServerlessSemver)) { return BbPromise.reject(new Error( // eslint-disable-next-line comma-dangle `Loaded Serverless version '${serverless.version}' but the compatible version is ${constants.CompatibleServerlessSemver}` )) } let SLS_DEBUG if (options.debug) { if (options.verbose) { console.log(`Running Serverless with argv: ${process.argv}`) } ({ SLS_DEBUG } = process.env) process.env.SLS_DEBUG = '*' } let result return serverless.init() .then(() => { // add our intercepter const invoke = serverless.pluginManager.plugins.find( plugin => Object.getPrototypeOf(plugin).constructor.name === 'AwsInvoke' // eslint-disable-line comma-dangle ) const { log } = invoke invoke.log = (response) => { if (response && 'Payload' in response && typeof response.Payload === 'string' && response.Payload.length) { try { result = JSON.parse(response.Payload) } catch (ex) { console.error(`exception parsing payload:${os.EOL }${JSON.stringify(response)}${os.EOL }${ex}${os.EOL}${os.EOL }Please report this error to this tool's GitHub repo so that we can dig into the cause:${os.EOL }https://github.com/Nordstrom/serverless-artillery/issues${os.EOL }Please scrub any sensitive information that may be present prior to submission.${os.EOL }Thank you!`) } } return log.call(invoke, response) } }) .then(() => serverless.run()) .then(() => { process.env.SLS_DEBUG = SLS_DEBUG return result }) .catch((ex) => { process.env.SLS_DEBUG = SLS_DEBUG console.error(ex) throw ex }) }, /** * Load and resolve serverless service file so that the content of the returned serverless.service object * will reflect the names of the resources in the cloudprovider console * use when you need the names of resources in the cloud */ serverlessLoader: () => { const serverless = new Serverless({ interactive: false, servicePath: impl.findServicePath(), }) return serverless.init() .then(() => serverless.variables.populateService(serverless.pluginManager.cliOptions)) .then(() => { serverless.service.mergeArrays() serverless.service.setFunctionNames(serverless.processedInput.options) return serverless }) }, } module.exports = { /** * Deploy the load generation function to the configured provider. Prefer a local service over the global service * but better to have one service over having none. * @return {Promise} A promise that completes after the deployment of the function and reporting of that * deployment. */ deploy: options => impl.serverlessLoader() .then((serverless) => { impl.validateServiceForDeployment(serverless.service) console.log(`${os.EOL}\tDeploying function...${os.EOL}`) return impl.serverlessRunner(options).then(() => { console.log(`${os.EOL}\tDeploy complete.${os.EOL}`) }) }), /** * Send a script to the remote function. Prefer a script identified by the `-s` or `--script` option over a * `script.yml` file in the current working directory over the global `script.yml`. * @param options The options given by the user. See the ~/bin/serverless-artillery implementation for details. * @return {Promise} A promise that completes after the invocation of the function with the script given * by the user (or a fallback option). */ invoke: options => impl.getScriptText(options) .then(impl.parseScript) .then((input) => { const script = input if (options.acceptance) { script.mode = modes.ACC } else if (options.monitoring) { script.mode = modes.MON } impl.replaceArgv(script) // check local assets version impl.validateServiceForInvocation(options, script) // analyze script if tool or script is in performance mode let completeMessage = `${os.EOL}\tYour function invocation has completed.${os.EOL}` if (!modes.isSamplingScript(script)) { const constraints = impl.scriptConstraints(script) if (constraints.allowance < constraints.required) { // exceeds limits? process.argv.push('-t') process.argv.push('Event') completeMessage = `${os.EOL}\tYour function has been invoked. The load is scheduled to be completed in ${constraints.required} seconds.${os.EOL}` } } const log = msg => console.log(msg) const logIf = (msg) => { if (!(options.jo || options.jsonOnly)) { log(msg) } } // run the given script on the deployed lambda logIf(`${os.EOL}\tInvoking test Lambda${os.EOL}`) return impl.serverlessRunner(options).then((result) => { logIf(completeMessage) log(JSON.stringify(result, null, 2)) if (options.acceptance || options.monitoring) { logIf('Results:') if (result && result.errorMessage) { logIf(`FAILED ${result.errorMessage}`) process.exit(result.errors) } else { logIf('PASSED') } } return result }) }), /** * Kill an invoked lambda that is actively executing. */ kill: (options) => { let funcName const lambdaOptions = {} if (options.region) lambdaOptions.region = options.region const lambda = new aws.Lambda(lambdaOptions) if (options.debug) { console.log('Rendering serverless.yml variables to obtain deployed function name') } return impl.serverlessLoader() .then((serverless) => { funcName = serverless.service.functions.loadGenerator.name if (options.debug) { console.log(`Setting concurrency to zero for function ${funcName}`) } const params = { FunctionName: funcName, // required ReservedConcurrentExecutions: 0, } return lambda.putFunctionConcurrency(params).promise() .catch((err) => { if (options.debug) { console.error(err.message) if (options.verbose) { console.error((new Error()).stack) } } if (err.code === 'ResourceNotFoundException') { throw new Error(`${os.EOL}Kill command failed: The function ${funcName} is not deployed${os.EOL}`) } else if (err.code === 'ConfigError') { throw new Error([ '', `Error: ${err.message}`, '', 'You must specify a region when running this command:', ' Use `--region` option, e.g. `--region=us-east-1`', ' or set AWS_REGION in environment, e.g. `AWS_REGION=us-east-1`', ' or configure a default region using the guide below.', '', 'Serverless will use the `us-east-1` region by default.', '', 'Please make sure that your AWS credentials are properly configured.', ' see: https://serverless.com/framework/docs/providers/aws/guide/credentials/', '', ].join(os.EOL)) } else { throw new Error(`${os.EOL}Unexpected error setting concurrency to zero: ${err.message} ${err.code}${os.EOL}`) } }) }) .then(() => { console.log(`${os.EOL}Concurrency successfully set to zero for ${funcName}.${os.EOL}`) options._ = 'remove' // eslint-disable-line no-param-reassign process.argv[2] = 'remove' return module.exports.remove(options) .then(() => { const oldDate = new Date() const deployTime = new Date(oldDate.getTime() + (5 * 60 * 1000)) // add five minutes to current time console.log(`${os.EOL}We advise to wait until ${deployTime.toLocaleTimeString('en-US')} before re-deploying to avoid possible problems. See https://github.com/Nordstrom/serverless-artillery#killing-in-progress-performance-test${os.EOL}`) }) }) }, /** * Remove the CloudFormation Stack (or equivalent) from the configured provider. * @return {Promise} A promise that completes after the removal of the stack and reporting of its * completion. */ remove: (options) => { console.log(`${os.EOL}\tRemoving function...${os.EOL}`) return impl.serverlessRunner(options).then(() => { console.log(`${os.EOL}\tRemoval complete.${os.EOL}`) }) }, /** * Generate a script using the user's given options. Place it into the given out path or the default out path if * none was given. * @param options The user's supplied options. */ script: (options) => { const destPath = options.out || 'script.yml' if (impl.fileExists(destPath)) { return BbPromise.reject(new Error(`${os.EOL}\tConflict at path '${destPath}'. File exists. No script generated.${os.EOL}`)) } else { if (options.debug) { console.log('Generating script...') } const newScript = impl.generateScript(options) if (options.debug) { console.log(`Writing script:${os.EOL}${newScript}${os.EOL}to path: '${destPath}'`) } return fs.writeFileAsync(destPath, newScript) .then(() => console.log([ `${os.EOL}\tYour script '${destPath}' is created.`, `${os.EOL}\tWe're very glad that you see enough value to create a custom script!`, `${os.EOL}\tEdit your script and review the documentation for your endpoint pummeling options at:`, `${os.EOL}\thttps://artillery.io/docs ${os.EOL}`, ].join(''))) } }, /** * Generate the function deployment assets and place them into the current working directory so that the user can * create and deploy a custom function definition. * @returns {Promise} A promise that completes after the generation of function assets for subequent deployment. */ configure: options => new BbPromise((resolve, reject) => { const conflicts = [] const cwd = process.cwd() // identify conflicts if (options.debug) { console.log('Identifying any file conflicts...') } constants.ServerlessFiles.forEach((file) => { const destPath = path.join(cwd, file) if (impl.fileExists(destPath)) { conflicts.push(destPath) } }) if (conflicts.length) { // report any conflicts if (options.debug) { console.log('Conflicts discovered, generating output message.') } let msg = `${os.EOL}\tConflict with existing files:` conflicts.forEach((file) => { msg += `${os.EOL}\t\t${file}` }) msg += `${os.EOL}\tNo files created.${os.EOL}` reject(new Error(msg)) } else { // create the configuration assets if (options.debug) { console.log('No conflicts found, creating a local copy of the function assets for deployment.') } constants.ServerlessFiles.forEach((file) => { const sourcePath = path.join(__dirname, 'lambda', file) const destPath = path.join(cwd, file) const content = fs.readFileSync(sourcePath, { encoding: 'utf8' }) fs.writeFileSync(destPath, content.replace( 'service: serverless-artillery', `service: serverless-artillery-${shortid.generate().replace(/_/g, 'A')}`)) }) const completeMessage = [ `${os.EOL}\tYour function assets have been created.`, `${os.EOL}\tWe are glad that you see enough value in the project to do some customization!`, `${os.EOL}\tEdit serverless.yml to customize your load function but please note that you must`, `${os.EOL}\t\tdeploy the function before invoking and also after making any modifications.`, `${os.EOL}\tTo continuously monitoring your service, you will need to enable the capability. See:`, `${os.EOL}\t\thttps://github.com/Nordstrom/serverless-artillery/README.md#monitoring-mode`, `${os.EOL}\tDocumentation available at https://docs.serverless.com ${os.EOL}`, ] try { if (options.debug) { console.log('Executing `npm install` to provide dependencies to the generated Serverless project.') } npm.install(cwd) resolve() } catch (ex) { completeMessage.push( `${os.EOL}`, `${os.EOL}An error occurred executing 'npm install' please note and resolve any errors `, 'and run \'npm install\' in the current working directory again.') reject(ex) } console.log(completeMessage.join('')) } }), /** * Upgrade the local function assets to their latest form. * @returns {Promise} A promise that completes after updating the local function assets. */ upgrade: () => { const localAssetsPath = process.cwd() const localAssetsVersioning = versioning(localAssetsPath) const noUpgradeAvailable = !localAssetsVersioning.upgradeAvailable() if (noUpgradeAvailable) { console.log('Your function assets are up to date: no need to upgrade.') return Promise.resolve() } try { console.log('Upgrading project...') const version = localAssetsVersioning.upgradeService() console.log() console.log('Upgrade complete.') console.log() console.log('Existing project files were copied to `backup`.') console.log() console.log('It\'s possible that the upgraded project contained additional customizations:') console.log(`Diff \`backup\` with \`original-assets-${version}\` to understand how the project was originally customized.`) console.log('Diff `serverless.yml` with `backup/serverless.yml` to compare the previous cloud resources with the upgraded service.') console.log('Diff `project.json` with `backup/project.json` to compare the changes in project dependencies.') console.log() console.log('Please merge any missing customizations into the project to complete the upgrade.') console.log() // Please inspect the upgraded files by comparing with the backup files to make sure that your custom changes are maintained and manually merge it where needed. } catch (ex) { console.error('Upgrade failed:') console.error(ex) return Promise.reject(ex) } return Promise.resolve() }, } // TODO remove before publishing? /* test-code */ module.exports.constants = constants module.exports.impl = impl /* end-test-code */