Compare commits

..

9 Commits

Author SHA1 Message Date
Jörg Thalheim
34cc793af2 fix nix flake cache with chroot stores
The flake cache was not properly checking store paths when custom stores
were used (e.g., when using --store flag or CLAN_TEST_STORE). This
caused cache validation to fail even when the store paths existed.

This fix:
- Updates store path detection to properly identify any path with
  "/store/" in it and a proper nix store item format (hash-name)
- Normalizes store paths to use the current store when checking
  if they exist (CLAN_TEST_STORE or nix config)
- Uses CLAN_TEST_STORE environment variable for test stores, which
  matches how nix --store flag works

Added comprehensive tests to verify the fix works with custom stores.
2025-06-17 15:35:37 +02:00
kenji
c303ed8347 Merge pull request 'agit: Set COMMIT_EDITMSG as filetype' (#3991) from kenji/agit: Set COMMIT_EDITMSG as filetype into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/3991
2025-06-17 12:02:03 +00:00
Mic92
2af619609a Merge pull request 'install: correctly pass nix options to nixos-anywhere' (#3989) from nixos-anywhere-options into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/3989
2025-06-17 11:49:13 +00:00
kenji
6ab082d080 Merge pull request 'agit: Add -a flag alias for --auto' (#3992) from kenji/agit: Add -a flag alias for --auto into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/3992
2025-06-17 11:44:04 +00:00
a-kenji
02f362df92 agit: Add -a flag alias for --auto 2025-06-17 12:45:37 +02:00
Jörg Thalheim
7ce65f429c install: correctly pass nix options to nixos-anywhere
The nix_options were collected but never passed to the nixos-anywhere command.
This fix adds the options to the command before the target host argument.
2025-06-17 12:43:20 +02:00
a-kenji
e6ed020423 agit: Set COMMIT_EDITMSG as filetype 2025-06-17 12:42:12 +02:00
kenji
4575251b7f Merge pull request 'clanServices: Enable garage by default' (#3987) from kenji/clanServices: Enable garge by default into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/3987
2025-06-17 10:33:26 +00:00
a-kenji
55db78fbde clanServices/garage: Enable garage by default 2025-06-17 12:06:41 +02:00
6 changed files with 184 additions and 17 deletions

View File

@@ -8,8 +8,15 @@
roles.default = {
perInstance.nixosModule =
{ config, pkgs, ... }:
{
config,
pkgs,
lib,
...
}:
{
services.garage.enable = lib.mkDefault true;
systemd.services.garage.serviceConfig = {
LoadCredential = [
"rpc_secret_path:${config.clan.core.vars.generators.garage-shared.files.rpc_secret.path}"

View File

@@ -49,7 +49,7 @@ def get_latest_commit_info() -> tuple[str, str]:
def open_editor_for_pr() -> tuple[str, str]:
"""Open editor to get PR title and description. First line is title, rest is description."""
with tempfile.NamedTemporaryFile(
mode="w+", suffix=".txt", delete=False
mode="w+", suffix="COMMIT_EDITMSG", delete=False
) as temp_file:
temp_file.write("\n")
temp_file.write("# Please enter the PR title on the first line.\n")
@@ -266,6 +266,7 @@ Examples:
)
create_parser.add_argument(
"-a",
"--auto",
action="store_true",
help="Skip editor and use commit message automatically",

View File

@@ -134,6 +134,10 @@ def install_machine(opts: InstallOptions) -> None:
if opts.debug:
cmd.append("--debug")
# Add nix options to nixos-anywhere
cmd.extend(opts.nix_options)
cmd.append(host.target)
if opts.use_tor:
# nix copy does not support tor socks proxy

View File

@@ -55,12 +55,6 @@ def upload_sources(machine: Machine, ssh: Remote) -> str:
is_local_input(node) for node in flake_data["locks"]["nodes"].values()
)
# Construct the remote URL with proper parameters for Darwin
remote_url = f"ssh://{ssh.target}"
# MacOS doesn't come with a proper login shell for ssh and therefore doesn't have nix in $PATH as it doesn't source /etc/profile
if machine._class_ == "darwin":
remote_url += "?remote-program=bash -lc 'exec nix-daemon --stdio'"
if not has_path_inputs:
# Just copy the flake to the remote machine, we can substitute other inputs there.
path = flake_data["path"]
@@ -68,7 +62,7 @@ def upload_sources(machine: Machine, ssh: Remote) -> str:
[
"copy",
"--to",
remote_url,
f"ssh://{ssh.target}",
"--no-check-sigs",
path,
]
@@ -90,7 +84,7 @@ def upload_sources(machine: Machine, ssh: Remote) -> str:
"flake",
"archive",
"--to",
remote_url,
f"ssh://{ssh.target}",
"--json",
flake_url,
]

View File

@@ -9,6 +9,7 @@ from tempfile import NamedTemporaryFile
from typing import Any
from clan_lib.errors import ClanError
from clan_lib.nix import nix_config
log = logging.getLogger(__name__)
@@ -319,9 +320,7 @@ class FlakeCacheEntry:
# strings need to be checked if they are store paths
# if they are, we store them as a dict with the outPath key
# this is to mirror nix behavior, where the outPath of an attrset is used if no further key is specified
elif isinstance(value, str) and value.startswith(
os.environ.get("NIX_STORE_DIR", "/nix/store")
):
elif isinstance(value, str) and self._is_store_path(value):
assert selectors == []
self.value = {"outPath": FlakeCacheEntry(value)}
@@ -335,14 +334,68 @@ class FlakeCacheEntry:
msg = f"Cannot insert {value} into cache, already have {self.value}"
raise TypeError(msg)
def _is_store_path(self, value: str) -> bool:
"""Check if a string is a nix store path."""
# A store path is any path that has "store" as one of its parent directories
# and contains a hash-prefixed name after it
path_parts = Path(value).parts
try:
store_idx = path_parts.index("store")
except ValueError:
return False
# Check if there's at least one more component after "store"
if store_idx + 1 < len(path_parts):
# Check if the component after store looks like a nix store item
# (starts with a hash)
store_item = path_parts[store_idx + 1]
# Basic check: nix store items typically start with a hash
return len(store_item) > 32 and "-" in store_item
return False
def _normalize_store_path(self, store_path: str) -> Path | None:
"""
Normalize a store path to use the current NIX_STORE_DIR.
Returns None if the path is not a valid store path.
"""
# Extract the store item (hash-name) from the path
path_parts = Path(store_path).parts
# Find the index of "store" in the path
try:
store_idx = path_parts.index("store")
except ValueError:
return None
if store_idx + 1 < len(path_parts):
store_item = path_parts[store_idx + 1]
# Get the current store path
# Check if we're using a test store first
test_store = os.environ.get("CLAN_TEST_STORE")
if test_store:
# In test mode, the store is at CLAN_TEST_STORE/nix/store
current_store = str(Path(test_store) / "nix" / "store")
else:
# Otherwise use nix config
config = nix_config()
current_store = config.get("store", "/nix/store")
return Path(current_store) / store_item if current_store else None
return None
def is_cached(self, selectors: list[Selector]) -> bool:
selector: Selector
# for store paths we have to check if they still exist, otherwise they have to be rebuild and are thus not cached
if isinstance(self.value, str) and self.value.startswith(
os.environ.get("NIX_STORE_DIR", "/nix/store")
):
return Path(self.value).exists()
if isinstance(self.value, str) and self._is_store_path(self.value):
normalized_path = self._normalize_store_path(self.value)
if normalized_path:
return normalized_path.exists()
return False
# if self.value is not dict but we request more selectors, we assume we are cached and an error will be thrown in the select function
if isinstance(self.value, str | float | int | None):

View File

@@ -0,0 +1,108 @@
"""Test flake cache with chroot stores."""
import os
import tempfile
from pathlib import Path
import pytest
from clan_lib.flake.flake import FlakeCache, FlakeCacheEntry
def test_flake_cache_with_chroot_store(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
"""Test that flake cache works correctly with chroot stores."""
# Create a mock store path
normal_store = "/nix/store"
chroot_store = str(tmp_path / "nix" / "store")
# Create the chroot store directory
Path(chroot_store).mkdir(parents=True)
# Create a fake derivation in the chroot store with proper nix store format
fake_drv = "abcd1234abcd1234abcd1234abcd1234-test-package"
fake_store_path = f"{chroot_store}/{fake_drv}"
Path(fake_store_path).mkdir(parents=True)
# Test 1: Cache entry with normal store path
cache = FlakeCache()
# Insert a store path that doesn't exist in the normal store
non_existent_path = f"{normal_store}/{fake_drv}"
cache.insert({"test": {"package": non_existent_path}}, "")
# Without chroot store, this should be uncached (path doesn't exist)
assert not cache.is_cached("test.package")
# Test 2: Set CLAN_TEST_STORE to chroot store parent
# CLAN_TEST_STORE should point to the parent of nix/store
monkeypatch.setenv("CLAN_TEST_STORE", str(tmp_path))
# Create a new cache with the chroot store
cache2 = FlakeCache()
# Insert the same path but now it should use chroot store
cache2.insert({"test": {"package": fake_store_path}}, "")
# This should be cached because the path exists in chroot store
assert cache2.is_cached("test.package")
# Test 3: Cache persistence with chroot store
cache_file = tmp_path / "cache.json"
cache2.save_to_file(cache_file)
# Load cache in a new instance
cache3 = FlakeCache()
cache3.load_from_file(cache_file)
# Should still be cached with chroot store
assert cache3.is_cached("test.package")
# Test 4: Cache validation fails when chroot store changes
monkeypatch.setenv("CLAN_TEST_STORE", "/different")
# Same cache should now be invalid
assert not cache3.is_cached("test.package")
def test_flake_cache_entry_store_path_validation() -> None:
"""Test that FlakeCacheEntry correctly validates store paths."""
# Test with default store
entry = FlakeCacheEntry()
# Insert a non-existent store path with proper format
fake_path = "/nix/store/abcd1234abcd1234abcd1234abcd1234-fake-package"
entry.insert(fake_path, [])
# Should not be cached because path doesn't exist
assert not entry.is_cached([])
# Test with environment variable
with tempfile.TemporaryDirectory() as tmpdir:
# Create nix/store structure
store_dir = Path(tmpdir) / "nix" / "store"
store_dir.mkdir(parents=True)
# Create a fake store path with proper format
fake_drv = "test1234test1234test1234test1234-package"
fake_path_obj = store_dir / fake_drv
fake_path_obj.mkdir()
fake_path = str(fake_path_obj)
# Set CLAN_TEST_STORE to parent of store dir
old_env = os.environ.get("CLAN_TEST_STORE")
try:
os.environ["CLAN_TEST_STORE"] = str(tmpdir)
entry2 = FlakeCacheEntry()
entry2.insert(str(fake_path), [])
# Should be cached because path exists
assert entry2.is_cached([])
finally:
if old_env is None:
os.environ.pop("CLAN_TEST_STORE", None)
else:
os.environ["CLAN_TEST_STORE"] = old_env