diff --git a/operator/pkg/tasks/init/apiserver_test.go b/operator/pkg/tasks/init/apiserver_test.go new file mode 100644 index 000000000000..3d71ee64ebb3 --- /dev/null +++ b/operator/pkg/tasks/init/apiserver_test.go @@ -0,0 +1,449 @@ +/* +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 tasks + +import ( + "fmt" + "strings" + "testing" + "time" + + corev1 "k8s.io/api/core/v1" + fakeclientset "k8s.io/client-go/kubernetes/fake" + "k8s.io/client-go/rest" + "k8s.io/utils/ptr" + + operatorv1alpha1 "github.com/karmada-io/karmada/operator/pkg/apis/operator/v1alpha1" + "github.com/karmada-io/karmada/operator/pkg/constants" + "github.com/karmada-io/karmada/operator/pkg/util" + "github.com/karmada-io/karmada/operator/pkg/util/apiclient" + "github.com/karmada-io/karmada/operator/pkg/workflow" +) + +func TestNewAPIServerTask(t *testing.T) { + tests := []struct { + name string + wantTask workflow.Task + }{ + { + name: "NewKarmadaApiserverTask_IsCalled_ExpectedWorkflowTask", + wantTask: workflow.Task{ + Name: "apiserver", + Run: runApiserver, + RunSubTasks: true, + Tasks: []workflow.Task{ + { + Name: constants.KarmadaAPIserverComponent, + Run: runKarmadaAPIServer, + }, + { + Name: fmt.Sprintf("%s-%s", "wait", constants.KarmadaAPIserverComponent), + Run: runWaitKarmadaAPIServer, + }, + }, + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + karmadaAPIServerTask := NewKarmadaApiserverTask() + err := util.DeepEqualTasks(karmadaAPIServerTask, test.wantTask) + if err != nil { + t.Errorf("unexpected error, got %v", err) + } + }) + } +} + +func TestNewKarmadaAggregatedApiserverTask(t *testing.T) { + tests := []struct { + name string + wantTask workflow.Task + }{ + { + name: "NewKarmadaAggregatedApiserverTask_IsCalled_ExpectedWorkflowTask", + wantTask: workflow.Task{ + Name: "aggregated-apiserver", + Run: runAggregatedApiserver, + RunSubTasks: true, + Tasks: []workflow.Task{ + { + Name: constants.KarmadaAggregatedAPIServerComponent, + Run: runKarmadaAggregatedAPIServer, + }, + { + Name: fmt.Sprintf("%s-%s", "wait", constants.KarmadaAggregatedAPIServerComponent), + Run: runWaitKarmadaAggregatedAPIServer, + }, + }, + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + karmadaAggregatedAPIServerTask := NewKarmadaAggregatedApiserverTask() + err := util.DeepEqualTasks(karmadaAggregatedAPIServerTask, test.wantTask) + if err != nil { + t.Errorf("unexpected error, got %v", err) + } + }) + } +} + +func TestRunAggregatedAPIServer(t *testing.T) { + tests := []struct { + name string + runData workflow.RunData + wantErr bool + errMsg string + }{ + { + name: "RunAggregatedApiserver_InvalidTypeAssertion_TypeAssertionFailed", + runData: MyTestData{Data: "test"}, + wantErr: true, + errMsg: "aggregated-apiserver task invoked with an invalid data struct", + }, + { + name: "RunAggregatedApiserver_ValidTypeAssertion_TypeAssertionSuceeded", + runData: &TestInitData{}, + wantErr: false, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + err := runAggregatedApiserver(test.runData) + if err == nil && test.wantErr { + t.Errorf("expected error, but got none") + } + if err != nil && !test.wantErr { + t.Errorf("unexpected error: %v", err) + } + if err != nil && test.wantErr && !strings.Contains(err.Error(), test.errMsg) { + t.Errorf("expected %s error msg to contain %s", err.Error(), test.errMsg) + } + }) + } +} + +func TestRunAPIServer(t *testing.T) { + tests := []struct { + name string + runData workflow.RunData + wantErr bool + errMsg string + }{ + { + name: "RunAPIServer_InvalidTypeAssertion_TypeAssertionFailed", + runData: MyTestData{Data: "test"}, + wantErr: true, + errMsg: "apiserver task invoked with an invalid data struct", + }, + { + name: "RunAPIServer_ValidTypeAssertion_TypeAssertionSuceeded", + runData: &TestInitData{}, + wantErr: false, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + err := runApiserver(test.runData) + if err == nil && test.wantErr { + t.Errorf("expected error, but got none") + } + if err != nil && !test.wantErr { + t.Errorf("unexpected error: %v", err) + } + if err != nil && test.wantErr && !strings.Contains(err.Error(), test.errMsg) { + t.Errorf("expected %s error msg to contain %s", err.Error(), test.errMsg) + } + }) + } +} + +func TestRunKarmadaAPIServer(t *testing.T) { + tests := []struct { + name string + runData workflow.RunData + wantErr bool + errMsg string + }{ + { + name: "RunKarmadaAPIServer_InvalidTypeAssertion_TypeAssertionFailed", + runData: &MyTestData{Data: "test"}, + wantErr: true, + errMsg: "KarmadaApiserver task invoked with an invalid data struct", + }, + { + name: "RunKarmadaAPIServer_NilKarmadaAPIServer_RunIsCompletedWithoutErrors", + runData: &TestInitData{ + ComponentsUnits: &operatorv1alpha1.KarmadaComponents{}, + }, + wantErr: false, + }, + { + name: "RunKarmadaAPIServer_InitializeKarmadaAPIServer_KarmadaAPIServerEnsured", + runData: &TestInitData{ + Name: "karmada-demo", + Namespace: "test", + ComponentsUnits: &operatorv1alpha1.KarmadaComponents{ + KarmadaAPIServer: &operatorv1alpha1.KarmadaAPIServer{ + CommonSettings: operatorv1alpha1.CommonSettings{ + Image: operatorv1alpha1.Image{ImageTag: "karmada-apiserver-image"}, + Replicas: ptr.To[int32](2), + Resources: corev1.ResourceRequirements{}, + ImagePullPolicy: corev1.PullIfNotPresent, + }, + ServiceSubnet: ptr.To("10.96.0.0/12"), + }, + }, + RemoteClientConnector: fakeclientset.NewSimpleClientset(), + FeatureGatesOptions: map[string]bool{ + "Feature1": true, + }, + }, + wantErr: false, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + err := runKarmadaAPIServer(test.runData) + if err == nil && test.wantErr { + t.Errorf("expected error, but got none") + } + if err != nil && !test.wantErr { + t.Errorf("unexpected error: %v", err) + } + if err != nil && test.wantErr && !strings.Contains(err.Error(), test.errMsg) { + t.Errorf("expected %s error msg to contain %s", err.Error(), test.errMsg) + } + }) + } +} + +func TestRunWaitKarmadaAPIServer(t *testing.T) { + tests := []struct { + name string + runData workflow.RunData + prep func(workflow.RunData) error + wantErr bool + errMsg string + }{ + { + name: "RunWaitKarmadaAPIServer_InvalidTypeAssertion_TypeAssertionFailed", + runData: &MyTestData{Data: "test"}, + prep: func(workflow.RunData) error { return nil }, + wantErr: true, + errMsg: "wait-KarmadaAPIServer task invoked with an invalid data struct", + }, + { + name: "RunWaitKarmadaAPIServer_TimeoutWaitingForSomeKarmadaAPIServersPods_Timeout", + runData: &TestInitData{ + Name: "karmada-demo", + Namespace: "test", + RemoteClientConnector: fakeclientset.NewSimpleClientset(), + ControlplaneConfigREST: &rest.Config{}, + FeatureGatesOptions: map[string]bool{ + "Feature1": true, + }, + }, + prep: func(workflow.RunData) error { + componentBeReadyTimeout = time.Second + return nil + }, + wantErr: true, + errMsg: "waiting for karmada-apiserver to ready timeout", + }, + { + name: "RunWaitKarmadaAPIServer_WaitingForSomeKarmadaAPIServersPods_KarmadaAPIServerIsReady", + runData: &TestInitData{ + Name: "karmada-demo", + Namespace: "test", + RemoteClientConnector: fakeclientset.NewSimpleClientset(), + ControlplaneConfigREST: &rest.Config{}, + FeatureGatesOptions: map[string]bool{ + "Feature1": true, + }, + }, + prep: func(rd workflow.RunData) error { + data := rd.(*TestInitData) + _, err := apiclient.CreatePods(data.RemoteClient(), data.Namespace, util.KarmadaAPIServerName(data.Name), 2, karmadaApiserverLabels, true) + return err + }, + wantErr: false, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + if err := test.prep(test.runData); err != nil { + t.Errorf("failed to prep waiting for Karmada APIServer: %v", err) + } + err := runWaitKarmadaAPIServer(test.runData) + if err == nil && test.wantErr { + t.Errorf("expected error, but got none") + } + if err != nil && !test.wantErr { + t.Errorf("unexpected error: %v", err) + } + if err != nil && test.wantErr && !strings.Contains(err.Error(), test.errMsg) { + t.Errorf("expected %s error msg to contain %s", err.Error(), test.errMsg) + } + }) + } +} + +func TestRunKarmadaAggregatedAPIServer(t *testing.T) { + tests := []struct { + name string + runData workflow.RunData + prep func() error + wantErr bool + errMsg string + }{ + { + name: "RunKarmadaAggregatedAPIServer_InvalidTypeAssertion_TypeAssertionFailed", + runData: &MyTestData{Data: "test"}, + prep: func() error { return nil }, + wantErr: true, + errMsg: "KarmadaAggregatedAPIServer task invoked with an invalid data struct", + }, + { + name: "RunKarmadaAggregatedAPIServer_NilKarmadaAggregatedAPIServer_RunIsCompletedWithoutErrors", + runData: &TestInitData{ + ComponentsUnits: &operatorv1alpha1.KarmadaComponents{}, + }, + prep: func() error { return nil }, + wantErr: false, + }, + { + name: "RunKarmadaAggregatedAPIServer_InitializeKarmadaAggregatedAPIServer_KarmadaAggregatedAPIServerEnsured", + runData: &TestInitData{ + Name: "karmada-demo", + Namespace: "test", + ComponentsUnits: &operatorv1alpha1.KarmadaComponents{ + KarmadaAggregatedAPIServer: &operatorv1alpha1.KarmadaAggregatedAPIServer{ + CommonSettings: operatorv1alpha1.CommonSettings{ + Image: operatorv1alpha1.Image{ImageTag: "karmada-aggregated-apiserver-image"}, + Replicas: ptr.To[int32](2), + Resources: corev1.ResourceRequirements{}, + ImagePullPolicy: corev1.PullIfNotPresent, + }, + }, + }, + RemoteClientConnector: fakeclientset.NewSimpleClientset(), + FeatureGatesOptions: map[string]bool{ + "Feature1": true, + }, + }, + wantErr: false, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + err := runKarmadaAggregatedAPIServer(test.runData) + if err == nil && test.wantErr { + t.Errorf("expected error, but got none") + } + if err != nil && !test.wantErr { + t.Errorf("unexpected error: %v", err) + } + if err != nil && test.wantErr && !strings.Contains(err.Error(), test.errMsg) { + t.Errorf("expected %s error msg to contain %s", err.Error(), test.errMsg) + } + }) + } +} + +func TestRunWaitKarmadaAggregatedAPIServer(t *testing.T) { + tests := []struct { + name string + runData workflow.RunData + prep func(workflow.RunData) error + wantErr bool + errMsg string + }{ + { + name: "RunWaitKarmadaAggregatedAPIServer_InvalidTypeAssertion_TypeAssertionFailed", + runData: &MyTestData{Data: "test"}, + prep: func(workflow.RunData) error { + return nil + }, + wantErr: true, + errMsg: "wait-KarmadaAggregatedAPIServer task invoked with an invalid data struct", + }, + { + name: "RunWaitKarmadaAggregatedAPIServer_TimeoutWaitingForSomeKarmadaAggregatedAPIServersPods_Timeout", + runData: &TestInitData{ + Name: "karmada-demo", + Namespace: "test", + RemoteClientConnector: fakeclientset.NewSimpleClientset(), + ControlplaneConfigREST: &rest.Config{}, + FeatureGatesOptions: map[string]bool{ + "Feature1": true, + }, + }, + prep: func(workflow.RunData) error { + componentBeReadyTimeout = time.Second + return nil + }, + wantErr: true, + errMsg: "waiting for karmada-aggregated-apiserver to ready timeout", + }, + { + name: "RunWaitKarmadaAggregatedAPIServer_WaitingForSomeKarmadaAggregatedAPIServersPods_KarmadaAggregatedAPIServerIsReady", + runData: &TestInitData{ + Name: "karmada-demo", + Namespace: "test", + RemoteClientConnector: fakeclientset.NewSimpleClientset(), + ControlplaneConfigREST: &rest.Config{}, + FeatureGatesOptions: map[string]bool{ + "Feature1": true, + }, + }, + prep: func(rd workflow.RunData) error { + data := rd.(*TestInitData) + _, err := apiclient.CreatePods(data.RemoteClient(), data.Namespace, util.KarmadaAggregatedAPIServerName(data.Name), 2, karmadaAggregatedAPIServerLabels, true) + return err + }, + wantErr: false, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + if err := test.prep(test.runData); err != nil { + t.Errorf("failed to prep waiting for Karmada Aggregated APIServer: %v", err) + } + err := runWaitKarmadaAggregatedAPIServer(test.runData) + if err == nil && test.wantErr { + t.Errorf("expected error, but got none") + } + if err != nil && !test.wantErr { + t.Errorf("unexpected error: %v", err) + } + if err != nil && test.wantErr && !strings.Contains(err.Error(), test.errMsg) { + t.Errorf("expected %s error msg to contain %s", err.Error(), test.errMsg) + } + }) + } +} diff --git a/operator/pkg/util/apiclient/test_helpers.go b/operator/pkg/util/apiclient/test_helpers.go new file mode 100644 index 000000000000..efa566ca20e4 --- /dev/null +++ b/operator/pkg/util/apiclient/test_helpers.go @@ -0,0 +1,125 @@ +/* +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 apiclient + +import ( + "context" + "fmt" + + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/discovery" + clientset "k8s.io/client-go/kubernetes" + "k8s.io/client-go/rest" +) + +// MockK8SRESTClient is a struct that implements clientset.Interface. +type MockK8SRESTClient struct { + clientset.Interface + RESTClientConnector rest.Interface +} + +// Discovery returns a mocked discovery interface. +func (m *MockK8SRESTClient) Discovery() discovery.DiscoveryInterface { + return &MockDiscovery{ + RESTClientConnector: m.RESTClientConnector, + } +} + +// MockDiscovery is a mock implementation of DiscoveryInterface. +type MockDiscovery struct { + discovery.DiscoveryInterface + RESTClientConnector rest.Interface +} + +// RESTClient returns a restClientConnector that is used to communicate with API server +// by this client implementation. +func (m *MockDiscovery) RESTClient() rest.Interface { + return m.RESTClientConnector +} + +// CreatePods creates a specified number of pods in the given namespace +// with the provided component name and optional labels. It uses a +// Kubernetes client to interact with the API and can mark the pods as +// running if the `markRunningState` flag is set. +// +// Parameters: +// - client: Kubernetes client interface for API requests. +// - namespace: Namespace for pod creation. +// - componentName: Base name for the pods and their containers. +// - replicaCount: Number of pods to create. +// - labels: Labels to apply to the pods. +// - markRunningState: If true, updates the pods' status to running. +// +// Returns: +// - A slice of pointers to corev1.Pod representing the created pods. +// - An error if pod creation fails. +func CreatePods(client clientset.Interface, namespace string, componentName string, replicaCount int32, labels map[string]string, markRunningState bool) ([]*corev1.Pod, error) { + pods := make([]*corev1.Pod, 0, replicaCount) + for i := int32(0); i < replicaCount; i++ { + podName := fmt.Sprintf("%s-pod-%d", componentName, i) + pod := &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: podName, + Namespace: namespace, + Labels: labels, + }, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{{ + Name: fmt.Sprintf("my-%s-container-%d", componentName, i), + Image: fmt.Sprintf("my-%s-image:latest", componentName), + Ports: []corev1.ContainerPort{{ContainerPort: 80}}, + }}, + }, + } + _, err := client.CoreV1().Pods(namespace).Create(context.TODO(), pod, metav1.CreateOptions{}) + if err != nil { + return nil, fmt.Errorf("failed to create pod %s: %w", podName, err) + } + + // Mark the pod as in running state if flag is set. + if markRunningState { + if err := UpdatePodStatus(client, pod); err != nil { + return nil, fmt.Errorf("failed to update pod status, got err: %v", err) + } + } + pods = append(pods, pod) + } + return pods, nil +} + +// UpdatePodStatus updates the status of a pod to PodRunning and sets the PodReady condition. +func UpdatePodStatus(client clientset.Interface, pod *corev1.Pod) error { + // Mark the pod as in running state. + pod.Status = corev1.PodStatus{ + Phase: corev1.PodRunning, + Conditions: []corev1.PodCondition{ + { + Type: corev1.PodReady, + Status: corev1.ConditionTrue, + }, + }, + } + + // Update the pod status in the Kubernetes cluster. + _, err := client.CoreV1().Pods(pod.GetNamespace()).UpdateStatus(context.TODO(), pod, metav1.UpdateOptions{}) + if err != nil { + return fmt.Errorf("failed to update status of the pod %s: %w", pod.GetName(), err) + } + + return nil +}