searxng: new chart (1.0.0+up2026.8.22.9fea41204)
SearXNG as a stateless Deployment plus a settings ConfigMap, with the four CI steps a new chart needs: publish workflow, change-app-version choice list, ci/searxng/ fixtures and a renovate.json5 block. Three points where the chart is not a cyberchef with another name: * The image entrypoint writes settings.yml into /etc/searxng only when the file is missing. The chart always mounts one by subPath, so the copy and the random-secret sed never run and the container needs no write access there - which is what allows readOnlyRootFilesystem: true. * The signing key is a required OnePasswordItem, not a ConfigMap key. It signs the settings cookie and the image-proxy URLs, and the entrypoint's self-generating fallback cannot fire here, so without it the instance would run on the literal "ultrasecretkey" - which webapp.py refuses with exit 1. * All three probes use /healthz, the only path the bot detection lets through unconditionally. Measured: with the limiter active, "/" answers 429 while /healthz still answers 200. ingress.domain is read a second time outside the Ingress template, for SEARXNG_BASE_URL, and carries its own required() there - so it stays mandatory with the Ingress switched off. The Renovate versioning regex deviates from the plan on the issue and matches the commit hash WITHOUT capturing it. Renovate parses a "build" group with Number.parseInt, so a hash becomes the digits leading it; measured on renovate 41.173.1 and 44.48.0, the planned form reports an image pushed earlier the same day as an update to a newer one. Reasoning in the block and in README.
This commit is contained in:
18 files changed
+1388
-5
No files matched your search
@@ -35,6 +35,7 @@ on:
|
||||
- minecraft-router
|
||||
- minecraft-server
|
||||
- rss-bridge
|
||||
- searxng
|
||||
# freesailarr is deliberately absent: none of its nine images takes
|
||||
# its tag from appVersion (they are all pinned in its values.yaml),
|
||||
# so this workflow would rewrite appVersion and republish the chart
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
name: Publish SearXNG Helm Chart
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- searxng/**
|
||||
jobs:
|
||||
publish-searxng:
|
||||
name: Publish SearXNG Helm Chart
|
||||
uses: ./.github/workflows/publish-helm-chart.yml
|
||||
with:
|
||||
chart-name: 'searxng'
|
||||
secrets: inherit
|
||||
@@ -22,6 +22,7 @@ Collection of some self-made helm-charts. Each top-level directory is a standalo
|
||||
| `rss-bridge` | RSS-Bridge | Deployment + whitelist ConfigMap; `ingress.hosts` is a required list of host + optional `tlsSecretName`, so several hosts can keep their own existing certificates (documented deviation from the one-TLS-secret rule, see its `values.yaml`) |
|
||||
| `samba` | Samba | StatefulSet, users secret + config ConfigMap |
|
||||
| `satisfactory` | Satisfactory game server | StatefulSet, no ingress |
|
||||
| `searxng` | SearXNG | Stateless Deployment + settings ConfigMap (`use_default_settings: true` plus a free-form `settings.extra` passthrough); the signing key is a required `OnePasswordItem`, an external Valkey an optional second one. `ingress.domain` is read twice — Ingress host and `SEARXNG_BASE_URL` — so it stays required with the Ingress switched off |
|
||||
| `teamspeak-server` | TeamSpeak server | StatefulSet, no ingress |
|
||||
| `template-chart` | — | Scaffold for new charts (placeholder `xxx`), never published |
|
||||
| `urlaubsverwaltung` | Urlaubsverwaltung | Deployment, DB + OIDC secrets |
|
||||
@@ -73,7 +74,9 @@ What Renovate keeps current:
|
||||
| GitHub Actions | Built-in `github-actions` manager |
|
||||
| Tool versions pinned as workflow env vars (`TRIVY_VERSION`) | `# renovate: datasource=… depName=…` comment above the line |
|
||||
|
||||
Each chart renders its main image as `<repo>:{{ .Chart.AppVersion }}` — repository in the template, version in `Chart.yaml`. Renovate cannot join two files, so the mapping lives in `renovate.json5`, one block per chart. It is deliberately *not* a `# renovate:` comment inside each `Chart.yaml`: that would touch all chart directories and force a version bump plus a publish per chart for a pure comment change. Charts whose tags carry a flavour or date suffix (`apache-tika`, `minecraft-server`, `rss-bridge`, `teamspeak-server`) override `versioning` in their block; the reasoning sits next to each one. `freesailarr` has no block on purpose — see the images exception above; a block would either duplicate the `helm-values` updates or rewrite an `appVersion` that selects no image.
|
||||
Each chart renders its main image as `<repo>:{{ .Chart.AppVersion }}` — repository in the template, version in `Chart.yaml`. Renovate cannot join two files, so the mapping lives in `renovate.json5`, one block per chart. It is deliberately *not* a `# renovate:` comment inside each `Chart.yaml`: that would touch all chart directories and force a version bump plus a publish per chart for a pure comment change. Charts whose tags carry a flavour or date suffix (`apache-tika`, `minecraft-server`, `rss-bridge`, `searxng`, `teamspeak-server`) override `versioning` in their block; the reasoning sits next to each one.
|
||||
|
||||
One rule those overrides share, learned from `searxng` and worth knowing before writing the next one: **a capture group is a claim about how the value is compared.** Renovate's regex versioning parses `major`, `minor`, `patch`, `build` and `revision` with `Number.parseInt` (`modules/versioning/regex/index.js`, `_parse`), so a non-numeric commit hash captured as `build` becomes whatever digits lead it — `9fea41204` is `9`, `777ba8fa4` is `777`, and a hash starting with a letter is `NaN`. Measured on renovate 41.173.1 and 44.48.0, that makes Renovate offer an image pushed *earlier the same day* as an update to a newer one, unattended under the patch/minor automerge rule. Parts of a tag that carry no ordering belong **outside** every group: the anchored pattern still requires them to be there, they simply take no part in the comparison. `prerelease` is not the way out either — it sorts correctly but marks every tag `isStable === false`, which hands the whole chart to `ignoreUnstable`. `freesailarr` has no block on purpose — see the images exception above; a block would either duplicate the `helm-values` updates or rewrite an `appVersion` that selects no image.
|
||||
|
||||
A consequence of pinning every tag in `values.yaml`: `ci/renovate-bump-chart.sh` sees an unchanged `appVersion` and bumps the chart **patch**. That holds for all nine images alike — none of them is special — and it is the intended outcome, since `appVersion` here is the chart's own marker rather than a tracked upstream version.
|
||||
|
||||
@@ -253,6 +256,6 @@ Reference implementation: `template-chart/values.schema.json` and `template-char
|
||||
|
||||
- `icon:` is mandatory in every `Chart.yaml`. **Documented exception — freesailarr, the only chart without one**: the rule everywhere else is "the icon is this chart's one upstream's logo", and a chart bundling nine independently versioned projects has no such upstream — picking one of the nine would imply a "main app" the stack deliberately does not have. That is the whole criterion, and it applies to no other chart in the repo: a single-upstream chart without an icon is a missing icon, not an exception. The omission costs an INFO line in `helm lint --strict` (`icon is recommended`), which still exits 0.
|
||||
- `helm lint --strict` must pass without findings.
|
||||
- `NOTES.txt` for every chart is planned (#43); seven charts ship one so far (`apache-tika`, `cyberchef`, `freesailarr`, `gotenberg`, `homeassistant`, `patchmon`, `rss-bridge`).
|
||||
- `NOTES.txt` for every chart is planned (#43); eight charts ship one so far (`apache-tika`, `cyberchef`, `freesailarr`, `gotenberg`, `homeassistant`, `patchmon`, `rss-bridge`, `searxng`).
|
||||
- CI: every installable chart has fixtures under `ci/<chart>/` (`test-values.yaml`, optionally `fixtures.yaml`/`fixtures.env.yaml`/`settings.env`) or a justified entry in `ci/excluded-charts.txt` — the install-test fails if both are missing.
|
||||
- Charts have no dependencies (`charts/` directories are empty).
|
||||
@@ -0,0 +1,15 @@
|
||||
# Fixtures for the searxng install-test: the one Secret the 1Password operator
|
||||
# would otherwise materialize (release name: ci).
|
||||
#
|
||||
# The value is a throwaway string, and this chart genuinely needs nothing more
|
||||
# than that - unlike freesailarr, whose fixtures need a real credential to get
|
||||
# past a VPN handshake. What it does need is a value that is NOT the upstream
|
||||
# placeholder "ultrasecretkey": SearXNG's webapp.py init() exits 1 on that, so
|
||||
# an empty or careless fixture would produce a crash loop rather than a
|
||||
# passing install-test.
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: ci-searxng-secret
|
||||
stringData:
|
||||
secret-key: searxng-ci-secret-key-0123456789abcdef0123456789abcdef
|
||||
@@ -0,0 +1,32 @@
|
||||
# searxng: stateless, one secret and no infrastructure to stand up. The
|
||||
# Ingress just has to apply - there is no ingress-nginx/cert-manager in the CI
|
||||
# cluster.
|
||||
#
|
||||
# The install runs the DEFAULT path (bot detection off, no Valkey), which is
|
||||
# what a real installation of this chart runs. The optional Valkey branch is
|
||||
# rendered but not installed here: switching the limiter on without a reachable
|
||||
# Valkey produces a Ready pod that logs an error per request, so it would make
|
||||
# the install-test slower without making it stricter.
|
||||
ingress:
|
||||
domain: searxng.ci.local
|
||||
clusterIssuer: ci
|
||||
|
||||
# The dummy 1Password Secret is created in fixtures.yaml; the operator does not
|
||||
# run in CI, so the OnePasswordItem CR stays unfulfilled and only the stub CRD
|
||||
# has to accept it.
|
||||
secrets:
|
||||
searxngSecretRef: op://ci/searxng/dummy
|
||||
|
||||
# Deliberately not the chart defaults. The install-test is the only place that
|
||||
# proves the ConfigMap is more than a fixed file: instanceName exercises the
|
||||
# first-class keys, and "extra" exercises the merge over them - including a
|
||||
# nested map that has to be joined rather than replaced (search.safe_search
|
||||
# from above must survive next to search.default_lang from here).
|
||||
settings:
|
||||
instanceName: "SearXNG CI"
|
||||
safeSearch: 1
|
||||
extra:
|
||||
search:
|
||||
default_lang: "en"
|
||||
ui:
|
||||
default_theme: simple
|
||||
+44
-3
@@ -21,7 +21,7 @@
|
||||
timezone: "Europe/Berlin",
|
||||
labels: ["dependencies"],
|
||||
|
||||
// 20 charts plus the workflows: without a cap the first run after a quiet
|
||||
// 21 charts plus the workflows: without a cap the first run after a quiet
|
||||
// week opens a wall of PRs, and every chart PR spins up a k3d install-test.
|
||||
prConcurrentLimit: 10,
|
||||
prHourlyLimit: 4,
|
||||
@@ -174,8 +174,8 @@
|
||||
// its own manager that hard-codes the repository and reads `appVersion:`.
|
||||
//
|
||||
// Deliberately *not* solved with a `# renovate:` comment inside each
|
||||
// Chart.yaml: that would touch all 20 chart directories and force 20
|
||||
// version bumps plus 20 publishes for a pure comment change. Keeping the
|
||||
// Chart.yaml: that would touch all 21 chart directories and force 21
|
||||
// version bumps plus 21 publishes for a pure comment change. Keeping the
|
||||
// mapping here leaves the charts untouched.
|
||||
//
|
||||
// When adding a chart, add a block here — see README "Creating a new chart".
|
||||
@@ -300,6 +300,47 @@
|
||||
depTypeTemplate: "app-version",
|
||||
versioningTemplate: "regex:^(?<major>\\d{4})-(?<minor>\\d{2})-(?<patch>\\d{2})$",
|
||||
},
|
||||
{
|
||||
description: [
|
||||
"searxng: every tag is <date>-<commit> (2026.8.22-9fea41204) - no semver line at all,",
|
||||
"and 'latest' moves daily. The date carries the ordering; the commit hash is matched",
|
||||
"but deliberately NOT captured.",
|
||||
],
|
||||
customType: "regex",
|
||||
managerFilePatterns: ["searxng/Chart.yaml"],
|
||||
matchStrings: ["(?m)^appVersion:\\s*\"?(?<currentValue>[^\"\\s]+)\"?"],
|
||||
depNameTemplate: "docker.io/searxng/searxng",
|
||||
datasourceTemplate: "docker",
|
||||
depTypeTemplate: "app-version",
|
||||
// The hash sits outside every capture group on purpose, and that is the
|
||||
// whole point of this line. Renovate's regex versioning reads a "build"
|
||||
// group NUMERICALLY - regex/index.js _parse() does
|
||||
// release.push(Number.parseInt(build)) - so a commit hash becomes
|
||||
// whatever digits happen to lead it: parseInt("9fea41204") is 9,
|
||||
// parseInt("777ba8fa4") is 777, and any hash starting with a letter
|
||||
// (6 of the 25 tags live today) is NaN.
|
||||
//
|
||||
// Verified against the real API on renovate 41.173.1 and 44.48.0, with
|
||||
// currentValue 2026.8.22-9fea41204:
|
||||
// with (?<build>[0-9a-f]+):
|
||||
// isGreaterThan("2026.8.22-777ba8fa4", current) -> true
|
||||
// without it (this line):
|
||||
// isGreaterThan("2026.8.22-777ba8fa4", current) -> false, equals -> true
|
||||
// 777ba8fa4 was pushed at 03:06 and 9fea41204 at 05:38 the SAME day, so
|
||||
// the build group makes Renovate offer the older image as an update -
|
||||
// a downgrade, unattended, because date updates land in the patch/minor
|
||||
// automerge rule. With NaN the pair is neither equal nor ordered at all.
|
||||
//
|
||||
// Uncaptured, the hash still has to be THERE (the pattern is anchored),
|
||||
// it just takes no part in the comparison: two builds of one day are
|
||||
// equal, so no sideways update is proposed, and a newer date wins
|
||||
// whatever the hash looks like.
|
||||
//
|
||||
// "prerelease" instead of "build" was measured too and rejected: it
|
||||
// sorts correctly but marks every single tag isStable === false, which
|
||||
// hands the whole chart to ignoreUnstable.
|
||||
versioningTemplate: "regex:^(?<major>\\d{4})\\.(?<minor>\\d{1,2})\\.(?<patch>\\d{1,2})-[0-9a-f]+$",
|
||||
},
|
||||
{
|
||||
customType: "regex",
|
||||
managerFilePatterns: ["samba/Chart.yaml"],
|
||||
|
||||
@@ -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/
|
||||
@@ -0,0 +1,16 @@
|
||||
apiVersion: v2
|
||||
name: searxng
|
||||
description: A Helm chart to deploy SearXNG on Kubernetes
|
||||
icon: https://raw.githubusercontent.com/searxng/searxng/master/client/simple/src/brand/searxng.svg
|
||||
|
||||
type: application
|
||||
|
||||
# SearXNG publishes no semver line at all: every image tag is <date>-<commit>
|
||||
# (2026.8.22-9fea41204) and "latest" moves daily. appVersion is therefore the
|
||||
# exact tag, and the +up suffix normalizes the dash to a dot as the versioning
|
||||
# scheme requires - the same shape rss-bridge and minecraft-server use.
|
||||
version: 1.0.0+up2026.8.22.9fea41204
|
||||
appVersion: "2026.8.22-9fea41204"
|
||||
|
||||
maintainers:
|
||||
- name: jklein
|
||||
@@ -0,0 +1,89 @@
|
||||
{{- $replicas := include "searxng.replicas" (dict "scaleDown" .Values.scaleDown "replicas" .Values.searxng.replicas) -}}
|
||||
{{- $settings := fromYaml (include "searxng.effectiveSettings" .Values.settings) -}}
|
||||
SearXNG {{ .Chart.AppVersion }} — release {{ .Release.Name }} in namespace {{ .Release.Namespace }}.
|
||||
|
||||
{{- if .Values.ingress.enabled }}
|
||||
|
||||
ACCESS
|
||||
https://{{ .Values.ingress.domain }}
|
||||
|
||||
The certificate is requested by cert-manager through the "{{ .Values.ingress.clusterIssuer }}"
|
||||
cluster issuer. Until it has been issued the browser shows a warning; follow it with
|
||||
|
||||
kubectl -n {{ .Release.Namespace }} get certificate tls-{{ .Release.Name }}-{{ .Chart.Name }}-ingress
|
||||
{{- else }}
|
||||
|
||||
ACCESS — NO INGRESS
|
||||
ingress.enabled is false, so this release has no Ingress and no certificate.
|
||||
ingress.domain is still required: it builds SEARXNG_BASE_URL, which SearXNG
|
||||
puts into its OpenSearch description, into the signed image-proxy URLs and
|
||||
into every absolute link. Reach the instance inside the cluster with
|
||||
|
||||
kubectl -n {{ .Release.Namespace }} port-forward svc/{{ .Release.Name }}-{{ .Chart.Name }}-service 8080:80
|
||||
|
||||
and open http://localhost:8080. Note that links on the page will still point
|
||||
at https://{{ .Values.ingress.domain }}/.
|
||||
{{- end }}
|
||||
{{ if eq $replicas "0" }}
|
||||
SCALED TO ZERO
|
||||
{{ if .Values.scaleDown }}scaleDown: true{{ else }}searxng.replicas: 0{{ end }} — the deployment runs with replicas: 0, so the endpoint
|
||||
above does not answer until you set it back and upgrade again. Service{{ if .Values.ingress.enabled }} and
|
||||
Ingress stay in place, so the certificate is not re-issued.{{ else }} stays in
|
||||
place.{{ end }}
|
||||
{{ end }}
|
||||
CONFIGURATION
|
||||
Instance name: {{ $settings.instanceName }}
|
||||
settings.yml is rendered from the "settings" values into
|
||||
{{ .Release.Name }}-{{ .Chart.Name }}-configmap and mounted at /etc/searxng/settings.yml.
|
||||
It starts with use_default_settings: true and names only the deviations, so
|
||||
every engine the image ships stays available and upstream engine changes
|
||||
arrive with the app version instead of with a chart release.
|
||||
|
||||
Anything the chart does not model first-class goes into settings.extra in
|
||||
SearXNG's own key names; it is merged over the generated block. Changing any
|
||||
of it rolls the pods — the deployment hashes the ConfigMap, because a subPath
|
||||
mount would otherwise never reach the running container.
|
||||
|
||||
WHAT THE GREEN CHECKMARK DOES AND DOES NOT MEAN
|
||||
All three probes hit /healthz, the only path SearXNG's bot detection lets
|
||||
through unconditionally. Its handler returns a fixed "OK". So a Ready pod
|
||||
means the worker answers — not that a search works, not that any engine is
|
||||
reachable, and not that a login placed in front of the Ingress is up (probes
|
||||
go straight to the pod and never pass the Ingress).
|
||||
{{- if not $settings.limiter }}
|
||||
|
||||
BOT DETECTION IS OFF
|
||||
settings.limiter is false and no Valkey is configured. The instance relies on
|
||||
whatever sits in front of it — put authentication there via
|
||||
ingress.annotations if it must not be open to everyone.
|
||||
{{- else if not .Values.secrets.valkeySecretRef }}
|
||||
|
||||
BOT DETECTION IS ON WITHOUT A VALKEY
|
||||
settings.limiter is true but secrets.valkeySecretRef is unset. The limiter
|
||||
needs a Valkey to keep its counters, so it does nothing at all: SearXNG logs
|
||||
|
||||
ERROR:searx.limiter: The limiter requires Valkey, please consult the
|
||||
documentation: https://docs.searxng.org/admin/searx.limiter.html
|
||||
|
||||
once at worker start, the pod becomes Ready, and every request is served
|
||||
unlimited. This release is therefore NOT rate limited, and the only trace of
|
||||
that is one line at the top of the log. Set secrets.valkeySecretRef, or set
|
||||
settings.limiter back to false.
|
||||
{{- end }}
|
||||
|
||||
ROTATING THE SIGNING KEY
|
||||
secrets.searxngSecretRef carries the key that signs the settings cookie and
|
||||
the image-proxy URLs. Pointing it at a DIFFERENT 1Password item rolls the
|
||||
pods; changing the value INSIDE the same item does not — Helm never sees that
|
||||
content. After such a rotation:
|
||||
|
||||
kubectl -n {{ .Release.Namespace }} rollout restart deployment/{{ .Release.Name }}-{{ .Chart.Name }}-deployment
|
||||
|
||||
A new key invalidates every user's saved preferences cookie, which is a
|
||||
logout-shaped event for the people using it.
|
||||
|
||||
NOTHING TO BACK UP
|
||||
The pod holds no state worth keeping: settings.yml comes from the ConfigMap,
|
||||
the signing key from the Secret, and the SQLite caches under /tmp plus
|
||||
/var/cache/searxng are emptyDirs that refill on their own. App-version
|
||||
upgrades and rollbacks therefore carry no data risk.
|
||||
@@ -0,0 +1,349 @@
|
||||
{{/*
|
||||
Render the body of a container "resources:" block from a values dict of the form
|
||||
|
||||
resources:
|
||||
limitFactor: 3 # optional, default 3
|
||||
requests: { cpu, memory, ephemeral-storage / ephemeralStorage, ... }
|
||||
limits: { ... } # optional, every entry optional
|
||||
|
||||
Rules (variant B, see issue #19):
|
||||
- requests are rendered as given (camelCase "ephemeralStorage" is normalized
|
||||
to the Kubernetes key "ephemeral-storage").
|
||||
- an explicitly set limit always wins.
|
||||
- a missing memory / ephemeral-storage limit is computed as
|
||||
limitFactor x the corresponding request.
|
||||
- a missing cpu limit is NOT set at all (the kubelet then treats CPU as
|
||||
unlimited; CPU is compressible, so this cannot kill the workload).
|
||||
|
||||
Usage (the values path to the resources dict varies per chart):
|
||||
resources:
|
||||
{{- include "searxng.resources" .Values.<app>.resources | nindent 12 }}
|
||||
|
||||
NOTE: charts in this repo deliberately have no dependencies, so this helper is
|
||||
duplicated into every chart's templates/_helpers.tpl with the chart's name as
|
||||
define prefix. template-chart holds the reference implementation.
|
||||
*/}}
|
||||
{{- define "searxng.resources" -}}
|
||||
{{- $resources := . | default dict -}}
|
||||
{{- $factor := 3.0 -}}
|
||||
{{- if not (kindIs "invalid" $resources.limitFactor) -}}
|
||||
{{- $factor = float64 $resources.limitFactor -}}
|
||||
{{- end -}}
|
||||
{{- $requests := dict -}}
|
||||
{{- range $key, $value := ($resources.requests | default dict) -}}
|
||||
{{- if not (kindIs "invalid" $value) -}}
|
||||
{{- $_ := set $requests (include "searxng.resourceKey" $key) $value -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
{{- $limits := dict -}}
|
||||
{{- range $key, $value := ($resources.limits | default dict) -}}
|
||||
{{- if not (kindIs "invalid" $value) -}}
|
||||
{{- $_ := set $limits (include "searxng.resourceKey" $key) $value -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
{{- range $key, $request := $requests -}}
|
||||
{{- if and (ne $key "cpu") (not (hasKey $limits $key)) -}}
|
||||
{{- $_ := set $limits $key (include "searxng.multiplyQuantity" (dict "quantity" $request "factor" $factor)) -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
{{- $out := dict -}}
|
||||
{{- with $requests }}{{- $_ := set $out "requests" . }}{{- end -}}
|
||||
{{- with $limits }}{{- $_ := set $out "limits" . }}{{- end -}}
|
||||
{{- toYaml $out -}}
|
||||
{{- end -}}
|
||||
|
||||
{{/*
|
||||
Normalize a resource name from values to its Kubernetes key.
|
||||
*/}}
|
||||
{{- define "searxng.resourceKey" -}}
|
||||
{{- if eq . "ephemeralStorage" -}}ephemeral-storage{{- else -}}{{ . }}{{- end -}}
|
||||
{{- end -}}
|
||||
|
||||
{{/*
|
||||
Multiply a Kubernetes quantity by a factor, keeping its suffix.
|
||||
Input: dict "quantity" <string|number> "factor" <float64>.
|
||||
Handles plain numbers ("2", "0.5", 1073741824), decimal ("1.5Gi"), binary
|
||||
("512Mi", "1Gi", "10Ki") and milli/SI suffixes ("100m", "500M"). Integer
|
||||
results are printed without a decimal point.
|
||||
*/}}
|
||||
{{- define "searxng.multiplyQuantity" -}}
|
||||
{{- $quantity := printf "%v" .quantity -}}
|
||||
{{- $number := regexFind "^[0-9]+(\\.[0-9]+)?" $quantity -}}
|
||||
{{- if not $number -}}
|
||||
{{- fail (printf "cannot parse resource quantity %q" $quantity) -}}
|
||||
{{- end -}}
|
||||
{{- $suffix := trimPrefix $number $quantity -}}
|
||||
{{- if not (regexMatch "^(m|k|M|G|T|P|E|Ki|Mi|Gi|Ti|Pi|Ei)?$" $suffix) -}}
|
||||
{{- fail (printf "unsupported resource quantity suffix %q in %q" $suffix $quantity) -}}
|
||||
{{- end -}}
|
||||
{{- $product := mulf (float64 $number) .factor -}}
|
||||
{{- if eq $product (floor $product) -}}
|
||||
{{- printf "%d%s" (int64 $product) $suffix -}}
|
||||
{{- else -}}
|
||||
{{- printf "%v%s" $product $suffix -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
|
||||
{{/*
|
||||
Render the replica count for a Deployment/StatefulSet.
|
||||
|
||||
Input: dict
|
||||
"scaleDown" the chart-global .Values.scaleDown flag (may be nil)
|
||||
"replicas" the workload's replicas value (may be nil)
|
||||
|
||||
Rules (issue #25):
|
||||
- scaleDown: true takes precedence and always yields 0.
|
||||
- otherwise an explicitly set replicas value is used as given, including an
|
||||
explicit 0 — Helm's "default" would swallow 0, hence the explicit
|
||||
kindIs "invalid" nil check.
|
||||
- a missing replicas value defaults to 1.
|
||||
|
||||
Usage (the values path to the workload's replicas varies per chart):
|
||||
replicas: {{ include "searxng.replicas" (dict "scaleDown" .Values.scaleDown "replicas" .Values.<app>.replicas) }}
|
||||
|
||||
NOTE: like the resources helper above, this is duplicated into every chart's
|
||||
templates/_helpers.tpl with the chart's name as define prefix. template-chart
|
||||
holds the reference implementation.
|
||||
*/}}
|
||||
{{- define "searxng.replicas" -}}
|
||||
{{- if .scaleDown -}}
|
||||
0
|
||||
{{- else if not (kindIs "invalid" .replicas) -}}
|
||||
{{- .replicas -}}
|
||||
{{- else -}}
|
||||
1
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
|
||||
{{/*
|
||||
Merge a user-supplied values sub-tree onto a dict of chart defaults so that a
|
||||
null / empty override falls back to the defaults instead of erasing them.
|
||||
|
||||
Input: dict "defaults" <dict> "given" <dict|nil> "path" <string, optional
|
||||
values path used in error messages>. Output: YAML of the merged dict (consume
|
||||
it with fromYaml).
|
||||
|
||||
Why this exists (issues #61 and #99): Helm merges values maps recursively, but
|
||||
an override key written without children — e.g. a leftover
|
||||
|
||||
securityContext:
|
||||
|
||||
after deleting its last remaining sub-key — is YAML null. Where the chart
|
||||
defines a default, Helm treats that null as a deletion of the default; the
|
||||
template then renders "securityContext: null": no readOnlyRootFilesystem, no
|
||||
dropped capabilities, no allowPrivilegeEscalation: false. The workload starts
|
||||
and reports healthy while being unhardened, so nothing points at the mistake.
|
||||
The same shape erases a probe (it disappears) or the resources block (no
|
||||
requests, no computed limits). This helper defines the opposite semantics: an
|
||||
empty block means "chart defaults apply".
|
||||
|
||||
Rules:
|
||||
- a nil "given" is treated as an empty dict (all defaults apply).
|
||||
- a key whose override value is null keeps the default (nulls never delete).
|
||||
- a key present in both as a map is merged recursively, so a partially
|
||||
filled block only overrides the keys it actually sets.
|
||||
- a scalar override for a key the defaults define as a map is rejected with
|
||||
a message naming the values path, instead of failing later with a template
|
||||
field error.
|
||||
- any other set value wins as given, including an explicit false or 0 —
|
||||
Helm's "default" and sprig's "mergeOverwrite" would both swallow those
|
||||
zero values, hence the explicit kindIs "invalid" nil check.
|
||||
- keys only present in the override are passed through unchanged.
|
||||
|
||||
NOTE: like the helpers above, this is duplicated into every chart's
|
||||
templates/_helpers.tpl with the chart's name as define prefix, because charts
|
||||
in this repo deliberately have no dependencies. template-chart holds the
|
||||
reference implementation.
|
||||
*/}}
|
||||
{{- define "searxng.defaultedDict" -}}
|
||||
{{- $defaults := .defaults | default dict -}}
|
||||
{{- $given := .given -}}
|
||||
{{- $path := .path | default "value" -}}
|
||||
{{- if kindIs "invalid" $given -}}
|
||||
{{- $given = dict -}}
|
||||
{{- end -}}
|
||||
{{- if not (kindIs "map" $given) -}}
|
||||
{{- fail (printf "%s must be a mapping or null, got %s (%v)" $path (kindOf $given) $given) -}}
|
||||
{{- end -}}
|
||||
{{- $out := deepCopy $defaults -}}
|
||||
{{- range $key, $value := $given -}}
|
||||
{{- if not (kindIs "invalid" $value) -}}
|
||||
{{- $default := index $defaults $key -}}
|
||||
{{- if and (kindIs "map" $value) (kindIs "map" $default) -}}
|
||||
{{- $_ := set $out $key (fromYaml (include "searxng.defaultedDict" (dict "defaults" $default "given" $value "path" (printf "%s.%s" $path $key)))) -}}
|
||||
{{- else if kindIs "map" $default -}}
|
||||
{{- fail (printf "%s.%s must be a mapping or null, got %s (%v)" $path $key (kindOf $value) $value) -}}
|
||||
{{- else -}}
|
||||
{{- $_ := set $out $key $value -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
{{- toYaml $out -}}
|
||||
{{- end -}}
|
||||
|
||||
{{/*
|
||||
Pick one sub-block out of a values block that may itself be null.
|
||||
|
||||
Input: dict "block" <dict|nil> "key" <string> "path" <string>.
|
||||
Returns YAML of dict "value" <the sub-block, may be nil>, after rejecting a
|
||||
non-mapping block with a message that names the values path. Needed because
|
||||
".Values.<app>.securityContext" can itself be erased, and indexing into nil
|
||||
inside a template is not an error but silently yields nothing.
|
||||
*/}}
|
||||
{{- define "searxng.subBlock" -}}
|
||||
{{- $block := .block -}}
|
||||
{{- if kindIs "invalid" $block -}}
|
||||
{{- $block = dict -}}
|
||||
{{- end -}}
|
||||
{{- if not (kindIs "map" $block) -}}
|
||||
{{- fail (printf "%s must be a mapping or null, got %s (%v)" .path (kindOf $block) $block) -}}
|
||||
{{- end -}}
|
||||
{{- toYaml (dict "value" (index $block .key)) -}}
|
||||
{{- end -}}
|
||||
|
||||
{{/*
|
||||
The effective security contexts, probes and resources (issue #99).
|
||||
|
||||
The defaults below MUST stay in sync with the corresponding blocks in
|
||||
values.yaml, which stays the documented, user-facing place for them; this copy
|
||||
is only the fallback for the case where an override has erased the block.
|
||||
|
||||
Two rules that these fallbacks exist to demonstrate (README, "Chart style
|
||||
guidelines"):
|
||||
|
||||
- Reproduce THIS chart's defaults, not the repo standard. Where a chart
|
||||
deviates on purpose (a root-starting image, a non-default limitFactor, a
|
||||
pinned uid), the fallback must reproduce the deviation. Restoring the
|
||||
"clean" standard here is not tidying, it is an outage - in one chart it
|
||||
would move the runtime uid from 1000 to 911 and make existing volume data
|
||||
inaccessible.
|
||||
- Carry the probe's CONTEXT, not just its timings. A probe default without
|
||||
its port name, Host header or exec command is worse than useless: in one
|
||||
chart the probe vanished entirely, in another it answered 403 and the pod
|
||||
never became Ready.
|
||||
*/}}
|
||||
{{- define "searxng.podSecurityContext" -}}
|
||||
{{- $given := (fromYaml (include "searxng.subBlock" (dict "block" . "key" "pod" "path" "searxng.securityContext"))).value -}}
|
||||
{{/* 977 is the image's searxng user, deliberately not the repo's generic
|
||||
20000/1000: the image creates /etc/searxng and /var/cache/searxng owned
|
||||
977:977 and the entrypoint checks that ownership by name. A "cleaned up"
|
||||
standard uid here would leave the config mount unreadable. */}}
|
||||
{{- $defaults := dict
|
||||
"runAsNonRoot" true
|
||||
"runAsUser" 977
|
||||
"runAsGroup" 977
|
||||
"fsGroup" 977
|
||||
"fsGroupChangePolicy" "Always"
|
||||
"seccompProfile" (dict "type" "RuntimeDefault") -}}
|
||||
{{- include "searxng.defaultedDict" (dict "defaults" $defaults "given" $given "path" "searxng.securityContext.pod") -}}
|
||||
{{- end -}}
|
||||
|
||||
{{- define "searxng.containerSecurityContext" -}}
|
||||
{{- $given := (fromYaml (include "searxng.subBlock" (dict "block" . "key" "container" "path" "searxng.securityContext"))).value -}}
|
||||
{{- $defaults := dict
|
||||
"allowPrivilegeEscalation" false
|
||||
"readOnlyRootFilesystem" true
|
||||
"runAsNonRoot" true
|
||||
"capabilities" (dict "drop" (list "ALL") "add" (list)) -}}
|
||||
{{- include "searxng.defaultedDict" (dict "defaults" $defaults "given" $given "path" "searxng.securityContext.container") -}}
|
||||
{{- end -}}
|
||||
|
||||
{{/* All three probe fallbacks use /healthz, not "/", and that path is part of
|
||||
the context this fallback has to carry (guideline 5). /healthz is the only
|
||||
path SearXNG's bot detection lets through unconditionally
|
||||
(searx/limiter.py, filter_request), and its handler returns a fixed "OK"
|
||||
without touching an engine or the cache - so liveness hangs on no external
|
||||
dependency either. The port NAME matters just as much: the container port
|
||||
is named "http" and a fallback without it would address nothing. */}}
|
||||
{{- define "searxng.livenessProbe" -}}
|
||||
{{- $defaults := dict
|
||||
"httpGet" (dict "path" "/healthz" "port" "http")
|
||||
"periodSeconds" 10
|
||||
"timeoutSeconds" 5
|
||||
"failureThreshold" 3 -}}
|
||||
{{- include "searxng.defaultedDict" (dict "defaults" $defaults "given" . "path" "searxng.livenessProbe") -}}
|
||||
{{- end -}}
|
||||
|
||||
{{- define "searxng.readinessProbe" -}}
|
||||
{{- $defaults := dict
|
||||
"httpGet" (dict "path" "/healthz" "port" "http")
|
||||
"periodSeconds" 10
|
||||
"timeoutSeconds" 5
|
||||
"failureThreshold" 3 -}}
|
||||
{{- include "searxng.defaultedDict" (dict "defaults" $defaults "given" . "path" "searxng.readinessProbe") -}}
|
||||
{{- end -}}
|
||||
|
||||
{{- define "searxng.startupProbe" -}}
|
||||
{{- $defaults := dict
|
||||
"httpGet" (dict "path" "/healthz" "port" "http")
|
||||
"periodSeconds" 5
|
||||
"timeoutSeconds" 5
|
||||
"failureThreshold" 30 -}}
|
||||
{{- include "searxng.defaultedDict" (dict "defaults" $defaults "given" . "path" "searxng.startupProbe") -}}
|
||||
{{- end -}}
|
||||
|
||||
{{/*
|
||||
The effective resources block, fed into the resources helper above so that an
|
||||
erased "resources:" still yields the chart's requests and computed limits
|
||||
instead of a container without any resource accounting.
|
||||
*/}}
|
||||
{{- define "searxng.effectiveResources" -}}
|
||||
{{- $defaults := dict
|
||||
"limitFactor" 3
|
||||
"requests" (dict "cpu" 0.1 "memory" "256Mi" "ephemeral-storage" "64Mi") -}}
|
||||
{{- $merged := fromYaml (include "searxng.defaultedDict" (dict "defaults" $defaults "given" . "path" "searxng.resources")) -}}
|
||||
{{- include "searxng.resources" $merged -}}
|
||||
{{- end -}}
|
||||
|
||||
{{/*
|
||||
The effective "settings" block, i.e. this chart's user-facing configuration
|
||||
keys with the same null-safe semantics as the blocks above: an erased
|
||||
"settings:" means "chart defaults apply" rather than an instance whose name,
|
||||
safe-search level and image proxy silently become empty.
|
||||
|
||||
The defaults MUST stay in sync with the settings block in values.yaml.
|
||||
*/}}
|
||||
{{- define "searxng.effectiveSettings" -}}
|
||||
{{- $defaults := dict
|
||||
"instanceName" "SearXNG"
|
||||
"safeSearch" 0
|
||||
"autocomplete" "duckduckgo"
|
||||
"imageProxy" true
|
||||
"limiter" false
|
||||
"publicInstance" false
|
||||
"extra" dict -}}
|
||||
{{- include "searxng.defaultedDict" (dict "defaults" $defaults "given" . "path" "settings") -}}
|
||||
{{- end -}}
|
||||
|
||||
{{/*
|
||||
The body of /etc/searxng/settings.yml.
|
||||
|
||||
Two things this is doing, both deliberate:
|
||||
|
||||
- It starts from use_default_settings: true and names ONLY the deviations,
|
||||
so the ~3500-line settings.yml of the image stays the source of truth for
|
||||
engines and every upstream change to them arrives without a chart release.
|
||||
- "settings.extra" is merged OVER the block the first-class keys build, with
|
||||
defaultedDict rather than sprig's mergeOverwrite. That difference is
|
||||
load-bearing: mergeOverwrite drops zero values from the source, so an
|
||||
"extra" entry setting some flag to false or 0 would be silently ignored -
|
||||
a config value that looks set and does nothing, which is exactly the class
|
||||
of bug the schema work (#68) exists to remove.
|
||||
|
||||
server.secret_key is deliberately absent: it arrives as SEARXNG_SECRET from
|
||||
the 1Password Secret (settings_defaults.py lets the environment override the
|
||||
file), so the signing key never lands in a ConfigMap.
|
||||
*/}}
|
||||
{{- define "searxng.settingsYaml" -}}
|
||||
{{- $settings := fromYaml (include "searxng.effectiveSettings" .Values.settings) -}}
|
||||
{{- $base := dict
|
||||
"use_default_settings" true
|
||||
"general" (dict "instance_name" $settings.instanceName)
|
||||
"search" (dict "safe_search" $settings.safeSearch "autocomplete" $settings.autocomplete)
|
||||
"server" (dict
|
||||
"limiter" $settings.limiter
|
||||
"public_instance" $settings.publicInstance
|
||||
"image_proxy" $settings.imageProxy) -}}
|
||||
{{- $merged := fromYaml (include "searxng.defaultedDict" (dict "defaults" $base "given" $settings.extra "path" "settings.extra")) -}}
|
||||
{{- toYaml $merged -}}
|
||||
{{- end -}}
|
||||
@@ -0,0 +1,11 @@
|
||||
{{- if .Values.secrets.valkeySecretRef }}
|
||||
apiVersion: onepassword.com/v1
|
||||
kind: OnePasswordItem
|
||||
metadata:
|
||||
name: {{ .Release.Name }}-{{ .Chart.Name }}-valkey-secret
|
||||
spec:
|
||||
# Optional, and only meaningful with settings.limiter: true. The item must
|
||||
# carry a "valkey-url" field pointing at an EXTERNAL Valkey/Redis; the chart
|
||||
# deploys none of its own, because charts in this repo have no dependencies.
|
||||
itemPath: {{ .Values.secrets.valkeySecretRef }}
|
||||
{{- end }}
|
||||
@@ -0,0 +1,14 @@
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: "{{ .Release.Name }}-{{ .Chart.Name }}-configmap"
|
||||
data:
|
||||
# Mounted at /etc/searxng/settings.yml via subPath. That path is not free
|
||||
# choice: the image exports __SEARXNG_SETTINGS_PATH=/etc/searxng/settings.yml
|
||||
# and its entrypoint creates the file from its own template only when it does
|
||||
# NOT exist. Because this mount is always there, the entrypoint finds the
|
||||
# file, skips both the copy and the sed that would otherwise invent a random
|
||||
# secret_key, and never needs to write into /etc/searxng - which is what lets
|
||||
# the container run with a read-only root filesystem.
|
||||
settings.yml: |
|
||||
{{- include "searxng.settingsYaml" . | nindent 4 }}
|
||||
@@ -0,0 +1,169 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: {{ .Release.Name }}-{{ .Chart.Name }}-deployment
|
||||
labels:
|
||||
app: {{ .Release.Name }}-{{ .Chart.Name }}-deployment
|
||||
spec:
|
||||
replicas: {{ include "searxng.replicas" (dict "scaleDown" .Values.scaleDown "replicas" .Values.searxng.replicas) }}
|
||||
selector:
|
||||
matchLabels:
|
||||
app: {{ .Release.Name }}-{{ .Chart.Name }}-deployment
|
||||
template:
|
||||
metadata:
|
||||
name: {{ .Release.Name }}-{{ .Chart.Name }}-deployment
|
||||
labels:
|
||||
app: {{ .Release.Name }}-{{ .Chart.Name }}-deployment
|
||||
annotations:
|
||||
# Hash of the rendered ConfigMap: a changed settings.yml changes the
|
||||
# pod template and thus rolls the pods. Without it the change would
|
||||
# never reach the running container - settings.yml is a subPath mount,
|
||||
# which the kubelet never refreshes, and SearXNG reads its
|
||||
# configuration once at start.
|
||||
checksum/config: {{ include (print $.Template.BasePath "/searxng.configmap.yaml") . | sha256sum }}
|
||||
# Hashes over the rendered OnePasswordItems. Separately named on
|
||||
# purpose: they only promise that the config SOURCE changed. Repointing
|
||||
# a *SecretRef at another vault item rolls the pods; rotating the value
|
||||
# inside the same item does not, because Helm never sees the content
|
||||
# (the operator fills the Secret in-cluster) and the values are consumed
|
||||
# as environment variables, which Kubernetes never refreshes in a
|
||||
# running container. After such a rotation, restart by hand.
|
||||
checksum/secret-ref: {{ include (print $.Template.BasePath "/searxng.secret.yaml") . | sha256sum }}
|
||||
checksum/secret-ref-valkey: {{ include (print $.Template.BasePath "/searxng-valkey.secret.yaml") . | sha256sum }}
|
||||
spec:
|
||||
automountServiceAccountToken: false
|
||||
securityContext:
|
||||
{{- include "searxng.podSecurityContext" .Values.searxng.securityContext | nindent 8 }}
|
||||
containers:
|
||||
- name: {{ .Release.Name }}-{{ .Chart.Name }}-deployment
|
||||
# Registry written out in full on purpose: an unqualified reference
|
||||
# would put "searxng" in the registry position, which says nothing
|
||||
# about where the image comes from and is reported as an untrusted
|
||||
# registry (AVD-KSV-0125). See ci/trivy/data/ksv0125.yaml.
|
||||
image: "docker.io/searxng/searxng:{{ .Chart.AppVersion }}"
|
||||
imagePullPolicy: IfNotPresent
|
||||
env:
|
||||
# The signing key for the settings cookie and the image-proxy URLs.
|
||||
# Set by the chart rather than left to searxng.env, because SearXNG
|
||||
# exits 1 when server.secret_key is still the upstream placeholder
|
||||
# and the image's self-generating fallback cannot fire here (it
|
||||
# only runs when settings.yml is missing, and this chart always
|
||||
# mounts one).
|
||||
- name: SEARXNG_SECRET
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: "{{ .Release.Name }}-{{ .Chart.Name }}-secret"
|
||||
key: "secret-key"
|
||||
# server.base_url. This is the SECOND read of ingress.domain, well
|
||||
# outside the Ingress template, so it carries its own required()
|
||||
# (README, "Values that must not lie", point 1). Without it, an
|
||||
# install with ingress.enabled: false would render base_url as
|
||||
# "https:///" - the pod would go green while the OpenSearch
|
||||
# description, every signed image-proxy URL and every absolute link
|
||||
# pointed at a broken address.
|
||||
- name: SEARXNG_BASE_URL
|
||||
value: "https://{{ required "A Domain/Host must be provided (ingress.domain), it is used for SEARXNG_BASE_URL even when the Ingress is disabled" .Values.ingress.domain }}/"
|
||||
{{- if .Values.secrets.valkeySecretRef }}
|
||||
# Only rendered when an external Valkey/Redis is configured. The
|
||||
# limiter is what needs it; see settings.limiter in values.yaml.
|
||||
- name: SEARXNG_VALKEY_URL
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: "{{ .Release.Name }}-{{ .Chart.Name }}-valkey-secret"
|
||||
key: "valkey-url"
|
||||
{{- end }}
|
||||
{{- range $value := .Values.searxng.env }}
|
||||
{{- if $value.value }}
|
||||
- name: {{ $value.name | quote }}
|
||||
# quote is required, not cosmetic: a value like "8080" or "true"
|
||||
# renders unquoted as a YAML number/bool, and the API server then
|
||||
# rejects the whole manifest with "cannot unmarshal number into
|
||||
# Go struct field EnvVar.value of type string". Reachable with
|
||||
# the very first entry someone adds here - SEARXNG_LIMITER and
|
||||
# GRANIAN_PORT are both of that shape.
|
||||
value: {{ tpl $value.value $ | quote }}
|
||||
{{- else if $value.valueFrom }}
|
||||
{{- if $value.valueFrom.secretKeyRef }}
|
||||
- name: {{ $value.name | quote }}
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: {{ tpl $value.valueFrom.secretKeyRef.name $ | quote }}
|
||||
key: {{ tpl $value.valueFrom.secretKeyRef.key $ | quote }}
|
||||
{{- else if $value.valueFrom.configMapKeyRef }}
|
||||
- name: {{ $value.name | quote }}
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
name: {{ tpl $value.valueFrom.configMapKeyRef.name $ | quote }}
|
||||
key: {{ tpl $value.valueFrom.configMapKeyRef.key $ | quote }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
ports:
|
||||
# 8080 is the image's own GRANIAN_PORT and its EXPOSE. It is not a
|
||||
# value, because a value here could lie: changing it would move the
|
||||
# containerPort and the probes while granian kept listening on
|
||||
# 8080. The way to move the port is to set both GRANIAN_PORT (or
|
||||
# its SEARXNG_PORT alias, which the entrypoint maps onto it) in
|
||||
# searxng.env and this number - which means a chart change, on
|
||||
# purpose.
|
||||
#
|
||||
# The port NAME is load-bearing and must not be renamed: the
|
||||
# Service selects it by name and so do all three probes.
|
||||
- containerPort: 8080
|
||||
protocol: TCP
|
||||
name: http
|
||||
# Probes, resources and both security contexts go through the
|
||||
# null-safe helpers (#99): an erased block means "chart defaults
|
||||
# apply" instead of a vanished probe or an unhardened container.
|
||||
# Never wrap these in an "if" - a guard hides the erased case
|
||||
# completely instead of rendering a visible null.
|
||||
livenessProbe:
|
||||
{{- include "searxng.livenessProbe" .Values.searxng.livenessProbe | nindent 12 }}
|
||||
readinessProbe:
|
||||
{{- include "searxng.readinessProbe" .Values.searxng.readinessProbe | nindent 12 }}
|
||||
startupProbe:
|
||||
{{- include "searxng.startupProbe" .Values.searxng.startupProbe | nindent 12 }}
|
||||
resources:
|
||||
{{- include "searxng.effectiveResources" .Values.searxng.resources | nindent 12 }}
|
||||
securityContext:
|
||||
{{- include "searxng.containerSecurityContext" .Values.searxng.securityContext | nindent 12 }}
|
||||
volumeMounts:
|
||||
- name: settings
|
||||
mountPath: /etc/searxng/settings.yml
|
||||
subPath: settings.yml
|
||||
readOnly: true
|
||||
# The two writable paths below are what keeps
|
||||
# readOnlyRootFilesystem: true possible. They are image facts
|
||||
# rather than user configuration, so they are not values.
|
||||
- name: tmp
|
||||
# NOT optional, and not obvious: SearXNG keeps three SQLite
|
||||
# caches (data, engines, favicons) and searx/cache.py falls back
|
||||
# to tempfile.gettempdir() when no db_url is configured - i.e.
|
||||
# /tmp. webapp.py init() connects to two of them before the
|
||||
# first request, so a read-only /tmp is not a degraded cache, it
|
||||
# is a container that never starts.
|
||||
mountPath: /tmp
|
||||
- name: cache
|
||||
# The image's __SEARXNG_DATA_PATH. The entrypoint aborts with
|
||||
# exit 127 if the directory is missing, so it has to exist -
|
||||
# which it does even unmounted, since the image ships it. It is
|
||||
# mounted anyway and deliberately: nothing in this version writes
|
||||
# there, but it is the path the application declares as its data
|
||||
# directory, and the day a release starts using it a read-only
|
||||
# root filesystem would turn that into a crash loop.
|
||||
#
|
||||
# EXPECTED, not a fault: fsGroup leaves this emptyDir owned
|
||||
# root:977, so the entrypoint prints a multi-line "!!! WARNING
|
||||
# ... is not owned by searxng:searxng" and carries on. It cannot
|
||||
# chown as a non-root process and does not need to - the group
|
||||
# bit is enough.
|
||||
mountPath: /var/cache/searxng
|
||||
volumes:
|
||||
- name: settings
|
||||
configMap:
|
||||
name: "{{ .Release.Name }}-{{ .Chart.Name }}-configmap"
|
||||
- name: tmp
|
||||
emptyDir: {}
|
||||
- name: cache
|
||||
emptyDir: {}
|
||||
restartPolicy: Always
|
||||
@@ -0,0 +1,31 @@
|
||||
{{- if .Values.ingress.enabled }}
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: {{ .Release.Name }}-{{ .Chart.Name }}-ingress
|
||||
annotations:
|
||||
cert-manager.io/cluster-issuer: {{ required "A Cluster-Issuer must be provided (ingress.clusterIssuer)" .Values.ingress.clusterIssuer }}
|
||||
# Extra annotations are MERGED with the cert-manager annotation above, not
|
||||
# substituted for it. This is where forward-auth (Authentik and friends)
|
||||
# goes; see the example in values.yaml.
|
||||
{{- with .Values.ingress.annotations }}
|
||||
{{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
spec:
|
||||
ingressClassName: {{ .Values.ingress.className }}
|
||||
rules:
|
||||
- host: {{ required "A Domain/Host must be provided (ingress.domain)" .Values.ingress.domain }}
|
||||
http:
|
||||
paths:
|
||||
- backend:
|
||||
service:
|
||||
name: {{ .Release.Name }}-{{ .Chart.Name }}-service
|
||||
port:
|
||||
name: http
|
||||
path: /
|
||||
pathType: Prefix
|
||||
tls:
|
||||
- hosts:
|
||||
- {{ required "A Domain/Host must be provided (ingress.domain)" .Values.ingress.domain }}
|
||||
secretName: tls-{{ .Release.Name }}-{{ .Chart.Name }}-ingress
|
||||
{{- end }}
|
||||
@@ -0,0 +1,10 @@
|
||||
apiVersion: onepassword.com/v1
|
||||
kind: OnePasswordItem
|
||||
metadata:
|
||||
name: {{ .Release.Name }}-{{ .Chart.Name }}-secret
|
||||
spec:
|
||||
# The item must carry a "secret-key" field; it becomes SEARXNG_SECRET and
|
||||
# thereby server.secret_key, which signs the settings cookie and the
|
||||
# image-proxy URLs. Without it SearXNG exits 1 at start (webapp.py refuses
|
||||
# to run on the upstream placeholder), so the value is required.
|
||||
itemPath: {{ required "You must provide a searxng Secret Reference (secrets.searxngSecretRef)" .Values.secrets.searxngSecretRef }}
|
||||
@@ -0,0 +1,13 @@
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: {{ .Release.Name }}-{{ .Chart.Name }}-service
|
||||
spec:
|
||||
selector:
|
||||
app: {{ .Release.Name }}-{{ .Chart.Name }}-deployment
|
||||
ports:
|
||||
- protocol: TCP
|
||||
port: 80
|
||||
targetPort: http
|
||||
name: http
|
||||
type: ClusterIP
|
||||
@@ -0,0 +1,215 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft-07/schema#",
|
||||
"title": "searxng values",
|
||||
"description": "Values schema for the searxng chart (issue #68). additionalProperties is false on every chart-owned object level, so a key the chart does not read is rejected at render time instead of being silently ignored. Objects that go verbatim into the Kubernetes pod spec (probes, security contexts, global, free-form annotations) stay open, and so does settings.extra - its schema belongs to SearXNG, not to this chart. The schema ADDS to the required() calls in the templates, it does not replace them.",
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"global": {
|
||||
"description": "Helm's conventional cross-chart values. Unused by this chart, kept open so an umbrella install cannot break on it.",
|
||||
"type": "object"
|
||||
},
|
||||
"scaleDown": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"secrets": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"searxngSecretRef": {
|
||||
"description": "op:// item path carrying a \"secret-key\" field. Required by the template - SearXNG exits 1 when server.secret_key is still the upstream placeholder.",
|
||||
"type": ["string", "null"]
|
||||
},
|
||||
"valkeySecretRef": {
|
||||
"description": "op:// item path carrying a \"valkey-url\" field for an external Valkey/Redis. Optional; only meaningful with settings.limiter: true.",
|
||||
"type": ["string", "null"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
"description": "The chart-owned part of /etc/searxng/settings.yml. May be null / empty: the chart then falls back to its own defaults (issue #99) instead of rendering an instance with an empty name and no image proxy.",
|
||||
"type": ["object", "null"],
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"instanceName": {
|
||||
"description": "general.instance_name.",
|
||||
"type": ["string", "null"]
|
||||
},
|
||||
"safeSearch": {
|
||||
"description": "search.safe_search - 0 off, 1 moderate, 2 strict. Constrained because SearXNG accepts nothing else and a typo here would otherwise reach the app as a config error at start.",
|
||||
"type": ["integer", "null"],
|
||||
"enum": [0, 1, 2, null]
|
||||
},
|
||||
"autocomplete": {
|
||||
"description": "search.autocomplete - the suggestion backend, or the empty string to disable suggestions. Not enumerated: the list of backends is upstream's and grows.",
|
||||
"type": ["string", "null"]
|
||||
},
|
||||
"imageProxy": {
|
||||
"description": "server.image_proxy.",
|
||||
"type": ["boolean", "null"]
|
||||
},
|
||||
"limiter": {
|
||||
"description": "server.limiter - the bot detection. Needs an external Valkey (secrets.valkeySecretRef) to do anything.",
|
||||
"type": ["boolean", "null"]
|
||||
},
|
||||
"publicInstance": {
|
||||
"description": "server.public_instance.",
|
||||
"type": ["boolean", "null"]
|
||||
},
|
||||
"extra": {
|
||||
"description": "Free-form SearXNG settings, merged over the block the keys above build and rendered verbatim into settings.yml. Deliberately open: the schema here is SearXNG's, and restricting it would only reject keys the upstream has not invented yet.",
|
||||
"type": ["object", "null"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"searxng": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"replicas": {
|
||||
"type": "integer",
|
||||
"minimum": 0
|
||||
},
|
||||
"resources": {
|
||||
"$ref": "#/definitions/resources"
|
||||
},
|
||||
"livenessProbe": {
|
||||
"$ref": "#/definitions/probe"
|
||||
},
|
||||
"readinessProbe": {
|
||||
"$ref": "#/definitions/probe"
|
||||
},
|
||||
"startupProbe": {
|
||||
"$ref": "#/definitions/probe"
|
||||
},
|
||||
"securityContext": {
|
||||
"$ref": "#/definitions/securityContext"
|
||||
},
|
||||
"env": {
|
||||
"$ref": "#/definitions/env"
|
||||
}
|
||||
}
|
||||
},
|
||||
"ingress": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"enabled": {
|
||||
"description": "Gates the Ingress template only. ingress.domain is read a second time in the Deployment (SEARXNG_BASE_URL) and keeps its own required() there, so switching the Ingress off frees clusterIssuer but NOT domain.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"clusterIssuer": {
|
||||
"type": ["string", "null"]
|
||||
},
|
||||
"domain": {
|
||||
"type": ["string", "null"]
|
||||
},
|
||||
"className": {
|
||||
"type": ["string", "null"]
|
||||
},
|
||||
"annotations": {
|
||||
"description": "Extra Ingress annotations, merged with the cert-manager annotation the chart always sets. Free-form keys by nature, so this object stays open.",
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"definitions": {
|
||||
"resources": {
|
||||
"description": "May be null / empty: the chart then falls back to its default requests and computed limits (issue #99) instead of rendering a container without resource accounting.",
|
||||
"type": ["object", "null"],
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"limitFactor": {
|
||||
"type": "number",
|
||||
"exclusiveMinimum": 0
|
||||
},
|
||||
"requests": {
|
||||
"$ref": "#/definitions/resourceList"
|
||||
},
|
||||
"limits": {
|
||||
"$ref": "#/definitions/resourceList"
|
||||
}
|
||||
}
|
||||
},
|
||||
"resourceList": {
|
||||
"description": "Both spellings of the ephemeral-storage key are accepted because the resources helper normalizes camelCase to the Kubernetes key.",
|
||||
"type": ["object", "null"],
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"cpu": {
|
||||
"type": ["string", "number", "null"]
|
||||
},
|
||||
"memory": {
|
||||
"type": ["string", "number", "null"]
|
||||
},
|
||||
"ephemeral-storage": {
|
||||
"type": ["string", "number", "null"]
|
||||
},
|
||||
"ephemeralStorage": {
|
||||
"type": ["string", "number", "null"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"env": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["name"],
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"value": {
|
||||
"type": "string"
|
||||
},
|
||||
"valueFrom": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"secretKeyRef": {
|
||||
"$ref": "#/definitions/keyRef"
|
||||
},
|
||||
"configMapKeyRef": {
|
||||
"$ref": "#/definitions/keyRef"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"keyRef": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["name", "key"],
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"key": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"securityContext": {
|
||||
"description": "May be null / empty at any level: the chart then falls back to its hardening defaults (issue #99) instead of rendering an unhardened workload. Those fallbacks reproduce THIS chart's uid/gid 977, not the repo's generic numbers.",
|
||||
"type": ["object", "null"],
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"pod": {
|
||||
"description": "Merged onto the chart's pod hardening defaults and rendered as the pod's securityContext; kept open because the chart does not interpret the individual fields.",
|
||||
"type": ["object", "null"]
|
||||
},
|
||||
"container": {
|
||||
"description": "Merged onto the chart's container hardening defaults and rendered as the container's securityContext; kept open because the chart does not interpret the individual fields.",
|
||||
"type": ["object", "null"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"probe": {
|
||||
"description": "Merged onto the chart's probe defaults and rendered as a container probe; kept open because the chart does not interpret the individual fields. May be null / empty, which means the defaults apply (issue #99) rather than the probe disappearing. The chart's defaults carry the probe's context - the /healthz path and the \"http\" port name - not just its timings.",
|
||||
"type": ["object", "null"]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,336 @@
|
||||
# Scale the whole chart down without uninstalling it: scaleDown: true renders
|
||||
# every Deployment/StatefulSet with replicas: 0 (and suspends CronJobs) and
|
||||
# takes precedence over any per-workload replicas value. PVCs, Secrets and
|
||||
# Services stay in place; scaleDown: false brings the workloads back up.
|
||||
scaleDown: false
|
||||
|
||||
secrets:
|
||||
# op:// item path for the 1Password operator; the item must carry a
|
||||
# "secret-key" field. REQUIRED - the chart has no default and fails to
|
||||
# render without it.
|
||||
#
|
||||
# Why it is required rather than optional: server.secret_key signs the
|
||||
# settings cookie and the image-proxy URLs. SearXNG refuses to start when it
|
||||
# is still the upstream placeholder - searx/webapp.py init():
|
||||
#
|
||||
# if not app.debug and get_setting("server.secret_key") == 'ultrasecretkey':
|
||||
# logger.error("server.secret_key is not changed. ...")
|
||||
# sys.exit(1)
|
||||
#
|
||||
# The image's entrypoint has a self-generating fallback, but it only fires
|
||||
# when /etc/searxng/settings.yml is MISSING, and this chart always mounts one
|
||||
# from its ConfigMap - so the fallback can never run here. Without this value
|
||||
# the instance would inherit "ultrasecretkey" from the upstream defaults and
|
||||
# exit 1 in a crash loop.
|
||||
#
|
||||
# And it is a Secret, not a ConfigMap key, because a ConfigMap is the place
|
||||
# people look into without thinking twice: anyone with read access in the
|
||||
# namespace would see the key that signs every user's cookies.
|
||||
searxngSecretRef: null
|
||||
# op:// item path for an EXTERNAL Valkey/Redis, carrying a "valkey-url"
|
||||
# field (e.g. redis://valkey.example.svc:6379/0). Optional: leave it null
|
||||
# unless the bot detection below is switched on.
|
||||
#
|
||||
# Setting it renders a second OnePasswordItem and passes the URL to the
|
||||
# container as SEARXNG_VALKEY_URL. The chart ships NO Valkey of its own -
|
||||
# charts in this repo have no dependencies (README, guideline 13), so an
|
||||
# external connection through a secret is the same shape outline and
|
||||
# paperless-ngx use for their databases.
|
||||
valkeySecretRef: null
|
||||
#
|
||||
# Documented limitation (README, guideline 9): the CONTENT of these items is
|
||||
# invisible to Helm - the 1Password operator fills the Kubernetes Secret in
|
||||
# the cluster after rendering, so there is nothing to hash and no checksum
|
||||
# can cover it. Repointing either *SecretRef at a different item DOES roll
|
||||
# the pods (the Deployment hashes the rendered OnePasswordItem into
|
||||
# checksum/secret-ref), but rotating the value inside the same item does
|
||||
# not: both are consumed as environment variables, which Kubernetes never
|
||||
# refreshes in a running container. After such a rotation, restart by hand:
|
||||
#
|
||||
# kubectl -n <namespace> rollout restart deployment/<release>-searxng-deployment
|
||||
|
||||
# The contents of /etc/searxng/settings.yml, rendered into the ConfigMap.
|
||||
#
|
||||
# The file is deliberately minimal and starts with use_default_settings: true,
|
||||
# so it inherits the ~3500-line settings.yml the image ships and only names the
|
||||
# deviations. That is what keeps every upstream engine change flowing in
|
||||
# without a chart release, and it is why this chart does NOT map SearXNG's
|
||||
# hundreds of engine options onto first-class values: such a mapping would be
|
||||
# out of date with the first upstream change and would then lie about what the
|
||||
# instance actually runs.
|
||||
#
|
||||
# Everything else goes through "extra" below, which is merged over the block
|
||||
# the four keys build. Changing anything here rolls the pods - the Deployment
|
||||
# carries a checksum over the rendered ConfigMap. Without it the change would
|
||||
# never reach the running container: settings.yml is a subPath mount, which
|
||||
# the kubelet never refreshes, and SearXNG reads its configuration once at
|
||||
# start.
|
||||
settings:
|
||||
# general.instance_name - the name shown in the UI and in the OpenSearch
|
||||
# description. Upstream default: "SearXNG".
|
||||
instanceName: "SearXNG"
|
||||
# search.safe_search - 0 = off, 1 = moderate, 2 = strict.
|
||||
safeSearch: 0
|
||||
# search.autocomplete - the suggestion backend, e.g. "duckduckgo", "google",
|
||||
# "startpage", "wikipedia". The empty string disables suggestions.
|
||||
# Upstream default: "duckduckgo".
|
||||
autocomplete: "duckduckgo"
|
||||
# server.image_proxy - route image thumbnails through the instance instead
|
||||
# of letting the browser fetch them from the origin. true is the value the
|
||||
# upstream container template sets (the library default is false).
|
||||
imageProxy: true
|
||||
# server.limiter - SearXNG's bot detection.
|
||||
#
|
||||
# Off by default, and that is a decision rather than an omission: the limiter
|
||||
# needs a Valkey/Redis to keep its counters, and this chart deploys none. The
|
||||
# instance is meant to sit behind authentication at the Ingress (see
|
||||
# ingress.annotations below), so nothing that the bot detection would look at
|
||||
# ever reaches the application - switching it on would only add a second
|
||||
# place to debug.
|
||||
#
|
||||
# If you do switch it on, set secrets.valkeySecretRef as well. What happens
|
||||
# WITHOUT it was measured rather than guessed, and the answer is the
|
||||
# uncomfortable middle one - a quiet fall-back, not a loud failure:
|
||||
#
|
||||
# * one ERROR at worker start, "searx.limiter: The limiter requires
|
||||
# Valkey, please consult the documentation: ...", and nothing after that
|
||||
# * the pod becomes Ready and stays Ready
|
||||
# * every request is served normally - /, /search and /healthz all
|
||||
# answered 200, with no rate limiting in effect at all
|
||||
#
|
||||
# So a limiter switched on without a Valkey is not protecting anything, and
|
||||
# the only sign of it is a single log line at start. With a reachable Valkey
|
||||
# the same setup does limit: measured against a throwaway Valkey, "/" then
|
||||
# answers 429 while /healthz still answers 200 - which is the whole reason
|
||||
# the probes use /healthz.
|
||||
limiter: false
|
||||
# server.public_instance - extra protections for instances listed publicly.
|
||||
# Off for the same reason as the limiter: it depends on the limiter's
|
||||
# infrastructure and this instance is not public.
|
||||
publicInstance: false
|
||||
# Free-form passthrough, merged OVER the block the keys above build and
|
||||
# rendered verbatim into settings.yml. This is where everything the chart
|
||||
# does not model first-class goes, in SearXNG's own key names:
|
||||
#
|
||||
# extra:
|
||||
# search:
|
||||
# default_lang: "de"
|
||||
# formats: ["html", "json"]
|
||||
# ui:
|
||||
# default_theme: simple
|
||||
# engines:
|
||||
# - name: wikipedia
|
||||
# disabled: false
|
||||
#
|
||||
# Merge semantics come from the same null-safe helper the security contexts
|
||||
# use: nested maps are merged key by key, an explicit false or 0 wins, and a
|
||||
# null keeps the value from the block above (nulls never delete). A scalar
|
||||
# written where the base has a map is rejected by name instead of failing
|
||||
# later with a template error.
|
||||
#
|
||||
# This object stays open in values.schema.json on purpose: its schema is
|
||||
# SearXNG's, not this chart's, and restricting it would only reject keys the
|
||||
# upstream has not invented yet.
|
||||
extra: {}
|
||||
|
||||
searxng:
|
||||
# Replica count for this workload; 0 is allowed and scales it down.
|
||||
# The chart-global scaleDown flag takes precedence.
|
||||
#
|
||||
# Note before raising this above 1: SearXNG keeps its settings in a signed
|
||||
# cookie rather than server-side, so several replicas do not need a shared
|
||||
# session store - but they DO need the same server.secret_key, which they
|
||||
# have here (one Secret for the whole release). The per-pod SQLite caches
|
||||
# under /tmp are independent and merely warm up separately.
|
||||
replicas: 1
|
||||
# uid/gid 977 is the "searxng" user of the image, and the number is not
|
||||
# interchangeable: /etc/searxng and /var/cache/searxng are created 0755 and
|
||||
# owned 977:977 in the image (searxng/base Dockerfile), and the entrypoint
|
||||
# compares the ownership of both against searxng:searxng by name.
|
||||
#
|
||||
# EXPECTED LOG LINE, not a fault: the emptyDir mounted at
|
||||
# /var/cache/searxng below is owned root:977 (fsGroup only sets the group),
|
||||
# so the entrypoint prints a multi-line
|
||||
#
|
||||
# !!! WARNING
|
||||
# !!! "/var/cache/searxng" directory is not owned by "searxng:searxng"
|
||||
#
|
||||
# and then continues. It cannot fix the ownership because the process is not
|
||||
# root - which is the point - and it does not need to: the directory is
|
||||
# group-writable through fsGroup. The warning is written down here because
|
||||
# otherwise the next person who finds it in the log spends an hour on it.
|
||||
securityContext:
|
||||
pod:
|
||||
runAsNonRoot: true
|
||||
runAsUser: 977
|
||||
runAsGroup: 977
|
||||
fsGroup: 977
|
||||
fsGroupChangePolicy: Always
|
||||
# RuntimeDefault applies the container runtime's default seccomp filter,
|
||||
# which blocks the syscalls a normal workload never makes. It sits in the
|
||||
# pod context on purpose: from here it covers every container in the pod
|
||||
# including init containers, while a container-level seccompProfile would
|
||||
# have to be repeated for each one (and silently missed on the next one
|
||||
# added). A container that genuinely needs a different filter can still
|
||||
# override it under securityContext.container, which wins over this value.
|
||||
seccompProfile:
|
||||
type: RuntimeDefault
|
||||
container:
|
||||
allowPrivilegeEscalation: false
|
||||
readOnlyRootFilesystem: true
|
||||
runAsNonRoot: true
|
||||
capabilities:
|
||||
drop:
|
||||
- ALL
|
||||
# All three probes hit /healthz, and that is a deliberate choice in both
|
||||
# directions.
|
||||
#
|
||||
# For it: /healthz is the ONLY path SearXNG's bot detection lets through
|
||||
# unconditionally - searx/limiter.py filter_request() returns early on
|
||||
# exactly this path before any of its checks run. A probe on "/" would risk
|
||||
# collecting a 429 the moment settings.limiter is switched on; the pod would
|
||||
# go NotReady and the symptom would point at the application instead of at
|
||||
# the bot detection. It also costs nothing: the handler is
|
||||
# `return Response('OK', mimetype='text/plain')`, no engines, no cache, no
|
||||
# network - so liveness carries no external dependency (README, "Values that
|
||||
# must not lie", point 3).
|
||||
#
|
||||
# Against it, stated plainly: a fixed string proves that the WSGI worker
|
||||
# answers. It does not prove that a search works, that any engine is
|
||||
# reachable, or that this chart's settings.yml was the file that got loaded.
|
||||
# A green pod here is a running process, nothing more.
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /healthz
|
||||
port: http
|
||||
periodSeconds: 10
|
||||
timeoutSeconds: 5
|
||||
failureThreshold: 3
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /healthz
|
||||
port: http
|
||||
periodSeconds: 10
|
||||
timeoutSeconds: 5
|
||||
failureThreshold: 3
|
||||
# The startup probe gates liveness/readiness, so those need no
|
||||
# initialDelaySeconds. Startup budget = 5s * 30 = 150s. Measured in k3d
|
||||
# (k3s v1.35.5) with the image already on the node: 6 s from container start
|
||||
# to Ready. The budget is therefore about 25x a healthy start - the margin is
|
||||
# for a cold node under load, not for a slow application.
|
||||
startupProbe:
|
||||
httpGet:
|
||||
path: /healthz
|
||||
port: http
|
||||
periodSeconds: 5
|
||||
timeoutSeconds: 5
|
||||
failureThreshold: 30
|
||||
# Extra environment variables, appended to the ones the chart always sets
|
||||
# (SEARXNG_SECRET, SEARXNG_BASE_URL and - when secrets.valkeySecretRef is
|
||||
# set - SEARXNG_VALKEY_URL). Values are rendered through tpl; the value,
|
||||
# secretKeyRef and configMapKeyRef forms are supported.
|
||||
#
|
||||
# NOTE that the list below is a DEFAULT, not an addition to one: an
|
||||
# installation that sets searxng.env replaces it wholesale and has to repeat
|
||||
# the GRANIAN_HOST entry.
|
||||
#
|
||||
# Also note the precedence: SearXNG's own SEARXNG_* variables
|
||||
# (settings_defaults.py) override the ConfigMap, so an env entry named e.g.
|
||||
# SEARXNG_LIMITER wins over settings.limiter above. That is a way out if you
|
||||
# need it, and a trap if you forget it.
|
||||
env:
|
||||
# The bind address of granian, the WSGI server the image runs since it
|
||||
# dropped uwsgi. It is a value rather than a template constant because it
|
||||
# is the one setting that could plausibly need correcting per cluster, and
|
||||
# the correction should be a values change, not a chart release.
|
||||
#
|
||||
# "::" is the image's own default and it was MEASURED here, not assumed.
|
||||
# In k3d (k3s v1.35.5) the pod is single-stack IPv4 - one podIP, and eth0
|
||||
# carries nothing but a link-local fe80:: address - and granian still logs
|
||||
#
|
||||
# [INFO] Listening at: http://:::8080
|
||||
#
|
||||
# while the kubelet's IPv4 probes are answered and the pod goes Ready. The
|
||||
# v4-mapped socket therefore works without a routable IPv6 address. On a
|
||||
# cluster where it does not (an IPv6-disabled kernel: granian would fail to
|
||||
# bind and the container would never answer), set this to "0.0.0.0".
|
||||
#
|
||||
# Note that there is no SEARXNG_* alias for the bind address: the
|
||||
# entrypoint maps only SEARXNG_PORT onto GRANIAN_PORT, and the
|
||||
# SEARXNG_BIND_ADDRESS setting is a leftover from the uwsgi era that
|
||||
# granian never reads.
|
||||
- name: GRANIAN_HOST
|
||||
value: "::"
|
||||
resources:
|
||||
# Limits are optional: memory and ephemeral-storage limits default to
|
||||
# limitFactor x the request, an explicitly set limit always wins, and a
|
||||
# CPU limit is only set when configured explicitly under "limits".
|
||||
limitFactor: 3
|
||||
requests:
|
||||
cpu: 0.1
|
||||
# Measured on the shipped image in k3d, cgroup memory.current of the
|
||||
# container: 152 MiB once the worker is up and answering, peaking at
|
||||
# 157 MiB across a real search. 256Mi covers that with headroom; the
|
||||
# computed limit (3x = 768Mi) leaves room for the per-request engine
|
||||
# work, which fans out to many upstreams at once.
|
||||
memory: 256Mi
|
||||
# SearXNG writes its SQLite caches into /tmp - searx/cache.py falls back
|
||||
# to tempfile.gettempdir() when no db_url is configured. Measured after a
|
||||
# real search: 356 KiB total (DATA_CACHE and ENGINES_CACHE, each a small
|
||||
# db plus its -shm/-wal files), and the application expires entries
|
||||
# itself. 64Mi is roughly 180x the measured footprint while still
|
||||
# bounding a runaway; the computed limit is 192Mi.
|
||||
ephemeralStorage: 64Mi
|
||||
# No CPU limit by default (repo standard): CPU is compressible, so a limit
|
||||
# only throttles.
|
||||
|
||||
ingress:
|
||||
# Gates the whole Ingress template.
|
||||
#
|
||||
# Checked in both directions per guideline 1, and the answer here is NOT the
|
||||
# comfortable one: ingress.domain is read a SECOND time, outside this
|
||||
# template, to build SEARXNG_BASE_URL in the Deployment. It therefore keeps
|
||||
# its own required() there and stays mandatory even with the Ingress turned
|
||||
# off; only clusterIssuer becomes unnecessary.
|
||||
#
|
||||
# Why that read cannot be dropped: server.base_url is what SearXNG puts into
|
||||
# the OpenSearch description, into the image-proxy URLs it signs and into
|
||||
# every absolute link it generates. Left empty, the instance renders and the
|
||||
# pod goes green while browsers get sent to a broken address - exactly the
|
||||
# silent failure the guideline exists for.
|
||||
enabled: true
|
||||
clusterIssuer: null
|
||||
# The hostname this instance is served under. Required (see above), with no
|
||||
# default - hostnames are installation data and have no business in a chart.
|
||||
domain: null
|
||||
className: nginx
|
||||
# Extra annotations for the Ingress; merged with the cert-manager
|
||||
# cluster-issuer annotation the chart always sets.
|
||||
#
|
||||
# This is where authentication in front of SearXNG belongs, and it needs no
|
||||
# chart change: forward-auth is entirely an ingress-nginx matter. With
|
||||
# Authentik, for example:
|
||||
#
|
||||
# annotations:
|
||||
# nginx.ingress.kubernetes.io/auth-url: |-
|
||||
# http://ak-outpost-authentik-embedded-outpost.authentik.svc.cluster.local:9000/outpost.goauthentik.io/auth/nginx
|
||||
# nginx.ingress.kubernetes.io/auth-signin: |-
|
||||
# https://searxng.example.com/outpost.goauthentik.io/start?rd=$escaped_request_uri
|
||||
# nginx.ingress.kubernetes.io/auth-response-headers: |-
|
||||
# Set-Cookie,X-authentik-username,X-authentik-groups,X-authentik-email,X-authentik-name,X-authentik-uid
|
||||
# nginx.ingress.kubernetes.io/auth-snippet: |
|
||||
# proxy_set_header X-Forwarded-Host $http_host;
|
||||
#
|
||||
# (example.com is a placeholder - substitute your own hostname.)
|
||||
#
|
||||
# Two things to know before relying on it:
|
||||
#
|
||||
# * The kubelet's probes go straight to the pod and never pass the
|
||||
# Ingress, so an authentication outpost that is down cannot turn this
|
||||
# release red. Good for availability - and it also means a green pod
|
||||
# says nothing about whether the login in front of it works.
|
||||
# * Anyone who adds this instance as a browser search engine sends their
|
||||
# queries through the same login. That is not a chart concern, but it is
|
||||
# the reason such a setup "suddenly stops working" after a session
|
||||
# expires.
|
||||
annotations: {}
|
||||
Reference in new issue
Block a user