Skip to content
Merged
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
2 changes: 1 addition & 1 deletion roles/github_runner_arc/tasks/install_registry_cache.yml
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@
kubernetes.core.k8s:
definition: "{{ item }}"
apply: true
loop: "{{ github_runner_arc_registry_cache_resources }}"
loop: "{{ github_runner_arc_registry_cache_manifests }}"
loop_control:
label: "{{ item.kind }}/{{ item.metadata.name }}"

Expand Down
4 changes: 2 additions & 2 deletions roles/github_runner_arc/tasks/render_registry_cache.yml
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
---
# Renders the registry cache's resources (templates/registry-cache.yaml.j2) on the control node into github_runner_arc_registry_cache_resources. Needs github_runner_arc_fleet, the inventory's expanded profiles that validate.yml sets. Included by install_registry_cache.yml, and by tests/registry_cache/render.yml so the tests render exactly what the role installs.
# Renders the registry cache's resources (templates/registry-cache.yaml.j2) on the control node into github_runner_arc_registry_cache_manifests, a name of its own because github_runner_arc_registry_cache_resources is the cache containers' requests and limits. Needs github_runner_arc_fleet, the inventory's expanded profiles that validate.yml sets. Included by install_registry_cache.yml, and by tests/registry_cache/render.yml so the tests render exactly what the role installs.

- name: "Registry cache: describe its resources"
ansible.builtin.set_fact:
github_runner_arc_registry_cache_resources: "{{ lookup('ansible.builtin.template', 'registry-cache.yaml.j2') | from_yaml_all | list }}"
github_runner_arc_registry_cache_manifests: "{{ lookup('ansible.builtin.template', 'registry-cache.yaml.j2') | from_yaml_all | list }}"
vars:
# Every scale set's namespace across the inventory, not only this host's, since the cache serves the whole cluster's runners.
github_runner_arc_registry_cache_runner_namespaces: "{{ github_runner_arc_fleet.profiles | map(attribute='namespace') | unique | sort }}"
10 changes: 10 additions & 0 deletions tests/registry_cache/install.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# Runs the ARC role's registry cache install tasks (tasks/install_registry_cache.yml) exactly as main.yml includes them, for tests/unit/test_registry_cache.py to read the outcome of. Takes github_runner_arc_fleet (the expanded profiles, as validate.yml sets it) and the github_runner_arc_registry_cache_* settings as extra variables, and needs ANSIBLE_COLLECTIONS_PATH to put a stand-in kubernetes.core collection ahead of playbooks/collections, so the role's tasks reach a stub that records nothing and changes nothing.
- name: Install the registry cache, or remove it when turned off
hosts: localhost
connection: local
gather_facts: false
tasks:
- name: Include the role's own install tasks
ansible.builtin.include_role:
name: exadev.github_runner.github_runner_arc
tasks_from: install_registry_cache
2 changes: 1 addition & 1 deletion tests/registry_cache/render.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,6 @@

- name: Write the resources
ansible.builtin.copy:
content: "{{ github_runner_arc_registry_cache_resources | to_nice_json }}"
content: "{{ github_runner_arc_registry_cache_manifests | to_nice_json }}"
dest: "{{ render_output }}"
mode: "0644"
61 changes: 61 additions & 0 deletions tests/unit/test_registry_cache.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
PLUGIN = ROOT / "plugins" / "filter" / "arc.py"
RENDER_VALUES = ROOT / "tests" / "capabilities" / "render.yml"
RENDER_CACHE = ROOT / "tests" / "registry_cache" / "render.yml"
INSTALL_CACHE = ROOT / "tests" / "registry_cache" / "install.yml"
_spec = importlib.util.spec_from_file_location("arc_filters", PLUGIN)
assert _spec is not None and _spec.loader is not None
arc = importlib.util.module_from_spec(_spec)
Expand Down Expand Up @@ -78,6 +79,42 @@ def run_playbook(playbook: Path, extra: dict[str, Any]) -> subprocess.CompletedP
return completed


