Fix docker_config check for add-ons (#6119)

* Fix docker_config check to ignore Docker VOLUME mounts

Only validate /media and /share mounts that are explicitly configured
in add-on map_volumes, not those created by Docker VOLUME statements.

* Check and test with custom map targets
This commit is contained in:
Stefan Agner 2025-08-22 10:38:41 +02:00 committed by GitHub
parent 9740de7a83
commit 1fb15772d7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 219 additions and 5 deletions

View File

@ -1,20 +1,53 @@
"""Helper to check if docker config for container needs an update."""
from ...addons.const import MappingType
from ...const import CoreState
from ...coresys import CoreSys
from ...docker.const import PropagationMode
from ...docker.const import PATH_MEDIA, PATH_SHARE, PropagationMode
from ...docker.interface import DockerInterface
from ..const import ContextType, IssueType, SuggestionType
from ..data import Issue
from .base import CheckBase
def _check_container(container: DockerInterface) -> bool:
"""Return true if container has a config issue."""
def _check_container(container: DockerInterface, addon=None) -> bool:
"""Check if container has mount propagation issues requiring recreate.
For add-ons, only validates mounts explicitly configured (not Docker VOLUMEs).
For Core/plugins, validates all /media and /share mounts.
"""
# For add-ons, check mounts against their actual configured targets
if addon is not None:
addon_mapping = addon.map_volumes
configured_targets = set()
# Get actual target paths from add-on configuration
if MappingType.MEDIA in addon_mapping:
target = addon_mapping[MappingType.MEDIA].path or PATH_MEDIA.as_posix()
configured_targets.add(target)
if MappingType.SHARE in addon_mapping:
target = addon_mapping[MappingType.SHARE].path or PATH_SHARE.as_posix()
configured_targets.add(target)
if not configured_targets:
return False
# Check if any configured targets have propagation issues
for mount in container.meta_mounts:
if (
mount.get("Destination") in configured_targets
and mount.get("Propagation") != PropagationMode.RSLAVE
):
return True
return False
# For Home Assistant Core and plugins, check default /media and /share paths
return any(
mount.get("Propagation") != PropagationMode.RSLAVE
for mount in container.meta_mounts
if mount.get("Destination") in ["/media", "/share"]
if mount.get("Destination") in [PATH_MEDIA.as_posix(), PATH_SHARE.as_posix()]
)
@ -50,7 +83,7 @@ class CheckDockerConfig(CheckBase):
new_issues.add(Issue(IssueType.DOCKER_CONFIG, ContextType.CORE))
for addon in self.sys_addons.installed:
if _check_container(addon.instance):
if _check_container(addon.instance, addon):
new_issues.add(
Issue(
IssueType.DOCKER_CONFIG, ContextType.ADDON, reference=addon.slug

View File

@ -37,6 +37,33 @@ def _make_mock_container_get(bad_config_names: list[str], folder: str = "media")
return mock_container_get
def _make_mock_container_get_with_volume_mount(
bad_config_names: list[str], folder: str = "media"
):
"""Make mock of container get with VOLUME mount (not managed by supervisor)."""
# This simulates a Docker VOLUME mount with wrong propagation
# but NOT created by supervisor configuration
mount = {
"Type": "bind",
"Source": f"/var/lib/docker/volumes/something_{folder}/_data", # Docker volume source
"Destination": f"/{folder}",
"Mode": "rw",
"RW": True,
"Propagation": "rprivate", # Wrong propagation, but not our mount
}
def mock_container_get(name):
out = MagicMock()
out.status = "running"
out.attrs = {"State": {}, "Mounts": []}
if name in bad_config_names:
out.attrs["Mounts"].append(mount)
return out
return mock_container_get
async def test_base(coresys: CoreSys):
"""Test check basics."""
docker_config = CheckDockerConfig(coresys)
@ -119,6 +146,160 @@ async def test_check(
)
@pytest.mark.parametrize("folder", ["media", "share"])
async def test_addon_volume_mount_not_flagged(
docker: DockerAPI, coresys: CoreSys, install_addon_ssh: Addon, folder: str
):
"""Test that add-on with VOLUME mount to media/share but not in config is not flagged."""
# Create an add-on that doesn't have media/share in its mapping configuration
# Remove the mapping from the addon configuration
install_addon_ssh.data["map"] = [
{"type": "config", "read_only": False},
{"type": "ssl", "read_only": True},
] # No media/share
# Mock container that has VOLUME mount to media/share with wrong propagation
docker.containers.get = _make_mock_container_get_with_volume_mount(
["addon_local_ssh"], folder
)
await coresys.core.set_state(CoreState.SETUP)
with patch.object(DockerInterface, "is_running", return_value=True):
await coresys.plugins.load()
await coresys.homeassistant.load()
await coresys.addons.load()
docker_config = CheckDockerConfig(coresys)
assert not coresys.resolution.issues
assert not coresys.resolution.suggestions
# Run check - should NOT create issue for add-on since mount wasn't requested
await docker_config.run_check()
# Should not create addon issue for VOLUME mounts not in config
addon_issues = [
issue
for issue in coresys.resolution.issues
if issue.context == ContextType.ADDON and issue.reference == "local_ssh"
]
assert len(addon_issues) == 0, (
"Add-on should not be flagged for VOLUME mounts not in config"
)
# No system issue should be created either if no containers have issues
system_issues = [
issue
for issue in coresys.resolution.issues
if issue.context == ContextType.SYSTEM
]
assert len(system_issues) == 0
@pytest.mark.parametrize("folder", ["media", "share"])
async def test_addon_configured_mount_still_flagged(
docker: DockerAPI, coresys: CoreSys, install_addon_ssh: Addon, folder: str
):
"""Test that add-on with configured media/share mount is still flagged when propagation wrong."""
# Keep the original configuration which includes media/share
# SSH addon config already has media:rw and share:rw
# Mock container that has supervisor-managed mount with wrong propagation
mount = {
"Type": "bind",
"Source": f"/mnt/data/supervisor/{folder}", # Supervisor-managed source
"Destination": f"/{folder}",
"Mode": "rw",
"RW": True,
"Propagation": "rprivate", # Wrong propagation
}
def mock_container_get(name):
out = MagicMock()
out.status = "running"
out.attrs = {"State": {}, "Mounts": []}
if name == "addon_local_ssh":
out.attrs["Mounts"].append(mount)
return out
docker.containers.get = mock_container_get
await coresys.core.set_state(CoreState.SETUP)
with patch.object(DockerInterface, "is_running", return_value=True):
await coresys.plugins.load()
await coresys.homeassistant.load()
await coresys.addons.load()
docker_config = CheckDockerConfig(coresys)
assert not coresys.resolution.issues
# Run check - should create issue for add-on since mount was requested in config
await docker_config.run_check()
# Should have addon issue since the mount was configured
addon_issues = [
issue
for issue in coresys.resolution.issues
if issue.context == ContextType.ADDON and issue.reference == "local_ssh"
]
assert len(addon_issues) == 1, (
"Add-on should be flagged for configured mounts with wrong propagation"
)
@pytest.mark.parametrize("folder", ["media", "share"])
async def test_addon_custom_target_path_flagged(
docker: DockerAPI, coresys: CoreSys, install_addon_ssh: Addon, folder: str
):
"""Test that add-on with custom target path for media/share is properly checked."""
# Configure add-on with custom target path
custom_path = f"/custom/{folder}"
mapping_type = "media" if folder == "media" else "share"
install_addon_ssh.data["map"] = [
{"type": mapping_type, "read_only": False, "path": custom_path},
]
def mock_container_get(name: str) -> MagicMock:
"""Mock container get with custom target path mount."""
out = MagicMock()
out.status = "running"
out.attrs = {"State": {}, "Mounts": []}
# Add mount with custom target path and wrong propagation
mount = {
"Source": f"/mnt/data/supervisor/{folder}",
"Destination": custom_path, # Custom target path
"Propagation": "rprivate", # Wrong propagation
}
if name == "addon_local_ssh":
out.attrs["Mounts"].append(mount)
return out
docker.containers.get = mock_container_get
await coresys.core.set_state(CoreState.SETUP)
with patch.object(DockerInterface, "is_running", return_value=True):
await coresys.plugins.load()
await coresys.homeassistant.load()
await coresys.addons.load()
docker_config = CheckDockerConfig(coresys)
assert not coresys.resolution.issues
# Run check - should create issue for add-on with custom target path
await docker_config.run_check()
# Should have addon issue since the mount with custom path was configured
addon_issues = [
issue
for issue in coresys.resolution.issues
if issue.context == ContextType.ADDON and issue.reference == "local_ssh"
]
assert len(addon_issues) == 1, (
"Add-on should be flagged for configured mounts with custom paths and wrong propagation"
)
async def test_did_run(coresys: CoreSys):
"""Test that the check ran as expected."""
docker_config = CheckDockerConfig(coresys)