#!/usr/bin/env bash
# Disposable local Kubernetes lab: a Ready Certificate with the wrong Ingress Secret.
# Requires Linux, Bash, Docker, kind, kubectl, curl, OpenSSL, GNU timeout/coreutils.
# Tested: kind 0.33.0, Kubernetes 1.35.8, cert-manager 1.21.3, Traefik 3.7.16.
# Creates and deletes its own cluster. Never uses the current kubectl context.
set -euo pipefail
umask 077
for command in docker kind kubectl curl openssl timeout sha256sum base64; do
  command -v "$command" >/dev/null
done
export KIND_EXPERIMENTAL_PROVIDER=docker
lab_dir=$(mktemp -d)
cluster="nocert-cm-${RANDOM}-${RANDOM}"
owned_cluster=false
forward_pid=""
cleanup() {
  if [[ -n "$forward_pid" ]]; then
    kill "$forward_pid" 2>/dev/null || true
    wait "$forward_pid" 2>/dev/null || true
  fi
  if [[ "$owned_cluster" == true ]]; then
    kind delete cluster --name "$cluster" --kubeconfig "$lab_dir/kubeconfig" >/dev/null 2>&1 || true
  fi
  rm -rf "$lab_dir"
}
trap cleanup EXIT
trap 'exit 130' INT
trap 'exit 143' TERM
if kind get clusters 2>/dev/null | grep -Fxq "$cluster"; then
  echo 'Refusing to reuse an existing cluster' >&2
  exit 1
fi
k() { kubectl --kubeconfig "$lab_dir/kubeconfig" --context "kind-$cluster" "$@"; }
cat > "$lab_dir/kind.yml" <<'YAML'
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
networking:
  apiServerAddress: "127.0.0.1"
nodes:
  - role: control-plane
YAML
owned_cluster=true
kind create cluster --name "$cluster" --kubeconfig "$lab_dir/kubeconfig" \
  --config "$lab_dir/kind.yml" --wait 120s \
  --image kindest/node:v1.35.8@sha256:07b2536e30b803ed61d1677a79df6115f798ce64c80f9e22f6ed45afd09323c0
curl --fail --silent --show-error --location \
  https://github.com/cert-manager/cert-manager/releases/download/v1.21.3/cert-manager.yaml \
  -o "$lab_dir/cert-manager.yaml"
printf '%s  %s\n' c04a51295adda2b1a775146751f973e787190778c7f35180861d9678876e5c74 \
  "$lab_dir/cert-manager.yaml" | sha256sum --check --status
k apply -f "$lab_dir/cert-manager.yaml" >/dev/null
k -n cert-manager wait --for=condition=Available deployment --all --timeout=240s
k create namespace demo >/dev/null
cat > "$lab_dir/certificate.yml" <<'YAML'
apiVersion: cert-manager.io/v1
kind: Issuer
metadata:
  name: lab-selfsigned
  namespace: demo
spec:
  selfSigned: {}
---
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
  name: site
  namespace: demo
spec:
  secretName: site-tls
  duration: 24h
  renewBefore: 8h
  dnsNames: [renewal.test]
  issuerRef:
    name: lab-selfsigned
    kind: Issuer
YAML
ready=false
for attempt in {1..30}; do
  if k apply --dry-run=server -f "$lab_dir/certificate.yml" >/dev/null 2>&1; then ready=true; break; fi
  sleep 2
done
[[ "$ready" == true ]] || { echo 'cert-manager webhook did not become ready' >&2; exit 1; }
k apply -f "$lab_dir/certificate.yml" >/dev/null
k -n demo wait --for=condition=Ready certificate/site --timeout=120s
# A different valid leaf for the SAME name simulates a stale, separately managed Secret.
openssl req -x509 -newkey rsa:2048 -nodes -days 2 \
  -subj '/CN=renewal.test' -addext 'subjectAltName=DNS:renewal.test' \
  -keyout "$lab_dir/stale.key" -out "$lab_dir/stale.crt" 2>/dev/null
k -n demo create secret tls stale-tls --cert="$lab_dir/stale.crt" --key="$lab_dir/stale.key" >/dev/null
cat > "$lab_dir/ingress.yml" <<'YAML'
apiVersion: v1
kind: ServiceAccount
metadata:
  name: traefik
  namespace: demo
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: lab-traefik
rules:
  - apiGroups: [""]
    resources: [services, endpoints, secrets, nodes]
    verbs: [get, list, watch]
  - apiGroups: [discovery.k8s.io]
    resources: [endpointslices]
    verbs: [get, list, watch]
  - apiGroups: [networking.k8s.io]
    resources: [ingresses, ingressclasses]
    verbs: [get, list, watch]
  - apiGroups: [networking.k8s.io]
    resources: [ingresses/status]
    verbs: [update]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: lab-traefik
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: lab-traefik
subjects:
  - kind: ServiceAccount
    name: traefik
    namespace: demo
