From 6635d70170233e91a5e140a66a5629bcf8a76d12 Mon Sep 17 00:00:00 2001 From: sirily11 <32106111+sirily11@users.noreply.github.com> Date: Tue, 27 Jan 2026 11:33:53 +0800 Subject: [PATCH] feat: add helm chart support (#1) feat: enhance Helm chart workflows and update image references --- .github/labeler.yml | 12 + .github/workflows/helm-release.yml | 124 +++++ .github/workflows/helm-test.yml | 125 +++++ README.md | 4 +- charts/clawdbot/.helmignore | 23 + charts/clawdbot/Chart.yaml | 21 + charts/clawdbot/PUBLISHING.md | 253 +++++++++ charts/clawdbot/README.md | 302 +++++++++++ charts/clawdbot/examples/values-basic.yaml | 33 ++ charts/clawdbot/examples/values-fly-like.yaml | 58 +++ .../clawdbot/examples/values-production.yaml | 92 ++++ charts/clawdbot/templates/NOTES.txt | 46 ++ charts/clawdbot/templates/_helpers.tpl | 84 +++ charts/clawdbot/templates/configmap.yaml | 11 + charts/clawdbot/templates/ingress.yaml | 41 ++ charts/clawdbot/templates/secret.yaml | 23 + charts/clawdbot/templates/service.yaml | 19 + charts/clawdbot/templates/serviceaccount.yaml | 12 + charts/clawdbot/templates/statefulset.yaml | 202 ++++++++ .../templates/tests/test-connection.yaml | 15 + charts/clawdbot/values.schema.json | 74 +++ charts/clawdbot/values.yaml | 227 +++++++++ docs/install/index.md | 1 + docs/install/kubernetes.md | 482 ++++++++++++++++++ scripts/publish-helm-chart.sh | 144 ++++++ scripts/test-helm-local.sh | 167 ++++++ 26 files changed, 2593 insertions(+), 2 deletions(-) create mode 100644 .github/workflows/helm-release.yml create mode 100644 .github/workflows/helm-test.yml create mode 100644 charts/clawdbot/.helmignore create mode 100644 charts/clawdbot/Chart.yaml create mode 100644 charts/clawdbot/PUBLISHING.md create mode 100644 charts/clawdbot/README.md create mode 100644 charts/clawdbot/examples/values-basic.yaml create mode 100644 charts/clawdbot/examples/values-fly-like.yaml create mode 100644 charts/clawdbot/examples/values-production.yaml create mode 100644 charts/clawdbot/templates/NOTES.txt create mode 100644 charts/clawdbot/templates/_helpers.tpl create mode 100644 charts/clawdbot/templates/configmap.yaml create mode 100644 charts/clawdbot/templates/ingress.yaml create mode 100644 charts/clawdbot/templates/secret.yaml create mode 100644 charts/clawdbot/templates/service.yaml create mode 100644 charts/clawdbot/templates/serviceaccount.yaml create mode 100644 charts/clawdbot/templates/statefulset.yaml create mode 100644 charts/clawdbot/templates/tests/test-connection.yaml create mode 100644 charts/clawdbot/values.schema.json create mode 100644 charts/clawdbot/values.yaml create mode 100644 docs/install/kubernetes.md create mode 100644 scripts/publish-helm-chart.sh create mode 100755 scripts/test-helm-local.sh diff --git a/.github/labeler.yml b/.github/labeler.yml index 5c19fa418..b2d44dfd2 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml @@ -220,3 +220,15 @@ - changed-files: - any-glob-to-any-file: - "extensions/qwen-portal-auth/**" + +"kubernetes": + - changed-files: + - any-glob-to-any-file: + - "charts/**" + - "docs/install/kubernetes.md" + - ".github/workflows/helm-test.yml" + +"helm": + - changed-files: + - any-glob-to-any-file: + - "charts/**" diff --git a/.github/workflows/helm-release.yml b/.github/workflows/helm-release.yml new file mode 100644 index 000000000..67c4662cb --- /dev/null +++ b/.github/workflows/helm-release.yml @@ -0,0 +1,124 @@ +name: Release Helm Chart + +on: + push: + branches: + - main + paths: + - 'charts/**' + tags: + - 'v*' + workflow_dispatch: + +env: + REGISTRY: ghcr.io + IMAGE_NAME: ${{ github.repository }} + +jobs: + # Validate chart on all triggers + validate: + name: Validate Helm Chart + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install Helm + uses: azure/setup-helm@v4 + with: + version: v3.13.0 + + - name: Lint chart + run: helm lint charts/clawdbot + + - name: Lint with production values + run: helm lint charts/clawdbot -f charts/clawdbot/values.yaml + + - name: Template validation + run: helm template test charts/clawdbot --debug + + # Release chart only on tag push + release: + name: Release Helm Chart + runs-on: ubuntu-latest + needs: validate + if: startsWith(github.ref, 'refs/tags/') + permissions: + contents: write + pages: write + id-token: write + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Configure Git + run: | + git config user.name "$GITHUB_ACTOR" + git config user.email "$GITHUB_ACTOR@users.noreply.github.com" + + - name: Install Helm + uses: azure/setup-helm@v4 + with: + version: v3.13.0 + + - name: Install yq + uses: mikefarah/yq@v4 + + - name: Extract version from tag + id: version + run: | + VERSION=${GITHUB_REF#refs/tags/v} + echo "version=$VERSION" >> $GITHUB_OUTPUT + echo "Extracted version: $VERSION" + + - name: Update Chart.yaml with version + run: | + VERSION="${{ steps.version.outputs.version }}" + echo "Updating Chart.yaml with version: $VERSION" + yq e '.version = strenv(VERSION)' -i charts/clawdbot/Chart.yaml + yq e '.appVersion = strenv(VERSION)' -i charts/clawdbot/Chart.yaml + cat charts/clawdbot/Chart.yaml + env: + VERSION: ${{ steps.version.outputs.version }} + + - name: Update values.yaml with image repository + run: | + echo "Updating image repository to: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}" + yq e '.image.registry = "${{ env.REGISTRY }}"' -i charts/clawdbot/values.yaml + yq e '.image.repository = "${{ env.IMAGE_NAME }}"' -i charts/clawdbot/values.yaml + cat charts/clawdbot/values.yaml | head -20 + + - name: Package Helm chart + run: | + mkdir -p .cr-release-packages + helm package charts/clawdbot -d .cr-release-packages + ls -la .cr-release-packages + + - name: Run chart-releaser + uses: helm/chart-releaser-action@v1.6.0 + with: + charts_dir: charts + skip_existing: true + env: + CR_TOKEN: "${{ secrets.GITHUB_TOKEN }}" + + # Update GitHub Pages index + publish-pages: + name: Publish to GitHub Pages + runs-on: ubuntu-latest + needs: release + if: startsWith(github.ref, 'refs/tags/') + steps: + - name: Checkout gh-pages branch + uses: actions/checkout@v4 + with: + ref: gh-pages + fetch-depth: 0 + + - name: Verify index updated + run: | + echo "Chart published successfully" + echo "Helm repo: https://clawdbot.github.io/clawdbot" + echo "Install with: helm repo add clawdbot https://clawdbot.github.io/clawdbot" diff --git a/.github/workflows/helm-test.yml b/.github/workflows/helm-test.yml new file mode 100644 index 000000000..5b7108486 --- /dev/null +++ b/.github/workflows/helm-test.yml @@ -0,0 +1,125 @@ +name: Helm Chart Tests + +on: + pull_request: + paths: + - 'charts/**' + - 'Dockerfile' + - '.github/workflows/helm-test.yml' + push: + branches: + - main + paths: + - 'charts/**' + - 'Dockerfile' + +jobs: + lint-test: + name: Lint and Test Helm Chart + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v6 + + - name: Set up Helm + uses: azure/setup-helm@v4 + with: + version: v3.13.0 + + - name: Lint Helm chart + run: | + helm lint charts/clawdbot + helm lint charts/clawdbot --values charts/clawdbot/examples/values-basic.yaml + helm lint charts/clawdbot --values charts/clawdbot/examples/values-production.yaml + + - name: Template validation + run: | + helm template test charts/clawdbot \ + --values charts/clawdbot/examples/values-basic.yaml \ + --set secrets.data.anthropicApiKey=test-key \ + > /tmp/manifests.yaml + cat /tmp/manifests.yaml + + kind-test: + name: Test on Kind + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v6 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Build Docker image + uses: docker/build-push-action@v6 + with: + context: . + file: ./Dockerfile + tags: clawdbot:test + load: true + cache-from: type=gha + cache-to: type=gha,mode=max + + - name: Create Kind cluster + uses: helm/kind-action@v1.8.0 + with: + cluster_name: clawdbot-test + config: | + kind: Cluster + apiVersion: kind.x-k8s.io/v1alpha4 + nodes: + - role: control-plane + + - name: Load image into Kind + run: | + kind load docker-image clawdbot:test --name clawdbot-test + + - name: Set up Helm + uses: azure/setup-helm@v4 + with: + version: v3.13.0 + + - name: Install Helm chart + run: | + helm install test-clawdbot charts/clawdbot \ + --values charts/clawdbot/examples/values-basic.yaml \ + --set image.repository=clawdbot \ + --set image.tag=test \ + --set image.pullPolicy=Never \ + --set secrets.data.anthropicApiKey=test-key \ + --set secrets.data.gatewayToken=$(openssl rand -hex 32) \ + --wait --timeout=3m + + - name: Verify deployment + run: | + kubectl get all -l app.kubernetes.io/name=clawdbot + kubectl wait --for=condition=ready pod -l app.kubernetes.io/name=clawdbot --timeout=120s + kubectl get pvc -l app.kubernetes.io/name=clawdbot + + - name: Check pod logs + if: always() + run: | + POD_NAME=$(kubectl get pod -l app.kubernetes.io/name=clawdbot -o jsonpath='{.items[0].metadata.name}') + kubectl logs $POD_NAME --tail=100 + + - name: Run Helm tests + run: | + helm test test-clawdbot + + - name: Test health command + run: | + POD_NAME=$(kubectl get pod -l app.kubernetes.io/name=clawdbot -o jsonpath='{.items[0].metadata.name}') + GATEWAY_TOKEN=$(kubectl get secret test-clawdbot -o jsonpath='{.data.gatewayToken}' | base64 -d) + kubectl exec $POD_NAME -- node dist/index.js health --token "$GATEWAY_TOKEN" || true + + - name: Verify persistence + run: | + POD_NAME=$(kubectl get pod -l app.kubernetes.io/name=clawdbot -o jsonpath='{.items[0].metadata.name}') + kubectl exec $POD_NAME -- ls -la /home/node/.clawdbot + kubectl exec $POD_NAME -- ls -la /home/node/clawd + + - name: Cleanup + if: always() + run: | + helm uninstall test-clawdbot || true + kubectl delete pvc -l app.kubernetes.io/name=clawdbot || true diff --git a/README.md b/README.md index 7e884be33..821c814ae 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ It answers you on the channels you already use (WhatsApp, Telegram, Slack, Disco If you want a personal, single-user assistant that feels local, fast, and always-on, this is it. -[Website](https://molt.bot) · [Docs](https://docs.molt.bot) · [Getting Started](https://docs.molt.bot/start/getting-started) · [Updating](https://docs.molt.bot/install/updating) · [Showcase](https://docs.molt.bot/start/showcase) · [FAQ](https://docs.molt.bot/start/faq) · [Wizard](https://docs.molt.bot/start/wizard) · [Nix](https://github.com/moltbot/nix-clawdbot) · [Docker](https://docs.molt.bot/install/docker) · [Discord](https://discord.gg/clawd) +[Website](https://molt.bot) · [Docs](https://docs.molt.bot) · [Getting Started](https://docs.molt.bot/start/getting-started) · [Updating](https://docs.molt.bot/install/updating) · [Showcase](https://docs.molt.bot/start/showcase) · [FAQ](https://docs.molt.bot/start/faq) · [Wizard](https://docs.molt.bot/start/wizard) · [Nix](https://github.com/moltbot/nix-clawdbot) · [Docker](https://docs.molt.bot/install/docker) · [Kubernetes](https://docs.molt.bot/install/kubernetes) · [Discord](https://discord.gg/clawd) Preferred setup: run the onboarding wizard (`moltbot onboard`). It walks through gateway, workspace, channels, and skills. The CLI wizard is the recommended path and works on **macOS, Linux, and Windows (via WSL2; strongly recommended)**. Works with npm, pnpm, or bun. @@ -164,7 +164,7 @@ Run `moltbot doctor` to surface risky/misconfigured DM policies. ### Ops + packaging - [Control UI](https://docs.molt.bot/web) + [WebChat](https://docs.molt.bot/web/webchat) served directly from the Gateway. - [Tailscale Serve/Funnel](https://docs.molt.bot/gateway/tailscale) or [SSH tunnels](https://docs.molt.bot/gateway/remote) with token/password auth. -- [Nix mode](https://docs.molt.bot/install/nix) for declarative config; [Docker](https://docs.molt.bot/install/docker)-based installs. +- [Nix mode](https://docs.molt.bot/install/nix) for declarative config; [Docker](https://docs.molt.bot/install/docker) and [Kubernetes (Helm)](https://docs.molt.bot/install/kubernetes) deployments. - [Doctor](https://docs.molt.bot/gateway/doctor) migrations, [logging](https://docs.molt.bot/logging). ## How it works (short) diff --git a/charts/clawdbot/.helmignore b/charts/clawdbot/.helmignore new file mode 100644 index 000000000..0e8a0eb36 --- /dev/null +++ b/charts/clawdbot/.helmignore @@ -0,0 +1,23 @@ +# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +.DS_Store +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*.orig +*~ +# Various IDEs +.project +.idea/ +*.tmproj +.vscode/ diff --git a/charts/clawdbot/Chart.yaml b/charts/clawdbot/Chart.yaml new file mode 100644 index 000000000..ff707fe0f --- /dev/null +++ b/charts/clawdbot/Chart.yaml @@ -0,0 +1,21 @@ +apiVersion: v2 +name: clawdbot +description: Personal AI assistant with multi-channel support +type: application +version: 1.0.0 +appVersion: "2026.1.25" +keywords: + - ai + - assistant + - whatsapp + - telegram + - discord + - claude + - chatgpt +home: https://github.com/clawdbot/clawdbot +sources: + - https://github.com/clawdbot/clawdbot +maintainers: + - name: Clawdbot Team + url: https://docs.clawd.bot +icon: https://github.com/clawdbot/clawdbot/raw/main/README-header.png diff --git a/charts/clawdbot/PUBLISHING.md b/charts/clawdbot/PUBLISHING.md new file mode 100644 index 000000000..18a245f43 --- /dev/null +++ b/charts/clawdbot/PUBLISHING.md @@ -0,0 +1,253 @@ +# Publishing the Clawdbot Helm Chart + +This guide covers how to publish the Helm chart to make it available for users. + +## Quick Start (Automated) + +The chart is automatically published to GitHub Pages when you: + +1. Update `charts/clawdbot/Chart.yaml` version +2. Commit and push to `main` branch +3. GitHub Actions automatically packages and publishes + +## Publishing Methods + +### Option 1: GitHub Pages (Recommended) + +#### Initial Setup + +1. **Enable GitHub Pages:** + - Go to repository Settings → Pages + - Source: Deploy from a branch + - Branch: `gh-pages` → `/ (root)` + - Click Save + +2. **Create gh-pages branch (first time only):** + ```bash + # Create empty gh-pages branch + git checkout --orphan gh-pages + git rm -rf . + echo "# Clawdbot Helm Charts" > README.md + git add README.md + git commit -m "Initial gh-pages" + git push origin gh-pages + git checkout main + ``` + +3. **The workflow will automatically:** + - Package the chart + - Create a GitHub release + - Update the chart repository index + - Publish to GitHub Pages + +#### Manual Publishing (if needed) + +```bash +# 1. Package the chart +helm package charts/clawdbot -d .cr-release-packages + +# 2. Create index +helm repo index .cr-release-packages --url https://clawdbot.github.io/clawdbot + +# 3. Commit to gh-pages branch +git checkout gh-pages +cp .cr-release-packages/* . +git add . +git commit -m "Release chart version X.Y.Z" +git push origin gh-pages +git checkout main +``` + +#### Usage for End Users + +Once published, users can install via: + +```bash +# Add repo +helm repo add clawdbot https://clawdbot.github.io/clawdbot +helm repo update + +# Install +helm install my-clawdbot clawdbot/clawdbot +``` + +### Option 2: OCI Registry (GitHub Container Registry) + +Modern approach using OCI registries: + +#### Setup + +```bash +# Login to GHCR +echo $GITHUB_TOKEN | helm registry login ghcr.io -u USERNAME --password-stdin + +# Package chart +helm package charts/clawdbot + +# Push to GHCR +helm push clawdbot-1.0.0.tgz oci://ghcr.io/clawdbot +``` + +#### Automate in GitHub Actions + +```yaml +- name: Login to GHCR + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + +- name: Push chart to GHCR + run: | + helm package charts/clawdbot + helm push clawdbot-*.tgz oci://ghcr.io/${{ github.repository_owner }} +``` + +#### Usage for End Users + +```bash +# Install directly from OCI +helm install my-clawdbot oci://ghcr.io/clawdbot/clawdbot --version 1.0.0 +``` + +### Option 3: Artifact Hub + +Make your chart discoverable on [Artifact Hub](https://artifacthub.io). + +#### Prerequisites + +- Chart published to GitHub Pages or OCI registry +- Artifact Hub metadata file + +#### Add Artifact Hub metadata + +Create `charts/clawdbot/artifacthub-repo.yml`: + +```yaml +repositoryID: +owners: + - name: Clawdbot Team + email: team@clawdbot.com +``` + +#### Submit to Artifact Hub + +1. Go to https://artifacthub.io +2. Sign in with GitHub +3. Add repository +4. Provide repository URL: `https://clawdbot.github.io/clawdbot` +5. Wait for verification + +### Option 4: ChartMuseum (Self-Hosted) + +For private/internal charts: + +```bash +# Run ChartMuseum +docker run -d \ + -p 8080:8080 \ + -v $(pwd)/charts:/charts \ + ghcr.io/helm/chartmuseum:latest \ + --storage local \ + --storage-local-rootdir /charts + +# Upload chart +curl --data-binary "@clawdbot-1.0.0.tgz" http://localhost:8080/api/charts +``` + +## Versioning + +Follow Semantic Versioning: + +- **Chart version** (`version` in Chart.yaml): Chart changes +- **App version** (`appVersion` in Chart.yaml): Clawdbot version + +### Bumping Versions + +```bash +# Update chart version +vim charts/clawdbot/Chart.yaml +# Change version: 1.0.0 → 1.1.0 + +# Update app version (when clawdbot version changes) +vim charts/clawdbot/Chart.yaml +# Change appVersion: "2026.1.25" → "2026.1.26" +``` + +### Version Guidelines + +- **Major** (1.0.0 → 2.0.0): Breaking changes to values.yaml or behavior +- **Minor** (1.0.0 → 1.1.0): New features, non-breaking changes +- **Patch** (1.0.0 → 1.0.1): Bug fixes, documentation + +## Release Checklist + +- [ ] Update `version` in `Chart.yaml` +- [ ] Update `appVersion` if clawdbot version changed +- [ ] Update `CHANGELOG.md` (if you have one) +- [ ] Test chart locally: `./scripts/test-helm-local.sh` +- [ ] Lint chart: `helm lint charts/clawdbot` +- [ ] Commit changes +- [ ] Push to main (triggers automated release) +- [ ] Verify GitHub Pages deployment +- [ ] Test installation from published repo + +## Testing Published Chart + +```bash +# Add your published repo +helm repo add clawdbot https://clawdbot.github.io/clawdbot +helm repo update + +# Search for chart +helm search repo clawdbot + +# Install from published repo +helm install test clawdbot/clawdbot --dry-run --debug +``` + +## Troubleshooting + +### Chart not appearing after publish + +1. Check GitHub Actions logs +2. Verify gh-pages branch exists +3. Check GitHub Pages settings are enabled +4. Wait 5-10 minutes for GitHub Pages to deploy + +### Users getting "not found" error + +```bash +# Check index.yaml exists +curl https://clawdbot.github.io/clawdbot/index.yaml + +# Verify chart package exists +curl https://clawdbot.github.io/clawdbot/clawdbot-1.0.0.tgz +``` + +### Permission denied during publishing + +Ensure GitHub Actions has write permissions: +- Settings → Actions → General → Workflow permissions +- Select "Read and write permissions" + +## Advanced: Multi-Chart Repository + +If you add more charts: + +``` +charts/ + clawdbot/ + clawdbot-operator/ + clawdbot-monitoring/ +``` + +The `helm/chart-releaser-action` automatically handles multiple charts. + +## References + +- [Helm Chart Repository Guide](https://helm.sh/docs/topics/chart_repository/) +- [Chart Releaser Action](https://github.com/helm/chart-releaser-action) +- [Artifact Hub](https://artifacthub.io/docs/topics/repositories/) +- [OCI Registry Support](https://helm.sh/docs/topics/registries/) diff --git a/charts/clawdbot/README.md b/charts/clawdbot/README.md new file mode 100644 index 000000000..52c6db06f --- /dev/null +++ b/charts/clawdbot/README.md @@ -0,0 +1,302 @@ +# Clawdbot Helm Chart + +Personal AI assistant with multi-channel support (WhatsApp, Telegram, Discord, and more) running on Kubernetes. + +## TL;DR + +```bash +helm install my-clawdbot ./clawdbot \ + --set secrets.data.anthropicApiKey=sk-ant-xxx \ + --set secrets.data.gatewayToken=$(openssl rand -hex 32) +``` + +## Introduction + +This chart deploys Clawdbot Gateway on a Kubernetes cluster using the Helm package manager. + +**Important:** Clawdbot is designed as a single-user personal assistant. The chart enforces `replicas: 1` and does not support horizontal scaling. + +## Prerequisites + +- Kubernetes 1.19+ +- Helm 3.x +- PV provisioner support in the underlying infrastructure (for persistent storage) +- **Optional:** Ingress controller (NGINX, Traefik, etc.) for external access +- **Optional:** cert-manager for automatic TLS certificates + +## Installing the Chart + +### Basic Installation + +```bash +helm install my-clawdbot ./clawdbot +``` + +### With Custom Values + +```bash +helm install my-clawdbot ./clawdbot \ + --values examples/values-production.yaml \ + --set secrets.data.anthropicApiKey=sk-ant-xxx \ + --set ingress.hosts[0].host=assistant.example.com +``` + +### From Examples + +```bash +# Basic (local testing) +helm install my-clawdbot ./clawdbot -f examples/values-basic.yaml + +# Production +helm install my-clawdbot ./clawdbot -f examples/values-production.yaml + +# Fly.io-like setup +helm install my-clawdbot ./clawdbot -f examples/values-fly-like.yaml +``` + +## Uninstalling the Chart + +```bash +helm uninstall my-clawdbot + +# Also delete PVCs (data will be lost) +kubectl delete pvc -l app.kubernetes.io/instance=my-clawdbot +``` + +## Configuration + +### Key Parameters + +| Parameter | Description | Default | +|-----------|-------------|---------| +| `replicaCount` | Number of replicas (must be 1) | `1` | +| `image.repository` | Clawdbot image repository | `clawdbot/clawdbot` | +| `image.tag` | Image tag | Chart appVersion | +| `gateway.bind` | Gateway binding mode (`loopback`, `lan`, `auto`) | `lan` | +| `gateway.port` | Gateway port | `18789` | +| `secrets.data.anthropicApiKey` | Anthropic API key | `""` | +| `secrets.data.openaiApiKey` | OpenAI API key | `""` | +| `secrets.data.gatewayToken` | Gateway authentication token (auto-generated if empty) | `""` | +| `persistence.enabled` | Enable persistent storage | `true` | +| `persistence.size` | PVC size | `10Gi` | +| `ingress.enabled` | Enable Ingress | `false` | +| `ingress.className` | Ingress class | `nginx` | +| `resources.limits.memory` | Memory limit | `2Gi` | +| `resources.requests.memory` | Memory request | `512Mi` | + +### Full Values Reference + +See [values.yaml](values.yaml) for all available parameters. + +## Storage + +The chart creates a StatefulSet with a persistent volume claim template that mounts storage at two locations: + +- `/home/node/.clawdbot` (subPath: `clawdbot-state`) - Configuration, sessions, device identity, SQLite databases +- `/home/node/clawd` (subPath: `clawdbot-workspace`) - Agent workspace files + +**Storage Class:** By default, uses the cluster's default storage class. Override with `persistence.storageClass`. + +**Size Recommendations:** +- Development/Testing: 5-10Gi +- Production (single user): 20Gi+ +- Depends on session history and workspace usage + +## Secrets Management + +### Option 1: Inline Secrets (Development Only) + +```bash +helm install my-clawdbot ./clawdbot \ + --set secrets.data.anthropicApiKey=sk-ant-xxx \ + --set secrets.data.gatewayToken=$(openssl rand -hex 32) +``` + +### Option 2: External Secret (Production) + +Create a Kubernetes Secret: + +```bash +kubectl create secret generic clawdbot-secrets \ + --from-literal=gatewayToken=$(openssl rand -hex 32) \ + --from-literal=anthropicApiKey=sk-ant-xxx \ + --from-literal=discordBotToken=MTQ... +``` + +Install with existing secret: + +```bash +helm install my-clawdbot ./clawdbot \ + --set secrets.create=false \ + --set secrets.existingSecret=clawdbot-secrets +``` + +### Option 3: External Secrets Operator + +Use External Secrets Operator to sync from Vault, AWS Secrets Manager, etc. + +## Ingress + +### NGINX Ingress Controller + +```yaml +ingress: + enabled: true + className: nginx + annotations: + cert-manager.io/cluster-issuer: letsencrypt-prod + nginx.ingress.kubernetes.io/proxy-read-timeout: "3600" + nginx.ingress.kubernetes.io/proxy-send-timeout: "3600" + nginx.ingress.kubernetes.io/websocket-services: "{{ include \"clawdbot.fullname\" . }}" + hosts: + - host: assistant.example.com + paths: + - path: / + pathType: Prefix + tls: + - secretName: clawdbot-tls + hosts: + - assistant.example.com +``` + +**Important:** WebSocket support requires extended timeouts (3600s recommended). + +### Traefik + +```yaml +ingress: + className: traefik + annotations: + traefik.ingress.kubernetes.io/router.entrypoints: websecure + traefik.ingress.kubernetes.io/router.tls: "true" +``` + +## Health Checks + +The chart uses exec-based probes that run the CLI health command: + +```yaml +livenessProbe: + exec: + command: + - sh + - -c + - node dist/index.js health --token "${CLAWDBOT_GATEWAY_TOKEN}" || exit 1 +``` + +## Upgrading + +```bash +# Pull latest chart changes +git pull + +# Upgrade with current values +helm upgrade my-clawdbot ./clawdbot --reuse-values + +# Upgrade with new values +helm upgrade my-clawdbot ./clawdbot -f values-production.yaml +``` + +## Troubleshooting + +### Pod Not Starting + +```bash +# Check pod events +kubectl describe pod my-clawdbot-0 + +# Check logs +kubectl logs my-clawdbot-0 +``` + +### OOM (Out of Memory) + +Increase memory limits: + +```yaml +resources: + limits: + memory: 4Gi + requests: + memory: 1Gi +``` + +**Note:** 512MB is too small for production. 2GB recommended minimum. + +### PVC Not Binding + +```bash +# Check PVC status +kubectl get pvc + +# Check storage class +kubectl get storageclass + +# Describe for events +kubectl describe pvc data-my-clawdbot-0 +``` + +### WebSocket Connections Timing Out + +Ensure Ingress has WebSocket annotations: + +```yaml +annotations: + nginx.ingress.kubernetes.io/proxy-read-timeout: "3600" + nginx.ingress.kubernetes.io/proxy-send-timeout: "3600" +``` + +### Gateway Lock File Issues + +If the gateway won't start due to stale lock files: + +```bash +kubectl exec my-clawdbot-0 -- rm -f /home/node/.clawdbot/gateway.*.lock +kubectl delete pod my-clawdbot-0 # Restart pod +``` + +## Examples + +### Local Testing (Minikube/Kind/Docker Desktop) + +```bash +# Build local image +docker build -t clawdbot:local . + +# Load into cluster (example for Minikube) +minikube image load clawdbot:local + +# Install chart +helm install test ./clawdbot \ + -f examples/values-basic.yaml \ + --set image.repository=clawdbot \ + --set image.tag=local \ + --set image.pullPolicy=Never +``` + +### Production Deployment + +```bash +# Create external secret first +kubectl create secret generic clawdbot-secrets \ + --from-literal=gatewayToken=$(openssl rand -hex 32) \ + --from-literal=anthropicApiKey=$ANTHROPIC_API_KEY + +# Install with production values +helm install my-clawdbot ./clawdbot -f examples/values-production.yaml +``` + +## Documentation + +- [Clawdbot Documentation](https://docs.clawd.bot) +- [Kubernetes Installation Guide](https://docs.clawd.bot/install/kubernetes) +- [GitHub Repository](https://github.com/clawdbot/clawdbot) + +## Support + +- [GitHub Issues](https://github.com/clawdbot/clawdbot/issues) +- [Documentation](https://docs.clawd.bot) + +## License + +See [LICENSE](https://github.com/clawdbot/clawdbot/blob/main/LICENSE) diff --git a/charts/clawdbot/examples/values-basic.yaml b/charts/clawdbot/examples/values-basic.yaml new file mode 100644 index 000000000..7a53ffd2d --- /dev/null +++ b/charts/clawdbot/examples/values-basic.yaml @@ -0,0 +1,33 @@ +# Basic configuration for local testing or development + +image: + repository: clawdbot + tag: local + pullPolicy: Never + +gateway: + bind: lan + port: 18789 + allowUnconfigured: false + +secrets: + create: true + data: + # Set via --set or environment variables + anthropicApiKey: "" + gatewayToken: "" + +persistence: + enabled: true + size: 5Gi + +ingress: + enabled: false + +resources: + limits: + memory: 2Gi + cpu: 1000m + requests: + memory: 512Mi + cpu: 250m diff --git a/charts/clawdbot/examples/values-fly-like.yaml b/charts/clawdbot/examples/values-fly-like.yaml new file mode 100644 index 000000000..14f8ebe1f --- /dev/null +++ b/charts/clawdbot/examples/values-fly-like.yaml @@ -0,0 +1,58 @@ +# Configuration similar to Fly.io deployment +# Mimics fly.toml settings + +image: + repository: ghcr.io/clawdbot/clawdbot + tag: "2026.1.25" + pullPolicy: IfNotPresent + +gateway: + bind: lan + port: 3000 # Fly.io uses port 3000 + allowUnconfigured: true + extraArgs: [] + +env: + NODE_ENV: production + CLAWDBOT_STATE_DIR: /home/node/.clawdbot + CLAWDBOT_WORKSPACE_DIR: /home/node/clawd + NODE_OPTIONS: "--max-old-space-size=1536" # Fly.io recommendation + +secrets: + create: false + existingSecret: clawdbot-secrets + +persistence: + enabled: true + size: 1Gi # Similar to Fly.io volume size + +resources: + limits: + memory: 2Gi # shared-cpu-2x on Fly.io + cpu: 1000m + requests: + memory: 512Mi + cpu: 250m + +service: + type: ClusterIP + port: 3000 + +ingress: + enabled: true + className: nginx + annotations: + cert-manager.io/cluster-issuer: letsencrypt-prod + nginx.ingress.kubernetes.io/force-ssl-redirect: "true" + nginx.ingress.kubernetes.io/proxy-read-timeout: "3600" + nginx.ingress.kubernetes.io/proxy-send-timeout: "3600" + nginx.ingress.kubernetes.io/websocket-services: "clawdbot" + hosts: + - host: my-clawdbot.example.com + paths: + - path: / + pathType: Prefix + tls: + - secretName: clawdbot-tls + hosts: + - my-clawdbot.example.com diff --git a/charts/clawdbot/examples/values-production.yaml b/charts/clawdbot/examples/values-production.yaml new file mode 100644 index 000000000..5373a9e3d --- /dev/null +++ b/charts/clawdbot/examples/values-production.yaml @@ -0,0 +1,92 @@ +# Production configuration with Ingress and external secrets + +image: + repository: ghcr.io/clawdbot/clawdbot + tag: "2026.1.25" + pullPolicy: IfNotPresent + +gateway: + bind: lan + port: 18789 + allowUnconfigured: false + +# Use external secret in production +secrets: + create: false + existingSecret: clawdbot-secrets + +config: + create: true + data: + agents: + defaults: + model: + primary: "anthropic/claude-opus-4-5" + fallbacks: + - "anthropic/claude-sonnet-4-5" + maxConcurrent: 4 + sandbox: + mode: "off" + list: + - id: main + default: true + auth: + profiles: + "anthropic:default": + mode: token + provider: anthropic + gateway: + mode: local + bind: auto + auth: + mode: token + controlUi: + enabled: true + channels: + discord: + enabled: true + meta: + lastTouchedVersion: "2026.1.25" + +persistence: + enabled: true + storageClass: fast-ssd + size: 20Gi + accessMode: ReadWriteOnce + +resources: + limits: + memory: 4Gi + cpu: 2000m + requests: + memory: 1Gi + cpu: 500m + +ingress: + enabled: true + className: nginx + annotations: + cert-manager.io/cluster-issuer: letsencrypt-prod + nginx.ingress.kubernetes.io/proxy-read-timeout: "3600" + nginx.ingress.kubernetes.io/proxy-send-timeout: "3600" + nginx.ingress.kubernetes.io/websocket-services: "clawdbot" + hosts: + - host: assistant.example.com + paths: + - path: / + pathType: Prefix + tls: + - secretName: clawdbot-tls + hosts: + - assistant.example.com + +affinity: + nodeAffinity: + preferredDuringSchedulingIgnoredDuringExecution: + - weight: 100 + preference: + matchExpressions: + - key: workload-type + operator: In + values: + - stateful diff --git a/charts/clawdbot/templates/NOTES.txt b/charts/clawdbot/templates/NOTES.txt new file mode 100644 index 000000000..ee6420b27 --- /dev/null +++ b/charts/clawdbot/templates/NOTES.txt @@ -0,0 +1,46 @@ +Thank you for installing {{ .Chart.Name }}! + +Your Clawdbot gateway is starting up. This may take 1-2 minutes. + +1. Check the gateway status: + + kubectl get statefulset {{ include "clawdbot.fullname" . }} -n {{ .Release.Namespace }} + kubectl logs -f {{ include "clawdbot.fullname" . }}-0 -n {{ .Release.Namespace }} + +2. Access the Control UI: + +{{- if .Values.ingress.enabled }} +{{- range .Values.ingress.hosts }} + https://{{ .host }} +{{- end }} +{{- else }} + # Port-forward to access locally: + kubectl port-forward {{ include "clawdbot.fullname" . }}-0 {{ .Values.gateway.port }}:{{ .Values.gateway.port }} -n {{ .Release.Namespace }} + + Then visit: http://localhost:{{ .Values.gateway.port }} +{{- end }} + +3. Your gateway token (for authentication): + + kubectl get secret {{ include "clawdbot.secretName" . }} -n {{ .Release.Namespace }} -o jsonpath='{.data.gatewayToken}' | base64 -d && echo + +4. Configure channels: + + kubectl exec -it {{ include "clawdbot.fullname" . }}-0 -n {{ .Release.Namespace }} -- node dist/index.js channels add --channel discord --token YOUR_BOT_TOKEN + +5. Check health: + + kubectl exec -it {{ include "clawdbot.fullname" . }}-0 -n {{ .Release.Namespace }} -- node dist/index.js health + +Documentation: https://docs.clawd.bot +Support: https://github.com/clawdbot/clawdbot/issues + +{{- if not .Values.persistence.enabled }} + +WARNING: Persistence is disabled. State will be lost on pod restart. +{{- end }} + +{{- if eq .Values.gateway.bind "loopback" }} + +WARNING: Gateway is bound to loopback. External access will not work. +{{- end }} diff --git a/charts/clawdbot/templates/_helpers.tpl b/charts/clawdbot/templates/_helpers.tpl new file mode 100644 index 000000000..c470d9262 --- /dev/null +++ b/charts/clawdbot/templates/_helpers.tpl @@ -0,0 +1,84 @@ +{{/* +Expand the name of the chart. +*/}} +{{- define "clawdbot.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +If release name contains chart name it will be used as a full name. +*/}} +{{- define "clawdbot.fullname" -}} +{{- if .Values.fullnameOverride }} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- $name := default .Chart.Name .Values.nameOverride }} +{{- if contains $name .Release.Name }} +{{- .Release.Name | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} +{{- end }} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "clawdbot.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Common labels +*/}} +{{- define "clawdbot.labels" -}} +helm.sh/chart: {{ include "clawdbot.chart" . }} +{{ include "clawdbot.selectorLabels" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{/* +Selector labels +*/}} +{{- define "clawdbot.selectorLabels" -}} +app.kubernetes.io/name: {{ include "clawdbot.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} + +{{/* +Create the name of the service account to use +*/}} +{{- define "clawdbot.serviceAccountName" -}} +{{- if .Values.serviceAccount.create }} +{{- default (include "clawdbot.fullname" .) .Values.serviceAccount.name }} +{{- else }} +{{- default "default" .Values.serviceAccount.name }} +{{- end }} +{{- end }} + +{{/* +Create the name of the secret to use +*/}} +{{- define "clawdbot.secretName" -}} +{{- if .Values.secrets.existingSecret }} +{{- .Values.secrets.existingSecret }} +{{- else }} +{{- include "clawdbot.fullname" . }} +{{- end }} +{{- end }} + +{{/* +Create the name of the config map to use +*/}} +{{- define "clawdbot.configMapName" -}} +{{- if .Values.config.existingConfigMap }} +{{- .Values.config.existingConfigMap }} +{{- else }} +{{- printf "%s-config" (include "clawdbot.fullname" .) }} +{{- end }} +{{- end }} diff --git a/charts/clawdbot/templates/configmap.yaml b/charts/clawdbot/templates/configmap.yaml new file mode 100644 index 000000000..d776903cf --- /dev/null +++ b/charts/clawdbot/templates/configmap.yaml @@ -0,0 +1,11 @@ +{{- if .Values.config.create -}} +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ include "clawdbot.configMapName" . }} + labels: + {{- include "clawdbot.labels" . | nindent 4 }} +data: + clawdbot.json: | + {{ .Values.config.data | toJson | nindent 4 }} +{{- end }} diff --git a/charts/clawdbot/templates/ingress.yaml b/charts/clawdbot/templates/ingress.yaml new file mode 100644 index 000000000..d406faaf5 --- /dev/null +++ b/charts/clawdbot/templates/ingress.yaml @@ -0,0 +1,41 @@ +{{- if .Values.ingress.enabled -}} +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: {{ include "clawdbot.fullname" . }} + labels: + {{- include "clawdbot.labels" . | nindent 4 }} + {{- with .Values.ingress.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + {{- if .Values.ingress.className }} + ingressClassName: {{ .Values.ingress.className }} + {{- end }} + {{- if .Values.ingress.tls }} + tls: + {{- range .Values.ingress.tls }} + - hosts: + {{- range .hosts }} + - {{ . | quote }} + {{- end }} + secretName: {{ .secretName }} + {{- end }} + {{- end }} + rules: + {{- range .Values.ingress.hosts }} + - host: {{ .host | quote }} + http: + paths: + {{- range .paths }} + - path: {{ .path }} + pathType: {{ .pathType }} + backend: + service: + name: {{ include "clawdbot.fullname" $ }} + port: + number: {{ $.Values.service.port }} + {{- end }} + {{- end }} +{{- end }} diff --git a/charts/clawdbot/templates/secret.yaml b/charts/clawdbot/templates/secret.yaml new file mode 100644 index 000000000..1d076f768 --- /dev/null +++ b/charts/clawdbot/templates/secret.yaml @@ -0,0 +1,23 @@ +{{- if .Values.secrets.create -}} +apiVersion: v1 +kind: Secret +metadata: + name: {{ include "clawdbot.fullname" . }} + labels: + {{- include "clawdbot.labels" . | nindent 4 }} +type: Opaque +stringData: + gatewayToken: {{ .Values.secrets.data.gatewayToken | default (randAlphaNum 32) | quote }} + {{- with .Values.secrets.data.anthropicApiKey }} + anthropicApiKey: {{ . | quote }} + {{- end }} + {{- with .Values.secrets.data.openaiApiKey }} + openaiApiKey: {{ . | quote }} + {{- end }} + {{- with .Values.secrets.data.discordBotToken }} + discordBotToken: {{ . | quote }} + {{- end }} + {{- with .Values.secrets.data.telegramBotToken }} + telegramBotToken: {{ . | quote }} + {{- end }} +{{- end }} diff --git a/charts/clawdbot/templates/service.yaml b/charts/clawdbot/templates/service.yaml new file mode 100644 index 000000000..64b0eee94 --- /dev/null +++ b/charts/clawdbot/templates/service.yaml @@ -0,0 +1,19 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ include "clawdbot.fullname" . }} + labels: + {{- include "clawdbot.labels" . | nindent 4 }} + {{- with .Values.service.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + type: {{ .Values.service.type }} + ports: + - port: {{ .Values.service.port }} + targetPort: gateway + protocol: TCP + name: gateway + selector: + {{- include "clawdbot.selectorLabels" . | nindent 4 }} diff --git a/charts/clawdbot/templates/serviceaccount.yaml b/charts/clawdbot/templates/serviceaccount.yaml new file mode 100644 index 000000000..70f06bded --- /dev/null +++ b/charts/clawdbot/templates/serviceaccount.yaml @@ -0,0 +1,12 @@ +{{- if .Values.serviceAccount.create -}} +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include "clawdbot.serviceAccountName" . }} + labels: + {{- include "clawdbot.labels" . | nindent 4 }} + {{- with .Values.serviceAccount.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +{{- end }} diff --git a/charts/clawdbot/templates/statefulset.yaml b/charts/clawdbot/templates/statefulset.yaml new file mode 100644 index 000000000..59c5c8f30 --- /dev/null +++ b/charts/clawdbot/templates/statefulset.yaml @@ -0,0 +1,202 @@ +apiVersion: apps/v1 +kind: StatefulSet +metadata: + name: {{ include "clawdbot.fullname" . }} + labels: + {{- include "clawdbot.labels" . | nindent 4 }} +spec: + serviceName: {{ include "clawdbot.fullname" . }} + replicas: {{ .Values.replicaCount }} + selector: + matchLabels: + {{- include "clawdbot.selectorLabels" . | nindent 6 }} + template: + metadata: + annotations: + {{- if .Values.config.create }} + checksum/config: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }} + {{- end }} + {{- if .Values.secrets.create }} + checksum/secret: {{ include (print $.Template.BasePath "/secret.yaml") . | sha256sum }} + {{- end }} + {{- with .Values.podAnnotations }} + {{- toYaml . | nindent 8 }} + {{- end }} + labels: + {{- include "clawdbot.selectorLabels" . | nindent 8 }} + spec: + {{- with .Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + serviceAccountName: {{ include "clawdbot.serviceAccountName" . }} + securityContext: + {{- toYaml .Values.podSecurityContext | nindent 8 }} + {{- if or .Values.config.create .Values.initContainers }} + initContainers: + {{- if .Values.config.create }} + - name: init-config + image: "{{ .Values.image.registry }}/{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + command: + - sh + - -c + - | + echo "Initializing configuration..." + mkdir -p /home/node/.clawdbot + if [ ! -f /home/node/.clawdbot/clawdbot.json ]; then + echo "Copying default config..." + cp /config/clawdbot.json /home/node/.clawdbot/clawdbot.json + echo "Config initialized" + else + echo "Config already exists, skipping" + fi + volumeMounts: + - name: config + mountPath: /config + - name: data + mountPath: /home/node/.clawdbot + subPath: clawdbot-state + securityContext: + {{- toYaml .Values.securityContext | nindent 12 }} + {{- end }} + {{- with .Values.initContainers }} + {{- toYaml . | nindent 8 }} + {{- end }} + {{- end }} + containers: + - name: gateway + image: "{{ .Values.image.registry }}/{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + command: + - node + - dist/index.js + - gateway + - --bind + - {{ .Values.gateway.bind }} + - --port + - {{ .Values.gateway.port | quote }} + {{- if .Values.gateway.allowUnconfigured }} + - --allow-unconfigured + {{- end }} + {{- range .Values.gateway.extraArgs }} + - {{ . }} + {{- end }} + env: + {{- range $key, $value := .Values.env }} + - name: {{ $key }} + value: {{ $value | quote }} + {{- end }} + - name: CLAWDBOT_GATEWAY_PORT + value: {{ .Values.gateway.port | quote }} + - name: CLAWDBOT_GATEWAY_TOKEN + valueFrom: + secretKeyRef: + name: {{ include "clawdbot.secretName" . }} + key: gatewayToken + {{- if .Values.secrets.data.anthropicApiKey }} + - name: ANTHROPIC_API_KEY + valueFrom: + secretKeyRef: + name: {{ include "clawdbot.secretName" . }} + key: anthropicApiKey + optional: true + {{- end }} + {{- if .Values.secrets.data.openaiApiKey }} + - name: OPENAI_API_KEY + valueFrom: + secretKeyRef: + name: {{ include "clawdbot.secretName" . }} + key: openaiApiKey + optional: true + {{- end }} + {{- if .Values.secrets.data.discordBotToken }} + - name: DISCORD_BOT_TOKEN + valueFrom: + secretKeyRef: + name: {{ include "clawdbot.secretName" . }} + key: discordBotToken + optional: true + {{- end }} + {{- if .Values.secrets.data.telegramBotToken }} + - name: TELEGRAM_BOT_TOKEN + valueFrom: + secretKeyRef: + name: {{ include "clawdbot.secretName" . }} + key: telegramBotToken + optional: true + {{- end }} + ports: + - name: gateway + containerPort: {{ .Values.gateway.port }} + protocol: TCP + volumeMounts: + - name: data + mountPath: /home/node/.clawdbot + subPath: clawdbot-state + - name: data + mountPath: /home/node/clawd + subPath: clawdbot-workspace + {{- if .Values.livenessProbe.enabled }} + livenessProbe: + {{- omit .Values.livenessProbe "enabled" | toYaml | nindent 12 }} + {{- end }} + {{- if .Values.readinessProbe.enabled }} + readinessProbe: + {{- omit .Values.readinessProbe "enabled" | toYaml | nindent 12 }} + {{- end }} + {{- if .Values.startupProbe.enabled }} + startupProbe: + {{- omit .Values.startupProbe "enabled" | toYaml | nindent 12 }} + {{- end }} + {{- with .Values.lifecycle }} + lifecycle: + {{- toYaml . | nindent 12 }} + {{- end }} + resources: + {{- toYaml .Values.resources | nindent 12 }} + securityContext: + {{- toYaml .Values.securityContext | nindent 12 }} + {{- with .Values.extraContainers }} + {{- toYaml . | nindent 8 }} + {{- end }} + volumes: + {{- if .Values.config.create }} + - name: config + configMap: + name: {{ include "clawdbot.configMapName" . }} + {{- end }} + {{- with .Values.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- if .Values.persistence.enabled }} + volumeClaimTemplates: + - metadata: + name: data + {{- with .Values.persistence.annotations }} + annotations: + {{- toYaml . | nindent 10 }} + {{- end }} + spec: + accessModes: + - {{ .Values.persistence.accessMode }} + {{- if .Values.persistence.storageClass }} + storageClassName: {{ .Values.persistence.storageClass }} + {{- end }} + resources: + requests: + storage: {{ .Values.persistence.size }} + {{- with .Values.persistence.selector }} + selector: + {{- toYaml . | nindent 10 }} + {{- end }} + {{- end }} diff --git a/charts/clawdbot/templates/tests/test-connection.yaml b/charts/clawdbot/templates/tests/test-connection.yaml new file mode 100644 index 000000000..5f1f44ba2 --- /dev/null +++ b/charts/clawdbot/templates/tests/test-connection.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Pod +metadata: + name: "{{ include "clawdbot.fullname" . }}-test-connection" + labels: + {{- include "clawdbot.labels" . | nindent 4 }} + annotations: + "helm.sh/hook": test +spec: + containers: + - name: wget + image: busybox + command: ['wget'] + args: ['{{ include "clawdbot.fullname" . }}:{{ .Values.service.port }}'] + restartPolicy: Never diff --git a/charts/clawdbot/values.schema.json b/charts/clawdbot/values.schema.json new file mode 100644 index 000000000..afb358141 --- /dev/null +++ b/charts/clawdbot/values.schema.json @@ -0,0 +1,74 @@ +{ + "$schema": "https://json-schema.org/draft-07/schema#", + "type": "object", + "properties": { + "replicaCount": { + "type": "integer", + "minimum": 1, + "maximum": 1, + "default": 1, + "description": "Must be 1 for single-user architecture. Clawdbot does not support horizontal scaling." + }, + "image": { + "type": "object", + "properties": { + "repository": { + "type": "string" + }, + "pullPolicy": { + "type": "string", + "enum": ["Always", "IfNotPresent", "Never"] + }, + "tag": { + "type": "string" + } + } + }, + "gateway": { + "type": "object", + "properties": { + "bind": { + "type": "string", + "enum": ["loopback", "lan", "auto"], + "description": "Gateway binding mode" + }, + "port": { + "type": "integer", + "minimum": 1, + "maximum": 65535 + } + } + }, + "persistence": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean" + }, + "size": { + "type": "string", + "pattern": "^[0-9]+(Gi|Mi|Ti)$" + }, + "accessMode": { + "type": "string", + "enum": ["ReadWriteOnce", "ReadOnlyMany", "ReadWriteMany"] + } + } + }, + "service": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": ["ClusterIP", "NodePort", "LoadBalancer"] + }, + "port": { + "type": "integer", + "minimum": 1, + "maximum": 65535 + } + } + } + }, + "required": ["replicaCount"] +} diff --git a/charts/clawdbot/values.yaml b/charts/clawdbot/values.yaml new file mode 100644 index 000000000..38506b01c --- /dev/null +++ b/charts/clawdbot/values.yaml @@ -0,0 +1,227 @@ +# Default values for clawdbot. +# This is a YAML-formatted file. +# Declare variables to be passed into your templates. + +# Image configuration +image: + registry: ghcr.io + repository: clawdbot/clawdbot + pullPolicy: IfNotPresent + # Overrides the image tag whose default is the chart appVersion. + tag: "" + +imagePullSecrets: [] +nameOverride: "" +fullnameOverride: "" + +# Replica count - MUST be 1 for single-user architecture +replicaCount: 1 + +# Service account +serviceAccount: + # Specifies whether a service account should be created + create: true + # Annotations to add to the service account + annotations: {} + # The name of the service account to use. + # If not set and create is true, a name is generated using the fullname template + name: "" + +# Pod annotations +podAnnotations: {} + +# Pod security context +podSecurityContext: + runAsNonRoot: true + runAsUser: 1000 + runAsGroup: 1000 + fsGroup: 1000 + +# Container security context +securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + readOnlyRootFilesystem: false # Node needs write access to /tmp + +# Gateway configuration +gateway: + # Binding mode: loopback, lan, auto + bind: lan + # Gateway port + port: 18789 + # Allow unconfigured startup (creates minimal config) + allowUnconfigured: false + # Additional CLI arguments + extraArgs: [] + +# Environment variables (non-sensitive) +env: + NODE_ENV: production + CLAWDBOT_STATE_DIR: /home/node/.clawdbot + CLAWDBOT_WORKSPACE_DIR: /home/node/clawd + NODE_OPTIONS: "--max-old-space-size=2048" + +# Secrets configuration +secrets: + # Create secret from values (dev/testing only - use existingSecret in production) + create: true + # Use existing secret (production) + existingSecret: "" + # Secret data (only used if create is true) + data: + anthropicApiKey: "" + openaiApiKey: "" + discordBotToken: "" + telegramBotToken: "" + gatewayToken: "" # Auto-generated if empty + +# Config file (clawdbot.json) +config: + # Create ConfigMap from inline config + create: true + # Use existing ConfigMap + existingConfigMap: "" + # Config data (JSON5 format) + data: + agents: + defaults: + model: + primary: "anthropic/claude-opus-4-5" + fallbacks: + - "anthropic/claude-sonnet-4-5" + - "openai/gpt-4o" + maxConcurrent: 4 + sandbox: + mode: "off" # Disable Docker-in-Docker for Kubernetes + list: + - id: main + default: true + auth: + profiles: + "anthropic:default": + mode: token + provider: anthropic + "openai:default": + mode: token + provider: openai + gateway: + mode: local + bind: auto + auth: + mode: token + controlUi: + enabled: true + channels: {} + meta: + lastTouchedVersion: "2026.1.25" + +# Persistence +persistence: + enabled: true + # Storage class (use cluster default if empty) + storageClass: "" + # Access mode + accessMode: ReadWriteOnce + # Volume size + size: 10Gi + # Annotations + annotations: {} + # Selector + selector: {} + +# Service configuration +service: + type: ClusterIP + port: 18789 + annotations: {} + +# Ingress configuration +ingress: + enabled: false + className: nginx + annotations: + # cert-manager.io/cluster-issuer: letsencrypt-prod + # nginx.ingress.kubernetes.io/proxy-read-timeout: "3600" + # nginx.ingress.kubernetes.io/proxy-send-timeout: "3600" + # nginx.ingress.kubernetes.io/websocket-services: "{{ include \"clawdbot.fullname\" . }}" + hosts: + - host: clawdbot.example.com + paths: + - path: / + pathType: Prefix + tls: [] + # - secretName: clawdbot-tls + # hosts: + # - clawdbot.example.com + +# Resource limits/requests +resources: + limits: + memory: 2Gi + cpu: 1000m + requests: + memory: 512Mi + cpu: 250m + +# Node selector +nodeSelector: {} + +# Tolerations +tolerations: [] + +# Affinity rules +affinity: {} + +# Init containers (for setup tasks) +initContainers: [] + +# Extra containers (sidecars) +extraContainers: [] + +# Lifecycle hooks +lifecycle: + preStop: + exec: + command: + - sh + - -c + - rm -f /home/node/.clawdbot/gateway.*.lock; sleep 10 + +# Probes +livenessProbe: + enabled: true + exec: + command: + - node + - dist/index.js + - health + initialDelaySeconds: 30 + periodSeconds: 30 + timeoutSeconds: 10 + failureThreshold: 3 + +readinessProbe: + enabled: true + exec: + command: + - node + - dist/index.js + - health + initialDelaySeconds: 15 + periodSeconds: 10 + timeoutSeconds: 5 + failureThreshold: 3 + +startupProbe: + enabled: true + exec: + command: + - node + - dist/index.js + - health + initialDelaySeconds: 10 + periodSeconds: 5 + timeoutSeconds: 5 + failureThreshold: 12 # 60 seconds max startup time diff --git a/docs/install/index.md b/docs/install/index.md index 8c6943589..8d9ba33bf 100644 --- a/docs/install/index.md +++ b/docs/install/index.md @@ -101,6 +101,7 @@ Tip: if you don’t have a global install yet, run repo commands via `pnpm moltb ### 4) Other install options - Docker: [Docker](/install/docker) +- Kubernetes: [Kubernetes (Helm)](/install/kubernetes) - Nix: [Nix](/install/nix) - Ansible: [Ansible](/install/ansible) - Bun (CLI only): [Bun](/install/bun) diff --git a/docs/install/kubernetes.md b/docs/install/kubernetes.md new file mode 100644 index 000000000..4b95eb5e2 --- /dev/null +++ b/docs/install/kubernetes.md @@ -0,0 +1,482 @@ +--- +title: Kubernetes (Helm) +description: Deploy Clawdbot on Kubernetes using Helm charts +--- + +# Kubernetes Deployment + +**Goal:** Clawdbot Gateway running on Kubernetes with Helm, persistent storage, automatic HTTPS, and channel access. + +## What you need + +- Kubernetes cluster (1.19+) +- kubectl CLI configured +- Helm 3.x +- Model auth: Anthropic API key (or other provider keys) +- Channel credentials: Discord bot token, Telegram token, etc. +- **Optional:** Ingress controller (NGINX, Traefik) for external access +- **Optional:** cert-manager for automatic TLS certificates + +## Beginner quick path + +1. Install Helm chart from source +2. Configure secrets (API keys) +3. Access Control UI via Ingress or port-forward +4. Configure channels + +## Prerequisites + +### 1) Kubernetes Cluster + +You need a running Kubernetes cluster. Options: + +**Local development:** +- Docker Desktop (macOS/Windows) - Enable Kubernetes in settings +- Minikube - `brew install minikube && minikube start` +- Kind - `brew install kind && kind create cluster` + +**Cloud providers:** +- GKE (Google Kubernetes Engine) +- EKS (Amazon Elastic Kubernetes Service) +- AKS (Azure Kubernetes Service) +- DigitalOcean Kubernetes +- Linode Kubernetes Engine + +### 2) kubectl + +Install kubectl: + +```bash +# macOS +brew install kubectl + +# Linux +curl -LO "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl" +sudo install -o root -g root -m 0755 kubectl /usr/local/bin/kubectl + +# Verify +kubectl version --client +kubectl cluster-info +``` + +### 3) Helm + +Install Helm 3.x: + +```bash +# macOS +brew install helm + +# Linux +curl https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 | bash + +# Verify +helm version +``` + +## Installation + +### 1) Clone the repository + +```bash +git clone https://github.com/clawdbot/clawdbot.git +cd clawdbot +``` + +### 2) Install Helm chart + +**Basic installation (development):** + +```bash +helm install my-clawdbot charts/clawdbot \ + --set secrets.data.anthropicApiKey=sk-ant-xxx \ + --set secrets.data.gatewayToken=$(openssl rand -hex 32) +``` + +**With custom values file:** + +```bash +helm install my-clawdbot charts/clawdbot \ + --values charts/clawdbot/examples/values-production.yaml \ + --set secrets.data.anthropicApiKey=sk-ant-xxx \ + --set ingress.hosts[0].host=assistant.example.com +``` + +**Create external secret (recommended for production):** + +```bash +# Create secret +kubectl create secret generic clawdbot-secrets \ + --from-literal=gatewayToken=$(openssl rand -hex 32) \ + --from-literal=anthropicApiKey=sk-ant-xxx \ + --from-literal=discordBotToken=YOUR_DISCORD_TOKEN + +# Install with external secret +helm install my-clawdbot charts/clawdbot \ + --values charts/clawdbot/examples/values-production.yaml \ + --set secrets.create=false \ + --set secrets.existingSecret=clawdbot-secrets +``` + +### 3) Verify installation + +```bash +# Check deployment status +helm status my-clawdbot +kubectl get all -l app.kubernetes.io/instance=my-clawdbot + +# Wait for pod to be ready +kubectl wait --for=condition=ready pod -l app.kubernetes.io/instance=my-clawdbot --timeout=120s + +# Check logs +kubectl logs -f my-clawdbot-0 +``` + +## Access the Gateway + +### Option 1: Port-forward (local access) + +```bash +kubectl port-forward my-clawdbot-0 18789:18789 +``` + +Then visit: http://localhost:18789 + +### Option 2: Ingress (external access) + +**Install NGINX Ingress Controller (if not already installed):** + +```bash +# For cloud providers +kubectl apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/controller-v1.9.0/deploy/static/provider/cloud/deploy.yaml + +# For bare metal/local +kubectl apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/controller-v1.9.0/deploy/static/provider/baremetal/deploy.yaml + +# For Minikube +minikube addons enable ingress + +# For Kind +kubectl apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/main/deploy/static/provider/kind/deploy.yaml +``` + +**Enable Ingress in values:** + +```yaml +ingress: + enabled: true + className: nginx + annotations: + cert-manager.io/cluster-issuer: letsencrypt-prod # If using cert-manager + nginx.ingress.kubernetes.io/proxy-read-timeout: "3600" + nginx.ingress.kubernetes.io/proxy-send-timeout: "3600" + nginx.ingress.kubernetes.io/websocket-services: "my-clawdbot" + hosts: + - host: assistant.example.com + paths: + - path: / + pathType: Prefix + tls: + - secretName: clawdbot-tls + hosts: + - assistant.example.com +``` + +**Upgrade with Ingress:** + +```bash +helm upgrade my-clawdbot charts/clawdbot \ + --reuse-values \ + --set ingress.enabled=true \ + --set ingress.hosts[0].host=assistant.example.com \ + --set ingress.hosts[0].paths[0].path=/ \ + --set ingress.hosts[0].paths[0].pathType=Prefix +``` + +### 3) Get gateway token + +```bash +kubectl get secret my-clawdbot -o jsonpath='{.data.gatewayToken}' | base64 -d && echo +``` + +Use this token to authenticate in the Control UI. + +## Configuration + +### Configure channels + +#### Discord + +```bash +# Exec into pod +kubectl exec -it my-clawdbot-0 -- sh + +# Inside pod +node dist/index.js channels add --channel discord --token YOUR_DISCORD_BOT_TOKEN +``` + +Or set via secret: + +```bash +kubectl create secret generic clawdbot-secrets \ + --from-literal=discordBotToken=YOUR_DISCORD_BOT_TOKEN \ + --dry-run=client -o yaml | kubectl apply -f - + +# Restart pod to apply +kubectl delete pod my-clawdbot-0 +``` + +#### Telegram + +```bash +kubectl exec -it my-clawdbot-0 -- node dist/index.js channels add \ + --channel telegram \ + --token YOUR_TELEGRAM_BOT_TOKEN +``` + +#### WhatsApp (QR code) + +```bash +# Exec into pod +kubectl exec -it my-clawdbot-0 -- node dist/index.js channels login +``` + +Scan the QR code with WhatsApp on your phone. + +### Update configuration + +Edit the config: + +```bash +# Get current config +kubectl get configmap my-clawdbot-config -o yaml > clawdbot-config.yaml + +# Edit clawdbot-config.yaml + +# Apply changes +kubectl apply -f clawdbot-config.yaml + +# Restart gateway to reload config +kubectl delete pod my-clawdbot-0 +``` + +## Storage + +The chart creates a persistent volume claim that stores: + +- `/home/node/.clawdbot` - Config, sessions, device identity, SQLite databases +- `/home/node/clawd` - Agent workspace files + +**Check storage:** + +```bash +kubectl get pvc +kubectl describe pvc data-my-clawdbot-0 +``` + +**Increase storage size:** + +```yaml +persistence: + size: 20Gi +``` + +## Upgrading + +```bash +# Pull latest changes +cd clawdbot +git pull + +# Upgrade with current values +helm upgrade my-clawdbot charts/clawdbot --reuse-values + +# Upgrade with new values +helm upgrade my-clawdbot charts/clawdbot -f values-production.yaml +``` + +## Troubleshooting + +### Pod not starting + +```bash +# Check events +kubectl describe pod my-clawdbot-0 + +# Check logs +kubectl logs my-clawdbot-0 + +# If pod crashed +kubectl logs my-clawdbot-0 --previous +``` + +### OOM (Out of Memory) + +Container keeps restarting. Signs: `SIGABRT`, `v8::internal::Runtime_AllocateInYoungGeneration`, or silent restarts. + +**Fix:** Increase memory in values.yaml: + +```yaml +resources: + limits: + memory: 4Gi + requests: + memory: 1Gi +``` + +**Note:** 512MB is too small. 2GB recommended minimum. + +### PVC not binding + +```bash +# Check PVC status +kubectl get pvc + +# Check events +kubectl describe pvc data-my-clawdbot-0 + +# Check if storage provisioner is available +kubectl get storageclass +``` + +**Fix for Minikube:** + +```bash +minikube addons enable storage-provisioner +minikube addons enable default-storageclass +``` + +### Gateway lock issues + +Gateway refuses to start with "already running" errors. + +```bash +# Delete lock file +kubectl exec my-clawdbot-0 -- rm -f /home/node/.clawdbot/gateway.*.lock + +# Restart pod +kubectl delete pod my-clawdbot-0 +``` + +### WebSocket connections timing out + +Ensure Ingress has proper annotations: + +```yaml +annotations: + nginx.ingress.kubernetes.io/proxy-read-timeout: "3600" + nginx.ingress.kubernetes.io/proxy-send-timeout: "3600" + nginx.ingress.kubernetes.io/websocket-services: "my-clawdbot" +``` + +### Config not being read + +If using `--allow-unconfigured`, the gateway creates a minimal config. Your custom config should be read on restart. + +```bash +# Verify config exists +kubectl exec my-clawdbot-0 -- cat /home/node/.clawdbot/clawdbot.json + +# Verify ConfigMap +kubectl get configmap my-clawdbot-config -o yaml +``` + +### Image pull errors (local testing) + +When testing with local images: + +```bash +# Ensure pullPolicy is Never +--set image.pullPolicy=Never + +# Load image into cluster +# Docker Desktop: Image already available +# Minikube: minikube image load clawdbot:local +# Kind: kind load docker-image clawdbot:local +``` + +## Local Testing + +Test the Helm chart locally before deploying to production: + +### Using Docker Desktop Kubernetes + +```bash +# Enable Kubernetes in Docker Desktop settings + +# Run automated test script +./scripts/test-helm-local.sh +``` + +### Using Minikube + +```bash +# Start Minikube +minikube start --memory=4096 --cpus=2 + +# Build and load image +docker build -t clawdbot:local . +minikube image load clawdbot:local + +# Install chart +helm install test charts/clawdbot \ + -f charts/clawdbot/examples/values-basic.yaml \ + --set image.repository=clawdbot \ + --set image.tag=local \ + --set image.pullPolicy=Never + +# Access via port-forward +kubectl port-forward test-clawdbot-0 18789:18789 +``` + +### Using Kind + +```bash +# Create cluster +kind create cluster + +# Build and load image +docker build -t clawdbot:local . +kind load docker-image clawdbot:local + +# Install chart +helm install test charts/clawdbot \ + -f charts/clawdbot/examples/values-basic.yaml \ + --set image.repository=clawdbot \ + --set image.tag=local \ + --set image.pullPolicy=Never +``` + +## Uninstall + +```bash +# Uninstall Helm release +helm uninstall my-clawdbot + +# Delete PVCs (data will be lost) +kubectl delete pvc -l app.kubernetes.io/instance=my-clawdbot +``` + +## Notes + +- Clawdbot is a **single-user application**. The chart enforces `replicas: 1`. +- WebSocket gateway requires long-lived connections (use proper Ingress timeouts). +- Persistent storage is required to preserve state across restarts. +- Docker-in-Docker sandboxing is disabled by default in Kubernetes deployments. +- For production, use external secret management (Kubernetes Secrets, External Secrets Operator, or Vault). + +## Documentation + +- [Chart README](https://github.com/clawdbot/clawdbot/tree/main/charts/clawdbot) +- [Clawdbot Documentation](https://docs.clawd.bot) +- [GitHub Repository](https://github.com/clawdbot/clawdbot) + +## Cost + +Kubernetes cluster costs vary by provider: + +- **Local (free):** Docker Desktop, Minikube, Kind +- **Cloud providers:** $50-200/month depending on node size and region +- **Recommended resources:** 2 CPU, 4GB RAM minimum + +See your cloud provider's pricing for details. diff --git a/scripts/publish-helm-chart.sh b/scripts/publish-helm-chart.sh new file mode 100644 index 000000000..e338fecea --- /dev/null +++ b/scripts/publish-helm-chart.sh @@ -0,0 +1,144 @@ +#!/usr/bin/env bash +# Publish Helm chart to GitHub Pages + +set -euo pipefail + +# Colors +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' + +log() { echo -e "${GREEN}[PUBLISH]${NC} $*"; } +warn() { echo -e "${YELLOW}[WARN]${NC} $*"; } +error() { echo -e "${RED}[ERROR]${NC} $*"; exit 1; } +info() { echo -e "${BLUE}[INFO]${NC} $*"; } + +# Configuration +CHART_PATH="charts/clawdbot" +REPO_URL="https://clawdbot.github.io/clawdbot" +OUTPUT_DIR=".cr-release-packages" + +# Check prerequisites +command -v helm >/dev/null 2>&1 || error "helm not found. Install it first." +command -v git >/dev/null 2>&1 || error "git not found." + +# Get chart version +CHART_VERSION=$(grep '^version:' "$CHART_PATH/Chart.yaml" | awk '{print $2}') +APP_VERSION=$(grep '^appVersion:' "$CHART_PATH/Chart.yaml" | awk '{print $2}' | tr -d '"') + +log "Publishing Helm Chart" +info "Chart version: $CHART_VERSION" +info "App version: $APP_VERSION" + +# Confirm +read -p "Continue with publishing? (y/N) " -n 1 -r +echo +if [[ ! $REPLY =~ ^[Yy]$ ]]; then + warn "Publishing cancelled" + exit 0 +fi + +# Step 1: Lint chart +log "Step 1: Linting chart..." +helm lint "$CHART_PATH" || error "Chart lint failed" + +# Step 2: Package chart +log "Step 2: Packaging chart..." +mkdir -p "$OUTPUT_DIR" +helm package "$CHART_PATH" -d "$OUTPUT_DIR" || error "Chart packaging failed" +info "Packaged: $OUTPUT_DIR/clawdbot-${CHART_VERSION}.tgz" + +# Step 3: Generate index +log "Step 3: Generating repository index..." + +# Check if we're on gh-pages branch +CURRENT_BRANCH=$(git branch --show-current) +if [ "$CURRENT_BRANCH" != "gh-pages" ]; then + warn "Not on gh-pages branch. Switching..." + + # Stash current changes if any + if ! git diff-index --quiet HEAD --; then + log "Stashing current changes..." + git stash push -m "publish-helm-chart: stash before gh-pages" + fi + + # Switch to gh-pages + git checkout gh-pages || { + error "Failed to checkout gh-pages branch. Create it first: + + git checkout --orphan gh-pages + git rm -rf . + echo '# Clawdbot Helm Charts' > README.md + git add README.md + git commit -m 'Initial gh-pages' + git push origin gh-pages + git checkout main" + } +fi + +# Copy package to gh-pages root +log "Step 4: Copying package to gh-pages..." +cp "$OUTPUT_DIR/clawdbot-${CHART_VERSION}.tgz" . + +# Update or create index.yaml +if [ -f "index.yaml" ]; then + log "Updating existing index.yaml..." + helm repo index . --url "$REPO_URL" --merge index.yaml +else + log "Creating new index.yaml..." + helm repo index . --url "$REPO_URL" +fi + +# Step 5: Commit and push +log "Step 5: Committing to gh-pages..." +git add "clawdbot-${CHART_VERSION}.tgz" index.yaml + +if git diff --staged --quiet; then + warn "No changes to commit. Chart version $CHART_VERSION may already be published." +else + git commit -m "Release chart version ${CHART_VERSION} + + Chart: clawdbot ${CHART_VERSION} + App: ${APP_VERSION} + + Published via scripts/publish-helm-chart.sh" + + log "Step 6: Pushing to origin/gh-pages..." + git push origin gh-pages || error "Failed to push to gh-pages" + + log "✓ Chart published successfully!" + info "Version: $CHART_VERSION" + info "URL: $REPO_URL/clawdbot-${CHART_VERSION}.tgz" +fi + +# Step 7: Switch back to original branch +log "Switching back to $CURRENT_BRANCH..." +git checkout "$CURRENT_BRANCH" + +# Restore stash if we created one +if git stash list | grep -q "publish-helm-chart: stash before gh-pages"; then + log "Restoring stashed changes..." + git stash pop +fi + +# Clean up +rm -rf "$OUTPUT_DIR" + +echo +log "============================================" +log "Chart published successfully! ✓" +log "============================================" +echo +info "Users can now install with:" +echo +echo " helm repo add clawdbot $REPO_URL" +echo " helm repo update" +echo " helm install my-clawdbot clawdbot/clawdbot" +echo +info "Chart URL: $REPO_URL/clawdbot-${CHART_VERSION}.tgz" +info "Wait 5-10 minutes for GitHub Pages to deploy" +echo +info "Verify deployment:" +echo " curl $REPO_URL/index.yaml" diff --git a/scripts/test-helm-local.sh b/scripts/test-helm-local.sh new file mode 100755 index 000000000..b1ccdb82f --- /dev/null +++ b/scripts/test-helm-local.sh @@ -0,0 +1,167 @@ +#!/usr/bin/env bash +# Test Helm chart locally on Docker Desktop K8s, Minikube, or Kind + +set -euo pipefail + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +log() { echo -e "${GREEN}[TEST]${NC} $*"; } +warn() { echo -e "${YELLOW}[WARN]${NC} $*"; } +error() { echo -e "${RED}[ERROR]${NC} $*"; exit 1; } +info() { echo -e "${BLUE}[INFO]${NC} $*"; } + +# Configuration +CHART_PATH="${CHART_PATH:-charts/clawdbot}" +IMAGE_NAME="${IMAGE_NAME:-clawdbot}" +IMAGE_TAG="${IMAGE_TAG:-local-test}" +RELEASE_NAME="${RELEASE_NAME:-test-clawdbot}" +NAMESPACE="${NAMESPACE:-default}" +ANTHROPIC_API_KEY="${ANTHROPIC_API_KEY:-sk-ant-test-dummy}" +GATEWAY_TOKEN="${GATEWAY_TOKEN:-$(openssl rand -hex 32)}" + +# Detect K8s environment +detect_k8s() { + if kubectl config current-context | grep -q docker-desktop 2>/dev/null; then + echo "docker-desktop" + elif kubectl config current-context | grep -q minikube 2>/dev/null; then + echo "minikube" + elif kubectl config current-context | grep -q kind 2>/dev/null; then + echo "kind" + else + echo "unknown" + fi +} + +K8S_ENV=$(detect_k8s) +info "Detected Kubernetes environment: $K8S_ENV" + +# Step 1: Lint chart +log "Step 1: Linting Helm chart..." +helm lint "$CHART_PATH" || error "Helm lint failed" +helm lint "$CHART_PATH" --values "$CHART_PATH/examples/values-basic.yaml" || error "Helm lint with values-basic.yaml failed" +log "✓ Lint passed" + +# Step 2: Template validation +log "Step 2: Validating templates..." +helm template test "$CHART_PATH" \ + --values "$CHART_PATH/examples/values-basic.yaml" \ + --set image.repository="$IMAGE_NAME" \ + --set image.tag="$IMAGE_TAG" \ + --set secrets.data.anthropicApiKey="$ANTHROPIC_API_KEY" \ + > /tmp/clawdbot-manifests.yaml || error "Template validation failed" +log "✓ Template validation passed" + +# Step 3: Build Docker image +log "Step 3: Building Docker image..." +docker build -t "$IMAGE_NAME:$IMAGE_TAG" -f Dockerfile . || error "Docker build failed" +log "✓ Image built: $IMAGE_NAME:$IMAGE_TAG" + +# Step 4: Load image into cluster +log "Step 4: Loading image into cluster..." +case "$K8S_ENV" in + docker-desktop) + info "Docker Desktop: Image already available" + ;; + minikube) + minikube image load "$IMAGE_NAME:$IMAGE_TAG" || error "Failed to load image into Minikube" + ;; + kind) + kind load docker-image "$IMAGE_NAME:$IMAGE_TAG" || error "Failed to load image into Kind" + ;; + *) + warn "Unknown K8s environment, skipping image load (may fail if image not available)" + ;; +esac +log "✓ Image loaded" + +# Step 5: Install Helm chart +log "Step 5: Installing Helm chart..." +helm install "$RELEASE_NAME" "$CHART_PATH" \ + --namespace "$NAMESPACE" \ + --values "$CHART_PATH/examples/values-basic.yaml" \ + --set image.repository="$IMAGE_NAME" \ + --set image.tag="$IMAGE_TAG" \ + --set image.pullPolicy=Never \ + --set secrets.data.anthropicApiKey="$ANTHROPIC_API_KEY" \ + --set secrets.data.gatewayToken="$GATEWAY_TOKEN" \ + --wait --timeout=3m || error "Helm install failed" +log "✓ Chart installed" + +# Step 6: Verify deployment +log "Step 6: Verifying deployment..." + +# Check StatefulSet +info "Checking StatefulSet..." +kubectl get statefulset -l app.kubernetes.io/instance="$RELEASE_NAME" -n "$NAMESPACE" + +# Check Pod +info "Waiting for pod to be ready..." +kubectl wait --for=condition=ready pod -l app.kubernetes.io/instance="$RELEASE_NAME" \ + -n "$NAMESPACE" --timeout=120s || error "Pod did not become ready" +log "✓ Pod is ready" + +# Check PVC +info "Checking PVC..." +kubectl get pvc -l app.kubernetes.io/instance="$RELEASE_NAME" -n "$NAMESPACE" +log "✓ PVC created" + +# Step 7: Health check +log "Step 7: Running health check..." +POD_NAME=$(kubectl get pod -l app.kubernetes.io/instance="$RELEASE_NAME" -n "$NAMESPACE" -o jsonpath='{.items[0].metadata.name}') +kubectl exec "$POD_NAME" -n "$NAMESPACE" -- node dist/index.js health --token "$GATEWAY_TOKEN" && log "✓ Health check passed" || warn "Health check failed (may be OK for test env)" + +# Step 8: Check logs +log "Step 8: Checking logs (last 50 lines)..." +kubectl logs "$POD_NAME" -n "$NAMESPACE" --tail=50 + +# Step 9: Run Helm tests +log "Step 9: Running Helm tests..." +helm test "$RELEASE_NAME" -n "$NAMESPACE" && log "✓ Helm tests passed" || warn "Helm tests failed" + +# Step 10: Verify persistence +log "Step 10: Testing persistence..." +info "Checking state directory..." +kubectl exec "$POD_NAME" -n "$NAMESPACE" -- ls -la /home/node/.clawdbot +info "Checking workspace directory..." +kubectl exec "$POD_NAME" -n "$NAMESPACE" -- ls -la /home/node/clawd +info "Checking config file..." +kubectl exec "$POD_NAME" -n "$NAMESPACE" -- cat /home/node/.clawdbot/clawdbot.json || warn "Config file not found (may be OK)" +log "✓ Persistence verified" + +# Step 11: Port-forward test +log "Step 11: Testing port-forward (5 seconds)..." +kubectl port-forward "$POD_NAME" -n "$NAMESPACE" 18789:18789 & +PF_PID=$! +sleep 5 +kill $PF_PID 2>/dev/null || true +log "✓ Port-forward test complete" + +# Success summary +echo +log "============================================" +log "All tests passed! ✓" +log "============================================" +info "Release: $RELEASE_NAME" +info "Namespace: $NAMESPACE" +info "Pod: $POD_NAME" +info "Gateway token: $GATEWAY_TOKEN" +echo + +# Cleanup prompt +read -p "Clean up resources? (y/N) " -n 1 -r +echo +if [[ $REPLY =~ ^[Yy]$ ]]; then + log "Cleaning up..." + helm uninstall "$RELEASE_NAME" -n "$NAMESPACE" + kubectl delete pvc -l app.kubernetes.io/instance="$RELEASE_NAME" -n "$NAMESPACE" + log "✓ Cleanup complete" +else + info "Skipping cleanup. To clean up later, run:" + echo " helm uninstall $RELEASE_NAME -n $NAMESPACE" + echo " kubectl delete pvc -l app.kubernetes.io/instance=$RELEASE_NAME -n $NAMESPACE" +fi