xref: /linux/scripts/container (revision df989b01b5f97dae8f9869cfacbda1308f2182c1)
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