From 4da61c2dc41b790074ceabc40c7d4228fa835fe4 Mon Sep 17 00:00:00 2001 From: Kumar Sukhani Date: Sat, 24 Oct 2026 10:46:42 +0200 Subject: [PATCH] Fix api key from valueFrom being ignored when rendered without cluster access Pass apiKey/readOnlyApiKey valueFrom to Qdrant as QDRANT__SERVICE__* env vars instead of copying the secret with lookup, which returns nothing under ArgoCD/helm template and left Qdrant without auth. Only create and mount the chart secret for string or generated keys; point ServiceMonitor at the user's secret. --- charts/qdrant/templates/_helpers.tpl | 41 +++++++++++-------- charts/qdrant/templates/secret.yaml | 2 +- charts/qdrant/templates/servicemonitor.yaml | 8 ++++ charts/qdrant/templates/statefulset.yaml | 14 +++++-- test/integration/external_api_key.bats | 5 +++ .../external_api_key_template.bats | 37 +++++++++++++++++ 6 files changed, 87 insertions(+), 20 deletions(-) create mode 100644 test/integration/external_api_key_template.bats diff --git a/charts/qdrant/templates/_helpers.tpl b/charts/qdrant/templates/_helpers.tpl index f45ce29..ba3e8db 100644 --- a/charts/qdrant/templates/_helpers.tpl +++ b/charts/qdrant/templates/_helpers.tpl @@ -73,21 +73,37 @@ Create the name of the service account to use {{- end }} {{- end }} +{{/* +Whether the api key is referenced from an existing secret via valueFrom +*/}} +{{- define "qdrant.apiKeyValueFrom" -}} +{{- if and (kindIs "map" .Values.apiKey) .Values.apiKey.valueFrom -}}true{{- end -}} +{{- end -}} + +{{/* +Whether the read-only api key is referenced from an existing secret via valueFrom +*/}} +{{- define "qdrant.readOnlyApiKeyValueFrom" -}} +{{- if and (kindIs "map" .Values.readOnlyApiKey) .Values.readOnlyApiKey.valueFrom -}}true{{- end -}} +{{- end -}} + +{{/* +Whether the chart creates its own -apikey secret, +i.e. at least one api key is set as a string or generated (true), not referenced via valueFrom +*/}} +{{- define "qdrant.createApiKeySecret" -}} +{{- if or (and .Values.apiKey (not (include "qdrant.apiKeyValueFrom" .))) (and .Values.readOnlyApiKey (not (include "qdrant.readOnlyApiKeyValueFrom" .))) -}}true{{- end -}} +{{- end -}} + {{/* Create secret +Api keys referenced via valueFrom are not part of it, they are passed to Qdrant as environment variables */}} {{- define "qdrant.secret" -}} {{- $readOnlyApiKey := false }} {{- $apiKey := false }} {{- if kindIs "map" .Values.apiKey -}} -{{- if .Values.apiKey.valueFrom -}} -{{- /* Retrieve the value from the secret as specified in valueFrom */ -}} -{{- $secretName := .Values.apiKey.valueFrom.secretKeyRef.name -}} -{{- $secretKey := .Values.apiKey.valueFrom.secretKeyRef.key -}} -{{- $secretObj := (lookup "v1" "Secret" .Release.Namespace $secretName) | default dict -}} -{{- $secretData := (get $secretObj "data") | default dict -}} -{{- $apiKey = (get $secretData $secretKey | b64dec) -}} -{{- end -}} +{{- /* valueFrom is passed to Qdrant as an environment variable, see statefulset.yaml */ -}} {{- else if .Values.apiKey | toJson | eq "true" -}} {{- /* Retrieve existing randomly generated api key or create a new one */ -}} {{- $secretObj := (lookup "v1" "Secret" .Release.Namespace (printf "%s-apikey" (include "qdrant.fullname" . ))) | default dict -}} @@ -97,14 +113,7 @@ Create secret {{- $apiKey = .Values.apiKey -}} {{- end -}} {{- if kindIs "map" .Values.readOnlyApiKey -}} -{{- if .Values.readOnlyApiKey.valueFrom -}} -{{- /* Retrieve the value from the secret as specified in valueFrom */ -}} -{{- $secretName := .Values.readOnlyApiKey.valueFrom.secretKeyRef.name -}} -{{- $secretKey := .Values.readOnlyApiKey.valueFrom.secretKeyRef.key -}} -{{- $secretObj := (lookup "v1" "Secret" .Release.Namespace $secretName) | default dict -}} -{{- $secretData := (get $secretObj "data") | default dict -}} -{{- $readOnlyApiKey = (get $secretData $secretKey | b64dec) -}} -{{- end -}} +{{- /* valueFrom is passed to Qdrant as an environment variable, see statefulset.yaml */ -}} {{- else if eq (.Values.readOnlyApiKey | toJson) "true" -}} {{- /* retrieve existing randomly generated api key or create new one */ -}} {{- $secretObj := (lookup "v1" "Secret" .Release.Namespace (printf "%s-apikey" (include "qdrant.fullname" . ))) | default dict -}} diff --git a/charts/qdrant/templates/secret.yaml b/charts/qdrant/templates/secret.yaml index d95db52..0e58f04 100644 --- a/charts/qdrant/templates/secret.yaml +++ b/charts/qdrant/templates/secret.yaml @@ -1,4 +1,4 @@ -{{- if or .Values.apiKey .Values.readOnlyApiKey }} +{{- if include "qdrant.createApiKeySecret" . }} apiVersion: v1 kind: Secret metadata: diff --git a/charts/qdrant/templates/servicemonitor.yaml b/charts/qdrant/templates/servicemonitor.yaml index 45af6b0..c76eb86 100644 --- a/charts/qdrant/templates/servicemonitor.yaml +++ b/charts/qdrant/templates/servicemonitor.yaml @@ -36,14 +36,22 @@ spec: authorization: type: Bearer credentials: +{{- if include "qdrant.readOnlyApiKeyValueFrom" . }} + {{- toYaml .Values.readOnlyApiKey.valueFrom.secretKeyRef | nindent 10 }} +{{- else }} name: {{ include "qdrant.fullname" . }}-apikey key: read-only-api-key +{{- end }} {{- else if .Values.apiKey }} authorization: type: Bearer credentials: +{{- if include "qdrant.apiKeyValueFrom" . }} + {{- toYaml .Values.apiKey.valueFrom.secretKeyRef | nindent 10 }} +{{- else }} name: {{ include "qdrant.fullname" . }}-apikey key: api-key +{{- end }} {{- end }} selector: matchLabels: diff --git a/charts/qdrant/templates/statefulset.yaml b/charts/qdrant/templates/statefulset.yaml index 0e87a0a..4df556c 100644 --- a/charts/qdrant/templates/statefulset.yaml +++ b/charts/qdrant/templates/statefulset.yaml @@ -27,7 +27,7 @@ spec: metadata: annotations: checksum/config: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }} - {{- if or .Values.apiKey .Values.readOnlyApiKey }} + {{- if include "qdrant.createApiKeySecret" . }} checksum/secret: {{ include (print $.Template.BasePath "/secret.yaml") . | sha256sum }} {{- end }} {{- with .Values.podAnnotations }} @@ -94,6 +94,14 @@ spec: env: - name: QDRANT_INIT_FILE_PATH value: /qdrant/init/.qdrant-initialized + {{- if include "qdrant.apiKeyValueFrom" . }} + - name: QDRANT__SERVICE__API_KEY + valueFrom: {{- toYaml .Values.apiKey.valueFrom | nindent 16 }} + {{- end }} + {{- if include "qdrant.readOnlyApiKeyValueFrom" . }} + - name: QDRANT__SERVICE__READ_ONLY_API_KEY + valueFrom: {{- toYaml .Values.readOnlyApiKey.valueFrom | nindent 16 }} + {{- end }} {{- range .Values.env }} - name: {{ .name }} {{- if .valueFrom }} @@ -199,7 +207,7 @@ spec: - name: qdrant-config mountPath: /qdrant/config/production.yaml subPath: production.yaml - {{- if or .Values.apiKey .Values.readOnlyApiKey }} + {{- if include "qdrant.createApiKeySecret" . }} - name: qdrant-secret mountPath: /qdrant/config/local.yaml subPath: local.yaml @@ -255,7 +263,7 @@ spec: {{- end }} - name: qdrant-init emptyDir: {} - {{- if or .Values.apiKey .Values.readOnlyApiKey }} + {{- if include "qdrant.createApiKeySecret" . }} - name: qdrant-secret secret: secretName: {{ include "qdrant.fullname" . }}-apikey diff --git a/test/integration/external_api_key.bats b/test/integration/external_api_key.bats index bc8f5f8..9798d87 100644 --- a/test/integration/external_api_key.bats +++ b/test/integration/external_api_key.bats @@ -14,4 +14,9 @@ setup_file() { run kubectl exec -n default curl -- curl -s http://qdrant.qdrant-helm-integration:6333/collections -H "api-key: test-read-only-api-key" --fail-with-body [ $status -eq 0 ] [[ "${output}" =~ .*\"status\":\"ok\".* ]] +} + +@test "external api key authentication fails with no key" { + run kubectl exec -n default curl -- curl -s -w " - %{response_code}" http://qdrant.qdrant-helm-integration:6333/collections + [ "${output}" = "Must provide an API key or an Authorization bearer token - 401" ] } \ No newline at end of file diff --git a/test/integration/external_api_key_template.bats b/test/integration/external_api_key_template.bats new file mode 100644 index 0000000..3fe5a9f --- /dev/null +++ b/test/integration/external_api_key_template.bats @@ -0,0 +1,37 @@ +setup_file() { + # Using a custom namespace to avoid side effects with other tests + kubectl create ns qdrant-external-apikey-template + kubectl -n qdrant-external-apikey-template create secret generic qdrant-external-apikey --from-literal=apiKey=test-api-key --from-literal=readOnlyApiKey=test-read-only-api-key + # Render without cluster access and apply the output, like ArgoCD and other GitOps tools do. + # Helm's lookup function returns nothing in this mode. + helm template qdrant charts/qdrant -n qdrant-external-apikey-template --skip-tests \ + --set apiKey.valueFrom.secretKeyRef.name=qdrant-external-apikey,apiKey.valueFrom.secretKeyRef.key=apiKey,readOnlyApiKey.valueFrom.secretKeyRef.name=qdrant-external-apikey,readOnlyApiKey.valueFrom.secretKeyRef.key=readOnlyApiKey \ + | kubectl apply -n qdrant-external-apikey-template -f - + kubectl rollout status statefulset qdrant -n qdrant-external-apikey-template --timeout 3m +} + +@test "external api key works when rendered with helm template" { + run kubectl exec -n default curl -- curl -s http://qdrant.qdrant-external-apikey-template:6333/collections -H "api-key: test-api-key" --fail-with-body + [ $status -eq 0 ] + [[ "${output}" =~ .*\"status\":\"ok\".* ]] +} + +@test "external read only api key works when rendered with helm template" { + run kubectl exec -n default curl -- curl -s http://qdrant.qdrant-external-apikey-template:6333/collections -H "api-key: test-read-only-api-key" --fail-with-body + [ $status -eq 0 ] + [[ "${output}" =~ .*\"status\":\"ok\".* ]] +} + +@test "external api key authentication fails with no key when rendered with helm template" { + run kubectl exec -n default curl -- curl -s -w " - %{response_code}" http://qdrant.qdrant-external-apikey-template:6333/collections + [ "${output}" = "Must provide an API key or an Authorization bearer token - 401" ] +} + +@test "external api key authentication fails with wrong key when rendered with helm template" { + run kubectl exec -n default curl -- curl -s -w " - %{response_code}" http://qdrant.qdrant-external-apikey-template:6333/collections -H 'api-key: invalid' + [ "${output}" = "Invalid API key or JWT - 401" ] +} + +teardown_file() { + kubectl delete ns qdrant-external-apikey-template +}