mirror of
https://github.com/home-assistant/supervisor.git
synced 2025-12-10 16:52:15 -06:00
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:
parent
9740de7a83
commit
1fb15772d7
@ -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
|
||||
|
||||
@ -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)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user