Sanitized single-commit public mirror of recipe-maintainer. - Removed test-ssh/.testenv (live creds); added test-ssh/.testenv.example placeholders. - Removed plans/ and planned-updates/ (deployment-planning docs) so no client/ deployment domains appear in the public repo. - All other secret stores were already gitignored. - docs.coopcloud.tech retained as a submodule (public upstream).
83 lines
2.5 KiB
Python
83 lines
2.5 KiB
Python
"""Secret resolution, syncing, and reading from containers.
|
|
|
|
Replaces the bash scripts resolve-secrets.sh and sync-secrets.sh
|
|
with Python equivalents that work with the new deployment model.
|
|
"""
|
|
|
|
from pathlib import Path
|
|
|
|
from lib.config import paths, load_recipe_toml
|
|
from lib.env import read_env_file
|
|
from lib.ssh import read_container_secrets
|
|
|
|
|
|
def secrets_dir() -> Path:
|
|
"""Return the secrets directory: recipe-info/testsecrets/."""
|
|
return paths.TESTSECRETS_DIR
|
|
|
|
|
|
def load_secrets(domain: str) -> dict[str, str]:
|
|
"""Load locally-stored secrets for a deployment.
|
|
|
|
Reads from recipe-info/testsecrets/<domain>.
|
|
Returns empty dict if no secrets file exists.
|
|
"""
|
|
p = secrets_dir() / domain
|
|
if not p.exists():
|
|
return {}
|
|
return read_env_file(p)
|
|
|
|
|
|
def save_secrets(domain: str, secrets: dict[str, str]) -> Path:
|
|
"""Save secrets to recipe-info/testsecrets/<domain>."""
|
|
d = secrets_dir()
|
|
d.mkdir(parents=True, exist_ok=True)
|
|
p = d / domain
|
|
with open(p, "w") as f:
|
|
for name in sorted(secrets.keys()):
|
|
f.write(f"{name}={secrets[name]}\n")
|
|
return p
|
|
|
|
|
|
def sync_secrets_for_deployment(server: str, domain: str) -> dict[str, str]:
|
|
"""Sync secrets from running containers to local file.
|
|
|
|
SSHes into the server, reads /run/secrets/ from all containers
|
|
of the stack, and saves to recipe-info/testsecrets/<domain>.
|
|
|
|
Returns the secrets dict.
|
|
"""
|
|
stack_prefix = domain.replace(".", "_")
|
|
secrets = read_container_secrets(server, stack_prefix)
|
|
if secrets:
|
|
save_secrets(domain, secrets)
|
|
print(f" Wrote {len(secrets)} secrets for {domain}", flush=True)
|
|
else:
|
|
print(f" No secrets found for {domain}", flush=True)
|
|
return secrets
|
|
|
|
|
|
def sync_all_secrets(server: str) -> dict[str, dict]:
|
|
"""Sync secrets for all recipes that have a recipe.toml.
|
|
|
|
Returns {domain: {secret_name: value}}.
|
|
"""
|
|
from lib.models import load_default_instance
|
|
|
|
inst = load_default_instance()
|
|
recipe_info_dir = paths.RECIPE_INFO_DIR
|
|
if not recipe_info_dir.exists():
|
|
return {}
|
|
|
|
results = {}
|
|
for d in sorted(recipe_info_dir.iterdir()):
|
|
recipe_toml = d / "recipe.toml"
|
|
if d.is_dir() and recipe_toml.exists():
|
|
recipe_name = d.name
|
|
domain = f"{recipe_name}.{inst.domain_suffix}"
|
|
print(f"=== {recipe_name} ({domain}) ===", flush=True)
|
|
results[domain] = sync_secrets_for_deployment(
|
|
server, domain
|
|
)
|
|
return results
|