Skip to content

Commit

Permalink
new feature 🎉 - invoke Lambda and StateMachines
Browse files Browse the repository at this point in the history
  • Loading branch information
ljacobsson committed Jun 23, 2023
1 parent 06654df commit 531d14e
Show file tree
Hide file tree
Showing 4 changed files with 431 additions and 0 deletions.
13 changes: 13 additions & 0 deletions src/commands/invoke/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
const program = require("commander");
const cons = require("./invoke");
program
.command("invoke")
.alias("in")
.description("Invokes a Lambda function or a StepFunctions state machine")
.option("-s, --stack-name [stackName]", "The name of the deployed stack")
.option("-pl, --payload [payload]", "The payload to send to the function. Could be stringified JSON, a file path to a JSON file or the name of a shared test event")
.option("-p, --profile [profile]", "AWS profile to use")
.option("--region [region]", "The AWS region to use. Falls back on AWS_REGION environment variable if not specified")
.action(async (cmd) => {
await cons.run(cmd);
});
136 changes: 136 additions & 0 deletions src/commands/invoke/invoke.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
const { CloudFormationClient, DescribeStackResourcesCommand } = require("@aws-sdk/client-cloudformation");
const { fromSSO } = require("@aws-sdk/credential-provider-sso");
const lambdaInvoker = require("./lambdaInvoker");
const stepFunctionsInvoker = require("./stepFunctionsInvoker");
const inputUtil = require('../../shared/inputUtil');
const parser = require("../../shared/parser");
const fs = require("fs");
const ini = require('ini');
const link2aws = require('link2aws');
const open = import('open');
let region;
async function run(cmd) {
if (fs.existsSync("samconfig.toml")) {
const config = ini.parse(fs.readFileSync("samconfig.toml", "utf8"));
const params = config?.default?.deploy?.parameters;
if (!cmd.stackName && params.stack_name) {
console.log("Using stack name from config:", params.stack_name);
cmd.stackName = params.stack_name;
}
if (!cmd.profile && params.profile) {
console.log("Using AWS profile from config:", params.profile);
cmd.profile = params.profile;
}
if (!cmd.region && params.region) {
console.log("Using AWS region from config:", params.region);
cmd.region = params.region;
region = params.region;
}
}
const credentials = await fromSSO({ profile: cmd.profile })();
if (!cmd.stackName) {
console.error("Missing required option: --stack-name");
process.exit(1);
}

const cloudFormation = new CloudFormationClient({ credentials: credentials, region: cmd.region });
let resources;
try {
resources = await cloudFormation.send(new DescribeStackResourcesCommand({ StackName: cmd.stackName }));
}
catch (e) {
console.log(`Failed to describe stack resources: ${e.message}`);
process.exit(1);
}
const targets = resources.StackResources.filter(r => ["AWS::Lambda::Function", "AWS::StepFunctions::StateMachine"].includes(r.ResourceType)).map(r => { return { name: `${r.LogicalResourceId} [${r.ResourceType.split("::")[1]}]`, value: r } });

if (targets.length === 0) {
console.log("No compatible resources found in stack");
return;
}
let resource;

if (targets.length === 1) {
resource = targets[0].value;
}
else {
resource = await inputUtil.autocomplete("Select a resource", targets);
}
if (resource.ResourceType === "AWS::StepFunctions::StateMachine") {
await stepFunctionsInvoker.invoke(cmd, resource.PhysicalResourceId);
}
else {
await lambdaInvoker.invoke(cmd, resource.PhysicalResourceId);
}
}

