github.com/oam-dev/kubevela@v1.9.11/pkg/multicluster/cluster_management.go (about)

     1  /*
     2  Copyright 2021 The KubeVela Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8  	http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package multicluster
    18  
    19  import (
    20  	"bytes"
    21  	"context"
    22  	"fmt"
    23  	"strings"
    24  	"time"
    25  
    26  	"github.com/briandowns/spinner"
    27  	"github.com/kubevela/pkg/util/k8s"
    28  	clusterv1alpha1 "github.com/oam-dev/cluster-gateway/pkg/apis/cluster/v1alpha1"
    29  	clustercommon "github.com/oam-dev/cluster-gateway/pkg/common"
    30  	"github.com/oam-dev/cluster-register/pkg/hub"
    31  	"github.com/oam-dev/cluster-register/pkg/spoke"
    32  	"github.com/pkg/errors"
    33  	corev1 "k8s.io/api/core/v1"
    34  	apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
    35  	apierrors "k8s.io/apimachinery/pkg/api/errors"
    36  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    37  	apitypes "k8s.io/apimachinery/pkg/types"
    38  	"k8s.io/client-go/rest"
    39  	"k8s.io/client-go/tools/clientcmd"
    40  	clientcmdapi "k8s.io/client-go/tools/clientcmd/api"
    41  	ocmclusterv1 "open-cluster-management.io/api/cluster/v1"
    42  	"sigs.k8s.io/controller-runtime/pkg/client"
    43  
    44  	"github.com/oam-dev/kubevela/pkg/utils"
    45  	cmdutil "github.com/oam-dev/kubevela/pkg/utils/util"
    46  )
    47  
    48  // ContextKey defines the key in context
    49  type ContextKey string
    50  
    51  // KubeConfigContext marks the kubeConfig object in context
    52  const KubeConfigContext ContextKey = "kubeConfig"
    53  
    54  // KubeClusterConfig info for cluster management
    55  type KubeClusterConfig struct {
    56  	FilePath        string
    57  	ClusterName     string
    58  	CreateNamespace string
    59  	*clientcmdapi.Config
    60  	*clientcmdapi.Cluster
    61  	*clientcmdapi.AuthInfo
    62  
    63  	// ClusterAlreadyExistCallback callback for handling cluster already exist,
    64  	// if no error returned, the logic will pass through
    65  	ClusterAlreadyExistCallback func(string) bool
    66  
    67  	// Logs records intermediate logs (which do not return error) during running
    68  	Logs bytes.Buffer
    69  }
    70  
    71  // SetClusterName set cluster name if not empty
    72  func (clusterConfig *KubeClusterConfig) SetClusterName(clusterName string) *KubeClusterConfig {
    73  	if clusterName != "" {
    74  		clusterConfig.ClusterName = clusterName
    75  	}
    76  	return clusterConfig
    77  }
    78  
    79  // SetCreateNamespace set create namespace, if empty, no namespace will be created
    80  func (clusterConfig *KubeClusterConfig) SetCreateNamespace(createNamespace string) *KubeClusterConfig {
    81  	clusterConfig.CreateNamespace = createNamespace
    82  	return clusterConfig
    83  }
    84  
    85  // Validate check if config is valid for join
    86  func (clusterConfig *KubeClusterConfig) Validate() error {
    87  	switch clusterConfig.ClusterName {
    88  	case "":
    89  		return errors.Errorf("ClusterName cannot be empty")
    90  	case ClusterLocalName:
    91  		return errors.Errorf("ClusterName cannot be `%s`, it is reserved as the local cluster", ClusterLocalName)
    92  	}
    93  	return nil
    94  }
    95  
    96  // PostRegistration try to create namespace after cluster registered. If failed, cluster will be unregistered.
    97  func (clusterConfig *KubeClusterConfig) PostRegistration(ctx context.Context, cli client.Client) error {
    98  	if clusterConfig.CreateNamespace == "" {
    99  		return nil
   100  	}
   101  	// retry 3 times.
   102  	for i := 0; i < 3; i++ {
   103  		if err := ensureNamespaceExists(ctx, cli, clusterConfig.ClusterName, clusterConfig.CreateNamespace); err != nil {
   104  			// Cluster gateway discovers the cluster maybe be deferred, so we should retry.
   105  			if strings.Contains(err.Error(), "no such cluster") {
   106  				if i < 2 {
   107  					time.Sleep(time.Second * 1)
   108  					continue
   109  				}
   110  			}
   111  			_ = DetachCluster(ctx, cli, clusterConfig.ClusterName, DetachClusterManagedClusterKubeConfigPathOption(clusterConfig.FilePath))
   112  			return fmt.Errorf("failed to ensure %s namespace installed in cluster %s: %w", clusterConfig.CreateNamespace, clusterConfig.ClusterName, err)
   113  		}
   114  		break
   115  	}
   116  	return nil
   117  }
   118  
   119  func (clusterConfig *KubeClusterConfig) createOrUpdateClusterSecret(ctx context.Context, cli client.Client, withEndpoint bool) error {
   120  	var credentialType clusterv1alpha1.CredentialType
   121  	data := map[string][]byte{}
   122  	if withEndpoint {
   123  		data["endpoint"] = []byte(clusterConfig.Cluster.Server)
   124  		if !clusterConfig.Cluster.InsecureSkipTLSVerify {
   125  			data["ca.crt"] = clusterConfig.Cluster.CertificateAuthorityData
   126  		}
   127  	}
   128  	if len(clusterConfig.AuthInfo.Token) > 0 {
   129  		credentialType = clusterv1alpha1.CredentialTypeServiceAccountToken
   130  		data["token"] = []byte(clusterConfig.AuthInfo.Token)
   131  	} else {
   132  		credentialType = clusterv1alpha1.CredentialTypeX509Certificate
   133  		data["tls.crt"] = clusterConfig.AuthInfo.ClientCertificateData
   134  		data["tls.key"] = clusterConfig.AuthInfo.ClientKeyData
   135  	}
   136  	if clusterConfig.Cluster.ProxyURL != "" {
   137  		data["proxy-url"] = []byte(clusterConfig.Cluster.ProxyURL)
   138  	}
   139  	secret := &corev1.Secret{}
   140  	if err := cli.Get(ctx, apitypes.NamespacedName{Name: clusterConfig.ClusterName, Namespace: ClusterGatewaySecretNamespace}, secret); client.IgnoreNotFound(err) != nil {
   141  		return err
   142  	}
   143  	secret.Name = clusterConfig.ClusterName
   144  	secret.Namespace = ClusterGatewaySecretNamespace
   145  	secret.Type = corev1.SecretTypeOpaque
   146  	_ = k8s.AddLabel(secret, clustercommon.LabelKeyClusterCredentialType, string(credentialType))
   147  	secret.Data = data
   148  	if secret.ResourceVersion == "" {
   149  		return cli.Create(ctx, secret)
   150  	}
   151  	return cli.Update(ctx, secret)
   152  }
   153  
   154  // RegisterByVelaSecret create cluster secrets for KubeVela to use
   155  func (clusterConfig *KubeClusterConfig) RegisterByVelaSecret(ctx context.Context, cli client.Client) error {
   156  	cluster, err := NewClusterClient(cli).Get(ctx, clusterConfig.ClusterName)
   157  	if client.IgnoreNotFound(err) != nil {
   158  		return err
   159  	}
   160  	if cluster != nil {
   161  		if clusterConfig.ClusterAlreadyExistCallback == nil {
   162  			return fmt.Errorf("cluster %s already exists", cluster.Name)
   163  		}
   164  		if !clusterConfig.ClusterAlreadyExistCallback(clusterConfig.ClusterName) {
   165  			return nil
   166  		}
   167  		if cluster.Spec.CredentialType == clusterv1alpha1.CredentialTypeInternal || cluster.Spec.CredentialType == clusterv1alpha1.CredentialTypeOCMManagedCluster {
   168  			return fmt.Errorf("cannot override %s typed cluster", cluster.Spec.CredentialType)
   169  		}
   170  	}
   171  
   172  	if err := clusterConfig.createOrUpdateClusterSecret(ctx, cli, true); err != nil {
   173  		return errors.Wrapf(err, "failed to add cluster to kubernetes")
   174  	}
   175  	return clusterConfig.PostRegistration(ctx, cli)
   176  }
   177  
   178  // CreateBootstrapConfigMapIfNotExists alternative to
   179  // https://github.com/kubernetes/kubernetes/blob/v1.24.1/cmd/kubeadm/app/phases/bootstraptoken/clusterinfo/clusterinfo.go#L43
   180  func CreateBootstrapConfigMapIfNotExists(ctx context.Context, cli client.Client) error {
   181  	cm := &corev1.ConfigMap{}
   182  	key := apitypes.NamespacedName{Namespace: metav1.NamespacePublic, Name: "cluster-info"}
   183  	if err := cli.Get(ctx, key, cm); err != nil {
   184  		if apierrors.IsNotFound(err) {
   185  			cm.ObjectMeta = metav1.ObjectMeta{Namespace: key.Namespace, Name: key.Name}
   186  			adminConfig, err := clientcmd.NewDefaultPathOptions().GetStartingConfig()
   187  			if err != nil {
   188  				return err
   189  			}
   190  			adminCluster := adminConfig.Contexts[adminConfig.CurrentContext].Cluster
   191  			bs, err := clientcmd.Write(clientcmdapi.Config{
   192  				Clusters: map[string]*clientcmdapi.Cluster{"": adminConfig.Clusters[adminCluster]},
   193  			})
   194  			if err != nil {
   195  				return err
   196  			}
   197  			cm.Data = map[string]string{"kubeconfig": string(bs)}
   198  			return cli.Create(ctx, cm)
   199  		}
   200  		return err
   201  	}
   202  	return nil
   203  }
   204  
   205  // RegisterClusterManagedByOCM create ocm managed cluster for use
   206  // TODO(somefive): OCM ManagedCluster only support cli join now
   207  func (clusterConfig *KubeClusterConfig) RegisterClusterManagedByOCM(ctx context.Context, cli client.Client, args *JoinClusterArgs) error {
   208  	newTrackingSpinner := args.trackingSpinnerFactory
   209  	hubCluster, err := hub.NewHubCluster(args.hubConfig)
   210  	if err != nil {
   211  		return errors.Wrap(err, "fail to create client connect to hub cluster")
   212  	}
   213  
   214  	hubTracker := newTrackingSpinner("Checking the environment of hub cluster..")
   215  	hubTracker.FinalMSG = "Hub cluster all set, continue registration.\n"
   216  	hubTracker.Start()
   217  	crdName := apitypes.NamespacedName{Name: "managedclusters." + ocmclusterv1.GroupName}
   218  	if err := hubCluster.Client.Get(context.Background(), crdName, &apiextensionsv1.CustomResourceDefinition{}); err != nil {
   219  		return err
   220  	}
   221  
   222  	clusters, err := ListVirtualClusters(context.Background(), hubCluster.Client)
   223  	if err != nil {
   224  		return err
   225  	}
   226  
   227  	for _, cluster := range clusters {
   228  		if cluster.Name == clusterConfig.ClusterName && cluster.Accepted {
   229  			return errors.Errorf("you have register a cluster named %s", clusterConfig.ClusterName)
   230  		}
   231  	}
   232  	hubTracker.Stop()
   233  
   234  	spokeRestConf, err := clientcmd.BuildConfigFromKubeconfigGetter("", func() (*clientcmdapi.Config, error) {
   235  		return clusterConfig.Config, nil
   236  	})
   237  	if err != nil {
   238  		return errors.Wrap(err, "fail to convert spoke-cluster kubeconfig")
   239  	}
   240  
   241  	if err = CreateBootstrapConfigMapIfNotExists(ctx, cli); err != nil {
   242  		return fmt.Errorf("failed to ensure cluster-info ConfigMap in kube-public namespace exists: %w", err)
   243  	}
   244  	spokeTracker := newTrackingSpinner("Building registration config for the managed cluster")
   245  	spokeTracker.FinalMSG = "Successfully prepared registration config.\n"
   246  	spokeTracker.Start()
   247  	overridingRegistrationEndpoint := ""
   248  	if !*args.inClusterBootstrap {
   249  		args.ioStreams.Infof("Using the api endpoint from hub kubeconfig %q as registration entry.\n", args.hubConfig.Host)
   250  		overridingRegistrationEndpoint = args.hubConfig.Host
   251  	}
   252  
   253  	hubKubeToken, err := hubCluster.GenerateHubClusterKubeConfig(ctx, overridingRegistrationEndpoint)
   254  	if err != nil {
   255  		return errors.Wrap(err, "fail to generate the token for spoke-cluster")
   256  	}
   257  
   258  	spokeCluster, err := spoke.NewSpokeCluster(clusterConfig.ClusterName, spokeRestConf, hubKubeToken)
   259  	if err != nil {
   260  		return errors.Wrap(err, "fail to connect spoke cluster")
   261  	}
   262  
   263  	err = spokeCluster.InitSpokeClusterEnv(ctx)
   264  	if err != nil {
   265  		return errors.Wrap(err, "fail to prepare the env for spoke-cluster")
   266  	}
   267  	spokeTracker.Stop()
   268  
   269  	registrationOperatorTracker := newTrackingSpinner("Waiting for registration operators running: (`kubectl -n open-cluster-management get pod -l app=klusterlet`)")
   270  	registrationOperatorTracker.FinalMSG = "Registration operator successfully deployed.\n"
   271  	registrationOperatorTracker.Start()
   272  	if err := spokeCluster.WaitForRegistrationOperatorReady(ctx); err != nil {
   273  		return errors.Wrap(err, "fail to setup registration operator for spoke-cluster")
   274  	}
   275  	registrationOperatorTracker.Stop()
   276  
   277  	registrationAgentTracker := newTrackingSpinner("Waiting for registration agent running: (`kubectl -n open-cluster-management-agent get pod -l app=klusterlet-registration-agent`)")
   278  	registrationAgentTracker.FinalMSG = "Registration agent successfully deployed.\n"
   279  	registrationAgentTracker.Start()
   280  	if err := spokeCluster.WaitForRegistrationAgentReady(ctx); err != nil {
   281  		return errors.Wrap(err, "fail to setup registration agent for spoke-cluster")
   282  	}
   283  	registrationAgentTracker.Stop()
   284  
   285  	csrCreationTracker := newTrackingSpinner("Waiting for CSRs created (`kubectl get csr -l open-cluster-management.io/cluster-name=" + spokeCluster.Name + "`)")
   286  	csrCreationTracker.FinalMSG = "Successfully found corresponding CSR from the agent.\n"
   287  	csrCreationTracker.Start()
   288  	if err := hubCluster.WaitForCSRCreated(ctx, spokeCluster.Name); err != nil {
   289  		return errors.Wrap(err, "failed found CSR created by registration agent")
   290  	}
   291  	csrCreationTracker.Stop()
   292  
   293  	args.ioStreams.Infof("Approving the CSR for cluster %q.\n", spokeCluster.Name)
   294  	if err := hubCluster.ApproveCSR(ctx, spokeCluster.Name); err != nil {
   295  		return errors.Wrap(err, "failed found CSR created by registration agent")
   296  	}
   297  
   298  	ready, err := hubCluster.WaitForSpokeClusterReady(ctx, clusterConfig.ClusterName)
   299  	if err != nil || !ready {
   300  		return errors.Errorf("fail to waiting for register request")
   301  	}
   302  
   303  	if err = hubCluster.RegisterSpokeCluster(ctx, spokeCluster.Name); err != nil {
   304  		return errors.Wrap(err, "fail to approve spoke cluster")
   305  	}
   306  	return nil
   307  }
   308  
   309  // LoadKubeClusterConfigFromFile create KubeClusterConfig from kubeconfig file
   310  func LoadKubeClusterConfigFromFile(filepath string) (*KubeClusterConfig, error) {
   311  	clusterConfig := &KubeClusterConfig{FilePath: filepath}
   312  	var err error
   313  	clusterConfig.Config, err = clientcmd.LoadFromFile(filepath)
   314  	if err != nil {
   315  		return nil, errors.Wrapf(err, "failed to get kubeconfig")
   316  	}
   317  	if len(clusterConfig.Config.CurrentContext) == 0 {
   318  		return nil, fmt.Errorf("current-context is not set")
   319  	}
   320  	var ok bool
   321  	ctx, ok := clusterConfig.Config.Contexts[clusterConfig.Config.CurrentContext]
   322  	if !ok {
   323  		return nil, fmt.Errorf("current-context %s not found", clusterConfig.Config.CurrentContext)
   324  	}
   325  	clusterConfig.Cluster, ok = clusterConfig.Config.Clusters[ctx.Cluster]
   326  	if !ok {
   327  		return nil, fmt.Errorf("cluster %s not found", ctx.Cluster)
   328  	}
   329  	clusterConfig.AuthInfo, ok = clusterConfig.Config.AuthInfos[ctx.AuthInfo]
   330  	if !ok {
   331  		return nil, fmt.Errorf("authInfo %s not found", ctx.AuthInfo)
   332  	}
   333  	clusterConfig.ClusterName = ctx.Cluster
   334  	if endpoint, err := utils.ParseAPIServerEndpoint(clusterConfig.Cluster.Server); err == nil {
   335  		clusterConfig.Cluster.Server = endpoint
   336  	} else {
   337  		_, _ = fmt.Fprintf(&clusterConfig.Logs, "failed to parse server endpoint: %v", err)
   338  	}
   339  	return clusterConfig, nil
   340  }
   341  
   342  const (
   343  	// ClusterGateWayEngine cluster-gateway cluster management solution
   344  	ClusterGateWayEngine = "cluster-gateway"
   345  	// OCMEngine ocm cluster management solution
   346  	OCMEngine = "ocm"
   347  )
   348  
   349  // JoinClusterArgs args for join cluster
   350  type JoinClusterArgs struct {
   351  	engine                      string
   352  	createNamespace             string
   353  	ioStreams                   cmdutil.IOStreams
   354  	hubConfig                   *rest.Config
   355  	inClusterBootstrap          *bool
   356  	trackingSpinnerFactory      func(string) *spinner.Spinner
   357  	clusterAlreadyExistCallback func(string) bool
   358  }
   359  
   360  func newJoinClusterArgs(options ...JoinClusterOption) *JoinClusterArgs {
   361  	args := &JoinClusterArgs{
   362  		engine: ClusterGateWayEngine,
   363  	}
   364  	for _, op := range options {
   365  		op.ApplyToArgs(args)
   366  	}
   367  	return args
   368  }
   369  
   370  // JoinClusterOption option for join cluster
   371  type JoinClusterOption interface {
   372  	ApplyToArgs(args *JoinClusterArgs)
   373  }
   374  
   375  // JoinClusterCreateNamespaceOption create namespace when join cluster, if empty, no creation
   376  type JoinClusterCreateNamespaceOption string
   377  
   378  // ApplyToArgs apply to args
   379  func (op JoinClusterCreateNamespaceOption) ApplyToArgs(args *JoinClusterArgs) {
   380  	args.createNamespace = string(op)
   381  }
   382  
   383  // JoinClusterEngineOption configure engine for join cluster, either cluster-gateway or ocm
   384  type JoinClusterEngineOption string
   385  
   386  // ApplyToArgs apply to args
   387  func (op JoinClusterEngineOption) ApplyToArgs(args *JoinClusterArgs) {
   388  	args.engine = string(op)
   389  }
   390  
   391  // JoinClusterAlreadyExistCallback configure the callback when cluster already exist
   392  type JoinClusterAlreadyExistCallback func(string) bool
   393  
   394  // ApplyToArgs apply to args
   395  func (op JoinClusterAlreadyExistCallback) ApplyToArgs(args *JoinClusterArgs) {
   396  	args.clusterAlreadyExistCallback = op
   397  }
   398  
   399  // JoinClusterOCMOptions options used when joining clusters by ocm, only support cli for now
   400  type JoinClusterOCMOptions struct {
   401  	IoStreams              cmdutil.IOStreams
   402  	HubConfig              *rest.Config
   403  	InClusterBootstrap     *bool
   404  	TrackingSpinnerFactory func(string) *spinner.Spinner
   405  }
   406  
   407  // ApplyToArgs apply to args
   408  func (op JoinClusterOCMOptions) ApplyToArgs(args *JoinClusterArgs) {
   409  	args.ioStreams = op.IoStreams
   410  	args.hubConfig = op.HubConfig
   411  	args.inClusterBootstrap = op.InClusterBootstrap
   412  	args.trackingSpinnerFactory = op.TrackingSpinnerFactory
   413  }
   414  
   415  // JoinClusterByKubeConfig add child cluster by kubeconfig path, return cluster info and error
   416  func JoinClusterByKubeConfig(ctx context.Context, cli client.Client, kubeconfigPath string, clusterName string, options ...JoinClusterOption) (*KubeClusterConfig, error) {
   417  	args := newJoinClusterArgs(options...)
   418  	clusterConfig, err := LoadKubeClusterConfigFromFile(kubeconfigPath)
   419  	if err != nil {
   420  		return nil, err
   421  	}
   422  	if err := clusterConfig.SetClusterName(clusterName).SetCreateNamespace(args.createNamespace).Validate(); err != nil {
   423  		return nil, err
   424  	}
   425  	clusterConfig.ClusterAlreadyExistCallback = args.clusterAlreadyExistCallback
   426  	switch args.engine {
   427  	case ClusterGateWayEngine:
   428  		if err = clusterConfig.RegisterByVelaSecret(ctx, cli); err != nil {
   429  			return nil, err
   430  		}
   431  	case OCMEngine:
   432  		if args.inClusterBootstrap == nil {
   433  			return nil, errors.Wrapf(err, "failed to determine the registration endpoint for the hub cluster "+
   434  				"when parsing --in-cluster-bootstrap flag")
   435  		}
   436  		if err = clusterConfig.RegisterClusterManagedByOCM(ctx, cli, args); err != nil {
   437  			return clusterConfig, err
   438  		}
   439  	}
   440  	if cfg, ok := ctx.Value(KubeConfigContext).(*rest.Config); ok {
   441  		if err = SetClusterVersionInfo(ctx, cfg, clusterConfig.ClusterName); err != nil {
   442  			return nil, err
   443  		}
   444  	}
   445  	return clusterConfig, nil
   446  }
   447  
   448  // DetachClusterArgs args for detaching cluster
   449  type DetachClusterArgs struct {
   450  	managedClusterKubeConfigPath string
   451  }
   452  
   453  func newDetachClusterArgs(options ...DetachClusterOption) *DetachClusterArgs {
   454  	args := &DetachClusterArgs{}
   455  	for _, op := range options {
   456  		op.ApplyToArgs(args)
   457  	}
   458  	return args
   459  }
   460  
   461  // DetachClusterOption option for detach cluster
   462  type DetachClusterOption interface {
   463  	ApplyToArgs(args *DetachClusterArgs)
   464  }
   465  
   466  // DetachClusterManagedClusterKubeConfigPathOption configure the managed cluster kubeconfig path while detach ocm cluster
   467  type DetachClusterManagedClusterKubeConfigPathOption string
   468  
   469  // ApplyToArgs apply to args
   470  func (op DetachClusterManagedClusterKubeConfigPathOption) ApplyToArgs(args *DetachClusterArgs) {
   471  	args.managedClusterKubeConfigPath = string(op)
   472  }
   473  
   474  // DetachCluster detach cluster by name, if cluster is using by application, it will return error
   475  func DetachCluster(ctx context.Context, cli client.Client, clusterName string, options ...DetachClusterOption) error {
   476  	args := newDetachClusterArgs(options...)
   477  	if clusterName == ClusterLocalName {
   478  		return ErrReservedLocalClusterName
   479  	}
   480  	vc, err := NewClusterClient(cli).Get(ctx, clusterName)
   481  	if err != nil {
   482  		return err
   483  	}
   484  
   485  	switch vc.Spec.CredentialType {
   486  	case clusterv1alpha1.CredentialTypeX509Certificate, clusterv1alpha1.CredentialTypeServiceAccountToken:
   487  		clusterSecret, err := getMutableClusterSecret(ctx, cli, clusterName)
   488  		if err != nil {
   489  			return errors.Wrapf(err, "cluster %s is not mutable now", clusterName)
   490  		}
   491  		if err := cli.Delete(ctx, clusterSecret); err != nil {
   492  			return errors.Wrapf(err, "failed to detach cluster %s", clusterName)
   493  		}
   494  	case clusterv1alpha1.CredentialTypeOCMManagedCluster:
   495  		if args.managedClusterKubeConfigPath == "" {
   496  			return errors.New("kubeconfig-path must be set to detach ocm managed cluster")
   497  		}
   498  		config, err := clientcmd.LoadFromFile(args.managedClusterKubeConfigPath)
   499  		if err != nil {
   500  			return err
   501  		}
   502  		restConfig, err := clientcmd.BuildConfigFromKubeconfigGetter("", func() (*clientcmdapi.Config, error) {
   503  			return config, nil
   504  		})
   505  		if err != nil {
   506  			return err
   507  		}
   508  		if err = spoke.CleanSpokeClusterEnv(restConfig); err != nil {
   509  			return err
   510  		}
   511  		managedCluster := ocmclusterv1.ManagedCluster{ObjectMeta: metav1.ObjectMeta{Name: clusterName}}
   512  		if err = cli.Delete(ctx, &managedCluster); err != nil {
   513  			if !apierrors.IsNotFound(err) {
   514  				return err
   515  			}
   516  		}
   517  	case clusterv1alpha1.CredentialTypeInternal:
   518  		return fmt.Errorf("cannot detach internal cluster `local`")
   519  	}
   520  	return nil
   521  }
   522  
   523  // RenameCluster rename cluster
   524  func RenameCluster(ctx context.Context, k8sClient client.Client, oldClusterName string, newClusterName string) error {
   525  	if newClusterName == ClusterLocalName {
   526  		return ErrReservedLocalClusterName
   527  	}
   528  	clusterSecret, err := getMutableClusterSecret(ctx, k8sClient, oldClusterName)
   529  	if err != nil {
   530  		return errors.Wrapf(err, "cluster %s is not mutable now", oldClusterName)
   531  	}
   532  	if err := ensureClusterNotExists(ctx, k8sClient, newClusterName); err != nil {
   533  		return errors.Wrapf(err, "cannot set cluster name to %s", newClusterName)
   534  	}
   535  	if err := k8sClient.Delete(ctx, clusterSecret); err != nil {
   536  		return errors.Wrapf(err, "failed to rename cluster from %s to %s", oldClusterName, newClusterName)
   537  	}
   538  	clusterSecret.ObjectMeta = metav1.ObjectMeta{
   539  		Name:        newClusterName,
   540  		Namespace:   ClusterGatewaySecretNamespace,
   541  		Labels:      clusterSecret.Labels,
   542  		Annotations: clusterSecret.Annotations,
   543  	}
   544  	if err := k8sClient.Create(ctx, clusterSecret); err != nil {
   545  		return errors.Wrapf(err, "failed to rename cluster from %s to %s", oldClusterName, newClusterName)
   546  	}
   547  	return nil
   548  }
   549  
   550  // AliasCluster alias cluster
   551  func AliasCluster(ctx context.Context, cli client.Client, clusterName string, aliasName string) error {
   552  	if clusterName == ClusterLocalName {
   553  		return ErrReservedLocalClusterName
   554  	}
   555  	vc, err := GetVirtualCluster(ctx, cli, clusterName)
   556  	if err != nil {
   557  		return err
   558  	}
   559  	setClusterAlias(vc.Object, aliasName)
   560  	return cli.Update(ctx, vc.Object)
   561  }
   562  
   563  // ensureClusterNotExists will check the cluster is not existed in control plane
   564  func ensureClusterNotExists(ctx context.Context, c client.Client, clusterName string) error {
   565  	_, err := NewClusterClient(c).Get(ctx, clusterName)
   566  	if err != nil {
   567  		return client.IgnoreNotFound(err)
   568  	}
   569  	return ErrClusterExists
   570  }
   571  
   572  // ensureNamespaceExists ensures vela namespace  to be installed in child cluster
   573  func ensureNamespaceExists(ctx context.Context, c client.Client, clusterName string, createNamespace string) error {
   574  	remoteCtx := ContextWithClusterName(ctx, clusterName)
   575  	if err := c.Get(remoteCtx, apitypes.NamespacedName{Name: createNamespace}, &corev1.Namespace{}); err != nil {
   576  		if !apierrors.IsNotFound(err) {
   577  			return errors.Wrapf(err, "failed to check if namespace %s exists", createNamespace)
   578  		}
   579  		if err = c.Create(remoteCtx, &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: createNamespace}}); err != nil {
   580  			return errors.Wrapf(err, "failed to create namespace %s", createNamespace)
   581  		}
   582  	}
   583  	return nil
   584  }
   585  
   586  // getMutableClusterSecret retrieves the cluster secret and check if any application is using the cluster
   587  // TODO(somefive): should rework the logic of checking application cluster usage
   588  func getMutableClusterSecret(ctx context.Context, c client.Client, clusterName string) (*corev1.Secret, error) {
   589  	clusterSecret := &corev1.Secret{}
   590  	if err := c.Get(ctx, apitypes.NamespacedName{Namespace: ClusterGatewaySecretNamespace, Name: clusterName}, clusterSecret); err != nil {
   591  		return nil, errors.Wrapf(err, "failed to find target cluster secret %s", clusterName)
   592  	}
   593  	labels := clusterSecret.GetLabels()
   594  	if labels == nil || labels[clustercommon.LabelKeyClusterCredentialType] == "" {
   595  		return nil, fmt.Errorf("invalid cluster secret %s: cluster credential type label %s is not set", clusterName, clustercommon.LabelKeyClusterCredentialType)
   596  	}
   597  	return clusterSecret, nil
   598  }