Skip to content

Commit a0b291f

Browse files
committed
feat: add indexers for ConnectionSecret controller
1 parent cfedfa2 commit a0b291f

7 files changed

+794
-0
lines changed
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
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+
)
27+
28+
const (
29+
AtlasDatabaseUserBySpecUsernameAndProjectID = "atlasdatabaseuser.projectID/spec.username"
30+
)
31+
32+
type AtlasDatabaseUserBySpecUsernameIndexer struct {
33+
ctx context.Context
34+
client client.Client
35+
logger *zap.SugaredLogger
36+
}
37+
38+
func NewAtlasDatabaseUserBySpecUsernameIndexer(ctx context.Context, client client.Client, logger *zap.Logger) *AtlasDatabaseUserBySpecUsernameIndexer {
39+
return &AtlasDatabaseUserBySpecUsernameIndexer{
40+
ctx: ctx,
41+
client: client,
42+
logger: logger.Named(AtlasDatabaseUserBySpecUsernameAndProjectID).Sugar(),
43+
}
44+
}
45+
46+
func (*AtlasDatabaseUserBySpecUsernameIndexer) Object() client.Object {
47+
return &akov2.AtlasDatabaseUser{}
48+
}
49+
50+
func (*AtlasDatabaseUserBySpecUsernameIndexer) Name() string {
51+
return AtlasDatabaseUserBySpecUsernameAndProjectID
52+
}
53+
54+
func (a *AtlasDatabaseUserBySpecUsernameIndexer) Keys(object client.Object) []string {
55+
user, ok := object.(*akov2.AtlasDatabaseUser)
56+
if !ok {
57+
a.logger.Errorf("expected *v1.AtlasDatabaseUser but got %T", object)
58+
return nil
59+
}
60+
61+
username := user.Spec.Username
62+
if username == "" {
63+
return nil
64+
}
65+
66+
// First check ExternalProjectRef
67+
if user.Spec.ExternalProjectRef != nil && user.Spec.ExternalProjectRef.ID != "" {
68+
return []string{fmt.Sprintf("%s/%s", user.Spec.ExternalProjectRef.ID, username)}
69+
}
70+
71+
// Fallback to resolving ProjectRef (name-based)
72+
if user.Spec.ProjectRef != nil && user.Spec.ProjectRef.Name != "" {
73+
project := &akov2.AtlasProject{}
74+
err := a.client.Get(a.ctx, *user.Spec.ProjectRef.GetObject(user.Namespace), project)
75+
if err != nil {
76+
a.logger.Errorf("unable to find project to index: %s", err)
77+
return nil
78+
}
79+
80+
if project.ID() != "" {
81+
return []string{fmt.Sprintf("%s/%s", project.ID(), username)}
82+
}
83+
}
84+
85+
return nil
86+
}
Lines changed: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,199 @@
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+
)
36+
37+
func TestAtlasDatabaseUserBySpecUsernameIndexer(t *testing.T) {
38+
tests := map[string]struct {
39+
object client.Object
40+
expectedKeys []string
41+
expectedLogs []observer.LoggedEntry
42+
}{
43+
"should return nil on wrong type": {
44+
object: &akov2.AtlasStreamInstance{},
45+
expectedLogs: []observer.LoggedEntry{
46+
{
47+
Context: []zapcore.Field{},
48+
Entry: zapcore.Entry{LoggerName: AtlasDatabaseUserBySpecUsernameAndProjectID, Level: zap.ErrorLevel, Message: "expected *v1.AtlasDatabaseUser but got *v1.AtlasStreamInstance"},
49+
},
50+
},
51+
},
52+
"should return nil when username is empty": {
53+
object: &akov2.AtlasDatabaseUser{
54+
Spec: akov2.AtlasDatabaseUserSpec{
55+
Username: "",
56+
},
57+
},
58+
expectedLogs: []observer.LoggedEntry{},
59+
},
60+
"should return nil when there are no references": {
61+
object: &akov2.AtlasDatabaseUser{
62+
Spec: akov2.AtlasDatabaseUserSpec{
63+
Username: "user",
64+
},
65+
},
66+
expectedLogs: []observer.LoggedEntry{},
67+
},
68+
"should return nil when there is an empty external reference": {
69+
object: &akov2.AtlasDatabaseUser{
70+
Spec: akov2.AtlasDatabaseUserSpec{
71+
Username: "user",
72+
ProjectDualReference: akov2.ProjectDualReference{
73+
ExternalProjectRef: &akov2.ExternalProjectReference{},
74+
},
75+
},
76+
},
77+
expectedLogs: []observer.LoggedEntry{},
78+
},
79+
"should return key with external project ID": {
80+
object: &akov2.AtlasDatabaseUser{
81+
Spec: akov2.AtlasDatabaseUserSpec{
82+
Username: "user",
83+
ProjectDualReference: akov2.ProjectDualReference{
84+
ExternalProjectRef: &akov2.ExternalProjectReference{
85+
ID: "external-project-id",
86+
},
87+
},
88+
},
89+
},
90+
expectedKeys: []string{"external-project-id/user"},
91+
expectedLogs: []observer.LoggedEntry{},
92+
},
93+
"should return nil when internal projectRef is empty": {
94+
object: &akov2.AtlasDatabaseUser{
95+
Spec: akov2.AtlasDatabaseUserSpec{
96+
Username: "user",
97+
ProjectDualReference: akov2.ProjectDualReference{
98+
ProjectRef: &common.ResourceRefNamespaced{},
99+
},
100+
},
101+
},
102+
expectedLogs: []observer.LoggedEntry{},
103+
},
104+
"should return key from internal projectRef (same ns)": {
105+
object: &akov2.AtlasDatabaseUser{
106+
ObjectMeta: metav1.ObjectMeta{
107+
Name: "user",
108+
Namespace: "ns",
109+
},
110+
Spec: akov2.AtlasDatabaseUserSpec{
111+
Username: "user",
112+
ProjectDualReference: akov2.ProjectDualReference{
113+
ProjectRef: &common.ResourceRefNamespaced{
114+
Name: "internal-project-id",
115+
},
116+
},
117+
},
118+
},
119+
expectedKeys: []string{"external-project-id/user"},
120+
expectedLogs: []observer.LoggedEntry{},
121+
},
122+
"should return key from internal projectRef (explicit ns)": {
123+
object: &akov2.AtlasDatabaseUser{
124+
ObjectMeta: metav1.ObjectMeta{
125+
Name: "user",
126+
Namespace: "nsUser",
127+
},
128+
Spec: akov2.AtlasDatabaseUserSpec{
129+
Username: "user",
130+
ProjectDualReference: akov2.ProjectDualReference{
131+
ProjectRef: &common.ResourceRefNamespaced{
132+
Name: "internal-project-id",
133+
Namespace: "ns",
134+
},
135+
},
136+
},
137+
},
138+
expectedKeys: []string{"external-project-id/user"},
139+
expectedLogs: []observer.LoggedEntry{},
140+
},
141+
"should log error if internal project not found": {
142+
object: &akov2.AtlasDatabaseUser{
143+
ObjectMeta: metav1.ObjectMeta{
144+
Name: "user",
145+
Namespace: "ns",
146+
},
147+
Spec: akov2.AtlasDatabaseUserSpec{
148+
Username: "user",
149+
ProjectDualReference: akov2.ProjectDualReference{
150+
ProjectRef: &common.ResourceRefNamespaced{
151+
Name: "not-found-project",
152+
},
153+
},
154+
},
155+
},
156+
expectedLogs: []observer.LoggedEntry{
157+
{
158+
Context: []zapcore.Field{},
159+
Entry: zapcore.Entry{LoggerName: AtlasDatabaseUserBySpecUsernameAndProjectID, Level: zap.ErrorLevel, Message: "unable to find project to index: atlasprojects.atlas.mongodb.com \"not-found-project\" not found"},
160+
},
161+
},
162+
},
163+
}
164+
165+
for name, tt := range tests {
166+
t.Run(name, func(t *testing.T) {
167+
project := &akov2.AtlasProject{
168+
ObjectMeta: metav1.ObjectMeta{
169+
Name: "internal-project-id",
170+
Namespace: "ns",
171+
},
172+
Spec: akov2.AtlasProjectSpec{
173+
Name: "Some Project",
174+
},
175+
Status: status.AtlasProjectStatus{
176+
ID: "external-project-id",
177+
},
178+
}
179+
180+
scheme := runtime.NewScheme()
181+
assert.NoError(t, akov2.AddToScheme(scheme))
182+
183+
client := fake.NewClientBuilder().
184+
WithScheme(scheme).
185+
WithObjects(project).
186+
WithStatusSubresource(project).
187+
Build()
188+
189+
core, logs := observer.New(zap.DebugLevel)
190+
indexer := NewAtlasDatabaseUserBySpecUsernameIndexer(context.Background(), client, zap.New(core))
191+
192+
keys := indexer.Keys(tt.object)
193+
sort.Strings(keys)
194+
195+
assert.Equal(t, tt.expectedKeys, keys)
196+
assert.Equal(t, tt.expectedLogs, logs.AllUntimed())
197+
})
198+
}
199+
}
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
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+
21+
"go.uber.org/zap"
22+
"sigs.k8s.io/controller-runtime/pkg/client"
23+
24+
akov2 "github.com/mongodb/mongodb-atlas-kubernetes/v2/api/v1"
25+
)
26+
27+
const (
28+
AtlasDeploymentByProject = "atlasdeployment.spec.projectRef,externalProjectID"
29+
)
30+
31+
type AtlasDeploymentByProjectIndexer struct {
32+
ctx context.Context
33+
client client.Client
34+
logger *zap.SugaredLogger
35+
}
36+
37+
func NewAtlasDeploymentByProjectIndexer(ctx context.Context, client client.Client, logger *zap.Logger) *AtlasDeploymentByProjectIndexer {
38+
return &AtlasDeploymentByProjectIndexer{
39+
ctx: ctx,
40+
client: client,
41+
logger: logger.Named(AtlasDeploymentByProject).Sugar(),
42+
}
43+
}
44+
45+
func (*AtlasDeploymentByProjectIndexer) Object() client.Object {
46+
return &akov2.AtlasDeployment{}
47+
}
48+
49+
func (*AtlasDeploymentByProjectIndexer) Name() string {
50+
return AtlasDeploymentByProject
51+
}
52+
53+
func (a *AtlasDeploymentByProjectIndexer) Keys(object client.Object) []string {
54+
deployment, ok := object.(*akov2.AtlasDeployment)
55+
if !ok {
56+
a.logger.Errorf("expected *v1.AtlasDeployment but got %T", object)
57+
return nil
58+
}
59+
60+
if deployment.Spec.ExternalProjectRef != nil && deployment.Spec.ExternalProjectRef.ID != "" {
61+
return []string{deployment.Spec.ExternalProjectRef.ID}
62+
}
63+
64+
if deployment.Spec.ProjectRef != nil && deployment.Spec.ProjectRef.Name != "" {
65+
project := &akov2.AtlasProject{}
66+
err := a.client.Get(a.ctx, *deployment.Spec.ProjectRef.GetObject(deployment.Namespace), project)
67+
if err != nil {
68+
a.logger.Errorf("unable to find project to index: %s", err)
69+
70+
return nil
71+
}
72+
73+
if project.ID() != "" {
74+
return []string{project.ID()}
75+
}
76+
}
77+
78+
return nil
79+
}

0 commit comments

Comments
 (0)