From 0187c52134bf54d3dc18ce456881a99820a4ddab Mon Sep 17 00:00:00 2001 From: Joseph Mearman Date: Mon, 2 Nov 2026 19:27:48 +0000 Subject: [PATCH 1/2] feat(cluster): read a Colima profile's home from its Docker context A filter that returns the COLIMA_HOME a profile needs from the endpoint of the Docker context Colima made for it, since Colima puts the profile's socket at //docker.sock. It is empty for Colima's own default home, and refuses an endpoint that is not that socket. --- plugins/filter/container_runtime.py | 34 ++++++++++++++++++++- tests/unit/test_container_runtime.py | 44 ++++++++++++++++++++++++++++ 2 files changed, 77 insertions(+), 1 deletion(-) diff --git a/plugins/filter/container_runtime.py b/plugins/filter/container_runtime.py index 79e6ddbe..11876ebb 100644 --- a/plugins/filter/container_runtime.py +++ b/plugins/filter/container_runtime.py @@ -2,6 +2,7 @@ from __future__ import annotations +import posixpath import re from typing import Any @@ -11,6 +12,11 @@ # Colima names the Docker context of its default profile "colima" and any other profile's "colima-". COLIMA_CONTEXT = "colima" COLIMA_DEFAULT_PROFILE = "default" +# Colima's default home, which it falls back to when COLIMA_HOME is not set, relative to the user's home directory. +COLIMA_DEFAULT_HOME = ".colima" +# The Docker socket Colima creates in a profile's directory, //docker.sock, and so the endpoint of the Docker context it creates for the profile. +COLIMA_SOCKET = "docker.sock" +UNIX_SCHEME = "unix://" # Docker Desktop's own context for its Linux engine. DOCKER_DESKTOP_CONTEXTS = ("desktop-linux",) # Lima's rule for an instance name, which is what a Colima profile becomes. It also keeps the value free of every character that would need escaping in a launchd property list or on a command line. @@ -66,9 +72,35 @@ def cluster_container_runtime(context: Any, requested: Any = "auto", colima_prof return {"kind": "colima", "colima_profile": profile or context_profile or COLIMA_DEFAULT_PROFILE} +def cluster_colima_home(endpoint: Any, profile: Any, user_home: Any) -> str: + """Return the ``COLIMA_HOME`` a Colima profile needs, read from the endpoint of the Docker context Colima made for it. + + Colima puts a profile's Docker socket at ``//docker.sock`` and points the profile's context at it, so the context says where the profile lives. A command that starts the profile has to run with that ``COLIMA_HOME``, because Colima falls back to ``~/.colima`` when the variable is missing and would then start a different profile of the same name there. + + :param endpoint: The Docker host of the profile's context, ``unix:////docker.sock``. + :param profile: The Colima profile the context belongs to. + :param user_home: The home directory of the user the profile belongs to. + :returns: The directory, or an empty string when it is ``/.colima``, the home Colima uses when ``COLIMA_HOME`` is not set, so a profile in the default home keeps running with no variable. + :raises AnsibleFilterError: a value is not a string, the profile is not a valid profile name, or the endpoint is not the socket of that profile in an absolute directory. + """ + host = _text("The Docker context's endpoint", endpoint) + profile_name = _text("The Colima profile", profile) + home = _text("The user's home directory", user_home) + if not PROFILE_PATTERN.match(profile_name): + raise AnsibleFilterError(f"The Colima profile {profile_name!r} is not a valid profile name: use letters and digits, separated by single dots, hyphens or underscores.") + path = host.removeprefix(UNIX_SCHEME) if host.startswith(UNIX_SCHEME) else "" + profile_directory, socket = posixpath.split(path) + colima_home, directory = posixpath.split(profile_directory) + if socket != COLIMA_SOCKET or directory != profile_name or not posixpath.isabs(colima_home): + raise AnsibleFilterError(f"The Docker context's endpoint {host!r} is not the socket of the Colima profile {profile_name!r} (unix:///{profile_name}/{COLIMA_SOCKET}), so the role cannot tell where that profile lives: set github_runner_cluster_colima_home.") + if colima_home == posixpath.join(home, COLIMA_DEFAULT_HOME): + return "" + return colima_home + + class FilterModule: """Registers the container runtime filter with Ansible.""" def filters(self) -> dict[str, Any]: """Return the filters this plugin provides.""" - return {"cluster_container_runtime": cluster_container_runtime} + return {"cluster_container_runtime": cluster_container_runtime, "cluster_colima_home": cluster_colima_home} diff --git a/tests/unit/test_container_runtime.py b/tests/unit/test_container_runtime.py index 8eb76905..b55cdb53 100644 --- a/tests/unit/test_container_runtime.py +++ b/tests/unit/test_container_runtime.py @@ -13,6 +13,9 @@ _spec.loader.exec_module(container_runtime) runtime = container_runtime.cluster_container_runtime +colima_home = container_runtime.cluster_colima_home +AnsibleFilterError = container_runtime.AnsibleFilterError +HOME = "/Users/example" class AutoDetectionTest(unittest.TestCase): @@ -85,6 +88,47 @@ def test_a_value_that_is_not_a_string_is_an_error(self) -> None: def test_the_filter_is_registered(self) -> None: self.assertIs(container_runtime.FilterModule().filters()["cluster_container_runtime"], runtime) + self.assertIs(container_runtime.FilterModule().filters()["cluster_colima_home"], colima_home) + + +class ColimaHomeTest(unittest.TestCase): + def test_a_profile_in_a_home_of_its_own_names_it(self) -> None: + self.assertEqual(colima_home("unix:///Users/example/.colima-node/node/docker.sock", "node", HOME), "/Users/example/.colima-node") + + def test_the_default_home_is_no_variable_at_all(self) -> None: + self.assertEqual(colima_home("unix:///Users/example/.colima/default/docker.sock", "default", HOME), "") + self.assertEqual(colima_home("unix:///Users/example/.colima/build/docker.sock", "build", HOME), "") + + def test_a_home_that_only_starts_like_the_default_is_a_home_of_its_own(self) -> None: + self.assertEqual(colima_home("unix:///Users/example/.colima2/build/docker.sock", "build", HOME), "/Users/example/.colima2") + + def test_the_endpoint_is_trimmed_as_docker_prints_it(self) -> None: + self.assertEqual(colima_home("unix:///Users/example/.colima-node/node/docker.sock\n", "node", HOME), "/Users/example/.colima-node") + + def test_an_endpoint_that_is_not_the_profiles_socket_is_refused(self) -> None: + for endpoint in ( + "", + "unix:///var/run/docker.sock", + "unix:///Users/example/.colima-node/other/docker.sock", + "unix:///Users/example/.colima-node/node/other.sock", + "tcp://127.0.0.1:2375", + "ssh://example@host/Users/example/.colima-node/node/docker.sock", + "unix://relative/node/docker.sock", + "unix://node/docker.sock", + ): + with self.subTest(endpoint=endpoint): + with self.assertRaisesRegex(AnsibleFilterError, "set github_runner_cluster_colima_home"): + colima_home(endpoint, "node", HOME) + + def test_a_profile_that_is_not_a_valid_name_is_refused(self) -> None: + with self.assertRaisesRegex(AnsibleFilterError, "not a valid profile name"): + colima_home("unix:///Users/example/.colima-node/no de/docker.sock", "no de", HOME) + + def test_a_value_that_is_not_a_string_is_refused(self) -> None: + for args in ((1, "node", HOME), ("unix:///a/node/docker.sock", ["node"], HOME), ("unix:///a/node/docker.sock", "node", 3)): + with self.subTest(args=args): + with self.assertRaises(AnsibleFilterError): + colima_home(*args) if __name__ == "__main__": From 0aa386fe4bf12ea4afeda44eab89a3f9f60ab750 Mon Sep 17 00:00:00 2001 From: Joseph Mearman Date: Mon, 2 Nov 2026 22:00:10 +0000 Subject: [PATCH 2/2] fix(cluster): start a Colima profile in its own home at login A profile that lives outside ~/.colima (the bridged node profile, with context colima-node and COLIMA_HOME ~/.colima-node) was started without COLIMA_HOME by both the role and its launchd agent, so Colima looked in ~/.colima and created a new VM there. After a reboot the node's container never came back. The role now takes the home from github_runner_cluster_colima_home when that is set, and otherwise from the endpoint of the profile's Docker context, read with docker context inspect (no running VM needed, so it works under --check). It passes the home to every colima call and writes it into the agent. A profile in ~/.colima gets the same agent and commands as before, with no COLIMA_HOME. --- roles/github_runner_cluster/README.md | 4 +- roles/github_runner_cluster/defaults/main.yml | 3 +- roles/github_runner_cluster/tasks/runtime.yml | 24 +++++- .../templates/colima-launchd.plist.j2 | 4 +- tests/runtime_autostart/render_plist.yml | 2 + tests/unit/test_runtime_autostart.py | 82 +++++++++++++++++-- 6 files changed, 106 insertions(+), 13 deletions(-) diff --git a/roles/github_runner_cluster/README.md b/roles/github_runner_cluster/README.md index 8885a014..35102e4e 100644 --- a/roles/github_runner_cluster/README.md +++ b/roles/github_runner_cluster/README.md @@ -145,7 +145,7 @@ Which runtime a Mac runs is taken from the Docker context the role drives (see [ **Colima.** With `github_runner_cluster_runtime_autostart` on (the default, which acts on Colima hosts only), the role: - starts the profile if it is stopped, with `colima start --profile --activate=false`, so the profile does not make its context the host's current one; -- installs a launchd agent, `com.exadev.github-runner.colima.`, as `~/Library/LaunchAgents/