1*8f989b3bSGuillaume Tucker#!/usr/bin/env python3 2*8f989b3bSGuillaume Tucker# SPDX-License-Identifier: GPL-2.0-only 3*8f989b3bSGuillaume Tucker# Copyright (C) 2025 Guillaume Tucker 4*8f989b3bSGuillaume Tucker 5*8f989b3bSGuillaume Tucker"""Containerized builds""" 6*8f989b3bSGuillaume Tucker 7*8f989b3bSGuillaume Tuckerimport abc 8*8f989b3bSGuillaume Tuckerimport argparse 9*8f989b3bSGuillaume Tuckerimport logging 10*8f989b3bSGuillaume Tuckerimport os 11*8f989b3bSGuillaume Tuckerimport pathlib 12*8f989b3bSGuillaume Tuckerimport shutil 13*8f989b3bSGuillaume Tuckerimport subprocess 14*8f989b3bSGuillaume Tuckerimport sys 15*8f989b3bSGuillaume Tuckerimport uuid 16*8f989b3bSGuillaume Tucker 17*8f989b3bSGuillaume Tucker 18*8f989b3bSGuillaume Tuckerclass ContainerRuntime(abc.ABC): 19*8f989b3bSGuillaume Tucker """Base class for a container runtime implementation""" 20*8f989b3bSGuillaume Tucker 21*8f989b3bSGuillaume Tucker name = None # Property defined in each implementation class 22*8f989b3bSGuillaume Tucker 23*8f989b3bSGuillaume Tucker def __init__(self, args, logger): 24*8f989b3bSGuillaume Tucker self._uid = args.uid or os.getuid() 25*8f989b3bSGuillaume Tucker self._gid = args.gid or args.uid or os.getgid() 26*8f989b3bSGuillaume Tucker self._env_file = args.env_file 27*8f989b3bSGuillaume Tucker self._shell = args.shell 28*8f989b3bSGuillaume Tucker self._logger = logger 29*8f989b3bSGuillaume Tucker 30*8f989b3bSGuillaume Tucker @classmethod 31*8f989b3bSGuillaume Tucker def is_present(cls): 32*8f989b3bSGuillaume Tucker """Determine whether the runtime is present on the system""" 33*8f989b3bSGuillaume Tucker return shutil.which(cls.name) is not None 34*8f989b3bSGuillaume Tucker 35*8f989b3bSGuillaume Tucker @abc.abstractmethod 36*8f989b3bSGuillaume Tucker def _do_run(self, image, cmd, container_name): 37*8f989b3bSGuillaume Tucker """Runtime-specific handler to run a command in a container""" 38*8f989b3bSGuillaume Tucker 39*8f989b3bSGuillaume Tucker @abc.abstractmethod 40*8f989b3bSGuillaume Tucker def _do_abort(self, container_name): 41*8f989b3bSGuillaume Tucker """Runtime-specific handler to abort a running container""" 42*8f989b3bSGuillaume Tucker 43*8f989b3bSGuillaume Tucker def run(self, image, cmd): 44*8f989b3bSGuillaume Tucker """Run a command in a runtime container""" 45*8f989b3bSGuillaume Tucker container_name = str(uuid.uuid4()) 46*8f989b3bSGuillaume Tucker self._logger.debug("container: %s", container_name) 47*8f989b3bSGuillaume Tucker try: 48*8f989b3bSGuillaume Tucker return self._do_run(image, cmd, container_name) 49*8f989b3bSGuillaume Tucker except KeyboardInterrupt: 50*8f989b3bSGuillaume Tucker self._logger.error("user aborted") 51*8f989b3bSGuillaume Tucker self._do_abort(container_name) 52*8f989b3bSGuillaume Tucker return 1 53*8f989b3bSGuillaume Tucker 54*8f989b3bSGuillaume Tucker 55*8f989b3bSGuillaume Tuckerclass CommonRuntime(ContainerRuntime): 56*8f989b3bSGuillaume Tucker """Common logic for Docker and Podman""" 57*8f989b3bSGuillaume Tucker 58*8f989b3bSGuillaume Tucker def _do_run(self, image, cmd, container_name): 59*8f989b3bSGuillaume Tucker cmdline = [self.name, 'run'] 60*8f989b3bSGuillaume Tucker cmdline += self._get_opts(container_name) 61*8f989b3bSGuillaume Tucker cmdline.append(image) 62*8f989b3bSGuillaume Tucker cmdline += cmd 63*8f989b3bSGuillaume Tucker self._logger.debug('command: %s', ' '.join(cmdline)) 64*8f989b3bSGuillaume Tucker return subprocess.call(cmdline) 65*8f989b3bSGuillaume Tucker 66*8f989b3bSGuillaume Tucker def _get_opts(self, container_name): 67*8f989b3bSGuillaume Tucker opts = [ 68*8f989b3bSGuillaume Tucker '--name', container_name, 69*8f989b3bSGuillaume Tucker '--rm', 70*8f989b3bSGuillaume Tucker '--volume', f'{pathlib.Path.cwd()}:/src', 71*8f989b3bSGuillaume Tucker '--workdir', '/src', 72*8f989b3bSGuillaume Tucker ] 73*8f989b3bSGuillaume Tucker if self._env_file: 74*8f989b3bSGuillaume Tucker opts += ['--env-file', self._env_file] 75*8f989b3bSGuillaume Tucker if self._shell: 76*8f989b3bSGuillaume Tucker opts += ['--interactive', '--tty'] 77*8f989b3bSGuillaume Tucker return opts 78*8f989b3bSGuillaume Tucker 79*8f989b3bSGuillaume Tucker def _do_abort(self, container_name): 80*8f989b3bSGuillaume Tucker subprocess.call([self.name, 'kill', container_name]) 81*8f989b3bSGuillaume Tucker 82*8f989b3bSGuillaume Tucker 83*8f989b3bSGuillaume Tuckerclass DockerRuntime(CommonRuntime): 84*8f989b3bSGuillaume Tucker """Run a command in a Docker container""" 85*8f989b3bSGuillaume Tucker 86*8f989b3bSGuillaume Tucker name = 'docker' 87*8f989b3bSGuillaume Tucker 88*8f989b3bSGuillaume Tucker def _get_opts(self, container_name): 89*8f989b3bSGuillaume Tucker return super()._get_opts(container_name) + [ 90*8f989b3bSGuillaume Tucker '--user', f'{self._uid}:{self._gid}' 91*8f989b3bSGuillaume Tucker ] 92*8f989b3bSGuillaume Tucker 93*8f989b3bSGuillaume Tucker 94*8f989b3bSGuillaume Tuckerclass PodmanRuntime(CommonRuntime): 95*8f989b3bSGuillaume Tucker """Run a command in a Podman container""" 96*8f989b3bSGuillaume Tucker 97*8f989b3bSGuillaume Tucker name = 'podman' 98*8f989b3bSGuillaume Tucker 99*8f989b3bSGuillaume Tucker def _get_opts(self, container_name): 100*8f989b3bSGuillaume Tucker return super()._get_opts(container_name) + [ 101*8f989b3bSGuillaume Tucker '--userns', f'keep-id:uid={self._uid},gid={self._gid}', 102*8f989b3bSGuillaume Tucker ] 103*8f989b3bSGuillaume Tucker 104*8f989b3bSGuillaume Tucker 105*8f989b3bSGuillaume Tuckerclass Runtimes: 106*8f989b3bSGuillaume Tucker """List of all supported runtimes""" 107*8f989b3bSGuillaume Tucker 108*8f989b3bSGuillaume Tucker runtimes = [PodmanRuntime, DockerRuntime] 109*8f989b3bSGuillaume Tucker 110*8f989b3bSGuillaume Tucker @classmethod 111*8f989b3bSGuillaume Tucker def get_names(cls): 112*8f989b3bSGuillaume Tucker """Get a list of all the runtime names""" 113*8f989b3bSGuillaume Tucker return list(runtime.name for runtime in cls.runtimes) 114*8f989b3bSGuillaume Tucker 115*8f989b3bSGuillaume Tucker @classmethod 116*8f989b3bSGuillaume Tucker def get(cls, name): 117*8f989b3bSGuillaume Tucker """Get a single runtime class matching the given name""" 118*8f989b3bSGuillaume Tucker for runtime in cls.runtimes: 119*8f989b3bSGuillaume Tucker if runtime.name == name: 120*8f989b3bSGuillaume Tucker if not runtime.is_present(): 121*8f989b3bSGuillaume Tucker raise ValueError(f"runtime not found: {name}") 122*8f989b3bSGuillaume Tucker return runtime 123*8f989b3bSGuillaume Tucker raise ValueError(f"unknown runtime: {name}") 124*8f989b3bSGuillaume Tucker 125*8f989b3bSGuillaume Tucker @classmethod 126*8f989b3bSGuillaume Tucker def find(cls): 127*8f989b3bSGuillaume Tucker """Find the first runtime present on the system""" 128*8f989b3bSGuillaume Tucker for runtime in cls.runtimes: 129*8f989b3bSGuillaume Tucker if runtime.is_present(): 130*8f989b3bSGuillaume Tucker return runtime 131*8f989b3bSGuillaume Tucker raise ValueError("no runtime found") 132*8f989b3bSGuillaume Tucker 133*8f989b3bSGuillaume Tucker 134*8f989b3bSGuillaume Tuckerdef _get_logger(verbose): 135*8f989b3bSGuillaume Tucker """Set up a logger with the appropriate level""" 136*8f989b3bSGuillaume Tucker logger = logging.getLogger('container') 137*8f989b3bSGuillaume Tucker handler = logging.StreamHandler() 138*8f989b3bSGuillaume Tucker handler.setFormatter(logging.Formatter( 139*8f989b3bSGuillaume Tucker fmt='[container {levelname}] {message}', style='{' 140*8f989b3bSGuillaume Tucker )) 141*8f989b3bSGuillaume Tucker logger.addHandler(handler) 142*8f989b3bSGuillaume Tucker logger.setLevel(logging.DEBUG if verbose is True else logging.INFO) 143*8f989b3bSGuillaume Tucker return logger 144*8f989b3bSGuillaume Tucker 145*8f989b3bSGuillaume Tucker 146*8f989b3bSGuillaume Tuckerdef main(args): 147*8f989b3bSGuillaume Tucker """Main entry point for the container tool""" 148*8f989b3bSGuillaume Tucker logger = _get_logger(args.verbose) 149*8f989b3bSGuillaume Tucker try: 150*8f989b3bSGuillaume Tucker cls = Runtimes.get(args.runtime) if args.runtime else Runtimes.find() 151*8f989b3bSGuillaume Tucker except ValueError as ex: 152*8f989b3bSGuillaume Tucker logger.error(ex) 153*8f989b3bSGuillaume Tucker return 1 154*8f989b3bSGuillaume Tucker logger.debug("runtime: %s", cls.name) 155*8f989b3bSGuillaume Tucker logger.debug("image: %s", args.image) 156*8f989b3bSGuillaume Tucker return cls(args, logger).run(args.image, args.cmd) 157*8f989b3bSGuillaume Tucker 158*8f989b3bSGuillaume Tucker 159*8f989b3bSGuillaume Tuckerif __name__ == '__main__': 160*8f989b3bSGuillaume Tucker parser = argparse.ArgumentParser( 161*8f989b3bSGuillaume Tucker 'container', 162*8f989b3bSGuillaume Tucker description="See the documentation for more details: " 163*8f989b3bSGuillaume Tucker "https://docs.kernel.org/dev-tools/container.html" 164*8f989b3bSGuillaume Tucker ) 165*8f989b3bSGuillaume Tucker parser.add_argument( 166*8f989b3bSGuillaume Tucker '-e', '--env-file', 167*8f989b3bSGuillaume Tucker help="Path to an environment file to load in the container." 168*8f989b3bSGuillaume Tucker ) 169*8f989b3bSGuillaume Tucker parser.add_argument( 170*8f989b3bSGuillaume Tucker '-g', '--gid', 171*8f989b3bSGuillaume Tucker help="Group ID to use inside the container." 172*8f989b3bSGuillaume Tucker ) 173*8f989b3bSGuillaume Tucker parser.add_argument( 174*8f989b3bSGuillaume Tucker '-i', '--image', required=True, 175*8f989b3bSGuillaume Tucker help="Container image name." 176*8f989b3bSGuillaume Tucker ) 177*8f989b3bSGuillaume Tucker parser.add_argument( 178*8f989b3bSGuillaume Tucker '-r', '--runtime', choices=Runtimes.get_names(), 179*8f989b3bSGuillaume Tucker help="Container runtime name. If not specified, the first one found " 180*8f989b3bSGuillaume Tucker "on the system will be used i.e. Podman if present, otherwise Docker." 181*8f989b3bSGuillaume Tucker ) 182*8f989b3bSGuillaume Tucker parser.add_argument( 183*8f989b3bSGuillaume Tucker '-s', '--shell', action='store_true', 184*8f989b3bSGuillaume Tucker help="Run the container in an interactive shell." 185*8f989b3bSGuillaume Tucker ) 186*8f989b3bSGuillaume Tucker parser.add_argument( 187*8f989b3bSGuillaume Tucker '-u', '--uid', 188*8f989b3bSGuillaume Tucker help="User ID to use inside the container. If the -g option is not " 189*8f989b3bSGuillaume Tucker "specified, the user ID will also be set as the group ID." 190*8f989b3bSGuillaume Tucker ) 191*8f989b3bSGuillaume Tucker parser.add_argument( 192*8f989b3bSGuillaume Tucker '-v', '--verbose', action='store_true', 193*8f989b3bSGuillaume Tucker help="Enable verbose output." 194*8f989b3bSGuillaume Tucker ) 195*8f989b3bSGuillaume Tucker parser.add_argument( 196*8f989b3bSGuillaume Tucker 'cmd', nargs='+', 197*8f989b3bSGuillaume Tucker help="Command to run in the container" 198*8f989b3bSGuillaume Tucker ) 199*8f989b3bSGuillaume Tucker sys.exit(main(parser.parse_args(sys.argv[1:]))) 200