Skip to content
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
3b8164d
Refactor user annotation key generation
ravisingal Feb 11, 2026
d0c1a21
add unit tests
ravisingal Feb 17, 2026
b0deb59
Merge branch 'main' into secret-annotation-key-length
ravisingal Feb 18, 2026
3c13d52
use constant for max annotation name length
ravisingal Feb 21, 2026
53ae4bb
Merge branch 'main' into secret-annotation-key-length
ravisingal Feb 21, 2026
5dc6fa5
Merge branch 'main' into secret-annotation-key-length
ravisingal Mar 2, 2026
cba96c1
Merge branch 'main' into secret-annotation-key-length
egegunes Mar 4, 2026
78a982d
Merge branch 'main' into secret-annotation-key-length
ravisingal Mar 11, 2026
e81adde
use assert to validate annotation key
ravisingal Mar 11, 2026
bb80a94
Merge branch 'main' into secret-annotation-key-length
ravisingal Mar 11, 2026
f148275
Merge branch 'main' into secret-annotation-key-length
ravisingal Mar 12, 2026
d67a311
Merge branch 'main' into secret-annotation-key-length
ravisingal Apr 8, 2026
5e0a03a
Merge branch 'main' into secret-annotation-key-length
ravisingal Apr 13, 2026
e4288fd
Merge branch 'main' into secret-annotation-key-length
ravisingal Apr 22, 2026
afe2cc5
Merge branch 'main' into secret-annotation-key-length
ravisingal May 4, 2026
2e80fde
Merge branch 'main' into secret-annotation-key-length
hors May 6, 2026
449eef0
Merge branch 'main' into secret-annotation-key-length
ravisingal May 9, 2026
aa58dcd
Merge branch 'main' into secret-annotation-key-length
hors May 13, 2026
84f37e7
Merge branch 'main' into secret-annotation-key-length
egegunes May 14, 2026
ceccfe8
Merge branch 'main' into secret-annotation-key-length
hors May 15, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 19 additions & 1 deletion pkg/controller/perconaservermongodb/custom_users.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ import (
s "github.com/percona/percona-server-mongodb-operator/pkg/psmdb/secret"
)

// maxAnnotationNameLength is the maximum length for Kubernetes annotation key names (63 characters).
const maxAnnotationNameLength = 63

