Skip to content

Commit

Permalink
Env-var substitution
Browse files Browse the repository at this point in the history
reinventing supervisord one step at a time.
(I kid)
  • Loading branch information
vanschelven committed Oct 3, 2024
1 parent de6db9d commit d8acc17
Show file tree
Hide file tree
Showing 3 changed files with 53 additions and 4 deletions.
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,9 @@ why this is a great idea](https://www.bugsink.com/multi-process-docker-images/)
* **Graceful Shutdown**: Ensure the parent process waits for all children to exit
before shutting down.

* **Variable Substition**: Substitute variables from the command line because [Docker
can't be bothered to do so itself](https://github.com/moby/moby/issues/5509)

### Installation

To use this script, clone the repository or install via your preferred method:
Expand Down
23 changes: 19 additions & 4 deletions monofy/scripts/monofy.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@
import sys
import os
from time import sleep
import re

# match both ${VAR} and $VAR:
MATCH_ENV_VAR = re.compile(r'\$\{([A-Za-z_][A-Za-z0-9_]*)\}|\$([A-Za-z_][A-Za-z0-9_]*)')


class ParentProcess:
Expand Down Expand Up @@ -82,7 +86,18 @@ def connect_childrens_fates(self):
self.terminate_children(except_child=child)

@classmethod
def get_pre_start_command_args(self, argv):
def substitute_env_vars(cls, arg):
# I don't _want_ to do this, but Docker doesn't want to do it either, so here we are.
#
# One (the only) other way to do this is to have CMD invoke a shell; but that has drawbacks too (e.g. signal
# forwarding). Since monofy is already the "extra layer" we add to the system, we might as well solve the
# problems here.
#
# https://github.com/moby/moby/issues/5509
return MATCH_ENV_VAR.sub(lambda m: os.environ.get(m.group(1) or m.group(2), ""), arg)

@classmethod
def get_pre_start_command_args(cls, argv):
"""Splits our own arguments into a list of args for each of the pre-start commands, we split on "&&"."""

# We don't want to pass the first argument, as that is the script name
Expand All @@ -97,12 +112,12 @@ def get_pre_start_command_args(self, argv):
result.append(this)
this = []
else:
this.append(arg)
this.append(cls.substitute_env_vars(arg))

return result

@classmethod
def get_parallel_command_args(self, argv):
def get_parallel_command_args(cls, argv):
"""Splits our own arguments into a list of args for each of the children each, we split on "|||"."""

# We don't want to pass the first argument, as that is the script name
Expand All @@ -116,7 +131,7 @@ def get_parallel_command_args(self, argv):
if arg == "|||":
result.append([])
else:
result[-1].append(arg)
result[-1].append(cls.substitute_env_vars(arg))

return result

Expand Down
31 changes: 31 additions & 0 deletions monofy/tests.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import os
import unittest

from monofy.scripts.monofy import ParentProcess
Expand Down Expand Up @@ -26,12 +27,24 @@ def _check(argv, expected_pre_start, expected_parallel):
[["a", "b"]],
)

_check(
["monofy.py", "a", "|||", "b", "|||", "c"],
[],
[["a"], ["b"], ["c"]],
)

_check(
["monofy.py", "a", "b", "|||", "c", "d", "|||", "e", "f"],
[],
[["a", "b"], ["c", "d"], ["e", "f"]],
)

_check(
["monofy.py", "a", "&&", "b", "|||", "c"],
[["a"]],
[["b"], ["c"]],
)

_check(
["monofy.py", "a", "b", "&&", "c", "d", "|||", "e", "f"],
[["a", "b"]],
Expand All @@ -56,6 +69,24 @@ def _check(argv, expected_pre_start, expected_parallel):
[["e", "f"], ["g", "h"], ["i", "j"]],
)

_check(
["monofy.py", "$USER", "&&", "$USER", "|||", "$USER"],
[[os.environ["USER"]]],
[[os.environ["USER"]], [os.environ["USER"]]],
)

def test_substitute_env_vars(self):
# test-the-test: we need a non-empty USER to be able to test against
self.assertTrue(os.environ.get("USER"))

self.assertEqual("donttouchme", ParentProcess.substitute_env_vars("donttouchme"))
self.assertEqual("", ParentProcess.substitute_env_vars(""))
self.assertEqual("foo %s" % os.environ["USER"], ParentProcess.substitute_env_vars("foo $USER"))
self.assertEqual("bar %s foo" % os.environ["USER"], ParentProcess.substitute_env_vars("bar ${USER} foo"))
self.assertEqual("", ParentProcess.substitute_env_vars("$THISWILLNOTEXIST"))
self.assertEqual(
"%s %s" % (os.environ["USER"], os.environ["USER"]), ParentProcess.substitute_env_vars("$USER $USER"))


if __name__ == '__main__':
unittest.main()

0 comments on commit d8acc17

Please sign in to comment.