forked from ljacobsson/samp-cli
-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
new feature 🎉 - invoke Lambda and StateMachines
- Loading branch information
ljacobsson
committed
Jun 23, 2023
1 parent
06654df
commit 531d14e
Showing
4 changed files
with
431 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
}; | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
Oops, something went wrong.