From 5b0e0c86579a8d7c3099cc1241df2cb114759c18 Mon Sep 17 00:00:00 2001 From: Christophe Mehay Date: Sun, 17 May 2020 23:11:20 +0200 Subject: [PATCH] Add support for handling commands matching setup It's now possible to map setups to specific commands. It's easier to handle many commands in one container. --- CHANGELOG | 7 +++ README.md | 49 +++++++++++++-- docs/config.rst | 56 ++++++++++++++++- pyentrypoint/config.py | 96 +++++++++++++++++++++++++----- pyproject.toml | 2 +- tests/configs/matching_command.yml | 62 +++++++++++++++++++ tests/pyentrypoint_test.py | 71 ++++++++++++++++++++++ 7 files changed, 323 insertions(+), 20 deletions(-) create mode 100644 tests/configs/matching_command.yml diff --git a/CHANGELOG b/CHANGELOG index c600484..09c13d3 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,2 +1,9 @@ +v0.7.0 + - Add mapping of config commands and conf files to root commands + +v0.6.0: + - Drop python 2 support + - Deprecation of `command` and `subcommands` settings for `commands` + v0.5.0: - add post_run_commands in entrypoint-config.yml diff --git a/README.md b/README.md index 64e48dc..43c9260 100644 --- a/README.md +++ b/README.md @@ -14,9 +14,12 @@ This tool avoids writing shell scripts to: ## Changelog - * V0.6.0 (2020-05-10) - * Drop python 2 support - * Deprecation of `command` and `subcommands` settings for `commands` (see bellow) +###### v0.7.0 (2020-05-17) + - Add command matching setup + +###### V0.6.0 (2020-05-10) + - Drop python 2 support + - Deprecation of `command` and `subcommands` settings for `commands` (see bellow) ## Usages @@ -88,7 +91,7 @@ This is an example of `entrypoint-config.yml` file. # This entry list commands handled by entrypoint. # If you run the container with a command not in this list, # pyentrypoint will run the command directly without any action -# If this option and `command` are not set, all commands will be handled. +# If this setting and `command` are not set, all commands will be handled. # Support wildcard commands: - git @@ -189,6 +192,44 @@ debug: true quiet: false ``` +#### Handled command matching + +All settings can be mapped to an handled command. + +For instance: + +```yaml + +# This config will handle command `abc` and `xyz` +commands: + - abc + - xyz + +# you can map commands to handled commands bellow +pre_conf_commands: + - abc: + - echo "will run for command abc" + - xyz: + - echo "will run for command xyz" + - echo "Can be multiple" + - echo "Will run for both commands" + +user: + - abc: 1000 + - xyz: 1001 + +# Mapping can also be a dictionnary +group: + abc: 1000 + xyz: 1001 + +# Etc +``` + +Not supported for deprecated settings `command`, `subcommands` and `links`. + + + ### Config templates You can generate configuration for your service with jinja2 template. diff --git a/docs/config.rst b/docs/config.rst index bb8d0ba..05e4082 100644 --- a/docs/config.rst +++ b/docs/config.rst @@ -110,6 +110,19 @@ This is an example of ``entrypoint-config.yml`` file. yaml references ~~~~~~~~~~~~~~~ +commands +^^^^^^^^ +This setup lists commands handled by entrypoint. +If you run the container with a command not in this list, +pyentrypoint will run the command directly without any action +If this setting and `command` are not set, all commands will be handled. +Support wildcard + +.. code:: yaml +commands: + - git + - sl* + command ^^^^^^^ @@ -118,6 +131,10 @@ command If the container is not started with this commande, the configuration will not be applied. +.. pull-quote:: + + **DEPRECATED**: This setup is remplaced by ``commands``. + subcommands ^^^^^^^^^^^ @@ -135,7 +152,7 @@ Running container with a matching subcommand run it with setuped ``command``. .. pull-quote:: - **Note**: Globbing pattern is enabled here. + **DEPRECATED**: This setup will be dropped. By default, all args started with hyphen are handled. @@ -289,3 +306,40 @@ quiet ^^^^^ Do not output anything except error + + +Handled command matching +======================== + +All settings can be mapped to an handled command. + +For instance: + +.. code:: yaml + + # This config will handle command `abc` and `xyz` + commands: + - abc + - xyz + + # you can map commands to handled commands bellow + pre_conf_commands: + - abc: + - echo "will run for command abc" + - xyz: + - echo "will run for command xyz" + - echo "Can be multiple" + - echo "Will run for both commands" + + user: + - abc: 1000 + - xyz: 1001 + + # Mapping can also be a dictionnary + group: + abc: 1000 + xyz: 1001 + + # Etc + +Not supported for deprecated settings `command`, `subcommands` and `links`. diff --git a/pyentrypoint/config.py b/pyentrypoint/config.py index d4f3020..53494c0 100755 --- a/pyentrypoint/config.py +++ b/pyentrypoint/config.py @@ -35,6 +35,64 @@ class ConfigMeta(object): return self._config[item] return [] + def _match_command(self, match): + if self._args: + return bool(fnmatch(self._args[0], match)) + return False + + def _check_command_match_key(self, dic, item): + if len(dic) != 1: + raise Exception('{item} setup missformated.'.format(item=item)) + + def _get_by_command(self, item=None, content=None, value_types=[]): + """Return settings for handled command""" + + def _mapping_list(content): + for d in content: + if not isinstance(d, dict): + raise Exception( + '{item} setup missformated.'.format(item=item)) + value = _mapping_dict(d) + if value: + return value + + def _mapping_dict(content, check_value=False): + for key, value in content.items(): + if check_value and not isinstance(value, list): + return content + if self._match_command(key): + return value + + if not content: + if item not in self._config: + return [] if list in value_types else None + content = self._config[item] + + if list not in value_types: + if isinstance(content, dict): + return _mapping_dict(content, dict in value_types) + if isinstance(content, list): + return _mapping_list(content) + return content + + if not isinstance(content, list): + raise Exception('{item} setup missformated.'.format(item=item)) + + rtn = [] + for line in content: + parsed = self._get_by_command(item=item, + content=line, + value_types=[ + t for t in value_types + if t is dict + ]) + if parsed and isinstance(parsed, list): + rtn.extend(parsed) + continue + if parsed: + rtn.append(parsed) + return rtn + def get_templates(self): """Returns iterator of tuple (template, config_file)""" config_files = self.config_files @@ -180,9 +238,10 @@ class Config(ConfigMeta): "Unix user or uid to run command." self._get_from_env(env='ENTRYPOINT_USER', key='user') if 'user' in self._config: - if isinstance(self._config['user'], int): - return self._config['user'] - return getpwnam(self._config['user']).pw_uid + user = self._get_by_command(item='user', value_types=[int, str]) + if isinstance(user, int): + return user + return getpwnam(user).pw_uid return os.getuid() @property @@ -190,20 +249,23 @@ class Config(ConfigMeta): "Unix group or gid to run command." self._get_from_env(env='ENTRYPOINT_GROUP', key='group') if 'group' in self._config: - if isinstance(self._config['group'], int): - return self._config['group'] - return getgrnam(self._config['group']).gr_gid + group = self._get_by_command(item='group', value_types=[int, str]) + if isinstance(group, int): + return group + return getgrnam(group).gr_gid return os.getgid() @property def config_files(self): "List of template config files." - return self._return_item_lst('config_files') + return self._get_by_command(item='config_files', + value_types=[list, dict]) @property def secret_env(self): """Environment variables to delete before running command.""" - return self._return_item_lst('secret_env') + return self._get_by_command(item='secret_env', + value_types=[list]) @property def links(self): @@ -221,17 +283,20 @@ class Config(ConfigMeta): @property def pre_conf_commands(self): """Return list of preconf commands""" - return self._return_item_lst('pre_conf_commands') + return self._get_by_command(item='pre_conf_commands', + value_types=[list]) @property def post_conf_commands(self): """Return list of postconf commands""" - return self._return_item_lst('post_conf_commands') + return self._get_by_command(item='post_conf_commands', + value_types=[list]) @property def post_run_commands(self): """Return list of post run commands""" - return self._return_item_lst('post_run_commands') + return self._get_by_command(item='post_run_commands', + value_types=[list]) @property def reload(self): @@ -249,14 +314,16 @@ class Config(ConfigMeta): def clean_env(self): """Clean env from linked containers before running command""" if 'clean_env' in self._config: - return bool(self._config['clean_env']) + return bool(self._get_by_command(item='clean_env', + value_types=[bool])) return True @property def remove_dockerenv(self): """Remove dockerenv and dockerinit files""" if 'remove_dockerenv' in self._config: - return bool(self._config['remove_dockerenv']) + return bool(self._get_by_command(item='remove_dockerenv', + value_types=[bool])) return True @property @@ -265,7 +332,8 @@ class Config(ConfigMeta): if envtobool('ENTRYPOINT_DEBUG', False): return True if 'debug' in self._config: - return bool(self._config['debug']) + return bool(self._get_by_command(item='debug', + value_types=[bool])) return False @property diff --git a/pyproject.toml b/pyproject.toml index bd93153..43e01f5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ [tool] [tool.poetry] name = "pyentrypoint" -version = "0.6.0" +version = "0.7.0" description = "pyentrypoint manages entrypoints in Docker containers." license = "WTFPL" classifiers = ["Programming Language :: Python", "Development Status :: 1 - Planning", "License :: OSI Approved :: BSD License", "Natural Language :: English", "Operating System :: POSIX :: Linux", "Programming Language :: Python :: 2", "Programming Language :: Python :: 3", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3.6", "Topic :: System :: Installation/Setup"] diff --git a/tests/configs/matching_command.yml b/tests/configs/matching_command.yml new file mode 100644 index 0000000..d76fbab --- /dev/null +++ b/tests/configs/matching_command.yml @@ -0,0 +1,62 @@ +commands: + - bash + +user: + - bash: 1000 + - zsh: 1001 + +group: + bash: 1002 + zsh: 1003 + +config_files: + - bash: + - file1.tpl + - file2: file3 + - file4 + - zsh: + - file5.tpl + - file6: file7 + - file8 + - file9 + - file10: file11 + +secret_env: + - secret1 + - bash: + - secret2 + - zsh: + - secret3 + +pre_conf_commands: + - bash: + - cmd1 + - zsh: + - cmd2 + - cmd3 + +post_conf_commands: + - cmd4 + - zsh: + - cmd5 + - bash: + - cmd6 + +post_run_commands: + - bash: + - cmd7 + - cmd8 + - zsh: + - cmd9 + +debug: + - zsh: false + - '*sh': true + +clean_env: + - bash: true + - '*sh': false + +remove_dockerenv: + - bash: true + - zsh: true diff --git a/tests/pyentrypoint_test.py b/tests/pyentrypoint_test.py index 10571c0..5d9426d 100644 --- a/tests/pyentrypoint_test.py +++ b/tests/pyentrypoint_test.py @@ -322,3 +322,74 @@ def test_commands_handling(): assert bash.is_handled assert not zsh.is_handled assert empty.is_handled + + +def test_command_matching_setup(): + bash = Entrypoint(conf='configs/matching_command.yml', args=['bash']) + zsh = Entrypoint(conf='configs/matching_command.yml', args=['zsh']) + + assert bash.config.user == 1000 + assert zsh.config.user == 1001 + + assert bash.config.group == 1002 + assert zsh.config.group == 1003 + + assert bash.config.config_files == [ + 'file1.tpl', + {'file2': 'file3'}, + 'file4', + 'file9', + {'file10': 'file11'}, + ] + assert zsh.config.config_files == [ + 'file5.tpl', + {'file6': 'file7'}, + 'file8', + 'file9', + {'file10': 'file11'}, + ] + + assert bash.config.secret_env == [ + 'secret1', + 'secret2', + ] + assert zsh.config.secret_env == [ + 'secret1', + 'secret3', + ] + + assert bash.config.pre_conf_commands == [ + 'cmd1', + 'cmd3', + ] + assert zsh.config.pre_conf_commands == [ + 'cmd2', + 'cmd3', + ] + + assert bash.config.post_conf_commands == [ + 'cmd4', + 'cmd6', + ] + assert zsh.config.post_conf_commands == [ + 'cmd4', + 'cmd5', + ] + + assert bash.config.post_run_commands == [ + 'cmd7', + 'cmd8', + ] + assert zsh.config.post_run_commands == [ + 'cmd8', + 'cmd9', + ] + + assert bash.config.debug + assert zsh.config.debug + + assert bash.config.clean_env + assert not zsh.config.clean_env + + assert bash.config.remove_dockerenv + assert zsh.config.remove_dockerenv