diff --git a/pkg/clusterdiscovery/clusterapi/clusterapi.go b/pkg/clusterdiscovery/clusterapi/clusterapi.go index 88677736e578..7e8aaa61bcd0 100644 --- a/pkg/clusterdiscovery/clusterapi/clusterapi.go +++ b/pkg/clusterdiscovery/clusterapi/clusterapi.go @@ -198,6 +198,7 @@ func (d *ClusterDetector) unJoinClusterAPICluster(clusterName string) error { DryRun: false, }, ClusterName: clusterName, + Wait: options.DefaultKarmadactlCommandDuration, } err := karmadactl.UnJoinCluster(d.ControllerPlaneConfig, nil, opts) if err != nil { diff --git a/pkg/karmadactl/options/global.go b/pkg/karmadactl/options/global.go index 324fce42cc9c..1129a97ebd85 100644 --- a/pkg/karmadactl/options/global.go +++ b/pkg/karmadactl/options/global.go @@ -1,11 +1,18 @@ package options -import "github.com/spf13/pflag" +import ( + "time" + + "github.com/spf13/pflag" +) // DefaultKarmadaClusterNamespace defines the default namespace where the member cluster objects are stored. // The secret owns by cluster objects will be stored in the namespace too. const DefaultKarmadaClusterNamespace = "karmada-cluster" +// DefaultKarmadactlCommandDuration defines the default timeout for karmadactl execute +const DefaultKarmadactlCommandDuration = 60 * time.Second + // GlobalCommandOptions holds the configuration shared by the all sub-commands of `karmadactl`. type GlobalCommandOptions struct { // KubeConfig holds the control plane KUBECONFIG file path. diff --git a/pkg/karmadactl/unjoin.go b/pkg/karmadactl/unjoin.go index 4eb1c4c8a8a1..af36502a0024 100644 --- a/pkg/karmadactl/unjoin.go +++ b/pkg/karmadactl/unjoin.go @@ -11,6 +11,7 @@ import ( "github.com/spf13/pflag" apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + utilerrors "k8s.io/apimachinery/pkg/util/errors" "k8s.io/apimachinery/pkg/util/wait" kubeclient "k8s.io/client-go/kubernetes" "k8s.io/client-go/rest" @@ -26,7 +27,11 @@ var ( unjoinShort = `Remove the registration of a cluster from control plane` unjoinLong = `Unjoin removes the registration of a cluster from control plane.` unjoinExample = ` +# Unjoin cluster from karamada control plane %s unjoin CLUSTER_NAME --cluster-kubeconfig= + +# Unjoin cluster from karamada control plane with timeout +%s unjoin CLUSTER_NAME --cluster-kubeconfig= --wait 2m ` ) @@ -38,12 +43,15 @@ func NewCmdUnjoin(cmdOut io.Writer, karmadaConfig KarmadaConfig, cmdStr string) Use: "unjoin CLUSTER_NAME --cluster-kubeconfig=", Short: unjoinShort, Long: unjoinLong, - Example: fmt.Sprintf(unjoinExample, cmdStr), + Example: getUnjoinExample(cmdStr), Run: func(cmd *cobra.Command, args []string) { err := opts.Complete(args) if err != nil { klog.Fatalf("Error: %v", err) } + if errs := opts.Validate(); len(errs) != 0 { + klog.Fatalf("Error: %v", utilerrors.NewAggregate(errs).Error()) + } err = RunUnjoin(cmdOut, karmadaConfig, opts) if err != nil { @@ -58,6 +66,10 @@ func NewCmdUnjoin(cmdOut io.Writer, karmadaConfig KarmadaConfig, cmdStr string) return cmd } +func getUnjoinExample(cmdStr string) string { + return fmt.Sprintf(unjoinExample, cmdStr, cmdStr) +} + // CommandUnjoinOption holds all command options. type CommandUnjoinOption struct { options.GlobalCommandOptions @@ -72,6 +84,9 @@ type CommandUnjoinOption struct { ClusterKubeConfig string forceDeletion bool + + // Wait tells maximum command execution time + Wait time.Duration } // Complete ensures that options are valid and marshals them if necessary. @@ -90,6 +105,16 @@ func (j *CommandUnjoinOption) Complete(args []string) error { return nil } +// Validate ensures that command unjoin options are valid. +func (j *CommandUnjoinOption) Validate() []error { + var errs []error + + if j.Wait < 0 { + errs = append(errs, fmt.Errorf(" --wait %v must be a positive duration, e.g. 1m0s ", j.Wait)) + } + return errs +} + // AddFlags adds flags to the specified FlagSet. func (j *CommandUnjoinOption) AddFlags(flags *pflag.FlagSet) { j.GlobalCommandOptions.AddFlags(flags) @@ -100,6 +125,8 @@ func (j *CommandUnjoinOption) AddFlags(flags *pflag.FlagSet) { "Path of the cluster's kubeconfig.") flags.BoolVar(&j.forceDeletion, "force", false, "Delete cluster and secret resources even if resources in the cluster targeted for unjoin are not removed successfully.") + + flags.DurationVar(&j.Wait, "wait", 60*time.Second, "wait for the unjoin command execution process(default 60s), if there is no success after this time, timeout will be returned.") } // RunUnjoin is the implementation of the 'unjoin' command. @@ -133,7 +160,7 @@ func UnJoinCluster(controlPlaneRestConfig, clusterConfig *rest.Config, opts Comm controlPlaneKarmadaClient := karmadaclientset.NewForConfigOrDie(controlPlaneRestConfig) // delete the cluster object in host cluster that associates the unjoining cluster - err = deleteClusterObject(controlPlaneKarmadaClient, opts.ClusterName, opts.DryRun) + err = deleteClusterObject(controlPlaneKarmadaClient, opts) if err != nil { klog.Errorf("Failed to delete cluster object. cluster name: %s, error: %v", opts.ClusterName, err) return err @@ -236,35 +263,35 @@ func deleteNamespaceFromUnjoinCluster(clusterKubeClient kubeclient.Interface, na } // deleteClusterObject delete the cluster object in host cluster that associates the unjoining cluster -func deleteClusterObject(controlPlaneKarmadaClient *karmadaclientset.Clientset, clusterName string, dryRun bool) error { - if dryRun { +func deleteClusterObject(controlPlaneKarmadaClient *karmadaclientset.Clientset, opts CommandUnjoinOption) error { + if opts.DryRun { return nil } - err := controlPlaneKarmadaClient.ClusterV1alpha1().Clusters().Delete(context.TODO(), clusterName, metav1.DeleteOptions{}) + err := controlPlaneKarmadaClient.ClusterV1alpha1().Clusters().Delete(context.TODO(), opts.ClusterName, metav1.DeleteOptions{}) if apierrors.IsNotFound(err) { return nil } if err != nil { - klog.Errorf("Failed to delete cluster object. cluster name: %s, error: %v", clusterName, err) + klog.Errorf("Failed to delete cluster object. cluster name: %s, error: %v", opts.ClusterName, err) return err } // make sure the given cluster object has been deleted - err = wait.Poll(1*time.Second, 1*time.Minute, func() (done bool, err error) { - _, err = controlPlaneKarmadaClient.ClusterV1alpha1().Clusters().Get(context.TODO(), clusterName, metav1.GetOptions{}) + err = wait.Poll(1*time.Second, opts.Wait, func() (done bool, err error) { + _, err = controlPlaneKarmadaClient.ClusterV1alpha1().Clusters().Get(context.TODO(), opts.ClusterName, metav1.GetOptions{}) if apierrors.IsNotFound(err) { return true, nil } if err != nil { - klog.Errorf("Failed to get cluster %s. err: %v", clusterName, err) + klog.Errorf("Failed to get cluster %s. err: %v", opts.ClusterName, err) return false, err } - klog.Infof("Waiting for the cluster object %s to be deleted", clusterName) + klog.Infof("Waiting for the cluster object %s to be deleted", opts.ClusterName) return false, nil }) if err != nil { - klog.Errorf("Failed to delete cluster object. cluster name: %s, error: %v", clusterName, err) + klog.Errorf("Failed to delete cluster object. cluster name: %s, error: %v", opts.ClusterName, err) return err } diff --git a/test/e2e/namespace_test.go b/test/e2e/namespace_test.go index ae15ebc4093c..7108b42bac1f 100644 --- a/test/e2e/namespace_test.go +++ b/test/e2e/namespace_test.go @@ -168,6 +168,7 @@ var _ = ginkgo.Describe("[namespace auto-provision] namespace auto-provision tes ClusterName: clusterName, ClusterContext: clusterContext, ClusterKubeConfig: kubeConfigPath, + Wait: options.DefaultKarmadactlCommandDuration, } err := karmadactl.RunUnjoin(os.Stdout, karmadaConfig, opts) gomega.Expect(err).ShouldNot(gomega.HaveOccurred())