Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add interface #80

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .github/workflows/lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,5 @@ jobs:
- name: golangci-lint
uses: golangci/golangci-lint-action@v3
with:
version: v1.54
version: v1.54
args: --timeout=10m
16 changes: 15 additions & 1 deletion pkg/cmd/iexec.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import (
"fmt"
"k8s.io/client-go/kubernetes"

"github.com/pkg/errors"

Expand Down Expand Up @@ -170,8 +171,21 @@
RemoteCmd: o.remoteCmd,
}

r := iexec.NewIexec(o.clientCfg, config)
// 1) Create a real Kubernetes clientset from o.clientCfg.
clientset, err := kubernetes.NewForConfig(o.clientCfg)
if err != nil {
log.Fatalf("Failed to create kubernetes client: %v", err)

Check warning on line 177 in pkg/cmd/iexec.go

View check run for this annotation

Codecov / codecov/patch

pkg/cmd/iexec.go#L175-L177

Added lines #L175 - L177 were not covered by tests
}

// 2) Create your real implementations for the interfaces.
ui := iexec.NewPromptUITerminal() // TerminalUI
k8s := iexec.NewRealK8sClient(clientset) // K8sClient
execRunner := iexec.NewSpdyExecRunner() // ExecRunner

Check warning on line 183 in pkg/cmd/iexec.go

View check run for this annotation

Codecov / codecov/patch

pkg/cmd/iexec.go#L181-L183

Added lines #L181 - L183 were not covered by tests

// 3) Pass them to NewIexec along with your config.
r := iexec.NewIexec(o.clientCfg, config, ui, k8s, execRunner)

Check warning on line 186 in pkg/cmd/iexec.go

View check run for this annotation

Codecov / codecov/patch

pkg/cmd/iexec.go#L186

Added line #L186 was not covered by tests

// 4) Run!
if err := r.Do(); err != nil {
log.Fatal(err)
}
Expand Down
95 changes: 95 additions & 0 deletions pkg/iexec/exec_runner_spdy.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
package iexec

import (
"context"
"fmt"
"os"

"github.com/pkg/errors"
log "github.com/sirupsen/logrus"
"golang.org/x/term"
corev1 "k8s.io/api/core/v1"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/kubernetes/scheme"
"k8s.io/client-go/rest"
"k8s.io/client-go/tools/remotecommand"
)

type SpdyExecRunner struct{}

func NewSpdyExecRunner() *SpdyExecRunner {
return &SpdyExecRunner{}

Check warning on line 21 in pkg/iexec/exec_runner_spdy.go

View check run for this annotation

Codecov / codecov/patch

pkg/iexec/exec_runner_spdy.go#L20-L21

Added lines #L20 - L21 were not covered by tests
}

// sizeQueue is a buffered channel for TerminalSize
type sizeQueue chan remotecommand.TerminalSize

// Next implements the TerminalSizeQueue interface
func (s sizeQueue) Next() *remotecommand.TerminalSize {
size, ok := <-s
if !ok {
return nil

Check warning on line 31 in pkg/iexec/exec_runner_spdy.go

View check run for this annotation

Codecov / codecov/patch

pkg/iexec/exec_runner_spdy.go#L28-L31

Added lines #L28 - L31 were not covered by tests
}
return &size

Check warning on line 33 in pkg/iexec/exec_runner_spdy.go

View check run for this annotation

Codecov / codecov/patch

pkg/iexec/exec_runner_spdy.go#L33

Added line #L33 was not covered by tests
}

