From 7bf7df03963bceaaea8a04e497d3173d6f8c9467 Mon Sep 17 00:00:00 2001 From: Patrick Ogenstad Date: Mon, 2 Nov 2026 13:43:08 +0000 Subject: [PATCH] feat(testing): let GitRepo give every user access to the repository it creates GitRepo copies its source directory, which keeps the source's permissions, and creates the repository as the user that runs the tests. When the Infrahub containers run as another user, they cannot read a repository copied from a private directory, such as one from pytest's tmp_path, and cannot push to any of them. With shared=True, GitRepo sets core.sharedRepository so that Git creates later files with the same access, and gives every user read and write access to the files that exist already. The default, False, keeps the current behavior. Co-Authored-By: Claude Opus 5.5 --- changelog/+gitrepo-shared-access.added.md | 1 + infrahub_sdk/testing/repository.py | 30 +++++++++++++++++ tests/unit/sdk/test_repository.py | 41 +++++++++++++++++++++++ 3 files changed, 72 insertions(+) create mode 100644 changelog/+gitrepo-shared-access.added.md diff --git a/changelog/+gitrepo-shared-access.added.md b/changelog/+gitrepo-shared-access.added.md new file mode 100644 index 000000000..4725cc5b2 --- /dev/null +++ b/changelog/+gitrepo-shared-access.added.md @@ -0,0 +1 @@ +Add the `shared` option to `GitRepo` in `infrahub_sdk.testing.repository`. With `shared=True`, every user on the machine can read and write the Git repository that `GitRepo` creates, so Infrahub containers that run as another user than the test process can fetch from it and push to it. The default, `False`, keeps the current behavior. diff --git a/infrahub_sdk/testing/repository.py b/infrahub_sdk/testing/repository.py index 941f118ee..0bd67e711 100644 --- a/infrahub_sdk/testing/repository.py +++ b/infrahub_sdk/testing/repository.py @@ -1,6 +1,7 @@ from __future__ import annotations import asyncio +import os import shutil from dataclasses import dataclass, field from enum import Enum @@ -43,6 +44,8 @@ class GitRepo: directories_to_ignore: list[str] = field(default_factory=list) remote_directory_name: str = "/remote" _branches: list[str] = field(default_factory=list) + shared: bool = False + """Give every user read and write access to the repository, for Infrahub containers that run as another user.""" @property def repo(self) -> GitRepoManager: @@ -75,6 +78,33 @@ def init(self) -> None: porcelain.commit(repo=self._repo.git.path, message="First commit") porcelain.checkout(repo=self._repo.git, target=self.initial_branch) + if self.shared: + self._share() + + def _share(self) -> None: + """Give every user read and write access to the repository, now and to the files Git adds later. + + `core.sharedRepository` makes Git create the files of a later commit or push with the same access. + The files that exist already, including those copied from `src_directory`, get it here. + """ + config = self.repo.git.get_config() + config.set((b"core",), b"sharedRepository", b"0666") + config.write_to_path() + + root_directory = self.dst_directory / self.name + root_directory.chmod(0o777) + for root, directories, files in os.walk(root_directory): + for directory in directories: + directory_path = Path(root) / directory + if not directory_path.is_symlink(): + directory_path.chmod(0o777) + for file in files: + file_path = Path(root) / file + if file_path.is_symlink(): + continue + mode = file_path.stat().st_mode + file_path.chmod(mode | 0o666 | (0o111 if mode & 0o111 else 0)) + async def add_to_infrahub(self, client: InfrahubClient, branch: str | None = None) -> dict: input_data = { "data": { diff --git a/tests/unit/sdk/test_repository.py b/tests/unit/sdk/test_repository.py index a8364b1c9..81933237b 100644 --- a/tests/unit/sdk/test_repository.py +++ b/tests/unit/sdk/test_repository.py @@ -1,3 +1,4 @@ +import stat import tempfile from collections.abc import Generator from pathlib import Path @@ -92,3 +93,43 @@ def test_gitrepo_init(temp_dir: str) -> None: commit = repo._repo.git[repo._repo.git.head()] assert isinstance(commit, Commit) assert commit.message.decode("utf-8") == "First commit" + + +def _private_source(temp_dir: str) -> Path: + """A source directory that only its owner can access, as pytest's `tmp_path` creates them.""" + source = Path(temp_dir) / "source" + source.mkdir(mode=0o700) + (source / ".infrahub.yml").write_text("---\n", encoding="utf-8") + (source / "script.sh").write_text("#!/bin/sh\n", encoding="utf-8") + (source / "script.sh").chmod(0o700) + return source + + +def _permissions(path: Path) -> int: + return stat.S_IMODE(path.stat().st_mode) + + +def test_gitrepo_keeps_the_permissions_of_the_source_by_default(temp_dir: str) -> None: + destination = Path(temp_dir) / "remotes" + repo = GitRepo(name="private", src_directory=_private_source(temp_dir), dst_directory=destination) + + assert _permissions(destination / "private") == 0o700 + with pytest.raises(KeyError): + repo.repo.git.get_config().get((b"core",), b"sharedRepository") + + +def test_gitrepo_shared_gives_every_user_read_and_write_access(temp_dir: str) -> None: + destination = Path(temp_dir) / "remotes" + repo = GitRepo(name="shared", src_directory=_private_source(temp_dir), dst_directory=destination, shared=True) + root = destination / "shared" + + assert repo.repo.git.get_config().get((b"core",), b"sharedRepository") == b"0666" + for path in [root, *root.rglob("*")]: + if path.is_symlink(): + continue + if path.is_dir(): + assert _permissions(path) == 0o777, path + else: + assert _permissions(path) & 0o666 == 0o666, path + assert _permissions(root / "script.sh") == 0o777 + assert _permissions(root / ".infrahub.yml") == 0o666