From e39618ede6dc1f14870d3c1c31509194a1094f35 Mon Sep 17 00:00:00 2001 From: Julian Schlarb Date: Tue, 22 Jul 2025 10:48:30 +0200 Subject: [PATCH] feat: Add support for custom username and password keys in auth. --- .../druid.apache.org_druidingestions.yaml | 8 + chart/crds/druid.apache.org_druids.yaml | 8 + .../druid.apache.org_druidingestions.yaml | 8 + config/crd/bases/druid.apache.org_druids.yaml | 8 + docs/api_specifications/druid.md | 180 ++++++++++++------ go.mod | 1 + go.sum | 1 + pkg/druidapi/druidapi.go | 32 +++- pkg/druidapi/druidapi_test.go | 110 +++++++++++ 9 files changed, 297 insertions(+), 59 deletions(-) diff --git a/chart/crds/druid.apache.org_druidingestions.yaml b/chart/crds/druid.apache.org_druidingestions.yaml index 70e7fe47..b5f09c5f 100644 --- a/chart/crds/druid.apache.org_druidingestions.yaml +++ b/chart/crds/druid.apache.org_druidingestions.yaml @@ -47,6 +47,10 @@ spec: properties: auth: properties: + passwordKey: + description: PasswordKey specifies the key within the Kubernetes + secret that contains the password for authentication. + type: string secretRef: description: |- SecretReference represents a Secret Reference. It has enough information to retrieve secret @@ -64,6 +68,10 @@ spec: x-kubernetes-map-type: atomic type: type: string + usernameKey: + description: UsernameKey specifies the key within the Kubernetes + secret that contains the username for authentication. + type: string required: - secretRef - type diff --git a/chart/crds/druid.apache.org_druids.yaml b/chart/crds/druid.apache.org_druids.yaml index 9bec0105..20d7d37b 100644 --- a/chart/crds/druid.apache.org_druids.yaml +++ b/chart/crds/druid.apache.org_druids.yaml @@ -1267,6 +1267,10 @@ spec: type: object auth: properties: + passwordKey: + description: PasswordKey specifies the key within the Kubernetes + secret that contains the password for authentication. + type: string secretRef: description: |- SecretReference represents a Secret Reference. It has enough information to retrieve secret @@ -1284,6 +1288,10 @@ spec: x-kubernetes-map-type: atomic type: type: string + usernameKey: + description: UsernameKey specifies the key within the Kubernetes + secret that contains the username for authentication. + type: string required: - secretRef - type diff --git a/config/crd/bases/druid.apache.org_druidingestions.yaml b/config/crd/bases/druid.apache.org_druidingestions.yaml index 70e7fe47..b5f09c5f 100644 --- a/config/crd/bases/druid.apache.org_druidingestions.yaml +++ b/config/crd/bases/druid.apache.org_druidingestions.yaml @@ -47,6 +47,10 @@ spec: properties: auth: properties: + passwordKey: + description: PasswordKey specifies the key within the Kubernetes + secret that contains the password for authentication. + type: string secretRef: description: |- SecretReference represents a Secret Reference. It has enough information to retrieve secret @@ -64,6 +68,10 @@ spec: x-kubernetes-map-type: atomic type: type: string + usernameKey: + description: UsernameKey specifies the key within the Kubernetes + secret that contains the username for authentication. + type: string required: - secretRef - type diff --git a/config/crd/bases/druid.apache.org_druids.yaml b/config/crd/bases/druid.apache.org_druids.yaml index 9bec0105..20d7d37b 100644 --- a/config/crd/bases/druid.apache.org_druids.yaml +++ b/config/crd/bases/druid.apache.org_druids.yaml @@ -1267,6 +1267,10 @@ spec: type: object auth: properties: + passwordKey: + description: PasswordKey specifies the key within the Kubernetes + secret that contains the password for authentication. + type: string secretRef: description: |- SecretReference represents a Secret Reference. It has enough information to retrieve secret @@ -1284,6 +1288,10 @@ spec: x-kubernetes-map-type: atomic type: type: string + usernameKey: + description: UsernameKey specifies the key within the Kubernetes + secret that contains the username for authentication. + type: string required: - secretRef - type diff --git a/docs/api_specifications/druid.md b/docs/api_specifications/druid.md index 993205d5..4517835b 100644 --- a/docs/api_specifications/druid.md +++ b/docs/api_specifications/druid.md @@ -173,56 +173,6 @@ Kubernetes core/v1.ResourceRequirements -

Auth -

-

-(Appears on: -DruidIngestionSpec) -

-
-
- - - - - - - - - - - - - - - - - -
FieldDescription
-type
- - -AuthType - - -
-
-secretRef
- - -Kubernetes core/v1.SecretReference - - -
-
-
-
-

AuthType -(string alias)

