Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions holmes_operator/handlers/healthcheck.py
Original file line number Diff line number Diff line change
Expand Up @@ -228,12 +228,17 @@ async def on_healthcheck_update(
):
logger.info(f"Re-running HealthCheck: {namespace}/{name}")

# Trigger re-execution by calling create handler
# Trigger re-execution by calling create handler.
# kopf also supplies spec/uid in kwargs, and both are passed
# explicitly below; forwarding them twice raises TypeError. Keep the
# rest of kwargs — the create handler reads `body` from it to attach
# events to the resource.
forwarded = {k: v for k, v in kwargs.items() if k not in ("spec", "uid")}
await on_healthcheck_create(
spec=new.get("spec", {}),
name=name,
namespace=namespace,
uid=new.get("metadata", {}).get("uid", ""),
logger=logger,
**kwargs,
**forwarded,
)
85 changes: 84 additions & 1 deletion tests/holmes_operator/test_healthcheck_component.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,10 @@
from holmes_operator import context
from holmes_operator.client.holmes_api_client import HolmesAPIClient
from holmes_operator.config import OperatorConfig
from holmes_operator.handlers.healthcheck import on_healthcheck_create
from holmes_operator.handlers.healthcheck import (
on_healthcheck_create,
on_healthcheck_update,
)
from holmes_operator.models import CheckPhase, CheckStatus, ConditionStatus


Expand Down Expand Up @@ -258,3 +261,83 @@ async def test_api_error_handling(
status = call_2[1]["body"]["status"]
assert status["phase"] == CheckPhase.FAILED.value
assert status["result"] == CheckStatus.ERROR.value


class TestHealthCheckRerun:
"""The holmesgpt.dev/rerun=true annotation path."""

@patch("holmes_operator.handlers.healthcheck.kopf.event")
async def test_rerun_annotation_reexecutes_check(
self, mock_event, setup_context, mock_k8s_api, mock_logger, respx_mock
):
"""Setting rerun=true re-executes the check.

The update handler forwards to the create handler, and kopf passes
spec/uid in kwargs as well as in the named arguments. Forwarding both
used to raise TypeError, so the check never re-ran and kopf retried
the handler indefinitely.
"""
respx_mock.post("http://mock-holmes-api:80/api/checks/execute").mock(
return_value=Response(
200,
json={
"status": "pass",
"message": "All systems operational",
"rationale": "Re-ran on request",
"duration": 1.0,
"model_used": "gpt-4.1",
"notifications": None,
},
)
)

spec = {"query": "Is the namespace healthy?", "timeout": 30, "mode": "monitor"}
body = {
"metadata": {
"name": "test-check-rerun",
"namespace": "default",
"uid": "test-uid-rerun",
"annotations": {"holmesgpt.dev/rerun": "true"},
},
"spec": spec,
}

await on_healthcheck_update(
old={"metadata": {"annotations": {}}, "spec": spec},
new=body,
name="test-check-rerun",
namespace="default",
logger=mock_logger,
# kopf supplies these alongside the named arguments above
spec=spec,
uid="test-uid-rerun",
body=body,
)

assert mock_k8s_api.patch_namespaced_custom_object_status.called
statuses = [
call[1]["body"]["status"]
for call in mock_k8s_api.patch_namespaced_custom_object_status.call_args_list
]
assert any(s.get("result") == CheckStatus.PASS.value for s in statuses)

@patch("holmes_operator.handlers.healthcheck.kopf.event")
async def test_rerun_annotation_unchanged_is_a_noop(
self, mock_event, setup_context, mock_k8s_api, mock_logger
):
"""An annotation that was already true does not re-execute."""
spec = {"query": "Is the namespace healthy?", "timeout": 30, "mode": "monitor"}
annotated = {"metadata": {"annotations": {"holmesgpt.dev/rerun": "true"}}, "spec": spec}

await on_healthcheck_update(
old=annotated,
new=annotated,
name="test-check-rerun",
namespace="default",
logger=mock_logger,
spec=spec,
uid="test-uid-rerun",
body=annotated,
)

assert not mock_k8s_api.patch_namespaced_custom_object_status.called