clan-cli: use new flake caching for machines
This commit is contained in:
@@ -154,6 +154,7 @@
|
|||||||
"inventory.json"
|
"inventory.json"
|
||||||
"lib/build-clan"
|
"lib/build-clan"
|
||||||
"lib/default.nix"
|
"lib/default.nix"
|
||||||
|
"lib/select.nix"
|
||||||
"lib/flake-module.nix"
|
"lib/flake-module.nix"
|
||||||
"lib/frontmatter"
|
"lib/frontmatter"
|
||||||
"lib/inventory"
|
"lib/inventory"
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import argparse
|
import argparse
|
||||||
import json
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from clan_cli.completions import (
|
from clan_cli.completions import (
|
||||||
@@ -15,7 +14,7 @@ log = logging.getLogger(__name__)
|
|||||||
|
|
||||||
def create_backup(machine: Machine, provider: str | None = None) -> None:
|
def create_backup(machine: Machine, provider: str | None = None) -> None:
|
||||||
machine.info(f"creating backup for {machine.name}")
|
machine.info(f"creating backup for {machine.name}")
|
||||||
backup_scripts = json.loads(machine.eval_nix("config.clan.core.backups"))
|
backup_scripts = machine.eval_nix("config.clan.core.backups")
|
||||||
if provider is None:
|
if provider is None:
|
||||||
if not backup_scripts["providers"]:
|
if not backup_scripts["providers"]:
|
||||||
msg = "No providers specified"
|
msg = "No providers specified"
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ class Backup:
|
|||||||
|
|
||||||
def list_provider(machine: Machine, provider: str) -> list[Backup]:
|
def list_provider(machine: Machine, provider: str) -> list[Backup]:
|
||||||
results = []
|
results = []
|
||||||
backup_metadata = json.loads(machine.eval_nix("config.clan.core.backups"))
|
backup_metadata = machine.eval_nix("config.clan.core.backups")
|
||||||
list_command = backup_metadata["providers"][provider]["list"]
|
list_command = backup_metadata["providers"][provider]["list"]
|
||||||
proc = machine.target_host.run(
|
proc = machine.target_host.run(
|
||||||
[list_command],
|
[list_command],
|
||||||
@@ -46,7 +46,7 @@ def list_provider(machine: Machine, provider: str) -> list[Backup]:
|
|||||||
|
|
||||||
|
|
||||||
def list_backups(machine: Machine, provider: str | None = None) -> list[Backup]:
|
def list_backups(machine: Machine, provider: str | None = None) -> list[Backup]:
|
||||||
backup_metadata = json.loads(machine.eval_nix("config.clan.core.backups"))
|
backup_metadata = machine.eval_nix("config.clan.core.backups")
|
||||||
results = []
|
results = []
|
||||||
if provider is None:
|
if provider is None:
|
||||||
for _provider in backup_metadata["providers"]:
|
for _provider in backup_metadata["providers"]:
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import argparse
|
import argparse
|
||||||
import json
|
|
||||||
|
|
||||||
from clan_cli.cmd import Log, RunOpts
|
from clan_cli.cmd import Log, RunOpts
|
||||||
from clan_cli.completions import (
|
from clan_cli.completions import (
|
||||||
@@ -12,14 +11,17 @@ from clan_cli.machines.machines import Machine
|
|||||||
|
|
||||||
|
|
||||||
def restore_service(machine: Machine, name: str, provider: str, service: str) -> None:
|
def restore_service(machine: Machine, name: str, provider: str, service: str) -> None:
|
||||||
backup_metadata = json.loads(machine.eval_nix("config.clan.core.backups"))
|
backup_metadata = machine.eval_nix("config.clan.core.backups")
|
||||||
backup_folders = json.loads(machine.eval_nix("config.clan.core.state"))
|
backup_folders = machine.eval_nix("config.clan.core.state")
|
||||||
|
|
||||||
if service not in backup_folders:
|
if service not in backup_folders:
|
||||||
msg = f"Service {service} not found in configuration. Available services are: {', '.join(backup_folders.keys())}"
|
msg = f"Service {service} not found in configuration. Available services are: {', '.join(backup_folders.keys())}"
|
||||||
raise ClanError(msg)
|
raise ClanError(msg)
|
||||||
|
|
||||||
folders = backup_folders[service]["folders"]
|
folders = backup_folders[service]["folders"].values()
|
||||||
|
assert all(isinstance(f, str) for f in folders), (
|
||||||
|
f"folders must be a list of strings instead of {folders}"
|
||||||
|
)
|
||||||
env = {}
|
env = {}
|
||||||
env["NAME"] = name
|
env["NAME"] = name
|
||||||
# FIXME: If we have too many folder this might overflow the stack.
|
# FIXME: If we have too many folder this might overflow the stack.
|
||||||
@@ -63,7 +65,7 @@ def restore_backup(
|
|||||||
) -> None:
|
) -> None:
|
||||||
errors = []
|
errors = []
|
||||||
if service is None:
|
if service is None:
|
||||||
backup_folders = json.loads(machine.eval_nix("config.clan.core.state"))
|
backup_folders = machine.eval_nix("config.clan.core.state")
|
||||||
for _service in backup_folders:
|
for _service in backup_folders:
|
||||||
try:
|
try:
|
||||||
restore_service(machine, name, provider, _service)
|
restore_service(machine, name, provider, _service)
|
||||||
|
|||||||
@@ -63,11 +63,11 @@ class FlakeCacheEntry:
|
|||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
value: str | float | dict[str, Any] | list[Any],
|
value: str | float | dict[str, Any] | list[Any] | None,
|
||||||
selectors: list[Selector],
|
selectors: list[Selector],
|
||||||
is_out_path: bool = False,
|
is_out_path: bool = False,
|
||||||
) -> None:
|
) -> None:
|
||||||
self.value: str | float | int | dict[str | int, FlakeCacheEntry]
|
self.value: str | float | int | None | dict[str | int, FlakeCacheEntry]
|
||||||
self.selector: set[int] | set[str] | AllSelector
|
self.selector: set[int] | set[str] | AllSelector
|
||||||
selector: Selector = AllSelector()
|
selector: Selector = AllSelector()
|
||||||
|
|
||||||
@@ -144,7 +144,7 @@ class FlakeCacheEntry:
|
|||||||
value, selectors[1:], is_out_path=True
|
value, selectors[1:], is_out_path=True
|
||||||
)
|
)
|
||||||
|
|
||||||
elif isinstance(value, (str | float | int)):
|
elif isinstance(value, (str | float | int | None)):
|
||||||
self.value = value
|
self.value = value
|
||||||
|
|
||||||
def insert(
|
def insert(
|
||||||
@@ -249,7 +249,7 @@ class FlakeCacheEntry:
|
|||||||
else:
|
else:
|
||||||
selector = selectors[0]
|
selector = selectors[0]
|
||||||
|
|
||||||
if isinstance(self.value, str | float | int):
|
if isinstance(self.value, str | float | int | None):
|
||||||
return selectors == []
|
return selectors == []
|
||||||
if isinstance(selector, AllSelector):
|
if isinstance(selector, AllSelector):
|
||||||
if isinstance(self.selector, AllSelector):
|
if isinstance(self.selector, AllSelector):
|
||||||
@@ -282,7 +282,7 @@ class FlakeCacheEntry:
|
|||||||
if selectors == [] and isinstance(self.value, dict) and "outPath" in self.value:
|
if selectors == [] and isinstance(self.value, dict) and "outPath" in self.value:
|
||||||
return self.value["outPath"].value
|
return self.value["outPath"].value
|
||||||
|
|
||||||
if isinstance(self.value, str | float | int):
|
if isinstance(self.value, str | float | int | None):
|
||||||
return self.value
|
return self.value
|
||||||
if isinstance(self.value, dict):
|
if isinstance(self.value, dict):
|
||||||
if isinstance(selector, AllSelector):
|
if isinstance(selector, AllSelector):
|
||||||
@@ -389,6 +389,9 @@ class Flake:
|
|||||||
return self._path
|
return self._path
|
||||||
|
|
||||||
def prefetch(self) -> None:
|
def prefetch(self) -> None:
|
||||||
|
"""
|
||||||
|
Run prefetch to flush the cache as well as initializing it.
|
||||||
|
"""
|
||||||
flake_prefetch = run(
|
flake_prefetch = run(
|
||||||
nix_command(
|
nix_command(
|
||||||
[
|
[
|
||||||
@@ -424,20 +427,36 @@ class Flake:
|
|||||||
self._is_local = False
|
self._is_local = False
|
||||||
self._path = Path(self.store_path)
|
self._path = Path(self.store_path)
|
||||||
|
|
||||||
def get_from_nix(self, selectors: list[str]) -> None:
|
def get_from_nix(
|
||||||
|
self,
|
||||||
|
selectors: list[str],
|
||||||
|
nix_options: list[str] | None = None,
|
||||||
|
) -> None:
|
||||||
if self._cache is None:
|
if self._cache is None:
|
||||||
self.prefetch()
|
self.prefetch()
|
||||||
assert self._cache is not None
|
assert self._cache is not None
|
||||||
|
|
||||||
|
if nix_options is None:
|
||||||
|
nix_options = []
|
||||||
|
|
||||||
config = nix_config()
|
config = nix_config()
|
||||||
nix_code = f"""
|
nix_code = f"""
|
||||||
let
|
let
|
||||||
flake = builtins.getFlake("path:{self.store_path}?narHash={self.hash}");
|
flake = builtins.getFlake("path:{self.store_path}?narHash={self.hash}");
|
||||||
in
|
in
|
||||||
flake.inputs.nixpkgs.legacyPackages.{config["system"]}.writeText "clan-flake-select" (builtins.toJSON [ ({" ".join([f'flake.clanInternals.lib.select "{attr}" flake' for attr in selectors])}) ])
|
flake.inputs.nixpkgs.legacyPackages.{config["system"]}.writeText "clan-flake-select" (
|
||||||
|
builtins.toJSON [ ({" ".join([f"flake.clanInternals.lib.select ''{attr}'' flake" for attr in selectors])}) ]
|
||||||
|
)
|
||||||
"""
|
"""
|
||||||
build_output = Path(run(nix_build(["--expr", nix_code])).stdout.strip())
|
|
||||||
if tmp_store := nix_test_store():
|
if tmp_store := nix_test_store():
|
||||||
|
nix_options += ["--store", str(tmp_store)]
|
||||||
|
nix_options.append("--impure")
|
||||||
|
|
||||||
|
build_output = Path(
|
||||||
|
run(nix_build(["--expr", nix_code, *nix_options])).stdout.strip()
|
||||||
|
)
|
||||||
|
|
||||||
|
if tmp_store:
|
||||||
build_output = tmp_store.joinpath(*build_output.parts[1:])
|
build_output = tmp_store.joinpath(*build_output.parts[1:])
|
||||||
outputs = json.loads(build_output.read_text())
|
outputs = json.loads(build_output.read_text())
|
||||||
if len(outputs) != len(selectors):
|
if len(outputs) != len(selectors):
|
||||||
@@ -448,7 +467,11 @@ class Flake:
|
|||||||
self._cache.insert(outputs[i], selector)
|
self._cache.insert(outputs[i], selector)
|
||||||
self._cache.save_to_file(self.flake_cache_path)
|
self._cache.save_to_file(self.flake_cache_path)
|
||||||
|
|
||||||
def select(self, selector: str) -> Any:
|
def select(
|
||||||
|
self,
|
||||||
|
selector: str,
|
||||||
|
nix_options: list[str] | None = None,
|
||||||
|
) -> Any:
|
||||||
if self._cache is None:
|
if self._cache is None:
|
||||||
self.prefetch()
|
self.prefetch()
|
||||||
assert self._cache is not None
|
assert self._cache is not None
|
||||||
@@ -456,5 +479,5 @@ class Flake:
|
|||||||
self._cache.load_from_file(self.flake_cache_path)
|
self._cache.load_from_file(self.flake_cache_path)
|
||||||
if not self._cache.is_cached(selector):
|
if not self._cache.is_cached(selector):
|
||||||
log.info(f"Cache miss for {selector}")
|
log.info(f"Cache miss for {selector}")
|
||||||
self.get_from_nix([selector])
|
self.get_from_nix([selector], nix_options)
|
||||||
return self._cache.select(selector)
|
return self._cache.select(selector)
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import logging
|
|||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
from functools import cached_property
|
from functools import cached_property
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from tempfile import NamedTemporaryFile
|
|
||||||
from time import time
|
from time import time
|
||||||
from typing import TYPE_CHECKING, Any, Literal
|
from typing import TYPE_CHECKING, Any, Literal
|
||||||
|
|
||||||
@@ -13,7 +12,7 @@ from clan_cli.errors import ClanError
|
|||||||
from clan_cli.facts import public_modules as facts_public_modules
|
from clan_cli.facts import public_modules as facts_public_modules
|
||||||
from clan_cli.facts import secret_modules as facts_secret_modules
|
from clan_cli.facts import secret_modules as facts_secret_modules
|
||||||
from clan_cli.flake import Flake
|
from clan_cli.flake import Flake
|
||||||
from clan_cli.nix import nix_build, nix_config, nix_eval, nix_metadata, nix_test_store
|
from clan_cli.nix import nix_build, nix_config, nix_eval, nix_test_store
|
||||||
from clan_cli.ssh.host import Host
|
from clan_cli.ssh.host import Host
|
||||||
from clan_cli.ssh.host_key import HostKeyCheck
|
from clan_cli.ssh.host_key import HostKeyCheck
|
||||||
from clan_cli.ssh.parse import parse_deployment_address
|
from clan_cli.ssh.parse import parse_deployment_address
|
||||||
@@ -35,16 +34,11 @@ class Machine:
|
|||||||
override_build_host: None | str = None
|
override_build_host: None | str = None
|
||||||
host_key_check: HostKeyCheck = HostKeyCheck.STRICT
|
host_key_check: HostKeyCheck = HostKeyCheck.STRICT
|
||||||
|
|
||||||
_eval_cache: dict[str, str] = field(default_factory=dict)
|
|
||||||
_build_cache: dict[str, Path] = field(default_factory=dict)
|
|
||||||
|
|
||||||
def get_id(self) -> str:
|
def get_id(self) -> str:
|
||||||
return f"{self.flake}#{self.name}"
|
return f"{self.flake}#{self.name}"
|
||||||
|
|
||||||
def flush_caches(self) -> None:
|
def flush_caches(self) -> None:
|
||||||
self.cached_deployment = None
|
self.flake.prefetch()
|
||||||
self._build_cache.clear()
|
|
||||||
self._eval_cache.clear()
|
|
||||||
|
|
||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
return f"Machine(name={self.name}, flake={self.flake})"
|
return f"Machine(name={self.name}, flake={self.flake})"
|
||||||
@@ -66,16 +60,9 @@ class Machine:
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def system(self) -> str:
|
def system(self) -> str:
|
||||||
# We filter out function attributes because they are not serializable.
|
return self.flake.select(
|
||||||
attr = f'(builtins.getFlake "{self.flake}").nixosConfigurations.{self.name}.pkgs.hostPlatform.system'
|
f"nixosConfigurations.{self.name}.pkgs.hostPlatform.system"
|
||||||
output = self._eval_cache.get(attr)
|
)
|
||||||
if output is None:
|
|
||||||
output = run_no_stdout(
|
|
||||||
nix_eval(["--impure", "--expr", attr]),
|
|
||||||
opts=RunOpts(prefix=self.name),
|
|
||||||
).stdout.strip()
|
|
||||||
self._eval_cache[attr] = output
|
|
||||||
return json.loads(output)
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def can_build_locally(self) -> bool:
|
def can_build_locally(self) -> bool:
|
||||||
@@ -83,13 +70,19 @@ class Machine:
|
|||||||
if self.system == config["system"] or self.system in config["extra-platforms"]:
|
if self.system == config["system"] or self.system in config["extra-platforms"]:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
nix_code = f"""
|
||||||
|
let
|
||||||
|
flake = builtins.getFlake("path:{self.flake.store_path}?narHash={self.flake.hash}");
|
||||||
|
in
|
||||||
|
flake.inputs.nixpkgs.legacyPackages.{self.system}.runCommandNoCC "clan-can-build-{int(time())}" {{ }} "touch $out").drvPath
|
||||||
|
"""
|
||||||
|
|
||||||
unsubstitutable_drv = json.loads(
|
unsubstitutable_drv = json.loads(
|
||||||
run_no_stdout(
|
run_no_stdout(
|
||||||
nix_eval(
|
nix_eval(
|
||||||
[
|
[
|
||||||
"--impure",
|
|
||||||
"--expr",
|
"--expr",
|
||||||
f'((builtins.getFlake "{self.flake}").inputs.nixpkgs.legacyPackages.{self.system}.runCommandNoCC "clan-can-build-{int(time())}" {{ }} "touch $out").drvPath',
|
nix_code,
|
||||||
]
|
]
|
||||||
),
|
),
|
||||||
opts=RunOpts(prefix=self.name),
|
opts=RunOpts(prefix=self.name),
|
||||||
@@ -234,81 +227,21 @@ class Machine:
|
|||||||
self,
|
self,
|
||||||
method: Literal["eval", "build"],
|
method: Literal["eval", "build"],
|
||||||
attr: str,
|
attr: str,
|
||||||
extra_config: None | dict = None,
|
|
||||||
nix_options: list[str] | None = None,
|
nix_options: list[str] | None = None,
|
||||||
) -> str | Path:
|
) -> Any:
|
||||||
"""
|
"""
|
||||||
Build the machine and return the path to the result
|
Build the machine and return the path to the result
|
||||||
accepts a secret store and a facts store # TODO
|
accepts a secret store and a facts store # TODO
|
||||||
"""
|
"""
|
||||||
if nix_options is None:
|
if nix_options is None:
|
||||||
nix_options = []
|
nix_options = []
|
||||||
|
|
||||||
config = nix_config()
|
config = nix_config()
|
||||||
system = config["system"]
|
system = config["system"]
|
||||||
|
|
||||||
file_info = {}
|
return self.flake.select(
|
||||||
with NamedTemporaryFile(mode="w") as config_json:
|
f'clanInternals.machines."{system}".{self.name}.{attr}',
|
||||||
if extra_config is not None:
|
nix_options=nix_options,
|
||||||
json.dump(extra_config, config_json, indent=2)
|
|
||||||
else:
|
|
||||||
json.dump({}, config_json)
|
|
||||||
config_json.flush()
|
|
||||||
|
|
||||||
file_info = json.loads(
|
|
||||||
run_no_stdout(
|
|
||||||
nix_eval(
|
|
||||||
[
|
|
||||||
"--impure",
|
|
||||||
"--expr",
|
|
||||||
f'let x = (builtins.fetchTree {{ type = "file"; url = "file://{config_json.name}"; }}); in {{ narHash = x.narHash; path = x.outPath; }}',
|
|
||||||
]
|
|
||||||
),
|
|
||||||
opts=RunOpts(prefix=self.name),
|
|
||||||
).stdout.strip()
|
|
||||||
)
|
|
||||||
|
|
||||||
args = []
|
|
||||||
|
|
||||||
# get git commit from flake
|
|
||||||
if extra_config is not None:
|
|
||||||
metadata = nix_metadata(self.flake_dir)
|
|
||||||
url = metadata["url"]
|
|
||||||
if (
|
|
||||||
"dirtyRevision" in metadata
|
|
||||||
or "dirtyRev" in metadata["locks"]["nodes"]["clan-core"]["locked"]
|
|
||||||
):
|
|
||||||
args += ["--impure"]
|
|
||||||
|
|
||||||
args += [
|
|
||||||
"--expr",
|
|
||||||
f"""
|
|
||||||
((builtins.getFlake "{url}").clanInternals.machinesFunc."{system}"."{self.name}" {{
|
|
||||||
extraConfig = builtins.fromJSON (builtins.readFile (builtins.fetchTree {{
|
|
||||||
type = "file";
|
|
||||||
url = if (builtins.compareVersions builtins.nixVersion "2.19") == -1 then "{file_info["path"]}" else "file:{file_info["path"]}";
|
|
||||||
narHash = "{file_info["narHash"]}";
|
|
||||||
}}));
|
|
||||||
}}).{attr}
|
|
||||||
""",
|
|
||||||
]
|
|
||||||
else:
|
|
||||||
if (self.flake_dir / ".git").exists():
|
|
||||||
flake = f"git+file://{self.flake_dir}"
|
|
||||||
else:
|
|
||||||
flake = f"path:{self.flake_dir}"
|
|
||||||
|
|
||||||
args += [f'{flake}#clanInternals.machines."{system}".{self.name}.{attr}']
|
|
||||||
args += nix_options + self.nix_options
|
|
||||||
|
|
||||||
if method == "eval":
|
|
||||||
output = run_no_stdout(
|
|
||||||
nix_eval(args), opts=RunOpts(prefix=self.name)
|
|
||||||
).stdout.strip()
|
|
||||||
return output
|
|
||||||
return Path(
|
|
||||||
run_no_stdout(
|
|
||||||
nix_build(args), opts=RunOpts(prefix=self.name)
|
|
||||||
).stdout.strip()
|
|
||||||
)
|
)
|
||||||
|
|
||||||
def eval_nix(
|
def eval_nix(
|
||||||
@@ -317,27 +250,23 @@ class Machine:
|
|||||||
refresh: bool = False,
|
refresh: bool = False,
|
||||||
extra_config: None | dict = None,
|
extra_config: None | dict = None,
|
||||||
nix_options: list[str] | None = None,
|
nix_options: list[str] | None = None,
|
||||||
) -> str:
|
) -> Any:
|
||||||
"""
|
"""
|
||||||
eval a nix attribute of the machine
|
eval a nix attribute of the machine
|
||||||
@attr: the attribute to get
|
@attr: the attribute to get
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
if extra_config:
|
||||||
|
log.warning("extra_config in eval_nix is no longer supported")
|
||||||
|
|
||||||
if nix_options is None:
|
if nix_options is None:
|
||||||
nix_options = []
|
nix_options = []
|
||||||
if attr in self._eval_cache and not refresh and extra_config is None:
|
|
||||||
return self._eval_cache[attr]
|
|
||||||
|
|
||||||
output = self.nix("eval", attr, extra_config, nix_options)
|
return self.nix("eval", attr, nix_options)
|
||||||
if isinstance(output, str):
|
|
||||||
self._eval_cache[attr] = output
|
|
||||||
return output
|
|
||||||
msg = "eval_nix returned not a string"
|
|
||||||
raise ClanError(msg)
|
|
||||||
|
|
||||||
def build_nix(
|
def build_nix(
|
||||||
self,
|
self,
|
||||||
attr: str,
|
attr: str,
|
||||||
refresh: bool = False,
|
|
||||||
extra_config: None | dict = None,
|
extra_config: None | dict = None,
|
||||||
nix_options: list[str] | None = None,
|
nix_options: list[str] | None = None,
|
||||||
) -> Path:
|
) -> Path:
|
||||||
@@ -346,17 +275,18 @@ class Machine:
|
|||||||
@attr: the attribute to get
|
@attr: the attribute to get
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
if extra_config:
|
||||||
|
log.warning("extra_config in build_nix is no longer supported")
|
||||||
|
|
||||||
if nix_options is None:
|
if nix_options is None:
|
||||||
nix_options = []
|
nix_options = []
|
||||||
if attr in self._build_cache and not refresh and extra_config is None:
|
|
||||||
return self._build_cache[attr]
|
|
||||||
|
|
||||||
output = self.nix("build", attr, extra_config, nix_options)
|
output = self.nix("build", attr, nix_options)
|
||||||
assert isinstance(output, Path), "Nix build did not result in a single path"
|
output = Path(output)
|
||||||
if tmp_store := nix_test_store():
|
if tmp_store := nix_test_store():
|
||||||
output = tmp_store.joinpath(*output.parts[1:])
|
output = tmp_store.joinpath(*output.parts[1:])
|
||||||
|
assert output.exists(), f"The output {output} doesn't exist"
|
||||||
if isinstance(output, Path):
|
if isinstance(output, Path):
|
||||||
self._build_cache[attr] = output
|
|
||||||
return output
|
return output
|
||||||
msg = "build_nix returned not a Path"
|
msg = "build_nix returned not a Path"
|
||||||
raise ClanError(msg)
|
raise ClanError(msg)
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import io
|
import io
|
||||||
import json
|
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import tarfile
|
import tarfile
|
||||||
@@ -32,9 +31,7 @@ class SecretStore(StoreBase):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def _store_backend(self) -> str:
|
def _store_backend(self) -> str:
|
||||||
backend = json.loads(
|
backend = self.machine.eval_nix("config.clan.core.vars.settings.passBackend")
|
||||||
self.machine.eval_nix("config.clan.core.vars.settings.passBackend")
|
|
||||||
)
|
|
||||||
return backend
|
return backend
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import argparse
|
import argparse
|
||||||
import dataclasses
|
import dataclasses
|
||||||
import json
|
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any
|
from typing import Any
|
||||||
@@ -55,7 +54,7 @@ class VmConfig:
|
|||||||
|
|
||||||
|
|
||||||
def inspect_vm(machine: Machine) -> VmConfig:
|
def inspect_vm(machine: Machine) -> VmConfig:
|
||||||
data = json.loads(machine.eval_nix("config.clan.core.vm.inspect"))
|
data = machine.eval_nix("config.clan.core.vm.inspect")
|
||||||
# HACK!
|
# HACK!
|
||||||
data["flake_url"] = dataclasses.asdict(machine.flake)
|
data["flake_url"] = dataclasses.asdict(machine.flake)
|
||||||
return VmConfig.from_json(data)
|
return VmConfig.from_json(data)
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ from typing import Any, NamedTuple
|
|||||||
import pytest
|
import pytest
|
||||||
from clan_cli.dirs import TemplateType, clan_templates, nixpkgs_source
|
from clan_cli.dirs import TemplateType, clan_templates, nixpkgs_source
|
||||||
from clan_cli.locked_open import locked_open
|
from clan_cli.locked_open import locked_open
|
||||||
|
from clan_cli.nix import nix_test_store
|
||||||
from fixture_error import FixtureError
|
from fixture_error import FixtureError
|
||||||
from root import CLAN_CORE
|
from root import CLAN_CORE
|
||||||
|
|
||||||
@@ -43,13 +44,13 @@ def substitute(
|
|||||||
for line in f:
|
for line in f:
|
||||||
line = line.replace("__NIXPKGS__", str(nixpkgs_source()))
|
line = line.replace("__NIXPKGS__", str(nixpkgs_source()))
|
||||||
if clan_core_flake:
|
if clan_core_flake:
|
||||||
line = line.replace("__CLAN_CORE__", str(clan_core_flake))
|
line = line.replace("__CLAN_CORE__", f"path:{clan_core_flake}")
|
||||||
line = line.replace(
|
line = line.replace(
|
||||||
"git+https://git.clan.lol/clan/clan-core", str(clan_core_flake)
|
"git+https://git.clan.lol/clan/clan-core", f"path:{clan_core_flake}"
|
||||||
)
|
)
|
||||||
line = line.replace(
|
line = line.replace(
|
||||||
"https://git.clan.lol/clan/clan-core/archive/main.tar.gz",
|
"https://git.clan.lol/clan/clan-core/archive/main.tar.gz",
|
||||||
str(clan_core_flake),
|
f"path:{clan_core_flake}",
|
||||||
)
|
)
|
||||||
line = line.replace("__CLAN_SOPS_KEY_PATH__", sops_key)
|
line = line.replace("__CLAN_SOPS_KEY_PATH__", sops_key)
|
||||||
line = line.replace("__CLAN_SOPS_KEY_DIR__", str(flake / "facts"))
|
line = line.replace("__CLAN_SOPS_KEY_DIR__", str(flake / "facts"))
|
||||||
@@ -278,6 +279,22 @@ def create_flake(
|
|||||||
flake_nix = flake / "flake.nix"
|
flake_nix = flake / "flake.nix"
|
||||||
# this is where we would install the sops key to, when updating
|
# this is where we would install the sops key to, when updating
|
||||||
substitute(flake_nix, clan_core_flake, flake)
|
substitute(flake_nix, clan_core_flake, flake)
|
||||||
|
nix_options = []
|
||||||
|
if tmp_store := nix_test_store():
|
||||||
|
nix_options += ["--store", str(tmp_store)]
|
||||||
|
|
||||||
|
sp.run(
|
||||||
|
[
|
||||||
|
"nix",
|
||||||
|
"flake",
|
||||||
|
"lock",
|
||||||
|
flake,
|
||||||
|
"--extra-experimental-features",
|
||||||
|
"nix-command flakes",
|
||||||
|
*nix_options,
|
||||||
|
],
|
||||||
|
check=True,
|
||||||
|
)
|
||||||
|
|
||||||
if "/tmp" not in str(os.environ.get("HOME")):
|
if "/tmp" not in str(os.environ.get("HOME")):
|
||||||
log.warning(
|
log.warning(
|
||||||
|
|||||||
@@ -2,21 +2,28 @@ from typing import TYPE_CHECKING
|
|||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from clan_cli.ssh.host import Host
|
from clan_cli.ssh.host import Host
|
||||||
from fixtures_flakes import FlakeForTest
|
from fixtures_flakes import ClanFlake
|
||||||
from helpers import cli
|
from helpers import cli
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from age_keys import KeyPair
|
from age_keys import KeyPair
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.impure
|
@pytest.mark.with_core
|
||||||
def test_secrets_upload(
|
def test_secrets_upload(
|
||||||
monkeypatch: pytest.MonkeyPatch,
|
monkeypatch: pytest.MonkeyPatch,
|
||||||
test_flake_with_core: FlakeForTest,
|
flake: ClanFlake,
|
||||||
hosts: list[Host],
|
hosts: list[Host],
|
||||||
age_keys: list["KeyPair"],
|
age_keys: list["KeyPair"],
|
||||||
) -> None:
|
) -> None:
|
||||||
monkeypatch.chdir(test_flake_with_core.path)
|
config = flake.machines["vm1"]
|
||||||
|
config["nixpkgs"]["hostPlatform"] = "x86_64-linux"
|
||||||
|
host = hosts[0]
|
||||||
|
addr = f"{host.user}@{host.host}:{host.port}?StrictHostKeyChecking=no&UserKnownHostsFile=/dev/null&IdentityFile={host.key}"
|
||||||
|
config["clan"]["networking"]["targetHost"] = addr
|
||||||
|
config["clan"]["core"]["facts"]["secretUploadDirectory"] = str(flake.path / "facts")
|
||||||
|
flake.refresh()
|
||||||
|
monkeypatch.chdir(str(flake.path))
|
||||||
monkeypatch.setenv("SOPS_AGE_KEY", age_keys[0].privkey)
|
monkeypatch.setenv("SOPS_AGE_KEY", age_keys[0].privkey)
|
||||||
|
|
||||||
cli.run(
|
cli.run(
|
||||||
@@ -25,7 +32,7 @@ def test_secrets_upload(
|
|||||||
"users",
|
"users",
|
||||||
"add",
|
"add",
|
||||||
"--flake",
|
"--flake",
|
||||||
str(test_flake_with_core.path),
|
str(flake.path),
|
||||||
"user1",
|
"user1",
|
||||||
age_keys[0].pubkey,
|
age_keys[0].pubkey,
|
||||||
]
|
]
|
||||||
@@ -37,27 +44,20 @@ def test_secrets_upload(
|
|||||||
"machines",
|
"machines",
|
||||||
"add",
|
"add",
|
||||||
"--flake",
|
"--flake",
|
||||||
str(test_flake_with_core.path),
|
str(flake.path),
|
||||||
"vm1",
|
"vm1",
|
||||||
age_keys[1].pubkey,
|
age_keys[1].pubkey,
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
monkeypatch.setenv("SOPS_NIX_SECRET", age_keys[0].privkey)
|
monkeypatch.setenv("SOPS_NIX_SECRET", age_keys[0].privkey)
|
||||||
cli.run(
|
cli.run(["secrets", "set", "--flake", str(flake.path), "vm1-age.key"])
|
||||||
["secrets", "set", "--flake", str(test_flake_with_core.path), "vm1-age.key"]
|
|
||||||
)
|
|
||||||
|
|
||||||
flake = test_flake_with_core.path.joinpath("flake.nix")
|
flake_path = flake.path.joinpath("flake.nix")
|
||||||
host = hosts[0]
|
|
||||||
addr = f"{host.user}@{host.host}:{host.port}?StrictHostKeyChecking=no&UserKnownHostsFile=/dev/null&IdentityFile={host.key}"
|
|
||||||
new_text = flake.read_text().replace("__CLAN_TARGET_ADDRESS__", addr)
|
|
||||||
|
|
||||||
flake.write_text(new_text)
|
cli.run(["facts", "upload", "--flake", str(flake_path), "vm1"])
|
||||||
|
|
||||||
cli.run(["facts", "upload", "--flake", str(test_flake_with_core.path), "vm1"])
|
|
||||||
|
|
||||||
# the flake defines this path as the location where the sops key should be installed
|
# the flake defines this path as the location where the sops key should be installed
|
||||||
sops_key = test_flake_with_core.path / "facts" / "key.txt"
|
sops_key = flake.path / "facts" / "key.txt"
|
||||||
|
|
||||||
assert sops_key.exists()
|
assert sops_key.exists()
|
||||||
assert sops_key.read_text() == age_keys[0].privkey
|
assert sops_key.read_text() == age_keys[0].privkey
|
||||||
|
|||||||
Reference in New Issue
Block a user