Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat/post start hook #5706

Open
wants to merge 11 commits into
base: development
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/node.js.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ jobs:

strategy:
matrix:
node-version: [20.x]
node-version: [20.x, 18.x]
# See supported Node.js release schedule at https://nodejs.org/en/about/releases/

steps:
Expand Down
5 changes: 3 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
## 5.3.1

- #5683 update badges
- #5684 auto switch light and dark mode logos
- #5686 Switch from Travis CI to Github Actions
- #5680 Fixed reserved keyword for ES6 Strict Mode when Bundling @juaneth
- #5683 update badges
- #5684 auto switch light and dark mode logos
- #5678 Bugfix/deploy ecosystem filename extension / esm module default ecosystem config name @TeleMediaCC
- #5660 Fix matching logic for logs from namespace when lines = 0 @bawjensen
- fix "vulnerabilities" in axios module

## 5.3.0

Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@
<picture>
<source
srcset="https://raw.githubusercontent.com/Unitech/pm2/master/pres/pm2-v4.png"
width=710px
media="(prefers-color-scheme: light)"
/>
<source
srcset="https://raw.githubusercontent.com/Unitech/pm2/development/pres/pm2-v4-dark-mode.png"
width=710px
media="(prefers-color-scheme: dark), (prefers-color-scheme: no-preference)"
/>
<img src="https://raw.githubusercontent.com/Unitech/pm2/master/pres/pm2-v4.png" />
Expand Down
2 changes: 1 addition & 1 deletion examples/cluster-http/http.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,5 @@ var server = http.createServer(function(req, res) {
res.writeHead(200);
res.end('hey');
}).listen(process.env.PORT || 8089, '0.0.0.0', function() {
console.log('App listening on port %d', server.address().port);
console.log('App listening on port 8089');
});
8 changes: 8 additions & 0 deletions examples/post-start-secret-delivery/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# Secret injection through post_start_hook
This example shows a method to retrieve run-time secrets from a vault and deliver them to newly-started app instances using a post_start_hook.
In this set-up PM2, with the help of the hook, acts as a 'trusted intermediate'; it is the only entity that has
full access to the secret store, and it is responsible for delivering the appropriate secrets to the appropriate app instances.

One key point of this solution is that the secret data is not passed through environment variables, which could be exposed.
Instead, the secret data is delivered to the app using its stdin file descriptor.
Note that this will not work in cluster mode, as in that case apps run with stdin detached.
25 changes: 25 additions & 0 deletions examples/post-start-secret-delivery/app.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
const readline = require('node:readline/promises');
const { stdin, stdout } = require('node:process');
const rl = readline.createInterface({ input: stdin, output: stdout });

const defaultConfig = require('./config.json');

async function main() {
// Read overrides from stdin
const overridesStr = await rl.question('overrides? ');
let overrides;
try {
overrides = JSON.parse(overridesStr);
} catch (e) {
console.error(`Error parsing >${overridesStr}<:`, e);
process.exit(1);
}

// Merge overrides into default config to form final config
const config = Object.assign({}, defaultConfig, overrides);
console.log(`App running with config: ${JSON.stringify(config, null, 2)}`);
// Keep it alive
setInterval(() => {}, 1000);
}

main().catch(console.error);
4 changes: 4 additions & 0 deletions examples/post-start-secret-delivery/config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"fooKey": "defaultFooValue",
"barKey": "defaultBarValue"
}
23 changes: 23 additions & 0 deletions examples/post-start-secret-delivery/post-start-hook.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
'use strict';
const fs = require('fs').promises;

/**
* This is a post-start hook that will be called after an app started.
* @param {object} info
* @param {number} info.pid The apps PID
* @param {Stream} info.stdin The apps STDIN stream
* @param {Stream} info.stdout The apps STDOUT stream
* @param {Stream} info.stderr The apps STDERR stream
* @param {object} pm2_env The apps environment variables
* @returns {Promise<void>}
*/
async function hook(info) {
const appName = info.pm2_env.name;
// In a real scenario secrets would be retrieved from some secret store
const allSecrets = JSON.parse(await fs.readFile('secrets.json', 'utf8'));
const appOverrides = allSecrets[appName] || {};
// Write the overrides json to the apps STDIN stream
info.stdin.write(JSON.stringify(appOverrides) + '\n');
}

module.exports = require('util').callbackify(hook);
7 changes: 7 additions & 0 deletions examples/post-start-secret-delivery/process.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
- name: app-1
script: app.js
post_start_hook: post-start-hook.js

