Skip to content

feat(helm): add Helm chart for Kubernetes deployment#1158

Open
wwagops wants to merge 1 commit into
ItzCrazyKns:masterfrom
wwagops:feat/kubernetes-helm-chart
Open

feat(helm): add Helm chart for Kubernetes deployment#1158
wwagops wants to merge 1 commit into
ItzCrazyKns:masterfrom
wwagops:feat/kubernetes-helm-chart

Conversation

@wwagops

@wwagops wwagops commented Jul 2, 2026

Copy link
Copy Markdown

Summary

Adds a Helm chart under charts/vane to deploy Vane on Kubernetes. A single variant value selects the official image, so both published images are covered by one chart:

variant Image SearXNG
full (default) itzcrazykns1337/vane:latest bundled inside the image
slim itzcrazykns1337/vane:slim-latest deployed by the chart (searxng.deploy=true) or external (searxng.url)

What's included

  • Vane workload: Deployment, Service, ServiceAccount, optional Ingress and HPA
  • Persistence: PVCs for the config/SQLite database (/home/vane/data) and uploads (/home/vane/uploads)
  • Optional in-cluster SearXNG (slim only): Deployment/Service/Secret generated from the repo's searxng/settings.yml (JSON format + Wolfram Alpha engine enabled), with a stable auto-generated secret_key, wired to Vane through SEARXNG_API_URL
  • Probes (liveness/readiness/startup), resource limits and scheduling controls (nodeSelector/tolerations/affinity)
  • values.schema.json for install-time validation of the values
  • Security default: automountServiceAccountToken: false
  • Guardrails (fail) for an invalid variant or a slim install with no search backend

Usage

# Full image (Vane + bundled SearXNG) — recommended default
helm install vane ./charts/vane

# Slim image with a chart-managed SearXNG
helm install vane ./charts/vane --set variant=slim --set searxng.deploy=true

# Slim image with an existing SearXNG instance
helm install vane ./charts/vane \
  --set variant=slim --set searxng.deploy=false \
  --set searxng.url=http://my-searxng:8080

See charts/vane/README.md for the full list of options.

Notes / tradeoffs

  • The image tag defaults to the floating latest / slim-latest tags; override image.tag to pin a release such as 1.12.2 / slim-1.12.2.
  • No enforced non-root securityContext by default: the full image starts SearXNG via sudo and both images run as root, so hardening is left configurable rather than forced.
  • Vane stores its config and history in a local SQLite database on a ReadWriteOnce volume, so the chart defaults to replicaCount: 1 with a Recreate strategy.

Validation

  • helm lint passes for full, slim + in-cluster SearXNG, and slim + external SearXNG
  • helm template renders valid manifests in all three modes; the JSON schema rejects invalid values (unknown keys, wrong types, out-of-range ports, invalid variant)
  • prettier clean — chart templates are excluded from Prettier via .prettierignore because Go templating conflicts with it

Summary by cubic

Add a Helm chart to deploy Vane on Kubernetes, supporting both full and slim images via a single variant switch. This makes K8s installs simple, with optional in-cluster SearXNG, persistence, and basic ops features.

  • New Features
    • Chart added under charts/vane with variant=full|slim; slim supports in-cluster or external SearXNG.
    • Core resources: Deployment, Service, optional Ingress and HPA, ServiceAccount with automountServiceAccountToken=false.
    • Persistence: PVCs for /home/vane/data and /home/vane/uploads.
    • Slim SearXNG: deploys searxng/searxng (JSON format + Wolfram Alpha enabled) with a stable auto-generated secret_key, or use searxng.url.
    • Probes and controls: liveness/readiness/startup probes; nodeSelector/tolerations/affinity.
    • Validation: values.schema.json and guardrails for invalid variant or slim without a search backend.
    • Docs/tooling: README adds Helm install steps; .prettierignore excludes Helm templates.

Written for commit b39f0a0. Summary will update on new commits.

Review in cubic

Add a Helm chart under charts/vane to deploy Vane on Kubernetes. A single
`variant` value selects the official image:

