Skip to content

Commit bf80f2b

Browse files
committed
New feature to allow the program environment to be loaded from an external file or program.
- This allows supervisord to be used in conjunction with any secrets pattern using a root-only file or a program that can provide environment variables that a program should have. It can be set globally in the supervisord section, or per program in a program section. The new options are environment_file or environment_loader. They are optional, and errors in one of them will prevent startup. They can be set in the [supervisord] section and then will be passed down to the programs, or in the program definitions. The file/loader is checked right before the calls to spawn() in order to avoid problems with calling a subprocess after the child fork, and to allow restarts to reload those environment values. - Updated the docs for these new options - Updated the tests to add a new test to check these new options.
1 parent 499fc10 commit bf80f2b

File tree

5 files changed

+289
-4
lines changed

5 files changed

+289
-4
lines changed

docs/configuration.rst

+67
Original file line numberDiff line numberDiff line change
@@ -464,6 +464,42 @@ follows.
464464

465465
*Introduced*: 3.0
466466

467+
``environment_file``
468+
469+
An absolute path to a file that contains a ``KEY=VAL`` entry on each line.
470+
Lines that begin with a '#' character are ignored. Leading and trailing
471+
whitespace are stripped off. Each valid ``KEY=VAL`` line will be placed
472+
in the environment of all child processes. The VAL entries must not be quoted,
473+
and interpolation is not supported for these values. The file must be readable
474+
by supervisord, and may be only readable by the user supervisord runs as since
475+
these values are loaded before any privileges are dropped for child processes.
476+
All other behaviors of the ``environment`` values are followed. When this is
477+
set in the supervisord section, it will be applied to all program sections unless
478+
they explicitly set either ``environment_file`` or ``environment_loader``. Only one of
479+
the program setting or the supervisord setting for environment_file is processed.
480+
481+
*Default*: no value
482+
483+
*Required*: No.
484+
485+
*Introduced*: 4.2.3
486+
487+
``environment_loader``
488+
489+
A shell command or an absolute path to a program that will be run by supervisord before launching
490+
the child processes, and the stdout will be captured and parsed according to the rules for
491+
``environment_file``. Only one of ``environment_file`` or ``environment_loader`` should be set, and
492+
``environment_file`` takes precedence. When this is set in the supervisord section,
493+
it will be applied to all program sections unless they explicitly set either
494+
``environment_file`` or ``environment_loader``. Only one of the program setting or the
495+
supervisord setting for environment_loader is processed.
496+
497+
*Default*: no value
498+
499+
*Required*: No.
500+
501+
*Introduced*: 4.2.3
502+
467503
``identifier``
468504

469505
The identifier string for this supervisor process, used by the RPC
@@ -1099,6 +1135,37 @@ where specified.
10991135

11001136
*Introduced*: 3.0
11011137

1138+
``environment_file``
1139+
1140+
An absolute path to a file that contains a ``KEY=VAL`` entry on each line.
1141+
Lines that begin with a '#' character are ignored. Leading and trailing
1142+
whitespace between the values are stripped off. Each valid ``KEY=VAL`` line will be placed
1143+
in the environment of all child processes. The VAL entries must not be quoted,
1144+
and interpolation is not supported for these values. The file must be readable
1145+
by supervisord, and may be only readable by the user supervisord runs as since
1146+
these values are loaded before any privileges are dropped for child processes.
1147+
All other behaviors of the ``environment`` values are followed.
1148+
1149+
*Default*: no value
1150+
1151+
*Required*: No.
1152+
1153+
*Introduced*: 4.2.3
1154+
1155+
``environment_loader``
1156+
1157+
A shell command or an absolute path to a program that will be by supervisord before launching
1158+
a child process, and the stdout will be captured and parsed according to the rules for
1159+
``environment_file``. The program must be executable by supervisord. Only one of
1160+
``environment_file`` or ``environment_loader`` should be set, and ``environment_file`` takes precedence.
1161+
1162+
*Default*: no values
1163+
1164+
*Required*: No.
1165+
1166+
*Introduced*: 4.2.3
1167+
1168+
11021169
``directory``
11031170

