github.com/1aal/kubeblocks@v0.0.0-20231107070852-e1c03e598921/pkg/cli/cmd/playground/init.go (about)

     1  /*
     2  Copyright (C) 2022-2023 ApeCloud Co., Ltd
     3  
     4  This file is part of KubeBlocks project
     5  
     6  This program is free software: you can redistribute it and/or modify
     7  it under the terms of the GNU Affero General Public License as published by
     8  the Free Software Foundation, either version 3 of the License, or
     9  (at your option) any later version.
    10  
    11  This program is distributed in the hope that it will be useful
    12  but WITHOUT ANY WARRANTY; without even the implied warranty of
    13  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    14  GNU Affero General Public License for more details.
    15  
    16  You should have received a copy of the GNU Affero General Public License
    17  along with this program.  If not, see <http://www.gnu.org/licenses/>.
    18  */
    19  
    20  package playground
    21  
    22  import (
    23  	"fmt"
    24  	"os"
    25  	"path/filepath"
    26  	"strings"
    27  	"time"
    28  
    29  	gv "github.com/hashicorp/go-version"
    30  	"github.com/pkg/errors"
    31  	"github.com/spf13/cobra"
    32  	"golang.org/x/exp/slices"
    33  	apierrors "k8s.io/apimachinery/pkg/api/errors"
    34  	"k8s.io/apimachinery/pkg/util/rand"
    35  	"k8s.io/cli-runtime/pkg/genericiooptions"
    36  	"k8s.io/klog/v2"
    37  	cmdutil "k8s.io/kubectl/pkg/cmd/util"
    38  	"k8s.io/kubectl/pkg/util/templates"
    39  
    40  	cp "github.com/1aal/kubeblocks/pkg/cli/cloudprovider"
    41  	cmdcluster "github.com/1aal/kubeblocks/pkg/cli/cmd/cluster"
    42  	"github.com/1aal/kubeblocks/pkg/cli/cmd/kubeblocks"
    43  	"github.com/1aal/kubeblocks/pkg/cli/printer"
    44  	"github.com/1aal/kubeblocks/pkg/cli/spinner"
    45  	"github.com/1aal/kubeblocks/pkg/cli/types"
    46  	"github.com/1aal/kubeblocks/pkg/cli/util"
    47  	"github.com/1aal/kubeblocks/pkg/cli/util/helm"
    48  	"github.com/1aal/kubeblocks/pkg/cli/util/prompt"
    49  	"github.com/1aal/kubeblocks/version"
    50  )
    51  
    52  var (
    53  	initLong = templates.LongDesc(`Bootstrap a kubernetes cluster and install KubeBlocks for playground.
    54  
    55  If no cloud provider is specified, a k3d cluster named kb-playground will be created on local host,
    56  otherwise a kubernetes cluster will be created on the specified cloud. Then KubeBlocks will be installed
    57  on the created kubernetes cluster, and an apecloud-mysql cluster named mycluster will be created.`)
    58  
    59  	initExample = templates.Examples(`
    60  		# create a k3d cluster on local host and install KubeBlocks
    61  		kbcli playground init
    62  
    63  		# create an AWS EKS cluster and install KubeBlocks, the region is required
    64  		kbcli playground init --cloud-provider aws --region us-west-1
    65  
    66  		# create an Alibaba cloud ACK cluster and install KubeBlocks, the region is required
    67  		kbcli playground init --cloud-provider alicloud --region cn-hangzhou
    68  
    69  		# create a Tencent cloud TKE cluster and install KubeBlocks, the region is required
    70  		kbcli playground init --cloud-provider tencentcloud --region ap-chengdu
    71  
    72  		# create a Google cloud GKE cluster and install KubeBlocks, the region is required
    73  		kbcli playground init --cloud-provider gcp --region us-east1
    74  
    75  		# after init, run the following commands to experience KubeBlocks quickly
    76  		# list database cluster and check its status
    77  		kbcli cluster list
    78  
    79  		# get cluster information
    80  		kbcli cluster describe mycluster
    81  
    82  		# connect to database
    83  		kbcli cluster connect mycluster
    84  
    85  		# view the Grafana
    86  		kbcli dashboard open kubeblocks-grafana
    87  
    88  		# destroy playground
    89  		kbcli playground destroy`)
    90  
    91  	supportedCloudProviders = []string{cp.Local, cp.AWS, cp.GCP, cp.AliCloud, cp.TencentCloud}
    92  
    93  	spinnerMsg = func(format string, a ...any) spinner.Option {
    94  		return spinner.WithMessage(fmt.Sprintf("%-50s", fmt.Sprintf(format, a...)))
    95  	}
    96  )
    97  
    98  type initOptions struct {
    99  	genericiooptions.IOStreams
   100  	helmCfg        *helm.Config
   101  	clusterDef     string
   102  	kbVersion      string
   103  	clusterVersion string
   104  	cloudProvider  string
   105  	region         string
   106  	autoApprove    bool
   107  	dockerVersion  *gv.Version
   108  
   109  	baseOptions
   110  }
   111  
   112  func newInitCmd(streams genericiooptions.IOStreams) *cobra.Command {
   113  	o := &initOptions{
   114  		IOStreams: streams,
   115  	}
   116  
   117  	cmd := &cobra.Command{
   118  		Use:     "init",
   119  		Short:   "Bootstrap a kubernetes cluster and install KubeBlocks for playground.",
   120  		Long:    initLong,
   121  		Example: initExample,
   122  		Run: func(cmd *cobra.Command, args []string) {
   123  			util.CheckErr(o.complete(cmd))
   124  			util.CheckErr(o.validate())
   125  			util.CheckErr(o.run())
   126  		},
   127  	}
   128  
   129  	cmd.Flags().StringVar(&o.clusterDef, "cluster-definition", defaultClusterDef, "Specify the cluster definition, run \"kbcli cd list\" to get the available cluster definitions")
   130  	cmd.Flags().StringVar(&o.clusterVersion, "cluster-version", "", "Specify the cluster version, run \"kbcli cv list\" to get the available cluster versions")
   131  	cmd.Flags().StringVar(&o.kbVersion, "version", version.DefaultKubeBlocksVersion, "KubeBlocks version")
   132  	cmd.Flags().StringVar(&o.cloudProvider, "cloud-provider", defaultCloudProvider, fmt.Sprintf("Cloud provider type, one of %v", supportedCloudProviders))
   133  	cmd.Flags().StringVar(&o.region, "region", "", "The region to create kubernetes cluster")
   134  	cmd.Flags().DurationVar(&o.Timeout, "timeout", 300*time.Second, "Time to wait for init playground, such as --timeout=10m")
   135  	cmd.Flags().BoolVar(&o.autoApprove, "auto-approve", false, "Skip interactive approval during the initialization of playground")
   136  
   137  	util.CheckErr(cmd.RegisterFlagCompletionFunc(
   138  		"cloud-provider",
   139  		func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
   140  			return cp.CloudProviders(), cobra.ShellCompDirectiveNoFileComp
   141  		}))
   142  	return cmd
   143  }
   144  
   145  func (o *initOptions) complete(cmd *cobra.Command) error {
   146  	var err error
   147  
   148  	if o.cloudProvider != cp.Local {
   149  		return nil
   150  	}
   151  
   152  	if o.dockerVersion, err = util.GetDockerVersion(); err != nil {
   153  		return err
   154  	}
   155  	// default write log to file
   156  	if err = util.EnableLogToFile(cmd.Flags()); err != nil {
   157  		fmt.Fprintf(o.Out, "Failed to enable the log file %s", err.Error())
   158  	}
   159  
   160  	return nil
   161  }
   162  
   163  func (o *initOptions) validate() error {
   164  	if !slices.Contains(supportedCloudProviders, o.cloudProvider) {
   165  		return fmt.Errorf("cloud provider %s is not supported, only support %v", o.cloudProvider, supportedCloudProviders)
   166  	}
   167  
   168  	if o.cloudProvider != cp.Local && o.region == "" {
   169  		return fmt.Errorf("region should be specified when cloud provider %s is specified", o.cloudProvider)
   170  	}
   171  
   172  	if o.clusterDef == "" {
   173  		return fmt.Errorf("a valid cluster definition is needed, use --cluster-definition to specify one")
   174  	}
   175  
   176  	if o.cloudProvider == cp.Local && o.dockerVersion.LessThan(version.MinimumDockerVersion) {
   177  		return fmt.Errorf("your docker version %s is lower than the minimum version %s, please upgrade your docker", o.dockerVersion, version.MinimumDockerVersion)
   178  	}
   179  
   180  	if err := o.baseOptions.validate(); err != nil {
   181  		return err
   182  	}
   183  	return o.checkExistedCluster()
   184  }
   185  
   186  func (o *initOptions) run() error {
   187  	if o.cloudProvider == cp.Local {
   188  		return o.local()
   189  	}
   190  	return o.cloud()
   191  }
   192  
   193  // local bootstraps a playground in the local host
   194  func (o *initOptions) local() error {
   195  	provider, err := cp.New(o.cloudProvider, "", o.Out, o.ErrOut)
   196  	if err != nil {
   197  		return err
   198  	}
   199  
   200  	o.startTime = time.Now()
   201  
   202  	var clusterInfo *cp.K8sClusterInfo
   203  	if o.prevCluster != nil {
   204  		clusterInfo = o.prevCluster
   205  	} else {
   206  		clusterInfo = &cp.K8sClusterInfo{
   207  			CloudProvider: provider.Name(),
   208  			ClusterName:   types.K3dClusterName,
   209  		}
   210  	}
   211  
   212  	if err = writeClusterInfo(o.stateFilePath, clusterInfo); err != nil {
   213  		return errors.Wrapf(err, "failed to write kubernetes cluster info to state file %s:\n  %v", o.stateFilePath, clusterInfo)
   214  	}
   215  
   216  	// create a local kubernetes cluster (k3d cluster) to deploy KubeBlocks
   217  	s := spinner.New(o.Out, spinnerMsg("Create k3d cluster: "+clusterInfo.ClusterName))
   218  	defer s.Fail()
   219  	if err = provider.CreateK8sCluster(clusterInfo); err != nil {
   220  		return errors.Wrap(err, "failed to set up k3d cluster")
   221  	}
   222  	s.Success()
   223  
   224  	clusterInfo, err = o.writeStateFile(provider)
   225  	if err != nil {
   226  		return err
   227  	}
   228  
   229  	if err = o.setKubeConfig(clusterInfo); err != nil {
   230  		return err
   231  	}
   232  
   233  	// install KubeBlocks and create a database cluster
   234  	return o.installKBAndCluster(clusterInfo)
   235  }
   236  
   237  // bootstraps a playground in the remote cloud
   238  func (o *initOptions) cloud() error {
   239  	cpPath, err := cloudProviderRepoDir("")
   240  	if err != nil {
   241  		return err
   242  	}
   243  
   244  	var clusterInfo *cp.K8sClusterInfo
   245  
   246  	// if kubernetes cluster exists, confirm to continue or not, if not, user should
   247  	// destroy the old cluster first
   248  	if o.prevCluster != nil {
   249  		clusterInfo = o.prevCluster
   250  		if err = o.confirmToContinue(); err != nil {
   251  			return err
   252  		}
   253  	} else {
   254  		clusterName := fmt.Sprintf("%s-%s", cloudClusterNamePrefix, rand.String(5))
   255  		clusterInfo = &cp.K8sClusterInfo{
   256  			ClusterName:   clusterName,
   257  			CloudProvider: o.cloudProvider,
   258  			Region:        o.region,
   259  		}
   260  		if err = o.confirmInitNewKubeCluster(); err != nil {
   261  			return err
   262  		}
   263  
   264  		fmt.Fprintf(o.Out, "\nWrite cluster info to state file %s\n", o.stateFilePath)
   265  		if err := writeClusterInfo(o.stateFilePath, clusterInfo); err != nil {
   266  			return errors.Wrapf(err, "failed to write kubernetes cluster info to state file %s:\n  %v", o.stateFilePath, clusterInfo)
   267  		}
   268  
   269  		fmt.Fprintf(o.Out, "Creating %s %s cluster %s ... \n", o.cloudProvider, cp.K8sService(o.cloudProvider), clusterName)
   270  	}
   271  
   272  	o.startTime = time.Now()
   273  	printer.PrintBlankLine(o.Out)
   274  
   275  	// clone apecloud/cloud-provider repo to local path
   276  	fmt.Fprintf(o.Out, "Clone ApeCloud cloud-provider repo to %s...\n", cpPath)
   277  	branchName := "kb-playground"
   278  	if version.Version != "" && version.Version != "edge" {
   279  		branchName = fmt.Sprintf("%s-%s", branchName, strings.Split(version.Version, "-")[0])
   280  	}
   281  	if err = util.CloneGitRepo(cp.GitRepoURL, branchName, cpPath); err != nil {
   282  		return err
   283  	}
   284  
   285  	provider, err := cp.New(o.cloudProvider, cpPath, o.Out, o.ErrOut)
   286  	if err != nil {
   287  		return err
   288  	}
   289  
   290  	// create a kubernetes cluster in the cloud
   291  	if err = provider.CreateK8sCluster(clusterInfo); err != nil {
   292  		klog.V(1).Infof("create K8S cluster failed: %s", err.Error())
   293  		return err
   294  	}
   295  	klog.V(1).Info("create K8S cluster success")
   296  
   297  	printer.PrintBlankLine(o.Out)
   298  
   299  	// write cluster info to state file and get new cluster info with kubeconfig
   300  	clusterInfo, err = o.writeStateFile(provider)
   301  	if err != nil {
   302  		return err
   303  	}
   304  
   305  	// write cluster kubeconfig to default kubeconfig file and switch current context to it
   306  	if err = o.setKubeConfig(clusterInfo); err != nil {
   307  		return err
   308  	}
   309  
   310  	// install KubeBlocks and create a database cluster
   311  	klog.V(1).Info("start to install KubeBlocks in K8S cluster... ")
   312  	return o.installKBAndCluster(clusterInfo)
   313  }
   314  
   315  // confirmToContinue confirms to continue init process if there is an existed kubernetes cluster
   316  func (o *initOptions) confirmToContinue() error {
   317  	clusterName := o.prevCluster.ClusterName
   318  	if !o.autoApprove {
   319  		printer.Warning(o.Out, "Found an existed cluster %s, do you want to continue to initialize this cluster?\n  Only 'yes' will be accepted to confirm.\n\n", clusterName)
   320  		entered, _ := prompt.NewPrompt("Enter a value:", nil, o.In).Run()
   321  		if entered != yesStr {
   322  			fmt.Fprintf(o.Out, "\nPlayground init cancelled, please destroy the old cluster first.\n")
   323  			return cmdutil.ErrExit
   324  		}
   325  	}
   326  	fmt.Fprintf(o.Out, "Continue to initialize %s %s cluster %s... \n",
   327  		o.cloudProvider, cp.K8sService(o.cloudProvider), clusterName)
   328  	return nil
   329  }
   330  
   331  func (o *initOptions) confirmInitNewKubeCluster() error {
   332  	printer.Warning(o.Out, `This action will create a kubernetes cluster on the cloud that may
   333    incur charges. Be sure to delete your infrastructure properly to avoid additional charges. 
   334  `)
   335  
   336  	fmt.Fprintf(o.Out, `
   337  The whole process will take about %s, please wait patiently,
   338  if it takes a long time, please check the network environment and try again.
   339  `, printer.BoldRed("20 minutes"))
   340  
   341  	if o.autoApprove {
   342  		return nil
   343  	}
   344  	// confirm to run
   345  	fmt.Fprintf(o.Out, "\nDo you want to perform this action?\n  Only 'yes' will be accepted to approve.\n\n")
   346  	entered, _ := prompt.NewPrompt("Enter a value:", nil, o.In).Run()
   347  	if entered != yesStr {
   348  		fmt.Fprintf(o.Out, "\nPlayground init cancelled.\n")
   349  		return cmdutil.ErrExit
   350  	}
   351  	return nil
   352  }
   353  
   354  // writeStateFile writes cluster info to state file and return the new cluster info with kubeconfig
   355  func (o *initOptions) writeStateFile(provider cp.Interface) (*cp.K8sClusterInfo, error) {
   356  	clusterInfo, err := provider.GetClusterInfo()
   357  	if err != nil {
   358  		return nil, err
   359  	}
   360  	if clusterInfo.KubeConfig == "" {
   361  		return nil, errors.New("failed to get kubernetes cluster kubeconfig")
   362  	}
   363  	if err = writeClusterInfo(o.stateFilePath, clusterInfo); err != nil {
   364  		return nil, errors.Wrapf(err, "failed to write kubernetes cluster info to state file %s:\n  %v",
   365  			o.stateFilePath, clusterInfo)
   366  	}
   367  	return clusterInfo, nil
   368  }
   369  
   370  // merge created kubernetes cluster kubeconfig to ~/.kube/config and set it as default
   371  func (o *initOptions) setKubeConfig(info *cp.K8sClusterInfo) error {
   372  	s := spinner.New(o.Out, spinnerMsg("Merge kubeconfig to "+defaultKubeConfigPath))
   373  	defer s.Fail()
   374  
   375  	// check if the default kubeconfig file exists, if not, create it
   376  	if _, err := os.Stat(defaultKubeConfigPath); os.IsNotExist(err) {
   377  		if err = os.MkdirAll(filepath.Dir(defaultKubeConfigPath), 0755); err != nil {
   378  			return errors.Wrapf(err, "failed to create directory %s", filepath.Dir(defaultKubeConfigPath))
   379  		}
   380  		if err = os.WriteFile(defaultKubeConfigPath, []byte{}, 0644); err != nil {
   381  			return errors.Wrapf(err, "failed to create file %s", defaultKubeConfigPath)
   382  		}
   383  	}
   384  
   385  	if err := kubeConfigWrite(info.KubeConfig, defaultKubeConfigPath,
   386  		writeKubeConfigOptions{UpdateExisting: true, UpdateCurrentContext: true}); err != nil {
   387  		return errors.Wrapf(err, "failed to write cluster %s kubeconfig", info.ClusterName)
   388  	}
   389  	s.Success()
   390  
   391  	currentContext, err := kubeConfigCurrentContext(info.KubeConfig)
   392  	s = spinner.New(o.Out, spinnerMsg("Switch current context to "+currentContext))
   393  	defer s.Fail()
   394  	if err != nil {
   395  		return err
   396  	}
   397  	s.Success()
   398  
   399  	return nil
   400  }
   401  
   402  func (o *initOptions) installKBAndCluster(info *cp.K8sClusterInfo) error {
   403  	var err error
   404  
   405  	// write kubeconfig content to a temporary file and use it
   406  	if err = writeAndUseKubeConfig(info.KubeConfig, o.kubeConfigPath, o.Out); err != nil {
   407  		return err
   408  	}
   409  
   410  	// create helm config
   411  	o.helmCfg = helm.NewConfig("", o.kubeConfigPath, "", klog.V(1).Enabled())
   412  
   413  	// install KubeBlocks
   414  	if err = o.installKubeBlocks(info.ClusterName); err != nil {
   415  		return errors.Wrap(err, "failed to install KubeBlocks")
   416  	}
   417  	klog.V(1).Info("KubeBlocks installed successfully")
   418  	// install database cluster
   419  	clusterInfo := "ClusterDefinition: " + o.clusterDef
   420  	if o.clusterVersion != "" {
   421  		clusterInfo += ", ClusterVersion: " + o.clusterVersion
   422  	}
   423  	s := spinner.New(o.Out, spinnerMsg("Create cluster %s (%s)", kbClusterName, clusterInfo))
   424  	defer s.Fail()
   425  	if err = o.createCluster(); err != nil && !apierrors.IsAlreadyExists(err) {
   426  		return errors.Wrapf(err, "failed to create cluster %s", kbClusterName)
   427  	}
   428  	s.Success()
   429  
   430  	fmt.Fprintf(os.Stdout, "\nKubeBlocks playground init SUCCESSFULLY!\n\n")
   431  	fmt.Fprintf(os.Stdout, "Kubernetes cluster \"%s\" has been created.\n", info.ClusterName)
   432  	fmt.Fprintf(os.Stdout, "Cluster \"%s\" has been created.\n", kbClusterName)
   433  
   434  	// output elapsed time
   435  	if !o.startTime.IsZero() {
   436  		fmt.Fprintf(o.Out, "Elapsed time: %s\n", time.Since(o.startTime).Truncate(time.Second))
   437  	}
   438  
   439  	fmt.Fprintf(o.Out, guideStr, kbClusterName)
   440  	return nil
   441  }
   442  
   443  func (o *initOptions) installKubeBlocks(k8sClusterName string) error {
   444  	f := util.NewFactory()
   445  	client, err := f.KubernetesClientSet()
   446  	if err != nil {
   447  		return err
   448  	}
   449  	dynamic, err := f.DynamicClient()
   450  	if err != nil {
   451  		return err
   452  	}
   453  	insOpts := kubeblocks.InstallOptions{
   454  		Options: kubeblocks.Options{
   455  			HelmCfg:   o.helmCfg,
   456  			Namespace: defaultNamespace,
   457  			IOStreams: o.IOStreams,
   458  			Client:    client,
   459  			Dynamic:   dynamic,
   460  			Wait:      true,
   461  			Timeout:   o.Timeout,
   462  		},
   463  		Version: o.kbVersion,
   464  		Quiet:   true,
   465  		Check:   true,
   466  	}
   467  
   468  	// enable monitor components by default
   469  	insOpts.ValueOpts.Values = append(insOpts.ValueOpts.Values,
   470  		"prometheus.enabled=true",
   471  		"grafana.enabled=true",
   472  		"agamotto.enabled=true",
   473  	)
   474  
   475  	if o.cloudProvider == cp.Local {
   476  		insOpts.ValueOpts.Values = append(insOpts.ValueOpts.Values,
   477  			// use hostpath csi driver to support snapshot
   478  			"snapshot-controller.enabled=true",
   479  			"csi-hostpath-driver.enabled=true",
   480  		)
   481  	} else if o.cloudProvider == cp.AWS {
   482  		insOpts.ValueOpts.Values = append(insOpts.ValueOpts.Values,
   483  			// enable aws-load-balancer-controller addon automatically on playground
   484  			"aws-load-balancer-controller.enabled=true",
   485  			fmt.Sprintf("aws-load-balancer-controller.clusterName=%s", k8sClusterName),
   486  		)
   487  	}
   488  
   489  	if err = insOpts.PreCheck(); err != nil {
   490  		// if the KubeBlocks has been installed, we ignore the error
   491  		errMsg := err.Error()
   492  		if strings.Contains(errMsg, "repeated installation is not supported") {
   493  			fmt.Fprintf(o.Out, strings.Split(errMsg, ",")[0]+"\n")
   494  			return nil
   495  		}
   496  		return err
   497  	}
   498  	if err = insOpts.CompleteInstallOptions(); err != nil {
   499  		return err
   500  	}
   501  	return insOpts.Install()
   502  }
   503  
   504  // createCluster constructs a cluster create options and run
   505  func (o *initOptions) createCluster() error {
   506  	c := cmdcluster.NewCreateOptions(util.NewFactory(), genericiooptions.NewTestIOStreamsDiscard())
   507  	c.ClusterDefRef = o.clusterDef
   508  	c.ClusterVersionRef = o.clusterVersion
   509  	c.Namespace = defaultNamespace
   510  	c.Name = kbClusterName
   511  	c.UpdatableFlags = cmdcluster.UpdatableFlags{
   512  		TerminationPolicy:  "WipeOut",
   513  		MonitoringInterval: 15,
   514  		PodAntiAffinity:    "Preferred",
   515  		Tenancy:            "SharedNode",
   516  	}
   517  
   518  	// if we are running on local, create cluster with one replica
   519  	if o.cloudProvider == cp.Local {
   520  		c.Values = append(c.Values, "replicas=1")
   521  	} else {
   522  		// if we are running on cloud, create cluster with three replicas
   523  		c.Values = append(c.Values, "replicas=3")
   524  	}
   525  
   526  	if err := c.CreateOptions.Complete(); err != nil {
   527  		return err
   528  	}
   529  	if err := c.Validate(); err != nil {
   530  		return err
   531  	}
   532  	if err := c.Complete(); err != nil {
   533  		return err
   534  	}
   535  	return c.Run()
   536  }
   537  
   538  // checkExistedCluster checks playground kubernetes cluster exists or not, a kbcli client only
   539  // support a single playground, they are bound to each other with a hidden context config file,
   540  // the hidden file ensures that when destroy the playground it always goes with the fixed context,
   541  // it makes the dangerous operation more safe and prevents from manipulating another context
   542  func (o *initOptions) checkExistedCluster() error {
   543  	if o.prevCluster == nil {
   544  		return nil
   545  	}
   546  
   547  	warningMsg := fmt.Sprintf("playground only supports one kubernetes cluster,\n  if a cluster is already existed, please destroy it first.\n%s\n", o.prevCluster.String())
   548  	// if cloud provider is not same with the existed cluster cloud provider, suggest
   549  	// user to destroy the previous cluster first
   550  	if o.prevCluster.CloudProvider != o.cloudProvider {
   551  		printer.Warning(o.Out, warningMsg)
   552  		return cmdutil.ErrExit
   553  	}
   554  
   555  	if o.prevCluster.CloudProvider == cp.Local {
   556  		return nil
   557  	}
   558  
   559  	// previous kubernetes cluster is a cloud provider cluster, check if the region
   560  	// is same with the new cluster region, if not, suggest user to destroy the previous
   561  	// cluster first
   562  	if o.prevCluster.Region != o.region {
   563  		printer.Warning(o.Out, warningMsg)
   564  		return cmdutil.ErrExit
   565  	}
   566  	return nil
   567  }