From 68345648d9d0661ab1f78557e4161f7742da4c4f Mon Sep 17 00:00:00 2001 From: Moritz Graf Date: Sun, 21 Jun 2026 07:22:53 +0200 Subject: [PATCH] Latest state of openclaw and traefik --- k8s/AGENTS.md | 12 +-- k8s/README.md | 27 ++++- k8s/development/registry_ingress.yaml | 4 +- k8s/kuard/ingress.yaml | 13 ++- k8s/nextcloud/nextcloud-phpmyadmin.yml | 4 +- k8s/openclaw/AGENTS.md | 98 +++++++++--------- k8s/openclaw/README.md | 109 +++++++++++++++++++++ k8s/openclaw/openclaw-instance.secret.yaml | Bin 0 -> 7795 bytes k8s/openclaw/openclaw.secret.yaml | Bin 19703 -> 514 bytes k8s/traefik/ingress-classes.yaml | 13 +++ k8s/traefik/traefik-values.yaml | 66 +++++++++++++ 11 files changed, 278 insertions(+), 68 deletions(-) create mode 100644 k8s/openclaw/README.md create mode 100644 k8s/openclaw/openclaw-instance.secret.yaml create mode 100644 k8s/traefik/ingress-classes.yaml create mode 100644 k8s/traefik/traefik-values.yaml diff --git a/k8s/AGENTS.md b/k8s/AGENTS.md index b4821f1..390bf73 100644 --- a/k8s/AGENTS.md +++ b/k8s/AGENTS.md @@ -89,16 +89,16 @@ ssh -t moritz@haumdaucher.de "sudo df -h" ``` ## Ingress Configuration -Ingress resources **must** follow these strict conventions to work with the cluster's ingress controller (`nginx`) and certificate manager (`cert-manager`). +Ingress resources **must** follow these strict conventions to work with the cluster's ingress controller (`traefik`) and certificate manager (`cert-manager`). ### Annotations All Ingress resources must include: ```yaml annotations: - kubernetes.io/ingress.class: "nginx" + kubernetes.io/ingress.class: "traefik" cert-manager.io/cluster-issuer: "letsencrypt-prod" kubernetes.io/tls-acme: "true" - # Standard nginx tweaks + # Standard nginx tweaks (if using dual class) or traefik configurations nginx.ingress.kubernetes.io/proxy-body-size: "0" nginx.ingress.kubernetes.io/ssl-redirect: "true" nginx.ingress.kubernetes.io/force-ssl-redirect: "true" @@ -107,13 +107,13 @@ annotations: ### Hostnames & TLS * **Domain**: Use a subdomain of `haumdaucher.de` or `moritzgraf.de`. * **TLS Secret Name**: Must use **hyphens** instead of dots. - * Pattern: `--` - * Example: `n8n.moritzgraf.de` -> `n8n-moritzgraf-de` +* **Pattern**: `--` +* **Example**: `n8n.moritzgraf.de` -> `n8n-moritzgraf-de` ### Example ```yaml spec: - ingressClassName: nginx + ingressClassName: traefik tls: - hosts: - n8n.moritzgraf.de diff --git a/k8s/README.md b/k8s/README.md index 18d1d28..0ba2a42 100644 --- a/k8s/README.md +++ b/k8s/README.md @@ -34,14 +34,33 @@ kubcetl edit -n mailu secret sh.helm.release.v1.mailu.v8 # Deployment (non persistent stuff) -## [ingress-nginx](https://github.com/kubernetes/ingress-nginx/tree/master/charts/ingress-nginx) +## [Traefik Ingress Controller](https://github.com/traefik/traefik-helm-chart) (Replaces retired ingress-nginx) -Apply with helm: +Apply Dual IngressClass configuration (supporting both legacy `nginx` and new `traefik` names on the same controller): ```bash -helm repo add ingress-nginx https://kubernetes.github.io/ingress-nginx +kubectl apply -f traefik/ingress-classes.yaml +``` + +Apply with Helm: + +```bash +helm repo add traefik https://traefik.github.io/charts helm repo update -helm upgrade --install --create-namespace ingress-nginx ingress-nginx/ingress-nginx -n ingress-nginx -f ingress-nginx/ingress-nginx.yaml +helm upgrade --install traefik traefik/traefik -n kube-system -f traefik/traefik-values.yaml +``` + +### [Legacy ingress-nginx (RETIRED)](https://github.com/kubernetes/ingress-nginx/tree/master/charts/ingress-nginx) + +The community ingress-nginx is **retired** (EOL March 2026). During hot-swap, it was scaled down to 0 replicas. Keep it scaled down for a few days before deleting: + +```bash +# To scale down (executed during migration) +kubectl scale daemonset ingress-nginx-controller -n ingress-nginx --replicas=0 + +# Permanent cleanup (execute after 3-7 days safety buffer) +# helm uninstall ingress-nginx -n ingress-nginx +# kubectl delete ns ingress-nginx ``` ## [cert-manager](https://cert-manager.io/docs/tutorials/acme/ingress/) diff --git a/k8s/development/registry_ingress.yaml b/k8s/development/registry_ingress.yaml index d44a200..d36d0ff 100644 --- a/k8s/development/registry_ingress.yaml +++ b/k8s/development/registry_ingress.yaml @@ -8,7 +8,7 @@ metadata: kubernetes.io/tls-acme: "true" # ---------------------------------------------- cert-manager.io/cluster-issuer: letsencrypt-prod - kubernetes.io/ingress.class: nginx + kubernetes.io/ingress.class: traefik meta.helm.sh/release-name: docker-registry meta.helm.sh/release-namespace: development nginx.ingress.kubernetes.io/force-ssl-redirect: "true" @@ -21,7 +21,7 @@ metadata: release: docker-registry spec: # --- ADDED: Critical for modern K8s --- - ingressClassName: nginx + ingressClassName: traefik # -------------------------------------- rules: - host: registry.haumdaucher.de diff --git a/k8s/kuard/ingress.yaml b/k8s/kuard/ingress.yaml index a64d479..661d185 100644 --- a/k8s/kuard/ingress.yaml +++ b/k8s/kuard/ingress.yaml @@ -1,12 +1,11 @@ -apiVersion: extensions/v1beta1 +apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: kuard namespace: kuard annotations: - kubernetes.io/ingress.class: "nginx" + kubernetes.io/ingress.class: "traefik" cert-manager.io/cluster-issuer: "letsencrypt-prod" - spec: tls: - hosts: @@ -17,6 +16,10 @@ spec: http: paths: - path: / + pathType: Prefix backend: - serviceName: kuard - servicePort: 80 + service: + name: kuard + port: + number: 80 + diff --git a/k8s/nextcloud/nextcloud-phpmyadmin.yml b/k8s/nextcloud/nextcloud-phpmyadmin.yml index a132959..3548dc1 100644 --- a/k8s/nextcloud/nextcloud-phpmyadmin.yml +++ b/k8s/nextcloud/nextcloud-phpmyadmin.yml @@ -5,7 +5,7 @@ ingress: enabled: true hostname: "nextcloud.phpmyadmin.haumdaucher.de" tls: "true" - ingressClassName: "nginx" + ingressClassName: "traefik" # hosts: # - path: "/" # tls: true @@ -14,5 +14,5 @@ ingress: annotations: cert-manager.io/cluster-issuer: "letsencrypt-prod" nginx.ingress.kubernetes.io/proxy-body-size: "0" - kubernetes.io/ingress.class: nginx + kubernetes.io/ingress.class: traefik nginx.ingress.kubernetes.io/force-ssl-redirect: "true" \ No newline at end of file diff --git a/k8s/openclaw/AGENTS.md b/k8s/openclaw/AGENTS.md index 2e41f55..8fc3e70 100644 --- a/k8s/openclaw/AGENTS.md +++ b/k8s/openclaw/AGENTS.md @@ -1,69 +1,69 @@ -# OpenClaw Agent Guide +# OpenClaw Agent Guide (Operator-Managed) -This document provides a comprehensive technical reference for AI agents to manage the **OpenClaw** deployment in this repository. +This document provides a technical reference for AI agents managing the **OpenClaw** deployment in this repository. + +--- ## 🏗️ Architecture & Configuration Lifecycle -### 1. Status -* **Telegram**: Configured with `dmPolicy: "allowlist"` for users `306373425` and `255114390`. -* **Skills**: Integrated `gog` (Workspace), `nano-banana-pro` (Image Gen), and various utility skills. -* **Authentication**: Multi-provider setup with Gemini CLI OAuth (Primary) and Gemini API Key (Backup). -* **Ollama**: Removed from the deployment. +The deployment has been migrated to the **OpenClaw Operator** framework. -### 2. Bootstrap Process -OpenClaw uses an `initContainer` to bootstrap the configuration: -1. The `openclaw-bootstrap-config` volume is mounted at `/mnt/config`. -2. The `initContainer` copies `/mnt/config/openclaw.json` to the persistent data volume at `/mnt/data/openclaw.json`. -3. The `initContainer` provisions authentication tokens (e.g., `google-gemini-cli.json`) from environment variables/secrets. -4. The main `openclaw` container identifies the persistent volume at `/home/node/.openclaw`. +### 1. Persistent Storage & State Protection +* **Storage Claim**: Uses the existing PVC `openclaw-data` (`openebs-hostpath`). +* **Zero-Drift Merge**: Configured with `mergeMode: merge`. Any declarative base config applied via K8s is safely deep-merged with runtime configuration mutations (e.g. linked channels, active sessions) written by the agent inside the PVC at `/home/openclaw/.openclaw/openclaw.json`. +* **Stateful Memory**: The agent's wisdom (`MEMORY.md`, `SOUL.md`, SQLite databases) resides in the persistent claim and survives pod restarts naturally. -### 3. Gemini OAuth Setup & Sync -This deployment uses a **local-to-remote** sync for Gemini OAuth: -1. **Local Login**: The user runs `openclaw models auth login --provider google-gemini-cli` on their local machine. -2. **Credential Capture**: This generates `~/.gemini/oauth_creds.json` locally. -3. **Secret Update**: The JSON content from that file is copied into the `gemini-oauth-token` field of `openclaw.secret.yaml`. -4. **Provisioning**: The `initContainer` in the K8s manifest reads the `GEMINI_OAUTH_TOKEN` env var (populated from the secret) and writes it to `/home/node/.openclaw/auth/google-gemini-cli.json`. +### 2. Sidecar Mapping -### 4. Applying Changes -To update the configuration or rotate tokens: -1. Modify the relevant fields in [openclaw.secret.yaml](file:///Users/moritz/src/infrapuzzle/k8s/openclaw/openclaw.secret.yaml). -2. Apply the manifest: `kubectl apply -f k8s/openclaw/openclaw.secret.yaml` -3. **Rotate Deployment**: You MUST restart the pod to trigger the `initContainer` bootstrap and inject new env vars: - `kubectl rollout restart deployment openclaw -n openclaw` +The multi-container pod structure is managed natively by the operator controller: + +| Legay Pod Container | Operator Controller Replacement | +|---|---| +| `chromium-sidecar` | Managed natively under `spec.chromium` with autowired CDP endpoint | +| `sidecar-proxy` | Managed natively by operator gateway proxy sidecar | +| `git-sync` | Retained under `spec.sidecars` to fetch skills from private git repository | +| `skill-stabilizer` | Retained under `spec.sidecars` to copy skills to the flat PVC structure | +| `install-uv-python` (init) | Managed natively by the operator under `spec.runtimeDeps.python: true` | --- -## 🔧 Configuration Reference (`openclaw.json`) +## 🔧 Applying Configuration Changes -### `models.providers` -- **`google`**: Built-in provider. Uses `GEMINI_API_KEY`. See [GEMINI_AUTH_GUIDE.md](file:///Users/moritz/src/infrapuzzle/k8s/openclaw/GEMINI_AUTH_GUIDE.md). -- **`google-gemini-cli`**: OAuth-based provider (Primary). Uses provisioned tokens. -### `agents.defaults` -- `model.primary`: `google-gemini-cli/gemini-3-flash-preview` -- `model.fallbacks`: `["google/gemini-flash-latest"]` - -> [!IMPORTANT] -> Gemini 3 requires `previewFeatures: true` in `~/.gemini/settings.json`, which is automatically provisioned by the `initContainer`. A **rollout restart** is required after any manifest change. - -### `plugins` -- `google-gemini-cli-auth`: MUST be enabled for the primary provider to function. +To perform modifications or rotate api keys: +1. **Secrets**: Edit string fields in [openclaw.secret.yaml](file:///Users/moritz/src/infrapuzzle/k8s/openclaw/openclaw.secret.yaml). +2. **Declarative Base Config**: Edit fields under `spec.config.raw` in [openclaw-instance.secret.yaml](file:///Users/moritz/src/infrapuzzle/k8s/openclaw/openclaw-instance.secret.yaml). +3. **Apply & Rollout**: Apply the files. The operator tracks config hashes and performs an automated rolling update of the StatefulSet automatically (no manual restart needed!): + ```bash + kubectl apply -f k8s/openclaw/openclaw.secret.yaml + kubectl apply -f k8s/openclaw/openclaw-instance.secret.yaml + ``` --- -## 🚨 Startup & Troubleshooting +## 🚨 Troubleshooting & Live Verification -### Investigating Issues +If the agent is offline or failing: + +### 1. Verify Deployment Health +Ensure that the `OpenClawInstance` is successfully reconciled: ```bash -# Check config -kubectl exec -it -n openclaw deployment/openclaw -c openclaw -- cat /home/node/.openclaw/openclaw.json - -# Check auth tokens -kubectl exec -it -n openclaw deployment/openclaw -c openclaw -- ls -la /home/node/.openclaw/auth/ +kubectl get openclawinstance openclaw -n openclaw +# Expected: PHASE=Running, READY=True ``` -### Applying Configuration Changes +### 2. Verify Pod & Container States +Check that all 6 containers inside the StatefulSet are fully functional: ```bash -kubectl apply -f k8s/openclaw/openclaw.secret.yaml -kubectl rollout restart deployment openclaw -n openclaw -kubectl rollout status deployment openclaw -n openclaw +kubectl get pods -n openclaw -l app.kubernetes.io/instance=openclaw +``` + +### 3. Check Live Logs +Inspect the core gateway engine: +```bash +kubectl logs -n openclaw openclaw-0 -c openclaw --tail=100 +``` + +Inspect the Chromium browser engine: +```bash +kubectl logs -n openclaw openclaw-0 -c chromium --tail=100 ``` diff --git a/k8s/openclaw/README.md b/k8s/openclaw/README.md new file mode 100644 index 0000000..d2c36fd --- /dev/null +++ b/k8s/openclaw/README.md @@ -0,0 +1,109 @@ +# OpenClaw Operations Guide (Operator-Managed) + +This directory manages the deployment of the **OpenClaw** stateful AI agent on the single-node `haumdaucher` cluster. + +The deployment is managed by the official **OpenClaw Operator** (`openclaw-operator`) using an `OpenClawInstance` Custom Resource, replacing the legacy 571-line manual deployment with a modern, zero-drift GitOps-friendly framework. + +--- + +## 🏗️ Architecture Layout + +* **Operator System**: Watches the `openclaw` namespace and reconciles the `OpenClawInstance` resource. +* **Deep-Merge Config**: Configured with `mergeMode: merge` to prevent configuration clobbering. Declarative settings from Git are safely merged with dynamic configurations (such as linked Telegram accounts or runtime credentials) mutated by the agent on the PVC. +* **Built-in Sidecars**: + * **Headless Chromium**: Auto-injected and wired to `http://127.0.0.1:9222` for browser automation and tools. + * **Gateway Proxy**: Handles token-based authentication and secure WebSocket forwarding natively. +* **Custom Sidecars**: + * **git-sync**: Pulls custom skills dynamically from your private repository `git.moritzgraf.de/moritz/mop-skills`. + * **skill-stabilizer**: Continuously copies checked-out skills into the flat `/home/openclaw/.openclaw/skills` workspace. + +--- + +## 🚀 Installation & Setup + +### 1. Install the Operator (One-time) + +The operator is installed globally via Helm: + +```bash +helm install openclaw-operator \ + oci://ghcr.io/openclaw-rocks/charts/openclaw-operator \ + --namespace openclaw-operator-system \ + --create-namespace +``` + +### 2. Apply Custom Secrets and CRD Instance + +The deployment resources are applied directly via `kubectl`: + +```bash +# Apply raw secrets (git-crypt encrypted in the repository) +kubectl apply -f k8s/openclaw/openclaw.secret.yaml + +# Apply the custom resource definition instance +kubectl apply -f k8s/openclaw/openclaw-instance.secret.yaml +``` + +--- + +## 🔑 Gateway Access (Token Auth) + +Basic Auth (`htpasswd`) has been retired. The operator-injected gateway proxy implements token-based authentication. + +To access the Control UI in your browser, append your gateway token as a URL fragment: + +``` +https://openclaw.haumdaucher.de/#token= +``` + +> The `` corresponds to the `gateway-token` value configured inside the `openclaw-secrets` Secret. + +--- + +## 💾 Backup & Disaster Recovery + +Because your deployment relies on `openebs-hostpath` persistent storage, **all state resides on a single physical node disk**. Local pre-migration backups should be kept in case of host or cluster recreation. + +### Perform a Local Backup +Run the following script to pull your memory files, SQLite databases, paired devices, and active configs locally: + +```bash +# Define target backup dir +BACKUP_DIR="k8s/openclaw/backup" +mkdir -p "$BACKUP_DIR" + +# Get active pod name +POD_NAME=$(kubectl get pods -n openclaw -l app.kubernetes.io/instance=openclaw -o jsonpath='{.items[0].metadata.name}') + +# Copy critical runtime files (excluding 2.9GB model files) +for item in openclaw.json workspace memory credentials identity devices telegram settings scripts tasks subagents canvas; do + kubectl cp -n openclaw -c openclaw "$POD_NAME:/home/openclaw/.openclaw/$item" "$BACKUP_DIR/$item" +done +``` + +### Restore from a Local Backup +If your PVC is recreated or wiped, you can push the backup files back to the new pod: + +```bash +# Push directories back to the running pod +kubectl cp "$BACKUP_DIR/workspace" openclaw-0:/home/openclaw/.openclaw/workspace -n openclaw -c openclaw +# Repeat for other folders as needed +``` + +--- + +## 🔧 Operational Verification Commands + +```bash +# Check the controller reconciliation phase (Expected: PHASE=Running) +kubectl get openclawinstances -n openclaw + +# View the status of all 6 containers inside the StatefulSet pod +kubectl get pods -n openclaw -o wide + +# Review live logs of the core OpenClaw engine +kubectl logs -n openclaw openclaw-0 -c openclaw --tail=100 -f + +# Review live logs of the Chromium sidecar +kubectl logs -n openclaw openclaw-0 -c chromium --tail=100 -f +``` diff --git a/k8s/openclaw/openclaw-instance.secret.yaml b/k8s/openclaw/openclaw-instance.secret.yaml new file mode 100644 index 0000000000000000000000000000000000000000..54dff82ebbd228d6e13728986c4b132cc7705875 GIT binary patch literal 7795 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 KX6|CoDiEUL^LbMM diff --git a/k8s/traefik/ingress-classes.yaml b/k8s/traefik/ingress-classes.yaml new file mode 100644 index 0000000..74b7c60 --- /dev/null +++ b/k8s/traefik/ingress-classes.yaml @@ -0,0 +1,13 @@ +apiVersion: networking.k8s.io/v1 +kind: IngressClass +metadata: + name: nginx +spec: + controller: traefik.io/ingress-controller +--- +apiVersion: networking.k8s.io/v1 +kind: IngressClass +metadata: + name: traefik +spec: + controller: traefik.io/ingress-controller diff --git a/k8s/traefik/traefik-values.yaml b/k8s/traefik/traefik-values.yaml new file mode 100644 index 0000000..900f115 --- /dev/null +++ b/k8s/traefik/traefik-values.yaml @@ -0,0 +1,66 @@ +deployment: + kind: DaemonSet + dnsPolicy: ClusterFirstWithHostNet + +hostNetwork: true + +# Bind directly to host ports 80 and 443 +ports: + web: + port: 80 + hostPort: 80 + expose: + default: true + exposedPort: 80 + websecure: + port: 443 + hostPort: 443 + expose: + default: true + exposedPort: 443 + # Avoid port collision with node-exporter on host network (9100) + metrics: + port: 9101 + hostPort: 9101 + exposedPort: 9101 + +# Configure Traefik to watch for standard Kubernetes Ingress resources +providers: + kubernetesIngress: + enabled: true + publishedService: + enabled: false + +# We will define IngressClass resources manually to achieve dual-class mapping +ingressClass: + enabled: false + +# Resource limits to ensure stable execution on a single node +resources: + requests: + cpu: 100m + memory: 128Mi + limits: + cpu: 500m + memory: 512Mi + +# Run as root (UID/GID 0) to bind to host network ports 80/443 +podSecurityContext: + runAsGroup: 0 + runAsNonRoot: false + runAsUser: 0 + +securityContext: + allowPrivilegeEscalation: true + capabilities: + drop: [] + add: + - NET_BIND_SERVICE + readOnlyRootFilesystem: false + +# Required for hostNetwork DaemonSets to allow rolling updates +updateStrategy: + type: RollingUpdate + rollingUpdate: + maxUnavailable: 1 + maxSurge: 0