mirror of
https://github.com/home-assistant/supervisor.git
synced 2025-11-15 14:00:27 +00:00
* 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
326 lines
11 KiB
Python
326 lines
11 KiB
Python
"""Test check Docker Config."""
|
|
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
import pytest
|
|
|
|
from supervisor.addons.addon import Addon
|
|
from supervisor.const import CoreState
|
|
from supervisor.coresys import CoreSys
|
|
from supervisor.docker.interface import DockerInterface
|
|
from supervisor.docker.manager import DockerAPI
|
|
from supervisor.resolution.checks.docker_config import CheckDockerConfig
|
|
from supervisor.resolution.const import ContextType, IssueType, SuggestionType
|
|
from supervisor.resolution.data import Issue, Suggestion
|
|
|
|
|
|
def _make_mock_container_get(bad_config_names: list[str], folder: str = "media"):
|
|
"""Make mock of container get."""
|
|
mount = {
|
|
"Type": "bind",
|
|
"Source": f"/mnt/data/supervisor/{folder}",
|
|
"Destination": f"/{folder}",
|
|
"Mode": "rw",
|
|
"RW": True,
|
|
"Propagation": "rprivate",
|
|
}
|
|
|
|
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
|
|
|
|
|
|
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)
|
|
assert docker_config.slug == "docker_config"
|
|
assert docker_config.enabled
|
|
|
|
|
|
@pytest.mark.parametrize("folder", ["media", "share"])
|
|
async def test_check(
|
|
docker: DockerAPI, coresys: CoreSys, install_addon_ssh: Addon, folder: str
|
|
):
|
|
"""Test check reports issue when containers have incorrect config."""
|
|
docker.containers.get = _make_mock_container_get(
|
|
["homeassistant", "hassio_audio", "addon_local_ssh"], folder
|
|
)
|
|
# Use state used in setup()
|
|
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
|
|
|
|
# An issue and suggestion is added per container with a config issue
|
|
await docker_config.run_check()
|
|
|
|
assert len(coresys.resolution.issues) == 4
|
|
assert Issue(IssueType.DOCKER_CONFIG, ContextType.CORE) in coresys.resolution.issues
|
|
assert (
|
|
Issue(IssueType.DOCKER_CONFIG, ContextType.ADDON, reference="local_ssh")
|
|
in coresys.resolution.issues
|
|
)
|
|
assert (
|
|
Issue(IssueType.DOCKER_CONFIG, ContextType.PLUGIN, reference="audio")
|
|
in coresys.resolution.issues
|
|
)
|
|
assert (
|
|
Issue(IssueType.DOCKER_CONFIG, ContextType.SYSTEM) in coresys.resolution.issues
|
|
)
|
|
|
|
assert len(coresys.resolution.suggestions) == 4
|
|
assert (
|
|
Suggestion(SuggestionType.EXECUTE_REBUILD, ContextType.CORE)
|
|
in coresys.resolution.suggestions
|
|
)
|
|
assert (
|
|
Suggestion(
|
|
SuggestionType.EXECUTE_REBUILD, ContextType.PLUGIN, reference="audio"
|
|
)
|
|
in coresys.resolution.suggestions
|
|
)
|
|
assert (
|
|
Suggestion(
|
|
SuggestionType.EXECUTE_REBUILD, ContextType.ADDON, reference="local_ssh"
|
|
)
|
|
in coresys.resolution.suggestions
|
|
)
|
|
assert (
|
|
Suggestion(SuggestionType.EXECUTE_REBUILD, ContextType.SYSTEM)
|
|
in coresys.resolution.suggestions
|
|
)
|
|
|
|
assert await docker_config.approve_check()
|
|
|
|
# IF config issue is resolved, all issues are removed except the main one. Which will be removed if check isn't approved
|
|
docker.containers.get = _make_mock_container_get([])
|
|
with patch.object(DockerInterface, "is_running", return_value=True):
|
|
await coresys.plugins.load()
|
|
await coresys.homeassistant.load()
|
|
await coresys.addons.load()
|
|
|
|
assert not await docker_config.approve_check()
|
|
assert len(coresys.resolution.issues) == 1
|
|
assert len(coresys.resolution.suggestions) == 1
|
|
assert (
|
|
Issue(IssueType.DOCKER_CONFIG, ContextType.SYSTEM) in coresys.resolution.issues
|
|
)
|
|
|
|
|
|
@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)
|
|
should_run = docker_config.states
|
|
should_not_run = [state for state in CoreState if state not in should_run]
|
|
assert len(should_run) != 0
|
|
assert len(should_not_run) != 0
|
|
|
|
with patch(
|
|
"supervisor.resolution.checks.docker_config.CheckDockerConfig.run_check",
|
|
return_value=None,
|
|
) as check:
|
|
for state in should_run:
|
|
await coresys.core.set_state(state)
|
|
await docker_config()
|
|
check.assert_called_once()
|
|
check.reset_mock()
|
|
|
|
for state in should_not_run:
|
|
await coresys.core.set_state(state)
|
|
await docker_config()
|
|
check.assert_not_called()
|
|
check.reset_mock()
|