diff --git a/charts/qdrant/README.md b/charts/qdrant/README.md index 9334e36..17a9660 100644 --- a/charts/qdrant/README.md +++ b/charts/qdrant/README.md @@ -68,7 +68,3 @@ Note: Make sure volume is on the same region and availability zone as where qdra Metrics are available through rest api (default port set to 6333) at `/metrics` Refer to [qdrant metrics configuration](https://qdrant.tech/documentation/telemetry/#metrics) for more information. - -### Enable rolling update on configuration change - -To enable rolling update on config map modification set `updateConfigurationOnChange` to true. diff --git a/charts/qdrant/templates/_helpers.tpl b/charts/qdrant/templates/_helpers.tpl index 9ea4417..821eedb 100644 --- a/charts/qdrant/templates/_helpers.tpl +++ b/charts/qdrant/templates/_helpers.tpl @@ -66,15 +66,33 @@ Create the name of the service account to use Create secret */}} {{- define "qdrant.secret" -}} +{{- $readOnlyApiKey := false }} +{{- $apiKey := false }} {{- if eq (.Values.apiKey | 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 -}} {{- $secretData := (get $secretObj "data") | default dict -}} -{{- $apiKey := (get $secretData "api-key" | b64dec) | default (randAlphaNum 32) -}} +{{- $apiKey = (get $secretData "api-key" | b64dec) | default (randAlphaNum 32) -}} +{{- else if .Values.apiKey -}} +{{- $apiKey = .Values.apiKey -}} +{{- end -}} +{{- 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 -}} +{{- $secretData := (get $secretObj "data") | default dict -}} +{{- $readOnlyApiKey = (get $secretData "read-only-api-key" | b64dec) | default (randAlphaNum 32) -}} +{{- else if .Values.readOnlyApiKey -}} +{{- $readOnlyApiKey = .Values.readOnlyApiKey -}} +{{- end -}} +{{- if and $apiKey $readOnlyApiKey -}} +api-key: {{ $apiKey | b64enc }} +read-only-api-key: {{ $readOnlyApiKey | b64enc }} +local.yaml: {{ printf "service:\n api_key: %s\n read_only_api_key: %s" $apiKey $readOnlyApiKey | b64enc }} +{{- else if $apiKey -}} api-key: {{ $apiKey | b64enc }} local.yaml: {{ printf "service:\n api_key: %s" $apiKey | b64enc }} -{{- else if .Values.apiKey -}} -api-key: {{ .Values.apiKey | b64enc }} -local.yaml: {{ printf "service:\n api_key: %s" .Values.apiKey | b64enc }} +{{- else if $readOnlyApiKey -}} +read-only-api-key: {{ $readOnlyApiKey | b64enc }} +local.yaml: {{ printf "service:\n read_only_api_key: %s" $readOnlyApiKey | b64enc }} {{- end -}} {{- end -}} \ No newline at end of file diff --git a/charts/qdrant/templates/secret.yaml b/charts/qdrant/templates/secret.yaml index 2c418b3..4c1d920 100644 --- a/charts/qdrant/templates/secret.yaml +++ b/charts/qdrant/templates/secret.yaml @@ -1,4 +1,4 @@ -{{- if .Values.apiKey }} +{{- if or .Values.apiKey .Values.readOnlyApiKey }} apiVersion: v1 kind: Secret metadata: diff --git a/charts/qdrant/templates/servicemonitor.yaml b/charts/qdrant/templates/servicemonitor.yaml index e7e04e9..b6b7386 100644 --- a/charts/qdrant/templates/servicemonitor.yaml +++ b/charts/qdrant/templates/servicemonitor.yaml @@ -30,6 +30,12 @@ spec: credentials: name: {{ include "qdrant.fullname" . }}-apikey key: api-key +{{- else if .Values.readOnlyApiKey }} + authorization: + type: Bearer + credentials: + name: {{ include "qdrant.fullname" . }}-apikey + key: read-only-api-key {{- end }} selector: matchLabels: diff --git a/charts/qdrant/templates/statefulset.yaml b/charts/qdrant/templates/statefulset.yaml index 44d6c4a..d6929bd 100644 --- a/charts/qdrant/templates/statefulset.yaml +++ b/charts/qdrant/templates/statefulset.yaml @@ -17,8 +17,9 @@ spec: template: metadata: annotations: - {{- if (default .Values.updateConfigurationOnChange false) }} checksum/config: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }} + {{- if or .Values.apiKey .Values.readOnlyApiKey }} + checksum/secret: {{ include (print $.Template.BasePath "/secret.yaml") . | sha256sum }} {{- end }} {{- with .Values.podAnnotations }} {{- toYaml . | nindent 8 }} @@ -171,7 +172,7 @@ spec: - name: qdrant-config mountPath: /qdrant/config/production.yaml subPath: production.yaml - {{- if .Values.apiKey }} + {{- if or .Values.apiKey .Values.readOnlyApiKey }} - name: qdrant-secret mountPath: /qdrant/config/local.yaml subPath: local.yaml @@ -219,7 +220,7 @@ spec: {{- end }} - name: qdrant-init emptyDir: {} - {{- if .Values.apiKey }} + {{- if or .Values.apiKey .Values.readOnlyApiKey }} - name: qdrant-secret secret: secretName: {{ include "qdrant.fullname" . }}-apikey diff --git a/charts/qdrant/templates/tests/test-db-interaction.yaml b/charts/qdrant/templates/tests/test-db-interaction.yaml index c6b3ffd..11eed1c 100644 --- a/charts/qdrant/templates/tests/test-db-interaction.yaml +++ b/charts/qdrant/templates/tests/test-db-interaction.yaml @@ -64,6 +64,8 @@ data: API_KEY_HEADER="" {{- if .Values.apiKey }} API_KEY_HEADER="Api-key: {{ .Values.apiKey }}" + {{- else if .Values.readOnlyApiKey }} + API_KEY_HEADER="Api-key: {{ .Values.readOnlyApiKey }}" {{- end }} # Delete collection if exists diff --git a/charts/qdrant/values.yaml b/charts/qdrant/values.yaml index 841234d..c914756 100644 --- a/charts/qdrant/values.yaml +++ b/charts/qdrant/values.yaml @@ -154,8 +154,6 @@ sidecarContainers: [] # memory: 100Mi # cpu: 100m -updateConfigurationOnChange: false - metrics: serviceMonitor: enabled: false @@ -190,6 +188,11 @@ podDisruptionBudget: # true: an api key will be auto-generated # string: the given string will be set as an apikey apiKey: false +# read-only api key for authentication at qdrant +# false: no read-only api key will be configured +# true: an read-only api key will be auto-generated +# string: the given string will be set as a read-only apikey +readOnlyApiKey: false additionalVolumes: [] # - name: volumeName diff --git a/test/integration/read_only_api_key.bats b/test/integration/read_only_api_key.bats new file mode 100644 index 0000000..6dfade3 --- /dev/null +++ b/test/integration/read_only_api_key.bats @@ -0,0 +1,15 @@ +setup_file() { + helm upgrade --install qdrant charts/qdrant --set readOnlyApiKey=barbaz -n qdrant-helm-integration --wait + kubectl rollout status statefulset qdrant -n qdrant-helm-integration +} + +@test "read-only api key authentication works" { + run kubectl exec -n default curl -- curl -s http://qdrant.qdrant-helm-integration:6333/collections -H 'api-key: barbaz' --fail-with-body + [ $status -eq 0 ] + [[ "${output}" =~ .*\"status\":\"ok\".* ]] +} + +@test "read-only api key authentication fails with key" { + run kubectl exec -n default curl -- curl -s http://qdrant.qdrant-helm-integration:6333/collections + [ "${output}" = "Invalid api-key" ] +} diff --git a/test/qdrant_api_key_test.go b/test/qdrant_api_key_test.go index b552dba..a8f6bec 100644 --- a/test/qdrant_api_key_test.go +++ b/test/qdrant_api_key_test.go @@ -144,3 +144,146 @@ func TestNoApiKey(t *testing.T) { require.Equal(t, hasSecretVolumeMount, false) require.Equal(t, hasSecretVolume, false) } + +func TestStringReadOnlyApiKey(t *testing.T) { + t.Parallel() + + helmChartPath, err := filepath.Abs("../charts/qdrant") + releaseName := "qdrant" + require.NoError(t, err) + + namespaceName := "qdrant-" + strings.ToLower(random.UniqueId()) + logger.Log(t, "Namespace: %s\n", namespaceName) + + options := &helm.Options{ + SetJsonValues: map[string]string{ + "readOnlyApiKey": `"test_api_key"`, + }, + KubectlOptions: k8s.NewKubectlOptions("", "", namespaceName), + } + + output := helm.RenderTemplate(t, options, helmChartPath, releaseName, []string{"templates/statefulset.yaml"}) + + var statefulSet appsv1.StatefulSet + helm.UnmarshalK8SYaml(t, output, &statefulSet) + + output = helm.RenderTemplate(t, options, helmChartPath, releaseName, []string{"templates/secret.yaml"}) + + var secret corev1.Secret + helm.UnmarshalK8SYaml(t, output, &secret) + + container, _ := lo.Find(statefulSet.Spec.Template.Spec.Containers, func(container corev1.Container) bool { + return container.Name == "qdrant" + }) + + secretVolumeMount, hasSecretVolumeMount := lo.Find(container.VolumeMounts, func(volumeMount corev1.VolumeMount) bool { + return volumeMount.Name == "qdrant-secret" + }) + + _, hasSecretVolume := lo.Find(statefulSet.Spec.Template.Spec.Volumes, func(volume corev1.Volume) bool { + return volume.Name == secretVolumeMount.Name + }) + + require.Equal(t, hasSecretVolumeMount, true) + require.Equal(t, hasSecretVolume, true) + require.Contains(t, lo.Keys(secret.Data), "local.yaml") + require.Contains(t, lo.Keys(secret.Data), "read-only-api-key") + + require.Equal(t, "service:\n read_only_api_key: test_api_key", string(secret.Data["local.yaml"])) +} + +func TestRandomReadOnlyApiKey(t *testing.T) { + t.Parallel() + + helmChartPath, err := filepath.Abs("../charts/qdrant") + releaseName := "qdrant" + require.NoError(t, err) + + namespaceName := "qdrant-" + strings.ToLower(random.UniqueId()) + logger.Log(t, "Namespace: %s\n", namespaceName) + + options := &helm.Options{ + SetJsonValues: map[string]string{ + "readOnlyApiKey": `true`, + }, + KubectlOptions: k8s.NewKubectlOptions("", "", namespaceName), + } + + output := helm.RenderTemplate(t, options, helmChartPath, releaseName, []string{"templates/statefulset.yaml"}) + + var statefulSet appsv1.StatefulSet + helm.UnmarshalK8SYaml(t, output, &statefulSet) + + output = helm.RenderTemplate(t, options, helmChartPath, releaseName, []string{"templates/secret.yaml"}) + + var secret corev1.Secret + helm.UnmarshalK8SYaml(t, output, &secret) + + container, _ := lo.Find(statefulSet.Spec.Template.Spec.Containers, func(container corev1.Container) bool { + return container.Name == "qdrant" + }) + + secretVolumeMount, hasSecretVolumeMount := lo.Find(container.VolumeMounts, func(volumeMount corev1.VolumeMount) bool { + return volumeMount.Name == "qdrant-secret" + }) + + _, hasSecretVolume := lo.Find(statefulSet.Spec.Template.Spec.Volumes, func(volume corev1.Volume) bool { + return volume.Name == secretVolumeMount.Name + }) + + require.Equal(t, hasSecretVolumeMount, true) + require.Equal(t, hasSecretVolume, true) + require.Contains(t, lo.Keys(secret.Data), "local.yaml") + require.Contains(t, lo.Keys(secret.Data), "read-only-api-key") + + require.Regexp(t, "^service:\n read_only_api_key: [a-zA-Z0-9]+$", string(secret.Data["local.yaml"])) +} + +func TestAdminAndReadOnlyApiKey(t *testing.T) { + t.Parallel() + + helmChartPath, err := filepath.Abs("../charts/qdrant") + releaseName := "qdrant" + require.NoError(t, err) + + namespaceName := "qdrant-" + strings.ToLower(random.UniqueId()) + logger.Log(t, "Namespace: %s\n", namespaceName) + + options := &helm.Options{ + SetJsonValues: map[string]string{ + "readOnlyApiKey": `true`, + "apiKey": `true`, + }, + KubectlOptions: k8s.NewKubectlOptions("", "", namespaceName), + } + + output := helm.RenderTemplate(t, options, helmChartPath, releaseName, []string{"templates/statefulset.yaml"}) + + var statefulSet appsv1.StatefulSet + helm.UnmarshalK8SYaml(t, output, &statefulSet) + + output = helm.RenderTemplate(t, options, helmChartPath, releaseName, []string{"templates/secret.yaml"}) + + var secret corev1.Secret + helm.UnmarshalK8SYaml(t, output, &secret) + + container, _ := lo.Find(statefulSet.Spec.Template.Spec.Containers, func(container corev1.Container) bool { + return container.Name == "qdrant" + }) + + secretVolumeMount, hasSecretVolumeMount := lo.Find(container.VolumeMounts, func(volumeMount corev1.VolumeMount) bool { + return volumeMount.Name == "qdrant-secret" + }) + + _, hasSecretVolume := lo.Find(statefulSet.Spec.Template.Spec.Volumes, func(volume corev1.Volume) bool { + return volume.Name == secretVolumeMount.Name + }) + + require.Equal(t, hasSecretVolumeMount, true) + require.Equal(t, hasSecretVolume, true) + require.Contains(t, lo.Keys(secret.Data), "local.yaml") + require.Contains(t, lo.Keys(secret.Data), "api-key") + require.Contains(t, lo.Keys(secret.Data), "read-only-api-key") + + require.Regexp(t, "^service:\n api_key: [a-zA-Z0-9]+\n read_only_api_key: [a-zA-Z0-9]+$", string(secret.Data["local.yaml"])) +}