# Homelab Cluster Remediation Runbook — Parallel Execution

Board: **24/32 green**. Goal: all ArgoCD apps `Synced/Healthy`, no non-Running pods.

## Parallel task groups (run agents in parallel within groups; groups are sequential)

```
GROUP 1 (independent, run all 3 in parallel)
├─ TASK 1a — temporal: deploy Cassandra + Elasticsearch  [**blocks TASK 2 (homelab-ingress)**]
├─ TASK 2a — queue-operator: add RBAC + TemporalWorker CRD
└─ TASK 3a — minio console: fix image tag

GROUP 2 (wait for GROUP 1 complete)
├─ TASK 4a — prometheus / sops-secrets: resolve OutOfSync
└─ TASK 5a — vault: one-time init + unseal

GROUP 3 (after temporal ns exists)
└─ TASK 6a — homelab-ingress: should auto-sync once temporal ns created
```

**Cloudflare** — deferred (not blocking cluster health).

---

## Cluster facts
- 3 Talos nodes: cp-1 (192.168.1.213), cp-2 (192.168.1.163), cp-3 (192.168.1.166) — all Ready.
- GitOps repo: `~/workplace/homelab`, branch `main`.
- **GitOps mandatory**: repo → commit → push → ArgoCD syncs. No hand-edits except vault init.
- Ingress LB: 192.168.1.160, wildcard TLS on prod (already fixed).

## Status baseline (run before + after each group)
```bash
kubectl get applications -A --no-headers | awk '{printf "%-24s %-12s %s\n",$2,$3,$4}'
kubectl get pods -A | grep -ivE 'Running|Completed'
```

---

## GROUP 1 — parallel tasks (all 3 agents)

### TASK 1a — temporal: deploy Cassandra + Elasticsearch (replaces PostgreSQL)

**Symptom**: `temporal` app sync=Unknown, helm render error on removed `cassandra:` top-level key.

**Decision**: Deploy Cassandra + Elasticsearch as sub-charts. Temporal chart supports both as embedded dependencies.

#### Steps
1. **Pin chart version** — edit `k8s/argocd/apps/60-applications.yaml`:
   ```bash
   grep -n "targetRevision" k8s/argocd/apps/60-applications.yaml | grep -A2 -B2 temporal
   ```
   Change `targetRevision: "*"` → `targetRevision: "0.64.0"` (confirmed to have cassandra/elasticsearch sub-charts).

2. **Rewrite temporal-values.yaml** — remove top-level `cassandra:`, enable cassandra + elasticsearch sub-charts:
   ```bash
   sed -n '1,50p' k8s/applications/temporal/temporal-values.yaml
   ```
   Rewrite to:
   ```yaml
   # Temporal with Cassandra (default store) + Elasticsearch (visibility)
   
   cassandra:
     enabled: true
     config:
       cluster_name: temporal
       num_tokens: 256
       seed_provider:
         class_name: org.apache.cassandra.locator.SimpleSeedProvider
         parameters:
           seeds: "127.0.0.1"
     resources:
       requests:
         memory: "512Mi"
         cpu: "250m"
       limits:
         memory: "1Gi"
         cpu: "500m"
     persistence:
       enabled: true
       size: 10Gi
   
   elasticsearch:
     enabled: true
     replicas: 1
     resources:
       requests:
         memory: "512Mi"
         cpu: "250m"
       limits:
         memory: "1Gi"
         cpu: "500m"
     persistence:
       enabled: true
       size: 10Gi
   
   postgresql:
     enabled: false        # disable embedded postgres
   
   server:
     config:
       persistence:
         defaultStore: default
         additionalStores: {}
         datastores:
           default:
             driver: cassandra
             cassandra:
               hosts:
                 - cassandra
               port: 9042
               keyspace: temporal
               user: ""
               password: ""
               maxConnsPerHost: 32
               consistency: LOCAL_QUORUM
           visibility:
             driver: elasticsearch
             elasticsearch:
               scheme: http
               host: elasticsearch
               port: 9200
   
   # Keep remaining server config (logging, etc.)
   # ... (existing server.lifecycleHooks, server.replicaCount, etc.)
   ```
   
   Key differences:
   - Top-level `cassandra.enabled: true` (sub-chart, not deprecated key).
   - `elasticsearch.enabled: true` for visibility store.
   - `server.config.persistence.datastores.default.driver: cassandra` (not SQL).
   - `server.config.persistence.datastores.visibility.driver: elasticsearch`.
   - No CNPG references — Cassandra/ES managed by Helm.

3. **Validate helm render locally**:
   ```bash
   helm template temporal temporal --repo https://go.temporal.io/helm-charts \
     --version 0.64.0 -n temporal \
     -f k8s/applications/temporal/temporal-values.yaml --include-crds 2>&1 | grep -iE "error|^kind:" | head
   # expect: kind lines (StatefulSet, Deployment, etc.), no errors
   ```

4. **Commit + push**:
   ```bash
   git add k8s/applications/temporal/temporal-values.yaml k8s/argocd/apps/60-applications.yaml
   git commit -m "fix(temporal): deploy Cassandra + Elasticsearch sub-charts, remove PostgreSQL, pin chart v0.64.0"
   git push origin main
   ```

