fix case when secrets are regenerated during update/install
All checks were successful
checks / check-links (pull_request) Successful in 14s
checks / checks-impure (pull_request) Successful in 1m53s
checks / checks (pull_request) Successful in 4m11s

This commit is contained in:
Jörg Thalheim 2024-04-12 14:38:21 +02:00
parent 573a462aee
commit b3522b73aa
3 changed files with 101 additions and 89 deletions

View File

@ -36,101 +36,101 @@ def generate_service_facts(
public_facts_store: FactStoreBase,
tmpdir: Path,
prompt: Callable[[str], str],
) -> None:
) -> bool:
service_dir = tmpdir / service
# check if all secrets exist and generate them if at least one is missing
needs_regeneration = not check_secrets(machine, service=service)
log.debug(f"{service} needs_regeneration: {needs_regeneration}")
if needs_regeneration:
if not isinstance(machine.flake, Path):
msg = f"flake is not a Path: {machine.flake}"
msg += "fact/secret generation is only supported for local flakes"
if not needs_regeneration:
return False
if not isinstance(machine.flake, Path):
msg = f"flake is not a Path: {machine.flake}"
msg += "fact/secret generation is only supported for local flakes"
env = os.environ.copy()
facts_dir = service_dir / "facts"
facts_dir.mkdir(parents=True)
env["facts"] = str(facts_dir)
secrets_dir = service_dir / "secrets"
secrets_dir.mkdir(parents=True)
env["secrets"] = str(secrets_dir)
# compatibility for old outputs.nix users
if isinstance(machine.facts_data[service]["generator"], str):
generator = machine.facts_data[service]["generator"]
env = os.environ.copy()
facts_dir = service_dir / "facts"
facts_dir.mkdir(parents=True)
env["facts"] = str(facts_dir)
secrets_dir = service_dir / "secrets"
secrets_dir.mkdir(parents=True)
env["secrets"] = str(secrets_dir)
# compatibility for old outputs.nix users
if isinstance(machine.facts_data[service]["generator"], str):
generator = machine.facts_data[service]["generator"]
else:
generator = machine.facts_data[service]["generator"]["finalScript"]
if machine.facts_data[service]["generator"]["prompt"]:
prompt_value = prompt(machine.facts_data[service]["generator"]["prompt"])
env["prompt_value"] = prompt_value
# fmt: off
cmd = nix_shell(
[
"nixpkgs#bash",
"nixpkgs#bubblewrap",
],
[
"bwrap",
"--ro-bind", "/nix/store", "/nix/store",
"--tmpfs", "/usr/lib/systemd",
"--dev", "/dev",
"--bind", str(facts_dir), str(facts_dir),
"--bind", str(secrets_dir), str(secrets_dir),
"--unshare-all",
"--unshare-user",
"--uid", "1000",
"--",
"bash", "-c", generator
],
)
# fmt: on
run(
cmd,
env=env,
)
files_to_commit = []
# store secrets
for secret in machine.facts_data[service]["secret"]:
if isinstance(secret, str):
# TODO: This is the old NixOS module, can be dropped everyone has updated.
secret_name = secret
groups = []
else:
generator = machine.facts_data[service]["generator"]["finalScript"]
if machine.facts_data[service]["generator"]["prompt"]:
prompt_value = prompt(
machine.facts_data[service]["generator"]["prompt"]
)
env["prompt_value"] = prompt_value
# fmt: off
cmd = nix_shell(
[
"nixpkgs#bash",
"nixpkgs#bubblewrap",
],
[
"bwrap",
"--ro-bind", "/nix/store", "/nix/store",
"--tmpfs", "/usr/lib/systemd",
"--dev", "/dev",
"--bind", str(facts_dir), str(facts_dir),
"--bind", str(secrets_dir), str(secrets_dir),
"--unshare-all",
"--unshare-user",
"--uid", "1000",
"--",
"bash", "-c", generator
],
)
# fmt: on
run(
cmd,
env=env,
)
files_to_commit = []
# store secrets
for secret in machine.facts_data[service]["secret"]:
if isinstance(secret, str):
# TODO: This is the old NixOS module, can be dropped everyone has updated.
secret_name = secret
groups = []
else:
secret_name = secret["name"]
groups = secret.get("groups", [])
secret_name = secret["name"]
groups = secret.get("groups", [])
secret_file = secrets_dir / secret_name
if not secret_file.is_file():
msg = f"did not generate a file for '{secret_name}' when running the following command:\n"
msg += generator
raise ClanError(msg)
secret_path = secret_facts_store.set(
service, secret_name, secret_file.read_bytes(), groups
)
if secret_path:
files_to_commit.append(secret_path)
# store facts
for name in machine.facts_data[service]["public"]:
fact_file = facts_dir / name
if not fact_file.is_file():
msg = f"did not generate a file for '{name}' when running the following command:\n"
msg += machine.facts_data[service]["generator"]
raise ClanError(msg)
fact_file = public_facts_store.set(service, name, fact_file.read_bytes())
if fact_file:
files_to_commit.append(fact_file)
commit_files(
files_to_commit,
machine.flake_dir,
f"Update facts/secrets for service {service} in machine {machine.name}",
secret_file = secrets_dir / secret_name
if not secret_file.is_file():
msg = f"did not generate a file for '{secret_name}' when running the following command:\n"
msg += generator
raise ClanError(msg)
secret_path = secret_facts_store.set(
service, secret_name, secret_file.read_bytes(), groups
)
if secret_path:
files_to_commit.append(secret_path)
# store facts
for name in machine.facts_data[service]["public"]:
fact_file = facts_dir / name
if not fact_file.is_file():
msg = f"did not generate a file for '{name}' when running the following command:\n"
msg += machine.facts_data[service]["generator"]
raise ClanError(msg)
fact_file = public_facts_store.set(service, name, fact_file.read_bytes())
if fact_file:
files_to_commit.append(fact_file)
commit_files(
files_to_commit,
machine.flake_dir,
f"Update facts/secrets for service {service} in machine {machine.name}",
)
return True
def generate_facts(
machine: Machine,
prompt: None | Callable[[str], str] = None,
) -> None:
) -> bool:
secret_facts_module = importlib.import_module(machine.secret_facts_module)
secret_facts_store = secret_facts_module.SecretStore(machine=machine)
@ -145,10 +145,11 @@ def generate_facts(
prompt = prompt_func
was_regenerated = False
with TemporaryDirectory() as tmp:
tmpdir = Path(tmp)
for service in machine.facts_data:
generate_service_facts(
was_regenerated |= generate_service_facts(
machine=machine,
service=service,
secret_facts_store=secret_facts_store,
@ -157,7 +158,12 @@ def generate_facts(
prompt=prompt,
)
print("successfully generated secrets")
if was_regenerated:
# flush caches to make sure the new secrets are available in evaluation
machine.flush_caches()
else:
print("All secrets and facts are already up to date")
return was_regenerated
def generate_command(args: argparse.Namespace) -> None:

View File

@ -81,6 +81,12 @@ class Machine:
self.vm: QMPWrapper = QMPWrapper(state_dir)
def flush_caches(self) -> None:
self._deployment_info = None
self._flake_path = None
self.build_cache.clear()
self.eval_cache.clear()
def __str__(self) -> str:
return f"Machine(name={self.data.name}, flake={self.data.flake_id})"

View File

@ -96,6 +96,11 @@ def deploy_nixos(hosts: HostGroup) -> None:
ssh_arg = f"-p {h.port}" if h.port else ""
env = os.environ.copy()
env["NIX_SSHOPTS"] = ssh_arg
machine: Machine = h.meta["machine"]
generate_facts(machine)
upload_secrets(machine)
path = upload_sources(".", target)
if h.host_key_check != HostKeyCheck.STRICT:
@ -105,11 +110,6 @@ def deploy_nixos(hosts: HostGroup) -> None:
ssh_arg += " -i " + h.key if h.key else ""
machine: Machine = h.meta["machine"]
generate_facts(machine)
upload_secrets(machine)
extra_args = h.meta.get("extra_args", [])
cmd = [
"nixos-rebuild",