diff --git a/examples/dotenvx-pm2/.env.initial b/examples/dotenvx-pm2/.env.initial new file mode 100644 index 000000000..315c7be05 --- /dev/null +++ b/examples/dotenvx-pm2/.env.initial @@ -0,0 +1,4 @@ +DE=dotenv_initial +SH_DE=dotenv_initial +DE_PM=dotenv_initial +SH_DE_PM=dotenv_initial diff --git a/examples/dotenvx-pm2/.env.updated b/examples/dotenvx-pm2/.env.updated new file mode 100644 index 000000000..3689eaaac --- /dev/null +++ b/examples/dotenvx-pm2/.env.updated @@ -0,0 +1,4 @@ +DE=dotenv_updated +SH_DE=dotenv_updated +DE_PM=dotenv_updated +SH_DE_PM=dotenv_updated diff --git a/examples/dotenvx-pm2/README.md b/examples/dotenvx-pm2/README.md new file mode 100644 index 000000000..6bf7fe210 --- /dev/null +++ b/examples/dotenvx-pm2/README.md @@ -0,0 +1,21 @@ +This is an example of using [dotenvx](https://dotenvx.com/) with pm2. + +It demonstrates how to update the environment for forked and clustered apps using dotenvx. + +In this example, the environment variables come from three sources and are applied in the following order (dotenv files take precedence because of the `--overload` option used): +- shell environment +- dotenv files `.env.initial` or `.env.updated`. +- pm2 ecosystem config `ecosystem.config.cjs` + +See `package.json` scripts for the actual commands. + +```bash +# install dotenvx +npm install +# start apps with `initial` environment and `.env.initial` dotenv file +npm run start:json +# reload apps with `updated` environment and `.env.updated` dotenv file +npm run reload:json +# delete apps +npm run delete:json +``` diff --git a/examples/dotenvx-pm2/ecosystem.config.cjs b/examples/dotenvx-pm2/ecosystem.config.cjs new file mode 100644 index 000000000..c708322b6 --- /dev/null +++ b/examples/dotenvx-pm2/ecosystem.config.cjs @@ -0,0 +1,42 @@ +module.exports = { + apps: [ + { + name: 'forked_app', + script: './index.js', + env_initial: { + PORT: 8001, + PM: 'pm2_initial', + SH_PM: 'pm2_initial', + DE_PM: 'pm2_initial', + SH_DE_PM: 'pm2_initial', + }, + env_updated: { + PORT: 8001, + PM: 'pm2_updated', + SH_PM: 'pm2_updated', + DE_PM: 'pm2_updated', + SH_DE_PM: 'pm2_updated', + }, + }, + { + name: 'clustered_app', + script: './index.js', + instances: 2, + exec_mode: 'cluster', + env_initial: { + PORT: 8002, + PM: 'pm2_initial', + SH_PM: 'pm2_initial', + DE_PM: 'pm2_initial', + SH_DE_PM: 'pm2_initial', + }, + env_updated: { + PORT: 8002, + PM: 'pm2_updated', + SH_PM: 'pm2_updated', + DE_PM: 'pm2_updated', + SH_DE_PM: 'pm2_updated', + }, + }, + ], +}; diff --git a/examples/dotenvx-pm2/index.js b/examples/dotenvx-pm2/index.js new file mode 100644 index 000000000..589c4b73c --- /dev/null +++ b/examples/dotenvx-pm2/index.js @@ -0,0 +1,27 @@ +import http from 'http'; + +const { + PORT, + SH, + DE, + PM, + SH_DE, + SH_PM, + DE_PM, + SH_DE_PM, +} = process.env; + +http.createServer((req, res) => { + res.writeHead(200); + res.end(JSON.stringify({ + SH, + DE, + PM, + SH_DE, + SH_PM, + DE_PM, + SH_DE_PM, + }, null, 2)); +}).listen(PORT, '0.0.0.0', () => { + console.log(`App listening on port ${PORT}`); +}); diff --git a/examples/dotenvx-pm2/package.json b/examples/dotenvx-pm2/package.json new file mode 100644 index 000000000..99fc0d3fe --- /dev/null +++ b/examples/dotenvx-pm2/package.json @@ -0,0 +1,18 @@ +{ + "name": "dotenvx-pm2", + "version": "1.0.0", + "description": "Example usage of dotenvx with pm2", + "main": "index.js", + "type": "module", + "author": "Michael Kalygin", + "license": "MIT", + "scripts": { + "start:json": "SH=shell_initial SH_DE=shell_initial SH_PM=shell_initial SH_DE_PM=shell_initial dotenvx run --env-file .env.initial --overload -- ../../bin/pm2 start ecosystem.config.cjs --env initial", + "reload:json": "SH=shell_updated SH_DE=shell_updated SH_PM=shell_updated SH_DE_PM=shell_updated dotenvx run --env-file .env.updated --overload -- ../../bin/pm2 reload ecosystem.config.cjs --env updated --update-env", + "delete:json": "../../bin/pm2 delete ecosystem.config.cjs", + "test:json": "../../bin/pm2 update && npm run delete:json && npm run start:json && npm run reload:json" + }, + "dependencies": { + "@dotenvx/dotenvx": "^1.10.2" + } +} diff --git a/lib/API.js b/lib/API.js index 8b7bf2fa6..8a4ed1bdd 100644 --- a/lib/API.js +++ b/lib/API.js @@ -1071,7 +1071,7 @@ class API { }); // When we are processing JSON, allow to keep the new env by default - env.updateEnv = true; + env.updateEnv = typeof(opts.updateEnv) === 'undefined' ? true : opts.updateEnv; // Pass `env` option that._operate(action, proc_name, env, function(err, ret) { diff --git a/lib/Utility.js b/lib/Utility.js index 547ec9bf6..fd35269f8 100644 --- a/lib/Utility.js +++ b/lib/Utility.js @@ -33,10 +33,10 @@ var Utility = module.exports = { }, extendExtraConfig : function(proc, opts) { if (opts.env && opts.env.current_conf) { - if (opts.env.current_conf.env && - typeof(opts.env.current_conf.env) === 'object' && - Object.keys(opts.env.current_conf.env).length === 0) - delete opts.env.current_conf.env + // NOTE: `env` gets overriden by `Utility.extendMix` call. Instead, we want to merge envs. + if (opts.env.current_conf.env && typeof(opts.env.current_conf.env) === 'object') + Utility.extendMix(proc.pm2_env.env, opts.env.current_conf.env); + delete opts.env.current_conf.env; Utility.extendMix(proc.pm2_env, opts.env.current_conf); delete opts.env.current_conf; diff --git a/test/e2e.sh b/test/e2e.sh old mode 100644 new mode 100755 diff --git a/test/e2e/cli/env-refresh.sh b/test/e2e/cli/env-refresh.sh index 418c9e2f5..110f3d2c4 100644 --- a/test/e2e/cli/env-refresh.sh +++ b/test/e2e/cli/env-refresh.sh @@ -88,3 +88,98 @@ $pm2 kill $pm2 l NODE_PATH='/test2' $pm2 start local_require.js -i 1 should 'should have loaded the right globalPaths' 'restart_time: 0' 1 + +# +# Ensuring that environment update works correctly when reloading with JSON config. +# +# Related issue: +# https://github.com/Unitech/pm2/issues/3192 +# + +# start with config +SH=shell_initial SH_PM=shell_initial $pm2 start update-env.config.js --env initial +>out-env.log + +sleep 0.5 +grep "SH=shell_initial PM=pm2_initial SH_PM=pm2_initial" out-env.log &> /dev/null +spec "should inject shell environment, then inject config environment on start with config" + +# restart config without --update-env +$pm2 delete all +SH=shell_initial SH_PM=shell_initial $pm2 start update-env.config.js --env initial +SH=shell_updated SH_PM=shell_updated $pm2 restart update-env.config.js --env updated +>out-env.log + +sleep 0.5 +grep "SH=shell_updated PM=pm2_updated SH_PM=pm2_updated" out-env.log &> /dev/null +spec "should inject shell environment, then inject config environment on restart with config and without --update-env option" + +# reload config without --update-env +$pm2 delete all +SH=shell_initial SH_PM=shell_initial $pm2 start update-env.config.js --env initial +SH=shell_updated SH_PM=shell_updated $pm2 reload update-env.config.js --env updated +>out-env.log + +sleep 0.5 +grep "SH=shell_updated PM=pm2_updated SH_PM=pm2_updated" out-env.log &> /dev/null +spec "should inject shell environment, then inject config environment on reload with config and without --update-env option" + +# restart config with --update-env +$pm2 delete all +SH=shell_initial SH_PM=shell_initial $pm2 start update-env.config.js --env initial +SH=shell_updated SH_PM=shell_updated $pm2 restart update-env.config.js --env updated --update-env +>out-env.log + +sleep 0.5 +grep "SH=shell_updated PM=pm2_updated SH_PM=pm2_updated" out-env.log &> /dev/null +spec "should inject shell environment, then inject config environment on restart with config and with --update-env option" + +# reload config with --update-env +$pm2 delete all +SH=shell_initial SH_PM=shell_initial $pm2 start update-env.config.js --env initial +SH=shell_updated SH_PM=shell_updated $pm2 reload update-env.config.js --env updated --update-env +>out-env.log + +sleep 0.5 +grep "SH=shell_updated PM=pm2_updated SH_PM=pm2_updated" out-env.log &> /dev/null +spec "should inject shell environment, then inject config environment on reload with config and with --update-env option" + +# restart pid without --update-env +$pm2 delete all +SH=shell_initial SH_PM=shell_initial $pm2 start update-env.config.js --env initial +SH=shell_updated SH_PM=shell_updated $pm2 restart update_env_app +>out-env.log + +sleep 0.5 +grep "SH=shell_initial PM=pm2_initial SH_PM=pm2_initial" out-env.log &> /dev/null +spec "should keep environment on restart with pid and without --update-env option" + +# reload pid without --update-env +$pm2 delete all +SH=shell_initial SH_PM=shell_initial $pm2 start update-env.config.js --env initial +SH=shell_updated SH_PM=shell_updated $pm2 reload update_env_app +>out-env.log + +sleep 0.5 +grep "SH=shell_initial PM=pm2_initial SH_PM=pm2_initial" out-env.log &> /dev/null +spec "should keep environment on reload with pid and without --update-env option" + +# restart pid with --update-env +$pm2 delete all +SH=shell_initial SH_PM=shell_initial $pm2 start update-env.config.js --env initial +SH=shell_updated SH_PM=shell_updated $pm2 restart update_env_app --update-env +>out-env.log + +sleep 0.5 +grep "SH=shell_updated PM=pm2_initial SH_PM=shell_updated" out-env.log &> /dev/null +spec "should inject shell environment on restart with pid and with --update-env option" + +# reload pid with --update-env +$pm2 delete all +SH=shell_initial SH_PM=shell_initial $pm2 start update-env.config.js --env initial +SH=shell_updated SH_PM=shell_updated $pm2 reload update_env_app --update-env +>out-env.log + +sleep 0.5 +grep "SH=shell_updated PM=pm2_initial SH_PM=shell_updated" out-env.log &> /dev/null +spec "should inject shell environment on reload with pid and with --update-env option" diff --git a/test/fixtures/update-env.config.js b/test/fixtures/update-env.config.js new file mode 100644 index 000000000..550bdaf6f --- /dev/null +++ b/test/fixtures/update-env.config.js @@ -0,0 +1,22 @@ +module.exports = { + apps: [ + { + name: 'update_env_app', + script: './update-env.js', + instances: 2, + exec_mode: 'cluster', + out_file: 'out-env.log', + merge_logs: true, + env_initial: { + NODE_ENV: 'test', + PM: 'pm2_initial', + SH_PM: 'pm2_initial', + }, + env_updated: { + NODE_ENV: 'test', + PM: 'pm2_updated', + SH_PM: 'pm2_updated', + }, + }, + ], +}; diff --git a/test/fixtures/update-env.js b/test/fixtures/update-env.js new file mode 100644 index 000000000..0654665b9 --- /dev/null +++ b/test/fixtures/update-env.js @@ -0,0 +1,5 @@ +setInterval(() => { + const { SH, PM, SH_PM } = process.env; + + console.log(`SH=${SH} PM=${PM} SH_PM=${SH_PM}`); +}, 100); diff --git a/test/programmatic/env_switching.js b/test/programmatic/env_switching.js index fe55ce424..da1a9d61f 100644 --- a/test/programmatic/env_switching.js +++ b/test/programmatic/env_switching.js @@ -130,14 +130,117 @@ describe('PM2 programmatic calls', function() { }); }); - // it('should start a script and NODE_ENV have right value', function(done) { - // pm2.start(json_declaration_simple, function(err, data) { - // proc1 = data[0]; - // should(err).be.null; - // proc1.pm2_env['NODE_ENV'].should.eql(json_declaration.env.NODE_ENV); - // done(); - // }); - // }); + /** + * Ensuring that environment update works correctly when reloading with JSON config. + * + * Related issue: + * https://github.com/Unitech/pm2/issues/3192 + */ + describe('with updateEnv option', () => { + const env = { + shell: { + initial: { + SH: 'shell_initial', + SH_PM: 'shell_initial', + }, + updated: { + SH: 'shell_updated', + SH_PM: 'shell_updated', + }, + }, + pm2: { + initial: { + PM: 'pm2_initial', + SH_PM: 'pm2_initia', + }, + updated: { + PM: 'pm2_updated', + SH_PM: 'pm2_updated', + }, + }, + }; + + const configInitial = { + name: 'child-update-env', + script: './../fixtures/env-switching/child.js', + instances: '2', + env: { + NODE_ENV: 'test', + ...env.pm2.initial, + }, + }; + + const configUpdated = { + ...configInitial, + env: { + NODE_ENV: 'test', + ...env.pm2.updated, + }, + }; + + it('should inject shell environment, then inject config environment on start', (done) => { + Object.assign(process.env, env.shell.initial); + + pm2.start(configInitial, (err, data) => { + try { + const pm2Env = data[0] ? data[0].pm2_env || {} : {}; + should(err).be.null(); + should(pm2Env.SH).eql(env.shell.initial.SH); + should(pm2Env.PM).eql(env.pm2.initial.PM); + should(pm2Env.SH_PM).eql(env.pm2.initial.SH_PM); + done(); + } catch (err) { + done(err); + } + }); + }); + + it('should inject only config environment on restart when disabled', (done) => { + Object.assign(process.env, env.shell.updated); + + pm2.restart(configUpdated, { updateEnv: false }, (err) => { + should(err).be.null(); + + pm2.list((err, data) => { + try { + const pm2Env = data.find(proc => proc.name === configInitial.name).pm2_env; + should(err).be.null(); + should(pm2Env.SH).eql(env.shell.initial.SH); + should(pm2Env.PM).eql(env.pm2.updated.PM); + should(pm2Env.SH_PM).eql(env.pm2.updated.SH_PM); + done(); + } catch (err) { + done(err); + } + }); + }); + }); + it('should inject shell environment, then inject config environment on start when endabled', (done) => { + Object.assign(process.env, env.shell.updated); + + pm2.restart(configUpdated, { updateEnv: true }, (err) => { + should(err).be.null(); + + pm2.list((err, data) => { + try { + const pm2Env = data.find(proc => proc.name === configInitial.name).pm2_env; + should(err).be.null(); + should(pm2Env.SH).eql(env.shell.updated.SH); + should(pm2Env.PM).eql(env.pm2.updated.PM); + should(pm2Env.SH_PM).eql(env.pm2.updated.SH_PM); + done(); + } catch (err) { + done(err); + } + }); + }); + }); + it('should delete all processes', (done) => { + pm2.delete('all', (err, ret) => { + done(); + }); + }); + }); }); diff --git a/test/unit.sh b/test/unit.sh old mode 100644 new mode 100755