Skip to content

Commit 381e719

Browse files
authored
Allow to force rebuild of add-ons (#6002)
1 parent 2960710 commit 381e719

File tree

4 files changed

+103
-3
lines changed

4 files changed

+103
-3
lines changed

supervisor/addons/manager.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -266,7 +266,7 @@ async def update(
266266
],
267267
on_condition=AddonsJobError,
268268
)
269-
async def rebuild(self, slug: str) -> asyncio.Task | None:
269+
async def rebuild(self, slug: str, *, force: bool = False) -> asyncio.Task | None:
270270
"""Perform a rebuild of local build add-on.
271271
272272
Returns a Task that completes when addon has state 'started' (see addon.start)
@@ -289,7 +289,7 @@ async def rebuild(self, slug: str) -> asyncio.Task | None:
289289
raise AddonsError(
290290
"Version changed, use Update instead Rebuild", _LOGGER.error
291291
)
292-
if not addon.need_build:
292+
if not force and not addon.need_build:
293293
raise AddonsNotSupportedError(
294294
"Can't rebuild a image based add-on", _LOGGER.error
295295
)

supervisor/api/addons.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
ATTR_DNS,
3737
ATTR_DOCKER_API,
3838
ATTR_DOCUMENTATION,
39+
ATTR_FORCE,
3940
ATTR_FULL_ACCESS,
4041
ATTR_GPIO,
4142
ATTR_HASSIO_API,
@@ -139,6 +140,8 @@
139140
SCHEMA_UNINSTALL = vol.Schema(
140141
{vol.Optional(ATTR_REMOVE_CONFIG, default=False): vol.Boolean()}
141142
)
143+
144+
SCHEMA_REBUILD = vol.Schema({vol.Optional(ATTR_FORCE, default=False): vol.Boolean()})
142145
# pylint: enable=no-value-for-parameter
143146

144147

@@ -461,7 +464,11 @@ async def restart(self, request: web.Request) -> None:
461464
async def rebuild(self, request: web.Request) -> None:
462465
"""Rebuild local build add-on."""
463466
addon = self.get_addon_for_request(request)
464-
if start_task := await asyncio.shield(self.sys_addons.rebuild(addon.slug)):
467+
body: dict[str, Any] = await api_validate(SCHEMA_REBUILD, request)
468+
469+
if start_task := await asyncio.shield(
470+
self.sys_addons.rebuild(addon.slug, force=body[ATTR_FORCE])
471+
):
465472
await start_task
466473

467474
@api_process

supervisor/const.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,7 @@
188188
ATTR_FILENAME = "filename"
189189
ATTR_FLAGS = "flags"
190190
ATTR_FOLDERS = "folders"
191+
ATTR_FORCE = "force"
191192
ATTR_FORCE_SECURITY = "force_security"
192193
ATTR_FREQUENCY = "frequency"
193194
ATTR_FULL_ACCESS = "full_access"

tests/api/test_addons.py

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -261,6 +261,98 @@ async def container_events_task(*args, **kwargs):
261261
assert resp.status == 200
262262

263263

264+
async def test_api_addon_rebuild_force(
265+
api_client: TestClient,
266+
coresys: CoreSys,
267+
install_addon_ssh: Addon,
268+
container: MagicMock,
269+
tmp_supervisor_data,
270+
path_extern,
271+
):
272+
"""Test rebuilding an image-based addon with force parameter."""
273+
coresys.hardware.disk.get_disk_free_space = lambda x: 5000
274+
container.status = "running"
275+
install_addon_ssh.path_data.mkdir()
276+
container.attrs["Config"] = {"Healthcheck": "exists"}
277+
await install_addon_ssh.load()
278+
await asyncio.sleep(0)
279+
assert install_addon_ssh.state == AddonState.STARTUP
280+
281+
state_changes: list[AddonState] = []
282+
_container_events_task: asyncio.Task | None = None
283+
284+
async def container_events():
285+
nonlocal state_changes
286+
287+
await install_addon_ssh.container_state_changed(
288+
_create_test_event(f"addon_{TEST_ADDON_SLUG}", ContainerState.STOPPED)
289+
)
290+
state_changes.append(install_addon_ssh.state)
291+
292+
await install_addon_ssh.container_state_changed(
293+
_create_test_event(f"addon_{TEST_ADDON_SLUG}", ContainerState.RUNNING)
294+
)
295+
state_changes.append(install_addon_ssh.state)
296+
await asyncio.sleep(0)
297+
298+
await install_addon_ssh.container_state_changed(
299+
_create_test_event(f"addon_{TEST_ADDON_SLUG}", ContainerState.HEALTHY)
300+
)
301+
302+
async def container_events_task(*args, **kwargs):
303+
nonlocal _container_events_task
304+
_container_events_task = asyncio.create_task(container_events())
305+
306+
# Test 1: Without force, image-based addon should fail
307+
with (
308+
patch.object(AddonBuild, "is_valid", return_value=True),
309+
patch.object(DockerAddon, "is_running", return_value=False),
310+
patch.object(
311+
Addon, "need_build", new=PropertyMock(return_value=False)
312+
), # Image-based
313+
patch.object(CpuArch, "supported", new=PropertyMock(return_value=["amd64"])),
314+
):
315+
resp = await api_client.post("/addons/local_ssh/rebuild")
316+
317+
assert resp.status == 400
318+
result = await resp.json()
319+
assert "Can't rebuild a image based add-on" in result["message"]
320+
321+
# Reset state for next test
322+
state_changes.clear()
323+
324+
# Test 2: With force=True, image-based addon should succeed
325+
with (
326+
patch.object(AddonBuild, "is_valid", return_value=True),
327+
patch.object(DockerAddon, "is_running", return_value=False),
328+
patch.object(
329+
Addon, "need_build", new=PropertyMock(return_value=False)
330+
), # Image-based
331+
patch.object(CpuArch, "supported", new=PropertyMock(return_value=["amd64"])),
332+
patch.object(DockerAddon, "run", new=container_events_task),
333+
patch.object(
334+
coresys.docker,
335+
"run_command",
336+
new=PropertyMock(return_value=CommandReturn(0, b"Build successful")),
337+
),
338+
patch.object(
339+
DockerAddon, "healthcheck", new=PropertyMock(return_value={"exists": True})
340+
),
341+
patch.object(
342+
type(coresys.config),
343+
"local_to_extern_path",
344+
return_value="/addon/path/on/host",
345+
),
346+
):
347+
resp = await api_client.post("/addons/local_ssh/rebuild", json={"force": True})
348+
349+
assert state_changes == [AddonState.STOPPED, AddonState.STARTUP]
350+
assert install_addon_ssh.state == AddonState.STARTED
351+
assert resp.status == 200
352+
353+
await _container_events_task
354+
355+
264356
async def test_api_addon_uninstall(
265357
api_client: TestClient,
266358
coresys: CoreSys,

0 commit comments

Comments
 (0)