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