diff --git a/AGENTS.md b/AGENTS.md index 3a4d8cb..a802716 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -38,6 +38,7 @@ Frontend UI for self-hosted Honcho instances — browse memories, peers, session | `packages/web/src/test/` | Vitest unit/integration tests + setup | | `packages/web/e2e/` | Playwright e2e specs | | `packages/desktop/` | Tauri shell that bundles the built web app | +| `charts/openconcho/` | Helm 3 chart for self-hosting on Kubernetes (OCI artifact on GHCR) | | `.claude/rules/` | Coding conventions (auto-loaded; stack-agnostic, applies to all agents) | | `docs/` | Architecture and references | diff --git a/README.md b/README.md index 3b14bac..fb2dfd1 100644 --- a/README.md +++ b/README.md @@ -114,6 +114,34 @@ profiles: `make up` runs the `dev` profile (`build: .`), `make prod` runs the (comma-separated host globs) for when you expose the proxy. Full details and env vars are in [`docs/docker.md`](docs/docker.md). +### Kubernetes (Helm) + +The chart is published as an OCI artifact to GHCR on every tagged release. + +```bash +helm install openconcho oci://ghcr.io/offendingcommit/charts/openconcho \ + --version 0.14.0 \ + --create-namespace --namespace openconcho \ + --set honcho.defaultUrl=https://honcho.example.com +``` + +Enable an Ingress and TLS: + +```bash +helm install openconcho oci://ghcr.io/offendingcommit/charts/openconcho \ + --version 0.14.0 \ + --create-namespace --namespace openconcho \ + --set honcho.defaultUrl=https://honcho.example.com \ + --set ingress.enabled=true \ + --set ingress.className=nginx \ + --set 'ingress.hosts[0].host=openconcho.example.com' \ + --set 'ingress.hosts[0].paths[0].path=/' \ + --set 'ingress.tls[0].secretName=openconcho-tls' \ + --set 'ingress.tls[0].hosts[0]=openconcho.example.com' +``` + +Full chart documentation, configuration reference, and an ArgoCD Application example are in [`charts/openconcho/README.md`](charts/openconcho/README.md). + ### Connecting to your instance 1. Enter the base URL of your Honcho instance (e.g. `http://localhost:8000`) diff --git a/charts/openconcho/README.md b/charts/openconcho/README.md new file mode 100644 index 0000000..82a0529 --- /dev/null +++ b/charts/openconcho/README.md @@ -0,0 +1,229 @@ +# openconcho Helm Chart + +Helm 3 chart for self-hosting the [openconcho](https://github.com/offendingcommit/openconcho) web UI on Kubernetes. + +The chart deploys a single nginx-unprivileged container (port 8080, UID 101) that serves the React SPA and reverse-proxies Honcho API calls under `/api` to avoid browser CORS issues. + +## Prerequisites + +- Kubernetes 1.25+ +- Helm 3.10+ +- A running [Honcho](https://github.com/plastic-labs/honcho) instance reachable from within the cluster (or via a configured ingress) + +## Installing + +Add the chart repository: + +```bash +helm registry login ghcr.io --username --password +``` + +Install the chart: + +```bash +helm install openconcho oci://ghcr.io/offendingcommit/charts/openconcho \ + --version 0.14.0 \ + --set honcho.defaultUrl=https://honcho.example.com +``` + +Or with a values file (recommended): + +```bash +helm install openconcho oci://ghcr.io/offendingcommit/charts/openconcho \ + --version 0.14.0 \ + -f my-values.yaml +``` + +## Upgrading + +```bash +helm upgrade openconcho oci://ghcr.io/offendingcommit/charts/openconcho \ + --version \ + -f my-values.yaml +``` + +## Uninstalling + +```bash +helm uninstall openconcho +``` + +## Configuration + +All values with their defaults are documented in [`values.yaml`](values.yaml). Key options: + +| Value | Default | Description | +|---|---|---| +| `replicaCount` | `1` | Number of pod replicas | +| `image.repository` | `ghcr.io/offendingcommit/openconcho-web` | Container image | +| `image.tag` | `""` | Tag; defaults to chart `appVersion` | +| `image.pullPolicy` | `IfNotPresent` | Image pull policy | +| `honcho.defaultUrl` | `""` | Honcho URL pre-seeded in the UI | +| `honcho.upstreamAllowlist` | `""` | SSRF guard (comma-separated host globs) | +| `service.type` | `ClusterIP` | `ClusterIP` / `NodePort` / `LoadBalancer` | +| `service.port` | `80` | Service port | +| `ingress.enabled` | `false` | Enable Ingress resource | +| `ingress.className` | `""` | IngressClass name | +| `autoscaling.enabled` | `false` | Enable HorizontalPodAutoscaler | +| `podDisruptionBudget.enabled` | `false` | Enable PodDisruptionBudget | +| `networkPolicy.enabled` | `false` | Enable NetworkPolicy (same-namespace only) | +| `resources.requests.memory` | `32Mi` | Memory request | +| `resources.limits.memory` | `128Mi` | Memory limit | + +## Examples + +### Minimal (ClusterIP, no ingress) + +```yaml +honcho: + defaultUrl: http://honcho.honcho.svc.cluster.local:8000 +``` + +### With Ingress and TLS (cert-manager) + +```yaml +honcho: + defaultUrl: https://honcho.example.com + +ingress: + enabled: true + className: nginx + annotations: + cert-manager.io/cluster-issuer: letsencrypt-prod + hosts: + - host: openconcho.example.com + paths: + - path: / + pathType: Prefix + tls: + - secretName: openconcho-tls + hosts: + - openconcho.example.com +``` + +### With autoscaling and disruption budget + +```yaml +replicaCount: 2 + +autoscaling: + enabled: true + minReplicas: 2 + maxReplicas: 10 + targetCPUUtilizationPercentage: 70 + +podDisruptionBudget: + enabled: true + minAvailable: 1 +``` + +### With NetworkPolicy + +> **Note:** When `networkPolicy.enabled=true` and `ingress.enabled=true`, you must add +> a policy that allows traffic from the ingress-controller namespace. Run +> `helm status ` for the exact `kubectl edit` command after install. + +```yaml +networkPolicy: + enabled: true + +ingress: + enabled: true + className: nginx + hosts: + - host: openconcho.example.com + paths: + - path: / + pathType: Prefix +``` + +### Private registry + +```yaml +image: + repository: registry.example.com/myorg/openconcho-web + tag: "0.14.0" + pullPolicy: Always + +imagePullSecrets: + - name: registry-credentials +``` + +## ArgoCD Application + +```yaml +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + name: openconcho + namespace: argocd +spec: + project: default + source: + repoURL: ghcr.io/offendingcommit/charts + chart: openconcho + targetRevision: 0.14.0 + helm: + valuesObject: + honcho: + defaultUrl: https://honcho.example.com + ingress: + enabled: true + className: nginx + annotations: + cert-manager.io/cluster-issuer: letsencrypt-prod + hosts: + - host: openconcho.example.com + paths: + - path: / + pathType: Prefix + tls: + - secretName: openconcho-tls + hosts: + - openconcho.example.com + destination: + server: https://kubernetes.default.svc + namespace: openconcho + syncPolicy: + automated: + prune: true + selfHeal: true + syncOptions: + - CreateNamespace=true +``` + +> OCI chart sources require ArgoCD 2.10+ (OCI Helm support GA). + +## Helm tests + +After install, run the bundled tests to verify the deployment is healthy: + +```bash +helm test openconcho +``` + +Two test pods run and exit 0 on success: + +| Test | What it checks | +|---|---| +| `test-healthz` | `GET /healthz` body equals `ok` | +| `test-spa-root` | `GET /` returns HTTP 200 | + +Pass `--logs` to see output from failing pods: + +```bash +helm test openconcho --logs +``` + +## Security posture + +| Control | Value | +|---|---| +| Run as UID/GID | 101 (nginx-unprivileged) | +| `runAsNonRoot` | `true` | +| `readOnlyRootFilesystem` | `true` | +| Linux capabilities | all dropped | +| `seccompProfile` | `RuntimeDefault` | +| `allowPrivilegeEscalation` | `false` | +| `automountServiceAccountToken` | `false` | +| Writable paths | `/var/cache/nginx`, `/var/run`, `/tmp` (tmpfs) | diff --git a/charts/openconcho/values.yaml b/charts/openconcho/values.yaml index 6448e12..e27a03a 100644 --- a/charts/openconcho/values.yaml +++ b/charts/openconcho/values.yaml @@ -1,23 +1,40 @@ +# Number of pod replicas. Increase for high availability or use autoscaling instead. replicaCount: 1 image: + # Container image repository. Override to use a custom registry or fork. repository: ghcr.io/offendingcommit/openconcho-web + # Image tag. Defaults to the chart appVersion when left empty. tag: "" pullPolicy: IfNotPresent +# Secrets for pulling images from private registries. +# Example: [{ name: my-registry-secret }] imagePullSecrets: [] + +# Override the name portion used in resource names and labels. nameOverride: "" +# Override the full resource name (normally release-name + chart-name). fullnameOverride: "" serviceAccount: + # Create a dedicated ServiceAccount for the pod. create: true + # Disable automatic ServiceAccount token mounting — the app never calls the Kubernetes API. automount: false + # Annotations to add to the ServiceAccount (e.g. for IRSA, Workload Identity, Vault). annotations: {} + # Use a pre-existing ServiceAccount instead of creating one. Ignored when create is true. name: "" +# Annotations applied to every pod (not the Deployment). Useful for Prometheus scraping, +# Vault agent injection, Datadog unified service tagging, etc. podAnnotations: {} +# Extra labels applied to every pod. podLabels: {} +# Pod-level security context shared by all containers. +# UID/GID 101 matches the nginx-unprivileged base image — do not change without rebuilding. podSecurityContext: runAsNonRoot: true runAsUser: 101 @@ -26,37 +43,62 @@ podSecurityContext: seccompProfile: type: RuntimeDefault +# Container-level security context. securityContext: allowPrivilegeEscalation: false readOnlyRootFilesystem: true capabilities: drop: [ALL] +# Directories mounted as ephemeral tmpfs (in-memory) to satisfy nginx's write requirements +# when the root filesystem is read-only. Add entries for any additional writable paths. tmpfsMounts: - mountPath: /var/cache/nginx - mountPath: /var/run - mountPath: /tmp service: + # Kubernetes Service type. Options: ClusterIP | NodePort | LoadBalancer type: ClusterIP + # Port exposed by the Service (what the Ingress or other pods target). port: 80 + # Port the container actually listens on (nginx-unprivileged default). containerPort: 8080 ingress: enabled: false + # IngressClass name. Leave empty to accept the cluster default. + # Examples: nginx | traefik | alb | kong className: "" + # Annotations forwarded verbatim to the Ingress resource. + # Example (cert-manager + nginx-ingress): + # kubernetes.io/ingress.class: nginx + # cert-manager.io/cluster-issuer: letsencrypt-prod annotations: {} hosts: - host: openconcho.example.com paths: - path: / pathType: Prefix + # TLS configuration. Provide a Secret containing the certificate. + # Example: + # - secretName: openconcho-tls + # hosts: + # - openconcho.example.com tls: [] honcho: + # Default Honcho instance URL pre-populated in the UI on first load. + # Users can change or add instances at runtime; this only seeds the initial value. + # Example: https://honcho.example.com defaultUrl: "" + # Optional SSRF guard: comma-separated host globs the nginx proxy is allowed to forward to. + # Leave empty to allow any upstream. Applies only when the proxy is publicly reachable. + # Example: honcho.example.com,*.internal.example.com upstreamAllowlist: "" +# CPU and memory requests / limits for the web container. +# The SPA is static HTML/JS, so memory is the primary concern and CPU is negligible at rest. resources: requests: cpu: 50m @@ -82,16 +124,55 @@ autoscaling: enabled: false minReplicas: 1 maxReplicas: 5 + # Scale up when average CPU utilization across pods exceeds this percentage. targetCPUUtilizationPercentage: 80 podDisruptionBudget: enabled: false + # Minimum number of pods that must remain available during voluntary disruptions + # (node drains, rolling upgrades). Set maxUnavailable instead to flip the direction. minAvailable: 1 + # Uncomment to use maxUnavailable instead — cannot set both simultaneously. + # maxUnavailable: 1 +# Restrict pod-to-pod traffic at the network layer. When enabled, only pods in the +# same namespace may reach the web container. +# WARNING: if ingress is also enabled, you must add a policy that allows traffic from +# the ingress-controller namespace — run `helm status ` to see the reminder. networkPolicy: enabled: false +# Spread pods across failure domains (availability zones, nodes, etc.) to reduce +# the blast radius of a single-node or single-zone failure. +# Example (zone spread): +# - maxSkew: 1 +# topologyKey: topology.kubernetes.io/zone +# whenUnsatisfiable: DoNotSchedule +# labelSelector: +# matchLabels: +# app.kubernetes.io/name: openconcho topologySpreadConstraints: [] + +# Constrain pods to nodes whose labels match these key/value pairs. +# Example: kubernetes.io/arch: amd64 nodeSelector: {} + +# Allow pods to be scheduled on tainted nodes. +# Example: +# - key: dedicated +# operator: Equal +# value: web +# effect: NoSchedule tolerations: [] + +# Advanced pod affinity / anti-affinity rules. +# Example (soft anti-affinity — prefer spreading across different nodes): +# podAntiAffinity: +# preferredDuringSchedulingIgnoredDuringExecution: +# - weight: 100 +# podAffinityTerm: +# labelSelector: +# matchLabels: +# app.kubernetes.io/name: openconcho +# topologyKey: kubernetes.io/hostname affinity: {}