Initial upload with full project structure
This commit is contained in:
100
app/src/confgen/secrets.py
Normal file
100
app/src/confgen/secrets.py
Normal file
@@ -0,0 +1,100 @@
|
||||
"""Secrets resolver for environment variable interpolation."""
|
||||
|
||||
import os
|
||||
import re
|
||||
|
||||
|
||||
class SecretsResolver:
|
||||
"""Resolver for secret values from environment variables."""
|
||||
|
||||
ENV_PREFIX = "env."
|
||||
VAULT_PREFIX = "vault."
|
||||
|
||||
def __init__(self):
|
||||
self.env_cache = {}
|
||||
|
||||
def resolve(self, content: str) -> str:
|
||||
"""Resolve all secret placeholders in content."""
|
||||
content = self._resolve_env_vars(content)
|
||||
content = self._resolve_vault_secrets(content)
|
||||
return content
|
||||
|
||||
def _resolve_env_vars(self, content: str) -> str:
|
||||
"""Resolve environment variable placeholders."""
|
||||
pattern = r"\{\{env\.([A-Z0-9_]+)\}\}"
|
||||
|
||||
def replace(match):
|
||||
var_name = match.group(1)
|
||||
value = os.environ.get(var_name)
|
||||
if value is None:
|
||||
if var_name in self.env_cache:
|
||||
value = self.env_cache[var_name]
|
||||
else:
|
||||
value = os.environ.get(f"CONFGEN_{var_name}")
|
||||
if value is None:
|
||||
raise ValueError(f"Environment variable '{var_name}' not found")
|
||||
return value
|
||||
|
||||
return re.sub(pattern, replace, content)
|
||||
|
||||
def _resolve_vault_secrets(self, content: str) -> str:
|
||||
"""Resolve vault secret placeholders."""
|
||||
pattern = r"\{\{vault\.([A-Z0-9_]+)\}\}"
|
||||
|
||||
def replace(match):
|
||||
secret_path = match.group(1)
|
||||
return self._get_from_vault(secret_path)
|
||||
|
||||
return re.sub(pattern, replace, content)
|
||||
|
||||
def _get_from_vault(self, secret_path: str) -> str:
|
||||
"""Get a secret from a vault backend."""
|
||||
vault_url = os.environ.get("CONFGEN_VAULT_URL", "")
|
||||
vault_token = os.environ.get("CONFGEN_VAULT_TOKEN", "")
|
||||
|
||||
if not vault_url or not vault_token:
|
||||
raise ValueError(
|
||||
"Vault not configured. Set CONFGEN_VAULT_URL and CONFGEN_VAULT_TOKEN."
|
||||
)
|
||||
|
||||
try:
|
||||
import requests
|
||||
|
||||
headers = {"X-Vault-Token": vault_token}
|
||||
response = requests.get(
|
||||
f"{vault_url}/v1/{secret_path}",
|
||||
headers=headers,
|
||||
timeout=5,
|
||||
)
|
||||
response.raise_for_status()
|
||||
data = response.json()
|
||||
return data.get("data", {}).get("value", "")
|
||||
except ImportError:
|
||||
raise ValueError("requests library required for vault integration")
|
||||
except Exception as e:
|
||||
raise ValueError(f"Failed to get secret from vault: {e}")
|
||||
|
||||
def is_secret_placeholder(self, text: str) -> bool:
|
||||
"""Check if text contains a secret placeholder."""
|
||||
return self.ENV_PREFIX in text or self.VAULT_PREFIX in text
|
||||
|
||||
def get_secret_names(self, content: str) -> list[str]:
|
||||
"""Extract secret names from content."""
|
||||
env_pattern = r"\{\{env\.([A-Z0-9_]+)\}\}"
|
||||
vault_pattern = r"\{\{vault\.([A-Z0-9_]+)\}\}"
|
||||
|
||||
env_matches = re.findall(env_pattern, content)
|
||||
vault_matches = re.findall(vault_pattern, content)
|
||||
|
||||
return env_matches + vault_matches
|
||||
|
||||
def check_secret_availability(self, content: str) -> dict[str, bool]:
|
||||
"""Check which secrets are available in environment."""
|
||||
secret_names = self.get_secret_names(content)
|
||||
availability = {}
|
||||
|
||||
for name in secret_names:
|
||||
available = bool(os.environ.get(name) or os.environ.get(f"CONFGEN_{name}"))
|
||||
availability[name] = available
|
||||
|
||||
return availability
|
||||
Reference in New Issue
Block a user