package karmadactl import ( "context" "errors" "io" "time" "github.com/spf13/cobra" "github.com/spf13/pflag" apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/util/wait" kubeclient "k8s.io/client-go/kubernetes" "k8s.io/klog/v2" karmadaclientset "github.com/karmada-io/karmada/pkg/generated/clientset/versioned" "github.com/karmada-io/karmada/pkg/karmadactl/options" "github.com/karmada-io/karmada/pkg/util" "github.com/karmada-io/karmada/pkg/util/names" ) var ( unjoinLong = `Unjoin removes the registration of a cluster from control plane.` unjoinExample = ` karmadactl unjoin CLUSTER_NAME --cluster-kubeconfig= ` ) // NewCmdUnjoin defines the `unjoin` command that removes registration of a cluster from control plane. func NewCmdUnjoin(cmdOut io.Writer, karmadaConfig KarmadaConfig) *cobra.Command { opts := CommandUnjoinOption{} cmd := &cobra.Command{ Use: "unjoin CLUSTER_NAME --cluster-kubeconfig=", Short: "Remove the registration of a cluster from control plane", Long: unjoinLong, Example: unjoinExample, Run: func(cmd *cobra.Command, args []string) { err := opts.Complete(args) if err != nil { klog.Errorf("Error: %v", err) return } err = RunUnjoin(cmdOut, karmadaConfig, opts) if err != nil { klog.Errorf("Error: %v", err) return } }, } flags := cmd.Flags() opts.AddFlags(flags) return cmd } // CommandUnjoinOption holds all command options. type CommandUnjoinOption struct { options.GlobalCommandOptions // ClusterName is the cluster's name that we are going to join with. ClusterName string // ClusterContext is the cluster's context that we are going to join with. ClusterContext string // ClusterKubeConfig is the cluster's kubeconfig path. ClusterKubeConfig string forceDeletion bool } // Complete ensures that options are valid and marshals them if necessary. func (j *CommandUnjoinOption) Complete(args []string) error { // Get cluster name from the command args. if len(args) == 0 { return errors.New("cluster name is required") } j.ClusterName = args[0] // If '--cluster-context' not specified, take the cluster name as the context. if len(j.ClusterContext) == 0 { j.ClusterContext = j.ClusterName } return nil } // AddFlags adds flags to the specified FlagSet. func (j *CommandUnjoinOption) AddFlags(flags *pflag.FlagSet) { j.GlobalCommandOptions.AddFlags(flags) flags.StringVar(&j.ClusterContext, "cluster-context", "", "Context name of cluster in kubeconfig. Only works when there are multiple contexts in the kubeconfig.") flags.StringVar(&j.ClusterKubeConfig, "cluster-kubeconfig", "", "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.") } // RunUnjoin is the implementation of the 'unjoin' command. // TODO(RainbowMango): consider to remove the 'KarmadaConfig'. func RunUnjoin(cmdOut io.Writer, karmadaConfig KarmadaConfig, opts CommandUnjoinOption) error { klog.V(1).Infof("unjoining cluster. cluster name: %s", opts.ClusterName) klog.V(1).Infof("unjoining cluster. cluster namespace: %s", opts.ClusterNamespace) // Get control plane kube-apiserver client controlPlaneRestConfig, err := karmadaConfig.GetRestConfig(opts.KarmadaContext, opts.KubeConfig) if err != nil { klog.Errorf("failed to get control plane rest config. context: %s, kube-config: %s, error: %v", opts.KarmadaContext, opts.KubeConfig, err) return err } controlPlaneKarmadaClient := karmadaclientset.NewForConfigOrDie(controlPlaneRestConfig) // delete the cluster object in host cluster that associates the unjoining cluster err = deleteClusterObject(controlPlaneKarmadaClient, opts.ClusterName, opts.DryRun) if err != nil { klog.Errorf("Failed to delete cluster object. cluster name: %s, error: %v", opts.ClusterName, err) return err } // Attempt to delete the cluster role, cluster rolebindings and service account from the unjoining cluster // if user provides the kubeconfig of cluster if opts.ClusterKubeConfig != "" { // Get cluster config clusterConfig, err := karmadaConfig.GetRestConfig(opts.ClusterContext, opts.ClusterKubeConfig) if err != nil { klog.V(1).Infof("failed to get unjoining cluster config. error: %v", err) return err } clusterKubeClient := kubeclient.NewForConfigOrDie(clusterConfig) klog.V(1).Infof("unjoining cluster config. endpoint: %s", clusterConfig.Host) // delete RBAC resource from unjoining cluster err = deleteRBACResources(clusterKubeClient, opts.ClusterName, opts.forceDeletion, opts.DryRun) if err != nil { klog.Errorf("Failed to delete RBAC resource in unjoining cluster %q: %v", opts.ClusterName, err) return err } // delete service account from unjoining cluster err = deleteServiceAccount(clusterKubeClient, opts.ClusterNamespace, opts.ClusterName, opts.forceDeletion, opts.DryRun) if err != nil { klog.Errorf("Failed to delete service account in unjoining cluster %q: %v", opts.ClusterName, err) return err } // delete namespace from unjoining cluster err = deleteNamespaceFromUnjoinCluster(clusterKubeClient, opts.ClusterNamespace, opts.ClusterName, opts.forceDeletion, opts.DryRun) if err != nil { klog.Errorf("Failed to delete namespace in unjoining cluster %q: %v", opts.ClusterName, err) return err } } return nil } // deleteRBACResources deletes the cluster role, cluster rolebindings from the unjoining cluster. func deleteRBACResources(clusterKubeClient kubeclient.Interface, unjoiningClusterName string, forceDeletion, dryRun bool) error { if dryRun { return nil } serviceAccountName := names.GenerateServiceAccountName(unjoiningClusterName) clusterRoleName := names.GenerateRoleName(serviceAccountName) clusterRoleBindingName := clusterRoleName err := util.DeleteClusterRoleBinding(clusterKubeClient, clusterRoleBindingName) if err != nil { if !forceDeletion { return err } klog.Errorf("Force deletion. Could not delete cluster role binding %q for service account %q in unjoining cluster %q: %v.", clusterRoleBindingName, serviceAccountName, unjoiningClusterName, err) } err = util.DeleteClusterRole(clusterKubeClient, clusterRoleName) if err != nil { if !forceDeletion { return err } klog.Errorf("Force deletion. Could not delete cluster role %q for service account %q in unjoining cluster %q: %v.", clusterRoleName, serviceAccountName, unjoiningClusterName, err) } return nil } // deleteServiceAccount deletes the service account from the unjoining cluster. func deleteServiceAccount(clusterKubeClient kubeclient.Interface, namespace, unjoiningClusterName string, forceDeletion, dryRun bool) error { if dryRun { return nil } serviceAccountName := names.GenerateServiceAccountName(unjoiningClusterName) err := util.DeleteServiceAccount(clusterKubeClient, namespace, serviceAccountName) if err != nil { if !forceDeletion { return err } klog.Errorf("Force deletion. Could not delete service account %q in unjoining cluster %q: %v.", serviceAccountName, unjoiningClusterName, err) } return nil } // deleteNSFromUnjoinCluster deletes the namespace from the unjoining cluster. func deleteNamespaceFromUnjoinCluster(clusterKubeClient kubeclient.Interface, namespace, unjoiningClusterName string, forceDeletion, dryRun bool) error { if dryRun { return nil } err := util.DeleteNamespace(clusterKubeClient, namespace) if err != nil { if !forceDeletion { return err } klog.Errorf("Force deletion. Could not delete namespace %q in unjoining cluster %q: %v.", namespace, unjoiningClusterName, err) } return nil } // 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 { return nil } err := controlPlaneKarmadaClient.ClusterV1alpha1().Clusters().Delete(context.TODO(), 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) return err } // make sure the given cluster object has been deleted err = wait.Poll(1*time.Second, 30*time.Second, func() (done bool, err error) { _, err = controlPlaneKarmadaClient.ClusterV1alpha1().Clusters().Get(context.TODO(), clusterName, metav1.GetOptions{}) if apierrors.IsNotFound(err) { return true, nil } if err != nil { klog.Errorf("Failed to get cluster %s. err: %v", clusterName, err) return false, err } klog.Infof("Waiting for the cluster object %s to be deleted", clusterName) return false, nil }) if err != nil { klog.Errorf("Failed to delete cluster object. cluster name: %s, error: %v", clusterName, err) return err } return nil }