enable port-forwards on pods and services
parent
7ad07b791d
commit
f1ef8d216c
8
go.mod
8
go.mod
|
|
@ -2,6 +2,8 @@ module github.com/derailed/k9s
|
||||||
|
|
||||||
go 1.13
|
go 1.13
|
||||||
|
|
||||||
|
replace github.com/derailed/tview => /Users/fernand/go_wk/derailed/src/github.com/derailed/tview
|
||||||
|
|
||||||
replace (
|
replace (
|
||||||
github.com/docker/docker => github.com/docker/docker v1.4.2-0.20181221150755-2cb26cfe9cbf
|
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
|
k8s.io/api => k8s.io/api v0.0.0-20190918155943-95b840bb6a1f
|
||||||
|
|
@ -30,6 +32,8 @@ replace (
|
||||||
require (
|
require (
|
||||||
fyne.io/fyne v1.2.2 // indirect
|
fyne.io/fyne v1.2.2 // indirect
|
||||||
github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 // 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/atotto/clipboard v0.1.2
|
||||||
github.com/derailed/tview v0.3.4
|
github.com/derailed/tview v0.3.4
|
||||||
github.com/drone/envsubst v1.0.2 // indirect
|
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/gregjones/httpcache v0.0.0-20190212212710-3befbb6ad0cc // indirect
|
||||||
github.com/kylelemons/godebug v1.1.0 // indirect
|
github.com/kylelemons/godebug v1.1.0 // indirect
|
||||||
github.com/mattn/go-runewidth v0.0.5
|
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-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/petergtz/pegomock v2.6.0+incompatible
|
||||||
github.com/rakyll/hey v0.1.2
|
github.com/rakyll/hey v0.1.2
|
||||||
github.com/rs/zerolog v1.17.2
|
github.com/rs/zerolog v1.17.2
|
||||||
|
|
|
||||||
5
go.sum
5
go.sum
|
|
@ -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/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-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||||
github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/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/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/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=
|
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 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI=
|
||||||
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
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/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/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/mrunalp/fileutils v0.0.0-20160930181131-4ee1cc9a8058/go.mod h1:x8F1gnqOkIEiO4rqoeEEEqQbo7HjGMTvyoq3gej4iT0=
|
||||||
github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d h1:7PxY7LVfSZm7PEeBTyK1rj1gABdCO2mbri6GKO1cMDs=
|
github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d h1:7PxY7LVfSZm7PEeBTyK1rj1gABdCO2mbri6GKO1cMDs=
|
||||||
|
|
|
||||||
|
|
@ -90,7 +90,6 @@ func (c *Chart) ToYAML(path string) (string, error) {
|
||||||
|
|
||||||
// Delete uninstall a Chart.
|
// Delete uninstall a Chart.
|
||||||
func (c *Chart) Delete(path string, cascade, force bool) error {
|
func (c *Chart) Delete(path string, cascade, force bool) error {
|
||||||
log.Debug().Msgf("CHART DELETE %q", path)
|
|
||||||
ns, n := client.Namespaced(path)
|
ns, n := client.Namespaced(path)
|
||||||
cfg, err := c.EnsureHelmConfig(ns)
|
cfg, err := c.EnsureHelmConfig(ns)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,7 @@ func ToYAML(o runtime.Object) (string, error) {
|
||||||
if o == nil {
|
if o == nil {
|
||||||
return "", errors.New("no object to yamlize")
|
return "", errors.New("no object to yamlize")
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
buff bytes.Buffer
|
buff bytes.Buffer
|
||||||
p printers.YAMLPrinter
|
p printers.YAMLPrinter
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
@ -47,6 +47,7 @@ func AccessorFor(f Factory, gvr client.GVR) (Accessor, error) {
|
||||||
client.NewGVR("batch/v1beta1/cronjobs"): &CronJob{},
|
client.NewGVR("batch/v1beta1/cronjobs"): &CronJob{},
|
||||||
client.NewGVR("batch/v1/jobs"): &Job{},
|
client.NewGVR("batch/v1/jobs"): &Job{},
|
||||||
client.NewGVR("charts"): &Chart{},
|
client.NewGVR("charts"): &Chart{},
|
||||||
|
client.NewGVR("openfaas"): &OpenFaas{},
|
||||||
}
|
}
|
||||||
|
|
||||||
r, ok := m[gvr]
|
r, ok := m[gvr]
|
||||||
|
|
@ -96,7 +97,7 @@ func (m *Meta) MetaFor(gvr client.GVR) (metav1.APIResource, error) {
|
||||||
// IsK8sMeta checks for non resource meta.
|
// IsK8sMeta checks for non resource meta.
|
||||||
func IsK8sMeta(m metav1.APIResource) bool {
|
func IsK8sMeta(m metav1.APIResource) bool {
|
||||||
for _, c := range m.Categories {
|
for _, c := range m.Categories {
|
||||||
if c == "k9s" || c == "helm" {
|
if c == "k9s" || c == "helm" || c == "faas" {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -135,6 +136,9 @@ func loadNonResource(m ResourceMetas) {
|
||||||
loadK9s(m)
|
loadK9s(m)
|
||||||
loadRBAC(m)
|
loadRBAC(m)
|
||||||
loadHelm(m)
|
loadHelm(m)
|
||||||
|
if IsOpenFaasEnabled() {
|
||||||
|
loadOpenFaas(m)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func loadK9s(m ResourceMetas) {
|
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) {
|
func loadRBAC(m ResourceMetas) {
|
||||||
m[client.NewGVR("rbac")] = metav1.APIResource{
|
m[client.NewGVR("rbac")] = metav1.APIResource{
|
||||||
Name: "rbacs",
|
Name: "rbacs",
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,10 @@ var Registry = map[string]ResourceMeta{
|
||||||
DAO: &dao.Chart{},
|
DAO: &dao.Chart{},
|
||||||
Renderer: &render.Chart{},
|
Renderer: &render.Chart{},
|
||||||
},
|
},
|
||||||
|
"openfaas": {
|
||||||
|
DAO: &dao.OpenFaas{},
|
||||||
|
Renderer: &render.OpenFaas{},
|
||||||
|
},
|
||||||
"containers": {
|
"containers": {
|
||||||
DAO: &dao.Container{},
|
DAO: &dao.Container{},
|
||||||
Renderer: &render.Container{},
|
Renderer: &render.Container{},
|
||||||
|
|
|
||||||
|
|
@ -66,7 +66,7 @@ func (c Chart) Render(o interface{}, ns string, r *Row) error {
|
||||||
// ----------------------------------------------------------------------------
|
// ----------------------------------------------------------------------------
|
||||||
// Helpers...
|
// Helpers...
|
||||||
|
|
||||||
// ChartRes represents an alias resource.
|
// ChartRes represents an helm chart resource.
|
||||||
type ChartRes struct {
|
type ChartRes struct {
|
||||||
Release *release.Release
|
Release *release.Release
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
@ -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"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
|
|
@ -134,7 +134,7 @@ func (a *App) toggleHeader(flag bool) {
|
||||||
}
|
}
|
||||||
if a.showHeader {
|
if a.showHeader {
|
||||||
flex.RemoveItemAtIndex(0)
|
flex.RemoveItemAtIndex(0)
|
||||||
flex.AddItemAtIndex(0, a.buildHeader(), 8, 1, false)
|
flex.AddItemAtIndex(0, a.buildHeader(), 7, 1, false)
|
||||||
} else {
|
} else {
|
||||||
flex.RemoveItemAtIndex(0)
|
flex.RemoveItemAtIndex(0)
|
||||||
flex.AddItemAtIndex(0, a.statusIndicator(), 1, 1, false)
|
flex.AddItemAtIndex(0, a.statusIndicator(), 1, 1, false)
|
||||||
|
|
|
||||||
|
|
@ -159,31 +159,35 @@ func (c *Container) preparePort(pp []string) string {
|
||||||
func (c *Container) portForward(address, lport, cport string) {
|
func (c *Container) portForward(address, lport, cport string) {
|
||||||
co := c.GetTable().GetSelectedCell(0)
|
co := c.GetTable().GetSelectedCell(0)
|
||||||
pf := dao.NewPortForwarder(c.App().Conn())
|
pf := dao.NewPortForwarder(c.App().Conn())
|
||||||
|
path := c.GetTable().GetSelectedItem()
|
||||||
ports := []string{lport + ":" + cport}
|
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 {
|
if err != nil {
|
||||||
c.App().Flash().Err(err)
|
c.App().Flash().Err(err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Debug().Msgf(">>> Starting port forward %q %v", c.GetTable().Path, ports)
|
log.Debug().Msgf(">>> Starting port forward %q %v", path, ports)
|
||||||
go c.runForward(pf, fw)
|
go runForward(c.App(), pf, fw)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Container) runForward(pf *dao.PortForwarder, f *portforward.PortForwarder) {
|
// ----------------------------------------------------------------------------
|
||||||
c.App().QueueUpdateDraw(func() {
|
// Helpers...
|
||||||
c.App().factory.AddForwarder(pf)
|
|
||||||
c.App().Flash().Infof("PortForward activated %s:%s", pf.Path(), pf.Ports()[0])
|
func runForward(a *App, pf *dao.PortForwarder, f *portforward.PortForwarder) {
|
||||||
dialog.DismissPortForward(c.App().Content.Pages)
|
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)
|
pf.SetActive(true)
|
||||||
if err := f.ForwardPorts(); err != nil {
|
if err := f.ForwardPorts(); err != nil {
|
||||||
c.App().Flash().Err(err)
|
a.Flash().Err(err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
c.App().QueueUpdateDraw(func() {
|
a.QueueUpdateDraw(func() {
|
||||||
c.App().factory.DeleteForwarder(pf.FQN())
|
a.factory.DeleteForwarder(pf.FQN())
|
||||||
pf.SetActive(false)
|
pf.SetActive(false)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@ func TestHelp(t *testing.T) {
|
||||||
v := view.NewHelp()
|
v := view.NewHelp()
|
||||||
|
|
||||||
assert.Nil(t, v.Init(ctx))
|
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, 8, v.GetColumnCount())
|
||||||
assert.Equal(t, "<ctrl-k>", strings.TrimSpace(v.GetCell(1, 0).Text))
|
assert.Equal(t, "<ctrl-k>", strings.TrimSpace(v.GetCell(1, 0).Text))
|
||||||
assert.Equal(t, "Kill", strings.TrimSpace(v.GetCell(1, 1).Text))
|
assert.Equal(t, "Kill", strings.TrimSpace(v.GetCell(1, 1).Text))
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
|
|
@ -11,6 +11,7 @@ import (
|
||||||
"github.com/derailed/k9s/internal/model"
|
"github.com/derailed/k9s/internal/model"
|
||||||
"github.com/derailed/k9s/internal/render"
|
"github.com/derailed/k9s/internal/render"
|
||||||
"github.com/derailed/k9s/internal/ui"
|
"github.com/derailed/k9s/internal/ui"
|
||||||
|
"github.com/derailed/k9s/internal/ui/dialog"
|
||||||
"github.com/derailed/k9s/internal/watch"
|
"github.com/derailed/k9s/internal/watch"
|
||||||
"github.com/fatih/color"
|
"github.com/fatih/color"
|
||||||
"github.com/gdamore/tcell"
|
"github.com/gdamore/tcell"
|
||||||
|
|
@ -49,6 +50,7 @@ func (p *Pod) bindKeys(aa ui.KeyActions) {
|
||||||
}
|
}
|
||||||
|
|
||||||
aa.Add(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.KeyShiftR: ui.NewKeyAction("Sort Ready", p.GetTable().SortColCmd(1, true), false),
|
||||||
ui.KeyShiftS: ui.NewKeyAction("Sort Status", p.GetTable().SortColCmd(2, 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),
|
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) {
|
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 := NewContainer(client.NewGVR("containers"))
|
||||||
co.SetContextFn(p.coContext)
|
co.SetContextFn(p.coContext)
|
||||||
if err := app.inject(co); err != nil {
|
if err := app.inject(co); err != nil {
|
||||||
|
|
@ -128,6 +129,51 @@ func (p *Pod) shellCmd(evt *tcell.EventKey) *tcell.EventKey {
|
||||||
return nil
|
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...
|
// 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) {
|
func resumeShellIn(a *App, c model.Component, path, co string) {
|
||||||
c.Stop()
|
c.Stop()
|
||||||
defer c.Start()
|
defer c.Start()
|
||||||
|
|
||||||
shellIn(a, path, co)
|
shellIn(a, path, co)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -205,10 +252,33 @@ func fetchContainers(f *watch.Factory, path string, includeInit bool) ([]string,
|
||||||
for _, c := range pod.Spec.Containers {
|
for _, c := range pod.Spec.Containers {
|
||||||
nn = append(nn, c.Name)
|
nn = append(nn, c.Name)
|
||||||
}
|
}
|
||||||
if includeInit {
|
if !includeInit {
|
||||||
for _, c := range pod.Spec.InitContainers {
|
return nn, nil
|
||||||
nn = append(nn, c.Name)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
for _, c := range pod.Spec.InitContainers {
|
||||||
|
nn = append(nn, c.Name)
|
||||||
|
}
|
||||||
|
|
||||||
return nn, nil
|
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
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ func TestPodNew(t *testing.T) {
|
||||||
|
|
||||||
assert.Nil(t, po.Init(makeCtx()))
|
assert.Nil(t, po.Init(makeCtx()))
|
||||||
assert.Equal(t, "Pods", po.Name())
|
assert.Equal(t, "Pods", po.Name())
|
||||||
assert.Equal(t, 18, len(po.Hints()))
|
assert.Equal(t, 19, len(po.Hints()))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helpers...
|
// Helpers...
|
||||||
|
|
|
||||||
|
|
@ -51,6 +51,9 @@ func miscViewers(vv MetaViewers) {
|
||||||
vv[client.NewGVR("contexts")] = MetaViewer{
|
vv[client.NewGVR("contexts")] = MetaViewer{
|
||||||
viewerFn: NewContext,
|
viewerFn: NewContext,
|
||||||
}
|
}
|
||||||
|
vv[client.NewGVR("openfaas")] = MetaViewer{
|
||||||
|
viewerFn: NewOpenFaas,
|
||||||
|
}
|
||||||
vv[client.NewGVR("containers")] = MetaViewer{
|
vv[client.NewGVR("containers")] = MetaViewer{
|
||||||
viewerFn: NewContainer,
|
viewerFn: NewContainer,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,8 +8,10 @@ import (
|
||||||
|
|
||||||
"github.com/derailed/k9s/internal/client"
|
"github.com/derailed/k9s/internal/client"
|
||||||
"github.com/derailed/k9s/internal/config"
|
"github.com/derailed/k9s/internal/config"
|
||||||
|
"github.com/derailed/k9s/internal/dao"
|
||||||
"github.com/derailed/k9s/internal/perf"
|
"github.com/derailed/k9s/internal/perf"
|
||||||
"github.com/derailed/k9s/internal/ui"
|
"github.com/derailed/k9s/internal/ui"
|
||||||
|
"github.com/derailed/k9s/internal/ui/dialog"
|
||||||
"github.com/gdamore/tcell"
|
"github.com/gdamore/tcell"
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
v1 "k8s.io/api/core/v1"
|
v1 "k8s.io/api/core/v1"
|
||||||
|
|
@ -40,19 +42,92 @@ func NewService(gvr client.GVR) ResourceViewer {
|
||||||
|
|
||||||
func (s *Service) bindKeys(aa ui.KeyActions) {
|
func (s *Service) bindKeys(aa ui.KeyActions) {
|
||||||
aa.Add(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),
|
tcell.KeyCtrlB: ui.NewKeyAction("Bench Run/Stop", s.toggleBenchCmd, true),
|
||||||
ui.KeyShiftT: ui.NewKeyAction("Sort Type", s.GetTable().SortColCmd(1, true), false),
|
ui.KeyShiftT: ui.NewKeyAction("Sort Type", s.GetTable().SortColCmd(1, true), false),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) showPods(app *App, _ ui.Tabular, gvr, path string) {
|
func podFromSelector(f dao.Factory, ns string, sel map[string]string) (string, error) {
|
||||||
o, err := app.factory.Get(gvr, path, true, labels.Everything())
|
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 {
|
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
|
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 {
|
if err != nil {
|
||||||
app.Flash().Err(err)
|
app.Flash().Err(err)
|
||||||
return
|
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) {
|
func benchTimedOut(app *App) {
|
||||||
<-time.After(2 * time.Second)
|
<-time.After(2 * time.Second)
|
||||||
app.QueueUpdate(func() {
|
app.QueueUpdate(func() {
|
||||||
|
|
|
||||||
|
|
@ -132,5 +132,5 @@ func TestServiceNew(t *testing.T) {
|
||||||
|
|
||||||
assert.Nil(t, s.Init(makeCtx()))
|
assert.Nil(t, s.Init(makeCtx()))
|
||||||
assert.Equal(t, "Services", s.Name())
|
assert.Equal(t, "Services", s.Name())
|
||||||
assert.Equal(t, 7, len(s.Hints()))
|
assert.Equal(t, 8, len(s.Hints()))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -229,6 +229,9 @@ func (f *Factory) DeleteForwarder(path string) {
|
||||||
|
|
||||||
// Forwarders returns all portforwards.
|
// Forwarders returns all portforwards.
|
||||||
func (f *Factory) Forwarders() Forwarders {
|
func (f *Factory) Forwarders() Forwarders {
|
||||||
|
f.mx.RLock()
|
||||||
|
defer f.mx.RUnlock()
|
||||||
|
|
||||||
return f.forwarders
|
return f.forwarders
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,4 @@
|
||||||
|
# K9s community plugins
|
||||||
|
|
||||||
|
These plugins provide for extending the K9s cli to provide for more cluster management fu.
|
||||||
|
|
||||||
|
|
@ -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 }}'
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
/usr/local/bin/kubectl logs -f $1 -n $2 --context $3 | jq -r '.message'
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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
|
||||||
Loading…
Reference in New Issue