Initial commit: Hermes Agent Skills collection

This commit is contained in:
Debian
2026-07-12 19:02:59 +00:00
commit e9cc106625
789 changed files with 233126 additions and 0 deletions
@@ -0,0 +1,162 @@
# RKE2 Cluster Cold-Boot Recovery — 2026-07-12
## Situation
RKE2 cluster (6 VMs: 3 CP + 3 Worker) was offline for 111 days. All VMs
were stopped across the PVE cluster. User requested cluster startup
and K8s evaluation for Docker workload migration.
## Startup Sequence
### 1. VM Discovery
VMs had been HA-migrated to various PVE nodes. Had to scan all nodes
to find where each VM landed:
```
118 (CP-01) → proxmox5
130 (CP-02) → proxmox3
129 (CP-03) → proxmox2
128 (Worker-01) → proxmox5
132 (Worker-02) → proxmox2
131 (Worker-03) → proxmox3
200 (mgmt-runner) → proxmox3
```
### 2. Start Order
1. Started all 6 RKE2 VMs via `qm start` (HA placed them on various nodes)
2. CP nodes began etcd convergence within 30s
3. Workers initially showed `NotReady` — waiting for
`serving-kubelet.crt` retrieval (503 from API server)
4. Worker-03 became Ready after ~2 min
5. Worker-02 became Ready after ~3 min (slower due to proxy reconnection)
6. All 6 nodes Ready within 5 min
### 3. Convergence Timeline
| T+0 | VMs started |
| T+30s | CPs booting, etcd forming quorum |
| T+1min | Workers connecting via wss://CP:9345/v1-rke2/connect |
| T+2min | First workers Ready (cert retrieval successful) |
| T+3min | All nodes Ready, Cilium agents starting |
| T+5min | ArgoCD, External Secrets, operators beginning reconciliation |
| T+10min | Most pods Running, old replicas Terminating |
| T+15min | Databases recovering (Galera, PostgreSQL) |
### 4. Pod States After Cold Boot
Most pods showed mixed states initially:
- `Running` — new replicas scheduled by deployment controller
- `Terminating` — old replicas from before shutdown (111 days old)
- `Unknown` — node was down when status was last reported
- `CrashLoopBackOff` — ceph-csi nodeplugin (2 of 6) — Ceph connectivity
- `CreateContainerConfigError` — memory-api, qdrant — missing secrets
(ExternalSecret hadn't synced yet)
All resolved naturally except:
- 3 ExternalSecrets with SecretSyncedError (1Password item issues)
- 2 ceph-csi-rbd-nodeplugin CrashLoopBackOff
## Cilium LB Pool Migration
### Problem
Cilium LB pool was set to `10.0.30.70-10.0.30.89`, overlapping with
Galera/MaxScale VIPs:
- 10.0.30.70 = Keepalived VIP for MaxScale (Galera)
- 10.0.30.71-73 = Galera VM IPs
K8s services had claimed:
- Traefik → 10.0.30.70 (conflicted with MaxScale VIP!)
- PostgreSQL RW → 10.0.30.71 (conflicted with Galera VM!)
- MariaDB MaxScale (K8s) → 10.0.30.72
### Fix Applied
1. Deleted live `CiliumLoadBalancerIPPool` CRD
2. Created new pool with `10.0.30.200-10.0.30.250`
3. Services auto-reassigned:
- Traefik → 10.0.30.202
- MariaDB MaxScale (K8s) → 10.0.30.201
- PostgreSQL RW → 10.0.30.200
4. Updated manifest on all 3 CP nodes:
`/var/lib/rancher/rke2/server/manifests/cilium-l2-lb.yaml`
5. Fixed IaC repo: `epic-2-k8s/ansible/playbook.yml` — committed + pushed
### Verification
- `10.0.30.70` responds to ping (Keepalived VIP for MaxScale — correct!)
- No K8s LoadBalancer service holds IPs in .70-.89 range
- Cilium LB pool shows 48 IPs available in .200-.250 range
## K8s MariaDB Removal
### Decision
User clarified: Galera lives as **native VMs** (VM300-302 + MaxScale
VM310), not in K8s. The K8s MariaDB Galera (3 pods) + MaxScale (2 pods)
in namespace `mariadb` was redundant and should be removed.
### Removal Steps
1. Removed YAML files from `iac-homelab` repo:
- `clusters/main/databases/mariadb/` (5 files: cluster.yaml,
external-secrets.yaml, maxscale.yaml, namespace.yaml, README.md)
- `clusters/main/operators/mariadb-operator.yaml`
- `clusters/main/operators/mariadb-operator-crds.yaml`
2. Committed + pushed to Gitea
3. ArgoCD detected change (`databases``OutOfSync`) but did NOT
auto-prune the operator-managed CRs
4. Manual cleanup required:
- `kubectl delete mariadb mariadb-galera -n mariadb` (CR deletion
triggers operator finalizer processing)
- `kubectl delete maxscale mariadb-maxscale -n mariadb` (already
gone — operator had processed it)
- `kubectl delete namespace mariadb` (clean)
- `kubectl delete namespace mariadb-operator` (stuck Terminating
briefly, cleared after ~15s)
- `kubectl delete crd ...` (9 mariadb CRDs — ArgoCD didn't prune
these, had to delete manually)
5. Forced ArgoCD hard-refresh: `kubectl annotate application root -n
argocd argocd.argoproj.io/refresh-options=hard-refresh --overwrite`
### Result
- Namespaces `mariadb` + `mariadb-operator` deleted
- 9 MariaDB CRDs deleted
- ArgoCD apps `mariadb-operator` + `mariadb-operator-crds` pruned
- `databases` app re-synced (only postgres remains)
- Native Galera VMs (300-302) + MaxScale VM310 untouched
- LB IP 10.0.30.201 freed (was K8s MaxScale, now available in pool)
### Key Lesson
ArgoCD `prune: true` handles standard K8s resources but **does not
reliably prune operator CRs and CRDs** — finalizers require the
operator to be alive, and CRD deletion isn't tracked by ArgoCD's
resource tree. Manual `kubectl delete` of CRs → namespace → CRDs is
required when removing an operator-managed application.
ArgoCD was already installed and configured:
- 8 Applications synced (operators, databases, backups, memory, root)
- Ingress via Traefik (host: `argocd`, routed through Traefik LB IP)
- App-of-Apps pattern pointing to `dominik/iac-homelab` on Gitea
- Auto-sync with `prune: true, selfHeal: true`
Admin password retrieved via:
```bash
kubectl -n argocd get secret argocd-initial-admin-secret \
-o jsonpath='{.data.password}' | base64 -d
```
## IaC Repository
`dominik/iac-homelab` on Gitea (10.0.30.105:3000) contains:
- 7 Epics (management VM, K8s cluster, networking, storage, GitOps, memory, MariaDB)
- OpenTofu for VM provisioning + Helm releases
- Ansible for RKE2 bootstrap
- GitHub Actions CI/CD (self-hosted runner on mgmt-runner VM)
- ArgoCD Application manifests in `clusters/main/apps/`
Separate repo: `dominik/observability-iac` — Loki, Prometheus/Thanos, Grafana
@@ -0,0 +1,229 @@
# Helm Chart Upgrades — 2026-07-12
## Goal
Upgrade all Helm-managed charts on the RKE2 cluster after the v1.35.6
upgrade. 5 components needed updates; RKE2-bundled charts were already
current.
## Version Matrix (Final)
| Component | Old Version | New Version | Method | Status |
|-----------|------------|-------------|--------|--------|
| External Secrets | 2.2.0 | 2.7.0 | Helm direct | ✅ |
| CNPG | 0.22.1 (app 1.24.1) | 0.29.0 (app 1.30.0) | ArgoCD GitOps | ✅ |
| Velero | 8.1.0 (app 1.15.0) | 12.1.0 (app 1.18.1) | ArgoCD GitOps | ✅ |
| Ceph CSI RBD | 3.10.1 | 3.17.0 | Helm direct | ✅ |
| ArgoCD | 7.8.13 (app v2.14.7) | 10.1.3 (app v3.4.5) | Helm direct | ✅ |
| Cilium | rke2-cilium-1.19.402 | — | RKE2-bundled | Already current |
| Traefik | rke2-traefik-40.1.003 | — | RKE2-bundled | Already current |
| CoreDNS | rke2-coredns-1.46.002 | — | RKE2-bundled | Already current |
| Metrics Server | rke2-metrics-server-3.13.100 | — | RKE2-bundled | Already current |
| Snapshot Ctrl | rke2-snapshot-controller-4.2.006 | — | RKE2-bundled | Already current |
## Upgrade Order (Safest → Riskiest)
1. External Secrets 2.2.0 → 2.7.0 (Minor, Helm direct, minimal values)
2. CNPG 0.22.1 → 0.29.0 (Major, ArgoCD GitOps, **required replica fix first**)
3. Velero 8.1.0 → 12.1.0 (Major, ArgoCD GitOps)
4. Ceph CSI RBD 3.10.1 → 3.17.0 (Major, Helm direct, **causes transient API outage**)
5. ArgoCD v2.14.7 → v3.4.5 (MAJOR v2→v3, Helm direct, **breaking change**)
## Detail: External Secrets (2.2.0 → 2.7.0)
Straightforward `helm upgrade --reuse-values`. Values were minimal
(`installCRDs: true`). No breaking changes. All ExternalSecrets
continued syncing. Two pre-existing broken ExternalSecrets
(argocd-repo-credentials, postgres-main-db) were already broken for
110+ days — unrelated to the upgrade.
## Detail: CNPG (0.22.1 → 0.29.0) — Replica Rebuild Required
### Pre-Existing Issue Found
`postgres-main-2` replica was broken for 109 days:
```
requested WAL segment 000000020000000000000008 has already been removed
WAL file not found in the recovery object store
```
Primary had recycled WAL segments the replica still needed. Cluster
was running with 2/3 Ready (primary + 1 replica), backups operational.
### Replica Rebuild
```bash
# Delete broken replica PVCs + pod
kubectl delete pvc postgres-main-2 postgres-main-2-wal -n postgres
kubectl delete pod postgres-main-2 -n postgres
# CNPG automatically created postgres-main-4 as replacement
# Wait for 3/3 Ready:
kubectl get pods -n postgres -w
```
### Operator Upgrade via GitOps
```bash
# Changed targetRevision in ArgoCD Application manifest
cd /root/iac-homelab
sed -i 's/targetRevision: 0.22.1/targetRevision: 0.29.0/' \
clusters/main/operators/cloudnativepg.yaml
git add -A && git commit -m "feat: upgrade CNPG 0.22.1 → 0.29.0" && git push
```
**Pitfall**: ArgoCD didn't auto-detect the chart version change despite
`selfHeal: true`. Required hard refresh:
```bash
kubectl annotate application cloudnativepg-operator -n argocd \
argocd.argoproj.io/refresh=hard --overwrite
```
After refresh, ArgoCD pulled new chart, created new operator pod with
image v1.30.0. Postgres cluster briefly went 2/3 during reconciliation
(operator triggered primary restart), then recovered to 3/3 Healthy.
## Detail: Velero (8.1.0 → 12.1.0)
Same GitOps pattern as CNPG — changed `targetRevision` in ArgoCD
Application manifest, pushed, hard-refreshed ArgoCD. All Velero
components rolled out: server + 6 node-agents (one per K8s node).
BSL (Backup Storage Location) remained Available throughout. Backup
schedules (daily-full-cluster, weekly-full-cluster) continued working.
## Detail: Ceph CSI RBD (3.10.1 → 3.17.0) — API Server Disruption
### Upgrade
```bash
helm upgrade ceph-csi-rbd cephcsi/ceph-csi-rbd \
--version 3.17.0 -n kube-system --reuse-values --timeout 120s
```
### Transient API Server Outage
The nodeplugin DaemonSet rolling update disrupted kubelet on CP nodes.
All 3 API servers became unreachable:
```
The connection to the server 10.0.30.51:6443 was refused
```
Recovery took ~5 minutes. Monitored with:
```bash
curl -sk --connect-timeout 5 https://10.0.30.51:6443/healthz
# 401 Unauthorized = server is UP (expected without credentials)
# Connection refused = server is DOWN
```
After recovery, provisioner pods (2 replicas) were 7/7 Running with
image `quay.io/cephcsi/cephcsi:v3.17.0`. One nodeplugin pod stayed in
`ContainerCreating` for several minutes (image pull delay on CP-01).
All 7 PVCs remained bound throughout — no data impact.
### Root Cause
The Ceph CSI nodeplugin DaemonSet runs on EVERY node including CP
nodes. Rolling the DaemonSet causes the CSI driver socket to be
briefly unavailable, which can block kubelet operations on CP nodes,
temporarily preventing the API server from serving requests.
## Detail: ArgoCD v2.14.7 → v3.4.5 (MAJOR Upgrade)
### Safe Path: Two-Step Upgrade
Step 1: Patch to latest v2.x (low risk):
```bash
helm upgrade argocd argo/argo-cd --version 7.9.1 -n argocd --reuse-values
# v2.14.7 → v2.14.11
```
Step 2: Major v2→v3 upgrade with explicit values:
```bash
# v3 chart requires redis.networkPolicy.create field (breaking change)
# --reuse-values FAILS with:
# nil pointer evaluating interface {}.create
# Must provide explicit values file with redis.networkPolicy.create: false
helm upgrade argocd argo/argo-cd --version 10.1.3 -n argocd \
-f /tmp/argocd-v3-values.yaml --timeout 300s
```
### v3 Values File
Key addition vs v2 values:
```yaml
redis:
networkPolicy:
create: false
```
All other values carried over unchanged (server.insecure, ingress,
replicas, affinity).
### Post-Upgrade Verification
- All ArgoCD pods Running (server ×2, repo-server ×2,
application-controller, applicationset-controller, dex-server,
notifications-controller, redis)
- Image: `quay.io/argoproj/argocd:v3.4.5`
- All applications Synced (except `backups` — pre-existing
ExternalSecret OutOfSync, see §13.7)
## Detail: ArgoCD `backups` App OutOfSync (Task 4)
### Root Cause
Single resource OutOfSync: `ExternalSecret/velero-s3-credentials` in
`velero` namespace. ESO adds default fields to the live spec that
aren't in the Git YAML:
- `conversionStrategy: "Default"`
- `decodingStrategy: "None"`
- `metadataPolicy: "None"`
- `deletionPolicy: "Retain"`
- `engineVersion: "v2"`
- `mergePolicy: "Replace"`
ArgoCD's `ignoreDifferences` only covers `/status`, not `/spec`.
### Impact
Cosmetic — ExternalSecret functions correctly (`secret synced`,
Velero backups operational). No data or functionality impact.
### Fix (Not Yet Applied — Read-Only Investigation)
Add ESO default fields to Git YAML at
`clusters/main/backups/velero/external-secret.yaml` so spec matches
exactly. Alternative: expand `ignoreDifferences` to cover specific
JSON pointers (less clean).
## GitOps Trail
Commits pushed to `dominik/iac-homelab` on Gitea:
- `36eb5c4``feat(helm): upgrade all managed charts` (values files +
ArgoCD Application targetRevision changes)
- `acd3231``chore: remove .kube cache from repo, add to .gitignore`
(cleanup of accidentally committed `.kube/cache/` directory)
## Lessons Learned
1. **Always check CNPG cluster health before upgrading the operator**
a broken replica (WAL gap) should be rebuilt first (delete PVC + pod,
CNPG auto-creates fresh replica from base backup).
2. **ArgoCD GitOps chart bumps need hard refresh** — changing
`targetRevision` in the Application manifest and pushing doesn't
guarantee ArgoCD pulls the new chart. Annotate with
`argocd.argoproj.io/refresh=hard` to force.
3. **Ceph CSI RBD DaemonSet upgrades disrupt API servers** — the
nodeplugin DaemonSet rolls across ALL nodes including CP nodes,
briefly disrupting kubelet and making API servers unreachable for
2-5 minutes. Schedule during maintenance windows.
4. **ArgoCD v2→v3 chart requires redis.networkPolicy.create**
`--reuse-values` fails with nil pointer error. Must provide
explicit values file with `redis.networkPolicy.create: false`.
5. **ExternalSecret perpetual OutOfSync** — ESO adds default fields to
live spec not present in Git YAML. Fix by adding those fields to the
Git source YAML.
6. **`.kube/cache` gets committed by `git add -A`** — always add
`.kube/` to `.gitignore` before running kubectl from a repo root.
7. **Save Helm values to IaC repo**`helm get values` → save to
`epic-2-k8s/helm/<chart>/values.yaml` for reproducible installs.
@@ -0,0 +1,95 @@
# Hindsight K8s Deployment — Session Detail (2026-07-12)
## Context
Hindsight daemon on Hermes host (Debian 12, GLIBC 2.36) couldn't start
because the Rust binary needs GLIBC 2.39. Decision: deploy hindsight-api
(Python PyPI package) as a K8s Deployment in the RKE2 cluster.
## What was done
1. Discovered `hindsight-api` is a Python package on PyPI (v0.8.4),
NOT the Rust binary. The Rust binary is only the CLI client.
2. Created K8s manifests:
- Namespace: `hindsight`
- StatefulSet: `hindsight-postgres` (image: `pgvector/pgvector:pg16`)
- Deployment: `hindsight-api` (image: `python:3.11-slim`, venv + pip install)
- Services, ExternalSecrets, ArgoCD Application
3. Created 1Password items in vault "Kubernetes ESO":
- `noris-api-key` (API Credential) → credential = noris API key
- `hindsight-db-password` (Password) → password = random string
4. Committed to `dominik/iac-homelab` repo, pushed to Gitea
5. ArgoCD auto-synced the new Application
## Issues hit and fixes (chronological)
### Issue 1: ExternalSecret API version
- **Error**: `"The Kubernetes API could not find version 'v1beta1'"`
- **Fix**: Changed `apiVersion` from `external-secrets.io/v1beta1` to `external-secrets.io/v1`
### Issue 2: StorageClass name
- **Error**: PVC stuck `Pending` with no PV created
- **Fix**: Changed `ceph-rbd``ceph-hdd-replica`
### Issue 3: ExternalSecret key format
- **Error**: `"secret reference has invalid format"`
- **Fix**: Combined into `key: item-name/field-name` format, removed `property:` field
### Issue 4: 1Password vault mismatch
- **Error**: `"no vault matched the secret reference query"`
- **Cause**: Items created in vault "Hermes" via local `op` CLI, but K8s
ClusterSecretStore uses vault "Kubernetes ESO" (`334ykdtj5kar3jlpcrztjvx2fu`)
- **Fix**: Used K8s service account token to create items in correct vault:
```bash
K8S_TOKEN=$(kubectl get secret onepassword-token -n external-secrets \
-o jsonpath='{.data.token}' | base64 -d)
OP_SERVICE_ACCOUNT_TOKEN="$K8S_TOKEN" op item create \
--category="API Credential" --title="noris-api-key" \
--vault="334ykdtj5kar3jlpcrztjvx2fu" credential=VALUE
```
- **Key learning**: K8s service account token CAN create items (not read-only)
### Issue 5: K8s env var interpolation order
- **Fix**: Moved `HINDSIGHT_DB_PASSWORD` to be the first env var (before `DATABASE_URL`)
### Issue 6: ArgoCD stale manifest cache
- **Fix**: Delete Application and recreate, or force hard refresh via annotation
### Issue 7: StatefulSet PVC immutability
- **Fix**: Delete STS + PVC manually, let ArgoCD recreate with corrected manifest
### Issue 8: PostgreSQL lost+found conflict on PVC
- **Error**: Postgres CrashLoopBackOff — initdb refuses non-empty data directory
- **Cause**: Ceph PVC has `lost+found` directory at root
- **Fix**: Set `PGDATA=/var/lib/postgresql/data/pgdata` (subdirectory)
### Issue 9: pip --target doesn't install console_scripts
- **Error**: Main container: `/bin/sh: 1: hindsight-api: not found`
- **Cause**: `pip install --target=/opt/packages` copies modules but NOT bin/ scripts
- **Fix**: Use `python -m venv /opt/venv` instead — creates full venv with bin/
### Issue 10: CUDA torch (526MB) causes OOMKill
- **Error**: Init container OOMKilled during pip install
- **Cause**: Default torch wheel includes CUDA libraries (526MB+)
- **Fix**: Install CPU torch first with `--index-url https://download.pytorch.org/whl/cpu`,
then main package with `--extra-index-url` for same CPU index
### Issue 11: kubectl rollout restart doesn't apply manifest changes
- **Error**: After `rollout restart`, new pod still uses old spec
- **Cause**: `rollout restart` only restarts with current deployment spec, doesn't pull new manifests
- **Fix**: `kubectl apply -f <manifest>` first, THEN `rollout restart` if needed
### Issue 12: ArgoCD doesn't recreate manually deleted resources
- **Error**: After `kubectl delete externalsecret`, ArgoCD reports "successfully synced"
but doesn't recreate the ES
- **Fix**: `kubectl apply -f <manifest>` manually
## State at session end
- Postgres pod: 1/1 Running ✅
- ExternalSecrets: Synced, Secrets created ✅
- LB Service: 10.0.30.201:9177 assigned ✅
- API pod: Init container pip install (venv + CPU torch) takes 5-10min
- Latest commit `8424566` uses venv approach
- Needs `kubectl apply -f` on VM200 to pick up the venv fix
- Previous `rollout restart` didn't apply the manifest change
- **Next step**: `kubectl apply -f clusters/main/hindsight/api-deployment.yaml` on VM200,
wait ~10min for init container, verify pod becomes Ready, then update Hermes config
@@ -0,0 +1,106 @@
# RKE2 Cluster Topology & Inventory
## Cluster Nodes
| VMID | Name | Role | IP | Specs | RKE2 Service |
|------|------|------|----|-------|-------------|
| 118 | rke2-cp-01 | control-plane,etcd | 10.0.30.51 | 4c/12GB/40GB | rke2-server |
| 130 | rke2-cp-02 | control-plane,etcd | 10.0.30.52 | 4c/12GB/40GB | rke2-server |
| 129 | rke2-cp-03 | control-plane,etcd | 10.0.30.53 | 4c/12GB/40GB | rke2-server |
| 128 | rke2-worker-01 | worker | 10.0.30.61 | 4c/12GB/80GB | rke2-agent |
| 132 | rke2-worker-02 | worker | 10.0.30.62 | 4c/12GB/80GB | rke2-agent |
| 131 | rke2-worker-03 | worker | 10.0.30.63 | 4c/12GB/80GB | rke2-agent |
- **RKE2 API VIP**: 10.0.30.50 (Keepalived on CP nodes)
- **RKE2 version**: v1.35.2+rke2r1
- **Container runtime**: containerd 2.1.5-k3s1
- **OS**: Debian 12 (bookworm), kernel 6.1.0-44-amd64
## Management VM
| VMID | Name | IP | Specs |
|------|------|----|-------|
| 200 | mgmt-runner-01 | 10.0.30.X (DHCP) | 4c/4GB/30GB |
- Has `kubectl`, `helm`, `git`, GitHub Actions runner
- Kubeconfig at `/root/.kube/config` (must set `KUBECONFIG=` explicitly)
- GitHub Actions runner: `actions.runner.dsgrafiniert-iac-homelab.mgmt-runner-01`
- OpenTofu state files in `/opt/tofu-state/`
## LoadBalancer IP Assignments
| Service | Namespace | LB IP | Ports |
|---------|-----------|-------|-------|
| Traefik Ingress | kube-system | 10.0.30.202 | 80, 443 |
| PostgreSQL RW (CNPG) | postgres | 10.0.30.200 | 5432 |
**Cilium LB Pool**: 10.0.30.200 - 10.0.30.250 (48 IPs available)
**Removed**: K8s MariaDB MaxScale (was 10.0.30.201, namespace deleted
2026-07-12). MariaDB Galera runs as native VMs, not in K8s.
**Non-K8s services on same subnet** (must not overlap with LB pool):
- 10.0.30.50 = RKE2 API VIP (Keepalived)
- 10.0.30.70 = MaxScale VIP (Keepalived, standalone Galera VMs)
- 10.0.30.71-73 = Galera VMs (VM300-302)
- 10.0.30.99 = Docker host (CT121)
- 10.0.30.104 = Frigate (CT120)
- 10.0.30.105 = Gitea (CT108)
- 10.0.30.10 = Home Assistant (VM106)
## Operators & Add-ons
| Component | Namespace | Status |
|-----------|-----------|--------|
| Cilium CNI | kube-system | Running (eBPF, L2 announcements) |
| Traefik Ingress | kube-system | Running (RKE2-bundled) |
| CoreDNS | kube-system | Running (RKE2-bundled) |
| Metrics Server | kube-system | Running |
| External Secrets Operator | external-secrets | Running (1Password backend) |
| CloudNativePG | cnpg-system | Running (PostgreSQL operator) |
| Velero | velero | Running (S3 backup) |
| Ceph CSI | kube-system | Running (RBD storage, external Ceph) |
| ArgoCD | argocd | Running (GitOps, App-of-Apps) |
**Removed** (2026-07-12): MariaDB Operator + CRDs — namespace
`mariadb` and `mariadb-operator` deleted. Galera runs as native VMs.
## ArgoCD Applications
| App | Sync Wave | Status | Description |
|-----|-----------|--------|-------------|
| root | — | Synced/Healthy | Root App-of-Apps |
| operators | 0 | Synced/Healthy | CNPG, Velero operators |
| databases | 1 | Synced/Healthy | PostgreSQL cluster |
| backups | 2 | Synced/Healthy | Velero schedules |
| memory | 3 | Unknown/Degraded | Qdrant + Ollama + Memory API |
| cloudnativepg-operator | — | Synced/Healthy | CNPG operator |
| velero-operator | — | Synced/Healthy | Velero operator |
**Removed** (2026-07-12): `mariadb-operator`, `mariadb-operator-crds`
apps — deleted from repo, CRs cleaned up manually, namespaces + CRDs purged.
## IaC Repositories (Gitea at 10.0.30.105:3000)
| Repo | Content |
|------|---------|
| dominik/iac-homelab | Main IaC: 7 epics, OpenTofu, Ansible, ArgoCD apps |
| dominik/observability-iac | Observability: Loki, Prometheus/Thanos, Grafana |
| d.schoen/terraform-proxmox-k8s | Older Terraform-Proxmox-K8s work |
| d.schoen/packer-proxmox | VM templates with Packer |
## PVE Node Map
| Node | IP | Notes |
|------|----|-------|
| proxmox1 | 10.0.20.10 | Cluster master (API endpoint) |
| proxmox2 | 10.0.20.20 | Hosts CP-03, Worker-02 |
| proxmox3 | 10.0.20.30 | Hosts CP-02, Worker-03, mgmt-runner |
| proxmox4 | 10.0.20.40 | Hosts MaxScale VM, Galera VM |
| proxmox5 | 10.0.20.50 | Hosts CP-01, Worker-01 |
| proxmox6 | 10.0.20.60 | Hosts Gitea, Authelia, Paperless |
| proxmox7 | 10.0.20.70 | Hosts monitoring, Omada, Ollama |
| n5pro | 10.0.20.91 | AMD GPU, Frigate, Seafile, Immich |
**Note**: VM placements change with HA migration. Always discover
current placement before operating (scan all nodes with `qm list`).
@@ -0,0 +1,411 @@
# RKE2 Upgrade Attempt & Tarball Recovery — 2026-07-12
## Goal
Update K8s cluster: OS updates on all VMs, RKE2 upgrade v1.35.2→v1.35.6,
Helm chart updates, fix broken components. Scope: K8s only (no PVE hosts).
## Phase 1: OS Updates (COMPLETED)
All 7 VMs updated via `qm guest exec` through Proxmox jumphost chain.
### VM→PVE-Node Discovery
Initial assumption that all VMs were on proxmox3 was WRONG. Had to scan
all PVE nodes to find actual placement:
```
118 (CP-01) → proxmox5 (NOT proxmox3)
130 (CP-02) → proxmox3
129 (CP-03) → proxmox2
128 (Worker-01) → proxmox5 (later moved to proxmox)
132 (Worker-02) → proxmox2
131 (Worker-03) → proxmox3
200 (mgmt-runner) → proxmox3
```
### Update Process
- VM200: Direct SSH (10.0.30.124) — 0 updates remaining
- VM118, VM129: Updated via qm guest exec on respective PVE nodes
- VM132: dpkg interrupted (timeout) — fixed with `dpkg --configure -a`
- All VMs: 0 remaining updates after completion
- Cluster stayed healthy throughout (6/6 nodes Ready)
### Command Pattern
```bash
ssh -i ~/.ssh/id_ed25519_proxmox root@10.0.20.10 \
"ssh -o ConnectTimeout=5 root@PVE_NODE 'qm guest exec VMID --timeout 600 \
-- sh -c \"apt-get update && apt-get upgrade -y\"'"
```
## Phase 2: RKE2 Upgrade (PARTIAL — TARBALL DISASTER)
### Initial Approach (WRONG)
Attempted manual tarball installation:
1. Downloaded `rke2.linux-amd64.tar.gz` (v1.35.6) to VM118
2. Extracted with `tar xf rke2.linux-amd64.tar.gz -C /`**FATAL ERROR**
### Tarball Destruction
The RKE2 tarball contains:
```
bin/ → RKE2 binaries
lib/ → systemd unit files
share/ → RKE2 data files
```
Extracting to `/` on Debian 12 (which uses usrmerge) replaced:
- `/bin` symlink (→ usr/bin) with a real directory containing only Rke2 binaries
- `/lib` symlink (→ usr/lib) with a real directory containing only systemd files
- Created bogus `/share` directory
Result: ALL system binaries inaccessible (`ls`, `sh`, `bash`, `systemctl`,
`mount`). `qm guest exec` returns exit code 29. VM was still running
(kernel + RKE2 processes in memory) but no new processes could start.
### Recovery: Ceph RBD Offline Mount
1. **Stopped VM118** (`qm stop 118`) — safe because 2/3 CP nodes still
provide etcd quorum
2. **Mapped RBD image** on proxmox5:
```bash
rbd map vm_disks/vm-118-disk-0 # → /dev/rbd1
sleep 2 # Wait for udev
```
3. **Found partitions**: `rbd1p1` (root fs 39.9G), `rbd1p14` (BIOS boot),
`rbd1p15` (EFI 124M)
4. **Mounted root partition**: `mount /dev/rbd1p1 /mnt/vm118-rescue`
5. **Verified damage**:
- `/mnt/vm118-rescue/bin` = real directory with only `rke2`, `rke2-killall.sh`, `rke2-uninstall.sh`
- `/mnt/vm118-rescue/lib` = real directory with only `systemd/`
- `/mnt/vm118-rescue/usr/bin/bash` = INTACT (original system binaries fine)
6. **Fixed symlinks**:
```bash
# Save RKE2 binaries
cp /mnt/vm118-rescue/bin/rke2 /mnt/vm118-rescue/usr/local/bin/
cp /mnt/vm118-rescue/bin/rke2-*.sh /mnt/vm118-rescue/usr/local/bin/
# Save systemd units
mkdir -p /mnt/vm118-rescue/usr/lib/systemd/system
cp -a /mnt/vm118-rescue/lib/systemd/system/rke2-*.service /mnt/vm118-rescue/usr/lib/systemd/system/
cp -a /mnt/vm118-rescue/lib/systemd/system/rke2-*.env /mnt/vm118-rescue/usr/lib/systemd/system/
# Restore symlinks
rm -rf /mnt/vm118-rescue/bin
ln -s usr/bin /mnt/vm118-rescue/bin
rm -rf /mnt/vm118-rescue/lib
ln -s usr/lib /mnt/vm118-rescue/lib
rm -rf /mnt/vm118-rescue/share
```
7. **Unmounted, unmapped, started VM**:
```bash
umount /mnt/vm118-rescue
rbd unmap /dev/rbd1
qm start 118
```
8. **Verified**: Guest agent responded after 60s boot. Shell works.
BUT RKE2 server stuck "activating" — v1.35.6 binary couldn't find
v1.35.6 runtime images (cached images were v1.35.2).
### Second Fix: Restore Matching Binary
Downloaded v1.35.2 tarball and extracted CORRECTLY:
```bash
wget https://github.com/rancher/rke2/releases/download/v1.35.2%2Brke2r1/rke2.linux-amd64.tar.gz
tar xf rke2.linux-amd64.tar.gz -C /usr/local/ # CORRECT!
rke2 --version # v1.35.2+rke2r1
systemctl start rke2-server
```
Node came back as Ready v1.35.2+rke2r1. Cluster fully healthy again.
### RBD Pitfalls Encountered
- **Device disappearance**: First `rbd map` created `/dev/rbd1` but it
vanished before mount. Had to unmap stale mappings and re-map.
- **No kpartx**: PVE node didn't have `kpartx`. Partition devices were
created by kernel automatically after `sleep 2` post-mapping.
- **partprobe failed**: `partprobe /dev/rbd1` gave "Could not stat device"
because the device had already disappeared. Re-mapping fixed it.
## Phase 2b: Proper Upgrade Path (PREREQS IDENTIFIED, BLOCKED ON SSH)
### IaC Repo Analysis
The `epic-2-k8s/ansible/playbook.yml` uses `lablabs.rke2` role v1.50.1:
```yaml
rke2_version: v1.35.2+rke2r1 # ← Change this to upgrade
rke2_token: "{{ lookup('env', 'RKE2_TOKEN') }}"
rke2_ha_mode: true
rke2_api_ip: 10.0.30.50
```
Play 2 does rolling restart: Workers (serial:1) → Masters (serial:1).
### Prerequisites Status
1. ✅ Ansible role installed on VM200 (`ansible-galaxy install -r requirements.yml -p /root/.ansible/roles`)
2. ✅ Inventory generated manually (tofu state shows VM IDs, IPs known)
3. ✅ RKE2 token extracted from VM130 config (`/etc/rancher/rke2/config.yaml` → `token:` line)
4. ❌ **SSH access from VM200 to K8s VMs** — `Permission denied (publickey)`
- mgmt-runner's SSH key NOT in `debian@VMs/.ssh/authorized_keys`
- Ansible inventory uses `ansible_user: debian`
- Need to deploy VM200's public key to all 6 K8s VMs before Ansible can run
### Tofu State Details
Tofu state at `/opt/tofu-state/rke2-cluster.tfstate` has VM IDs:
```
rke2-cp-01: vm_id=118, node=proxmox (original placement, HA may move)
rke2-cp-02: vm_id=130, node=proxmox2
rke2-cp-03: vm_id=129, node=proxmox3
rke2-worker-01: vm_id=128, node=proxmox
rke2-worker-02: vm_id=132, node=proxmox2
rke2-worker-03: vm_id=131, node=proxmox3
```
### User Directive
User instructed: "Nutze GitOps!" and "Neue nodes mit neuer version
erstellen. Cluster join. Verify. Workloads migrieren. Alte nodes löschen"
This means: prefer GitOps/Ansible for upgrades, or blue-green with new
VMs if rolling upgrade is too risky. NEVER manual tarball installation.
## Phase 3: openclaw-memory Removal (COMPLETED via GitOps)
### Context
User: "Openclaw memory kann weg. Nutze ich nicht mehr"
The `openclaw-memory` namespace had 3 broken pods:
- `memory-api` — CreateContainerConfigError (missing secret)
- `qdrant-0` — CreateContainerConfigError (missing secret)
- `ollama` — Running but unused
Two ExternalSecrets in `SecretSyncedError` state (wrong 1Password vault).
### Removal Steps (Clean GitOps)
1. **Deleted from Git repo**:
```bash
rm clusters/main/apps/memory.yaml # ArgoCD Application
rm -rf clusters/main/memory/ # All manifests
rm .github/workflows/epic6_prepare-memory-secrets.yml
git commit -m "chore: remove openclaw-memory (unused, deprecated)"
git push origin main # commit ebc4da5
```
2. **Deleted ArgoCD Application** (cascade=foreground):
```bash
kubectl --server=https://10.0.30.52:6443 \
delete application memory -n argocd --cascade=foreground
```
3. **Deleted namespace** (pods cleared, namespace went Terminating → Gone):
```bash
kubectl --server=https://10.0.30.52:6443 delete ns openclaw-memory
```
4. **Verified**: Namespace NotFound, 0 non-running pods, ArgoCD apps
all Healthy except `backups` (OutOfSync, non-critical).
### Key Observations
- ArgoCD Application `--cascade=foreground` cleaned up managed resources
(deployments, services) but namespace remained — needed manual `kubectl delete ns`.
- No operator CRs involved, so no finalizer issues (unlike MariaDB removal).
- `backups` ArgoCD app remains OutOfSync (pre-existing, not related).
## Phase 2c: SSH Key Deployment & Ansible Launch (COMPLETED)
### Problem
Ansible on VM200 could not SSH to K8s VMs — `Permission denied (publickey)`.
VM200 had no SSH keypair at all (`~/.ssh/` only had `authorized_keys` and
`known_hosts`).
### Fix
1. **Generated SSH key on VM200**:
```bash
ssh root@10.0.30.124 "ssh-keygen -t ed25519 -f ~/.ssh/id_ed25519 -N '' -C 'vm200-ansible'"
```
2. **Confirmed actual VM→PVE-Node mapping** (all 6 nodes scanned):
```
118 (CP-01) → proxmox5
130 (CP-02) → proxmox3
129 (CP-03) → proxmox2
128 (Worker-01) → proxmox5 (later: proxmox)
131 (Worker-03) → proxmox3
132 (Worker-02) → proxmox2
```
3. **Distributed public key to all 6 VMs** via `qm guest exec` — each VM
got the key appended to `/home/debian/.ssh/authorized_keys` with
correct ownership (debian:debian) and permissions (700/600).
All 6 returned OK.
4. **Verified Ansible connectivity**:
```bash
cd /root/iac-homelab/epic-2-k8s/ansible && ansible all -m ping
```
All 6 nodes: SUCCESS => { "ping": "pong" }
### Ansible Playbook Execution
With all prerequisites met, launched the upgrade:
```bash
cd /root/iac-homelab/epic-2-k8s/ansible
export RKE2_TOKEN='EyMCxVDobAkWjgyPDcwvXRJQHgWHoH6qgDcvFhQUkd'
ansible-playbook playbook.yml -v
```
The playbook:
- Play 1: `lablabs.rke2` role applies `rke2_version: v1.35.6+rke2r1` to
all nodes (writes config, downloads/installs new binaries)
- Play 2: Rolling restart Workers (serial: 1) — each node restarted,
waits for RKE2 agent active + node Ready
- Play 3: Rolling restart Masters (serial: 1) — same pattern for CP
GitOps trail:
- Commit `6cb1b3c`: `feat(rke2): upgrade v1.35.2 → v1.35.6+rke2r1`
- Commit `ebc4da5`: `chore: remove openclaw-memory (unused, deprecated)`
## Phase 2d: dpkg Lock Blocking Ansible (RESOLVED)
### Problem
First Ansible playbook run failed on 5 of 6 nodes. Only Worker-01
succeeded (it had been updated cleanly earlier). The other 5 failed
at the pre_tasks stage with:
```
E: dpkg was interrupted, you must manually run 'sudo dpkg --configure -a'
```
Root cause: Previous OS updates via `qm guest exec` left dpkg in a
half-configured state on 5 nodes. The `openssh-server` package was
mid-upgrade (held a config file conflict prompt that hung without a TTY).
### Fix Sequence
Had to fix each node individually via `qm guest exec` (could not use
Ansible ad-hoc commands because Ansible itself triggers apt):
```bash
# For each broken node, on its hosting PVE node:
ssh root@PVE_NODE "qm guest exec VMID --timeout 120 -- bash -c \"
fuser -k /var/lib/dpkg/lock-frontend 2>/dev/null;
fuser -k /var/lib/dpkg/lock 2>/dev/null;
fuser -k /var/cache/debconf/config.dat 2>/dev/null;
sleep 2;
rm -f /var/lib/dpkg/lock-frontend /var/lib/dpkg/lock;
DEBIAN_FRONTEND=noninteractive dpkg --configure -a &&
apt-get install -y ceph-common rbd-nbd &&
echo FIXED
\""
```
All 5 nodes fixed (CP-01, CP-02, CP-03, Worker-02, Worker-03). Then
re-ran `ansible-playbook playbook.yml` successfully.
### Key Insight
The `lablabs.rke2` role's pre_tasks install `ceph-common` and `rbd-nbd`
via apt. If any node has dpkg in an interrupted state, the ENTIRE
playbook fails on that node. Must fix dpkg on ALL nodes before running
the upgrade playbook. Use `DEBIAN_FRONTEND=noninteractive` to avoid
config file conflict prompts hanging under `qm guest exec` (no TTY).
## Phase 2e: Upgrade Completion & Transient CP Failure (COMPLETED)
### Result
Second Ansible playbook run succeeded. All 6 nodes upgraded to
v1.35.6+rke2r1. PLAY RECAP showed `failed=1` on CP-03 (VM129) — but
investigation via `qm guest exec 129` confirmed the service was already
active and running v1.35.6+rke2r1. The failure was a timing race:
Ansible's `systemctl restart rke2-server` raced with RKE2's internal
reload from the binary swap. The service had already started before
Ansible tried to restart it, causing a transient "already activating"
error.
### Verification
```bash
kubectl get nodes -o wide
# All 6 nodes: Ready, v1.35.6+rke2r1, containerd 2.2.5-k3s2
kubectl get pods -A | grep -v Running | grep -v Completed
# Empty — 0 broken pods
```
### Lesson: Transient CP restart failures can be benign
If a CP node shows `failed=1` in the PLAY RECAP but is `Ready` and
running the target version, the failure was a race condition between
Ansible's `systemctl restart` and RKE2's internal reload during the
binary swap. No remediation needed — verify cluster state with
`kubectl get nodes` rather than blindly re-running the playbook.
## Remaining Work
1. ✅ OS Updates — DONE
2. ✅ RKE2 Upgrade v1.35.2→v1.35.6 — COMPLETED (commit 6cb1b3c)
3. ✅ Helm Chart Updates — COMPLETED (commits 36eb5c4, acd3231)
See `references/helm-chart-upgrades-2026-07.md` for full detail.
ArgoCD v2→v3, External Secrets, Ceph CSI RBD, CNPG, Velero all upgraded.
4. ✅ openclaw-memory — REMOVED via GitOps (commit ebc4da5)
5. ✅ ArgoCD `backups` app OutOfSync — ROOT CAUSE FOUND (cosmetic:
ESO default fields in live spec not in Git YAML). Fix pending
user approval (read-only investigation completed).
## Lessons Learned
1. **NEVER extract RKE2 tarball to `/`** — always use `/usr/local/`
2. **Debian 12 usrmerge**: `/bin`, `/lib`, `/sbin` are symlinks, not
real directories. Any tarball that creates `bin/` or `lib/` at root
will destroy the system.
3. **Ceph RBD offline mount** is viable for VM filesystem repair when
guest agent is broken — stop VM, map RBD, mount partition, fix, unmount, start.
4. **Always discover VM→PVE-Node mapping dynamically** — tofu state and
actual placement diverge due to HA migrations.
5. **Etcd quorum**: With 3 CP nodes, stopping 1 is safe (2/3 quorum).
Never stop 2 simultaneously.
6. **RKE2 version must match cached runtime images** — a newer binary
without matching images will loop "activating" forever.
7. **GitOps-first**: User expects cluster changes through IaC repo, not
manual interventions. Manual SSH/qm-guest-exec should be last resort.
8. **Non-operator app removal via GitOps** is straightforward: delete
manifests → push → delete ArgoCD App (cascade) → delete namespace.
No finalizer complications unlike operator-managed resources.
9. **Ansible SSH prerequisite**: mgmt-runner (VM200) needs its SSH public
key in `debian@VMs/.ssh/authorized_keys` before Ansible can reach K8s nodes.
Generate keypair on VM200, distribute via `qm guest exec` to all 6 VMs,
verify with `ansible all -m ping` before running the playbook.
10. **GitOps-first principle reinforced**: User explicitly directed to use
the IaC repo (`rke2_version` variable in Ansible playbook) for upgrades
rather than manual operations. All cluster changes should go through
`dominik/iac-homelab` on Gitea → commit → push → Ansible/ArgoCD.
11. **dpkg interrupted state blocks Ansible**: If OS updates via `qm guest
exec` leave dpkg half-configured (e.g. openssh-server mid-upgrade with
a config file conflict prompt hanging without TTY), the Ansible
playbook's pre_tasks (`apt install ceph-common rbd-nbd`) will fail on
that node. Fix: `fuser -k` on dpkg AND debconf locks, then
`DEBIAN_FRONTEND=noninteractive dpkg --configure -a` on each affected
node BEFORE running the upgrade playbook.
12. **debconf has its own lock**: Beyond `/var/lib/dpkg/lock-frontend` and
`/var/lib/dpkg/lock`, debconf maintains `/var/cache/debconf/config.dat`.
A stale process holding this lock causes `dpkg --configure -a` to fail
even after clearing dpkg locks. Must `fuser -k` all three.
13. **Transient CP restart failures can be benign**: Ansible's
`systemctl restart rke2-server` can race with RKE2's internal reload
during the binary swap. If a CP node shows `failed=1` in PLAY RECAP
but is `Ready` and running the target version, the failure was a
timing race — no remediation needed. Always verify with
`kubectl get nodes -o wide` rather than blindly re-running the playbook.