Add a --registry / -R command line argument to the container tool to specify an altnerative image registry and support it in the TOML configuration file accordingly.
This uses a mechanism based on regular expressions to parse the container registry and image names and construct the final one based on a set of priority rules. A registry can be included directly in an image name; an empty string means a local image. It can then be overridden by explicitly specifying a registry name via the CLI option or the TOML file, with the CLI option taking precedence as always. Signed-off-by: Guillaume Tucker <[email protected]> --- scripts/container | 126 ++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 117 insertions(+), 9 deletions(-) diff --git a/scripts/container b/scripts/container index a3a55180d6ed..de72725656a8 100755 --- a/scripts/container +++ b/scripts/container @@ -10,6 +10,7 @@ import dataclasses import logging import os import pathlib +import re import shutil import subprocess import sys @@ -17,6 +18,89 @@ import tomllib import uuid +def _init_registry_regex(): + domain = r'[{anum}][{anum}-]*\.[{anum}\.-]*[{anum}]'.format(anum=r'a-z0-9') + ipv6 = r'\[[a-fA-F0-9\:]+\]' + port = r'[\d]{1,5}' + return rf'((?P<domain>{domain})|(?P<ipv6>{ipv6}))(\:(?P<port>{port}))?' + + +def _init_image_regex(): + step = r'([{anum}]|[{anum}][a-z0-9_\.\-]*[{anum}])'.format(anum=r'a-z0-9') + tag = r'[a-zA-Z0-9_][a-zA-Z0-9_\.\-\/]{0,127}' + return rf'(?P<path>{step}(\/{step})*)(\:(?P<tag>{tag}))?' + + +REGISTRY_REGEX = _init_registry_regex() +IMAGE_REGEX = _init_image_regex() + + +class ImageParserMixin: + """Helper mixin to parse container image names""" + + @classmethod + def from_fields(cls, fields): + """Get an object using a valid subset of fields or None""" + kwargs = {} + for field in dataclasses.fields(cls): + if value := fields.get(field.name): + kwargs[field.name] = value + return cls(**kwargs) if kwargs else None + + @classmethod + def from_name(cls, name): + """Get an object using fields in the name matching ._regex or None""" + re_match = cls._regex.match(name) + return cls.from_fields(re_match.groupdict()) if re_match else None + + [email protected] +class RegistryName(ImageParserMixin): + """Parsed container registry name with no image""" + registry: str = None + domain: str = None + ipv6: str = None + port: str = None + _regex = re.compile(rf'^(?P<registry>{REGISTRY_REGEX})$') + + [email protected] +class ImageName(ImageParserMixin): + """Parsed container image name with no registry""" + image: str = None + path: str = None + tag: str = None + _regex = re.compile(rf'^(?P<image>{IMAGE_REGEX})$') + + [email protected] +class FullImageName(ImageParserMixin): + """Parsed image name including an optional registry""" + image: ImageName + registry: RegistryName = None + _regex = re.compile( + rf'^((?P<registry>{REGISTRY_REGEX})\/)?(?P<image>{IMAGE_REGEX})?$' + ) + + @property + def name(self): + """Full name in registry/image:tag format with optional registry""" + return ( + '/'.join((self.registry.registry, self.image.image)) + if self.registry else self.image.image + ) + + @classmethod + def from_name(cls, name): + if re_match := cls._regex.match(name): + fields = re_match.groupdict() + return FullImageName( + registry=RegistryName.from_fields(fields), + image=ImageName.from_fields(fields), + ) + return None + + class ContainerRuntime(abc.ABC): """Base class for a container runtime implementation""" @@ -145,6 +229,7 @@ class Config: """Container configuration""" image: str = None runtime: str = None + registry: str = None env_file: str = None uid: int = None gid: int = None @@ -188,23 +273,40 @@ def _get_logger(verbose): return logger +def _get_image(args, config): + image_name = args.image or config.image + if not image_name: + raise ValueError("no image specified") + image = FullImageName.from_name(image_name) + if image is None: + raise ValueError(f"invalid image name: {image_name}") + if args.registry == '': + image.registry = None + elif args.registry is not None: + image.registry = RegistryName.from_name(args.registry) + if image.registry is None: + raise ValueError(f"invalid registry name: {args.registry}") + elif config.registry is not None and image.registry is None: + image.registry = RegistryName.from_name(config.registry) + if image.registry is None: + raise ValueError(f"invalid registry name: {config.registry}") + return image + + def main(args): """Main entry point for the container tool""" logger = _get_logger(args.verbose) try: config = Config.from_toml(args.config_file, args.config_profile) - runtime = args.runtime or config.runtime - cls = Runtimes.get(runtime) if runtime else Runtimes.find() + runtime_name = args.runtime or config.runtime + cls = Runtimes.get(runtime_name) if runtime_name else Runtimes.find() + logger.debug("runtime: %s", cls.name) + image = _get_image(args, config) + logger.debug("image: %s", image.name) except ValueError as ex: logger.error(ex) return 1 - logger.debug("runtime: %s", cls.name) - image = args.image or config.image - if not image: - logger.error("no image specified") - return 1 - logger.debug("image: %s", image) - return cls(args, config, logger).run(image, args.cmd) + return cls(args, config, logger).run(image.name, args.cmd) if __name__ == '__main__': @@ -235,6 +337,12 @@ if __name__ == '__main__': help="Profile section to use in the config file. This will override " "any values defined in the DEFAULT section." ) + parser.add_argument( + '-R', '--registry', nargs='?', const='', + help="Container registry name. Any registry already included in the " + "image name will be replaced or removed when using this option with " + "no value." + ) parser.add_argument( '-r', '--runtime', choices=Runtimes.get_names(), help="Container runtime name. If not specified, the first one found " -- 2.47.3