- full (default): itzcrazykns1337/vane:latest, with SearXNG bundled in the image
- slim: itzcrazykns1337/vane:slim-latest, with SearXNG either deployed by the
  chart (searxng.deploy=true) or referenced externally (searxng.url)

Highlights:
- Persistent volumes for the config/SQLite database and uploaded files
- Optional in-cluster SearXNG (Deployment/Service/Secret) configured with JSON
  format and the Wolfram Alpha engine, with a stable auto-generated secret key
- Ingress, HPA, liveness/readiness/startup probes and scheduling controls
- values.schema.json for install-time validation of the values
- ServiceAccount API token auto-mounting disabled by default
- Guardrails that fail early on an invalid variant or a slim install with no
  search backend

Also document the chart in the README installation section and exclude the
chart templates from Prettier (Go templating conflicts with it).

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

7 issues found across 19 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="charts/vane/README.md">

<violation number="1" location="charts/vane/README.md:26">
P2: The README hardcodes `vane` as the Helm release name across all example commands without any note or placeholder to indicate it should be substituted. Since the service name is rendered via `vane.fullname` and the PVC label selector targets `app.kubernetes.io/instance={{ .Release.Name }}`, any user who installs with a different release name will have broken follow-up commands (`kubectl port-forward svc/vane` will target the wrong service, `kubectl delete pvc -l app.kubernetes.io/instance=vane` will silently miss resources). Consider adding a note such as "Replace `vane` with your chosen Helm release name in the commands below" or switching to a `<release-name>` placeholder so the documentation stays correct regardless of the install name.</violation>
</file>

<file name="charts/vane/values.schema.json">

<violation number="1" location="charts/vane/values.schema.json:76">
P2: Allowing `ExternalName` in the service type enum without an accompanying `externalName` property creates a broken contract: Kubernetes requires `spec.externalName` for `ExternalName` Services, but the schema blocks that field (`additionalProperties: false`) and the templates never reference it. Users who select `ExternalName` will either fail schema validation or produce an invalid Kubernetes manifest. Consider removing `ExternalName` from the enum, or adding an `externalName` string property to the schema and referencing it in the Service templates.</violation>
</file>

<file name="charts/vane/templates/_helpers.tpl">

<violation number="1" location="charts/vane/templates/_helpers.tpl:147">
P1: The `vane.searxng.secretKey` helper falls back to `randAlphaNum 64` when `lookup` finds no existing secret. Because `lookup` returns empty during `helm template` (used by ArgoCD, Flux, and many CI diff steps), every render generates a different secret. That produces non-deterministic manifests and perpetual GitOps drift. On every sync the generated Secret changes, which also rolls the SearXNG pod because the Deployment includes a `checksum/secret` annotation keyed off the rendered secret manifest. Consider adding clear documentation (and ideally a schema guard) that `searxng.secretKey` must be explicitly set for deterministic/GitOps deployments, or replace the non-deterministic fallback with a stable derivation when no cluster state is available.</violation>
</file>

<file name="charts/vane/values.yaml">

<violation number="1" location="charts/vane/values.yaml:18">
P2: Default image tags use floating references (`latest` / `slim-latest`), which breaks Helm's reproducibility guarantees and makes rollbacks unreliable. Because `pullPolicy` is `IfNotPresent`, nodes that already cached an older `latest` image will continue running it while new nodes pull the current one, creating cluster-level version skew. Consider defaulting the Vane image to the chart's declared `appVersion` (`1.12.2`) and pinning SearXNG to a specific release tag instead of `latest`.</violation>
</file>

<file name="charts/vane/templates/hpa.yaml">

<violation number="1" location="charts/vane/templates/hpa.yaml:14">
P2: The HPA allows scaling to `maxReplicas` (default 3), but the chart defaults to a ReadWriteOnce PVC for a local SQLite database that does not support multi-writer access. When autoscaling triggers additional replicas, those pods will fail to schedule because the RWO volume cannot be mounted concurrently, leading to multi-attach volume errors and a broken deployment. Consider adding a chart guard or schema validation that prevents enabling autoscaling with a `maxReplicas > 1` while the default RWO persistence is in use, or at least automatically cap `maxReplicas` to 1 when using a single-writer volume.</violation>
</file>