func (r *SpdyExecRunner) RunExec(restCfg *rest.Config, pod corev1.Pod, container corev1.Container, cmd []string) error {
client, err := kubernetes.NewForConfig(restCfg)
if err != nil {
return errors.Wrap(err, "unable to create kubernetes client")

Check warning on line 39 in pkg/iexec/exec_runner_spdy.go

View check run for this annotation

Codecov / codecov/patch

pkg/iexec/exec_runner_spdy.go#L36-L39

Added lines #L36 - L39 were not covered by tests
}

req := client.CoreV1().RESTClient().
Post().
Namespace(pod.Namespace).
Resource("pods").
Name(pod.Name).
SubResource("exec").
VersionedParams(&corev1.PodExecOptions{
Container: container.Name,
Command: cmd,
Stdin: true,
Stdout: true,
Stderr: true,
TTY: true,
}, scheme.ParameterCodec)

Check warning on line 55 in pkg/iexec/exec_runner_spdy.go

View check run for this annotation

Codecov / codecov/patch

pkg/iexec/exec_runner_spdy.go#L42-L55

Added lines #L42 - L55 were not covered by tests

log.WithField("URL", req.URL()).Debug("SPDY Exec request")

Check warning on line 57 in pkg/iexec/exec_runner_spdy.go

View check run for this annotation

Codecov / codecov/patch

pkg/iexec/exec_runner_spdy.go#L57

Added line #L57 was not covered by tests

executor, err := remotecommand.NewSPDYExecutor(restCfg, "POST", req.URL())
if err != nil {
return errors.Wrap(err, "unable to create SPDY executor")

Check warning on line 61 in pkg/iexec/exec_runner_spdy.go

View check run for this annotation

Codecov / codecov/patch

pkg/iexec/exec_runner_spdy.go#L59-L61

Added lines #L59 - L61 were not covered by tests
}

// Put terminal into raw mode
fd := int(os.Stdin.Fd())
oldState, err := term.MakeRaw(fd)
if err != nil {
return errors.Wrap(err, "unable to init terminal raw mode")

Check warning on line 68 in pkg/iexec/exec_runner_spdy.go

View check run for this annotation

Codecov / codecov/patch

pkg/iexec/exec_runner_spdy.go#L65-L68

Added lines #L65 - L68 were not covered by tests
}
defer func() {
_ = term.Restore(fd, oldState)
}()

Check warning on line 72 in pkg/iexec/exec_runner_spdy.go

View check run for this annotation

Codecov / codecov/patch

pkg/iexec/exec_runner_spdy.go#L70-L72

Added lines #L70 - L72 were not covered by tests

// Get terminal size
w, h, err := term.GetSize(fd)
if err != nil {
log.Errorf("Error getting terminal size: %v", err)

Check warning on line 77 in pkg/iexec/exec_runner_spdy.go

View check run for this annotation

Codecov / codecov/patch

pkg/iexec/exec_runner_spdy.go#L75-L77

Added lines #L75 - L77 were not covered by tests
}
sz := make(sizeQueue, 1)
sz <- remotecommand.TerminalSize{Width: uint16(w), Height: uint16(h)}

Check warning on line 80 in pkg/iexec/exec_runner_spdy.go

View check run for this annotation

Codecov / codecov/patch

pkg/iexec/exec_runner_spdy.go#L79-L80

Added lines #L79 - L80 were not covered by tests

// Stream
if err := executor.StreamWithContext(context.Background(), remotecommand.StreamOptions{
Stdin: os.Stdin,
Stdout: os.Stdout,
Stderr: os.Stderr,
Tty: true,
TerminalSizeQueue: sz,
}); err != nil {
return errors.Wrap(err, "SPDY Stream failed")

Check warning on line 90 in pkg/iexec/exec_runner_spdy.go

View check run for this annotation

Codecov / codecov/patch

pkg/iexec/exec_runner_spdy.go#L83-L90

Added lines #L83 - L90 were not covered by tests
}

fmt.Println()
return nil

Check warning on line 94 in pkg/iexec/exec_runner_spdy.go

View check run for this annotation

Codecov / codecov/patch

pkg/iexec/exec_runner_spdy.go#L93-L94

Added lines #L93 - L94 were not covered by tests
}
198 changes: 39 additions & 159 deletions pkg/iexec/iexec.go
Original file line number Diff line number Diff line change
@@ -1,27 +1,12 @@
package iexec

