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.
170 lines
9.2 KiB
YAML
170 lines
9.2 KiB
YAML
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
|