mirror of
https://github.com/basnijholt/compose-farm.git
synced 2026-03-01 16:52:55 +00:00
* config: Add local_host and web_stack options Allow configuring local_host and web_stack in compose-farm.yaml instead of requiring environment variables. This makes it easier to deploy the web UI with just a config file mount. - local_host: specifies which host is "local" for Glances connectivity - web_stack: identifies the web UI stack for self-update detection Environment variables (CF_LOCAL_HOST, CF_WEB_STACK) still work as fallback for backwards compatibility. Closes #152 * docs: Clarify glances_stack is used by CLI and web UI * config: Env vars override config, add docs - Change precedence: environment variables now override config values (follows 12-factor app pattern) - Document all CF_* environment variables in configuration.md - Update example-config.yaml to mention env var overrides * config: Consolidate env vars, prefer config options - Update docker-compose.yml to comment out CF_WEB_STACK and CF_LOCAL_HOST (now prefer setting in compose-farm.yaml) - Update init-env to comment out CF_LOCAL_HOST (can be set in config) - Update docker-deployment.md with new "Config option" column - Simplify troubleshooting to prefer config over env vars * config: Generate CF_LOCAL_HOST with config alternative note Instead of commenting out CF_LOCAL_HOST, generate it normally but add a note in the comment that it can also be set as 'local_host' in config. * config: Extend local_host to all web UI operations When running the web UI in a Docker container, is_local() can't detect which host the container is on due to different network namespaces. Previously local_host/CF_LOCAL_HOST only affected Glances connectivity. Now it also affects: - Container exec/shell (runs locally instead of via SSH) - File editing (uses local filesystem instead of SSH) Added is_local_host() helper that checks CF_LOCAL_HOST/config.local_host first, then falls back to is_local() detection. * refactor: DRY get_web_stack helper, add tests - Move get_web_stack to deps.py to avoid duplication in streaming.py and actions.py - Add tests for config.local_host and config.web_stack parsing - Add tests for is_local_host, get_web_stack, and get_local_host helpers - Tests verify env var precedence over config values * glances: rely on CF_WEB_STACK for container mode Restore docker-compose env defaults and document local_host scope. * web: ignore local_host outside container Document container-only behavior and adjust tests. * web: infer local host from web_stack Drop local_host config option and update docs/tests. * Remove CF_LOCAL_HOST override * refactor: move web_stack helpers to Config class - Add get_web_stack() and get_local_host_from_web_stack() as Config methods - Remove duplicate _get_local_host_from_web_stack() from glances.py and deps.py - Update deps.py get_web_stack() to delegate to Config method - Add comprehensive tests for the new Config methods * config: remove web_stack config option The web_stack config option was redundant since: - In Docker, CF_WEB_STACK env var is always set - Outside Docker, the container-specific behavior is disabled anyway Simplify by only using the CF_WEB_STACK environment variable. * refactor: remove get_web_stack wrapper from deps Callers now use config.get_web_stack() directly instead of going through a pointless wrapper function. * prompts: add rule to identify pointless wrapper functions
217 lines
8.0 KiB
Python
217 lines
8.0 KiB
Python
"""Tests for config module."""
|
|
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
import yaml
|
|
|
|
from compose_farm.config import Config, Host, load_config
|
|
|
|
|
|
class TestHost:
|
|
"""Tests for Host model."""
|
|
|
|
def test_host_with_all_fields(self) -> None:
|
|
host = Host(address="192.168.1.10", user="docker", port=2222)
|
|
assert host.address == "192.168.1.10"
|
|
assert host.user == "docker"
|
|
assert host.port == 2222
|
|
|
|
def test_host_defaults(self) -> None:
|
|
host = Host(address="192.168.1.10")
|
|
assert host.address == "192.168.1.10"
|
|
assert host.port == 22
|
|
# user defaults to current user, just check it's set
|
|
assert host.user
|
|
|
|
def test_local_host(self) -> None:
|
|
host = Host(address="local")
|
|
assert host.address == "local"
|
|
|
|
|
|
class TestConfig:
|
|
"""Tests for Config model."""
|
|
|
|
def test_config_validation(self) -> None:
|
|
config = Config(
|
|
compose_dir=Path("/opt/compose"),
|
|
hosts={"nas01": Host(address="192.168.1.10")},
|
|
stacks={"plex": "nas01"},
|
|
)
|
|
assert config.compose_dir == Path("/opt/compose")
|
|
assert "nas01" in config.hosts
|
|
assert config.stacks["plex"] == "nas01"
|
|
|
|
def test_config_invalid_stack_host(self) -> None:
|
|
with pytest.raises(ValueError, match="unknown host"):
|
|
Config(
|
|
compose_dir=Path("/opt/compose"),
|
|
hosts={"nas01": Host(address="192.168.1.10")},
|
|
stacks={"plex": "nonexistent"},
|
|
)
|
|
|
|
def test_get_host(self) -> None:
|
|
config = Config(
|
|
compose_dir=Path("/opt/compose"),
|
|
hosts={"nas01": Host(address="192.168.1.10")},
|
|
stacks={"plex": "nas01"},
|
|
)
|
|
host = config.get_host("plex")
|
|
assert host.address == "192.168.1.10"
|
|
|
|
def test_get_host_unknown_stack(self) -> None:
|
|
config = Config(
|
|
compose_dir=Path("/opt/compose"),
|
|
hosts={"nas01": Host(address="192.168.1.10")},
|
|
stacks={"plex": "nas01"},
|
|
)
|
|
with pytest.raises(ValueError, match="Unknown stack"):
|
|
config.get_host("unknown")
|
|
|
|
def test_get_compose_path(self) -> None:
|
|
config = Config(
|
|
compose_dir=Path("/opt/compose"),
|
|
hosts={"nas01": Host(address="192.168.1.10")},
|
|
stacks={"plex": "nas01"},
|
|
)
|
|
path = config.get_compose_path("plex")
|
|
# Defaults to compose.yaml when no file exists
|
|
assert path == Path("/opt/compose/plex/compose.yaml")
|
|
|
|
def test_get_web_stack_returns_env_var(self, monkeypatch: pytest.MonkeyPatch) -> None:
|
|
"""get_web_stack returns CF_WEB_STACK env var."""
|
|
monkeypatch.setenv("CF_WEB_STACK", "compose-farm")
|
|
config = Config(
|
|
compose_dir=Path("/opt/compose"),
|
|
hosts={"nas": Host(address="192.168.1.6")},
|
|
stacks={"compose-farm": "nas"},
|
|
)
|
|
assert config.get_web_stack() == "compose-farm"
|
|
|
|
def test_get_web_stack_returns_empty_when_not_set(
|
|
self, monkeypatch: pytest.MonkeyPatch
|
|
) -> None:
|
|
"""get_web_stack returns empty string when env var not set."""
|
|
monkeypatch.delenv("CF_WEB_STACK", raising=False)
|
|
config = Config(
|
|
compose_dir=Path("/opt/compose"),
|
|
hosts={"nas": Host(address="192.168.1.6")},
|
|
stacks={"compose-farm": "nas"},
|
|
)
|
|
assert config.get_web_stack() == ""
|
|
|
|
def test_get_local_host_from_web_stack_returns_host(
|
|
self, monkeypatch: pytest.MonkeyPatch
|
|
) -> None:
|
|
"""get_local_host_from_web_stack returns the web stack host in container."""
|
|
monkeypatch.setenv("CF_WEB_STACK", "compose-farm")
|
|
config = Config(
|
|
compose_dir=Path("/opt/compose"),
|
|
hosts={"nas": Host(address="192.168.1.6"), "nuc": Host(address="192.168.1.2")},
|
|
stacks={"compose-farm": "nas"},
|
|
)
|
|
assert config.get_local_host_from_web_stack() == "nas"
|
|
|
|
def test_get_local_host_from_web_stack_returns_none_outside_container(
|
|
self, monkeypatch: pytest.MonkeyPatch
|
|
) -> None:
|
|
"""get_local_host_from_web_stack returns None when not in container."""
|
|
monkeypatch.delenv("CF_WEB_STACK", raising=False)
|
|
config = Config(
|
|
compose_dir=Path("/opt/compose"),
|
|
hosts={"nas": Host(address="192.168.1.6")},
|
|
stacks={"compose-farm": "nas"},
|
|
)
|
|
assert config.get_local_host_from_web_stack() is None
|
|
|
|
def test_get_local_host_from_web_stack_returns_none_for_unknown_stack(
|
|
self, monkeypatch: pytest.MonkeyPatch
|
|
) -> None:
|
|
"""get_local_host_from_web_stack returns None if web stack not in stacks."""
|
|
monkeypatch.setenv("CF_WEB_STACK", "unknown-stack")
|
|
config = Config(
|
|
compose_dir=Path("/opt/compose"),
|
|
hosts={"nas": Host(address="192.168.1.6")},
|
|
stacks={"plex": "nas"},
|
|
)
|
|
assert config.get_local_host_from_web_stack() is None
|
|
|
|
def test_get_local_host_from_web_stack_returns_none_for_multi_host(
|
|
self, monkeypatch: pytest.MonkeyPatch
|
|
) -> None:
|
|
"""get_local_host_from_web_stack returns None if web stack runs on multiple hosts."""
|
|
monkeypatch.setenv("CF_WEB_STACK", "compose-farm")
|
|
config = Config(
|
|
compose_dir=Path("/opt/compose"),
|
|
hosts={"nas": Host(address="192.168.1.6"), "nuc": Host(address="192.168.1.2")},
|
|
stacks={"compose-farm": ["nas", "nuc"]},
|
|
)
|
|
assert config.get_local_host_from_web_stack() is None
|
|
|
|
|
|
class TestLoadConfig:
|
|
"""Tests for load_config function."""
|
|
|
|
def test_load_config_full_host_format(self, tmp_path: Path) -> None:
|
|
config_data = {
|
|
"compose_dir": "/opt/compose",
|
|
"hosts": {
|
|
"nas01": {"address": "192.168.1.10", "user": "docker", "port": 2222},
|
|
},
|
|
"stacks": {"plex": "nas01"},
|
|
}
|
|
config_file = tmp_path / "sdc.yaml"
|
|
config_file.write_text(yaml.dump(config_data))
|
|
|
|
config = load_config(config_file)
|
|
assert config.hosts["nas01"].address == "192.168.1.10"
|
|
assert config.hosts["nas01"].user == "docker"
|
|
assert config.hosts["nas01"].port == 2222
|
|
|
|
def test_load_config_simple_host_format(self, tmp_path: Path) -> None:
|
|
config_data = {
|
|
"compose_dir": "/opt/compose",
|
|
"hosts": {"nas01": "192.168.1.10"},
|
|
"stacks": {"plex": "nas01"},
|
|
}
|
|
config_file = tmp_path / "sdc.yaml"
|
|
config_file.write_text(yaml.dump(config_data))
|
|
|
|
config = load_config(config_file)
|
|
assert config.hosts["nas01"].address == "192.168.1.10"
|
|
|
|
def test_load_config_mixed_host_formats(self, tmp_path: Path) -> None:
|
|
config_data = {
|
|
"compose_dir": "/opt/compose",
|
|
"hosts": {
|
|
"nas01": {"address": "192.168.1.10", "user": "docker"},
|
|
"nas02": "192.168.1.11",
|
|
},
|
|
"stacks": {"plex": "nas01", "jellyfin": "nas02"},
|
|
}
|
|
config_file = tmp_path / "sdc.yaml"
|
|
config_file.write_text(yaml.dump(config_data))
|
|
|
|
config = load_config(config_file)
|
|
assert config.hosts["nas01"].user == "docker"
|
|
assert config.hosts["nas02"].address == "192.168.1.11"
|
|
|
|
def test_load_config_not_found(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
|
monkeypatch.chdir(tmp_path)
|
|
monkeypatch.delenv("CF_CONFIG", raising=False)
|
|
monkeypatch.setenv("XDG_CONFIG_HOME", str(tmp_path / "empty_config"))
|
|
with pytest.raises(FileNotFoundError, match="Config file not found"):
|
|
load_config()
|
|
|
|
def test_load_config_local_host(self, tmp_path: Path) -> None:
|
|
config_data = {
|
|
"compose_dir": "/opt/compose",
|
|
"hosts": {"local": "localhost"},
|
|
"stacks": {"test": "local"},
|
|
}
|
|
config_file = tmp_path / "sdc.yaml"
|
|
config_file.write_text(yaml.dump(config_data))
|
|
|
|
config = load_config(config_file)
|
|
assert config.hosts["local"].address == "localhost"
|