mirror of
https://github.com/home-assistant/supervisor.git
synced 2025-07-08 09:46:29 +00:00
Remove anonymous volumes when removing containers (#5977)
* Remove anonymous volumes when removing containers * Add tests for docker.run_command()
This commit is contained in:
parent
779f47e25d
commit
b8852872fe
@ -327,7 +327,7 @@ class DockerAPI:
|
|||||||
# cleanup container
|
# cleanup container
|
||||||
if container:
|
if container:
|
||||||
with suppress(docker_errors.DockerException, requests.RequestException):
|
with suppress(docker_errors.DockerException, requests.RequestException):
|
||||||
container.remove(force=True)
|
container.remove(force=True, v=True)
|
||||||
|
|
||||||
return CommandReturn(result.get("StatusCode"), output)
|
return CommandReturn(result.get("StatusCode"), output)
|
||||||
|
|
||||||
@ -442,7 +442,7 @@ class DockerAPI:
|
|||||||
if remove_container:
|
if remove_container:
|
||||||
with suppress(DockerException, requests.RequestException):
|
with suppress(DockerException, requests.RequestException):
|
||||||
_LOGGER.info("Cleaning %s application", name)
|
_LOGGER.info("Cleaning %s application", name)
|
||||||
docker_container.remove(force=True)
|
docker_container.remove(force=True, v=True)
|
||||||
|
|
||||||
def start_container(self, name: str) -> None:
|
def start_container(self, name: str) -> None:
|
||||||
"""Start Docker container."""
|
"""Start Docker container."""
|
||||||
|
@ -843,7 +843,7 @@ async def test_addon_loads_wrong_image(
|
|||||||
with patch("pathlib.Path.is_file", return_value=True):
|
with patch("pathlib.Path.is_file", return_value=True):
|
||||||
await install_addon_ssh.load()
|
await install_addon_ssh.load()
|
||||||
|
|
||||||
container.remove.assert_called_once_with(force=True)
|
container.remove.assert_called_once_with(force=True, v=True)
|
||||||
assert coresys.docker.images.remove.call_args_list[0].kwargs == {
|
assert coresys.docker.images.remove.call_args_list[0].kwargs == {
|
||||||
"image": "local/aarch64-addon-ssh:latest",
|
"image": "local/aarch64-addon-ssh:latest",
|
||||||
"force": True,
|
"force": True,
|
||||||
|
134
tests/docker/test_manager.py
Normal file
134
tests/docker/test_manager.py
Normal file
@ -0,0 +1,134 @@
|
|||||||
|
"""Test Docker manager."""
|
||||||
|
|
||||||
|
from unittest.mock import MagicMock
|
||||||
|
|
||||||
|
from docker.errors import DockerException
|
||||||
|
import pytest
|
||||||
|
from requests import RequestException
|
||||||
|
|
||||||
|
from supervisor.docker.manager import CommandReturn, DockerAPI
|
||||||
|
from supervisor.exceptions import DockerError
|
||||||
|
|
||||||
|
|
||||||
|
async def test_run_command_success(docker: DockerAPI):
|
||||||
|
"""Test successful command execution."""
|
||||||
|
# Mock container and its methods
|
||||||
|
mock_container = MagicMock()
|
||||||
|
mock_container.wait.return_value = {"StatusCode": 0}
|
||||||
|
mock_container.logs.return_value = b"command output"
|
||||||
|
|
||||||
|
# Mock docker containers.run to return our mock container
|
||||||
|
docker.docker.containers.run.return_value = mock_container
|
||||||
|
|
||||||
|
# Execute the command
|
||||||
|
result = docker.run_command(
|
||||||
|
image="alpine", tag="3.18", command="echo hello", stdout=True, stderr=True
|
||||||
|
)
|
||||||
|
|
||||||
|
# Verify the result
|
||||||
|
assert isinstance(result, CommandReturn)
|
||||||
|
assert result.exit_code == 0
|
||||||
|
assert result.output == b"command output"
|
||||||
|
|
||||||
|
# Verify docker.containers.run was called correctly
|
||||||
|
docker.docker.containers.run.assert_called_once_with(
|
||||||
|
"alpine:3.18",
|
||||||
|
command="echo hello",
|
||||||
|
network=docker.network.name,
|
||||||
|
use_config_proxy=False,
|
||||||
|
stdout=True,
|
||||||
|
stderr=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Verify container cleanup
|
||||||
|
mock_container.remove.assert_called_once_with(force=True, v=True)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_run_command_with_defaults(docker: DockerAPI):
|
||||||
|
"""Test command execution with default parameters."""
|
||||||
|
# Mock container and its methods
|
||||||
|
mock_container = MagicMock()
|
||||||
|
mock_container.wait.return_value = {"StatusCode": 1}
|
||||||
|
mock_container.logs.return_value = b"error output"
|
||||||
|
|
||||||
|
# Mock docker containers.run to return our mock container
|
||||||
|
docker.docker.containers.run.return_value = mock_container
|
||||||
|
|
||||||
|
# Execute the command with minimal parameters
|
||||||
|
result = docker.run_command(image="ubuntu")
|
||||||
|
|
||||||
|
# Verify the result
|
||||||
|
assert isinstance(result, CommandReturn)
|
||||||
|
assert result.exit_code == 1
|
||||||
|
assert result.output == b"error output"
|
||||||
|
|
||||||
|
# Verify docker.containers.run was called with defaults
|
||||||
|
docker.docker.containers.run.assert_called_once_with(
|
||||||
|
"ubuntu:latest", # default tag
|
||||||
|
command=None, # default command
|
||||||
|
network=docker.network.name,
|
||||||
|
use_config_proxy=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Verify container.logs was called with default stdout/stderr
|
||||||
|
mock_container.logs.assert_called_once_with(stdout=True, stderr=True)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_run_command_docker_exception(docker: DockerAPI):
|
||||||
|
"""Test command execution when Docker raises an exception."""
|
||||||
|
# Mock docker containers.run to raise DockerException
|
||||||
|
docker.docker.containers.run.side_effect = DockerException("Docker error")
|
||||||
|
|
||||||
|
# Execute the command and expect DockerError
|
||||||
|
with pytest.raises(DockerError, match="Can't execute command: Docker error"):
|
||||||
|
docker.run_command(image="alpine", command="test")
|
||||||
|
|
||||||
|
|
||||||
|
async def test_run_command_request_exception(docker: DockerAPI):
|
||||||
|
"""Test command execution when requests raises an exception."""
|
||||||
|
# Mock docker containers.run to raise RequestException
|
||||||
|
docker.docker.containers.run.side_effect = RequestException("Connection error")
|
||||||
|
|
||||||
|
# Execute the command and expect DockerError
|
||||||
|
with pytest.raises(DockerError, match="Can't execute command: Connection error"):
|
||||||
|
docker.run_command(image="alpine", command="test")
|
||||||
|
|
||||||
|
|
||||||
|
async def test_run_command_cleanup_on_exception(docker: DockerAPI):
|
||||||
|
"""Test that container cleanup happens even when an exception occurs."""
|
||||||
|
# Mock container
|
||||||
|
mock_container = MagicMock()
|
||||||
|
|
||||||
|
# Mock docker.containers.run to return container, but container.wait to raise exception
|
||||||
|
docker.docker.containers.run.return_value = mock_container
|
||||||
|
mock_container.wait.side_effect = DockerException("Wait failed")
|
||||||
|
|
||||||
|
# Execute the command and expect DockerError
|
||||||
|
with pytest.raises(DockerError):
|
||||||
|
docker.run_command(image="alpine", command="test")
|
||||||
|
|
||||||
|
# Verify container cleanup still happened
|
||||||
|
mock_container.remove.assert_called_once_with(force=True, v=True)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_run_command_custom_stdout_stderr(docker: DockerAPI):
|
||||||
|
"""Test command execution with custom stdout/stderr settings."""
|
||||||
|
# Mock container and its methods
|
||||||
|
mock_container = MagicMock()
|
||||||
|
mock_container.wait.return_value = {"StatusCode": 0}
|
||||||
|
mock_container.logs.return_value = b"output"
|
||||||
|
|
||||||
|
# Mock docker containers.run to return our mock container
|
||||||
|
docker.docker.containers.run.return_value = mock_container
|
||||||
|
|
||||||
|
# Execute the command with custom stdout/stderr
|
||||||
|
result = docker.run_command(
|
||||||
|
image="alpine", command="test", stdout=False, stderr=True
|
||||||
|
)
|
||||||
|
|
||||||
|
# Verify container.logs was called with the correct parameters
|
||||||
|
mock_container.logs.assert_called_once_with(stdout=False, stderr=True)
|
||||||
|
|
||||||
|
# Verify the result
|
||||||
|
assert result.exit_code == 0
|
||||||
|
assert result.output == b"output"
|
@ -200,7 +200,8 @@ async def test_start(
|
|||||||
coresys.docker.containers.get.return_value.stop.assert_not_called()
|
coresys.docker.containers.get.return_value.stop.assert_not_called()
|
||||||
if container_exists:
|
if container_exists:
|
||||||
coresys.docker.containers.get.return_value.remove.assert_called_once_with(
|
coresys.docker.containers.get.return_value.remove.assert_called_once_with(
|
||||||
force=True
|
force=True,
|
||||||
|
v=True,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
coresys.docker.containers.get.return_value.remove.assert_not_called()
|
coresys.docker.containers.get.return_value.remove.assert_not_called()
|
||||||
@ -397,7 +398,7 @@ async def test_core_loads_wrong_image_for_machine(
|
|||||||
|
|
||||||
await coresys.homeassistant.core.load()
|
await coresys.homeassistant.core.load()
|
||||||
|
|
||||||
container.remove.assert_called_once_with(force=True)
|
container.remove.assert_called_once_with(force=True, v=True)
|
||||||
assert coresys.docker.images.remove.call_args_list[0].kwargs == {
|
assert coresys.docker.images.remove.call_args_list[0].kwargs == {
|
||||||
"image": "ghcr.io/home-assistant/odroid-n2-homeassistant:latest",
|
"image": "ghcr.io/home-assistant/odroid-n2-homeassistant:latest",
|
||||||
"force": True,
|
"force": True,
|
||||||
@ -444,7 +445,7 @@ async def test_core_loads_wrong_image_for_architecture(
|
|||||||
|
|
||||||
await coresys.homeassistant.core.load()
|
await coresys.homeassistant.core.load()
|
||||||
|
|
||||||
container.remove.assert_called_once_with(force=True)
|
container.remove.assert_called_once_with(force=True, v=True)
|
||||||
assert coresys.docker.images.remove.call_args_list[0].kwargs == {
|
assert coresys.docker.images.remove.call_args_list[0].kwargs == {
|
||||||
"image": "ghcr.io/home-assistant/qemux86-64-homeassistant:latest",
|
"image": "ghcr.io/home-assistant/qemux86-64-homeassistant:latest",
|
||||||
"force": True,
|
"force": True,
|
||||||
|
@ -363,7 +363,7 @@ async def test_load_with_incorrect_image(
|
|||||||
|
|
||||||
await plugin.load()
|
await plugin.load()
|
||||||
|
|
||||||
container.remove.assert_called_once_with(force=True)
|
container.remove.assert_called_once_with(force=True, v=True)
|
||||||
assert coresys.docker.images.remove.call_args_list[0].kwargs == {
|
assert coresys.docker.images.remove.call_args_list[0].kwargs == {
|
||||||
"image": f"{old_image}:latest",
|
"image": f"{old_image}:latest",
|
||||||
"force": True,
|
"force": True,
|
||||||
|
@ -76,7 +76,9 @@ async def test_fixup_stopped_core(
|
|||||||
|
|
||||||
assert not coresys.resolution.issues
|
assert not coresys.resolution.issues
|
||||||
assert not coresys.resolution.suggestions
|
assert not coresys.resolution.suggestions
|
||||||
docker.containers.get("addon_local_ssh").remove.assert_called_once_with(force=True)
|
docker.containers.get("addon_local_ssh").remove.assert_called_once_with(
|
||||||
|
force=True, v=True
|
||||||
|
)
|
||||||
assert "Addon local_ssh is stopped" in caplog.text
|
assert "Addon local_ssh is stopped" in caplog.text
|
||||||
|
|
||||||
|
|
||||||
|
@ -65,7 +65,9 @@ async def test_fixup_stopped_core(
|
|||||||
|
|
||||||
assert not coresys.resolution.issues
|
assert not coresys.resolution.issues
|
||||||
assert not coresys.resolution.suggestions
|
assert not coresys.resolution.suggestions
|
||||||
docker.containers.get("homeassistant").remove.assert_called_once_with(force=True)
|
docker.containers.get("homeassistant").remove.assert_called_once_with(
|
||||||
|
force=True, v=True
|
||||||
|
)
|
||||||
assert "Home Assistant is stopped" in caplog.text
|
assert "Home Assistant is stopped" in caplog.text
|
||||||
|
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user