KUBECTL_ROUTE (agent default, founder-sudo = ssh railiance01 sudo -n) for the bounded proof scripts; one attended script with go/no-go gates, typed ADOPT confirmation, prune/force-off SSA sync and post-checks; evaluator and tests. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Assistant: claude-code Assistant-Model: opus Assistant-Process: 527080@bnt-lap001 Assistant-Session: 5a0e864a-67be-4605-a77a-16635f7dd292
245 lines
17 KiB
Python
245 lines
17 KiB
Python
#!/usr/bin/env python3
|
|
"""Read-only manual workload adoption proof; never enables automation or runs hooks."""
|
|
from datetime import datetime, timezone
|
|
import argparse
|
|
import copy
|
|
import hashlib
|
|
import json
|
|
from pathlib import Path
|
|
import re
|
|
import subprocess
|
|
from urllib.request import urlopen
|
|
|
|
from argocd_adoption_proof import _kube_route, checked_run, identity, timestamp, tracking_settings
|
|
|
|
PIN = 'a3a8a27a8cda32ae3c7b55353ee16a131c50a0a0'
|
|
CONTRACT = {
|
|
'estate_ip': '92.205.62.239', 'application': 'target-revenue',
|
|
'project': 'railiance-tenants', 'destination': {'server': 'https://kubernetes.default.svc', 'namespace': 'target-revenue'},
|
|
'repo_url': 'https://forgejo.coulomb.social/coulomb/target-revenue.git', 'source_path': 'k8s/railiance',
|
|
'revision': PIN, 'tracking_method': 'annotation', 'controller_version': 'v3.5.3',
|
|
'minimum_proving_seconds': 86400, 'maximum_observation_age_seconds': 300,
|
|
'health_url': 'https://revenue.coulomb.social/healthz',
|
|
'resources': [{'group': group, 'kind': kind, 'namespace': 'target-revenue', 'name': name}
|
|
for group, kind, name in [('', 'Service', 'target-revenue'), ('apps', 'Deployment', 'target-revenue'),
|
|
('external-secrets.io', 'ExternalSecret', 'target-revenue-runtime'),
|
|
('networking.k8s.io', 'Ingress', 'target-revenue'), ('postgresql.cnpg.io', 'Cluster', 'target-revenue-pg')]],
|
|
'hooks': ['target-revenue-bootstrap-binky', 'target-revenue-migrate'],
|
|
'ready_checks': [{'kind': kind, 'namespace': 'target-revenue', 'name': name, 'condition': condition}
|
|
for kind, name, condition in [('Deployment', 'target-revenue', 'Available'),
|
|
('ExternalSecret', 'target-revenue-runtime', 'Ready'), ('Cluster', 'target-revenue-pg', 'Ready')]],
|
|
}
|
|
|
|
|
|
def validate_contract(contract):
|
|
if (not re.fullmatch(r'[a-f0-9]{40}', contract['revision']) or contract['minimum_proving_seconds'] < 86400
|
|
or not 0 < contract['maximum_observation_age_seconds'] <= 300):
|
|
raise ValueError('invalid proof bounds')
|
|
if any(r['kind'] not in {'Service', 'Deployment', 'ExternalSecret', 'Ingress', 'Cluster'} for r in contract['resources']):
|
|
raise ValueError('resource kinds not allowlisted')
|
|
if len({identity(r) for r in contract['resources']}) != len(contract['resources']):
|
|
raise ValueError('duplicate resource identities')
|
|
for r in [*contract['resources'], *contract['ready_checks']]:
|
|
if r['kind'] not in {'Service', 'Deployment', 'ExternalSecret', 'Ingress', 'Cluster'}:
|
|
raise ValueError('readiness kind not allowlisted')
|
|
if any(not re.fullmatch(r'[a-z0-9][a-z0-9.-]*', r[k]) for k in ('name', 'namespace')):
|
|
raise ValueError('invalid resource identity')
|
|
|
|
|
|
def normalize(items, contract, settings):
|
|
validate_contract(contract)
|
|
if (settings['method'] != 'annotation' or settings['controller_version'] != contract['controller_version']
|
|
or settings['installation_id_absent'] is not True):
|
|
raise ValueError('tracking semantics unverified')
|
|
resources, hooks = [], []
|
|
for raw in items:
|
|
obj = copy.deepcopy(raw)
|
|
meta = obj['metadata']
|
|
annotation = meta.get('annotations', {})
|
|
if 'argocd.argoproj.io/hook' in annotation:
|
|
if obj['kind'] != 'Job' or annotation['argocd.argoproj.io/hook'] != 'Sync':
|
|
raise ValueError('unsupported hook')
|
|
hooks.append(meta['name'])
|
|
continue
|
|
group = obj['apiVersion'].split('/')[0] if '/' in obj['apiVersion'] else ''
|
|
ident = (group, obj['kind'], meta.get('namespace', ''), meta['name'])
|
|
if ident not in {identity(r) for r in contract['resources']}:
|
|
raise ValueError('render resource not admitted')
|
|
tracking = f"{contract['application']}:{group}/{obj['kind']}:{meta['namespace']}/{meta['name']}"
|
|
annotations = meta.setdefault('annotations', {})
|
|
if annotations.get('argocd.argoproj.io/tracking-id', tracking) != tracking:
|
|
raise ValueError('conflicting source tracking annotation')
|
|
annotations['argocd.argoproj.io/tracking-id'] = tracking
|
|
resources.append(obj)
|
|
if (len(resources) != len(contract['resources']) or len({(r['apiVersion'], r['kind'], r['metadata']['name']) for r in resources}) != len(resources)
|
|
or sorted(hooks) != sorted(contract['hooks'])):
|
|
raise ValueError('render resource or hook set differs')
|
|
return resources
|
|
|
|
|
|
def decode_objects(raw):
|
|
text = raw.decode()
|
|
decoder = json.JSONDecoder()
|
|
items = []
|
|
while text.strip():
|
|
value, end = decoder.raw_decode(text.lstrip())
|
|
items.extend(value["items"] if value.get("kind") == "List" else [value])
|
|
text = text.lstrip()[end:]
|
|
return items
|
|
|
|
|
|
def collect(rendered, contract):
|
|
validate_contract(contract)
|
|
def get(args):
|
|
return json.loads(checked_run(['kubectl', *args, '-o', 'json']))
|
|
nodes = get(['get', 'nodes'])
|
|
if contract['estate_ip'] not in [a['address'] for n in nodes['items'] for a in n['status']['addresses']]:
|
|
raise ValueError('estate identity unverified')
|
|
settings = tracking_settings(contract)
|
|
parsed = decode_objects(checked_run(['kubectl', 'create', '--dry-run=client', '--validate=false', '-f', '-', '-o', 'json'], input=rendered))
|
|
desired = normalize(parsed, contract, settings)
|
|
payload = json.dumps({'apiVersion': 'v1', 'kind': 'List', 'items': desired}, sort_keys=True).encode()
|
|
diff = subprocess.run(['kubectl', 'diff', '-f', '-'], input=payload, capture_output=True, timeout=120)
|
|
if diff.returncode not in (0, 1):
|
|
raise ValueError('native client diff failed; protected output suppressed')
|
|
obj = get(['-n', 'argocd', 'get', 'application', contract['application']])
|
|
spec, status = obj['spec'], obj.get('status', {})
|
|
op = status.get('operationState', {})
|
|
sync = op.get('operation', {}).get('sync', {})
|
|
checks = []
|
|
for check in contract['ready_checks']:
|
|
r = get(['-n', check['namespace'], 'get', check['kind'], check['name']])
|
|
ready = [{k: c.get(k) for k in ('status', 'lastTransitionTime')} for c in r.get('status', {}).get('conditions', []) if c.get('type') == check['condition']]
|
|
group = r['apiVersion'].split('/')[0] if '/' in r['apiVersion'] else ''
|
|
tracking = f"{contract['application']}:{group}/{r['kind']}:{check['namespace']}/{check['name']}"
|
|
checks.append({**check, 'uid': r['metadata']['uid'], 'ready': ready,
|
|
'tracking_matches': r['metadata'].get('annotations', {}).get('argocd.argoproj.io/tracking-id') == tracking,
|
|
'observed_generation': r.get('status', {}).get('observedGeneration'), 'generation': r['metadata'].get('generation'),
|
|
'replicas': r.get('status', {}).get('replicas'), 'available_replicas': r.get('status', {}).get('availableReplicas'),
|
|
'instances': r.get('status', {}).get('instances'), 'ready_instances': r.get('status', {}).get('readyInstances')})
|
|
jobs = get(['-n', contract['destination']['namespace'], 'get', 'jobs'])
|
|
live_hooks = sorted(r['metadata']['name'] for r in jobs['items'] if r['metadata']['name'] in contract['hooks'])
|
|
return {'captured_at': datetime.now(timezone.utc).isoformat(), 'estate_ip': contract['estate_ip'], 'tracking_settings': settings,
|
|
'application': {'name': obj['metadata']['name'], 'uid': obj['metadata']['uid'], 'project': spec.get('project'),
|
|
'source': {k: spec.get('source', {}).get(k) for k in ('repoURL', 'path', 'targetRevision')}, 'destination': spec.get('destination'),
|
|
'automated_present': 'automated' in spec.get('syncPolicy', {}), 'pending_operation': bool(obj.get('operation')),
|
|
'sync': status.get('sync', {}).get('status'), 'health': status.get('health', {}).get('status'), 'revision': status.get('sync', {}).get('revision'),
|
|
'condition_types': [c.get('type') for c in status.get('conditions', [])],
|
|
'operation': {k: op.get(k) for k in ('phase', 'startedAt', 'finishedAt')},
|
|
'sync_prune': sync.get('prune', False), 'sync_strategy_apply': 'apply' in sync.get('syncStrategy', {}),
|
|
'sync_strategy_force': sync.get('syncStrategy', {}).get('apply', {}).get('force', False), 'operation_revision': sync.get('revision'),
|
|
'resources': [{k: r.get(k) for k in ('group', 'kind', 'namespace', 'name', 'status')} for r in status.get('resources', [])]},
|
|
'ready_checks': checks, 'live_hooks': live_hooks,
|
|
'pinned_render_diff': {'revision': contract['revision'], 'path': contract['source_path'], 'client_side_exit_code': diff.returncode,
|
|
'render_sha256': hashlib.sha256(rendered).hexdigest(), 'normalized_render_sha256': hashlib.sha256(payload).hexdigest(),
|
|
'render_resource_count': len(desired), 'skipped_hooks': sorted(contract['hooks']), 'output_suppressed': True}}
|
|
|
|
|
|
def verify(observation, contract, now=None):
|
|
validate_contract(contract)
|
|
now = now or datetime.now(timezone.utc)
|
|
captured = timestamp(observation['captured_at'])
|
|
if not 0 <= (now - captured).total_seconds() <= contract['maximum_observation_age_seconds']:
|
|
raise ValueError('observation stale or future')
|
|
if not -5 <= (captured - timestamp(observation['metadata_captured_at'])).total_seconds() <= contract['maximum_observation_age_seconds']:
|
|
raise ValueError('metadata observation stale or clock skew exceeds bound')
|
|
settings = observation['tracking_settings']
|
|
if (observation['estate_ip'] != contract['estate_ip'] or settings.get('method') != contract['tracking_method']
|
|
or settings.get('controller_version') != contract['controller_version'] or settings.get('installation_id_absent') is not True):
|
|
raise ValueError('estate or tracking semantics unverified')
|
|
app = observation['application']
|
|
if (app['name'] != contract['application'] or not app['uid'] or app['project'] != contract['project'] or app['destination'] != contract['destination']
|
|
or app['source'] != {'repoURL': contract['repo_url'], 'path': contract['source_path'], 'targetRevision': contract['revision']}
|
|
or app['revision'] != contract['revision']):
|
|
raise ValueError('source or application identity mismatch')
|
|
if (app['automated_present'] is not False or app['pending_operation'] is not False or app['sync'] != 'Synced' or app['health'] != 'Healthy'
|
|
or any(c != 'OrphanedResourceWarning' for c in app['condition_types'])):
|
|
raise ValueError('manual idle healthy adoption unverified')
|
|
op = app['operation']
|
|
if (op['phase'] != 'Succeeded' or app['sync_prune'] is not False or app['sync_strategy_apply'] is not True or app['sync_strategy_force'] is not False or app['operation_revision'] != contract['revision']):
|
|
raise ValueError('pinned manual apply strategy unverified')
|
|
start, finish = timestamp(op['startedAt']), timestamp(op['finishedAt'])
|
|
proof_age = (captured - finish).total_seconds()
|
|
if finish < start or proof_age < contract['minimum_proving_seconds']:
|
|
raise ValueError('completed 24h proving period unverified')
|
|
expected = {identity(r) for r in contract['resources']}
|
|
if (len(app['resources']) != len(expected) or {identity(r) for r in app['resources']} != expected or any(r['status'] != 'Synced' for r in app['resources'])):
|
|
raise ValueError('exact synced resource set unverified')
|
|
checks = observation['ready_checks']
|
|
expected_checks = {(c['kind'], c['namespace'], c['name'], c['condition']) for c in contract['ready_checks']}
|
|
if len(checks) != len(expected_checks) or {(c['kind'], c['namespace'], c['name'], c['condition']) for c in checks} != expected_checks:
|
|
raise ValueError('readiness checks incomplete')
|
|
for c in checks:
|
|
if not c['uid'] or c['tracking_matches'] is not True or len(c['ready']) != 1 or c['ready'][0]['status'] != 'True':
|
|
raise ValueError('tracked resource readiness unverified')
|
|
if timestamp(c['ready'][0]['lastTransitionTime']) > start:
|
|
raise ValueError('readiness changed during proving period')
|
|
if c['kind'] == 'Deployment' and (c['observed_generation'] != c['generation'] or not c['replicas'] or c['available_replicas'] != c['replicas']):
|
|
raise ValueError('deployment availability incomplete')
|
|
if c['kind'] == 'Cluster' and (not c['instances'] or c['ready_instances'] != c['instances']):
|
|
raise ValueError('CNPG readiness incomplete')
|
|
diff = observation['pinned_render_diff']
|
|
if (diff.get('revision') != contract['revision'] or diff.get('path') != contract['source_path'] or type(diff.get('client_side_exit_code')) is not int
|
|
or diff['client_side_exit_code'] != 0 or diff.get('render_resource_count') != len(expected)
|
|
or diff.get('skipped_hooks') != sorted(contract['hooks']) or diff.get('output_suppressed') is not True
|
|
or any(not re.fullmatch(r'[a-f0-9]{64}', diff.get(k, '')) for k in ('render_sha256', 'normalized_render_sha256'))):
|
|
raise ValueError('pinned normalized client zero-diff unverified')
|
|
if observation.get('live_hooks') != [] or observation.get('public_health') != {'url': contract['health_url'], 'status': 200}:
|
|
raise ValueError('hook absence or public health unverified')
|
|
return {'schema_version': 'argocd-workload-adoption-proof/v1', 'status': 'verified', 'application': contract['application'],
|
|
'revision': contract['revision'], 'captured_at': observation['captured_at'], 'verified_at': now.isoformat(),
|
|
'proving_seconds': proof_age, 'resource_count': len(expected), 'ready_count': len(checks),
|
|
'manual_apply_prune_off': True, 'hooks_skipped': sorted(contract['hooks']), 'hooks_absent': True,
|
|
'child_automation_enabled': False, 'public_health_status': 200, 'native_mutation': False,
|
|
'verifier_sha256': hashlib.sha256(Path(__file__).read_bytes()).hexdigest(),
|
|
'tracking_helper_sha256': hashlib.sha256(Path(__file__).with_name('argocd_adoption_proof.py').read_bytes()).hexdigest(),
|
|
'contract_sha256': hashlib.sha256(json.dumps(contract, sort_keys=True).encode()).hexdigest(),
|
|
'observation_sha256': hashlib.sha256(json.dumps(observation, sort_keys=True).encode()).hexdigest()}
|
|
|
|
|
|
def remote_program(common, rendered):
|
|
import base64
|
|
return ('import sys,types,base64,json\n' + 'm=types.ModuleType("argocd_adoption_proof");sys.modules[m.__name__]=m\n'
|
|
+ 'exec(compile(base64.b64decode(' + repr(base64.b64encode(common).decode()) + '),"argocd_adoption_proof.py","exec"),m.__dict__)\n'
|
|
+ 'n={"__name__":"workload_proof"};exec(compile(base64.b64decode(' + repr(base64.b64encode(Path(__file__).read_bytes()).decode())
|
|
+ '),"workload_adoption_proof.py","exec"),n)\n'
|
|
+ 'try:\n print(json.dumps(n["collect"](base64.b64decode(' + repr(base64.b64encode(rendered).decode()) + '),n["CONTRACT"]),sort_keys=True))\n'
|
|
+ 'except Exception:\n raise SystemExit("adoption observation incomplete; protected output suppressed")\n').encode()
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument('--live', action='store_true')
|
|
parser.add_argument('--source-root', type=Path)
|
|
parser.add_argument('--observation', type=Path, required=True)
|
|
parser.add_argument('--receipt', type=Path, required=True)
|
|
args = parser.parse_args()
|
|
try:
|
|
if args.live:
|
|
if not args.source_root:
|
|
raise ValueError('source root required')
|
|
import tempfile
|
|
with tempfile.TemporaryDirectory(prefix='workload-adoption-pin-') as temp:
|
|
archive = checked_run(['git', '-C', str(args.source_root), 'archive', CONTRACT['revision'], CONTRACT['source_path']])
|
|
checked_run(['tar', '-x', '-C', temp], input=archive)
|
|
rendered = checked_run(['kubectl', 'kustomize', str(Path(temp) / CONTRACT['source_path'])])
|
|
common = Path(__file__).with_name('argocd_adoption_proof.py').read_bytes()
|
|
observation = json.loads(checked_run(_kube_route().ssh_argv(['python3', '-']), input=remote_program(common, rendered), timeout=180))
|
|
with urlopen(CONTRACT['health_url'], timeout=30) as response:
|
|
observation['public_health'] = {'url': response.geturl(), 'status': response.status}
|
|
observation['metadata_captured_at'] = observation['captured_at']
|
|
observation['captured_at'] = datetime.now(timezone.utc).isoformat()
|
|
args.observation.write_text(json.dumps(observation, indent=2) + '\n')
|
|
else:
|
|
observation = json.loads(args.observation.read_text())
|
|
receipt = verify(observation, CONTRACT)
|
|
except Exception:
|
|
receipt = {'schema_version': 'argocd-workload-adoption-proof/v1', 'status': 'unverified',
|
|
'error': 'adoption proof incomplete; protected output suppressed', 'native_mutation': False}
|
|
args.receipt.write_text(json.dumps(receipt, indent=2) + '\n')
|
|
print(json.dumps(receipt, sort_keys=True))
|
|
return 0 if receipt['status'] == 'verified' else 1
|
|
|
|
|
|
if __name__ == '__main__':
|
|
raise SystemExit(main())
|