199 lines · 5.9 KB · Text
RawDownload
#!/usr/bin/env python3# SPDX-License-Identifier: GPL-2.0-only# Copyright (C) 2025 Guillaume Tucker """Containerized builds""" import abcimport argparseimport loggingimport osimport pathlibimport shutilimport subprocessimport sysimport uuid  class ContainerRuntime(abc.ABC):    """Base class for a container runtime implementation"""     name = None  # Property defined in each implementation class     def __init__(self, args, logger):        self._uid = args.uid or os.getuid()        self._gid = args.gid or args.uid or os.getgid()        self._env_file = args.env_file        self._shell = args.shell        self._logger = logger     @classmethod    def is_present(cls):        """Determine whether the runtime is present on the system"""        return shutil.which(cls.name) is not None     @abc.abstractmethod    def _do_run(self, image, cmd, container_name):        """Runtime-specific handler to run a command in a container"""     @abc.abstractmethod    def _do_abort(self, container_name):        """Runtime-specific handler to abort a running container"""     def run(self, image, cmd):        """Run a command in a runtime container"""        container_name = str(uuid.uuid4())        self._logger.debug("container: %s", container_name)        try:            return self._do_run(image, cmd, container_name)        except KeyboardInterrupt:            self._logger.error("user aborted")            self._do_abort(container_name)            return 1  class CommonRuntime(ContainerRuntime):    """Common logic for Docker and Podman"""     def _do_run(self, image, cmd, container_name):        cmdline = [self.name, 'run']        cmdline += self._get_opts(container_name)        cmdline.append(image)        cmdline += cmd        self._logger.debug('command: %s', ' '.join(cmdline))        return subprocess.call(cmdline)     def _get_opts(self, container_name):        opts = [            '--name', container_name,            '--rm',            '--volume', f'{pathlib.Path.cwd()}:/src',            '--workdir', '/src',        ]        if self._env_file:            opts += ['--env-file', self._env_file]        if self._shell:            opts += ['--interactive', '--tty']        return opts     def _do_abort(self, container_name):        subprocess.call([self.name, 'kill', container_name])  class DockerRuntime(CommonRuntime):    """Run a command in a Docker container"""     name = 'docker'     def _get_opts(self, container_name):        return super()._get_opts(container_name) + [            '--user', f'{self._uid}:{self._gid}'        ]  class PodmanRuntime(CommonRuntime):    """Run a command in a Podman container"""     name = 'podman'     def _get_opts(self, container_name):        return super()._get_opts(container_name) + [            '--userns', f'keep-id:uid={self._uid},gid={self._gid}',        ]  class Runtimes:    """List of all supported runtimes"""     runtimes = [PodmanRuntime, DockerRuntime]     @classmethod    def get_names(cls):        """Get a list of all the runtime names"""        return list(runtime.name for runtime in cls.runtimes)     @classmethod    def get(cls, name):        """Get a single runtime class matching the given name"""        for runtime in cls.runtimes:            if runtime.name == name:                if not runtime.is_present():                    raise ValueError(f"runtime not found: {name}")                return runtime        raise ValueError(f"unknown runtime: {name}")     @classmethod    def find(cls):        """Find the first runtime present on the system"""        for runtime in cls.runtimes:            if runtime.is_present():                return runtime        raise ValueError("no runtime found")  def _get_logger(verbose):    """Set up a logger with the appropriate level"""    logger = logging.getLogger('container')    handler = logging.StreamHandler()    handler.setFormatter(logging.Formatter(        fmt='[container {levelname}] {message}', style='{'    ))    logger.addHandler(handler)    logger.setLevel(logging.DEBUG if verbose is True else logging.INFO)    return logger  def main(args):    """Main entry point for the container tool"""    logger = _get_logger(args.verbose)    try:        cls = Runtimes.get(args.runtime) if args.runtime else Runtimes.find()    except ValueError as ex:        logger.error(ex)        return 1    logger.debug("runtime: %s", cls.name)    logger.debug("image: %s", args.image)    return cls(args, logger).run(args.image, args.cmd)  if __name__ == '__main__':    parser = argparse.ArgumentParser(        'container',        description="See the documentation for more details: "        "https://docs.kernel.org/dev-tools/container.html"    )    parser.add_argument(        '-e', '--env-file',        help="Path to an environment file to load in the container."    )    parser.add_argument(        '-g', '--gid',        help="Group ID to use inside the container."    )    parser.add_argument(        '-i', '--image', required=True,        help="Container image name."    )    parser.add_argument(        '-r', '--runtime', choices=Runtimes.get_names(),        help="Container runtime name.  If not specified, the first one found "        "on the system will be used i.e. Podman if present, otherwise Docker."    )    parser.add_argument(        '-s', '--shell', action='store_true',        help="Run the container in an interactive shell."    )    parser.add_argument(        '-u', '--uid',        help="User ID to use inside the container.  If the -g option is not "        "specified, the user ID will also be set as the group ID."    )    parser.add_argument(        '-v', '--verbose', action='store_true',        help="Enable verbose output."    )    parser.add_argument(        'cmd', nargs='+',        help="Command to run in the container"    )    sys.exit(main(parser.parse_args(sys.argv[1:])))