mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2026-11-08 11:46:35 +00:00
fix(prometheus): emit the health probe only where a backend answers it
The monitoring block a vhost includes ends in two locations that proxy to
the application's own backend, and resolve_upstream raises rather than
returning empty when no port exists to build that upstream from. A role
that renders a vhost without running a container of its own has none, so
including the block aborted its play. b64e4a0558 made web-svc-mirror the
first such role, four months after the assumption was written down.
The probe halves are now conditional. /healthz/live stays: it answers from
openresty, which is what liveness means for a role whose only artifact is
a vhost. /healthz/ready and /_live_probe go, because both proxy to a
backend that is not there. Guarding only the second one left the first
referencing a variable the guard no longer set, which is its own failure
and the reason this took two passes.
The condition asks a lookup rather than rebuilding the rule in the
template. Hand-derived, it tested the local port alone, which is correct
outside swarm and wrong inside it, where the upstream comes from the
internal port; web-app-matrix declares only that one and would have lost
its probe silently. The lookup reads both scopes and the port a role
resolved for itself, which is what resolve_upstream consumes.
No working endpoint is removed. A vhost rendering the block today must
already carry a port, or the lookup would be raising for it as well, so
the condition can only skip what could never have rendered. Not yet
rendered: the deploy that proves the template resolves has not run.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
777ac4d595
commit
514a1fe74b
3 files changed
+149
No files matched your search
@@ -0,0 +1,80 @@
|
||||
"""Return whether an application has a backend a vhost can probe.
|
||||
|
||||
True when a port exists for ``resolve_upstream`` to build an upstream from:
|
||||
the local one outside swarm, the internal one inside it. Asking that lookup
|
||||
for a role that has neither raises rather than returning empty.
|
||||
|
||||
Usage in a template::
|
||||
|
||||
{% if lookup('probeable_backend', application_id) %}
|
||||
...health probe against the app's own backend...
|
||||
{% endif %}
|
||||
|
||||
Pass ``application_id`` as term 0, defaulting to the templating variable of the
|
||||
same name. ``http_port`` and ``proxy_internal_port`` are read from the same
|
||||
variables, so a role that resolved its port itself is not reported as
|
||||
backendless. ``applications`` comes from ``lookup('applications')``, the
|
||||
merged-config SPOT.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from ansible.plugins.loader import lookup_loader
|
||||
from ansible.plugins.lookup import LookupBase
|
||||
|
||||
from utils.roles.entity.name import get_entity_name
|
||||
|
||||
PORT_SCOPES = ("local", "internal")
|
||||
|
||||
|
||||
class LookupModule(LookupBase):
|
||||
def run(
|
||||
self,
|
||||
terms: list[Any],
|
||||
variables: dict[str, Any] | None = None,
|
||||
**kwargs: Any,
|
||||
) -> list[bool]:
|
||||
"""Return ``[True]`` when a port exists to probe, ``[False]`` otherwise.
|
||||
|
||||
Args:
|
||||
terms: term 0 is the application id.
|
||||
variables: templating variables.
|
||||
kwargs: unused.
|
||||
"""
|
||||
vars_ = variables or getattr(self._templar, "available_variables", {}) or {}
|
||||
|
||||
if vars_.get("http_port") or vars_.get("proxy_internal_port"):
|
||||
return [True]
|
||||
|
||||
if terms and isinstance(terms[0], str):
|
||||
application_id = terms[0]
|
||||
else:
|
||||
application_id = vars_.get("application_id", "")
|
||||
if not application_id:
|
||||
return [False]
|
||||
|
||||
applications = lookup_loader.get(
|
||||
"applications",
|
||||
loader=self._loader,
|
||||
templar=getattr(self, "_templar", None),
|
||||
).run([], variables=vars_)[0]
|
||||
|
||||
entity = get_entity_name(application_id)
|
||||
ports = (
|
||||
applications.get(application_id, {})
|
||||
.get("services", {})
|
||||
.get(entity, {})
|
||||
.get("ports", {})
|
||||
)
|
||||
if not isinstance(ports, dict):
|
||||
return [False]
|
||||
|
||||
return [
|
||||
any(
|
||||
(ports.get(scope) or {}).get("http")
|
||||
for scope in PORT_SCOPES
|
||||
if isinstance(ports.get(scope), dict)
|
||||
)
|
||||
]
|
||||
@@ -28,6 +28,8 @@
|
||||
end
|
||||
end
|
||||
|
||||
{% set _probe_backend = lookup('probeable_backend', application_id) %}
|
||||
{% if _probe_backend %}
|
||||
-- Step 2: TCP/HTTP reachability — catches crashed or exited containers.
|
||||
if alive then
|
||||
local res = ngx.location.capture("/_live_probe")
|
||||
@@ -35,6 +37,7 @@
|
||||
alive = false
|
||||
end
|
||||
end
|
||||
{% endif %}
|
||||
|
||||
if alive then
|
||||
ngx.status = ngx.HTTP_OK
|
||||
@@ -48,6 +51,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
{% if _probe_backend %}
|
||||
{% set prometheus_backend = lookup('proxy_pass', application_id, proxy_service_key | default(application_id | get_entity_name), 'http', tail='/', local_port=http_port, internal_port=proxy_internal_port | default('')) %}
|
||||
location = /_live_probe {
|
||||
internal;
|
||||
@@ -66,3 +70,4 @@
|
||||
proxy_read_timeout 5s;
|
||||
proxy_intercept_errors off;
|
||||
}
|
||||
{% endif %}
|
||||
@@ -0,0 +1,64 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import unittest
|
||||
from unittest import mock
|
||||
from unittest.mock import patch
|
||||
|
||||
from ansible.plugins.loader import lookup_loader
|
||||
|
||||
from . import PROJECT_ROOT # noqa: F401
|
||||
|
||||
|
||||
class _DummyTemplar:
|
||||
def __init__(self, available_vars: dict):
|
||||
self.available_variables = available_vars
|
||||
|
||||
|
||||
class ProbeableBackendTests(unittest.TestCase):
|
||||
def setUp(self) -> None:
|
||||
self.lookup = lookup_loader.get("probeable_backend")
|
||||
self.lookup._templar = _DummyTemplar({})
|
||||
self.lookup._loader = mock.MagicMock()
|
||||
|
||||
def _run(self, terms, applications: dict, **vars_):
|
||||
class _StubApplications:
|
||||
def run(self, terms_, variables=None, **kw):
|
||||
return [applications]
|
||||
|
||||
with patch.object(lookup_loader, "get", return_value=_StubApplications()):
|
||||
return self.lookup.run(terms, variables=vars_)
|
||||
|
||||
def test_a_role_without_any_port_has_nothing_to_probe(self) -> None:
|
||||
apps = {"web-svc-mirror": {"services": {"mirror": {}}}}
|
||||
|
||||
self.assertEqual(self._run(["web-svc-mirror"], apps), [False])
|
||||
|
||||
def test_a_local_port_is_probeable(self) -> None:
|
||||
apps = {
|
||||
"web-app-foo": {"services": {"foo": {"ports": {"local": {"http": 8080}}}}}
|
||||
}
|
||||
|
||||
self.assertEqual(self._run(["web-app-foo"], apps), [True])
|
||||
|
||||
def test_an_internal_port_alone_is_probeable(self) -> None:
|
||||
apps = {
|
||||
"web-app-foo": {
|
||||
"services": {"foo": {"ports": {"internal": {"http": 8008}}}}
|
||||
}
|
||||
}
|
||||
|
||||
self.assertEqual(
|
||||
self._run(["web-app-foo"], apps),
|
||||
[True],
|
||||
"swarm resolves the upstream from the internal port, so a role "
|
||||
"carrying only that one still has a backend to probe",
|
||||
)
|
||||
|
||||
def test_a_port_the_role_resolved_itself_counts(self) -> None:
|
||||
apps = {"web-app-foo": {"services": {"foo": {}}}}
|
||||
|
||||
self.assertEqual(self._run(["web-app-foo"], apps, http_port="8021"), [True])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in new issue
Block a user