diff --git a/Makefile b/Makefile index 7f2d948..1613b78 100644 --- a/Makefile +++ b/Makefile @@ -8,9 +8,12 @@ build: pre-build CGO_ENABLE=0 go build $(BUILDFLAGS) -o bin/ks build-plugin: pre-build - CGO_ENABLE=0 go build ${BUILDFLAGS} -o bin/kubectl-ks kubectl-plugin/* + CGO_ENABLE=0 go build ${BUILDFLAGS} -o bin/kubectl-ks kubectl-plugin/*.go -pre-build: fmt lint +pre-build: fmt lint mod-tidy + +mod-tidy: + go mod tidy fmt: go fmt ./... diff --git a/README.md b/README.md index 76150e7..8d620fb 100644 --- a/README.md +++ b/README.md @@ -15,18 +15,52 @@ It's also [a plugin of kubectl](https://github.com/kubernetes-sigs/krew). All features below work with [Kubesphere](https://github.com/kubsphere/kubesphere) instead of other concept. * Pipeline management + * Edit a Pipeline without give the fullname (namespace/name) * User Management * Component Management - * Enable (disable) a component (TODO) - * Update a component manually or automatically (Working on) + * Enable (disable) a component + * Update a component manually or automatically + * Output the logs of a KubeSphere component + * Edit a KubeSphere component ## Pipeline -You can delete the pipelines from Kubesphere interactively: ``` -➜ ~ kubectl ks pipeline delete -? Please select the namespace whose you want to check: rick5rqdt -? Please select the namespace whose you want to check: [Use arrows to move, space to select, to all, to none, type to filter] -> [ ] 123 - [ ] abc +➜ ~ kubectl ks pip +Usage: + ks pipeline [flags] + ks pipeline [command] + +Aliases: + pipeline, pip + +Available Commands: + delete Delete a specific Pipeline of KubeSphere DevOps + edit Edit the target pipeline + view Output the YAML format of a Pipeline + +Flags: + -h, --help help for pipeline + +Use "ks pipeline [command] --help" for more information about a command. +``` + +## Component + ``` +➜ ~ kubectl ks com +Manage the components of KubeSphere + +Usage: + ks component [command] + +Aliases: + component, com + +Available Commands: + edit edit the target component + enable Enable or disable the specific KubeSphere component + log output the log of KubeSphere component + reset reset the component by name + watch Update images of ks-apiserver, ks-controller-manager, ks-console +``` \ No newline at end of file diff --git a/go.mod b/go.mod index 5088f9c..19f8f5c 100644 --- a/go.mod +++ b/go.mod @@ -4,13 +4,12 @@ go 1.15 require ( github.com/AlecAivazis/survey/v2 v2.2.2 - github.com/ghodss/yaml v1.0.0 github.com/imdario/mergo v0.3.9 // indirect github.com/linuxsuren/cobra-extension v0.0.5 github.com/linuxsuren/go-cli-alias v0.0.4 github.com/spf13/cobra v1.1.1 github.com/spf13/pflag v1.0.5 - gopkg.in/yaml.v2 v2.4.0 + k8s.io/api v0.19.4 k8s.io/apimachinery v0.19.4 k8s.io/cli-runtime v0.19.4 k8s.io/client-go v0.19.4 diff --git a/go.sum b/go.sum index bd88855..080b98d 100644 --- a/go.sum +++ b/go.sum @@ -28,6 +28,7 @@ github.com/Azure/go-autorest/tracing v0.5.0/go.mod h1:r/s2XiOKccPW3HrqB+W0TQzfbt github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ= +github.com/Netflix/go-expect v0.0.0-20180615182759-c93bf25de8e8 h1:xzYJEypr/85nBpB11F9br+3HUrpgb+fcm5iADzXXYEw= github.com/Netflix/go-expect v0.0.0-20180615182759-c93bf25de8e8/go.mod h1:oX5x61PbNXchhh0oikYAH+4Pcfw5LKv21+Jnpr6r6Pc= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= github.com/Pallinder/go-randomdata v1.2.0/go.mod h1:yHmJgulpD2Nfrm0cR9tI/+oAgRqCQQixsA8HyRZfV9Y= @@ -66,6 +67,7 @@ github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3Ee github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/creack/pty v1.1.7 h1:6pwm8kMQKCmgUg0ZHTm5+/YvRK0s3THD/28+T6/kk4A= github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= github.com/danieljoos/wincred v1.0.2/go.mod h1:SnuYRW9lp1oJrZX/dXJqr0cPK5gYXqx3EJbmjhLdK9U= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -201,6 +203,7 @@ github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ= github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I= github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc= +github.com/hinshun/vt10x v0.0.0-20180616224451-1954e6464174 h1:WlZsjVhE8Af9IcZDGgJGQpNflI3+MJSBhsgT5PCtzBQ= github.com/hinshun/vt10x v0.0.0-20180616224451-1954e6464174/go.mod h1:DqJ97dSdRW1W22yXSB90986pcOyQ7r45iio1KN2ez1A= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= @@ -238,6 +241,7 @@ github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfn github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/pty v1.1.4/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/pty v1.1.5/go.mod h1:9r2w37qlBe7rQ6e1fg1S/9xpWHSnaqNdHD3WcMdbPDA= +github.com/kr/pty v1.1.8 h1:AkaSdXYQOWeaO3neb8EM634ahkXXe3jYbVh/F9lq+GI= github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= diff --git a/kubectl-plugin/cmd.go b/kubectl-plugin/cmd.go index 8e82d1c..9bca28c 100644 --- a/kubectl-plugin/cmd.go +++ b/kubectl-plugin/cmd.go @@ -5,11 +5,14 @@ import ( "fmt" pkg "github.com/linuxsuren/cobra-extension" extver "github.com/linuxsuren/cobra-extension/version" + "github.com/linuxsuren/ks/kubectl-plugin/component" + kstype "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/cli-runtime/pkg/genericclioptions" "k8s.io/client-go/dynamic" + "k8s.io/client-go/kubernetes" "k8s.io/client-go/rest" "k8s.io/client-go/tools/clientcmd" "os" @@ -29,6 +32,7 @@ See also https://github.com/kubesphere/kubesphere`, var config *rest.Config var err error var client dynamic.Interface + var clientSet *kubernetes.Clientset if config, err = clientcmd.BuildConfigFromFlags("", kubeconfig); err != nil { panic(err) @@ -39,11 +43,17 @@ See also https://github.com/kubesphere/kubesphere`, return } + if clientSet, err = kubernetes.NewForConfig(config); err != nil { + panic(err) + return + } + cmd.AddCommand(NewUserCmd(client), NewPipelineCmd(client), NewUpdateCmd(client), extver.NewVersionCmd("linuxsuren", "ks", "kubectl-ks", nil), - pkg.NewCompletionCmd(cmd)) + pkg.NewCompletionCmd(cmd), + component.NewComponentCmd(client, clientSet)) return } @@ -56,7 +66,7 @@ func NewUserCmd(client dynamic.Interface) (cmd *cobra.Command) { RunE: func(cmd *cobra.Command, args []string) (err error) { name := args[0] - _, err = client.Resource(GetUserSchema()).Patch(context.TODO(), + _, err = client.Resource(kstype.GetUserSchema()).Patch(context.TODO(), name, types.MergePatchType, []byte(fmt.Sprintf(`{"spec":{"password":"%s"},"metadata":{"annotations":null}}`, name)), diff --git a/kubectl-plugin/common/editor.go b/kubectl-plugin/common/editor.go new file mode 100644 index 0000000..a2711e5 --- /dev/null +++ b/kubectl-plugin/common/editor.go @@ -0,0 +1,61 @@ +package common + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "github.com/AlecAivazis/survey/v2" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/client-go/dynamic" + "os" + "sigs.k8s.io/yaml" +) + +// UpdateWithEditor update the k8s resources with a editor +func UpdateWithEditor(resource schema.GroupVersionResource, ns, name string, client dynamic.Interface) (err error) { + var rawPip *unstructured.Unstructured + var data []byte + ctx := context.TODO() + + buf := bytes.NewBuffer(data) + if rawPip, err = client.Resource(resource).Namespace(ns).Get(ctx, name, metav1.GetOptions{}); err == nil { + enc := json.NewEncoder(buf) + enc.SetIndent("", " ") + if err = enc.Encode(rawPip); err != nil { + return + } + } else { + err = fmt.Errorf("cannot get component, error: %#v", err) + return + } + + var yamlData []byte + if yamlData, err = yaml.JSONToYAML(buf.Bytes()); err != nil { + return + } + + var fileName = "*.yaml" + var content = string(yamlData) + + prompt := &survey.Editor{ + Message: fmt.Sprintf("Edit component %s/%s", ns, name), + FileName: fileName, + Default: string(yamlData), + HideDefault: true, + AppendDefault: true, + } + + err = survey.AskOne(prompt, &content, survey.WithStdio(os.Stdin, os.Stdout, os.Stderr)) + if content == string(yamlData) { + fmt.Println("Edit cancelled, no changes made.") + return + } + + if err = yaml.Unmarshal([]byte(content), rawPip); err == nil { + _, err = client.Resource(resource).Namespace(ns).Update(context.TODO(), rawPip, metav1.UpdateOptions{}) + } + return +} diff --git a/kubectl-plugin/component/component.go b/kubectl-plugin/component/component.go new file mode 100644 index 0000000..05611da --- /dev/null +++ b/kubectl-plugin/component/component.go @@ -0,0 +1,506 @@ +package component + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "github.com/AlecAivazis/survey/v2" + "github.com/linuxsuren/ks/kubectl-plugin/common" + kstypes "github.com/linuxsuren/ks/kubectl-plugin/types" + "github.com/spf13/cobra" + "io" + v1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/dynamic" + "k8s.io/client-go/kubernetes" + "os" + "os/signal" + "strconv" + "strings" + "time" +) + +// NewComponentCmd returns a command to manage components of KubeSphere +func NewComponentCmd(client dynamic.Interface, clientset *kubernetes.Clientset) (cmd *cobra.Command) { + cmd = &cobra.Command{ + Use: "component", + Aliases: []string{"com"}, + Short: "Manage the components of KubeSphere", + } + + cmd.AddCommand(NewComponentEnableCmd(client), + NewComponentEditCmd(client), + NewComponentResetCmd(client), + NewComponentWatchCmd(client), + NewComponentLogCmd(client, clientset)) + return +} + +// Option is the common option for component command +type Option struct { + Name string + Release bool + Tag string + Client dynamic.Interface + Clientset *kubernetes.Clientset +} + +// ResetOption is the option for component reset command +type ResetOption struct { + Option + + ResetAll bool +} + +// WatchOption is the option for component watch command +type WatchOption struct { + Option + + Watch bool + WatchImage string + WatchTag string + WatchDeploy string + + Registry string + RegistryUsername string + RegistryPassword string + PrivateRegistry string + PrivateAsLocal bool +} + +// 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.") + 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 + 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 +} + +// NewComponentWatchCmd returns a command to enable (or disable) a component by name +func NewComponentWatchCmd(client dynamic.Interface) (cmd *cobra.Command) { + opt := &WatchOption{ + Option: Option{ + Client: client, + }, + } + cmd = &cobra.Command{ + Use: "watch", + Short: "Update images of ks-apiserver, ks-controller-manager, ks-console", + RunE: opt.watchRunE, + } + + flags := cmd.Flags() + flags.BoolVarP(&opt.Release, "release", "r", true, + "Indicate if you want to update KubeSphere deploy image to release. Released images come from kubesphere/xxx. Otherwise images come from kubespheredev/xxx") + flags.StringVarP(&opt.Tag, "tag", "t", kstypes.KsVersion, + "The tag of KubeSphere deploys") + flags.BoolVarP(&opt.Watch, "watch", "w", false, + "Watch a container image then update it") + flags.StringVarP(&opt.WatchDeploy, "watch-deploy", "", "", + "Watch a deploy then update it") + flags.StringVarP(&opt.WatchImage, "watch-image", "", "", + "which image you want to watch") + flags.StringVarP(&opt.WatchTag, "watch-tag", "", "", + "which image tag you want to watch") + flags.StringVarP(&opt.Registry, "registry", "", "docker", + "supported list [docker, aliyun, qingcloud, private], we only support beijing area of aliyun") + flags.StringVarP(&opt.PrivateRegistry, "private-registry", "", "", + "a private registry, for example: docker run -d -p 5000:5000 --restart always --name registry registry:2 ") + flags.BoolVarP(&opt.PrivateAsLocal, "private-as-local", "", true, + "use 127.0.0.1 as the private registry host") + return +} + +func (o *WatchOption) getDigest(image, tag string) string { + dClient := kstypes.DockerClient{ + Image: image, + Registry: o.Registry, + PrivateRegistry: o.PrivateRegistry, + } + token := dClient.GetToken() + dClient.Token = token + return dClient.GetDigest(tag) +} + +func (o *WatchOption) watchRunE(cmd *cobra.Command, args []string) (err error) { + var currentDigest string + digestChain := make(chan string) + go func(digestChain chan<- string) { + for { + digestChain <- o.getDigest(o.WatchImage, o.WatchTag) + time.Sleep(time.Second * 2) + } + }(digestChain) + + sigChan := make(chan os.Signal) + signal.Notify(sigChan, os.Kill) + signal.Notify(sigChan, os.Interrupt) + + for { + select { + case digest := <-digestChain: + if digest != currentDigest && digest != "" { + fmt.Println("prepare to patch image, new digest is", digest, "old digest is", currentDigest) + fmt.Println("image", o.getFullImagePath(fmt.Sprintf("%s:%s@%s", o.WatchImage, o.WatchTag, digest))) + currentDigest = digest + + ctx := context.TODO() + _, err = o.Client.Resource(kstypes.GetDeploySchema()).Namespace("kubesphere-system").Patch(ctx, + o.WatchDeploy, types.JSONPatchType, + []byte(fmt.Sprintf(`[{"op": "replace", "path": "/spec/template/spec/containers/0/image", "value": "%s"}]`, + o.getFullImagePath(fmt.Sprintf("%s:%s@%s", o.WatchImage, o.WatchTag, digest)))), + metav1.PatchOptions{}) + } + case sig := <-sigChan: + fmt.Println(sig) + return + } + } + return nil +} + +func (o *WatchOption) getFullImagePath(image string) string { + switch o.Registry { + default: + fallthrough + case "docker": + return image + case "aliyun": + return fmt.Sprintf("registry.cn-beijing.aliyuncs.com/%s", image) + case "qingcloud": + return fmt.Sprintf("dockerhub.qingcloud.com/%s", image) + case "private": + if o.PrivateAsLocal { + regAndPort := strings.Split(o.PrivateRegistry, ":") + return fmt.Sprintf("127.0.0.1:%s/%s", regAndPort[1], image) + } + return fmt.Sprintf("%s/%s", o.PrivateRegistry, image) + } +} + +// NewComponentResetCmd returns a command to enable (or disable) a component by name +func NewComponentResetCmd(client dynamic.Interface) (cmd *cobra.Command) { + opt := &ResetOption{ + Option: Option{ + Client: client, + }, + } + cmd = &cobra.Command{ + Use: "reset", + Short: "reset the component by name", + RunE: opt.resetRunE, + } + + flags := cmd.Flags() + flags.BoolVarP(&opt.Release, "release", "r", true, + "Indicate if you want to update KubeSphere deploy image to release. Released images come from KubeSphere/xxx. Otherwise images come from kubespheredev/xxx") + flags.StringVarP(&opt.Tag, "tag", "t", kstypes.KsVersion, + "The tag of KubeSphere deploys") + flags.BoolVarP(&opt.ResetAll, "all", "a", false, + "Indicate if you want to all supported components") + flags.StringVarP(&opt.Name, "name", "n", "", + "The name of target component which you want to reset. This does not work if you provide flag --all") + return +} + +func (o *ResetOption) resetRunE(cmd *cobra.Command, args []string) (err error) { + if o.Tag == "" { + // let users choose it if the tag option is empty + dc := kstypes.DockerClient{ + Image: "kubesphere/ks-apiserver", + } + + var tags *kstypes.DockerTags + if tags, err = dc.GetTags(); err != nil { + err = fmt.Errorf("cannot get the tags, %#v", err) + return + } + + prompt := &survey.Select{ + Message: "Please select the tag which you want to check:", + Options: tags.Tags, + } + if err = survey.AskOne(prompt, &o.Tag); err != nil { + return + } + } + + imageOrg := "kubespheredev" + if o.Release { + imageOrg = "kubesphere" + } else { + o.Tag = "latest" + } + + if o.ResetAll { + o.Name = "apiserver" + if err = o.updateBy(imageOrg, o.Tag); err != nil { + return + } + + o.Name = "controller" + if err = o.updateBy(imageOrg, o.Tag); err != nil { + return + } + + o.Name = "console" + if err = o.updateBy(imageOrg, o.Tag); err != nil { + return + } + } else { + err = o.updateBy(imageOrg, o.Tag) + } + return +} + +func (o *Option) getNsAndName(component string) (ns, name string) { + ns = "kubesphere-system" + switch o.Name { + case "apiserver": + name = "ks-apiserver" + case "controller", "controller-manager": + name = "ks-controller-manager" + case "console": + name = "ks-console" + case "jenkins": + name = "ks-jenkins" + ns = "kubesphere-devops-system" + } + return +} + +func (o *Option) getResourceType(component string) schema.GroupVersionResource { + switch o.Name { + default: + fallthrough + case "apiserver", "controller", "controller-manager", "console": + return kstypes.GetDeploySchema() + } +} + +func (o *Option) updateBy(image, tag string) (err error) { + ns, name := o.getNsAndName(o.Name) + err = o.updateDeploy(ns, name, fmt.Sprintf("%s/%s", image, name), o.Tag) + return +} + +func (o *Option) updateDeploy(ns, name, image, tag string) (err error) { + client := o.Client + + dClient := kstypes.DockerClient{ + Image: image, + } + token := dClient.GetToken() + dClient.Token = token + digest := dClient.GetDigest(tag) + + image = fmt.Sprintf("%s:%s@%s", image, tag, digest) + fmt.Println("prepare to patch image", image) + + ctx := context.TODO() + _, err = client.Resource(kstypes.GetDeploySchema()).Namespace(ns).Patch(ctx, + name, types.JSONPatchType, + []byte(fmt.Sprintf(`[{"op": "replace", "path": "/spec/template/spec/containers/0/image", "value": "%s"}]`, image)), + metav1.PatchOptions{}) + return +} + +// LogOption is the option for component log command +type LogOption struct { + Option + + Follow bool + Tail int64 +} + +// NewComponentLogCmd returns a command to enable (or disable) a component by name +func NewComponentLogCmd(client dynamic.Interface, clientset *kubernetes.Clientset) (cmd *cobra.Command) { + opt := &LogOption{ + Option: Option{ + Clientset: clientset, + Client: client, + }, + } + cmd = &cobra.Command{ + Use: "log", + Short: "output the log of KubeSphere component", + PreRunE: opt.componentNameCheck, + RunE: opt.logRunE, + } + + flags := cmd.Flags() + flags.StringVarP(&opt.Name, "name", "n", "", + "The name of target component which you want to reset.") + flags.BoolVarP(&opt.Follow, "follow", "f", true, + "Specify if the logs should be streamed.") + flags.Int64VarP(&opt.Tail, "tail", "", 50, + `Lines of recent log file to display.`) + return +} + +func (o *LogOption) logRunE(cmd *cobra.Command, args []string) (err error) { + if o.Clientset == nil { + err = fmt.Errorf("kubernetes clientset is nil") + return + } + + ctx := context.TODO() + ns, name := o.getNsAndName(o.Name) + + var data []byte + buf := bytes.NewBuffer(data) + var rawPip *unstructured.Unstructured + deploy := &simpleDeploy{} + if rawPip, err = o.Client.Resource(kstypes.GetDeploySchema()).Namespace(ns).Get(ctx, name, metav1.GetOptions{}); err == nil { + enc := json.NewEncoder(buf) + enc.SetIndent("", " ") + if err = enc.Encode(rawPip); err != nil { + return + } + + cmd.Println(buf) + if err = json.Unmarshal(buf.Bytes(), deploy); err != nil { + return + } + } + + var podList *v1.PodList + var podName string + if podList, err = o.Clientset.CoreV1().Pods(ns).List(ctx, metav1.ListOptions{ + LabelSelector: labels.SelectorFromSet(deploy.Spec.Selector.MatchLabels).String(), + }); err == nil { + if len(podList.Items) > 0 { + podName = podList.Items[0].Name + } + } else { + return + } + + if podName == "" { + err = fmt.Errorf("cannot found the pod with deployment '%s'", name) + return + } + + if len(deploy.Spec.Selector.MatchLabels) > 0 { + req := o.Clientset.CoreV1().Pods(ns).GetLogs(podName, &v1.PodLogOptions{ + Follow: o.Follow, + TailLines: &o.Tail, + }) + var podLogs io.ReadCloser + if podLogs, err = req.Stream(context.TODO()); err == nil { + defer func() { + _ = podLogs.Close() + }() + + _, err = io.Copy(cmd.OutOrStdout(), podLogs) + } + } + return +} + +type simpleDeploy struct { + Spec struct { + Selector struct { + MatchLabels map[string]string `json:"matchLabels"` + } `json:"selector"` + } `json:"spec"` +} + +// NewComponentEditCmd returns a command to enable (or disable) a component by name +func NewComponentEditCmd(client dynamic.Interface) (cmd *cobra.Command) { + opt := &Option{ + Client: client, + } + cmd = &cobra.Command{ + Use: "edit", + Short: "edit the target component", + PreRunE: opt.componentNameCheck, + RunE: opt.editRunE, + } + + flags := cmd.Flags() + flags.StringVarP(&opt.Name, "name", "n", "", + "The name of target component which you want to reset.") + return +} + +func (o *Option) componentNameCheck(cmd *cobra.Command, args []string) (err error) { + if len(args) > 0 { + o.Name = args[0] + } + + if o.Name == "" { + err = fmt.Errorf("please provide the name of component") + } + return +} + +func (o *Option) editRunE(cmd *cobra.Command, args []string) (err error) { + ns, name := o.getNsAndName(o.Name) + resource := o.getResourceType(o.Name) + + err = common.UpdateWithEditor(resource, ns, name, o.Client) + return +} diff --git a/kubectl-plugin/pipeline.go b/kubectl-plugin/pipeline.go index d60b5bb..dc09bac 100644 --- a/kubectl-plugin/pipeline.go +++ b/kubectl-plugin/pipeline.go @@ -6,6 +6,7 @@ import ( "encoding/json" "fmt" "github.com/AlecAivazis/survey/v2" + "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" @@ -19,6 +20,7 @@ func NewPipelineCmd(client dynamic.Interface) (cmd *cobra.Command) { cmd = &cobra.Command{ Use: "pipeline", Aliases: []string{"pip"}, + Short: "Manage the Pipeline of KubeSphere DevOps", RunE: func(cmd *cobra.Command, args []string) (err error) { var pips []string if _, pips, err = getPipelines(client, args); err == nil { @@ -45,7 +47,8 @@ func NewPipelineCmd(client dynamic.Interface) (cmd *cobra.Command) { func NewPipelineViewCmd(client dynamic.Interface) (cmd *cobra.Command) { ctx := context.TODO() cmd = &cobra.Command{ - Use: "view", + Use: "view", + Short: "Output the YAML format of a Pipeline", RunE: func(cmd *cobra.Command, args []string) (err error) { var pips []string var ns string @@ -54,7 +57,7 @@ func NewPipelineViewCmd(client dynamic.Interface) (cmd *cobra.Command) { var rawPip *unstructured.Unstructured var data []byte buf := bytes.NewBuffer(data) - if rawPip, err = client.Resource(GetPipelineSchema()).Namespace(ns).Get(ctx, pip, metav1.GetOptions{}); err == nil { + if rawPip, err = client.Resource(types.GetPipelineSchema()).Namespace(ns).Get(ctx, pip, metav1.GetOptions{}); err == nil { enc := json.NewEncoder(buf) enc.SetIndent("", " ") if err = enc.Encode(rawPip); err != nil { @@ -84,13 +87,14 @@ func NewDelPipelineCmd(client dynamic.Interface) (cmd *cobra.Command) { cmd = &cobra.Command{ Use: "delete", Aliases: []string{"del", "remove", "rm"}, + Short: "Delete a specific Pipeline of KubeSphere DevOps", RunE: func(cmd *cobra.Command, args []string) (err error) { var pips []string var ns string if ns, pips, err = getPipelinesWithConfirm(client, args); err == nil { for _, pip := range pips { fmt.Println(pip) - if err = client.Resource(GetPipelineSchema()).Namespace(ns).Delete(context.TODO(), pip, metav1.DeleteOptions{}); err != nil { + if err = client.Resource(types.GetPipelineSchema()).Namespace(ns).Delete(context.TODO(), pip, metav1.DeleteOptions{}); err != nil { break } } @@ -107,6 +111,7 @@ func NewPipelineEditCmd(client dynamic.Interface) (cmd *cobra.Command) { cmd = &cobra.Command{ Use: "edit", Aliases: []string{"e"}, + Short: "Edit the target pipeline", RunE: func(cmd *cobra.Command, args []string) (err error) { var pips []string var ns string @@ -116,7 +121,7 @@ func NewPipelineEditCmd(client dynamic.Interface) (cmd *cobra.Command) { var data []byte buf := bytes.NewBuffer(data) cmd.Printf("get pipeline %s/%s\n", ns, pip) - if rawPip, err = client.Resource(GetPipelineSchema()).Namespace(ns).Get(ctx, pip, metav1.GetOptions{}); err == nil { + if rawPip, err = client.Resource(types.GetPipelineSchema()).Namespace(ns).Get(ctx, pip, metav1.GetOptions{}); err == nil { enc := json.NewEncoder(buf) enc.SetIndent("", " ") if err = enc.Encode(rawPip); err != nil { @@ -146,7 +151,7 @@ func NewPipelineEditCmd(client dynamic.Interface) (cmd *cobra.Command) { err = survey.AskOne(prompt, &content, survey.WithStdio(os.Stdin, os.Stdout, os.Stderr)) if err = yaml.Unmarshal([]byte(content), rawPip); err == nil { - _, err = client.Resource(GetPipelineSchema()).Namespace(ns).Update(context.TODO(), rawPip, metav1.UpdateOptions{}) + _, err = client.Resource(types.GetPipelineSchema()).Namespace(ns).Update(context.TODO(), rawPip, metav1.UpdateOptions{}) } } } @@ -183,7 +188,7 @@ func getPipelines(client dynamic.Interface, args []string) (ns string, pips []st } var list *unstructured.UnstructuredList - if list, err = client.Resource(GetPipelineSchema()).Namespace(ns).List(context.TODO(), metav1.ListOptions{}); err == nil { + if list, err = client.Resource(types.GetPipelineSchema()).Namespace(ns).List(context.TODO(), metav1.ListOptions{}); err == nil { for _, item := range list.Items { pips = append(pips, item.GetName()) } @@ -213,7 +218,7 @@ func getNamespace(client dynamic.Interface, args []string) (ns string, err error } func getAllNamespace(client dynamic.Interface) (nsList []string) { - if list, err := client.Resource(GetNamespaceSchema()).List(context.TODO(), metav1.ListOptions{ + if list, err := client.Resource(types.GetNamespaceSchema()).List(context.TODO(), metav1.ListOptions{ LabelSelector: "kubesphere.io/devopsproject", }); err == nil { nsList = make([]string, len(list.Items)) diff --git a/kubectl-plugin/constant.go b/kubectl-plugin/types/constant.go similarity index 84% rename from kubectl-plugin/constant.go rename to kubectl-plugin/types/constant.go index 2124f35..c54e767 100644 --- a/kubectl-plugin/constant.go +++ b/kubectl-plugin/types/constant.go @@ -1,4 +1,4 @@ -package main +package types const ( // KsVersion is the latest release version diff --git a/kubectl-plugin/types/docker.go b/kubectl-plugin/types/docker.go new file mode 100644 index 0000000..623168f --- /dev/null +++ b/kubectl-plugin/types/docker.go @@ -0,0 +1,127 @@ +package types + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "net/http" +) + +// DockerClient is a simple Docker client +type DockerClient struct { + Image string + Token string + Registry string + PrivateRegistry string +} + +// DockerTags represents the docker tag list +type DockerTags struct { + Name string + Tags []string +} + +// GetTags returns the tag list +func (d *DockerClient) GetTags() (tags *DockerTags, err error) { + client := http.Client{} + + token := d.GetToken() + d.Token = token + + var req *http.Request + if req, err = http.NewRequest(http.MethodGet, fmt.Sprintf("https://index.docker.io/v2/%s/tags/list", d.Image), nil); err != nil { + return + } + + req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", d.Token)) + req.Header.Set("Accept", "application/vnd.docker.distribution.manifest.v2+json") + + var rsp *http.Response + if rsp, err = client.Do(req); err == nil && rsp != nil && rsp.StatusCode == http.StatusOK { + var data []byte + if data, err = ioutil.ReadAll(rsp.Body); err == nil { + if err = json.Unmarshal(data, tags); err != nil { + err = fmt.Errorf("unexpected docker image tag data, %#v", err) + } + } + } + return +} + +// GetDigest returns the digest of the specific image tag +func (d *DockerClient) GetDigest(tag string) string { + client := http.Client{} + + if tag == "" { + tag = "latest" + } + + var api string + switch d.Registry { + default: + fallthrough + case "docker": + api = fmt.Sprintf("https://index.docker.io/v2/%s/manifests/%s", d.Image, tag) + case "aliyun": + api = fmt.Sprintf("https://registry.cn-beijing.aliyuncs.com/v2/%s/manifests/%s", d.Image, tag) + case "qingcloud": + api = fmt.Sprintf("https://dockerhub.qingcloud.com/v2/%s/manifests/%s", d.Image, tag) + case "private": + api = fmt.Sprintf("http://%s/v2/%s/manifests/%s", d.PrivateRegistry, d.Image, tag) + } + + var req *http.Request + var err error + if req, err = http.NewRequest(http.MethodGet, api, nil); err != nil { + return "" + } + + req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", d.Token)) + req.Header.Set("Accept", "application/vnd.docker.distribution.manifest.v2+json") + + if rsp, err := client.Do(req); err == nil && rsp != nil { + if rsp.StatusCode != http.StatusOK { + fmt.Println(req) + if data, err := ioutil.ReadAll(rsp.Body); err == nil { + fmt.Println(string(data)) + } + } + return rsp.Header.Get("Docker-Content-Digest") + } + return "" +} + +type token struct { + Token string +} + +// GetToken returns the token of target docker image provider +func (d *DockerClient) GetToken() string { + var api string + switch d.Registry { + default: + fallthrough + case "docker": + api = fmt.Sprintf("https://auth.docker.io/token?service=registry.docker.io&scope=repository:%s:pull", d.Image) + case "aliyun": + api = fmt.Sprintf("https://dockerauth.cn-beijing.aliyuncs.com/auth?&service=registry.aliyuncs.com:cn-beijing&scope=repository:%s:pull", d.Image) + case "qingcloud": + api = fmt.Sprintf("https://dockerauth.qingcloud.com:6000/auth?&service=dockerhub.qingcloud.com&scope=repository:%s:pull", d.Image) + case "private": + api = fmt.Sprintf("http://%s/auth?&service=%s&scope=repository:%s:pull", d.PrivateRegistry, d.PrivateRegistry, d.Image) + } + + if req, err := http.NewRequest(http.MethodGet, api, nil); err == nil { + httpClient := http.Client{} + + if rsp, err := httpClient.Do(req); err == nil && rsp.StatusCode == http.StatusOK { + if data, err := ioutil.ReadAll(rsp.Body); err == nil { + token := token{} + if err = json.Unmarshal(data, &token); err == nil { + return token.Token + } + } + } + } + return "" +} diff --git a/kubectl-plugin/schema.go b/kubectl-plugin/types/schema.go similarity index 75% rename from kubectl-plugin/schema.go rename to kubectl-plugin/types/schema.go index e51a450..c65f58a 100644 --- a/kubectl-plugin/schema.go +++ b/kubectl-plugin/types/schema.go @@ -1,4 +1,4 @@ -package main +package types import "k8s.io/apimachinery/pkg/runtime/schema" @@ -36,3 +36,12 @@ func GetDeploySchema() schema.GroupVersionResource { Resource: "deployments", } } + +// GetClusterConfiguration returns the schema of ClusterConfiguration +func GetClusterConfiguration() schema.GroupVersionResource { + return schema.GroupVersionResource{ + Group: "installer.kubesphere.io", + Version: "v1alpha1", + Resource: "clusterconfigurations", + } +} diff --git a/kubectl-plugin/update.go b/kubectl-plugin/update.go index eab6f4a..a422241 100644 --- a/kubectl-plugin/update.go +++ b/kubectl-plugin/update.go @@ -5,6 +5,7 @@ import ( "encoding/json" "fmt" "github.com/AlecAivazis/survey/v2" + types2 "github.com/linuxsuren/ks/kubectl-plugin/types" "github.com/spf13/cobra" "io/ioutil" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -40,18 +41,19 @@ func NewUpdateCmd(client dynamic.Interface) (cmd *cobra.Command) { } cmd = &cobra.Command{ - Use: "update", - Short: "Update images of ks-apiserver, ks-controller-manager, ks-console", - Aliases: []string{"up"}, - PreRun: opt.preRun, - Args: opt.args, - RunE: opt.RunE, + Use: "update", + Short: "Update images of ks-apiserver, ks-controller-manager, ks-console", + Aliases: []string{"up"}, + Deprecated: "This command will be removed after v0.1.0. Please use kubectl ks component xxx instead.", + PreRun: opt.preRun, + Args: opt.args, + RunE: opt.RunE, } flags := cmd.Flags() flags.BoolVarP(&opt.Release, "release", "r", true, "Indicate if you want to update Kubesphere deploy image to release. Released images come from kubesphere/xxx. Otherwise images come from kubespheredev/xxx") - flags.StringVarP(&opt.Tag, "tag", "t", KsVersion, + flags.StringVarP(&opt.Tag, "tag", "t", types2.KsVersion, "The tag of Kubesphere deploys") flags.BoolVarP(&opt.Watch, "watch", "w", false, "Watch a container image then update it") @@ -86,7 +88,7 @@ func (o *updateCmdOption) args(cmd *cobra.Command, args []string) (err error) { func (o *updateCmdOption) preRun(cmd *cobra.Command, args []string) { if o.Release { - o.Tag = KsVersion + o.Tag = types2.KsVersion } else { o.Tag = "latest" } @@ -146,7 +148,7 @@ func (o *updateCmdOption) RunE(cmd *cobra.Command, args []string) (err error) { currentDigest = digest ctx := context.TODO() - _, err = o.Client.Resource(GetDeploySchema()).Namespace("kubesphere-system").Patch(ctx, + _, err = o.Client.Resource(types2.GetDeploySchema()).Namespace("kubesphere-system").Patch(ctx, o.WatchDeploy, types.JSONPatchType, []byte(fmt.Sprintf(`[{"op": "replace", "path": "/spec/template/spec/containers/0/image", "value": "%s"}]`, o.getFullImagePath(fmt.Sprintf("%s:%s@%s", o.WatchImage, o.WatchTag, digest)))), @@ -204,7 +206,7 @@ func (o *updateCmdOption) updateDeploy(ns, name, image, tag string) (err error) fmt.Println("prepare to patch image", image) ctx := context.TODO() - _, err = client.Resource(GetDeploySchema()).Namespace(ns).Patch(ctx, + _, err = client.Resource(types2.GetDeploySchema()).Namespace(ns).Patch(ctx, name, types.JSONPatchType, []byte(fmt.Sprintf(`[{"op": "replace", "path": "/spec/template/spec/containers/0/image", "value": "%s"}]`, image)), metav1.PatchOptions{})