gowalk-cicd 1.0.177 → 1.0.178
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -0
- package/backend-action/edge_health.py +78 -16
- package/docs/BACKEND_EDGE.md +16 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -524,6 +524,10 @@ remain readable by the panel. Masked fields are never reconstructed and credenti
|
|
|
524
524
|
Protected vhost failures add `backend_vhost_failed` (`gowalk-cicd/backend-vhost-failed.v1`) with fixed
|
|
525
525
|
brace-free `operation`, `stage`, `reason`, `child_exit` and `signals`; see `docs/BACKEND_EDGE.md`.
|
|
526
526
|
It preserves the existing refusal and records only bounded observed facts, never private child output.
|
|
527
|
+
Edge health failures add `backend_health_failed` (`gowalk-cicd/backend-health-failed.v1`) with fixed
|
|
528
|
+
brace-free `probe`, `reason`, `curl_exit`, `http_status` and `attempt`; see `docs/BACKEND_EDGE.md`.
|
|
529
|
+
Only the final failed probe's bounded observations appear, with no URLs, bodies, headers or credentials.
|
|
530
|
+
Ingress refusal, strict TLS, exact-build acceptance, retry bounds and the original failed result remain.
|
|
527
531
|
An interrupted locked Pod fetch can retry once within the existing 900-second install budget.
|
|
528
532
|
`native_pods_retry` (`gowalk-cicd/native-pods-retry.v1`) reports `attempt:2` and `reason:partial_transfer`;
|
|
529
533
|
successful preparation reports `pod_attempts`. Dependency locks, relay and TLS checks remain enforced.
|
|
@@ -9,6 +9,25 @@ import time
|
|
|
9
9
|
from edge_inputs import load, TOKEN_HEADER
|
|
10
10
|
from edge_vhost import NGINX_DIR, paths, recorded_domain
|
|
11
11
|
|
|
12
|
+
PROBES = frozenset({"configuration", "origin_missing_token", "origin_invalid_token", "origin_authorized",
|
|
13
|
+
"legacy_identity", "legacy", "public"})
|
|
14
|
+
REASONS = frozenset({"unclassified", "path_invalid", "request_failed", "http_refused", "json_invalid",
|
|
15
|
+
"body_shape", "build_missing", "build_mismatch"})
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def report(probe, reason, *, curl_exit=None, http_status=None, attempt=None):
|
|
19
|
+
probe = probe if type(probe) is str and probe in PROBES else "unclassified"
|
|
20
|
+
reason = reason if type(reason) is str and reason in REASONS else "unclassified"
|
|
21
|
+
code = str(curl_exit) if type(curl_exit) is int and -255 <= curl_exit <= 255 else "none"
|
|
22
|
+
status = str(http_status) if type(http_status) is int and 100 <= http_status <= 599 else "none"
|
|
23
|
+
count = str(attempt) if type(attempt) is int and 1 <= attempt <= 15 else "none"
|
|
24
|
+
message = ("::error title=backend_health_failed::gowalk-cicd/backend-health-failed.v1 "
|
|
25
|
+
"probe={} reason={} curl_exit={} http_status={} attempt={}\n")
|
|
26
|
+
try:
|
|
27
|
+
sys.stderr.write(message.format(probe, reason, code, status, count))
|
|
28
|
+
except (OSError, ValueError):
|
|
29
|
+
pass # Diagnostic delivery must not replace the original refusal.
|
|
30
|
+
|
|
12
31
|
|
|
13
32
|
def request(url, *, headers=None, run=subprocess.run):
|
|
14
33
|
# curl retains its strict TLS/time bounds. Private headers and bounded response
|
|
@@ -27,46 +46,89 @@ def request(url, *, headers=None, run=subprocess.run):
|
|
|
27
46
|
return result.returncode, int(status) if status.isdigit() else 0, data
|
|
28
47
|
|
|
29
48
|
|
|
30
|
-
def
|
|
49
|
+
def response_reason(code, status, body, sha):
|
|
50
|
+
if code != 0:
|
|
51
|
+
return "request_failed"
|
|
52
|
+
if not 200 <= status < 300:
|
|
53
|
+
return "http_refused"
|
|
54
|
+
if sha:
|
|
55
|
+
try:
|
|
56
|
+
value = json.loads(body)
|
|
57
|
+
except ValueError:
|
|
58
|
+
return "json_invalid"
|
|
59
|
+
if not isinstance(value, dict):
|
|
60
|
+
return "body_shape"
|
|
61
|
+
if "build_sha" not in value:
|
|
62
|
+
return "build_missing"
|
|
63
|
+
if value.get("build_sha") != sha:
|
|
64
|
+
return "build_mismatch"
|
|
65
|
+
return None
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def healthy(url, sha="", *, headers=None, fetch=request, sleep=time.sleep, probe="public"):
|
|
31
69
|
for attempt in range(15):
|
|
70
|
+
code = status = None
|
|
71
|
+
reason = "unclassified"
|
|
32
72
|
try:
|
|
33
73
|
code, status, body = fetch(url, headers=headers)
|
|
34
|
-
|
|
35
|
-
if
|
|
36
|
-
value = json.loads(body)
|
|
37
|
-
valid = isinstance(value, dict) and value.get("build_sha") == sha
|
|
38
|
-
if valid:
|
|
74
|
+
reason = response_reason(code, status, body, sha)
|
|
75
|
+
if reason is None:
|
|
39
76
|
return
|
|
40
77
|
except (OSError, ValueError, subprocess.SubprocessError):
|
|
41
|
-
|
|
78
|
+
reason = "request_failed" if code is None else "unclassified"
|
|
42
79
|
if attempt != 14:
|
|
43
80
|
sleep(2)
|
|
81
|
+
report(probe, reason, curl_exit=code, http_status=status, attempt=15)
|
|
44
82
|
raise ValueError("backend_public_health_failed")
|
|
45
83
|
|
|
46
84
|
|
|
85
|
+
def origin_refusal(url, headers, probe, fetch):
|
|
86
|
+
try:
|
|
87
|
+
code, status, _body = fetch(url, headers=headers)
|
|
88
|
+
except (OSError, ValueError, subprocess.SubprocessError):
|
|
89
|
+
report(probe, "request_failed", attempt=1)
|
|
90
|
+
raise
|
|
91
|
+
if code != 0 or status != 403:
|
|
92
|
+
report(probe, "request_failed" if code != 0 else "http_refused",
|
|
93
|
+
curl_exit=code, http_status=status, attempt=1)
|
|
94
|
+
raise ValueError("backend_origin_ingress_unverified")
|
|
95
|
+
|
|
96
|
+
|
|
47
97
|
def verify(config, app, domain, health, *, nginx_dir=NGINX_DIR, fetch=request, sleep=time.sleep):
|
|
48
|
-
|
|
98
|
+
try:
|
|
99
|
+
value = load(config)
|
|
100
|
+
except (OSError, ValueError, subprocess.SubprocessError):
|
|
101
|
+
report("configuration", "unclassified")
|
|
102
|
+
raise
|
|
49
103
|
if not health.startswith("/") or any(character in health for character in "\r\n?#"):
|
|
104
|
+
report("configuration", "path_invalid")
|
|
50
105
|
raise ValueError("backend_health_path_invalid")
|
|
51
106
|
origin_url = f"https://{domain}{health}"
|
|
52
107
|
if value["ingress_required"] == "true":
|
|
53
|
-
for headers in (
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
raise ValueError("backend_origin_ingress_unverified")
|
|
108
|
+
for probe, headers in (("origin_missing_token", None),
|
|
109
|
+
("origin_invalid_token", {TOKEN_HEADER: "invalid-backend-ingress-token"})):
|
|
110
|
+
origin_refusal(origin_url, headers, probe, fetch)
|
|
57
111
|
healthy(origin_url, value["expected_build_sha"], headers={TOKEN_HEADER: value["ingress_token"]},
|
|
58
|
-
fetch=fetch, sleep=sleep)
|
|
59
|
-
|
|
112
|
+
fetch=fetch, sleep=sleep, probe="origin_authorized")
|
|
113
|
+
try:
|
|
114
|
+
legacy = recorded_domain(paths(config, app, nginx_dir)[0])
|
|
115
|
+
except (OSError, ValueError, subprocess.SubprocessError):
|
|
116
|
+
report("legacy_identity", "unclassified")
|
|
117
|
+
raise
|
|
60
118
|
if legacy:
|
|
61
|
-
healthy(f"https://{legacy}{health}", fetch=fetch, sleep=sleep)
|
|
119
|
+
healthy(f"https://{legacy}{health}", fetch=fetch, sleep=sleep, probe="legacy")
|
|
62
120
|
public = value["public_health_url"] or origin_url
|
|
63
121
|
healthy(public, value["expected_build_sha"], fetch=fetch, sleep=sleep)
|
|
64
122
|
return {"ok": True, "public_health_verified": True, "build_sha": value["expected_build_sha"],
|
|
65
123
|
"ingress_verified": value["ingress_required"] == "true"}
|
|
66
124
|
|
|
67
125
|
|
|
68
|
-
|
|
126
|
+
def main():
|
|
69
127
|
try:
|
|
70
128
|
print(json.dumps(verify(*sys.argv[1:5])))
|
|
71
129
|
except (OSError, ValueError, subprocess.SubprocessError):
|
|
72
130
|
sys.exit("backend_edge_health_failed")
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
if __name__ == "__main__":
|
|
134
|
+
main()
|
package/docs/BACKEND_EDGE.md
CHANGED
|
@@ -57,3 +57,19 @@ They are observed patterns, not a unique cause or retry permission. No raw error
|
|
|
57
57
|
token or child output is included. A failed restoration adds a separate rollback refusal while keeping
|
|
58
58
|
the original failure visible; absence of that extra line alone does not prove successful restoration.
|
|
59
59
|
The generic `backend_edge_vhost_refused`, changed/unchanged stdout, ingress, TLS and deploy gates remain.
|
|
60
|
+
|
|
61
|
+
Health refusals add the fixed stderr annotation `backend_health_failed` with schema
|
|
62
|
+
`gowalk-cicd/backend-health-failed.v1`. Its brace-free fields are `probe`, `reason`, `curl_exit`,
|
|
63
|
+
`http_status` and `attempt`. Probes are configuration, origin_missing_token, origin_invalid_token,
|
|
64
|
+
origin_authorized, legacy_identity, legacy and public; unknown metadata becomes unclassified.
|
|
65
|
+
Reasons are unclassified, path_invalid, request_failed, http_refused, json_invalid, body_shape,
|
|
66
|
+
build_missing and build_mismatch. No URL, response body, build identity, header, credential, path,
|
|
67
|
+
exception text or subprocess output enters the annotation. Curl exit is an observed integer from
|
|
68
|
+
-255 to 255, HTTP status from 100 to 599 and attempt from 1 to 15; other values become `none`.
|
|
69
|
+
An exhausted health check emits only its final attempt's facts, clearing any previous observations.
|
|
70
|
+
These facts identify the failed probe, not a unique cause or permission to retry or bypass it.
|
|
71
|
+
Missing-token and invalid-token checks remain single requests requiring completed 403 responses.
|
|
72
|
+
Healthy-origin, retained-legacy and public checks retain 15 attempts with two seconds between them,
|
|
73
|
+
strict TLS, existing curl time/body bounds and exact build checks. A successful check emits no
|
|
74
|
+
failure annotation and returns the existing result. Original exceptions and the generic CLI refusal
|
|
75
|
+
`backend_edge_health_failed` with exit 1 remain unchanged, including when diagnostic delivery fails.
|