async function getPhysicalId(cloudFormation, stackName, logicalId) {
const params = {
StackName: stackName,
LogicalResourceId: logicalId
};
if (logicalId.endsWith(" Logs")) {
const logGroupParentResource = logicalId.split(" ")[0];
params.LogicalResourceId = logGroupParentResource;
}

const response = await cloudFormation.send(new DescribeStackResourcesCommand(params));
if (response.StackResources.length === 0) {
console.log(`No stack resource found for ${logicalId}`);
process.exit(1);
}
if (logicalId.endsWith(" Logs")) {
const logGroup = `/aws/lambda/${response.StackResources[0].PhysicalResourceId}`;
const source = `$257E$2527${logGroup}`.replace(/\//g, "*2f")
const query = `fields*20*40timestamp*2c*20*40message*2c*20*40logStream*2c*20*40log*0a*7c*20filter*20*40message*20like*20*2f*2f*0a*7c*20sort*20*40timestamp*20desc*0a*7c*20limit*2020`;
return `https://${region}.console.aws.amazon.com/cloudwatch/home?region=${region}#logsV2:logs-insights$3FqueryDetail$3D$257E$2528end$257E0$257Estart$257E-3600$257EtimeType$257E$2527RELATIVE$257Eunit$257E$2527seconds$257EeditorString$257E$2527${query}$257EisLiveTail$257Efalse$257Esource$257E$2528${source}$2529$2529`
}

return response.StackResources[0].PhysicalResourceId;
}

function createARN(resourceType, resourceName) {
if (!resourceType.includes("::")) {
console.log(`Can't create ARN for ${resourceType}`);
return;
}
if (resourceName.startsWith("arn:")) {
return resourceName;
}
let service = resourceType.split("::")[1].toLowerCase();
const noResourceTypeArns = [
"s3",
"sqs",
"sns",
]
const type = noResourceTypeArns.includes(service) ? "" : resourceType.split("::")[2].toLowerCase();

if (service === "sqs") {
resourceName = resourceName.split("/").pop();
}

//map sam to cloudformation
if (service === "serverless") {
switch (type) {
case "function":
service = "lambda";
break;
case "api":
service = "apigateway";
break;
case "table":
service = "dynamodb";
break;
case "statemachine":
service = "states";
break;
}
}

return `arn:aws:${service}:${region}::${type}:${resourceName}`;
}

module.exports = {
run
};

134 changes: 134 additions & 0 deletions src/commands/invoke/lambdaInvoker.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
const { LambdaClient, InvokeCommand } = require('@aws-sdk/client-lambda');
const { SchemasClient, DescribeSchemaCommand, UpdateSchemaCommand, CreateSchemaCommand, CreateRegistryCommand } = require('@aws-sdk/client-schemas');
const { fromSSO } = require("@aws-sdk/credential-provider-sso");
const fs = require('fs');
const inputUtil = require('../../shared/inputUtil');

async function invoke(cmd, resourceName) {
const lambdaClient = new LambdaClient({ credentials: await fromSSO({ profile: cmd.profile }) });
const schemasClient = new SchemasClient({ credentials: await fromSSO({ profile: cmd.profile }) });
if (!cmd.payload) {
const payloadSource = await inputUtil.list("Select a payload source", ["Local JSON file", "Shared test event", "Input JSON"]);
if (payloadSource === "Local JSON file") {
cmd.payload = await inputUtil.file("Select file(s) to use as payload", "json");
} else if (payloadSource === "Shared test event") {
try {
const sharedEvents = await schemasClient.send(new DescribeSchemaCommand({ RegistryName: "lambda-testevent-schemas", SchemaName: `_${resourceName}-schema` }));
const schema = JSON.parse(sharedEvents.Content);
const savedEvents = Object.keys(schema.components.examples);
const event = await inputUtil.autocomplete("Select an event", savedEvents);
cmd.payload = JSON.stringify(schema.components.examples[event].value);
} catch (e) {
console.log("Failed to fetch shared test events", e.message);
process.exit(1);
}
} else if (payloadSource === "Input JSON") {
do {
cmd.payload = await inputUtil.text("Enter payload JSON");
} while (!isValidJson(cmd.payload, true));
const save = await inputUtil.prompt("Save as shared test event?", "No");
if (save) {
const name = await inputUtil.text("Enter a name for the event");
try {
try {
await schemasClient.send(new CreateRegistryCommand({ RegistryName: registryName }));
} catch (e) {
// do nothing
}

const schema = await schemasClient.send(new DescribeSchemaCommand({ RegistryName: "lambda-testevent-schemas", SchemaName: `_${resourceName}-schema` }));
const schemaContent = JSON.parse(schema.Content);
schemaContent.components.examples[name] = { value: JSON.parse(cmd.payload) };
await schemasClient.send(new UpdateSchemaCommand({ RegistryName: "lambda-testevent-schemas", SchemaName: `_${resourceName}-schema`, Type: "OpenApi3", Content: JSON.stringify(schemaContent) }));
} catch (e) {
if (e.message.includes("does not exist")) {
console.log("Creating new schema");
const schemaContent = {
openapi: "3.0.0",
info: {
title: `Event`,
version: "1.0.0"
},
paths: {},
components: {
examples: {
[name]: {
value: JSON.parse(cmd.payload)
}
}
}
};
await schemasClient.send(new CreateSchemaCommand({ RegistryName: "lambda-testevent-schemas", SchemaName: `_${resourceName}-schema`, Type: "OpenApi3", Content: JSON.stringify(schemaContent) }));
} else {

console.log("Failed to save shared test event", e.message);
process.exit(1);
}
}
console.log(`Saved event '${name}'`);
}
}
}

if (isFilePath(cmd.payload)) {
cmd.payload = fs.readFileSync(cmd.payload).toString();
}

if (!isValidJson(cmd.payload)) {
try {
const sharedEvents = await schemasClient.send(new DescribeSchemaCommand({ RegistryName: "lambda-testevent-schemas", SchemaName: `_${resourceName}-schema` }));
const schema = JSON.parse(sharedEvents.Content);
cmd.payload = JSON.stringify(schema.components.examples[cmd.payload].value);
} catch (e) {
console.log("Failed to fetch shared test events", e.message);
process.exit(1);
}
}

if (isValidJson(cmd.payload)) {
const params = new InvokeCommand({
FunctionName: resourceName,
Payload: cmd.payload
});
try {
console.log("Invoking function with payload:", concatenateAndAddDots(cmd.payload, 100))
const data = await lambdaClient.send(params);
const response = JSON.parse(Buffer.from(data.Payload).toString());
try {
console.log("Response:", JSON.stringify(JSON.parse(response), null, 2));
} catch (e) {
console.log("Response:", response);
}
}
catch (err) {
console.log("Error", err);
}
} else {
console.log("Invalid JSON, please try again");
}
}

function concatenateAndAddDots(str, maxLength) {
if (str.length <= maxLength) {
return str;
}
return str.substring(0, maxLength - 3) + "...";
}

function isFilePath(str) {
return str.startsWith("./") || str.startsWith("../") || str.startsWith("/") || str.startsWith("~") || str.startsWith("file://")
&& fs.existsSync(str);
}

function isValidJson(str, logInfo) {
try {
JSON.parse(str);
} catch (e) {
if (logInfo)
console.log("Invalid JSON, please try again");
return false;
}
return true;
}

exports.invoke = invoke;
Loading

0 comments on commit 531d14e

Please sign in to comment.