Skip to content

Commit 531d14e

Browse files
author
ljacobsson
committed
new feature 🎉 - invoke Lambda and StateMachines
1 parent 06654df commit 531d14e

File tree

4 files changed

+431
-0
lines changed

4 files changed

+431
-0
lines changed

src/commands/invoke/index.js

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
const program = require("commander");
2+
const cons = require("./invoke");
3+
program
4+
.command("invoke")
5+
.alias("in")
6+
.description("Invokes a Lambda function or a StepFunctions state machine")
7+
.option("-s, --stack-name [stackName]", "The name of the deployed stack")
8+
.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")
9+
.option("-p, --profile [profile]", "AWS profile to use")
10+
.option("--region [region]", "The AWS region to use. Falls back on AWS_REGION environment variable if not specified")
11+
.action(async (cmd) => {
12+
await cons.run(cmd);
13+
});

src/commands/invoke/invoke.js

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
const { CloudFormationClient, DescribeStackResourcesCommand } = require("@aws-sdk/client-cloudformation");
2+
const { fromSSO } = require("@aws-sdk/credential-provider-sso");
3+
const lambdaInvoker = require("./lambdaInvoker");
4+
const stepFunctionsInvoker = require("./stepFunctionsInvoker");
5+
const inputUtil = require('../../shared/inputUtil');
6+
const parser = require("../../shared/parser");
7+
const fs = require("fs");
8+
const ini = require('ini');
9+
const link2aws = require('link2aws');
10+
const open = import('open');
11+
let region;
12+
async function run(cmd) {
13+
if (fs.existsSync("samconfig.toml")) {
14+
const config = ini.parse(fs.readFileSync("samconfig.toml", "utf8"));
15+
const params = config?.default?.deploy?.parameters;
16+
if (!cmd.stackName && params.stack_name) {
17+
console.log("Using stack name from config:", params.stack_name);
18+
cmd.stackName = params.stack_name;
19+
}
20+
if (!cmd.profile && params.profile) {
21+
console.log("Using AWS profile from config:", params.profile);
22+
cmd.profile = params.profile;
23+
}
24+
if (!cmd.region && params.region) {
25+
console.log("Using AWS region from config:", params.region);
26+
cmd.region = params.region;
27+
region = params.region;
28+
}
29+
}
30+
const credentials = await fromSSO({ profile: cmd.profile })();
31+
if (!cmd.stackName) {
32+
console.error("Missing required option: --stack-name");
33+
process.exit(1);
34+
}
35+
36+
const cloudFormation = new CloudFormationClient({ credentials: credentials, region: cmd.region });
37+
let resources;
38+
try {
39+
resources = await cloudFormation.send(new DescribeStackResourcesCommand({ StackName: cmd.stackName }));
40+
}
41+
catch (e) {
42+
console.log(`Failed to describe stack resources: ${e.message}`);
43+
process.exit(1);
44+
}
45+
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 } });
46+
47+
if (targets.length === 0) {
48+
console.log("No compatible resources found in stack");
49+
return;
50+
}
51+
let resource;
52+
53+
if (targets.length === 1) {
54+
resource = targets[0].value;
55+
}
56+
else {
57+
resource = await inputUtil.autocomplete("Select a resource", targets);
58+
}
59+
if (resource.ResourceType === "AWS::StepFunctions::StateMachine") {
60+
await stepFunctionsInvoker.invoke(cmd, resource.PhysicalResourceId);
61+
}
62+
else {
63+
await lambdaInvoker.invoke(cmd, resource.PhysicalResourceId);
64+
}
65+
}
66+
67+
async function getPhysicalId(cloudFormation, stackName, logicalId) {
68+
const params = {
69+
StackName: stackName,
70+
LogicalResourceId: logicalId
71+
};
72+
if (logicalId.endsWith(" Logs")) {
73+
const logGroupParentResource = logicalId.split(" ")[0];
74+
params.LogicalResourceId = logGroupParentResource;
75+
}
76+
77+
const response = await cloudFormation.send(new DescribeStackResourcesCommand(params));
78+
if (response.StackResources.length === 0) {
79+
console.log(`No stack resource found for ${logicalId}`);
80+
process.exit(1);
81+
}
82+
if (logicalId.endsWith(" Logs")) {
83+
const logGroup = `/aws/lambda/${response.StackResources[0].PhysicalResourceId}`;
84+
const source = `$257E$2527${logGroup}`.replace(/\//g, "*2f")
85+
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`;
86+
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`
87+
}
88+
89+
return response.StackResources[0].PhysicalResourceId;
90+
}
91+
92+
function createARN(resourceType, resourceName) {
93+
if (!resourceType.includes("::")) {
94+
console.log(`Can't create ARN for ${resourceType}`);
95+
return;
96+
}
97+
if (resourceName.startsWith("arn:")) {
98+
return resourceName;
99+
}
100+
let service = resourceType.split("::")[1].toLowerCase();
101+
const noResourceTypeArns = [
102+
"s3",
103+
"sqs",
104+
"sns",
105+
]
106+
const type = noResourceTypeArns.includes(service) ? "" : resourceType.split("::")[2].toLowerCase();
107+
108+
if (service === "sqs") {
109+
resourceName = resourceName.split("/").pop();
110+
}
111+
112+
//map sam to cloudformation
113+
if (service === "serverless") {
114+
switch (type) {
115+
case "function":
116+
service = "lambda";
117+
break;
118+
case "api":
119+
service = "apigateway";
120+
break;
121+
case "table":
122+
service = "dynamodb";
123+
break;
124+
case "statemachine":
125+
service = "states";
126+
break;
127+
}
128+
}
129+
130+
return `arn:aws:${service}:${region}::${type}:${resourceName}`;
131+
}
132+
133+
module.exports = {
134+
run
135+
};
136+
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
const { LambdaClient, InvokeCommand } = require('@aws-sdk/client-lambda');
2+
const { SchemasClient, DescribeSchemaCommand, UpdateSchemaCommand, CreateSchemaCommand, CreateRegistryCommand } = require('@aws-sdk/client-schemas');
3+
const { fromSSO } = require("@aws-sdk/credential-provider-sso");
4+
const fs = require('fs');
5+
const inputUtil = require('../../shared/inputUtil');
6+
7+
async function invoke(cmd, resourceName) {
8+
const lambdaClient = new LambdaClient({ credentials: await fromSSO({ profile: cmd.profile }) });
9+
const schemasClient = new SchemasClient({ credentials: await fromSSO({ profile: cmd.profile }) });
10+
if (!cmd.payload) {
11+
const payloadSource = await inputUtil.list("Select a payload source", ["Local JSON file", "Shared test event", "Input JSON"]);
12+
if (payloadSource === "Local JSON file") {
13+
cmd.payload = await inputUtil.file("Select file(s) to use as payload", "json");
14+
} else if (payloadSource === "Shared test event") {
15+
try {
16+
const sharedEvents = await schemasClient.send(new DescribeSchemaCommand({ RegistryName: "lambda-testevent-schemas", SchemaName: `_${resourceName}-schema` }));
17+
const schema = JSON.parse(sharedEvents.Content);
18+
const savedEvents = Object.keys(schema.components.examples);
19+
const event = await inputUtil.autocomplete("Select an event", savedEvents);
20+
cmd.payload = JSON.stringify(schema.components.examples[event].value);
21+
} catch (e) {
22+
console.log("Failed to fetch shared test events", e.message);
23+
process.exit(1);
24+
}
25+
} else if (payloadSource === "Input JSON") {
26+
do {
27+
cmd.payload = await inputUtil.text("Enter payload JSON");
28+
} while (!isValidJson(cmd.payload, true));
29+
const save = await inputUtil.prompt("Save as shared test event?", "No");
30+
if (save) {
31+
const name = await inputUtil.text("Enter a name for the event");
32+
try {
33+
try {
34+
await schemasClient.send(new CreateRegistryCommand({ RegistryName: registryName }));
35+
} catch (e) {
36+
// do nothing
37+
}
38+
39+
const schema = await schemasClient.send(new DescribeSchemaCommand({ RegistryName: "lambda-testevent-schemas", SchemaName: `_${resourceName}-schema` }));
40+
const schemaContent = JSON.parse(schema.Content);
41+
schemaContent.components.examples[name] = { value: JSON.parse(cmd.payload) };
42+
await schemasClient.send(new UpdateSchemaCommand({ RegistryName: "lambda-testevent-schemas", SchemaName: `_${resourceName}-schema`, Type: "OpenApi3", Content: JSON.stringify(schemaContent) }));
43+
} catch (e) {
44+
if (e.message.includes("does not exist")) {
45+
console.log("Creating new schema");
46+
const schemaContent = {
47+
openapi: "3.0.0",
48+
info: {
49+
title: `Event`,
50+
version: "1.0.0"
51+
},
52+
paths: {},
53+
components: {
54+
examples: {
55+
[name]: {
56+
value: JSON.parse(cmd.payload)
57+
}
58+
}
59+
}
60+
};
61+
await schemasClient.send(new CreateSchemaCommand({ RegistryName: "lambda-testevent-schemas", SchemaName: `_${resourceName}-schema`, Type: "OpenApi3", Content: JSON.stringify(schemaContent) }));
62+
} else {
63+
64+
console.log("Failed to save shared test event", e.message);
65+
process.exit(1);
66+
}
67+
}
68+
console.log(`Saved event '${name}'`);
69+
}
70+
}
71+
}
72+
73+
if (isFilePath(cmd.payload)) {
74+
cmd.payload = fs.readFileSync(cmd.payload).toString();
75+
}
76+
77+
if (!isValidJson(cmd.payload)) {
78+
try {
79+
const sharedEvents = await schemasClient.send(new DescribeSchemaCommand({ RegistryName: "lambda-testevent-schemas", SchemaName: `_${resourceName}-schema` }));
80+
const schema = JSON.parse(sharedEvents.Content);
81+
cmd.payload = JSON.stringify(schema.components.examples[cmd.payload].value);
82+
} catch (e) {
83+
console.log("Failed to fetch shared test events", e.message);
84+
process.exit(1);
85+
}
86+
}
87+
88+
if (isValidJson(cmd.payload)) {
89+
const params = new InvokeCommand({
90+
FunctionName: resourceName,
91+
Payload: cmd.payload
92+
});
93+
try {
94+
console.log("Invoking function with payload:", concatenateAndAddDots(cmd.payload, 100))
95+
const data = await lambdaClient.send(params);
96+
const response = JSON.parse(Buffer.from(data.Payload).toString());
97+
try {
98+
console.log("Response:", JSON.stringify(JSON.parse(response), null, 2));
99+
} catch (e) {
100+
console.log("Response:", response);
101+
}
102+
}
103+
catch (err) {
104+
console.log("Error", err);
105+
}
106+
} else {
107+
console.log("Invalid JSON, please try again");
108+
}
109+
}
110+
111+
function concatenateAndAddDots(str, maxLength) {
112+
if (str.length <= maxLength) {
113+
return str;
114+
}
115+
return str.substring(0, maxLength - 3) + "...";
116+
}
117+
118+
function isFilePath(str) {
119+
return str.startsWith("./") || str.startsWith("../") || str.startsWith("/") || str.startsWith("~") || str.startsWith("file://")
120+
&& fs.existsSync(str);
121+
}
122+
123+
function isValidJson(str, logInfo) {
124+
try {
125+
JSON.parse(str);
126+
} catch (e) {
127+
if (logInfo)
128+
console.log("Invalid JSON, please try again");
129+
return false;
130+
}
131+
return true;
132+
}
133+
134+
exports.invoke = invoke;

0 commit comments

Comments
 (0)