-

-(Appears on: -Auth) -

DeepStorageSpec

@@ -909,6 +859,29 @@ string +dynamicConfig
+ +k8s.io/apimachinery/pkg/runtime.RawExtension + + + +(Optional) +

Dynamic Configurations for Druid. Applied through the dynamic configuration API.

+ + + + +auth
+ +github.com/datainfrahq/druid-operator/pkg/druidapi.Auth + + + +(Optional) + + + + dnsPolicy
@@ -1156,9 +1129,7 @@ IngestionSpec auth
-
-Auth - +github.com/datainfrahq/druid-operator/pkg/druidapi.Auth @@ -1243,9 +1214,7 @@ IngestionSpec auth
- -Auth - +github.com/datainfrahq/druid-operator/pkg/druidapi.Auth @@ -1344,6 +1313,18 @@ string
+

CurrentIngestionSpec is a string instead of RawExtension to maintain compatibility with existing +IngestionSpecs that are stored as JSON strings.

+ + + + +rules
+ +[]k8s.io/apimachinery/pkg/runtime.RawExtension + + + @@ -1972,6 +1953,30 @@ Kubernetes autoscaling/v2.HorizontalPodAutoscalerSpec +serviceAccountName
+ +string + + + +(Optional) +

ServiceAccountName Kubernetes native serviceAccountName specification.

+ + + + +dynamicConfig
+ +k8s.io/apimachinery/pkg/runtime.RawExtension + + + +(Optional) +

Dynamic Configurations for Druid. Applied through the dynamic configuration API.

+ + + + dnsPolicy
@@ -2684,6 +2689,29 @@ string +dynamicConfig
+ +k8s.io/apimachinery/pkg/runtime.RawExtension + + + +(Optional) +

Dynamic Configurations for Druid. Applied through the dynamic configuration API.

+ + + + +auth
+ +github.com/datainfrahq/druid-operator/pkg/druidapi.Auth + + + +(Optional) + + + + dnsPolicy
@@ -2750,12 +2778,52 @@ string +(Optional) +

Spec should be passed in as a JSON string. +Note: This field is planned for deprecation in favor of nativeSpec.



+ + +nativeSpec
+ +k8s.io/apimachinery/pkg/runtime.RawExtension + + + +(Optional) +

nativeSpec allows the ingestion specification to be defined in a native Kubernetes format. +This is particularly useful for environment-specific configurations and will eventually +replace the JSON-based Spec field. +Note: Spec will be ignored if nativeSpec is provided.

