Skip to content

Commit

Permalink
Improved import customisation + readme
Browse files Browse the repository at this point in the history
  • Loading branch information
ljacobsson committed Apr 4, 2021
1 parent 50ed7ef commit d1dd15b
Show file tree
Hide file tree
Showing 8 changed files with 230 additions and 36 deletions.
56 changes: 55 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,61 @@ If you create your own collection you need to follow this structure:
└── template.yaml
```

## Customise pattern imports using placeholders and property manipulation
Say you host a pattern that creates an SQS queue and a Lambda function and sets the queue as an event source:
```
AWSTemplateFormatVersion: 2010-09-09
Transform:
- AWS::Serverless-2016-10-31
Resources:
The_Consumer:
Type: AWS::Serverless::Function
Properties:
Runtime: nodejs14.x
CodeUri: src/
Handler: The_Consumer.handler
Timeout: 3
Events:
SQS:
Type: SQS
Properties:
BatchSize: 10
Queue: !GetAtt The_Queue.Arn
The_Queue:
Type: AWS::SQS::Queue
```
When a user of this snippet imports it into their template they are likely to jump straight at renaming the resources to something that semantically describes the type of messages the queue is handling. Also, the runtime is likely to be something different.

As a pattern producer you can help out with this by defining a Metadata object on the pattern template:
```
Metadata:
PatternTransform:
Placeholders:
- Placeholder: The_
Message: Message type
Properties:
- JSONPath: $.Resources.The_Consumer.Properties.Events.SQS.Properties.BatchSize
InputType: number
Message: Enter batch size
- JSONPath: $.Resources.The_Consumer.Properties.CodeUri
InputType: string
Message: Enter CodeUri
```

*`PatternTransform.Placeholders` (array)*
* `Placeholder`: the string to be replaced
* `Message`: A message to be displayed (optional)

*`PatternTransform.Properties` (array)*
* `JSONPath`: The [JSONPath](https://support.smartbear.com/alertsite/docs/monitors/api/endpoint/jsonpath.html) to the property to be modified
* `Message`: A message to be displayed (optional)
* `InputType`: The datatype. Currently supports `string`, `number` or `runtime-select`

The input type `runtime-select` lets the user select a valid Lambda runtime. This metadata is automatically applied to all patterns, so there's no need to explicitly add it. If the user always writes code in a specific language they can export environment variable `SAM_PATTERNS_DEFAULT_RUNTIME` to a valid [Lambda runtime identifier](https://docs.aws.amazon.com/lambda/latest/dg/lambda-runtimes.html)

[Demo](images/demo3.gif)

## Known issues and limitations
* Comments in YAML disappear when parsing the template
* Only content form the template.yaml file will be imported. Any supporting files like lambda functions or openapi schemas will not be imported.
* Only content form the template.yaml file will be imported. Any supporting files like lambda functions or openapi schemas will be skipped.
* Only works with SAM templates
Binary file added images/demo3.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 5 additions & 0 deletions src/commands/import/baseFile.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"AWSTemplateFormatVersion": "2010-09-09",
"Transform": ["AWS::Serverless-2016-10-31"],
"Resources": {}
}
53 changes: 45 additions & 8 deletions src/commands/import/import.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,26 @@ const githubUtil = require("../../shared/githubUtil");
const { Octokit } = require("@octokit/rest");
const { Separator } = require("inquirer");
const transformer = require("./transformer");
const baseFile = require("./baseFile.json");
const templateAnatomy = require("./templateAnatomy.json");
const fs = require("fs");
const github = new Octokit({
auth: `token ${process.env.GITHUB_TOKEN}`,
});

const cfnDia = require("@mhlabs/cfn-diagram/graph/Vis");
const { templates } = require("@mhlabs/cfn-diagram/shared/templateCache");
const templateType = "SAM"; // to allow for more template frameworks
async function run(cmd) {
if (!fs.existsSync(cmd.template)) {
console.log(
`Can't find ${cmd.template}. Use -t option to specify template filename`
);
return;
const create = await inputUtil.prompt(`Create ${cmd.template}?`);
if (create) {
fs.writeFileSync(cmd.template, parser.stringify("yaml", baseFile));
} else {
return;
}
}