5. **Trigger sync** (ArgoCD may auto-sync; force if needed):
   ```bash
   kubectl -n argocd annotate application temporal argocd.argoproj.io/refresh=hard --overwrite
   ```

#### Verify
```bash
kubectl get ns temporal                           # namespace exists
kubectl -n temporal get pods | head               # server/cassandra/elasticsearch pods Running
kubectl -n argocd get application temporal -o jsonpath='{.status.sync.status} {.status.health.status}{"\n"}'  # Synced Healthy
```

#### Troubleshoot
- Still `Unknown` / render error → confirm cassandra/elasticsearch keys are nested under top-level, not at column-0. Check Chart.yaml: `dependencies: [{name: cassandra}, {name: elasticsearch}]`.
- Cassandra pod Pending → PVC not bound. Check: `kubectl -n temporal get pvc`. If no PVC, storage class missing — verify `kubectl get storageclass`.
- Elasticsearch OOMKilled → bump resource limits in values.
- Schema job pending → cassandra not ready. Wait: `kubectl -n temporal logs job/temporal-schema-setup --tail=20`.

---

### TASK 2a — queue-operator: add RBAC + TemporalWorker CRD (parallel)

**Symptom**: operator crashloop, logs show `forbidden: cannot list deployments` + `no matches for kind TemporalWorker`.

**Cause**: ClusterRole missing `deployments.apps` permission; TemporalWorker CRD not installed.

#### Steps
1. **Edit RBAC** — `k8s/applications/sqs/charts/queue-crd/templates/rbac.yaml`, add to ClusterRole rules:
   ```yaml
   - apiGroups: ["apps"]
     resources: ["deployments"]
     verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
   ```
   (Full file already has rules for queues, leases, events, pods, nodes; append the above.)

2. **Install TemporalWorker CRD** — kmsvc-managed. Fetch from kmsvc source (or import from your internal docs):
   Create `k8s/applications/sqs/charts/queue-crd/templates/temporalworker-crd.yaml`:
   ```yaml
   apiVersion: apiextensions.k8s.io/v1
   kind: CustomResourceDefinition
   metadata:
     name: temporalworkers.kmsvc.io
   spec:
     group: kmsvc.io
     names:
       kind: TemporalWorker
       plural: temporalworkers
     scope: Namespaced
     versions:
     - name: v1
       served: true
       storage: true
       schema:
         openAPIV3Schema:
           type: object
           properties:
             spec:
               type: object
               properties:
                 workflowType:
                   type: string
                 concurrency:
                   type: integer
             status:
               type: object
   ```
   (Adjust schema per kmsvc spec; this is minimal template.)

3. **Commit + push**:
   ```bash
   git add k8s/applications/sqs/charts/queue-crd/templates/rbac.yaml \
           k8s/applications/sqs/charts/queue-crd/templates/temporalworker-crd.yaml
   git commit -m "fix(sqs): grant queue-operator deployments RBAC, install TemporalWorker CRD"
   git push origin main
   ```

4. **Restart operator**:
   ```bash
   kubectl -n sqs rollout restart deploy/queue-operator
   ```

#### Verify
```bash
kubectl get crd | grep -iE 'queues.kmsvc|temporalworkers.kmsvc'       # both exist
kubectl -n sqs logs deploy/queue-operator --tail=5 | grep -iE 'error|forbidden' || echo CLEAN
kubectl -n sqs get pods -l app=queue-operator                         # Running, restarts stable
```

---

### TASK 3a — minio console: fix image tag (parallel)

**Symptom**: ns `storage`, pod `console-*` stuck ImagePullBackOff, image `minio/console:v0.30.0` doesn't exist.

**Fix**: remove the standalone console deploy (deprecated); MinIO tenant has built-in console.

#### Steps
1. **Find console reference**:
   ```bash
   grep -rn "v0.30.0\|console:" k8s/infrastructure/minio k8s/applications/minio* 2>/dev/null | head
   ```

2. **Remove from kustomization/values**:
   ```bash
   grep -n "console\|apiVersion:" k8s/infrastructure/minio/kustomization.yaml
   ```
   Delete lines referencing `console` deployment/service. Keep operator + tenant.

3. **Commit + push**:
   ```bash
   git add k8s/infrastructure/minio/
   git commit -m "fix(minio): remove deprecated standalone console (use tenant built-in)"
   git push origin main
   ```

#### Verify
```bash
kubectl -n storage get pods | grep -i console || echo "console removed"
kubectl -n argocd get application minio-operator -o jsonpath='{.status.health.status}{"\n"}'  # Healthy
```

---

## Wait for GROUP 1 complete (all 3 tasks done)

Check:
```bash
kubectl -n argocd get app temporal queue-crd minio-operator -o jsonpath='{range .items[*]}{.metadata.name}{": "}{.status.sync.status}{" "}{.status.health.status}{"\n"}{end}'
# want all: Synced Healthy
```

---

## GROUP 2 — sequential tasks (after GROUP 1 green)

### TASK 4a — prometheus + sops-secrets OutOfSync (drift resolution)

