Skip to content

Commit

Permalink
StackToolbox#41 Add Layers support by makefile build method
Browse files Browse the repository at this point in the history
  • Loading branch information
baxneo committed Aug 21, 2021
1 parent 7276a4a commit 78800bb
Show file tree
Hide file tree
Showing 4 changed files with 197 additions and 0 deletions.
4 changes: 4 additions & 0 deletions __mocks__/child_process.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
const child_process = jest.genMockFromModule("child_process") as any;

child_process.exec = jest.fn((cmd, cb) => cb({ code: 0 }));
module.exports = child_process;
4 changes: 4 additions & 0 deletions __mocks__/path.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,4 +46,8 @@ path.relative = (folder: string, name: string): string => {
throw new Error(`Unknown relative ${folder}, ${name}`);
};

path.resolve = (folder: string): string => {
return process.cwd() + "/" + folder;
};

module.exports = path;
96 changes: 96 additions & 0 deletions src/__tests__/integration.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import SamPlugin from "../index";
import fs from "fs";
import path from "path";
import child_process from "child_process";

jest.mock("child_process");
jest.mock("fs");
jest.mock("path");

Expand All @@ -20,6 +22,44 @@ Resources:
CodeUri: src/my-lambda
Handler: app.handler
`;
const samTemplateWithLayer = `
AWSTemplateFormatVersion: "2010-09-09"
Transform: AWS::Serverless-2016-10-31
Globals:
Function:
Runtime: nodejs10.x
Resources:
MyLambda:
Type: AWS::Serverless::Function
Properties:
CodeUri: src/my-lambda
Handler: app.handler
LayerSharp:
Type: AWS::Serverless::LayerVersion
Metadata:
BuildMethod: makefile
Properties:
LayerName: layer-sharp
Description: Package sharp
ContentUri: layers/sharp
CompatibleRuntimes:
- nodejs14.x
RetentionPolicy: Retain
LayerSharp2:
Type: AWS::Serverless::LayerVersion
Metadata:
BuildMethod: makefile
Properties:
LayerName: layer-sharp2
Description: Package sharp2
ContentUri: layers/sharp2
CompatibleRuntimes:
- nodejs14.x
RetentionPolicy: Retain
`;

test("Happy path with default constructor works", () => {
const plugin = new SamPlugin();
Expand Down Expand Up @@ -50,6 +90,7 @@ test("Happy path with default constructor works", () => {
tap: (n: string, f: (_compilation: any) => void) => {
afterEmit = f;
},
tapPromise: async (n: string, f: (_compilation: any) => Promise<void>) => {},
},
},
});
Expand Down Expand Up @@ -89,6 +130,7 @@ test("Happy path with empty options in the constructor works", () => {
tap: (n: string, f: (_compilation: any) => void) => {
afterEmit = f;
},
tapPromise: async (n: string, f: (_compilation: any) => Promise<void>) => {},
},
},
});
Expand Down Expand Up @@ -132,6 +174,7 @@ test("Happy path with empty options in the constructor works and an existing .vs
tap: (n: string, f: (_compilation: any) => void) => {
afterEmit = f;
},
tapPromise: async (n: string, f: (_compilation: any) => Promise<void>) => {},
},
},
});
Expand Down Expand Up @@ -175,6 +218,7 @@ test("Happy path with VS Code debugging disabled", () => {
tap: (n: string, f: (_compilation: any) => void) => {
afterEmit = f;
},
tapPromise: async (n: string, f: (_compilation: any) => Promise<void>) => {},
},
},
});
Expand Down Expand Up @@ -218,6 +262,7 @@ test("Happy path with multiple projects works", () => {
tap: (n: string, f: (_compilation: any) => void) => {
afterEmit = f;
},
tapPromise: async (n: string, f: (_compilation: any) => Promise<void>) => {},
},
},
});
Expand Down Expand Up @@ -260,6 +305,7 @@ test("Happy path with multiple projects and different template names works", ()
tap: (n: string, f: (_compilation: any) => void) => {
afterEmit = f;
},
tapPromise: async (n: string, f: (_compilation: any) => Promise<void>) => {},
},
},
});
Expand All @@ -286,6 +332,7 @@ test("Calling apply() before entry() throws an error", () => {
tap: (n: string, f: (_compilation: any) => void) => {
afterEmit = f;
},
tapPromise: async (n: string, f: (_compilation: any) => Promise<void>) => {},
},
},
});
Expand Down Expand Up @@ -395,6 +442,7 @@ test("Happy path with an output file specified", () => {
tap: (n: string, f: (_compilation: any) => void) => {
afterEmit = f;
},
tapPromise: async (n: string, f: (_compilation: any) => Promise<void>) => {},
},
},
});
Expand All @@ -404,3 +452,51 @@ test("Happy path with an output file specified", () => {
// @ts-ignore
expect({ entryPoints, files: fs.__getMockWrittenFiles() }).toMatchSnapshot();
});

test("Happy exec make template with layers", async () => {
const plugin = new SamPlugin({ outFile: "index" });

// @ts-ignore
fs.__clearMocks();
// @ts-ignore
fs.__setMockDirs(["."]);
// @ts-ignore
fs.__setMockFiles({ "./template.yaml": samTemplateWithLayer });

// @ts-ignore
path.__clearMocks();
// @ts-ignore
path.__setMockBasenames({ "./template.yaml": "template.yaml" });
// @ts-ignore
path.__setMockDirnames({ "./template.yaml": "." });
// @ts-ignore
path.__setMockRelatives({ ".#.": "" });

const entryPoints = plugin.entry();

// let afterEmit: (_compilation: any) => void;
let afterEmitPromise: (_compilation: any) => Promise<void>;

plugin.apply({
hooks: {
afterEmit: {
tap: (n: string, f: (_compilation: any) => void) => {
// afterEmit = f;
},
tapPromise: async (n: string, f: (_compilation: any) => Promise<void>) => {
afterEmitPromise = f;
},
},
},
});

const execMocked = child_process.exec as unknown as jest.Mock;
execMocked.mockClear();
// @ts-ignore
await afterEmitPromise(null);

expect(execMocked.mock.calls.length).toBe(2);
expect(execMocked.mock.calls[0][0]).toMatch(
/make -C ".\/layers\/sharp" ARTIFACTS_DIR="[^"]+\/\.aws-sam\/build\/LayerSharp" build-LayerSharp/
);
});
93 changes: 93 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import * as fs from "fs";
import * as path from "path";
import { exec } from "child_process";
import { schema } from "yaml-cfn";
import yaml from "js-yaml";

Expand Down Expand Up @@ -32,11 +33,20 @@ interface IEntryForResult {
samConfigs: SamConfig[];
}

interface ILayerConfig {
templateName: string;
resourceKey: string;
buildRoot: string;
contentDir: string;
buildMethod?: string;
}

class AwsSamPlugin {
private static defaultTemplates = ["template.yaml", "template.yml"];
private launchConfig: any;
private options: AwsSamPluginOptions;
private samConfigs: SamConfig[];
private layersConfigs: ILayerConfig[] = [];

constructor(options?: Partial<AwsSamPluginOptions>) {
this.options = {
Expand Down Expand Up @@ -252,6 +262,42 @@ class AwsSamPlugin {
templateName: projectTemplateName,
});
}

if (resource.Type === "AWS::Serverless::LayerVersion") {
const properties = resource.Properties;
if (!properties || typeof properties !== "object") {
throw new Error(`${resourceKey} is missing Properties`);
}

// Check we have a CodeUri
const contentUri = properties.ContentUri ?? defaultCodeUri;
if (!contentUri) {
throw new Error(`${resourceKey} is missing a CodeUri`);
}

const basePathPrefix = projectPath === "" ? "." : `./${projectPath}`;
const contentDir = `${basePathPrefix}/${contentUri}`;

const buildMethod = resource.Metadata?.BuildMethod;
if (buildMethod === "makefile") {
if (
!this.layersConfigs.find(
(e) =>
e.templateName === projectTemplateName && e.resourceKey === resourceKey && e.buildRoot === buildRoot
)
) {
this.layersConfigs.push({
templateName: projectTemplateName,
resourceKey,
buildRoot,
contentDir,
buildMethod,
});
}
} else {
throw new Error(`Unsupported layer BuildMethod '${buildMethod}'`);
}
}
}

return { entryPoints, launchConfigs, samConfigs };
Expand Down Expand Up @@ -317,6 +363,53 @@ class AwsSamPlugin {
}

public apply(compiler: any) {
compiler.hooks.afterEmit?.tapPromise(
"SamPlugin",
async (_compilation: any /* webpack.Compilation */): Promise<void> => {
if (!(this.samConfigs && this.launchConfig)) {
throw new Error("It looks like AwsSamPlugin.entry() was not called");
}

for (const layerConfig of this.layersConfigs) {
const { templateName, resourceKey, buildRoot, contentDir, buildMethod } = layerConfig;
if (buildMethod === "makefile") {
console.log("Start building layer %s#%s ... ", templateName, resourceKey);
const artifactsDir = `${buildRoot}/${resourceKey}`;
try {
fs.mkdirSync(buildRoot);
} catch (err) {
if (!(err?.code === "EEXIST")) throw err;
}
try {
fs.mkdirSync(artifactsDir);
} catch (err) {
if (!(err?.code === "EEXIST")) throw err;
}
const cmdLine = [
//
`make`,
`-C "${contentDir}"`,
`ARTIFACTS_DIR="${path.resolve(artifactsDir)}"`,
`build-${resourceKey}`,
].join(" ");
// console.info("MAKE %s cmdLine: %s", resourceKey, cmdLine);
try {
await new Promise((res, rej) => {
exec(cmdLine, (e) => (e?.code ? rej(e) : res(e)));
});
} catch (err) {
if (err.cmd) {
console.error(err.stdout);
console.error(err.stderr);
}
throw err;
}
} else {
throw new Error(`Unsupported layer BuildMethod '${buildMethod}'`);
}
}
}
);
compiler.hooks.afterEmit.tap("SamPlugin", (_compilation: any) => {
if (this.samConfigs && this.launchConfig) {
for (const samConfig of this.samConfigs) {
Expand Down

0 comments on commit 78800bb

Please sign in to comment.