Remove anonymous volumes when removing containers (#5977)

* Remove anonymous volumes when removing containers

* Add tests for docker.run_command()
This commit is contained in:
Felipe Santos 2025-06-30 08:31:41 -03:00 committed by GitHub
parent 779f47e25d
commit b8852872fe
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 148 additions and 9 deletions

View File

@ -327,7 +327,7 @@ class DockerAPI:
# cleanup container
if container:
with suppress(docker_errors.DockerException, requests.RequestException):
container.remove(force=True)
container.remove(force=True, v=True)
return CommandReturn(result.get("StatusCode"), output)
@ -442,7 +442,7 @@ class DockerAPI:
if remove_container:
with suppress(DockerException, requests.RequestException):
_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:
"""Start Docker container."""

View File

@ -843,7 +843,7 @@ async def test_addon_loads_wrong_image(
with patch("pathlib.Path.is_file", return_value=True):
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 == {
"image": "local/aarch64-addon-ssh:latest",
"force": True,

View 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"

View File

@ -200,7 +200,8 @@ async def test_start(
coresys.docker.containers.get.return_value.stop.assert_not_called()
if container_exists:
coresys.docker.containers.get.return_value.remove.assert_called_once_with(
force=True
force=True,
v=True,
)
else:
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()
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 == {
"image": "ghcr.io/home-assistant/odroid-n2-homeassistant:latest",
"force": True,
@ -444,7 +445,7 @@ async def test_core_loads_wrong_image_for_architecture(
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 == {
"image": "ghcr.io/home-assistant/qemux86-64-homeassistant:latest",
"force": True,

View File

@ -363,7 +363,7 @@ async def test_load_with_incorrect_image(
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 == {
"image": f"{old_image}:latest",
"force": True,

View File

@ -76,7 +76,9 @@ async def test_fixup_stopped_core(
assert not coresys.resolution.issues
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

View File

@ -65,7 +65,9 @@ async def test_fixup_stopped_core(
assert not coresys.resolution.issues
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