Skip to content

Commit

Permalink
Add exec command
Browse files Browse the repository at this point in the history
Should be functionally equivalent to `kubectl exec` including
handling window resizing and such, except that as with everything
else in kubectl-daemons, you don't need to get pod names

Closes #6

Signed-off-by: Phil Dibowitz <[email protected]>
  • Loading branch information
jaymzh committed Mar 12, 2024
1 parent 2dcbd6b commit b751efb
Show file tree
Hide file tree
Showing 10 changed files with 206 additions and 9 deletions.
2 changes: 1 addition & 1 deletion cmd/delete.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
2 changes: 1 addition & 1 deletion cmd/describe.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
1 change: 1 addition & 0 deletions cmd/dsh.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
183 changes: 183 additions & 0 deletions cmd/exec.go
Original file line number Diff line number Diff line change
@@ -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 <daemonset> [<options>] -- <command> [args...]",
Short: "execute arbitrary commands in pod for <daemonset>",
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
}
2 changes: 1 addition & 1 deletion cmd/get.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
2 changes: 1 addition & 1 deletion cmd/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
2 changes: 1 addition & 1 deletion cmd/log.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
7 changes: 4 additions & 3 deletions cmd/util.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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(
Expand Down
5 changes: 4 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -29,18 +30,21 @@ 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
github.com/josharian/intern v1.0.0 // indirect
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
Expand All @@ -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
Expand Down
9 changes: 9 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand Down Expand Up @@ -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=
Expand All @@ -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=
Expand All @@ -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=
Expand Down

0 comments on commit b751efb

Please sign in to comment.