const ownTemplate = parser.parse("own", fs.readFileSync(cmd.template));
ownTemplate.Resources = ownTemplate.Resources || {};

const patterns = await githubUtil.getPatterns();

Expand Down Expand Up @@ -51,6 +56,7 @@ async function run(cmd) {
}

let template = parser.parse("import", templateString);
setDefaultMetadata(template);
template = await transformer.transform(template);
const sections = {
Parameters: template.Parameters,
Expand Down Expand Up @@ -90,8 +96,17 @@ async function run(cmd) {

console.log(`Added ${block.name} under ${block.section}`);
}

fs.writeFileSync(cmd.template, parser.stringify("own", ownTemplate));
const orderedTemplate = Object.keys(ownTemplate)
.sort(
(a, b) =>
templateAnatomy[templateType][a].order -
templateAnatomy[templateType][b].order
)
.reduce((obj, key) => {
obj[key] = ownTemplate[key];
return obj;
}, {});
fs.writeFileSync(cmd.template, parser.stringify("own", orderedTemplate));

console.log(
`${cmd.template} updated with ${
Expand All @@ -106,3 +121,25 @@ async function run(cmd) {
module.exports = {
run,
};
function setDefaultMetadata(template) {
template.Metadata = template.Metadata || { PatternTransform: {} };
template.Metadata.PatternTransform = template.Metadata.PatternTransform || {
Properties: [],
};
template.Metadata.PatternTransform.Properties =
template.Metadata.PatternTransform.Properties || [];
template.Metadata.PatternTransform.Properties.unshift({
JSONPath: "$.Globals.Function.Runtime",
InputType: "runtime-select",
});
for (const resource of Object.keys(template.Resources).filter((p) => ["AWS::Serverless::Function", "AWS::Lambda::Function"].includes(
template.Resources[p].Type
)
)) {
template.Metadata.PatternTransform.Properties.unshift({
JSONPath: "$.Resources." + resource + ".Properties.Runtime",
InputType: "runtime-select",
});
}
}

79 changes: 79 additions & 0 deletions src/commands/import/runtimes.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
[
{
"name": "nodejs10.x",
"extension": ".js",
"handlerFormat": "filename.handler"
},
{
"name": "nodejs12.x",
"extension": ".js",
"handlerFormat": "filename.handler"
},
{
"name": "nodejs14.x",
"extension": ".js",
"handlerFormat": "filename.handler",
"latest": true
},
{
"name": "python3.8",
"extension": ".js",
"handlerFormat": "filename.handler",
"latest": true
},
{
"name": "python3.7",
"extension": ".js",
"handlerFormat": "filename.handler"
},
{
"name": "python3.6",
"extension": ".js",
"handlerFormat": "filename.handler"
},
{
"name": "python2.7",
"extension": ".js",
"handlerFormat": "filename.handler"
},
{
"name": "ruby2.7",
"extension": ".js",
"handlerFormat": "filename.handler",
"latest": true
},
{
"name": "ruby2.5",
"extension": ".js",
"handlerFormat": "filename.handler"
},
{
"name": "java11",
"extension": ".js",
"handlerFormat": "filename.handler",
"latest": true
},
{
"name": "java8.al2",
"extension": ".js",
"handlerFormat": "filename.handler"
},
{ "name": "java8", "extension": ".js", "handlerFormat": "filename.handler" },
{
"name": "go1.x",
"extension": ".js",
"latest": true,
"handlerFormat": "filename.handler"
},
{
"name": "dotnetcore3.1",
"extension": ".js",
"handlerFormat": "filename.handler",
"latest": true
},
{
"name": "dotnetcore2.1",
"extension": ".js",
"handlerFormat": "filename.handler"
}
]
14 changes: 14 additions & 0 deletions src/commands/import/templateAnatomy.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"SAM": {
"AWSTemplateFormatVersion": { "order": 0 },
"Transform": { "order": 1 },
"Globals": { "order": 2 },
"Description": { "order": 3 },
"Metadata": { "order": 4 },
"Parameters": { "order": 5 },
"Mappings": { "order": 6 },
"Conditions": { "order": 7 },
"Resources": { "order": 8 },
"Outputs": { "order": 9 }
}
}
50 changes: 26 additions & 24 deletions src/commands/import/transformer.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
const inputUtil = require("../../shared/inputUtil");
const jp = require("jsonpath");
const runtimes = require("./runtimes.json");
const { Separator } = require("inquirer");

async function transform(template) {
const metadata = template.Metadata;
Expand All @@ -17,7 +19,7 @@ async function placeholderTransforms(template) {

let templateString = JSON.stringify(template);
for (const property of metadata.PatternTransform.Placeholders || []) {
const value = await inputUtil.text(
const value = await inputUtil.text(property.Message ||
`Placeholder value for ${property.Placeholder}:`
);
templateString = templateString.replaceAll(property.Placeholder, value);
Expand All @@ -27,19 +29,21 @@ async function placeholderTransforms(template) {
async function propertyTransforms(template) {
const metadata = template.Metadata;
for (const property of metadata.PatternTransform.Properties || []) {
if (!jp.query(template, property.JSONPath).length)
continue
let defaultValue, value;
switch (property.InputType) {
case "number":
defaultValue = jp.query(template, property.JSONPath);
console.log("Set value for " + property.JSONPath);
value = parseInt(
await inputUtil.text(
`${property.Message}:`,
`${property.Message || "Set value for " + property.JSONPath}:`,
defaultValue.length ? defaultValue[0] : undefined
)
);

break;
case "string":
case "text":
defaultValue = jp.query(template, property.JSONPath);
value = await inputUtil.text(
Expand All @@ -48,33 +52,31 @@ async function propertyTransforms(template) {
);
break;
case "runtime-select":
value = await inputUtil.list(
"Select Lambda runtime",
[
"nodejs10.x",
"nodejs12.x",
"nodejs14.x",
"python3.8",
"python3.7",
"python3.6",
"python2.7",
"ruby2.7",
"ruby2.5",
"java11",
"java8.al2",
"java8",
"go1.x",
"dotnetcore3.1",
"dotnetcore2.1",
].sort()
);
value =
process.env.SAM_PATTERNS_DEFAULT_RUNTIME ||
(await inputUtil.list(
`Select Lambda runtime for ${JSONPathToFrieldlyName(
property.JSONPath
)}.`,
[
...runtimes.filter((p) => p.latest),
new Separator("*** Older versions ***"),
...runtimes.filter((p) => !p.latest),
]
));
break;
}
jp.value(template, property.JSONPath, value);
if (value) jp.value(template, property.JSONPath, value);
}
return template;
}

function JSONPathToFrieldlyName(jsonPath) {
const split = jsonPath.split(".");
if (split[1] === "Globals") return "Globals";
else return "function " + split[2];
}

module.exports = {
transform,
};
9 changes: 6 additions & 3 deletions src/shared/parser.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
const yamlCfn = require("yaml-cfn");
let format = {};
let format = {
yaml: "yaml",
};
function parse(identifier, str) {
try {
const parsed = JSON.parse(str);
Expand All @@ -13,10 +15,11 @@ function parse(identifier, str) {
}
function stringify(identifier, obj) {
if (format[identifier] === "json") return JSON.stringify(obj, null, 2);
if (format[identifier] === "yaml") return yamlCfn.yamlDump(obj).replace(/!<(.+?)>/g, "$1");
if (format[identifier] === "yaml")
return yamlCfn.yamlDump(obj).replace(/!<(.+?)>/g, "$1");
}

module.exports = {
parse,
stringify
stringify,
};

0 comments on commit d1dd15b

Please sign in to comment.