Skip to content

Commit 1d4b57c

Browse files
pkg/search/backendstore: unit test store
In this commit, we unit test backend store in karmada search on init backendstore manager, add backendstore, delete backendstore, and on get backendstore operations in addition to testing the concurrent calls on those ops. Signed-off-by: Mohamed Awnallah <[email protected]>
1 parent 93f07b5 commit 1d4b57c

File tree

3 files changed

+367
-3
lines changed

3 files changed

+367
-3
lines changed

pkg/search/backendstore/opensearch.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -282,6 +282,10 @@ func (os *OpenSearch) indexName(us *unstructured.Unstructured) (string, error) {
282282
return name, nil
283283
}
284284

285+
var openSearchClientBuilder = func(cfg opensearch.Config) (*opensearch.Client, error) {
286+
return opensearch.NewClient(cfg)
287+
}
288+
285289
func (os *OpenSearch) initClient(bsc *searchv1alpha1.BackendStoreConfig) error {
286290
if bsc == nil || bsc.OpenSearch == nil {
287291
return errors.New("opensearch config is nil")
@@ -312,7 +316,7 @@ func (os *OpenSearch) initClient(bsc *searchv1alpha1.BackendStoreConfig) error {
312316
cfg.Password = pwd
313317
}
314318

315-
client, err := opensearch.NewClient(cfg)
319+
client, err := openSearchClientBuilder(cfg)
316320
if err != nil {
317321
return fmt.Errorf("cannot create opensearch client: %v", err)
318322
}

pkg/search/backendstore/store.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,11 +35,11 @@ type BackendStore interface {
3535
var (
3636
backendLock sync.Mutex
3737
backends map[string]BackendStore
38-
k8sClient *kubernetes.Clientset
38+
k8sClient kubernetes.Interface
3939
)
4040

4141
// Init init backend store manager
42-
func Init(cs *kubernetes.Clientset) {
42+
func Init(cs kubernetes.Interface) {
4343
backendLock.Lock()
4444
backends = make(map[string]BackendStore)
4545
k8sClient = cs

pkg/search/backendstore/store_test.go

Lines changed: 360 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,360 @@
1+
/*
2+
Copyright 2024 The Karmada Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package backendstore
18+
19+
import (
20+
"context"
21+
"encoding/base64"
22+
"fmt"
23+
"net/http"
24+
"reflect"
25+
"sync"
26+
"testing"
27+
28+
"github.com/opensearch-project/opensearch-go"
29+
"github.com/opensearch-project/opensearch-go/opensearchapi"
30+
corev1 "k8s.io/api/core/v1"
31+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
32+
clientset "k8s.io/client-go/kubernetes"
33+
fakeclientset "k8s.io/client-go/kubernetes/fake"
34+
35+
clusterv1alpha1 "github.com/karmada-io/karmada/pkg/apis/cluster/v1alpha1"
36+
searchv1alpha1 "github.com/karmada-io/karmada/pkg/apis/search/v1alpha1"
37+
)
38+
39+
// mockTransport is a mock implementation of opensearchtransport.Interface for testing purposes.
40+
type mockTransport struct {
41+
PerformFunc func(req *http.Request) (*http.Response, error)
42+
}
43+
44+
// Perform calls the mock PerformFunc if set; otherwise, it returns a default response.
45+
func (m mockTransport) Perform(req *http.Request) (*http.Response, error) {
46+
if m.PerformFunc != nil {
47+
return m.PerformFunc(req)
48+
}
49+
return &http.Response{
50+
StatusCode: 200,
51+
Body: http.NoBody,
52+
}, nil
53+
}
54+
55+
func TestInitBackendStoreManager(t *testing.T) {
56+
tests := []struct {
57+
name string
58+
numGoroutines int
59+
client clientset.Interface
60+
wrapInit func(client clientset.Interface, numGoroutines int) error
61+
}{
62+
{
63+
name: "Init_WithNoConcurrentCalls_Initialized",
64+
client: fakeclientset.NewSimpleClientset(),
65+
wrapInit: func(client clientset.Interface, _ int) error {
66+
Init(client)
67+
return nil
68+
},
69+
},
70+
{
71+
name: "Init_WithMultipleConcurrentCalls_Initialized",
72+
numGoroutines: 5,
73+
client: fakeclientset.NewSimpleClientset(),
74+
wrapInit: func(client clientset.Interface, numGoroutines int) error {
75+
// Use sync.WaitGroup to wait for all goroutines to finish.
76+
var wg sync.WaitGroup
77+
78+
// Call Init concurrently from multiple goroutines.
79+
for i := 0; i < numGoroutines; i++ {
80+
wg.Add(1)
81+
go func() {
82+
defer wg.Done()
83+
Init(client)
84+
}()
85+
}
86+
87+
// Wait for all goroutines to finish.
88+
wg.Wait()
89+
90+
return nil
91+
},
92+
},
93+
}
94+
for _, test := range tests {
95+
t.Run(test.name, func(t *testing.T) {
96+
if err := test.wrapInit(test.client, test.numGoroutines); err != nil {
97+
t.Fatalf("failed to init backend store manager, got: %v", err)
98+
}
99+
if err := verifyInitBackendStoreManager(test.client); err != nil {
100+
t.Errorf("failed to verify init backend store manager, got: %v", err)
101+
}
102+
})
103+
}
104+
}
105+
106+
func TestAddBackendStore(t *testing.T) {
107+
secretName, namespace := "opensearch-credentials", "default"
108+
tests := []struct {
109+
name string
110+
clusterName string
111+
numGoroutines int
112+
cfg *searchv1alpha1.BackendStoreConfig
113+
client clientset.Interface
114+
wrapAddBackend func(clusterName string, cfg *searchv1alpha1.BackendStoreConfig, numGoroutines int) error
115+
prep func(clientset.Interface) error
116+
}{
117+
{
118+
name: "AddBackend_DefaultBackend_DefaultBackendStoreAdded",
119+
clusterName: "member1",
120+
client: fakeclientset.NewSimpleClientset(),
121+
wrapAddBackend: func(clusterName string, cfg *searchv1alpha1.BackendStoreConfig, _ int) error {
122+
AddBackend(clusterName, cfg)
123+
return nil
124+
},
125+
prep: func(clientset.Interface) error { return nil },
126+
},
127+
{
128+
name: "AddBackend_OpenSearchBackend_OpenSearchBackendStoreAdded",
129+
clusterName: "member1",
130+
client: fakeclientset.NewSimpleClientset(),
131+
numGoroutines: 5,
132+
cfg: &searchv1alpha1.BackendStoreConfig{
133+
OpenSearch: &searchv1alpha1.OpenSearchConfig{
134+
Addresses: []string{"https://10.0.0.1:9200"},
135+
SecretRef: clusterv1alpha1.LocalSecretReference{
136+
Name: secretName,
137+
Namespace: namespace,
138+
},
139+
},
140+
},
141+
wrapAddBackend: func(clusterName string, cfg *searchv1alpha1.BackendStoreConfig, numGoroutines int) error {
142+
// Use sync.WaitGroup to wait for all goroutines to finish.
143+
var wg sync.WaitGroup
144+
145+
// Call Init concurrently from multiple goroutines.
146+
for i := 0; i < numGoroutines; i++ {
147+
wg.Add(1)
148+
go func() {
149+
defer wg.Done()
150+
AddBackend(clusterName, cfg)
151+
}()
152+
}
153+
154+
// Wait for all goroutines to finish.
155+
wg.Wait()
156+
157+
return nil
158+
},
159+
prep: func(client clientset.Interface) error {
160+
username, password := "opensearchuser", "opensearchpass"
161+
if err := createOpenSearchCredentialsSecret(client, username, password, secretName, namespace); err != nil {
162+
return fmt.Errorf("failed to create open search credentials secret, got: %v", err)
163+
}
164+
openSearchClientBuilder = func(opensearch.Config) (*opensearch.Client, error) {
165+
client := &opensearch.Client{Transport: mockTransport{}}
166+
client.API = opensearchapi.New(client)
167+
return client, nil
168+
}
169+
return nil
170+
},
171+
},
172+
}
173+
for _, test := range tests {
174+
t.Run(test.name, func(t *testing.T) {
175+
Init(test.client)
176+
if err := test.prep(test.client); err != nil {
177+
t.Fatalf("failed to prep test environment before verifying adding the backend store, got: %v", err)
178+
}
179+
if err := test.wrapAddBackend(test.clusterName, test.cfg, test.numGoroutines); err != nil {
180+
t.Fatalf("failed to add backend, got error: %v", err)
181+
}
182+
if err := verifyBackendStoreAdded(test.clusterName); err != nil {
183+
t.Errorf("failed to verify adding the backend store, got: %v", err)
184+
}
185+
})
186+
}
187+
}
188+
189+
func TestDeleteBackendStore(t *testing.T) {
190+
tests := []struct {
191+
name string
192+
clusterName string
193+
client clientset.Interface
194+
prep func(clusterName string) error
195+
}{
196+
{
197+
name: "DeleteBackendStore_Existent_BackendStoreDeleted",
198+
clusterName: "member1",
199+
client: fakeclientset.NewSimpleClientset(),
200+
prep: func(clusterName string) error {
201+
AddBackend(clusterName, nil)
202+
return nil
203+
},
204+
},
205+
{
206+
name: "DeleteBackendStore_NonExistent_NoError",
207+
clusterName: "nonexistent",
208+
client: fakeclientset.NewSimpleClientset(),
209+
prep: func(string) error { return nil },
210+
},
211+
}
212+
213+
for _, test := range tests {
214+
t.Run(test.name, func(t *testing.T) {
215+
Init(test.client)
216+
if err := test.prep(test.clusterName); err != nil {
217+
t.Fatalf("failed to prep test environment before deleting backend store, got: %v", err)
218+
}
219+
DeleteBackend(test.clusterName)
220+
if err := verifyBackendStoreDeleted(test.clusterName); err != nil {
221+
t.Errorf("failed to verify backend store deleted for cluster name %s, got: %v", test.clusterName, err)
222+
}
223+
})
224+
}
225+
}
226+
227+
func TestGetBackendStore(t *testing.T) {
228+
tests := []struct {
229+
name string
230+
clusterName string
231+
client clientset.Interface
232+
numGoroutines int
233+
wrapGetBackend func(clusterName string, numGoroutines int) BackendStore
234+
prep func(clusterName string) error
235+
want BackendStore
236+
wantBackendStore bool
237+
}{
238+
{
239+
name: "GetBackendStore_Existent_BackendStoreRetrieved",
240+
clusterName: "member1",
241+
client: fakeclientset.NewSimpleClientset(),
242+
wrapGetBackend: func(clusterName string, _ int) BackendStore {
243+
return GetBackend(clusterName)
244+
},
245+
prep: func(clusterName string) error {
246+
AddBackend(clusterName, nil)
247+
return nil
248+
},
249+
wantBackendStore: true,
250+
},
251+
{
252+
name: "GetBackendStore_ExistentWithMultipleConcurrentCalls_BackendStoreRetrieved",
253+
clusterName: "member1",
254+
client: fakeclientset.NewSimpleClientset(),
255+
numGoroutines: 5,
256+
wrapGetBackend: func(clusterName string, numGoroutines int) BackendStore {
257+
// Use sync.WaitGroup to wait for all goroutines to finish.
258+
var wg sync.WaitGroup
259+
260+
// Call Init concurrently from multiple goroutines.
261+
for i := 0; i < numGoroutines; i++ {
262+
wg.Add(1)
263+
go func() {
264+
defer wg.Done()
265+
_ = GetBackend(clusterName)
266+
}()
267+
}
268+
269+
// Wait for all goroutines to finish.
270+
wg.Wait()
271+
272+
return GetBackend(clusterName)
273+
},
274+
prep: func(clusterName string) error {
275+
AddBackend(clusterName, nil)
276+
return nil
277+
},
278+
wantBackendStore: true,
279+
},
280+
{
281+
name: "GetBackendStore_NonExistent_NoError",
282+
clusterName: "nonexistent",
283+
client: fakeclientset.NewSimpleClientset(),
284+
wrapGetBackend: func(clusterName string, _ int) BackendStore {
285+
return GetBackend(clusterName)
286+
},
287+
prep: func(string) error { return nil },
288+
wantBackendStore: false,
289+
},
290+
}
291+
292+
for _, test := range tests {
293+
t.Run(test.name, func(t *testing.T) {
294+
Init(test.client)
295+
if err := test.prep(test.clusterName); err != nil {
296+
t.Fatalf("failed to prep test environment before getting backend store, got: %v", err)
297+
}
298+
bc := test.wrapGetBackend(test.clusterName, test.numGoroutines)
299+
if bc == nil && test.wantBackendStore {
300+
t.Error("expected backend store, but got none")
301+
}
302+
if bc != nil && !test.wantBackendStore {
303+
t.Errorf("unexpected backend store retrieved, got: %v", bc)
304+
}
305+
})
306+
}
307+
}
308+
309+
func verifyInitBackendStoreManager(client clientset.Interface) error {
310+
if backends == nil {
311+
return fmt.Errorf("expected backends to be initialized, but got %v", backends)
312+
}
313+
if len(backends) != 0 {
314+
return fmt.Errorf("expected backends to be empty, but got %d backends", len(backends))
315+
}
316+
if !reflect.DeepEqual(client, k8sClient) {
317+
return fmt.Errorf("expected k8s client global varible to be %v, but got %v", client, k8sClient)
318+
}
319+
return nil
320+
}
321+
322+
func verifyBackendStoreAdded(clusterName string) error {
323+
_, ok := backends[clusterName]
324+
if !ok {
325+
return fmt.Errorf("expected cluster name %s to be in the backend store", clusterName)
326+
}
327+
return nil
328+
}
329+
330+
func verifyBackendStoreDeleted(clusterName string) error {
331+
_, ok := backends[clusterName]
332+
if ok {
333+
return fmt.Errorf("expected backend store for cluster %s to be deleted, but it still exists", clusterName)
334+
}
335+
return nil
336+
}
337+
338+
func createOpenSearchCredentialsSecret(client clientset.Interface, username, password, secretName, namespace string) error {
339+
userNameEncoded := base64.StdEncoding.EncodeToString([]byte(username))
340+
passwordEncoded := base64.StdEncoding.EncodeToString([]byte(password))
341+
secret := &corev1.Secret{
342+
TypeMeta: metav1.TypeMeta{
343+
APIVersion: "v1",
344+
Kind: "Secret",
345+
},
346+
ObjectMeta: metav1.ObjectMeta{
347+
Name: secretName,
348+
Namespace: namespace,
349+
},
350+
Type: corev1.SecretTypeOpaque,
351+
Data: map[string][]byte{
352+
"username": []byte(userNameEncoded),
353+
"password": []byte(passwordEncoded),
354+
},
355+
}
356+
if _, err := client.CoreV1().Secrets(namespace).Create(context.TODO(), secret, metav1.CreateOptions{}); err != nil {
357+
return fmt.Errorf("failed to create secret %s in namespace %s, got error: %v", secretName, namespace, err)
358+
}
359+
return nil
360+
}

0 commit comments

Comments
 (0)