Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -61,4 +61,5 @@ typings/
.next

# Vscode folders
.vscode
.vscode/
.idea/
18 changes: 18 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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?
Expand Down Expand Up @@ -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
Expand Down
64 changes: 54 additions & 10 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ export default class Go {
cmd: 'go build -ldflags="-s -w"',
monorepo: false,
concurrency: 5,
beforeBuild: [],
};

this.config = this.getConfig();
Expand All @@ -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),
Expand All @@ -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;
Expand All @@ -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);

Expand All @@ -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);
Expand All @@ -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;
Expand All @@ -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);

Expand All @@ -136,7 +149,7 @@ export default class Go {
return;
}

if (arch == "arm64") {
if (arch === "arm64") {
config.env["GOARCH"] = "arm64";
}

Expand Down Expand Up @@ -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.
*
Expand All @@ -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);
Expand All @@ -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 };
Expand Down
153 changes: 153 additions & 0 deletions index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
});
Loading