121 lines
10 KiB
Python
121 lines
10 KiB
Python
"""Stdin-only public metadata and exact server-dry-run; never persist raw output.
|
|
Local coordinator supplies frozen JSON manifests. No SSH credential inspection.
|
|
"""
|
|
import datetime, hashlib, json, os, subprocess
|
|
|
|
def digest(x):
|
|
return hashlib.sha256(json.dumps(x,sort_keys=True,separators=(',',':')).encode()).hexdigest()
|
|
|
|
def invoke(argv, stdin=None):
|
|
try:
|
|
r=subprocess.run(argv,input=stdin,capture_output=True,text=True,timeout=35,env={**os.environ,'KUBECONFIG':'/etc/rancher/k3s/k3s.yaml'})
|
|
return r.returncode,r.stdout,bool(r.stderr)
|
|
except (OSError,subprocess.TimeoutExpired):
|
|
return -1,'',True
|
|
|
|
def query(resource, fields, namespace=None, name=None, allns=False):
|
|
argv=['kubectl','get',resource]
|
|
if namespace: argv+=['-n',namespace]
|
|
if name: argv+=[name]
|
|
if allns: argv+=['-A']
|
|
template=('{range .items[*]}' if not name else '')+'{"\\u001e"}'.join('{.'+f+'}' for f in fields)+'{"\\n"}'+('{end}' if not name else '')
|
|
rc,out,err=invoke(argv+['-o','jsonpath='+template])
|
|
rows=[]
|
|
if rc==0:
|
|
for line in out.split('\n'):
|
|
if not line: continue
|
|
parts=line.split('\x1e')
|
|
if len(parts)!=len(fields):return {'ok':False,'failure':'metadata-shape','stderr_present':err}
|
|
row={}
|
|
for key,value in zip(fields,parts):
|
|
try: row[key]=json.loads(value) if value else None
|
|
except ValueError: row[key]=value
|
|
rows.append(row)
|
|
return {'ok':rc==0,'exit_code':rc,'stderr_present':err,'rows':rows}
|
|
|
|
def require(condition,message):
|
|
if not condition:raise ValueError(message)
|
|
|
|
# Exact AST source extracted from pinned Platform module SHA256
|
|
# 4fb9ce6608ad04c4f5dc98a95796dc88a08a2453dc15c0d40c5c688fc02f573f
|
|
def deployment_projection(deployment):
|
|
spec=deployment['spec']['template']['spec'];containers=spec.get('containers',[])
|
|
require(type(containers) is list and len(containers)==1 and not spec.get('initContainers'),'single fixed signer container required')
|
|
item=containers[0]
|
|
# Never hash arbitrary env values or an entire Secret/spec response.
|
|
return {'service_account':spec.get('serviceAccountName'),'image':item.get('image'),
|
|
'args':item.get('args'),'volume_mounts':item.get('volumeMounts',[]),
|
|
'volumes':spec.get('volumes',[]),'security_context':spec.get('securityContext')}
|
|
|
|
|
|
def run(payload):
|
|
receipt={'schema':'netkingdom.native-public-preflight.v1','observed_at':datetime.datetime.now(datetime.timezone.utc).isoformat(),'source_pins':payload['source_pins'],'persistent_mutations':False,'held_fence':False,'observations':{}}
|
|
obs=receipt['observations']
|
|
obs['nodes']=query('nodes',['metadata.name','metadata.uid','status.addresses','status.allocatable','status.conditions[*].type','status.conditions[*].status','status.conditions[*].reason'])
|
|
obs['cluster']=query('namespace',['metadata.name','metadata.uid'],name='kube-system')
|
|
nodes=obs['nodes'].get('rows',[]);cluster=obs['cluster'].get('rows',[])
|
|
valid_node=any(type(row.get('status.addresses')) is list and any(type(address) is dict and address.get('address')=='92.205.62.239' and address.get('type') in {'InternalIP','ExternalIP'} for address in row['status.addresses']) for row in nodes if type(row) is dict)
|
|
if not obs['nodes'].get('ok') or not obs['cluster'].get('ok') or not valid_node or len(cluster)!=1 or type(cluster[0]) is not dict or cluster[0].get('metadata.uid')!='a553c742-0115-43d4-99a4-a5ca56fe0786':
|
|
receipt['refused']='target-identity';return receipt
|
|
# conditions contain only public node health, no annotations/secret/env/config.
|
|
common=['metadata.name','metadata.namespace','metadata.uid','metadata.generation','metadata.ownerReferences','metadata.deletionTimestamp']
|
|
obs['deployment']=query('deployment',common+['spec.replicas','spec.strategy','status.observedGeneration','status.replicas','status.readyReplicas','spec.template.spec.serviceAccountName','spec.template.spec.containers[*].image','spec.template.spec.containers[*].resources'],namespace='flex-auth',name='flex-auth-secrets-engine')
|
|
obs['replicasets']=query('replicasets',common+['spec.replicas','status.replicas'],namespace='flex-auth')
|
|
obs['pods']=query('pods',common+['status.phase','spec.nodeName','spec.serviceAccountName','spec.volumes[*].secret.secretName','spec.volumes[*].persistentVolumeClaim.claimName','spec.containers[*].image'],namespace='flex-auth')
|
|
obs['hpa']=query('hpa',common+['spec.scaleTargetRef','spec.minReplicas','spec.maxReplicas'],namespace='flex-auth')
|
|
# Only identity/owner/SA/volume references from potential workload writers.
|
|
for resource in ['jobs','cronjobs','statefulsets','daemonsets']:
|
|
obs[resource]=query(resource,common,namespace='flex-auth')
|
|
obs['quota']=query('resourcequota',['metadata.name','spec.hard','status.hard','status.used'],namespace='flex-auth')
|
|
obs['limits']=query('limitrange',['metadata.name','spec.limits'],namespace='flex-auth')
|
|
obs['pvc']=query('pvc',common+['spec.storageClassName','spec.accessModes','spec.resources.requests.storage','spec.volumeName','status.phase','status.capacity'],namespace='flex-auth')
|
|
obs['storageclass']=query('storageclass',['metadata.name','metadata.uid','provisioner','reclaimPolicy','volumeBindingMode','allowVolumeExpansion'],name='local-path')
|
|
obs['applications']=query('applications.argoproj.io',common+['spec.destination','spec.syncPolicy.automated','status.operationState.phase','status.sync.status','status.resources[*].kind','status.resources[*].namespace','status.resources[*].name'],namespace='argocd')
|
|
# Metrics and allocation JSONPath select only resource fields of all pods.
|
|
obs['resource_requests']=query('pods',['metadata.namespace','metadata.name','spec.nodeName','status.phase','spec.containers[*].resources','spec.initContainers[*].resources','spec.overhead'],allns=True)
|
|
rc,out,err=invoke(['kubectl','top','nodes','--no-headers']);obs['node_usage']={'ok':rc==0,'exit_code':rc,'stderr_present':err,'public_metrics':out.strip() if rc==0 else None}
|
|
rc,out,err=invoke(['df','-B1','--output=size,used,avail,pcent,target','/var/lib/rancher/k3s/storage']);obs['storage_free']={'ok':rc==0,'exit_code':rc,'stderr_present':err,'public_filesystem_metrics':out.strip() if rc==0 else None}
|
|
# helm history returns release metadata, no values/manifests/Secret reads by wrapper.
|
|
rc,out,err=invoke(['helm','history','flex-auth-secrets-engine','-n','flex-auth','--output','json','--max','10'])
|
|
history={'ok':rc==0,'exit_code':rc,'stderr_present':err}
|
|
try: history['revisions']=[{k:x.get(k) for k in ('revision','updated','status','chart','app_version')} for x in json.loads(out)] if rc==0 else []
|
|
except (ValueError,TypeError):history={'ok':False,'failure':'history-shape'}
|
|
obs['helm_history']=history
|
|
# Sanitized client comparison: no arbitrary live args or projection fields emitted.
|
|
mapping={'service_account':'spec.template.spec.serviceAccountName','image':'spec.template.spec.containers[0].image','args':'spec.template.spec.containers[0].args','volume_mounts':'spec.template.spec.containers[0].volumeMounts','volumes':'spec.template.spec.volumes','security_context':'spec.template.spec.securityContext'}
|
|
live=query('deployment',list(mapping.values()),namespace='flex-auth',name='flex-auth-secrets-engine')
|
|
if live['ok'] and len(live['rows'])==1:
|
|
current={k:live['rows'][0][path] for k,path in mapping.items()}
|
|
for key in ['volume_mounts','volumes']:current[key]=current[key] or []
|
|
obs['client_projection_diff']={'ok':True,'live_sha256':digest(current),'candidate_sha256':digest(payload['expected_projection']),'differing_fields':[k for k in current if current[k]!=payload['expected_projection'][k]],'field_digests':{k:{'live':digest(current[k]),'candidate':digest(payload['expected_projection'][k])} for k in current}}
|
|
else:obs['client_projection_diff']={'ok':False}
|
|
receipt['dry_runs']=[]
|
|
for obj in payload['resources']:
|
|
kind=obj['kind'];name=obj['metadata']['name']
|
|
if kind not in {'ServiceAccount','Role','RoleBinding','ClusterRole','ClusterRoleBinding','Service','Deployment','NetworkPolicy','PersistentVolumeClaim'} or obj['metadata'].get('namespace','flex-auth')!='flex-auth':
|
|
receipt['refused']='resource-allowlist';return receipt
|
|
fields=list(mapping.values())+['spec.replicas']
|
|
selected='jsonpath='+('{"\\u001e"}'.join('{.'+f+'}' for f in fields)) if kind=='Deployment' else 'name'
|
|
rc,out,err=invoke(['kubectl','apply','--dry-run=server','--validate=strict','-n','flex-auth','-f','-','-o',selected],json.dumps(obj))
|
|
row={'kind':kind,'name':name,'exit_code':rc,'stderr_present':err,'admitted':rc==0}
|
|
if rc==0:
|
|
try:
|
|
if kind=='Deployment':
|
|
raw=out.strip().split('\x1e')
|
|
if len(raw)!=len(fields):raise ValueError('projection-shape')
|
|
values=[]
|
|
for value in raw:
|
|
try:values.append(json.loads(value) if value else None)
|
|
except ValueError:values.append(value)
|
|
p=dict(zip(mapping,values[:-1]))
|
|
row.update(replicas=values[-1],projection_sha256=digest(p),expected_projection_sha256=digest(payload['expected_projection']),projection_matches=p==payload['expected_projection'],differing_projection_fields=[k for k in p if p[k]!=payload['expected_projection'].get(k)])
|
|
except (ValueError,TypeError,KeyError):row.update(admitted=False,failure='admission-shape')
|
|
receipt['dry_runs'].append(row)
|
|
receipt['completed_at']=datetime.datetime.now(datetime.timezone.utc).isoformat()
|
|
receipt['limitations']=['Dated metadata and dry-run only; no held writer fence or native owner disposition.','Helm values/manifests/config/env/annotations/Secrets not fetched. Live args only compared and hashed.','Application workload refs and current writer hints do not authenticate manual/CI writers.','No actual PVC binding/PV Retain, stop/create/start/custody or token request.','kubectl apply dry-run is admission evidence, not Helm upgrade execution equivalence.']
|
|
return receipt
|
|
|
|
if __name__=='__main__':
|
|
try: result=run(PAYLOAD)
|
|
except Exception: result={'schema':'netkingdom.native-public-preflight.v1','refused':'sanitized-wrapper-failure','persistent_mutations':False,'held_fence':False}
|
|
print(json.dumps(result,sort_keys=True))
|