Skip to content

Commit

Permalink
adapt playground init
Browse files Browse the repository at this point in the history
  • Loading branch information
yipeng1030 committed Oct 21, 2024
1 parent ce3daf9 commit ca5e892
Show file tree
Hide file tree
Showing 8 changed files with 186 additions and 291 deletions.
146 changes: 131 additions & 15 deletions pkg/cmd/cluster/cluster_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,15 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
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"
Expand All @@ -40,33 +46,143 @@ import (
)

var _ = Describe("Cluster", func() {

const (
clusterType = "apecloud-mysql"
clusterName = "test"
namespace = "default"
)
var streams genericiooptions.IOStreams
var tf *cmdtesting.TestFactory
fakeConfigData := map[string]string{
"config.yaml": `# the default storage class name.
DEFAULT_STORAGE_CLASS: ""`,
}
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
}
)

BeforeEach(func() {
_ = kbappsv1.AddToScheme(scheme.Scheme)
_ = metav1.AddMetaToScheme(scheme.Scheme)
streams, _, _, _ = genericiooptions.NewTestIOStreams()
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{}
tf = mockClient(testing.FakeCompDef())
createOptions = &action.CreateOptions{
IOStreams: streams,
Factory: tf,
}
})

AfterEach(func() {
tf.Cleanup()
})

// TODO: add create cluster case
Context("delete cluster", func() {
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())
})

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())
})

})

Expand Down
20 changes: 15 additions & 5 deletions pkg/cmd/cluster/create_subcmds.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import (
"context"
"fmt"
"os"
"regexp"

"github.com/spf13/cobra"
"k8s.io/apimachinery/pkg/api/errors"
Expand Down Expand Up @@ -97,8 +98,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())
},
}
Expand Down Expand Up @@ -150,7 +151,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
Expand All @@ -162,7 +163,9 @@ func (o *CreateSubCmdsOptions) complete(cmd *cobra.Command) error {
}

// get values from flags
o.Values = getValuesFromFlags(cmd.LocalNonPersistentFlags())
if cmd != nil {
o.Values = getValuesFromFlags(cmd.LocalNonPersistentFlags())
}

// get all the rendered objects
objs, err := o.getObjectsInfo()
Expand Down Expand Up @@ -211,7 +214,14 @@ func (o *CreateSubCmdsOptions) complete(cmd *cobra.Command) error {
return nil
}

func (o *CreateSubCmdsOptions) validate() error {
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")
}
return cluster.ValidateValues(o.ChartInfo, o.Values)
}

Expand Down
8 changes: 4 additions & 4 deletions pkg/cmd/cluster/create_subcmds_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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"
Expand Down
76 changes: 1 addition & 75 deletions pkg/cmd/playground/destroy.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,6 @@ 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"
Expand All @@ -48,7 +47,6 @@ 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 (
Expand Down Expand Up @@ -95,10 +93,7 @@ func (o *destroyOptions) destroy() error {
return fmt.Errorf("no playground cluster found")
}

if o.prevCluster.CloudProvider == cp.Local {
return o.destroyLocal()
}
return o.destroyCloud()
return o.destroyLocal()
}

// destroyLocal destroy local k3d cluster that will destroy all resources
Expand All @@ -120,75 +115,6 @@ 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

Expand Down
Loading

0 comments on commit ca5e892

Please sign in to comment.