diff --git a/src/code-binding.js b/src/code-binding.js new file mode 100644 index 0000000..310172a --- /dev/null +++ b/src/code-binding.js @@ -0,0 +1,37 @@ +const AWS = require("aws-sdk"); +const schemas = new AWS.Schemas(); +const inputUtil = require("./input-util"); +const languages = ["Java8", "Python36", "TypeScript3"]; +const fs = require("fs"); +const inquirer = require("inquirer"); +const prompt = inquirer.createPromptModule(); + +async function browse(l) { + const registry = await inputUtil.getRegistry(schemas); + const schemaResponse = await schemas + .listSchemas({ RegistryName: registry.id }) + .promise(); + + const sourceName = await inputUtil.getSourceName(schemaResponse); + const detailTypeName = await inputUtil.getDetailTypeName( + schemaResponse, + sourceName + ); + const schemaName = `${sourceName}@${detailTypeName}`; + + const language = await prompt({ + name: "id", + type: "list", + message: `Select language`, + choices: languages + }); + + // const source = await schemas.putCodeBinding({Language: language.id, RegistryName: registry.id, SchemaName: schemaName}).promise(); + // console.log(source.$response.data); + + const binding = await schemas.getCodeBindingSource({ Language: language.id, SchemaName:schemaName, RegistryName: registry.id}).promise(); + fs.writeFileSync("binding.zip", binding.Body); +} +module.exports = { + browse +}; diff --git a/src/input-util.js b/src/input-util.js new file mode 100644 index 0000000..ff243b1 --- /dev/null +++ b/src/input-util.js @@ -0,0 +1,211 @@ +const inquirer = require("inquirer"); +const prompt = inquirer.createPromptModule(); +const BACK = "↩ back"; +const UNDO = "⎌ undo"; +const DONE = "✔ done"; +const CONSUME_LOCALLY = "⚡ consume locally"; +const backNavigation = [BACK, new inquirer.Separator("-------------")]; +const doneNavigation = [DONE, UNDO, new inquirer.Separator("-------------")]; +const filterRules = [ + "equals", + "prefix", + "anything-but", + "numeric", + "exists", + "null", +]; + +const numericOperators = [">", "<", "=", ">=", "<=", "!=", "range"]; + +async function text(message, def) { + const response = await prompt({ + name: "id", + type: "input", + message: message, + default: def, + }); + return response.id; +} + +async function getStringValue(fieldName, type) { + const rules = JSON.parse(JSON.stringify(filterRules)); + const rule = await prompt({ + name: "id", + type: "list", + message: `Enter rule for ${fieldName} matching`, + choices: [...rules], + }); + + let val = undefined; + if (rule.id !== "exists" && rule.id !== "numeric") { + const value = await prompt({ + name: "id", + type: "input", + message: `Enter value for ${fieldName}. Comma separate for array`, + }); + val = value.id.includes(",") + ? value.id.split(",").map((p) => p.trim()) + : value.id; + } else if (rule.id === "exists") { + val = true; + } else if (rule.id === "numeric") { + const operator = await prompt({ + name: "id", + type: "list", + message: `Select operator`, + choices: numericOperators, + }); + if (operator.id === "range") { + const lower = await prompt({ + name: "id", + type: "input", + message: `Lower bound for ${fieldName}`, + }); + const upper = await prompt({ + name: "id", + type: "input", + message: `Upper bound for ${fieldName}`, + }); + val = [">=", parseFloat(lower.id), "<", parseFloat(upper.id)]; + + } else { + const value = await prompt({ + name: "id", + type: "input", + message: `Enter value for ${fieldName}`, + }); + + val = [operator.id, parseFloat(value.id)]; + } + } + let returnObj = {}; + + let ruleObj = rule.id === "equals" ? val : undefined; + if (!ruleObj) { + ruleObj = {}; + ruleObj[rule.id] = val; + } + if (!Array.isArray(ruleObj)) { + returnObj[fieldName] = []; + returnObj[fieldName].push(ruleObj); + } else { + returnObj[fieldName] = ruleObj; + } + + return returnObj; +} + +async function getProperty(currentObject, objectArray) { + let fieldList = Object.keys(currentObject.properties); + const property = await prompt({ + name: "id", + type: "list", + message: `Add ${ + objectArray[objectArray.length - 1] || + currentObject["x-amazon-events-detail-type"] + } item`, + choices: [ + ...(objectArray.length ? backNavigation : doneNavigation), + ...fieldList, + ], + }); + objectArray.push(property.id); + const chosenProp = currentObject.properties[property.id]; + + return { property, chosenProp }; +} + +async function getDetailTypeName(schemas, sourceName) { + const detailTypes = schemas.Schemas.filter((p) => + p.SchemaName.startsWith(`${sourceName}@`) + ).map((p) => p.SchemaName.split("@")[1]); + const detailType = await prompt({ + name: "id", + type: "list", + message: "Select detail-type", + choices: detailTypes, + }); + + const detailTypeName = detailType.id; + return detailTypeName; +} + +async function getSourceName(schemas) { + const sources = [ + ...new Set(schemas.Schemas.map((p) => p.SchemaName.split("@")[0])), + ]; + const source = await prompt({ + name: "id", + type: "list", + message: "Select source", + choices: sources, + }); + const sourceName = source.id; + return sourceName; +} + +async function getRegistry(schemas) { + const registriesResponse = await schemas.listRegistries().promise(); + const registries = [ + ...new Set(registriesResponse.Registries.map((p) => p.RegistryName)), + ]; + const registry = await prompt({ + name: "id", + type: "list", + message: "Select registry", + choices: registries, + }); + return registry; +} + +async function selectFrom(list, message, skipBack) { + const answer = await prompt({ + name: "id", + type: "list", + message: message || "Please select", + choices: [!skipBack ? BACK : null, ...list].filter((p) => p), + }); + return answer.id; +} + +async function getEventBusName(eventbridge) { + const eventBusesResponse = await eventbridge.listEventBuses().promise(); + const eventBuses = [ + ...new Set(eventBusesResponse.EventBuses.map((p) => p.Name)), + ]; + const eventBusName = await prompt({ + name: "id", + type: "list", + message: "Select eventbus", + choices: eventBuses, + }); + return eventBusName.id; +} + +async function getPropertyValue(chosenProp, property) { + let answer = undefined; + switch (chosenProp.type) { + case "string": + answer = await getStringValue(property.id, chosenProp.type); + break; + // placeholder for dateselector, etc + default: + answer = await getStringValue(property.id, chosenProp.type); + } + return answer; +} + +module.exports = { + getEventBusName, + getRegistry, + getSourceName, + getDetailTypeName, + selectFrom, + getProperty, + getPropertyValue, + text, + BACK, + DONE, + UNDO, + CONSUME_LOCALLY, +}; diff --git a/src/pattern-builder.js b/src/pattern-builder.js new file mode 100644 index 0000000..59e0e68 --- /dev/null +++ b/src/pattern-builder.js @@ -0,0 +1,285 @@ +const inputUtil = require("./input-util"); +const YAML = require("json-to-pretty-yaml"); +const localIntegration = require("../local-integration"); + +function init(source, detailType) { + return { source: [source], "detail-type": [detailType] }; +} + +function isObject(item) { + return item && typeof item === "object" && !Array.isArray(item); +} + +function deepMerge(target, ...sources) { + if (!sources.length) return target; + const source = sources.shift(); + + if (isObject(target) && isObject(source)) { + for (const key in source) { + if (isObject(source[key])) { + if (!target[key]) Object.assign(target, { [key]: {} }); + deepMerge(target[key], source[key]); + } else { + Object.assign(target, { [key]: source[key] }); + } + } + } + + return deepMerge(target, ...sources); +} + +function buildSegment(answer, objectArray) { + let x = {}; + let current = answer; + for (let i = objectArray.length - 2; i >= 0; i--) { + const newObj = {}; + newObj[objectArray[i]] = current; + x[objectArray[i]] = newObj; + current = x[objectArray[i]]; + } + return current; +} + +async function buildPattern(format, schemas) { + const { sourceName, schema } = await getSchema(schemas); + + let pattern = init( + sourceName, + schema.components.schemas.AWSEvent["x-amazon-events-detail-type"] + ); + + let currentObject = schema.components.schemas.AWSEvent; + let history = [JSON.parse(JSON.stringify(pattern))]; + let objectArray = []; + while (true) { + const { property, chosenProp } = await inputUtil.getProperty( + currentObject, + objectArray + ); + if (property.id === inputUtil.BACK) { + reset(); + continue; + } + if (property.id === inputUtil.UNDO) { + const lastItem = undo(history); + if (lastItem) { + pattern = Object.assign({}, lastItem); + } + objectArray = []; + outputPattern(pattern, format); + continue; + } + if (property.id === inputUtil.DONE) { + outputPattern(pattern, format); + break; + } + console.log(chosenProp); + const path = chosenProp.$ref; + if (path) { + // If property points at reference, go to reference in schema and continue + currentObject = findCurrent(path, schema); + if (currentObject.properties) { + continue; + } + } + + let answer = await inputUtil.getPropertyValue(chosenProp, property); + + let current = buildSegment(answer, objectArray); + + pattern = deepMerge(pattern, current); + outputPattern(pattern, format); + history.push(Object.assign({}, pattern)); + reset(); + } + return pattern; + + function undo(history) { + if (history.length <= 1) { + return null; + } + history.pop(); + return history[history.length - 1]; + } + + function reset() { + objectArray = []; + currentObject = schema.components.schemas.AWSEvent; + } +} + +async function buildInputTransformer(format, schemas) { + const { sourceName, schema } = await getSchema(schemas); + + let currentObject = schema.components.schemas.AWSEvent; + + let objectArray = []; + const output = { + InputPathsMap: {}, + InputTemplate: {}, + }; + while (true) { + const { property, chosenProp } = await inputUtil.getProperty( + currentObject, + objectArray + ); + if (property.id === inputUtil.BACK) { + reset(); + continue; + } + if (property.id === inputUtil.DONE) { + outputPattern(output, format); + process.exit(0); + } + + const path = chosenProp.$ref; + if (path) { + // If property points at reference, go to reference in schema and continue + currentObject = findCurrent(path, schema); + continue; + } + + const key = await inputUtil.text( + "Key name", + objectArray[objectArray.length - 1].replace(/-(\w)/g, ($, $1) => + $1.toUpperCase() + ) + ); + + output.InputPathsMap[key] = `$.${objectArray.join(".")}`; + output.InputTemplate = `{${Object.keys(output.InputPathsMap) + .map((p) => `"${p}": <${p}>`) + .join(", ")}}`; + + outputPattern(output, format); + reset(); + } + function reset() { + objectArray = []; + currentObject = schema.components.schemas.AWSEvent; + } +} + +function outputPattern(output, format) { + console.log("Generated output:"); + if (!format || format === "json") { + console.log(JSON.stringify(output, null, 2)); + } else { + console.log(YAML.stringify(output, null, 2)); + } +} + +async function browseEvents(format, schemas, eventbridge) { + while (true) { + const { targets } = await getTargets(schemas); + if (targets.length) { + while (true) { + console.log("CTRL+C to exit"); + const target = await inputUtil.selectFrom( + targets, + "Select target for more info" + ); + + if (target === inputUtil.BACK) { + break; + } + + let details = [{ name: "EventPattern", value: target.pattern }]; + for (const key of Object.keys(target.target)) { + details.push({ name: key, value: target.target[key] }); + } + details.push(inputUtil.CONSUME_LOCALLY); + const detail = await inputUtil.selectFrom( + details, + "Select property for more info" + ); + if (detail === inputUtil.BACK) { + continue; + } + if (detail === inputUtil.CONSUME_LOCALLY) { + await localIntegration.createLocalConsumer(target); + } + + console.log("\n" + JSON.stringify(detail, null, 2) + "\n"); + } + } else { + console.log("No subscribers found"); + } + } +} + +async function getTargets(schemas) { + const { schema, sourceName } = await getSchema(schemas); + const AWS = require("aws-sdk"); + const evb = new AWS.EventBridge(); + const eventBusName = await inputUtil.getEventBusName(evb); + const targets = []; + const resp = await evb + .listRules({ EventBusName: eventBusName, Limit: 100 }) + .promise(); + for (const rule of resp.Rules) { + if (!rule.EventPattern) { + continue; + } + const pattern = JSON.parse(rule.EventPattern); + if ( + pattern.source == sourceName && + pattern["detail-type"] == + schema.components.schemas.AWSEvent["x-amazon-events-detail-type"] + ) { + const targetResponse = await evb + .listTargetsByRule({ + Rule: rule.Name, + EventBusName: eventBusName, + }) + .promise(); + for (const target of targetResponse.Targets) { + const arnSplit = target.Arn.split(":"); + const service = arnSplit[2]; + const name = arnSplit[arnSplit.length - 1]; + targets.push({ + name: `${service}: ${name}`, + value: { pattern, target }, + }); + } + } + } + return { schema, targets }; +} + +async function getSchema(schemas) { + const registry = await inputUtil.getRegistry(schemas); + const schemaResponse = await schemas + .listSchemas({ RegistryName: registry.id }) + .promise(); + const sourceName = await inputUtil.getSourceName(schemaResponse); + const detailTypeName = await inputUtil.getDetailTypeName( + schemaResponse, + sourceName + ); + const schemaName = `${sourceName}@${detailTypeName}`.replace(/\//g, "-"); + const describeSchemaResponse = await schemas + .describeSchema({ RegistryName: registry.id, SchemaName: schemaName }) + .promise(); + const schema = JSON.parse(describeSchemaResponse.Content); + return { sourceName, schema }; +} + +function findCurrent(path, schema) { + const pathArray = path.split("/"); + pathArray.shift(); // Remove leading # + let current = schema; + for (var node of pathArray) { + current = current[node]; + } + return current; +} + +module.exports = { + init, + deepMerge, + buildSegment, + buildPattern, + buildInputTransformer, + browseEvents, +}; diff --git a/src/template-parser.js b/src/template-parser.js new file mode 100644 index 0000000..6f48cf1 --- /dev/null +++ b/src/template-parser.js @@ -0,0 +1,93 @@ +const fs = require("fs"); +const YAML = require("./yaml-wrapper"); +const AWS = require("aws-sdk"); +const inputUtil = require("./input-util"); +const patternBuilder = require("./pattern-builder"); + +let template; +let format; +let templatePath; +function load(filePath) { + templatePath = filePath; + try { + const templateFile = fs.readFileSync(filePath); + + try { + template = JSON.parse(templateFile.toString()); + format = "json"; + return; + } catch (err) {} + try { + template = YAML.parse(templateFile.toString()); + format = "yaml"; + return; + } catch (err) { + console.log(err.message); + } + } catch (err) { + console.log( + `Could not find ${filePath}. Will write pattern to stdout. Use -t ` + ); + } +} + +function getFormattedResourceList(template) { + return Object.keys(template.Resources) + .map((p) => { + return `[${template.Resources[p].Type}] ${p}`; + }) + .sort(); +} + +function getLambdaFunctions() { + return Object.keys(template.Resources) + .filter((p) => template.Resources[p].Type === "AWS::Serverless::Function") + .sort(); +} + +function getEventRules() { + return Object.keys(template.Resources) + .filter((p) => template.Resources[p].Type === "AWS::Events::Rule") + .sort(); +} + +async function injectPattern(pattern) { + const choices = []; + const resources = [...getLambdaFunctions(), ...getEventRules()]; + for (const key of resources) { + choices.push({ + name: key, + value: { name: key, value: template.Resources[key] }, + }); + } + + const resource = await inputUtil.selectFrom(choices, "Add pattern to", true); + if (resource.value.Type === "AWS::Serverless::Function") { + const eventName = await inputUtil.text("Event name", "MyEvent"); + const eventBus = await inputUtil.getEventBusName(new AWS.EventBridge()); + if (!resource.value.Properties.Events) { + resource.value.Properties.Events = {}; + } + resource.value.Properties.Events[eventName] = { + Type: "EventBridgeRule", + Properties: { + EventBusName: eventBus, + Pattern: pattern, + }, + }; + template.Resources[resource.name] = resource.value; + fs.writeFileSync( + templatePath, + format === "json" + ? JSON.stringify(template, null, 2) + : YAML.stringify(template) + ); + } +} + +module.exports = { + getFormattedResourceList, + getLambdaFunctions, + load, + injectPattern, +}; diff --git a/src/yaml-wrapper.js b/src/yaml-wrapper.js new file mode 100644 index 0000000..5002e84 --- /dev/null +++ b/src/yaml-wrapper.js @@ -0,0 +1,14 @@ +const yamlCfn = require("yaml-cfn"); + +function parse(str) { + return yamlCfn.yamlParse(str); +} + +function stringify(obj) { + return yamlCfn.yamlDump(obj).replace(/!<(.+?)>/g, "$1"); +} + +module.exports = { + parse, + stringify +};