fix: pass mqtt credentials through compose
This commit is contained in:
@@ -6,6 +6,9 @@ CONTROLLER_SECRETS_DIR=/srv/video-player-controller/secrets
|
|||||||
|
|
||||||
MQTT_HOST=10.40.0.2
|
MQTT_HOST=10.40.0.2
|
||||||
MQTT_PORT=1883
|
MQTT_PORT=1883
|
||||||
|
MQTT_USERNAME=
|
||||||
|
MQTT_PASSWORD=
|
||||||
|
MQTT_CLIENT_ID=yt-controller
|
||||||
CONTROLLER_WEB_PORT=7531
|
CONTROLLER_WEB_PORT=7531
|
||||||
|
|
||||||
# Match the owner of the host media and data directories.
|
# Match the owner of the host media and data directories.
|
||||||
|
|||||||
+17
-2
@@ -116,6 +116,14 @@ def _text(value: Any, name: str, *, allow_none: bool = False) -> str | None:
|
|||||||
return value.strip()
|
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:
|
def _integer(value: Any, name: str, *, minimum: int = 1, maximum: int | None = None) -> int:
|
||||||
if isinstance(value, bool):
|
if isinstance(value, bool):
|
||||||
raise ConfigError(f"{name} must be an integer")
|
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
|
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(
|
def load_config(
|
||||||
path: str | Path | None = None,
|
path: str | Path | None = None,
|
||||||
*,
|
*,
|
||||||
@@ -192,8 +207,8 @@ def load_config(
|
|||||||
mqtt = MqttConfig(
|
mqtt = MqttConfig(
|
||||||
host=_text(_env_value(env, "MQTT_HOST", mqtt_raw.get("host", "10.40.0.2")), "mqtt.host"),
|
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),
|
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),
|
username=_optional_text(_optional_env_value(env, "MQTT_USERNAME", mqtt_raw.get("username")), "mqtt.username"),
|
||||||
password=_text(_env_value(env, "MQTT_PASSWORD", mqtt_raw.get("password")), "mqtt.password", allow_none=True),
|
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"),
|
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"),
|
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"),
|
reconnect_min_seconds=_integer(mqtt_raw.get("reconnect_min_seconds", 1), "mqtt.reconnect_min_seconds"),
|
||||||
|
|||||||
@@ -16,6 +16,9 @@ services:
|
|||||||
FLASK_SECRET_KEY: ${FLASK_SECRET_KEY:?Set FLASK_SECRET_KEY in .env}
|
FLASK_SECRET_KEY: ${FLASK_SECRET_KEY:?Set FLASK_SECRET_KEY in .env}
|
||||||
MQTT_HOST: ${MQTT_HOST:-10.40.0.2}
|
MQTT_HOST: ${MQTT_HOST:-10.40.0.2}
|
||||||
MQTT_PORT: ${MQTT_PORT:-1883}
|
MQTT_PORT: ${MQTT_PORT:-1883}
|
||||||
|
MQTT_USERNAME: ${MQTT_USERNAME:-}
|
||||||
|
MQTT_PASSWORD: ${MQTT_PASSWORD:-}
|
||||||
|
MQTT_CLIENT_ID: ${MQTT_CLIENT_ID:-yt-controller}
|
||||||
PORT: 7531
|
PORT: 7531
|
||||||
YTPLAYER_DATA_BASE: /controller-data
|
YTPLAYER_DATA_BASE: /controller-data
|
||||||
YTPLAYER_MEDIA_BASE: /mnt/Media/YTPlayer
|
YTPLAYER_MEDIA_BASE: /mnt/Media/YTPlayer
|
||||||
|
|||||||
@@ -96,11 +96,37 @@ class ConfigurationTests(unittest.TestCase):
|
|||||||
config = load_config(environ=env)
|
config = load_config(environ=env)
|
||||||
self.assertEqual(config.mqtt.host, "broker")
|
self.assertEqual(config.mqtt.host, "broker")
|
||||||
self.assertEqual(config.mqtt.port, 2883)
|
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.media_base, Path("/media-test"))
|
||||||
self.assertEqual(config.storage.data_base, Path("/data-test"))
|
self.assertEqual(config.storage.data_base, Path("/data-test"))
|
||||||
self.assertIsNone(config.downloads.cookie_path)
|
self.assertIsNone(config.downloads.cookie_path)
|
||||||
self.assertEqual(config.web.port, 9000)
|
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):
|
def test_slot_count_cannot_break_stable_identities(self):
|
||||||
with tempfile.TemporaryDirectory() as directory:
|
with tempfile.TemporaryDirectory() as directory:
|
||||||
path = Path(directory) / "config.yaml"
|
path = Path(directory) / "config.yaml"
|
||||||
|
|||||||
@@ -213,6 +213,18 @@ class ComposeRuntimeTests(unittest.TestCase):
|
|||||||
published_targets.add(match.group(1))
|
published_targets.add(match.group(1))
|
||||||
self.assertIn("7531", published_targets)
|
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):
|
def test_compose_uses_dedicated_data_media_and_cookie_mounts(self):
|
||||||
mounts = _volume_mounts(self.service.get("volumes"))
|
mounts = _volume_mounts(self.service.get("volumes"))
|
||||||
by_target = {target: (source, read_only) for source, target, read_only in mounts}
|
by_target = {target: (source, read_only) for source, target, read_only in mounts}
|
||||||
|
|||||||
Reference in New Issue
Block a user