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
@@ -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
|
||||
Reference in new issue
Block a user