import (
"context"
"fmt"
"os"

"github.com/pkg/errors"

"github.com/manifoldco/promptui"
log "github.com/sirupsen/logrus"
"golang.org/x/term"
corev1 "k8s.io/api/core/v1"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/kubernetes/scheme"

// auth needed for proxy.
_ "k8s.io/client-go/plugin/pkg/client/auth"
"k8s.io/client-go/rest"
"k8s.io/client-go/tools/remotecommand"
)

type sizeQueue chan remotecommand.TerminalSize

// Iexecer remains for backward compatibility if needed
type Iexecer interface {
Do() error
}
Expand All @@ -36,12 +21,18 @@
RemoteCmd []string
}

// Iexec is our main struct, now with interfaces for UI, K8s fetching, and exec
type Iexec struct {
restConfig *rest.Config
config *Config

ui TerminalUI
k8sClient K8sClient
exec ExecRunner
}

func NewIexec(restConfig *rest.Config, config *Config) *Iexec {
// NewIexec constructs the Iexec with your chosen implementations
func NewIexec(restConfig *rest.Config, config *Config, ui TerminalUI, k8s K8sClient, exec ExecRunner) *Iexec {
log.WithFields(log.Fields{
"containerFilter": config.ContainerFilter,
"remote command": config.RemoteCmd,
Expand All @@ -52,90 +43,52 @@
"LabelSelector": config.LabelSelector,
}).Debug("iexec config values...")

return &Iexec{restConfig: restConfig, config: config}
}

func selectPod(pods []corev1.Pod, config Config) (corev1.Pod, error) {
if len(pods) == 1 {
return pods[0], nil
}

templates := podTemplate

if config.Naked {
templates = podTemplateNaked
}

podsPrompt := promptui.Select{
Label: "Select Pod",
Items: pods,
Templates: templates,
IsVimMode: config.VimMode,
}

i, _, err := podsPrompt.Run()
if err != nil {
return pods[i], errors.Wrap(err, "unable to run prompt")
}

return pods[i], nil
}

func containerPrompt(containers []corev1.Container, config Config) (corev1.Container, error) {
if len(containers) == 1 {
return containers[0], nil
}

templates := containerTemplates

if config.Naked {
templates = containerTemplatesNaked
}

containersPrompt := promptui.Select{
Label: "Select Container",
Items: containers,
Templates: templates,
IsVimMode: config.VimMode,
return &Iexec{
restConfig: restConfig,
config: config,
ui: ui,
k8sClient: k8s,
exec: exec,
}

i, _, err := containersPrompt.Run()
if err != nil {
return containers[i], errors.Wrap(err, "unable to get prompt")
}

return containers[i], nil
}

// Do is our main plugin flow, now purely orchestrating the calls
func (r *Iexec) Do() error {
client, err := kubernetes.NewForConfig(r.restConfig)
// 1. Fetch pods
podsList, err := r.k8sClient.FetchAllPods(r.config.Namespace, r.config.LabelSelector)
if err != nil {
return errors.Wrap(err, "unable to get kubernetes for config")
return errors.Wrap(err, "fetchAllPods failed")
}

pods, err := getAllPods(client, r.config.Namespace, r.config.LabelSelector)
// 2. Match/Filter pods
filtered, err := r.k8sClient.MatchPods(podsList, *r.config)
if err != nil {
return err
return errors.Wrap(err, "matchPods failed")

Check warning on line 66 in pkg/iexec/iexec.go

View check run for this annotation

Codecov / codecov/patch

pkg/iexec/iexec.go#L66

Added line #L66 was not covered by tests
}

filteredPods, err := r.matchPods(pods)
if err != nil {
return err
pods := filtered.Items
if len(pods) == 0 {
return errors.New("no matching pods found")

Check warning on line 70 in pkg/iexec/iexec.go

View check run for this annotation

Codecov / codecov/patch

pkg/iexec/iexec.go#L70

Added line #L70 was not covered by tests
}

pod, err := selectPod(filteredPods.Items, *r.config)
// 3. Prompt user to select a pod
pod, err := r.ui.SelectPod(pods, *r.config)
if err != nil {
return err
return errors.Wrap(err, "SelectPod failed")

Check warning on line 76 in pkg/iexec/iexec.go

View check run for this annotation

Codecov / codecov/patch

pkg/iexec/iexec.go#L76

Added line #L76 was not covered by tests
}

containers, err := matchContainers(pod, *r.config)
// 4. Match containers
containers, err := r.k8sClient.MatchContainers(pod, *r.config)
if err != nil {
return err
return errors.Wrap(err, "matchContainers failed")

Check warning on line 82 in pkg/iexec/iexec.go

View check run for this annotation

Codecov / codecov/patch

pkg/iexec/iexec.go#L82

Added line #L82 was not covered by tests
}
if len(containers) == 0 {
return errors.New("no matching containers found")

Check warning on line 85 in pkg/iexec/iexec.go

View check run for this annotation

Codecov / codecov/patch

pkg/iexec/iexec.go#L85

Added line #L85 was not covered by tests
}

container, err := containerPrompt(containers, *r.config)
// 5. Prompt user to select a container
container, err := r.ui.SelectContainer(containers, *r.config)
if err != nil {
return err
return errors.Wrap(err, "SelectContainer failed")

Check warning on line 91 in pkg/iexec/iexec.go

View check run for this annotation

Codecov / codecov/patch

pkg/iexec/iexec.go#L91

Added line #L91 was not covered by tests
}

log.WithFields(log.Fields{
Expand All @@ -144,83 +97,10 @@
"namespace": r.config.Namespace,
}).Info("Exec to pod...")

err = exec(r.restConfig, pod, container, r.config.RemoteCmd)
if err != nil {
return err
}
return nil
}

func exec(restCfg *rest.Config, pod corev1.Pod, container corev1.Container, cmd []string) error {
client, err := kubernetes.NewForConfig(restCfg)
if err != nil {
return errors.Wrap(err, "unable to get kubernetes client config")
// 6. Execute remote command
if err := r.exec.RunExec(r.restConfig, pod, container, r.config.RemoteCmd); err != nil {
return errors.Wrap(err, "RunExec failed")

Check warning on line 102 in pkg/iexec/iexec.go

View check run for this annotation

Codecov / codecov/patch

pkg/iexec/iexec.go#L102

Added line #L102 was not covered by tests
}

req := client.CoreV1().RESTClient().
Post().
Namespace(pod.GetNamespace()).
Resource("pods").
Name(pod.GetName()).
SubResource("exec").
VersionedParams(&corev1.PodExecOptions{
Container: container.Name,
Command: cmd,
Stdin: true,
Stdout: true,
Stderr: true,
TTY: true,
}, scheme.ParameterCodec)

log.WithFields(log.Fields{
"URL": req.URL(),
}).Debug("Request")

exec, err := remotecommand.NewSPDYExecutor(restCfg, "POST", req.URL())
if err != nil {
return errors.Wrap(err, "unable to execute remote command")
}

fd := int(os.Stdin.Fd())

// Put the terminal into raw mode to prevent it echoing characters twice.
oldState, err := term.MakeRaw(fd)
if err != nil {
return errors.Wrap(err, "unable to init terminal")
}

termWidth, termHeight, _ := term.GetSize(fd)
termSize := remotecommand.TerminalSize{Width: uint16(termWidth), Height: uint16(termHeight)}
s := make(sizeQueue, 1)
s <- termSize

defer func() {
err := term.Restore(fd, oldState)
if err != nil {
log.Error(err)
}
}()

// Connect this process' std{in,out,err} to the remote shell process.
err = exec.StreamWithContext(context.Background(), remotecommand.StreamOptions{
Stdin: os.Stdin,
Stdout: os.Stdout,
Stderr: os.Stderr,
Tty: true,
TerminalSizeQueue: s,
})
if err != nil {
return errors.Wrap(err, "unable to stream shell process")
}

fmt.Println()
return nil
}

func (s sizeQueue) Next() *remotecommand.TerminalSize {
size, ok := <-s
if !ok {
return nil
}
return &size
}
Loading
Loading