diff --git a/.gitignore b/.gitignore index ea822ca..b8d171b 100644 --- a/.gitignore +++ b/.gitignore @@ -61,4 +61,5 @@ typings/ .next # Vscode folders -.vscode +.vscode/ +.idea/ diff --git a/README.md b/README.md index 8e35c0e..9c56349 100644 --- a/README.md +++ b/README.md @@ -62,6 +62,10 @@ custom: env: GOOS: 'linux' # Default compile OS CGO_ENABLED: '0' # By default CGO is disabled + + # Bash commands to execute before compilation + # These commands will be executed once before all compilations + beforeBuild: [] ``` ### How does it work? @@ -93,6 +97,20 @@ functions: handler: test/main.go ``` +### How to execute commands before compilation? + +You can define bash commands to be executed before any compilation action using the `beforeBuild` configuration: + +```yaml +custom: + go: + beforeBuild: + - 'go generate ./...' + - 'go mod tidy' +``` + +These commands will be executed once before all compilations, not for each function individually. This applies whether you're using `serverless deploy`, `serverless deploy function`, or `serverless invoke local`. + ## Caveats This implementation doesn't allow to add any other files to the lambda artifact apart from the binary. To address this problem you should diff --git a/index.js b/index.js index 4f9669b..3c6a677 100644 --- a/index.js +++ b/index.js @@ -31,6 +31,7 @@ export default class Go { cmd: 'go build -ldflags="-s -w"', monorepo: false, concurrency: 5, + beforeBuild: [], }; this.config = this.getConfig(); @@ -39,6 +40,7 @@ export default class Go { "before:deploy:function:packageFunction": this.compileFunction.bind(this), "before:package:createDeploymentArtifacts": this.compileFunctions.bind(this), + // Because of https://github.com/serverless/serverless/blob/master/lib/plugins/aws/invokeLocal/index.js#L361 // plugin needs to compile a function and then ignore packaging. "before:invoke:local:invoke": this.compileToInvoke.bind(this), @@ -60,7 +62,7 @@ export default class Go { } /** - * Execute single function compilation and package. + * Execute a single function compilation and package. */ async compileFunction() { const name = this.options.function; @@ -69,6 +71,10 @@ export default class Go { this.logInfo(`Starting compilation...`); const timeStart = process.hrtime(); + + // Execute beforeBuild commands once before compilation + await this.execBeforeBuildCommands(this.config.baseDir, this.config.env); + await this.compile(name, func); const timeEnd = process.hrtime(timeStart); @@ -85,6 +91,9 @@ export default class Go { const timeStart = process.hrtime(); + // Execute beforeBuild commands once before all compilations + await this.execBeforeBuildCommands(this.config.baseDir, this.config.env); + this.logDebug(`using concurrency limit ${this.config.concurrency}...`); const limit = plimit(this.config.concurrency); @@ -102,8 +111,8 @@ export default class Go { } /** - * Execute single function compilation and do not create the final artifact. - * This is used when `invoke` command is performed. + * Execute a single function compilation and do not create the final artifact. + * This is used when the ` invoke ` command is performed. */ async compileToInvoke() { const name = this.options.function; @@ -112,6 +121,10 @@ export default class Go { this.logInfo(`Starting compilation...`); const timeStart = process.hrtime(); + + // Execute beforeBuild commands once before compilation + await this.execBeforeBuildCommands(this.config.baseDir, this.config.env); + await this.compile(name, func, false); const timeEnd = process.hrtime(timeStart); @@ -136,7 +149,7 @@ export default class Go { return; } - if (arch == "arm64") { + if (arch === "arm64") { config.env["GOARCH"] = "arm64"; } @@ -187,6 +200,34 @@ export default class Go { } } + /** + * Execute bash commands from beforeBuild configuration. + * + * @param {string} cwd Working directory + * @param {Object} env Set of environment variables + */ + async execBeforeBuildCommands(cwd, env) { + if (!this.config.beforeBuild || !this.config.beforeBuild.length) { + return; + } + + const execOpts = { cwd: cwd, env: { ...process.env, ...env } }; + + for (const cmd of this.config.beforeBuild) { + this.logDebug(`Executing beforeBuild command: ${cmd}`); + try { + await this.exec(cmd, execOpts); + } catch (e) { + this.logError( + `Error executing beforeBuild command (cwd: ${cwd}): ${e.message}`, + ); + throw new Error( + `error executing beforeBuild command '${cmd}' (cwd: ${cwd})`, + ); + } + } + } + /** * Execute compilation command from collected configuration. * @@ -212,11 +253,11 @@ export default class Go { } /** - * Create package to deploy lambda + * Packages a bootstrap file into a zip archive and updates the service function's package configuration. * - * @param {string} name Name of the function config - * @param {Object} baseConfig Configuration object - * @param {string} binPath Path to generated binary + * @param {string} name - The name of the serverless function to update the package for. + * @param {string} binPath - The path to the bootstrap binary file. + * @return {void} This method does not return any value. */ packageBootstrap(name, binPath) { this.zip.addFile("bootstrap", this.readFileSync(binPath), "", 0o755); @@ -231,8 +272,11 @@ export default class Go { } /** - * Merge default con - * @returns {Object} + * Retrieves and consolidates the configuration by merging the default configuration + * with user-defined custom configurations and environment variables. + * + * @return {Object} The final configuration object containing merged settings, + * environment variables as strings, and concurrency settings. */ getConfig() { let config = { ...this.defaultConfig }; diff --git a/index.test.js b/index.test.js index ab5d94f..036a35a 100644 --- a/index.test.js +++ b/index.test.js @@ -400,4 +400,157 @@ describe("Go Plugin", () => { delete process.env["SP_GO_CONCURRENCY"]; }); + + it("Should execute beforeBuild commands once before all compilations", async () => { + config.service + .setCustom({ + go: { + beforeBuild: ["echo 'before build command'", "go mod tidy"], + }, + }) + .setFunctions({ + testFunc1: { + name: "testFunc1", + runtime: "provided.al2", + handler: "functions/func1/main.go", + }, + testFunc2: { + name: "testFunc2", + runtime: "provided.al2", + handler: "functions/func2/main.go", + }, + }); + + const plugin = new Go(config); + plugin.exec = execStub; + plugin.readFileSync = readFileSyncStub; + plugin.zip = admZipStub; + + // when + await plugin.compileFunctions(); + + // then + // Verify beforeBuild commands were executed once + expect(execStub).to.have.been.calledWith("echo 'before build command'", { + cwd: ".", + env: { ...process.env, ...{ CGO_ENABLED: "0", GOOS: "linux" } }, + }); + expect(execStub).to.have.been.calledWith("go mod tidy", { + cwd: ".", + env: { ...process.env, ...{ CGO_ENABLED: "0", GOOS: "linux" } }, + }); + + // Verify compilation commands were executed for each function + expect(execStub).to.have.been.calledWith( + `go build -ldflags="-s -w" -o .bin/testFunc1 functions/func1/main.go`, + { + cwd: ".", + env: { ...process.env, ...{ CGO_ENABLED: "0", GOOS: "linux" } }, + }, + ); + expect(execStub).to.have.been.calledWith( + `go build -ldflags="-s -w" -o .bin/testFunc2 functions/func2/main.go`, + { + cwd: ".", + env: { ...process.env, ...{ CGO_ENABLED: "0", GOOS: "linux" } }, + }, + ); + + // Total calls should be 4: 2 beforeBuild commands + 2 compilation commands + expect(execStub.callCount).to.equal(4); + }); + + it("Should execute beforeBuild commands once for compileFunction", async () => { + config.service + .setCustom({ + go: { + beforeBuild: ["echo 'before build command'", "go mod tidy"], + }, + }) + .setFunctions({ + testFunc1: { + name: "testFunc1", + runtime: "provided.al2", + handler: "functions/func1/main.go", + }, + }); + + const plugin = new Go(config, { function: "testFunc1" }); + plugin.exec = execStub; + plugin.readFileSync = readFileSyncStub; + plugin.zip = admZipStub; + + // when + await plugin.compileFunction(); + + // then + // Verify beforeBuild commands were executed once + expect(execStub).to.have.been.calledWith("echo 'before build command'", { + cwd: ".", + env: { ...process.env, ...{ CGO_ENABLED: "0", GOOS: "linux" } }, + }); + expect(execStub).to.have.been.calledWith("go mod tidy", { + cwd: ".", + env: { ...process.env, ...{ CGO_ENABLED: "0", GOOS: "linux" } }, + }); + + // Verify the compilation command was executed + expect(execStub).to.have.been.calledWith( + `go build -ldflags="-s -w" -o .bin/testFunc1 functions/func1/main.go`, + { + cwd: ".", + env: { ...process.env, ...{ CGO_ENABLED: "0", GOOS: "linux" } }, + }, + ); + + // Total calls should be 3: 2 beforeBuild commands + 1 compilation command + expect(execStub.callCount).to.equal(3); + }); + + it("Should execute beforeBuild commands once for compileToInvoke", async () => { + config.service + .setCustom({ + go: { + beforeBuild: ["echo 'before build command'", "go mod tidy"], + }, + }) + .setFunctions({ + testFunc1: { + name: "testFunc1", + runtime: "provided.al2", + handler: "functions/func1/main.go", + }, + }); + + const plugin = new Go(config, { function: "testFunc1" }); + plugin.exec = execStub; + plugin.readFileSync = readFileSyncStub; + plugin.zip = admZipStub; + + // when + await plugin.compileToInvoke(); + + // then + // Verify beforeBuild commands were executed once + expect(execStub).to.have.been.calledWith("echo 'before build command'", { + cwd: ".", + env: { ...process.env, ...{ CGO_ENABLED: "0", GOOS: "linux" } }, + }); + expect(execStub).to.have.been.calledWith("go mod tidy", { + cwd: ".", + env: { ...process.env, ...{ CGO_ENABLED: "0", GOOS: "linux" } }, + }); + + // Verify compilation command was executed + expect(execStub).to.have.been.calledWith( + `go build -ldflags="-s -w" -o .bin/testFunc1 functions/func1/main.go`, + { + cwd: ".", + env: { ...process.env, ...{ CGO_ENABLED: "0", GOOS: "linux" } }, + }, + ); + + // Total calls should be 3: 2 beforeBuild commands + 1 compilation command + expect(execStub.callCount).to.equal(3); + }); }); diff --git a/package-lock.json b/package-lock.json index a4496d7..b30a003 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "serverless-plugin-go", - "version": "0.0.2", + "version": "0.0.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "serverless-plugin-go", - "version": "0.0.2", + "version": "0.0.3", "license": "MIT", "dependencies": { "adm-zip": "^0.5.10", @@ -495,10 +495,11 @@ } }, "node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "dev": true, + "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -686,10 +687,11 @@ "dev": true }, "node_modules/cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "dev": true, + "license": "MIT", "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", @@ -1595,10 +1597,11 @@ } }, "node_modules/glob/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "dev": true, + "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" } @@ -2355,10 +2358,11 @@ } }, "node_modules/mocha/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "dev": true, + "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" } @@ -2645,10 +2649,11 @@ "dev": true }, "node_modules/path-to-regexp": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.2.2.tgz", - "integrity": "sha512-GQX3SSMokngb36+whdpRXE+3f9V8UzyAorlYvOGx87ufGHehNTn5lCxrKtLyZ4Yl/wEKnNnr98ZzOwwDZV5ogw==", - "dev": true + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.3.0.tgz", + "integrity": "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==", + "dev": true, + "license": "MIT" }, "node_modules/pathval": { "version": "1.1.1",