Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

pkg/search/backendstore: unit test store #5811

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
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
}
Loading