Skip to content

Commit

Permalink
pkg/search/backendstore: unit test store
Browse files Browse the repository at this point in the history
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 <mohamedmohey2352@gmail.com>
  • Loading branch information
mohamedawnallah committed Nov 16, 2024
1 parent 3acc14c commit 89e2693
Show file tree
Hide file tree
Showing 3 changed files with 367 additions and 3 deletions.
6 changes: 5 additions & 1 deletion pkg/search/backendstore/opensearch.go
Original file line number Diff line number Diff line change
Expand Up @@ -282,6 +282,10 @@ func (os *OpenSearch) indexName(us *unstructured.Unstructured) (string, error) {
return name, nil
}

var openSearchClientBuilder = func(cfg opensearch.Config) (*opensearch.Client, error) {
return opensearch.NewClient(cfg)
}

func (os *OpenSearch) initClient(bsc *searchv1alpha1.BackendStoreConfig) error {
if bsc == nil || bsc.OpenSearch == nil {
return errors.New("opensearch config is nil")
Expand Down Expand Up @@ -312,7 +316,7 @@ func (os *OpenSearch) initClient(bsc *searchv1alpha1.BackendStoreConfig) error {
cfg.Password = pwd
}

client, err := opensearch.NewClient(cfg)
client, err := openSearchClientBuilder(cfg)
if err != nil {
return fmt.Errorf("cannot create opensearch client: %v", err)
}
Expand Down
4 changes: 2 additions & 2 deletions pkg/search/backendstore/store.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,11 @@ type BackendStore interface {
var (
backendLock sync.Mutex
backends map[string]BackendStore
k8sClient *kubernetes.Clientset
k8sClient kubernetes.Interface
)

// Init init backend store manager
func Init(cs *kubernetes.Clientset) {
func Init(cs kubernetes.Interface) {
backendLock.Lock()
backends = make(map[string]BackendStore)
k8sClient = cs
Expand Down
360 changes: 360 additions & 0 deletions pkg/search/backendstore/store_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,360 @@
/*
Copyright 2024 The Karmada Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package backendstore

import (
"context"
"encoding/base64"
"fmt"
"net/http"
"reflect"
"sync"
"testing"

"github.com/opensearch-project/opensearch-go"
"github.com/opensearch-project/opensearch-go/opensearchapi"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
clientset "k8s.io/client-go/kubernetes"
fakeclientset "k8s.io/client-go/kubernetes/fake"

clusterv1alpha1 "github.com/karmada-io/karmada/pkg/apis/cluster/v1alpha1"
searchv1alpha1 "github.com/karmada-io/karmada/pkg/apis/search/v1alpha1"
)

// mockTransport is a mock implementation of opensearchtransport.Interface for testing purposes.
type mockTransport struct {
PerformFunc func(req *http.Request) (*http.Response, error)
}

// Perform calls the mock PerformFunc if set; otherwise, it returns a default response.
func (m mockTransport) Perform(req *http.Request) (*http.Response, error) {
if m.PerformFunc != nil {
return m.PerformFunc(req)
}
return &http.Response{
StatusCode: 200,
Body: http.NoBody,
}, nil
}

func TestInitBackendStoreManager(t *testing.T) {
tests := []struct {
name string
numGoroutines int
client clientset.Interface
wrapInit func(client clientset.Interface, numGoroutines int) error
}{
{
name: "Init_WithNoConcurrentCalls_Initialized",
client: fakeclientset.NewSimpleClientset(),
wrapInit: func(client clientset.Interface, _ int) error {
Init(client)
return nil
},
},
{
name: "Init_WithMultipleConcurrentCalls_Initialized",
numGoroutines: 5,
client: fakeclientset.NewSimpleClientset(),
wrapInit: func(client clientset.Interface, numGoroutines int) error {
// Use sync.WaitGroup to wait for all goroutines to finish.
var wg sync.WaitGroup

// Call Init concurrently from multiple goroutines.
for i := 0; i < numGoroutines; i++ {
wg.Add(1)
go func() {
defer wg.Done()
Init(client)
}()
}

// Wait for all goroutines to finish.
wg.Wait()

return nil
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
if err := test.wrapInit(test.client, test.numGoroutines); err != nil {
t.Fatalf("failed to init backend store manager, got: %v", err)
}
if err := verifyInitBackendStoreManager(test.client); err != nil {
t.Errorf("failed to verify init backend store manager, got: %v", err)
}
})
}
}

func TestAddBackendStore(t *testing.T) {
secretName, namespace := "opensearch-credentials", "default"
tests := []struct {
name string
clusterName string
numGoroutines int
cfg *searchv1alpha1.BackendStoreConfig
client clientset.Interface
wrapAddBackend func(clusterName string, cfg *searchv1alpha1.BackendStoreConfig, numGoroutines int) error
prep func(clientset.Interface) error
}{
{
name: "AddBackend_DefaultBackend_DefaultBackendStoreAdded",
clusterName: "member1",
client: fakeclientset.NewSimpleClientset(),
wrapAddBackend: func(clusterName string, cfg *searchv1alpha1.BackendStoreConfig, _ int) error {
AddBackend(clusterName, cfg)
return nil
},
prep: func(clientset.Interface) error { return nil },
},
{
name: "AddBackend_OpenSearchBackend_OpenSearchBackendStoreAdded",
clusterName: "member1",
client: fakeclientset.NewSimpleClientset(),
numGoroutines: 5,
cfg: &searchv1alpha1.BackendStoreConfig{
OpenSearch: &searchv1alpha1.OpenSearchConfig{
Addresses: []string{"https://10.0.0.1:9200"},
SecretRef: clusterv1alpha1.LocalSecretReference{
Name: secretName,
Namespace: namespace,
},
},
},
wrapAddBackend: func(clusterName string, cfg *searchv1alpha1.BackendStoreConfig, numGoroutines int) error {
// Use sync.WaitGroup to wait for all goroutines to finish.
var wg sync.WaitGroup

// Call Init concurrently from multiple goroutines.
for i := 0; i < numGoroutines; i++ {
wg.Add(1)
go func() {
defer wg.Done()
AddBackend(clusterName, cfg)
}()
}

// Wait for all goroutines to finish.
wg.Wait()

return nil
},
prep: func(client clientset.Interface) error {
username, password := "opensearchuser", "opensearchpass"
if err := createOpenSearchCredentialsSecret(client, username, password, secretName, namespace); err != nil {
return fmt.Errorf("failed to create open search credentials secret, got: %v", err)
}
openSearchClientBuilder = func(opensearch.Config) (*opensearch.Client, error) {
client := &opensearch.Client{Transport: mockTransport{}}
client.API = opensearchapi.New(client)
return client, nil
}
return nil
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
Init(test.client)
if err := test.prep(test.client); err != nil {
t.Fatalf("failed to prep test environment before verifying adding the backend store, got: %v", err)
}
if err := test.wrapAddBackend(test.clusterName, test.cfg, test.numGoroutines); err != nil {
t.Fatalf("failed to add backend, got error: %v", err)
}
if err := verifyBackendStoreAdded(test.clusterName); err != nil {
t.Errorf("failed to verify adding the backend store, got: %v", err)
}
})
}
}

func TestDeleteBackendStore(t *testing.T) {
tests := []struct {
name string
clusterName string
client clientset.Interface
prep func(clusterName string) error
}{
{
name: "DeleteBackendStore_Existent_BackendStoreDeleted",
clusterName: "member1",
client: fakeclientset.NewSimpleClientset(),
prep: func(clusterName string) error {
AddBackend(clusterName, nil)
return nil
},
},
{
name: "DeleteBackendStore_NonExistent_NoError",
clusterName: "nonexistent",
client: fakeclientset.NewSimpleClientset(),
prep: func(string) error { return nil },
},
}

for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
Init(test.client)
if err := test.prep(test.clusterName); err != nil {
t.Fatalf("failed to prep test environment before deleting backend store, got: %v", err)
}
DeleteBackend(test.clusterName)
if err := verifyBackendStoreDeleted(test.clusterName); err != nil {
t.Errorf("failed to verify backend store deleted for cluster name %s, got: %v", test.clusterName, err)
}
})
}
}

func TestGetBackendStore(t *testing.T) {
tests := []struct {
name string
clusterName string
client clientset.Interface
numGoroutines int
wrapGetBackend func(clusterName string, numGoroutines int) BackendStore
prep func(clusterName string) error
want BackendStore
wantBackendStore bool
}{
{
name: "GetBackendStore_Existent_BackendStoreRetrieved",
clusterName: "member1",
client: fakeclientset.NewSimpleClientset(),
wrapGetBackend: func(clusterName string, _ int) BackendStore {
return GetBackend(clusterName)
},
prep: func(clusterName string) error {
AddBackend(clusterName, nil)
return nil
},
wantBackendStore: true,
},
{
name: "GetBackendStore_ExistentWithMultipleConcurrentCalls_BackendStoreRetrieved",
clusterName: "member1",
client: fakeclientset.NewSimpleClientset(),
numGoroutines: 5,
wrapGetBackend: func(clusterName string, numGoroutines int) BackendStore {
// Use sync.WaitGroup to wait for all goroutines to finish.
var wg sync.WaitGroup

// Call Init concurrently from multiple goroutines.
for i := 0; i < numGoroutines; i++ {
wg.Add(1)
go func() {
defer wg.Done()
_ = GetBackend(clusterName)
}()
}

// Wait for all goroutines to finish.
wg.Wait()

return GetBackend(clusterName)
},
prep: func(clusterName string) error {
AddBackend(clusterName, nil)
return nil
},
wantBackendStore: true,
},
{
name: "GetBackendStore_NonExistent_NoError",
clusterName: "nonexistent",
client: fakeclientset.NewSimpleClientset(),
wrapGetBackend: func(clusterName string, _ int) BackendStore {
return GetBackend(clusterName)
},
prep: func(string) error { return nil },
wantBackendStore: false,
},
}

for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
Init(test.client)
if err := test.prep(test.clusterName); err != nil {
t.Fatalf("failed to prep test environment before getting backend store, got: %v", err)
}
bc := test.wrapGetBackend(test.clusterName, test.numGoroutines)
if bc == nil && test.wantBackendStore {
t.Error("expected backend store, but got none")
}
if bc != nil && !test.wantBackendStore {
t.Errorf("unexpected backend store retrieved, got: %v", bc)
}
})
}
}

func verifyInitBackendStoreManager(client clientset.Interface) error {
if backends == nil {
return fmt.Errorf("expected backends to be initialized, but got %v", backends)
}
if len(backends) != 0 {
return fmt.Errorf("expected backends to be empty, but got %d backends", len(backends))
}
if !reflect.DeepEqual(client, k8sClient) {
return fmt.Errorf("expected k8s client global varible to be %v, but got %v", client, k8sClient)
}
return nil
}

func verifyBackendStoreAdded(clusterName string) error {
_, ok := backends[clusterName]
if !ok {
return fmt.Errorf("expected cluster name %s to be in the backend store", clusterName)
}
return nil
}

func verifyBackendStoreDeleted(clusterName string) error {
_, ok := backends[clusterName]
if ok {
return fmt.Errorf("expected backend store for cluster %s to be deleted, but it still exists", clusterName)
}
return nil
}

func createOpenSearchCredentialsSecret(client clientset.Interface, username, password, secretName, namespace string) error {
userNameEncoded := base64.StdEncoding.EncodeToString([]byte(username))
passwordEncoded := base64.StdEncoding.EncodeToString([]byte(password))
secret := &corev1.Secret{
TypeMeta: metav1.TypeMeta{
APIVersion: "v1",
Kind: "Secret",
},
ObjectMeta: metav1.ObjectMeta{
Name: secretName,
Namespace: namespace,
},
Type: corev1.SecretTypeOpaque,
Data: map[string][]byte{
"username": []byte(userNameEncoded),
"password": []byte(passwordEncoded),
},
}
if _, err := client.CoreV1().Secrets(namespace).Create(context.TODO(), secret, metav1.CreateOptions{}); err != nil {
return fmt.Errorf("failed to create secret %s in namespace %s, got error: %v", secretName, namespace, err)
}
return nil
}

0 comments on commit 89e2693

Please sign in to comment.