Skip to content

Commit

Permalink
Python cdk (#26)
Browse files Browse the repository at this point in the history
* python cdk wip

* wip

* [samp local] python cdk support

---------

Co-authored-by: ljacobsson <[email protected]>
  • Loading branch information
ljacobsson and ljacobsson authored Sep 14, 2023
1 parent cc3e726 commit beb9bd1
Show file tree
Hide file tree
Showing 14 changed files with 320 additions and 84 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "samp-cli",
"version": "1.0.55",
"version": "1.0.56",
"description": "CLI tool for extended productivity with AWS Serverless Application Model (SAM)",
"main": "index.js",
"scripts": {
Expand Down
47 changes: 0 additions & 47 deletions src/commands/local/cdk-construct-finder.js

This file was deleted.

1 change: 1 addition & 0 deletions src/commands/local/lib/connect.js
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,7 @@ if (!fs.existsSync(".lambda-debug")) {
const stackName = targetConfig.stack_name;

template = parse("template", fs.readFileSync(findSAMTemplateFile('.')).toString());

stack = await cfnClient.send(new ListStackResourcesCommand({ StackName: stackName }));
let token = stack.NextToken;
if (token) {
Expand Down
5 changes: 4 additions & 1 deletion src/commands/local/lib/handler-finder.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ export function locatePythonHandler(template, obj, baseDir) {
const globals = template.Globals?.Function || {};
const handlerFolders = (obj.handler || globals.Handler).split('/');
const functionHandler = handlerFolders.pop();
baseDir += "/" + handlerFolders.join('/');
// remove folders if they don't exist on disk
handlerFolders.forEach((folder, index) => {
if (!fs.existsSync(`${process.cwd()}/${baseDir}${handlerFolders.slice(0, index + 1).join('/')}`)) {
Expand All @@ -44,11 +45,13 @@ export function locatePythonHandler(template, obj, baseDir) {
const handler = (obj.handler + '/' + functionHandler.split('.')[0]).replace(/\/\//g, '/');
const handlerMethod = functionHandler.split('.')[1];
let pyExt = ".py";
const module = `file://${`${process.cwd()}/${baseDir}${handler}${pyExt}`.replace(/\/\//g, '/')}`.replace('.samp-out/./', '.samp-out/');
const module = `file://${`${process.cwd()}/${baseDir}${handler}${pyExt}`.replace(/\/\//g, '/')}`.replace('.samp-out/./', '.samp-out/').replace('.samp-out/', '');

return { module, handlerMethod, runtime: obj.runtime || globals.Runtime || "python" };
}

export function locateHandler(template, obj, baseDir) {
console.log("LOCATE HANDLER", obj);
if (!obj.runtime || obj.runtime.startsWith("nodejs")) return locateJsHandler(template, obj, baseDir);
if (obj.runtime.startsWith("dotnet")) return locateDotnetHandler(template, obj, baseDir);
if (obj.runtime.startsWith("python")) return locatePythonHandler(template, obj, baseDir);
Expand Down
23 changes: 16 additions & 7 deletions src/commands/local/local.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ const path = require('path');
const inputUtil = require('../../shared/inputUtil');
const settingsUtil = require('../../shared/settingsUtil');
const glob = require('glob');
const { findConstructs } = require('./cdk-construct-finder');
const { fromSSO } = require('@aws-sdk/credential-provider-sso');
const { CloudFormationClient, ListStackResourcesCommand } = require('@aws-sdk/client-cloudformation');
const samConfigParser = require('../../shared/samConfigParser');
Expand All @@ -16,6 +15,11 @@ function setEnvVars(cmd) {
process.env.SAMP_REGION = cmd.region || process.env.AWS_REGION || process.env.AWS_DEFAULT_REGION || '';
process.env.SAMP_STACKNAME = process.env.SAMP_STACKNAME || cmd.stackName || '';
process.env.SAMP_CDK_STACK_PATH = cmd.construct || process.env.SAMP_CDK_STACK_PATH || '';

if (!process.env.SAMP_REGION) {
console.log("Please specify a region using --region or by setting the AWS_REGION or AWS_DEFAULT_REGION environment variable");
process.exit(1);
}
process.env.SAMP_TEMPLATE_PATH = cmd.template || process.env.SAMP_TEMPLATE_PATH || undefined;
}

Expand Down Expand Up @@ -120,7 +124,7 @@ function setupCDK_TS(initialised, cmd) {
print(data);
if (data.toString().includes("Watching for file changes") && !initialised) {

const cdkWrapper = exec(`node ${__dirname}/cdk-wrapper.js .samp-out/${cmd.construct.replace(".ts", "")}.js`, {});
const cdkWrapper = exec(`node ${__dirname}/runtime-support/${env.runtime}/cdk/cdk-wrapper.js .samp-out/${cmd.construct.replace(".ts", "")}.js`, {});
cdkWrapper.stdout.on('data', (data) => {
print(data);
});
Expand Down Expand Up @@ -160,11 +164,14 @@ async function setupDebug(cmd) {
let selectedFunctionsCsv = cmd.functions || targetConfig.selected_functions;
let construct;
if (env.iac === "cdk") {
const constructs = findConstructs();
constructs.push("Enter manually");
construct = await inputUtil.autocomplete("Which stack construct do you want to debug?", constructs);
if (construct === "Enter manually") {
construct = await inputUtil.text("Enter which stack construct do you want to debug?");
if (env.isNodeJS) {
const constructs = require(`./runtime-support/${env.runtime}/cdk/cdk-construct-finder`).findConstructs();
constructs.push("Enter manually");
construct = await inputUtil.autocomplete("Which stack construct do you want to debug?", constructs);
if (construct === "Enter manually") {
construct = await inputUtil.text("Enter which stack construct do you want to debug");
}
cmd.construct = construct;
}
const cdkTree = JSON.parse(fs.readFileSync("cdk.out/tree.json", "utf8"));
const stacks = Object.keys(cdkTree.tree.children).filter(c => c !== "Tree");
Expand All @@ -173,6 +180,8 @@ async function setupDebug(cmd) {
if (stack === "Enter manually") {
stack = await inputUtil.text("What's the name of the deployed stack?");
}
cmd.stackName = stack;
process.env.SAMP_STACKNAME = stack;
const regions = [
"ap-south-1",
"eu-north-1",
Expand Down
31 changes: 12 additions & 19 deletions src/commands/local/runtime-env-finder.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,30 +12,23 @@ function determineRuntime() {
const runtime = (template.Resources[firstFunction].Properties.Runtime || template.Globals?.Function?.Runtime).substring(0, 3);
switch (runtime) {
case "dot":
return { iac: "sam", functionLanguage: "dotnet" };
return { iac: "sam", functionLanguage: "dotnet", runtime: "dotnet" };
case "pyt":
return { iac: "sam", functionLanguage: "python" };
return { iac: "sam", functionLanguage: "python", runtime: "python" };
case "jav":
return { iac: "sam", functionLanguage: "java" };
return { iac: "sam", functionLanguage: "java", runtime: "java" };
case "nod": {
const isTs = fs.existsSync(path.join(process.cwd(), 'tsconfig.json'));
return { iac: "sam", functionLanguage: isTs ? "ts" : "js", runtime: "nodejs", isNodeJS: true };
}
}
}
if (fs.existsSync('cdk.json')) return { iac: "cdk", functionLanguage: "ts", isNodeJS: true };
if (fs.existsSync('tsconfig.json')) return { iac: "sam", functionLanguage: "ts", isNodeJS: true };;

// does a file ending -csproj exist in the current folder or subfolders?
const csprojFiles = [];
const walkSync = (dir, filelist = []) => {
fs.readdirSync(dir).forEach(file => {
const dirFile = path.join(dir, file);
try {
filelist = walkSync(dirFile, filelist);
} catch (err) {
if (err.code === 'ENOTDIR' || err.code === 'EBUSY') filelist = [...filelist, dirFile];
else throw err;
}
});
return filelist;
if (fs.existsSync('cdk.json')) {
const cdkJson = JSON.parse(fs.readFileSync('cdk.json', 'utf8'));
if (cdkJson.app.includes('python')) return { iac: "cdk", functionLanguage: "python", runtime: "python" };
return { iac: "cdk", functionLanguage: "ts", runtime: "nodejs", isNodeJS: true };
}
if (fs.existsSync('tsconfig.json')) return { iac: "sam", functionLanguage: "ts", runtime: "nodejs", isNodeJS: true };
walkSync(process.cwd()).forEach(file => {
if (file.endsWith('.csproj')) csprojFiles.push(file);
});
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
const { findFilesWithExtension } = require('../../../../../shared/fileFinder');
const fs = require('fs');

function findConstructs() {
const rootDir = '.';
const fileExtension = '.ts';
const searchString = /extends.*Stack/g;
const files = findFilesWithExtension(rootDir, fileExtension);
const matchedFiles = [];

files.forEach((file) => {
const fileContent = fs.readFileSync(file, 'utf8');

if (fileContent.match(searchString)) {
matchedFiles.push(file);
}
});

return matchedFiles;
}


module.exports = {
findConstructs
};
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ for (const file of getAllJsFiles(baseDir)) {
}
}


const TargetStack = require(`${process.cwd()}/${process.argv[2]}`);
const className = Object.keys(TargetStack)[0];

Expand All @@ -32,21 +31,21 @@ for (const key of Object.keys(resources)) {
const resource = JSON.parse(JSON.stringify(resources[key], getCircularReplacer()));
delete resource.node?._children?._children?._children?.stack;
const fingerprintOptions = findShallowestOccurrence(resource, 'fingerprintOptions').occurrence;

const entry = fingerprintOptions?.bundling?.relativeEntryPath || (fingerprintOptions?.path ? `${fingerprintOptions?.path}/` : null);

let logicalId = null;
let handler
if (entry) {
for (const fn of Object.keys(synthedTemplate.Resources)) {
const resource = synthedTemplate.Resources[fn];
if (resource.Type === "AWS::Lambda::Function") {
if (!resource.Properties?.Code?.S3Bucket) continue;
if (!resource.Properties?.Code?.S3Bucket) continue;
if (!resource.Properties?.Runtime.startsWith("node")) continue;
if (resource.Metadata?.["aws:asset:is-bundled"] === false) continue;
if (resource.Metadata?.['aws:cdk:path'].includes(`/${key}/`) && resource.Metadata?.['aws:cdk:path'].includes('/Resource')) {
if (resource.Metadata?.['aws:cdk:path'].includes(`/${key}/`) && resource.Metadata?.['aws:cdk:path'].includes('/Resource')) {
//get filename from entry

const entryFile = entry.substring(entry.lastIndexOf("/") + 1).split(".")[0];
logicalId = fn;
handler = `${entryFile}.${resource.Properties.Handler.split(".")[1]}`;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ function copyConfig(name, args) {
if (process.env.SAMP_TEMPLATE_PATH) {
task.command += ` --template ${process.env.SAMP_TEMPLATE_PATH}`;
}
task.command += ` --region ${process.env.SAMP_REGION}`;
let tasksJson;
if (fs.existsSync(`${pwd}/.vscode/tasks.json`)) {
tasksJson = commentJson.parse(fs.readFileSync(`${pwd}/.vscode/tasks.json`, "utf8"));
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
const { findFilesWithExtension } = require('../../../../../shared/fileFinder');
const fs = require('fs');

function findConstructs() {
const rootDir = '.';
const fileExtension = '.py';
const searchString = /from aws_cdk import/g;
const files = findFilesWithExtension(rootDir, fileExtension);
const matchedFiles = [];

files.forEach((file) => {
const fileContent = fs.readFileSync(file, 'utf8');

if (fileContent.match(searchString)) {
matchedFiles.push(file);
}
});

return matchedFiles;
}


module.exports = {
findConstructs
};
84 changes: 84 additions & 0 deletions src/commands/local/runtime-support/python/cdk/cdk-wrapper.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
const fs = require('fs');
const path = require('path');
const crypto = require('crypto');
const yamlDump = require('js-yaml').dump;
function calculateHash(filePath) {
const fileContent = fs.readFileSync(filePath);
return crypto.createHash('md5').update(fileContent).digest('hex');
}

function findMatchingSource(assetHash, sourceDirectories) {
for (const sourceDir of sourceDirectories) {
const sourceFiles = fs.readdirSync(sourceDir);
for (const sourceFile of sourceFiles) {
if (fs.statSync(path.join(sourceDir, sourceFile)).isDirectory()) {
const matchingSourceDir = findMatchingSource(assetHash, [path.join(sourceDir, sourceFile)]);
if (matchingSourceDir !== null) {
return matchingSourceDir;
}
} else {
const sourceFilePath = path.join(sourceDir, sourceFile);
const sourceHash = calculateHash(sourceFilePath);
if (sourceHash === assetHash) {
return sourceDir;
}
}
}
}
return null;
}

function createAssetSourceMap(assetDirectories, sourceDirectories) {

const assetSourceMap = {};

for (const assetDir of assetDirectories) {
const assetFiles = fs.readdirSync(assetDir);
for (const assetFile of assetFiles) {
const assetFilePath = path.join(assetDir, assetFile);
const assetHash = calculateHash(assetFilePath);
const matchingSource = findMatchingSource(assetHash, sourceDirectories);
if (matchingSource) {
const assetHashKey = path.basename(assetDir);
assetSourceMap[assetHashKey] = matchingSource;
}
}
}

return assetSourceMap;
}



const assetDirectories = fs.readdirSync('cdk.out')
.map(folder => path.join('cdk.out', folder))
.filter(folderPath => fs.statSync(folderPath).isDirectory());

const sourceDirectories = fs.readdirSync('.')
.map(handler => path.join('.', handler))
.filter(handlerPath => fs.statSync(handlerPath).isDirectory() && !handlerPath.startsWith('.') && !handlerPath.startsWith('__') && !handlerPath.startsWith('cdk.out'));
const assetSourceMap = createAssetSourceMap(assetDirectories, sourceDirectories);

const template = JSON.parse(fs.readFileSync('cdk.out/' + process.argv[2] + '.template.json', 'utf-8'));
// Get all AWS::Lambda::Function resources
const functions = Object.keys(template.Resources)
.filter(key => template.Resources[key].Type === "AWS::Lambda::Function")
const mockTemplate = {
Resources: {}
};

for (const asset of Object.keys(assetSourceMap)) {
for (const fn of functions) {
const resource = template.Resources[fn];
if (resource.Metadata['aws:asset:path'] === asset) {
const handlerSplit = resource.Properties.Handler.split('/');
mockTemplate.Resources[fn] = {
Type: "AWS::Serverless::Function",
Properties: { CodeUri: '.', Handler: `${assetSourceMap[asset]}/${handlerSplit[handlerSplit.length - 1]}`, Runtime: 'python' }
};
break;
}
}
}

fs.writeFileSync(".samp-out/mock-template.yaml", yamlDump(mockTemplate));
Loading

0 comments on commit beb9bd1

Please sign in to comment.