Coverage for arclith / adapters / output / vault / secret_adapter.py: 90%
32 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-03-25 15:02 +0100
« prev ^ index » next coverage.py v7.13.5, created at 2026-03-25 15:02 +0100
1from __future__ import annotations
3import os
4from pathlib import Path
6from arclith.domain.ports.secret_resolver import SecretResolver
9def _read_vault_token() -> str | None:
10 token = os.environ.get("VAULT_TOKEN")
11 if token:
12 return token
13 token_file = Path.home() / ".vault-token"
14 if token_file.exists(): 14 ↛ 16line 14 didn't jump to line 16 because the condition on line 14 was always true
15 return token_file.read_text().strip() or None
16 return None
19class VaultSecretAdapter(SecretResolver):
20 """Reads secrets from HashiCorp Vault KV v2.
22 Each secret_key is a Vault path relative to the mount point.
23 The secret must expose its value under the field "value".
25 Example:
26 vault kv put kv/rekipe/recipe/mongodb value="mongodb://..."
27 """
29 def __init__(self, addr: str, mount: str = "kv") -> None:
30 self._addr = addr
31 self._mount = mount
33 def get(self, field_path: str, secret_key: str) -> str | None:
34 try:
35 import hvac # type: ignore[import-untyped]
36 except ImportError:
37 return None
39 token = _read_vault_token()
40 if not token: 40 ↛ 41line 40 didn't jump to line 41 because the condition on line 40 was never true
41 return None
43 try:
44 client = hvac.Client(url=self._addr, token=token)
45 if not client.is_authenticated():
46 return None
47 response = client.secrets.kv.v2.read_secret_version(
48 path=secret_key,
49 mount_point=self._mount,
50 raise_on_deleted_version=True,
51 )
52 return response["data"]["data"].get("value")
53 except Exception:
54 return None