liab-deployments/liab_deployments/deploy/forgejo_runner.py
Michael Hanke 8d31238bc2
feat: allow for podman network configurability in runner deployment
The concrete need was to enable a runner deployment on the same host as
a forgejo deployment, but the VM network setup did not allow podman's
default setup to reach the host's service from the container.

This change allows for adding arbitrary podman options. The can also be
given as host defaults for multiple deployments.
2026-03-16 08:26:40 +01:00

204 lines
5.8 KiB
Python

#
# Deploy Forgejo runner (https://forgejo.org/docs/latest/admin/actions/runner-installation)
#
# This setup is based on the "offline registration" approach described
# in the forgejo docs. This creates a registration using a known 40-char
# secret that is used, both, at the forgejo site _and_ the runner.
# The example below, e.g., requires that the following command was executed
# at the respective forgejo site in order to make the registration possible.#
#
# [podman exec forgejo] forgejo forgejo-cli actions register --name <name> --secret <40char-random-hex-encoded>
#
# An example inventory may look like this:
#
# group = [
# (
# 'edu.datalad.org',
# {
# 'forgejo_runner': {
# # some settings support global defaults for homogeneity
# 'defaults': {
# 'user': ('forgejo-runner', '3000'),
# 'runner_site_name': 'edu',
# 'container_tag': 'data.forgejo.org/forgejo/runner:11',
# 'podman_network_opts': '--add-host 'host.containers.internal:xxx.xxx.xxx.xxx',
# },
# 'runners': [
# {
# 'name': 'git.example.org',
# 'forgejo_instance': 'https://git.example.org',
# 'config_file_asset': 'assets/example-forgejo-runner-config.yaml',
# 'secret': 'cf67f6e55b9c39256f30dca65af3657552eb65b3',
# # global scope when none is defined
# 'scope': 'exampleorg',
# #'runner_runtime_dir': '/bigdrive/runner',
# },
# ],
# },
# ...
#
from pyinfra.api import deploy
from pyinfra import (
host,
)
from pyinfra.operations import (
files,
server,
)
from liab_deployments.operations import (
user,
user_systemd,
)
service_unit_tmpl = """\
[Unit]
Description=Podman Forgejo runner service
Wants=network-online.target
After=network-online.target
RequiresMountsFor=%t/containers
[Service]
Environment=PODMAN_SYSTEMD_UNIT=%n
Restart=always
TimeoutStopSec=70
ExecStartPre=/bin/rm \\
-f %t/%n.ctr-id
ExecStart=/usr/bin/podman run \\
--cidfile=%t/%n.ctr-id \\
--cgroups=no-conmon \\
--rm \\
--sdnotify=conmon \\
--replace \\
--name {name} \\
--detach \\
--user 0:0 \\
-v {runner_runtime_dir}:/data \\
-v /run/user/{uid}/podman/podman.sock:/var/run/docker.sock:rw \\
-e DOCKER_HOST=unix:///var/run/docker.sock \\
{podman_network_opts} \\
{container_tag} \\
forgejo-runner \\
-c /data/config/config.yml \\
daemon
ExecStop=/usr/bin/podman stop \\
--ignore -t 10 \\
--cidfile=%t/%n.ctr-id
ExecStopPost=/usr/bin/podman rm \\
-f \
--ignore -t 10 \
--cidfile=%t/%n.ctr-id
Type=notify
NotifyAccess=all
[Install]
WantedBy=default.target
"""
@deploy("Deploy Forgejo runner")
def deploy_runners():
if not hasattr(host.data, 'forgejo_runner'):
return
defaults = host.data.forgejo_runner.get('defaults', {})
for spec in host.data.forgejo_runner.get('runners', []):
_deploy_runner(
spec['container_tag']
if 'container_tag' in spec
else defaults['container_tag'],
spec['user']
if 'user' in spec
else defaults['user'],
spec['config_file_asset']
if 'config_file_asset' in spec
else defaults['config_file_asset'],
spec['secret'],
spec['forgejo_instance'],
name=spec.get('name', 'forgejo_runner'),
scope=spec.get('scope', None),
runner_site_name=spec['runner_site_name']
if 'runner_site_name' in spec
else defaults.get('runner_site_name', None),
podman_network_opts=spec.get(
'podman_network_opts',
defaults.get('podman_network_opts')),
)
def _deploy_runner(
container_tag: str,
user_spec: tuple[str, int],
config_file_asset,
secret: str,
forgejo_instance,
*,
name,
runner_runtime_dir=None,
scope=None,
runner_site_name=None,
podman_network_opts: str | None = None,
):
user_name, uid = user_spec
user_home = f'/home/{user_name}'
runner_runtime_dir = runner_runtime_dir or f'{user_home}/{name}'
runner_site_name = runner_site_name or 'runner'
user.systemd_service(
user_name,
uid,
user_home,
)
files.directory(
path=runner_runtime_dir,
present=True,
_sudo_user=user_name,
)
user_systemd.service_unit(
user_name,
user_home,
name,
service_unit_tmpl.format(
name=name,
user_home=user_home,
uid=uid,
runner_runtime_dir=runner_runtime_dir,
container_tag=container_tag,
podman_network_opts=podman_network_opts or '',
),
)
files.put(
name=f'Forgejo runner {name!r} config',
src=config_file_asset,
dest=f'{runner_runtime_dir}/config/config.yml',
_sudo_user=user_name,
)
user_systemd.run_service(
user_name,
uid,
'podman.socket',
)
server.shell(
name=f"Register {name!r}",
commands=[
'podman run --rm --user 0:0'
f' -v "{runner_runtime_dir}:/data"'
f' {container_tag}'
f' forgejo-runner create-runner-file'
f' --instance "{forgejo_instance}"'
f' --name "{runner_site_name}"'
f' --secret "{secret}"'
],
_chdir=user_home,
_sudo_user=user_name,
)
user_systemd.run_service(
user_name,
uid,
name,
)