Skip to content

Commit 2f950c0

Browse files
committed
chore: sync new vault client to all repos now support enviornments in vault
- Updated all repos to support prometheus metrics
1 parent 9dc562e commit 2f950c0

File tree

1 file changed

+62
-60
lines changed

1 file changed

+62
-60
lines changed

src/app/utils/vault_client.py

Lines changed: 62 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -4,98 +4,100 @@
44
"""
55

66
import os
7-
import time
7+
from functools import lru_cache
8+
from typing import Any, Optional
89

910
import hvac
11+
from tenacity import retry, stop_after_attempt, wait_fixed
1012

1113
from app.utils.setup_logger import setup_logger
1214

13-
__all__ = ["VaultClient", "get_secret_or_env"]
14-
1515
logger = setup_logger(__name__)
1616

17+
VAULT_ADDR: str = os.getenv("VAULT_ADDR", "http://127.0.0.1:8200")
18+
VAULT_ROLE_ID: str | None = os.getenv("VAULT_ROLE_ID")
19+
VAULT_SECRET_ID: str | None = os.getenv("VAULT_SECRET_ID")
20+
POLLER_NAME: str | None = os.getenv("POLLER_NAME")
21+
ENVIRONMENT: str = os.getenv("ENVIRONMENT", "dev")
22+
1723

1824
class VaultClient:
19-
"""Handles interaction with HashiCorp Vault using AppRole authentication."""
25+
"""VaultClient handles authentication and secret retrieval from HashiCorp Vault using AppRole."""
2026

2127
def __init__(self) -> None:
22-
"""Initialize the VaultClient using environment variables and authenticate."""
23-
self.vault_addr: str = os.getenv("VAULT_ADDR", "http://127.0.0.1:8200")
24-
self.role_id: str | None = os.getenv("VAULT_ROLE_ID")
25-
self.secret_id: str | None = os.getenv("VAULT_SECRET_ID")
26-
self.poller: str = os.getenv("POLLER_NAME", "stock_data_poller")
27-
self.environment: str = os.getenv("ENVIRONMENT", "dev")
28-
self.client: hvac.Client = hvac.Client(url=self.vault_addr)
29-
self.secrets: dict[str, str] = {}
28+
"""Initialize the Vault client and authenticate using AppRole.
3029
30+
Raises:
31+
RuntimeError: If authentication fails or no token is returned.
32+
33+
"""
34+
self.client: hvac.Client = hvac.Client(url=VAULT_ADDR)
3135
self._authenticate()
32-
self._load_secrets()
3336

37+
@retry(stop=stop_after_attempt(3), wait=wait_fixed(2))
3438
def _authenticate(self) -> None:
35-
"""Authenticate to Vault using AppRole credentials from the environment."""
36-
if not self.role_id or not self.secret_id:
37-
logger.warning("🔐 VAULT_ROLE_ID or VAULT_SECRET_ID not set — skipping Vault load.")
38-
return
39+
"""Authenticate to Vault using AppRole credentials.
3940
40-
for attempt in range(1, 4):
41+
Raises:
42+
RuntimeError: If authentication response is missing a token.
43+
44+
"""
45+
if VAULT_ROLE_ID and VAULT_SECRET_ID:
4146
try:
42-
login = self.client.auth.approle.login(
43-
role_id=self.role_id,
44-
secret_id=self.secret_id,
45-
)
46-
if login and login.get("auth"):
47-
self.client.token = login["auth"]["client_token"]
48-
logger.info(f"🔓 Authenticated to Vault as {self.poller}.")
49-
return
50-
logger.warning("⚠️ Vault login response missing 'auth'.")
47+
response: dict[str, Any] = self.client.auth_approle(VAULT_ROLE_ID, VAULT_SECRET_ID)
48+
if not response["auth"].get("client_token"):
49+
raise RuntimeError("❌ Failed to retrieve Vault token from response.")
50+
logger.info("🔐 Vault AppRole authentication successful.")
5151
except Exception as e:
52-
logger.warning(f"⚠️ Vault login attempt {attempt} failed: %s", e)
53-
time.sleep(2)
54-
55-
logger.error("❌ Failed to authenticate to Vault after 3 attempts.")
52+
logger.warning("⚠️ Vault authentication failed: %s", str(e))
53+
raise
54+
else:
55+
logger.warning("⚠️ VAULT_ROLE_ID or VAULT_SECRET_ID not provided. Vault auth skipped.")
5656

57-
def _load_secrets(self) -> None:
58-
"""Load secrets from Vault's KV v2 engine using the configured path."""
59-
try:
60-
path = f"{self.poller}/{self.environment}"
61-
response = self.client.secrets.kv.v2.read_secret_version(path=path)
62-
self.secrets = response["data"]["data"]
63-
logger.info(f"📦 Loaded {len(self.secrets)} secrets from Vault.")
64-
except Exception as e:
65-
logger.warning("❌ Failed to load secrets from Vault: %s", e)
66-
self.secrets = {}
67-
68-
def get(self, key: str, default: str | None = None) -> str | None:
69-
"""Retrieve a secret by key.
57+
@retry(stop=stop_after_attempt(3), wait=wait_fixed(2))
58+
def get(self, key: str, fallback: str | None = None) -> str | None:
59+
"""Retrieve a value from Vault for the given key.
7060
7161
Args:
72-
key (str): The secret key to retrieve.
73-
default (str | None): The fallback value if key is not found.
62+
key (str): The key to retrieve from Vault.
63+
fallback (Optional[str]): Value to return if Vault lookup fails.
7464
7565
Returns:
76-
str | None: The secret value or the default.
66+
Optional[str]: The retrieved value or fallback if not found.
7767
7868
"""
79-
return self.secrets.get(key, default)
69+
if not POLLER_NAME:
70+
logger.warning("⚠️ POLLER_NAME not set. Skipping Vault lookup for key: %s", key)
71+
return fallback
8072

73+
secret_path: str = f"secret/data/{POLLER_NAME}/{ENVIRONMENT}"
8174

82-
# Singleton Vault client
83-
_vault_client: VaultClient | None = None
75+
try:
76+
secret: dict[str, Any] = self.client.secrets.kv.v2.read_secret_version(
77+
path=f"{POLLER_NAME}/{ENVIRONMENT}"
78+
)
79+
value: Any | None = secret["data"]["data"].get(key)
80+
if value is not None:
81+
logger.debug("🔑 Vault value retrieved for key: %s", key)
82+
return str(value)
83+
logger.warning("⚠️ Key '%s' not found in Vault path '%s'.", key, secret_path)
84+
except Exception as e:
85+
logger.warning("⚠️ Failed to retrieve key '%s' from Vault: %s", key, str(e))
86+
87+
return fallback
8488

8589

86-
def get_secret_or_env(key: str, default: str = "") -> str:
87-
"""Return the secret from Vault or fall back to environment variable.
90+
@lru_cache
91+
def get_secret_or_env(key: str, default: str | None = None) -> str | None:
92+
"""Retrieve a secret value from Vault or fallback to environment variable or default.
8893
8994
Args:
90-
key (str): The secret key or environment variable name.
91-
default (str): Fallback value if not found.
95+
key (str): The secret key to retrieve.
96+
default (Optional[str]): Default value to return if key not found in Vault or env.
9297
9398
Returns:
94-
str: The resolved value.
99+
Optional[str]: Retrieved value or fallback.
95100
96101
"""
97-
global _vault_client
98-
if _vault_client is None:
99-
_vault_client = VaultClient()
100-
101-
return _vault_client.get(key, os.getenv(key, default)) or default
102+
vault_client: VaultClient = VaultClient()
103+
return vault_client.get(key, fallback=os.getenv(key, default))

0 commit comments

Comments
 (0)