diff --git a/operator/pkg/controller/karmada/planner.go b/operator/pkg/controller/karmada/planner.go index b3455c322c74..b55619a85976 100644 --- a/operator/pkg/controller/karmada/planner.go +++ b/operator/pkg/controller/karmada/planner.go @@ -69,7 +69,7 @@ func NewPlannerFor(karmada *operatorv1alpha1.Karmada, c client.Client, config *r } options := operator.NewJobInitOptions(opts...) - job = operator.NewInitJob(options) + job = operator.NewInitJob(options, operator.DefaultInitTasks) case DeInitAction: opts := []operator.DeInitOpt{ @@ -78,7 +78,7 @@ func NewPlannerFor(karmada *operatorv1alpha1.Karmada, c client.Client, config *r } options := operator.NewJobDeInitOptions(opts...) - job = operator.NewDeInitDataJob(options) + job = operator.NewDeInitDataJob(options, operator.DefaultDeInitTasks) default: return nil, fmt.Errorf("failed to recognize action for karmada %s", karmada.Name) } diff --git a/operator/pkg/deinit.go b/operator/pkg/deinit.go index e2fd08d658e8..3b0a2bd02521 100644 --- a/operator/pkg/deinit.go +++ b/operator/pkg/deinit.go @@ -30,6 +30,15 @@ import ( "github.com/karmada-io/karmada/operator/pkg/workflow" ) +var ( + // DefaultDeInitTasks contains the default tasks to be executed during the deinitialization process. + DefaultDeInitTasks = []workflow.Task{ + tasks.NewRemoveComponentTask(), + tasks.NewCleanupCertTask(), + tasks.NewCleanupKubeconfigTask(), + } +) + // DeInitOptions defines all the Deinit workflow options. type DeInitOptions struct { Name string @@ -53,15 +62,19 @@ type deInitData struct { // NewDeInitDataJob initializes a deInit job with a list of sub tasks. and build // deinit runData object -func NewDeInitDataJob(opt *DeInitOptions) *workflow.Job { +func NewDeInitDataJob(opt *DeInitOptions, deInitTasks []workflow.Task) *workflow.Job { deInitJob := workflow.NewJob() - deInitJob.AppendTask(tasks.NewRemoveComponentTask()) - deInitJob.AppendTask(tasks.NewCleanupCertTask()) - deInitJob.AppendTask(tasks.NewCleanupKubeconfigTask()) + for _, task := range deInitTasks { + deInitJob.AppendTask(task) + } deInitJob.SetDataInitializer(func() (workflow.RunData, error) { - localClusterClient, err := clientset.NewForConfig(opt.Kubeconfig) + if len(opt.Name) == 0 || len(opt.Namespace) == 0 { + return nil, errors.New("unexpected empty name or namespace") + } + + localClusterClient, err := clientFactory(opt.Kubeconfig) if err != nil { return nil, fmt.Errorf("error when creating local cluster client, err: %w", err) } @@ -72,16 +85,12 @@ func NewDeInitDataJob(opt *DeInitOptions) *workflow.Job { if util.IsInCluster(opt.HostCluster) { remoteClient = localClusterClient } else { - remoteClient, err = util.BuildClientFromSecretRef(localClusterClient, opt.HostCluster.SecretRef) + remoteClient, err = buildClientFromSecretRefFactory(localClusterClient, opt.HostCluster.SecretRef) if err != nil { return nil, fmt.Errorf("error when creating cluster client to install karmada, err: %w", err) } } - if len(opt.Name) == 0 || len(opt.Namespace) == 0 { - return nil, errors.New("unexpected empty name or namespace") - } - return &deInitData{ name: opt.Name, namespace: opt.Namespace, diff --git a/operator/pkg/deinit_test.go b/operator/pkg/deinit_test.go new file mode 100644 index 000000000000..fba9ba4d05a0 --- /dev/null +++ b/operator/pkg/deinit_test.go @@ -0,0 +1,159 @@ +/* +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 karmada + +import ( + "fmt" + "strings" + "testing" + + clientset "k8s.io/client-go/kubernetes" + fakeclientset "k8s.io/client-go/kubernetes/fake" + "k8s.io/client-go/rest" + + operatorv1alpha1 "github.com/karmada-io/karmada/operator/pkg/apis/operator/v1alpha1" + "github.com/karmada-io/karmada/operator/pkg/workflow" +) + +func TestNewDeInitDataJob(t *testing.T) { + tests := []struct { + name string + deInitOptions *DeInitOptions + tasksExpected []workflow.Task + }{ + { + name: "NewDeInitDataJob_WithInitTasks_AllIsSubset", + deInitOptions: &DeInitOptions{ + Name: "test_deinit", + Namespace: "test", + Kubeconfig: &rest.Config{}, + HostCluster: &operatorv1alpha1.HostCluster{}, + }, + tasksExpected: DefaultDeInitTasks, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + deInitJob := NewDeInitDataJob(test.deInitOptions, test.tasksExpected) + err := containAllTasks(deInitJob.Tasks, test.tasksExpected) + if err != nil { + t.Errorf("unexpected error, got: %v", err) + } + }) + } +} + +func TestRunNewDeInitJob(t *testing.T) { + tests := []struct { + name string + deInitOptions *DeInitOptions + tasksExpected []workflow.Task + mockFunc func() + wantErr bool + errMsg string + }{ + { + name: "RunNewDeInitJob_EmptyNamespace_NamespaceIsEmpty", + deInitOptions: &DeInitOptions{ + Name: "test_deinit", + Kubeconfig: &rest.Config{}, + HostCluster: &operatorv1alpha1.HostCluster{}, + }, + tasksExpected: DefaultDeInitTasks, + mockFunc: func() {}, + wantErr: true, + errMsg: "unexpected empty name or namespace", + }, + { + name: "RunNewDeInitJob_EmptyName_NameIsEmpty", + deInitOptions: &DeInitOptions{ + Namespace: "test", + Kubeconfig: &rest.Config{}, + HostCluster: &operatorv1alpha1.HostCluster{}, + }, + tasksExpected: DefaultDeInitTasks, + mockFunc: func() {}, + wantErr: true, + errMsg: "unexpected empty name", + }, + { + name: "RunNewDeInitJob_FailedToCreateLocalClusterClient_LocalClusterClientCreationError", + deInitOptions: &DeInitOptions{ + Name: "test_deinit", + Namespace: "test", + Kubeconfig: &rest.Config{}, + HostCluster: &operatorv1alpha1.HostCluster{}, + }, + tasksExpected: DefaultDeInitTasks, + mockFunc: func() { + clientFactory = func(*rest.Config) (clientset.Interface, error) { + return nil, fmt.Errorf("failed to create local cluster client") + } + }, + wantErr: true, + errMsg: "failed to create local cluster client", + }, + { + name: "RunNewDeInitJob_FailedToCreateRemoteClusterClient_RemoteClusterClientCreationError", + deInitOptions: &DeInitOptions{ + Name: "test_deinit", + Namespace: "test", + Kubeconfig: &rest.Config{}, + HostCluster: &operatorv1alpha1.HostCluster{}, + }, + tasksExpected: DefaultDeInitTasks, + mockFunc: func() { + clientFactory = func(*rest.Config) (clientset.Interface, error) { + return nil, fmt.Errorf("failed to create remote cluster client") + } + }, + wantErr: true, + errMsg: "failed to create remote cluster client", + }, + { + name: "RunNewDeInitJob_WithDeInitTasks_RunIsSuccessful", + deInitOptions: &DeInitOptions{ + Name: "test_deinit", + Namespace: "test", + Kubeconfig: &rest.Config{}, + HostCluster: &operatorv1alpha1.HostCluster{}, + }, + mockFunc: func() { + clientFactory = func(*rest.Config) (clientset.Interface, error) { + return fakeclientset.NewSimpleClientset(), nil + } + }, + tasksExpected: DefaultDeInitTasks, + wantErr: false, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + test.mockFunc() + deInitJob := NewDeInitDataJob(test.deInitOptions, test.tasksExpected) + err := deInitJob.Run() + if (err != nil && !test.wantErr) || (err == nil && test.wantErr) { + t.Errorf("RunNewDeInitJob() = got %v error, but want %t error", err, test.wantErr) + } + if (err != nil && test.wantErr) && (!strings.Contains(err.Error(), test.errMsg)) { + t.Errorf("RunNewDeInitJob() = got %s, want %s", err.Error(), test.errMsg) + } + }) + } +} diff --git a/operator/pkg/init.go b/operator/pkg/init.go index 31a0bce99cdb..92ed53eac65a 100644 --- a/operator/pkg/init.go +++ b/operator/pkg/init.go @@ -23,7 +23,6 @@ import ( "sync" corev1 "k8s.io/api/core/v1" - utilerrors "k8s.io/apimachinery/pkg/util/errors" utilversion "k8s.io/apimachinery/pkg/util/version" clientset "k8s.io/client-go/kubernetes" "k8s.io/client-go/rest" @@ -39,6 +38,24 @@ import ( ) var ( + // DefaultInitTasks contains the default tasks to be executed during the initialization process. + DefaultInitTasks = []workflow.Task{ + tasks.NewPrepareCrdsTask(), + tasks.NewCertTask(), + tasks.NewNamespaceTask(), + tasks.NewUploadCertsTask(), + tasks.NewEtcdTask(), + tasks.NewKarmadaApiserverTask(), + tasks.NewUploadKubeconfigTask(), + tasks.NewKarmadaAggregatedApiserverTask(), + tasks.NewCheckApiserverHealthTask(), + tasks.NewKarmadaResourcesTask(), + tasks.NewRBACTask(), + tasks.NewComponentTask(), + tasks.NewWaitControlPlaneTask(), + } + + // defaultCrdURL is the URL for fetching CRDs. defaultCrdURL = "https://github.com/karmada-io/karmada/releases/download/%s/crds.tar.gz" ) @@ -55,16 +72,17 @@ type InitOptions struct { // Validate is used to validate the initOptions before creating initJob. func (opt *InitOptions) Validate() error { - var errs []error - if len(opt.Name) == 0 || len(opt.Namespace) == 0 { return errors.New("unexpected empty name or namespace") } if opt.CRDTarball.HTTPSource != nil { if _, err := url.Parse(opt.CRDTarball.HTTPSource.URL); err != nil { - return fmt.Errorf("unexpected invalid crds remote url %s", opt.CRDTarball.HTTPSource.URL) + return fmt.Errorf("unexpected invalid crds remote url %s, err: %w", opt.CRDTarball.HTTPSource.URL, err) } } + if opt.Karmada == nil || opt.Karmada.Spec.Components == nil || opt.Karmada.Spec.Components.KarmadaAPIServer == nil { + return fmt.Errorf("invalid Karmada configuration: Karmada, Karmada components, and Karmada API server must be defined") + } if !util.IsInCluster(opt.Karmada.Spec.HostCluster) && opt.Karmada.Spec.Components.KarmadaAPIServer.ServiceType == corev1.ServiceTypeClusterIP { return fmt.Errorf("if karmada is installed in a remote cluster, the service type of karmada-apiserver must be either NodePort or LoadBalancer") } @@ -73,7 +91,7 @@ func (opt *InitOptions) Validate() error { return fmt.Errorf("unexpected karmada invalid version %s", opt.KarmadaVersion) } - if opt.Karmada.Spec.Components.Etcd.Local != nil && opt.Karmada.Spec.Components.Etcd.Local.CommonSettings.Replicas != nil { + if opt.Karmada.Spec.Components.Etcd != nil && opt.Karmada.Spec.Components.Etcd.Local != nil && opt.Karmada.Spec.Components.Etcd.Local.CommonSettings.Replicas != nil { replicas := *opt.Karmada.Spec.Components.Etcd.Local.CommonSettings.Replicas if (replicas % 2) == 0 { @@ -81,7 +99,7 @@ func (opt *InitOptions) Validate() error { } } - return utilerrors.NewAggregate(errs) + return nil } // InitOpt defines a type of function to set InitOptions values. @@ -111,23 +129,13 @@ type initData struct { // NewInitJob initializes a job with list of init sub-task. and build // init runData object. -func NewInitJob(opt *InitOptions) *workflow.Job { +func NewInitJob(opt *InitOptions, initTasks []workflow.Task) *workflow.Job { initJob := workflow.NewJob() // add the all tasks to the init job workflow. - initJob.AppendTask(tasks.NewPrepareCrdsTask()) - initJob.AppendTask(tasks.NewCertTask()) - initJob.AppendTask(tasks.NewNamespaceTask()) - initJob.AppendTask(tasks.NewUploadCertsTask()) - initJob.AppendTask(tasks.NewEtcdTask()) - initJob.AppendTask(tasks.NewKarmadaApiserverTask()) - initJob.AppendTask(tasks.NewUploadKubeconfigTask()) - initJob.AppendTask(tasks.NewKarmadaAggregatedApiserverTask()) - initJob.AppendTask(tasks.NewCheckApiserverHealthTask()) - initJob.AppendTask(tasks.NewKarmadaResourcesTask()) - initJob.AppendTask(tasks.NewRBACTask()) - initJob.AppendTask(tasks.NewComponentTask()) - initJob.AppendTask(tasks.NewWaitControlPlaneTask()) + for _, task := range initTasks { + initJob.AppendTask(task) + } initJob.SetDataInitializer(func() (workflow.RunData, error) { return newRunData(opt) @@ -141,7 +149,7 @@ func newRunData(opt *InitOptions) (*initData, error) { return nil, err } - localClusterClient, err := clientset.NewForConfig(opt.Kubeconfig) + localClusterClient, err := clientFactory(opt.Kubeconfig) if err != nil { return nil, fmt.Errorf("error when creating local cluster client, err: %w", err) } @@ -152,7 +160,7 @@ func newRunData(opt *InitOptions) (*initData, error) { if util.IsInCluster(opt.Karmada.Spec.HostCluster) { remoteClient = localClusterClient } else { - remoteClient, err = util.BuildClientFromSecretRef(localClusterClient, opt.Karmada.Spec.HostCluster.SecretRef) + remoteClient, err = buildClientFromSecretRefFactory(localClusterClient, opt.Karmada.Spec.HostCluster.SecretRef) if err != nil { return nil, fmt.Errorf("error when creating cluster client to install karmada, err: %w", err) } @@ -177,6 +185,11 @@ func newRunData(opt *InitOptions) (*initData, error) { } } + var hostClusterDNSDomain string + if opt.Karmada.Spec.HostCluster != nil && opt.Karmada.Spec.HostCluster.Networking != nil && opt.Karmada.Spec.HostCluster.Networking.DNSDomain != nil { + hostClusterDNSDomain = *opt.Karmada.Spec.HostCluster.Networking.DNSDomain + } + return &initData{ name: opt.Name, namespace: opt.Namespace, @@ -188,7 +201,7 @@ func newRunData(opt *InitOptions) (*initData, error) { privateRegistry: privateRegistry, components: opt.Karmada.Spec.Components, featureGates: opt.Karmada.Spec.FeatureGates, - dnsDomain: *opt.Karmada.Spec.HostCluster.Networking.DNSDomain, + dnsDomain: hostClusterDNSDomain, CertStore: certs.NewCertStore(), }, nil } diff --git a/operator/pkg/init_test.go b/operator/pkg/init_test.go new file mode 100644 index 000000000000..93d6a9f53ac3 --- /dev/null +++ b/operator/pkg/init_test.go @@ -0,0 +1,550 @@ +/* +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 karmada + +import ( + "fmt" + "reflect" + "strings" + "testing" + + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + utilversion "k8s.io/apimachinery/pkg/util/version" + clientset "k8s.io/client-go/kubernetes" + 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/certs" + "github.com/karmada-io/karmada/operator/pkg/constants" + "github.com/karmada-io/karmada/operator/pkg/workflow" +) + +func TestValidate(t *testing.T) { + tests := []struct { + name string + initOptions *InitOptions + wantErr bool + errMsg string + }{ + { + name: "Validate_WithoutInitWorkflowNameOpt_UnexpectedEmptyName", + initOptions: &InitOptions{ + Namespace: "test", + Karmada: &operatorv1alpha1.Karmada{}, + KarmadaVersion: operatorv1alpha1.DefaultKarmadaImageVersion, + KarmadaDataDir: constants.KarmadaDataDir, + }, + wantErr: true, + errMsg: "unexpected empty name", + }, + { + name: "Validate_WithoutInitWorkflowNamespaceOpt_UnexpectedEmptyNamespace", + initOptions: &InitOptions{ + Name: "test_init", + Karmada: &operatorv1alpha1.Karmada{}, + KarmadaVersion: operatorv1alpha1.DefaultKarmadaImageVersion, + KarmadaDataDir: constants.KarmadaDataDir, + }, + wantErr: true, + errMsg: "unexpected empty name or namespace", + }, + { + name: "Validate_InvalidWorkflowCRDTarballURL_UnexpectedInvalidCRDs", + initOptions: &InitOptions{ + Name: "test_init", + Namespace: "test", + Karmada: &operatorv1alpha1.Karmada{}, + KarmadaVersion: operatorv1alpha1.DefaultKarmadaImageVersion, + KarmadaDataDir: constants.KarmadaDataDir, + CRDTarball: operatorv1alpha1.CRDTarball{ + HTTPSource: &operatorv1alpha1.HTTPSource{ + URL: "http://%41:8080/", + }, + }, + }, + wantErr: true, + errMsg: fmt.Sprintf("invalid URL escape \"%s\"", "%41"), + }, + { + name: "Validate_InvalidKarmadaConfig_KarmadaConfigMustBeDefined", + initOptions: &InitOptions{ + Name: "test_init", + Namespace: "test", + KarmadaVersion: operatorv1alpha1.DefaultKarmadaImageVersion, + KarmadaDataDir: constants.KarmadaDataDir, + }, + wantErr: true, + errMsg: "invalid Karmada configuration: Karmada, Karmada components, and Karmada API server must be defined", + }, + { + name: "Validate_InvalidKarmadaAPIServerServiceType_UnexpectedServiceType", + initOptions: &InitOptions{ + Name: "test_init", + Namespace: "test", + Karmada: &operatorv1alpha1.Karmada{ + Spec: operatorv1alpha1.KarmadaSpec{ + HostCluster: &operatorv1alpha1.HostCluster{ + APIEndpoint: "10.0.0.1", + SecretRef: &operatorv1alpha1.LocalSecretReference{ + Name: "test-secret", + Namespace: "test", + }, + }, + Components: &operatorv1alpha1.KarmadaComponents{ + KarmadaAPIServer: &operatorv1alpha1.KarmadaAPIServer{ + ServiceType: corev1.ServiceTypeClusterIP, + }, + }, + }, + }, + KarmadaVersion: operatorv1alpha1.DefaultKarmadaImageVersion, + KarmadaDataDir: constants.KarmadaDataDir, + }, + wantErr: true, + errMsg: "service type of karmada-apiserver must be either NodePort or LoadBalancer", + }, + { + name: "Validate_InvalidKarmadaVersion_UnexpectedKarmadaInvalidVersion", + initOptions: &InitOptions{ + Name: "test_init", + Namespace: "test", + Karmada: &operatorv1alpha1.Karmada{ + Spec: operatorv1alpha1.KarmadaSpec{ + Components: &operatorv1alpha1.KarmadaComponents{ + KarmadaAPIServer: &operatorv1alpha1.KarmadaAPIServer{ + ServiceType: corev1.ServiceTypeClusterIP, + }, + }, + }, + }, + KarmadaVersion: "v1;1;0", + KarmadaDataDir: constants.KarmadaDataDir, + }, + wantErr: true, + errMsg: fmt.Sprintf("unexpected karmada invalid version %s", "v1;1;0"), + }, + { + name: "Validate_ValidOptions_IsValidated", + initOptions: &InitOptions{ + Name: "test_init", + Namespace: "test", + Karmada: &operatorv1alpha1.Karmada{ + Spec: operatorv1alpha1.KarmadaSpec{ + Components: &operatorv1alpha1.KarmadaComponents{ + KarmadaAPIServer: &operatorv1alpha1.KarmadaAPIServer{ + ServiceType: corev1.ServiceTypeNodePort, + }, + Etcd: &operatorv1alpha1.Etcd{ + Local: &operatorv1alpha1.LocalEtcd{ + CommonSettings: operatorv1alpha1.CommonSettings{ + Replicas: ptr.To[int32](5), + }, + }, + }, + }, + HostCluster: &operatorv1alpha1.HostCluster{ + APIEndpoint: "10.0.0.1", + SecretRef: &operatorv1alpha1.LocalSecretReference{ + Name: "test-secret", + Namespace: "test", + }, + }, + }, + }, + KarmadaVersion: operatorv1alpha1.DefaultKarmadaImageVersion, + KarmadaDataDir: constants.KarmadaDataDir, + }, + wantErr: false, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + err := test.initOptions.Validate() + if (err != nil && !test.wantErr) || (err == nil && test.wantErr) { + t.Errorf("Validate() = got %v error, but want %t error", err, test.wantErr) + } + if (err != nil && test.wantErr) && (!strings.Contains(err.Error(), test.errMsg)) { + t.Errorf("Validate() = got %s, want %s", err.Error(), test.errMsg) + } + }) + } +} + +func TestNewInitJob(t *testing.T) { + tests := []struct { + name string + initOptions *InitOptions + tasksExpected []workflow.Task + }{ + { + name: "NewInitJob_WithInitTasks_AllIsSubset", + initOptions: &InitOptions{ + Name: "test_init", + Namespace: "test", + Karmada: &operatorv1alpha1.Karmada{ + Spec: operatorv1alpha1.KarmadaSpec{ + Components: &operatorv1alpha1.KarmadaComponents{ + KarmadaAPIServer: &operatorv1alpha1.KarmadaAPIServer{ + ServiceType: corev1.ServiceTypeNodePort, + }, + Etcd: &operatorv1alpha1.Etcd{ + Local: &operatorv1alpha1.LocalEtcd{ + CommonSettings: operatorv1alpha1.CommonSettings{ + Replicas: ptr.To[int32](5), + }, + }, + }, + }, + }, + }, + KarmadaVersion: operatorv1alpha1.DefaultKarmadaImageVersion, + KarmadaDataDir: constants.KarmadaDataDir, + Kubeconfig: &rest.Config{}, + }, + tasksExpected: DefaultInitTasks, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + initJob := NewInitJob(test.initOptions, test.tasksExpected) + err := containAllTasks(initJob.Tasks, test.tasksExpected) + if err != nil { + t.Errorf("unexpected error, got: %v", err) + } + }) + } +} + +func TestNewRunData(t *testing.T) { + karmadaVersion, err := utilversion.ParseGeneric(operatorv1alpha1.DefaultKarmadaImageVersion) + if err != nil { + t.Fatalf("failed to parrse karmada version: %v", err) + } + + client := fakeclientset.NewSimpleClientset( + &corev1.Node{ + ObjectMeta: metav1.ObjectMeta{ + Name: "node-1", + Labels: map[string]string{ + "node-role.kubernetes.io/master": "", + }, + }, + Status: corev1.NodeStatus{ + Addresses: []corev1.NodeAddress{ + { + Type: corev1.NodeInternalIP, + Address: "192.168.1.1", + }, + }, + }, + }, + &corev1.Node{ + ObjectMeta: metav1.ObjectMeta{ + Name: "node-2", + }, + Status: corev1.NodeStatus{ + Addresses: []corev1.NodeAddress{ + { + Type: corev1.NodeInternalIP, + Address: "192.168.1.2", + }, + }, + }, + }, + ) + + tests := []struct { + name string + initOptions *InitOptions + initTasks []workflow.Task + mockFunc func() + wantInitData *initData + wantErr bool + errMsg string + }{ + { + name: "NewRunData_EmptyNamespace_NamespaceIsEmpty", + initOptions: &InitOptions{ + Name: "test_init", + Karmada: &operatorv1alpha1.Karmada{ + Spec: operatorv1alpha1.KarmadaSpec{ + Components: &operatorv1alpha1.KarmadaComponents{ + KarmadaAPIServer: &operatorv1alpha1.KarmadaAPIServer{ + ServiceType: corev1.ServiceTypeNodePort, + }, + Etcd: &operatorv1alpha1.Etcd{ + Local: &operatorv1alpha1.LocalEtcd{ + CommonSettings: operatorv1alpha1.CommonSettings{ + Replicas: ptr.To[int32](5), + }, + }, + }, + }, + }, + }, + KarmadaVersion: karmadaVersion.String(), + KarmadaDataDir: constants.KarmadaDataDir, + }, + mockFunc: func() {}, + wantErr: true, + errMsg: "unexpected empty name or namespace", + }, + { + name: "NewRunData_FailedToCreateLocalClusterClient_LocalClusterClientCreationError", + initOptions: &InitOptions{ + Name: "test_init", + Namespace: "test", + Karmada: &operatorv1alpha1.Karmada{ + Spec: operatorv1alpha1.KarmadaSpec{ + Components: &operatorv1alpha1.KarmadaComponents{ + KarmadaAPIServer: &operatorv1alpha1.KarmadaAPIServer{ + ServiceType: corev1.ServiceTypeNodePort, + }, + Etcd: &operatorv1alpha1.Etcd{ + Local: &operatorv1alpha1.LocalEtcd{ + CommonSettings: operatorv1alpha1.CommonSettings{ + Replicas: ptr.To[int32](5), + }, + }, + }, + }, + }, + }, + KarmadaVersion: karmadaVersion.String(), + KarmadaDataDir: constants.KarmadaDataDir, + }, + mockFunc: func() { + clientFactory = func(*rest.Config) (clientset.Interface, error) { + return nil, fmt.Errorf("failed to create local cluster client") + } + }, + wantErr: true, + errMsg: "failed to create local cluster client", + }, + { + name: "NewRunData_FailedToCreateRemoteClusterClient_RemoteClusterClientCreationError", + initOptions: &InitOptions{ + Name: "test_init", + Namespace: "test", + Karmada: &operatorv1alpha1.Karmada{ + Spec: operatorv1alpha1.KarmadaSpec{ + Components: &operatorv1alpha1.KarmadaComponents{ + KarmadaAPIServer: &operatorv1alpha1.KarmadaAPIServer{ + ServiceType: corev1.ServiceTypeNodePort, + }, + Etcd: &operatorv1alpha1.Etcd{ + Local: &operatorv1alpha1.LocalEtcd{ + CommonSettings: operatorv1alpha1.CommonSettings{ + Replicas: ptr.To[int32](5), + }, + }, + }, + }, + HostCluster: &operatorv1alpha1.HostCluster{ + APIEndpoint: "10.0.0.1", + SecretRef: &operatorv1alpha1.LocalSecretReference{ + Name: "test-secret", + Namespace: "test", + }, + }, + }, + }, + KarmadaVersion: karmadaVersion.String(), + KarmadaDataDir: constants.KarmadaDataDir, + }, + mockFunc: func() { + clientFactory = func(*rest.Config) (clientset.Interface, error) { + return fakeclientset.NewSimpleClientset(), nil + } + buildClientFromSecretRefFactory = func(clientset.Interface, *operatorv1alpha1.LocalSecretReference) (clientset.Interface, error) { + return nil, fmt.Errorf("failed to create remote cluster client") + } + }, + wantErr: true, + errMsg: "failed to create remote cluster client", + }, + { + name: "NewRunData_InvalidKarmadaVersion_UnexpectedKarmadaVersion", + initOptions: &InitOptions{ + Name: "test_init", + Namespace: "test", + Karmada: &operatorv1alpha1.Karmada{ + Spec: operatorv1alpha1.KarmadaSpec{ + Components: &operatorv1alpha1.KarmadaComponents{ + KarmadaAPIServer: &operatorv1alpha1.KarmadaAPIServer{ + ServiceType: corev1.ServiceTypeNodePort, + }, + Etcd: &operatorv1alpha1.Etcd{ + Local: &operatorv1alpha1.LocalEtcd{ + CommonSettings: operatorv1alpha1.CommonSettings{ + Replicas: ptr.To[int32](5), + }, + }, + }, + }, + }, + }, + KarmadaVersion: "v1;1;0", + KarmadaDataDir: constants.KarmadaDataDir, + }, + mockFunc: func() { + clientFactory = func(*rest.Config) (clientset.Interface, error) { + return fakeclientset.NewSimpleClientset(), nil + } + }, + wantErr: true, + errMsg: fmt.Sprintf("karmada invalid version %s", "v1;1;0"), + }, + { + name: "NewRunData_ValidInitOptions_ExpectedInitDataReturned", + initOptions: &InitOptions{ + Name: "test_init", + Namespace: "test", + Karmada: &operatorv1alpha1.Karmada{ + Spec: operatorv1alpha1.KarmadaSpec{ + Components: &operatorv1alpha1.KarmadaComponents{ + KarmadaAPIServer: &operatorv1alpha1.KarmadaAPIServer{ + ServiceType: corev1.ServiceTypeNodePort, + }, + Etcd: &operatorv1alpha1.Etcd{ + Local: &operatorv1alpha1.LocalEtcd{ + CommonSettings: operatorv1alpha1.CommonSettings{ + Replicas: ptr.To[int32](5), + }, + }, + }, + }, + FeatureGates: map[string]bool{}, + }, + }, + CRDTarball: operatorv1alpha1.CRDTarball{}, + KarmadaVersion: karmadaVersion.String(), + KarmadaDataDir: constants.KarmadaDataDir, + }, + mockFunc: func() { + clientFactory = func(*rest.Config) (clientset.Interface, error) { + return client, nil + } + }, + wantInitData: &initData{ + name: "test_init", + namespace: "test", + karmadaVersion: karmadaVersion, + controlplaneAddress: "192.168.1.1", + remoteClient: client, + CRDTarball: operatorv1alpha1.CRDTarball{}, + karmadaDataDir: constants.KarmadaDataDir, + privateRegistry: "", + components: &operatorv1alpha1.KarmadaComponents{ + KarmadaAPIServer: &operatorv1alpha1.KarmadaAPIServer{ + ServiceType: corev1.ServiceTypeNodePort, + }, + Etcd: &operatorv1alpha1.Etcd{ + Local: &operatorv1alpha1.LocalEtcd{ + CommonSettings: operatorv1alpha1.CommonSettings{ + Replicas: ptr.To[int32](5), + }, + }, + }, + }, + featureGates: map[string]bool{}, + dnsDomain: "", + CertStore: certs.NewCertStore(), + }, + wantErr: false, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + test.mockFunc() + initData, err := newRunData(test.initOptions) + if (err != nil && !test.wantErr) || (err == nil && test.wantErr) { + t.Errorf("newRunData() = got %v error, but want %t error", err, test.wantErr) + } + if (err != nil && test.wantErr) && (!strings.Contains(err.Error(), test.errMsg)) { + t.Errorf("newRunData() = got %s, want %s", err.Error(), test.errMsg) + } + + err = deepEqualInitData(initData, test.wantInitData) + if err != nil { + t.Errorf("newRunData() = initData and wantInitData are not matched, got err: %v", err) + } + }) + } +} + +func deepEqualInitData(data1, data2 *initData) error { + if data1 == nil && data2 == nil { + return nil + } + + if data1.name != data2.name { + return fmt.Errorf("expected name %s, got %s", data2.name, data1.name) + } + + if data1.namespace != data2.namespace { + return fmt.Errorf("expected namespace %s, got %s", data2.namespace, data1.namespace) + } + + if data1.controlplaneAddress != data2.controlplaneAddress { + return fmt.Errorf("expected control plane address %s, got %s", data2.controlplaneAddress, data1.controlplaneAddress) + } + + if data1.karmadaDataDir != data2.karmadaDataDir { + return fmt.Errorf("expected karmada data dir %s, got %s", data2.karmadaDataDir, data1.karmadaDataDir) + } + + if data1.privateRegistry != data2.privateRegistry { + return fmt.Errorf("expected private registry %s, got %s", data2.privateRegistry, data1.privateRegistry) + } + + if data1.dnsDomain != data2.dnsDomain { + return fmt.Errorf("expected dns domain %s, got %s", data2.dnsDomain, data1.dnsDomain) + } + + if data1.KarmadaVersion() != data2.KarmadaVersion() { + return fmt.Errorf("expected karamda version %s, got %s", data2.KarmadaVersion(), data1.KarmadaVersion()) + } + + if !reflect.DeepEqual(data1.featureGates, data2.featureGates) { + return fmt.Errorf("expected feature gates %v, got %v", data2.featureGates, data1.featureGates) + } + + if !reflect.DeepEqual(data1.components, data2.components) { + return fmt.Errorf("expected karmada components %v, got %v", data2.components, data1.components) + } + + if !reflect.DeepEqual(data1.remoteClient, data2.remoteClient) { + return fmt.Errorf("expected remote client %v, got %v", data2.remoteClient, data1.remoteClient) + } + + if !reflect.DeepEqual(data1.CRDTarball, data2.CRDTarball) { + return fmt.Errorf("expected CRD Tarball %v, got %v", data2.CRDTarball, data1.CRDTarball) + } + + if !reflect.DeepEqual(data1.CertStore.CertList(), data2.CertStore.CertList()) { + return fmt.Errorf("expdcted cert store list %v, got %v", data2.CertStore.CertList(), data1.CertStore.CertList()) + } + + return nil +} diff --git a/operator/pkg/test_helper.go b/operator/pkg/test_helper.go new file mode 100644 index 000000000000..6c961b47683a --- /dev/null +++ b/operator/pkg/test_helper.go @@ -0,0 +1,87 @@ +/* +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 karmada + +import ( + "fmt" + "reflect" + + clientset "k8s.io/client-go/kubernetes" + "k8s.io/client-go/rest" + + operatorv1alpha1 "github.com/karmada-io/karmada/operator/pkg/apis/operator/v1alpha1" + "github.com/karmada-io/karmada/operator/pkg/util" + "github.com/karmada-io/karmada/operator/pkg/workflow" +) + +var ( + // clientFactory creates a new Kubernetes clientset from the provided kubeconfig. + clientFactory = func(kubeconfig *rest.Config) (clientset.Interface, error) { + return clientset.NewForConfig(kubeconfig) + } + + // buildClientFromSecretRefFactory constructs a Kubernetes clientset using a LocalSecretReference. + buildClientFromSecretRefFactory = func(client clientset.Interface, ref *operatorv1alpha1.LocalSecretReference) (clientset.Interface, error) { + return util.BuildClientFromSecretRef(client, ref) + } +) + +func containAllTasks(tasks, subset []workflow.Task) error { + for _, subsetTask := range subset { + found := false + for _, task := range tasks { + found = deepEqualTasks(task, subsetTask) == nil + if found { + break + } + } + if !found { + return fmt.Errorf("subset task %v not found in tasks", subsetTask) + } + } + return nil +} + +func deepEqualTasks(t1, t2 workflow.Task) error { + if t1.Name != t2.Name { + return fmt.Errorf("expected t1 name %s, but got %s", t2.Name, t1.Name) + } + + if t1.RunSubTasks != t2.RunSubTasks { + return fmt.Errorf("expected t1 RunSubTasks flag %t, but got %t", t2.RunSubTasks, t1.RunSubTasks) + } + + if len(t1.Tasks) != len(t2.Tasks) { + return fmt.Errorf("expected t1 tasks length %d, but got %d", len(t2.Tasks), len(t1.Tasks)) + } + + for index := range t1.Tasks { + err := deepEqualTasks(t1.Tasks[index], t2.Tasks[index]) + if err != nil { + return fmt.Errorf("unexpected error; tasks are not equal, got %v", err) + } + } + + if reflect.ValueOf(t1.Skip).Pointer() != reflect.ValueOf(t2.Skip).Pointer() { + return fmt.Errorf("expected t1 Skip func %v, but got %v", reflect.ValueOf(t2.Skip).Pointer(), reflect.ValueOf(t1.Skip).Pointer()) + } + if reflect.ValueOf(t1.Run).Pointer() != reflect.ValueOf(t2.Run).Pointer() { + return fmt.Errorf("expected t1 Run func %v, but got %v", reflect.ValueOf(t2.Run).Pointer(), reflect.ValueOf(t1.Run).Pointer()) + } + + return nil +} diff --git a/operator/pkg/util/kubeconfig.go b/operator/pkg/util/kubeconfig.go index 6fb65b7adade..3904d2cd8ea1 100644 --- a/operator/pkg/util/kubeconfig.go +++ b/operator/pkg/util/kubeconfig.go @@ -67,7 +67,7 @@ func IsInCluster(hostCluster *operatorv1alpha1.HostCluster) bool { } // BuildClientFromSecretRef builds a clientset from the secret reference. -func BuildClientFromSecretRef(client *clientset.Clientset, ref *operatorv1alpha1.LocalSecretReference) (*clientset.Clientset, error) { +func BuildClientFromSecretRef(client clientset.Interface, ref *operatorv1alpha1.LocalSecretReference) (clientset.Interface, error) { secret, err := client.CoreV1().Secrets(ref.Namespace).Get(context.TODO(), ref.Name, metav1.GetOptions{}) if err != nil { return nil, err @@ -81,7 +81,7 @@ func BuildClientFromSecretRef(client *clientset.Clientset, ref *operatorv1alpha1 return newClientSetForConfig(kubeconfigBytes) } -func newClientSetForConfig(kubeconfig []byte) (*clientset.Clientset, error) { +func newClientSetForConfig(kubeconfig []byte) (clientset.Interface, error) { clientConfig, err := clientcmd.NewClientConfigFromBytes(kubeconfig) if err != nil { return nil, err