diff --git a/cmd/delete.go b/cmd/delete.go index 51da2ea..abc488c 100644 --- a/cmd/delete.go +++ b/cmd/delete.go @@ -31,7 +31,7 @@ func newDshDeleteCommand( func (sv *dshCmd) deletePods( ccontext string, namespace string, ds string, nodeName string, ) error { - clientset, err := getClientSet(ccontext) + clientset, _, err := getClientSet(ccontext) if err != nil { return err } diff --git a/cmd/describe.go b/cmd/describe.go index 67885d3..4eccb21 100644 --- a/cmd/describe.go +++ b/cmd/describe.go @@ -39,7 +39,7 @@ func newDshDescribeCommand( func (sv *dshCmd) describePods( ccontext string, namespace string, ds string, nodeName string, ) error { - clientset, err := getClientSet(ccontext) + clientset, _, err := getClientSet(ccontext) if err != nil { return err } diff --git a/cmd/dsh.go b/cmd/dsh.go index 038f32c..f65742d 100644 --- a/cmd/dsh.go +++ b/cmd/dsh.go @@ -41,5 +41,6 @@ func NewDshCommand(streams genericclioptions.IOStreams) *cobra.Command { dshCmd.AddCommand(newDshDescribeCommand(streams.Out, &context, &namespace, &nodeName)) dshCmd.AddCommand(newDshLogCommand(streams.Out, &context, &namespace, &nodeName)) dshCmd.AddCommand(newDshListCommand(streams.Out, &context, &namespace, &nodeName)) + dshCmd.AddCommand(newDshExecCommand(streams.Out, &context, &namespace, &nodeName)) return dshCmd } diff --git a/cmd/exec.go b/cmd/exec.go new file mode 100644 index 0000000..62c6a94 --- /dev/null +++ b/cmd/exec.go @@ -0,0 +1,183 @@ +package cmd + +import ( + "context" + "errors" + "fmt" + "github.com/spf13/cobra" + "io" + "os" + "os/signal" + "syscall" + + "k8s.io/client-go/kubernetes/scheme" + "k8s.io/client-go/tools/remotecommand" + "golang.org/x/term" + + v1 "k8s.io/api/core/v1" +) + + +func newDshExecCommand( + out io.Writer, context *string, namespace *string, nodeName *string, +) *cobra.Command { + var container string + var stdin bool + var tty bool + + dshExec := &dshCmd{ + out: out, + } + + cmd := &cobra.Command{ + Use: "exec [] -- [args...]", + Short: "execute arbitrary commands in pod for ", + Args: cobra.MatchAll(cobra.MinimumNArgs(1)), + RunE: func(cmd *cobra.Command, args []string) error { + if len(args) > 1 && cmd.ArgsLenAtDash() != -1 { + remoteCommand := args[cmd.ArgsLenAtDash():] + return dshExec.execPod( + *context, *namespace, args[0], *nodeName, container, stdin, + tty, remoteCommand, + ) + } else { + return errors.New("At least some command is required") + } + }, + } + + cmd.Flags().StringVarP( + &container, "container", "c", "", "The container to exec into", + ) + cmd.Flags().BoolVarP( + &stdin, "stdin", "i", false, "Pass stdin to the container", + ) + cmd.Flags().BoolVarP( + &tty, "tty", "t", false, "Stdin is a TTY", + ) + return cmd +} + +type terminalSizeQueue struct { + sizeQueue chan remotecommand.TerminalSize +} + +func (t *terminalSizeQueue) Next() *remotecommand.TerminalSize { + size, ok := <- t.sizeQueue + if !ok { + return nil + } + return &size +} + +func monitorTerminalResize(sizeQueue chan remotecommand.TerminalSize) { + ch := make(chan os.Signal, 1) + signal.Notify(ch, syscall.SIGWINCH) + defer signal.Stop(ch) + + for range ch { + if width, height, err := term.GetSize(int(os.Stdin.Fd())); err == nil { + sizeQueue <- remotecommand.TerminalSize{ + Width: uint16(width), Height: uint16(height), + } + } + } +} + +func (sv *dshCmd) execPod( + kcontext string, namespace string, ds string, nodeName string, + container string, stdin bool, tty bool, cmd []string, +) error { + clientset, config, err := getClientSet(kcontext) + if err != nil { + return err + } + + pods, err := getPodsForDaemonSet(clientset, ds, namespace, nodeName) + if err != nil { + return err + } + + if len(pods) == 0 { + fmt.Printf("No pods found\n") + return nil + } + + if len(pods) > 1 { + fmt.Printf("More than one pod found, wut?!") + return nil + } + + req := clientset.CoreV1().RESTClient(). + Post(). + Resource("pods"). + Name(pods[0].Name). + Namespace(namespace). + SubResource("exec"). + VersionedParams(&v1.PodExecOptions{ + Command: cmd, + Container: container, + Stdin: stdin, + Stdout: true, + Stderr: true, + TTY: tty, + }, scheme.ParameterCodec) + + exec, err := remotecommand.NewSPDYExecutor(config, "POST", req.URL()) + if err != nil { + return err + } + + var streamOptions remotecommand.StreamOptions + if tty { + initialState, err := term.MakeRaw(int(os.Stdin.Fd())) + if err != nil { + return err + } + defer func() { + if err := term.Restore(int(os.Stdin.Fd()), initialState); err != nil { + // Handle the error, e.g., log it or print it. + fmt.Fprintf(os.Stderr, "Error restoring terminal: %v\n", err) + } + }() + + // This queue has to be made with size 1 so that sending the original + // size before there's a listener won't cause a freeze + sizeQueue := make(chan remotecommand.TerminalSize, 1) + tQueue := &terminalSizeQueue{sizeQueue: sizeQueue} + + // Send the initial terminal size. + if width, height, err := term.GetSize(int(os.Stdin.Fd())); err == nil { + sizeQueue <- remotecommand.TerminalSize{ + Width: uint16(width), Height: uint16(height), + } + } + + go monitorTerminalResize(sizeQueue) + + streamOptions = remotecommand.StreamOptions{ + Stdin: os.Stdin, + Stdout: os.Stdout, + Stderr: os.Stdout, + Tty: tty, + TerminalSizeQueue: tQueue, + } + } else { + streamOptions = remotecommand.StreamOptions{ + Stdin: os.Stdin, + Stdout: os.Stdout, + Stderr: os.Stdout, + Tty: tty, + } + } + + if !stdin { + streamOptions.Stdin = nil + } + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + err = exec.StreamWithContext(ctx, streamOptions) + return err +} diff --git a/cmd/get.go b/cmd/get.go index ad199d2..be55a6d 100644 --- a/cmd/get.go +++ b/cmd/get.go @@ -48,7 +48,7 @@ func newDshGetCommand( func (sv *dshCmd) getPods( context string, namespace string, ds string, nodeName string, output string, ) error { - clientset, err := getClientSet(context) + clientset, _, err := getClientSet(context) if err != nil { return err } diff --git a/cmd/list.go b/cmd/list.go index 777575b..d63cbeb 100644 --- a/cmd/list.go +++ b/cmd/list.go @@ -39,7 +39,7 @@ func (sv *dshCmd) getDaemonSets( return errors.New("You must specify a node") } - clientset, err := getClientSet(context) + clientset, _, err := getClientSet(context) if err != nil { return err } diff --git a/cmd/log.go b/cmd/log.go index 471a073..8916ec1 100644 --- a/cmd/log.go +++ b/cmd/log.go @@ -53,7 +53,7 @@ func (sv *dshCmd) getLogs( ccontext string, namespace string, ds string, nodeName string, container string, follow bool, lines *int, ) error { - clientset, err := getClientSet(ccontext) + clientset, _, err := getClientSet(ccontext) if err != nil { return err } diff --git a/cmd/util.go b/cmd/util.go index a545dd7..ac17ccf 100644 --- a/cmd/util.go +++ b/cmd/util.go @@ -4,11 +4,12 @@ import ( "context" "k8s.io/client-go/tools/clientcmd" "k8s.io/client-go/kubernetes" + "k8s.io/client-go/rest" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) -func getClientSet(context string) (*kubernetes.Clientset, error) { +func getClientSet(context string) (*kubernetes.Clientset, *rest.Config, error) { loadingRules := clientcmd.NewDefaultClientConfigLoadingRules() configOverrides := &clientcmd.ConfigOverrides{ CurrentContext: context, @@ -19,11 +20,11 @@ func getClientSet(context string) (*kubernetes.Clientset, error) { config, err := kubeConfig.ClientConfig() if err != nil { - return nil, err + return nil, nil, err } clientset, err := kubernetes.NewForConfig(config) - return clientset, err + return clientset, config, err } func getDaemonSetsForNode( diff --git a/go.mod b/go.mod index b3b046c..6bc0773 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,7 @@ go 1.21.4 require ( github.com/spf13/cobra v1.8.0 + golang.org/x/term v0.15.0 golang.org/x/text v0.14.0 gopkg.in/yaml.v3 v3.0.1 k8s.io/api v0.29.2 @@ -29,6 +30,7 @@ require ( github.com/google/gofuzz v1.2.0 // indirect github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect github.com/google/uuid v1.3.0 // indirect + github.com/gorilla/websocket v1.5.0 // indirect github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7 // indirect github.com/imdario/mergo v0.3.6 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect @@ -36,11 +38,13 @@ require ( github.com/json-iterator/go v1.1.12 // indirect github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de // indirect github.com/mailru/easyjson v0.7.7 // indirect + github.com/moby/spdystream v0.2.0 // indirect github.com/moby/term v0.0.0-20221205130635-1aeaba878587 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f // indirect github.com/peterbourgon/diskv v2.0.1+incompatible // indirect github.com/pkg/errors v0.9.1 // indirect github.com/spf13/pflag v1.0.5 // indirect @@ -50,7 +54,6 @@ require ( golang.org/x/oauth2 v0.10.0 // indirect golang.org/x/sync v0.5.0 // indirect golang.org/x/sys v0.15.0 // indirect - golang.org/x/term v0.15.0 // indirect golang.org/x/time v0.3.0 // indirect google.golang.org/appengine v1.6.7 // indirect google.golang.org/protobuf v1.31.0 // indirect diff --git a/go.sum b/go.sum index cc5dbcc..b01345b 100644 --- a/go.sum +++ b/go.sum @@ -2,6 +2,8 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMT github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8= github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= +github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= @@ -71,6 +73,9 @@ github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaU github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= +github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7 h1:pdN6V1QBWetyv/0+wjACpqVH+eVULgEjkurDLq3goeM= github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= github.com/imdario/mergo v0.3.6 h1:xTNEAn+kxVO7dTZGu0CegyqKZmoWFI0rF8UxjlB2d28= @@ -94,6 +99,8 @@ github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de h1:9TO3cAIGXtEhn github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de/go.mod h1:zAbeS9B/r2mtpb6U+EI2rYA5OAXxsYw6wTamcNW+zcE= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/moby/spdystream v0.2.0 h1:cjW1zVyyoiM0T7b6UoySUFqzXMoqRckQtXwGPiBhOM8= +github.com/moby/spdystream v0.2.0/go.mod h1:f7i0iNDQJ059oMTcWxx8MA/zKFIuD/lY+0GqbN2Wy8c= github.com/moby/term v0.0.0-20221205130635-1aeaba878587 h1:HfkjXDfhgVaN5rmueG8cL8KKeFNecRCXFhaJ2qZ5SKA= github.com/moby/term v0.0.0-20221205130635-1aeaba878587/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -105,6 +112,8 @@ github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 h1:n6/ github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00/go.mod h1:Pm3mSP3c5uWn86xMLZ5Sa7JB9GsEZySvHYXCTK4E9q4= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f h1:y5//uYreIhSUg3J1GEMiLbxo1LJaP8RfCpH6pymGZus= +github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= github.com/onsi/ginkgo/v2 v2.13.0 h1:0jY9lJquiL8fcf3M4LAXN5aMlS/b2BV86HFFPCPMgE4= github.com/onsi/ginkgo/v2 v2.13.0/go.mod h1:TE309ZR8s5FsKKpuB1YAQYBzCaAfUgatB/xlT/ETL/o= github.com/onsi/gomega v1.29.0 h1:KIA/t2t5UBzoirT4H9tsML45GEbo3ouUnBHsCfD2tVg=