All checks were successful
Build and Push Docker Image / build (push) Successful in 14s
When Grist validates the Host header (common with reverse proxy setups), internal Docker networking fails because requests arrive with Host: container-name instead of the external domain. The new host_header config option allows overriding the Host header sent to Grist while still connecting via internal Docker hostnames.
102 lines
2.5 KiB
Python
102 lines
2.5 KiB
Python
"""Configuration loading and parsing."""
|
|
|
|
import os
|
|
import re
|
|
from dataclasses import dataclass
|
|
from pathlib import Path
|
|
|
|
import yaml
|
|
|
|
|
|
@dataclass
|
|
class Document:
|
|
"""A Grist document configuration."""
|
|
url: str
|
|
doc_id: str
|
|
api_key: str
|
|
host_header: str | None = None # Override Host header for Docker networking
|
|
|
|
|
|
@dataclass
|
|
class TokenScope:
|
|
"""Access scope for a single document."""
|
|
document: str
|
|
permissions: list[str]
|
|
|
|
|
|
@dataclass
|
|
class Token:
|
|
"""An agent token with its access scopes."""
|
|
token: str
|
|
name: str
|
|
scope: list[TokenScope]
|
|
|
|
|
|
@dataclass
|
|
class Config:
|
|
"""Full server configuration."""
|
|
documents: dict[str, Document]
|
|
tokens: list[Token]
|
|
|
|
|
|
def _substitute_env_vars(value: str) -> str:
|
|
"""Replace ${VAR} patterns with environment variable values."""
|
|
pattern = r'\$\{([^}]+)\}'
|
|
|
|
def replacer(match: re.Match) -> str:
|
|
var_name = match.group(1)
|
|
env_value = os.environ.get(var_name)
|
|
if env_value is None:
|
|
raise ValueError(f"Environment variable not set: {var_name}")
|
|
return env_value
|
|
|
|
return re.sub(pattern, replacer, value)
|
|
|
|
|
|
def _substitute_env_vars_recursive(obj):
|
|
"""Recursively substitute env vars in a data structure."""
|
|
if isinstance(obj, str):
|
|
return _substitute_env_vars(obj)
|
|
elif isinstance(obj, dict):
|
|
return {k: _substitute_env_vars_recursive(v) for k, v in obj.items()}
|
|
elif isinstance(obj, list):
|
|
return [_substitute_env_vars_recursive(item) for item in obj]
|
|
return obj
|
|
|
|
|
|
def load_config(config_path: str) -> Config:
|
|
"""Load and parse configuration from YAML file."""
|
|
path = Path(config_path)
|
|
raw = yaml.safe_load(path.read_text())
|
|
|
|
# Substitute environment variables
|
|
raw = _substitute_env_vars_recursive(raw)
|
|
|
|
# Parse documents
|
|
documents = {}
|
|
for name, doc_data in raw.get("documents", {}).items():
|
|
documents[name] = Document(
|
|
url=doc_data["url"],
|
|
doc_id=doc_data["doc_id"],
|
|
api_key=doc_data["api_key"],
|
|
host_header=doc_data.get("host_header"),
|
|
)
|
|
|
|
# Parse tokens
|
|
tokens = []
|
|
for token_data in raw.get("tokens", []):
|
|
scope = [
|
|
TokenScope(
|
|
document=s["document"],
|
|
permissions=s["permissions"],
|
|
)
|
|
for s in token_data.get("scope", [])
|
|
]
|
|
tokens.append(Token(
|
|
token=token_data["token"],
|
|
name=token_data["name"],
|
|
scope=scope,
|
|
))
|
|
|
|
return Config(documents=documents, tokens=tokens)
|