# A stand-in for the kubernetes.core modules the install tasks call: it appends the module name and its arguments to the file named by STUB_LOG, reports no change and finds no resources, so the tasks' control flow runs without a cluster.
STUB_MODULE = """from ansible.module_utils.basic import AnsibleModule
import json, os

ARGUMENTS = ("api_version", "apply", "definition", "kind", "name", "namespace", "state", "wait")


def main():
module = AnsibleModule(argument_spec={key: {"type": "raw"} for key in ARGUMENTS}, supports_check_mode=True)
with open(os.environ["STUB_LOG"], "a") as log:
log.write(json.dumps({"module": MODULE, **module.params}) + "\\n")
module.exit_json(changed=False, resources=[])


main()
"""


def run_install(extra: dict[str, Any], *flags: str) -> tuple[subprocess.CompletedProcess[str], list[dict[str, Any]]]:
"""Run the role's install tasks against the stand-in kubernetes.core collection, returning the completed process and the module calls the stub recorded."""
with tempfile.TemporaryDirectory() as work:
modules = Path(work) / "stubs" / "ansible_collections" / "kubernetes" / "core" / "plugins" / "modules"
modules.mkdir(parents=True)
for module in ("k8s", "k8s_info"):
(modules / f"{module}.py").write_text(STUB_MODULE.replace("MODULE,", repr(module) + ","))
log = Path(work) / "calls.jsonl"
env = ansible_env(work)
env["ANSIBLE_COLLECTIONS_PATH"] = os.pathsep.join([str(Path(work) / "stubs"), env["ANSIBLE_COLLECTIONS_PATH"]])
env["STUB_LOG"] = str(log)
extra_file = Path(work) / "extra.json"
extra_file.write_text(json.dumps(extra))
completed = subprocess.run([os.environ.get("ANSIBLE_PLAYBOOK", "ansible-playbook"), str(INSTALL_CACHE), "-e", f"@{extra_file}", *flags], env=env, cwd=work, capture_output=True, text=True, stdin=subprocess.DEVNULL)
calls = [json.loads(line) for line in log.read_text().splitlines()] if log.exists() else []
return completed, calls


def expanded(profile: dict[str, Any]) -> dict[str, Any]:
"""Return the one profile arc_profiles expands from profile, failing on any error."""
result = arc.arc_profiles([{"name": "Example", "image": RUNNER_IMAGE, "scale_set_profiles": [profile]}], require_app_id=False)
Expand Down Expand Up @@ -325,5 +362,29 @@ def test_only_the_runner_namespaces_may_reach_it(self) -> None:
self.assertEqual([port["port"] for port in policy["ingress"][0]["ports"]], [5000, 5001, 5002])


class InstalledCacheTest(unittest.TestCase):
"""The role's install tasks, run against a stand-in for kubernetes.core, in a normal run and in check mode."""

def install(self, *flags: str, **settings: Any) -> list[dict[str, Any]]:
fleet = arc.arc_profiles([{"name": "Example", "image": RUNNER_IMAGE, "scale_set_profiles": [{"max_runners": 1}]}], require_app_id=False)
completed, calls = run_install({"github_runner_arc_fleet": fleet, **settings}, *flags)
self.assertEqual(completed.returncode, 0, completed.stdout + completed.stderr)
return calls

def test_off_removes_the_cache_without_rendering_or_applying_anything(self) -> None:
for flags in ((), ("--check",)):
with self.subTest(flags=flags):
calls = self.install(*flags)
self.assertEqual([(call["kind"], call["state"]) for call in calls if call["module"] == "k8s"], [("Deployment", "absent"), ("Service", "absent"), ("NetworkPolicy", "absent"), ("ConfigMap", "absent"), ("PersistentVolumeClaim", "absent"), ("Namespace", "absent")])
self.assertEqual([call for call in calls if call["apply"]], [])

def test_on_applies_each_rendered_resource(self) -> None:
for flags in ((), ("--check",)):
with self.subTest(flags=flags):
calls = self.install(*flags, github_runner_arc_registry_cache_enabled=True, github_runner_arc_registry_cache_registries=[{"name": "ghcr", "host": "ghcr.io"}])
applied = [call["definition"]["kind"] for call in calls if call["apply"]]
self.assertEqual(applied, ["ConfigMap", "PersistentVolumeClaim", "Deployment", "Service", "NetworkPolicy"])


if __name__ == "__main__":
unittest.main()
Loading