fix: pass mqtt credentials through compose

This commit is contained in:
Media
2026-08-06 19:30:22 -05:00
parent 63ea314b18
commit 72cc66df92
5 changed files with 61 additions and 2 deletions
+3
View File
@@ -6,6 +6,9 @@ CONTROLLER_SECRETS_DIR=/srv/video-player-controller/secrets
MQTT_HOST=10.40.0.2
MQTT_PORT=1883
MQTT_USERNAME=
MQTT_PASSWORD=
MQTT_CLIENT_ID=yt-controller
CONTROLLER_WEB_PORT=7531
# Match the owner of the host media and data directories.
+17 -2
View File
@@ -116,6 +116,14 @@ def _text(value: Any, name: str, *, allow_none: bool = False) -> str | None:
return value.strip()
def _optional_text(value: Any, name: str) -> str | None:
"""Normalize unset/blank optional environment values to ``None``."""
if value is None or (isinstance(value, str) and not value.strip()):
return None
return _text(value, name)
def _integer(value: Any, name: str, *, minimum: int = 1, maximum: int | None = None) -> int:
if isinstance(value, bool):
raise ConfigError(f"{name} must be an integer")
@@ -167,6 +175,13 @@ def _env_value(env: Mapping[str, str], key: str, fallback: Any) -> Any:
return env[key] if key in env else fallback
def _optional_env_value(env: Mapping[str, str], key: str, fallback: Any) -> Any:
value = env.get(key)
if value is None or not value.strip():
return fallback
return value
def load_config(
path: str | Path | None = None,
*,
@@ -192,8 +207,8 @@ def load_config(
mqtt = MqttConfig(
host=_text(_env_value(env, "MQTT_HOST", mqtt_raw.get("host", "10.40.0.2")), "mqtt.host"),
port=_integer(_env_value(env, "MQTT_PORT", mqtt_raw.get("port", 1883)), "mqtt.port", maximum=65535),
username=_text(_env_value(env, "MQTT_USERNAME", mqtt_raw.get("username")), "mqtt.username", allow_none=True),
password=_text(_env_value(env, "MQTT_PASSWORD", mqtt_raw.get("password")), "mqtt.password", allow_none=True),
username=_optional_text(_optional_env_value(env, "MQTT_USERNAME", mqtt_raw.get("username")), "mqtt.username"),
password=_optional_text(_optional_env_value(env, "MQTT_PASSWORD", mqtt_raw.get("password")), "mqtt.password"),
client_id=_text(_env_value(env, "MQTT_CLIENT_ID", mqtt_raw.get("client_id", "yt-controller")), "mqtt.client_id"),
keepalive_seconds=_integer(mqtt_raw.get("keepalive_seconds", 30), "mqtt.keepalive_seconds"),
reconnect_min_seconds=_integer(mqtt_raw.get("reconnect_min_seconds", 1), "mqtt.reconnect_min_seconds"),
+3
View File
@@ -16,6 +16,9 @@ services:
FLASK_SECRET_KEY: ${FLASK_SECRET_KEY:?Set FLASK_SECRET_KEY in .env}
MQTT_HOST: ${MQTT_HOST:-10.40.0.2}
MQTT_PORT: ${MQTT_PORT:-1883}
MQTT_USERNAME: ${MQTT_USERNAME:-}
MQTT_PASSWORD: ${MQTT_PASSWORD:-}
MQTT_CLIENT_ID: ${MQTT_CLIENT_ID:-yt-controller}
PORT: 7531
YTPLAYER_DATA_BASE: /controller-data
YTPLAYER_MEDIA_BASE: /mnt/Media/YTPlayer
+26
View File
@@ -96,11 +96,37 @@ class ConfigurationTests(unittest.TestCase):
config = load_config(environ=env)
self.assertEqual(config.mqtt.host, "broker")
self.assertEqual(config.mqtt.port, 2883)
self.assertEqual(config.mqtt.username, "user")
self.assertEqual(config.mqtt.password, "pass")
self.assertEqual(config.storage.media_base, Path("/media-test"))
self.assertEqual(config.storage.data_base, Path("/data-test"))
self.assertIsNone(config.downloads.cookie_path)
self.assertEqual(config.web.port, 9000)
def test_blank_optional_mqtt_credentials_are_treated_as_unset(self):
config = load_config(
environ={
"MQTT_USERNAME": "",
"MQTT_PASSWORD": " ",
}
)
self.assertIsNone(config.mqtt.username)
self.assertIsNone(config.mqtt.password)
def test_blank_compose_credentials_do_not_mask_yaml_credentials(self):
with tempfile.TemporaryDirectory() as directory:
path = Path(directory) / "config.yaml"
path.write_text(
"mqtt:\n username: yaml-user\n password: yaml-pass\n",
encoding="utf-8",
)
config = load_config(
path,
environ={"MQTT_USERNAME": "", "MQTT_PASSWORD": ""},
)
self.assertEqual(config.mqtt.username, "yaml-user")
self.assertEqual(config.mqtt.password, "yaml-pass")
def test_slot_count_cannot_break_stable_identities(self):
with tempfile.TemporaryDirectory() as directory:
path = Path(directory) / "config.yaml"
+12
View File
@@ -213,6 +213,18 @@ class ComposeRuntimeTests(unittest.TestCase):
published_targets.add(match.group(1))
self.assertIn("7531", published_targets)
def test_compose_passes_all_supported_mqtt_environment_overrides(self):
environment = _environment_map(self.service.get("environment"))
for name in (
"MQTT_HOST",
"MQTT_PORT",
"MQTT_USERNAME",
"MQTT_PASSWORD",
"MQTT_CLIENT_ID",
):
with self.subTest(name=name):
self.assertIn(name, environment)
def test_compose_uses_dedicated_data_media_and_cookie_mounts(self):
mounts = _volume_mounts(self.service.get("volumes"))
by_target = {target: (source, read_only) for source, target, read_only in mounts}