269 lines
6.4 KiB
Go
269 lines
6.4 KiB
Go
package view
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/derailed/k9s/internal/client"
|
|
"github.com/derailed/k9s/internal/config"
|
|
"github.com/derailed/k9s/internal/dao"
|
|
"github.com/derailed/k9s/internal/perf"
|
|
"github.com/derailed/k9s/internal/ui"
|
|
"github.com/derailed/k9s/internal/ui/dialog"
|
|
"github.com/gdamore/tcell"
|
|
"github.com/rs/zerolog/log"
|
|
v1 "k8s.io/api/core/v1"
|
|
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
|
"k8s.io/apimachinery/pkg/labels"
|
|
"k8s.io/apimachinery/pkg/runtime"
|
|
)
|
|
|
|
// Service represents a service viewer.
|
|
type Service struct {
|
|
ResourceViewer
|
|
|
|
bench *perf.Benchmark
|
|
}
|
|
|
|
// NewService returns a new viewer.
|
|
func NewService(gvr client.GVR) ResourceViewer {
|
|
s := Service{
|
|
ResourceViewer: NewLogsExtender(NewBrowser(gvr), nil),
|
|
}
|
|
s.SetBindKeysFn(s.bindKeys)
|
|
s.GetTable().SetEnterFn(s.showPods)
|
|
|
|
return &s
|
|
}
|
|
|
|
// Protocol...
|
|
|
|
func (s *Service) bindKeys(aa ui.KeyActions) {
|
|
aa.Add(ui.KeyActions{
|
|
ui.KeyShiftF: ui.NewKeyAction("Port-Forward", s.portFwdCmd, true),
|
|
tcell.KeyCtrlB: ui.NewKeyAction("Bench Run/Stop", s.toggleBenchCmd, true),
|
|
ui.KeyShiftT: ui.NewKeyAction("Sort Type", s.GetTable().SortColCmd(1, true), false),
|
|
})
|
|
}
|
|
|
|
func podFromSelector(f dao.Factory, ns string, sel map[string]string) (string, error) {
|
|
log.Debug().Msgf("Looking for pods %q:%v -- %v", ns, sel, labels.Set(sel).AsSelector())
|
|
oo, err := f.List("v1/pods", ns, true, labels.Set(sel).AsSelector())
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
if len(oo) == 0 {
|
|
return "", fmt.Errorf("no matching pods for %v", sel)
|
|
}
|
|
|
|
var pod v1.Pod
|
|
err = runtime.DefaultUnstructuredConverter.FromUnstructured(oo[0].(*unstructured.Unstructured).Object, &pod)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
return client.FQN(pod.Namespace, pod.Name), nil
|
|
}
|
|
|
|
func (s *Service) portFwdCmd(evt *tcell.EventKey) *tcell.EventKey {
|
|
path := s.GetTable().GetSelectedItem()
|
|
if path == "" {
|
|
return evt
|
|
}
|
|
|
|
svc, err := fetchService(s.App().factory, s.GVR(), path)
|
|
if err != nil {
|
|
s.App().Flash().Err(err)
|
|
return nil
|
|
}
|
|
|
|
ns, _ := client.Namespaced(path)
|
|
pod, err := podFromSelector(s.App().factory, ns, svc.Spec.Selector)
|
|
if err != nil {
|
|
s.App().Flash().Err(err)
|
|
return nil
|
|
}
|
|
|
|
pp, err := fetchPodPorts(s.App().factory, pod)
|
|
if err != nil {
|
|
s.App().Flash().Err(err)
|
|
return nil
|
|
}
|
|
ports := make([]string, 0, len(pp))
|
|
for _, p := range pp {
|
|
if p.Protocol == v1.ProtocolTCP {
|
|
port := fmt.Sprintf("%s:%d", p.Name, p.ContainerPort)
|
|
if p.Name == "" {
|
|
port = fmt.Sprintf("%d", p.ContainerPort)
|
|
}
|
|
ports = append(ports, port)
|
|
}
|
|
}
|
|
|
|
if len(ports) == 0 {
|
|
s.App().Flash().Err(fmt.Errorf("no tcp ports found on %s", path))
|
|
return nil
|
|
}
|
|
|
|
dialog.ShowPortForwards(s.App().Content.Pages, s.App().Styles, pod, ports, s.portForward)
|
|
|
|
return nil
|
|
}
|
|
|
|
func (s *Service) portForward(path, address, lport, cport string) {
|
|
pf := dao.NewPortForwarder(s.App().Conn())
|
|
ports := []string{lport + ":" + cport}
|
|
fw, err := pf.Start(path, "", address, ports)
|
|
if err != nil {
|
|
s.App().Flash().Err(err)
|
|
return
|
|
}
|
|
|
|
log.Debug().Msgf(">>> Starting port forward %q %v", path, ports)
|
|
go runForward(s.App(), pf, fw)
|
|
}
|
|
|
|
func (s *Service) showPods(app *App, _ ui.Tabular, gvr, path string) {
|
|
svc, err := fetchService(app.factory, gvr, path)
|
|
if err != nil {
|
|
app.Flash().Err(err)
|
|
return
|
|
}
|
|
|
|
showPodsWithLabels(app, path, svc.Spec.Selector)
|
|
}
|
|
|
|
func (s *Service) checkSvc(row int) error {
|
|
svcType := trimCellRelative(s.GetTable(), row, 1)
|
|
if svcType != "NodePort" && svcType != "LoadBalancer" {
|
|
return errors.New("You must select a reachable service")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *Service) getExternalPort(row int) (string, error) {
|
|
ports := trimCellRelative(s.GetTable(), row, 5)
|
|
|
|
pp := strings.Split(ports, " ")
|
|
if len(pp) == 0 {
|
|
return "", errors.New("No ports found")
|
|
}
|
|
|
|
// Grap the first port pair for now...
|
|
tokens := strings.Split(pp[0], "►")
|
|
if len(tokens) < 2 {
|
|
return "", errors.New("No ports pair found")
|
|
}
|
|
|
|
return tokens[1], nil
|
|
}
|
|
|
|
func (s *Service) reloadBenchCfg() error {
|
|
path := ui.BenchConfig(s.App().Config.K9s.CurrentCluster)
|
|
return s.App().Bench.Reload(path)
|
|
}
|
|
|
|
func (s *Service) toggleBenchCmd(evt *tcell.EventKey) *tcell.EventKey {
|
|
if s.bench != nil {
|
|
log.Debug().Msg(">>> Benchmark canceled!!")
|
|
s.App().Status(ui.FlashErr, "Benchmark Canceled!")
|
|
s.bench.Cancel()
|
|
s.App().ClearStatus(true)
|
|
return nil
|
|
}
|
|
|
|
sel := s.GetTable().GetSelectedItem()
|
|
if sel == "" || s.bench != nil {
|
|
return evt
|
|
}
|
|
|
|
if err := s.reloadBenchCfg(); err != nil {
|
|
s.App().Flash().Err(err)
|
|
return nil
|
|
}
|
|
|
|
cfg, ok := s.App().Bench.Benchmarks.Services[sel]
|
|
if !ok {
|
|
s.App().Flash().Errf("No bench config found for service %s", sel)
|
|
return nil
|
|
}
|
|
cfg.Name = sel
|
|
log.Debug().Msgf("Benchmark config %#v", cfg)
|
|
|
|
row := s.GetTable().GetSelectedRowIndex()
|
|
if err := s.checkSvc(row); err != nil {
|
|
s.App().Flash().Err(err)
|
|
return nil
|
|
}
|
|
port, err := s.getExternalPort(row)
|
|
if err != nil {
|
|
s.App().Flash().Err(err)
|
|
return nil
|
|
}
|
|
if err := s.runBenchmark(port, cfg); err != nil {
|
|
s.App().Flash().Errf("Benchmark failed %v", err)
|
|
s.App().ClearStatus(false)
|
|
s.bench = nil
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// BOZO!! Refactor used by forwards
|
|
func (s *Service) runBenchmark(port string, cfg config.BenchConfig) error {
|
|
if cfg.HTTP.Host == "" {
|
|
return fmt.Errorf("Invalid benchmark host %q", cfg.HTTP.Host)
|
|
}
|
|
|
|
var err error
|
|
base := "http://" + cfg.HTTP.Host + ":" + port + cfg.HTTP.Path
|
|
if s.bench, err = perf.NewBenchmark(base, s.App().version, cfg); err != nil {
|
|
return err
|
|
}
|
|
|
|
s.App().Status(ui.FlashWarn, "Benchmark in progress...")
|
|
log.Debug().Msg("Bench starting...")
|
|
go s.bench.Run(s.App().Config.K9s.CurrentCluster, s.benchDone)
|
|
|
|
return nil
|
|
}
|
|
|
|
func (s *Service) benchDone() {
|
|
log.Debug().Msg("Bench Completed!")
|
|
s.App().QueueUpdate(func() {
|
|
if s.bench.Canceled() {
|
|
s.App().Status(ui.FlashInfo, "Benchmark canceled")
|
|
} else {
|
|
s.App().Status(ui.FlashInfo, "Benchmark Completed!")
|
|
s.bench.Cancel()
|
|
}
|
|
s.bench = nil
|
|
go benchTimedOut(s.App())
|
|
})
|
|
}
|
|
|
|
// ----------------------------------------------------------------------------
|
|
// Helpers...
|
|
|
|
func fetchService(f dao.Factory, gvr, path string) (*v1.Service, error) {
|
|
o, err := f.Get(gvr, path, true, labels.Everything())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var svc v1.Service
|
|
err = runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &svc)
|
|
|
|
return &svc, err
|
|
}
|
|
|
|
func benchTimedOut(app *App) {
|
|
<-time.After(2 * time.Second)
|
|
app.QueueUpdate(func() {
|
|
app.ClearStatus(true)
|
|
})
|
|
}
|