enable port-forwards on pods and services

mine
derailed 2020-02-13 12:31:30 -07:00
parent 7ad07b791d
commit f1ef8d216c
26 changed files with 745 additions and 30 deletions

8
go.mod
View File

@ -2,6 +2,8 @@ module github.com/derailed/k9s
go 1.13
replace github.com/derailed/tview => /Users/fernand/go_wk/derailed/src/github.com/derailed/tview
replace (
github.com/docker/docker => github.com/docker/docker v1.4.2-0.20181221150755-2cb26cfe9cbf
k8s.io/api => k8s.io/api v0.0.0-20190918155943-95b840bb6a1f
@ -30,6 +32,8 @@ replace (
require (
fyne.io/fyne v1.2.2 // indirect
github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 // indirect
github.com/alexellis/go-execute v0.0.0-20200124154445-8697e4e28c5e // indirect
github.com/alexellis/hmac v0.0.0-20180624211220-5c52ab81c0de // indirect
github.com/atotto/clipboard v0.1.2
github.com/derailed/tview v0.3.4
github.com/drone/envsubst v1.0.2 // indirect
@ -42,9 +46,9 @@ require (
github.com/gregjones/httpcache v0.0.0-20190212212710-3befbb6ad0cc // indirect
github.com/kylelemons/godebug v1.1.0 // indirect
github.com/mattn/go-runewidth v0.0.5
github.com/openfaas/faas v0.0.0-20200207215241-6afae214e3ec // indirect
github.com/openfaas/faas v0.0.0-20200207215241-6afae214e3ec
github.com/openfaas/faas-cli v0.0.0-20200124160744-30b7cec9634c
github.com/openfaas/faas-provider v0.15.0 // indirect
github.com/openfaas/faas-provider v0.15.0
github.com/petergtz/pegomock v2.6.0+incompatible
github.com/rakyll/hey v0.1.2
github.com/rs/zerolog v1.17.2

5
go.sum
View File

@ -67,6 +67,10 @@ github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuy
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/alexellis/go-execute v0.0.0-20200124154445-8697e4e28c5e h1:0cv4CUENL7e67/ZlNrvExWqa6oKH/9iv0KQn0/+hYaY=
github.com/alexellis/go-execute v0.0.0-20200124154445-8697e4e28c5e/go.mod h1:zfRbgnPVxXCSpiKrg1CE72hNUWInqxExiaz2D9ppTts=
github.com/alexellis/hmac v0.0.0-20180624211220-5c52ab81c0de h1:jiPEvtW8VT0KwJxRyjW2VAAvlssjj9SfecsQ3Vgv5tk=
github.com/alexellis/hmac v0.0.0-20180624211220-5c52ab81c0de/go.mod h1:uAbpy8G7sjNB4qYdY6ymf5OIQ+TLDPApBYiR0Vc3lhk=
github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
github.com/asaskevich/govalidator v0.0.0-20180720115003-f9ffefc3facf/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY=
@ -470,6 +474,7 @@ github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lN
github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI=
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/mohae/deepcopy v0.0.0-20170603005431-491d3605edfb/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8=
github.com/morikuni/aec v0.0.0-20170113033406-39771216ff4c h1:nXxl5PrvVm2L/wCy8dQu6DMTwH4oIuGN8GJDAlqDdVE=
github.com/morikuni/aec v0.0.0-20170113033406-39771216ff4c/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
github.com/mrunalp/fileutils v0.0.0-20160930181131-4ee1cc9a8058/go.mod h1:x8F1gnqOkIEiO4rqoeEEEqQbo7HjGMTvyoq3gej4iT0=
github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d h1:7PxY7LVfSZm7PEeBTyK1rj1gABdCO2mbri6GKO1cMDs=

View File

@ -90,7 +90,6 @@ func (c *Chart) ToYAML(path string) (string, error) {
// Delete uninstall a Chart.
func (c *Chart) Delete(path string, cascade, force bool) error {
log.Debug().Msgf("CHART DELETE %q", path)
ns, n := client.Namespaced(path)
cfg, err := c.EnsureHelmConfig(ns)
if err != nil {

View File

@ -29,6 +29,7 @@ func ToYAML(o runtime.Object) (string, error) {
if o == nil {
return "", errors.New("no object to yamlize")
}
var (
buff bytes.Buffer
p printers.YAMLPrinter

216
internal/dao/ofaas.go Normal file
View File

@ -0,0 +1,216 @@
package dao
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"net/http"
"net/url"
"os"
"path"
"strings"
"time"
"github.com/derailed/k9s/internal/client"
"github.com/derailed/k9s/internal/render"
"github.com/openfaas/faas-cli/proxy"
"github.com/openfaas/faas/gateway/requests"
"github.com/rs/zerolog/log"
"k8s.io/apimachinery/pkg/runtime"
"sigs.k8s.io/yaml"
)
const (
oFaasGatewayEnv = "OPENFAAS_GATEWAY"
oFaasJWTTokenEnv = "OPENFAAS_JWT_TOKEN"
oFaasTLSInsecure = "OPENFAAS_TLS_INSECURE"
)
var (
_ Accessor = (*OpenFaas)(nil)
_ Nuker = (*OpenFaas)(nil)
_ Describer = (*OpenFaas)(nil)
)
// OpenFaas represents a faas gateway connection.
type OpenFaas struct {
NonResource
}
// IsOpenFaasEnabled returns true if a gateway url is set in the environment.
func IsOpenFaasEnabled() bool {
return os.Getenv(oFaasGatewayEnv) != ""
}
func getOpenFAASFlags() (string, string, bool) {
gw, token := os.Getenv(oFaasGatewayEnv), os.Getenv(oFaasJWTTokenEnv)
tlsInsecure := false
if os.Getenv(oFaasTLSInsecure) == "true" {
tlsInsecure = true
}
return gw, token, tlsInsecure
}
// List returns a collection of functions
func (f *OpenFaas) Get(ctx context.Context, path string) (runtime.Object, error) {
ns, n := client.Namespaced(path)
oo, err := f.List(ctx, ns)
if err != nil {
return nil, err
}
var found runtime.Object
for _, o := range oo {
r, ok := o.(render.OpenFaasRes)
if !ok {
continue
}
if r.Function.Name == n {
found = o
break
}
}
if found == nil {
return nil, fmt.Errorf("unable to locate function %q", path)
}
return found, nil
}
// List returns a collection of functions
func (f *OpenFaas) List(_ context.Context, ns string) ([]runtime.Object, error) {
if !IsOpenFaasEnabled() {
return nil, errors.New("OpenFAAS is not enabled on this cluster")
}
gw, token, tls := getOpenFAASFlags()
ff, err := proxy.ListFunctionsToken(gw, tls, token, ns)
if err != nil {
return nil, err
}
oo := make([]runtime.Object, 0, len(ff))
for _, f := range ff {
oo = append(oo, render.OpenFaasRes{Function: f})
}
return oo, nil
}
func (f *OpenFaas) Delete(path string, _, _ bool) error {
gw, token, tls := getOpenFAASFlags()
ns, n := client.Namespaced(path)
// BOZO!! openfaas spews to stdout. Not good for us...
return deleteFunctionToken(gw, n, tls, token, ns)
}
func (f *OpenFaas) ToYAML(path string) (string, error) {
return f.Describe(path)
}
func (f *OpenFaas) Describe(path string) (string, error) {
o, err := f.Get(context.Background(), path)
if err != nil {
return "", err
}
fn, ok := o.(render.OpenFaasRes)
if !ok {
return "", fmt.Errorf("expecting OpenFaasRes but got %T", o)
}
raw, err := json.Marshal(fn)
if err != nil {
return "", err
}
bytes, err := yaml.JSONToYAML(raw)
if err != nil {
return "", err
}
return string(bytes), nil
}
// BOZO!! Meow! openfaas fn prints to stdout have to dup ;(
func deleteFunctionToken(gateway string, functionName string, tlsInsecure bool, token string, namespace string) error {
defaultCommandTimeout := 60 * time.Second
gateway = strings.TrimRight(gateway, "/")
delReq := requests.DeleteFunctionRequest{FunctionName: functionName}
reqBytes, _ := json.Marshal(&delReq)
reader := bytes.NewReader(reqBytes)
c := proxy.MakeHTTPClient(&defaultCommandTimeout, tlsInsecure)
deleteEndpoint, err := createSystemEndpoint(gateway, namespace)
if err != nil {
return err
}
req, err := http.NewRequest("DELETE", deleteEndpoint, reader)
if err != nil {
fmt.Println(err)
return err
}
req.Header.Set("Content-Type", "application/json")
if len(token) > 0 {
proxy.SetToken(req, token)
} else {
proxy.SetAuth(req, gateway)
}
delRes, delErr := c.Do(req)
if delErr != nil {
fmt.Printf("Error removing existing function: %s, gateway=%s, functionName=%s\n", delErr.Error(), gateway, functionName)
return delErr
}
if delRes.Body != nil {
defer func() {
if err := delRes.Body.Close(); err != nil {
log.Error().Err(err).Msgf("closing delete-gtw body")
}
}()
}
switch delRes.StatusCode {
case http.StatusOK, http.StatusCreated, http.StatusAccepted:
return nil
case http.StatusNotFound:
return fmt.Errorf("no function named %s found", functionName)
case http.StatusUnauthorized:
return fmt.Errorf("unauthorized access, run \"faas-cli login\" to setup authentication for this server")
default:
bytesOut, err := ioutil.ReadAll(delRes.Body)
if err != nil {
return err
}
return fmt.Errorf("server returned unexpected status code %d %s", delRes.StatusCode, string(bytesOut))
}
}
func createSystemEndpoint(gateway, namespace string) (string, error) {
const systemPath = "/system/functions"
gatewayURL, err := url.Parse(gateway)
if err != nil {
return "", fmt.Errorf("invalid gateway URL: %s", err.Error())
}
gatewayURL.Path = path.Join(gatewayURL.Path, systemPath)
if len(namespace) > 0 {
q := gatewayURL.Query()
q.Set("namespace", namespace)
gatewayURL.RawQuery = q.Encode()
}
return gatewayURL.String(), nil
}

View File

@ -47,6 +47,7 @@ func AccessorFor(f Factory, gvr client.GVR) (Accessor, error) {
client.NewGVR("batch/v1beta1/cronjobs"): &CronJob{},
client.NewGVR("batch/v1/jobs"): &Job{},
client.NewGVR("charts"): &Chart{},
client.NewGVR("openfaas"): &OpenFaas{},
}
r, ok := m[gvr]
@ -96,7 +97,7 @@ func (m *Meta) MetaFor(gvr client.GVR) (metav1.APIResource, error) {
// IsK8sMeta checks for non resource meta.
func IsK8sMeta(m metav1.APIResource) bool {
for _, c := range m.Categories {
if c == "k9s" || c == "helm" {
if c == "k9s" || c == "helm" || c == "faas" {
return false
}
}
@ -135,6 +136,9 @@ func loadNonResource(m ResourceMetas) {
loadK9s(m)
loadRBAC(m)
loadHelm(m)
if IsOpenFaasEnabled() {
loadOpenFaas(m)
}
}
func loadK9s(m ResourceMetas) {
@ -203,6 +207,17 @@ func loadHelm(m ResourceMetas) {
}
}
func loadOpenFaas(m ResourceMetas) {
m[client.NewGVR("openfaas")] = metav1.APIResource{
Name: "openfaas",
Kind: "OpenFaaS",
ShortNames: []string{"ofaas", "ofa"},
Namespaced: true,
Verbs: []string{"delete"},
Categories: []string{"faas"},
}
}
func loadRBAC(m ResourceMetas) {
m[client.NewGVR("rbac")] = metav1.APIResource{
Name: "rbacs",

View File

@ -14,6 +14,10 @@ var Registry = map[string]ResourceMeta{
DAO: &dao.Chart{},
Renderer: &render.Chart{},
},
"openfaas": {
DAO: &dao.OpenFaas{},
Renderer: &render.OpenFaas{},
},
"containers": {
DAO: &dao.Container{},
Renderer: &render.Container{},

View File

@ -66,7 +66,7 @@ func (c Chart) Render(o interface{}, ns string, r *Row) error {
// ----------------------------------------------------------------------------
// Helpers...
// ChartRes represents an alias resource.
// ChartRes represents an helm chart resource.
type ChartRes struct {
Release *release.Release
}

102
internal/render/ofaas.go Normal file
View File

@ -0,0 +1,102 @@
package render
import (
"fmt"
"strconv"
"time"
"github.com/derailed/k9s/internal/client"
"github.com/derailed/tview"
"github.com/gdamore/tcell"
ofaas "github.com/openfaas/faas-provider/types"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
)
const (
fnStatusReady = "Ready"
fnStatusNotReady = "Not Ready"
)
// OpenFaas renders an openfaas function to screen.
type OpenFaas struct{}
// ColorerFunc colors a resource row.
func (OpenFaas) ColorerFunc() ColorerFunc {
return func(ns string, re RowEvent) tcell.Color {
return tcell.ColorPaleTurquoise
}
}
// Header returns a header row.
func (OpenFaas) Header(ns string) HeaderRow {
var h HeaderRow
if client.IsAllNamespaces(ns) {
h = append(h, Header{Name: "NAMESPACE"})
}
return append(h,
Header{Name: "NAME"},
Header{Name: "STATUS"},
Header{Name: "IMAGE"},
Header{Name: "LABELS"},
Header{Name: "INVOCATIONS", Align: tview.AlignRight},
Header{Name: "REPLICAS", Align: tview.AlignRight},
Header{Name: "AVAILABLE", Align: tview.AlignRight},
Header{Name: "AGE", Decorator: AgeDecorator},
)
}
// Render renders a chart to screen.
func (f OpenFaas) Render(o interface{}, ns string, r *Row) error {
fn, ok := o.(OpenFaasRes)
if !ok {
return fmt.Errorf("expected OpenFaasRes, but got %T", o)
}
var labels string
if fn.Function.Labels != nil {
labels = mapToStr(*fn.Function.Labels)
}
var status = fnStatusReady
if fn.Function.AvailableReplicas == 0 {
status = fnStatusNotReady
}
r.ID = client.FQN(fn.Function.Namespace, fn.Function.Name)
r.Fields = make(Fields, 0, len(f.Header(ns)))
if client.IsAllNamespaces(ns) {
r.Fields = append(r.Fields, fn.Function.Namespace)
}
r.Fields = append(r.Fields,
fn.Function.Name,
status,
fn.Function.Image,
labels,
strconv.Itoa(int(fn.Function.InvocationCount)),
strconv.Itoa(int(fn.Function.Replicas)),
strconv.Itoa(int(fn.Function.AvailableReplicas)),
toAge(metav1.Time{Time: time.Now()}),
)
return nil
}
// ----------------------------------------------------------------------------
// Helpers...
// OpenFaasRes represents an openfaas function resource.
type OpenFaasRes struct {
Function ofaas.FunctionStatus `json:"function"`
}
// GetObjectKind returns a schema object.
func (OpenFaasRes) GetObjectKind() schema.ObjectKind {
return nil
}
// DeepCopyObject returns a container copy.
func (h OpenFaasRes) DeepCopyObject() runtime.Object {
return h
}

View File

@ -0,0 +1,34 @@
package render_test
import (
"testing"
"github.com/derailed/k9s/internal/render"
ofaas "github.com/openfaas/faas-provider/types"
"github.com/stretchr/testify/assert"
)
func TestOpenFaasRender(t *testing.T) {
c := render.OpenFaas{}
r := render.NewRow(9)
c.Render(makeFn("blee"), "", &r)
assert.Equal(t, "default/blee", r.ID)
assert.Equal(t, render.Fields{"default", "blee", "Ready", "nginx:0", "fred=blee", "10", "1", "1"}, r.Fields[:8])
}
// Helpers...
func makeFn(n string) render.OpenFaasRes {
return render.OpenFaasRes{
Function: ofaas.FunctionStatus{
Name: n,
Namespace: "default",
Image: "nginx:0",
InvocationCount: 10,
Replicas: 1,
AvailableReplicas: 1,
Labels: &map[string]string{"fred": "blee"},
},
}
}

View File

@ -0,0 +1,62 @@
package dialog
import (
"fmt"
"github.com/derailed/k9s/internal/config"
"github.com/derailed/k9s/internal/ui"
"github.com/derailed/tview"
)
type PortForwardFunc func(path, address, lport, cport string)
// ShowPortForwards pops a port forwarding configuration dialog.
func ShowPortForwards(p *ui.Pages, s *config.Styles, path string, ports []string, okFn PortForwardFunc) {
f := tview.NewForm()
f.SetItemPadding(0)
f.SetButtonsAlign(tview.AlignCenter).
SetButtonBackgroundColor(s.BgColor()).
SetButtonTextColor(s.FgColor()).
SetLabelColor(config.AsColor(s.K9s.Info.FgColor)).
SetFieldTextColor(config.AsColor(s.K9s.Info.SectionColor))
p1, p2, address := ports[0], ports[0], "localhost"
f.AddDropDown("Container Ports", ports, 0, func(sel string, _ int) {
p1, p2 = sel, stripPort(sel)
})
dropD, ok := f.GetFormItem(0).(*tview.DropDown)
if ok {
dropD.SetFieldBackgroundColor(s.BgColor())
list := dropD.GetList()
list.SetMainTextColor(s.FgColor())
list.SetSelectedTextColor(s.FgColor())
list.SetSelectedBackgroundColor(config.AsColor(s.Table().CursorColor))
list.SetBackgroundColor(s.BgColor() + 100)
}
f.AddInputField("Local Port:", p2, 20, nil, func(p string) {
p2 = p
})
f.AddInputField("Address:", address, 20, nil, func(h string) {
address = h
})
f.AddButton("OK", func() {
okFn(path, address, stripPort(p2), stripPort(p1))
})
f.AddButton("Cancel", func() {
DismissPortForward(p)
})
modal := tview.NewModalForm(fmt.Sprintf("<PortForward on %s>", path), f)
modal.SetDoneFunc(func(_ int, b string) {
DismissPortForward(p)
})
p.AddPage(portForwardKey, modal, false, false)
p.ShowPage(portForwardKey)
}
// DismissPortForward dismiss the port forward dialog.
func DismissPortForwards(p *ui.Pages) {
p.RemovePage(portForwardKey)
}

View File

@ -134,7 +134,7 @@ func (a *App) toggleHeader(flag bool) {
}
if a.showHeader {
flex.RemoveItemAtIndex(0)
flex.AddItemAtIndex(0, a.buildHeader(), 8, 1, false)
flex.AddItemAtIndex(0, a.buildHeader(), 7, 1, false)
} else {
flex.RemoveItemAtIndex(0)
flex.AddItemAtIndex(0, a.statusIndicator(), 1, 1, false)

View File

@ -159,31 +159,35 @@ func (c *Container) preparePort(pp []string) string {
func (c *Container) portForward(address, lport, cport string) {
co := c.GetTable().GetSelectedCell(0)
pf := dao.NewPortForwarder(c.App().Conn())
path := c.GetTable().GetSelectedItem()
ports := []string{lport + ":" + cport}
fw, err := pf.Start(c.GetTable().Path, co, address, ports)
fw, err := pf.Start(path, co, address, ports)
if err != nil {
c.App().Flash().Err(err)
return
}
log.Debug().Msgf(">>> Starting port forward %q %v", c.GetTable().Path, ports)
go c.runForward(pf, fw)
log.Debug().Msgf(">>> Starting port forward %q %v", path, ports)
go runForward(c.App(), pf, fw)
}
func (c *Container) runForward(pf *dao.PortForwarder, f *portforward.PortForwarder) {
c.App().QueueUpdateDraw(func() {
c.App().factory.AddForwarder(pf)
c.App().Flash().Infof("PortForward activated %s:%s", pf.Path(), pf.Ports()[0])
dialog.DismissPortForward(c.App().Content.Pages)
// ----------------------------------------------------------------------------
// Helpers...
func runForward(a *App, pf *dao.PortForwarder, f *portforward.PortForwarder) {
a.QueueUpdateDraw(func() {
a.factory.AddForwarder(pf)
a.Flash().Infof("PortForward activated %s:%s", pf.Path(), pf.Ports()[0])
dialog.DismissPortForward(a.Content.Pages)
})
pf.SetActive(true)
if err := f.ForwardPorts(); err != nil {
c.App().Flash().Err(err)
a.Flash().Err(err)
return
}
c.App().QueueUpdateDraw(func() {
c.App().factory.DeleteForwarder(pf.FQN())
a.QueueUpdateDraw(func() {
a.factory.DeleteForwarder(pf.FQN())
pf.SetActive(false)
})
}

View File

@ -21,7 +21,7 @@ func TestHelp(t *testing.T) {
v := view.NewHelp()
assert.Nil(t, v.Init(ctx))
assert.Equal(t, 19, v.GetRowCount())
assert.Equal(t, 20, v.GetRowCount())
assert.Equal(t, 8, v.GetColumnCount())
assert.Equal(t, "<ctrl-k>", strings.TrimSpace(v.GetCell(1, 0).Text))
assert.Equal(t, "Kill", strings.TrimSpace(v.GetCell(1, 1).Text))

46
internal/view/ofaas.go Normal file
View File

@ -0,0 +1,46 @@
package view
import (
"strings"
"github.com/derailed/k9s/internal/client"
"github.com/derailed/k9s/internal/render"
"github.com/derailed/k9s/internal/ui"
)
// OpenFaas represents an OpenFaaS viewer.
type OpenFaas struct {
ResourceViewer
}
// NewOpenFaas returns a new viewer.
func NewOpenFaas(gvr client.GVR) ResourceViewer {
o := OpenFaas{ResourceViewer: NewBrowser(gvr)}
o.SetBindKeysFn(o.bindKeys)
o.GetTable().SetEnterFn(o.showPods)
o.GetTable().SetColorerFn(render.OpenFaas{}.ColorerFunc())
return &o
}
func (o *OpenFaas) bindKeys(aa ui.KeyActions) {
aa.Add(ui.KeyActions{
ui.KeyShiftS: ui.NewKeyAction("Sort Status", o.GetTable().SortColCmd(2, true), false),
ui.KeyShiftT: ui.NewKeyAction("Sort Invocations", o.GetTable().SortColCmd(5, false), false),
ui.KeyShiftC: ui.NewKeyAction("Sort Replicas", o.GetTable().SortColCmd(6, false), false),
ui.KeyShiftM: ui.NewKeyAction("Sort Available", o.GetTable().SortColCmd(7, false), false),
})
}
func (o *OpenFaas) showPods(a *App, _ ui.Tabular, _, path string) {
labels := o.GetTable().GetSelectedCell(4)
sels := make(map[string]string)
tokens := strings.Split(labels, ",")
for _, t := range tokens {
s := strings.Split(t, "=")
sels[s[0]] = s[1]
}
showPodsWithLabels(a, path, sels)
}

View File

@ -11,6 +11,7 @@ import (
"github.com/derailed/k9s/internal/model"
"github.com/derailed/k9s/internal/render"
"github.com/derailed/k9s/internal/ui"
"github.com/derailed/k9s/internal/ui/dialog"
"github.com/derailed/k9s/internal/watch"
"github.com/fatih/color"
"github.com/gdamore/tcell"
@ -49,6 +50,7 @@ func (p *Pod) bindKeys(aa ui.KeyActions) {
}
aa.Add(ui.KeyActions{
ui.KeyShiftF: ui.NewKeyAction("Port-Forward", p.portFwdCmd, true),
ui.KeyShiftR: ui.NewKeyAction("Sort Ready", p.GetTable().SortColCmd(1, true), false),
ui.KeyShiftS: ui.NewKeyAction("Sort Status", p.GetTable().SortColCmd(2, true), false),
ui.KeyShiftT: ui.NewKeyAction("Sort Restart", p.GetTable().SortColCmd(3, false), false),
@ -64,7 +66,6 @@ func (p *Pod) bindKeys(aa ui.KeyActions) {
}
func (p *Pod) showContainers(app *App, model ui.Tabular, gvr, path string) {
log.Debug().Msgf("SHOW CONTAINERS %q -- %q -- %q", gvr, model.GetNamespace(), path)
co := NewContainer(client.NewGVR("containers"))
co.SetContextFn(p.coContext)
if err := app.inject(co); err != nil {
@ -128,6 +129,51 @@ func (p *Pod) shellCmd(evt *tcell.EventKey) *tcell.EventKey {
return nil
}
func (p *Pod) portFwdCmd(evt *tcell.EventKey) *tcell.EventKey {
path := p.GetTable().GetSelectedItem()
if path == "" {
return evt
}
pp, err := fetchPodPorts(p.App().factory, path)
if err != nil {
p.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 {
p.App().Flash().Err(fmt.Errorf("no tcp ports found on %s", path))
return nil
}
dialog.ShowPortForwards(p.App().Content.Pages, p.App().Styles, path, ports, p.portForward)
return nil
}
func (p *Pod) portForward(path, address, lport, cport string) {
pf := dao.NewPortForwarder(p.App().Conn())
ports := []string{lport + ":" + cport}
fw, err := pf.Start(path, "", address, ports)
if err != nil {
p.App().Flash().Err(err)
return
}
log.Debug().Msgf(">>> Starting port forward %q %v", path, ports)
go runForward(p.App(), pf, fw)
}
// ----------------------------------------------------------------------------
// Helpers...
@ -160,6 +206,7 @@ func containerShellin(a *App, comp model.Component, path, co string) error {
func resumeShellIn(a *App, c model.Component, path, co string) {
c.Stop()
defer c.Start()
shellIn(a, path, co)
}
@ -205,10 +252,33 @@ func fetchContainers(f *watch.Factory, path string, includeInit bool) ([]string,
for _, c := range pod.Spec.Containers {
nn = append(nn, c.Name)
}
if includeInit {
for _, c := range pod.Spec.InitContainers {
nn = append(nn, c.Name)
}
if !includeInit {
return nn, nil
}
for _, c := range pod.Spec.InitContainers {
nn = append(nn, c.Name)
}
return nn, nil
}
func fetchPodPorts(f *watch.Factory, path string) ([]v1.ContainerPort, error) {
log.Debug().Msgf("Fetching ports on pod %q", path)
o, err := f.Get("v1/pods", path, false, labels.Everything())
if err != nil {
return nil, err
}
var pod v1.Pod
err = runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &pod)
if err != nil {
return nil, err
}
pp := make([]v1.ContainerPort, 0, len(pod.Spec.Containers))
for _, c := range pod.Spec.Containers {
pp = append(pp, c.Ports...)
}
return pp, nil
}

View File

@ -16,7 +16,7 @@ func TestPodNew(t *testing.T) {
assert.Nil(t, po.Init(makeCtx()))
assert.Equal(t, "Pods", po.Name())
assert.Equal(t, 18, len(po.Hints()))
assert.Equal(t, 19, len(po.Hints()))
}
// Helpers...

View File

@ -51,6 +51,9 @@ func miscViewers(vv MetaViewers) {
vv[client.NewGVR("contexts")] = MetaViewer{
viewerFn: NewContext,
}
vv[client.NewGVR("openfaas")] = MetaViewer{
viewerFn: NewOpenFaas,
}
vv[client.NewGVR("containers")] = MetaViewer{
viewerFn: NewContainer,
}

View File

@ -8,8 +8,10 @@ import (
"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"
@ -40,19 +42,92 @@ func NewService(gvr client.GVR) ResourceViewer {
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 (s *Service) showPods(app *App, _ ui.Tabular, gvr, path string) {
o, err := app.factory.Get(gvr, path, true, labels.Everything())
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 {
app.Flash().Err(err)
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
}
var svc v1.Service
err = runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &svc)
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
@ -170,6 +245,21 @@ func (s *Service) benchDone() {
})
}
// ----------------------------------------------------------------------------
// 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() {

View File

@ -132,5 +132,5 @@ func TestServiceNew(t *testing.T) {
assert.Nil(t, s.Init(makeCtx()))
assert.Equal(t, "Services", s.Name())
assert.Equal(t, 7, len(s.Hints()))
assert.Equal(t, 8, len(s.Hints()))
}

View File

@ -229,6 +229,9 @@ func (f *Factory) DeleteForwarder(path string) {
// Forwarders returns all portforwards.
func (f *Factory) Forwarders() Forwarders {
f.mx.RLock()
defer f.mx.RUnlock()
return f.forwarders
}

4
plugins/README.md Normal file
View File

@ -0,0 +1,4 @@
# K9s community plugins
These plugins provide for extending the K9s cli to provide for more cluster management fu.

19
plugins/job_suspend.yml Normal file
View File

@ -0,0 +1,19 @@
plugin:
# Suspends/Resumes a cronjob
suspendCronsToggle:
shortCut: Ctrl-S
scopes:
- cj
description: Suspend toggle
command: kubectl
background: true
args:
- patch
- cronjobs
- $NAME
- ns
- $NAMESPACE
- --context
- $CONTEXT
- -p
- '{"spec" : {"suspend" : $COL3 }}'

3
plugins/kubectl/kubectl-jq Executable file
View File

@ -0,0 +1,3 @@
#!/bin/bash
/usr/local/bin/kubectl logs -f $1 -n $2 --context $3 | jq -r '.message'

14
plugins/log_jq.yml Normal file
View File

@ -0,0 +1,14 @@
plugin:
# Sends logs over to jq for processing. This leverages kubectl plugin kubectl-jq.
jqlogs:
shortCut: Ctrl-J
description: "Logs (jq)"
scopes:
- po
command: kubectl
background: false
args:
- jq
- $NAME
- $NAMESPACE
- $CONTEXT

17
plugins/log_stern.yml Normal file
View File

@ -0,0 +1,17 @@
plugin:
# Leverage stern (https://github.com/wercker/stern) to output logs.
stern:
shortCut: Ctrl-L
description: "Logs <Stern>"
scopes:
- pods
command: stern
background: false
args:
- --tail
- 50
- $FILTER
- -n
- $NAMESPACE
- --context
- $CONTEXT