<file name="charts/vane/templates/deployment.yaml">

<violation number="1" location="charts/vane/templates/deployment.yaml:21">
P2: When `.Values.podAnnotations` is unset, the template renders `metadata.annotations` as `null` instead of omitting the key. Strict schema validators and policy engines may reject `null` for object-typed fields like `annotations`. It is safer to wrap the entire `annotations:` block in the `with` so the key is only emitted when annotations actually exist.</violation>

<violation number="2" location="charts/vane/templates/deployment.yaml:90">
P2: `volumeMounts:` and `volumes:` are always emitted, but their list items are conditional. When a user disables both PVCs (set `persistence.data.enabled=false` and `persistence.uploads.enabled=false`) and provides no extras, both fields render as null arrays. Some strict Kubernetes validators or admission policies reject null list values, which can break CI or deployment pipelines. The adjacent `env:` block already avoids this by wrapping the whole field in a conditional; applying the same guard to `volumeMounts` and `volumes` would make the chart more robust.

Wrap each list field in an `or` check (e.g., `{{- if or .Values.persistence.data.enabled .Values.persistence.uploads.enabled .Values.extraVolumeMounts }}`) so the header is only emitted when at least one item will follow.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

{{- if and $existing $existing.data (index $existing.data "secretKey") -}}
{{- index $existing.data "secretKey" | b64dec -}}
{{- else -}}
{{- randAlphaNum 64 -}}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: The vane.searxng.secretKey helper falls back to randAlphaNum 64 when lookup finds no existing secret. Because lookup returns empty during helm template (used by ArgoCD, Flux, and many CI diff steps), every render generates a different secret. That produces non-deterministic manifests and perpetual GitOps drift. On every sync the generated Secret changes, which also rolls the SearXNG pod because the Deployment includes a checksum/secret annotation keyed off the rendered secret manifest. Consider adding clear documentation (and ideally a schema guard) that searxng.secretKey must be explicitly set for deterministic/GitOps deployments, or replace the non-deterministic fallback with a stable derivation when no cluster state is available.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At charts/vane/templates/_helpers.tpl, line 147:

<comment>The `vane.searxng.secretKey` helper falls back to `randAlphaNum 64` when `lookup` finds no existing secret. Because `lookup` returns empty during `helm template` (used by ArgoCD, Flux, and many CI diff steps), every render generates a different secret. That produces non-deterministic manifests and perpetual GitOps drift. On every sync the generated Secret changes, which also rolls the SearXNG pod because the Deployment includes a `checksum/secret` annotation keyed off the rendered secret manifest. Consider adding clear documentation (and ideally a schema guard) that `searxng.secretKey` must be explicitly set for deterministic/GitOps deployments, or replace the non-deterministic fallback with a stable derivation when no cluster state is available.</comment>

<file context>
@@ -0,0 +1,164 @@
+{{- if and $existing $existing.data (index $existing.data "secretKey") -}}
+{{- index $existing.data "secretKey" | b64dec -}}
+{{- else -}}
+{{- randAlphaNum 64 -}}
+{{- end -}}
+{{- end -}}
</file context>

Comment thread charts/vane/README.md

```bash
# Full image (Vane + bundled SearXNG) — the recommended default
helm install vane ./charts/vane

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: The README hardcodes vane as the Helm release name across all example commands without any note or placeholder to indicate it should be substituted. Since the service name is rendered via vane.fullname and the PVC label selector targets app.kubernetes.io/instance={{ .Release.Name }}, any user who installs with a different release name will have broken follow-up commands (kubectl port-forward svc/vane will target the wrong service, kubectl delete pvc -l app.kubernetes.io/instance=vane will silently miss resources). Consider adding a note such as "Replace vane with your chosen Helm release name in the commands below" or switching to a <release-name> placeholder so the documentation stays correct regardless of the install name.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At charts/vane/README.md, line 26:

<comment>The README hardcodes `vane` as the Helm release name across all example commands without any note or placeholder to indicate it should be substituted. Since the service name is rendered via `vane.fullname` and the PVC label selector targets `app.kubernetes.io/instance={{ .Release.Name }}`, any user who installs with a different release name will have broken follow-up commands (`kubectl port-forward svc/vane` will target the wrong service, `kubectl delete pvc -l app.kubernetes.io/instance=vane` will silently miss resources). Consider adding a note such as "Replace `vane` with your chosen Helm release name in the commands below" or switching to a `<release-name>` placeholder so the documentation stays correct regardless of the install name.</comment>

<file context>
@@ -0,0 +1,129 @@
+
+```bash
+# Full image (Vane + bundled SearXNG) — the recommended default
+helm install vane ./charts/vane
+
+# Slim image, letting the chart deploy SearXNG for you
</file context>