11041171
A file path representing a directory to which :program:`supervisord`

supervisor/options.py

+65-1
Original file line numberDiff line numberDiff line change
@@ -656,6 +656,8 @@ def get(opt, default, **kwargs):
656656
environ_str = get('environment', '')
657657
environ_str = expand(environ_str, expansions, 'environment')
658658
section.environment = dict_of_key_value_pairs(environ_str)
659+
section.environment_file = get('environment_file', None)
660+
section.environment_loader = get('environment_loader', None)
659661

660662
# extend expansions for global from [supervisord] environment definition
661663
for k, v in section.environment.items():
@@ -674,6 +676,13 @@ def get(opt, default, **kwargs):
674676
env = section.environment.copy()
675677
env.update(proc.environment)
676678
proc.environment = env
679+
680+
# set the environment file/loader on the process configs but let them override it
681+
if not proc.environment_file and not proc.environment_loader:
682+
if section.environment_file:
683+
proc.environment_file = section.environment_file
684+
elif section.environment_loader:
685+
proc.environment_loader = section.environment_loader
677686
section.server_configs = self.server_configs_from_parser(parser)
678687
section.profile_options = None
679688
return section
@@ -925,6 +934,8 @@ def get(section, opt, *args, **kwargs):
925934
numprocs = integer(get(section, 'numprocs', 1))
926935
numprocs_start = integer(get(section, 'numprocs_start', 0))
927936
environment_str = get(section, 'environment', '', do_expand=False)
937+
environment_file = get(section, 'environment_file', '', do_expand=False)
938+
environment_loader = get(section, 'environment_loader', '', do_expand=False)
928939
stdout_cmaxbytes = byte_size(get(section,'stdout_capture_maxbytes','0'))
929940
stdout_events = boolean(get(section, 'stdout_events_enabled','false'))
930941
stderr_cmaxbytes = byte_size(get(section,'stderr_capture_maxbytes','0'))
@@ -1057,6 +1068,8 @@ def get(section, opt, *args, **kwargs):
10571068
exitcodes=exitcodes,
10581069
redirect_stderr=redirect_stderr,
10591070
environment=environment,
1071+
environment_file=environment_file,
1072+
environment_loader=environment_loader,
10601073
serverurl=serverurl)
10611074

10621075
programs.append(pconfig)
@@ -1875,7 +1888,7 @@ class ProcessConfig(Config):
18751888
'stderr_events_enabled', 'stderr_syslog',
18761889
'stopsignal', 'stopwaitsecs', 'stopasgroup', 'killasgroup',
18771890
'exitcodes', 'redirect_stderr' ]
1878-
optional_param_names = [ 'environment', 'serverurl' ]
1891+
optional_param_names = [ 'environment', 'environment_file', 'environment_loader', 'serverurl' ]
18791892

18801893
def __init__(self, options, **params):
18811894
self.options = options
@@ -1939,6 +1952,57 @@ def make_dispatchers(self, proc):
19391952
dispatchers[stdin_fd] = PInputDispatcher(proc, 'stdin', stdin_fd)
19401953
return dispatchers, p
19411954

