Assistant: codex Assistant-Model: gpt-6.1-sol Assistant-Session: 01a126e1-76f8-7643-8cf3-e93336539ade
136 lines
6.9 KiB
Python
136 lines
6.9 KiB
Python
"""Executable read-only sampler for the existing durable release HealthObserver.
|
|
|
|
No signer, identity admission or mutation is provided. Each failed/unavailable
|
|
probe records False, missed intervals reset, and a new ledger starts now.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
from datetime import datetime, timezone
|
|
import ipaddress
|
|
import json
|
|
from pathlib import Path
|
|
import time
|
|
import urllib.request
|
|
|
|
import yaml
|
|
|
|
from .release_observer import HealthObserver
|
|
from .release_operations import revision, update_child
|
|
from .release_transport import run
|
|
|
|
CLUSTER_UID = 'a553c742-0115-43d4-99a4-a5ca56fe0786'
|
|
BASELINE = 'a86a14332d76d92a7ed176ea34dca4429902c396'
|
|
DEPLOYMENTS = {'actcore-api', 'actcore-worker', 'actcore-event-router'}
|
|
RESOURCE_SET = {
|
|
('ConfigMap', name) for name in ('actcore-runtime-config','actcore-external-activity-definitions','actcore-report-schemas','actcore-ops-service-inventory')
|
|
} | {('Service',name) for name in ('actcore-api','actcore-worker-metrics')} | {('Deployment',name) for name in DEPLOYMENTS}
|
|
|
|
|
|
def http_health(url):
|
|
with urllib.request.urlopen(url, timeout=20) as response:
|
|
if response.status != 200:
|
|
raise ValueError('invariant service unavailable')
|
|
body = response.read(16385)
|
|
if len(body) > 16384:
|
|
raise ValueError('invariant response exceeds fixed bound')
|
|
return json.loads(body)
|
|
|
|
|
|
def invariant_matches(value, now):
|
|
if (not isinstance(value, dict) or value.get('schema_version') != 'activity-core-release-health/v1'
|
|
or value.get('healthy') is not True or value.get('definitions_and_report_sinks_match') is not True
|
|
or value.get('schedules_match') is not True or value.get('definition_count') != 3
|
|
or value.get('schedule_count') != 3 or 'error' in value):
|
|
return False
|
|
at = datetime.fromisoformat(value['observed_at'].replace('Z', '+00:00'))
|
|
# Same estate observer should use synchronized clocks; unknown/future time
|
|
# is a failed sample, never a manufactured healthy interval.
|
|
return at.tzinfo is not None and 0 <= (now-at).total_seconds() <= 90
|
|
|
|
|
|
class Probe:
|
|
"""Only fixed GETs, a cluster-pinned service origin and public boolean health."""
|
|
def __init__(self, runner=run, reader=http_health):
|
|
self.runner, self.reader = runner, reader
|
|
|
|
def get(self, *args):
|
|
return json.loads(self.runner(['kubectl','--request-timeout=20s',*args,'-o','json'],timeout=25))
|
|
|
|
def __call__(self):
|
|
nodes = self.get('get','nodes')
|
|
if '92.205.62.239' not in [a['address'] for n in nodes['items'] for a in n['status']['addresses']]:
|
|
raise ValueError('estate identity mismatch')
|
|
if self.get('get','namespace','kube-system')['metadata']['uid'] != CLUSTER_UID:
|
|
raise ValueError('cluster UID mismatch')
|
|
app = self.get('-n','argocd','get','application','activity-core')
|
|
target = revision(app['spec']['source']['targetRevision'])
|
|
update_child(yaml.safe_dump(app),target,target)
|
|
status = app.get('status',{})
|
|
healthy = (not app.get('operation') and status.get('sync',{}).get('status') == 'Synced'
|
|
and status.get('sync',{}).get('revision') == target
|
|
and status.get('health',{}).get('status') == 'Healthy' and not status.get('conditions'))
|
|
resources = status.get('resources',[])
|
|
healthy &= (len(resources) == 9 and {(r['kind'],r['name']) for r in resources} == RESOURCE_SET
|
|
and all(r.get('namespace') == 'activity-core' and r.get('status') == 'Synced'
|
|
and (r.get('group') or '') == ('apps' if r['kind']=='Deployment' else '') for r in resources))
|
|
for name in sorted(DEPLOYMENTS):
|
|
deployment = self.get('-n','activity-core','get','deployment',name)
|
|
desired = deployment['spec'].get('replicas',1);ds=deployment.get('status',{})
|
|
healthy &= (type(desired) is int and desired>0 and ds.get('observedGeneration') == deployment['metadata']['generation']
|
|
and all(ds.get(k,0) == desired for k in ('readyReplicas','updatedReplicas','availableReplicas')))
|
|
service = self.get('-n','activity-core','get','service','actcore-api')
|
|
address = ipaddress.ip_address(service['spec']['clusterIP'])
|
|
if not any(p.get('port') == 8010 for p in service['spec'].get('ports',[])):
|
|
raise ValueError('fixed invariant API port unavailable')
|
|
host = f'[{address}]' if address.version == 6 else str(address)
|
|
value = self.reader(f'http://{host}:8010/release/health')
|
|
healthy &= invariant_matches(value,datetime.now(timezone.utc))
|
|
return target,bool(healthy)
|
|
|
|
|
|
class Sampler:
|
|
def __init__(self, database, probe, *, anchor_revision=BASELINE):
|
|
self.observer=HealthObserver(database,max_gap_seconds=90)
|
|
self.probe=probe;self.anchor=revision(anchor_revision)
|
|
|
|
def sample(self,now=None):
|
|
explicit_now=now
|
|
try:
|
|
target,healthy=self.probe()
|
|
revision(target)
|
|
if type(healthy) is not bool:raise ValueError('explicit health required')
|
|
except Exception:
|
|
# Use the last durably seen identity only for a failed sample. No
|
|
# raw error, fabricated timestamp or successful identity is emitted.
|
|
with self.observer.connect() as db:
|
|
row=db.execute('SELECT revision FROM observations ORDER BY rowid DESC LIMIT 1').fetchone()
|
|
target=row[0] if row else self.anchor;healthy=False
|
|
# Measure after the probe, not before a slow/failed network operation.
|
|
measured=explicit_now if explicit_now is not None else datetime.now(timezone.utc)
|
|
since=self.observer.observe(target,healthy,measured)
|
|
try:attestation=self.observer.attestation(target,measured)
|
|
except ValueError:attestation=None
|
|
return {'schema_version':'activity-core-release-sample/v1','revision':target,
|
|
'observed_at':measured.isoformat(),'healthy':healthy,'healthy_since':since,
|
|
'continuous_24h_ready':attestation is not None,'attestation':attestation,
|
|
'native_mutation':False}
|
|
|
|
|
|
def main():
|
|
parser=argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument('--database',type=Path,required=True)
|
|
parser.add_argument('--receipt',type=Path,required=True)
|
|
parser.add_argument('--watch',action='store_true',help='read-only 60s sampler; requires an owner-deployed process')
|
|
args=parser.parse_args()
|
|
sampler=Sampler(args.database,Probe())
|
|
while True:
|
|
try:result=sampler.sample()
|
|
except Exception:result={'schema_version':'activity-core-release-sample/v1','healthy':False,'continuous_24h_ready':False,'error':'durable observation unavailable','native_mutation':False}
|
|
args.receipt.write_text(json.dumps(result,sort_keys=True)+'\n')
|
|
if not args.watch:
|
|
print(json.dumps(result,sort_keys=True));return 0 if result.get('healthy') else 1
|
|
time.sleep(60)
|
|
|
|
|
|
if __name__=='__main__':raise SystemExit(main())
|