func (r *ReconcilePerconaServerMongoDB) reconcileCustomUsers(ctx context.Context, cr *api.PerconaServerMongoDB) error {
if cr.Spec.Users == nil && len(cr.Spec.Users) == 0 && cr.Spec.Roles == nil && len(cr.Spec.Roles) == 0 {
return nil
Expand Down Expand Up @@ -105,7 +108,7 @@ func handleUsers(ctx context.Context, cr *api.PerconaServerMongoDB, mongoCli mon
continue
}

annotationKey := fmt.Sprintf("percona.com/%s-%s-hash", cr.Name, user.Name)
annotationKey := buildAnnotationKey(cr.Name, user.Name)

if userInfo == nil && !user.IsExternalDB() {
err = createUser(ctx, client, mongoCli, &user, sec, annotationKey, userSecretPassKey)
Expand Down Expand Up @@ -422,6 +425,21 @@ func createUser(
return nil
}

// buildAnnotationKey creates a Kubernetes annotation key for tracking user password hashes.
// Kubernetes annotation keys have a limit of 63 characters for the name part.
// Format: "percona.com/<name>" where it must be less than or equal to 63 characters.
// We need to keep the "-hash" suffix (5 chars), so we have 58 chars for the prefix.
func buildAnnotationKey(crName, userName string) string {
annotationKeyBase := fmt.Sprintf("percona.com/%s-%s", crName, userName)
const hashSuffix = "-hash"
const maxPrefixLength = maxAnnotationNameLength - len(hashSuffix)

if len(annotationKeyBase) > maxPrefixLength {
annotationKeyBase = annotationKeyBase[:maxPrefixLength]
}
return fmt.Sprintf("%s%s", annotationKeyBase, hashSuffix)
}

// getCustomUserSecret gets secret by name defined by `user.PasswordSecretRef.Name` or returns a secret
// with newly generated password if name matches defaultName
func getCustomUserSecret(ctx context.Context, cl client.Client, cr *api.PerconaServerMongoDB, user *api.User, passKey string) (*corev1.Secret, error) {
Expand Down
89 changes: 89 additions & 0 deletions pkg/controller/perconaservermongodb/custom_users_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package perconaservermongodb

import (
"context"
"strings"
"testing"

"github.com/pkg/errors"
Expand Down Expand Up @@ -411,3 +412,91 @@ func TestGetCustomUserSecret(t *testing.T) {
})
}
}

func TestBuildAnnotationKey(t *testing.T) {
tests := []struct {
name string
crName string
userName string
want string
wantLen int
maxLength int
}{
{
name: "short names within limit",
crName: "my-cluster",
userName: "user1",
want: "percona.com/my-cluster-user1-hash",
wantLen: 33,
maxLength: maxAnnotationNameLength,
},
{
name: "exactly at limit",
crName: "a",
userName: strings.Repeat("x", 44), // 1 + 5 + 44 + 13 (percona.com/-) = 63, will not be truncated
want: "percona.com/a-" + strings.Repeat("x", 44) + "-hash",
wantLen: maxAnnotationNameLength,
maxLength: maxAnnotationNameLength,
},
{
name: "exceeds limit - truncates but keeps hash suffix",
crName: "very-long-cluster-name-that-exceeds",
userName: "very-long-user-name-that-also-exceeds",
want: "percona.com/very-long-cluster-name-that-exceeds-very-long--hash",
wantLen: maxAnnotationNameLength,
maxLength: maxAnnotationNameLength,
},
{
name: "very long cluster name",
crName: strings.Repeat("a", 100),
userName: "user",
want: "percona.com/" + strings.Repeat("a", 46) + "-hash",
wantLen: maxAnnotationNameLength,
maxLength: maxAnnotationNameLength,
},
{
name: "very long user name",
crName: "cluster",
userName: strings.Repeat("b", 100),
want: "percona.com/cluster-" + strings.Repeat("b", 38) + "-hash",
wantLen: maxAnnotationNameLength,
maxLength: maxAnnotationNameLength,
},
{
name: "both names very long",
crName: strings.Repeat("c", 50),
userName: strings.Repeat("d", 50),
want: "percona.com/" + strings.Repeat("c", 46) + "-hash",
wantLen: maxAnnotationNameLength,
maxLength: maxAnnotationNameLength,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := buildAnnotationKey(tt.crName, tt.userName)

// Extract the name part (after "percona.com/")
prefix := "percona.com/"
assert.True(t, strings.HasPrefix(got, prefix), "buildAnnotationKey() = %v, should start with %v", got, prefix)

// Verify the annotation key name part is within Kubernetes limit
namePart := got[len(prefix):]
gotLen := len(got)
assert.True(t, len(namePart) <= tt.maxLength, "buildAnnotationKey() name part length = %v, should be <= %v. Got: %v", len(namePart), tt.maxLength, got)

// Verify it ends with "-hash"
assert.True(t, strings.HasSuffix(got, "-hash"), "buildAnnotationKey() = %v, should end with '-hash'", got)

// Verify exact match for non-truncated cases
if gotLen <= tt.maxLength && tt.want != "" {
assert.Equal(t, tt.want, got, "buildAnnotationKey() = %v, want %v", got, tt.want)
}

// Verify length matches expected for all cases
if tt.wantLen > 0 {
assert.Equal(t, tt.wantLen, gotLen, "buildAnnotationKey() length = %v, want %v", gotLen, tt.wantLen)
}
})
}
}