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 }