import path from 'path' import validate from './lib/validate.js' import stdin from 'get-stdin' import formatLambdaLogEvent from './utils/format-lambda-log-event.js' import ServerlessError from '../../serverless-error.js' import utils from '@serverlessinc/sf-core/src/utils.js' const { writeText, style } = utils class AwsInvoke { constructor(serverless, options, pluginUtils) { this.serverless = serverless this.options = options || {} this.provider = this.serverless.getProvider('aws') this.logger = pluginUtils.log this.progress = pluginUtils.progress Object.assign(this, validate) this.hooks = { 'invoke:invoke': async () => { this.progress.notice('Invoking function') await this.extendedValidate() this.log(await this.invoke()) }, } } async validateFile(key) { const absolutePath = path.resolve( this.serverless.serviceDir, this.options[key], ) try { return await this.serverless.utils.readFile(absolutePath) } catch (err) { if (err.code === 'ENOENT') { throw new ServerlessError( 'The file you provided does not exist.', 'FILE_NOT_FOUND', ) } throw err } } async extendedValidate() { this.validate() // validate function exists in service this.options.functionObj = this.serverless.service.getFunction( this.options.function, ) this.options.data = this.options.data || '' if (!this.options.data) { if (this.options.path) { this.options.data = await this.validateFile('path') } else { try { this.options.data = await stdin() } catch { // continue if no stdin was provided } } } if (!this.options.context && this.options.contextPath) { this.options.context = await this.validateFile('contextPath') } try { if (!this.options.raw) { this.options.data = JSON.parse(this.options.data) } } catch (exception) { // do nothing if it's a simple string or object already } try { if (!this.options.raw && this.options.context) { this.options.context = JSON.parse(this.options.context) } } catch (exception) { // do nothing if it's a simple string or object already } } async invoke() { const invocationType = this.options.type || 'RequestResponse' if (invocationType !== 'RequestResponse') { this.options.log = 'None' } else { this.options.log = this.options.log ? 'Tail' : 'None' } const params = { FunctionName: this.options.functionObj.name, InvocationType: invocationType, LogType: this.options.log, Payload: Buffer.from(JSON.stringify(this.options.data || {})), } if (this.options.context) { params.ClientContext = Buffer.from( JSON.stringify(this.options.context), ).toString('base64') } if (this.options.qualifier) { params.Qualifier = this.options.qualifier } return this.provider.request('Lambda', 'invoke', params) } log(invocationReply) { this.progress.remove() if (invocationReply.Payload) { const response = JSON.parse(invocationReply.Payload) writeText(JSON.stringify(response, null, 4)) } if (invocationReply.LogResult) { this.logger.blankLine() writeText(style.aside('----------------------')) this.logger.blankLine() const logResult = Buffer.from( invocationReply.LogResult, 'base64', ).toString() const logResultLines = logResult.split('\n') // Loop through and ensure the log that starts with "START" is always shown first // and the log that starts with "REPORT" is always shown last const startLog = logResultLines.find((line) => line.startsWith('START')) const reportLog = logResultLines.find((line) => line.startsWith('REPORT')) if (startLog) { this.logger.aside(formatLambdaLogEvent(startLog)) } logResultLines.forEach((line) => { if ( !line.startsWith('START') && !line.startsWith('END') && !line.startsWith('REPORT') ) { this.logger.aside(formatLambdaLogEvent(line)) } }) if (reportLog) { this.logger.aside(formatLambdaLogEvent(reportLog)) } } if (invocationReply.FunctionError) { throw new ServerlessError( 'Invoked function failed', 'AWS_LAMBDA_INVOCATION_FAILED', ) } } } export default AwsInvoke