Skip to content

Commit

Permalink
feat: Implement Tab Completion
Browse files Browse the repository at this point in the history
* feat: inital completion poc
* feat: finish completion script poc
* doc: Update README.md
  • Loading branch information
jpts authored Jun 22, 2023
1 parent 4ea47c8 commit de59df6
Show file tree
Hide file tree
Showing 4 changed files with 188 additions and 10 deletions.
12 changes: 10 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ The Kubernetes API server has support for exec over WebSockets, but it has yet t
Usage:
```
Usage:
execws <pod name> [options] <cmd>
kubectl-execws <pod name> [options] -- <cmd>
Flags:
-c, --container string Container name
Expand All @@ -31,6 +31,14 @@ Flags:
* Supports a full TTY (terminal raw mode)
* Can bypass the API server with direct connection to the nodes kubelet API

### Acknowledgements
## Tab Completion

Tab completion is available for various shells `[bash|zsh|fish|powershell]`.

This can be used with the standalone binary through use of the `completion` subcommand, eg. `source <(kubectl-execws completion zsh)`

Completion is also available when using as a kubectl plugin. To set this up it is necessary to symlink to the multi-call binary with a special name: `ln -s kubectl-execws kubectl_complete-execws`.

## Acknowledgements

Work inspired by [rmohr/kubernetes-custom-exec](https://github.com/rmohr/kubernetes-custom-exec) and [kairen/websocket-exec](https://github.com/kairen/websocket-exec).
98 changes: 98 additions & 0 deletions cmd/completion.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
package cmd

import (
"context"
"strings"

"github.com/spf13/cobra"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

func MainValidArgs(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
s, cerr := initCompletionCliSession()
if cerr != nil {
return nil, cobra.ShellCompDirectiveError
}
if len(args) != 0 {
return nil, cobra.ShellCompDirectiveNoFileComp
}
return completeAvailablePods(s, toComplete)
}

func NamespaceValidArgs(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
s, cerr := initCompletionCliSession()
if cerr != nil {
return nil, cobra.ShellCompDirectiveError
}
return completeAvailableNS(s, toComplete)
}

func ContainerValidArgs(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
s, cerr := initCompletionCliSession()
if cerr != nil {
return nil, cobra.ShellCompDirectiveError
}
s.opts.Pod = args[0]
return completeAvailableContainers(s, toComplete)
}

func initCompletionCliSession() (*cliSession, error) {
opts := Options{
Kconfig: kconfig,
Namespace: namespace,
noTLSVerify: noTLSVerify,
}
return NewCliSession(&opts)
}

func completeAvailableNS(c *cliSession, toComplete string) ([]string, cobra.ShellCompDirective) {
res, err := c.k8sClient.CoreV1().Namespaces().List(context.TODO(), metav1.ListOptions{})
if err != nil {
return nil, cobra.ShellCompDirectiveError
}
var nspaces []string
for _, ns := range res.Items {
if strings.HasPrefix(ns.Name, toComplete) {
nspaces = append(nspaces, ns.Name)
}
}

return nspaces, cobra.ShellCompDirectiveNoFileComp
}

func completeAvailablePods(c *cliSession, toComplete string) ([]string, cobra.ShellCompDirective) {
res, err := c.k8sClient.CoreV1().Pods(c.namespace).List(context.TODO(), metav1.ListOptions{})
if err != nil {
return nil, cobra.ShellCompDirectiveError
}
var pods []string
for _, pod := range res.Items {
if strings.HasPrefix(pod.Name, toComplete) {
//noCtrs := len(pod.Spec.Containers)
//noEphem := len(pod.Spec.EphemeralContainers)
pods = append(pods, pod.Name)
}
}

return pods, cobra.ShellCompDirectiveNoFileComp
}

func completeAvailableContainers(c *cliSession, toComplete string) ([]string, cobra.ShellCompDirective) {
res, err := c.k8sClient.CoreV1().Pods(c.namespace).Get(context.TODO(), c.opts.Pod, metav1.GetOptions{})
if err != nil {
return nil, cobra.ShellCompDirectiveError
}
var ctrs []string
for _, ctr := range res.Spec.Containers {
if strings.HasPrefix(ctr.Name, toComplete) {
ctrs = append(ctrs, ctr.Name)
}
}
for _, ctr := range res.Spec.EphemeralContainers {
if strings.HasPrefix(ctr.Name, toComplete) {
ctrs = append(ctrs, ctr.Name)
}
}

return ctrs, cobra.ShellCompDirectiveNoFileComp
}
73 changes: 67 additions & 6 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"os"
"strings"

"github.com/moby/term"
"github.com/spf13/cobra"
"k8s.io/klog/v2"
)
Expand All @@ -28,12 +29,20 @@ var (
)

var rootCmd = &cobra.Command{
Use: "execws <pod name> [options] -- <cmd>",
Short: "kubectl exec over WebSockets",
Long: `A replacement for "kubectl exec" that works over WebSocket connections.`,
Args: cobra.MinimumNArgs(1),
SilenceUsage: true,
SilenceErrors: true,
Use: "kubectl-execws <pod name> [options] -- <cmd>",
DisableFlagsInUseLine: true,
Short: "kubectl exec over WebSockets",
Long: `A replacement for "kubectl exec" that works over WebSocket connections.`,
Args: cobra.MinimumNArgs(1),
Version: releaseVersion,
SilenceUsage: true,
SilenceErrors: true,
/*CompletionOptions: cobra.CompletionOptions{
DisableDefaultCmd: false,
HiddenDefaultCmd: true,
DisableNoDescFlag: true,
DisableDescriptions: false,
},*/
RunE: func(cmd *cobra.Command, args []string) error {
var object, pod string
var command []string
Expand Down Expand Up @@ -111,8 +120,44 @@ var rootCmd = &cobra.Command{
return s.doExec(req)

},
ValidArgsFunction: MainValidArgs,
}

// add our own explicit completion helper
var completionCmd = &cobra.Command{
Use: "completion [bash|zsh|fish|powershell]",
DisableFlagsInUseLine: true,
Short: "Generate completion script",
Long: fmt.Sprintf(`Generate the autocompletion script for %[1]s for the specified shell.`, rootCmd.Root().Name()),
ValidArgs: []string{"bash", "zsh", "fish", "powershell"},
Args: cobra.MatchAll(cobra.ExactArgs(1), cobra.OnlyValidArgs),
Hidden: true,
Run: func(cmd *cobra.Command, args []string) {
_, stdOut, _ := term.StdStreams()
switch args[0] {
case "bash":
cmd.Root().GenBashCompletionV2(stdOut, true)
case "zsh":
cmd.Root().GenZshCompletion(stdOut)
case "fish":
cmd.Root().GenFishCompletion(stdOut, true)
case "powershell":
cmd.Root().GenPowerShellCompletionWithDesc(stdOut)
}
},
}

/*var versionCmd = &cobra.Command{
Use: "version",
Short: "Print program version",
DisableFlagsInUseLine: true,
Hidden: true,
Args: cobra.ExactArgs(0),
Run: func(cmd *cobra.Command, args []string) {
fmt.Printf(releaseVersion)
},
}*/

func Execute() {
klog.InitFlags(nil)

Expand All @@ -123,6 +168,16 @@ func Execute() {
os.Exit(0)
}

// shortcut to the hidden subcomand used for completion
func Complete() {
rootCmd.SetArgs(append([]string{cobra.ShellCompRequestCmd}, os.Args[1:]...))
err := rootCmd.Execute()
if err != nil {
os.Exit(1)
}
os.Exit(0)
}

func init() {
rootCmd.PersistentFlags().StringVar(&kconfig, "kubeconfig", "", "kubeconfig file (default is $HOME/.kube/config)")
rootCmd.PersistentFlags().StringVarP(&namespace, "namespace", "n", "", "Set namespace")
Expand All @@ -135,4 +190,10 @@ func init() {
rootCmd.Flags().BoolVar(&noSanityCheck, "no-sanity-check", false, "Don't make preflight request to ensure pod exists")
rootCmd.Flags().BoolVar(&directExec, "node-direct-exec", false, "Partially bypass the API server, by using the kubelet API")
rootCmd.Flags().StringVar(&directExecNodeIp, "node-direct-exec-ip", "", "Node IP to use with direct-exec feature")

rootCmd.AddCommand(completionCmd)
//rootCmd.AddCommand(versionCmd)
rootCmd.RegisterFlagCompletionFunc("namespace", NamespaceValidArgs)
rootCmd.RegisterFlagCompletionFunc("container", ContainerValidArgs)
rootCmd.SetHelpCommand(&cobra.Command{Hidden: true})
}
15 changes: 13 additions & 2 deletions main.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,18 @@
package main

import "github.com/jpts/kubectl-execws/cmd"
import (
"os"
"path/filepath"

"github.com/jpts/kubectl-execws/cmd"
)

func main() {
cmd.Execute()
name := filepath.Base(os.Args[0])
switch name {
case "kubectl_complete-execws":
cmd.Complete()
default:
cmd.Execute()
}
}

0 comments on commit de59df6

Please sign in to comment.