Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 15 additions & 8 deletions charms/garm/src/garm_template.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,23 @@

from charm_state import SSHDebugInfo
from garm_api import GarmApiError, GarmAuthenticatedClient
from runner_paths import RUNNER_ENV_PATH, prepend_after_shebang
from runner_paths import RUNNER_ENV_PATH, RUNNER_USER, prepend_after_shebang

logger = logging.getLogger(__name__)

GARM_BASE_TEMPLATE_NAME: typing.Final[str] = "github_linux"
GARM_CHARMED_TEMPLATE_NAME: typing.Final[str] = "github_linux_charmed"

# GARM's agent mode starts the runner from a system service with no login session,
# so jobs get no XDG_RUNTIME_DIR and tools keeping per-user state there (skopeo,
# podman, buildah) fall back to root-owned /run/containers. Lingering keeps
# /run/user/<uid> alive without a session; the .env entry exports it to every job.
_RUNNER_RUNTIME_DIR_SNIPPET: typing.Final[str] = f"""\
sudo loginctl enable-linger {RUNNER_USER} || true
mkdir -p $(dirname {RUNNER_ENV_PATH})
echo "XDG_RUNTIME_DIR=/run/user/$(id -u {RUNNER_USER})" >> {RUNNER_ENV_PATH}
"""


class CharmedTemplateError(Exception):
"""Raised when charmed template management fails; message is used as the unit status."""
Expand All @@ -28,10 +38,9 @@ def apply_charmed_template(
"""Create or update the charmed runner install template and return its id.

Derives ``github_linux_charmed`` from the built-in ``github_linux`` base by
prepending shell snippets. The debug-ssh snippet (tmate env vars) is added
only when connections are present; with none the charmed template is still
maintained as a snippet-free copy of the base so scalesets can reference it
unconditionally (future snippets attach the same way).
prepending shell snippets. The runner runtime-directory setup is always added;
the debug-ssh snippet (tmate env vars) only when connections are present.
Scalesets reference the charmed template unconditionally.

Args:
client: Authenticated GARM API client.
Expand Down Expand Up @@ -119,9 +128,7 @@ def _build_charmed_template_data(

# GARM returns the template body as a base64-encoded string.
base_script = base64.b64decode(base_template.data).decode("utf-8")
snippet = build_tmate_env_snippet(connections)
if not snippet:
return base_script.encode("utf-8")
snippet = _RUNNER_RUNTIME_DIR_SNIPPET + build_tmate_env_snippet(connections)
return prepend_after_shebang(base_script, snippet).encode("utf-8")


Expand Down
24 changes: 24 additions & 0 deletions charms/garm/tests/unit/test_garm_template.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import garm_template
from charm_state import SSHDebugInfo
from garm_api import GarmApiError
from runner_paths import RUNNER_ENV_PATH

_CONN = SSHDebugInfo(host="h", port=10022, rsa_fingerprint="rsa-fp", ed25519_fingerprint="ed-fp")

Expand Down Expand Up @@ -246,6 +247,29 @@ def test_build_charmed_template_data_prepends_snippet_after_shebang():
assert "echo hello" in decoded


def test_build_charmed_template_data_sets_up_runner_runtime_dir():
"""
arrange: get_template returns a base script; no debug-ssh connections.
act: Call _build_charmed_template_data().
assert: The runner user gets lingering and XDG_RUNTIME_DIR in the runner .env, so
jobs of a runner started without a login session can use tools that keep
per-user state there (skopeo/podman login), and it precedes the base body.
"""
client = MagicMock()
base_script = "#!/bin/bash\necho hello\n"
template = MagicMock()
template.data = base64.b64encode(base_script.encode()).decode()
client.get_template.return_value = template

result = garm_template._build_charmed_template_data(client, 1, [])

decoded = result.decode()
assert decoded.startswith("#!/bin/bash\n")
assert "sudo loginctl enable-linger runner" in decoded

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

runner or ubuntu?

assert f'XDG_RUNTIME_DIR=/run/user/$(id -u runner)" >> {RUNNER_ENV_PATH}' in decoded
assert decoded.index("enable-linger") < decoded.index("echo hello")


_PATCHED = b"patched-data"


Expand Down
4 changes: 4 additions & 0 deletions docs/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).

Each revision is versioned by the date of the revision.

## 2026-10-08

- `garm`: set `XDG_RUNTIME_DIR` for runner jobs. Runners start without a login session, so tools that keep per-user state there, such as `skopeo login`, `podman` and `buildah`, failed with `mkdir /run/containers: permission denied`.

## 2026-10-07

- `garm`: bump the GARM workload so that scale set labels longer than 64 characters work on PostgreSQL. GARM stored labels in a `varchar(64)` column, so such a label failed with `value too long for type character varying(64)`; the column is now `varchar(255)`, and GARM migrates existing databases on startup.
Expand Down
Loading