313 lines
7.3 KiB
Go
313 lines
7.3 KiB
Go
package view
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"os/exec"
|
|
"os/signal"
|
|
"strings"
|
|
"syscall"
|
|
"time"
|
|
|
|
"github.com/derailed/k9s/internal/client"
|
|
"github.com/derailed/k9s/internal/config"
|
|
"github.com/rs/zerolog/log"
|
|
v1 "k8s.io/api/core/v1"
|
|
kerrors "k8s.io/apimachinery/pkg/api/errors"
|
|
"k8s.io/apimachinery/pkg/api/resource"
|
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
|
"k8s.io/apimachinery/pkg/labels"
|
|
"k8s.io/apimachinery/pkg/runtime"
|
|
)
|
|
|
|
const (
|
|
shellCheck = `command -v bash >/dev/null && exec bash || exec sh`
|
|
bannerFmt = "<<K9s-Shell>> Pod: %s | Container: %s \n"
|
|
)
|
|
|
|
type shellOpts struct {
|
|
clear, background bool
|
|
binary string
|
|
banner string
|
|
args []string
|
|
}
|
|
|
|
func runK(a *App, opts shellOpts) bool {
|
|
bin, err := exec.LookPath("kubectl")
|
|
if err != nil {
|
|
log.Error().Err(err).Msgf("kubectl command is not in your path")
|
|
return false
|
|
}
|
|
var args []string
|
|
if u, err := a.Conn().Config().ImpersonateUser(); err == nil {
|
|
args = append(args, "--as", u)
|
|
}
|
|
if g, err := a.Conn().Config().ImpersonateGroups(); err == nil {
|
|
args = append(args, "--as-group", g)
|
|
}
|
|
args = append(args, "--context", a.Config.K9s.CurrentContext)
|
|
if cfg := a.Conn().Config().Flags().KubeConfig; cfg != nil && *cfg != "" {
|
|
args = append(args, "--kubeconfig", *cfg)
|
|
}
|
|
if len(args) > 0 {
|
|
opts.args = append(args, opts.args...)
|
|
}
|
|
opts.binary, opts.background = bin, false
|
|
|
|
return run(a, opts)
|
|
}
|
|
|
|
func run(a *App, opts shellOpts) bool {
|
|
a.Halt()
|
|
defer a.Resume()
|
|
|
|
return a.Suspend(func() {
|
|
if err := execute(opts); err != nil {
|
|
a.Flash().Errf("Command exited: %v", err)
|
|
}
|
|
})
|
|
}
|
|
|
|
func edit(a *App, opts shellOpts) bool {
|
|
bin, err := exec.LookPath(os.Getenv("K9S_EDITOR"))
|
|
if err != nil {
|
|
bin, err = exec.LookPath(os.Getenv("EDITOR"))
|
|
if err != nil {
|
|
log.Error().Err(err).Msgf("K9S_EDITOR|EDITOR not set")
|
|
return false
|
|
}
|
|
}
|
|
opts.binary, opts.background = bin, false
|
|
|
|
return run(a, opts)
|
|
}
|
|
|
|
func execute(opts shellOpts) error {
|
|
if opts.clear {
|
|
clearScreen()
|
|
}
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
defer func() {
|
|
cancel()
|
|
clearScreen()
|
|
}()
|
|
|
|
sigChan := make(chan os.Signal, 1)
|
|
signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM)
|
|
go func() {
|
|
<-sigChan
|
|
log.Debug().Msg("Command canceled with signal!")
|
|
cancel()
|
|
}()
|
|
|
|
log.Debug().Msgf("Running command> %s %s", opts.binary, strings.Join(opts.args, " "))
|
|
cmd := exec.Command(opts.binary, opts.args...)
|
|
|
|
var err error
|
|
if opts.background {
|
|
err = cmd.Start()
|
|
} else {
|
|
cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr
|
|
_, _ = cmd.Stdout.Write([]byte(opts.banner))
|
|
err = cmd.Run()
|
|
}
|
|
|
|
select {
|
|
case <-ctx.Done():
|
|
return errors.New("canceled by operator")
|
|
default:
|
|
return err
|
|
}
|
|
}
|
|
|
|
func runKu(a *App, opts shellOpts) (string, error) {
|
|
bin, err := exec.LookPath("kubectl")
|
|
if err != nil {
|
|
log.Error().Err(err).Msgf("kubectl command is not in your path")
|
|
return "", err
|
|
}
|
|
var args []string
|
|
if u, err := a.Conn().Config().ImpersonateUser(); err == nil {
|
|
args = append(args, "--as", u)
|
|
}
|
|
if g, err := a.Conn().Config().ImpersonateGroups(); err == nil {
|
|
args = append(args, "--as-group", g)
|
|
}
|
|
args = append(args, "--context", a.Config.K9s.CurrentContext)
|
|
if cfg := a.Conn().Config().Flags().KubeConfig; cfg != nil && *cfg != "" {
|
|
args = append(args, "--kubeconfig", *cfg)
|
|
}
|
|
if len(args) > 0 {
|
|
opts.args = append(args, opts.args...)
|
|
}
|
|
opts.binary, opts.background = bin, false
|
|
|
|
return oneShoot(opts)
|
|
}
|
|
|
|
func oneShoot(opts shellOpts) (string, error) {
|
|
if opts.clear {
|
|
clearScreen()
|
|
}
|
|
|
|
log.Debug().Msgf("Running command> %s %s", opts.binary, strings.Join(opts.args, " "))
|
|
cmd := exec.Command(opts.binary, opts.args...)
|
|
|
|
var err error
|
|
buff := bytes.NewBufferString("")
|
|
cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, buff, buff
|
|
_, _ = cmd.Stdout.Write([]byte(opts.banner))
|
|
err = cmd.Run()
|
|
|
|
return strings.Trim(buff.String(), "\n"), err
|
|
}
|
|
|
|
func clearScreen() {
|
|
fmt.Print("\033[H\033[2J")
|
|
}
|
|
|
|
const (
|
|
k9sShell = "k9s-shell"
|
|
k9sShellRetryCount = 10
|
|
k9sShellRetryDelay = 500 * time.Millisecond
|
|
)
|
|
|
|
func ssh(a *App, node string) error {
|
|
if err := nukeK9sShell(a); err != nil {
|
|
return err
|
|
}
|
|
defer func() {
|
|
if err := nukeK9sShell(a); err != nil {
|
|
log.Error().Err(err).Msgf("nuking k9s shell pod")
|
|
}
|
|
}()
|
|
if err := launchShellPod(a, node); err != nil {
|
|
return err
|
|
}
|
|
ns := a.Config.K9s.ActiveCluster().ShellPod.Namespace
|
|
shellIn(a, client.FQN(ns, k9sShellPodName()), k9sShell)
|
|
|
|
return nil
|
|
}
|
|
|
|
func nukeK9sShell(a *App) error {
|
|
cl := a.Config.K9s.CurrentCluster
|
|
if !a.Config.K9s.Clusters[cl].FeatureGates.NodeShell {
|
|
return nil
|
|
}
|
|
|
|
ns := a.Config.K9s.ActiveCluster().ShellPod.Namespace
|
|
ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond)
|
|
defer cancel()
|
|
|
|
dial, err := a.Conn().Dial()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
err = dial.CoreV1().Pods(ns).Delete(ctx, k9sShellPodName(), metav1.DeleteOptions{})
|
|
if kerrors.IsNotFound(err) {
|
|
return nil
|
|
}
|
|
|
|
return err
|
|
}
|
|
|
|
func launchShellPod(a *App, node string) error {
|
|
ns := a.Config.K9s.ActiveCluster().ShellPod.Namespace
|
|
spec := k9sShellPod(node, a.Config.K9s.ActiveCluster().ShellPod)
|
|
ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond)
|
|
defer cancel()
|
|
|
|
dial, err := a.Conn().Dial()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
conn := dial.CoreV1().Pods(ns)
|
|
if _, err := conn.Create(ctx, &spec, metav1.CreateOptions{}); err != nil {
|
|
return err
|
|
}
|
|
|
|
for i := 0; i < k9sShellRetryCount; i++ {
|
|
o, err := a.factory.Get("v1/pods", client.FQN(ns, k9sShellPodName()), true, labels.Everything())
|
|
if err != nil {
|
|
time.Sleep(k9sShellRetryDelay)
|
|
continue
|
|
}
|
|
var pod v1.Pod
|
|
if err := runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &pod); err != nil {
|
|
return err
|
|
}
|
|
if pod.Status.Phase == v1.PodRunning {
|
|
return nil
|
|
}
|
|
time.Sleep(k9sShellRetryDelay)
|
|
}
|
|
|
|
return fmt.Errorf("Unable to launch shell pod on node %s", node)
|
|
}
|
|
|
|
func k9sShellPodName() string {
|
|
return fmt.Sprintf("%s-%d", k9sShell, os.Getpid())
|
|
}
|
|
|
|
func k9sShellPod(node string, cfg *config.ShellPod) v1.Pod {
|
|
var grace int64
|
|
var priv bool = true
|
|
|
|
return v1.Pod{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: k9sShellPodName(),
|
|
Namespace: cfg.Namespace,
|
|
},
|
|
Spec: v1.PodSpec{
|
|
NodeName: node,
|
|
RestartPolicy: v1.RestartPolicyNever,
|
|
HostPID: true,
|
|
HostNetwork: true,
|
|
TerminationGracePeriodSeconds: &grace,
|
|
Volumes: []v1.Volume{
|
|
{
|
|
Name: "root-vol",
|
|
VolumeSource: v1.VolumeSource{
|
|
HostPath: &v1.HostPathVolumeSource{
|
|
Path: "/",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
Containers: []v1.Container{
|
|
{
|
|
Name: k9sShell,
|
|
Image: cfg.Image,
|
|
VolumeMounts: []v1.VolumeMount{
|
|
{
|
|
Name: "root-vol",
|
|
MountPath: "/host",
|
|
ReadOnly: true,
|
|
},
|
|
},
|
|
Resources: asResource(cfg.Limits),
|
|
Stdin: true,
|
|
SecurityContext: &v1.SecurityContext{
|
|
Privileged: &priv,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
func asResource(r config.Limits) v1.ResourceRequirements {
|
|
return v1.ResourceRequirements{
|
|
Limits: v1.ResourceList{
|
|
v1.ResourceCPU: resource.MustParse(r[v1.ResourceCPU]),
|
|
v1.ResourceMemory: resource.MustParse(r[v1.ResourceMemory]),
|
|
},
|
|
}
|
|
}
|