mirror of
https://github.com/home-assistant/supervisor.git
synced 2025-07-25 01:56:34 +00:00
Allow to force rebuild of add-ons (#6002)
This commit is contained in:
parent
296071067d
commit
381e719a0e
@ -266,7 +266,7 @@ class AddonManager(CoreSysAttributes):
|
|||||||
],
|
],
|
||||||
on_condition=AddonsJobError,
|
on_condition=AddonsJobError,
|
||||||
)
|
)
|
||||||
async def rebuild(self, slug: str) -> asyncio.Task | None:
|
async def rebuild(self, slug: str, *, force: bool = False) -> asyncio.Task | None:
|
||||||
"""Perform a rebuild of local build add-on.
|
"""Perform a rebuild of local build add-on.
|
||||||
|
|
||||||
Returns a Task that completes when addon has state 'started' (see addon.start)
|
Returns a Task that completes when addon has state 'started' (see addon.start)
|
||||||
@ -289,7 +289,7 @@ class AddonManager(CoreSysAttributes):
|
|||||||
raise AddonsError(
|
raise AddonsError(
|
||||||
"Version changed, use Update instead Rebuild", _LOGGER.error
|
"Version changed, use Update instead Rebuild", _LOGGER.error
|
||||||
)
|
)
|
||||||
if not addon.need_build:
|
if not force and not addon.need_build:
|
||||||
raise AddonsNotSupportedError(
|
raise AddonsNotSupportedError(
|
||||||
"Can't rebuild a image based add-on", _LOGGER.error
|
"Can't rebuild a image based add-on", _LOGGER.error
|
||||||
)
|
)
|
||||||
|
@ -36,6 +36,7 @@ from ..const import (
|
|||||||
ATTR_DNS,
|
ATTR_DNS,
|
||||||
ATTR_DOCKER_API,
|
ATTR_DOCKER_API,
|
||||||
ATTR_DOCUMENTATION,
|
ATTR_DOCUMENTATION,
|
||||||
|
ATTR_FORCE,
|
||||||
ATTR_FULL_ACCESS,
|
ATTR_FULL_ACCESS,
|
||||||
ATTR_GPIO,
|
ATTR_GPIO,
|
||||||
ATTR_HASSIO_API,
|
ATTR_HASSIO_API,
|
||||||
@ -139,6 +140,8 @@ SCHEMA_SECURITY = vol.Schema({vol.Optional(ATTR_PROTECTED): vol.Boolean()})
|
|||||||
SCHEMA_UNINSTALL = vol.Schema(
|
SCHEMA_UNINSTALL = vol.Schema(
|
||||||
{vol.Optional(ATTR_REMOVE_CONFIG, default=False): vol.Boolean()}
|
{vol.Optional(ATTR_REMOVE_CONFIG, default=False): vol.Boolean()}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
SCHEMA_REBUILD = vol.Schema({vol.Optional(ATTR_FORCE, default=False): vol.Boolean()})
|
||||||
# pylint: enable=no-value-for-parameter
|
# pylint: enable=no-value-for-parameter
|
||||||
|
|
||||||
|
|
||||||
@ -461,7 +464,11 @@ class APIAddons(CoreSysAttributes):
|
|||||||
async def rebuild(self, request: web.Request) -> None:
|
async def rebuild(self, request: web.Request) -> None:
|
||||||
"""Rebuild local build add-on."""
|
"""Rebuild local build add-on."""
|
||||||
addon = self.get_addon_for_request(request)
|
addon = self.get_addon_for_request(request)
|
||||||
if start_task := await asyncio.shield(self.sys_addons.rebuild(addon.slug)):
|
body: dict[str, Any] = await api_validate(SCHEMA_REBUILD, request)
|
||||||
|
|
||||||
|
if start_task := await asyncio.shield(
|
||||||
|
self.sys_addons.rebuild(addon.slug, force=body[ATTR_FORCE])
|
||||||
|
):
|
||||||
await start_task
|
await start_task
|
||||||
|
|
||||||
@api_process
|
@api_process
|
||||||
|
@ -188,6 +188,7 @@ ATTR_FEATURES = "features"
|
|||||||
ATTR_FILENAME = "filename"
|
ATTR_FILENAME = "filename"
|
||||||
ATTR_FLAGS = "flags"
|
ATTR_FLAGS = "flags"
|
||||||
ATTR_FOLDERS = "folders"
|
ATTR_FOLDERS = "folders"
|
||||||
|
ATTR_FORCE = "force"
|
||||||
ATTR_FORCE_SECURITY = "force_security"
|
ATTR_FORCE_SECURITY = "force_security"
|
||||||
ATTR_FREQUENCY = "frequency"
|
ATTR_FREQUENCY = "frequency"
|
||||||
ATTR_FULL_ACCESS = "full_access"
|
ATTR_FULL_ACCESS = "full_access"
|
||||||
|
@ -261,6 +261,98 @@ async def test_api_addon_rebuild_healthcheck(
|
|||||||
assert resp.status == 200
|
assert resp.status == 200
|
||||||
|
|
||||||
|
|
||||||
|
async def test_api_addon_rebuild_force(
|
||||||
|
api_client: TestClient,
|
||||||
|
coresys: CoreSys,
|
||||||
|
install_addon_ssh: Addon,
|
||||||
|
container: MagicMock,
|
||||||
|
tmp_supervisor_data,
|
||||||
|
path_extern,
|
||||||
|
):
|
||||||
|
"""Test rebuilding an image-based addon with force parameter."""
|
||||||
|
coresys.hardware.disk.get_disk_free_space = lambda x: 5000
|
||||||
|
container.status = "running"
|
||||||
|
install_addon_ssh.path_data.mkdir()
|
||||||
|
container.attrs["Config"] = {"Healthcheck": "exists"}
|
||||||
|
await install_addon_ssh.load()
|
||||||
|
await asyncio.sleep(0)
|
||||||
|
assert install_addon_ssh.state == AddonState.STARTUP
|
||||||
|
|
||||||
|
state_changes: list[AddonState] = []
|
||||||
|
_container_events_task: asyncio.Task | None = None
|
||||||
|
|
||||||
|
async def container_events():
|
||||||
|
nonlocal state_changes
|
||||||
|
|
||||||
|
await install_addon_ssh.container_state_changed(
|
||||||
|
_create_test_event(f"addon_{TEST_ADDON_SLUG}", ContainerState.STOPPED)
|
||||||
|
)
|
||||||
|
state_changes.append(install_addon_ssh.state)
|
||||||
|
|
||||||
|
await install_addon_ssh.container_state_changed(
|
||||||
|
_create_test_event(f"addon_{TEST_ADDON_SLUG}", ContainerState.RUNNING)
|
||||||
|
)
|
||||||
|
state_changes.append(install_addon_ssh.state)
|
||||||
|
await asyncio.sleep(0)
|
||||||
|
|
||||||
|
await install_addon_ssh.container_state_changed(
|
||||||
|
_create_test_event(f"addon_{TEST_ADDON_SLUG}", ContainerState.HEALTHY)
|
||||||
|
)
|
||||||
|
|
||||||
|
async def container_events_task(*args, **kwargs):
|
||||||
|
nonlocal _container_events_task
|
||||||
|
_container_events_task = asyncio.create_task(container_events())
|
||||||
|
|
||||||
|
# Test 1: Without force, image-based addon should fail
|
||||||
|
with (
|
||||||
|
patch.object(AddonBuild, "is_valid", return_value=True),
|
||||||
|
patch.object(DockerAddon, "is_running", return_value=False),
|
||||||
|
patch.object(
|
||||||
|
Addon, "need_build", new=PropertyMock(return_value=False)
|
||||||
|
), # Image-based
|
||||||
|
patch.object(CpuArch, "supported", new=PropertyMock(return_value=["amd64"])),
|
||||||
|
):
|
||||||
|
resp = await api_client.post("/addons/local_ssh/rebuild")
|
||||||
|
|
||||||
|
assert resp.status == 400
|
||||||
|
result = await resp.json()
|
||||||
|
assert "Can't rebuild a image based add-on" in result["message"]
|
||||||
|
|
||||||
|
# Reset state for next test
|
||||||
|
state_changes.clear()
|
||||||
|
|
||||||
|
# Test 2: With force=True, image-based addon should succeed
|
||||||
|
with (
|
||||||
|
patch.object(AddonBuild, "is_valid", return_value=True),
|
||||||
|
patch.object(DockerAddon, "is_running", return_value=False),
|
||||||
|
patch.object(
|
||||||
|
Addon, "need_build", new=PropertyMock(return_value=False)
|
||||||
|
), # Image-based
|
||||||
|
patch.object(CpuArch, "supported", new=PropertyMock(return_value=["amd64"])),
|
||||||
|
patch.object(DockerAddon, "run", new=container_events_task),
|
||||||
|
patch.object(
|
||||||
|
coresys.docker,
|
||||||
|
"run_command",
|
||||||
|
new=PropertyMock(return_value=CommandReturn(0, b"Build successful")),
|
||||||
|
),
|
||||||
|
patch.object(
|
||||||
|
DockerAddon, "healthcheck", new=PropertyMock(return_value={"exists": True})
|
||||||
|
),
|
||||||
|
patch.object(
|
||||||
|
type(coresys.config),
|
||||||
|
"local_to_extern_path",
|
||||||
|
return_value="/addon/path/on/host",
|
||||||
|
),
|
||||||
|
):
|
||||||
|
resp = await api_client.post("/addons/local_ssh/rebuild", json={"force": True})
|
||||||
|
|
||||||
|
assert state_changes == [AddonState.STOPPED, AddonState.STARTUP]
|
||||||
|
assert install_addon_ssh.state == AddonState.STARTED
|
||||||
|
assert resp.status == 200
|
||||||
|
|
||||||
|
await _container_events_task
|
||||||
|
|
||||||
|
|
||||||
async def test_api_addon_uninstall(
|
async def test_api_addon_uninstall(
|
||||||
api_client: TestClient,
|
api_client: TestClient,
|
||||||
coresys: CoreSys,
|
coresys: CoreSys,
|
||||||
|
Loading…
x
Reference in New Issue
Block a user