Перенесены секреты из .env в SM
This commit is contained in:
@@ -0,0 +1,175 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Synchronize runtime secrets and execute a command without exporting their values."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
from secrets_loader import LoaderError, load_json, run
|
||||
|
||||
|
||||
def load_public_config(path: Path) -> dict[str, str]:
|
||||
values: dict[str, str] = {}
|
||||
try:
|
||||
lines = path.read_text(encoding="utf-8").splitlines()
|
||||
except OSError as exc:
|
||||
raise LoaderError(
|
||||
f"cannot read non-secret config: {exc.strerror or exc.__class__.__name__}"
|
||||
) from None
|
||||
for number, raw in enumerate(lines, 1):
|
||||
line = raw.strip()
|
||||
if not line or line.startswith("#"):
|
||||
continue
|
||||
if "=" not in line:
|
||||
raise LoaderError(f"non-secret config has invalid syntax at line {number}")
|
||||
key, value = line.split("=", 1)
|
||||
key = key.strip()
|
||||
if not key or key in values:
|
||||
raise LoaderError(f"non-secret config has an invalid key at line {number}")
|
||||
values[key] = value.strip()
|
||||
return values
|
||||
|
||||
|
||||
def loader_config_path(
|
||||
public: dict[str, str],
|
||||
explicit: Path | None,
|
||||
environ: dict[str, str],
|
||||
) -> tuple[str, Path]:
|
||||
source = public.get("SECRETS_SOURCE")
|
||||
if source not in {"selectel", "file"}:
|
||||
raise LoaderError("SECRETS_SOURCE must explicitly be selectel or file")
|
||||
if explicit is not None:
|
||||
return source, explicit
|
||||
override = environ.get(f"HAN_SECRETS_{source.upper()}_CONFIG")
|
||||
if override:
|
||||
return source, Path(override)
|
||||
environment = public.get("APP_ENV", "production")
|
||||
return source, Path(f"/etc/han/secrets/{environment}.{source}.json")
|
||||
|
||||
|
||||
def prepare_environment(
|
||||
config_path: Path,
|
||||
source: str,
|
||||
environ: dict[str, str],
|
||||
*,
|
||||
synchronize: bool,
|
||||
) -> dict[str, str]:
|
||||
document = load_json(config_path)
|
||||
if document.get("mode") != source:
|
||||
raise LoaderError("selected loader configuration mode does not match SECRETS_SOURCE")
|
||||
runtime = document.get("runtime_dir")
|
||||
if not isinstance(runtime, str) or not Path(runtime).is_absolute():
|
||||
raise LoaderError("loader configuration has an invalid runtime_dir")
|
||||
runtime_dir = Path(runtime)
|
||||
manifest = runtime_dir / "manifest"
|
||||
state_path = runtime_dir / "state.json"
|
||||
consumers: list[str] = []
|
||||
if synchronize or (source == "file" and not state_path.is_file()):
|
||||
consumers = run(config_path, environ=environ)
|
||||
state = {
|
||||
"version": 1,
|
||||
"source": source,
|
||||
"loader_config": str(config_path.resolve()),
|
||||
}
|
||||
descriptor, temporary = tempfile.mkstemp(
|
||||
prefix=".state.", suffix=".tmp", dir=runtime_dir
|
||||
)
|
||||
temporary_path = Path(temporary)
|
||||
try:
|
||||
os.chmod(temporary_path, 0o600)
|
||||
with os.fdopen(descriptor, "w", encoding="utf-8") as stream:
|
||||
descriptor = -1
|
||||
json.dump(state, stream, separators=(",", ":"))
|
||||
stream.write("\n")
|
||||
stream.flush()
|
||||
os.fsync(stream.fileno())
|
||||
os.replace(temporary_path, state_path)
|
||||
except BaseException:
|
||||
if descriptor >= 0:
|
||||
os.close(descriptor)
|
||||
temporary_path.unlink(missing_ok=True)
|
||||
raise
|
||||
elif not state_path.is_file():
|
||||
raise LoaderError(
|
||||
"runtime secrets are not synchronized; restart han-secrets systemd unit"
|
||||
)
|
||||
else:
|
||||
state = load_json(state_path)
|
||||
if (
|
||||
state.get("version") != 1
|
||||
or state.get("source") != source
|
||||
or state.get("loader_config") != str(config_path.resolve())
|
||||
):
|
||||
raise LoaderError(
|
||||
"runtime secret state does not match selected source/config; synchronize first"
|
||||
)
|
||||
if not manifest.is_file():
|
||||
raise LoaderError("runtime secret manifest was not materialized")
|
||||
manifest_entries: dict[str, str] = {}
|
||||
for line in manifest.read_text(encoding="utf-8").splitlines():
|
||||
key, value_path = line.split("=", 1)
|
||||
manifest_entries[key] = value_path
|
||||
specs = document.get("secrets")
|
||||
if not isinstance(specs, dict) or set(manifest_entries) != set(specs):
|
||||
raise LoaderError("runtime secret manifest does not match loader configuration")
|
||||
child = dict(environ)
|
||||
child["HAN_SECRETS_ACTIVE"] = "1"
|
||||
child["HAN_RUNTIME_SECRET_DIR"] = str(runtime_dir)
|
||||
child["HAN_RUNTIME_SECRET_MANIFEST"] = str(manifest)
|
||||
for key, value_path in manifest_entries.items():
|
||||
if not Path(value_path).is_file():
|
||||
raise LoaderError("runtime secret manifest references a missing file")
|
||||
child[f"{key}_FILE"] = value_path
|
||||
if synchronize or consumers:
|
||||
print(
|
||||
f"han-secrets: synchronized {len(consumers)} service scope(s) from {source}",
|
||||
file=sys.stderr,
|
||||
)
|
||||
return child
|
||||
|
||||
|
||||
def main(argv: list[str] | None = None) -> int:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("action", choices=("sync", "run"))
|
||||
parser.add_argument("--config", type=Path, default=Path(".env"))
|
||||
parser.add_argument("--loader-config", type=Path)
|
||||
arguments, command = parser.parse_known_args(argv)
|
||||
if command and command[0] == "--":
|
||||
command.pop(0)
|
||||
if arguments.action == "run" and not command:
|
||||
parser.error("run requires a command after --")
|
||||
if arguments.action == "sync" and command:
|
||||
parser.error("sync does not accept a command")
|
||||
|
||||
environment = dict(os.environ)
|
||||
try:
|
||||
public = load_public_config(arguments.config)
|
||||
source, loader_config = loader_config_path(
|
||||
public, arguments.loader_config, environment
|
||||
)
|
||||
child = prepare_environment(
|
||||
loader_config,
|
||||
source,
|
||||
environment,
|
||||
synchronize=arguments.action == "sync",
|
||||
)
|
||||
except (LoaderError, OSError, ValueError, json.JSONDecodeError) as exc:
|
||||
message = str(exc) if isinstance(exc, LoaderError) else exc.__class__.__name__
|
||||
print(f"han-secrets: {message}", file=sys.stderr)
|
||||
return 1
|
||||
if arguments.action == "sync":
|
||||
return 0
|
||||
if os.name == "nt":
|
||||
return subprocess.call(command, env=child)
|
||||
os.execvpe(command[0], command, child)
|
||||
return 127
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user