From 0a8c01b68b16f61b4e0f19804e29edba0e1098b7 Mon Sep 17 00:00:00 2001 From: yipeng1030 <58055769+yipeng1030@users.noreply.github.com> Date: Mon, 21 Oct 2024 12:02:20 +0800 Subject: [PATCH] Revert "feat: playground only support k3d (#471)" This reverts commit ae1093d6cb2211dfb3c1d977759cf6bb4c69f1fa. --- docs/user_docs/cli/kbcli_playground_init.md | 25 ++- pkg/cmd/cluster/cluster_test.go | 129 ++----------- pkg/cmd/cluster/create_subcmds.go | 20 +- pkg/cmd/cluster/create_subcmds_test.go | 8 +- pkg/cmd/playground/destroy.go | 76 +++++++- pkg/cmd/playground/init.go | 201 +++++++++++++++++--- pkg/cmd/playground/init_test.go | 23 ++- pkg/cmd/playground/types.go | 2 +- pkg/testing/fake.go | 1 - 9 files changed, 310 insertions(+), 175 deletions(-) diff --git a/docs/user_docs/cli/kbcli_playground_init.md b/docs/user_docs/cli/kbcli_playground_init.md index 0b75de340..03fc26c31 100644 --- a/docs/user_docs/cli/kbcli_playground_init.md +++ b/docs/user_docs/cli/kbcli_playground_init.md @@ -20,6 +20,18 @@ kbcli playground init [flags] # create a k3d cluster on local host and install KubeBlocks kbcli playground init + # create an AWS EKS cluster and install KubeBlocks, the region is required + kbcli playground init --cloud-provider aws --region us-west-1 + + # create an Alibaba cloud ACK cluster and install KubeBlocks, the region is required + kbcli playground init --cloud-provider alicloud --region cn-hangzhou + + # create a Tencent cloud TKE cluster and install KubeBlocks, the region is required + kbcli playground init --cloud-provider tencentcloud --region ap-chengdu + + # create a Google cloud GKE cluster and install KubeBlocks, the region is required + kbcli playground init --cloud-provider gcp --region us-east1 + # after init, run the following commands to experience KubeBlocks quickly # list database cluster and check its status kbcli cluster list @@ -40,12 +52,13 @@ kbcli playground init [flags] ### Options ``` - --auto-approve Skip interactive approval during the initialization of playground - --cluster-type string Specify the cluster type to create, use 'kbcli cluster create --help' to get the available cluster type. (default "apecloud-mysql") - -h, --help help for init - --region string The region to create kubernetes cluster - --timeout duration Time to wait for init playground, such as --timeout=10m (default 10m0s) - --version string KubeBlocks version + --auto-approve Skip interactive approval during the initialization of playground + --cloud-provider string Cloud provider type, one of [local aws gcp alicloud tencentcloud] (default "local") + --cluster-definition string Specify the cluster definition, run "kbcli cd list" to get the available cluster definitions (default "apecloud-mysql") + -h, --help help for init + --region string The region to create kubernetes cluster + --timeout duration Time to wait for init playground, such as --timeout=10m (default 10m0s) + --version string KubeBlocks version ``` ### Options inherited from parent commands diff --git a/pkg/cmd/cluster/cluster_test.go b/pkg/cmd/cluster/cluster_test.go index f683f582f..507493839 100644 --- a/pkg/cmd/cluster/cluster_test.go +++ b/pkg/cmd/cluster/cluster_test.go @@ -20,15 +20,9 @@ along with this program. If not, see . package cluster import ( - "github.com/apecloud/kbcli/pkg/cluster" - "github.com/apecloud/kbcli/pkg/printer" kbappsv1 "github.com/apecloud/kubeblocks/apis/apps/v1" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/runtime" - "k8s.io/apimachinery/pkg/version" - fakediscovery "k8s.io/client-go/discovery/fake" "k8s.io/client-go/kubernetes/scheme" "fmt" @@ -46,126 +40,33 @@ import ( ) var _ = Describe("Cluster", func() { + const ( - clusterType = "apecloud-mysql" clusterName = "test" namespace = "default" ) - var ( - tf *cmdtesting.TestFactory - streams genericiooptions.IOStreams - createOptions *action.CreateOptions - mockClient = func(data runtime.Object) *cmdtesting.TestFactory { - tf = testing.NewTestFactory(testing.Namespace) - codec := scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...) - tf.UnstructuredClient = &clientfake.RESTClient{ - NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer, - GroupVersion: schema.GroupVersion{Group: types.AppsAPIGroup, Version: types.AppsAPIVersion}, - Resp: &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, data)}, - } - tf.Client = tf.UnstructuredClient - tf.FakeDynamicClient = testing.FakeDynamicClient(data) - tf.WithDiscoveryClient(cmdtesting.NewFakeCachedDiscoveryClient()) - return tf - } - ) - + var streams genericiooptions.IOStreams + var tf *cmdtesting.TestFactory + fakeConfigData := map[string]string{ + "config.yaml": `# the default storage class name. + DEFAULT_STORAGE_CLASS: ""`, + } BeforeEach(func() { - _ = kbappsv1.AddToScheme(scheme.Scheme) - _ = metav1.AddMetaToScheme(scheme.Scheme) streams, _, _, _ = genericiooptions.NewTestIOStreams() - tf = mockClient(testing.FakeCompDef()) - createOptions = &action.CreateOptions{ - IOStreams: streams, - Factory: tf, - } + tf = cmdtesting.NewTestFactory().WithNamespace(namespace) + cd := testing.FakeClusterDef() + fakeDefaultStorageClass := testing.FakeStorageClass(testing.StorageClassName, testing.IsDefault) + // TODO: remove unused codes? + tf.FakeDynamicClient = testing.FakeDynamicClient(cd, fakeDefaultStorageClass, testing.FakeConfigMap("kubeblocks-manager-config", types.DefaultNamespace, fakeConfigData), testing.FakeSecret(types.DefaultNamespace, clusterName)) + tf.Client = &clientfake.RESTClient{} }) AfterEach(func() { tf.Cleanup() }) - Context("create", func() { - It("without name", func() { - o, err := NewSubCmdsOptions(createOptions, clusterType) - Expect(err).Should(Succeed()) - Expect(o).ShouldNot(BeNil()) - Expect(o.ChartInfo).ShouldNot(BeNil()) - o.Format = printer.YAML - - Expect(o.CreateOptions.Complete()).To(Succeed()) - o.Client = testing.FakeClientSet() - fakeDiscovery1, _ := o.Client.Discovery().(*fakediscovery.FakeDiscovery) - fakeDiscovery1.FakedServerVersion = &version.Info{Major: "1", Minor: "27", GitVersion: "v1.27.0"} - Expect(o.Complete(nil)).To(Succeed()) - Expect(o.Validate()).To(Succeed()) - Expect(o.Name).ShouldNot(BeEmpty()) - Expect(o.Run()).Should(Succeed()) - }) - }) - - Context("create validate", func() { - var o *CreateSubCmdsOptions - BeforeEach(func() { - o = &CreateSubCmdsOptions{ - CreateOptions: &action.CreateOptions{ - Factory: tf, - Namespace: namespace, - Dynamic: tf.FakeDynamicClient, - IOStreams: streams, - }, - } - o.Name = "mycluster" - o.ChartInfo, _ = cluster.BuildChartInfo(clusterType) - }) - - It("can validate the cluster name must begin with a letter and can only contain lowercase letters, numbers, and '-'.", func() { - type fn func() - var succeed = func(name string) fn { - return func() { - o.Name = name - Expect(o.Validate()).Should(Succeed()) - } - } - var failed = func(name string) fn { - return func() { - o.Name = name - Expect(o.Validate()).Should(HaveOccurred()) - } - } - // more case to add - invalidCase := []string{ - "1abcd", "abcd-", "-abcd", "abc#d", "ABCD", "*&(&%", - } - - validCase := []string{ - "abcd", "abcd1", "a1-2b-3d", - } - - for i := range invalidCase { - failed(invalidCase[i]) - } - - for i := range validCase { - succeed(validCase[i]) - } - - }) - - It("can validate whether the name is not longer than 16 characters when create a new cluster", func() { - Expect(len(o.Name)).Should(BeNumerically("<=", 16)) - Expect(o.Validate()).Should(Succeed()) - moreThan16 := 17 - bytes := make([]byte, 0) - var clusterNameMoreThan16 string - for i := 0; i < moreThan16; i++ { - bytes = append(bytes, byte(i%26+'a')) - } - clusterNameMoreThan16 = string(bytes) - Expect(len(clusterNameMoreThan16)).Should(BeNumerically(">", 16)) - o.Name = clusterNameMoreThan16 - Expect(o.Validate()).Should(HaveOccurred()) - }) + // TODO: add create cluster case + Context("delete cluster", func() { }) diff --git a/pkg/cmd/cluster/create_subcmds.go b/pkg/cmd/cluster/create_subcmds.go index fa3e57701..1fb5c86d4 100644 --- a/pkg/cmd/cluster/create_subcmds.go +++ b/pkg/cmd/cluster/create_subcmds.go @@ -23,7 +23,6 @@ import ( "context" "fmt" "os" - "regexp" "github.com/spf13/cobra" "k8s.io/apimachinery/pkg/api/errors" @@ -98,8 +97,8 @@ func buildCreateSubCmds(createOptions *action.CreateOptions) []*cobra.Command { Run: func(cmd *cobra.Command, args []string) { o.Args = args cmdutil.CheckErr(o.CreateOptions.Complete()) - cmdutil.CheckErr(o.Complete(cmd)) - cmdutil.CheckErr(o.Validate()) + cmdutil.CheckErr(o.complete(cmd)) + cmdutil.CheckErr(o.validate()) cmdutil.CheckErr(o.Run()) }, } @@ -151,7 +150,7 @@ func generateClusterName(dynamic dynamic.Interface, namespace string) (string, e return "", fmt.Errorf("failed to generate cluster name") } -func (o *CreateSubCmdsOptions) Complete(cmd *cobra.Command) error { +func (o *CreateSubCmdsOptions) complete(cmd *cobra.Command) error { var err error // if name is not specified, generate a random cluster name @@ -163,9 +162,7 @@ func (o *CreateSubCmdsOptions) Complete(cmd *cobra.Command) error { } // get values from flags - if cmd != nil { - o.Values = getValuesFromFlags(cmd.LocalNonPersistentFlags()) - } + o.Values = getValuesFromFlags(cmd.LocalNonPersistentFlags()) // get all the rendered objects objs, err := o.getObjectsInfo() @@ -214,14 +211,7 @@ func (o *CreateSubCmdsOptions) Complete(cmd *cobra.Command) error { return nil } -func (o *CreateSubCmdsOptions) Validate() error { - matched, _ := regexp.MatchString(`^[a-z]([-a-z0-9]*[a-z0-9])?$`, o.Name) - if !matched { - return fmt.Errorf("cluster name must begin with a letter and can only contain lowercase letters, numbers, and '-'") - } - if len(o.Name) > 16 { - return fmt.Errorf("cluster name should be less than 16 characters") - } +func (o *CreateSubCmdsOptions) validate() error { return cluster.ValidateValues(o.ChartInfo, o.Values) } diff --git a/pkg/cmd/cluster/create_subcmds_test.go b/pkg/cmd/cluster/create_subcmds_test.go index 5eef97a87..63e429cbc 100644 --- a/pkg/cmd/cluster/create_subcmds_test.go +++ b/pkg/cmd/cluster/create_subcmds_test.go @@ -111,14 +111,14 @@ var _ = Describe("create cluster by cluster type", func() { o.Client = testing.FakeClientSet() fakeDiscovery1, _ := o.Client.Discovery().(*fakediscovery.FakeDiscovery) fakeDiscovery1.FakedServerVersion = &version.Info{Major: "1", Minor: "27", GitVersion: "v1.27.0"} - Expect(o.Complete(mysqlCmd)).Should(Succeed()) + Expect(o.complete(mysqlCmd)).Should(Succeed()) Expect(o.Name).ShouldNot(BeEmpty()) Expect(o.Values).ShouldNot(BeNil()) Expect(o.ChartInfo.ClusterDef).Should(Equal(apeCloudMysql)) By("validate") o.Dynamic = testing.FakeDynamicClient() - Expect(o.Validate()).Should(Succeed()) + Expect(o.validate()).Should(Succeed()) By("run") o.DryRun = "client" @@ -157,14 +157,14 @@ var _ = Describe("create cluster by cluster type", func() { fakeDiscovery1.FakedServerVersion = &version.Info{Major: "1", Minor: "27", GitVersion: "v1.27.0"} Expect(shardCmd.Flags().Set("mode", "cluster")).Should(Succeed()) - Expect(o.Complete(shardCmd)).Should(Succeed()) + Expect(o.complete(shardCmd)).Should(Succeed()) Expect(o.Name).ShouldNot(BeEmpty()) Expect(o.Values).ShouldNot(BeNil()) Expect(o.ChartInfo.ComponentDef[0]).Should(Equal(redisComponent)) By("validate") o.Dynamic = testing.FakeDynamicClient() - Expect(o.Validate()).Should(Succeed()) + Expect(o.validate()).Should(Succeed()) By("run") o.DryRun = "client" diff --git a/pkg/cmd/playground/destroy.go b/pkg/cmd/playground/destroy.go index b10631ed5..de5dda092 100644 --- a/pkg/cmd/playground/destroy.go +++ b/pkg/cmd/playground/destroy.go @@ -36,6 +36,7 @@ import ( "k8s.io/client-go/dynamic" "k8s.io/client-go/kubernetes" "k8s.io/klog/v2" + cmdutil "k8s.io/kubectl/pkg/cmd/util" "k8s.io/kubectl/pkg/util/templates" appsv1alpha1 "github.com/apecloud/kubeblocks/apis/apps/v1alpha1" @@ -47,6 +48,7 @@ import ( "github.com/apecloud/kbcli/pkg/types" "github.com/apecloud/kbcli/pkg/util" "github.com/apecloud/kbcli/pkg/util/helm" + "github.com/apecloud/kbcli/pkg/util/prompt" ) var ( @@ -93,7 +95,10 @@ func (o *destroyOptions) destroy() error { return fmt.Errorf("no playground cluster found") } - return o.destroyLocal() + if o.prevCluster.CloudProvider == cp.Local { + return o.destroyLocal() + } + return o.destroyCloud() } // destroyLocal destroy local k3d cluster that will destroy all resources @@ -115,6 +120,75 @@ func (o *destroyOptions) destroyLocal() error { return o.removeStateFile() } +// destroyCloud destroys cloud kubernetes cluster, before destroying, we should delete +// all clusters created by KubeBlocks, uninstall KubeBlocks and remove the KubeBlocks +// namespace that will destroy all resources created by KubeBlocks, avoid to leave resources behind +func (o *destroyOptions) destroyCloud() error { + var err error + + printer.Warning(o.Out, `This action will destroy the kubernetes cluster, there may be residual resources, + please confirm and manually clean up related resources after this action. + +`) + + fmt.Fprintf(o.Out, "Do you really want to destroy the kubernetes cluster %s?\n%s\n\n The operation cannot be rollbacked. Only 'yes' will be accepted to confirm.\n\n", + o.prevCluster.ClusterName, o.prevCluster.String()) + + // confirm to destroy + if !o.autoApprove { + entered, _ := prompt.NewPrompt("Enter a value:", nil, o.In).Run() + if entered != yesStr { + fmt.Fprintf(o.Out, "\nPlayground destroy cancelled.\n") + return cmdutil.ErrExit + } + } + + o.startTime = time.Now() + + // for cloud provider, we should delete all clusters created by KubeBlocks first, + // uninstall KubeBlocks and remove the KubeBlocks namespace, then destroy the + // playground cluster, avoid to leave resources behind. + // delete all clusters created by KubeBlocks, MUST BE VERY CAUTIOUS, use the right + // kubeconfig and context, otherwise, it will delete the wrong cluster. + if err = o.deleteClustersAndUninstallKB(); err != nil { + if strings.Contains(err.Error(), kubeClusterUnreachableErr.Error()) { + printer.Warning(o.Out, err.Error()) + } else { + return err + } + } + + // destroy playground kubernetes cluster + cpPath, err := cloudProviderRepoDir(o.prevCluster.KbcliVersion) + if err != nil { + return err + } + + provider, err := cp.New(o.prevCluster.CloudProvider, cpPath, o.Out, o.ErrOut) + if err != nil { + return err + } + + fmt.Fprintf(o.Out, "Destroy %s %s cluster %s...\n", + o.prevCluster.CloudProvider, cp.K8sService(o.prevCluster.CloudProvider), o.prevCluster.ClusterName) + if err = provider.DeleteK8sCluster(o.prevCluster); err != nil { + return err + } + + // remove the cluster kubeconfig from the use default kubeconfig + if err = o.removeKubeConfig(); err != nil { + return err + } + + // at last, remove the state file + if err = o.removeStateFile(); err != nil { + return err + } + + fmt.Fprintf(o.Out, "Playground destroy completed in %s.\n", time.Since(o.startTime).Truncate(time.Second)) + return nil +} + func (o *destroyOptions) deleteClustersAndUninstallKB() error { var err error diff --git a/pkg/cmd/playground/init.go b/pkg/cmd/playground/init.go index e53f4d7af..1d103eeb7 100644 --- a/pkg/cmd/playground/init.go +++ b/pkg/cmd/playground/init.go @@ -29,15 +29,15 @@ import ( gv "github.com/hashicorp/go-version" "github.com/pkg/errors" "github.com/spf13/cobra" + "golang.org/x/exp/slices" apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/util/rand" "k8s.io/cli-runtime/pkg/genericiooptions" "k8s.io/klog/v2" cmdutil "k8s.io/kubectl/pkg/cmd/util" "k8s.io/kubectl/pkg/util/templates" cp "github.com/apecloud/kbcli/pkg/cloudprovider" - "github.com/apecloud/kbcli/pkg/cluster" - cmdcluster "github.com/apecloud/kbcli/pkg/cmd/cluster" "github.com/apecloud/kbcli/pkg/cmd/kubeblocks" "github.com/apecloud/kbcli/pkg/printer" "github.com/apecloud/kbcli/pkg/spinner" @@ -59,6 +59,18 @@ on the created kubernetes cluster, and an apecloud-mysql cluster named mycluster # create a k3d cluster on local host and install KubeBlocks kbcli playground init + # create an AWS EKS cluster and install KubeBlocks, the region is required + kbcli playground init --cloud-provider aws --region us-west-1 + + # create an Alibaba cloud ACK cluster and install KubeBlocks, the region is required + kbcli playground init --cloud-provider alicloud --region cn-hangzhou + + # create a Tencent cloud TKE cluster and install KubeBlocks, the region is required + kbcli playground init --cloud-provider tencentcloud --region ap-chengdu + + # create a Google cloud GKE cluster and install KubeBlocks, the region is required + kbcli playground init --cloud-provider gcp --region us-east1 + # after init, run the following commands to experience KubeBlocks quickly # list database cluster and check its status kbcli cluster list @@ -75,6 +87,8 @@ on the created kubernetes cluster, and an apecloud-mysql cluster named mycluster # destroy playground kbcli playground destroy`) + supportedCloudProviders = []string{cp.Local, cp.AWS, cp.GCP, cp.AliCloud, cp.TencentCloud} + spinnerMsg = func(format string, a ...any) spinner.Option { return spinner.WithMessage(fmt.Sprintf("%-50s", fmt.Sprintf(format, a...))) } @@ -83,8 +97,9 @@ on the created kubernetes cluster, and an apecloud-mysql cluster named mycluster type initOptions struct { genericiooptions.IOStreams helmCfg *helm.Config - clusterType string + clusterDef string kbVersion string + cloudProvider string region string autoApprove bool dockerVersion *gv.Version @@ -109,18 +124,28 @@ func newInitCmd(streams genericiooptions.IOStreams) *cobra.Command { }, } - cmd.Flags().StringVar(&o.clusterType, "cluster-type", defaultClusterType, "Specify the cluster type to create, use 'kbcli cluster create --help' to get the available cluster type.") + cmd.Flags().StringVar(&o.clusterDef, "cluster-definition", defaultClusterDef, "Specify the cluster definition, run \"kbcli cd list\" to get the available cluster definitions") cmd.Flags().StringVar(&o.kbVersion, "version", version.DefaultKubeBlocksVersion, "KubeBlocks version") + cmd.Flags().StringVar(&o.cloudProvider, "cloud-provider", defaultCloudProvider, fmt.Sprintf("Cloud provider type, one of %v", supportedCloudProviders)) cmd.Flags().StringVar(&o.region, "region", "", "The region to create kubernetes cluster") cmd.Flags().DurationVar(&o.Timeout, "timeout", 600*time.Second, "Time to wait for init playground, such as --timeout=10m") cmd.Flags().BoolVar(&o.autoApprove, "auto-approve", false, "Skip interactive approval during the initialization of playground") + util.CheckErr(cmd.RegisterFlagCompletionFunc( + "cloud-provider", + func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + return cp.CloudProviders(), cobra.ShellCompDirectiveNoFileComp + })) return cmd } func (o *initOptions) complete(cmd *cobra.Command) error { var err error + if o.cloudProvider != cp.Local { + return nil + } + if o.dockerVersion, err = util.GetDockerVersion(); err != nil { return err } @@ -133,11 +158,19 @@ func (o *initOptions) complete(cmd *cobra.Command) error { } func (o *initOptions) validate() error { - if o.clusterType == "" { - return fmt.Errorf("a valid cluster type is needed, use --cluster-type to specify one") + if !slices.Contains(supportedCloudProviders, o.cloudProvider) { + return fmt.Errorf("cloud provider %s is not supported, only support %v", o.cloudProvider, supportedCloudProviders) + } + + if o.cloudProvider != cp.Local && o.region == "" { + return fmt.Errorf("region should be specified when cloud provider %s is specified", o.cloudProvider) } - if o.dockerVersion.LessThan(version.MinimumDockerVersion) { + if o.clusterDef == "" { + return fmt.Errorf("a valid cluster definition is needed, use --cluster-definition to specify one") + } + + if o.cloudProvider == cp.Local && o.dockerVersion.LessThan(version.MinimumDockerVersion) { return fmt.Errorf("your docker version %s is lower than the minimum version %s, please upgrade your docker", o.dockerVersion, version.MinimumDockerVersion) } @@ -148,12 +181,15 @@ func (o *initOptions) validate() error { } func (o *initOptions) run() error { - return o.local() + if o.cloudProvider == cp.Local { + return o.local() + } + return o.cloud() } // local bootstraps a playground in the local host func (o *initOptions) local() error { - provider, err := cp.New(cp.Local, "", o.Out, o.ErrOut) + provider, err := cp.New(o.cloudProvider, "", o.Out, o.ErrOut) if err != nil { return err } @@ -195,6 +231,81 @@ func (o *initOptions) local() error { return o.installKBAndCluster(clusterInfo) } +// bootstraps a playground in the remote cloud +func (o *initOptions) cloud() error { + cpPath, err := cloudProviderRepoDir("") + if err != nil { + return err + } + + var clusterInfo *cp.K8sClusterInfo + + // if kubernetes cluster exists, confirm to continue or not, if not, user should + // destroy the old cluster first + if o.prevCluster != nil { + clusterInfo = o.prevCluster + if err = o.confirmToContinue(); err != nil { + return err + } + } else { + clusterName := fmt.Sprintf("%s-%s", cloudClusterNamePrefix, rand.String(5)) + clusterInfo = &cp.K8sClusterInfo{ + ClusterName: clusterName, + CloudProvider: o.cloudProvider, + Region: o.region, + } + if err = o.confirmInitNewKubeCluster(); err != nil { + return err + } + + fmt.Fprintf(o.Out, "\nWrite cluster info to state file %s\n", o.stateFilePath) + if err := writeClusterInfo(o.stateFilePath, clusterInfo); err != nil { + return errors.Wrapf(err, "failed to write kubernetes cluster info to state file %s:\n %v", o.stateFilePath, clusterInfo) + } + + fmt.Fprintf(o.Out, "Creating %s %s cluster %s ... \n", o.cloudProvider, cp.K8sService(o.cloudProvider), clusterName) + } + + o.startTime = time.Now() + printer.PrintBlankLine(o.Out) + + // clone apecloud/cloud-provider repo to local path + fmt.Fprintf(o.Out, "Clone ApeCloud cloud-provider repo to %s...\n", cpPath) + branchName := "kb-playground" + if err = util.CloneGitRepo(cp.GitRepoURL, branchName, cpPath); err != nil { + return err + } + + provider, err := cp.New(o.cloudProvider, cpPath, o.Out, o.ErrOut) + if err != nil { + return err + } + + // create a kubernetes cluster in the cloud + if err = provider.CreateK8sCluster(clusterInfo); err != nil { + klog.V(1).Infof("create K8S cluster failed: %s", err.Error()) + return err + } + klog.V(1).Info("create K8S cluster success") + + printer.PrintBlankLine(o.Out) + + // write cluster info to state file and get new cluster info with kubeconfig + clusterInfo, err = o.writeStateFile(provider) + if err != nil { + return err + } + + // write cluster kubeconfig to default kubeconfig file and switch current context to it + if err = o.setKubeConfig(clusterInfo); err != nil { + return err + } + + // install KubeBlocks and create a database cluster + klog.V(1).Info("start to install KubeBlocks in K8S cluster... ") + return o.installKBAndCluster(clusterInfo) +} + // confirmToContinue confirms to continue init process if there is an existed kubernetes cluster func (o *initOptions) confirmToContinue() error { clusterName := o.prevCluster.ClusterName @@ -207,7 +318,7 @@ func (o *initOptions) confirmToContinue() error { } } fmt.Fprintf(o.Out, "Continue to initialize %s %s cluster %s... \n", - cp.Local, cp.K8sService(cp.Local), clusterName) + o.cloudProvider, cp.K8sService(o.cloudProvider), clusterName) return nil } @@ -299,7 +410,7 @@ func (o *initOptions) installKBAndCluster(info *cp.K8sClusterInfo) error { } klog.V(1).Info("KubeBlocks installed successfully") // install database cluster - clusterInfo := "ClusterType: " + o.clusterType + clusterInfo := "ClusterDefinition: " + o.clusterDef s := spinner.New(o.Out, spinnerMsg("Create cluster %s (%s)", kbClusterName, clusterInfo)) defer s.Fail() if err = o.createCluster(); err != nil && !apierrors.IsAlreadyExists(err) { @@ -352,11 +463,19 @@ func (o *initOptions) installKubeBlocks(k8sClusterName string) error { "agamotto.enabled=true", ) - insOpts.ValueOpts.Values = append(insOpts.ValueOpts.Values, - // use hostpath csi driver to support snapshot - "snapshot-controller.enabled=true", - "csi-hostpath-driver.enabled=true", - ) + if o.cloudProvider == cp.Local { + insOpts.ValueOpts.Values = append(insOpts.ValueOpts.Values, + // use hostpath csi driver to support snapshot + "snapshot-controller.enabled=true", + "csi-hostpath-driver.enabled=true", + ) + } else if o.cloudProvider == cp.AWS { + insOpts.ValueOpts.Values = append(insOpts.ValueOpts.Values, + // enable aws-load-balancer-controller addon automatically on playground + "aws-load-balancer-controller.enabled=true", + fmt.Sprintf("aws-load-balancer-controller.clusterName=%s", k8sClusterName), + ) + } if err = insOpts.PreCheck(); err != nil { // if the KubeBlocks has been installed, we ignore the error @@ -375,24 +494,38 @@ func (o *initOptions) installKubeBlocks(k8sClusterName string) error { // createCluster constructs a cluster create options and run func (o *initOptions) createCluster() error { - c, err := cmdcluster.NewSubCmdsOptions(&cmdcluster.NewCreateOptions(util.NewFactory(), genericiooptions.NewTestIOStreamsDiscard()).CreateOptions, cluster.ClusterType(o.clusterType)) - if err != nil { - return err + // TODO: Update with new creation cmd + /*c := cmdcluster.NewCreateOptions(util.NewFactory(), genericiooptions.NewTestIOStreamsDiscard()) + c.ClusterDefRef = o.clusterDef + // c.ClusterVersionRef = o.clusterVersion + c.Namespace = defaultNamespace + c.Name = kbClusterName + c.UpdatableFlags = cmdcluster.UpdatableFlags{ + TerminationPolicy: "WipeOut", + DisableExporter: false, + PodAntiAffinity: "Preferred", + Tenancy: "SharedNode", + } + + // if we are running on local, create cluster with one replica + if o.cloudProvider == cp.Local { + c.Values = append(c.Values, "replicas=1") + } else { + // if we are running on cloud, create cluster with three replicas + c.Values = append(c.Values, "replicas=3") } - c.Args = []string{kbClusterName} - err = c.CreateOptions.Complete() - if err != nil { + + if err := c.CreateOptions.Complete(); err != nil { return err } - err = c.Complete(nil) - if err != nil { + if err := c.Validate(); err != nil { return err } - err = c.Validate() - if err != nil { + if err := c.Complete(); err != nil { return err } - return c.Run() + return c.Run()*/ + return nil } // checkExistedCluster checks playground kubernetes cluster exists or not, a kbcli client only @@ -407,7 +540,19 @@ func (o *initOptions) checkExistedCluster() error { 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()) // if cloud provider is not same with the existed cluster cloud provider, suggest // user to destroy the previous cluster first - if o.prevCluster.CloudProvider != cp.Local { + if o.prevCluster.CloudProvider != o.cloudProvider { + printer.Warning(o.Out, warningMsg) + return cmdutil.ErrExit + } + + if o.prevCluster.CloudProvider == cp.Local { + return nil + } + + // previous kubernetes cluster is a cloud provider cluster, check if the region + // is same with the new cluster region, if not, suggest user to destroy the previous + // cluster first + if o.prevCluster.Region != o.region { printer.Warning(o.Out, warningMsg) return cmdutil.ErrExit } diff --git a/pkg/cmd/playground/init_test.go b/pkg/cmd/playground/init_test.go index 3f43a7997..a273c6d58 100644 --- a/pkg/cmd/playground/init_test.go +++ b/pkg/cmd/playground/init_test.go @@ -28,6 +28,7 @@ import ( gv "github.com/hashicorp/go-version" "k8s.io/cli-runtime/pkg/genericiooptions" + cp "github.com/apecloud/kbcli/pkg/cloudprovider" clitesting "github.com/apecloud/kbcli/pkg/testing" "github.com/apecloud/kbcli/pkg/types" "github.com/apecloud/kbcli/pkg/util/helm" @@ -51,26 +52,38 @@ var _ = Describe("playground", func() { Expect(cmd != nil).Should(BeTrue()) o := &initOptions{ - clusterType: clitesting.ClusterType, + clusterDef: clitesting.ClusterDefName, IOStreams: streams, + cloudProvider: defaultCloudProvider, helmCfg: helm.NewConfig("", testKubeConfigPath, "", false), dockerVersion: version.MinimumDockerVersion, } Expect(o.validate()).Should(Succeed()) Expect(o.run()).Should(HaveOccurred()) Expect(o.installKubeBlocks("test")).Should(HaveOccurred()) - Expect(o.createCluster()).Should(HaveOccurred()) + // TODO: re-add it when updating cluster creation function + // Expect(o.createCluster()).Should(HaveOccurred()) }) It("init at local host without outdate docker", func() { var err error o := &initOptions{ - clusterType: clitesting.ClusterType, - IOStreams: streams, - helmCfg: helm.NewConfig("", testKubeConfigPath, "", false), + clusterDef: clitesting.ClusterDefName, + IOStreams: streams, + cloudProvider: defaultCloudProvider, + helmCfg: helm.NewConfig("", testKubeConfigPath, "", false), } o.dockerVersion, err = gv.NewVersion("20.10.0") Expect(err).Should(BeNil()) Expect(o.validate()).Should(HaveOccurred()) }) + + It("init at remote cloud", func() { + o := &initOptions{ + IOStreams: streams, + clusterDef: clitesting.ClusterDefName, + cloudProvider: cp.AWS, + } + Expect(o.validate()).Should(HaveOccurred()) + }) }) diff --git a/pkg/cmd/playground/types.go b/pkg/cmd/playground/types.go index 13ede14ff..969edb055 100644 --- a/pkg/cmd/playground/types.go +++ b/pkg/cmd/playground/types.go @@ -32,7 +32,7 @@ const ( const ( defaultCloudProvider = cloudprovider.Local - defaultClusterType = "apecloud-mysql" + defaultClusterDef = "apecloud-mysql" // defaultNamespace is the namespace of playground cluster defaultNamespace = "default" diff --git a/pkg/testing/fake.go b/pkg/testing/fake.go index 804c0e677..42d45be5c 100644 --- a/pkg/testing/fake.go +++ b/pkg/testing/fake.go @@ -54,7 +54,6 @@ const ( ClusterName = "fake-cluster-name" Namespace = "fake-namespace" ClusterDefName = "fake-cluster-definition" - ClusterType = "fake-cluster-Type" CompDefName = "fake-component-definition" ComponentName = "fake-component-name" NodeName = "fake-node-name"