Skip to content

Commit f220dc4

Browse files
authored
feat: add indexers for ConnectionSecret controller (#2562)
1 parent 0a4f3bd commit f220dc4

7 files changed

+868
-0
lines changed
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
// Copyright 2025 MongoDB Inc
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
//nolint:dupl
16+
package indexer
17+
18+
import (
19+
"context"
20+
"fmt"
21+
22+
"go.uber.org/zap"
23+
"sigs.k8s.io/controller-runtime/pkg/client"
24+
25+
akov2 "github.com/mongodb/mongodb-atlas-kubernetes/v2/api/v1"
26+
"github.com/mongodb/mongodb-atlas-kubernetes/v2/internal/kube"
27+
)
28+
29+
// Index Format:
30+
// <project-id>-<normalized-username>
31+
//
32+
// Where:
33+
// - <project-id> is resolved from either ExternalProjectRef.ID or the resolved AtlasProject.Status.ID
34+
// - <normalized-username> is produced via kube.NormalizeIdentifier(user.Spec.Username)
35+
//
36+
// Purpose:
37+
// This index enables fast lookup of AtlasDatabaseUser objects by a combination of project ID and username,
38+
// such as when reconciling resources linked to a connection secret.
39+
40+
const (
41+
AtlasDatabaseUserBySpecUsernameAndProjectID = "atlasdatabaseuser.projectID/spec.username"
42+
)
43+
44+
type AtlasDatabaseUserBySpecUsernameIndexer struct {
45+
ctx context.Context
46+
client client.Client
47+
logger *zap.SugaredLogger
48+
}
49+
50+
func NewAtlasDatabaseUserBySpecUsernameIndexer(ctx context.Context, client client.Client, logger *zap.Logger) *AtlasDatabaseUserBySpecUsernameIndexer {
51+
return &AtlasDatabaseUserBySpecUsernameIndexer{
52+
ctx: ctx,
53+
client: client,
54+
logger: logger.Named(AtlasDatabaseUserBySpecUsernameAndProjectID).Sugar(),
55+
}
56+
}
57+
58+
func (*AtlasDatabaseUserBySpecUsernameIndexer) Object() client.Object {
59+
return &akov2.AtlasDatabaseUser{}
60+
}
61+
62+
func (*AtlasDatabaseUserBySpecUsernameIndexer) Name() string {
63+
return AtlasDatabaseUserBySpecUsernameAndProjectID
64+
}
65+
66+
func (a *AtlasDatabaseUserBySpecUsernameIndexer) Keys(object client.Object) []string {
67+
user, ok := object.(*akov2.AtlasDatabaseUser)
68+
if !ok {
69+
a.logger.Errorf("expected *v1.AtlasDatabaseUser but got %T", object)
70+
return nil
71+
}
72+
73+
username := user.Spec.Username
74+
if username == "" {
75+
return nil
76+
}
77+
78+
username = kube.NormalizeIdentifier(username)
79+
if user.Spec.ExternalProjectRef != nil && user.Spec.ExternalProjectRef.ID != "" {
80+
return []string{fmt.Sprintf("%s-%s", user.Spec.ExternalProjectRef.ID, username)}
81+
}
82+
83+
if user.Spec.ProjectRef != nil && user.Spec.ProjectRef.Name != "" {
84+
project := &akov2.AtlasProject{}
85+
err := a.client.Get(a.ctx, *user.Spec.ProjectRef.GetObject(user.Namespace), project)
86+
if err != nil {
87+
a.logger.Errorf("unable to find project to index: %s", err)
88+
return nil
89+
}
90+
91+
if project.ID() != "" {
92+
return []string{fmt.Sprintf("%s-%s", project.ID(), username)}
93+
}
94+
}
95+
96+
return nil
97+
}
Lines changed: 223 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,223 @@
1+
// Copyright 2025 MongoDB Inc
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
//nolint:dupl
16+
package indexer
17+
18+
import (
19+
"context"
20+
"sort"
21+
"testing"
22+
23+
"github.com/stretchr/testify/assert"
24+
"go.uber.org/zap"
25+
"go.uber.org/zap/zapcore"
26+
"go.uber.org/zap/zaptest/observer"
27+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
28+
"k8s.io/apimachinery/pkg/runtime"
29+
"sigs.k8s.io/controller-runtime/pkg/client"
30+
"sigs.k8s.io/controller-runtime/pkg/client/fake"
31+
32+
akov2 "github.com/mongodb/mongodb-atlas-kubernetes/v2/api/v1"
33+
"github.com/mongodb/mongodb-atlas-kubernetes/v2/api/v1/common"
34+
"github.com/mongodb/mongodb-atlas-kubernetes/v2/api/v1/status"
35+
"github.com/mongodb/mongodb-atlas-kubernetes/v2/internal/kube"
36+
)
37+
38+
func TestAtlasDatabaseUserBySpecUsernameIndexer(t *testing.T) {
39+
tests := map[string]struct {
40+
object client.Object
41+
expectedKeys []string
42+
expectedLogs []observer.LoggedEntry
43+
}{
44+
"should return nil on wrong type": {
45+
object: &akov2.AtlasStreamInstance{},
46+
expectedLogs: []observer.LoggedEntry{
47+
{
48+
Context: []zapcore.Field{},
49+
Entry: zapcore.Entry{
50+
LoggerName: AtlasDatabaseUserBySpecUsernameAndProjectID,
51+
Level: zap.ErrorLevel,
52+
Message: "expected *v1.AtlasDatabaseUser but got *v1.AtlasStreamInstance",
53+
},
54+
},
55+
},
56+
},
57+
"should return nil when username is empty": {
58+
object: &akov2.AtlasDatabaseUser{
59+
Spec: akov2.AtlasDatabaseUserSpec{
60+
Username: "",
61+
},
62+
},
63+
expectedLogs: []observer.LoggedEntry{},
64+
},
65+
"should return nil when there are no references": {
66+
object: &akov2.AtlasDatabaseUser{
67+
Spec: akov2.AtlasDatabaseUserSpec{
68+
Username: "test-my-user",
69+
},
70+
},
71+
expectedLogs: []observer.LoggedEntry{},
72+
},
73+
"should return nil when there is an empty external reference": {
74+
object: &akov2.AtlasDatabaseUser{
75+
Spec: akov2.AtlasDatabaseUserSpec{
76+
Username: "test-my-user",
77+
ProjectDualReference: akov2.ProjectDualReference{
78+
ExternalProjectRef: &akov2.ExternalProjectReference{},
79+
},
80+
},
81+
},
82+
expectedLogs: []observer.LoggedEntry{},
83+
},
84+
"should return key with external project ID": {
85+
object: &akov2.AtlasDatabaseUser{
86+
Spec: akov2.AtlasDatabaseUserSpec{
87+
Username: "test-my-user",
88+
ProjectDualReference: akov2.ProjectDualReference{
89+
ExternalProjectRef: &akov2.ExternalProjectReference{
90+
ID: "test-external-id",
91+
},
92+
},
93+
},
94+
},
95+
expectedKeys: []string{"test-external-id-" + kube.NormalizeIdentifier("test-my-user")},
96+
expectedLogs: []observer.LoggedEntry{},
97+
},
98+
"should return nil when internal projectRef is empty": {
99+
object: &akov2.AtlasDatabaseUser{
100+
Spec: akov2.AtlasDatabaseUserSpec{
101+
Username: "test-my-user",
102+
ProjectDualReference: akov2.ProjectDualReference{
103+
ProjectRef: &common.ResourceRefNamespaced{},
104+
},
105+
},
106+
},
107+
expectedLogs: []observer.LoggedEntry{},
108+
},
109+
"should return key from internal projectRef (same ns)": {
110+
object: &akov2.AtlasDatabaseUser{
111+
ObjectMeta: metav1.ObjectMeta{
112+
Name: "test-user",
113+
Namespace: "test-ns",
114+
},
115+
Spec: akov2.AtlasDatabaseUserSpec{
116+
Username: "test-my-user",
117+
ProjectDualReference: akov2.ProjectDualReference{
118+
ProjectRef: &common.ResourceRefNamespaced{
119+
Name: "test-name",
120+
Namespace: "test-ns",
121+
},
122+
},
123+
},
124+
},
125+
expectedKeys: []string{"test-project-id-" + kube.NormalizeIdentifier("test-my-user")},
126+
expectedLogs: []observer.LoggedEntry{},
127+
},
128+
"should return key from internal projectRef (explicit ns)": {
129+
object: &akov2.AtlasDatabaseUser{
130+
ObjectMeta: metav1.ObjectMeta{
131+
Name: "test-user",
132+
Namespace: "user-ns",
133+
},
134+
Spec: akov2.AtlasDatabaseUserSpec{
135+
Username: "test-my-user",
136+
ProjectDualReference: akov2.ProjectDualReference{
137+
ProjectRef: &common.ResourceRefNamespaced{
138+
Name: "test-name",
139+
Namespace: "test-ns",
140+
},
141+
},
142+
},
143+
},
144+
expectedKeys: []string{"test-project-id-" + kube.NormalizeIdentifier("test-my-user")},
145+
expectedLogs: []observer.LoggedEntry{},
146+
},
147+
"should normalize username before indexing": {
148+
object: &akov2.AtlasDatabaseUser{
149+
Spec: akov2.AtlasDatabaseUserSpec{
150+
Username: "My.User+123",
151+
ProjectDualReference: akov2.ProjectDualReference{
152+
ExternalProjectRef: &akov2.ExternalProjectReference{
153+
ID: "test-external-id",
154+
},
155+
},
156+
},
157+
},
158+
expectedKeys: []string{"test-external-id-" + kube.NormalizeIdentifier("My.User+123")},
159+
expectedLogs: []observer.LoggedEntry{},
160+
},
161+
"should log error if internal project not found": {
162+
object: &akov2.AtlasDatabaseUser{
163+
ObjectMeta: metav1.ObjectMeta{
164+
Name: "test-user",
165+
Namespace: "test-ns",
166+
},
167+
Spec: akov2.AtlasDatabaseUserSpec{
168+
Username: "test-my-user",
169+
ProjectDualReference: akov2.ProjectDualReference{
170+
ProjectRef: &common.ResourceRefNamespaced{
171+
Name: "nonexistent-project",
172+
},
173+
},
174+
},
175+
},
176+
expectedLogs: []observer.LoggedEntry{
177+
{
178+
Context: []zapcore.Field{},
179+
Entry: zapcore.Entry{
180+
LoggerName: AtlasDatabaseUserBySpecUsernameAndProjectID,
181+
Level: zap.ErrorLevel,
182+
Message: "unable to find project to index: atlasprojects.atlas.mongodb.com \"nonexistent-project\" not found",
183+
},
184+
},
185+
},
186+
},
187+
}
188+
189+
for name, tt := range tests {
190+
t.Run(name, func(t *testing.T) {
191+
project := &akov2.AtlasProject{
192+
ObjectMeta: metav1.ObjectMeta{
193+
Name: "test-name",
194+
Namespace: "test-ns",
195+
},
196+
Spec: akov2.AtlasProjectSpec{
197+
Name: "Some Project",
198+
},
199+
Status: status.AtlasProjectStatus{
200+
ID: "test-project-id",
201+
},
202+
}
203+
204+
scheme := runtime.NewScheme()
205+
assert.NoError(t, akov2.AddToScheme(scheme))
206+
207+
client := fake.NewClientBuilder().
208+
WithScheme(scheme).
209+
WithObjects(project).
210+
WithStatusSubresource(project).
211+
Build()
212+
213+
core, logs := observer.New(zap.DebugLevel)
214+
indexer := NewAtlasDatabaseUserBySpecUsernameIndexer(context.Background(), client, zap.New(core))
215+
216+
keys := indexer.Keys(tt.object)
217+
sort.Strings(keys)
218+
219+
assert.Equal(t, tt.expectedKeys, keys)
220+
assert.Equal(t, tt.expectedLogs, logs.AllUntimed())
221+
})
222+
}
223+
}

0 commit comments

Comments
 (0)