diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml index 9e8fd80..9b0394d 100644 --- a/.github/workflows/docker-publish.yml +++ b/.github/workflows/docker-publish.yml @@ -44,3 +44,34 @@ jobs: labels: ${{ steps.meta.outputs.labels }} cache-from: type=gha cache-to: type=gha,mode=max + + publish-chart: + name: Package & push Helm chart to GHCR + runs-on: ubuntu-latest + needs: [publish] + if: startsWith(github.ref, 'refs/tags/') + steps: + - uses: actions/checkout@v4 + + - uses: azure/setup-helm@v4 + + - name: Derive chart version + id: version + run: echo "VERSION=${GITHUB_REF_NAME#v}" >> "$GITHUB_OUTPUT" + + - name: Log in to GHCR (Helm OCI) + run: | + echo "${{ secrets.GITHUB_TOKEN }}" | helm registry login ghcr.io \ + --username "${{ github.actor }}" \ + --password-stdin + + - name: Package chart + run: | + helm package charts/openconcho \ + --version "${{ steps.version.outputs.VERSION }}" \ + --app-version "${{ steps.version.outputs.VERSION }}" + + - name: Push chart + run: | + helm push "openconcho-${{ steps.version.outputs.VERSION }}.tgz" \ + oci://ghcr.io/${{ github.repository_owner }}/charts 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/Chart.yaml b/charts/openconcho/Chart.yaml new file mode 100644 index 0000000..12a4099 --- /dev/null +++ b/charts/openconcho/Chart.yaml @@ -0,0 +1,16 @@ +apiVersion: v2 +name: openconcho +description: Self-hosted UI for Honcho — browse memories, peers, sessions, conclusions, and chat with memory context. +type: application +version: 0.14.0 +appVersion: "0.14.0" +keywords: + - honcho + - memory + - ai +home: https://github.com/offendingcommit/openconcho +sources: + - https://github.com/offendingcommit/openconcho +maintainers: + - name: offendingcommit + url: https://github.com/offendingcommit 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/templates/NOTES.txt b/charts/openconcho/templates/NOTES.txt new file mode 100644 index 0000000..7cc7ea7 --- /dev/null +++ b/charts/openconcho/templates/NOTES.txt @@ -0,0 +1,40 @@ +OpenConcho {{ .Chart.AppVersion }} deployed to namespace {{ .Release.Namespace }}. + +{{- if .Values.ingress.enabled }} +Access: +{{- range .Values.ingress.hosts }} + http{{ if $.Values.ingress.tls }}s{{ end }}://{{ .host }} +{{- end }} +{{- else if eq .Values.service.type "NodePort" }} +Access (NodePort): + export NODE_PORT=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "openconcho.fullname" . }} -o jsonpath="{.spec.ports[0].nodePort}") + export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}") + echo "http://$NODE_IP:$NODE_PORT" +{{- else if eq .Values.service.type "LoadBalancer" }} +Access (LoadBalancer — IP may take a few minutes): + export LB_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "openconcho.fullname" . }} --template '{{ "{{" }}range (index .status.loadBalancer.ingress 0){{ "}}" }}{{ "{{" }}.{{ "}}" }}{{ "{{" }}end{{ "}}" }}') + echo "http://$LB_IP:{{ .Values.service.port }}" +{{- else }} +Access (port-forward): + kubectl port-forward --namespace {{ .Release.Namespace }} svc/{{ include "openconcho.fullname" . }} 8080:{{ .Values.service.port }} + Then open http://localhost:8080 +{{- end }} + +Run Helm tests to verify the deployment: + helm test {{ .Release.Name }} + +{{- if and .Values.networkPolicy.enabled .Values.ingress.enabled }} + +WARNING: NetworkPolicy + Ingress are both enabled. +The default NetworkPolicy allows port {{ .Values.service.containerPort }} only from pods within +namespace {{ .Release.Namespace }}. Ingress controllers typically run in a separate +namespace (ingress-nginx, kube-system, etc.) and will be blocked. +To allow ingress-controller traffic, add a namespaceSelector rule: + + kubectl edit networkpolicy --namespace {{ .Release.Namespace }} {{ include "openconcho.fullname" . }} + + # Under spec.ingress[0].from, add: + - namespaceSelector: + matchLabels: + kubernetes.io/metadata.name: +{{- end }} diff --git a/charts/openconcho/templates/_helpers.tpl b/charts/openconcho/templates/_helpers.tpl new file mode 100644 index 0000000..ef8fb6a --- /dev/null +++ b/charts/openconcho/templates/_helpers.tpl @@ -0,0 +1,46 @@ +{{- define "openconcho.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{- define "openconcho.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 }} + +{{- define "openconcho.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{- define "openconcho.labels" -}} +helm.sh/chart: {{ include "openconcho.chart" . }} +{{ include "openconcho.selectorLabels" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{- define "openconcho.selectorLabels" -}} +app.kubernetes.io/name: {{ include "openconcho.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} + +{{- define "openconcho.serviceAccountName" -}} +{{- if .Values.serviceAccount.create }} +{{- default (include "openconcho.fullname" .) .Values.serviceAccount.name }} +{{- else }} +{{- default "default" .Values.serviceAccount.name }} +{{- end }} +{{- end }} + +{{- define "openconcho.imageTag" -}} +{{- .Values.image.tag | default .Chart.AppVersion }} +{{- end }} diff --git a/charts/openconcho/templates/deployment.yaml b/charts/openconcho/templates/deployment.yaml new file mode 100644 index 0000000..900d459 --- /dev/null +++ b/charts/openconcho/templates/deployment.yaml @@ -0,0 +1,84 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "openconcho.fullname" . }} + labels: + {{- include "openconcho.labels" . | nindent 4 }} +spec: + {{- if not .Values.autoscaling.enabled }} + replicas: {{ .Values.replicaCount }} + {{- end }} + selector: + matchLabels: + {{- include "openconcho.selectorLabels" . | nindent 6 }} + template: + metadata: + {{- with .Values.podAnnotations }} + annotations: + {{- toYaml . | nindent 8 }} + {{- end }} + labels: + {{- include "openconcho.selectorLabels" . | nindent 8 }} + {{- with .Values.podLabels }} + {{- toYaml . | nindent 8 }} + {{- end }} + spec: + {{- with .Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + serviceAccountName: {{ include "openconcho.serviceAccountName" . }} + securityContext: + {{- toYaml .Values.podSecurityContext | nindent 8 }} + containers: + - name: {{ .Chart.Name }} + securityContext: + {{- toYaml .Values.securityContext | nindent 12 }} + image: "{{ .Values.image.repository }}:{{ include "openconcho.imageTag" . }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + ports: + - name: http + containerPort: {{ .Values.service.containerPort }} + protocol: TCP + env: + - name: OPENCONCHO_DEFAULT_HONCHO_URL + value: {{ .Values.honcho.defaultUrl | quote }} + - name: OPENCONCHO_UPSTREAM_ALLOWLIST + value: {{ .Values.honcho.upstreamAllowlist | quote }} + livenessProbe: + {{- toYaml .Values.livenessProbe | nindent 12 }} + readinessProbe: + {{- toYaml .Values.readinessProbe | nindent 12 }} + resources: + {{- toYaml .Values.resources | nindent 12 }} + {{- if .Values.tmpfsMounts }} + volumeMounts: + {{- range .Values.tmpfsMounts }} + - name: {{ .mountPath | trimPrefix "/" | replace "/" "-" | trunc 63 | trimSuffix "-" }} + mountPath: {{ .mountPath }} + {{- end }} + {{- end }} + {{- if .Values.tmpfsMounts }} + volumes: + {{- range .Values.tmpfsMounts }} + - name: {{ .mountPath | trimPrefix "/" | replace "/" "-" | trunc 63 | trimSuffix "-" }} + emptyDir: + medium: Memory + {{- end }} + {{- 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 }} + {{- with .Values.topologySpreadConstraints }} + topologySpreadConstraints: + {{- toYaml . | nindent 8 }} + {{- end }} diff --git a/charts/openconcho/templates/hpa.yaml b/charts/openconcho/templates/hpa.yaml new file mode 100644 index 0000000..43a9b83 --- /dev/null +++ b/charts/openconcho/templates/hpa.yaml @@ -0,0 +1,24 @@ +{{- if .Values.autoscaling.enabled }} +apiVersion: autoscaling/v2 +kind: HorizontalPodAutoscaler +metadata: + name: {{ include "openconcho.fullname" . }} + labels: + {{- include "openconcho.labels" . | nindent 4 }} +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: {{ include "openconcho.fullname" . }} + minReplicas: {{ .Values.autoscaling.minReplicas }} + maxReplicas: {{ .Values.autoscaling.maxReplicas }} + {{- if .Values.autoscaling.targetCPUUtilizationPercentage }} + metrics: + - type: Resource + resource: + name: cpu + target: + type: Utilization + averageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }} + {{- end }} +{{- end }} diff --git a/charts/openconcho/templates/ingress.yaml b/charts/openconcho/templates/ingress.yaml new file mode 100644 index 0000000..27404eb --- /dev/null +++ b/charts/openconcho/templates/ingress.yaml @@ -0,0 +1,39 @@ +{{- if .Values.ingress.enabled -}} +{{- $fullName := include "openconcho.fullname" . -}} +{{- $svcPort := .Values.service.port -}} +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: {{ $fullName }} + labels: + {{- include "openconcho.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: + {{- toYaml .Values.ingress.tls | nindent 4 }} + {{- end }} + {{- if .Values.ingress.hosts }} + rules: + {{- range .Values.ingress.hosts }} + - host: {{ .host | quote }} + http: + paths: + {{- range .paths }} + - path: {{ .path }} + pathType: {{ .pathType | default "Prefix" }} + backend: + service: + name: {{ $fullName }} + port: + number: {{ $svcPort }} + {{- end }} + {{- end }} + {{- end }} +{{- end }} diff --git a/charts/openconcho/templates/networkpolicy.yaml b/charts/openconcho/templates/networkpolicy.yaml new file mode 100644 index 0000000..b7dfe4a --- /dev/null +++ b/charts/openconcho/templates/networkpolicy.yaml @@ -0,0 +1,20 @@ +{{- if .Values.networkPolicy.enabled }} +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: {{ include "openconcho.fullname" . }} + labels: + {{- include "openconcho.labels" . | nindent 4 }} +spec: + podSelector: + matchLabels: + {{- include "openconcho.selectorLabels" . | nindent 6 }} + policyTypes: + - Ingress + ingress: + - ports: + - port: {{ .Values.service.containerPort }} + protocol: TCP + from: + - podSelector: {} +{{- end }} diff --git a/charts/openconcho/templates/pdb.yaml b/charts/openconcho/templates/pdb.yaml new file mode 100644 index 0000000..de4b44b --- /dev/null +++ b/charts/openconcho/templates/pdb.yaml @@ -0,0 +1,17 @@ +{{- if .Values.podDisruptionBudget.enabled }} +apiVersion: policy/v1 +kind: PodDisruptionBudget +metadata: + name: {{ include "openconcho.fullname" . }} + labels: + {{- include "openconcho.labels" . | nindent 4 }} +spec: + {{- if not (kindIs "invalid" .Values.podDisruptionBudget.maxUnavailable) }} + maxUnavailable: {{ .Values.podDisruptionBudget.maxUnavailable }} + {{- else }} + minAvailable: {{ .Values.podDisruptionBudget.minAvailable | default 1 }} + {{- end }} + selector: + matchLabels: + {{- include "openconcho.selectorLabels" . | nindent 6 }} +{{- end }} diff --git a/charts/openconcho/templates/service.yaml b/charts/openconcho/templates/service.yaml new file mode 100644 index 0000000..a542306 --- /dev/null +++ b/charts/openconcho/templates/service.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ include "openconcho.fullname" . }} + labels: + {{- include "openconcho.labels" . | nindent 4 }} +spec: + type: {{ .Values.service.type }} + ports: + - port: {{ .Values.service.port }} + targetPort: http + protocol: TCP + name: http + selector: + {{- include "openconcho.selectorLabels" . | nindent 4 }} diff --git a/charts/openconcho/templates/serviceaccount.yaml b/charts/openconcho/templates/serviceaccount.yaml new file mode 100644 index 0000000..433c583 --- /dev/null +++ b/charts/openconcho/templates/serviceaccount.yaml @@ -0,0 +1,13 @@ +{{- if .Values.serviceAccount.create -}} +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include "openconcho.serviceAccountName" . }} + labels: + {{- include "openconcho.labels" . | nindent 4 }} + {{- with .Values.serviceAccount.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +automountServiceAccountToken: {{ .Values.serviceAccount.automount }} +{{- end }} diff --git a/charts/openconcho/templates/tests/test-healthz.yaml b/charts/openconcho/templates/tests/test-healthz.yaml new file mode 100644 index 0000000..5a512e4 --- /dev/null +++ b/charts/openconcho/templates/tests/test-healthz.yaml @@ -0,0 +1,25 @@ +apiVersion: v1 +kind: Pod +metadata: + name: "{{ include "openconcho.fullname" . }}-test-healthz" + labels: + {{- include "openconcho.labels" . | nindent 4 }} + annotations: + helm.sh/hook: test + helm.sh/hook-delete-policy: before-hook-creation +spec: + restartPolicy: Never + activeDeadlineSeconds: 60 + containers: + - name: healthz + image: busybox:1.36 + command: + - sh + - -c + - | + RESPONSE=$(wget -T 10 -qO- http://{{ include "openconcho.fullname" . }}:{{ .Values.service.port }}/healthz) + if [ "$RESPONSE" != "ok" ]; then + echo "FAIL: expected 'ok', got '$RESPONSE'" + exit 1 + fi + echo "PASS: /healthz returned 'ok'" diff --git a/charts/openconcho/templates/tests/test-spa-root.yaml b/charts/openconcho/templates/tests/test-spa-root.yaml new file mode 100644 index 0000000..e6acda8 --- /dev/null +++ b/charts/openconcho/templates/tests/test-spa-root.yaml @@ -0,0 +1,24 @@ +apiVersion: v1 +kind: Pod +metadata: + name: "{{ include "openconcho.fullname" . }}-test-spa-root" + labels: + {{- include "openconcho.labels" . | nindent 4 }} + annotations: + helm.sh/hook: test + helm.sh/hook-delete-policy: before-hook-creation +spec: + restartPolicy: Never + activeDeadlineSeconds: 60 + containers: + - name: spa-root + image: busybox:1.36 + command: + - sh + - -c + - | + if ! wget -T 10 -q --spider http://{{ include "openconcho.fullname" . }}:{{ .Values.service.port }}/; then + echo "FAIL: GET / did not return HTTP 200" + exit 1 + fi + echo "PASS: / returned HTTP 200" diff --git a/charts/openconcho/values.schema.json b/charts/openconcho/values.schema.json new file mode 100644 index 0000000..2b15f7b --- /dev/null +++ b/charts/openconcho/values.schema.json @@ -0,0 +1,67 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "properties": { + "replicaCount": { + "type": "integer", + "minimum": 1 + }, + "image": { + "type": "object", + "required": ["repository", "pullPolicy"], + "properties": { + "repository": { "type": "string", "minLength": 1 }, + "tag": { "type": "string" }, + "pullPolicy": { + "type": "string", + "enum": ["Always", "IfNotPresent", "Never"] + } + } + }, + "service": { + "type": "object", + "required": ["type", "port", "containerPort"], + "properties": { + "type": { + "type": "string", + "enum": ["ClusterIP", "NodePort", "LoadBalancer"] + }, + "port": { "type": "integer", "minimum": 1, "maximum": 65535 }, + "containerPort": { "type": "integer", "minimum": 1, "maximum": 65535 } + } + }, + "honcho": { + "type": "object", + "properties": { + "defaultUrl": { "type": "string" }, + "upstreamAllowlist": { "type": "string" } + } + }, + "autoscaling": { + "type": "object", + "properties": { + "enabled": { "type": "boolean" }, + "minReplicas": { "type": "integer", "minimum": 1 }, + "maxReplicas": { "type": "integer", "minimum": 1 }, + "targetCPUUtilizationPercentage": { + "type": "integer", + "minimum": 1, + "maximum": 100 + } + } + }, + "podDisruptionBudget": { + "type": "object", + "properties": { + "enabled": { "type": "boolean" }, + "minAvailable": { "type": "integer", "minimum": 0 } + } + }, + "networkPolicy": { + "type": "object", + "properties": { + "enabled": { "type": "boolean" } + } + } + } +} diff --git a/charts/openconcho/values.yaml b/charts/openconcho/values.yaml new file mode 100644 index 0000000..e27a03a --- /dev/null +++ b/charts/openconcho/values.yaml @@ -0,0 +1,178 @@ +# 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 + runAsGroup: 101 + fsGroup: 101 + 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 + memory: 32Mi + limits: + memory: 128Mi + +livenessProbe: + httpGet: + path: /healthz + port: http + initialDelaySeconds: 5 + periodSeconds: 10 + +readinessProbe: + httpGet: + path: /healthz + port: http + initialDelaySeconds: 5 + periodSeconds: 10 + +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: {}