+ + + + +compaction
+ +k8s.io/apimachinery/pkg/runtime.RawExtension + + + +(Optional) + + + + +rules
+ +[]k8s.io/apimachinery/pkg/runtime.RawExtension + + + +(Optional) + + diff --git a/go.mod b/go.mod index 14ca62f7..01f34480 100644 --- a/go.mod +++ b/go.mod @@ -20,6 +20,7 @@ require ( github.com/cespare/xxhash/v2 v2.2.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/emicklei/go-restful/v3 v3.9.0 // indirect + github.com/evanphx/json-patch v4.12.0+incompatible // indirect github.com/evanphx/json-patch/v5 v5.6.0 // indirect github.com/fsnotify/fsnotify v1.6.0 // indirect github.com/go-logr/zapr v1.2.4 // indirect diff --git a/go.sum b/go.sum index 76aaa3b6..43ed29b7 100644 --- a/go.sum +++ b/go.sum @@ -23,6 +23,7 @@ github.com/emicklei/go-restful/v3 v3.9.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/evanphx/json-patch v4.12.0+incompatible h1:4onqiflcdA9EOZ4RxV643DvftH5pOlLGNtQ5lPWQu84= +github.com/evanphx/json-patch v4.12.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= github.com/evanphx/json-patch/v5 v5.6.0 h1:b91NhWfaz02IuVxO9faSllyAtNXHMPkC5J8sJCLunww= github.com/evanphx/json-patch/v5 v5.6.0/go.mod h1:G79N1coSVB93tBe7j6PhzjmR3/2VvlbKOFpnXhI9Bw4= github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= diff --git a/pkg/druidapi/druidapi.go b/pkg/druidapi/druidapi.go index 8b020dcb..effda506 100644 --- a/pkg/druidapi/druidapi.go +++ b/pkg/druidapi/druidapi.go @@ -30,6 +30,12 @@ type Auth struct { Type AuthType `json:"type"` // +required SecretRef v1.SecretReference `json:"secretRef"` + + // UsernameKey specifies the key within the Kubernetes secret that contains the username for authentication. + UsernameKey string `json:"usernameKey,omitempty"` + + // PasswordKey specifies the key within the Kubernetes secret that contains the password for authentication. + PasswordKey string `json:"passwordKey,omitempty"` } // GetAuthCreds retrieves basic authentication credentials from a Kubernetes secret. @@ -42,12 +48,23 @@ type Auth struct { // // Returns: // -// BasicAuth: The basic authentication credentials. +// BasicAuth: The basic authentication credentials, or an error if authentication retrieval fails. func GetAuthCreds( ctx context.Context, c client.Client, auth Auth, ) (internalhttp.BasicAuth, error) { + userNameKey := OperatorUserName + passwordKey := OperatorPassword + + if auth.UsernameKey != "" { + userNameKey = auth.UsernameKey + } + + if auth.PasswordKey != "" { + passwordKey = auth.PasswordKey + } + // Check if the mentioned secret exists if auth != (Auth{}) { secret := v1.Secret{} @@ -57,9 +74,18 @@ func GetAuthCreds( }, &secret); err != nil { return internalhttp.BasicAuth{}, err } + + if _, ok := secret.Data[userNameKey]; !ok { + return internalhttp.BasicAuth{}, fmt.Errorf("username key %q not found in secret %s/%s", userNameKey, auth.SecretRef.Namespace, auth.SecretRef.Name) + } + + if _, ok := secret.Data[passwordKey]; !ok { + return internalhttp.BasicAuth{}, fmt.Errorf("password key %q not found in secret %s/%s", passwordKey, auth.SecretRef.Namespace, auth.SecretRef.Name) + } + creds := internalhttp.BasicAuth{ - UserName: string(secret.Data[OperatorUserName]), - Password: string(secret.Data[OperatorPassword]), + UserName: string(secret.Data[userNameKey]), + Password: string(secret.Data[passwordKey]), } return creds, nil diff --git a/pkg/druidapi/druidapi_test.go b/pkg/druidapi/druidapi_test.go index 3b94e993..10a686c1 100644 --- a/pkg/druidapi/druidapi_test.go +++ b/pkg/druidapi/druidapi_test.go @@ -1,9 +1,119 @@ package druidapi import ( + "context" + internalhttp "github.com/datainfrahq/druid-operator/pkg/http" + "github.com/stretchr/testify/assert" + v1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/client/fake" "testing" ) +func TestGetAuthCreds(t *testing.T) { + tests := []struct { + name string + auth Auth + expected internalhttp.BasicAuth + expectErr bool + }{ + { + name: "default keys present", + auth: Auth{ + Type: BasicAuth, + SecretRef: v1.SecretReference{Name: "test-default", Namespace: "test"}, + }, + expected: internalhttp.BasicAuth{UserName: "test-user", Password: "test-password"}, + expectErr: false, + }, + { + name: "custom keys present", + auth: Auth{ + Type: BasicAuth, + SecretRef: v1.SecretReference{Name: "test", Namespace: "default"}, + UsernameKey: "usr", + PasswordKey: "pwd", + }, + expected: internalhttp.BasicAuth{UserName: "admin", Password: "admin"}, + expectErr: false, + }, + { + name: "custom user key is missing", + auth: Auth{ + Type: BasicAuth, + SecretRef: v1.SecretReference{Name: "test", Namespace: "default"}, + UsernameKey: "nope", + PasswordKey: "pwd", + }, + expected: internalhttp.BasicAuth{}, + expectErr: true, + }, + { + name: "custom user key with default password key", + auth: Auth{ + Type: BasicAuth, + SecretRef: v1.SecretReference{Name: "test", Namespace: "default"}, + UsernameKey: "usr", + }, + expected: internalhttp.BasicAuth{UserName: "admin", Password: "also-admin"}, + expectErr: false, + }, + { + name: "custom password key is missing", + auth: Auth{ + Type: BasicAuth, + SecretRef: v1.SecretReference{Name: "test", Namespace: "default"}, + UsernameKey: "usr", + PasswordKey: "nope", + }, + expected: internalhttp.BasicAuth{}, + expectErr: true, + }, + { + name: "empty auth struct returns no creds", + auth: Auth{}, + expected: internalhttp.BasicAuth{}, + expectErr: false, + }, + } + + client := fake.NewClientBuilder(). + WithObjects(&v1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-default", + Namespace: "test", + }, + Data: map[string][]byte{ + OperatorUserName: []byte("test-user"), + OperatorPassword: []byte("test-password"), + }, + }). + WithObjects(&v1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test", + Namespace: "default", + }, + Data: map[string][]byte{ + "usr": []byte("admin"), + "pwd": []byte("admin"), + OperatorPassword: []byte("also-admin"), + }, + }).Build() + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + actual, err := GetAuthCreds(context.TODO(), client, tt.auth) + if tt.expectErr { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + + assert.Equal(t, tt.expected, actual) + }) + } +} + func TestMakePath(t *testing.T) { tests := []struct { name string