**Symptom**: both apps Healthy but OutOfSync (CRD annotation drift).

#### Steps
1. **Inspect diff**:
   ```bash
   kubectl -n argocd get application prometheus -o json | jq -r '.status.conditions[]?.message' | head
   ```

2. **Enable server-side apply** — edit the Application defining prometheus in `k8s/argocd/apps/`:
   ```bash
   grep -n "prometheus" k8s/argocd/apps/*.yaml | grep -i "name:"
   ```
   Add to `syncPolicy.syncOptions`:
   ```yaml
   syncOptions:
     - ServerSideApply=true
   ```

3. **For sops-secrets**, check ownership:
   ```bash
   kubectl -n argocd get application sops-secrets -o json | jq '.spec.source'
   ```
   If it's in homelab-root as nested app, ensure no dual-ownership (no other ArgoCD app managing the same Secret). If conflict, update the parent app path.

4. **Commit + push**:
   ```bash
   git add k8s/argocd/apps/
   git commit -m "fix(argocd): enable server-side apply for prometheus CRD, reconcile sops-secrets ownership"
   git push origin main
   ```

5. **Refresh**:
   ```bash
   kubectl -n argocd annotate application prometheus argocd.argoproj.io/refresh=hard --overwrite
   kubectl -n argocd annotate application sops-secrets argocd.argoproj.io/refresh=hard --overwrite
   ```

#### Verify
```bash
kubectl -n argocd get app prometheus sops-secrets -o jsonpath='{range .items[*]}{.metadata.name}{": "}{.status.sync.status}{"\n"}{end}'
# Synced Synced
```

---

### TASK 5a — vault: one-time init + unseal (manual)

**Symptom**: vault-0 CreateContainerConfigError, needs secret `vault-unseal-keys`.

**Security**: store unseal keys + root token in your offline password manager. Never commit plaintext to git.

#### Steps
1. **Create placeholder secret** (to let pod start):
   ```bash
   kubectl -n iam create secret generic vault-unseal-keys \
     --from-literal=key1=placeholder --from-literal=key2=placeholder --from-literal=key3=placeholder
   kubectl -n iam rollout status sts/vault --timeout=2m
   ```

2. **Init** (generates real keys + root token — SAVE THESE):
   ```bash
   kubectl -n iam exec -it vault-0 -- vault operator init -key-shares=3 -key-threshold=3
   # Output:
   # Unseal Key 1: ...
   # Unseal Key 2: ...
   # Unseal Key 3: ...
   # Initial Root Token: ...
   ```
   **STORE SECURELY** in password manager / encrypted file. Do NOT paste into terminals or commit.

3. **Unseal** (run all 3 keys):
   ```bash
   key1="<paste from password manager>"
   key2="<paste from password manager>"
   key3="<paste from password manager>"
   
   kubectl -n iam exec -it vault-0 -- vault operator unseal $key1
   kubectl -n iam exec -it vault-0 -- vault operator unseal $key2
   kubectl -n iam exec -it vault-0 -- vault operator unseal $key3
   ```

4. **Update secret with real keys** (via SOPS for GitOps, or direct kubectl):
   ```bash
   # Option A: direct (fast for now, not GitOps; set up SOPS encrypt later)
   kubectl -n iam create secret generic vault-unseal-keys \
     --from-literal=key1="$key1" --from-literal=key2="$key2" --from-literal=key3="$key3" \
     --dry-run=client -o yaml | kubectl apply -f -
   ```
   **TODO later**: encrypt this secret via SOPS and commit to repo so re-provision has keys.

5. **Restart pod** to pick up real keys (reloader watches secret):
   ```bash
   kubectl -n iam rollout restart sts/vault
   kubectl -n iam rollout status sts/vault
   ```

#### Verify
```bash
kubectl -n iam get pod vault-0                          # 1/1 Running
kubectl -n iam exec -it vault-0 -- vault status         # Initialized true, Sealed false
kubectl -n argocd get application vault -o jsonpath='{.status.health.status}{"\n"}'  # Healthy
```

---

## Final check (all apps green)

```bash
kubectl get applications -A --no-headers | awk '{print $3,$4}' | sort | uniq -c
# expect: 32 "Synced Healthy"

kubectl get pods -A | grep -ivE 'Running|Completed'
# expect: no output (or only Completed jobs)

kubectl get nodes
# expect: 3 Ready

# Cluster ready for next steps
echo "✓ Cluster healthy"
```

---

## Rollback (per-task)
Each task has a `git revert --no-edit HEAD && git push origin main` — run that commit hash if needed.

---

## Notes for agents

- **All repo changes use GitOps**: edit files, commit, push `main`, ArgoCD auto-syncs (within 3–5 min, or force with annotate).
- **Parallel GROUP 1**: tasks 1a, 2a, 3a have no interdependencies — agents can work simultaneously.
- **Sequential GROUP 2**: wait for GROUP 1 all-green before starting 4a/5a.
- **Vault (5a)** is the only manual step — unseal keys must be handled securely offline, not in terminal history.
- **Post-remediation**: cluster is health-ready for applications/workloads; Cloudflare tunneling deferred.