1955+
def load_external_environment_definition(self):
1956+
return self.load_external_environment_definition_for_config(self)
1957+
1958+
# this is separated out in order to make it easier to test
1959+
@classmethod
1960+
def load_external_environment_definition_for_config(cls, config):
1961+
# lazily load extra env vars before we drop privileges so that this can be used to load a secrets file
1962+
# or execute a program to get more env configuration. It doesn't have to be secrets, just config that
1963+
# needs to be separate from the supervisor config for whatever reason. The supervisor config interpolation
1964+
# is not supported here. The data format is just plain text, with one k=v value per line. Lines starting
1965+
# with '#' are ignored.
1966+
env = {}
1967+
envdata = None
1968+
if config.environment_file:
1969+
if os.path.exists(config.environment_file):
1970+
try:
1971+
with open(config.environment_file, 'r') as f:
1972+
envdata = f.read()
1973+
1974+
except Exception as e:
1975+
raise ProcessException("environment_file read failure on %s: %s" % (config.environment_file, e))
1976+
1977+
elif config.environment_loader:
1978+
try:
1979+
from subprocess import check_output, CalledProcessError
1980+
kwargs = dict(shell=True)
1981+
if not PY2:
1982+
kwargs['text'] = True
1983+
1984+
envdata = check_output(config.environment_loader, **kwargs)
1985+
1986+
except CalledProcessError as e:
1987+
raise ProcessException("environment_loader failure with %s: %d, %s" % (config.environment_loader, e.returncode, e.output))
1988+
1989+
if envdata:
1990+
extra_env = {}
1991+
1992+
for line in envdata.splitlines():
1993+
line = line.strip()
1994+
if line.startswith('#'): # ignore comments
1995+
continue
1996+
1997+
key, val = [s.strip() for s in line.split('=', 1)]
1998+
if key:
1999+
extra_env[key.upper()] = val
2000+
2001+
if extra_env:
2002+
env.update(extra_env)
2003+
2004+
return env
2005+
19422006
class EventListenerConfig(ProcessConfig):
19432007
def make_dispatchers(self, proc):
19442008
# always use_stderr=True for eventlisteners because mixing stderr

supervisor/process.py

+9-2
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,10 @@ def spawn(self):
217217

218218
try:
219219
filename, argv = self.get_execv_args()
220+
221+
# check the environment_file/environment_loader options before we fork to simplify child process management
222+
extra_env = self.config.load_external_environment_definition()
223+
220224
except ProcessException as what:
221225
self.record_spawnerr(what.args[0])
222226
self._assertInState(ProcessStates.STARTING)
@@ -260,7 +264,7 @@ def spawn(self):
260264
return self._spawn_as_parent(pid)
261265

262266
else:
263-
return self._spawn_as_child(filename, argv)
267+
return self._spawn_as_child(filename, argv, extra_env=extra_env)
264268

265269
def _spawn_as_parent(self, pid):
266270
# Parent
@@ -284,7 +288,7 @@ def _prepare_child_fds(self):
284288
for i in range(3, options.minfds):
285289
options.close_fd(i)
286290

287-
def _spawn_as_child(self, filename, argv):
291+
def _spawn_as_child(self, filename, argv, extra_env=None):
288292
options = self.config.options
289293
try:
290294
# prevent child from receiving signals sent to the
@@ -322,6 +326,9 @@ def _spawn_as_child(self, filename, argv):
322326
if self.config.environment is not None:
323327
env.update(self.config.environment)
324328

329+
if extra_env:
330+
env.update(extra_env)
331+
325332
# change directory
326333
cwd = self.config.directory
327334
try:

supervisor/tests/base.py

+8-1
Original file line numberDiff line numberDiff line change
@@ -520,7 +520,8 @@ def __init__(self, options, name, command, directory=None, umask=None,
520520
stderr_syslog=False,
521521
redirect_stderr=False,
522522
stopsignal=None, stopwaitsecs=10, stopasgroup=False, killasgroup=False,
523-
exitcodes=(0,), environment=None, serverurl=None):
523+
exitcodes=(0,), environment=None, environment_file=None, environment_loader=None,
524+
serverurl=None):
524525
self.options = options
525526
self.name = name
526527
self.command = command
@@ -552,6 +553,8 @@ def __init__(self, options, name, command, directory=None, umask=None,
552553
self.killasgroup = killasgroup
553554
self.exitcodes = exitcodes
554555
self.environment = environment
556+
self.environment_file = environment_file
557+
self.environment_loader = environment_loader
555558
self.directory = directory
556559
self.umask = umask
557560
self.autochildlogs_created = False
@@ -582,6 +585,10 @@ def make_dispatchers(self, proc):
582585
dispatchers[stdin_fd] = DummyDispatcher(writable=True)
583586
return dispatchers, pipes
584587

588+
def load_external_environment_definition(self):
589+
from supervisor.options import ProcessConfig
590+
return ProcessConfig.load_external_environment_definition_for_config(self)
591+
585592
def makeExecutable(file, substitutions=None):
586593
import os
587594
import sys

0 commit comments

Comments
 (0)