diff --git a/docs/docs/configuration/advanced.md b/docs/docs/configuration/advanced.md index 17eb2053d..c04cec97c 100644 --- a/docs/docs/configuration/advanced.md +++ b/docs/docs/configuration/advanced.md @@ -44,13 +44,21 @@ go2rtc: ### `environment_vars` -This section can be used to set environment variables for those unable to modify the environment of the container, like within Home Assistant OS. +This section can be used to set environment variables for those unable to modify the environment of the container, like within Home Assistant OS. Docker users should set environment variables in their `docker run` command (`-e FRIGATE_MQTT_PASSWORD=secret`) or `docker-compose.yml` file (`environment:` section) instead. Note that values set here are stored in plain text in your config file, so if the goal is to keep credentials out of your configuration, use Docker environment variables or Docker secrets instead. + +Variables prefixed with `FRIGATE_` can be referenced in config fields that support environment variable substitution (such as MQTT host and credentials, camera stream URLs, and ONVIF host and credentials) using the `{FRIGATE_VARIABLE_NAME}` syntax. Example: ```yaml environment_vars: - VARIABLE_NAME: variable_value + FRIGATE_MQTT_USER: my_mqtt_user + FRIGATE_MQTT_PASSWORD: my_mqtt_password + +mqtt: + host: "{FRIGATE_MQTT_HOST}" + user: "{FRIGATE_MQTT_USER}" + password: "{FRIGATE_MQTT_PASSWORD}" ``` #### TensorFlow Thread Configuration diff --git a/docs/docs/configuration/index.md b/docs/docs/configuration/index.md index c1b0dc903..efa59246f 100644 --- a/docs/docs/configuration/index.md +++ b/docs/docs/configuration/index.md @@ -50,6 +50,7 @@ Frigate supports the use of environment variables starting with `FRIGATE_` **onl ```yaml mqtt: + host: "{FRIGATE_MQTT_HOST}" user: "{FRIGATE_MQTT_USER}" password: "{FRIGATE_MQTT_PASSWORD}" ``` @@ -60,7 +61,7 @@ mqtt: ```yaml onvif: - host: 10.0.10.10 + host: "{FRIGATE_ONVIF_HOST}" port: 8000 user: "{FRIGATE_RTSP_USER}" password: "{FRIGATE_RTSP_PASSWORD}" diff --git a/docs/docs/configuration/reference.md b/docs/docs/configuration/reference.md index 206d7012e..19a705192 100644 --- a/docs/docs/configuration/reference.md +++ b/docs/docs/configuration/reference.md @@ -16,6 +16,8 @@ mqtt: # Optional: Enable mqtt server (default: shown below) enabled: True # Required: host name + # NOTE: MQTT host can be specified with an environment variable or docker secrets that must begin with 'FRIGATE_'. + # e.g. host: '{FRIGATE_MQTT_HOST}' host: mqtt.server.com # Optional: port (default: shown below) port: 1883 @@ -906,6 +908,8 @@ cameras: onvif: # Required: host of the camera being connected to. # NOTE: HTTP is assumed by default; HTTPS is supported if you specify the scheme, ex: "https://0.0.0.0". + # NOTE: ONVIF host, user, and password can be specified with environment variables or docker secrets + # that must begin with 'FRIGATE_'. e.g. host: '{FRIGATE_ONVIF_HOST}' host: 0.0.0.0 # Optional: ONVIF port for device (default: shown below). port: 8000 diff --git a/frigate/config/camera/onvif.py b/frigate/config/camera/onvif.py index d4955799b..fd35fa537 100644 --- a/frigate/config/camera/onvif.py +++ b/frigate/config/camera/onvif.py @@ -72,7 +72,7 @@ class PtzAutotrackConfig(FrigateBaseModel): class OnvifConfig(FrigateBaseModel): - host: str = Field(default="", title="Onvif Host") + host: EnvString = Field(default="", title="Onvif Host") port: int = Field(default=8000, title="Onvif Port") user: Optional[EnvString] = Field(default=None, title="Onvif Username") password: Optional[EnvString] = Field(default=None, title="Onvif Password") diff --git a/frigate/config/env.py b/frigate/config/env.py index 6534ff411..db094a8af 100644 --- a/frigate/config/env.py +++ b/frigate/config/env.py @@ -24,8 +24,10 @@ EnvString = Annotated[str, AfterValidator(validate_env_string)] def validate_env_vars(v: dict[str, str], info: ValidationInfo) -> dict[str, str]: if isinstance(info.context, dict) and info.context.get("install", False): - for k, v in v.items(): - os.environ[k] = v + for k, val in v.items(): + os.environ[k] = val + if k.startswith("FRIGATE_"): + FRIGATE_ENV_VARS[k] = val return v diff --git a/frigate/config/mqtt.py b/frigate/config/mqtt.py index a760d0a1f..3e2f99294 100644 --- a/frigate/config/mqtt.py +++ b/frigate/config/mqtt.py @@ -13,7 +13,7 @@ __all__ = ["MqttConfig"] class MqttConfig(FrigateBaseModel): enabled: bool = Field(default=True, title="Enable MQTT Communication.") - host: str = Field(default="", title="MQTT Host") + host: EnvString = Field(default="", title="MQTT Host") port: int = Field(default=1883, title="MQTT Port") topic_prefix: str = Field(default="frigate", title="MQTT Topic Prefix") client_id: str = Field(default="frigate", title="MQTT Client ID") diff --git a/frigate/test/test_env.py b/frigate/test/test_env.py new file mode 100644 index 000000000..fe2ce8d3e --- /dev/null +++ b/frigate/test/test_env.py @@ -0,0 +1,105 @@ +"""Tests for environment variable handling.""" + +import os +import unittest + +from frigate.config.env import ( + FRIGATE_ENV_VARS, + validate_env_string, + validate_env_vars, +) + + +class TestEnvString(unittest.TestCase): + def setUp(self): + self._original_env_vars = dict(FRIGATE_ENV_VARS) + + def tearDown(self): + FRIGATE_ENV_VARS.clear() + FRIGATE_ENV_VARS.update(self._original_env_vars) + + def test_substitution(self): + """EnvString substitutes FRIGATE_ env vars.""" + FRIGATE_ENV_VARS["FRIGATE_TEST_HOST"] = "192.168.1.100" + result = validate_env_string("{FRIGATE_TEST_HOST}") + self.assertEqual(result, "192.168.1.100") + + def test_substitution_in_url(self): + """EnvString substitutes vars embedded in a URL.""" + FRIGATE_ENV_VARS["FRIGATE_CAM_USER"] = "admin" + FRIGATE_ENV_VARS["FRIGATE_CAM_PASS"] = "secret" + result = validate_env_string( + "rtsp://{FRIGATE_CAM_USER}:{FRIGATE_CAM_PASS}@10.0.0.1/stream" + ) + self.assertEqual(result, "rtsp://admin:secret@10.0.0.1/stream") + + def test_no_placeholder(self): + """Plain strings pass through unchanged.""" + result = validate_env_string("192.168.1.1") + self.assertEqual(result, "192.168.1.1") + + def test_unknown_var_raises(self): + """Referencing an unknown var raises KeyError.""" + with self.assertRaises(KeyError): + validate_env_string("{FRIGATE_NONEXISTENT_VAR}") + + +class TestEnvVars(unittest.TestCase): + def setUp(self): + self._original_env_vars = dict(FRIGATE_ENV_VARS) + self._original_environ = os.environ.copy() + + def tearDown(self): + FRIGATE_ENV_VARS.clear() + FRIGATE_ENV_VARS.update(self._original_env_vars) + # Clean up any env vars we set + for key in list(os.environ.keys()): + if key not in self._original_environ: + del os.environ[key] + + def _make_context(self, install: bool): + """Create a mock ValidationInfo with the given install flag.""" + + class MockContext: + def __init__(self, ctx): + self.context = ctx + + mock = MockContext({"install": install}) + return mock + + def test_install_sets_os_environ(self): + """validate_env_vars with install=True sets os.environ.""" + ctx = self._make_context(install=True) + validate_env_vars({"MY_CUSTOM_VAR": "value123"}, ctx) + self.assertEqual(os.environ.get("MY_CUSTOM_VAR"), "value123") + + def test_install_updates_frigate_env_vars(self): + """validate_env_vars with install=True updates FRIGATE_ENV_VARS for FRIGATE_ keys.""" + ctx = self._make_context(install=True) + validate_env_vars({"FRIGATE_MQTT_PASS": "secret"}, ctx) + self.assertEqual(FRIGATE_ENV_VARS["FRIGATE_MQTT_PASS"], "secret") + + def test_install_skips_non_frigate_in_env_vars_dict(self): + """Non-FRIGATE_ keys are set in os.environ but not in FRIGATE_ENV_VARS.""" + ctx = self._make_context(install=True) + validate_env_vars({"OTHER_VAR": "value"}, ctx) + self.assertEqual(os.environ.get("OTHER_VAR"), "value") + self.assertNotIn("OTHER_VAR", FRIGATE_ENV_VARS) + + def test_no_install_does_not_set(self): + """validate_env_vars without install=True does not modify state.""" + ctx = self._make_context(install=False) + validate_env_vars({"FRIGATE_SKIP": "nope"}, ctx) + self.assertNotIn("FRIGATE_SKIP", FRIGATE_ENV_VARS) + self.assertNotIn("FRIGATE_SKIP", os.environ) + + def test_env_vars_available_for_env_string(self): + """Vars set via validate_env_vars are usable in validate_env_string.""" + ctx = self._make_context(install=True) + validate_env_vars({"FRIGATE_BROKER": "mqtt.local"}, ctx) + result = validate_env_string("{FRIGATE_BROKER}") + self.assertEqual(result, "mqtt.local") + + +if __name__ == "__main__": + unittest.main()