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..faee2f43e7a6 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 := util.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 = util.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..f918ec04cf8f --- /dev/null +++ b/operator/pkg/deinit_test.go @@ -0,0 +1,168 @@ +/* +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/util" + "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 := util.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() { + util.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{ + SecretRef: &operatorv1alpha1.LocalSecretReference{ + Namespace: "test", + Name: "karmada-demo", + }, + }, + }, + tasksExpected: DefaultDeInitTasks, + mockFunc: func() { + util.ClientFactory = func(*rest.Config) (clientset.Interface, error) { + return fakeclientset.NewSimpleClientset(), nil + } + util.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: "RunNewDeInitJob_WithDeInitTasks_RunIsSuccessful", + deInitOptions: &DeInitOptions{ + Name: "test_deinit", + Namespace: "test", + Kubeconfig: &rest.Config{}, + HostCluster: &operatorv1alpha1.HostCluster{}, + }, + mockFunc: func() { + util.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..0468e7b86e39 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,14 +72,12 @@ 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 !util.IsInCluster(opt.Karmada.Spec.HostCluster) && opt.Karmada.Spec.Components.KarmadaAPIServer.ServiceType == corev1.ServiceTypeClusterIP { @@ -81,7 +96,7 @@ func (opt *InitOptions) Validate() error { } } - return utilerrors.NewAggregate(errs) + return nil } // InitOpt defines a type of function to set InitOptions values. @@ -111,23 +126,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 +146,7 @@ func newRunData(opt *InitOptions) (*initData, error) { return nil, err } - localClusterClient, err := clientset.NewForConfig(opt.Kubeconfig) + localClusterClient, err := util.ClientFactory(opt.Kubeconfig) if err != nil { return nil, fmt.Errorf("error when creating local cluster client, err: %w", err) } @@ -152,7 +157,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 = util.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) } diff --git a/operator/pkg/init_test.go b/operator/pkg/init_test.go new file mode 100644 index 000000000000..9cd32585f4e2 --- /dev/null +++ b/operator/pkg/init_test.go @@ -0,0 +1,571 @@ +/* +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/util" + "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](4), + }, + }, + }, + }, + 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 := util.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 parse karmada version: %v", err) + } + + clientWithoutAPIServiceIP := fakeclientset.NewSimpleClientset() + clientWithAPIServiceIP := 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() { + util.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() { + util.ClientFactory = func(*rest.Config) (clientset.Interface, error) { + return fakeclientset.NewSimpleClientset(), nil + } + util.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_MissingAPIServerNodeIP_ExpectedValidNodeIPForAPIServer", + 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), + }, + }, + }, + }, + PrivateRegistry: &operatorv1alpha1.ImageRegistry{ + Registry: "myprivateregistry.example.com", + }, + FeatureGates: map[string]bool{}, + HostCluster: &operatorv1alpha1.HostCluster{ + Networking: &operatorv1alpha1.Networking{ + DNSDomain: ptr.To("example.com"), + }, + }, + }, + }, + CRDTarball: operatorv1alpha1.CRDTarball{}, + KarmadaVersion: karmadaVersion.String(), + KarmadaDataDir: constants.KarmadaDataDir, + }, + mockFunc: func() { + util.ClientFactory = func(*rest.Config) (clientset.Interface, error) { + return clientWithoutAPIServiceIP, nil + } + }, + wantInitData: nil, + wantErr: true, + errMsg: "there are no nodes in cluster", + }, + { + 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), + }, + }, + }, + }, + PrivateRegistry: &operatorv1alpha1.ImageRegistry{ + Registry: "myprivateregistry.example.com", + }, + FeatureGates: map[string]bool{}, + HostCluster: &operatorv1alpha1.HostCluster{ + Networking: &operatorv1alpha1.Networking{ + DNSDomain: ptr.To[string]("example.com"), + }, + }, + }, + }, + CRDTarball: operatorv1alpha1.CRDTarball{}, + KarmadaVersion: karmadaVersion.String(), + KarmadaDataDir: constants.KarmadaDataDir, + }, + mockFunc: func() { + util.ClientFactory = func(*rest.Config) (clientset.Interface, error) { + return clientWithAPIServiceIP, nil + } + }, + wantInitData: &initData{ + name: "test_init", + namespace: "test", + karmadaVersion: karmadaVersion, + controlplaneAddress: "192.168.1.1", + remoteClient: clientWithAPIServiceIP, + CRDTarball: operatorv1alpha1.CRDTarball{}, + karmadaDataDir: constants.KarmadaDataDir, + privateRegistry: "myprivateregistry.example.com", + 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: "example.com", + 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 +}