Skip to content

Commit

Permalink
Support read_only_api_key in Qdrant config, similar to the api_key co… (
Browse files Browse the repository at this point in the history
#146)

* Support read_only_api_key in Qdrant config, similar to the api_key config value

* Fix updates of secret

* Fix updates of secret

* Fix updates of secret

* Fix updates of secret
  • Loading branch information
bashofmann authored Mar 5, 2024
1 parent b9725f0 commit ee2589c
Show file tree
Hide file tree
Showing 9 changed files with 198 additions and 14 deletions.
4 changes: 0 additions & 4 deletions charts/qdrant/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
26 changes: 22 additions & 4 deletions charts/qdrant/templates/_helpers.tpl
Original file line number Diff line number Diff line change
Expand Up @@ -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 -}}
2 changes: 1 addition & 1 deletion charts/qdrant/templates/secret.yaml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
{{- if .Values.apiKey }}
{{- if or .Values.apiKey .Values.readOnlyApiKey }}
apiVersion: v1
kind: Secret
metadata:
Expand Down
6 changes: 6 additions & 0 deletions charts/qdrant/templates/servicemonitor.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
7 changes: 4 additions & 3 deletions charts/qdrant/templates/statefulset.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }}
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions charts/qdrant/templates/tests/test-db-interaction.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
7 changes: 5 additions & 2 deletions charts/qdrant/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -154,8 +154,6 @@ sidecarContainers: []
# memory: 100Mi
# cpu: 100m

updateConfigurationOnChange: false

metrics:
serviceMonitor:
enabled: false
Expand Down Expand Up @@ -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
Expand Down
15 changes: 15 additions & 0 deletions test/integration/read_only_api_key.bats
Original file line number Diff line number Diff line change
@@ -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" ]
}
143 changes: 143 additions & 0 deletions test/qdrant_api_key_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"]))
}

0 comments on commit ee2589c

Please sign in to comment.