diff --git a/roles/github_runner_arc/tasks/install_registry_cache.yml b/roles/github_runner_arc/tasks/install_registry_cache.yml index c81921fe..f09e6663 100644 --- a/roles/github_runner_arc/tasks/install_registry_cache.yml +++ b/roles/github_runner_arc/tasks/install_registry_cache.yml @@ -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 }}" diff --git a/roles/github_runner_arc/tasks/render_registry_cache.yml b/roles/github_runner_arc/tasks/render_registry_cache.yml index c7b1166e..111c729c 100644 --- a/roles/github_runner_arc/tasks/render_registry_cache.yml +++ b/roles/github_runner_arc/tasks/render_registry_cache.yml @@ -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 }}" diff --git a/tests/registry_cache/install.yml b/tests/registry_cache/install.yml new file mode 100644 index 00000000..cd9987ec --- /dev/null +++ b/tests/registry_cache/install.yml @@ -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 diff --git a/tests/registry_cache/render.yml b/tests/registry_cache/render.yml index 1b824d8f..1458f425 100644 --- a/tests/registry_cache/render.yml +++ b/tests/registry_cache/render.yml @@ -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" diff --git a/tests/unit/test_registry_cache.py b/tests/unit/test_registry_cache.py index 4e92b01c..e54c395e 100644 --- a/tests/unit/test_registry_cache.py +++ b/tests/unit/test_registry_cache.py @@ -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) @@ -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) @@ -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()