@risk-labs/serverless-orchestration 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +661 -0
- package/README.md +19 -0
- package/index.js +1 -0
- package/package.json +49 -0
- package/src/ServerlessHub.js +700 -0
- package/src/ServerlessSpoke.js +130 -0
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @notice This script enables serverless functions to execute any arbitrary command from the UMA/Across Docker container.
|
|
3
|
+
* This can be run on a local machine, within GCP cloud run or GCP cloud function environments. Cloud Run provides a
|
|
4
|
+
* privileged REST endpoint that can be called to spin up a Docker container. This endpoint is expected to respond on
|
|
5
|
+
* PORT. Upon receiving a request, this script executes a child process and responds to the REST query with the output
|
|
6
|
+
* of the process execution. The REST query sent to the API is expected to be a POST with a body formatted as:
|
|
7
|
+
* {"serverlessCommand":<some-command-to-run>, environmentVariables: <env-variable-object>}
|
|
8
|
+
* the some-command-to-run is any execution process within the UMA/Across docker container. For example to run the monitor bot
|
|
9
|
+
* this could be set to: { "serverlessCommand":"yarn --silent monitors --network mainnet_mnemonic" }. `environmentVariables` is
|
|
10
|
+
* optional. If included the child process will have additional parameters appended with these params.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const express = require("express");
|
|
14
|
+
const spoke = express();
|
|
15
|
+
spoke.use(express.json()); // Enables json to be parsed by the express process.
|
|
16
|
+
const spawn = require("child_process").spawn;
|
|
17
|
+
|
|
18
|
+
const { delay, createNewLogger } = require("@risk-labs/logger");
|
|
19
|
+
|
|
20
|
+
let customLogger;
|
|
21
|
+
|
|
22
|
+
const waitForLoggerDelay = process.env.WAIT_FOR_LOGGER_DELAY || 5;
|
|
23
|
+
|
|
24
|
+
spoke.post("/", async (req, res) => {
|
|
25
|
+
// Use a custom logger if provided. Otherwise, initialize a local logger with a run identifier if passed from the Hub.
|
|
26
|
+
// Note: no reason to put this into the try-catch since a logger is required to throw the error.
|
|
27
|
+
const logger =
|
|
28
|
+
customLogger || createNewLogger(undefined, undefined, undefined, req.body?.environmentVariables?.RUN_IDENTIFIER);
|
|
29
|
+
try {
|
|
30
|
+
logger.debug({
|
|
31
|
+
at: "ServerlessSpoke",
|
|
32
|
+
message: "Executing serverless spoke call",
|
|
33
|
+
childProcessIdentifier: _getChildProcessIdentifier(req),
|
|
34
|
+
reqBody: req.body,
|
|
35
|
+
});
|
|
36
|
+
if (!req.body.serverlessCommand) {
|
|
37
|
+
throw new Error("Missing serverlessCommand in json body! At least this param is needed to run the spoke");
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Iterate over the provided environment variables and ensure that they are all strings. This enables json configs
|
|
41
|
+
// to be passed in the req body and then set as environment variables in the child_process as a string
|
|
42
|
+
let processedEnvironmentVariables = {};
|
|
43
|
+
|
|
44
|
+
if (req.body.environmentVariables) {
|
|
45
|
+
Object.keys(req.body.environmentVariables).forEach((key) => {
|
|
46
|
+
// All env variables must be a string. If they are not a string (int, object ect) convert them to a string.
|
|
47
|
+
processedEnvironmentVariables[key] =
|
|
48
|
+
typeof req.body.environmentVariables[key] == "string"
|
|
49
|
+
? req.body.environmentVariables[key]
|
|
50
|
+
: JSON.stringify(req.body.environmentVariables[key]);
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
await _execShellCommand(req.body.serverlessCommand, processedEnvironmentVariables, req.body.strategyRunnerSpoke);
|
|
55
|
+
|
|
56
|
+
logger.debug({
|
|
57
|
+
at: "ServerlessSpoke",
|
|
58
|
+
message: "Process exited with no error",
|
|
59
|
+
childProcessIdentifier: _getChildProcessIdentifier(req),
|
|
60
|
+
});
|
|
61
|
+
await delay(waitForLoggerDelay); // Wait a few seconds to be sure the the winston logs are processed upstream.
|
|
62
|
+
|
|
63
|
+
res.status(200).send({
|
|
64
|
+
message: "Process exited with no error",
|
|
65
|
+
childProcessIdentifier: _getChildProcessIdentifier(req),
|
|
66
|
+
success: true,
|
|
67
|
+
});
|
|
68
|
+
} catch (execResponse) {
|
|
69
|
+
// If there is an error, send a debug log to the winston transport to capture in GCP. We dont want to trigger a
|
|
70
|
+
// `logger.error` here as this will be dealt with one layer up in the Hub implementation.
|
|
71
|
+
logger.debug({
|
|
72
|
+
at: "ServerlessSpoke",
|
|
73
|
+
message: "Process exited with error 🚨",
|
|
74
|
+
childProcessIdentifier: _getChildProcessIdentifier(req),
|
|
75
|
+
jsonBody: req.body,
|
|
76
|
+
error: execResponse instanceof Error ? execResponse.message : execResponse,
|
|
77
|
+
});
|
|
78
|
+
await delay(waitForLoggerDelay); // Wait a few seconds to be sure the the winston logs are processed upstream.
|
|
79
|
+
res.status(500).send({
|
|
80
|
+
message: "Process exited with error",
|
|
81
|
+
childProcessIdentifier: _getChildProcessIdentifier(req),
|
|
82
|
+
error: execResponse instanceof Error ? execResponse.message : execResponse,
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
function _execShellCommand(cmd, inputEnv) {
|
|
88
|
+
return new Promise((resolve, reject) => {
|
|
89
|
+
const child = spawn(cmd, [], { env: { ...process.env, ...inputEnv }, stdio: "pipe", shell: true });
|
|
90
|
+
|
|
91
|
+
// Wait for the process to exit to resolve the promise.
|
|
92
|
+
child.on("exit", (code, signal) => {
|
|
93
|
+
if (code === 0) {
|
|
94
|
+
resolve();
|
|
95
|
+
} else if (code !== null) {
|
|
96
|
+
// Process exited on its own with a non-zero exit code.
|
|
97
|
+
reject(new Error(`Process exited with code ${code}`));
|
|
98
|
+
} else {
|
|
99
|
+
// Process exited because of a signal.
|
|
100
|
+
reject(new Error(`Process exited with signal ${signal}`));
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
// Pipe the stdout and stderr to the parent process.
|
|
105
|
+
child.stdout.pipe(process.stdout);
|
|
106
|
+
child.stderr.pipe(process.stderr);
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function _getChildProcessIdentifier(req) {
|
|
111
|
+
if (!req.body.environmentVariables) return null;
|
|
112
|
+
return req.body.environmentVariables.BOT_IDENTIFIER || null;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Start the spoke's async listening process. Enables injection of a logging instance & port for testing.
|
|
116
|
+
async function Poll(_customLogger, port = 8080) {
|
|
117
|
+
customLogger = _customLogger;
|
|
118
|
+
// Use custom logger if passed in. Otherwise, create a local logger.
|
|
119
|
+
const logger = customLogger || createNewLogger();
|
|
120
|
+
return spoke.listen(port, () => {
|
|
121
|
+
logger.debug({ at: "ServerlessSpoke", message: "serverless spoke initialized", port, env: process.env });
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
// If called directly by node, start the Poll process. If imported as a module then do nothing.
|
|
125
|
+
if (require.main === module) {
|
|
126
|
+
Poll(null, process.env.PORT).then(() => {}); // Use the default winston logger & env port.
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
spoke.Poll = Poll;
|
|
130
|
+
module.exports = spoke;
|