#!/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())