From 0ae0e1437afa957a89a3e2b5e2bca7486e3ceb5f Mon Sep 17 00:00:00 2001 From: Yuriy Losev Date: Mon, 12 Oct 2026 13:13:08 +0400 Subject: [PATCH] [fix] Put the validation error text into "message" ValidationsCollector.error() used the text itself as the key of the response: {"allowed": false, "": "Internal webhook error: "}. shell-operator reads the reason of a denial from "message" and drops the rest, so the request was denied without a reason. The deckhouse webhook-operator template calls error() for every exception a ValidationWebhook handler raises. tests/validations_test.py covers allow, allow with warnings, deny and error; the error case fails on the previous code. Signed-off-by: Yuriy Losev --- deckhouse/validations.py | 2 +- tests/validations_test.py | 26 ++++++++++++++++++++++++++ 2 files changed, 27 insertions(+), 1 deletion(-) create mode 100644 tests/validations_test.py diff --git a/deckhouse/validations.py b/deckhouse/validations.py index 60445d7..b1a604f 100644 --- a/deckhouse/validations.py +++ b/deckhouse/validations.py @@ -43,4 +43,4 @@ def deny(self, message: typing.Union[str, None] = None): self.collect(response) def error(self, message: str): - self.collect({"allowed": False, message: f"Internal webhook error: {message}"}) + self.collect({"allowed": False, "message": f"Internal webhook error: {message}"}) diff --git a/tests/validations_test.py b/tests/validations_test.py new file mode 100644 index 0000000..30ac90c --- /dev/null +++ b/tests/validations_test.py @@ -0,0 +1,26 @@ +from deckhouse import hook + + +def run(handler): + return hook.testrun(handler).validations.data + + +def test_allow(): + assert run(lambda ctx: ctx.output.validations.allow()) == [{"allowed": True}] + + +def test_allow_with_warnings(): + data = run(lambda ctx: ctx.output.validations.allow("first", "second")) + assert data == [{"allowed": True, "warnings": ("first", "second")}] + + +def test_deny(): + data = run(lambda ctx: ctx.output.validations.deny("not allowed")) + assert data == [{"allowed": False, "message": "not allowed"}] + + +def test_error_is_a_denial_with_the_message(): + # shell-operator reads the reason from "message"; any other key is dropped and the + # requester sees a denial without a reason. + data = run(lambda ctx: ctx.output.validations.error("snapshot is missing")) + assert data == [{"allowed": False, "message": "Internal webhook error: snapshot is missing"}]