#!/usr/bin/env bash # End-to-end acceptance for the private observability access and alerting # cutover. No-argument mode is intentionally a no-contact plan printer. set -Eeuo pipefail set +x umask 077 readonly EXPECTED_CONTEXT='default' readonly EXPECTED_API_SERVER='https://127.0.0.1:6443' readonly EXPECTED_NODE='donghyeon-system-product-name' readonly NAMESPACE='observability' readonly GRAFANA_HOST='grafana.learn.hyeonworks.com' readonly GITEA_HOST='git.learn.hyeonworks.com' readonly KEYCLOAK_HOST='id.learn.hyeonworks.com' readonly LAN_IP='192.168.0.107' readonly TAILSCALE_IP='100.92.240.34' readonly PROMETHEUS_SERVICE='observability-core-kube-pr-prometheus' readonly ALERTMANAGER_SERVICE='observability-core-kube-pr-alertmanager' readonly RUNBOOK_URL='https://git.learn.hyeonworks.com/donghyeon.kang/project-infra/src/branch/main/docs/runbooks/2026-07-31-observability-access-cutover.md' readonly BUSYBOX_IMAGE='docker.io/library/busybox:1.37.0@sha256:7a3ebe5bfd1a4a19797d20b0c0bb39d44393e9a03fd852c0865b0f540d868df0' readonly KUBECTL_DEFAULT='/usr/local/bin/kubectl' readonly CURL_DEFAULT='/usr/bin/curl' readonly DIG_DEFAULT='/usr/bin/dig' readonly OPENSSL_DEFAULT='/usr/bin/openssl' readonly NC_DEFAULT='/usr/bin/nc.openbsd' readonly SUDO_DEFAULT='/usr/bin/sudo' readonly DATE_DEFAULT='/usr/bin/date' readonly CERTBOT_BIN='/snap/bin/certbot' readonly CERTBOT_REAL='/usr/bin/snap' readonly SYSTEMCTL_BIN='/usr/bin/systemctl' readonly TIMEOUT_BIN='/usr/bin/timeout' readonly JQ_BIN='/usr/bin/jq' readonly PYTHON_BIN='/usr/bin/python3.12' readonly CAT_BIN='/usr/bin/cat' readonly CHMOD_BIN='/usr/bin/chmod' readonly CMP_BIN='/usr/bin/cmp' readonly MKTEMP_BIN='/usr/bin/mktemp' readonly MV_BIN='/usr/bin/mv' readonly READLINK_BIN='/usr/bin/readlink' readonly RM_BIN='/usr/bin/rm' readonly STAT_BIN='/usr/bin/stat' readonly REQUEST_TIMEOUT='10s' readonly PROCESS_TIMEOUT='20s' readonly HUMAN_TIMEOUT_SECONDS=300 KUBECTL_BIN="$KUBECTL_DEFAULT" CURL_BIN="$CURL_DEFAULT" DIG_BIN="$DIG_DEFAULT" OPENSSL_BIN="$OPENSSL_DEFAULT" NC_BIN="$NC_DEFAULT" SUDO_BIN="$SUDO_DEFAULT" DATE_BIN="$DATE_DEFAULT" TEST_MODE=false TEST_CONFIRMATIONS='' CONFIRMATION_INDEX=0 declare -a CONFIRMATION_QUEUE=() execute_requested=false WORK='' API_PROXY_SOCKET='' API_PROXY_PID='' API_PROXY_START='' EXIT_HANDLER_RUNNING=false MANUAL_RECOVERY=false SILENCE_ID='' SILENCE_RUN='' SILENCE_POLL_ATTEMPTS=30 SILENCE_POLL_DELAY=1 MUTATION_CRITICAL_DEPTH=0 DEFERRED_SIGNAL_RC=0 EXPECTED_NODE_UID='' PUBLIC_EDGE_IP='' KEYCLOAK_TEST_ADDED=false KEYCLOAK_MEMBERSHIP_PENDING=false KEYCLOAK_MEMBERSHIP_REMOVED=false KEYCLOAK_USER_ID='' KEYCLOAK_VIEWER_GROUP_ID='' KEYCLOAK_MEMBERSHIP_BEFORE='' KEYCLOAK_AUTH_CONFIG='' declare -a OWNED_KIND=() declare -a OWNED_NAMESPACE=() declare -a OWNED_NAME=() declare -a OWNED_UID=() declare -a OWNED_RUN=() fail() { printf 'ERROR: %s\n' "$*" >&2; return 1; } emit_manual_recovery() { [[ "$MANUAL_RECOVERY" == true ]] && return 0 MANUAL_RECOVERY=true printf 'MANUAL_RECOVERY_REQUIRED=YES\n' >&2 } usage() { "$CAT_BIN" <<'USAGE' Usage: bash scripts/validate/observability-smoke.sh bash scripts/validate/observability-smoke.sh --execute No arguments prints the exact acceptance plan without external reads or mutations. Execute requires the visible `APPLY default` gate, creates only short-lived acceptance objects, restarts only Grafana/Alertmanager Pods, and requires exact human OIDC, Slack, and external-client evidence tokens. USAGE } print_dry_run() { printf '%s\n' \ 'OBSERVABILITY_SMOKE_DRY_RUN=PASS' \ 'CHECK_01=Grafana public/private DNS exactness' \ 'CHECK_02=Grafana certificate chain,hostname,one-SAN,expiry' \ 'CHECK_03=LAN,Tailscale,denied-source,public-edge HTTPS status' \ 'CHECK_04=Gitea,Grafana,Keycloak public metrics denial' \ 'CHECK_05=unknown-SNI and Traefik NodePort boundary' \ 'CHECK_06=exact Prometheus target allowlist and health' \ 'CHECK_07=Gitea,Keycloak,CNPG,AIStor,Traefik metric contracts' \ 'CHECK_08=untrusted-Pod AIStor and blackbox denial' \ 'CHECK_09=public,private,internal,TLS blackbox series' \ 'CHECK_10=Grafana datasource health and Loki-to-Tempo trace link' \ 'CHECK_11=AIStor quota and recording-rule series' \ 'CHECK_12=Grafana,Alertmanager restart persistence' \ 'CHECK_13=Certbot dry-run deploy-hook and Nginx health' \ 'HUMAN_OIDC=admin,viewer,denied,break-glass,session-revoke,relogin-denied' \ 'HUMAN_SLACK=firing,resolved' \ 'HUMAN_EXTERNAL_CLIENT=required' \ 'TEMPORARY_OBJECTS=NetworkPolicy,Pod,AlertmanagerSilence,PrometheusRule' \ 'PRESERVE=Secret,PVC,preexisting-Keycloak-membership' \ 'MUTATION=NOT_REQUESTED' } parse_arguments() { if (( $# == 0 )); then return 0; fi if (( $# == 1 )) && [[ "$1" == --execute ]]; then execute_requested=true; return 0; fi usage >&2 fail 'supported arguments are empty or exactly --execute' } reject_production_overrides() { local variable for variable in ${!PLATFORM_OBSERVABILITY_SMOKE_@}; do fail "production boundary override is forbidden: $variable" return 1 done } validate_dns_contract() { (( $# == 8 )) || return 1 [[ "$1" == '' && "$2" == '' && "$3" == "$LAN_IP" && "$4" == '' && "$5" == "$TAILSCALE_IP" && "$6" == '' && "$7" == "$LAN_IP" && "$8" == '' ]] } validate_certificate_contract() { local sans=$1 verify_rc=$2 expiry_rc=$3 [[ "$verify_rc" == 0 && "$expiry_rc" == 0 && "$sans" == "DNS:$GRAFANA_HOST" ]] } validate_certificate_extension_contract() { local extension=$1 verify_rc=$2 expiry_rc=$3 [[ "$verify_rc" == 0 && "$expiry_rc" == 0 ]] || return 1 python_bounded -I -S - "$extension" "$GRAFANA_HOST" <<'PY' import sys extension,host=sys.argv[1:] lines=[line.strip() for line in extension.splitlines() if line.strip()] if len(lines)<2 or lines[0]!='X509v3 Subject Alternative Name:': raise SystemExit(1) names=[name.strip() for name in ' '.join(lines[1:]).split(',')] if names != ['DNS:'+host]: raise SystemExit(1) PY } validate_target_contract() { local file=$1 python_bounded -I -S - "$file" <<'PY' import json,pathlib,re,sys from urllib.parse import urlsplit expected={ "podMonitor/platform-data/platform-postgres/0":1, "probe/observability/platform-private-edge":3, "probe/observability/platform-private-internal":3, "probe/observability/platform-public-edge":2, "serviceMonitor/gitea/gitea/0":1, "serviceMonitor/keycloak/keycloak/0":1, "serviceMonitor/kube-system/traefik/0":1, "serviceMonitor/object-storage/aistor-bucket-usage/0":1, "serviceMonitor/observability-agent/alloy/0":1, "serviceMonitor/observability-agent/node-exporter/0":1, "serviceMonitor/observability/blackbox-exporter/0":1, "serviceMonitor/observability/grafana/0":1, "serviceMonitor/observability/loki/0":1, "serviceMonitor/observability/observability-core-kube-pr-alertmanager/0":1, "serviceMonitor/observability/observability-core-kube-pr-alertmanager/1":1, "serviceMonitor/observability/observability-core-kube-pr-apiserver/0":1, "serviceMonitor/observability/observability-core-kube-pr-coredns/0":1, "serviceMonitor/observability/observability-core-kube-pr-kubelet/0":1, "serviceMonitor/observability/observability-core-kube-pr-kubelet/1":1, "serviceMonitor/observability/observability-core-kube-pr-kubelet/2":1, "serviceMonitor/observability/observability-core-kube-pr-operator/0":1, "serviceMonitor/observability/observability-core-kube-pr-prometheus/0":1, "serviceMonitor/observability/observability-core-kube-pr-prometheus/1":1, "serviceMonitor/observability/observability-core-kube-state-metrics/0":1, "serviceMonitor/observability/tempo/0":1, } expected_probe_targets={ "probe/observability/platform-private-edge":{ ("http://blackbox-exporter.observability.svc.cluster.local:9115/probe?module=http_private_edge_403&target=https%3A%2F%2Fdb-admin.learn.hyeonworks.com%2F","https://db-admin.learn.hyeonworks.com/","blackbox-private-edge","private-edge"), ("http://blackbox-exporter.observability.svc.cluster.local:9115/probe?module=http_private_edge_403&target=https%3A%2F%2Fgrafana.learn.hyeonworks.com%2F","https://grafana.learn.hyeonworks.com/","blackbox-private-edge","private-edge"), ("http://blackbox-exporter.observability.svc.cluster.local:9115/probe?module=http_private_edge_403&target=https%3A%2F%2Fstorage-admin.learn.hyeonworks.com%2F","https://storage-admin.learn.hyeonworks.com/","blackbox-private-edge","private-edge"), }, "probe/observability/platform-private-internal":{ ("http://blackbox-exporter.observability.svc.cluster.local:9115/probe?hostname=db-admin.learn.hyeonworks.com&module=http_private_internal_200&target=http%3A%2F%2Fpgadmin.platform-admin.svc.cluster.local%2Fmisc%2Fping","http://pgadmin.platform-admin.svc.cluster.local/misc/ping","blackbox-private-internal","private-internal"), ("http://blackbox-exporter.observability.svc.cluster.local:9115/probe?hostname=grafana.learn.hyeonworks.com&module=http_private_internal_200&target=http%3A%2F%2Fgrafana.observability.svc.cluster.local%2Fapi%2Fhealth","http://grafana.observability.svc.cluster.local/api/health","blackbox-private-internal","private-internal"), ("http://blackbox-exporter.observability.svc.cluster.local:9115/probe?hostname=storage-admin.learn.hyeonworks.com&module=http_private_internal_200&target=http%3A%2F%2Fminio-aistor-console.object-storage.svc.cluster.local%3A9090%2F","http://minio-aistor-console.object-storage.svc.cluster.local:9090/","blackbox-private-internal","private-internal"), }, "probe/observability/platform-public-edge":{ ("http://blackbox-exporter.observability.svc.cluster.local:9115/probe?module=http_2xx&target=https%3A%2F%2Fgit.learn.hyeonworks.com%2Fapi%2Fhealthz","https://git.learn.hyeonworks.com/api/healthz","blackbox-public-edge","public-edge"), ("http://blackbox-exporter.observability.svc.cluster.local:9115/probe?module=http_2xx&target=https%3A%2F%2Fid.learn.hyeonworks.com%2Frealms%2Fhyeonworks%2F.well-known%2Fopenid-configuration","https://id.learn.hyeonworks.com/realms/hyeonworks/.well-known/openid-configuration","blackbox-public-edge","public-edge"), }, } endpoint_patterns={ "podMonitor/platform-data/platform-postgres/0":r"^http://(?:[0-9]{1,3}\.){3}[0-9]{1,3}:9187/metrics$", "serviceMonitor/gitea/gitea/0":r"^http://(?:[0-9]{1,3}\.){3}[0-9]{1,3}:3000/metrics$", "serviceMonitor/keycloak/keycloak/0":r"^http://(?:[0-9]{1,3}\.){3}[0-9]{1,3}:9000/metrics$", "serviceMonitor/kube-system/traefik/0":r"^http://(?:[0-9]{1,3}\.){3}[0-9]{1,3}:9100/metrics$", "serviceMonitor/object-storage/aistor-bucket-usage/0":r"^http://(?:[0-9]{1,3}\.){3}[0-9]{1,3}:9000/minio/metrics/v3/cluster/usage/buckets$", "serviceMonitor/observability-agent/alloy/0":r"^http://(?:[0-9]{1,3}\.){3}[0-9]{1,3}:12345/metrics$", "serviceMonitor/observability-agent/node-exporter/0":r"^http://(?:[0-9]{1,3}\.){3}[0-9]{1,3}:9100/metrics$", "serviceMonitor/observability/blackbox-exporter/0":r"^http://(?:[0-9]{1,3}\.){3}[0-9]{1,3}:9115/metrics$", "serviceMonitor/observability/grafana/0":r"^http://(?:[0-9]{1,3}\.){3}[0-9]{1,3}:3000/metrics$", "serviceMonitor/observability/loki/0":r"^http://(?:[0-9]{1,3}\.){3}[0-9]{1,3}:3100/metrics$", "serviceMonitor/observability/observability-core-kube-pr-alertmanager/0":r"^http://(?:[0-9]{1,3}\.){3}[0-9]{1,3}:9093/metrics$", "serviceMonitor/observability/observability-core-kube-pr-alertmanager/1":r"^http://(?:[0-9]{1,3}\.){3}[0-9]{1,3}:8080/metrics$", "serviceMonitor/observability/observability-core-kube-pr-apiserver/0":r"^https://(?:[0-9]{1,3}\.){3}[0-9]{1,3}:6443/metrics$", "serviceMonitor/observability/observability-core-kube-pr-coredns/0":r"^http://(?:[0-9]{1,3}\.){3}[0-9]{1,3}:9153/metrics$", "serviceMonitor/observability/observability-core-kube-pr-kubelet/0":r"^https://(?:[0-9]{1,3}\.){3}[0-9]{1,3}:10250/metrics$", "serviceMonitor/observability/observability-core-kube-pr-kubelet/1":r"^https://(?:[0-9]{1,3}\.){3}[0-9]{1,3}:10250/metrics/cadvisor$", "serviceMonitor/observability/observability-core-kube-pr-kubelet/2":r"^https://(?:[0-9]{1,3}\.){3}[0-9]{1,3}:10250/metrics/probes$", "serviceMonitor/observability/observability-core-kube-pr-operator/0":r"^http://(?:[0-9]{1,3}\.){3}[0-9]{1,3}:8080/metrics$", "serviceMonitor/observability/observability-core-kube-pr-prometheus/0":r"^http://(?:[0-9]{1,3}\.){3}[0-9]{1,3}:9090/metrics$", "serviceMonitor/observability/observability-core-kube-pr-prometheus/1":r"^http://(?:[0-9]{1,3}\.){3}[0-9]{1,3}:8080/metrics$", "serviceMonitor/observability/observability-core-kube-state-metrics/0":r"^http://(?:[0-9]{1,3}\.){3}[0-9]{1,3}:8080/metrics$", "serviceMonitor/observability/tempo/0":r"^http://(?:[0-9]{1,3}\.){3}[0-9]{1,3}:3200/metrics$", } target_identities={ "podMonitor/platform-data/platform-postgres/0":("platform-postgres","platform-data"), "serviceMonitor/gitea/gitea/0":("gitea","gitea"), "serviceMonitor/keycloak/keycloak/0":("keycloak-service","keycloak"), "serviceMonitor/kube-system/traefik/0":("traefik","kube-system"), "serviceMonitor/object-storage/aistor-bucket-usage/0":("minio-aistor","object-storage"), "serviceMonitor/observability-agent/alloy/0":("alloy","observability-agent"), "serviceMonitor/observability-agent/node-exporter/0":("prometheus-node-exporter","observability-agent"), "serviceMonitor/observability/blackbox-exporter/0":("blackbox-exporter","observability"), "serviceMonitor/observability/grafana/0":("grafana","observability"), "serviceMonitor/observability/loki/0":("observability/loki","observability"), "serviceMonitor/observability/observability-core-kube-pr-alertmanager/0":("observability-core-kube-pr-alertmanager","observability"), "serviceMonitor/observability/observability-core-kube-pr-alertmanager/1":("observability-core-kube-pr-alertmanager","observability"), "serviceMonitor/observability/observability-core-kube-pr-apiserver/0":("apiserver","default"), "serviceMonitor/observability/observability-core-kube-pr-coredns/0":("coredns","kube-system"), "serviceMonitor/observability/observability-core-kube-pr-kubelet/0":("kubelet","kube-system"), "serviceMonitor/observability/observability-core-kube-pr-kubelet/1":("kubelet","kube-system"), "serviceMonitor/observability/observability-core-kube-pr-kubelet/2":("kubelet","kube-system"), "serviceMonitor/observability/observability-core-kube-pr-operator/0":("observability-core-kube-pr-operator","observability"), "serviceMonitor/observability/observability-core-kube-pr-prometheus/0":("observability-core-kube-pr-prometheus","observability"), "serviceMonitor/observability/observability-core-kube-pr-prometheus/1":("observability-core-kube-pr-prometheus","observability"), "serviceMonitor/observability/observability-core-kube-state-metrics/0":("kube-state-metrics","observability"), "serviceMonitor/observability/tempo/0":("tempo","observability"), } try: item=json.loads(pathlib.Path(sys.argv[1]).read_text()) except Exception: raise SystemExit(1) targets=((item.get("data") or {}).get("activeTargets")) if item.get("status")!="success" or not isinstance(targets,list) or len(targets)!=30: raise SystemExit(1) actual={} probe_targets={pool:set() for pool in expected_probe_targets} for target in targets: if target.get("health")!="up" or target.get("lastError","") not in ("",None): raise SystemExit(1) pool=target.get("scrapePool") url=target.get("scrapeUrl") if not isinstance(pool,str) or not isinstance(url,str): raise SystemExit(1) if pool in endpoint_patterns and re.fullmatch(endpoint_patterns[pool],url) is None: raise SystemExit(1) if pool in target_identities: labels=target.get("labels") discovered=target.get("discoveredLabels") parsed=urlsplit(url) expected_job,expected_namespace=target_identities[pool] if (not isinstance(labels,dict) or labels.get("job")!=expected_job or labels.get("namespace")!=expected_namespace or labels.get("instance")!=parsed.netloc): raise SystemExit(1) if (target.get("globalUrl")!=url or not isinstance(discovered,dict) or discovered.get("__address__")!=parsed.netloc or discovered.get("__scheme__")!=parsed.scheme or discovered.get("__metrics_path__")!=parsed.path): raise SystemExit(1) if pool in expected_probe_targets: labels=target.get("labels") if not isinstance(labels,dict) or set(labels)!={"instance","job","namespace","observability.hyeonworks.com/probe-group"}: raise SystemExit(1) identity=(url,labels.get("instance"),labels.get("job"),labels.get("observability.hyeonworks.com/probe-group")) if labels.get("namespace")!="observability" or identity in probe_targets[pool]: raise SystemExit(1) probe_targets[pool].add(identity) actual[pool]=actual.get(pool,0)+1 if actual!=expected or probe_targets!=expected_probe_targets: raise SystemExit(1) PY } validate_series_contract() { local file=$1 expected_count=$2 required_csv=$3 expected_values_csv=$4 value_label=$5 python_bounded -I -S - "$file" "$expected_count" "$required_csv" "$expected_values_csv" "$value_label" <<'PY' import json,pathlib,sys path,count,required,values,label=sys.argv[1:] try: item=json.loads(pathlib.Path(path).read_text()) except Exception: raise SystemExit(1) result=((item.get("data") or {}).get("result")) if item.get("status")!="success" or not isinstance(result,list) or len(result)!=int(count): raise SystemExit(1) required=set(filter(None,required.split(','))) expected=sorted(filter(None,values.split(','))) seen=[] for series in result: metric=series.get("metric") if not isinstance(metric,dict) or not required.issubset(metric): raise SystemExit(1) value=series.get("value") if not isinstance(value,list) or len(value)!=2: raise SystemExit(1) try: float(value[1]) except (TypeError,ValueError): raise SystemExit(1) if label: seen.append(metric.get(label)) if label and (any(not isinstance(x,str) for x in seen) or sorted(seen)!=expected): raise SystemExit(1) PY } validate_series_identity_contract() { local file=$1 expected_json=$2 instance_pattern=${3:-} "$TIMEOUT_BIN" --signal=TERM --kill-after=1s 10s "$PYTHON_BIN" -I -S - \ "$file" "$expected_json" "$instance_pattern" <<'PY' import json,pathlib,re,sys path,expected_raw,instance_pattern=sys.argv[1:] try: item=json.loads(pathlib.Path(path).read_text()) expected=json.loads(expected_raw) except Exception: raise SystemExit(1) result=((item.get('data') or {}).get('result')) if item.get('status')!='success' or not isinstance(result,list) or not isinstance(expected,list) or len(result)!=len(expected) or not expected: raise SystemExit(1) if any(not isinstance(value,dict) or not value for value in expected): raise SystemExit(1) keys=set(expected[0]) if any(set(value)!=keys or any(not isinstance(x,str) for x in value.values()) for value in expected): raise SystemExit(1) def canonical(value): return json.dumps(value,sort_keys=True,separators=(',',':')) expected_rows=sorted(canonical(value) for value in expected) if len(set(expected_rows))!=len(expected_rows): raise SystemExit(1) actual=[] for series in result: metric=series.get('metric') if not isinstance(metric,dict) or any(not isinstance(metric.get(key),str) for key in keys): raise SystemExit(1) if instance_pattern: instance=metric.get('instance') if not isinstance(instance,str) or re.fullmatch(instance_pattern,instance) is None: raise SystemExit(1) actual.append(canonical({key:metric[key] for key in keys})) if sorted(actual)!=expected_rows: raise SystemExit(1) PY } validate_datasource_contract() { local file=$1 python_bounded -I -S - "$file" <<'PY' import base64,binascii,json,pathlib,re,sys try: item=json.loads(pathlib.Path(sys.argv[1]).read_text()) except Exception: raise SystemExit(1) if any((item.get(x) or {}).get("status")!="OK" for x in ("prometheus","loki","tempo")): raise SystemExit(1) loki=item.get("loki_config") or {}; tempo=item.get("tempo_config") or {} fields=((loki.get("jsonData") or {}).get("derivedFields")) or [] if not any(x.get("name")=="trace_id" and x.get("datasourceUid")=="tempo" and x.get("url")=="${__value.raw}" for x in fields): raise SystemExit(1) link=((tempo.get("jsonData") or {}).get("tracesToLogsV2")) or {} if link.get("datasourceUid")!="loki" or link.get("filterByTraceID") is not True: raise SystemExit(1) lines=[] for stream in (((item.get("loki_query") or {}).get("data") or {}).get("result") or []): for value in stream.get("values") or []: if isinstance(value,list) and len(value)==2 and isinstance(value[1],str): lines.append(value[1]) selected=item.get('selected_trace') if not isinstance(selected,str) or re.fullmatch(r'[0-9a-f]{32}',selected) is None: raise SystemExit(1) search=(item.get('tempo_search') or {}).get('traces') if not isinstance(search,list): raise SystemExit(1) candidates=[x for x in search if isinstance(x,dict) and x.get('traceID')==selected and x.get('rootServiceName')=='platform-smoke'] if len(candidates)!=1: raise SystemExit(1) matches=[] for line in lines: matches.extend(re.findall(r'"trace_id"\s*:\s*"([0-9a-f]{32})"',line)) if not matches or set(matches)!={selected}: raise SystemExit(1) batches=(item.get("tempo_query") or {}).get("batches") if not isinstance(batches,list) or not batches: raise SystemExit(1) seen_span=False for batch in batches: if not isinstance(batch,dict): raise SystemExit(1) attrs=((batch.get('resource') or {}).get('attributes')) if not isinstance(attrs,list): raise SystemExit(1) service=[x for x in attrs if isinstance(x,dict) and x.get('key')=='service.name'] if len(service)!=1 or ((service[0].get('value') or {}).get('stringValue'))!='platform-smoke': raise SystemExit(1) for scope in batch.get('scopeSpans') or []: for span in scope.get('spans') or []: encoded=span.get('traceId') if not isinstance(encoded,str): raise SystemExit(1) try: decoded=base64.b64decode(encoded,validate=True).hex() except (binascii.Error,ValueError): raise SystemExit(1) if decoded!=selected: raise SystemExit(1) seen_span=True if not seen_span: raise SystemExit(1) PY } synthetic_loki_query() { local trace=$1 [[ "$trace" =~ ^[0-9a-f]{32}$ ]] || return 1 printf '{cluster="home",namespace="observability"} | json | trace_id="%s"' "$trace" } validate_human_tokens() { local -a expected=(OIDC_ADMIN_CONFIRMED OIDC_VIEWER_CONFIRMED OIDC_DENIED_CONFIRMED BREAK_GLASS_CONFIRMED REMOVAL_VIEWER_CONFIRMED SESSION_REVOKED RELOGIN_DENIED FIRING RESOLVED EXTERNAL_CLIENT_VERIFIED) local -a actual=() mapfile -t actual (( ${#actual[@]} == ${#expected[@]} )) || return 1 local index for ((index=0; index<${#expected[@]}; index++)); do [[ "${actual[$index]}" == "${expected[$index]}" ]] || return 1 done } validate_ipv4() { local value=$1 octet local -a parts=() [[ "$value" =~ ^[0-9]{1,3}([.][0-9]{1,3}){3}$ ]] || return 1 IFS=. read -r -a parts <<<"$value" (( ${#parts[@]} == 4 )) || return 1 for octet in "${parts[@]}"; do (( 10#$octet <= 255 )) || return 1 done } external_attestation_token() { local public_ip=$1 validate_ipv4 "$public_ip" || return 1 printf 'EXTERNAL_CLIENT_VERIFIED public_ip=%s status=403 source=outside-lan-tailscale proxy=disabled' "$public_ip" } render_external_attestation() { local public_ip=$1 token token="$(external_attestation_token "$public_ip")" || return 1 printf '%s\n' \ 'Run this from a client outside both LAN and Tailscale, with no HTTP(S) proxy:' \ "/usr/bin/curl --disable --silent --show-error --output /dev/null --write-out '%{http_code}\\n' --connect-timeout 5 --max-time 15 --noproxy '*' --resolve $GRAFANA_HOST:443:$public_ip https://$GRAFANA_HOST/" \ 'Expected exact result: HTTP 403' \ "After verifying the client source and result, type: $token" } new_smoke_run_id() { local prefix=$1 stamp nonce [[ "$prefix" =~ ^[a-z][a-z0-9-]{0,14}$ ]] || return 1 stamp="$("$TIMEOUT_BIN" --signal=TERM --kill-after=1s 5s "$DATE_BIN" -u +%Y%m%dT%H%M%SZ)" || return 1 nonce="$("$TIMEOUT_BIN" --signal=TERM --kill-after=1s 5s "$PYTHON_BIN" -I -S - <<'PY' import os print(os.urandom(16).hex(),end='') PY )" || return 1 [[ "$stamp" =~ ^[0-9]{8}T[0-9]{6}Z$ && "$nonce" =~ ^[0-9a-f]{32}$ ]] || return 1 printf '%s-%s-%s' "$prefix" "${stamp,,}" "$nonce" } kubernetes_smoke_name() { local run=$1 nonce [[ "$run" =~ ^[a-z][a-z0-9-]{0,14}-[0-9]{8}t[0-9]{6}z-([0-9a-f]{32})$ ]] || return 1 nonce=${BASH_REMATCH[1]} printf 'observability-smoke-%s' "$nonce" } handle_termination_signal() { local rc=$1 [[ "$rc" =~ ^(129|130|143)$ ]] || rc=143 if (( MUTATION_CRITICAL_DEPTH > 0 )); then (( DEFERRED_SIGNAL_RC == 0 )) && DEFERRED_SIGNAL_RC=$rc return 0 fi exit "$rc" } begin_mutation_critical() { MUTATION_CRITICAL_DEPTH=$((MUTATION_CRITICAL_DEPTH + 1)) } end_mutation_critical() { (( MUTATION_CRITICAL_DEPTH > 0 )) || return 1 MUTATION_CRITICAL_DEPTH=$((MUTATION_CRITICAL_DEPTH - 1)) if (( MUTATION_CRITICAL_DEPTH == 0 && DEFERRED_SIGNAL_RC != 0 )); then local rc=$DEFERRED_SIGNAL_RC DEFERRED_SIGNAL_RC=0 exit "$rc" fi } require_confirmation() { local expected=$1 answer='' if [[ "$TEST_MODE" == true ]]; then (( CONFIRMATION_INDEX < ${#CONFIRMATION_QUEUE[@]} )) || return 1 answer=${CONFIRMATION_QUEUE[$CONFIRMATION_INDEX]} CONFIRMATION_INDEX=$((CONFIRMATION_INDEX + 1)) else [[ -t 0 ]] || { fail '--execute requires an interactive terminal'; return 1; } printf 'Type %s: ' "$expected" >&2 IFS= read -r -t "$HUMAN_TIMEOUT_SECONDS" answer || return 1 fi [[ "$answer" == "$expected" ]] } confirmation_remaining_seconds() { local deadline=$1 now=$2 remaining [[ "$deadline" =~ ^[0-9]+$ && "$now" =~ ^[0-9]+$ ]] || return 1 remaining=$((deadline - now)) (( remaining >= 1 && remaining <= HUMAN_TIMEOUT_SECONDS )) || return 1 printf '%s' "$remaining" } require_confirmation_before() { local expected=$1 deadline=$2 now remaining answer='' if [[ "$TEST_MODE" == true ]]; then require_confirmation "$expected"; return; fi now="$("$TIMEOUT_BIN" --signal=TERM --kill-after=1s 5s "$DATE_BIN" -u +%s)" || return 1 remaining="$(confirmation_remaining_seconds "$deadline" "$now")" || return 1 [[ -t 0 ]] || return 1 printf 'Type %s within the shared deadline: ' "$expected" >&2 IFS= read -r -t "$remaining" answer || return 1 [[ "$answer" == "$expected" ]] } read_identity() { local purpose=$1 answer='' [[ "$TEST_MODE" == false ]] || return 1 [[ -t 0 ]] || return 1 printf 'Enter the existing %s username (not recorded): ' "$purpose" >&2 IFS= read -r -t "$HUMAN_TIMEOUT_SECONDS" answer || return 1 [[ "$answer" =~ ^[A-Za-z0-9][A-Za-z0-9._@+-]{0,127}$ ]] || return 1 printf '%s' "$answer" } process_start_time() { local pid=$1 python_bounded -I -S - "$pid" <<'PY' import pathlib,sys raw=pathlib.Path('/proc',sys.argv[1],'stat').read_text() end=raw.rfind(') ') if end<0: raise SystemExit(1) fields=raw[end+2:].split() if len(fields)<20 or not fields[19].isdigit(): raise SystemExit(1) print(fields[19],end='') PY } stop_owned_process() { local pid=$1 expected_start=$2 current attempt [[ "$pid" =~ ^[0-9]+$ && "$expected_start" =~ ^[0-9]+$ ]] || return 0 current="$(process_start_time "$pid" 2>/dev/null)" || { wait "$pid" 2>/dev/null || true; return 0; } [[ "$current" == "$expected_start" ]] || { emit_manual_recovery; return 1; } /bin/kill -TERM "$pid" 2>/dev/null || true for ((attempt=0; attempt<40; attempt++)); do /bin/kill -0 "$pid" 2>/dev/null || { wait "$pid" 2>/dev/null || true; return 0; } /usr/bin/sleep 0.05 done current="$(process_start_time "$pid" 2>/dev/null)" || { wait "$pid" 2>/dev/null || true; return 0; } [[ "$current" == "$expected_start" ]] || { emit_manual_recovery; return 1; } /bin/kill -KILL "$pid" 2>/dev/null || true wait "$pid" 2>/dev/null || true } kubectl_bounded() { "$TIMEOUT_BIN" --signal=TERM --kill-after=2s "$PROCESS_TIMEOUT" \ "$KUBECTL_BIN" --request-timeout="$REQUEST_TIMEOUT" "$@" } kubectl_wait_90_bounded() { "$TIMEOUT_BIN" --signal=TERM --kill-after=2s 100s \ "$KUBECTL_BIN" --request-timeout=95s "$@" } kubectl_wait_180_bounded() { "$TIMEOUT_BIN" --signal=TERM --kill-after=2s 190s \ "$KUBECTL_BIN" --request-timeout=185s "$@" } jq_bounded() { "$TIMEOUT_BIN" --signal=TERM --kill-after=1s 15s "$JQ_BIN" "$@" } python_bounded() { "$TIMEOUT_BIN" --signal=TERM --kill-after=1s 15s "$PYTHON_BIN" "$@" } safe_work_file() { [[ -n "$WORK" && "$WORK" == /tmp/platform-observability-smoke.???????? && "$1" == "$WORK"/* && -f "$1" && ! -L "$1" && -O "$1" && "$("$STAT_BIN" -c '%a:%h' -- "$1")" == 600:1 ]] } make_work() { WORK="$("$MKTEMP_BIN" -d /tmp/platform-observability-smoke.XXXXXXXX)" || return 1 [[ "$WORK" == /tmp/platform-observability-smoke.???????? && -d "$WORK" && ! -L "$WORK" ]] || return 1 "$CHMOD_BIN" 0700 -- "$WORK" } start_api_proxy() { [[ -n "$WORK" ]] || return 1 API_PROXY_SOCKET="$WORK/kube-api.sock" "$KUBECTL_BIN" --request-timeout="$REQUEST_TIMEOUT" proxy \ --unix-socket="$API_PROXY_SOCKET" --api-prefix=/ \ --accept-paths='^/(api|apis)/.*$' >"$WORK/kube-api-proxy.log" 2>&1 & API_PROXY_PID=$! API_PROXY_START="$(process_start_time "$API_PROXY_PID")" || return 1 local attempt for ((attempt=0; attempt<100; attempt++)); do [[ -S "$API_PROXY_SOCKET" ]] && { "$CHMOD_BIN" 0600 -- "$API_PROXY_SOCKET"; return 0; } /bin/kill -0 "$API_PROXY_PID" 2>/dev/null || return 1 /usr/bin/sleep 0.05 done return 1 } proxy_request() { local method=$1 path=$2 output=$3 body=${4:-} extra=${5:-} local config status [[ "$method" =~ ^(GET|POST|PUT|DELETE)$ && "$path" == /api* && "$path" != *$'\n'* && "$path" != *'"'* && "$path" != *'\\'* ]] || return 1 config="$("$MKTEMP_BIN" "$WORK/curl.XXXXXXXX")" || return 1 "$CHMOD_BIN" 0600 -- "$config" : >"$output"; "$CHMOD_BIN" 0600 -- "$output" { printf 'silent\nshow-error\nconnect-timeout = 2\nmax-time = 12\n' printf 'unix-socket = "%s"\nrequest = "%s"\n' "$API_PROXY_SOCKET" "$method" printf 'url = "http://localhost%s"\noutput = "%s"\nwrite-out = "%%{http_code}"\n' "$path" "$output" [[ -z "$body" ]] || printf 'header = "Content-Type: application/json"\ndata-binary = "@%s"\n' "$body" [[ -z "$extra" ]] || /bin/cat -- "$extra" } >"$config" safe_work_file "$config" && safe_work_file "$output" || return 1 status="$("$TIMEOUT_BIN" --signal=TERM --kill-after=1s 15s "$CURL_BIN" --disable --config "$config")" || return 1 [[ "$status" =~ ^[0-9]{3}$ ]] || return 1 printf '%s' "$status" } prometheus_path() { printf '/api/v1/namespaces/observability/services/http:%s:9090/proxy%s' "$PROMETHEUS_SERVICE" "$1" } alertmanager_path() { printf '/api/v1/namespaces/observability/services/http:%s:9093/proxy%s' "$ALERTMANAGER_SERVICE" "$1" } grafana_path() { printf '/api/v1/namespaces/observability/services/http:grafana:80/proxy%s' "$1"; } keycloak_path() { printf '/api/v1/namespaces/keycloak/services/http:keycloak-service:8080/proxy%s' "$1"; } proxy_get_200() { local path=$1 output=$2 extra=${3:-} status status="$(proxy_request GET "$path" "$output" '' "$extra")" || return 1 [[ "$status" == 200 && -s "$output" ]] } prom_query() { local expression=$1 output=$2 encoded encoded="$(jq_bounded -nr --arg value "$expression" '$value|@uri')" || return 1 proxy_get_200 "$(prometheus_path "/api/v1/query?query=$encoded")" "$output" } api_path_for_owned() { local kind=$1 namespace=$2 name=$3 [[ "$namespace" =~ ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ && "$name" =~ ^[a-z0-9]([-a-z0-9.]*[a-z0-9])?$ ]] || return 1 case "$kind" in Pod) printf '/api/v1/namespaces/%s/pods/%s' "$namespace" "$name" ;; NetworkPolicy) printf '/apis/networking.k8s.io/v1/namespaces/%s/networkpolicies/%s' "$namespace" "$name" ;; PrometheusRule) printf '/apis/monitoring.coreos.com/v1/namespaces/%s/prometheusrules/%s' "$namespace" "$name" ;; *) return 1 ;; esac } kind_resource() { case "$1" in Pod) printf pod ;; NetworkPolicy) printf networkpolicy ;; PrometheusRule) printf prometheusrule ;; *) return 1 ;; esac } owned_object_state() { local kind=$1 namespace=$2 name=$3 resource file uid run resource="$(kind_resource "$kind")" || return 4 file="$("$MKTEMP_BIN" "$WORK/state.XXXXXXXX")" || return 4 "$CHMOD_BIN" 0600 -- "$file" kubectl_bounded -n "$namespace" get "$resource" "$name" --ignore-not-found -o json >"$file" || return 4 [[ -s "$file" ]] || return 1 uid="$(jq_bounded -er '.metadata.uid | select(type=="string" and length>0)' "$file")" || return 4 run="$(jq_bounded -er '.metadata.labels["platform.hyeonworks.com/smoke-run"] | select(type=="string" and length>0)' "$file")" || return 4 printf '%s|%s\n' "$uid" "$run" } delete_owned_uid() { local kind=$1 namespace=$2 name=$3 uid=$4 path body response status state attempt [[ "$uid" =~ ^[A-Za-z0-9][A-Za-z0-9._-]*$ ]] || return 1 path="$(api_path_for_owned "$kind" "$namespace" "$name")" || return 1 body="$("$MKTEMP_BIN" "$WORK/delete.XXXXXXXX")" || return 1 response="$("$MKTEMP_BIN" "$WORK/delete-response.XXXXXXXX")" || return 1 "$CHMOD_BIN" 0600 -- "$body" "$response" jq_bounded -nc --arg uid "$uid" '{apiVersion:"meta.k8s.io/v1",kind:"DeleteOptions",propagationPolicy:"Background",preconditions:{uid:$uid}}' >"$body" status="$(proxy_request DELETE "$path" "$response" "$body")" || status=000 for ((attempt=0; attempt<30; attempt++)); do state="$(owned_object_state "$kind" "$namespace" "$name" 2>/dev/null)" && { [[ "${state%%|*}" == "$uid" ]] || return 1 } || { [[ $? == 1 ]] && return 0 return 1 } /usr/bin/sleep 1 done return 1 } register_owned() { OWNED_KIND+=("$1"); OWNED_NAMESPACE+=("$2"); OWNED_NAME+=("$3"); OWNED_UID+=("$4"); OWNED_RUN+=("$5") } cleanup_owned_objects() { local index state uid run rc=0 for ((index=${#OWNED_KIND[@]}-1; index>=0; index--)); do state="$(owned_object_state "${OWNED_KIND[$index]}" "${OWNED_NAMESPACE[$index]}" "${OWNED_NAME[$index]}" 2>/dev/null)" || { [[ $? == 1 ]] && continue emit_manual_recovery; rc=1; continue } IFS='|' read -r uid run <<<"$state" if [[ "$uid" != "${OWNED_UID[$index]}" || "$run" != "${OWNED_RUN[$index]}" ]]; then emit_manual_recovery; rc=1; continue fi delete_owned_uid "${OWNED_KIND[$index]}" "${OWNED_NAMESPACE[$index]}" "${OWNED_NAME[$index]}" "$uid" || { emit_manual_recovery; rc=1; } done OWNED_KIND=(); OWNED_NAMESPACE=(); OWNED_NAME=(); OWNED_UID=(); OWNED_RUN=() return "$rc" } validate_commit_environment() { local permission result context api node_uid validate_command_boundaries || return 1 context="$(kubectl_bounded config current-context)" || return 1 [[ "$context" == "$EXPECTED_CONTEXT" ]] || { fail 'Kubernetes context is not default'; return 1; } api="$(kubectl_bounded config view --minify -o jsonpath='{.clusters[0].cluster.server}')" || return 1 [[ "$api" == "$EXPECTED_API_SERVER" ]] || { fail 'Kubernetes API server is not the exact local endpoint'; return 1; } kubectl_bounded get --raw=/readyz >/dev/null || { fail 'Kubernetes API is not ready'; return 1; } node_uid="$(kubectl_bounded get node "$EXPECTED_NODE" -o json | jq_bounded -er --arg node "$EXPECTED_NODE" ' select(.metadata.name==$node and (.metadata.uid|type=="string" and length>0) and any(.status.conditions[]?; .type=="Ready" and .status=="True")) | .metadata.uid ')" || { fail 'target node identity/readiness changed'; return 1; } if [[ -z "$EXPECTED_NODE_UID" ]]; then EXPECTED_NODE_UID=$node_uid; fi [[ "$node_uid" == "$EXPECTED_NODE_UID" ]] || { fail 'target node UID changed'; return 1; } for permission in \ 'get pods observability' 'list pods observability' 'watch pods observability' \ 'create pods observability' 'delete pods observability' 'get pods/log observability' \ 'get services observability' 'get services object-storage' 'get services kube-system' \ 'get services/proxy observability' 'create services/proxy observability' \ 'delete services/proxy observability' \ 'get services/proxy keycloak' 'create services/proxy keycloak' \ 'update services/proxy keycloak' 'delete services/proxy keycloak' \ 'get persistentvolumeclaims observability' 'list persistentvolumeclaims observability' \ 'get configmaps observability' 'list configmaps observability' \ 'get deployments.apps observability' 'watch deployments.apps observability' \ 'get statefulsets.apps observability' 'watch statefulsets.apps observability' \ 'get networkpolicies.networking.k8s.io observability' \ 'create networkpolicies.networking.k8s.io observability' \ 'delete networkpolicies.networking.k8s.io observability' \ 'get prometheusrules.monitoring.coreos.com observability' \ 'create prometheusrules.monitoring.coreos.com observability' \ 'delete prometheusrules.monitoring.coreos.com observability' \ 'get secrets observability' 'get secrets keycloak'; do read -r -a _permission_parts <<<"$permission" result="$(kubectl_bounded auth can-i "${_permission_parts[0]}" "${_permission_parts[1]}" -n "${_permission_parts[2]}")" || return 1 [[ "$result" == yes ]] || { fail "missing Kubernetes authority: $permission"; return 1; } done } require_execute_environment() { validate_commit_environment || return 1 make_work || { fail 'cannot create private work directory'; return 1; } start_api_proxy || { fail 'cannot start bounded Kubernetes API proxy'; return 1; } } validate_command_boundaries() { local path certbot_real metadata for path in "$KUBECTL_BIN" "$CURL_BIN" "$DIG_BIN" "$OPENSSL_BIN" "$NC_BIN" \ "$SUDO_BIN" "$DATE_BIN" "$TIMEOUT_BIN" "$JQ_BIN" "$PYTHON_BIN" \ "$CAT_BIN" "$CHMOD_BIN" "$CMP_BIN" "$MKTEMP_BIN" "$MV_BIN" \ "$READLINK_BIN" "$RM_BIN" "$STAT_BIN"; do [[ -x "$path" && ! -L "$path" ]] || { fail "required command boundary is unavailable: $path"; return 1; } done [[ -L "$CERTBOT_BIN" && -x "$CERTBOT_BIN" ]] || { fail "Certbot command boundary is unavailable: $CERTBOT_BIN"; return 1; } certbot_real="$("$READLINK_BIN" -f -- "$CERTBOT_BIN")" || return 1 [[ "$certbot_real" == "$CERTBOT_REAL" && -x "$CERTBOT_REAL" && ! -L "$CERTBOT_REAL" ]] || { fail 'Certbot command boundary does not resolve to the fixed snap executable'; return 1; } metadata="$("$STAT_BIN" -c '%u:%g:%a:%F' -- "$CERTBOT_REAL")" || return 1 [[ "$metadata" == '0:0:755:regular file' ]] || { fail 'Certbot real executable metadata is unsafe'; return 1; } [[ -x "$SYSTEMCTL_BIN" && ! -L "$SYSTEMCTL_BIN" && "$("$STAT_BIN" -c '%u:%g:%a:%F' -- "$SYSTEMCTL_BIN")" == '0:0:755:regular file' ]] || { fail 'systemctl command boundary is unsafe'; return 1; } } dig_values() { local resolver=$1 record=$2 host=$3 "$TIMEOUT_BIN" --signal=TERM --kill-after=1s 8s "$DIG_BIN" +time=2 +tries=1 +short \ "@$resolver" "$record" "$host" | /usr/bin/sed '/^[[:space:]]*$/d' | LC_ALL=C /usr/bin/sort -u } http_status() { local -a args=(--disable --silent --show-error --output /dev/null --write-out '%{http_code}' --connect-timeout 3 --max-time 12 --noproxy '*') "$TIMEOUT_BIN" --signal=TERM --kill-after=1s 15s "$CURL_BIN" "${args[@]}" "$@" } check_dns_host() { local public_a public_aaaa lan_a lan_aaaa tail_a tail_aaaa public_a="$(dig_values 1.1.1.1 A "$GRAFANA_HOST")" || return 1 public_aaaa="$(dig_values 1.1.1.1 AAAA "$GRAFANA_HOST")" || return 1 lan_a="$(dig_values "$LAN_IP" A "$GRAFANA_HOST")" || return 1 lan_aaaa="$(dig_values "$LAN_IP" AAAA "$GRAFANA_HOST")" || return 1 tail_a="$(dig_values "$TAILSCALE_IP" A "$GRAFANA_HOST")" || return 1 tail_aaaa="$(dig_values "$TAILSCALE_IP" AAAA "$GRAFANA_HOST")" || return 1 validate_dns_contract "$public_a" "$public_aaaa" "$lan_a" "$lan_aaaa" \ "$tail_a" "$tail_aaaa" "$LAN_IP" '' || return 1 printf 'OBSERVABILITY_MACHINE_DNS_HOST=PASS\n' } check_certificate() { local raw cert extension verify_rc=0 expiry_rc=0 raw="$WORK/grafana-sclient.txt"; cert="$WORK/grafana-leaf.pem" : >"$raw"; : >"$cert"; "$CHMOD_BIN" 0600 -- "$raw" "$cert" "$TIMEOUT_BIN" --signal=TERM --kill-after=1s 15s "$OPENSSL_BIN" s_client \ -connect "$LAN_IP:443" -servername "$GRAFANA_HOST" -showcerts "$raw" 2>/dev/null || return 1 /usr/bin/awk '/-----BEGIN CERTIFICATE-----/{copy=1} copy{print} /-----END CERTIFICATE-----/{exit}' "$raw" >"$cert" [[ -s "$cert" ]] || return 1 "$TIMEOUT_BIN" --signal=TERM --kill-after=1s 15s "$OPENSSL_BIN" s_client -connect "$LAN_IP:443" -servername "$GRAFANA_HOST" \ -verify_hostname "$GRAFANA_HOST" -verify_return_error /dev/null 2>&1 || verify_rc=$? "$TIMEOUT_BIN" --signal=TERM --kill-after=1s 10s "$OPENSSL_BIN" x509 -in "$cert" -noout -checkend 604800 >/dev/null 2>&1 || expiry_rc=$? extension="$("$TIMEOUT_BIN" --signal=TERM --kill-after=1s 10s "$OPENSSL_BIN" x509 \ -in "$cert" -noout -ext subjectAltName 2>/dev/null)" || return 1 validate_certificate_extension_contract "$extension" "$verify_rc" "$expiry_rc" || return 1 printf 'OBSERVABILITY_MACHINE_CERTIFICATE=PASS\n' } check_nodeport_identity() { local service="$WORK/traefik-nodeport-service.json" kubectl_bounded -n kube-system get service traefik -o json >"$service" || return 1 "$CHMOD_BIN" 0600 -- "$service" jq_bounded -e ' .apiVersion=="v1" and .kind=="Service" and .metadata.name=="traefik" and .metadata.namespace=="kube-system" and (.metadata.uid|type=="string" and length>0) and .spec.type=="NodePort" and .spec.externalTrafficPolicy=="Cluster" and .spec.selector=={ "app.kubernetes.io/name":"traefik", "app.kubernetes.io/instance":"traefik-kube-system"} and ([.spec.ports[]|{name,port,protocol,targetPort,nodePort}]|sort_by(.name))==([ {name:"web",port:80,protocol:"TCP",targetPort:"web",nodePort:30080}, {name:"websecure",port:443,protocol:"TCP",targetPort:"websecure",nodePort:30443} ]|sort_by(.name)) ' "$service" >/dev/null || return 1 [[ "$(http_status --header 'Host: observability-smoke.invalid' http://127.0.0.1:30080/)" == 404 ]] } check_https_boundaries() { local public_ip [[ "$(http_status --interface "$LAN_IP" --resolve "$GRAFANA_HOST:443:$LAN_IP" "https://$GRAFANA_HOST/")" =~ ^(200|302)$ ]] || return 1 [[ "$(http_status --interface "$TAILSCALE_IP" --resolve "$GRAFANA_HOST:443:$TAILSCALE_IP" "https://$GRAFANA_HOST/")" =~ ^(200|302)$ ]] || return 1 [[ "$(http_status --resolve "$GRAFANA_HOST:443:127.0.0.1" "https://$GRAFANA_HOST/")" == 403 ]] || return 1 public_ip="$(dig_values 1.1.1.1 A "$GITEA_HOST" | /usr/bin/head -n1)" || return 1 validate_ipv4 "$public_ip" || return 1 if [[ -n "$PUBLIC_EDGE_IP" && "$PUBLIC_EDGE_IP" != "$public_ip" ]]; then return 1; fi PUBLIC_EDGE_IP=$public_ip [[ "$(http_status --resolve "$GRAFANA_HOST:443:$public_ip" "https://$GRAFANA_HOST/")" == 403 ]] || return 1 [[ "$(http_status --resolve "$GRAFANA_HOST:443:$LAN_IP" "https://$GRAFANA_HOST/metrics")" == 404 ]] || return 1 [[ "$(http_status "https://$GITEA_HOST/metrics")" == 404 ]] || return 1 [[ "$(http_status "https://$KEYCLOAK_HOST/metrics")" == 404 ]] || return 1 [[ "$(http_status "$RUNBOOK_URL")" == 200 ]] || return 1 if "$TIMEOUT_BIN" --signal=TERM --kill-after=1s 8s "$OPENSSL_BIN" s_client -connect "$LAN_IP:443" \ -servername unknown-sni.invalid /dev/null 2>&1; then return 1; fi ! "$TIMEOUT_BIN" --signal=TERM --kill-after=1s 5s "$NC_BIN" -z -w 2 "$LAN_IP" 30080 || return 1 ! "$TIMEOUT_BIN" --signal=TERM --kill-after=1s 5s "$NC_BIN" -z -w 2 "$LAN_IP" 30443 || return 1 check_nodeport_identity || return 1 printf 'OBSERVABILITY_MACHINE_EDGE=PASS\n' } check_targets() { local file="$WORK/prometheus-targets.json" proxy_get_200 "$(prometheus_path '/api/v1/targets?state=active')" "$file" || return 1 validate_target_contract "$file" || return 1 printf 'OBSERVABILITY_MACHINE_TARGETS=PASS\n' } check_one_query() { local name=$1 expression=$2 count=$3 labels=$4 values=${5:-} value_label=${6:-} local identity_json=${7:-} instance_pattern=${8:-} local file="$WORK/query-$name.json" prom_query "$expression" "$file" || return 1 validate_series_contract "$file" "$count" "$labels" "$values" "$value_label" || return 1 [[ -z "$identity_json" ]] || validate_series_identity_contract "$file" "$identity_json" "$instance_pattern" } check_metric_contracts() { check_one_query gitea 'gitea_build_info{job="gitea",namespace="gitea"}' 1 'instance,job,namespace' '' '' \ '[{"job":"gitea","namespace":"gitea"}]' '^([0-9]{1,3}[.]){3}[0-9]{1,3}:3000$' || return 1 check_one_query keycloak 'process_uptime_seconds{job="keycloak-service",namespace="keycloak"}' 1 'instance,job,namespace' '' '' \ '[{"job":"keycloak-service","namespace":"keycloak"}]' '^([0-9]{1,3}[.]){3}[0-9]{1,3}:9000$' || return 1 check_one_query cnpg 'cnpg_collector_up{job="platform-postgres",namespace="platform-data",cluster="platform-postgres"}' 1 'cluster,instance,job,namespace' '' '' \ '[{"cluster":"platform-postgres","job":"platform-postgres","namespace":"platform-data"}]' '^([0-9]{1,3}[.]){3}[0-9]{1,3}:9187$' || return 1 check_one_query traefik 'traefik_config_last_reload_success{job="traefik",namespace="kube-system"}' 1 'instance,job,namespace' '' '' \ '[{"job":"traefik","namespace":"kube-system"}]' '^([0-9]{1,3}[.]){3}[0-9]{1,3}:9100$' || return 1 check_one_query aistor-usage 'minio_cluster_usage_buckets_total_bytes{bucket=~"loki|tempo"}' 2 \ 'bucket,instance,job,namespace' 'loki,tempo' bucket \ '[{"bucket":"loki","job":"minio-aistor","namespace":"object-storage"},{"bucket":"tempo","job":"minio-aistor","namespace":"object-storage"}]' \ '^([0-9]{1,3}[.]){3}[0-9]{1,3}:9000$' || return 1 check_one_query aistor-quota 'minio_cluster_usage_buckets_quota_total_bytes{bucket=~"loki|tempo"} > 0' 2 \ 'bucket,instance,job,namespace' 'loki,tempo' bucket \ '[{"bucket":"loki","job":"minio-aistor","namespace":"object-storage"},{"bucket":"tempo","job":"minio-aistor","namespace":"object-storage"}]' \ '^([0-9]{1,3}[.]){3}[0-9]{1,3}:9000$' || return 1 check_one_query quota-rule 'platform:aistor_bucket_quota_usage_percent' 2 'bucket' 'loki,tempo' bucket \ '[{"bucket":"loki"},{"bucket":"tempo"}]' || return 1 printf 'OBSERVABILITY_MACHINE_METRICS_QUOTA=PASS\n' } check_blackbox_series() { local public='[{"instance":"https://git.learn.hyeonworks.com/api/healthz","job":"blackbox-public-edge","namespace":"observability","observability.hyeonworks.com/probe-group":"public-edge"},{"instance":"https://id.learn.hyeonworks.com/realms/hyeonworks/.well-known/openid-configuration","job":"blackbox-public-edge","namespace":"observability","observability.hyeonworks.com/probe-group":"public-edge"}]' local private='[{"instance":"https://db-admin.learn.hyeonworks.com/","job":"blackbox-private-edge","namespace":"observability","observability.hyeonworks.com/probe-group":"private-edge"},{"instance":"https://grafana.learn.hyeonworks.com/","job":"blackbox-private-edge","namespace":"observability","observability.hyeonworks.com/probe-group":"private-edge"},{"instance":"https://storage-admin.learn.hyeonworks.com/","job":"blackbox-private-edge","namespace":"observability","observability.hyeonworks.com/probe-group":"private-edge"}]' local internal='[{"instance":"http://grafana.observability.svc.cluster.local/api/health","job":"blackbox-private-internal","namespace":"observability","observability.hyeonworks.com/probe-group":"private-internal"},{"instance":"http://minio-aistor-console.object-storage.svc.cluster.local:9090/","job":"blackbox-private-internal","namespace":"observability","observability.hyeonworks.com/probe-group":"private-internal"},{"instance":"http://pgadmin.platform-admin.svc.cluster.local/misc/ping","job":"blackbox-private-internal","namespace":"observability","observability.hyeonworks.com/probe-group":"private-internal"}]' check_one_query probe-public 'probe_success{namespace="observability","observability.hyeonworks.com/probe-group"="public-edge"} == 1' 2 \ 'instance,job,namespace,observability.hyeonworks.com/probe-group' '' '' "$public" || return 1 check_one_query probe-private 'probe_success{namespace="observability","observability.hyeonworks.com/probe-group"="private-edge"} == 1' 3 \ 'instance,job,namespace,observability.hyeonworks.com/probe-group' '' '' "$private" || return 1 check_one_query probe-private-status 'probe_http_status_code{namespace="observability","observability.hyeonworks.com/probe-group"="private-edge"} == 403' 3 \ 'instance,job,namespace,observability.hyeonworks.com/probe-group' '' '' "$private" || return 1 check_one_query probe-internal 'probe_success{namespace="observability","observability.hyeonworks.com/probe-group"="private-internal"} == 1' 3 \ 'instance,job,namespace,observability.hyeonworks.com/probe-group' '' '' "$internal" || return 1 local tls="$WORK/query-probe-tls.json" prom_query 'probe_ssl_earliest_cert_expiry{job=~"blackbox-public-edge|blackbox-private-edge"} > time()' "$tls" || return 1 validate_series_identity_contract "$tls" "$(jq_bounded -cn --argjson public "$public" --argjson private "$private" '$public+$private')" || return 1 printf 'OBSERVABILITY_MACHINE_BLACKBOX=PASS\n' } decode_exact_secret_to_netrc() { local secret_json=$1 output=$2 python_bounded -I -S - "$secret_json" "$output" <<'PY' import base64,binascii,json,os,pathlib,sys source,destination=sys.argv[1:] try: item=json.loads(pathlib.Path(source).read_text()) except Exception: raise SystemExit(1) meta=item.get('metadata') or {}; data=item.get('data') if item.get('kind')!='Secret' or item.get('type')!='Opaque' or meta.get('namespace')!='observability' or meta.get('name')!='grafana-admin' or sorted(data or {})!=['admin-password','admin-user']: raise SystemExit(1) values=[] for key in ('admin-user','admin-password'): try: value=base64.b64decode(data[key],validate=True).decode('utf-8') except (binascii.Error,UnicodeError,ValueError): raise SystemExit(1) if not value or any(ch.isspace() for ch in value) or '\x00' in value: raise SystemExit(1) values.append(value) fd=os.open(destination,os.O_WRONLY|os.O_CREAT|os.O_EXCL,0o600) with os.fdopen(fd,'w') as stream: stream.write('machine localhost\nlogin '+values[0]+'\npassword '+values[1]+'\n') PY } prepare_grafana_auth() { local secret="$WORK/grafana-admin-secret.json" netrc="$WORK/grafana.netrc" extra="$WORK/grafana-curl.conf" if safe_work_file "$netrc" && safe_work_file "$extra"; then printf '%s' "$extra" return 0 fi kubectl_bounded -n observability get secret grafana-admin -o json >"$secret" || return 1 "$CHMOD_BIN" 0600 -- "$secret"; safe_work_file "$secret" || return 1 decode_exact_secret_to_netrc "$secret" "$netrc" || return 1 printf 'netrc\nnetrc-file = "%s"\n' "$netrc" >"$extra"; "$CHMOD_BIN" 0600 -- "$extra" safe_work_file "$netrc" && safe_work_file "$extra" || return 1 printf '%s' "$extra" } grafana_get() { local api_path=$1 output=$2 auth=$3 proxy_get_200 "$(grafana_path "$api_path")" "$output" "$auth" } check_datasources() { local auth prometheus loki tempo loki_config tempo_config tempo_search loki_query tempo_query combined trace encoded query auth="$(prepare_grafana_auth)" || return 1 prometheus="$WORK/grafana-prometheus-health.json"; loki="$WORK/grafana-loki-health.json"; tempo="$WORK/grafana-tempo-health.json" loki_config="$WORK/grafana-loki-config.json"; tempo_config="$WORK/grafana-tempo-config.json" tempo_search="$WORK/grafana-tempo-search.json"; loki_query="$WORK/grafana-loki-query.json" tempo_query="$WORK/grafana-tempo-query.json"; combined="$WORK/grafana-datasource-contract.json" grafana_get '/api/datasources/uid/prometheus/health' "$prometheus" "$auth" || return 1 grafana_get '/api/datasources/uid/loki/health' "$loki" "$auth" || return 1 grafana_get '/api/datasources/uid/tempo/health' "$tempo" "$auth" || return 1 grafana_get '/api/datasources/uid/loki' "$loki_config" "$auth" || return 1 grafana_get '/api/datasources/uid/tempo' "$tempo_config" "$auth" || return 1 encoded="$(jq_bounded -nr --arg value 'service.name=platform-smoke' '$value|@uri')" || return 1 grafana_get "/api/datasources/proxy/uid/tempo/api/search?tags=$encoded&limit=20" "$tempo_search" "$auth" || return 1 trace="$("$TIMEOUT_BIN" --signal=TERM --kill-after=1s 10s "$PYTHON_BIN" -I -S - "$tempo_search" <<'PY' import json,pathlib,re,sys try: item=json.loads(pathlib.Path(sys.argv[1]).read_text()) except Exception: raise SystemExit(1) traces=item.get('traces') if not isinstance(traces,list): raise SystemExit(1) candidates=[] for trace in traces: if not isinstance(trace,dict) or trace.get('rootServiceName')!='platform-smoke': continue identifier=trace.get('traceID') if isinstance(identifier,str) and re.fullmatch(r'[0-9a-f]{32}',identifier): candidates.append(identifier) if not candidates: raise SystemExit(1) print(candidates[0],end='') PY )" || return 1 [[ "$trace" =~ ^[0-9a-f]{32}$ ]] || return 1 query="$(synthetic_loki_query "$trace")" || return 1 encoded="$(jq_bounded -nr --arg value "$query" '$value|@uri')" || return 1 grafana_get "/api/datasources/proxy/uid/loki/loki/api/v1/query_range?query=$encoded&limit=20&direction=backward&since=720h" "$loki_query" "$auth" || return 1 grafana_get "/api/datasources/proxy/uid/tempo/api/traces/$trace" "$tempo_query" "$auth" || return 1 jq_bounded -n --slurpfile prometheus "$prometheus" --slurpfile loki "$loki" --slurpfile tempo "$tempo" \ --slurpfile loki_config "$loki_config" --slurpfile tempo_config "$tempo_config" \ --slurpfile tempo_search "$tempo_search" --slurpfile loki_query "$loki_query" --slurpfile tempo_query "$tempo_query" \ --arg selected_trace "$trace" \ '{prometheus:$prometheus[0],loki:$loki[0],tempo:$tempo[0],loki_config:$loki_config[0],tempo_config:$tempo_config[0],tempo_search:$tempo_search[0],selected_trace:$selected_trace,loki_query:$loki_query[0],tempo_query:$tempo_query[0]}' >"$combined" || return 1 "$CHMOD_BIN" 0600 -- "$combined" validate_datasource_contract "$combined" || return 1 printf 'OBSERVABILITY_MACHINE_DATASOURCES=PASS\n' } run_read_only_machine_checks() { check_dns_host || { fail 'DNS host acceptance failed'; return 1; } check_certificate || { fail 'certificate acceptance failed'; return 1; } check_https_boundaries || { fail 'HTTPS/NodePort boundary acceptance failed'; return 1; } check_targets || { fail 'Prometheus target acceptance failed'; return 1; } check_metric_contracts || { fail 'metric/quota acceptance failed'; return 1; } check_blackbox_series || { fail 'blackbox acceptance failed'; return 1; } check_datasources || { fail 'Grafana datasource/link acceptance failed'; return 1; } } repeat_commit_gate() { validate_commit_environment } validate_owned_object_json() { local file=$1 kind=$2 namespace=$3 name=$4 run=$5 require_uid=${6:-false} jq_bounded -e --arg kind "$kind" --arg namespace "$namespace" --arg name "$name" --arg run "$run" --argjson require_uid "$require_uid" ' .kind==$kind and .metadata.namespace==$namespace and .metadata.name==$name and .metadata.labels["app.kubernetes.io/managed-by"]=="platform-observability-smoke" and .metadata.labels["platform.hyeonworks.com/smoke-run"]==$run and (($require_uid|not) or (.metadata.uid|type=="string" and length>0)) ' "$file" >/dev/null } validate_live_matches_desired() { local desired=$1 live=$2 python_bounded -I -S - "$desired" "$live" <<'PY' import json,pathlib,sys try: desired=json.loads(pathlib.Path(sys.argv[1]).read_text()) live=json.loads(pathlib.Path(sys.argv[2]).read_text()) except Exception: raise SystemExit(1) def subset(want,got): if isinstance(want,dict): return isinstance(got,dict) and all(key in got and subset(value,got[key]) for key,value in want.items()) if isinstance(want,list): return isinstance(got,list) and len(want)==len(got) and all(subset(a,b) for a,b in zip(want,got)) return want==got if desired.get('apiVersion')!=live.get('apiVersion') or desired.get('kind')!=live.get('kind'): raise SystemExit(1) dmeta=desired.get('metadata') or {}; lmeta=live.get('metadata') or {} for field in ('name','namespace'): if dmeta.get(field)!=lmeta.get(field): raise SystemExit(1) if dmeta.get('labels')!=lmeta.get('labels'): raise SystemExit(1) if not isinstance(lmeta.get('uid'),str) or not lmeta['uid'] or not isinstance(lmeta.get('resourceVersion'),str) or not lmeta['resourceVersion']: raise SystemExit(1) if lmeta.get('deletionTimestamp') or lmeta.get('ownerReferences') or lmeta.get('finalizers'): raise SystemExit(1) if not subset(desired.get('spec'),live.get('spec')): raise SystemExit(1) if desired.get('kind')=='Pod': dspec=desired.get('spec') or {}; lspec=live.get('spec') or {} if len(lspec.get('containers') or [])!=len(dspec.get('containers') or []): raise SystemExit(1) if lspec.get('initContainers') or lspec.get('ephemeralContainers'): raise SystemExit(1) PY } create_owned_from_file() { local file=$1 kind=$2 namespace=$3 name=$4 run=$5 resource live rc=0 uid state outcome=0 resource="$(kind_resource "$kind")" || return 1 if state="$(owned_object_state "$kind" "$namespace" "$name" 2>/dev/null)"; then fail "temporary object already exists: $kind/$namespace/$name"; return 1 else [[ $? == 1 ]] || return 1; fi validate_owned_object_json "$file" "$kind" "$namespace" "$name" "$run" false || return 1 kubectl_bounded create --dry-run=server -f "$file" -o json >/dev/null || return 1 live="$WORK/created-${kind}-${name}.json" begin_mutation_critical if kubectl_bounded create -f "$file" -o json >"$live"; then rc=0; else rc=$?; fi "$CHMOD_BIN" 0600 -- "$live" if (( rc != 0 )); then if ! kubectl_bounded -n "$namespace" get "$resource" "$name" --ignore-not-found -o json >"$live"; then emit_manual_recovery; outcome=1 fi fi if (( outcome == 0 )) && [[ ! -s "$live" ]]; then emit_manual_recovery; outcome=1; fi if (( outcome == 0 )) && ! validate_owned_object_json "$live" "$kind" "$namespace" "$name" "$run" true; then emit_manual_recovery; outcome=1 fi if (( outcome == 0 )); then uid="$(jq_bounded -r '.metadata.uid' "$live")" || { emit_manual_recovery; outcome=1; } fi if (( outcome == 0 && rc == 0 )); then # A successful create response is authoritative ownership even if an # admission/defaulting drift makes the object unsafe; register its UID so # the exit transaction can remove only that exact object. register_owned "$kind" "$namespace" "$name" "$uid" "$run" validate_live_matches_desired "$file" "$live" || outcome=1 elif (( outcome == 0 )); then # A nonzero create is response-ambiguous. Copied labels are insufficient: # only an exact desired-spec match can be classified as ours and cleaned. if validate_live_matches_desired "$file" "$live"; then register_owned "$kind" "$namespace" "$name" "$uid" "$run" else emit_manual_recovery fi outcome=1 fi end_mutation_critical return "$outcome" } validate_deployed_ingress_policy_contracts() { local aistor=$1 blackbox=$2 jq_bounded -e ' .apiVersion=="networking.k8s.io/v1" and .kind=="NetworkPolicy" and .metadata.name=="object-storage-allow-prometheus-metrics" and .metadata.namespace=="object-storage" and (.metadata.uid|type=="string" and length>0) and (.metadata.resourceVersion|type=="string" and length>0) and (.metadata.deletionTimestamp==null) and .spec=={ podSelector:{matchLabels:{"aistor.min.io/objectStore":"minio-aistor"}}, policyTypes:["Ingress"], ingress:[{from:[{ namespaceSelector:{matchLabels:{"kubernetes.io/metadata.name":"observability"}}, podSelector:{matchLabels:{ "app.kubernetes.io/name":"prometheus", "app.kubernetes.io/instance":"observability-core-kube-pr-prometheus"}} }],ports:[{protocol:"TCP",port:9000}]}] } ' "$aistor" >/dev/null || return 1 jq_bounded -e ' .apiVersion=="networking.k8s.io/v1" and .kind=="NetworkPolicy" and .metadata.name=="observability-allow-prometheus-to-blackbox" and .metadata.namespace=="observability" and (.metadata.uid|type=="string" and length>0) and (.metadata.resourceVersion|type=="string" and length>0) and (.metadata.deletionTimestamp==null) and .spec=={ podSelector:{matchLabels:{ "app.kubernetes.io/name":"prometheus-blackbox-exporter", "app.kubernetes.io/instance":"blackbox-exporter"}}, policyTypes:["Ingress"], ingress:[{from:[{podSelector:{matchLabels:{ "app.kubernetes.io/name":"prometheus", "app.kubernetes.io/instance":"observability-core-kube-pr-prometheus"}} }],ports:[{protocol:"TCP",port:9115}]}] } ' "$blackbox" >/dev/null } run_network_boundary_transaction() { local run name policy pod minio_ip blackbox_ip pod_result aistor_ingress blackbox_ingress run="$(new_smoke_run_id network)" || return 1 name="$(kubernetes_smoke_name "$run")" || return 1 policy="$WORK/network-policy.json"; pod="$WORK/network-pod.json" aistor_ingress="$WORK/aistor-ingress-policy.json" blackbox_ingress="$WORK/blackbox-ingress-policy.json" kubectl_bounded -n object-storage get networkpolicy object-storage-allow-prometheus-metrics -o json >"$aistor_ingress" || return 1 kubectl_bounded -n observability get networkpolicy observability-allow-prometheus-to-blackbox -o json >"$blackbox_ingress" || return 1 "$CHMOD_BIN" 0600 -- "$aistor_ingress" "$blackbox_ingress" validate_deployed_ingress_policy_contracts "$aistor_ingress" "$blackbox_ingress" || return 1 minio_ip="$(kubectl_bounded -n object-storage get service minio -o jsonpath='{.spec.clusterIP}')" || return 1 blackbox_ip="$(kubectl_bounded -n observability get service blackbox-exporter -o jsonpath='{.spec.clusterIP}')" || return 1 [[ "$minio_ip" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ && "$blackbox_ip" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]] || return 1 jq_bounded -n --arg name "$name" --arg run "$run" '{ apiVersion:"networking.k8s.io/v1",kind:"NetworkPolicy", metadata:{name:$name,namespace:"observability",labels:{ "app.kubernetes.io/managed-by":"platform-observability-smoke", "platform.hyeonworks.com/smoke-run":$run}}, spec:{podSelector:{matchLabels:{"platform.hyeonworks.com/smoke-run":$run}}, policyTypes:["Egress"],egress:[{to:[{ namespaceSelector:{matchLabels:{"kubernetes.io/metadata.name":"kube-system"}}, podSelector:{matchLabels:{"k8s-app":"kube-dns"}}}], ports:[{protocol:"UDP",port:53},{protocol:"TCP",port:53}]},{to:[{ namespaceSelector:{matchLabels:{"kubernetes.io/metadata.name":"object-storage"}}, podSelector:{matchLabels:{"aistor.min.io/objectStore":"minio-aistor"}}}], ports:[{protocol:"TCP",port:9000}]},{to:[{ namespaceSelector:{matchLabels:{"kubernetes.io/metadata.name":"observability"}}, podSelector:{matchLabels:{ "app.kubernetes.io/name":"prometheus-blackbox-exporter", "app.kubernetes.io/instance":"blackbox-exporter"}}}], ports:[{protocol:"TCP",port:9115}]}]}} ' >"$policy" || return 1 "$CHMOD_BIN" 0600 -- "$policy" local command command="set -eu resolved=\$(nslookup $GRAFANA_HOST | awk '/^Address: /{print \$2}' | tail -n1) [ \"\$resolved\" = $LAN_IP ] if nc -z -w 4 $minio_ip 9000; then exit 41; fi if nc -z -w 4 $blackbox_ip 9115; then exit 42; fi printf '%s\\n' OBSERVABILITY_NETWORK_BOUNDARY_PASS" jq_bounded -n --arg name "$name" --arg run "$run" --arg image "$BUSYBOX_IMAGE" --arg command "$command" '{ apiVersion:"v1",kind:"Pod",metadata:{name:$name,namespace:"observability",labels:{ "app.kubernetes.io/managed-by":"platform-observability-smoke", "platform.hyeonworks.com/smoke-run":$run}}, spec:{automountServiceAccountToken:false,restartPolicy:"Never",terminationGracePeriodSeconds:1, securityContext:{runAsNonRoot:true,runAsUser:65534,runAsGroup:65534,seccompProfile:{type:"RuntimeDefault"}}, containers:[{name:"network-smoke",image:$image,imagePullPolicy:"IfNotPresent", command:["sh","-ec",$command],securityContext:{allowPrivilegeEscalation:false, readOnlyRootFilesystem:true,capabilities:{drop:["ALL"]}}}]}} ' >"$pod" || return 1 "$CHMOD_BIN" 0600 -- "$pod" create_owned_from_file "$policy" NetworkPolicy observability "$name" "$run" || return 1 create_owned_from_file "$pod" Pod observability "$name" "$run" || return 1 kubectl_wait_90_bounded -n observability wait --for=jsonpath='{.status.phase}'=Succeeded \ --timeout=90s "pod/$name" >/dev/null || return 1 pod_result="$(kubectl_bounded -n observability logs "$name" -c network-smoke)" || return 1 [[ "$pod_result" == OBSERVABILITY_NETWORK_BOUNDARY_PASS ]] || return 1 cleanup_owned_objects || return 1 printf 'OBSERVABILITY_MACHINE_NETWORK_POLICY=PASS\n' } canonical_object_snapshot() { local file=$1 jq_bounded -S '{metadata:{name:.metadata.name,namespace:.metadata.namespace,uid:.metadata.uid},data:(.data//{}),status:(.status//{})}' "$file" } list_one_ready_pod() { local selector=$1 output=$2 kubectl_bounded -n observability get pods -l "$selector" -o json >"$output" || return 1 "$CHMOD_BIN" 0600 -- "$output" jq_bounded -e '.items|length==1 and .[0].metadata.uid and any(.[0].status.conditions[]?; .type=="Ready" and .status=="True")' "$output" >/dev/null } delete_pod_uid_for_restart() { local name=$1 uid=$2 body response path status attempt current body="$("$MKTEMP_BIN" "$WORK/restart-delete.XXXXXXXX")" || return 1 response="$("$MKTEMP_BIN" "$WORK/restart-response.XXXXXXXX")" || return 1 "$CHMOD_BIN" 0600 -- "$body" "$response" jq_bounded -nc --arg uid "$uid" '{apiVersion:"meta.k8s.io/v1",kind:"DeleteOptions",propagationPolicy:"Background",preconditions:{uid:$uid}}' >"$body" path="/api/v1/namespaces/observability/pods/$name" status="$(proxy_request DELETE "$path" "$response" "$body")" || status=000 for ((attempt=0; attempt<60; attempt++)); do current="$(kubectl_bounded -n observability get pod "$name" --ignore-not-found -o json 2>/dev/null | jq_bounded -r '.metadata.uid//empty')" || return 1 [[ -z "$current" || "$current" != "$uid" ]] && return 0 /usr/bin/sleep 1 done return 1 } fetch_silences() { local output=$1 status status="$(proxy_request GET "$(alertmanager_path '/api/v2/silences')" "$output")" || return 1 [[ "$status" == 200 ]] || return 1 jq_bounded -e 'type=="array"' "$output" >/dev/null } silence_inventory_state() { local source=$1 run=$2 python_bounded -I -S - "$source" "$run" <<'PY' import json,pathlib,re,sys try: items=json.loads(pathlib.Path(sys.argv[1]).read_text()) except Exception: raise SystemExit(1) run=sys.argv[2] if not isinstance(items,list) or re.fullmatch(r'persistence-[0-9]{8}t[0-9]{6}z-[0-9a-f]{32}',run) is None: raise SystemExit(1) related=[]; active=[]; expired=[]; structural=[] expected={ ('alertname','PlatformObservabilityPersistenceSmoke',False,True), ('instance',run,False,True), } for item in items: if not isinstance(item,dict): raise SystemExit(1) matchers=item.get('matchers') if not isinstance(matchers,list): continue tuples=[] malformed=False for matcher in matchers: if not isinstance(matcher,dict): malformed=True; break value=(matcher.get('name'),matcher.get('value'),matcher.get('isRegex'),matcher.get('isEqual')) tuples.append(value) if malformed: continue if any(value[0]=='instance' and value[1]==run for value in tuples): related.append(item) state=((item.get('status') or {}).get('state')) if set(tuples)==expected and len(tuples)==2 and item.get('createdBy')=='platform-observability-smoke' and item.get('comment')=='Temporary restart-persistence acceptance '+run: identifier=item.get('id') if not isinstance(identifier,str) or re.fullmatch(r'[A-Za-z0-9][A-Za-z0-9._-]*',identifier) is None: raise SystemExit(1) structural.append(identifier) if state in ('active','pending'): active.append(identifier) elif state=='expired': expired.append(identifier) print(f'{len(related)}|{len(active)}|{len(expired)}|{structural[0] if len(structural)==1 else ""}') PY } read_silence_state() { local run=$1 output output="$("$MKTEMP_BIN" "$WORK/silence-list.XXXXXXXX")" || return 1 "$CHMOD_BIN" 0600 -- "$output" fetch_silences "$output" || return 1 silence_inventory_state "$output" "$run" } create_persistence_silence() { local body="$WORK/silence-create.json" response="$WORK/silence-create-response.json" local status='' start end state related active expired observed_id response_id='' request_rc=0 attempt outcome=1 classified=false SILENCE_RUN="$(new_smoke_run_id persistence)" || return 1 state="$(read_silence_state "$SILENCE_RUN")" || return 1 IFS='|' read -r related active expired observed_id <<<"$state" if [[ "$related" != 0 || "$active" != 0 || "$expired" != 0 || -n "$observed_id" ]]; then emit_manual_recovery; return 1 fi start="$("$TIMEOUT_BIN" --signal=TERM --kill-after=1s 5s "$DATE_BIN" -u +%Y-%m-%dT%H:%M:%SZ)" || return 1 end="$("$TIMEOUT_BIN" --signal=TERM --kill-after=1s 5s "$DATE_BIN" -u -d '+20 minutes' +%Y-%m-%dT%H:%M:%SZ)" || return 1 jq_bounded -n --arg start "$start" --arg end "$end" --arg run "$SILENCE_RUN" '{ matchers:[ {name:"alertname",value:"PlatformObservabilityPersistenceSmoke",isRegex:false,isEqual:true}, {name:"instance",value:$run,isRegex:false,isEqual:true} ],startsAt:$start,endsAt:$end,createdBy:"platform-observability-smoke", comment:("Temporary restart-persistence acceptance "+$run) }' >"$body" || return 1 "$CHMOD_BIN" 0600 -- "$body" begin_mutation_critical status="$(proxy_request POST "$(alertmanager_path '/api/v2/silences')" "$response" "$body")" || request_rc=$? if (( request_rc == 0 )) && [[ "$status" == 200 ]]; then response_id="$(jq_bounded -er '.silenceID|select(type=="string" and test("^[A-Za-z0-9][A-Za-z0-9._-]*$"))' "$response" 2>/dev/null)" || response_id='' fi for ((attempt=0; attempt= 2 )); then SILENCE_ID=''; SILENCE_RUN=''; return 0; fi else terminal=0 if [[ "$related" != 1 || "$active" != 1 || "$expired" != 0 || "$observed_id" != "$SILENCE_ID" ]]; then emit_manual_recovery; return 1 fi fi (( SILENCE_POLL_DELAY == 0 )) || /usr/bin/sleep "$SILENCE_POLL_DELAY" done emit_manual_recovery return 1 } run_restart_persistence_transaction() { local grafana_before="$WORK/grafana-pod-before.json" grafana_after="$WORK/grafana-pod-after.json" local alert_before="$WORK/alertmanager-pod-before.json" alert_after="$WORK/alertmanager-pod-after.json" local pvc_before="$WORK/pvc-before.json" pvc_after="$WORK/pvc-after.json" local dashboards_before="$WORK/dashboards-before.json" dashboards_after="$WORK/dashboards-after.json" local api_before="$WORK/dashboard-api-before.json" api_after="$WORK/dashboard-api-after.json" local silence="$WORK/silence-after-restart.json" auth name uid state related active expired observed_id auth="$WORK/grafana-curl.conf"; safe_work_file "$auth" || return 1 list_one_ready_pod 'app.kubernetes.io/name=grafana,app.kubernetes.io/instance=grafana' "$grafana_before" || return 1 list_one_ready_pod 'app.kubernetes.io/name=alertmanager,app.kubernetes.io/instance=observability-core-kube-pr-alertmanager' "$alert_before" || return 1 { kubectl_bounded -n observability get pvc grafana -o json kubectl_bounded -n observability get pvc -l 'app.kubernetes.io/name=alertmanager,app.kubernetes.io/instance=observability-core-kube-pr-alertmanager' -o json } | jq_bounded -s '.[0] as $grafana | .[1] as $am | {grafana:{name:$grafana.metadata.name,uid:$grafana.metadata.uid,phase:$grafana.status.phase},alertmanager:[$am.items[]|{name:.metadata.name,uid:.metadata.uid,phase:.status.phase}]}' >"$pvc_before" || return 1 kubectl_bounded -n observability get configmaps -l grafana_dashboard=1 -o json | jq_bounded -S \ '[.items[]|{name:.metadata.name,uid:.metadata.uid,data:.data}]|sort_by(.name)' >"$dashboards_before" || return 1 grafana_get '/api/search?type=dash-db' "$api_before" "$auth" || return 1 jq_bounded -S '[.[]|{uid,title,type}]|sort_by(.uid)' "$api_before" >"$api_before.canonical" || return 1 "$MV_BIN" -- "$api_before.canonical" "$api_before" "$CHMOD_BIN" 0600 -- "$pvc_before" "$dashboards_before" "$api_before" jq_bounded -e '.grafana.phase=="Bound" and (.alertmanager|length==1) and all(.alertmanager[];.phase=="Bound")' "$pvc_before" >/dev/null || return 1 jq_bounded -e 'length==5' "$dashboards_before" >/dev/null || return 1 jq_bounded -e 'length==5 and all(.[];.uid|type=="string" and length>0)' "$api_before" >/dev/null || return 1 create_persistence_silence || return 1 name="$(jq_bounded -r '.items[0].metadata.name' "$grafana_before")"; uid="$(jq_bounded -r '.items[0].metadata.uid' "$grafana_before")" delete_pod_uid_for_restart "$name" "$uid" || return 1 kubectl_wait_180_bounded -n observability rollout status deployment/grafana --timeout=180s >/dev/null || return 1 name="$(jq_bounded -r '.items[0].metadata.name' "$alert_before")"; uid="$(jq_bounded -r '.items[0].metadata.uid' "$alert_before")" delete_pod_uid_for_restart "$name" "$uid" || return 1 kubectl_wait_180_bounded -n observability rollout status statefulset/alertmanager-observability-core-kube-pr-alertmanager --timeout=180s >/dev/null || return 1 list_one_ready_pod 'app.kubernetes.io/name=grafana,app.kubernetes.io/instance=grafana' "$grafana_after" || return 1 list_one_ready_pod 'app.kubernetes.io/name=alertmanager,app.kubernetes.io/instance=observability-core-kube-pr-alertmanager' "$alert_after" || return 1 [[ "$(jq_bounded -r '.items[0].metadata.uid' "$grafana_before")" != "$(jq_bounded -r '.items[0].metadata.uid' "$grafana_after")" ]] || return 1 [[ "$(jq_bounded -r '.items[0].metadata.uid' "$alert_before")" != "$(jq_bounded -r '.items[0].metadata.uid' "$alert_after")" ]] || return 1 { kubectl_bounded -n observability get pvc grafana -o json kubectl_bounded -n observability get pvc -l 'app.kubernetes.io/name=alertmanager,app.kubernetes.io/instance=observability-core-kube-pr-alertmanager' -o json } | jq_bounded -s '.[0] as $grafana | .[1] as $am | {grafana:{name:$grafana.metadata.name,uid:$grafana.metadata.uid,phase:$grafana.status.phase},alertmanager:[$am.items[]|{name:.metadata.name,uid:.metadata.uid,phase:.status.phase}]}' >"$pvc_after" || return 1 kubectl_bounded -n observability get configmaps -l grafana_dashboard=1 -o json | jq_bounded -S \ '[.items[]|{name:.metadata.name,uid:.metadata.uid,data:.data}]|sort_by(.name)' >"$dashboards_after" || return 1 grafana_get '/api/search?type=dash-db' "$api_after" "$auth" || return 1 jq_bounded -S '[.[]|{uid,title,type}]|sort_by(.uid)' "$api_after" >"$api_after.canonical" || return 1 "$MV_BIN" -- "$api_after.canonical" "$api_after" "$CMP_BIN" --silent -- "$pvc_before" "$pvc_after" || return 1 "$CMP_BIN" --silent -- "$dashboards_before" "$dashboards_after" || return 1 "$CMP_BIN" --silent -- "$api_before" "$api_after" || return 1 fetch_silences "$silence" || return 1 state="$(silence_inventory_state "$silence" "$SILENCE_RUN")" || return 1 IFS='|' read -r related active expired observed_id <<<"$state" [[ "$related" == 1 && "$active" == 1 && "$expired" == 0 && "$observed_id" == "$SILENCE_ID" ]] || return 1 delete_persistence_silence || return 1 printf 'OBSERVABILITY_MACHINE_RESTART_PERSISTENCE=PASS\n' } run_certbot_acceptance() { "$TIMEOUT_BIN" --signal=TERM --kill-after=2s 30s "$SUDO_BIN" -v || return 1 "$TIMEOUT_BIN" --signal=TERM --kill-after=1s 10s "$SUDO_BIN" -n /usr/bin/true || return 1 "$TIMEOUT_BIN" --signal=TERM --kill-after=5s 610s "$SUDO_BIN" -n "$TIMEOUT_BIN" --signal=TERM --kill-after=5s 600s \ /snap/bin/certbot renew --dry-run --run-deploy-hooks >/dev/null || return 1 [[ "$("$TIMEOUT_BIN" --signal=TERM --kill-after=1s 10s "$SUDO_BIN" -n /usr/bin/systemctl is-active nginx)" == active ]] || return 1 printf 'OBSERVABILITY_MACHINE_CERTBOT=PASS\n' } decode_keycloak_admin_secret() { local secret_json=$1 username_file=$2 password_file=$3 python_bounded -I -S - "$secret_json" "$username_file" "$password_file" <<'PY' import base64,binascii,json,os,pathlib,sys source,user_path,password_path=sys.argv[1:] try: item=json.loads(pathlib.Path(source).read_text()) except Exception: raise SystemExit(1) meta=item.get('metadata') or {}; data=item.get('data') if item.get('kind')!='Secret' or item.get('type')!='Opaque' or meta.get('namespace')!='keycloak' or meta.get('name')!='keycloak-initial-admin' or sorted(data or {})!=['password','username']: raise SystemExit(1) for key,destination in [('username',user_path),('password',password_path)]: try: value=base64.b64decode(data[key],validate=True) except (binascii.Error,ValueError): raise SystemExit(1) if not value or b'\x00' in value or b'\r' in value or b'\n' in value: raise SystemExit(1) fd=os.open(destination,os.O_WRONLY|os.O_CREAT|os.O_EXCL,0o600) with os.fdopen(fd,'wb') as stream: stream.write(value) PY } prepare_keycloak_auth() { local secret="$WORK/keycloak-admin-secret.json" username="$WORK/keycloak-admin-user" password="$WORK/keycloak-admin-password" local form="$WORK/keycloak-token-form" config="$WORK/keycloak-token-curl.conf" response="$WORK/keycloak-token.json" status token kubectl_bounded -n keycloak get secret keycloak-initial-admin -o json >"$secret" || return 1 "$CHMOD_BIN" 0600 -- "$secret"; decode_keycloak_admin_secret "$secret" "$username" "$password" || return 1 python_bounded -I -S - "$username" "$password" "$form" <<'PY' import os,pathlib,sys,urllib.parse user,password,destination=sys.argv[1:] values={'grant_type':'password','client_id':'admin-cli','username':pathlib.Path(user).read_text(),'password':pathlib.Path(password).read_text()} fd=os.open(destination,os.O_WRONLY|os.O_CREAT|os.O_EXCL,0o600) with os.fdopen(fd,'w') as stream: stream.write(urllib.parse.urlencode(values)) PY printf 'header = "Content-Type: application/x-www-form-urlencoded"\ndata-binary = "@%s"\n' "$form" >"$config"; "$CHMOD_BIN" 0600 -- "$config" status="$(proxy_request POST "$(keycloak_path '/realms/master/protocol/openid-connect/token')" "$response" '' "$config")" || return 1 [[ "$status" == 200 ]] || return 1 token="$(jq_bounded -er '.access_token|select(type=="string" and test("^[A-Za-z0-9._~-]+$"))' "$response")" || return 1 KEYCLOAK_AUTH_CONFIG="$WORK/keycloak-auth.conf" printf 'header = "Authorization: Bearer %s"\n' "$token" >"$KEYCLOAK_AUTH_CONFIG"; "$CHMOD_BIN" 0600 -- "$KEYCLOAK_AUTH_CONFIG" safe_work_file "$KEYCLOAK_AUTH_CONFIG" } keycloak_request() { local method=$1 api_path=$2 output=$3 body=${4:-} status safe_work_file "$KEYCLOAK_AUTH_CONFIG" || return 1 status="$(proxy_request "$method" "$(keycloak_path "$api_path")" "$output" "$body" "$KEYCLOAK_AUTH_CONFIG")" || return 1 printf '%s' "$status" } keycloak_lookup_group() { local name=$1 expected_path=$2 output=$3 encoded status encoded="$(jq_bounded -nr --arg value "$name" '$value|@uri')" || return 1 status="$(keycloak_request GET "/admin/realms/hyeonworks/groups?search=$encoded&exact=true&max=2" "$output")" || return 1 [[ "$status" == 200 ]] || return 1 jq_bounded -er --arg name "$name" --arg path "$expected_path" ' [ .[] | select(.name==$name and .path==$path) ] as $items | select(($items|length)==1) | $items[0].id | select(type=="string" and test("^[A-Za-z0-9._-]+$")) ' "$output" } keycloak_lookup_user() { local username=$1 output=$2 encoded status encoded="$(jq_bounded -nr --arg value "$username" '$value|@uri')" || return 1 status="$(keycloak_request GET "/admin/realms/hyeonworks/users?username=$encoded&exact=true&max=2" "$output")" || return 1 [[ "$status" == 200 ]] || return 1 jq_bounded -er --arg username "$username" ' [ .[] | select(.username==$username and .enabled==true) ] as $items | select(($items|length)==1) | $items[0].id | select(type=="string" and test("^[A-Za-z0-9._-]+$")) ' "$output" } keycloak_memberships() { local output=$1 status page aggregate next count first=0 pages=0 aggregate="$("$MKTEMP_BIN" "$WORK/keycloak-memberships-all.XXXXXXXX")" || return 1 printf '[]\n' >"$aggregate"; "$CHMOD_BIN" 0600 -- "$aggregate" while (( pages < 100 )); do page="$("$MKTEMP_BIN" "$WORK/keycloak-memberships-page.XXXXXXXX")" || return 1 "$CHMOD_BIN" 0600 -- "$page" status="$(keycloak_request GET "/admin/realms/hyeonworks/users/$KEYCLOAK_USER_ID/groups?briefRepresentation=true&first=$first&max=100" "$page")" || return 1 [[ "$status" == 200 ]] || return 1 count="$(jq_bounded -er 'select(type=="array" and length<=100)|length' "$page")" || return 1 next="$("$MKTEMP_BIN" "$WORK/keycloak-memberships-next.XXXXXXXX")" || return 1 "$CHMOD_BIN" 0600 -- "$next" jq_bounded -s '.[0]+.[1]' "$aggregate" "$page" >"$next" || return 1 "$MV_BIN" -- "$next" "$aggregate" pages=$((pages + 1)) (( count == 100 )) || break first=$((first + 100)) done (( pages < 100 || count < 100 )) || return 1 jq_bounded -S ' select(type=="array" and all(.[]; (.id|type=="string" and test("^[A-Za-z0-9._-]+$")) and (.name|type=="string" and length>0) and (.path|type=="string" and startswith("/")))) | select(([.[].id]|length)==([.[].id]|unique|length)) | sort_by(.id) ' "$aggregate" >"$output" || return 1 "$CHMOD_BIN" 0600 -- "$output" } keycloak_membership_state() { local memberships=$1 group_id=$2 jq_bounded -er --arg id "$group_id" ' [ .[] | select(.id==$id) ] as $items | if ($items|length)==0 then "absent" elif ($items|length)==1 and $items[0].name=="platform-observability-viewers" and $items[0].path=="/platform-observability-viewers" then "present" else error("ambiguous viewer membership") end ' "$memberships" } capture_keycloak_absent_baseline() { local admin_group_id=$1 first second state [[ "$admin_group_id" =~ ^[A-Za-z0-9._-]+$ && "$KEYCLOAK_VIEWER_GROUP_ID" =~ ^[A-Za-z0-9._-]+$ ]] || return 1 first="$("$MKTEMP_BIN" "$WORK/keycloak-memberships-before-a.XXXXXXXX")" || return 1 second="$("$MKTEMP_BIN" "$WORK/keycloak-memberships-before-b.XXXXXXXX")" || return 1 keycloak_memberships "$first" || return 1 keycloak_memberships "$second" || return 1 "$CMP_BIN" --silent -- "$first" "$second" || return 1 state="$(keycloak_membership_state "$first" "$KEYCLOAK_VIEWER_GROUP_ID")" || return 1 [[ "$state" == absent ]] || return 1 jq_bounded -e --arg admin "$admin_group_id" 'all(.[]?; .id!=$admin)' "$first" >/dev/null || return 1 [[ -n "$KEYCLOAK_MEMBERSHIP_BEFORE" && "$KEYCLOAK_MEMBERSHIP_BEFORE" == "$WORK"/* ]] || return 1 "$MV_BIN" -- "$first" "$KEYCLOAK_MEMBERSHIP_BEFORE" "$CHMOD_BIN" 0600 -- "$KEYCLOAK_MEMBERSHIP_BEFORE" } add_keycloak_viewer_membership() { local response memberships status=000 state outcome=1 [[ -f "$KEYCLOAK_MEMBERSHIP_BEFORE" && ! -L "$KEYCLOAK_MEMBERSHIP_BEFORE" ]] || return 1 [[ "$(keycloak_membership_state "$KEYCLOAK_MEMBERSHIP_BEFORE" "$KEYCLOAK_VIEWER_GROUP_ID")" == absent ]] || return 1 response="$WORK/keycloak-membership-add-response.json" memberships="$WORK/keycloak-memberships-added.json" begin_mutation_critical KEYCLOAK_MEMBERSHIP_PENDING=true status="$(keycloak_request PUT "/admin/realms/hyeonworks/users/$KEYCLOAK_USER_ID/groups/$KEYCLOAK_VIEWER_GROUP_ID" "$response")" || status=000 if keycloak_memberships "$memberships"; then state="$(keycloak_membership_state "$memberships" "$KEYCLOAK_VIEWER_GROUP_ID")" || state=ambiguous if [[ "$state" == present ]]; then KEYCLOAK_TEST_ADDED=true KEYCLOAK_MEMBERSHIP_PENDING=false outcome=0 elif [[ "$state" == absent ]]; then KEYCLOAK_MEMBERSHIP_PENDING=false fi fi end_mutation_critical (( outcome == 0 )) || return 1 [[ "$status" == 204 || "$status" == 000 ]] } cleanup_keycloak_membership() { local response status memberships verify state [[ ( "$KEYCLOAK_TEST_ADDED" == true || "$KEYCLOAK_MEMBERSHIP_PENDING" == true ) && "$KEYCLOAK_MEMBERSHIP_REMOVED" == false ]] || return 0 [[ "$KEYCLOAK_USER_ID" =~ ^[A-Za-z0-9._-]+$ && "$KEYCLOAK_VIEWER_GROUP_ID" =~ ^[A-Za-z0-9._-]+$ ]] || { emit_manual_recovery; return 1; } [[ -f "$KEYCLOAK_MEMBERSHIP_BEFORE" && ! -L "$KEYCLOAK_MEMBERSHIP_BEFORE" && "$(keycloak_membership_state "$KEYCLOAK_MEMBERSHIP_BEFORE" "$KEYCLOAK_VIEWER_GROUP_ID" 2>/dev/null)" == absent ]] || { emit_manual_recovery; return 1; } memberships="$WORK/keycloak-cleanup-memberships-before.json" keycloak_memberships "$memberships" || { emit_manual_recovery; return 1; } state="$(keycloak_membership_state "$memberships" "$KEYCLOAK_VIEWER_GROUP_ID")" || { emit_manual_recovery; return 1; } if [[ "$state" == absent ]]; then KEYCLOAK_MEMBERSHIP_PENDING=false; KEYCLOAK_MEMBERSHIP_REMOVED=true return 0 fi response="$WORK/keycloak-cleanup-response.json" status="$(keycloak_request DELETE "/admin/realms/hyeonworks/users/$KEYCLOAK_USER_ID/groups/$KEYCLOAK_VIEWER_GROUP_ID" "$response")" || status=000 verify="$WORK/keycloak-cleanup-memberships-after.json" keycloak_memberships "$verify" || { emit_manual_recovery; return 1; } [[ "$(keycloak_membership_state "$verify" "$KEYCLOAK_VIEWER_GROUP_ID")" == absent ]] || { emit_manual_recovery; return 1; } KEYCLOAK_MEMBERSHIP_PENDING=false KEYCLOAK_MEMBERSHIP_REMOVED=true } run_oidc_acceptance() { local identity admin_identity viewer_identity denied_identity removal_identity local admin_group viewer_group user memberships response status encoded grafana_user grafana_auth deadline now printf '%s\n' 'Browser check: organization Admin and server-admin=false.' >&2 admin_identity="$(read_identity 'OIDC organization Admin')" || return 1 require_confirmation OIDC_ADMIN_CONFIRMED || return 1 printf '%s\n' 'Browser check: Viewer and edit/admin actions denied.' >&2 viewer_identity="$(read_identity 'OIDC Viewer')" || return 1 require_confirmation OIDC_VIEWER_CONFIRMED || return 1 printf '%s\n' 'Browser check: no observability groups and login denied.' >&2 denied_identity="$(read_identity 'OIDC denied-user')" || return 1 require_confirmation OIDC_DENIED_CONFIRMED || return 1 printf '%s\n' 'Browser check: local break-glass login and server-admin=true.' >&2 require_confirmation BREAK_GLASS_CONFIRMED || return 1 [[ "$admin_identity" != "$viewer_identity" && "$admin_identity" != "$denied_identity" && "$viewer_identity" != "$denied_identity" ]] || return 1 removal_identity="$(read_identity 'group-removal test')" || return 1 [[ "$removal_identity" != "$admin_identity" && "$removal_identity" != "$viewer_identity" && "$removal_identity" != "$denied_identity" ]] || return 1 prepare_keycloak_auth || return 1 admin_group="$WORK/keycloak-admin-group.json"; viewer_group="$WORK/keycloak-viewer-group.json"; user="$WORK/keycloak-removal-user.json" local admin_group_id admin_group_id="$(keycloak_lookup_group platform-observability-admins /platform-observability-admins "$admin_group")" || return 1 KEYCLOAK_VIEWER_GROUP_ID="$(keycloak_lookup_group platform-observability-viewers /platform-observability-viewers "$viewer_group")" || return 1 KEYCLOAK_USER_ID="$(keycloak_lookup_user "$removal_identity" "$user")" || return 1 KEYCLOAK_MEMBERSHIP_BEFORE="$WORK/keycloak-memberships-before.json" capture_keycloak_absent_baseline "$admin_group_id" || return 1 add_keycloak_viewer_membership || return 1 printf '%s\n' 'Browser check: removal-test user is Viewer before membership removal.' >&2 require_confirmation REMOVAL_VIEWER_CONFIRMED || return 1 cleanup_keycloak_membership || return 1 response="$WORK/keycloak-session-revoke.json" status="$(keycloak_request POST "/admin/realms/hyeonworks/users/$KEYCLOAK_USER_ID/logout" "$response")" || return 1 [[ "$status" == 204 ]] || return 1 grafana_auth="$WORK/grafana-curl.conf"; safe_work_file "$grafana_auth" || return 1 encoded="$(jq_bounded -nr --arg value "$removal_identity" '$value|@uri')" || return 1 grafana_user="$WORK/grafana-removal-user.json" grafana_get "/api/users/lookup?loginOrEmail=$encoded" "$grafana_user" "$grafana_auth" || return 1 local grafana_id grafana_id="$(jq_bounded -er '.id|select(type=="number" and .>0 and floor==.)' "$grafana_user")" || return 1 response="$WORK/grafana-session-revoke.json" status="$(proxy_request POST "$(grafana_path "/api/admin/users/$grafana_id/logout")" "$response" '' "$grafana_auth")" || return 1 [[ "$status" == 200 || "$status" == 204 ]] || return 1 now="$("$TIMEOUT_BIN" --signal=TERM --kill-after=1s 5s "$DATE_BIN" -u +%s)" || return 1 [[ "$now" =~ ^[0-9]+$ ]] || return 1 deadline=$((now + HUMAN_TIMEOUT_SECONDS)) printf '%s\n' 'Within five minutes, verify old session rejection and OIDC relogin denial.' >&2 require_confirmation_before SESSION_REVOKED "$deadline" || return 1 require_confirmation_before RELOGIN_DENIED "$deadline" || return 1 printf 'OBSERVABILITY_OIDC_ACCEPTANCE=PASS\n' } wait_for_smoke_alert() { local run=$1 expected=$2 attempt query alerts for ((attempt=0; attempt<60; attempt++)); do query="$WORK/slack-query-$attempt.json"; alerts="$WORK/slack-alerts-$attempt.json" prom_query "ALERTS{alertname=\"PlatformObservabilitySmoke\",instance=\"$run\",alertstate=\"firing\"}" "$query" || return 1 proxy_get_200 "$(alertmanager_path '/api/v2/alerts')" "$alerts" || return 1 if [[ "$expected" == present ]]; then if jq_bounded -e --arg run "$run" --arg runbook "$RUNBOOK_URL" ' (.status=="success" and (.data.result|length)==1 and .data.result[0].metric.alertname=="PlatformObservabilitySmoke" and .data.result[0].metric.instance==$run) ' "$query" >/dev/null && jq_bounded -e --arg run "$run" --arg runbook "$RUNBOOK_URL" ' [.[]|select(.labels.alertname=="PlatformObservabilitySmoke" and .labels.instance==$run)] as $items | ($items|length)==1 and $items[0].labels.cluster=="home" and $items[0].labels.namespace=="observability" and $items[0].labels.severity=="info" and $items[0].annotations.runbook_url==$runbook ' "$alerts" >/dev/null; then return 0; fi else if jq_bounded -e '.status=="success" and (.data.result|length)==0' "$query" >/dev/null && jq_bounded -e --arg run "$run" '[.[]|select(.labels.alertname=="PlatformObservabilitySmoke" and .labels.instance==$run)]|length==0' "$alerts" >/dev/null; then return 0; fi fi /usr/bin/sleep 5 done return 1 } run_slack_acceptance() { local run name rule run="$(new_smoke_run_id alert)" || return 1 name="$(kubernetes_smoke_name "$run")" || return 1 rule="$WORK/slack-smoke-rule.json" jq_bounded -n --arg name "$name" --arg run "$run" --arg runbook "$RUNBOOK_URL" '{ apiVersion:"monitoring.coreos.com/v1",kind:"PrometheusRule", metadata:{name:$name,namespace:"observability",labels:{ "app.kubernetes.io/managed-by":"platform-observability-smoke", "platform.hyeonworks.com/smoke-run":$run, "observability.hyeonworks.com/instance":"home"}}, spec:{groups:[{name:("platform.observability-smoke."+$run),rules:[{ alert:"PlatformObservabilitySmoke",expr:"vector(1)",for:"0m", labels:{cluster:"home",namespace:"observability",instance:$run,severity:"info"}, annotations:{summary:"Observability acceptance smoke",runbook_url:$runbook} }]}]}} ' >"$rule" || return 1 "$CHMOD_BIN" 0600 -- "$rule" create_owned_from_file "$rule" PrometheusRule observability "$name" "$run" || return 1 wait_for_smoke_alert "$run" present || return 1 printf '%s\n' 'Confirm the Slack message groups cluster, namespace, alertname and includes severity/runbook.' >&2 require_confirmation FIRING || return 1 cleanup_owned_objects || return 1 printf '%s\n' 'Confirm the matching Slack resolved message.' >&2 require_confirmation RESOLVED || return 1 wait_for_smoke_alert "$run" absent || return 1 printf 'OBSERVABILITY_SLACK_ACCEPTANCE=PASS\n' } cleanup_all() { local rc=0 cleanup_owned_objects || rc=1 cleanup_keycloak_membership || rc=1 delete_persistence_silence || rc=1 if [[ -n "$API_PROXY_PID" ]]; then stop_owned_process "$API_PROXY_PID" "$API_PROXY_START" || rc=1 API_PROXY_PID=''; API_PROXY_START='' fi if [[ -n "$WORK" ]]; then if [[ "$WORK" == /tmp/platform-observability-smoke.???????? && -d "$WORK" && ! -L "$WORK" ]]; then "$RM_BIN" -rf -- "$WORK" WORK='' else emit_manual_recovery; rc=1 fi fi return "$rc" } on_exit() { local rc=$? [[ "$EXIT_HANDLER_RUNNING" == false ]] || exit "$rc" EXIT_HANDLER_RUNNING=true trap - EXIT HUP INT TERM cleanup_all || { (( rc != 0 )) || rc=1; } exit "$rc" } main() { parse_arguments "$@" || return 1 if [[ "$TEST_MODE" == false ]]; then reject_production_overrides || return 1; fi if [[ "$execute_requested" == false ]]; then print_dry_run; return 0; fi trap on_exit EXIT trap 'handle_termination_signal 129' HUP trap 'handle_termination_signal 130' INT trap 'handle_termination_signal 143' TERM require_execute_environment || { fail 'execute preflight failed'; return 1; } run_read_only_machine_checks || return 1 require_confirmation 'APPLY default' || { fail 'confirmation did not match APPLY default'; return 1; } repeat_commit_gate || { fail 'post-confirmation commit gate changed'; return 1; } run_network_boundary_transaction || { fail 'network boundary transaction failed'; return 1; } run_restart_persistence_transaction || { fail 'restart persistence transaction failed'; return 1; } run_certbot_acceptance || { fail 'Certbot/Nginx acceptance failed'; return 1; } repeat_commit_gate || { fail 'post-mutation context/API gate changed'; return 1; } run_read_only_machine_checks || { fail 'post-mutation machine acceptance failed'; return 1; } printf 'OBSERVABILITY_MACHINE_ACCEPTANCE=PASS\n' run_oidc_acceptance || { fail 'OIDC human/session acceptance failed'; return 1; } run_slack_acceptance || { fail 'Slack firing/resolved acceptance failed'; return 1; } local external_token external_token="$(external_attestation_token "$PUBLIC_EDGE_IP")" || return 1 render_external_attestation "$PUBLIC_EDGE_IP" >&2 || return 1 if ! require_confirmation "$external_token"; then printf 'OBSERVABILITY_EXTERNAL_BOUNDARY=PARTIAL\n' return 2 fi printf '%s\n' 'OBSERVABILITY_EXTERNAL_BOUNDARY=PASS' 'OBSERVABILITY_SMOKE=PASS' } if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then main "$@" fi