- name: app-2
script: app.js
post_start_hook: post-start-hook.js
8 changes: 8 additions & 0 deletions examples/post-start-secret-delivery/secrets.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"app-1": {
"barKey": "secretBarValueForApp1"
},
"app-2": {
"barKey": "secretBarValueForApp2"
}
}
4 changes: 4 additions & 0 deletions lib/API/schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,10 @@
"docDefault": false,
"docDescription": "Start a script even if it is already running (only the script path is considered)"
},
"post_start_hook": {
"type": "string",
"docDescription": "Script to run after app start"
},
"append_env_to_name": {
"type": "boolean",
"docDefault": false,
Expand Down
61 changes: 54 additions & 7 deletions lib/God.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ var Utility = require('./Utility');
var cst = require('../constants.js');
var timesLimit = require('async/timesLimit');
var Configuration = require('./Configuration.js');
var which = require('./tools/which');

/**
* Override cluster module configuration
Expand Down Expand Up @@ -212,7 +213,53 @@ God.executeApp = function executeApp(env, cb) {
God.registerCron(env_copy)

/** Callback when application is launched */
var readyCb = function ready(proc) {
var appRunningCb = function(clu) {
var post_start_hook = env_copy['post_start_hook'];
if (post_start_hook) {
// Full path script resolution
var hook_path = path.resolve(clu.pm2_env.cwd, post_start_hook);

// If script does not exist after resolution
if (!fs.existsSync(hook_path)) {
var ckd;
// Try resolve command available in $PATH
if ((ckd = which(post_start_hook))) {
if (typeof(ckd) !== 'string')
ckd = ckd.toString();
hook_path = ckd;
}
else
// Throw critical error
return new Error(`post_start_hook not found: ${post_start_hook}`);
}
try {
var hookFn = require(hook_path);
if (typeof hookFn !== 'function') {
throw new Error('post_start_hook module.exports must be a function');
}
hookFn({
pid: clu.process.pid,
stdin: clu.stdin,
stdout: clu.stdout,
stderr: clu.stderr,
pm2_env: clu.pm2_env,
}, function (hook_err) {
if (hook_err) {
console.error('post_start_hook returned error:', hook_err);
}
return hooksDoneCb(clu);
});
} catch (require_hook_err) {
console.error('executing post_start_hook failed:', require_hook_err.message);
return hooksDoneCb(clu);
}
} else {
return hooksDoneCb(clu);
}
};

/** Callback when post-start hook is done */
var hooksDoneCb = function ready(proc) {
// If vizion enabled run versioning retrieval system
if (proc.pm2_env.vizion !== false && proc.pm2_env.vizion !== "false")
God.finalizeProcedure(proc);
Expand Down Expand Up @@ -265,12 +312,12 @@ God.executeApp = function executeApp(env, cb) {

return clu.once('online', function () {
if (!clu.pm2_env.wait_ready)
return readyCb(clu);
return appRunningCb(clu);

// Timeout if the ready message has not been sent before listen_timeout
var ready_timeout = setTimeout(function() {
God.bus.removeListener('process:msg', listener)
return readyCb(clu)
return appRunningCb(clu)
}, clu.pm2_env.listen_timeout || cst.GRACEFUL_LISTEN_TIMEOUT);

var listener = function (packet) {
Expand All @@ -279,7 +326,7 @@ God.executeApp = function executeApp(env, cb) {
packet.process.pm_id === clu.pm2_env.pm_id) {
clearTimeout(ready_timeout);
God.bus.removeListener('process:msg', listener)
return readyCb(clu)
return appRunningCb(clu)
}
}

Expand Down Expand Up @@ -321,12 +368,12 @@ God.executeApp = function executeApp(env, cb) {
});

if (!clu.pm2_env.wait_ready)
return readyCb(clu);
return appRunningCb(clu);

// Timeout if the ready message has not been sent before listen_timeout
var ready_timeout = setTimeout(function() {
God.bus.removeListener('process:msg', listener)
return readyCb(clu)
return appRunningCb(clu)
}, clu.pm2_env.listen_timeout || cst.GRACEFUL_LISTEN_TIMEOUT);

var listener = function (packet) {
Expand All @@ -335,7 +382,7 @@ God.executeApp = function executeApp(env, cb) {
packet.process.pm_id === clu.pm2_env.pm_id) {
clearTimeout(ready_timeout);
God.bus.removeListener('process:msg', listener)
return readyCb(clu)
return appRunningCb(clu)
}
}
God.bus.on('process:msg', listener);
Expand Down