diff --git a/artifacts/deploy/bootstrap-token-configuration.yaml b/artifacts/deploy/bootstrap-token-configuration.yaml index 11b496f26980..a25fba7bb1f1 100644 --- a/artifacts/deploy/bootstrap-token-configuration.yaml +++ b/artifacts/deploy/bootstrap-token-configuration.yaml @@ -108,6 +108,7 @@ rules: - watch - patch - update + - delete - apiGroups: - cluster.karmada.io resources: diff --git a/charts/karmada/templates/_karmada_bootstrap_token_configuration.tpl b/charts/karmada/templates/_karmada_bootstrap_token_configuration.tpl index 4ec33dd6e445..93b27322fe9d 100644 --- a/charts/karmada/templates/_karmada_bootstrap_token_configuration.tpl +++ b/charts/karmada/templates/_karmada_bootstrap_token_configuration.tpl @@ -134,6 +134,7 @@ rules: - watch - patch - update + - delete - apiGroups: - cluster.karmada.io resources: diff --git a/pkg/karmadactl/cmdinit/karmada/rbac.go b/pkg/karmadactl/cmdinit/karmada/rbac.go index e4d51252cc32..7ee4281fee83 100644 --- a/pkg/karmadactl/cmdinit/karmada/rbac.go +++ b/pkg/karmadactl/cmdinit/karmada/rbac.go @@ -73,7 +73,7 @@ func grantAccessPermissionToAgent(clientSet kubernetes.Interface) error { { APIGroups: []string{"cluster.karmada.io"}, Resources: []string{"clusters"}, - Verbs: []string{"create", "get", "list", "watch", "patch", "update"}, + Verbs: []string{"create", "get", "list", "watch", "patch", "update", "delete"}, }, { APIGroups: []string{"cluster.karmada.io"}, diff --git a/pkg/karmadactl/karmadactl.go b/pkg/karmadactl/karmadactl.go index 069c7c1ca58f..c0dff7ba96e7 100644 --- a/pkg/karmadactl/karmadactl.go +++ b/pkg/karmadactl/karmadactl.go @@ -56,6 +56,7 @@ import ( "github.com/karmada-io/karmada/pkg/karmadactl/token" "github.com/karmada-io/karmada/pkg/karmadactl/top" "github.com/karmada-io/karmada/pkg/karmadactl/unjoin" + "github.com/karmada-io/karmada/pkg/karmadactl/unregister" "github.com/karmada-io/karmada/pkg/karmadactl/util" utilcomp "github.com/karmada-io/karmada/pkg/karmadactl/util/completion" "github.com/karmada-io/karmada/pkg/version/sharedcommand" @@ -123,6 +124,7 @@ func NewKarmadaCtlCommand(cmdUse, parentCommand string) *cobra.Command { unjoin.NewCmdUnjoin(f, parentCommand), token.NewCmdToken(f, parentCommand, ioStreams), register.NewCmdRegister(parentCommand), + unregister.NewCmdUnregister(parentCommand), }, }, { diff --git a/pkg/karmadactl/register/register.go b/pkg/karmadactl/register/register.go index 852c2150d1b8..bfcc1cb9c3b5 100644 --- a/pkg/karmadactl/register/register.go +++ b/pkg/karmadactl/register/register.go @@ -90,8 +90,14 @@ const ( // KarmadaAgentKubeConfigFileName defines the file name for the kubeconfig that the karmada-agent will use to do // the TLS bootstrap to get itself an unique credential KarmadaAgentKubeConfigFileName = "karmada-agent.conf" - // KarmadaKubeconfigName is the name of karmada kubeconfig - KarmadaKubeconfigName = "karmada-kubeconfig" + // KarmadaConfigSecretName is the secret name of karmada config for agent + KarmadaConfigSecretName = "karmada-agent-config" //nolint:gosec + // KarmadaConfigSecretKey is the key name in karmada config secret + KarmadaConfigSecretKey = "karmada.config" //nolint:gosec + // KarmadaConfigVolume is the volume name to mount karmada config secret + KarmadaConfigVolume = "karmada-config" + // KarmadaConfigMountPath is the mount path of karmada config secret + KarmadaConfigMountPath = "/etc/karmada/config" // KarmadaAgentName is the name of karmada-agent KarmadaAgentName = "karmada-agent" // KarmadaAgentServiceAccountName is the name of karmada-agent serviceaccount @@ -419,9 +425,9 @@ func (o *CommandRegisterOption) preflight() []error { // check if relative resources already exist in member cluster _, err := o.memberClusterClient.CoreV1().Namespaces().Get(context.TODO(), o.Namespace, metav1.GetOptions{}) if err == nil { - _, err = o.memberClusterClient.CoreV1().Secrets(o.Namespace).Get(context.TODO(), KarmadaKubeconfigName, metav1.GetOptions{}) + _, err = o.memberClusterClient.CoreV1().Secrets(o.Namespace).Get(context.TODO(), KarmadaConfigSecretName, metav1.GetOptions{}) if err == nil { - errlist = append(errlist, fmt.Errorf("%s/%s Secret already exists", o.Namespace, KarmadaKubeconfigName)) + errlist = append(errlist, fmt.Errorf("%s/%s Secret already exists", o.Namespace, KarmadaConfigSecretName)) } else if !apierrors.IsNotFound(err) { errlist = append(errlist, err) } @@ -605,12 +611,12 @@ func (o *CommandRegisterOption) createSecretAndRBACInMemberCluster(karmadaAgentC Kind: "Secret", }, ObjectMeta: metav1.ObjectMeta{ - Name: KarmadaKubeconfigName, + Name: KarmadaConfigSecretName, Namespace: o.Namespace, Labels: labels, }, Type: corev1.SecretTypeOpaque, - StringData: map[string]string{KarmadaKubeconfigName: string(configBytes)}, + StringData: map[string]string{KarmadaConfigSecretKey: string(configBytes)}, } // create karmada-kubeconfig secret to be used by karmada-agent component. @@ -711,7 +717,7 @@ func (o *CommandRegisterOption) makeKarmadaAgentDeployment() *appsv1.Deployment Image: o.KarmadaAgentImage, Command: []string{ "/bin/karmada-agent", - "--karmada-kubeconfig=/etc/kubeconfig/karmada-kubeconfig", + fmt.Sprintf("--karmada-kubeconfig=%s", filepath.Join(KarmadaConfigMountPath, KarmadaConfigSecretKey)), fmt.Sprintf("--cluster-name=%s", o.ClusterName), fmt.Sprintf("--cluster-api-endpoint=%s", o.memberClusterEndpoint), fmt.Sprintf("--cluster-provider=%s", o.ClusterProvider), @@ -735,18 +741,18 @@ func (o *CommandRegisterOption) makeKarmadaAgentDeployment() *appsv1.Deployment }, VolumeMounts: []corev1.VolumeMount{ { - Name: "kubeconfig", - MountPath: "/etc/kubeconfig", + Name: KarmadaConfigVolume, + MountPath: KarmadaConfigMountPath, }, }, }, }, Volumes: []corev1.Volume{ { - Name: "kubeconfig", + Name: KarmadaConfigVolume, VolumeSource: corev1.VolumeSource{ Secret: &corev1.SecretVolumeSource{ - SecretName: KarmadaKubeconfigName, + SecretName: KarmadaConfigSecretName, }, }, }, diff --git a/pkg/karmadactl/unjoin/unjoin.go b/pkg/karmadactl/unjoin/unjoin.go index 4e0fa4450b0e..92faf95644c4 100644 --- a/pkg/karmadactl/unjoin/unjoin.go +++ b/pkg/karmadactl/unjoin/unjoin.go @@ -17,15 +17,11 @@ limitations under the License. package unjoin import ( - "context" "fmt" "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/client-go/rest" "k8s.io/klog/v2" @@ -187,7 +183,7 @@ func (j *CommandUnjoinOption) RunUnJoinCluster(controlPlaneRestConfig, clusterCo controlPlaneKarmadaClient := karmadaclientset.NewForConfigOrDie(controlPlaneRestConfig) // delete the cluster object in host cluster that associates the unjoining cluster - err := j.deleteClusterObject(controlPlaneKarmadaClient) + err := cmdutil.DeleteClusterObject(controlPlaneKarmadaClient, j.ClusterName, j.Wait, j.DryRun) if err != nil { klog.Errorf("Failed to delete cluster object. cluster name: %s, error: %v", j.ClusterName, err) return err @@ -225,42 +221,6 @@ func (j *CommandUnjoinOption) RunUnJoinCluster(controlPlaneRestConfig, clusterCo return nil } -// deleteClusterObject delete the cluster object in host cluster that associates the unjoining cluster -func (j *CommandUnjoinOption) deleteClusterObject(controlPlaneKarmadaClient *karmadaclientset.Clientset) error { - if j.DryRun { - return nil - } - - err := controlPlaneKarmadaClient.ClusterV1alpha1().Clusters().Delete(context.TODO(), j.ClusterName, metav1.DeleteOptions{}) - if apierrors.IsNotFound(err) { - return fmt.Errorf("no cluster object %s found in karmada control Plane", j.ClusterName) - } - if err != nil { - klog.Errorf("Failed to delete cluster object. cluster name: %s, error: %v", j.ClusterName, err) - return err - } - - // make sure the given cluster object has been deleted - err = wait.PollUntilContextTimeout(context.TODO(), 1*time.Second, j.Wait, false, func(context.Context) (done bool, err error) { - _, err = controlPlaneKarmadaClient.ClusterV1alpha1().Clusters().Get(context.TODO(), j.ClusterName, metav1.GetOptions{}) - if apierrors.IsNotFound(err) { - return true, nil - } - if err != nil { - klog.Errorf("Failed to get cluster %s. err: %v", j.ClusterName, err) - return false, err - } - klog.Infof("Waiting for the cluster object %s to be deleted", j.ClusterName) - return false, nil - }) - if err != nil { - klog.Errorf("Failed to delete cluster object. cluster name: %s, error: %v", j.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 { diff --git a/pkg/karmadactl/unregister/unregister.go b/pkg/karmadactl/unregister/unregister.go new file mode 100644 index 000000000000..78ec5caf245d --- /dev/null +++ b/pkg/karmadactl/unregister/unregister.go @@ -0,0 +1,378 @@ +/* +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 unregister + +import ( + "context" + "fmt" + "os" + "path/filepath" + "strings" + "time" + + "github.com/spf13/cobra" + "github.com/spf13/pflag" + appsv1 "k8s.io/api/apps/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + kubeclient "k8s.io/client-go/kubernetes" + "k8s.io/client-go/tools/clientcmd" + clientcmdapi "k8s.io/client-go/tools/clientcmd/api" + "k8s.io/klog/v2" + "k8s.io/kubectl/pkg/util/templates" + + "github.com/karmada-io/karmada/pkg/apis/cluster/validation" + karmadaclientset "github.com/karmada-io/karmada/pkg/generated/clientset/versioned" + "github.com/karmada-io/karmada/pkg/karmadactl/options" + "github.com/karmada-io/karmada/pkg/karmadactl/register" + cmdutil "github.com/karmada-io/karmada/pkg/karmadactl/util" + "github.com/karmada-io/karmada/pkg/karmadactl/util/apiclient" + "github.com/karmada-io/karmada/pkg/util" + "github.com/karmada-io/karmada/pkg/util/names" +) + +var ( + unregisterLong = templates.LongDesc(` + Unregister removes a member cluster from Karmada, it will clean up the cluster object in the control plane and Karmada resources in the member cluster.`) + + unregisterExample = templates.Examples(` + # Unregister cluster from karmada control plane + %[1]s unregister CLUSTER_NAME --cluster-kubeconfig= [--cluster-context=] + + # Unregister cluster from karmada control plane with timeout + %[1]s unregister CLUSTER_NAME --cluster-kubeconfig= --wait 2m + + # Unregister cluster from karmada control plane, manually specify the location of the karmada config + %[1]s unregister CLUSTER_NAME --karmada-config= [--karmada-context=] --cluster-kubeconfig= [--cluster-context=] + `) +) + +// NewCmdUnregister defines the `unregister` command that removes registration of a pull mode cluster from control plane. +func NewCmdUnregister(parentCommand string) *cobra.Command { + opts := CommandUnregisterOption{} + + cmd := &cobra.Command{ + Use: "unregister CLUSTER_NAME", + Short: "Remove a pull mode cluster from Karmada control plane", + Long: unregisterLong, + Example: fmt.Sprintf(unregisterExample, parentCommand), + SilenceUsage: true, + DisableFlagsInUseLine: true, + RunE: func(_ *cobra.Command, args []string) error { + if err := opts.Complete(args); err != nil { + return err + } + if err := opts.Validate(args); err != nil { + return err + } + if err := opts.Run(); err != nil { + return err + } + return nil + }, + Annotations: map[string]string{ + cmdutil.TagCommandGroup: cmdutil.GroupClusterRegistration, + }, + } + + flags := cmd.Flags() + opts.AddFlags(flags) + + return cmd +} + +// CommandUnregisterOption holds all command options. +type CommandUnregisterOption struct { + // ClusterName is the cluster's name that we are going to unregister. + ClusterName string + + // KarmadaConfig is the path of config to access karmada-apiserver. + KarmadaConfig string + + // KarmadaContext is the context in KarmadaConfig file to access karmada-apiserver. + KarmadaContext string + + // ClusterKubeConfig is the KUBECONFIG file path to access unregistering member cluster. + ClusterKubeConfig string + + // ClusterContext is the context in ClusterKubeConfig to access unregistering member cluster. + ClusterContext string + + // Namespace is the namespace that karmada-agent component deployed. + Namespace string + + // ClusterNamespace holds namespace where the member cluster secrets are stored + ClusterNamespace string + + // Wait tells maximum command execution time + Wait time.Duration + + // DryRun tells if run the command in dry-run mode, without making any server requests. + DryRun bool + + // ControlPlaneClient control plane client set + ControlPlaneClient *karmadaclientset.Clientset + + // MemberClusterClient member cluster client set + MemberClusterClient *kubeclient.Clientset +} + +// AddFlags adds flags to the specified FlagSet. +func (j *CommandUnregisterOption) AddFlags(flags *pflag.FlagSet) { + flags.StringVar(&j.ClusterKubeConfig, "cluster-kubeconfig", "", "KUBECONFIG file path to access unregistering member cluster, required.") + flags.StringVar(&j.ClusterContext, "cluster-context", "", "Context in cluster-kubeconfig to access unregistering member cluster, optional, defaults to current context.") + flags.StringVar(&j.KarmadaConfig, "karmada-config", "", "Path of config to access karmada-apiserver, optional, defaults to fetch automatically from member cluster.") + flags.StringVar(&j.KarmadaContext, "karmada-context", "", "Context in karmada-config to access karmada-apiserver, optional, defaults to current context.") + + flags.StringVarP(&j.Namespace, "namespace", "n", "karmada-system", "Namespace of the karmada-agent component deployed.") + flags.StringVar(&j.ClusterNamespace, "cluster-namespace", options.DefaultKarmadaClusterNamespace, "Namespace in the control plane where member cluster secrets are stored.") + 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.") + flags.BoolVar(&j.DryRun, "dry-run", false, "Run the command in dry-run mode, without making any server requests.") +} + +// Complete ensures that options are valid and marshals them if necessary. +func (j *CommandUnregisterOption) Complete(args []string) error { + // Get cluster name from the command args. + if len(args) > 0 { + j.ClusterName = args[0] + } + return nil +} + +// Validate ensures that command unjoin options are valid. +func (j *CommandUnregisterOption) Validate(args []string) error { + if len(args) > 1 { + return fmt.Errorf("only the cluster name is allowed as an argument") + } + if errMsgs := validation.ValidateClusterName(j.ClusterName); len(errMsgs) != 0 { + return fmt.Errorf("invalid cluster name(%s): %s", j.ClusterName, strings.Join(errMsgs, ";")) + } + if j.ClusterKubeConfig == "" { + return fmt.Errorf("--cluster-kubeconfig is required to specify KUBECONFIG file path to access unregistering member cluster") + } + if j.Wait <= 0 { + return fmt.Errorf(" --wait %v must be a positive duration, e.g. 1m0s ", j.Wait) + } + return nil +} + +// Run is the implementation of the 'unregister' command. +func (j *CommandUnregisterOption) Run() error { + klog.V(1).Infof("Unregistering cluster. cluster name: %s", j.ClusterName) + klog.V(1).Infof("Unregistering cluster. karmada-agent deployed in namespace: %s", j.Namespace) + klog.V(1).Infof("Unregistering cluster. member cluster secrets stored in namespace: %s", j.ClusterNamespace) + + // 1. build member cluster client + err := j.buildClusterClientSet() + if err != nil { + return err + } + + // 2. build karmada control plane client + if j.KarmadaConfig != "" { + err = j.buildKarmadaClientSetFromFile() + } else { + err = j.buildKarmadaClientSetFromAgent() + } + if err != nil { + return err + } + + return j.RunUnregisterCluster() +} + +func (j *CommandUnregisterOption) buildClusterClientSet() error { + restConfig, err := apiclient.RestConfig(j.ClusterContext, j.ClusterKubeConfig) + if err != nil { + return fmt.Errorf("failed to read member cluster rest config: %w", err) + } + j.MemberClusterClient, err = apiclient.NewClientSet(restConfig) + if err != nil { + return fmt.Errorf("failed to build member cluster clientset: %w", err) + } + return nil +} + +func (j *CommandUnregisterOption) buildKarmadaClientSetFromFile() error { + karmadaCfg, err := clientcmd.LoadFromFile(j.KarmadaConfig) + if err != nil { + return fmt.Errorf("failed to load karmada config: %w", err) + } + if j.KarmadaContext != "" { + karmadaCfg.CurrentContext = j.KarmadaContext + } + j.ControlPlaneClient, err = register.ToKarmadaClient(karmadaCfg) + if err != nil { + return fmt.Errorf("failed to build karmada control plane clientset: %w", err) + } + return nil +} + +func (j *CommandUnregisterOption) buildKarmadaClientSetFromAgent() error { + // 1. get karmada-agent deployment + agent, err := j.MemberClusterClient.AppsV1().Deployments(j.Namespace).Get(context.TODO(), register.KarmadaAgentName, metav1.GetOptions{}) + if err != nil { + return fmt.Errorf("failed to get deployment karmada-agent in member cluster: %w", err) + } + + // 2. get karmada config from karmada-agent deployment + karmadaCfg, err := j.getKarmadaAgentConfig(agent) + if err != nil { + return fmt.Errorf("failed to get karmada config from karmada-agent deployment: %w", err) + } + + // 3. get karmada context from karmada-agent deployment + const karmadaContextPrefix = "--karmada-context=" + for _, cmd := range agent.Spec.Template.Spec.Containers[0].Command { + if strings.HasPrefix(cmd, karmadaContextPrefix) { + karmadaCfg.CurrentContext = cmd[len(karmadaContextPrefix):] + } + } + + j.ControlPlaneClient, err = register.ToKarmadaClient(karmadaCfg) + return err +} + +func (j *CommandUnregisterOption) getKarmadaAgentConfig(agent *appsv1.Deployment) (*clientcmdapi.Config, error) { + var mountPath, fileName, volumeName, agentConfigSecretName string + + const karmadaConfigPrefix = "--karmada-kubeconfig=" + for _, cmd := range agent.Spec.Template.Spec.Containers[0].Command { + if strings.HasPrefix(cmd, karmadaConfigPrefix) { + karmadaConfigPath := cmd[len(karmadaConfigPrefix):] + mountPath, fileName = filepath.Dir(karmadaConfigPath), filepath.Base(karmadaConfigPath) + } + } + + for _, mount := range agent.Spec.Template.Spec.Containers[0].VolumeMounts { + if filepath.Clean(mount.MountPath) == mountPath { + volumeName = mount.Name + } + } + + for _, volume := range agent.Spec.Template.Spec.Volumes { + if volume.Name == volumeName { + agentConfigSecretName = volume.Secret.SecretName + } + } + + if agentConfigSecretName == "" { + return nil, fmt.Errorf("failed to get secret name of karmada agent config") + } + + agentConfigSecret, err := j.MemberClusterClient.CoreV1().Secrets(j.Namespace).Get(context.TODO(), agentConfigSecretName, metav1.GetOptions{}) + if err != nil { + return nil, fmt.Errorf("failed to get the secret which stores the karmada agent config") + } + return clientcmd.Load(agentConfigSecret.Data[fileName]) +} + +type obj struct{ Kind, Name, Namespace string } + +func (o *obj) ToString() string { + if o.Namespace == "" { + return fmt.Sprintf("%s/%s", o.Kind, o.Name) + } + return fmt.Sprintf("%s/%s/%s", o.Kind, o.Namespace, o.Name) +} + +// RunUnregisterCluster unregister the pull mode cluster from karmada. +func (j *CommandUnregisterOption) RunUnregisterCluster() error { + if j.DryRun { + return nil + } + + // 1. delete the cluster object in host cluster that associates the unregistering cluster + if err := cmdutil.DeleteClusterObject(j.ControlPlaneClient, j.ClusterName, j.Wait, j.DryRun); err != nil { + klog.Errorf("Failed to delete cluster object. cluster name: %s, error: %v", j.ClusterName, err) + return err + } + klog.Infof("Successfully delete cluster object (%s) from control plane.", j.ClusterName) + + // 2. delete resource created by karmada in member cluster + var err error + for _, resource := range j.getDeletingClusterResources() { + switch resource.Kind { + case "ClusterRole": + err = util.DeleteClusterRole(j.MemberClusterClient, resource.Name) + case "ClusterRoleBinding": + err = util.DeleteClusterRoleBinding(j.MemberClusterClient, resource.Name) + case "ServiceAccount": + err = util.DeleteServiceAccount(j.MemberClusterClient, resource.Namespace, resource.Name) + case "Secret": + err = util.DeleteSecret(j.MemberClusterClient, resource.Namespace, resource.Name) + case "Deployment": + err = deleteDeployment(j.MemberClusterClient, resource.Namespace, resource.Name) + case "Namespace": + err = util.DeleteNamespace(j.MemberClusterClient, resource.Name) + } + + if err != nil { + klog.Errorf("Failed to delete (%v) in unregistering cluster (%s): %+v.", resource, j.ClusterName, err) + return err + } + klog.Infof("Successfully delete resource (%v) from member cluster (%s).", resource, j.ClusterName) + } + + // 3. delete local obsolete files generated by karmadactl + localObsoleteFiles := []obj{ + {Kind: "File", Name: filepath.Join(register.KarmadaDir, register.KarmadaAgentKubeConfigFileName)}, + {Kind: "File", Name: register.CACertPath}, + } + for _, obj := range localObsoleteFiles { + if err = os.Remove(obj.Name); err != nil { + if os.IsNotExist(err) { + continue + } + klog.Errorf("Failed to delete local file (%v) in current node: %+v.", obj.Name, err) + return err + } + klog.Infof("Successfully delete local file (%v) in current node.", obj.Name) + } + + return nil +} + +// getDeletingClusterResources list resources to be deleted which created by karmada in member cluster +func (j *CommandUnregisterOption) getDeletingClusterResources() []obj { + return []obj{ + // 2.1 delete rbac resource which should upload to control plane to serve the controller-manager and aggregated-apiserver + {Kind: "ClusterRole", Name: names.GenerateRoleName(names.GenerateServiceAccountName(j.ClusterName))}, + {Kind: "ClusterRoleBinding", Name: names.GenerateRoleName(names.GenerateServiceAccountName(j.ClusterName))}, + {Kind: "ServiceAccount", Namespace: j.ClusterNamespace, Name: names.GenerateServiceAccountName(j.ClusterName)}, + {Kind: "Secret", Namespace: j.ClusterNamespace, Name: names.GenerateServiceAccountName(j.ClusterName)}, + {Kind: "ServiceAccount", Namespace: j.ClusterNamespace, Name: names.GenerateServiceAccountName("impersonator")}, + {Kind: "Secret", Namespace: j.ClusterNamespace, Name: names.GenerateServiceAccountName("impersonator")}, + {Kind: "Namespace", Name: j.ClusterNamespace}, + + // 2.2 delete karmada-agent deployment, its own rbac resource and secret/karmada-agent-config + {Kind: "ClusterRole", Name: register.KarmadaAgentName}, + {Kind: "ClusterRoleBinding", Name: register.KarmadaAgentName}, + {Kind: "ServiceAccount", Namespace: j.Namespace, Name: register.KarmadaAgentServiceAccountName}, + {Kind: "Secret", Namespace: j.Namespace, Name: register.KarmadaConfigSecretName}, + {Kind: "Deployment", Namespace: j.Namespace, Name: register.KarmadaAgentName}, + } +} + +// deleteDeployment just try to delete the Deployment. +func deleteDeployment(client kubeclient.Interface, namespace, name string) error { + err := client.AppsV1().Deployments(namespace).Delete(context.TODO(), name, metav1.DeleteOptions{}) + if err != nil && !apierrors.IsNotFound(err) { + return err + } + return nil +} diff --git a/pkg/karmadactl/util/cluster.go b/pkg/karmadactl/util/cluster.go new file mode 100644 index 000000000000..74f3c633dda7 --- /dev/null +++ b/pkg/karmadactl/util/cluster.go @@ -0,0 +1,67 @@ +/* +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 util + +import ( + "context" + "fmt" + "time" + + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/wait" + "k8s.io/klog/v2" + + karmadaclientset "github.com/karmada-io/karmada/pkg/generated/clientset/versioned" +) + +// DeleteClusterObject delete the cluster object in host cluster +func DeleteClusterObject(controlPlaneKarmadaClient *karmadaclientset.Clientset, clusterName string, + timeout time.Duration, dryRun bool) error { + if dryRun { + return nil + } + + err := controlPlaneKarmadaClient.ClusterV1alpha1().Clusters().Delete(context.TODO(), clusterName, metav1.DeleteOptions{}) + if apierrors.IsNotFound(err) { + return fmt.Errorf("no cluster object %s found in karmada control Plane", clusterName) + } + 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.PollUntilContextTimeout(context.TODO(), 1*time.Second, timeout, false, func(context.Context) (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 +} diff --git a/pkg/util/secret.go b/pkg/util/secret.go index ab05bccfeff8..c4b93dc417e1 100644 --- a/pkg/util/secret.go +++ b/pkg/util/secret.go @@ -60,3 +60,12 @@ func PatchSecret(client kubeclient.Interface, namespace, name string, pt types.P } return nil } + +// DeleteSecret just try to delete the Secret. +func DeleteSecret(client kubeclient.Interface, namespace, name string) error { + err := client.CoreV1().Secrets(namespace).Delete(context.Background(), name, metav1.DeleteOptions{}) + if err != nil && !apierrors.IsNotFound(err) { + return err + } + return nil +}