diff --git a/go.mod b/go.mod index 6047c60..f6e56b6 100644 --- a/go.mod +++ b/go.mod @@ -11,6 +11,7 @@ require ( github.com/imdario/mergo v0.3.9 // indirect github.com/linuxsuren/cobra-extension v0.0.10 github.com/linuxsuren/go-cli-alias v0.0.4 + github.com/magiconair/properties v1.8.1 github.com/mitchellh/copystructure v1.1.1 // indirect github.com/spf13/cobra v1.1.1 github.com/spf13/pflag v1.0.5 diff --git a/kubectl-plugin/common/completion.go b/kubectl-plugin/common/completion.go new file mode 100644 index 0000000..86178ce --- /dev/null +++ b/kubectl-plugin/common/completion.go @@ -0,0 +1,15 @@ +package common + +import "github.com/spf13/cobra" + +// NoFileCompletion avoid completion with files +func NoFileCompletion(_ *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) { + return nil, cobra.ShellCompDirectiveNoFileComp +} + +// ArrayCompletion return a completion which base on an array +func ArrayCompletion(array ...string) func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + return array, cobra.ShellCompDirectiveNoFileComp + } +} diff --git a/kubectl-plugin/component/component.go b/kubectl-plugin/component/component.go index c581981..2dc49c9 100644 --- a/kubectl-plugin/component/component.go +++ b/kubectl-plugin/component/component.go @@ -11,7 +11,6 @@ import ( "k8s.io/apimachinery/pkg/types" "k8s.io/client-go/dynamic" "k8s.io/client-go/kubernetes" - "strconv" ) // NewComponentCmd returns a command to manage components of KubeSphere @@ -26,7 +25,8 @@ func NewComponentCmd(client dynamic.Interface, clientset *kubernetes.Clientset) NewComponentEditCmd(client), NewComponentResetCmd(client), NewComponentWatchCmd(client), - NewComponentLogCmd(client, clientset)) + NewComponentLogCmd(client, clientset), + newComponentsExecCmd(client)) return } @@ -67,88 +67,6 @@ type WatchOption struct { PrivateLocal string } -// EnableOption is the option for component enable command -type EnableOption struct { - Option - - Edit bool - Toggle bool -} - -// NewComponentEnableCmd returns a command to enable (or disable) a component by name -func NewComponentEnableCmd(client dynamic.Interface) (cmd *cobra.Command) { - opt := &EnableOption{ - Option: Option{ - Client: client, - }, - } - cmd = &cobra.Command{ - Use: "enable", - Short: "Enable or disable the specific KubeSphere component", - PreRunE: opt.enablePreRunE, - RunE: opt.enableRunE, - } - - flags := cmd.Flags() - flags.BoolVarP(&opt.Edit, "edit", "e", false, - "Indicate if you want to edit it instead of enable/disable a specified one. This flag will make others not work.") - flags.BoolVarP(&opt.Toggle, "toggle", "t", false, - "Indicate if you want to disable a component") - flags.StringVarP(&opt.Name, "name", "n", "", - "The name of target component which you want to enable/disable. Please provide option --sonarqube if you want to enable SonarQube.") - flags.StringVarP(&opt.SonarQube, "sonarqube", "", "", - "The SonarQube URL") - flags.StringVarP(&opt.SonarQube, "sonar", "", "", - "The SonarQube URL") - flags.StringVarP(&opt.SonarQubeToken, "sonarqube-token", "", "", - "The token of SonarQube") - - // these are aliased options - _ = flags.MarkHidden("sonar") - return -} - -func (o *EnableOption) enablePreRunE(cmd *cobra.Command, args []string) (err error) { - if o.Edit { - return - } - - return o.componentNameCheck(cmd, args) -} - -func (o *EnableOption) enableRunE(cmd *cobra.Command, args []string) (err error) { - if o.Edit { - err = common.UpdateWithEditor(kstypes.GetClusterConfiguration(), "kubesphere-system", "ks-installer", o.Client) - } else { - enabled := strconv.FormatBool(!o.Toggle) - ns, name := "kubesphere-system", "ks-installer" - var patchTarget string - switch o.Name { - case "devops", "alerting", "auditing", "events", "logging", "metrics_server", "networkpolicy", "notification", "openpitrix", "servicemesh": - patchTarget = o.Name - case "sonarqube", "sonar": - if o.SonarQube == "" || o.SonarQubeToken == "" { - err = fmt.Errorf("SonarQube or token is empty, please provide --sonarqube") - } else { - name = "ks-console-config" - err = integrateSonarQube(o.Client, ns, name, o.SonarQube, o.SonarQubeToken) - } - return - default: - err = fmt.Errorf("not support [%s] yet", o.Name) - return - } - - patch := fmt.Sprintf(`[{"op": "replace", "path": "/spec/%s/enabled", "value": %s}]`, patchTarget, enabled) - ctx := context.TODO() - _, err = o.Client.Resource(kstypes.GetClusterConfiguration()).Namespace(ns).Patch(ctx, - name, types.JSONPatchType, - []byte(patch), - metav1.PatchOptions{}) - } - return -} - func (o *Option) getNsAndName(component string) (ns, name string) { ns = "kubesphere-system" switch o.Name { diff --git a/kubectl-plugin/component/enable.go b/kubectl-plugin/component/enable.go new file mode 100644 index 0000000..efc2f83 --- /dev/null +++ b/kubectl-plugin/component/enable.go @@ -0,0 +1,105 @@ +package component + +import ( + "context" + "fmt" + "github.com/linuxsuren/ks/kubectl-plugin/common" + kstypes "github.com/linuxsuren/ks/kubectl-plugin/types" + "github.com/spf13/cobra" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/dynamic" + "strconv" +) + +// EnableOption is the option for component enable command +type EnableOption struct { + Option + + Edit bool + Toggle bool +} + +func getAvailableComponents() func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + return common.ArrayCompletion("devops", "alerting", "auditing", "events", "logging", "metrics_server", "networkpolicy", "notification", "openpitrix", "servicemesh") +} + +// NewComponentEnableCmd returns a command to enable (or disable) a component by name +func NewComponentEnableCmd(client dynamic.Interface) (cmd *cobra.Command) { + opt := &EnableOption{ + Option: Option{ + Client: client, + }, + } + + availableComs := getAvailableComponents() + + cmd = &cobra.Command{ + Use: "enable", + Short: "Enable or disable the specific KubeSphere component", + PreRunE: opt.enablePreRunE, + ValidArgsFunction: availableComs, + RunE: opt.enableRunE, + } + + flags := cmd.Flags() + flags.BoolVarP(&opt.Edit, "edit", "e", false, + "Indicate if you want to edit it instead of enable/disable a specified one. This flag will make others not work.") + flags.BoolVarP(&opt.Toggle, "toggle", "t", false, + "Indicate if you want to disable a component") + flags.StringVarP(&opt.Name, "name", "n", "", + "The name of target component which you want to enable/disable. Please provide option --sonarqube if you want to enable SonarQube.") + flags.StringVarP(&opt.SonarQube, "sonarqube", "", "", + "The SonarQube URL") + flags.StringVarP(&opt.SonarQube, "sonar", "", "", + "The SonarQube URL") + flags.StringVarP(&opt.SonarQubeToken, "sonarqube-token", "", "", + "The token of SonarQube") + + cmd.RegisterFlagCompletionFunc("name", availableComs) + + // these are aliased options + _ = flags.MarkHidden("sonar") + return +} + +func (o *EnableOption) enablePreRunE(cmd *cobra.Command, args []string) (err error) { + if o.Edit { + return + } + + return o.componentNameCheck(cmd, args) +} + +func (o *EnableOption) enableRunE(cmd *cobra.Command, args []string) (err error) { + if o.Edit { + err = common.UpdateWithEditor(kstypes.GetClusterConfiguration(), "kubesphere-system", "ks-installer", o.Client) + } else { + enabled := strconv.FormatBool(!o.Toggle) + ns, name := "kubesphere-system", "ks-installer" + var patchTarget string + switch o.Name { + case "devops", "alerting", "auditing", "events", "logging", "metrics_server", "networkpolicy", "notification", "openpitrix", "servicemesh": + patchTarget = o.Name + case "sonarqube", "sonar": + if o.SonarQube == "" || o.SonarQubeToken == "" { + err = fmt.Errorf("SonarQube or token is empty, please provide --sonarqube") + } else { + name = "ks-console-config" + err = integrateSonarQube(o.Client, ns, name, o.SonarQube, o.SonarQubeToken) + } + return + default: + err = fmt.Errorf("not support [%s] yet", o.Name) + return + } + + patch := fmt.Sprintf(`[{"op": "replace", "path": "/spec/%s/enabled", "value": %s}]`, patchTarget, enabled) + ctx := context.TODO() + _, err = o.Client.Resource(kstypes.GetClusterConfiguration()).Namespace(ns).Patch(ctx, + name, types.JSONPatchType, + []byte(patch), + metav1.PatchOptions{}) + } + return +} diff --git a/kubectl-plugin/component/exec.go b/kubectl-plugin/component/exec.go new file mode 100644 index 0000000..71a6f56 --- /dev/null +++ b/kubectl-plugin/component/exec.go @@ -0,0 +1,81 @@ +package component + +import ( + "context" + "fmt" + "github.com/linuxsuren/ks/kubectl-plugin/common" + kstypes "github.com/linuxsuren/ks/kubectl-plugin/types" + "github.com/spf13/cobra" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/client-go/dynamic" + "os" + "os/exec" + "strings" + "syscall" +) + +func newComponentsExecCmd(client dynamic.Interface) (cmd *cobra.Command) { + availableComs := common.ArrayCompletion("jenkins", "apiserver") + + cmd = &cobra.Command{ + Use: "exec", + Short: "Execute a command in a container.", + Long: `Execute a command in a container. +This command is similar with kubectl exec, the only difference is that you don't need to type the fullname'`, + ValidArgsFunction: availableComs, + Args: cobra.MinimumNArgs(1), + RunE: func(cmd *cobra.Command, args []string) (err error) { + var kubectl string + if kubectl, err = exec.LookPath("kubectl"); err != nil { + return + } + + switch args[0] { + case "jenkins": + var jenkinsPodName string + var list *unstructured.UnstructuredList + if list, err = client.Resource(kstypes.GetPodSchema()).Namespace("kubesphere-devops-system").List( + context.TODO(), metav1.ListOptions{}); err == nil { + for _, item := range list.Items { + if strings.HasPrefix(item.GetName(), "ks-jenkins") { + jenkinsPodName = item.GetName() + } + } + } else { + fmt.Println(err) + return + } + + if jenkinsPodName == "" { + err = fmt.Errorf("cannot found ks-jenkins pod") + } else { + err = syscall.Exec(kubectl, []string{"kubectl", "-n", "kubesphere-devops-system", "exec", "-it", jenkinsPodName, "bash"}, os.Environ()) + } + case "apiserver": + var apiserverPodName string + var list *unstructured.UnstructuredList + if list, err = client.Resource(kstypes.GetPodSchema()).Namespace("kubesphere-system").List( + context.TODO(), metav1.ListOptions{}); err == nil { + for _, item := range list.Items { + if strings.HasPrefix(item.GetName(), "ks-apiserver") { + apiserverPodName = item.GetName() + } + } + } else { + fmt.Println(err) + return + } + + if apiserverPodName == "" { + err = fmt.Errorf("cannot found ks-jenkins pod") + } else { + err = syscall.Exec(kubectl, []string{"kubectl", "-n", "kubesphere-system", "exec", "-it", apiserverPodName, "sh"}, os.Environ()) + } + } + return + }, + } + + return +} diff --git a/kubectl-plugin/component/watch.go b/kubectl-plugin/component/watch.go index 5ba7f6c..88bd1cf 100644 --- a/kubectl-plugin/component/watch.go +++ b/kubectl-plugin/component/watch.go @@ -3,6 +3,7 @@ package component import ( "context" "fmt" + "github.com/linuxsuren/ks/kubectl-plugin/common" kstypes "github.com/linuxsuren/ks/kubectl-plugin/types" "github.com/spf13/cobra" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -54,6 +55,9 @@ take value from environment 'KS_REPO' if you don't set it`) flags.StringVarP(&opt.PrivateLocal, "private-local", "", "127.0.0.1", `The local address of registry take value from environment 'KS_PRIVATE_LOCAL' if you don't set it`) + + cmd.RegisterFlagCompletionFunc("watch-deploy", common.ArrayCompletion("apiserver", "controller", "console")) + cmd.RegisterFlagCompletionFunc("registry", common.ArrayCompletion("docker", " aliyun", "qingcloud", "private")) return } diff --git a/kubectl-plugin/install/kind.go b/kubectl-plugin/install/kind.go index 4bc7d99..2eca7e6 100644 --- a/kubectl-plugin/install/kind.go +++ b/kubectl-plugin/install/kind.go @@ -2,6 +2,7 @@ package install import ( "fmt" + "github.com/linuxsuren/ks/kubectl-plugin/common" "github.com/spf13/cobra" "html/template" "io" @@ -14,9 +15,10 @@ import ( func newInstallWithKindCmd() (cmd *cobra.Command) { opt := &kindOption{} cmd = &cobra.Command{ - Use: "kind", - Short: "install KubeSphere with kind", - RunE: opt.runE, + Use: "kind", + Short: "install KubeSphere with kind", + Example: "ks install kind --components devops", + RunE: opt.runE, } flags := cmd.Flags() @@ -33,20 +35,35 @@ func newInstallWithKindCmd() (cmd *cobra.Command) { flags.BoolVarP(&opt.Reset, "reset", "", false, "") flags.StringVarP(&opt.Nightly, "nightly", "", "", "Supported date format is '20200101', or you can use 'latest' which means yesterday") + + cmd.RegisterFlagCompletionFunc("components", common.ArrayCompletion("devops")) return } -func (o *kindOption) reset(cmd *cobra.Command, args []string) (err error) { - var tag string - // try to parse the nightly date - if o.Nightly == "latest" { - tag = fmt.Sprintf("nightly-%s", time.Now().AddDate(0, 0, -1).Format("20060102")) - } else if o.Nightly != "" { - layout := "2006-01-02" +func getNightlyTag(date string) (dateStr, tag string) { + if date == "latest" { + dateStr = time.Now().AddDate(0, 0, -1).Format("20060102") + tag = fmt.Sprintf("nightly-%s", dateStr) + } else if date != "" { var targetDate time.Time - if targetDate, err = time.Parse(layout, o.Nightly); err == nil { - tag = fmt.Sprintf("nightly-%s", targetDate.Format("20060102")) + var err error + // try to parse the date from different layouts + if targetDate, err = time.Parse("2006-01-02", date); err != nil { + targetDate, err = time.Parse("20060102", date) } + + if err == nil { + dateStr = targetDate.Format("20060102") + tag = fmt.Sprintf("nightly-%s", dateStr) + } + } + return +} + +func (o *kindOption) reset(cmd *cobra.Command, args []string) (err error) { + var tag string + if o.Nightly, tag = getNightlyTag(o.Nightly); tag == "" { + return } var wg sync.WaitGroup @@ -63,7 +80,7 @@ func (o *kindOption) reset(cmd *cobra.Command, args []string) (err error) { return } wg.Wait() - if err = execCommand("kubectl", "ks", "com", "reset", "--nightly", "latest", "-a"); err != nil { + if err = execCommand("kubectl", "ks", "com", "reset", "--nightly", o.Nightly, "-a"); err != nil { return } return diff --git a/kubectl-plugin/install/kind_test.go b/kubectl-plugin/install/kind_test.go new file mode 100644 index 0000000..ad5d78f --- /dev/null +++ b/kubectl-plugin/install/kind_test.go @@ -0,0 +1,50 @@ +package install + +import ( + "fmt" + "github.com/magiconair/properties/assert" + "testing" + "time" +) + +func TestGetNightlyTag(t *testing.T) { + yesterday := time.Now().AddDate(0, 0, -1).Format("20060102") + + table := []struct { + date string + expectDate string + expectTag string + message string + }{{ + date: "", + expectDate: "", + expectTag: "", + message: "should return an empty string if the date is empty", + }, { + date: "invalid", + expectDate: "", + expectTag: "", + message: "should return an empty string if the date is invalid", + }, { + date: "20060102", + expectDate: "20060102", + expectTag: "nightly-20060102", + message: "should return 20060102", + }, { + date: "2020-01-01", + expectDate: "20200101", + expectTag: "nightly-20200101", + message: "should return 20200101", + }, { + date: "latest", + expectDate: yesterday, + expectTag: fmt.Sprintf("nightly-%s", yesterday), + message: fmt.Sprintf("should return %s", yesterday), + }} + + for _, item := range table { + date, tag := getNightlyTag(item.date) + assert.Equal(t, date, item.expectDate, item.message) + assert.Equal(t, tag, item.expectTag, item.message) + } +} diff --git a/kubectl-plugin/types/schema.go b/kubectl-plugin/types/schema.go index 47fa0d7..9fd06f1 100644 --- a/kubectl-plugin/types/schema.go +++ b/kubectl-plugin/types/schema.go @@ -46,6 +46,15 @@ func GetNamespaceSchema() schema.GroupVersionResource { } } +// GetPodSchema returns the schema of deploy +func GetPodSchema() schema.GroupVersionResource { + return schema.GroupVersionResource{ + Group: "", + Version: "v1", + Resource: "pods", + } +} + // GetDeploySchema returns the schema of deploy func GetDeploySchema() schema.GroupVersionResource { return schema.GroupVersionResource{