From 3dbd45d940356a52cd011c6d061ff12f0b3668a2 Mon Sep 17 00:00:00 2001 From: ysicing Date: Thu, 21 Jul 2022 17:15:56 +0800 Subject: [PATCH] feat(exec): add exec support add exec support Signed-off-by: ysicing --- VERSION | 2 +- cmd/boot/boot.go | 4 +-- cmd/manage.go | 9 +++++ cmd/manage/exec.go | 72 +++++++++++++++++++++++++++++++++++++ cmd/manage/get.go | 2 +- cmd/root.go | 1 + common/func.go | 7 ++++ go.mod | 4 ++- go.sum | 10 ++++++ internal/app/debug/fetch.go | 5 ++- internal/pkg/k8s/client.go | 50 +++++++++++++++++++++++--- 11 files changed, 156 insertions(+), 10 deletions(-) create mode 100644 cmd/manage/exec.go diff --git a/VERSION b/VERSION index 66c4c226..7ee7020b 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.0.9 +1.0.10 diff --git a/cmd/boot/boot.go b/cmd/boot/boot.go index 5ac8268c..c9060689 100644 --- a/cmd/boot/boot.go +++ b/cmd/boot/boot.go @@ -31,8 +31,8 @@ func initRootDirectory() error { return errors.Errorf("failed to mkdir %s, err: %s", dir, err) } } - if err := os.MkdirAll(common.DefaultQuickonDir, common.FileMode0777); err != nil { - return errors.Errorf("failed to mkdir %s, err: %s", common.DefaultQuickonDir, err) + if err := os.MkdirAll(common.GetDefaultQuickonDir(), common.FileMode0777); err != nil { + return errors.Errorf("failed to mkdir %s, err: %s", common.GetDefaultQuickonDir(), err) } return nil } diff --git a/cmd/manage.go b/cmd/manage.go index 03e5256a..75e07794 100644 --- a/cmd/manage.go +++ b/cmd/manage.go @@ -33,3 +33,12 @@ func newCmdManageGet(f factory.Factory) *cobra.Command { m.AddCommand(manage.NewCmdGetApp(f)) return m } + +func newCmdManageExec(f factory.Factory) *cobra.Command { + m := &cobra.Command{ + Use: "exec", + Short: "Execute a command in a app", + } + m.AddCommand(manage.NewCmdExecApp(f)) + return m +} diff --git a/cmd/manage/exec.go b/cmd/manage/exec.go new file mode 100644 index 00000000..b650c084 --- /dev/null +++ b/cmd/manage/exec.go @@ -0,0 +1,72 @@ +// Copyright (c) 2021-2022 北京渠成软件有限公司(Beijing Qucheng Software Co., Ltd. www.qucheng.com) All rights reserved. +// Use of this source code is covered by the following dual licenses: +// (1) Z PUBLIC LICENSE 1.2 (ZPL 1.2) +// (2) Affero General Public License 3.0 (AGPL 3.0) +// license that can be found in the LICENSE file. + +package manage + +import ( + "context" + "fmt" + + "github.com/easysoft/qcadmin/internal/app/debug" + "github.com/easysoft/qcadmin/internal/pkg/k8s" + "github.com/easysoft/qcadmin/internal/pkg/util/factory" + "github.com/manifoldco/promptui" + "github.com/sirupsen/logrus" + "github.com/spf13/cobra" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/labels" +) + +func NewCmdExecApp(f factory.Factory) *cobra.Command { + log := f.GetLog() + app := &cobra.Command{ + Use: "app", + Aliases: []string{"apps"}, + Short: "exec app", + Args: cobra.ExactArgs(1), + Example: `q exec app http://console.efbb.haogs.cn/instance-view-39.html`, + RunE: func(cmd *cobra.Command, args []string) error { + url := args[0] + apidebug := log.GetLevel() == logrus.DebugLevel + log.Infof("start exec app: %s", url) + appdata, err := debug.GetNameByURL(url, apidebug) + if err != nil { + return err + } + k8sClient, err := k8s.NewSimpleClient() + if err != nil { + log.Errorf("k8s client err: %v", err) + return err + } + ctx := context.Background() + podlist, _ := k8sClient.ListPods(ctx, "default", metav1.ListOptions{ + LabelSelector: labels.SelectorFromSet(map[string]string{ + "release": appdata.K8Name, + }).String(), + }) + if len(podlist.Items) < 1 { + return fmt.Errorf("podnum %d, app maybe not running", len(podlist.Items)) + } + templates := &promptui.SelectTemplates{ + Label: "{{ . }}?", + Active: "\U0001F44F {{ .Name | cyan }}", + Inactive: " {{ .Name | cyan }}", + Selected: "\U0001F31D {{ .Name | red | cyan }}", + } + + prompt := promptui.Select{ + Label: "select pod", + Items: podlist.Items, + Templates: templates, + Size: 5, + } + it, _, _ := prompt.Run() + + return k8sClient.ExecPodWithTTY(ctx, "default", podlist.Items[it].Name, podlist.Items[it].Spec.Containers[0].Name, []string{"/bin/sh", "-c", "sh"}) + }, + } + return app +} diff --git a/cmd/manage/get.go b/cmd/manage/get.go index 4f9391fc..1586d647 100644 --- a/cmd/manage/get.go +++ b/cmd/manage/get.go @@ -53,7 +53,7 @@ func NewCmdGetApp(f factory.Factory) *cobra.Command { // if apidebug { // spew.Dump(appdata) // } - extargs := []string{"exp", "kubectl", "get", "-o", "wide", "pods,deploy,pvc,svc,ing", "-l", "release=" + appdata.K8Name, "-l", "app=" + appdata.Chart} + extargs := []string{"exp", "kubectl", "get", "-o", "wide", "pods,deploy,pvc,svc,ing", "-l", "release=" + appdata.K8Name} // extargs = append(extargs, args...) return qcexec.CommandRun(os.Args[0], extargs...) }, diff --git a/cmd/root.go b/cmd/root.go index 7ed095a7..03da5e09 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -71,6 +71,7 @@ func BuildRoot(f factory.Factory) *cobra.Command { rootCmd.AddCommand(newCmdUpgrade(f)) rootCmd.AddCommand(newCmdManage(f)) rootCmd.AddCommand(newCmdManageGet(f)) + rootCmd.AddCommand(newCmdManageExec(f)) // Add plugin commands rootCmd.AddCommand(NewCmdExperimental(f)) diff --git a/common/func.go b/common/func.go index 4751a875..931af3d6 100644 --- a/common/func.go +++ b/common/func.go @@ -113,3 +113,10 @@ func GetAPI(path string) string { path = strings.TrimLeft(path, "/") return fmt.Sprintf("https://api.qucheng.com/%s", path) } + +func GetDefaultQuickonDir() string { + if zos.IsMacOS() { + return fmt.Sprintf("%v/%v", zos.GetHomeDir(), DefaultQuickonDir) + } + return DefaultQuickonDir +} diff --git a/go.mod b/go.mod index a1eef731..c484cd11 100644 --- a/go.mod +++ b/go.mod @@ -28,7 +28,7 @@ require ( github.com/spf13/pflag v1.0.5 github.com/ulikunitz/xz v0.5.10 golang.org/x/sync v0.0.0-20210220032951-036812b2e83c - golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e + golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 gopkg.in/natefinch/lumberjack.v2 v2.0.0 gopkg.in/yaml.v2 v2.4.0 gotest.tools v2.2.0+incompatible @@ -64,6 +64,7 @@ require ( github.com/cespare/xxhash/v2 v2.1.2 // indirect github.com/chai2010/gettext-go v0.0.0-20160711120539-c6fed771bfd5 // indirect github.com/cheekybits/genny v1.0.0 // indirect + github.com/chzyer/readline v1.5.1 // indirect github.com/containerd/containerd v1.6.6 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect github.com/cyphar/filepath-securejoin v0.2.3 // indirect @@ -117,6 +118,7 @@ require ( github.com/lithammer/dedent v1.1.0 // indirect github.com/lucas-clemente/quic-go v0.28.0 // indirect github.com/mailru/easyjson v0.7.6 // indirect + github.com/manifoldco/promptui v0.9.0 // indirect github.com/marten-seemann/qpack v0.2.1 // indirect github.com/marten-seemann/qtls-go1-16 v0.1.5 // indirect github.com/marten-seemann/qtls-go1-17 v0.1.2 // indirect diff --git a/go.sum b/go.sum index 86094cbd..8d4626a5 100644 --- a/go.sum +++ b/go.sum @@ -211,8 +211,12 @@ github.com/cheekybits/genny v1.0.0/go.mod h1:+tQajlRqAUrPI7DOSpB0XAqZYtQakVtB7wX github.com/cheggaaa/pb/v3 v3.1.0 h1:3uouEsl32RL7gTiQsuaXD4Bzbfl5tGztXGUvXbs4O04= github.com/cheggaaa/pb/v3 v3.1.0/go.mod h1:YjrevcBqadFDaGQKRdmZxTY42pXEqda48Ea3lt0K/BE= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/logex v1.2.1/go.mod h1:JLbx6lG2kDbNRFnfkgvh4eRJRPX1QCoOIWomwysCBrQ= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/readline v1.5.1 h1:upd/6fQk4src78LMRzh5vItIt361/o4uq553V8B5sGI= +github.com/chzyer/readline v1.5.1/go.mod h1:Eh+b79XXUwfKfcPLepksvw2tcLE/Ct21YObkaSkeBlk= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8= github.com/cilium/ebpf v0.0.0-20200110133405-4032b1d8aae3/go.mod h1:MA5e5Lr8slmEg9bt0VpxxWqJlO4iwu3FBdHUzV7wQVg= github.com/cilium/ebpf v0.0.0-20200702112145-1c8d4c9ef775/go.mod h1:7cR51M8ViRLIdUjrmSXlK9pkrsDlLHbO8jiB8X8JnOc= github.com/cilium/ebpf v0.2.0/go.mod h1:To2CFviqOWL/M0gIMsvSMlqe7em/l1ALkX1PyjrX2Qs= @@ -821,6 +825,8 @@ github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN github.com/mailru/easyjson v0.7.0/go.mod h1:KAzv3t3aY1NaHWoQz1+4F1ccyAH66Jk7yos7ldAVICs= github.com/mailru/easyjson v0.7.6 h1:8yTIVnZgCoiM1TgqoeTl+LfU5Jg6/xL3QhGQnimLYnA= github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/manifoldco/promptui v0.9.0 h1:3V4HzJk1TtXW1MTZMP7mdlwbBpIinw3HztaIlYthEiA= +github.com/manifoldco/promptui v0.9.0/go.mod h1:ka04sppxSGFAtxX0qhlYQjISsg9mR4GWtQEhdbn6Pgg= github.com/markbates/errx v1.1.0 h1:QDFeR+UP95dO12JgW+tgi2UVfo0V8YBHiUIOaeBPiEI= github.com/markbates/errx v1.1.0/go.mod h1:PLa46Oex9KNbVDZhKel8v1OT7hD5JZ2eI7AHhA0wswc= github.com/markbates/oncer v1.0.0 h1:E83IaVAHygyndzPimgUYJjbshhDTALZyXxvk9FOlQRY= @@ -1502,6 +1508,7 @@ golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5h golang.org/x/sys v0.0.0-20181029174526-d69651ed3497/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -1615,11 +1622,14 @@ golang.org/x/sys v0.0.0-20211205182925-97ca703d548d/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e h1:CsOuNlbOuf0mzxJIefr6Q4uAUetRUwZE4qt7VfzP+xo= golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 h1:0A+M6Uqn+Eje4kHMK80dtF3JCXC4ykBgQG4Fe06QRhQ= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= diff --git a/internal/app/debug/fetch.go b/internal/app/debug/fetch.go index f5570d4a..28aa2691 100644 --- a/internal/app/debug/fetch.go +++ b/internal/app/debug/fetch.go @@ -48,7 +48,10 @@ type AppData struct { func GetNameByURL(url string, debug bool) (*AppData, error) { // 获取ID k := strings.Split(url, "-") - key := strings.Trim(k[len(k)-1], ".html") + if len(k) < 3 { + return nil, fmt.Errorf("url err") + } + key := k[2] cfg, _ := config.LoadConfig() if cfg.APIToken == "" { diff --git a/internal/pkg/k8s/client.go b/internal/pkg/k8s/client.go index 48e0c3f1..e8cd5060 100644 --- a/internal/pkg/k8s/client.go +++ b/internal/pkg/k8s/client.go @@ -16,6 +16,7 @@ import ( "time" "github.com/ergoapi/util/exmap" + "golang.org/x/term" appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" networkingv1 "k8s.io/api/networking/v1" @@ -29,6 +30,8 @@ import ( "k8s.io/client-go/rest" "k8s.io/client-go/tools/clientcmd" clientcmdapi "k8s.io/client-go/tools/clientcmd/api" + "k8s.io/client-go/tools/remotecommand" + "k8s.io/kubectl/pkg/scheme" metrics "k8s.io/metrics/pkg/client/clientset/versioned" ) @@ -43,11 +46,14 @@ type Client struct { } func NewSimpleClient() (*Client, error) { - dir, err := os.UserHomeDir() - if err != nil { - return nil, err + kubeconfig := os.Getenv("KUBECONFIG") + if kubeconfig == "" { + dir, err := os.UserHomeDir() + if err != nil { + return nil, err + } + kubeconfig = filepath.Join(dir, ".kube", "config") } - kubeconfig := filepath.Join(dir, ".kube", "config") config, err := clientcmd.BuildConfigFromFlags("", kubeconfig) if err != nil { return nil, err @@ -428,6 +434,42 @@ func (c *Client) ExecInPod(ctx context.Context, namespace, pod, container string return result.Stdout, nil } +func (c *Client) ExecPodWithTTY(ctx context.Context, namespace, podName, container string, command []string) error { + req := c.Clientset.CoreV1().RESTClient().Post(). + Resource("pods"). + Name(podName). + Namespace(namespace). + SubResource("exec"). + VersionedParams(&corev1.PodExecOptions{ + Command: command, + Stdin: true, + Stdout: true, + Stderr: true, + TTY: true, + }, scheme.ParameterCodec) + exec, err := remotecommand.NewSPDYExecutor(c.Config, "POST", req.URL()) + if err != nil { + return err + } + if !term.IsTerminal(0) || !term.IsTerminal(1) { + return fmt.Errorf("stdin/stdout must be a terminal") + } + oldstate, _ := term.MakeRaw(0) + defer term.Restore(0, oldstate) + screen := struct { + io.Reader + io.Writer + }{os.Stdin, os.Stdout} + + err = exec.Stream(remotecommand.StreamOptions{ + Stdin: screen, + Stdout: screen, + Stderr: screen, + Tty: true, + }) + return err +} + func (c *Client) CreateIngressClass(ctx context.Context, ingressClass *networkingv1.IngressClass, opts metav1.CreateOptions) (*networkingv1.IngressClass, error) { return c.Clientset.NetworkingV1().IngressClasses().Create(ctx, ingressClass, opts) }