---
apiVersion: networking.k8s.io/v1
kind: IngressClass
metadata:
  name: traefik-lab
spec:
  controller: traefik.io/ingress-controller
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: traefik
  namespace: demo
spec:
  replicas: 1
  selector:
    matchLabels: {app: traefik}
  template:
    metadata:
      labels: {app: traefik}
    spec:
      serviceAccountName: traefik
      containers:
        - name: traefik
          image: traefik@sha256:c8a121e4bb1347e8fd52657fab12f2cb70b68f8a018895e2068f92c9e519b9c2
          args:
            - --entrypoints.websecure.address=:8443
            - --providers.kubernetesingress=true
            - --providers.kubernetesingress.ingressclass=traefik-lab
            - --providers.kubernetesingress.namespaces=demo
            - --global.checknewversion=false
            - --global.sendanonymoususage=false
          ports:
            - {name: websecure, containerPort: 8443}
---
apiVersion: v1
kind: Service
metadata:
  name: traefik
  namespace: demo
spec:
  selector: {app: traefik}
  ports:
    - {name: websecure, port: 443, targetPort: websecure}
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: whoami
  namespace: demo
spec:
  replicas: 1
  selector:
    matchLabels: {app: whoami}
  template:
    metadata:
      labels: {app: whoami}
    spec:
      containers:
        - name: whoami
          image: traefik/whoami@sha256:200689790a0a0ea48ca45992e0450bc26ccab5307375b41c84dfc4f2475937ab
          ports:
            - {name: http, containerPort: 80}
---
apiVersion: v1
kind: Service
metadata:
  name: whoami
  namespace: demo
spec:
  selector: {app: whoami}
  ports:
    - {name: http, port: 80, targetPort: http}
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: site
  namespace: demo
  annotations:
    traefik.ingress.kubernetes.io/router.entrypoints: websecure
    traefik.ingress.kubernetes.io/router.tls: "true"
spec:
  ingressClassName: traefik-lab
  tls:
    - hosts: [renewal.test]
      secretName: stale-tls
  rules:
    - host: renewal.test
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: whoami
                port: {number: 80}
YAML
k apply -f "$lab_dir/ingress.yml" >/dev/null
k -n demo wait --for=condition=Available deployment --all --timeout=180s
k -n demo port-forward --address 127.0.0.1 service/traefik :443 > "$lab_dir/forward.log" 2>&1 &
forward_pid=$!
address=""
for attempt in {1..40}; do
  address=$(awk '/Forwarding from 127.0.0.1:/ {print $3; exit}' "$lab_dir/forward.log")
  [[ -n "$address" ]] && break
  kill -0 "$forward_pid" 2>/dev/null || { cat "$lab_dir/forward.log" >&2; exit 1; }
  sleep 0.25
done
[[ -n "$address" ]] || { echo 'Local port-forward did not start' >&2; exit 1; }
secret_fingerprint() {
  # Extract the PUBLIC certificate only, not tls.key or the full Secret.
  k -n demo get secret "$1" -o jsonpath='{.data.tls\.crt}' |
    base64 --decode | openssl x509 -noout -sha256 -fingerprint
}
probe() {
  timeout 5 openssl s_client -connect "$address" -servername renewal.test </dev/null 2>/dev/null |
    openssl x509 -noout -sha256 -fingerprint
}
expect_fingerprint() {
  local expected=$1
  for attempt in {1..60}; do
    if served=$(probe) && [[ "$served" == "$expected" ]]; then return; fi
    sleep 0.5
  done
  echo 'FAIL: served fingerprint did not match' >&2
  k -n demo logs deployment/traefik --tail=30 >&2
  return 1
}
expected=$(secret_fingerprint site-tls)
stale=$(secret_fingerprint stale-tls)
[[ "$expected" != "$stale" ]]
expect_fingerprint "$stale"
printf 'READY:    %s\n' "$(k -n demo get certificate site -o jsonpath='{.status.conditions[?(@.type=="Ready")].status}')"
printf 'EXPECTED: %s\nBEFORE:   %s\n' "$expected" "$served"
# Only this disposable lab Ingress is changed; no existing cluster is touched.
k -n demo patch ingress site --type=json \
  -p='[{"op":"replace","path":"/spec/tls/0/secretName","value":"site-tls"}]' >/dev/null
expect_fingerprint "$expected"
[[ "$(k -n demo get certificate site -o jsonpath='{.status.conditions[?(@.type=="Ready")].status}')" == True ]]
printf 'AFTER:    %s\nPASS: Ready stayed True; changing the Ingress Secret changed the served certificate.\n' "$served"
