From 352df0d24aff4cd797da4cf25026af131845be48 Mon Sep 17 00:00:00 2001 From: Ryan Roberts Date: Wed, 20 Mar 2024 16:25:26 +0000 Subject: [PATCH 1/7] runtime: On MacOS run as root in docker container Previously tuxmake was erroneously invoking docker with --user=:, on MacOS, which was not a valid username in the container image. Fix this by running as root. This required some dynamic patching to tuxmake. MacOS uses GIDs that overlap with already defined GIDs in the container so we can't just bind the macos host UID/GID to the shrinkwrap user in the container, like we do for Linux. This concept doesn't really work anyway, because on MacOS the container is running on a completely separate (linux) kernel in a VM. Fortunately docker maps the VM to the current MacOS user when touching mapped volumes so it all works out. So on MacOS run as root. Unfortunately, tuxmake tries to be too clever (it assumes a linux host) and tries to map the in-container user to the host UID/GID. This fails when the in-container user is root. So we have this ugly workaround to override the user-opts with nothing. By passing nothing, we implicitly run as root and tuxmake doesn't try to run usermod. Signed-off-by: Ryan Roberts --- shrinkwrap/utils/runtime.py | 31 ++++++++++++++++++++++++------- 1 file changed, 24 insertions(+), 7 deletions(-) diff --git a/shrinkwrap/utils/runtime.py b/shrinkwrap/utils/runtime.py index da43bd4..d8f98f2 100644 --- a/shrinkwrap/utils/runtime.py +++ b/shrinkwrap/utils/runtime.py @@ -5,11 +5,16 @@ import os import subprocess import sys import tuxmake.runtime +import types _stack = [] +def get_null_user_opts(self): + return [] + + class Runtime: """ Wraps tuxmake.runtime to provide an interface for executing commands in @@ -25,13 +30,25 @@ class Runtime: self._rt = tuxmake.runtime.Runtime.get(name) self._rt.set_image(image) - if not sys.platform.startswith('darwin'): - # Macos uses GIDs that overlap with already defined GIDs - # in the container so this fails. However, it appears - # that on macos, if we run as root in the container, any - # generated files on the host filesystem are still owned - # my the real macos user, so it seems we don't need this - # UID/GID fixup in the first place on macos. + + # MacOS uses GIDs that overlap with already defined GIDs in the + # container so we can't just bind the macos host UID/GID to the + # shrinkwrap user in the container. This concept doesn't really + # work anyway, because on MacOS the container is running on a + # completely separate (linux) kernel in a VM. Fortunately docker + # maps the VM to the current MacOS user when touching mapped + # volumes so it all works out. So on MacOS run as root. + # Unfortunately, tuxmake tries to be too clever (it assumes a + # linux host) and tries to map the in-container user to the host + # UID/GID. This fails when the in-container user is root. So we + # have this ugly workaround to override the user-opts with + # nothing. By passing nothing, we implicitly run as root and + # tuxmake doesn't try to run usermod. + if sys.platform.startswith('darwin') and \ + self._rt.name.startswith('docker'): + self._rt.get_user_opts = \ + types.MethodType(get_null_user_opts, self._rt) + else: self._rt.set_user('shrinkwrap') self._rt.set_group('shrinkwrap') -- GitLab From e46dac5788db785d95d3c2c79dabd144b087b7f6 Mon Sep 17 00:00:00 2001 From: Gareth Stockwell Date: Mon, 18 Mar 2024 15:37:48 +0000 Subject: [PATCH 2/7] runtime: enforce use of keyword arguments This prepares the function for subsequent patches, which add further optional arguments. Signed-off-by: Gareth Stockwell --- shrinkwrap/commands/buildall.py | 2 +- shrinkwrap/commands/clean.py | 2 +- shrinkwrap/commands/run.py | 2 +- shrinkwrap/utils/runtime.py | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/shrinkwrap/commands/buildall.py b/shrinkwrap/commands/buildall.py index 916a4ac..f98fc1e 100644 --- a/shrinkwrap/commands/buildall.py +++ b/shrinkwrap/commands/buildall.py @@ -130,7 +130,7 @@ def build(configs, btvarss, nosync, args): # Run under a runtime environment, which may just run commands # natively on the host or may execute commands in a container, # depending on what the user specified. - with runtime.Runtime(args.runtime, args.image) as rt: + with runtime.Runtime(name=args.runtime, image=args.image) as rt: def add_volume(path, levels_up=0): while levels_up: path = os.path.dirname(path) diff --git a/shrinkwrap/commands/clean.py b/shrinkwrap/commands/clean.py index 0ffafa1..d74bdbd 100644 --- a/shrinkwrap/commands/clean.py +++ b/shrinkwrap/commands/clean.py @@ -112,7 +112,7 @@ def dispatch(args): # Run under a runtime environment, which may just run commands # natively on the host or may execute commands in a container, # depending on what the user specified. - with runtime.Runtime(args.runtime, args.image) as rt: + with runtime.Runtime(name=args.runtime, image=args.image) as rt: def add_volume(path, levels_up=0): while levels_up: path = os.path.dirname(path) diff --git a/shrinkwrap/commands/run.py b/shrinkwrap/commands/run.py index 7bc9522..7c191d5 100644 --- a/shrinkwrap/commands/run.py +++ b/shrinkwrap/commands/run.py @@ -228,7 +228,7 @@ def dispatch(args): # Run under a runtime environment, which may just run commands natively # on the host or may execute commands in a container, depending on what # the user specified. - with runtime.Runtime(args.runtime, args.image) as rt: + with runtime.Runtime(name=args.runtime, image=args.image) as rt: for rtvar in resolver['run']['rtvars'].values(): if rtvar['type'] == 'path': rt.add_volume(rtvar['value']) diff --git a/shrinkwrap/utils/runtime.py b/shrinkwrap/utils/runtime.py index d8f98f2..974621d 100644 --- a/shrinkwrap/utils/runtime.py +++ b/shrinkwrap/utils/runtime.py @@ -23,7 +23,7 @@ class Runtime: host. The 'docker', 'docker-local', 'podman' and 'podman-local' runtimes execute the commands in a container. """ - def __init__(self, name, image=None, modal=True): + def __init__(self, *, name, image=None, modal=True): self._modal = modal self._rt = None self._mountpoints = set() -- GitLab From c6987cfdda0265a6620850417ff77fb46061d789 Mon Sep 17 00:00:00 2001 From: Gareth Stockwell Date: Mon, 18 Mar 2024 15:41:01 +0000 Subject: [PATCH 3/7] runtime: remove unused "modal" argument Signed-off-by: Gareth Stockwell --- shrinkwrap/utils/runtime.py | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/shrinkwrap/utils/runtime.py b/shrinkwrap/utils/runtime.py index 974621d..ef59ec0 100644 --- a/shrinkwrap/utils/runtime.py +++ b/shrinkwrap/utils/runtime.py @@ -23,8 +23,7 @@ class Runtime: host. The 'docker', 'docker-local', 'podman' and 'podman-local' runtimes execute the commands in a container. """ - def __init__(self, *, name, image=None, modal=True): - self._modal = modal + def __init__(self, *, name, image=None): self._rt = None self._mountpoints = set() @@ -52,8 +51,7 @@ class Runtime: self._rt.set_user('shrinkwrap') self._rt.set_group('shrinkwrap') - if self._modal: - _stack.append(self) + _stack.append(self) def start(self): for mp in self._mountpoints: @@ -123,9 +121,8 @@ print(ip) if self._rt: self._rt.cleanup() self._rt = None - if self._modal: - s = _stack.pop() - assert(s == self) + s = _stack.pop() + assert(s == self) def __enter__(self): return self @@ -136,8 +133,7 @@ print(ip) def get(): """ - Returns the current modal Runtime instance. At least one Runtime - instance must be living that was created with modal=True. + Returns the current Runtime instance. """ assert(len(_stack) > 0) return _stack[-1] -- GitLab From 1db05f5ab4e3cb5743610f7a043361c54e48d057 Mon Sep 17 00:00:00 2001 From: Gareth Stockwell Date: Tue, 19 Mar 2024 15:46:06 +0000 Subject: [PATCH 4/7] runtime: replace modal stack with singleton Signed-off-by: Gareth Stockwell --- shrinkwrap/utils/runtime.py | 27 ++++++++++++--------------- 1 file changed, 12 insertions(+), 15 deletions(-) diff --git a/shrinkwrap/utils/runtime.py b/shrinkwrap/utils/runtime.py index ef59ec0..cd39613 100644 --- a/shrinkwrap/utils/runtime.py +++ b/shrinkwrap/utils/runtime.py @@ -8,7 +8,7 @@ import tuxmake.runtime import types -_stack = [] +_instance = None def get_null_user_opts(self): @@ -51,8 +51,6 @@ class Runtime: self._rt.set_user('shrinkwrap') self._rt.set_group('shrinkwrap') - _stack.append(self) - def start(self): for mp in self._mountpoints: self._rt.add_volume(mp) @@ -116,27 +114,26 @@ print(ip) if res.returncode == 0: return res.stdout.strip() return '127.0.0.1' - - def cleanup(self): - if self._rt: - self._rt.cleanup() - self._rt = None - s = _stack.pop() - assert(s == self) - + def __enter__(self): + global _instance + assert _instance is None + _instance = self return self - + def __exit__(self, exc_type, exc_val, exc_tb): - self.cleanup() + global _instance + assert _instance == self + _instance = None def get(): """ Returns the current Runtime instance. """ - assert(len(_stack) > 0) - return _stack[-1] + global _instance + assert _instance is not None + return _instance def mkcmd(cmd, interactive=False): -- GitLab From 232912dbe930130558c143a586dbc1494e3b84c9 Mon Sep 17 00:00:00 2001 From: Gareth Stockwell Date: Mon, 18 Mar 2024 15:35:49 +0000 Subject: [PATCH 5/7] docker: add SSH client Signed-off-by: Gareth Stockwell --- docker/Dockerfile.slim | 1 + 1 file changed, 1 insertion(+) diff --git a/docker/Dockerfile.slim b/docker/Dockerfile.slim index 6e6a5ca..cad1760 100644 --- a/docker/Dockerfile.slim +++ b/docker/Dockerfile.slim @@ -29,6 +29,7 @@ ENV PATH="/pyvenv/bin:${PATH}" # Install packages that Shrinkwrap relies upon. RUN apt-get install --assume-yes --no-install-recommends --option=debug::pkgProblemResolver=yes \ netcat-openbsd \ + openssh-client \ python3 \ python3-pip \ telnet \ -- GitLab From dd420c1a07f9499c0c351537a0b5a94504e71a93 Mon Sep 17 00:00:00 2001 From: Gareth Stockwell Date: Tue, 19 Mar 2024 09:52:38 +0000 Subject: [PATCH 6/7] docker: suppress confirmation request for unknown ssh hosts Signed-off-by: Gareth Stockwell --- docker/Dockerfile.slim | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docker/Dockerfile.slim b/docker/Dockerfile.slim index cad1760..5603910 100644 --- a/docker/Dockerfile.slim +++ b/docker/Dockerfile.slim @@ -185,5 +185,8 @@ RUN cd /tools \ ENV TCH_PATH_AARCH64="/tools/${TCH_PATH_AARCH64}" ENV PATH="${TCH_PATH_AARCH64}:${PATH}" +# Set ssh options used by git. +ENV GIT_SSH_COMMAND="ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no" + # Create a user. RUN useradd --create-home shrinkwrap -- GitLab From 9528129cf0aff9d40ae8b391fd6bfac252fcef39 Mon Sep 17 00:00:00 2001 From: Gareth Stockwell Date: Mon, 18 Mar 2024 15:42:37 +0000 Subject: [PATCH 7/7] Add support for ssh-agent This allows GIT over SSH to work, including when using a container runtime. * If the user already has an ssh-agent process running (indicated by the SSH_AUTH_SOCK environment variable being set), map this socket into the container. * If the user does not have an ssh-agent process running, or wishes to expose only a subset of their keys to the container, a new --ssh-agent option causes shrinkwrap to start an ssh-agent subprocess, and to add specified keys. As above, the socket on which the ssh-agent is listening is mapped into the container. Signed-off-by: Gareth Stockwell --- docker/Dockerfile.slim | 3 + documentation/userguide/recipes.rst | 26 ++++++++ shrinkwrap/commands/buildall.py | 3 +- shrinkwrap/commands/clean.py | 3 +- shrinkwrap/commands/run.py | 3 +- shrinkwrap/shrinkwrap.py | 18 ++++++ shrinkwrap/utils/runtime.py | 23 ++++++- shrinkwrap/utils/ssh_agent.py | 98 +++++++++++++++++++++++++++++ 8 files changed, 171 insertions(+), 6 deletions(-) create mode 100644 shrinkwrap/utils/ssh_agent.py diff --git a/docker/Dockerfile.slim b/docker/Dockerfile.slim index 5603910..c6bf3f6 100644 --- a/docker/Dockerfile.slim +++ b/docker/Dockerfile.slim @@ -188,5 +188,8 @@ ENV PATH="${TCH_PATH_AARCH64}:${PATH}" # Set ssh options used by git. ENV GIT_SSH_COMMAND="ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no" +# Set ssh-agent socket path. +ENV SSH_AUTH_SOCK=/run/host-services/ssh-auth.sock + # Create a user. RUN useradd --create-home shrinkwrap diff --git a/documentation/userguide/recipes.rst b/documentation/userguide/recipes.rst index 8c39cc8..78fd857 100644 --- a/documentation/userguide/recipes.rst +++ b/documentation/userguide/recipes.rst @@ -264,3 +264,29 @@ This is equivalent: .. code-block:: shell shrinkwrap run ns-edk2.yaml --rtvar=KERNEL=path/to/Image --overlay='{"run":{"params":{"-C bp.pl011_uart0.out_file":"uart0.log"}}}' + +**************** +Provide SSH keys +**************** + +The GIT subprocess used by Shrinkwrap to synchronise source code may require +access to SSH keys. This is supported, via ``ssh-agent``. + +If ``ssh-agent`` is already running, it will be automatically detected (via +the ``SSH_AUTH_SOCK`` environment variable) and used by Shrinkwrap. + +Alternatively, the ``--ssh-agent`` option can be used to request Shrinkwrap +to start an ``ssh-agent`` subprocess and add default keys. + +.. code-block:: shell + + shrinkwrap --ssh-agent build ... + +In order to add only specified keys, the ``--ssh-agent-key`` can be used. + +.. code-block:: shell + + shrinkwrap \ + --ssh-agent-key ~/.ssh/my-first-key \ + --ssh-agent-key ~/.ssh/my-second-key \ + build ... diff --git a/shrinkwrap/commands/buildall.py b/shrinkwrap/commands/buildall.py index f98fc1e..012cf69 100644 --- a/shrinkwrap/commands/buildall.py +++ b/shrinkwrap/commands/buildall.py @@ -130,7 +130,8 @@ def build(configs, btvarss, nosync, args): # Run under a runtime environment, which may just run commands # natively on the host or may execute commands in a container, # depending on what the user specified. - with runtime.Runtime(name=args.runtime, image=args.image) as rt: + with runtime.Runtime(name=args.runtime, image=args.image, + ssh_agent_keys=args.ssh_agent_keys) as rt: def add_volume(path, levels_up=0): while levels_up: path = os.path.dirname(path) diff --git a/shrinkwrap/commands/clean.py b/shrinkwrap/commands/clean.py index d74bdbd..cc50b68 100644 --- a/shrinkwrap/commands/clean.py +++ b/shrinkwrap/commands/clean.py @@ -112,7 +112,8 @@ def dispatch(args): # Run under a runtime environment, which may just run commands # natively on the host or may execute commands in a container, # depending on what the user specified. - with runtime.Runtime(name=args.runtime, image=args.image) as rt: + with runtime.Runtime(name=args.runtime, image=args.image, + ssh_agent_keys=args.ssh_agent_keys) as rt: def add_volume(path, levels_up=0): while levels_up: path = os.path.dirname(path) diff --git a/shrinkwrap/commands/run.py b/shrinkwrap/commands/run.py index 7c191d5..9c566ec 100644 --- a/shrinkwrap/commands/run.py +++ b/shrinkwrap/commands/run.py @@ -228,7 +228,8 @@ def dispatch(args): # Run under a runtime environment, which may just run commands natively # on the host or may execute commands in a container, depending on what # the user specified. - with runtime.Runtime(name=args.runtime, image=args.image) as rt: + with runtime.Runtime(name=args.runtime, image=args.image, + ssh_agent_keys=args.ssh_agent_keys) as rt: for rtvar in resolver['run']['rtvars'].values(): if rtvar['type'] == 'path': rt.add_volume(rtvar['value']) diff --git a/shrinkwrap/shrinkwrap.py b/shrinkwrap/shrinkwrap.py index 1b32e62..dbfcf74 100755 --- a/shrinkwrap/shrinkwrap.py +++ b/shrinkwrap/shrinkwrap.py @@ -83,6 +83,21 @@ def main(): help="""If using a container runtime, specifies the name of the image to use. Defaults to the official shrinkwrap image.""") + parser.add_argument('--ssh-agent', + default=False, + action='store_true', + required=False, + help="""Start ssh-agent and add default keys.""") + + parser.add_argument('--ssh-agent-key', + dest='ssh_agent_keys', + default=[], + metavar='key', + action='append', + type=str, + required=False, + help="""Start ssh-agent and add specified key.""") + subparsers = parser.add_subparsers(dest='command', metavar='', title=f'Supported commands (run ' @@ -102,6 +117,9 @@ def main(): args = parser.parse_args() config_verbose_flag(args) + if args.ssh_agent: + args.ssh_agent_keys.append(None) + # Dispatch to the correct command. if args.command in cmds: cmds[args.command].dispatch(args) diff --git a/shrinkwrap/utils/runtime.py b/shrinkwrap/utils/runtime.py index cd39613..9b564c0 100644 --- a/shrinkwrap/utils/runtime.py +++ b/shrinkwrap/utils/runtime.py @@ -6,6 +6,10 @@ import subprocess import sys import tuxmake.runtime import types +import shrinkwrap.utils.ssh_agent as ssh_agent_lib + +_SSH_AUTH_SOCK = '/run/host-services/ssh-auth.sock' +"""Path to ssh-agent socket.""" _instance = None @@ -23,13 +27,16 @@ class Runtime: host. The 'docker', 'docker-local', 'podman' and 'podman-local' runtimes execute the commands in a container. """ - def __init__(self, *, name, image=None): + def __init__(self, *, name, image=None, ssh_agent_keys=None): self._rt = None self._mountpoints = set() self._rt = tuxmake.runtime.Runtime.get(name) self._rt.set_image(image) + is_mac = sys.platform.startswith('darwin') + is_docker = name.startswith('docker') + # MacOS uses GIDs that overlap with already defined GIDs in the # container so we can't just bind the macos host UID/GID to the # shrinkwrap user in the container. This concept doesn't really @@ -43,14 +50,24 @@ class Runtime: # have this ugly workaround to override the user-opts with # nothing. By passing nothing, we implicitly run as root and # tuxmake doesn't try to run usermod. - if sys.platform.startswith('darwin') and \ - self._rt.name.startswith('docker'): + if is_mac and is_docker: self._rt.get_user_opts = \ types.MethodType(get_null_user_opts, self._rt) else: self._rt.set_user('shrinkwrap') self._rt.set_group('shrinkwrap') + for key in ssh_agent_keys: + ssh_agent_lib.add(key) + + socket = ssh_agent_lib.socket() + + if name != 'null' and socket is not None: + if is_mac: + socket = _SSH_AUTH_SOCK + + self._rt.add_volume(socket, _SSH_AUTH_SOCK) + def start(self): for mp in self._mountpoints: self._rt.add_volume(mp) diff --git a/shrinkwrap/utils/ssh_agent.py b/shrinkwrap/utils/ssh_agent.py new file mode 100644 index 0000000..422228e --- /dev/null +++ b/shrinkwrap/utils/ssh_agent.py @@ -0,0 +1,98 @@ +# Copyright (c) 2024, Arm Limited. +# SPDX-License-Identifier: MIT + +import atexit +import os +import re +import subprocess +from typing import Optional + +import shrinkwrap.utils.logger as logger + + +_logger = logger.Logger(27) +"""Logger used within this module.""" + + +def _log(msg): + """Print a message to the console.""" + _logger.print(msg, tag='ssh-agent', cont=False) + + +_proc = None +"""ssh-agent process started by this module.""" + + +def _start(): + """Start an ssh-agent process.""" + global _proc + + assert _proc is None + _proc = subprocess.run( + ['ssh-agent', '-s'], stdout=subprocess.PIPE, text=True) + + # Extract socket path and PID from ssh-agent output + output = _proc.stdout + match = re.search( + r'SSH_AUTH_SOCK=(?P[^;]+).*SSH_AGENT_PID=(?P[^;]+)', + output, + re.DOTALL | re.MULTILINE) + + if not match: + raise Exception(f'Failed to parse ssh-agent output: {output}') + + values = match.groupdict() + + os.environ['SSH_AUTH_SOCK'] = values['sock'] + os.environ['SSH_AGENT_PID'] = values['pid'] + + _log(f'Started ssh-agent pid {values["pid"]}') + + # Register a handler to kill the ssh-agent process + atexit.register(_stop) + + +def _stop(): + """Stop the ssh-agent process.""" + global _proc + + assert _proc is not None + + _log('Stopping ssh-agent') + + subprocess.check_call(['ssh-agent', '-k'], stdout=subprocess.DEVNULL) + + del os.environ['SSH_AUTH_SOCK'] + del os.environ['SSH_AGENT_PID'] + + _proc = None + + +def socket() -> Optional[str]: + """Return path to socket.""" + return os.environ.get('SSH_AUTH_SOCK') + + +def add(key: Optional[str]=None) -> None: + """ + Add specified key, or add default keys if key is None. + + If no ssh-agent process is running, one is started. + """ + if socket() is None: + _start() + else: + global _proc + if _proc is None: + raise Exception('Adding a key to an ssh-agent not owned by shrinkwrap is not supported.') + + if key is None: + _log('Adding default keys') + else: + _log(f'Adding key {key}') + + args = ['ssh-add'] + if key is not None: + args.append(key) + + subprocess.check_call(args) -- GitLab