controller: ${VAR} env interpolation в YAML config (для secrets)

Config.from_yaml теперь recursively expands ${VAR} и ${VAR:-default}
в string values через os.environ. Позволяет хранить tokens / passwords
в gitignored .env, passed контейнеру через compose env section:

  controller.yaml:
    extra_http_headers:
      Authorization: "Bearer ${GRAFANA_TOKEN}"

  .env (gitignored):
    GRAFANA_TOKEN=glsa_xxx

  docker-compose.override.yml controller:
    environment:
      GRAFANA_TOKEN: "${GRAFANA_TOKEN:-}"  # compose interpolates от .env

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
gx
2026-05-22 05:46:03 +01:00
parent c287caf7c1
commit 450cee3556
+22 -1
View File
@@ -28,13 +28,33 @@
from __future__ import annotations
import os
import re
from pathlib import Path
from typing import Self
from typing import Any, Self
import yaml
from pydantic import BaseModel, Field, field_validator
_ENV_PATTERN = re.compile(r"\$\{(\w+)(?::-([^}]*))?\}")
def _expand_env(obj: Any) -> Any:
"""Recursively replace ${VAR} and ${VAR:-default} в string values
через os.environ. Используется при load YAML config — secrets вроде
API tokens хранятся в .env (gitignored) и interpolate'ятся при start."""
if isinstance(obj, str):
return _ENV_PATTERN.sub(
lambda m: os.environ.get(m.group(1), m.group(2) or ""),
obj,
)
if isinstance(obj, dict):
return {k: _expand_env(v) for k, v in obj.items()}
if isinstance(obj, list):
return [_expand_env(v) for v in obj]
return obj
class BrokerCfg(BaseModel):
host: str = "localhost"
port: int = 1883
@@ -202,4 +222,5 @@ class Config(BaseModel):
def from_yaml(cls, path: Path | str) -> Self:
with open(path) as f:
data = yaml.safe_load(f) or {}
data = _expand_env(data)
return cls.model_validate(data)