flow
This commit is contained in:
151
core/config.py
Normal file
151
core/config.py
Normal file
@@ -0,0 +1,151 @@
|
||||
"""Configuration loading (INI config + YAML manifest) and FlowContext."""
|
||||
|
||||
import configparser
|
||||
from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
import yaml
|
||||
|
||||
from flow.core.console import ConsoleLogger
|
||||
from flow.core import paths
|
||||
from flow.core.platform import PlatformInfo
|
||||
|
||||
|
||||
@dataclass
|
||||
class TargetConfig:
|
||||
namespace: str
|
||||
platform: str
|
||||
ssh_host: str
|
||||
ssh_identity: Optional[str] = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class AppConfig:
|
||||
dotfiles_url: str = ""
|
||||
dotfiles_branch: str = "main"
|
||||
projects_dir: str = "~/projects"
|
||||
container_registry: str = "registry.tomastm.com"
|
||||
container_tag: str = "latest"
|
||||
tmux_session: str = "default"
|
||||
targets: List[TargetConfig] = field(default_factory=list)
|
||||
|
||||
|
||||
def _parse_target_config(key: str, value: str) -> Optional[TargetConfig]:
|
||||
"""Parse a target line from config.
|
||||
|
||||
Supported formats:
|
||||
1) namespace = platform ssh_host [ssh_identity]
|
||||
2) namespace@platform = ssh_host [ssh_identity]
|
||||
"""
|
||||
parts = value.split()
|
||||
if not parts:
|
||||
return None
|
||||
|
||||
if "@" in key:
|
||||
namespace, platform = key.split("@", 1)
|
||||
ssh_host = parts[0]
|
||||
ssh_identity = parts[1] if len(parts) > 1 else None
|
||||
if not namespace or not platform:
|
||||
return None
|
||||
return TargetConfig(
|
||||
namespace=namespace,
|
||||
platform=platform,
|
||||
ssh_host=ssh_host,
|
||||
ssh_identity=ssh_identity,
|
||||
)
|
||||
|
||||
if len(parts) < 2:
|
||||
return None
|
||||
|
||||
return TargetConfig(
|
||||
namespace=key,
|
||||
platform=parts[0],
|
||||
ssh_host=parts[1],
|
||||
ssh_identity=parts[2] if len(parts) > 2 else None,
|
||||
)
|
||||
|
||||
|
||||
def load_config(path: Optional[Path] = None) -> AppConfig:
|
||||
"""Load INI config file into AppConfig with cascading priority.
|
||||
|
||||
Priority:
|
||||
1. Dotfiles repo (self-hosted): ~/.local/share/devflow/dotfiles/flow/.config/flow/config
|
||||
2. Local override: ~/.config/devflow/config
|
||||
3. Empty fallback
|
||||
"""
|
||||
cfg = AppConfig()
|
||||
|
||||
if path is None:
|
||||
# Priority 1: Check dotfiles repo for self-hosted config
|
||||
if paths.DOTFILES_CONFIG.exists():
|
||||
path = paths.DOTFILES_CONFIG
|
||||
# Priority 2: Fall back to local config
|
||||
else:
|
||||
path = paths.CONFIG_FILE
|
||||
|
||||
assert path is not None
|
||||
|
||||
if not path.exists():
|
||||
return cfg
|
||||
|
||||
parser = configparser.ConfigParser()
|
||||
parser.read(path)
|
||||
|
||||
if parser.has_section("repository"):
|
||||
cfg.dotfiles_url = parser.get("repository", "dotfiles_url", fallback=cfg.dotfiles_url)
|
||||
cfg.dotfiles_branch = parser.get("repository", "dotfiles_branch", fallback=cfg.dotfiles_branch)
|
||||
|
||||
if parser.has_section("paths"):
|
||||
cfg.projects_dir = parser.get("paths", "projects_dir", fallback=cfg.projects_dir)
|
||||
|
||||
if parser.has_section("defaults"):
|
||||
cfg.container_registry = parser.get("defaults", "container_registry", fallback=cfg.container_registry)
|
||||
cfg.container_tag = parser.get("defaults", "container_tag", fallback=cfg.container_tag)
|
||||
cfg.tmux_session = parser.get("defaults", "tmux_session", fallback=cfg.tmux_session)
|
||||
|
||||
if parser.has_section("targets"):
|
||||
for key in parser.options("targets"):
|
||||
raw_value = parser.get("targets", key)
|
||||
tc = _parse_target_config(key, raw_value)
|
||||
if tc is not None:
|
||||
cfg.targets.append(tc)
|
||||
|
||||
return cfg
|
||||
|
||||
|
||||
def load_manifest(path: Optional[Path] = None) -> Dict[str, Any]:
|
||||
"""Load YAML manifest file with cascading priority.
|
||||
|
||||
Priority:
|
||||
1. Dotfiles repo (self-hosted): ~/.local/share/devflow/dotfiles/flow/.config/flow/manifest.yaml
|
||||
2. Local override: ~/.config/devflow/manifest.yaml
|
||||
3. Empty fallback
|
||||
"""
|
||||
if path is None:
|
||||
# Priority 1: Check dotfiles repo for self-hosted manifest
|
||||
if paths.DOTFILES_MANIFEST.exists():
|
||||
path = paths.DOTFILES_MANIFEST
|
||||
# Priority 2: Fall back to local manifest
|
||||
else:
|
||||
path = paths.MANIFEST_FILE
|
||||
|
||||
assert path is not None
|
||||
|
||||
if not path.exists():
|
||||
return {}
|
||||
|
||||
try:
|
||||
with open(path, "r") as f:
|
||||
data = yaml.safe_load(f)
|
||||
except yaml.YAMLError as e:
|
||||
raise RuntimeError(f"Invalid YAML in {path}: {e}") from e
|
||||
return data if isinstance(data, dict) else {}
|
||||
|
||||
|
||||
@dataclass
|
||||
class FlowContext:
|
||||
config: AppConfig
|
||||
manifest: Dict[str, Any]
|
||||
platform: PlatformInfo
|
||||
console: ConsoleLogger
|
||||
Reference in New Issue
Block a user