"properties": {
"type": {
"type": "string",
"enum": ["ClusterIP", "NodePort", "LoadBalancer", "ExternalName"]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Allowing ExternalName in the service type enum without an accompanying externalName property creates a broken contract: Kubernetes requires spec.externalName for ExternalName Services, but the schema blocks that field (additionalProperties: false) and the templates never reference it. Users who select ExternalName will either fail schema validation or produce an invalid Kubernetes manifest. Consider removing ExternalName from the enum, or adding an externalName string property to the schema and referencing it in the Service templates.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At charts/vane/values.schema.json, line 76:

<comment>Allowing `ExternalName` in the service type enum without an accompanying `externalName` property creates a broken contract: Kubernetes requires `spec.externalName` for `ExternalName` Services, but the schema blocks that field (`additionalProperties: false`) and the templates never reference it. Users who select `ExternalName` will either fail schema validation or produce an invalid Kubernetes manifest. Consider removing `ExternalName` from the enum, or adding an `externalName` string property to the schema and referencing it in the Service templates.</comment>

<file context>
@@ -0,0 +1,211 @@
+          "properties": {
+            "type": {
+              "type": "string",
+              "enum": ["ClusterIP", "NodePort", "LoadBalancer", "ExternalName"]
+            },
+            "port": { "type": "integer", "minimum": 1, "maximum": 65535 }
</file context>

Comment thread charts/vane/values.yaml
repository: itzcrazykns1337/vane
# -- Overrides the image tag. When empty the tag is derived from `variant`:
# "full" -> "latest", "slim" -> "slim-latest".
tag: ''

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Default image tags use floating references (latest / slim-latest), which breaks Helm's reproducibility guarantees and makes rollbacks unreliable. Because pullPolicy is IfNotPresent, nodes that already cached an older latest image will continue running it while new nodes pull the current one, creating cluster-level version skew. Consider defaulting the Vane image to the chart's declared appVersion (1.12.2) and pinning SearXNG to a specific release tag instead of latest.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At charts/vane/values.yaml, line 18:

<comment>Default image tags use floating references (`latest` / `slim-latest`), which breaks Helm's reproducibility guarantees and makes rollbacks unreliable. Because `pullPolicy` is `IfNotPresent`, nodes that already cached an older `latest` image will continue running it while new nodes pull the current one, creating cluster-level version skew. Consider defaulting the Vane image to the chart's declared `appVersion` (`1.12.2`) and pinning SearXNG to a specific release tag instead of `latest`.</comment>

<file context>
@@ -0,0 +1,200 @@
+  repository: itzcrazykns1337/vane
+  # -- Overrides the image tag. When empty the tag is derived from `variant`:
+  # "full" -> "latest", "slim" -> "slim-latest".
+  tag: ''
+  pullPolicy: IfNotPresent
+
</file context>

kind: Deployment
name: {{ include "vane.fullname" . }}
minReplicas: {{ .Values.autoscaling.minReplicas }}
maxReplicas: {{ .Values.autoscaling.maxReplicas }}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: The HPA allows scaling to maxReplicas (default 3), but the chart defaults to a ReadWriteOnce PVC for a local SQLite database that does not support multi-writer access. When autoscaling triggers additional replicas, those pods will fail to schedule because the RWO volume cannot be mounted concurrently, leading to multi-attach volume errors and a broken deployment. Consider adding a chart guard or schema validation that prevents enabling autoscaling with a maxReplicas > 1 while the default RWO persistence is in use, or at least automatically cap maxReplicas to 1 when using a single-writer volume.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At charts/vane/templates/hpa.yaml, line 14:

<comment>The HPA allows scaling to `maxReplicas` (default 3), but the chart defaults to a ReadWriteOnce PVC for a local SQLite database that does not support multi-writer access. When autoscaling triggers additional replicas, those pods will fail to schedule because the RWO volume cannot be mounted concurrently, leading to multi-attach volume errors and a broken deployment. Consider adding a chart guard or schema validation that prevents enabling autoscaling with a `maxReplicas > 1` while the default RWO persistence is in use, or at least automatically cap `maxReplicas` to 1 when using a single-writer volume.</comment>

<file context>
@@ -0,0 +1,32 @@
+    kind: Deployment
+    name: {{ include "vane.fullname" . }}
+  minReplicas: {{ .Values.autoscaling.minReplicas }}
+  maxReplicas: {{ .Values.autoscaling.maxReplicas }}
+  metrics:
+    {{- if .Values.autoscaling.targetCPUUtilizationPercentage }}
</file context>

resources:
{{- toYaml . | nindent 12 }}
{{- end }}
volumeMounts:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: volumeMounts: and volumes: are always emitted, but their list items are conditional. When a user disables both PVCs (set persistence.data.enabled=false and persistence.uploads.enabled=false) and provides no extras, both fields render as null arrays. Some strict Kubernetes validators or admission policies reject null list values, which can break CI or deployment pipelines. The adjacent env: block already avoids this by wrapping the whole field in a conditional; applying the same guard to volumeMounts and volumes would make the chart more robust.

Wrap each list field in an or check (e.g., {{- if or .Values.persistence.data.enabled .Values.persistence.uploads.enabled .Values.extraVolumeMounts }}) so the header is only emitted when at least one item will follow.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At charts/vane/templates/deployment.yaml, line 90:

<comment>`volumeMounts:` and `volumes:` are always emitted, but their list items are conditional. When a user disables both PVCs (set `persistence.data.enabled=false` and `persistence.uploads.enabled=false`) and provides no extras, both fields render as null arrays. Some strict Kubernetes validators or admission policies reject null list values, which can break CI or deployment pipelines. The adjacent `env:` block already avoids this by wrapping the whole field in a conditional; applying the same guard to `volumeMounts` and `volumes` would make the chart more robust.

Wrap each list field in an `or` check (e.g., `{{- if or .Values.persistence.data.enabled .Values.persistence.uploads.enabled .Values.extraVolumeMounts }}`) so the header is only emitted when at least one item will follow.</comment>

<file context>
@@ -0,0 +1,127 @@
+          resources:
+            {{- toYaml . | nindent 12 }}
+          {{- end }}
+          volumeMounts:
+            {{- if .Values.persistence.data.enabled }}
+            - name: data
</file context>

Comment on lines +21 to +23
annotations:
{{- with .Values.podAnnotations }}
{{- toYaml . | nindent 8 }}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: When .Values.podAnnotations is unset, the template renders metadata.annotations as null instead of omitting the key. Strict schema validators and policy engines may reject null for object-typed fields like annotations. It is safer to wrap the entire annotations: block in the with so the key is only emitted when annotations actually exist.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At charts/vane/templates/deployment.yaml, line 21:

<comment>When `.Values.podAnnotations` is unset, the template renders `metadata.annotations` as `null` instead of omitting the key. Strict schema validators and policy engines may reject `null` for object-typed fields like `annotations`. It is safer to wrap the entire `annotations:` block in the `with` so the key is only emitted when annotations actually exist.</comment>

<file context>
@@ -0,0 +1,127 @@
+      {{- include "vane.selectorLabels" . | nindent 6 }}
+  template:
+    metadata:
+      annotations:
+        {{- with .Values.podAnnotations }}
+        {{- toYaml . | nindent 8 }}
</file context>
Suggested change
annotations:
{{- with .Values.podAnnotations }}
{{- toYaml . | nindent 8 }}
{{- with .Values.podAnnotations }}
annotations:
{{- toYaml . | nindent 8 }}
{{- end }}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant