checkpoint
parent
7a7d66564d
commit
4c2c4793dc
5
go.mod
5
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 (
|
||||||
k8s.io/api => k8s.io/api v0.0.0-20190918155943-95b840bb6a1f
|
k8s.io/api => k8s.io/api v0.0.0-20190918155943-95b840bb6a1f
|
||||||
k8s.io/apiextensions-apiserver => k8s.io/apiextensions-apiserver v0.0.0-20190918161926-8f644eb6e783
|
k8s.io/apiextensions-apiserver => k8s.io/apiextensions-apiserver v0.0.0-20190918161926-8f644eb6e783
|
||||||
|
|
@ -36,6 +38,7 @@ require (
|
||||||
github.com/gdamore/tcell v1.3.0
|
github.com/gdamore/tcell v1.3.0
|
||||||
github.com/ghodss/yaml v1.0.0 // indirect
|
github.com/ghodss/yaml v1.0.0 // indirect
|
||||||
github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef // indirect
|
github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef // indirect
|
||||||
|
github.com/golang/mock v1.2.0
|
||||||
github.com/google/btree v1.0.0 // indirect
|
github.com/google/btree v1.0.0 // indirect
|
||||||
github.com/googleapis/gnostic v0.2.0 // indirect
|
github.com/googleapis/gnostic v0.2.0 // indirect
|
||||||
github.com/gregjones/httpcache v0.0.0-20190212212710-3befbb6ad0cc // indirect
|
github.com/gregjones/httpcache v0.0.0-20190212212710-3befbb6ad0cc // indirect
|
||||||
|
|
@ -44,7 +47,7 @@ require (
|
||||||
github.com/mattn/go-runewidth v0.0.5
|
github.com/mattn/go-runewidth v0.0.5
|
||||||
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.14.3
|
github.com/rs/zerolog v1.17.2
|
||||||
github.com/sahilm/fuzzy v0.1.0
|
github.com/sahilm/fuzzy v0.1.0
|
||||||
github.com/spf13/cobra v0.0.5
|
github.com/spf13/cobra v0.0.5
|
||||||
github.com/stretchr/testify v1.3.0
|
github.com/stretchr/testify v1.3.0
|
||||||
|
|
|
||||||
6
go.sum
6
go.sum
|
|
@ -176,6 +176,7 @@ github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfU
|
||||||
github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||||
github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||||
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||||
|
github.com/golang/mock v1.2.0 h1:28o5sBqPkBsMGnC6b4MvE2TzSr5/AT4c/1fLqVGIwlk=
|
||||||
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||||
github.com/golang/protobuf v0.0.0-20161109072736-4bd1920723d7/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
github.com/golang/protobuf v0.0.0-20161109072736-4bd1920723d7/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||||
|
|
@ -346,6 +347,8 @@ github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFR
|
||||||
github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ=
|
github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ=
|
||||||
github.com/rs/zerolog v1.14.3 h1:4EGfSkR2hJDB0s3oFfrlPqjU1e4WLncergLil3nEKW0=
|
github.com/rs/zerolog v1.14.3 h1:4EGfSkR2hJDB0s3oFfrlPqjU1e4WLncergLil3nEKW0=
|
||||||
github.com/rs/zerolog v1.14.3/go.mod h1:3WXPzbXEEliJ+a6UFE4vhIxV8qR1EML6ngzP9ug4eYg=
|
github.com/rs/zerolog v1.14.3/go.mod h1:3WXPzbXEEliJ+a6UFE4vhIxV8qR1EML6ngzP9ug4eYg=
|
||||||
|
github.com/rs/zerolog v1.17.2 h1:RMRHFw2+wF7LO0QqtELQwo8hqSmqISyCJeFeAAuWcRo=
|
||||||
|
github.com/rs/zerolog v1.17.2/go.mod h1:9nvC1axdVrAHcu/s9taAVfBuIdTZLVQmKQyvrUjF5+I=
|
||||||
github.com/rubiojr/go-vhd v0.0.0-20160810183302-0bfd3b39853c/go.mod h1:DM5xW0nvfNNm2uytzsvhI3OnX8uzaRAg8UX/CnDqbto=
|
github.com/rubiojr/go-vhd v0.0.0-20160810183302-0bfd3b39853c/go.mod h1:DM5xW0nvfNNm2uytzsvhI3OnX8uzaRAg8UX/CnDqbto=
|
||||||
github.com/russross/blackfriday v0.0.0-20170610170232-067529f716f4/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
|
github.com/russross/blackfriday v0.0.0-20170610170232-067529f716f4/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
|
||||||
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
|
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
|
||||||
|
|
@ -433,6 +436,7 @@ golang.org/x/net v0.0.0-20190328230028-74de082e2cca/go.mod h1:t9HGtf8HONx5eT2rtn
|
||||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
|
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
golang.org/x/net v0.0.0-20190812203447-cdfb69ac37fc h1:gkKoSkUmnU6bpS/VhkuO27bzQeSA51uaEfbOW5dNb68=
|
golang.org/x/net v0.0.0-20190812203447-cdfb69ac37fc h1:gkKoSkUmnU6bpS/VhkuO27bzQeSA51uaEfbOW5dNb68=
|
||||||
golang.org/x/net v0.0.0-20190812203447-cdfb69ac37fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/net v0.0.0-20190812203447-cdfb69ac37fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||||
|
|
@ -485,6 +489,8 @@ golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBn
|
||||||
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||||
golang.org/x/tools v0.0.0-20190614205625-5aca471b1d59/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
golang.org/x/tools v0.0.0-20190614205625-5aca471b1d59/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||||
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||||
|
golang.org/x/tools v0.0.0-20190828213141-aed303cbaa74/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
gonum.org/v1/gonum v0.0.0-20190331200053-3d26580ed485/go.mod h1:2ltnJ7xHfj0zHS40VVPYEAAMTa3ZGguvHGBSJeRWqE0=
|
gonum.org/v1/gonum v0.0.0-20190331200053-3d26580ed485/go.mod h1:2ltnJ7xHfj0zHS40VVPYEAAMTa3ZGguvHGBSJeRWqE0=
|
||||||
gonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0/go.mod h1:wa6Ws7BG/ESfp6dHfk7C6KdzKA7wR7u/rKwOGE66zvw=
|
gonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0/go.mod h1:wa6Ws7BG/ESfp6dHfk7C6KdzKA7wR7u/rKwOGE66zvw=
|
||||||
gonum.org/v1/netlib v0.0.0-20190331212654-76723241ea4e/go.mod h1:kS+toOQn6AQKjmKJ7gzohV1XkqsFehRA2FbsbkopSuQ=
|
gonum.org/v1/netlib v0.0.0-20190331212654-76723241ea4e/go.mod h1:kS+toOQn6AQKjmKJ7gzohV1XkqsFehRA2FbsbkopSuQ=
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,9 @@ var K9sAlias = filepath.Join(K9sHome, "alias.yml")
|
||||||
// Alias tracks shortname to GVR mappings.
|
// Alias tracks shortname to GVR mappings.
|
||||||
type Alias map[string]string
|
type Alias map[string]string
|
||||||
|
|
||||||
|
// ShortNames represents a collection of shortnames for aliases.
|
||||||
|
type ShortNames map[string][]string
|
||||||
|
|
||||||
// Aliases represents a collection of aliases.
|
// Aliases represents a collection of aliases.
|
||||||
type Aliases struct {
|
type Aliases struct {
|
||||||
Alias Alias `yaml:"alias"`
|
Alias Alias `yaml:"alias"`
|
||||||
|
|
@ -88,13 +91,13 @@ func (a Aliases) Get(k string) (string, bool) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Define declares a new alias.
|
// Define declares a new alias.
|
||||||
func (a Aliases) Define(command, alias string) {
|
func (a Aliases) Define(gvr string, aliases ...string) {
|
||||||
|
for _, alias := range aliases {
|
||||||
if _, ok := a.Alias[alias]; ok {
|
if _, ok := a.Alias[alias]; ok {
|
||||||
// Don't override aliases. Take order of alias registration as precedence.
|
continue
|
||||||
return
|
}
|
||||||
|
a.Alias[alias] = gvr
|
||||||
}
|
}
|
||||||
|
|
||||||
a.Alias[alias] = command
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// LoadAliases loads alias from a given file.
|
// LoadAliases loads alias from a given file.
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,6 @@ import (
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
||||||
"github.com/derailed/k9s/internal/k8s"
|
"github.com/derailed/k9s/internal/k8s"
|
||||||
"github.com/derailed/k9s/internal/resource"
|
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
"gopkg.in/yaml.v2"
|
"gopkg.in/yaml.v2"
|
||||||
v1 "k8s.io/api/core/v1"
|
v1 "k8s.io/api/core/v1"
|
||||||
|
|
@ -123,7 +122,7 @@ func (c *Config) ActiveNamespace() string {
|
||||||
return cl.Namespace.Active
|
return cl.Namespace.Active
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return resource.DefaultNamespace
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
// FavNamespaces returns fav namespaces in the current cluster.
|
// FavNamespaces returns fav namespaces in the current cluster.
|
||||||
|
|
|
||||||
|
|
@ -216,7 +216,7 @@ func newTable() Table {
|
||||||
FgColor: "aqua",
|
FgColor: "aqua",
|
||||||
BgColor: "black",
|
BgColor: "black",
|
||||||
CursorColor: "aqua",
|
CursorColor: "aqua",
|
||||||
MarkColor: "khaki",
|
MarkColor: "violet",
|
||||||
Header: newTableHeader(),
|
Header: newTableHeader(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,122 @@
|
||||||
|
package dao
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/derailed/k9s/internal/k8s"
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||||
|
"k8s.io/client-go/tools/clientcmd"
|
||||||
|
"k8s.io/client-go/tools/clientcmd/api"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Context struct {
|
||||||
|
Resource
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ Accessor = &Context{}
|
||||||
|
var _ Switchable = &Context{}
|
||||||
|
|
||||||
|
func (c *Context) config() *k8s.Config {
|
||||||
|
return c.Factory.Client().Config()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get a Context.
|
||||||
|
func (c *Context) Get(_, n string) (runtime.Object, error) {
|
||||||
|
ctx, err := c.config().GetContext(n)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &NamedContext{Name: n, Context: ctx}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// List all Contexts on the current cluster.
|
||||||
|
func (c *Context) List(string, metav1.ListOptions) ([]runtime.Object, error) {
|
||||||
|
ctxs, err := c.config().Contexts()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
cc := make([]runtime.Object, 0, len(ctxs))
|
||||||
|
for k, v := range ctxs {
|
||||||
|
cc = append(cc, NewNamedContext(c.config(), k, v))
|
||||||
|
}
|
||||||
|
|
||||||
|
return cc, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete a Context.
|
||||||
|
func (c *Context) Delete(ns, n string, cascade, force bool) error {
|
||||||
|
ctx, err := c.config().CurrentContextName()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if ctx == n {
|
||||||
|
return fmt.Errorf("trying to delete your current context %s", n)
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.config().DelContext(n)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MustCurrentContextName return the active context name.
|
||||||
|
func (c *Context) MustCurrentContextName() string {
|
||||||
|
cl, err := c.config().CurrentContextName()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal().Err(err).Msg("Fetching current context")
|
||||||
|
}
|
||||||
|
return cl
|
||||||
|
}
|
||||||
|
|
||||||
|
// Switch to another context.
|
||||||
|
func (c *Context) Switch(ctx string) error {
|
||||||
|
c.Factory.Client().SwitchContextOrDie(ctx)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// KubeUpdate modifies kubeconfig default context.
|
||||||
|
func (c *Context) KubeUpdate(n string) error {
|
||||||
|
config, err := c.config().RawConfig()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := c.Switch(n); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return clientcmd.ModifyConfig(
|
||||||
|
clientcmd.NewDefaultPathOptions(), config, true,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
// NamedContext represents a named cluster context.
|
||||||
|
type NamedContext struct {
|
||||||
|
Name string
|
||||||
|
Context *api.Context
|
||||||
|
config *k8s.Config
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewNamedContext returns a new named context.
|
||||||
|
func NewNamedContext(c *k8s.Config, n string, ctx *api.Context) *NamedContext {
|
||||||
|
return &NamedContext{Name: n, Context: ctx, config: c}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MustCurrentContextName return the active context name.
|
||||||
|
func (c *NamedContext) MustCurrentContextName() string {
|
||||||
|
cl, err := c.config.CurrentContextName()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal().Err(err).Msg("Fetching current context")
|
||||||
|
}
|
||||||
|
return cl
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetObjectKind returns a schema object.
|
||||||
|
func (c *NamedContext) GetObjectKind() schema.ObjectKind {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopyObject returns a container copy.
|
||||||
|
func (c *NamedContext) DeepCopyObject() runtime.Object {
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,37 @@
|
||||||
|
package dao
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/derailed/k9s/internal/k8s"
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
"k8s.io/kubectl/pkg/describe"
|
||||||
|
"k8s.io/kubectl/pkg/describe/versioned"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Describe(c k8s.Connection, gvr GVR, ns, n string) (string, error) {
|
||||||
|
mapper := k8s.RestMapper{Connection: c}
|
||||||
|
m, err := mapper.ToRESTMapper()
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Msgf("No REST mapper for resource %s", gvr)
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
GVR := k8s.GVR(gvr)
|
||||||
|
gvk, err := m.KindFor(GVR.AsGVR())
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Msgf("No GVK for resource %s", gvr)
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
mapping, err := mapper.ResourceFor(GVR.ResName(), gvk.Kind)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Msgf("Unable to find mapper for %s %s", gvr, n)
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
d, err := versioned.Describer(c.Config().Flags(), mapping)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Msgf("Unable to find describer for %#v", mapping)
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return d.Describe(ns, n, describe.DescriberSettings{ShowEvents: true})
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,80 @@
|
||||||
|
package dao
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
appsv1 "k8s.io/api/apps/v1"
|
||||||
|
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"
|
||||||
|
"k8s.io/apimachinery/pkg/types"
|
||||||
|
"k8s.io/kubectl/pkg/polymorphichelpers"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Deployment struct {
|
||||||
|
Resource
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ Accessor = &Deployment{}
|
||||||
|
var _ Loggable = &Deployment{}
|
||||||
|
var _ Restartable = &Deployment{}
|
||||||
|
var _ Scalable = &Deployment{}
|
||||||
|
|
||||||
|
// Scale a Deployment.
|
||||||
|
func (d *Deployment) Scale(ns, n string, replicas int32) error {
|
||||||
|
scale, err := d.Client().DialOrDie().AppsV1().Deployments(ns).GetScale(n, metav1.GetOptions{})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
scale.Spec.Replicas = replicas
|
||||||
|
_, err = d.Client().DialOrDie().AppsV1().Deployments(ns).UpdateScale(n, scale)
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Restart a Deployment rollout.
|
||||||
|
func (d *Deployment) Restart(ns, n string) error {
|
||||||
|
o, err := d.Get(ns, string(d.gvr), n, labels.Everything())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var ds appsv1.Deployment
|
||||||
|
err = runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &ds)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
update, err := polymorphichelpers.ObjectRestarterFn(&ds)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = d.Client().DialOrDie().AppsV1().Deployments(ns).Patch(ds.Name, types.StrategicMergePatchType, update)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Logs tail logs for all pods represented by this Deployment.
|
||||||
|
func (d *Deployment) TailLogs(ctx context.Context, c chan<- string, opts LogOptions) error {
|
||||||
|
log.Debug().Msgf("Tailing Deployment %q -- %q", opts.Namespace, opts.Name)
|
||||||
|
o, err := d.Get(opts.Namespace, string(d.gvr), opts.Name, labels.Everything())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var dp appsv1.Deployment
|
||||||
|
err = runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &dp)
|
||||||
|
if err != nil {
|
||||||
|
return errors.New("expecting Deployment resource")
|
||||||
|
}
|
||||||
|
|
||||||
|
if dp.Spec.Selector == nil || len(dp.Spec.Selector.MatchLabels) == 0 {
|
||||||
|
return fmt.Errorf("No valid selector found on Deployment %s", opts.FQN())
|
||||||
|
}
|
||||||
|
|
||||||
|
return podLogs(ctx, c, dp.Spec.Selector.MatchLabels, opts)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,122 @@
|
||||||
|
package dao
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/derailed/k9s/internal"
|
||||||
|
"github.com/derailed/k9s/internal/watch"
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
appsv1 "k8s.io/api/apps/v1"
|
||||||
|
v1 "k8s.io/api/core/v1"
|
||||||
|
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"
|
||||||
|
"k8s.io/apimachinery/pkg/types"
|
||||||
|
"k8s.io/kubectl/pkg/polymorphichelpers"
|
||||||
|
)
|
||||||
|
|
||||||
|
type DaemonSet struct {
|
||||||
|
Resource
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ Accessor = &DaemonSet{}
|
||||||
|
var _ Loggable = &DaemonSet{}
|
||||||
|
var _ Restartable = &DaemonSet{}
|
||||||
|
|
||||||
|
// Restart a DaemonSet rollout.
|
||||||
|
func (d *DaemonSet) Restart(ns, n string) error {
|
||||||
|
o, err := d.Get(ns, string(d.gvr), n, labels.Everything())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var ds appsv1.DaemonSet
|
||||||
|
err = runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &ds)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
update, err := polymorphichelpers.ObjectRestarterFn(&ds)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = d.Client().DialOrDie().AppsV1().DaemonSets(ns).Patch(ds.Name, types.StrategicMergePatchType, update)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Logs tail logs for all pods represented by this DaemonSet.
|
||||||
|
func (d *DaemonSet) TailLogs(ctx context.Context, c chan<- string, opts LogOptions) error {
|
||||||
|
log.Debug().Msgf("Tailing DaemonSet %q -- %q", opts.Namespace, opts.Name)
|
||||||
|
o, err := d.Get(opts.Namespace, "apps/v1/daemonsets", opts.Name, labels.Everything())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var ds appsv1.DaemonSet
|
||||||
|
err = runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &ds)
|
||||||
|
if err != nil {
|
||||||
|
return errors.New("expecting daemonset resource")
|
||||||
|
}
|
||||||
|
|
||||||
|
if ds.Spec.Selector == nil || len(ds.Spec.Selector.MatchLabels) == 0 {
|
||||||
|
return fmt.Errorf("No valid selector found on daemonset %s", opts.FQN())
|
||||||
|
}
|
||||||
|
|
||||||
|
return podLogs(ctx, c, ds.Spec.Selector.MatchLabels, opts)
|
||||||
|
}
|
||||||
|
|
||||||
|
func podLogs(ctx context.Context, c chan<- string, sel map[string]string, opts LogOptions) error {
|
||||||
|
f, ok := ctx.Value(internal.KeyFactory).(*watch.Factory)
|
||||||
|
if !ok {
|
||||||
|
return errors.New("expecting a context factory")
|
||||||
|
}
|
||||||
|
ls, err := metav1.ParseToLabelSelector(toSelector(sel))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
lsel, err := metav1.LabelSelectorAsSelector(ls)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
oo, err := f.List(opts.Namespace, "v1/pods", lsel)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(oo) > 1 {
|
||||||
|
opts.MultiPods = true
|
||||||
|
}
|
||||||
|
|
||||||
|
po := Pod{}
|
||||||
|
for _, o := range oo {
|
||||||
|
var pod v1.Pod
|
||||||
|
err := runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &pod)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if pod.Status.Phase == v1.PodRunning {
|
||||||
|
opts.Namespace, opts.Name = pod.Namespace, pod.Name
|
||||||
|
if err := po.TailLogs(ctx, c, opts); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helpers...
|
||||||
|
|
||||||
|
func toSelector(m map[string]string) string {
|
||||||
|
s := make([]string, 0, len(m))
|
||||||
|
for k, v := range m {
|
||||||
|
s = append(s, k+"="+v)
|
||||||
|
}
|
||||||
|
|
||||||
|
return strings.Join(s, ",")
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,124 @@
|
||||||
|
package dao
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"path"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||||
|
"vbom.ml/util/sortorder"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GVR represents a kubernetes resource schema as a string.
|
||||||
|
// Format is group/version/resources
|
||||||
|
type GVR string
|
||||||
|
|
||||||
|
// NewGVR builds a new gvr from a group, version, resource.
|
||||||
|
func NewGVR(g, v, r string) GVR {
|
||||||
|
return GVR(path.Join(g, v, r))
|
||||||
|
}
|
||||||
|
|
||||||
|
// FromGVAndR builds a gvr from a group/version and resource.
|
||||||
|
func FromGVAndR(gv, r string) GVR {
|
||||||
|
return GVR(path.Join(gv, r))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResName returns a resource . separated descriptor in the shape of kind.version.group.
|
||||||
|
func (g GVR) ResName() string {
|
||||||
|
return g.ToR() + "." + g.ToV() + "." + g.ToG()
|
||||||
|
}
|
||||||
|
|
||||||
|
// AsGV returns the group version scheme representation.
|
||||||
|
func (g GVR) AsGV() schema.GroupVersion {
|
||||||
|
return schema.GroupVersion{
|
||||||
|
Group: g.ToG(),
|
||||||
|
Version: g.ToV(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// AsGVR returns a a full schema representation.
|
||||||
|
func (g GVR) AsGVR() schema.GroupVersionResource {
|
||||||
|
return schema.GroupVersionResource{
|
||||||
|
Group: g.ToG(),
|
||||||
|
Version: g.ToV(),
|
||||||
|
Resource: g.ToR(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToV returns the resource version.
|
||||||
|
func (g GVR) ToV() string {
|
||||||
|
tokens := strings.Split(string(g), "/")
|
||||||
|
if len(tokens) < 2 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return tokens[len(tokens)-2]
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToR returns the resource name.
|
||||||
|
func (g GVR) ToR() string {
|
||||||
|
tokens := strings.Split(string(g), "/")
|
||||||
|
return tokens[len(tokens)-1]
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToG returns the resource group name.
|
||||||
|
func (g GVR) ToG() string {
|
||||||
|
tokens := strings.Split(string(g), "/")
|
||||||
|
switch len(tokens) {
|
||||||
|
case 3:
|
||||||
|
return tokens[0]
|
||||||
|
default:
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type GVRs []GVR
|
||||||
|
|
||||||
|
func (g GVRs) Len() int {
|
||||||
|
return len(g)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g GVRs) Swap(i, j int) {
|
||||||
|
g[i], g[j] = g[j], g[i]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g GVRs) Less(i, j int) bool {
|
||||||
|
g1, g2 := g[i].ToG(), g[j].ToG()
|
||||||
|
|
||||||
|
return sortorder.NaturalLess(g1, g2)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper...
|
||||||
|
|
||||||
|
// Can determines the available actions for a given resource.
|
||||||
|
func Can(verbs []string, v string) bool {
|
||||||
|
for _, verb := range verbs {
|
||||||
|
candidates, err := mapVerb(v)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Msgf("verb mapping failed")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for _, c := range candidates {
|
||||||
|
if verb == c {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func mapVerb(v string) ([]string, error) {
|
||||||
|
switch v {
|
||||||
|
case "describe":
|
||||||
|
return []string{"get"}, nil
|
||||||
|
case "view":
|
||||||
|
return []string{"get", "list"}, nil
|
||||||
|
case "delete":
|
||||||
|
return []string{"delete"}, nil
|
||||||
|
case "edit":
|
||||||
|
return []string{"patch", "update"}, nil
|
||||||
|
default:
|
||||||
|
return []string{}, fmt.Errorf("no standard verb for %q", v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,104 @@
|
||||||
|
package dao
|
||||||
|
|
||||||
|
import (
|
||||||
|
"path"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/derailed/k9s/internal/color"
|
||||||
|
"github.com/derailed/tview"
|
||||||
|
runewidth "github.com/mattn/go-runewidth"
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
// Fqn uniquely describes a container
|
||||||
|
Fqn struct {
|
||||||
|
Namespace, Name, Container string
|
||||||
|
}
|
||||||
|
|
||||||
|
// LogOptions represent logger options.
|
||||||
|
LogOptions struct {
|
||||||
|
Fqn
|
||||||
|
|
||||||
|
Lines int64
|
||||||
|
Color color.Paint
|
||||||
|
Previous bool
|
||||||
|
SingleContainer bool
|
||||||
|
MultiPods bool
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// HasContainer checks if a container is present.
|
||||||
|
func (o LogOptions) HasContainer() bool {
|
||||||
|
return o.Container != ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// FQN returns resource fully qualified name.
|
||||||
|
func (o LogOptions) FQN() string {
|
||||||
|
return FQN(o.Namespace, o.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Path returns resource descriptor path.
|
||||||
|
func (o LogOptions) Path() string {
|
||||||
|
return o.FQN() + ":" + o.Container
|
||||||
|
}
|
||||||
|
|
||||||
|
// FixedSizeName returns a normalize fixed size pod name if possible.
|
||||||
|
func (o LogOptions) FixedSizeName() string {
|
||||||
|
tokens := strings.Split(o.Name, "-")
|
||||||
|
if len(tokens) < 3 {
|
||||||
|
return o.Name
|
||||||
|
}
|
||||||
|
var s []string
|
||||||
|
for i := 0; i < len(tokens)-1; i++ {
|
||||||
|
s = append(s, tokens[i])
|
||||||
|
}
|
||||||
|
return Truncate(strings.Join(s, "-"), 15) + "-" + tokens[len(tokens)-1]
|
||||||
|
}
|
||||||
|
|
||||||
|
func colorize(c color.Paint, txt string) string {
|
||||||
|
if c == 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
return color.Colorize(txt, c)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DecorateLog add a log header to display po/co information along with the log message.
|
||||||
|
func (o LogOptions) DecorateLog(msg string) string {
|
||||||
|
if msg == "" {
|
||||||
|
return msg
|
||||||
|
}
|
||||||
|
|
||||||
|
if o.MultiPods {
|
||||||
|
return colorize(o.Color, o.Name+":"+o.Container+" ") + msg
|
||||||
|
}
|
||||||
|
|
||||||
|
if !o.SingleContainer {
|
||||||
|
return colorize(o.Color, o.Container+" ") + msg
|
||||||
|
}
|
||||||
|
|
||||||
|
return msg
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helpers...
|
||||||
|
|
||||||
|
// BOZO!! Consolidate!!
|
||||||
|
// Truncate a string to the given l and suffix ellipsis if needed.
|
||||||
|
func Truncate(str string, width int) string {
|
||||||
|
return runewidth.Truncate(str, width, string(tview.SemigraphicsHorizontalEllipsis))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Namespaced return a namesapace and a name.
|
||||||
|
func Namespaced(n string) (string, string) {
|
||||||
|
ns, po := path.Split(n)
|
||||||
|
|
||||||
|
return strings.Trim(ns, "/"), po
|
||||||
|
}
|
||||||
|
|
||||||
|
// FQN returns a fully qualified resource name.
|
||||||
|
func FQN(ns, n string) string {
|
||||||
|
if ns == "" {
|
||||||
|
return n
|
||||||
|
}
|
||||||
|
return ns + "/" + n
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
package dao
|
||||||
|
|
@ -0,0 +1,196 @@
|
||||||
|
package dao
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"sync/atomic"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/derailed/k9s/internal"
|
||||||
|
"github.com/derailed/k9s/internal/color"
|
||||||
|
"github.com/derailed/k9s/internal/watch"
|
||||||
|
"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"
|
||||||
|
restclient "k8s.io/client-go/rest"
|
||||||
|
)
|
||||||
|
|
||||||
|
const defaultTimeout = 1 * time.Second
|
||||||
|
|
||||||
|
type Logger interface {
|
||||||
|
Logs(ns, n string, opts *v1.PodLogOptions) *restclient.Request
|
||||||
|
}
|
||||||
|
|
||||||
|
type Pod struct {
|
||||||
|
Resource
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ Accessor = &Pod{}
|
||||||
|
|
||||||
|
// Logs fetch container logs for a given pod and container.
|
||||||
|
func (p *Pod) Logs(ns, n string, opts *v1.PodLogOptions) *restclient.Request {
|
||||||
|
return p.Client().DialOrDie().CoreV1().Pods(ns).GetLogs(n, opts)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Containers returns all container names on pod
|
||||||
|
func (p *Pod) Containers(ns, n string, includeInit bool) ([]string, error) {
|
||||||
|
o, err := p.Get(ns, "v1/pod", n, 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
|
||||||
|
}
|
||||||
|
|
||||||
|
cc := []string{}
|
||||||
|
for _, c := range pod.Spec.Containers {
|
||||||
|
cc = append(cc, c.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
if includeInit {
|
||||||
|
for _, c := range pod.Spec.InitContainers {
|
||||||
|
cc = append(cc, c.Name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return cc, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Logs tails a given container logs
|
||||||
|
func (p *Pod) TailLogs(ctx context.Context, c chan<- string, opts LogOptions) error {
|
||||||
|
if !opts.HasContainer() {
|
||||||
|
return p.logs(ctx, c, opts)
|
||||||
|
}
|
||||||
|
return tailLogs(ctx, p, c, opts)
|
||||||
|
}
|
||||||
|
|
||||||
|
// PodLogs tail logs for all containers in a running Pod.
|
||||||
|
func (p *Pod) logs(ctx context.Context, c chan<- string, opts LogOptions) error {
|
||||||
|
fac, ok := ctx.Value(internal.KeyFactory).(*watch.Factory)
|
||||||
|
if !ok {
|
||||||
|
return errors.New("Expecting an informer")
|
||||||
|
}
|
||||||
|
ns, n := Namespaced(opts.FQN())
|
||||||
|
o, err := fac.Get(ns, "v1/pods", n, labels.Everything())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var po v1.Pod
|
||||||
|
if runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &po); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
opts.Color = asColor(po.Name)
|
||||||
|
if len(po.Spec.InitContainers)+len(po.Spec.Containers) == 1 {
|
||||||
|
opts.SingleContainer = true
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, co := range po.Spec.InitContainers {
|
||||||
|
opts.Container = co.Name
|
||||||
|
if err := p.TailLogs(ctx, c, opts); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
rcos := loggableContainers(po.Status)
|
||||||
|
for _, co := range po.Spec.Containers {
|
||||||
|
if in(rcos, co.Name) {
|
||||||
|
opts.Container = co.Name
|
||||||
|
if err := p.TailLogs(ctx, c, opts); err != nil {
|
||||||
|
log.Error().Err(err).Msgf("Getting logs for %s failed", co.Name)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func tailLogs(ctx context.Context, logger Logger, c chan<- string, opts LogOptions) error {
|
||||||
|
log.Debug().Msgf("Tailing logs for %q -- %q -- %q", opts.Namespace, opts.Name, opts.Container)
|
||||||
|
o := v1.PodLogOptions{
|
||||||
|
Container: opts.Container,
|
||||||
|
Follow: true,
|
||||||
|
TailLines: &opts.Lines,
|
||||||
|
Previous: opts.Previous,
|
||||||
|
}
|
||||||
|
req := logger.Logs(opts.Namespace, opts.Name, &o)
|
||||||
|
ctxt, cancelFunc := context.WithCancel(ctx)
|
||||||
|
req.Context(ctxt)
|
||||||
|
|
||||||
|
var blocked int32 = 1
|
||||||
|
go logsTimeout(cancelFunc, &blocked)
|
||||||
|
|
||||||
|
// This call will block if nothing is in the stream!!
|
||||||
|
stream, err := req.Stream()
|
||||||
|
atomic.StoreInt32(&blocked, 0)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Msgf("Log stream failed for `%s", opts.Path())
|
||||||
|
return fmt.Errorf("Unable to obtain log stream for %s", opts.Path())
|
||||||
|
}
|
||||||
|
go readLogs(ctx, stream, c, opts)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func logsTimeout(cancel context.CancelFunc, blocked *int32) {
|
||||||
|
<-time.After(defaultTimeout)
|
||||||
|
if atomic.LoadInt32(blocked) == 1 {
|
||||||
|
log.Debug().Msg("Timed out reading the log stream")
|
||||||
|
cancel()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func readLogs(ctx context.Context, stream io.ReadCloser, c chan<- string, opts LogOptions) {
|
||||||
|
defer func() {
|
||||||
|
log.Debug().Msgf(">>> Closing stream `%s", opts.Path())
|
||||||
|
if err := stream.Close(); err != nil {
|
||||||
|
log.Error().Err(err).Msg("Cloing stream")
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
scanner := bufio.NewScanner(stream)
|
||||||
|
for scanner.Scan() {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
c <- opts.DecorateLog(scanner.Text())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helpers...
|
||||||
|
|
||||||
|
func loggableContainers(s v1.PodStatus) []string {
|
||||||
|
var rcos []string
|
||||||
|
for _, c := range s.ContainerStatuses {
|
||||||
|
rcos = append(rcos, c.Name)
|
||||||
|
}
|
||||||
|
return rcos
|
||||||
|
}
|
||||||
|
|
||||||
|
func asColor(n string) color.Paint {
|
||||||
|
var sum int
|
||||||
|
for _, r := range n {
|
||||||
|
sum += int(r)
|
||||||
|
}
|
||||||
|
return color.Paint(30 + 2 + sum%6)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if string is in a string list.
|
||||||
|
func in(ll []string, s string) bool {
|
||||||
|
for _, l := range ll {
|
||||||
|
if l == s {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,100 @@
|
||||||
|
package dao
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/derailed/k9s/internal"
|
||||||
|
"github.com/derailed/k9s/internal/model"
|
||||||
|
"github.com/derailed/k9s/internal/render"
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Reconcile previous vs current state and emits delta events.
|
||||||
|
func Reconcile(ctx context.Context, table render.TableData, gvr GVR) (render.TableData, error) {
|
||||||
|
path, ok := ctx.Value(internal.KeySelection).(string)
|
||||||
|
if !ok {
|
||||||
|
return table, fmt.Errorf("no path specified for %s", gvr)
|
||||||
|
}
|
||||||
|
if path != "" {
|
||||||
|
log.Debug().Msgf("########## OVERRIDING NS %q", path)
|
||||||
|
table.Namespace = path
|
||||||
|
}
|
||||||
|
log.Debug().Msgf(" Reconcile %q in ns %q with path %q", gvr, table.Namespace, path)
|
||||||
|
factory, ok := ctx.Value(internal.KeyFactory).(Factory)
|
||||||
|
if !ok {
|
||||||
|
return table, fmt.Errorf("no factory found for %s", gvr)
|
||||||
|
}
|
||||||
|
m, ok := model.Registry[string(gvr)]
|
||||||
|
if !ok {
|
||||||
|
log.Warn().Msgf("Resource %s not found in registry. Going generic!", gvr)
|
||||||
|
m = model.ResourceMeta{
|
||||||
|
Model: &model.Generic{},
|
||||||
|
Renderer: &render.Generic{},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if m.Model == nil {
|
||||||
|
m.Model = &model.Resource{}
|
||||||
|
}
|
||||||
|
m.Model.Init(table.Namespace, string(gvr), factory)
|
||||||
|
|
||||||
|
table.Header = m.Renderer.Header(table.Namespace)
|
||||||
|
oo, err := m.Model.List(ctx)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
log.Debug().Msgf("Model returned [%d] items", len(oo))
|
||||||
|
rows := make(render.Rows, len(oo))
|
||||||
|
if err := m.Model.Hydrate(oo, rows, m.Renderer); err != nil {
|
||||||
|
return table, err
|
||||||
|
}
|
||||||
|
update(&table, rows)
|
||||||
|
|
||||||
|
log.Debug().Msgf("Table returned [%d] events", len(table.RowEvents))
|
||||||
|
return table, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func update(table *render.TableData, rows render.Rows) {
|
||||||
|
cacheEmpty := len(table.RowEvents) == 0
|
||||||
|
kk := make([]string, 0, len(rows))
|
||||||
|
var blankDelta render.DeltaRow
|
||||||
|
for _, row := range rows {
|
||||||
|
kk = append(kk, row.ID)
|
||||||
|
if cacheEmpty {
|
||||||
|
table.RowEvents = append(table.RowEvents, render.NewRowEvent(render.EventAdd, row))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if index, ok := table.RowEvents.FindIndex(row.ID); ok {
|
||||||
|
delta := render.NewDeltaRow(table.RowEvents[index].Row, row)
|
||||||
|
if delta.IsBlank() {
|
||||||
|
table.RowEvents[index].Kind, table.RowEvents[index].Deltas = render.EventUnchanged, blankDelta
|
||||||
|
} else {
|
||||||
|
table.RowEvents[index] = render.NewDeltaRowEvent(row, delta)
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
table.RowEvents = append(table.RowEvents, render.NewRowEvent(render.EventAdd, row))
|
||||||
|
}
|
||||||
|
|
||||||
|
if cacheEmpty {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ensureDeletes(table, kk)
|
||||||
|
}
|
||||||
|
|
||||||
|
// EnsureDeletes delete items in cache that are no longer valid.
|
||||||
|
func ensureDeletes(table *render.TableData, newKeys []string) {
|
||||||
|
for _, re := range table.RowEvents {
|
||||||
|
var found bool
|
||||||
|
for i, key := range newKeys {
|
||||||
|
if key == re.Row.ID {
|
||||||
|
found = true
|
||||||
|
newKeys = append(newKeys[:i], newKeys[i+1:]...)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
table.RowEvents = table.RowEvents.Delete(re.Row.ID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,213 @@
|
||||||
|
package dao
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"sort"
|
||||||
|
|
||||||
|
"github.com/derailed/k9s/internal/watch"
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
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"
|
||||||
|
)
|
||||||
|
|
||||||
|
// MetaViewers represents a collection of meta viewers.
|
||||||
|
type ResourceMetas map[GVR]metav1.APIResource
|
||||||
|
|
||||||
|
var resMetas ResourceMetas
|
||||||
|
|
||||||
|
func AccessorFor(f Factory, gvr GVR) (Accessor, error) {
|
||||||
|
m := map[GVR]Accessor{
|
||||||
|
"contexts": &Context{},
|
||||||
|
"screendumps": &ScreenDump{},
|
||||||
|
"apps/v1/deployments": &Deployment{},
|
||||||
|
"apps/v1/daemonsets": &DaemonSet{},
|
||||||
|
"extensions/v1beta1/daemonsets": &DaemonSet{},
|
||||||
|
"apps/v1/statefulsets": &StatefulSet{},
|
||||||
|
}
|
||||||
|
|
||||||
|
r, ok := m[gvr]
|
||||||
|
if !ok {
|
||||||
|
r = &Resource{}
|
||||||
|
log.Warn().Msgf("No DAO registry entry for %q. Going generic!", gvr)
|
||||||
|
}
|
||||||
|
r.Init(f, gvr)
|
||||||
|
|
||||||
|
return r, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func AllGVRs() []GVR {
|
||||||
|
kk := make(GVRs, 0, len(resMetas))
|
||||||
|
for k := range resMetas {
|
||||||
|
kk = append(kk, k)
|
||||||
|
}
|
||||||
|
sort.Sort(kk)
|
||||||
|
|
||||||
|
return kk
|
||||||
|
}
|
||||||
|
|
||||||
|
// MetaFor returns a resource metadata for a given gvr.
|
||||||
|
func MetaFor(gvr GVR) (metav1.APIResource, error) {
|
||||||
|
m, ok := resMetas[gvr]
|
||||||
|
if !ok {
|
||||||
|
return metav1.APIResource{}, fmt.Errorf("no resource meta defined for %q", gvr)
|
||||||
|
}
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load hydrates server preferred+CRDs resource metadata.
|
||||||
|
func Load(f *watch.Factory) error {
|
||||||
|
resMetas = make(ResourceMetas, 100)
|
||||||
|
if err := loadPreferred(f, resMetas); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := loadNonResource(resMetas); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return loadCRDs(f, resMetas)
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadNonResource(m ResourceMetas) error {
|
||||||
|
m["contexts"] = metav1.APIResource{
|
||||||
|
Name: "contexts",
|
||||||
|
SingularName: "context",
|
||||||
|
Namespaced: false,
|
||||||
|
Kind: "Context",
|
||||||
|
ShortNames: []string{"ctx"},
|
||||||
|
Verbs: []string{},
|
||||||
|
Categories: []string{"K9s"},
|
||||||
|
}
|
||||||
|
m["screendumps"] = metav1.APIResource{
|
||||||
|
Name: "screendumps",
|
||||||
|
SingularName: "screendump",
|
||||||
|
Namespaced: false,
|
||||||
|
Kind: "ScreenDump",
|
||||||
|
ShortNames: []string{"sd"},
|
||||||
|
Verbs: []string{"delete"},
|
||||||
|
Categories: []string{"K9s"},
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadPreferred(f *watch.Factory, m ResourceMetas) error {
|
||||||
|
discovery, err := f.Client().CachedDiscovery()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
rr, err := discovery.ServerPreferredResources()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for _, r := range rr {
|
||||||
|
for _, res := range r.APIResources {
|
||||||
|
gvr := FromGVAndR(r.GroupVersion, res.Name)
|
||||||
|
res.Group, res.Version = gvr.ToG(), gvr.ToV()
|
||||||
|
m[gvr] = res
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadCRDs(f *watch.Factory, m ResourceMetas) error {
|
||||||
|
oo, err := f.List("", "apiextensions.k8s.io/v1beta1/customresourcedefinitions", labels.Everything())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
f.WaitForCacheSync()
|
||||||
|
|
||||||
|
for _, o := range oo {
|
||||||
|
meta, errs := extractMeta(o)
|
||||||
|
if len(errs) > 0 {
|
||||||
|
log.Error().Err(errs[0]).Msgf("Fail to extract CRD meta (%d) errors", len(errs))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
gvr := NewGVR(meta.Group, meta.Version, meta.Name)
|
||||||
|
m[gvr] = meta
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func extractMeta(o runtime.Object) (metav1.APIResource, []error) {
|
||||||
|
var (
|
||||||
|
m metav1.APIResource
|
||||||
|
errs []error
|
||||||
|
)
|
||||||
|
|
||||||
|
crd, ok := o.(*unstructured.Unstructured)
|
||||||
|
if !ok {
|
||||||
|
return m, append(errs, fmt.Errorf("Expected CustomResourceDefinition, but got %T", o))
|
||||||
|
}
|
||||||
|
|
||||||
|
var spec map[string]interface{}
|
||||||
|
spec, errs = extractMap(crd.Object, "spec", errs)
|
||||||
|
|
||||||
|
var meta map[string]interface{}
|
||||||
|
meta, errs = extractMap(crd.Object, "metadata", errs)
|
||||||
|
|
||||||
|
m.Name, errs = extractStr(meta, "name", errs)
|
||||||
|
m.Group, errs = extractStr(spec, "group", errs)
|
||||||
|
m.Version, errs = extractStr(spec, "version", errs)
|
||||||
|
|
||||||
|
var scope string
|
||||||
|
scope, errs = extractStr(spec, "scope", errs)
|
||||||
|
|
||||||
|
m.Namespaced = isNamespaced(scope)
|
||||||
|
|
||||||
|
var names map[string]interface{}
|
||||||
|
names, errs = extractMap(spec, "names", errs)
|
||||||
|
m.Kind, errs = extractStr(names, "kind", errs)
|
||||||
|
m.SingularName, errs = extractStr(names, "singular", errs)
|
||||||
|
m.Name, errs = extractStr(names, "plural", errs)
|
||||||
|
m.ShortNames, errs = extractSlice(names, "shortNames", errs)
|
||||||
|
|
||||||
|
return m, errs
|
||||||
|
}
|
||||||
|
|
||||||
|
func isNamespaced(scope string) bool {
|
||||||
|
return scope == "Namespaced"
|
||||||
|
}
|
||||||
|
|
||||||
|
func extractSlice(m map[string]interface{}, n string, errs []error) ([]string, []error) {
|
||||||
|
if m[n] == nil {
|
||||||
|
return nil, errs
|
||||||
|
}
|
||||||
|
s, ok := m[n].([]string)
|
||||||
|
if ok {
|
||||||
|
return s, errs
|
||||||
|
}
|
||||||
|
|
||||||
|
ii, ok := m[n].([]interface{})
|
||||||
|
if !ok {
|
||||||
|
return s, append(errs, fmt.Errorf("failed to extract slice %s -- %#v", n, m))
|
||||||
|
}
|
||||||
|
|
||||||
|
ss := make([]string, len(ii))
|
||||||
|
for i, name := range ii {
|
||||||
|
ss[i], ok = name.(string)
|
||||||
|
if !ok {
|
||||||
|
return s, append(errs, fmt.Errorf("expecting string shortnames"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return s, errs
|
||||||
|
}
|
||||||
|
|
||||||
|
func extractStr(m map[string]interface{}, n string, errs []error) (string, []error) {
|
||||||
|
s, ok := m[n].(string)
|
||||||
|
if !ok {
|
||||||
|
return s, append(errs, fmt.Errorf("failed to extract string %s", n))
|
||||||
|
}
|
||||||
|
return s, errs
|
||||||
|
}
|
||||||
|
|
||||||
|
func extractMap(m map[string]interface{}, n string, errs []error) (map[string]interface{}, []error) {
|
||||||
|
v, ok := m[n].(map[string]interface{})
|
||||||
|
if !ok {
|
||||||
|
return v, append(errs, fmt.Errorf("failed to extract field %s", n))
|
||||||
|
}
|
||||||
|
return v, errs
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,32 @@
|
||||||
|
package dao
|
||||||
|
|
||||||
|
import (
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
"k8s.io/client-go/dynamic"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Resource struct {
|
||||||
|
Factory
|
||||||
|
|
||||||
|
gvr GVR
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Resource) Init(f Factory, gvr GVR) {
|
||||||
|
r.Factory, r.gvr = f, gvr
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete a Generic.
|
||||||
|
func (r *Resource) Delete(ns, n string, cascade, force bool) error {
|
||||||
|
p := metav1.DeletePropagationOrphan
|
||||||
|
if cascade {
|
||||||
|
p = metav1.DeletePropagationBackground
|
||||||
|
}
|
||||||
|
|
||||||
|
return r.dynClient().Namespace(ns).Delete(n, &metav1.DeleteOptions{
|
||||||
|
PropagationPolicy: &p,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Resource) dynClient() dynamic.NamespaceableResourceInterface {
|
||||||
|
return r.Client().DynDialOrDie().Resource(r.gvr.AsGVR())
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
package dao
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ScreenDump struct {
|
||||||
|
Resource
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ Accessor = &ScreenDump{}
|
||||||
|
var _ Nuker = &ScreenDump{}
|
||||||
|
|
||||||
|
// Delete a ScreenDump.
|
||||||
|
func (d *ScreenDump) Delete(dir, sel string, cascade, force bool) error {
|
||||||
|
log.Debug().Msgf("ScreenDump DELETE %q:%q", dir, sel)
|
||||||
|
return os.Remove(filepath.Join("/"+dir, sel))
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,80 @@
|
||||||
|
package dao
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
appsv1 "k8s.io/api/apps/v1"
|
||||||
|
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"
|
||||||
|
"k8s.io/apimachinery/pkg/types"
|
||||||
|
"k8s.io/kubectl/pkg/polymorphichelpers"
|
||||||
|
)
|
||||||
|
|
||||||
|
type StatefulSet struct {
|
||||||
|
Resource
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ Accessor = &StatefulSet{}
|
||||||
|
var _ Loggable = &StatefulSet{}
|
||||||
|
var _ Restartable = &StatefulSet{}
|
||||||
|
var _ Scalable = &StatefulSet{}
|
||||||
|
|
||||||
|
// Scale a StatefulSet.
|
||||||
|
func (s *StatefulSet) Scale(ns, n string, replicas int32) error {
|
||||||
|
scale, err := s.Client().DialOrDie().AppsV1().StatefulSets(ns).GetScale(n, metav1.GetOptions{})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
scale.Spec.Replicas = replicas
|
||||||
|
_, err = s.Client().DialOrDie().AppsV1().StatefulSets(ns).UpdateScale(n, scale)
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Restart a StatefulSet rollout.
|
||||||
|
func (s *StatefulSet) Restart(ns, n string) error {
|
||||||
|
o, err := s.Get(ns, string(s.gvr), n, labels.Everything())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var ds appsv1.StatefulSet
|
||||||
|
err = runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &ds)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
update, err := polymorphichelpers.ObjectRestarterFn(&ds)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = s.Client().DialOrDie().AppsV1().StatefulSets(ns).Patch(ds.Name, types.StrategicMergePatchType, update)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Logs tail logs for all pods represented by this StatefulSet.
|
||||||
|
func (s *StatefulSet) TailLogs(ctx context.Context, c chan<- string, opts LogOptions) error {
|
||||||
|
log.Debug().Msgf("Tailing StatefulSet %q -- %q", opts.Namespace, opts.Name)
|
||||||
|
o, err := s.Get(opts.Namespace, string(s.gvr), opts.Name, labels.Everything())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var dp appsv1.StatefulSet
|
||||||
|
err = runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &dp)
|
||||||
|
if err != nil {
|
||||||
|
return errors.New("expecting StatefulSet resource")
|
||||||
|
}
|
||||||
|
|
||||||
|
if dp.Spec.Selector == nil || len(dp.Spec.Selector.MatchLabels) == 0 {
|
||||||
|
return fmt.Errorf("No valid selector found on StatefulSet %s", opts.FQN())
|
||||||
|
}
|
||||||
|
|
||||||
|
return podLogs(ctx, c, dp.Spec.Selector.MatchLabels, opts)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,64 @@
|
||||||
|
package dao
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/derailed/k9s/internal/k8s"
|
||||||
|
"k8s.io/apimachinery/pkg/labels"
|
||||||
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||||
|
"k8s.io/client-go/informers"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Factory interface {
|
||||||
|
// Client retrieves an api client.
|
||||||
|
Client() k8s.Connection
|
||||||
|
|
||||||
|
// Get fetch a given resource.
|
||||||
|
Get(ns, gvr, n string, sel labels.Selector) (runtime.Object, error)
|
||||||
|
|
||||||
|
// List fetch a collection of resources.
|
||||||
|
List(ns, gvr string, sel labels.Selector) ([]runtime.Object, error)
|
||||||
|
|
||||||
|
// ForResource fetch an informer for a given resource.
|
||||||
|
ForResource(ns, gvr string) informers.GenericInformer
|
||||||
|
|
||||||
|
// WaitForCacheSync synchronize the cache.
|
||||||
|
WaitForCacheSync() map[schema.GroupVersionResource]bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// Accessor represents an accessible k8s resource.
|
||||||
|
type Accessor interface {
|
||||||
|
Nuker
|
||||||
|
|
||||||
|
// Init the resource with a factory object.
|
||||||
|
Init(Factory, GVR)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Loggable represents resources with logs.
|
||||||
|
type Loggable interface {
|
||||||
|
// TaiLogs streams resource logs.
|
||||||
|
TailLogs(ctx context.Context, c chan<- string, opts LogOptions) error
|
||||||
|
}
|
||||||
|
|
||||||
|
type Scalable interface {
|
||||||
|
Scale(ns, n string, replicas int32) error
|
||||||
|
}
|
||||||
|
|
||||||
|
// Nuker represents a resource deleter.
|
||||||
|
type Nuker interface {
|
||||||
|
// Delete removes a resource from the api server.
|
||||||
|
Delete(ns, n string, cascade, force bool) error
|
||||||
|
}
|
||||||
|
|
||||||
|
// Switchable represents a switchable resource.
|
||||||
|
type Switchable interface {
|
||||||
|
// Switch changes the active context.
|
||||||
|
Switch(ctx string) error
|
||||||
|
}
|
||||||
|
|
||||||
|
// Restartable represents a restartable resource.
|
||||||
|
type Restartable interface {
|
||||||
|
// Restart performs a rollout restart.
|
||||||
|
Restart(ns, n string) error
|
||||||
|
}
|
||||||
|
|
@ -6,8 +6,6 @@ import (
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"k8s.io/client-go/discovery/cached/disk"
|
|
||||||
|
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
authorizationv1 "k8s.io/api/authorization/v1"
|
authorizationv1 "k8s.io/api/authorization/v1"
|
||||||
v1 "k8s.io/api/core/v1"
|
v1 "k8s.io/api/core/v1"
|
||||||
|
|
@ -15,6 +13,7 @@ import (
|
||||||
"k8s.io/apimachinery/pkg/fields"
|
"k8s.io/apimachinery/pkg/fields"
|
||||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||||
"k8s.io/apimachinery/pkg/version"
|
"k8s.io/apimachinery/pkg/version"
|
||||||
|
"k8s.io/client-go/discovery/cached/disk"
|
||||||
"k8s.io/client-go/dynamic"
|
"k8s.io/client-go/dynamic"
|
||||||
"k8s.io/client-go/kubernetes"
|
"k8s.io/client-go/kubernetes"
|
||||||
restclient "k8s.io/client-go/rest"
|
restclient "k8s.io/client-go/rest"
|
||||||
|
|
@ -107,7 +106,6 @@ func (a *APIClient) CheckNSAccess(n string) error {
|
||||||
|
|
||||||
func makeSAR(ns, gvr string) *authorizationv1.SelfSubjectAccessReview {
|
func makeSAR(ns, gvr string) *authorizationv1.SelfSubjectAccessReview {
|
||||||
res := GVR(gvr).AsGVR()
|
res := GVR(gvr).AsGVR()
|
||||||
log.Debug().Msgf("GVR for %s -- %#v", gvr, res)
|
|
||||||
return &authorizationv1.SelfSubjectAccessReview{
|
return &authorizationv1.SelfSubjectAccessReview{
|
||||||
Spec: authorizationv1.SelfSubjectAccessReviewSpec{
|
Spec: authorizationv1.SelfSubjectAccessReviewSpec{
|
||||||
ResourceAttributes: &authorizationv1.ResourceAttributes{
|
ResourceAttributes: &authorizationv1.ResourceAttributes{
|
||||||
|
|
@ -168,6 +166,7 @@ func (a *APIClient) ValidNamespaces() ([]v1.Namespace, error) {
|
||||||
|
|
||||||
// NodePods returns a collection of all available pods on a given node.
|
// NodePods returns a collection of all available pods on a given node.
|
||||||
func (a *APIClient) NodePods(node string) (*v1.PodList, error) {
|
func (a *APIClient) NodePods(node string) (*v1.PodList, error) {
|
||||||
|
panic("NYI")
|
||||||
const selFmt = "spec.nodeName=%s,status.phase!=%s,status.phase!=%s"
|
const selFmt = "spec.nodeName=%s,status.phase!=%s,status.phase!=%s"
|
||||||
fieldSelector, err := fields.ParseSelector(fmt.Sprintf(selFmt, node, v1.PodSucceeded, v1.PodFailed))
|
fieldSelector, err := fields.ParseSelector(fmt.Sprintf(selFmt, node, v1.PodSucceeded, v1.PodFailed))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
||||||
|
|
@ -17,11 +17,13 @@ func NewClusterRole(c Connection) *ClusterRole {
|
||||||
|
|
||||||
// Get a cluster role.
|
// Get a cluster role.
|
||||||
func (c *ClusterRole) Get(_, n string) (interface{}, error) {
|
func (c *ClusterRole) Get(_, n string) (interface{}, error) {
|
||||||
|
panic("NYI")
|
||||||
return c.DialOrDie().RbacV1().ClusterRoles().Get(n, metav1.GetOptions{})
|
return c.DialOrDie().RbacV1().ClusterRoles().Get(n, metav1.GetOptions{})
|
||||||
}
|
}
|
||||||
|
|
||||||
// List all ClusterRoles on a cluster.
|
// List all ClusterRoles on a cluster.
|
||||||
func (c *ClusterRole) List(ns string, opts metav1.ListOptions) (Collection, error) {
|
func (c *ClusterRole) List(ns string, opts metav1.ListOptions) (Collection, error) {
|
||||||
|
panic("NYI")
|
||||||
rr, err := c.DialOrDie().RbacV1().ClusterRoles().List(opts)
|
rr, err := c.DialOrDie().RbacV1().ClusterRoles().List(opts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|
|
||||||
|
|
@ -17,11 +17,13 @@ func NewClusterRoleBinding(c Connection) *ClusterRoleBinding {
|
||||||
|
|
||||||
// Get a service.
|
// Get a service.
|
||||||
func (c *ClusterRoleBinding) Get(_, n string) (interface{}, error) {
|
func (c *ClusterRoleBinding) Get(_, n string) (interface{}, error) {
|
||||||
|
panic("NYI")
|
||||||
return c.DialOrDie().RbacV1().ClusterRoleBindings().Get(n, metav1.GetOptions{})
|
return c.DialOrDie().RbacV1().ClusterRoleBindings().Get(n, metav1.GetOptions{})
|
||||||
}
|
}
|
||||||
|
|
||||||
// List all ClusterRoleBindings on a cluster.
|
// List all ClusterRoleBindings on a cluster.
|
||||||
func (c *ClusterRoleBinding) List(ns string, opts metav1.ListOptions) (Collection, error) {
|
func (c *ClusterRoleBinding) List(ns string, opts metav1.ListOptions) (Collection, error) {
|
||||||
|
panic("NYI")
|
||||||
rr, err := c.DialOrDie().RbacV1().ClusterRoleBindings().List(opts)
|
rr, err := c.DialOrDie().RbacV1().ClusterRoleBindings().List(opts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return Collection{}, err
|
return Collection{}, err
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,6 @@ import (
|
||||||
|
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
|
||||||
"k8s.io/client-go/tools/clientcmd"
|
"k8s.io/client-go/tools/clientcmd"
|
||||||
"k8s.io/client-go/tools/clientcmd/api"
|
"k8s.io/client-go/tools/clientcmd/api"
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -19,11 +19,13 @@ func NewDeployment(c Connection) *Deployment {
|
||||||
|
|
||||||
// Get a deployment.
|
// Get a deployment.
|
||||||
func (d *Deployment) Get(ns, n string) (interface{}, error) {
|
func (d *Deployment) Get(ns, n string) (interface{}, error) {
|
||||||
|
panic("NYI")
|
||||||
return d.DialOrDie().AppsV1().Deployments(ns).Get(n, metav1.GetOptions{})
|
return d.DialOrDie().AppsV1().Deployments(ns).Get(n, metav1.GetOptions{})
|
||||||
}
|
}
|
||||||
|
|
||||||
// List all Deployments in a given namespace.
|
// List all Deployments in a given namespace.
|
||||||
func (d *Deployment) List(ns string, opts metav1.ListOptions) (Collection, error) {
|
func (d *Deployment) List(ns string, opts metav1.ListOptions) (Collection, error) {
|
||||||
|
panic("NYI")
|
||||||
rr, err := d.DialOrDie().AppsV1().Deployments(ns).List(opts)
|
rr, err := d.DialOrDie().AppsV1().Deployments(ns).List(opts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|
|
||||||
|
|
@ -1,64 +1,73 @@
|
||||||
package k8s
|
package k8s
|
||||||
|
|
||||||
import (
|
// BOZO!!
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
// import (
|
||||||
"k8s.io/apimachinery/pkg/types"
|
// metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
"k8s.io/kubectl/pkg/polymorphichelpers"
|
// "k8s.io/apimachinery/pkg/types"
|
||||||
)
|
// "k8s.io/kubectl/pkg/polymorphichelpers"
|
||||||
|
// )
|
||||||
|
|
||||||
// DaemonSet represents a Kubernetes DaemonSet
|
// // DaemonSet represents a Kubernetes DaemonSet
|
||||||
type DaemonSet struct {
|
// type DaemonSet struct {
|
||||||
*base
|
// *base
|
||||||
Connection
|
// Connection
|
||||||
}
|
// }
|
||||||
|
|
||||||
// NewDaemonSet returns a new DaemonSet.
|
// // NewDaemonSet returns a new DaemonSet.
|
||||||
func NewDaemonSet(c Connection) *DaemonSet {
|
// func NewDaemonSet(c Connection) *DaemonSet {
|
||||||
return &DaemonSet{&base{}, c}
|
// return &DaemonSet{&base{}, c}
|
||||||
}
|
// }
|
||||||
|
|
||||||
// Get a DaemonSet.
|
// // Get a DaemonSet.
|
||||||
func (d *DaemonSet) Get(ns, n string) (interface{}, error) {
|
// func (d *DaemonSet) Get(ns, n string) (interface{}, error) {
|
||||||
return d.DialOrDie().AppsV1().DaemonSets(ns).Get(n, metav1.GetOptions{})
|
// panic("NYI")
|
||||||
}
|
// return d.DialOrDie().AppsV1().DaemonSets(ns).Get(n, metav1.GetOptions{})
|
||||||
|
// }
|
||||||
|
|
||||||
// List all DaemonSets in a given namespace.
|
// // List all DaemonSets in a given namespace.
|
||||||
func (d *DaemonSet) List(ns string, opts metav1.ListOptions) (Collection, error) {
|
// func (d *DaemonSet) List(ns string, opts metav1.ListOptions) (Collection, error) {
|
||||||
rr, err := d.DialOrDie().AppsV1().DaemonSets(ns).List(opts)
|
// panic("NYI")
|
||||||
if err != nil {
|
// rr, err := d.DialOrDie().AppsV1().DaemonSets(ns).List(opts)
|
||||||
return nil, err
|
// if err != nil {
|
||||||
}
|
// return nil, err
|
||||||
cc := make(Collection, len(rr.Items))
|
// }
|
||||||
for i, r := range rr.Items {
|
// cc := make(Collection, len(rr.Items))
|
||||||
cc[i] = r
|
// for i, r := range rr.Items {
|
||||||
}
|
// cc[i] = r
|
||||||
|
// }
|
||||||
|
|
||||||
return cc, nil
|
// return cc, nil
|
||||||
}
|
// }
|
||||||
|
|
||||||
// Delete a DaemonSet.
|
// // Delete a DaemonSet.
|
||||||
func (d *DaemonSet) Delete(ns, n string, cascade, force bool) error {
|
// func (d *DaemonSet) Delete(ns, n string, cascade, force bool) error {
|
||||||
p := metav1.DeletePropagationOrphan
|
// p := metav1.DeletePropagationOrphan
|
||||||
if cascade {
|
// if cascade {
|
||||||
p = metav1.DeletePropagationBackground
|
// p = metav1.DeletePropagationBackground
|
||||||
}
|
// }
|
||||||
return d.DialOrDie().AppsV1().DaemonSets(ns).Delete(n, &metav1.DeleteOptions{
|
// return d.DialOrDie().AppsV1().DaemonSets(ns).Delete(n, &metav1.DeleteOptions{
|
||||||
PropagationPolicy: &p,
|
// PropagationPolicy: &p,
|
||||||
})
|
// })
|
||||||
}
|
// }
|
||||||
|
|
||||||
// Restart a DaemonSet rollout.
|
// // Restart a DaemonSet rollout.
|
||||||
func (d *DaemonSet) Restart(ns, n string) error {
|
// func (d *DaemonSet) Restart(f *watch.Factory, ns, n string) error {
|
||||||
|
// o, err := f.Get(ns, "apps/v1/deamonsets", n, labels.Everything())
|
||||||
|
// if err != nil {
|
||||||
|
// return err
|
||||||
|
// }
|
||||||
|
|
||||||
ds, err := d.DialOrDie().AppsV1().DaemonSets(ns).Get(n, metav1.GetOptions{})
|
// var ds appsv1.DaemonSet
|
||||||
if err != nil {
|
// err = runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &ds)
|
||||||
return err
|
// if err != nil {
|
||||||
}
|
// return err
|
||||||
update, err := polymorphichelpers.ObjectRestarterFn(ds)
|
// }
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = d.DialOrDie().AppsV1().DaemonSets(ns).Patch(ds.Name, types.StrategicMergePatchType, update)
|
// update, err := polymorphichelpers.ObjectRestarterFn(ds)
|
||||||
return err
|
// if err != nil {
|
||||||
}
|
// return err
|
||||||
|
// }
|
||||||
|
|
||||||
|
// _, err = f.Client().DialOrDie().AppsV1().DaemonSets(ns).Patch(ds.Name, types.StrategicMergePatchType, update)
|
||||||
|
// return err
|
||||||
|
// }
|
||||||
|
|
|
||||||
|
|
@ -25,8 +25,8 @@ func (g GVR) ResName() string {
|
||||||
return g.ToR() + "." + g.ToV() + "." + g.ToG()
|
return g.ToR() + "." + g.ToV() + "." + g.ToG()
|
||||||
}
|
}
|
||||||
|
|
||||||
// AsGR returns the group version.
|
// AsGV returns the group version.
|
||||||
func (g GVR) AsGR() schema.GroupVersion {
|
func (g GVR) AsGV() schema.GroupVersion {
|
||||||
return schema.GroupVersion{
|
return schema.GroupVersion{
|
||||||
Group: g.ToG(),
|
Group: g.ToG(),
|
||||||
Version: g.ToV(),
|
Version: g.ToV(),
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ import (
|
||||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestAsGR(t *testing.T) {
|
func TestAsGV(t *testing.T) {
|
||||||
uu := map[string]struct {
|
uu := map[string]struct {
|
||||||
gvr string
|
gvr string
|
||||||
e schema.GroupVersion
|
e schema.GroupVersion
|
||||||
|
|
@ -21,7 +21,7 @@ func TestAsGR(t *testing.T) {
|
||||||
for k := range uu {
|
for k := range uu {
|
||||||
u := uu[k]
|
u := uu[k]
|
||||||
t.Run(k, func(t *testing.T) {
|
t.Run(k, func(t *testing.T) {
|
||||||
assert.Equal(t, u.e, k8s.GVR(u.gvr).AsGR())
|
assert.Equal(t, u.e, k8s.GVR(u.gvr).AsGV())
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -73,7 +73,6 @@ func (r *RestMapper) resourceFor(resourceArg string) (schema.GroupVersionResourc
|
||||||
}
|
}
|
||||||
|
|
||||||
fullGVR, gr := schema.ParseResourceArg(strings.ToLower(resourceArg))
|
fullGVR, gr := schema.ParseResourceArg(strings.ToLower(resourceArg))
|
||||||
log.Debug().Msgf("GVR %#v -- %#v", fullGVR, gr)
|
|
||||||
if fullGVR != nil {
|
if fullGVR != nil {
|
||||||
return mapper.ResourceFor(*fullGVR)
|
return mapper.ResourceFor(*fullGVR)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,11 +17,13 @@ func NewNamespace(c Connection) *Namespace {
|
||||||
|
|
||||||
// Get a active namespace.
|
// Get a active namespace.
|
||||||
func (n *Namespace) Get(_, name string) (interface{}, error) {
|
func (n *Namespace) Get(_, name string) (interface{}, error) {
|
||||||
|
panic("NYI")
|
||||||
return n.DialOrDie().CoreV1().Namespaces().Get(name, metav1.GetOptions{})
|
return n.DialOrDie().CoreV1().Namespaces().Get(name, metav1.GetOptions{})
|
||||||
}
|
}
|
||||||
|
|
||||||
// List all active namespaces on the cluster.
|
// List all active namespaces on the cluster.
|
||||||
func (n *Namespace) List(ns string, opts metav1.ListOptions) (Collection, error) {
|
func (n *Namespace) List(ns string, opts metav1.ListOptions) (Collection, error) {
|
||||||
|
panic("NYI")
|
||||||
rr, err := n.DialOrDie().CoreV1().Namespaces().List(opts)
|
rr, err := n.DialOrDie().CoreV1().Namespaces().List(opts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|
|
||||||
|
|
@ -22,11 +22,14 @@ func NewPod(c Connection) *Pod {
|
||||||
|
|
||||||
// Get a pod.
|
// Get a pod.
|
||||||
func (p *Pod) Get(ns, name string) (interface{}, error) {
|
func (p *Pod) Get(ns, name string) (interface{}, error) {
|
||||||
|
panic("POd GEt")
|
||||||
return p.DialOrDie().CoreV1().Pods(ns).Get(name, metav1.GetOptions{})
|
return p.DialOrDie().CoreV1().Pods(ns).Get(name, metav1.GetOptions{})
|
||||||
}
|
}
|
||||||
|
|
||||||
// List all pods in a given namespace.
|
// List all pods in a given namespace.
|
||||||
func (p *Pod) List(ns string, opts metav1.ListOptions) (Collection, error) {
|
func (p *Pod) List(ns string, opts metav1.ListOptions) (Collection, error) {
|
||||||
|
panic("POd List")
|
||||||
|
|
||||||
rr, err := p.DialOrDie().CoreV1().Pods(ns).List(opts)
|
rr, err := p.DialOrDie().CoreV1().Pods(ns).List(opts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|
|
||||||
|
|
@ -76,7 +76,7 @@ func (r *Resource) listAll(ns, n string) (runtime.Object, error) {
|
||||||
|
|
||||||
func (r *Resource) getClient() (*rest.RESTClient, error) {
|
func (r *Resource) getClient() (*rest.RESTClient, error) {
|
||||||
crConfig := r.RestConfigOrDie()
|
crConfig := r.RestConfigOrDie()
|
||||||
gv := r.gvr.AsGR()
|
gv := r.gvr.AsGV()
|
||||||
crConfig.GroupVersion = &gv
|
crConfig.GroupVersion = &gv
|
||||||
crConfig.APIPath = "/apis"
|
crConfig.APIPath = "/apis"
|
||||||
if len(r.gvr.ToG()) == 0 {
|
if len(r.gvr.ToG()) == 0 {
|
||||||
|
|
@ -94,7 +94,7 @@ func (r *Resource) getClient() (*rest.RESTClient, error) {
|
||||||
|
|
||||||
func (r *Resource) codec() (serializer.CodecFactory, runtime.ParameterCodec) {
|
func (r *Resource) codec() (serializer.CodecFactory, runtime.ParameterCodec) {
|
||||||
scheme := runtime.NewScheme()
|
scheme := runtime.NewScheme()
|
||||||
gv := r.gvr.AsGR()
|
gv := r.gvr.AsGV()
|
||||||
metav1.AddToGroupVersion(scheme, gv)
|
metav1.AddToGroupVersion(scheme, gv)
|
||||||
scheme.AddKnownTypes(gv, &metav1beta1.Table{}, &metav1beta1.TableOptions{})
|
scheme.AddKnownTypes(gv, &metav1beta1.Table{}, &metav1beta1.TableOptions{})
|
||||||
scheme.AddKnownTypes(metav1beta1.SchemeGroupVersion, &metav1beta1.Table{}, &metav1beta1.TableOptions{})
|
scheme.AddKnownTypes(metav1beta1.SchemeGroupVersion, &metav1beta1.Table{}, &metav1beta1.TableOptions{})
|
||||||
|
|
|
||||||
|
|
@ -18,11 +18,13 @@ func NewRole(c Connection) *Role {
|
||||||
|
|
||||||
// Get a Role.
|
// Get a Role.
|
||||||
func (r *Role) Get(ns, n string) (interface{}, error) {
|
func (r *Role) Get(ns, n string) (interface{}, error) {
|
||||||
|
panic("NYI")
|
||||||
return r.DialOrDie().RbacV1().Roles(ns).Get(n, metav1.GetOptions{})
|
return r.DialOrDie().RbacV1().Roles(ns).Get(n, metav1.GetOptions{})
|
||||||
}
|
}
|
||||||
|
|
||||||
// List all Roles in a given namespace.
|
// List all Roles in a given namespace.
|
||||||
func (r *Role) List(ns string, opts metav1.ListOptions) (Collection, error) {
|
func (r *Role) List(ns string, opts metav1.ListOptions) (Collection, error) {
|
||||||
|
panic("NYI")
|
||||||
rr, err := r.DialOrDie().RbacV1().Roles(ns).List(opts)
|
rr, err := r.DialOrDie().RbacV1().Roles(ns).List(opts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|
|
||||||
|
|
@ -15,11 +15,13 @@ func NewRoleBinding(c Connection) *RoleBinding {
|
||||||
|
|
||||||
// Get a RoleBinding.
|
// Get a RoleBinding.
|
||||||
func (r *RoleBinding) Get(ns, n string) (interface{}, error) {
|
func (r *RoleBinding) Get(ns, n string) (interface{}, error) {
|
||||||
|
panic("NYI")
|
||||||
return r.DialOrDie().RbacV1().RoleBindings(ns).Get(n, metav1.GetOptions{})
|
return r.DialOrDie().RbacV1().RoleBindings(ns).Get(n, metav1.GetOptions{})
|
||||||
}
|
}
|
||||||
|
|
||||||
// List all RoleBindings in a given namespace.
|
// List all RoleBindings in a given namespace.
|
||||||
func (r *RoleBinding) List(ns string, opts metav1.ListOptions) (Collection, error) {
|
func (r *RoleBinding) List(ns string, opts metav1.ListOptions) (Collection, error) {
|
||||||
|
panic("NYI")
|
||||||
rr, err := r.DialOrDie().RbacV1().RoleBindings(ns).List(opts)
|
rr, err := r.DialOrDie().RbacV1().RoleBindings(ns).List(opts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|
|
||||||
|
|
@ -17,11 +17,13 @@ func NewService(c Connection) *Service {
|
||||||
|
|
||||||
// Get a service.
|
// Get a service.
|
||||||
func (s *Service) Get(ns, n string) (interface{}, error) {
|
func (s *Service) Get(ns, n string) (interface{}, error) {
|
||||||
|
panic("NYI")
|
||||||
return s.DialOrDie().CoreV1().Services(ns).Get(n, metav1.GetOptions{})
|
return s.DialOrDie().CoreV1().Services(ns).Get(n, metav1.GetOptions{})
|
||||||
}
|
}
|
||||||
|
|
||||||
// List all Services in a given namespace.
|
// List all Services in a given namespace.
|
||||||
func (s *Service) List(ns string, opts metav1.ListOptions) (Collection, error) {
|
func (s *Service) List(ns string, opts metav1.ListOptions) (Collection, error) {
|
||||||
|
panic("NYI")
|
||||||
rr, err := s.DialOrDie().CoreV1().Services(ns).List(opts)
|
rr, err := s.DialOrDie().CoreV1().Services(ns).List(opts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
package internal
|
||||||
|
|
||||||
|
// ContextKey represents context key.
|
||||||
|
type ContextKey string
|
||||||
|
|
||||||
|
const (
|
||||||
|
// Factory represents a factory context key.
|
||||||
|
KeyFactory ContextKey = "factory"
|
||||||
|
KeySelection = "selection"
|
||||||
|
KeyLabels = "labels"
|
||||||
|
KeyFields = "fields"
|
||||||
|
KeyTable = "table"
|
||||||
|
KeyDir = "dir"
|
||||||
|
)
|
||||||
|
|
@ -1,6 +1,9 @@
|
||||||
package model
|
package model
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/derailed/k9s/internal"
|
||||||
"github.com/derailed/k9s/internal/k8s"
|
"github.com/derailed/k9s/internal/k8s"
|
||||||
"github.com/derailed/k9s/internal/render"
|
"github.com/derailed/k9s/internal/render"
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
|
|
@ -9,6 +12,7 @@ import (
|
||||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||||
"k8s.io/apimachinery/pkg/labels"
|
"k8s.io/apimachinery/pkg/labels"
|
||||||
"k8s.io/apimachinery/pkg/runtime"
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||||
mv1beta1 "k8s.io/metrics/pkg/apis/metrics/v1beta1"
|
mv1beta1 "k8s.io/metrics/pkg/apis/metrics/v1beta1"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -16,18 +20,15 @@ var _ render.ContainerWithMetrics = &ContainerWithMetrics{}
|
||||||
|
|
||||||
// Container represents a container model.
|
// Container represents a container model.
|
||||||
type Container struct {
|
type Container struct {
|
||||||
*Resource
|
Resource
|
||||||
}
|
|
||||||
|
|
||||||
// NewContainer returns a new container model
|
pod *v1.Pod
|
||||||
func NewContainer() *Container {
|
|
||||||
return &Container{
|
|
||||||
Resource: NewResource(),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// List returns a collection of containers
|
// List returns a collection of containers
|
||||||
func (c *Container) List(sel string) ([]runtime.Object, error) {
|
func (c *Container) List(ctx context.Context) ([]runtime.Object, error) {
|
||||||
|
c.pod = nil
|
||||||
|
sel := ctx.Value(internal.KeySelection).(string)
|
||||||
ns, n := render.Namespaced(sel)
|
ns, n := render.Namespaced(sel)
|
||||||
c.namespace = ns
|
c.namespace = ns
|
||||||
o, err := c.factory.Get(ns, "v1/pods", n, labels.Everything())
|
o, err := c.factory.Get(ns, "v1/pods", n, labels.Everything())
|
||||||
|
|
@ -40,46 +41,43 @@ func (c *Container) List(sel string) ([]runtime.Object, error) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
c.pod = &po
|
||||||
|
|
||||||
|
res := make([]runtime.Object, 0, len(po.Spec.InitContainers)+len(po.Spec.Containers))
|
||||||
|
for _, co := range po.Spec.InitContainers {
|
||||||
|
res = append(res, ContainerRes{co})
|
||||||
|
}
|
||||||
|
for _, co := range po.Spec.Containers {
|
||||||
|
res = append(res, ContainerRes{co})
|
||||||
|
}
|
||||||
|
|
||||||
res := make([]runtime.Object, 1, len(po.Spec.InitContainers)+len(po.Spec.Containers))
|
|
||||||
res[0] = &po
|
|
||||||
return res, nil
|
return res, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Hydrate returns a pod as container rows.
|
// Hydrate returns a pod as container rows.
|
||||||
func (c *Container) Hydrate(cc []runtime.Object, rr render.Rows, re Renderer) error {
|
func (c *Container) Hydrate(oo []runtime.Object, rr render.Rows, re Renderer) error {
|
||||||
po := cc[0].(*v1.Pod)
|
|
||||||
mx := k8s.NewMetricsServer(c.factory.Client().(k8s.Connection))
|
mx := k8s.NewMetricsServer(c.factory.Client().(k8s.Connection))
|
||||||
mmx, err := mx.FetchPodMetrics(c.namespace, po.Name)
|
mmx, err := mx.FetchPodMetrics(c.namespace, c.pod.Name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
log.Warn().Err(err).Msgf("No metrics found for pod %q:%q", c.namespace, c.pod.Name)
|
||||||
}
|
}
|
||||||
|
|
||||||
var index int
|
var index int
|
||||||
size := len(re.Header(c.namespace))
|
for _, o := range oo {
|
||||||
for _, co := range po.Spec.InitContainers {
|
co := o.(ContainerRes)
|
||||||
row, err := renderCoRow(co.Name, index, size, coMetricsFor(co, po, mmx, true), re)
|
row, err := renderCoRow(co.Container.Name, index, coMetricsFor(co.Container, c.pod, mmx, true), re)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
rr[index] = row
|
rr[index] = row
|
||||||
log.Debug().Msgf("Init Containers %#v", rr[index])
|
|
||||||
index++
|
|
||||||
}
|
|
||||||
for _, co := range po.Spec.Containers {
|
|
||||||
row, err := renderCoRow(co.Name, index, size, coMetricsFor(co, po, mmx, false), re)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
rr[index] = row
|
|
||||||
log.Debug().Msgf("Containers %#v", row)
|
|
||||||
index++
|
index++
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func renderCoRow(n string, index, size int, pmx *ContainerWithMetrics, re Renderer) (render.Row, error) {
|
func renderCoRow(n string, index int, pmx *ContainerWithMetrics, re Renderer) (render.Row, error) {
|
||||||
row := render.Row{Fields: make([]string, size)}
|
var row render.Row
|
||||||
if err := re.Render(pmx, n, &row); err != nil {
|
if err := re.Render(pmx, n, &row); err != nil {
|
||||||
return render.Row{}, err
|
return render.Row{}, err
|
||||||
}
|
}
|
||||||
|
|
@ -98,9 +96,7 @@ func coMetricsFor(co v1.Container, po *v1.Pod, mmx *mv1beta1.PodMetrics, isInit
|
||||||
|
|
||||||
func containerMetrics(n string, mx runtime.Object) *mv1beta1.ContainerMetrics {
|
func containerMetrics(n string, mx runtime.Object) *mv1beta1.ContainerMetrics {
|
||||||
pmx := mx.(*mv1beta1.PodMetrics)
|
pmx := mx.(*mv1beta1.PodMetrics)
|
||||||
log.Debug().Msgf("CO MX fo %s", n)
|
|
||||||
for _, m := range pmx.Containers {
|
for _, m := range pmx.Containers {
|
||||||
log.Debug().Msgf("Container Metrics %#v", m)
|
|
||||||
if m.Name == n {
|
if m.Name == n {
|
||||||
return &m
|
return &m
|
||||||
}
|
}
|
||||||
|
|
@ -155,3 +151,20 @@ func (c *ContainerWithMetrics) Metrics() *mv1beta1.ContainerMetrics {
|
||||||
func (c *ContainerWithMetrics) Age() metav1.Time {
|
func (c *ContainerWithMetrics) Age() metav1.Time {
|
||||||
return c.age
|
return c.age
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
// ContainerRes represents a container K8s resource.
|
||||||
|
type ContainerRes struct {
|
||||||
|
v1.Container
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetObjectKind returns a schema object.
|
||||||
|
func (c ContainerRes) GetObjectKind() schema.ObjectKind {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopyObject returns a container copy.
|
||||||
|
func (c ContainerRes) DeepCopyObject() runtime.Object {
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,49 @@
|
||||||
|
package model
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/derailed/k9s/internal/render"
|
||||||
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Context represents a kube context model.
|
||||||
|
type Context struct {
|
||||||
|
Resource
|
||||||
|
}
|
||||||
|
|
||||||
|
// List returns a collection of node resources.
|
||||||
|
func (c *Context) List(_ context.Context) ([]runtime.Object, error) {
|
||||||
|
cfg := c.factory.Client().Config()
|
||||||
|
ctxs, err := cfg.Contexts()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
cc := make([]runtime.Object, 0, len(ctxs))
|
||||||
|
for name, ctx := range ctxs {
|
||||||
|
cc = append(cc, render.NewNamedContext(cfg, name, ctx))
|
||||||
|
}
|
||||||
|
|
||||||
|
return cc, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hydrate returns nodes as rows.
|
||||||
|
func (n *Context) Hydrate(oo []runtime.Object, rr render.Rows, re Renderer) error {
|
||||||
|
var index int
|
||||||
|
for _, o := range oo {
|
||||||
|
ctx, ok := o.(*render.NamedContext)
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("expecting named context but got %T", o)
|
||||||
|
}
|
||||||
|
|
||||||
|
var row render.Row
|
||||||
|
if err := re.Render(ctx, "", &row); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
rr[index] = row
|
||||||
|
index++
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,130 @@
|
||||||
|
package model
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/derailed/k9s/internal/k8s"
|
||||||
|
"github.com/derailed/k9s/internal/render"
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
metav1beta1 "k8s.io/apimachinery/pkg/apis/meta/v1beta1"
|
||||||
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||||
|
"k8s.io/apimachinery/pkg/runtime/serializer"
|
||||||
|
"k8s.io/client-go/rest"
|
||||||
|
)
|
||||||
|
|
||||||
|
const gvFmt = "application/json;as=Table;v=%s;g=%s, application/json"
|
||||||
|
|
||||||
|
// Generic represents a generic model.
|
||||||
|
type Generic struct {
|
||||||
|
Resource
|
||||||
|
|
||||||
|
table *metav1beta1.Table
|
||||||
|
}
|
||||||
|
|
||||||
|
// List returns a collection of node resources.
|
||||||
|
func (g *Generic) List(ctx context.Context) ([]runtime.Object, error) {
|
||||||
|
// Ensures the factory is tracking this resource
|
||||||
|
_ = g.factory.ForResource(g.namespace, g.gvr)
|
||||||
|
|
||||||
|
gvr := k8s.GVR(g.gvr)
|
||||||
|
fcodec, codec := g.codec(gvr.AsGV())
|
||||||
|
|
||||||
|
c, err := g.client(fcodec, gvr)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// BOZO!! Need to know if gvr is namespaced or not
|
||||||
|
o, err := c.Get().
|
||||||
|
SetHeader("Accept", fmt.Sprintf(gvFmt, metav1beta1.SchemeGroupVersion.Version, metav1beta1.GroupName)).
|
||||||
|
// Namespace(g.namespace).
|
||||||
|
Resource(gvr.ToR()).
|
||||||
|
VersionedParams(&metav1beta1.TableOptions{}, codec).
|
||||||
|
Do().Get()
|
||||||
|
|
||||||
|
table, ok := o.(*metav1beta1.Table)
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("invalid table found on generic %s -- %T", g.gvr, o)
|
||||||
|
}
|
||||||
|
g.table = table
|
||||||
|
res := make([]runtime.Object, len(g.table.Rows))
|
||||||
|
for i := range g.table.Rows {
|
||||||
|
res[i] = RowRes{&g.table.Rows[i]}
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Debug().Msgf("!!!!GENERIC lister returns %d", len(res))
|
||||||
|
return res, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hydrate returns nodes as rows.
|
||||||
|
func (g *Generic) Hydrate(oo []runtime.Object, rr render.Rows, re Renderer) error {
|
||||||
|
gr, ok := re.(*render.Generic)
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("expecting generic renderer for %s but got %T", g.gvr, re)
|
||||||
|
}
|
||||||
|
gr.SetTable(g.table)
|
||||||
|
for i, o := range oo {
|
||||||
|
res, ok := o.(RowRes)
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("expecting RowRes but got %#v", o)
|
||||||
|
}
|
||||||
|
count := len(res.Cells)
|
||||||
|
if g.namespace == "" {
|
||||||
|
count++
|
||||||
|
}
|
||||||
|
if err := gr.Render(res.TableRow, g.namespace, &rr[i]); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
// Helpers...
|
||||||
|
|
||||||
|
func (g *Generic) client(codec serializer.CodecFactory, gvr k8s.GVR) (*rest.RESTClient, error) {
|
||||||
|
crConfig := g.factory.Client().RestConfigOrDie()
|
||||||
|
gv := gvr.AsGV()
|
||||||
|
crConfig.GroupVersion = &gv
|
||||||
|
crConfig.APIPath = "/apis"
|
||||||
|
if len(gvr.ToG()) == 0 {
|
||||||
|
crConfig.APIPath = "/api"
|
||||||
|
}
|
||||||
|
crConfig.NegotiatedSerializer = codec.WithoutConversion()
|
||||||
|
|
||||||
|
crRestClient, err := rest.RESTClientFor(crConfig)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return crRestClient, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Resource) codec(gv schema.GroupVersion) (serializer.CodecFactory, runtime.ParameterCodec) {
|
||||||
|
scheme := runtime.NewScheme()
|
||||||
|
metav1.AddToGroupVersion(scheme, gv)
|
||||||
|
scheme.AddKnownTypes(gv, &metav1beta1.Table{}, &metav1beta1.TableOptions{})
|
||||||
|
scheme.AddKnownTypes(metav1beta1.SchemeGroupVersion, &metav1beta1.Table{}, &metav1beta1.TableOptions{})
|
||||||
|
|
||||||
|
return serializer.NewCodecFactory(scheme), runtime.NewParameterCodec(scheme)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
// RowRes represents a table row.
|
||||||
|
type RowRes struct {
|
||||||
|
*metav1beta1.TableRow
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetObjectKind returns a schema object.
|
||||||
|
func (r RowRes) GetObjectKind() schema.ObjectKind {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopyObject returns a container copy.
|
||||||
|
func (r RowRes) DeepCopyObject() runtime.Object {
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
@ -1,9 +1,10 @@
|
||||||
package model
|
package model
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
"github.com/derailed/k9s/internal/k8s"
|
"github.com/derailed/k9s/internal/k8s"
|
||||||
"github.com/derailed/k9s/internal/render"
|
"github.com/derailed/k9s/internal/render"
|
||||||
"github.com/derailed/k9s/internal/watch"
|
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
v1 "k8s.io/api/core/v1"
|
v1 "k8s.io/api/core/v1"
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
|
@ -17,16 +18,11 @@ var _ render.NodeWithMetrics = &NodeWithMetrics{}
|
||||||
|
|
||||||
// Node represents a node model.
|
// Node represents a node model.
|
||||||
type Node struct {
|
type Node struct {
|
||||||
*Resource
|
Resource
|
||||||
}
|
|
||||||
|
|
||||||
// NewNode returns a new node model.
|
|
||||||
func NewNode() *Node {
|
|
||||||
return &Node{Resource: NewResource()}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// List returns a collection of node resources.
|
// List returns a collection of node resources.
|
||||||
func (n *Node) List(_ string) ([]runtime.Object, error) {
|
func (n *Node) List(_ context.Context) ([]runtime.Object, error) {
|
||||||
nn, err := n.factory.Client().DialOrDie().CoreV1().Nodes().List(metav1.ListOptions{})
|
nn, err := n.factory.Client().DialOrDie().CoreV1().Nodes().List(metav1.ListOptions{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|
@ -52,19 +48,17 @@ func (n *Node) Hydrate(oo []runtime.Object, rr render.Rows, re Renderer) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
var index int
|
var index int
|
||||||
size := len(re.Header(""))
|
|
||||||
for _, no := range oo {
|
for _, no := range oo {
|
||||||
o := no.(*unstructured.Unstructured)
|
o := no.(*unstructured.Unstructured)
|
||||||
pods, err := n.nodePods(n.factory, o.Object["metadata"].(map[string]interface{})["name"].(string))
|
pods, err := n.nodePods(n.factory, o.Object["metadata"].(map[string]interface{})["name"].(string))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
return err
|
||||||
}
|
|
||||||
row := render.Row{Fields: make([]string, size)}
|
|
||||||
nmx := NodeWithMetrics{
|
|
||||||
o,
|
|
||||||
nodeMetricsFor(o, mmx),
|
|
||||||
pods,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
row render.Row
|
||||||
|
nmx = NodeWithMetrics{object: o, mx: nodeMetricsFor(o, mmx), pods: pods}
|
||||||
|
)
|
||||||
if err := re.Render(&nmx, "", &row); err != nil {
|
if err := re.Render(&nmx, "", &row); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
@ -85,7 +79,7 @@ func nodeMetricsFor(o runtime.Object, mmx *mv1beta1.NodeMetricsList) *mv1beta1.N
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (n *Node) nodePods(f *watch.Factory, node string) ([]*v1.Pod, error) {
|
func (n *Node) nodePods(f Factory, node string) ([]*v1.Pod, error) {
|
||||||
pp, err := f.List("", "v1/pods", labels.Everything())
|
pp, err := f.List("", "v1/pods", labels.Everything())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|
@ -1,9 +1,13 @@
|
||||||
package model
|
package model
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/derailed/k9s/internal"
|
||||||
"github.com/derailed/k9s/internal/k8s"
|
"github.com/derailed/k9s/internal/k8s"
|
||||||
"github.com/derailed/k9s/internal/render"
|
"github.com/derailed/k9s/internal/render"
|
||||||
v1 "k8s.io/api/core/v1"
|
"github.com/rs/zerolog/log"
|
||||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||||
"k8s.io/apimachinery/pkg/labels"
|
"k8s.io/apimachinery/pkg/labels"
|
||||||
"k8s.io/apimachinery/pkg/runtime"
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
|
|
@ -12,37 +16,42 @@ import (
|
||||||
|
|
||||||
// Pod represents a pod model.
|
// Pod represents a pod model.
|
||||||
type Pod struct {
|
type Pod struct {
|
||||||
*Resource
|
Resource
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewPod returns a new pod model.
|
// List returns a collection of nodes.
|
||||||
func NewPod() *Pod {
|
func (p *Pod) List(ctx context.Context) ([]runtime.Object, error) {
|
||||||
return &Pod{NewResource()}
|
oo, err := p.Resource.List(ctx)
|
||||||
}
|
if err != nil {
|
||||||
|
return oo, err
|
||||||
|
}
|
||||||
|
|
||||||
func (p *Pod) FetchContainers(sel string, includeInit bool) ([]string, error) {
|
fieldSel, ok := ctx.Value(internal.KeyFields).(string)
|
||||||
o, err := p.factory.Get(p.namespace, p.gvr, sel, labels.Everything())
|
if !ok {
|
||||||
|
return oo, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
sel, err := labels.ConvertSelectorToLabelsMap(fieldSel)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
var po v1.Pod
|
nodeName, ok := sel["spec.nodeName"]
|
||||||
if runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &po); err != nil {
|
if !ok {
|
||||||
return nil, err
|
return nil, fmt.Errorf("NYI field selector %q", nodeName)
|
||||||
}
|
}
|
||||||
|
|
||||||
cc := make([]string, 0, len(po.Spec.Containers))
|
var res []runtime.Object
|
||||||
for _, c := range po.Spec.Containers {
|
for _, o := range oo {
|
||||||
cc = append(cc, c.Name)
|
u := o.(*unstructured.Unstructured)
|
||||||
}
|
spec := u.Object["spec"].(map[string]interface{})
|
||||||
|
log.Debug().Msgf("Spec node %q -- %q", nodeName, spec["nodeName"])
|
||||||
if includeInit {
|
if spec["nodeName"] == nodeName {
|
||||||
for _, c := range po.Spec.InitContainers {
|
res = append(res, o)
|
||||||
cc = append(cc, c.Name)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return cc, nil
|
return res, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Render returns pod resources as rows.
|
// Render returns pod resources as rows.
|
||||||
|
|
@ -50,14 +59,15 @@ func (p *Pod) Hydrate(oo []runtime.Object, rr render.Rows, re Renderer) error {
|
||||||
mx := k8s.NewMetricsServer(p.factory.Client().(k8s.Connection))
|
mx := k8s.NewMetricsServer(p.factory.Client().(k8s.Connection))
|
||||||
mmx, err := mx.FetchPodsMetrics(p.namespace)
|
mmx, err := mx.FetchPodsMetrics(p.namespace)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
log.Warn().Err(err).Msgf("No metrics found for pod")
|
||||||
}
|
}
|
||||||
|
|
||||||
var index int
|
var index int
|
||||||
size := len(re.Header(p.namespace))
|
|
||||||
for _, o := range oo {
|
for _, o := range oo {
|
||||||
row := render.Row{Fields: make([]string, size)}
|
var (
|
||||||
pmx := PodWithMetrics{o, podMetricsFor(o, mmx)}
|
row render.Row
|
||||||
|
pmx = PodWithMetrics{object: o, mx: podMetricsFor(o, mmx)}
|
||||||
|
)
|
||||||
if err := re.Render(&pmx, p.namespace, &row); err != nil {
|
if err := re.Render(&pmx, p.namespace, &row); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
@ -2,51 +2,81 @@ package model
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/derailed/k9s/internal/render"
|
"github.com/derailed/k9s/internal/render"
|
||||||
"github.com/derailed/k9s/internal/watch"
|
|
||||||
"k8s.io/apimachinery/pkg/runtime"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type Renderer interface {
|
// BOZO!! Break up deps and merge into single registrar
|
||||||
// Render converts raw resources to tabular data.
|
|
||||||
Render(o interface{}, ns string, row *render.Row) error
|
|
||||||
|
|
||||||
// Header returns the resource header.
|
|
||||||
Header(ns string) render.HeaderRow
|
|
||||||
|
|
||||||
ColorerFunc() render.ColorerFunc
|
|
||||||
}
|
|
||||||
|
|
||||||
type Lister interface {
|
|
||||||
// Init initializes a resource.
|
|
||||||
Init(ns, gvr string, f *watch.Factory)
|
|
||||||
|
|
||||||
// List returns a collection of resources.
|
|
||||||
List(sel string) ([]runtime.Object, error)
|
|
||||||
|
|
||||||
// Hydrate converts resource rows into tabular data.
|
|
||||||
Hydrate([]runtime.Object, render.Rows, Renderer) error
|
|
||||||
}
|
|
||||||
|
|
||||||
type ResourceMeta struct {
|
|
||||||
Model Lister
|
|
||||||
Renderer Renderer
|
|
||||||
}
|
|
||||||
|
|
||||||
var Registry = map[string]ResourceMeta{
|
var Registry = map[string]ResourceMeta{
|
||||||
|
"containers": ResourceMeta{
|
||||||
|
Model: &Container{},
|
||||||
|
Renderer: &render.Container{},
|
||||||
|
},
|
||||||
|
"contexts": ResourceMeta{
|
||||||
|
Model: &Context{},
|
||||||
|
Renderer: &render.Context{},
|
||||||
|
},
|
||||||
|
"screendumps": ResourceMeta{
|
||||||
|
Model: &ScreenDump{},
|
||||||
|
Renderer: &render.ScreenDump{},
|
||||||
|
},
|
||||||
|
|
||||||
"v1/pods": ResourceMeta{
|
"v1/pods": ResourceMeta{
|
||||||
Model: NewPod(),
|
Model: &Pod{},
|
||||||
Renderer: &render.Pod{},
|
Renderer: &render.Pod{},
|
||||||
},
|
},
|
||||||
"v1/nodes": ResourceMeta{
|
"v1/nodes": ResourceMeta{
|
||||||
Model: NewNode(),
|
Model: &Node{},
|
||||||
Renderer: &render.Node{},
|
Renderer: &render.Node{},
|
||||||
},
|
},
|
||||||
"v1/configmaps": ResourceMeta{
|
"v1/namespaces": ResourceMeta{
|
||||||
Model: NewResource(),
|
Renderer: &render.Namespace{},
|
||||||
Renderer: &render.ConfigMap{},
|
|
||||||
},
|
},
|
||||||
"containers": ResourceMeta{
|
|
||||||
Model: NewContainer(),
|
"apps/v1/deployments": ResourceMeta{
|
||||||
Renderer: &render.Container{},
|
Renderer: &render.Deployment{},
|
||||||
|
},
|
||||||
|
"apps/v1/replicasets": ResourceMeta{
|
||||||
|
Renderer: &render.ReplicaSet{},
|
||||||
|
},
|
||||||
|
"apps/v1/statefulsets": ResourceMeta{
|
||||||
|
Renderer: &render.StatefulSet{},
|
||||||
|
},
|
||||||
|
"apps/v1/daemonsets": ResourceMeta{
|
||||||
|
Renderer: &render.DaemonSet{},
|
||||||
|
},
|
||||||
|
"extensions/v1beta1/daemonsets": ResourceMeta{
|
||||||
|
Renderer: &render.DaemonSet{},
|
||||||
|
},
|
||||||
|
|
||||||
|
// "v1/services": ResourceMeta{
|
||||||
|
// Renderer: &render.Service{},
|
||||||
|
// },
|
||||||
|
// "v1/configmaps": ResourceMeta{
|
||||||
|
// Renderer: &render.ConfigMap{},
|
||||||
|
// },
|
||||||
|
// "v1/secrets": ResourceMeta{
|
||||||
|
// Renderer: &render.ConfigMap{},
|
||||||
|
// },
|
||||||
|
// "batch/v1beta1/cronjobs": ResourceMeta{
|
||||||
|
// Renderer: &render.CronJob{},
|
||||||
|
// },
|
||||||
|
// "batch/v1/jobs": ResourceMeta{
|
||||||
|
// Renderer: &render.Job{},
|
||||||
|
// },
|
||||||
|
|
||||||
|
"apiextensions.k8s.io/v1beta1/customresourcedefinitions": ResourceMeta{
|
||||||
|
Renderer: &render.CustomResourceDefinition{},
|
||||||
|
},
|
||||||
|
|
||||||
|
"rbac.authorization.k8s.io/v1/clusterroles": ResourceMeta{
|
||||||
|
Renderer: &render.ClusterRole{},
|
||||||
|
},
|
||||||
|
"rbac.authorization.k8s.io/v1/clusterrolebindings": ResourceMeta{
|
||||||
|
Renderer: &render.ClusterRoleBinding{},
|
||||||
|
},
|
||||||
|
"rbac.authorization.k8s.io/v1/roles": ResourceMeta{
|
||||||
|
Renderer: &render.Role{},
|
||||||
|
},
|
||||||
|
"rbac.authorization.k8s.io/v1/rolebindings": ResourceMeta{
|
||||||
|
Renderer: &render.RoleBinding{},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,11 @@
|
||||||
package model
|
package model
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/derailed/k9s/internal"
|
||||||
"github.com/derailed/k9s/internal/render"
|
"github.com/derailed/k9s/internal/render"
|
||||||
"github.com/derailed/k9s/internal/watch"
|
"github.com/rs/zerolog/log"
|
||||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||||
"k8s.io/apimachinery/pkg/labels"
|
"k8s.io/apimachinery/pkg/labels"
|
||||||
"k8s.io/apimachinery/pkg/runtime"
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
|
|
@ -11,30 +14,35 @@ import (
|
||||||
// Resource represents a generic resource model.
|
// Resource represents a generic resource model.
|
||||||
type Resource struct {
|
type Resource struct {
|
||||||
namespace, gvr string
|
namespace, gvr string
|
||||||
factory *watch.Factory
|
factory Factory
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewResource() *Resource {
|
func (r *Resource) Init(ns, gvr string, f Factory) {
|
||||||
return &Resource{}
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewResource returns a new model.
|
|
||||||
func (r *Resource) Init(ns, gvr string, f *watch.Factory) {
|
|
||||||
r.namespace, r.gvr, r.factory = ns, gvr, f
|
r.namespace, r.gvr, r.factory = ns, gvr, f
|
||||||
}
|
}
|
||||||
|
|
||||||
// List returns a collection of nodes.
|
// List returns a collection of nodes.
|
||||||
func (r *Resource) List(_ string) ([]runtime.Object, error) {
|
func (r *Resource) List(ctx context.Context) ([]runtime.Object, error) {
|
||||||
return r.factory.List(r.namespace, r.gvr, labels.Everything())
|
strLabel, ok := ctx.Value(internal.KeyLabels).(string)
|
||||||
|
lsel := labels.Everything()
|
||||||
|
if sel, err := labels.ConvertSelectorToLabelsMap(strLabel); ok && err == nil {
|
||||||
|
lsel = sel.AsSelector()
|
||||||
|
}
|
||||||
|
|
||||||
|
oo, err := r.factory.List(r.namespace, r.gvr, lsel)
|
||||||
|
r.factory.WaitForCacheSync()
|
||||||
|
|
||||||
|
return oo, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Render returns a node as a row.
|
// Render returns a node as a row.
|
||||||
func (r *Resource) Hydrate(oo []runtime.Object, rr render.Rows, re Renderer) error {
|
func (r *Resource) Hydrate(oo []runtime.Object, rr render.Rows, re Renderer) error {
|
||||||
|
log.Debug().Msgf("^^^^^^ HYDRATING (%q) %d", r.namespace, len(oo))
|
||||||
|
|
||||||
var index int
|
var index int
|
||||||
size := len(re.Header(r.namespace))
|
|
||||||
for _, o := range oo {
|
for _, o := range oo {
|
||||||
res := o.(*unstructured.Unstructured)
|
res := o.(*unstructured.Unstructured)
|
||||||
row := render.Row{Fields: make([]string, size)}
|
var row render.Row
|
||||||
if err := re.Render(res, r.namespace, &row); err != nil {
|
if err := re.Render(res, r.namespace, &row); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,81 @@
|
||||||
|
package model
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/derailed/k9s/internal"
|
||||||
|
"github.com/derailed/k9s/internal/render"
|
||||||
|
v1 "k8s.io/api/core/v1"
|
||||||
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ScreenDump represents a container model.
|
||||||
|
type ScreenDump struct {
|
||||||
|
Resource
|
||||||
|
|
||||||
|
pod *v1.Pod
|
||||||
|
}
|
||||||
|
|
||||||
|
// List returns a collection of containers
|
||||||
|
func (c *ScreenDump) List(ctx context.Context) ([]runtime.Object, error) {
|
||||||
|
dir, ok := ctx.Value(internal.KeyDir).(string)
|
||||||
|
if !ok {
|
||||||
|
return nil, errors.New("no screendump dir found in context")
|
||||||
|
}
|
||||||
|
|
||||||
|
ff, err := ioutil.ReadDir(dir)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
oo := make([]runtime.Object, len(ff))
|
||||||
|
for i, f := range ff {
|
||||||
|
oo[i] = FileRes{file: f, dir: dir}
|
||||||
|
}
|
||||||
|
|
||||||
|
return oo, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hydrate returns a pod as container rows.
|
||||||
|
func (c *ScreenDump) Hydrate(oo []runtime.Object, rr render.Rows, re Renderer) error {
|
||||||
|
for i, o := range oo {
|
||||||
|
res, ok := o.(FileRes)
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("expecting a file resource but got %T", o)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := re.Render(res, render.NonResource, &rr[i]); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
// FileRes represents a file resource.
|
||||||
|
type FileRes struct {
|
||||||
|
file os.FileInfo
|
||||||
|
dir string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c FileRes) GetFile() os.FileInfo { return c.file }
|
||||||
|
func (c FileRes) GetDir() string { return c.dir }
|
||||||
|
|
||||||
|
// GetObjectKind returns a schema object.
|
||||||
|
func (c FileRes) GetObjectKind() schema.ObjectKind {
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopyObject returns a container copy.
|
||||||
|
func (c FileRes) DeepCopyObject() runtime.Object {
|
||||||
|
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
@ -3,7 +3,13 @@ package model
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
|
||||||
|
"github.com/derailed/k9s/internal/k8s"
|
||||||
|
"github.com/derailed/k9s/internal/render"
|
||||||
"github.com/derailed/tview"
|
"github.com/derailed/tview"
|
||||||
|
"k8s.io/apimachinery/pkg/labels"
|
||||||
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||||
|
"k8s.io/client-go/informers"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Igniter represents a runnable view.
|
// Igniter represents a runnable view.
|
||||||
|
|
@ -20,6 +26,7 @@ type Igniter interface {
|
||||||
|
|
||||||
// Hinter represent a menu mnemonic provider.
|
// Hinter represent a menu mnemonic provider.
|
||||||
type Hinter interface {
|
type Hinter interface {
|
||||||
|
// Hints returns a collection of menu hints.
|
||||||
Hints() MenuHints
|
Hints() MenuHints
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -37,3 +44,74 @@ type Component interface {
|
||||||
Igniter
|
Igniter
|
||||||
Hinter
|
Hinter
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Renderer represents a resource renderer.
|
||||||
|
type Renderer interface {
|
||||||
|
// Render converts raw resources to tabular data.
|
||||||
|
Render(o interface{}, ns string, row *render.Row) error
|
||||||
|
|
||||||
|
// Header returns the resource header.
|
||||||
|
Header(ns string) render.HeaderRow
|
||||||
|
|
||||||
|
// ColorerFunc returns a row colorer function.
|
||||||
|
ColorerFunc() render.ColorerFunc
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lister represents a resource lister.
|
||||||
|
type Lister interface {
|
||||||
|
// Init initializes a resource.
|
||||||
|
Init(ns, gvr string, f Factory)
|
||||||
|
|
||||||
|
// List returns a collection of resources.
|
||||||
|
List(context.Context) ([]runtime.Object, error)
|
||||||
|
|
||||||
|
// Hydrate converts resource rows into tabular data.
|
||||||
|
Hydrate([]runtime.Object, render.Rows, Renderer) error
|
||||||
|
}
|
||||||
|
|
||||||
|
// BOZO!!
|
||||||
|
// type Connection interface {
|
||||||
|
// // DialOrDie dials client api.
|
||||||
|
// DialOrDie() kubernetes.Interface
|
||||||
|
|
||||||
|
// // MXDial dials metrics api.
|
||||||
|
// MXDial() (*versioned.Clientset, error)
|
||||||
|
|
||||||
|
// // DynDialOrDie dials dynamic client api.
|
||||||
|
// DynDialOrDie() dynamic.Interface
|
||||||
|
|
||||||
|
// // RestConfigOrDie return a client configuration.
|
||||||
|
// RestConfigOrDie() *restclient.Config
|
||||||
|
|
||||||
|
// // Config returns the current kubeconfig.
|
||||||
|
// Config() *k8s.Config
|
||||||
|
|
||||||
|
// // CachedDiscovery returns a cached client.
|
||||||
|
// CachedDiscovery() (*disk.CachedDiscoveryClient, error)
|
||||||
|
|
||||||
|
// // SwithContextOrDie switch to a new kube context.
|
||||||
|
// SwitchContextOrDie(ctx string)
|
||||||
|
// }
|
||||||
|
|
||||||
|
type Factory interface {
|
||||||
|
// Client retrieves an api client.
|
||||||
|
Client() k8s.Connection
|
||||||
|
|
||||||
|
// Get fetch a given resource.
|
||||||
|
Get(ns, gvr, n string, sel labels.Selector) (runtime.Object, error)
|
||||||
|
|
||||||
|
// List fetch a collection of resources.
|
||||||
|
List(ns, gvr string, sel labels.Selector) ([]runtime.Object, error)
|
||||||
|
|
||||||
|
// ForResource fetch an informer for a given resource.
|
||||||
|
ForResource(ns, gvr string) informers.GenericInformer
|
||||||
|
|
||||||
|
// WaitForCacheSync synchronize the cache.
|
||||||
|
WaitForCacheSync() map[schema.GroupVersionResource]bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResourceMeta represents model info about a resource.
|
||||||
|
type ResourceMeta struct {
|
||||||
|
Model Lister
|
||||||
|
Renderer Renderer
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,64 @@
|
||||||
|
package render
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/derailed/k9s/internal/k8s"
|
||||||
|
"github.com/gdamore/tcell"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Alias renders a aliases to screen.
|
||||||
|
type Alias struct{}
|
||||||
|
|
||||||
|
// ColorerFunc colors a resource row.
|
||||||
|
func (Alias) ColorerFunc() ColorerFunc {
|
||||||
|
return func(ns string, re RowEvent) tcell.Color {
|
||||||
|
return tcell.ColorMediumSpringGreen
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Header returns a header row.
|
||||||
|
func (Alias) Header(ns string) HeaderRow {
|
||||||
|
return HeaderRow{
|
||||||
|
Header{Name: "RESOURCE"},
|
||||||
|
Header{Name: "COMMAND"},
|
||||||
|
Header{Name: "APIGROUP"},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render renders a K8s resource to screen.
|
||||||
|
func (Alias) Render(o interface{}, gvr string, r *Row) error {
|
||||||
|
aliases, ok := o.([]string)
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("Expected Alias, but got %T", o)
|
||||||
|
}
|
||||||
|
|
||||||
|
g := k8s.GVR(gvr)
|
||||||
|
r.ID = string(gvr)
|
||||||
|
r.Fields = Fields{
|
||||||
|
g.ToR(),
|
||||||
|
strings.Join(aliases, ","),
|
||||||
|
g.ToG(),
|
||||||
|
// Pad(g.ToR(), 30),
|
||||||
|
// Pad(strings.Join(aliases, ","), 70),
|
||||||
|
// Pad(g.ToG(), 30),
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helpers...
|
||||||
|
|
||||||
|
// Pad a string up to the given length or truncates if greater than length.
|
||||||
|
func Pad(s string, width int) string {
|
||||||
|
if len(s) == width {
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(s) > width {
|
||||||
|
return Truncate(s, width)
|
||||||
|
}
|
||||||
|
|
||||||
|
return s + strings.Repeat(" ", width-len(s))
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,158 @@
|
||||||
|
package render
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
"regexp"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/derailed/tview"
|
||||||
|
"github.com/gdamore/tcell"
|
||||||
|
"golang.org/x/text/language"
|
||||||
|
"golang.org/x/text/message"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
totalRx = regexp.MustCompile(`Total:\s+([0-9.]+)\ssecs`)
|
||||||
|
reqRx = regexp.MustCompile(`Requests/sec:\s+([0-9.]+)`)
|
||||||
|
okRx = regexp.MustCompile(`\[2\d{2}\]\s+(\d+)\s+responses`)
|
||||||
|
errRx = regexp.MustCompile(`\[[4-5]\d{2}\]\s+(\d+)\s+responses`)
|
||||||
|
toastRx = regexp.MustCompile(`Error distribution`)
|
||||||
|
)
|
||||||
|
|
||||||
|
// BenchInfo represents benchmark run info.
|
||||||
|
type BenchInfo struct {
|
||||||
|
File os.FileInfo
|
||||||
|
Path string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bench renders a benchmarks to screen.
|
||||||
|
type Bench struct{}
|
||||||
|
|
||||||
|
// ColorerFunc colors a resource row.
|
||||||
|
func (Bench) ColorerFunc() ColorerFunc {
|
||||||
|
return func(ns string, re RowEvent) tcell.Color {
|
||||||
|
c := tcell.ColorPaleGreen
|
||||||
|
statusCol := 2
|
||||||
|
if strings.TrimSpace(re.Row.Fields[statusCol]) != "pass" {
|
||||||
|
c = ErrColor
|
||||||
|
}
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Header returns a header row.
|
||||||
|
func (Bench) Header(ns string) HeaderRow {
|
||||||
|
return HeaderRow{
|
||||||
|
Header{Name: "NAMESPACE", Align: tview.AlignLeft},
|
||||||
|
Header{Name: "NAME", Align: tview.AlignLeft},
|
||||||
|
Header{Name: "STATUS", Align: tview.AlignLeft},
|
||||||
|
Header{Name: "TIME", Align: tview.AlignLeft},
|
||||||
|
Header{Name: "REQ/S", Align: tview.AlignRight},
|
||||||
|
Header{Name: "2XX", Align: tview.AlignRight},
|
||||||
|
Header{Name: "4XX/5XX", Align: tview.AlignRight},
|
||||||
|
Header{Name: "REPORT", Align: tview.AlignLeft},
|
||||||
|
Header{Name: "AGE", Align: tview.AlignLeft},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render renders a K8s resource to screen.
|
||||||
|
func (b Bench) Render(o interface{}, ns string, r *Row) error {
|
||||||
|
bench, ok := o.(BenchInfo)
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("Expected string, but got %T", o)
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := b.readFile(bench.Path)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Unable to load bench file %s", bench.Path)
|
||||||
|
}
|
||||||
|
|
||||||
|
r.Fields = make(Fields, len(b.Header(ns)))
|
||||||
|
if err := b.initRow(r.Fields, bench.File); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
b.augmentRow(r.Fields, data)
|
||||||
|
r.ID = bench.Path
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helpers...
|
||||||
|
|
||||||
|
func (Bench) readFile(file string) (string, error) {
|
||||||
|
data, err := ioutil.ReadFile(file)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return string(data), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (Bench) initRow(row Fields, f os.FileInfo) error {
|
||||||
|
tokens := strings.Split(f.Name(), "_")
|
||||||
|
if len(tokens) < 2 {
|
||||||
|
return fmt.Errorf("Invalid file name %s", f.Name())
|
||||||
|
}
|
||||||
|
row[0] = tokens[0]
|
||||||
|
row[1] = tokens[1]
|
||||||
|
row[7] = f.Name()
|
||||||
|
row[8] = time.Since(f.ModTime()).String()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b Bench) augmentRow(fields Fields, data string) {
|
||||||
|
if len(data) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
col := 2
|
||||||
|
fields[col] = "pass"
|
||||||
|
mf := toastRx.FindAllStringSubmatch(data, 1)
|
||||||
|
if len(mf) > 0 {
|
||||||
|
fields[col] = "fail"
|
||||||
|
}
|
||||||
|
col++
|
||||||
|
|
||||||
|
mt := totalRx.FindAllStringSubmatch(data, 1)
|
||||||
|
if len(mt) > 0 {
|
||||||
|
fields[col] = mt[0][1]
|
||||||
|
}
|
||||||
|
col++
|
||||||
|
|
||||||
|
mr := reqRx.FindAllStringSubmatch(data, 1)
|
||||||
|
if len(mr) > 0 {
|
||||||
|
fields[col] = mr[0][1]
|
||||||
|
}
|
||||||
|
col++
|
||||||
|
|
||||||
|
ms := okRx.FindAllStringSubmatch(data, -1)
|
||||||
|
fields[col] = b.countReq(ms)
|
||||||
|
col++
|
||||||
|
|
||||||
|
me := errRx.FindAllStringSubmatch(data, -1)
|
||||||
|
fields[col] = b.countReq(me)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (Bench) countReq(rr [][]string) string {
|
||||||
|
if len(rr) == 0 {
|
||||||
|
return "0"
|
||||||
|
}
|
||||||
|
|
||||||
|
var sum int
|
||||||
|
for _, m := range rr {
|
||||||
|
if m, err := strconv.Atoi(string(m[1])); err == nil {
|
||||||
|
sum += m
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return asNum(sum)
|
||||||
|
}
|
||||||
|
|
||||||
|
// AsNumb prints a number with thousand separator.
|
||||||
|
func asNum(n int) string {
|
||||||
|
p := message.NewPrinter(language.English)
|
||||||
|
return p.Sprintf("%d", n)
|
||||||
|
}
|
||||||
|
|
@ -30,7 +30,7 @@ func (CronJob) Header(ns string) HeaderRow {
|
||||||
Header{Name: "SUSPEND"},
|
Header{Name: "SUSPEND"},
|
||||||
Header{Name: "ACTIVE"},
|
Header{Name: "ACTIVE"},
|
||||||
Header{Name: "LAST_SCHEDULE"},
|
Header{Name: "LAST_SCHEDULE"},
|
||||||
Header{Name: "AGE"},
|
Header{Name: "AGE", Decorator: ageDecorator},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,7 @@ func (ConfigMap) Header(ns string) HeaderRow {
|
||||||
return append(h,
|
return append(h,
|
||||||
Header{Name: "NAME"},
|
Header{Name: "NAME"},
|
||||||
Header{Name: "DATA", Align: tview.AlignRight},
|
Header{Name: "DATA", Align: tview.AlignRight},
|
||||||
Header{Name: "AGE"},
|
Header{Name: "AGE", Decorator: ageDecorator},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,280 @@
|
||||||
|
package render
|
||||||
|
|
||||||
|
// BOZO!!
|
||||||
|
// type (
|
||||||
|
// colorerUC struct {
|
||||||
|
// ns string
|
||||||
|
// r RowEvent
|
||||||
|
// e tcell.Color
|
||||||
|
// }
|
||||||
|
// colorerUCs []colorerUC
|
||||||
|
// )
|
||||||
|
|
||||||
|
// func TestNSColorer(t *testing.T) {
|
||||||
|
// var (
|
||||||
|
// ns = Row{Fields: Fields{"blee", "Active"}}
|
||||||
|
// term = Row{Fields: Fields{"blee", Terminating}}
|
||||||
|
// dead = Row{Fields: Fields{"blee", "Inactive"}}
|
||||||
|
// )
|
||||||
|
|
||||||
|
// uu := colorerUCs{
|
||||||
|
// // Add AllNS
|
||||||
|
// {"", RowEvent{
|
||||||
|
// Kind: EventAdd,
|
||||||
|
// Row: ns,
|
||||||
|
// },
|
||||||
|
// AddColor},
|
||||||
|
// // Mod AllNS
|
||||||
|
// {"", RowEvent{Kind: EventUpdate, Row: ns}, ModColor},
|
||||||
|
// // MoChange AllNS
|
||||||
|
// {"", RowEvent{Kind: EventUnchanged, Row: ns}, StdColor},
|
||||||
|
// // Bust NS
|
||||||
|
// {"", RowEvent{Kind: EventUnchanged, Row: term}, ErrColor},
|
||||||
|
// // Bust NS
|
||||||
|
// {"", RowEvent{Kind: EventUnchanged, Row: dead}, ErrColor},
|
||||||
|
// }
|
||||||
|
// for _, u := range uu {
|
||||||
|
// assert.Equal(t, u.e, nsColorer(u.ns, u.r))
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// func TestEvColorer(t *testing.T) {
|
||||||
|
// var (
|
||||||
|
// ns = Row{Fields: Fields{"", "blee", "fred", "Normal"}}
|
||||||
|
// nonNS = Row{Fields: Fields{"", "fred", "Normal"}}
|
||||||
|
// failNS = Row{Fields: Fields{"", "blee", "fred", "Failed"}}
|
||||||
|
// failNoNS = Row{Fields: Fields{"", "fred", "Failed"}}
|
||||||
|
// killNS = Row{Fields: Fields{"", "blee", "fred", "Killing"}}
|
||||||
|
// killNoNS = Row{Fields: Fields{"", "fred", "Killing"}}
|
||||||
|
// )
|
||||||
|
|
||||||
|
// uu := colorerUCs{
|
||||||
|
// // Add AllNS
|
||||||
|
// {"", RowEvent{Kind: EventAdd, Row: ns}, AddColor},
|
||||||
|
// // Add NS
|
||||||
|
// {"blee", RowEvent{Kind: EventAdd, Row: nonNS}, AddColor},
|
||||||
|
// // Mod AllNS
|
||||||
|
// {"", RowEvent{Kind: EventUpdate, Row: ns}, ModColor},
|
||||||
|
// // Mod NS
|
||||||
|
// {"blee", RowEvent{Kind: EventUpdate, Row: nonNS}, ModColor},
|
||||||
|
// // Bust AllNS
|
||||||
|
// {"", RowEvent{Kind: EventUnchanged, Row: failNS}, ErrColor},
|
||||||
|
// // Bust NS
|
||||||
|
// {"blee", RowEvent{Kind: EventUnchanged, Row: failNoNS}, ErrColor},
|
||||||
|
// // Bust AllNS
|
||||||
|
// {"", RowEvent{Kind: EventUnchanged, Row: killNS}, KillColor},
|
||||||
|
// // Bust NS
|
||||||
|
// {"blee", RowEvent{Kind: EventUnchanged, Row: killNoNS}, KillColor},
|
||||||
|
// }
|
||||||
|
// for _, u := range uu {
|
||||||
|
// assert.Equal(t, u.e, evColorer(u.ns, u.r))
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// func TestRSColorer(t *testing.T) {
|
||||||
|
// var (
|
||||||
|
// ns = Row{Fields: Fields{"blee", "fred", "1", "1"}}
|
||||||
|
// noNs = Row{Fields: Fields{"fred", "1", "1"}}
|
||||||
|
// bustNS = Row{Fields: Fields{"blee", "fred", "1", "0"}}
|
||||||
|
// bustNoNS = Row{Fields: Fields{"fred", "1", "0"}}
|
||||||
|
// )
|
||||||
|
|
||||||
|
// uu := colorerUCs{
|
||||||
|
// // Add AllNS
|
||||||
|
// {"", RowEvent{Kind: EventAdd, Row: ns}, AddColor},
|
||||||
|
// // Add NS
|
||||||
|
// {"blee", RowEvent{Kind: EventAdd, Row: noNs}, AddColor},
|
||||||
|
// // Bust AllNS
|
||||||
|
// {"", RowEvent{Kind: EventUnchanged, Row: bustNS}, ErrColor},
|
||||||
|
// // Bust NS
|
||||||
|
// {"blee", RowEvent{Kind: EventUnchanged, Row: bustNoNS}, ErrColor},
|
||||||
|
// // Nochange AllNS
|
||||||
|
// {"", RowEvent{Kind: EventUnchanged, Row: ns}, StdColor},
|
||||||
|
// // Nochange NS
|
||||||
|
// {"blee", RowEvent{Kind: EventUnchanged, Row: noNs}, StdColor},
|
||||||
|
// }
|
||||||
|
// for _, u := range uu {
|
||||||
|
// assert.Equal(t, u.e, rsColorer(u.ns, u.r))
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// func TestStsColorer(t *testing.T) {
|
||||||
|
// var (
|
||||||
|
// ns = Row{Fields: Fields{"blee", "fred", "1", "1"}}
|
||||||
|
// nonNS = Row{Fields: Fields{"fred", "1", "1"}}
|
||||||
|
// bustNS = Row{Fields: Fields{"blee", "fred", "2", "1"}}
|
||||||
|
// bustNoNS = Row{Fields: Fields{"fred", "2", "1"}}
|
||||||
|
// )
|
||||||
|
|
||||||
|
// uu := colorerUCs{
|
||||||
|
// // Add AllNS
|
||||||
|
// {"", RowEvent{Kind: EventAdd, Row: ns}, AddColor},
|
||||||
|
// // Add NS
|
||||||
|
// {"blee", RowEvent{Kind: EventAdd, Row: nonNS}, AddColor},
|
||||||
|
// // Mod AllNS
|
||||||
|
// {"", RowEvent{Kind: EventUpdate, Row: ns}, ModColor},
|
||||||
|
// // Mod NS
|
||||||
|
// {"blee", RowEvent{Kind: EventUpdate, Row: nonNS}, ModColor},
|
||||||
|
// // Bust AllNS
|
||||||
|
// {"", RowEvent{Kind: EventUnchanged, Row: bustNS}, ErrColor},
|
||||||
|
// // Bust NS
|
||||||
|
// {"blee", RowEvent{Kind: EventUnchanged, Row: bustNoNS}, ErrColor},
|
||||||
|
// // Unchanged cool AllNS
|
||||||
|
// {"", RowEvent{Kind: EventUnchanged, Row: ns}, StdColor},
|
||||||
|
// }
|
||||||
|
// for _, u := range uu {
|
||||||
|
// assert.Equal(t, u.e, stsColorer(u.ns, u.r))
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// func TestDpColorer(t *testing.T) {
|
||||||
|
// var (
|
||||||
|
// ns = Row{Fields: Fields{"blee", "fred", "1", "1"}}
|
||||||
|
// nonNS = Row{Fields: Fields{"fred", "1", "1"}}
|
||||||
|
// bustNS = Row{Fields: Fields{"blee", "fred", "2", "1"}}
|
||||||
|
// bustNoNS = Row{Fields: Fields{"fred", "2", "1"}}
|
||||||
|
// )
|
||||||
|
|
||||||
|
// uu := colorerUCs{
|
||||||
|
// // Add AllNS
|
||||||
|
// {"", RowEvent{Kind: EventAdd, Row: ns}, AddColor},
|
||||||
|
// // Add NS
|
||||||
|
// {"blee", RowEvent{Kind: EventAdd, Row: nonNS}, AddColor},
|
||||||
|
// // Mod AllNS
|
||||||
|
// {"", RowEvent{Kind: EventUpdate, Row: ns}, ModColor},
|
||||||
|
// // Mod NS
|
||||||
|
// {"blee", RowEvent{Kind: EventUpdate, Row: nonNS}, ModColor},
|
||||||
|
// // Unchanged cool
|
||||||
|
// {"", RowEvent{Kind: EventUnchanged, Row: ns}, StdColor},
|
||||||
|
// // Bust AllNS
|
||||||
|
// {"", RowEvent{Kind: EventUnchanged, Row: bustNS}, ErrColor},
|
||||||
|
// // Bust NS
|
||||||
|
// {"blee", RowEvent{Kind: EventUnchanged, Row: bustNoNS}, ErrColor},
|
||||||
|
// }
|
||||||
|
// for _, u := range uu {
|
||||||
|
// assert.Equal(t, u.e, dpColorer(u.ns, u.r))
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// func TestPdbColorer(t *testing.T) {
|
||||||
|
// var (
|
||||||
|
// ns = Row{Fields: Fields{"blee", "fred", "1", "1", "1", "1", "1"}}
|
||||||
|
// nonNS = Row{Fields: Fields{"fred", "1", "1", "1", "1", "1"}}
|
||||||
|
// bustNS = Row{Fields: Fields{"blee", "fred", "1", "1", "1", "1", "2"}}
|
||||||
|
// bustNoNS = Row{Fields: Fields{"fred", "1", "1", "1", "1", "2"}}
|
||||||
|
// )
|
||||||
|
|
||||||
|
// uu := colorerUCs{
|
||||||
|
// // Add AllNS
|
||||||
|
// {"", RowEvent{Kind: EventAdd, Row: ns}, AddColor},
|
||||||
|
// // Add NS
|
||||||
|
// {"blee", RowEvent{Kind: EventAdd, Row: nonNS}, AddColor},
|
||||||
|
// // Mod AllNS
|
||||||
|
// {"", RowEvent{Kind: EventUpdate, Row: ns}, ModColor},
|
||||||
|
// // Mod NS
|
||||||
|
// {"blee", RowEvent{Kind: EventUpdate, Row: nonNS}, ModColor},
|
||||||
|
// // Unchanged cool
|
||||||
|
// {"", RowEvent{Kind: EventUnchanged, Row: ns}, StdColor},
|
||||||
|
// // Bust AllNS
|
||||||
|
// {"", RowEvent{Kind: EventUnchanged, Row: bustNS}, ErrColor},
|
||||||
|
// // Bust NS
|
||||||
|
// {"blee", RowEvent{Kind: EventUnchanged, Row: bustNoNS}, ErrColor},
|
||||||
|
// }
|
||||||
|
// for _, u := range uu {
|
||||||
|
// assert.Equal(t, u.e, pdbColorer(u.ns, u.r))
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// func TestPVColorer(t *testing.T) {
|
||||||
|
// var (
|
||||||
|
// pv = Row{Fields: Fields{"blee", "1G", "RO", "Duh", "Bound"}}
|
||||||
|
// bustPv = Row{Fields: Fields{"blee", "1G", "RO", "Duh", "UnBound"}}
|
||||||
|
// )
|
||||||
|
|
||||||
|
// uu := colorerUCs{
|
||||||
|
// // Add Normal
|
||||||
|
// {"", RowEvent{Kind: EventAdd, Row: pv}, AddColor},
|
||||||
|
// // Unchanged Bound
|
||||||
|
// {"", RowEvent{Kind: EventUnchanged, Row: pv}, StdColor},
|
||||||
|
// // Unchanged Bound
|
||||||
|
// {"", RowEvent{Kind: EventUnchanged, Row: bustPv}, ErrColor},
|
||||||
|
// }
|
||||||
|
// for _, u := range uu {
|
||||||
|
// assert.Equal(t, u.e, pvColorer(u.ns, u.r))
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// func TestPVCColorer(t *testing.T) {
|
||||||
|
// var (
|
||||||
|
// pvc = Row{Fields: Fields{"blee", "fred", "Bound"}}
|
||||||
|
// bustPvc = Row{Fields: Fields{"blee", "fred", "UnBound"}}
|
||||||
|
// )
|
||||||
|
|
||||||
|
// uu := colorerUCs{
|
||||||
|
// // Add Normal
|
||||||
|
// {"", RowEvent{Kind: EventAdd, Row: pvc}, AddColor},
|
||||||
|
// // Add Bound
|
||||||
|
// {"", RowEvent{Kind: EventUnchanged, Row: bustPvc}, ErrColor},
|
||||||
|
// }
|
||||||
|
// for _, u := range uu {
|
||||||
|
// assert.Equal(t, u.e, pvcColorer(u.ns, u.r))
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// func TestCtxColorer(t *testing.T) {
|
||||||
|
// var (
|
||||||
|
// ctx = Row{Fields: Fields{"blee"}}
|
||||||
|
// defCtx = Row{Fields: Fields{"blee*"}}
|
||||||
|
// )
|
||||||
|
|
||||||
|
// uu := colorerUCs{
|
||||||
|
// // Add Normal
|
||||||
|
// {"", RowEvent{Kind: EventAdd, Row: ctx}, AddColor},
|
||||||
|
// // Add Default
|
||||||
|
// {"", RowEvent{Kind: EventAdd, Row: defCtx}, AddColor},
|
||||||
|
// // Mod Normal
|
||||||
|
// {"", RowEvent{Kind: EventUpdate, Row: ctx}, ModColor},
|
||||||
|
// // Mod Default
|
||||||
|
// {"", RowEvent{Kind: EventUpdate, Row: defCtx}, ModColor},
|
||||||
|
// // Unchanged Normal
|
||||||
|
// {"", RowEvent{Kind: EventUnchanged, Row: ctx}, StdColor},
|
||||||
|
// // Unchanged Default
|
||||||
|
// {"", RowEvent{Kind: EventUnchanged, Row: defCtx}, HighlightColor},
|
||||||
|
// }
|
||||||
|
// for _, u := range uu {
|
||||||
|
// assert.Equal(t, u.e, ctxColorer(u.ns, u.r))
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// func TestPodColorer(t *testing.T) {
|
||||||
|
// var (
|
||||||
|
// nsRow = Row{Fields: Fields{"blee", "fred", "1/1", "Running"}}
|
||||||
|
// toastNS = Row{Fields: Fields{"blee", "fred", "1/1", "Boom"}}
|
||||||
|
// notReadyNS = Row{Fields: Fields{"blee", "fred", "0/1", "Boom"}}
|
||||||
|
// row = Row{Fields: Fields{"fred", "1/1", "Running"}}
|
||||||
|
// toast = Row{Fields: Fields{"fred", "1/1", "Boom"}}
|
||||||
|
// notReady = Row{Fields: Fields{"fred", "0/1", "Boom"}}
|
||||||
|
// )
|
||||||
|
|
||||||
|
// uu := colorerUCs{
|
||||||
|
// // Add allNS
|
||||||
|
// {"", RowEvent{Kind: EventAdd, Row: nsRow}, AddColor},
|
||||||
|
// // Add Namespaced
|
||||||
|
// {"blee", RowEvent{Kind: EventAdd, Row: row}, AddColor},
|
||||||
|
// // Mod AllNS
|
||||||
|
// {"", RowEvent{Kind: EventUpdate, Row: nsRow}, ModColor},
|
||||||
|
// // Mod Namespaced
|
||||||
|
// {"blee", RowEvent{Kind: EventUpdate, Row: row}, ModColor},
|
||||||
|
// // Mod Busted AllNS
|
||||||
|
// {"", RowEvent{Kind: EventUpdate, Row: toastNS}, ErrColor},
|
||||||
|
// // Mod Busted Namespaced
|
||||||
|
// {"blee", RowEvent{Kind: EventUpdate, Row: toast}, ErrColor},
|
||||||
|
// // NotReady AllNS
|
||||||
|
// {"", RowEvent{Kind: EventUpdate, Row: notReadyNS}, ErrColor},
|
||||||
|
// // NotReady Namespaced
|
||||||
|
// {"blee", RowEvent{Kind: EventUpdate, Row: notReady}, ErrColor},
|
||||||
|
// }
|
||||||
|
// for _, u := range uu {
|
||||||
|
// assert.Equal(t, u.e, podColorer(u.ns, u.r))
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
@ -7,6 +7,7 @@ import (
|
||||||
|
|
||||||
"github.com/derailed/k9s/internal/k8s"
|
"github.com/derailed/k9s/internal/k8s"
|
||||||
"github.com/derailed/tview"
|
"github.com/derailed/tview"
|
||||||
|
"github.com/gdamore/tcell"
|
||||||
v1 "k8s.io/api/core/v1"
|
v1 "k8s.io/api/core/v1"
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
mv1beta1 "k8s.io/metrics/pkg/apis/metrics/v1beta1"
|
mv1beta1 "k8s.io/metrics/pkg/apis/metrics/v1beta1"
|
||||||
|
|
@ -35,7 +36,29 @@ type Container struct{}
|
||||||
|
|
||||||
// ColorerFunc colors a resource row.
|
// ColorerFunc colors a resource row.
|
||||||
func (Container) ColorerFunc() ColorerFunc {
|
func (Container) ColorerFunc() ColorerFunc {
|
||||||
return DefaultColorer
|
return func(ns string, r RowEvent) tcell.Color {
|
||||||
|
c := DefaultColorer(ns, r)
|
||||||
|
|
||||||
|
readyCol := 2
|
||||||
|
if strings.TrimSpace(r.Row.Fields[readyCol]) == "false" {
|
||||||
|
c = ErrColor
|
||||||
|
}
|
||||||
|
|
||||||
|
stateCol := readyCol + 1
|
||||||
|
switch strings.TrimSpace(r.Row.Fields[stateCol]) {
|
||||||
|
case ContainerCreating, PodInitializing:
|
||||||
|
return AddColor
|
||||||
|
case Terminating, Initialized:
|
||||||
|
return HighlightColor
|
||||||
|
case Completed:
|
||||||
|
return CompletedColor
|
||||||
|
case Running:
|
||||||
|
default:
|
||||||
|
c = ErrColor
|
||||||
|
}
|
||||||
|
|
||||||
|
return c
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Header returns a header row.
|
// Header returns a header row.
|
||||||
|
|
@ -53,12 +76,12 @@ func (Container) Header(ns string) HeaderRow {
|
||||||
Header{Name: "%CPU", Align: tview.AlignRight},
|
Header{Name: "%CPU", Align: tview.AlignRight},
|
||||||
Header{Name: "%MEM", Align: tview.AlignRight},
|
Header{Name: "%MEM", Align: tview.AlignRight},
|
||||||
Header{Name: "PORTS"},
|
Header{Name: "PORTS"},
|
||||||
Header{Name: "AGE"},
|
Header{Name: "AGE", Decorator: ageDecorator},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Render renders a K8s resource to screen.
|
// Render renders a K8s resource to screen.
|
||||||
func (Container) Render(o interface{}, name string, r *Row) error {
|
func (c Container) Render(o interface{}, name string, r *Row) error {
|
||||||
oo, ok := o.(ContainerWithMetrics)
|
oo, ok := o.(ContainerWithMetrics)
|
||||||
if !ok {
|
if !ok {
|
||||||
return fmt.Errorf("Expected ContainerWithMetrics, but got %T", o)
|
return fmt.Errorf("Expected ContainerWithMetrics, but got %T", o)
|
||||||
|
|
@ -66,14 +89,15 @@ func (Container) Render(o interface{}, name string, r *Row) error {
|
||||||
|
|
||||||
co, cs := oo.Container(), oo.ContainerStatus()
|
co, cs := oo.Container(), oo.ContainerStatus()
|
||||||
|
|
||||||
c, p := gatherMetrics(co, oo.Metrics())
|
cur, perc := gatherMetrics(co, oo.Metrics())
|
||||||
ready, state, restarts := "false", MissingValue, "0"
|
ready, state, restarts := "false", MissingValue, "0"
|
||||||
if cs != nil {
|
if cs != nil {
|
||||||
ready, state, restarts = boolToStr(cs.Ready), toState(cs.State), strconv.Itoa(int(cs.RestartCount))
|
ready, state, restarts = boolToStr(cs.Ready), toState(cs.State), strconv.Itoa(int(cs.RestartCount))
|
||||||
}
|
}
|
||||||
|
|
||||||
fields := make(Fields, 0, len(r.Fields))
|
r.ID = co.Name
|
||||||
fields = append(fields,
|
r.Fields = make(Fields, 0, len(c.Header(AllNamespaces)))
|
||||||
|
r.Fields = append(r.Fields,
|
||||||
co.Name,
|
co.Name,
|
||||||
co.Image,
|
co.Image,
|
||||||
ready,
|
ready,
|
||||||
|
|
@ -81,14 +105,13 @@ func (Container) Render(o interface{}, name string, r *Row) error {
|
||||||
boolToStr(oo.IsInit()),
|
boolToStr(oo.IsInit()),
|
||||||
restarts,
|
restarts,
|
||||||
probe(co.LivenessProbe)+":"+probe(co.ReadinessProbe),
|
probe(co.LivenessProbe)+":"+probe(co.ReadinessProbe),
|
||||||
c.cpu,
|
cur.cpu,
|
||||||
c.mem,
|
cur.mem,
|
||||||
p.cpu,
|
perc.cpu,
|
||||||
p.mem,
|
perc.mem,
|
||||||
toStrPorts(co.Ports),
|
toStrPorts(co.Ports),
|
||||||
toAge(oo.Age()),
|
toAge(oo.Age()),
|
||||||
)
|
)
|
||||||
r.ID, r.Fields = co.Name, fields
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
@ -96,21 +119,6 @@ func (Container) Render(o interface{}, name string, r *Row) error {
|
||||||
// ----------------------------------------------------------------------------
|
// ----------------------------------------------------------------------------
|
||||||
// Helpers...
|
// Helpers...
|
||||||
|
|
||||||
// func findContainer(po v1.Pod, n string) *v1.Container {
|
|
||||||
// for _, c := range po.Spec.InitContainers {
|
|
||||||
// if c.Name == n {
|
|
||||||
// return &c
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// for _, c := range po.Spec.Containers {
|
|
||||||
// if c.Name == n {
|
|
||||||
// return &c
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// return nil
|
|
||||||
// }
|
|
||||||
|
|
||||||
func gatherMetrics(co *v1.Container, mx *mv1beta1.ContainerMetrics) (c, p metric) {
|
func gatherMetrics(co *v1.Container, mx *mv1beta1.ContainerMetrics) (c, p metric) {
|
||||||
c, p = noMetric(), noMetric()
|
c, p = noMetric(), noMetric()
|
||||||
if mx == nil {
|
if mx == nil {
|
||||||
|
|
@ -2,8 +2,14 @@ package render
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
api "k8s.io/client-go/tools/clientcmd/api"
|
"github.com/derailed/k9s/internal/k8s"
|
||||||
|
"github.com/gdamore/tcell"
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||||
|
"k8s.io/client-go/tools/clientcmd/api"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Context renders a K8s ConfigMap to screen.
|
// Context renders a K8s ConfigMap to screen.
|
||||||
|
|
@ -11,7 +17,17 @@ type Context struct{}
|
||||||
|
|
||||||
// ColorerFunc colors a resource row.
|
// ColorerFunc colors a resource row.
|
||||||
func (Context) ColorerFunc() ColorerFunc {
|
func (Context) ColorerFunc() ColorerFunc {
|
||||||
return DefaultColorer
|
return func(ns string, r RowEvent) tcell.Color {
|
||||||
|
c := DefaultColorer(ns, r)
|
||||||
|
if r.Kind == EventAdd || r.Kind == EventUpdate {
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
if strings.Contains(strings.TrimSpace(r.Row.Fields[0]), "*") {
|
||||||
|
c = HighlightColor
|
||||||
|
}
|
||||||
|
|
||||||
|
return c
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Header returns a header row.
|
// Header returns a header row.
|
||||||
|
|
@ -25,16 +41,58 @@ func (Context) Header(ns string) HeaderRow {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Render renders a K8s resource to screen.
|
// Render renders a K8s resource to screen.
|
||||||
func (Context) Render(o interface{}, _ string, r *Row) error {
|
func (c Context) Render(o interface{}, _ string, r *Row) error {
|
||||||
i, ok := o.(*api.Context)
|
ctx, ok := o.(*NamedContext)
|
||||||
if !ok {
|
if !ok {
|
||||||
return fmt.Errorf("Expected api.Context, but got %T", o)
|
return fmt.Errorf("Expected NamedContext, but got %T", o)
|
||||||
}
|
}
|
||||||
|
|
||||||
r.Fields[0] = r.ID
|
name := ctx.Name
|
||||||
r.Fields[1] = i.Cluster
|
if ctx.IsCurrentContext(ctx.Name) {
|
||||||
r.Fields[2] = i.AuthInfo
|
name += "(*)"
|
||||||
r.Fields[3] = i.Namespace
|
}
|
||||||
|
|
||||||
|
r.ID = ctx.Name
|
||||||
|
r.Fields = Fields{
|
||||||
|
name,
|
||||||
|
ctx.Context.Cluster,
|
||||||
|
ctx.Context.AuthInfo,
|
||||||
|
ctx.Context.Namespace,
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Helpers...
|
||||||
|
|
||||||
|
// NamedContext represents a named cluster context.
|
||||||
|
type NamedContext struct {
|
||||||
|
Name string
|
||||||
|
Context *api.Context
|
||||||
|
config *k8s.Config
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewNamedContext returns a new named context.
|
||||||
|
func NewNamedContext(c *k8s.Config, n string, ctx *api.Context) *NamedContext {
|
||||||
|
return &NamedContext{Name: n, Context: ctx, config: c}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MustCurrentContextName return the active context name.
|
||||||
|
func (c *NamedContext) IsCurrentContext(n string) bool {
|
||||||
|
cl, err := c.config.CurrentContextName()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal().Err(err).Msg("Fetching current context")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return cl == n
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetObjectKind returns a schema object.
|
||||||
|
func (c *NamedContext) GetObjectKind() schema.ObjectKind {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopyObject returns a container copy.
|
||||||
|
func (c *NamedContext) DeepCopyObject() runtime.Object {
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@ func (ClusterRole) ColorerFunc() ColorerFunc {
|
||||||
func (ClusterRole) Header(string) HeaderRow {
|
func (ClusterRole) Header(string) HeaderRow {
|
||||||
return HeaderRow{
|
return HeaderRow{
|
||||||
Header{Name: "NAME"},
|
Header{Name: "NAME"},
|
||||||
Header{Name: "AGE"},
|
Header{Name: "AGE", Decorator: ageDecorator},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,7 @@ func (ClusterRoleBinding) Header(string) HeaderRow {
|
||||||
Header{Name: "ROLE"},
|
Header{Name: "ROLE"},
|
||||||
Header{Name: "KIND"},
|
Header{Name: "KIND"},
|
||||||
Header{Name: "SUBJECTS"},
|
Header{Name: "SUBJECTS"},
|
||||||
Header{Name: "AGE"},
|
Header{Name: "AGE", Decorator: ageDecorator},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
package render
|
package render
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
|
@ -22,7 +21,7 @@ func (CustomResourceDefinition) ColorerFunc() ColorerFunc {
|
||||||
func (CustomResourceDefinition) Header(string) HeaderRow {
|
func (CustomResourceDefinition) Header(string) HeaderRow {
|
||||||
return HeaderRow{
|
return HeaderRow{
|
||||||
Header{Name: "NAME"},
|
Header{Name: "NAME"},
|
||||||
Header{Name: "AGE"},
|
Header{Name: "AGE", Decorator: ageDecorator},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -50,52 +49,53 @@ func (CustomResourceDefinition) Render(o interface{}, ns string, r *Row) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// TypeMeta represents resource type meta data.
|
// BOZO!!
|
||||||
type TypeMeta struct {
|
// // TypeMeta represents resource type meta data.
|
||||||
Name string
|
// type TypeMeta struct {
|
||||||
Namespaced bool
|
// Name string
|
||||||
Group string
|
// Namespaced bool
|
||||||
Version string
|
// Group string
|
||||||
Kind string
|
// Version string
|
||||||
Singular string
|
// Kind string
|
||||||
Plural string
|
// Singular string
|
||||||
ShortNames []string
|
// Plural string
|
||||||
}
|
// ShortNames []string
|
||||||
|
// }
|
||||||
|
|
||||||
func (CustomResourceDefinition) Meta(o interface{}) (TypeMeta, error) {
|
// func (CustomResourceDefinition) Meta(o interface{}) (TypeMeta, error) {
|
||||||
var m TypeMeta
|
// var m TypeMeta
|
||||||
|
|
||||||
crd, ok := o.(*unstructured.Unstructured)
|
// crd, ok := o.(*unstructured.Unstructured)
|
||||||
if !ok {
|
// if !ok {
|
||||||
return m, fmt.Errorf("Expected CustomResourceDefinition, but got %T", o)
|
// return m, fmt.Errorf("Expected CustomResourceDefinition, but got %T", o)
|
||||||
}
|
// }
|
||||||
|
|
||||||
spec, ok := crd.Object["spec"].(map[string]interface{})
|
// spec, ok := crd.Object["spec"].(map[string]interface{})
|
||||||
if !ok {
|
// if !ok {
|
||||||
return m, errors.New("missing crd specs")
|
// return m, errors.New("missing crd specs")
|
||||||
}
|
// }
|
||||||
|
|
||||||
if meta, ok := crd.Object["metadata"].(map[string]interface{}); ok {
|
// if meta, ok := crd.Object["metadata"].(map[string]interface{}); ok {
|
||||||
m.Name = meta["name"].(string)
|
// m.Name = meta["name"].(string)
|
||||||
}
|
// }
|
||||||
m.Group, m.Version = spec["group"].(string), spec["version"].(string)
|
// m.Group, m.Version = spec["group"].(string), spec["version"].(string)
|
||||||
m.Namespaced = isNamespaced(spec["scope"].(string))
|
// m.Namespaced = isNamespaced(spec["scope"].(string))
|
||||||
names, ok := spec["names"].(map[string]interface{})
|
// names, ok := spec["names"].(map[string]interface{})
|
||||||
if !ok {
|
// if !ok {
|
||||||
return m, errors.New("missing crd names")
|
// return m, errors.New("missing crd names")
|
||||||
}
|
// }
|
||||||
m.Kind = names["kind"].(string)
|
// m.Kind = names["kind"].(string)
|
||||||
m.Singular, m.Plural = names["singular"].(string), names["plural"].(string)
|
// m.Singular, m.Plural = names["singular"].(string), names["plural"].(string)
|
||||||
if names["shortNames"] != nil {
|
// if names["shortNames"] != nil {
|
||||||
for _, s := range names["shortNames"].([]interface{}) {
|
// for _, s := range names["shortNames"].([]interface{}) {
|
||||||
m.ShortNames = append(m.ShortNames, s.(string))
|
// m.ShortNames = append(m.ShortNames, s.(string))
|
||||||
}
|
// }
|
||||||
} else {
|
// } else {
|
||||||
m.ShortNames = nil
|
// m.ShortNames = nil
|
||||||
}
|
// }
|
||||||
return m, nil
|
// return m, nil
|
||||||
}
|
// }
|
||||||
|
|
||||||
func isNamespaced(scope string) bool {
|
// func isNamespaced(scope string) bool {
|
||||||
return scope == "Namespaced"
|
// return scope == "Namespaced"
|
||||||
}
|
// }
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
package render
|
package render
|
||||||
|
|
||||||
|
import "github.com/rs/zerolog/log"
|
||||||
|
|
||||||
// DeltaRow represents a collection of row detlas between old and new row.
|
// DeltaRow represents a collection of row detlas between old and new row.
|
||||||
type DeltaRow []string
|
type DeltaRow []string
|
||||||
|
|
||||||
|
|
@ -7,10 +9,11 @@ type DeltaRow []string
|
||||||
func NewDeltaRow(o, n Row) DeltaRow {
|
func NewDeltaRow(o, n Row) DeltaRow {
|
||||||
deltas := make(DeltaRow, len(o.Fields))
|
deltas := make(DeltaRow, len(o.Fields))
|
||||||
// Exclude age col
|
// Exclude age col
|
||||||
fields := o.Fields[:len(o.Fields)-1]
|
oldFields := o.Fields[:len(o.Fields)-1]
|
||||||
for i, v := range fields {
|
for i, old := range oldFields {
|
||||||
if v != "" && n.Fields[i] != v {
|
if old != "" && old != n.Fields[i] {
|
||||||
deltas[i] = v
|
log.Debug().Msgf("OLD VS NEW %q:%q", old, n.Fields[i])
|
||||||
|
deltas[i] = old
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -31,3 +34,13 @@ func (d DeltaRow) IsBlank() bool {
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Clone returns a delta copy.
|
||||||
|
func (d DeltaRow) Clone() DeltaRow {
|
||||||
|
res := make(DeltaRow, len(d))
|
||||||
|
for i, f := range d {
|
||||||
|
res[i] = f
|
||||||
|
}
|
||||||
|
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,9 +3,13 @@ package render
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/derailed/tview"
|
"github.com/derailed/tview"
|
||||||
|
"github.com/gdamore/tcell"
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
appsv1 "k8s.io/api/apps/v1"
|
appsv1 "k8s.io/api/apps/v1"
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||||
"k8s.io/apimachinery/pkg/runtime"
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
)
|
)
|
||||||
|
|
@ -19,7 +23,23 @@ func isAllNamespace(ns string) bool {
|
||||||
|
|
||||||
// ColorerFunc colors a resource row.
|
// ColorerFunc colors a resource row.
|
||||||
func (Deployment) ColorerFunc() ColorerFunc {
|
func (Deployment) ColorerFunc() ColorerFunc {
|
||||||
return DefaultColorer
|
return func(ns string, r RowEvent) tcell.Color {
|
||||||
|
c := DefaultColorer(ns, r)
|
||||||
|
if r.Kind == EventAdd || r.Kind == EventUpdate {
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
markCol := 2
|
||||||
|
if ns != AllNamespaces {
|
||||||
|
markCol = 1
|
||||||
|
}
|
||||||
|
tokens := strings.Split(r.Row.Fields[markCol], "/")
|
||||||
|
if tokens[0] != tokens[1] {
|
||||||
|
return ErrColor
|
||||||
|
}
|
||||||
|
|
||||||
|
return StdColor
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Header returns a header row.
|
// Header returns a header row.
|
||||||
|
|
@ -31,16 +51,16 @@ func (Deployment) Header(ns string) HeaderRow {
|
||||||
|
|
||||||
return append(h,
|
return append(h,
|
||||||
Header{Name: "NAME"},
|
Header{Name: "NAME"},
|
||||||
Header{Name: "DESIRED", Align: tview.AlignRight},
|
Header{Name: "READY"},
|
||||||
Header{Name: "CURRENT", Align: tview.AlignRight},
|
|
||||||
Header{Name: "UP-TO-DATE", Align: tview.AlignRight},
|
Header{Name: "UP-TO-DATE", Align: tview.AlignRight},
|
||||||
Header{Name: "AVAILABLE", Align: tview.AlignRight},
|
Header{Name: "AVAILABLE", Align: tview.AlignRight},
|
||||||
Header{Name: "AGE"},
|
Header{Name: "SELECTOR"},
|
||||||
|
Header{Name: "AGE", Decorator: ageDecorator},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Render renders a K8s resource to screen.
|
// Render renders a K8s resource to screen.
|
||||||
func (Deployment) Render(o interface{}, ns string, r *Row) error {
|
func (d Deployment) Render(o interface{}, ns string, r *Row) error {
|
||||||
raw, ok := o.(*unstructured.Unstructured)
|
raw, ok := o.(*unstructured.Unstructured)
|
||||||
if !ok {
|
if !ok {
|
||||||
return fmt.Errorf("Expected Deployment, but got %T", o)
|
return fmt.Errorf("Expected Deployment, but got %T", o)
|
||||||
|
|
@ -51,20 +71,31 @@ func (Deployment) Render(o interface{}, ns string, r *Row) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
fields := make(Fields, 0, len(r.Fields))
|
r.ID = MetaFQN(dp.ObjectMeta)
|
||||||
|
r.Fields = make(Fields, 0, len(d.Header(ns)))
|
||||||
if isAllNamespace(ns) {
|
if isAllNamespace(ns) {
|
||||||
fields = append(fields, dp.Namespace)
|
r.Fields = append(r.Fields, dp.Namespace)
|
||||||
}
|
}
|
||||||
fields = append(fields,
|
r.Fields = append(r.Fields,
|
||||||
dp.Name,
|
dp.Name,
|
||||||
strconv.Itoa(int(*dp.Spec.Replicas)),
|
strconv.Itoa(int(dp.Status.AvailableReplicas))+"/"+strconv.Itoa(int(*dp.Spec.Replicas)),
|
||||||
strconv.Itoa(int(dp.Status.Replicas)),
|
|
||||||
strconv.Itoa(int(dp.Status.UpdatedReplicas)),
|
strconv.Itoa(int(dp.Status.UpdatedReplicas)),
|
||||||
strconv.Itoa(int(dp.Status.AvailableReplicas)),
|
strconv.Itoa(int(dp.Status.AvailableReplicas)),
|
||||||
|
asSelector(dp.Spec.Selector),
|
||||||
toAge(dp.ObjectMeta.CreationTimestamp),
|
toAge(dp.ObjectMeta.CreationTimestamp),
|
||||||
)
|
)
|
||||||
|
|
||||||
r.ID, r.Fields = MetaFQN(dp.ObjectMeta), fields
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//Helpers...
|
||||||
|
|
||||||
|
func asSelector(s *metav1.LabelSelector) string {
|
||||||
|
sel, err := metav1.LabelSelectorAsSelector(s)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Msg("Selector conversion failed")
|
||||||
|
return NAValue
|
||||||
|
}
|
||||||
|
|
||||||
|
return sel.String()
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,5 +13,5 @@ func TestDeploymentRender(t *testing.T) {
|
||||||
c.Render(load(t, "dp"), "", &r)
|
c.Render(load(t, "dp"), "", &r)
|
||||||
|
|
||||||
assert.Equal(t, "icx/icx-db", r.ID)
|
assert.Equal(t, "icx/icx-db", r.ID)
|
||||||
assert.Equal(t, render.Fields{"icx", "icx-db", "1", "1", "1", "1"}, r.Fields[:6])
|
assert.Equal(t, render.Fields{"icx", "icx-db", "1/1", "1", "1", "app=icx-db"}, r.Fields[:6])
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,8 +3,10 @@ package render
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/derailed/tview"
|
"github.com/derailed/tview"
|
||||||
|
"github.com/gdamore/tcell"
|
||||||
appsv1 "k8s.io/api/apps/v1"
|
appsv1 "k8s.io/api/apps/v1"
|
||||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||||
"k8s.io/apimachinery/pkg/runtime"
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
|
|
@ -15,7 +17,22 @@ type DaemonSet struct{}
|
||||||
|
|
||||||
// ColorerFunc colors a resource row.
|
// ColorerFunc colors a resource row.
|
||||||
func (DaemonSet) ColorerFunc() ColorerFunc {
|
func (DaemonSet) ColorerFunc() ColorerFunc {
|
||||||
return DefaultColorer
|
return func(ns string, r RowEvent) tcell.Color {
|
||||||
|
c := DefaultColorer(ns, r)
|
||||||
|
if r.Kind == EventAdd || r.Kind == EventUpdate {
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
markCol := 2
|
||||||
|
if ns != AllNamespaces {
|
||||||
|
markCol = 1
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(r.Row.Fields[markCol]) != strings.TrimSpace(r.Row.Fields[markCol+2]) {
|
||||||
|
return ErrColor
|
||||||
|
}
|
||||||
|
|
||||||
|
return StdColor
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Header returns a header row.
|
// Header returns a header row.
|
||||||
|
|
@ -33,12 +50,12 @@ func (DaemonSet) Header(ns string) HeaderRow {
|
||||||
Header{Name: "UP-TO-DATE", Align: tview.AlignRight},
|
Header{Name: "UP-TO-DATE", Align: tview.AlignRight},
|
||||||
Header{Name: "AVAILABLE", Align: tview.AlignRight},
|
Header{Name: "AVAILABLE", Align: tview.AlignRight},
|
||||||
Header{Name: "NODE_SELECTOR"},
|
Header{Name: "NODE_SELECTOR"},
|
||||||
Header{Name: "AGE"},
|
Header{Name: "AGE", Decorator: ageDecorator},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Render renders a K8s resource to screen.
|
// Render renders a K8s resource to screen.
|
||||||
func (DaemonSet) Render(o interface{}, ns string, r *Row) error {
|
func (d DaemonSet) Render(o interface{}, ns string, r *Row) error {
|
||||||
raw, ok := o.(*unstructured.Unstructured)
|
raw, ok := o.(*unstructured.Unstructured)
|
||||||
if !ok {
|
if !ok {
|
||||||
return fmt.Errorf("Expected DaemonSet, but got %T", o)
|
return fmt.Errorf("Expected DaemonSet, but got %T", o)
|
||||||
|
|
@ -49,11 +66,12 @@ func (DaemonSet) Render(o interface{}, ns string, r *Row) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
fields := make(Fields, 0, len(r.Fields))
|
r.ID = MetaFQN(ds.ObjectMeta)
|
||||||
|
r.Fields = make(Fields, 0, len(d.Header(ns)))
|
||||||
if isAllNamespace(ns) {
|
if isAllNamespace(ns) {
|
||||||
fields = append(fields, ds.Namespace)
|
r.Fields = append(r.Fields, ds.Namespace)
|
||||||
}
|
}
|
||||||
fields = append(fields,
|
r.Fields = append(r.Fields,
|
||||||
ds.Name,
|
ds.Name,
|
||||||
strconv.Itoa(int(ds.Status.DesiredNumberScheduled)),
|
strconv.Itoa(int(ds.Status.DesiredNumberScheduled)),
|
||||||
strconv.Itoa(int(ds.Status.CurrentNumberScheduled)),
|
strconv.Itoa(int(ds.Status.CurrentNumberScheduled)),
|
||||||
|
|
@ -63,7 +81,6 @@ func (DaemonSet) Render(o interface{}, ns string, r *Row) error {
|
||||||
mapToStr(ds.Spec.Template.Spec.NodeSelector),
|
mapToStr(ds.Spec.Template.Spec.NodeSelector),
|
||||||
toAge(ds.ObjectMeta.CreationTimestamp),
|
toAge(ds.ObjectMeta.CreationTimestamp),
|
||||||
)
|
)
|
||||||
r.ID, r.Fields = MetaFQN(ds.ObjectMeta), fields
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,7 @@ func (Endpoints) Header(ns string) HeaderRow {
|
||||||
return append(h,
|
return append(h,
|
||||||
Header{Name: "NAME"},
|
Header{Name: "NAME"},
|
||||||
Header{Name: "ENDPOINTS"},
|
Header{Name: "ENDPOINTS"},
|
||||||
Header{Name: "AGE"},
|
Header{Name: "AGE", Decorator: ageDecorator},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,8 +3,10 @@ package render
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/derailed/tview"
|
"github.com/derailed/tview"
|
||||||
|
"github.com/gdamore/tcell"
|
||||||
v1 "k8s.io/api/core/v1"
|
v1 "k8s.io/api/core/v1"
|
||||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||||
"k8s.io/apimachinery/pkg/runtime"
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
|
|
@ -15,7 +17,22 @@ type Event struct{}
|
||||||
|
|
||||||
// ColorerFunc colors a resource row.
|
// ColorerFunc colors a resource row.
|
||||||
func (Event) ColorerFunc() ColorerFunc {
|
func (Event) ColorerFunc() ColorerFunc {
|
||||||
return DefaultColorer
|
return func(ns string, r RowEvent) tcell.Color {
|
||||||
|
c := DefaultColorer(ns, r)
|
||||||
|
|
||||||
|
markCol := 3
|
||||||
|
if ns != AllNamespaces {
|
||||||
|
markCol = 2
|
||||||
|
}
|
||||||
|
switch strings.TrimSpace(r.Row.Fields[markCol]) {
|
||||||
|
case "Failed":
|
||||||
|
c = ErrColor
|
||||||
|
case "Killing":
|
||||||
|
c = KillColor
|
||||||
|
}
|
||||||
|
|
||||||
|
return c
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Header returns a header rbw.
|
// Header returns a header rbw.
|
||||||
|
|
@ -31,7 +48,7 @@ func (Event) Header(ns string) HeaderRow {
|
||||||
Header{Name: "SOURCE"},
|
Header{Name: "SOURCE"},
|
||||||
Header{Name: "COUNT", Align: tview.AlignRight},
|
Header{Name: "COUNT", Align: tview.AlignRight},
|
||||||
Header{Name: "MESSAGE"},
|
Header{Name: "MESSAGE"},
|
||||||
Header{Name: "AGE"},
|
Header{Name: "AGE", Decorator: ageDecorator},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,11 @@
|
||||||
package render
|
package render
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"sort"
|
"sort"
|
||||||
|
|
||||||
"github.com/gdamore/tcell"
|
"github.com/gdamore/tcell"
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
|
@ -53,26 +55,63 @@ func NewDeltaRowEvent(row Row, delta DeltaRow) RowEvent {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Clone returns a rowevent deep copy.
|
||||||
|
func (r RowEvent) Clone() RowEvent {
|
||||||
|
return RowEvent{
|
||||||
|
Kind: r.Kind,
|
||||||
|
Row: r.Row.Clone(),
|
||||||
|
Deltas: r.Deltas.Clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clone returns a rowevents deep copy.
|
||||||
|
func (rr RowEvents) Clone() RowEvents {
|
||||||
|
res := make(RowEvents, len(rr))
|
||||||
|
for i, r := range rr {
|
||||||
|
res[i] = r.Clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
// Upsert add or update a row if it exists.
|
||||||
|
func (rr RowEvents) Upsert(e RowEvent) RowEvents {
|
||||||
|
if idx, ok := rr.FindIndex(e.Row.ID); ok {
|
||||||
|
rr[idx] = e
|
||||||
|
} else {
|
||||||
|
rr = append(rr, e)
|
||||||
|
}
|
||||||
|
return rr
|
||||||
|
}
|
||||||
|
|
||||||
// Delete removes an element by id.
|
// Delete removes an element by id.
|
||||||
func (re RowEvents) Delete(id string) RowEvents {
|
func (rr RowEvents) Delete(id string) RowEvents {
|
||||||
idx, ok := re.FindIndex(id)
|
idx, ok := rr.FindIndex(id)
|
||||||
if !ok {
|
if !ok {
|
||||||
return re
|
return rr
|
||||||
}
|
}
|
||||||
|
|
||||||
if idx == 0 {
|
if idx == 0 {
|
||||||
return re[1:]
|
return rr[1:]
|
||||||
}
|
}
|
||||||
if idx == len(re)-1 {
|
if idx == len(rr)-1 {
|
||||||
return re[:len(re)-1]
|
return rr[:len(rr)-1]
|
||||||
}
|
}
|
||||||
|
|
||||||
return append(re[:idx], re[idx+1:]...)
|
return append(rr[:idx], rr[idx+1:]...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear delete all row events
|
||||||
|
func (rr RowEvents) Clear() RowEvents {
|
||||||
|
for _, e := range rr {
|
||||||
|
rr = rr.Delete(e.Row.ID)
|
||||||
|
}
|
||||||
|
return rr
|
||||||
}
|
}
|
||||||
|
|
||||||
// FindIndex locates a row index by id. Returns false is not found.
|
// FindIndex locates a row index by id. Returns false is not found.
|
||||||
func (re RowEvents) FindIndex(id string) (int, bool) {
|
func (rr RowEvents) FindIndex(id string) (int, bool) {
|
||||||
for i, e := range re {
|
for i, e := range rr {
|
||||||
if e.Row.ID == id {
|
if e.Row.ID == id {
|
||||||
return i, true
|
return i, true
|
||||||
}
|
}
|
||||||
|
|
@ -82,9 +121,28 @@ func (re RowEvents) FindIndex(id string) (int, bool) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sort rows based on column index and order.
|
// Sort rows based on column index and order.
|
||||||
func (re RowEvents) Sort(ns string, col int, asc bool) {
|
func (rr RowEvents) Sort(ns string, col int, asc bool) {
|
||||||
t := RowEventSorter{NS: ns, Events: re, Index: col, Asc: asc}
|
t := RowEventSorter{NS: ns, Events: rr, Index: col, Asc: asc}
|
||||||
sort.Sort(t)
|
sort.Sort(t)
|
||||||
|
|
||||||
|
gg, kk := map[string][]string{}, make(StringSet, 0, len(rr))
|
||||||
|
for _, e := range rr {
|
||||||
|
g := e.Row.Fields[col]
|
||||||
|
kk = kk.Add(g)
|
||||||
|
if ss, ok := gg[g]; ok {
|
||||||
|
gg[g] = append(ss, e.Row.ID)
|
||||||
|
} else {
|
||||||
|
gg[g] = []string{e.Row.ID}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ids := make([]string, 0, len(rr))
|
||||||
|
for _, k := range kk {
|
||||||
|
sort.StringSlice(gg[k]).Sort()
|
||||||
|
ids = append(ids, gg[k]...)
|
||||||
|
}
|
||||||
|
s := IdSorter{Ids: ids, Events: rr}
|
||||||
|
sort.Sort(s)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ----------------------------------------------------------------------------
|
// ----------------------------------------------------------------------------
|
||||||
|
|
@ -107,17 +165,39 @@ func (r RowEventSorter) Swap(i, j int) {
|
||||||
|
|
||||||
func (r RowEventSorter) Less(i, j int) bool {
|
func (r RowEventSorter) Less(i, j int) bool {
|
||||||
f1, f2 := r.Events[i].Row.Fields, r.Events[j].Row.Fields
|
f1, f2 := r.Events[i].Row.Fields, r.Events[j].Row.Fields
|
||||||
|
return Less(r.Asc, f1[r.Index], f2[r.Index])
|
||||||
|
}
|
||||||
|
|
||||||
var col int
|
// ----------------------------------------------------------------------------
|
||||||
if r.NS == "" {
|
|
||||||
col++
|
|
||||||
}
|
|
||||||
if col >= len(f1) || col >= len(f2) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
n1, n2 := f1[col], f2[col]
|
|
||||||
|
|
||||||
return Less(r.Asc, f1[r.Index]+n1, f2[r.Index]+n2)
|
// IdSorter sorts row events by a given id.
|
||||||
|
type IdSorter struct {
|
||||||
|
Ids []string
|
||||||
|
Events RowEvents
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s IdSorter) Len() int {
|
||||||
|
return len(s.Events)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s IdSorter) Swap(i, j int) {
|
||||||
|
s.Events[i], s.Events[j] = s.Events[j], s.Events[i]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s IdSorter) Less(i, j int) bool {
|
||||||
|
id1, id2 := s.Events[i].Row.ID, s.Events[j].Row.ID
|
||||||
|
i1, i2 := findIndex(s.Ids, id1), findIndex(s.Ids, id2)
|
||||||
|
return i1 < i2
|
||||||
|
}
|
||||||
|
|
||||||
|
func findIndex(ss []string, s string) int {
|
||||||
|
for i := range ss {
|
||||||
|
if ss[i] == s {
|
||||||
|
return i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
log.Error().Err(fmt.Errorf("Doh! index not found for %s", s))
|
||||||
|
return -1
|
||||||
}
|
}
|
||||||
|
|
||||||
// ----------------------------------------------------------------------------
|
// ----------------------------------------------------------------------------
|
||||||
|
|
@ -140,11 +220,11 @@ var (
|
||||||
)
|
)
|
||||||
|
|
||||||
// ColorerFunc represents a resource row colorer.
|
// ColorerFunc represents a resource row colorer.
|
||||||
type ColorerFunc func(ns string, evt ResEvent, r Row) tcell.Color
|
type ColorerFunc func(ns string, evt RowEvent) tcell.Color
|
||||||
|
|
||||||
// DefaultColorer set the default table row colors.
|
// DefaultColorer set the default table row colors.
|
||||||
func DefaultColorer(ns string, evt ResEvent, r Row) tcell.Color {
|
func DefaultColorer(ns string, evt RowEvent) tcell.Color {
|
||||||
switch evt {
|
switch evt.Kind {
|
||||||
case EventAdd:
|
case EventAdd:
|
||||||
return AddColor
|
return AddColor
|
||||||
case EventUpdate:
|
case EventUpdate:
|
||||||
|
|
@ -155,3 +235,25 @@ func DefaultColorer(ns string, evt ResEvent, r Row) tcell.Color {
|
||||||
return StdColor
|
return StdColor
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type StringSet []string
|
||||||
|
|
||||||
|
func (ss StringSet) Add(item string) StringSet {
|
||||||
|
if ss.In(item) {
|
||||||
|
return ss
|
||||||
|
}
|
||||||
|
return append(ss, item)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ss StringSet) In(item string) bool {
|
||||||
|
return ss.indexOf(item) >= 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ss StringSet) indexOf(item string) int {
|
||||||
|
for i, s := range ss {
|
||||||
|
if s == item {
|
||||||
|
return i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -52,7 +52,7 @@ func TestDefaultColorer(t *testing.T) {
|
||||||
|
|
||||||
for k, u := range uu {
|
for k, u := range uu {
|
||||||
t.Run(k, func(t *testing.T) {
|
t.Run(k, func(t *testing.T) {
|
||||||
assert.Equal(t, u.e, render.DefaultColorer("", u.k, render.Row{}))
|
assert.Equal(t, u.e, render.DefaultColorer("", render.RowEvent{}))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,110 @@
|
||||||
|
package render
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/gdamore/tcell"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Forwarder represents a port forwarder.
|
||||||
|
type Forwarder interface {
|
||||||
|
// Path returns a resource FQN.
|
||||||
|
Path() string
|
||||||
|
|
||||||
|
// Container returns a container name.
|
||||||
|
Container() string
|
||||||
|
|
||||||
|
// Ports returns container exposed ports.
|
||||||
|
Ports() []string
|
||||||
|
|
||||||
|
// Active returns forwarder current state.
|
||||||
|
Active() bool
|
||||||
|
|
||||||
|
// Age returns forwarder age.
|
||||||
|
Age() string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Forward renders a portforwards to screen.
|
||||||
|
type Forward struct{}
|
||||||
|
|
||||||
|
// ColorerFunc colors a resource row.
|
||||||
|
func (Forward) ColorerFunc() ColorerFunc {
|
||||||
|
return func(ns string, re RowEvent) tcell.Color {
|
||||||
|
return tcell.ColorSkyblue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Header returns a header row.
|
||||||
|
func (Forward) Header(ns string) HeaderRow {
|
||||||
|
return HeaderRow{
|
||||||
|
Header{Name: "NAMESPACE"},
|
||||||
|
Header{Name: "NAME"},
|
||||||
|
Header{Name: "CONTAINER"},
|
||||||
|
Header{Name: "PORTS"},
|
||||||
|
Header{Name: "URL"},
|
||||||
|
Header{Name: "C"},
|
||||||
|
Header{Name: "N"},
|
||||||
|
Header{Name: "AGE", Decorator: ageDecorator},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render renders a K8s resource to screen.
|
||||||
|
func (f Forward) Render(o interface{}, gvr string, r *Row) error {
|
||||||
|
pf, ok := o.(PortForwarder)
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("expecting a portforward but got %T", o)
|
||||||
|
}
|
||||||
|
|
||||||
|
ports := strings.Split(pf.Ports()[0], ":")
|
||||||
|
ns, na := Namespaced(pf.Path())
|
||||||
|
|
||||||
|
r.ID = pf.Path()
|
||||||
|
r.Fields = Fields{
|
||||||
|
ns,
|
||||||
|
na,
|
||||||
|
pf.Container(),
|
||||||
|
strings.Join(pf.Ports(), ","),
|
||||||
|
UrlFor(pf.Host(), pf.HttpPath(), ports[0]),
|
||||||
|
asNum(pf.C()),
|
||||||
|
asNum(pf.N()),
|
||||||
|
pf.Age(),
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helpers...
|
||||||
|
|
||||||
|
type PortForwarder interface {
|
||||||
|
Forwarder
|
||||||
|
BenchConfigurator
|
||||||
|
}
|
||||||
|
|
||||||
|
type BenchConfigurators map[string]BenchConfigurator
|
||||||
|
|
||||||
|
type BenchConfigurator interface {
|
||||||
|
// C returns the number of concurent connections.
|
||||||
|
C() int
|
||||||
|
|
||||||
|
// N returns the number of requests.
|
||||||
|
N() int
|
||||||
|
|
||||||
|
// Host returns the forward host address.
|
||||||
|
Host() string
|
||||||
|
|
||||||
|
// Path returns the http path.
|
||||||
|
HttpPath() string
|
||||||
|
}
|
||||||
|
|
||||||
|
// UrlFor computes fq url for a given benchmark configuration.
|
||||||
|
func UrlFor(host, path, port string) string {
|
||||||
|
if host == "" {
|
||||||
|
host = "localhost"
|
||||||
|
}
|
||||||
|
if path == "" {
|
||||||
|
path = "/"
|
||||||
|
}
|
||||||
|
|
||||||
|
return "http://" + host + ":" + port + path
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,99 @@
|
||||||
|
package render
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
metav1beta1 "k8s.io/apimachinery/pkg/apis/meta/v1beta1"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Generic renders a generic resource to screen.
|
||||||
|
type Generic struct {
|
||||||
|
table *metav1beta1.Table
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Generic) SetTable(t *metav1beta1.Table) {
|
||||||
|
g.table = t
|
||||||
|
}
|
||||||
|
|
||||||
|
// ColorerFunc colors a resource row.
|
||||||
|
func (Generic) ColorerFunc() ColorerFunc {
|
||||||
|
return DefaultColorer
|
||||||
|
}
|
||||||
|
|
||||||
|
// Header returns a header row.
|
||||||
|
func (g *Generic) Header(ns string) HeaderRow {
|
||||||
|
h := make(HeaderRow, 0, len(g.table.ColumnDefinitions))
|
||||||
|
|
||||||
|
if ns == "" {
|
||||||
|
h = append(h, Header{Name: "NAMESPACE"})
|
||||||
|
}
|
||||||
|
for _, c := range g.table.ColumnDefinitions {
|
||||||
|
h = append(h, Header{Name: strings.ToUpper(c.Name)})
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Debug().Msgf("Generic Header %#v", h)
|
||||||
|
return h
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render renders a K8s resource to screen.
|
||||||
|
func (g *Generic) Render(o interface{}, ns string, r *Row) error {
|
||||||
|
row, ok := o.(*metav1beta1.TableRow)
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("expecting a table but got %#v", o)
|
||||||
|
}
|
||||||
|
|
||||||
|
count := len(row.Cells)
|
||||||
|
if ns == AllNamespaces {
|
||||||
|
count++
|
||||||
|
}
|
||||||
|
r.ID, ok = row.Cells[0].(string)
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("expecting row id to be a string but got %#v", row.Cells[0])
|
||||||
|
}
|
||||||
|
r.Fields = make(Fields, count)
|
||||||
|
|
||||||
|
var index int
|
||||||
|
if ns == AllNamespaces {
|
||||||
|
rns, err := extractNamespace(row.Object.Raw)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
r.Fields[index] = rns
|
||||||
|
r.ID = FQN(rns, r.ID)
|
||||||
|
index++
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, c := range row.Cells {
|
||||||
|
r.Fields[index] = fmt.Sprintf("%v", c)
|
||||||
|
index++
|
||||||
|
}
|
||||||
|
log.Debug().Msgf("Generic row %#v", r)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
// Helpers...
|
||||||
|
|
||||||
|
func extractNamespace(raw []byte) (string, error) {
|
||||||
|
var obj map[string]interface{}
|
||||||
|
err := json.Unmarshal(raw, &obj)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
meta, ok := obj["metadata"].(map[string]interface{})
|
||||||
|
if !ok {
|
||||||
|
return "", errors.New("no metadata found on generic resource")
|
||||||
|
}
|
||||||
|
ns, ok := meta["namespace"].(string)
|
||||||
|
if !ok {
|
||||||
|
return "", errors.New("invalid namespace found on generic metadata")
|
||||||
|
}
|
||||||
|
|
||||||
|
return ns, nil
|
||||||
|
}
|
||||||
|
|
@ -152,13 +152,13 @@ func boolToStr(b bool) string {
|
||||||
}
|
}
|
||||||
|
|
||||||
func toAge(timestamp metav1.Time) string {
|
func toAge(timestamp metav1.Time) string {
|
||||||
return toAgeHuman(time.Since(timestamp.Time).String())
|
return time.Since(timestamp.Time).String()
|
||||||
}
|
}
|
||||||
|
|
||||||
func toAgeHuman(s string) string {
|
func toAgeHuman(s string) string {
|
||||||
d, err := time.ParseDuration(s)
|
d, err := time.ParseDuration(s)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "<unknown>"
|
return NAValue
|
||||||
}
|
}
|
||||||
|
|
||||||
return duration.HumanDuration(d)
|
return duration.HumanDuration(d)
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,7 @@ func (HorizontalPodAutoscaler) Header(ns string) HeaderRow {
|
||||||
Header{Name: "MINPODS", Align: tview.AlignRight},
|
Header{Name: "MINPODS", Align: tview.AlignRight},
|
||||||
Header{Name: "MAXPODS", Align: tview.AlignRight},
|
Header{Name: "MAXPODS", Align: tview.AlignRight},
|
||||||
Header{Name: "REPLICAS", Align: tview.AlignRight},
|
Header{Name: "REPLICAS", Align: tview.AlignRight},
|
||||||
Header{Name: "AGE"},
|
Header{Name: "AGE", Decorator: ageDecorator},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,7 @@ func (Ingress) Header(ns string) HeaderRow {
|
||||||
Header{Name: "HOSTS"},
|
Header{Name: "HOSTS"},
|
||||||
Header{Name: "ADDRESS"},
|
Header{Name: "ADDRESS"},
|
||||||
Header{Name: "PORT"},
|
Header{Name: "PORT"},
|
||||||
Header{Name: "AGE"},
|
Header{Name: "AGE", Decorator: ageDecorator},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,7 @@ func (Job) Header(ns string) HeaderRow {
|
||||||
Header{Name: "DURATION"},
|
Header{Name: "DURATION"},
|
||||||
Header{Name: "CONTAINERS"},
|
Header{Name: "CONTAINERS"},
|
||||||
Header{Name: "IMAGES"},
|
Header{Name: "IMAGES"},
|
||||||
Header{Name: "AGE"},
|
Header{Name: "AGE", Decorator: ageDecorator},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -50,7 +50,7 @@ func (Node) Header(_ string) HeaderRow {
|
||||||
Header{Name: "%MEM", Align: tview.AlignRight},
|
Header{Name: "%MEM", Align: tview.AlignRight},
|
||||||
Header{Name: "ACPU", Align: tview.AlignRight},
|
Header{Name: "ACPU", Align: tview.AlignRight},
|
||||||
Header{Name: "AMEM", Align: tview.AlignRight},
|
Header{Name: "AMEM", Align: tview.AlignRight},
|
||||||
Header{Name: "AGE"},
|
Header{Name: "AGE", Decorator: ageDecorator},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -33,7 +33,7 @@ func (NetworkPolicy) Header(ns string) HeaderRow {
|
||||||
Header{Name: "EGR-SELECTOR"},
|
Header{Name: "EGR-SELECTOR"},
|
||||||
Header{Name: "EGR-PORTS"},
|
Header{Name: "EGR-PORTS"},
|
||||||
Header{Name: "EGR-BLOCK"},
|
Header{Name: "EGR-BLOCK"},
|
||||||
Header{Name: "AGE"},
|
Header{Name: "AGE", Decorator: ageDecorator},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,9 @@ package render
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/gdamore/tcell"
|
||||||
v1 "k8s.io/api/core/v1"
|
v1 "k8s.io/api/core/v1"
|
||||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||||
"k8s.io/apimachinery/pkg/runtime"
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
|
|
@ -13,7 +15,22 @@ type Namespace struct{}
|
||||||
|
|
||||||
// ColorerFunc colors a resource row.
|
// ColorerFunc colors a resource row.
|
||||||
func (Namespace) ColorerFunc() ColorerFunc {
|
func (Namespace) ColorerFunc() ColorerFunc {
|
||||||
return DefaultColorer
|
return func(ns string, r RowEvent) tcell.Color {
|
||||||
|
c := DefaultColorer(ns, r)
|
||||||
|
|
||||||
|
if r.Kind == EventAdd || r.Kind == EventUpdate {
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
switch strings.TrimSpace(r.Row.Fields[1]) {
|
||||||
|
case "Inactive", Terminating:
|
||||||
|
c = ErrColor
|
||||||
|
}
|
||||||
|
if strings.Contains(strings.TrimSpace(r.Row.Fields[0]), "*") {
|
||||||
|
c = HighlightColor
|
||||||
|
}
|
||||||
|
|
||||||
|
return c
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Header returns a header rbw.
|
// Header returns a header rbw.
|
||||||
|
|
@ -21,7 +38,7 @@ func (Namespace) Header(string) HeaderRow {
|
||||||
return HeaderRow{
|
return HeaderRow{
|
||||||
Header{Name: "NAME"},
|
Header{Name: "NAME"},
|
||||||
Header{Name: "STATUS"},
|
Header{Name: "STATUS"},
|
||||||
Header{Name: "AGE"},
|
Header{Name: "AGE", Decorator: ageDecorator},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,8 +3,10 @@ package render
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/derailed/tview"
|
"github.com/derailed/tview"
|
||||||
|
"github.com/gdamore/tcell"
|
||||||
v1beta1 "k8s.io/api/policy/v1beta1"
|
v1beta1 "k8s.io/api/policy/v1beta1"
|
||||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||||
"k8s.io/apimachinery/pkg/runtime"
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
|
|
@ -16,7 +18,23 @@ type PodDisruptionBudget struct{}
|
||||||
|
|
||||||
// ColorerFunc colors a resource row.
|
// ColorerFunc colors a resource row.
|
||||||
func (PodDisruptionBudget) ColorerFunc() ColorerFunc {
|
func (PodDisruptionBudget) ColorerFunc() ColorerFunc {
|
||||||
return DefaultColorer
|
return func(ns string, r RowEvent) tcell.Color {
|
||||||
|
c := DefaultColorer(ns, r)
|
||||||
|
if r.Kind == EventAdd || r.Kind == EventUpdate {
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
markCol := 5
|
||||||
|
if ns != AllNamespaces {
|
||||||
|
markCol = 4
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(r.Row.Fields[markCol]) != strings.TrimSpace(r.Row.Fields[markCol+1]) {
|
||||||
|
return ErrColor
|
||||||
|
}
|
||||||
|
|
||||||
|
return StdColor
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Header returns a header row.
|
// Header returns a header row.
|
||||||
|
|
@ -34,7 +52,7 @@ func (PodDisruptionBudget) Header(ns string) HeaderRow {
|
||||||
Header{Name: "CURRENT", Align: tview.AlignRight},
|
Header{Name: "CURRENT", Align: tview.AlignRight},
|
||||||
Header{Name: "DESIRED", Align: tview.AlignRight},
|
Header{Name: "DESIRED", Align: tview.AlignRight},
|
||||||
Header{Name: "EXPECTED", Align: tview.AlignRight},
|
Header{Name: "EXPECTED", Align: tview.AlignRight},
|
||||||
Header{Name: "AGE"},
|
Header{Name: "AGE", Decorator: ageDecorator},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -28,9 +28,9 @@ type PodWithMetrics interface {
|
||||||
type Pod struct{}
|
type Pod struct{}
|
||||||
|
|
||||||
// ColorerFunc colors a resource row.
|
// ColorerFunc colors a resource row.
|
||||||
func (Pod) ColorerFunc() ColorerFunc {
|
func (p Pod) ColorerFunc() ColorerFunc {
|
||||||
return func(ns string, evt ResEvent, r Row) tcell.Color {
|
return func(ns string, re RowEvent) tcell.Color {
|
||||||
c := DefaultColorer(ns, evt, r)
|
c := DefaultColorer(ns, re)
|
||||||
|
|
||||||
readyCol := 2
|
readyCol := 2
|
||||||
if len(ns) != 0 {
|
if len(ns) != 0 {
|
||||||
|
|
@ -38,29 +38,39 @@ func (Pod) ColorerFunc() ColorerFunc {
|
||||||
}
|
}
|
||||||
statusCol := readyCol + 1
|
statusCol := readyCol + 1
|
||||||
|
|
||||||
tokens := strings.Split(strings.TrimSpace(r.Fields[readyCol]), "/")
|
ready, status := strings.TrimSpace(re.Row.Fields[readyCol]), strings.TrimSpace(re.Row.Fields[statusCol])
|
||||||
if len(tokens) == 2 && (tokens[0] == "0" || tokens[0] != tokens[1]) {
|
c = p.checkReadyCol(ready, status, c)
|
||||||
if strings.TrimSpace(r.Fields[statusCol]) != "Completed" {
|
|
||||||
c = ErrColor
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
switch strings.TrimSpace(r.Fields[statusCol]) {
|
switch status {
|
||||||
case "ContainerCreating", "PodInitializing":
|
case ContainerCreating, PodInitializing:
|
||||||
return AddColor
|
return AddColor
|
||||||
case "Terminating", "Initialized":
|
case Initialized:
|
||||||
return HighlightColor
|
return HighlightColor
|
||||||
case "Completed":
|
case Completed:
|
||||||
return CompletedColor
|
return CompletedColor
|
||||||
case "Running":
|
case Running:
|
||||||
|
case Terminating:
|
||||||
|
return KillColor
|
||||||
default:
|
default:
|
||||||
c = ErrColor
|
return ErrColor
|
||||||
}
|
}
|
||||||
|
|
||||||
return c
|
return c
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (Pod) checkReadyCol(readyCol, statusCol string, c tcell.Color) tcell.Color {
|
||||||
|
if statusCol == "Completed" {
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
tokens := strings.Split(readyCol, "/")
|
||||||
|
if len(tokens) == 2 && (tokens[0] == "0" || tokens[0] != tokens[1]) {
|
||||||
|
return ErrColor
|
||||||
|
}
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
// Header returns a header row.
|
// Header returns a header row.
|
||||||
func (Pod) Header(ns string) HeaderRow {
|
func (Pod) Header(ns string) HeaderRow {
|
||||||
var h HeaderRow
|
var h HeaderRow
|
||||||
|
|
@ -80,7 +90,7 @@ func (Pod) Header(ns string) HeaderRow {
|
||||||
Header{Name: "IP"},
|
Header{Name: "IP"},
|
||||||
Header{Name: "NODE"},
|
Header{Name: "NODE"},
|
||||||
Header{Name: "QOS"},
|
Header{Name: "QOS"},
|
||||||
Header{Name: "AGE"},
|
Header{Name: "AGE", Decorator: ageDecorator},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -94,7 +104,7 @@ func (p Pod) Render(o interface{}, ns string, r *Row) error {
|
||||||
var po v1.Pod
|
var po v1.Pod
|
||||||
err := runtime.DefaultUnstructuredConverter.FromUnstructured(oo.Object().(*unstructured.Unstructured).Object, &po)
|
err := runtime.DefaultUnstructuredConverter.FromUnstructured(oo.Object().(*unstructured.Unstructured).Object, &po)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().Err(err).Msg("Converting Pod")
|
log.Error().Err(err).Msg("Expecting a pod resource")
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -102,11 +112,12 @@ func (p Pod) Render(o interface{}, ns string, r *Row) error {
|
||||||
cr, _, rc := p.statuses(ss)
|
cr, _, rc := p.statuses(ss)
|
||||||
c, perc := p.gatherPodMX(&po, oo.Metrics())
|
c, perc := p.gatherPodMX(&po, oo.Metrics())
|
||||||
|
|
||||||
fields := make(Fields, 0, len(r.Fields))
|
r.ID = MetaFQN(po.ObjectMeta)
|
||||||
|
r.Fields = make(Fields, 0, len(p.Header(ns)))
|
||||||
if isAllNamespace(ns) {
|
if isAllNamespace(ns) {
|
||||||
fields = append(fields, po.Namespace)
|
r.Fields = append(r.Fields, po.Namespace)
|
||||||
}
|
}
|
||||||
fields = append(fields,
|
r.Fields = append(r.Fields,
|
||||||
po.ObjectMeta.Name,
|
po.ObjectMeta.Name,
|
||||||
strconv.Itoa(cr)+"/"+strconv.Itoa(len(ss)),
|
strconv.Itoa(cr)+"/"+strconv.Itoa(len(ss)),
|
||||||
p.phase(&po),
|
p.phase(&po),
|
||||||
|
|
@ -120,11 +131,8 @@ func (p Pod) Render(o interface{}, ns string, r *Row) error {
|
||||||
p.mapQOS(po.Status.QOSClass),
|
p.mapQOS(po.Status.QOSClass),
|
||||||
toAge(po.ObjectMeta.CreationTimestamp),
|
toAge(po.ObjectMeta.CreationTimestamp),
|
||||||
)
|
)
|
||||||
r.ID = MetaFQN(po.ObjectMeta)
|
|
||||||
r.Fields = fields
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ----------------------------------------------------------------------------
|
// ----------------------------------------------------------------------------
|
||||||
|
|
@ -0,0 +1,49 @@
|
||||||
|
package render
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/gdamore/tcell"
|
||||||
|
)
|
||||||
|
|
||||||
|
func rbacVerbHeader() HeaderRow {
|
||||||
|
return HeaderRow{
|
||||||
|
Header{Name: "GET "},
|
||||||
|
Header{Name: "LIST "},
|
||||||
|
Header{Name: "WATCH "},
|
||||||
|
Header{Name: "CREATE"},
|
||||||
|
Header{Name: "PATCH "},
|
||||||
|
Header{Name: "UPDATE"},
|
||||||
|
Header{Name: "DELETE"},
|
||||||
|
Header{Name: "DLIST "},
|
||||||
|
Header{Name: "EXTRAS"},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Policy renders a rbac policy to screen.
|
||||||
|
type Policy struct{}
|
||||||
|
|
||||||
|
// ColorerFunc colors a resource row.
|
||||||
|
func (Policy) ColorerFunc() ColorerFunc {
|
||||||
|
return func(ns string, re RowEvent) tcell.Color {
|
||||||
|
return tcell.ColorMediumSpringGreen
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Header returns a header row.
|
||||||
|
func (Policy) Header(ns string) HeaderRow {
|
||||||
|
h := HeaderRow{
|
||||||
|
Header{Name: "NAMESPACE"},
|
||||||
|
Header{Name: "NAME"},
|
||||||
|
Header{Name: "API GROUP"},
|
||||||
|
Header{Name: "BINDING"},
|
||||||
|
}
|
||||||
|
|
||||||
|
return append(h, rbacVerbHeader()...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render renders a K8s resource to screen.
|
||||||
|
func (Policy) Render(o interface{}, gvr string, r *Row) error {
|
||||||
|
panic("NYI")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helpers...
|
||||||
|
|
@ -5,6 +5,7 @@ import (
|
||||||
"path"
|
"path"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/gdamore/tcell"
|
||||||
v1 "k8s.io/api/core/v1"
|
v1 "k8s.io/api/core/v1"
|
||||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||||
"k8s.io/apimachinery/pkg/runtime"
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
|
|
@ -15,7 +16,25 @@ type PersistentVolume struct{}
|
||||||
|
|
||||||
// ColorerFunc colors a resource row.
|
// ColorerFunc colors a resource row.
|
||||||
func (PersistentVolume) ColorerFunc() ColorerFunc {
|
func (PersistentVolume) ColorerFunc() ColorerFunc {
|
||||||
return DefaultColorer
|
return func(ns string, r RowEvent) tcell.Color {
|
||||||
|
c := DefaultColorer(ns, r)
|
||||||
|
if r.Kind == EventAdd || r.Kind == EventUpdate {
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
status := strings.TrimSpace(r.Row.Fields[4])
|
||||||
|
switch status {
|
||||||
|
case "Bound":
|
||||||
|
c = StdColor
|
||||||
|
case "Available":
|
||||||
|
c = tcell.ColorYellow
|
||||||
|
default:
|
||||||
|
c = ErrColor
|
||||||
|
}
|
||||||
|
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Header returns a header rbw.
|
// Header returns a header rbw.
|
||||||
|
|
@ -29,7 +48,7 @@ func (PersistentVolume) Header(string) HeaderRow {
|
||||||
Header{Name: "CLAIM"},
|
Header{Name: "CLAIM"},
|
||||||
Header{Name: "STORAGECLASS"},
|
Header{Name: "STORAGECLASS"},
|
||||||
Header{Name: "REASON"},
|
Header{Name: "REASON"},
|
||||||
Header{Name: "AGE"},
|
Header{Name: "AGE", Decorator: ageDecorator},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,9 @@ package render
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/gdamore/tcell"
|
||||||
v1 "k8s.io/api/core/v1"
|
v1 "k8s.io/api/core/v1"
|
||||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||||
"k8s.io/apimachinery/pkg/runtime"
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
|
|
@ -13,7 +15,24 @@ type PersistentVolumeClaim struct{}
|
||||||
|
|
||||||
// ColorerFunc colors a resource row.
|
// ColorerFunc colors a resource row.
|
||||||
func (PersistentVolumeClaim) ColorerFunc() ColorerFunc {
|
func (PersistentVolumeClaim) ColorerFunc() ColorerFunc {
|
||||||
return DefaultColorer
|
return func(ns string, r RowEvent) tcell.Color {
|
||||||
|
c := DefaultColorer(ns, r)
|
||||||
|
if r.Kind == EventAdd || r.Kind == EventUpdate {
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
markCol := 2
|
||||||
|
if ns != AllNamespaces {
|
||||||
|
markCol = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.TrimSpace(r.Row.Fields[markCol]) != "Bound" {
|
||||||
|
c = ErrColor
|
||||||
|
}
|
||||||
|
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Header returns a header rbw.
|
// Header returns a header rbw.
|
||||||
|
|
@ -30,7 +49,7 @@ func (PersistentVolumeClaim) Header(ns string) HeaderRow {
|
||||||
Header{Name: "CAPACITY"},
|
Header{Name: "CAPACITY"},
|
||||||
Header{Name: "ACCESS MODES"},
|
Header{Name: "ACCESS MODES"},
|
||||||
Header{Name: "STORAGECLASS"},
|
Header{Name: "STORAGECLASS"},
|
||||||
Header{Name: "AGE"},
|
Header{Name: "AGE", Decorator: ageDecorator},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,7 @@ func (RoleBinding) Header(ns string) HeaderRow {
|
||||||
Header{Name: "ROLE"},
|
Header{Name: "ROLE"},
|
||||||
Header{Name: "KIND"},
|
Header{Name: "KIND"},
|
||||||
Header{Name: "SUBJECTS"},
|
Header{Name: "SUBJECTS"},
|
||||||
Header{Name: "AGE"},
|
Header{Name: "AGE", Decorator: ageDecorator},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,33 @@
|
||||||
|
package render
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/gdamore/tcell"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Rbac renders a rbac to screen.
|
||||||
|
type Rbac struct{}
|
||||||
|
|
||||||
|
// ColorerFunc colors a resource row.
|
||||||
|
func (Rbac) ColorerFunc() ColorerFunc {
|
||||||
|
return func(ns string, re RowEvent) tcell.Color {
|
||||||
|
return tcell.ColorMediumSpringGreen
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Header returns a header row.
|
||||||
|
func (Rbac) Header(ns string) HeaderRow {
|
||||||
|
h := HeaderRow{
|
||||||
|
Header{Name: "NAME"},
|
||||||
|
Header{Name: "API GROUP"},
|
||||||
|
}
|
||||||
|
|
||||||
|
return append(h, rbacVerbHeader()...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render renders a K8s resource to screen.
|
||||||
|
func (Rbac) Render(o interface{}, gvr string, r *Row) error {
|
||||||
|
panic("NYI")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helpers...
|
||||||
|
|
@ -25,7 +25,7 @@ func (Role) Header(ns string) HeaderRow {
|
||||||
|
|
||||||
return append(h,
|
return append(h,
|
||||||
Header{Name: "NAME"},
|
Header{Name: "NAME"},
|
||||||
Header{Name: "AGE"},
|
Header{Name: "AGE", Decorator: ageDecorator},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -23,11 +23,16 @@ type Rows []Row
|
||||||
type Header struct {
|
type Header struct {
|
||||||
Name string
|
Name string
|
||||||
Align int
|
Align int
|
||||||
|
Decorator DecoratorFunc
|
||||||
}
|
}
|
||||||
|
|
||||||
// HeaderRow represents a table header.
|
// HeaderRow represents a table header.
|
||||||
type HeaderRow []Header
|
type HeaderRow []Header
|
||||||
|
|
||||||
|
func (h HeaderRow) AgeCol(col int) bool {
|
||||||
|
return col == len(h)-1
|
||||||
|
}
|
||||||
|
|
||||||
// RowSorter sorts rows.
|
// RowSorter sorts rows.
|
||||||
type RowSorter struct {
|
type RowSorter struct {
|
||||||
Rows Rows
|
Rows Rows
|
||||||
|
|
@ -35,6 +40,22 @@ type RowSorter struct {
|
||||||
Asc bool
|
Asc bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r Row) Clone() Row {
|
||||||
|
return Row{
|
||||||
|
ID: r.ID,
|
||||||
|
Fields: r.Fields.Clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f Fields) Clone() Fields {
|
||||||
|
res := make(Fields, len(f))
|
||||||
|
for i, f := range f {
|
||||||
|
res[i] = f
|
||||||
|
}
|
||||||
|
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
// Delete removes an element by id.
|
// Delete removes an element by id.
|
||||||
func (rr Rows) Delete(id string) Rows {
|
func (rr Rows) Delete(id string) Rows {
|
||||||
idx, ok := rr.Find(id)
|
idx, ok := rr.Find(id)
|
||||||
|
|
@ -57,6 +78,16 @@ func NewRow(cols int) Row {
|
||||||
return Row{Fields: make([]string, cols)}
|
return Row{Fields: make([]string, cols)}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (rr Rows) Upsert(r Row) Rows {
|
||||||
|
idx, ok := rr.Find(r.ID)
|
||||||
|
if !ok {
|
||||||
|
return append(rr, r)
|
||||||
|
}
|
||||||
|
rr[idx] = r
|
||||||
|
|
||||||
|
return rr
|
||||||
|
}
|
||||||
|
|
||||||
// Find locates a row by id. Retturns false is not found.
|
// Find locates a row by id. Retturns false is not found.
|
||||||
func (rr Rows) Find(id string) (int, bool) {
|
func (rr Rows) Find(id string) (int, bool) {
|
||||||
for i, r := range rr {
|
for i, r := range rr {
|
||||||
|
|
|
||||||
|
|
@ -3,8 +3,10 @@ package render
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/derailed/tview"
|
"github.com/derailed/tview"
|
||||||
|
"github.com/gdamore/tcell"
|
||||||
appsv1 "k8s.io/api/apps/v1"
|
appsv1 "k8s.io/api/apps/v1"
|
||||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||||
"k8s.io/apimachinery/pkg/runtime"
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
|
|
@ -15,7 +17,23 @@ type ReplicaSet struct{}
|
||||||
|
|
||||||
// ColorerFunc colors a resource row.
|
// ColorerFunc colors a resource row.
|
||||||
func (ReplicaSet) ColorerFunc() ColorerFunc {
|
func (ReplicaSet) ColorerFunc() ColorerFunc {
|
||||||
return DefaultColorer
|
return func(ns string, r RowEvent) tcell.Color {
|
||||||
|
c := DefaultColorer(ns, r)
|
||||||
|
if r.Kind == EventAdd || r.Kind == EventUpdate {
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
markCol := 2
|
||||||
|
if ns != AllNamespaces {
|
||||||
|
markCol = 1
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(r.Row.Fields[markCol]) != strings.TrimSpace(r.Row.Fields[markCol+1]) {
|
||||||
|
return ErrColor
|
||||||
|
}
|
||||||
|
|
||||||
|
return StdColor
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Header returns a header row.
|
// Header returns a header row.
|
||||||
|
|
@ -30,7 +48,7 @@ func (ReplicaSet) Header(ns string) HeaderRow {
|
||||||
Header{Name: "DESIRED", Align: tview.AlignRight},
|
Header{Name: "DESIRED", Align: tview.AlignRight},
|
||||||
Header{Name: "CURRENT", Align: tview.AlignRight},
|
Header{Name: "CURRENT", Align: tview.AlignRight},
|
||||||
Header{Name: "READY", Align: tview.AlignRight},
|
Header{Name: "READY", Align: tview.AlignRight},
|
||||||
Header{Name: "AGE"},
|
Header{Name: "AGE", Decorator: ageDecorator},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,7 @@ func (ServiceAccount) Header(ns string) HeaderRow {
|
||||||
return append(h,
|
return append(h,
|
||||||
Header{Name: "NAME"},
|
Header{Name: "NAME"},
|
||||||
Header{Name: "SECRET"},
|
Header{Name: "SECRET"},
|
||||||
Header{Name: "AGE"},
|
Header{Name: "AGE", Decorator: ageDecorator},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,61 @@
|
||||||
|
package render
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/gdamore/tcell"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ScreenDump renders a screendumps to screen.
|
||||||
|
type ScreenDump struct{}
|
||||||
|
|
||||||
|
// ColorerFunc colors a resource row.
|
||||||
|
func (ScreenDump) ColorerFunc() ColorerFunc {
|
||||||
|
return func(ns string, re RowEvent) tcell.Color {
|
||||||
|
return tcell.ColorNavajoWhite
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type DecoratorFunc func(string) string
|
||||||
|
|
||||||
|
var ageDecorator = func(a string) string {
|
||||||
|
return toAgeHuman(a)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Header returns a header row.
|
||||||
|
func (ScreenDump) Header(ns string) HeaderRow {
|
||||||
|
return HeaderRow{
|
||||||
|
Header{Name: "NAME"},
|
||||||
|
Header{Name: "AGE", Decorator: ageDecorator},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render renders a K8s resource to screen.
|
||||||
|
func (b ScreenDump) Render(o interface{}, ns string, r *Row) error {
|
||||||
|
f, ok := o.(ScreenDumper)
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("Expected string, but got %T", o)
|
||||||
|
}
|
||||||
|
|
||||||
|
r.ID = filepath.Join(f.GetDir(), f.GetFile().Name())
|
||||||
|
r.Fields = Fields{
|
||||||
|
f.GetFile().Name(),
|
||||||
|
timeToAge(f.GetFile().ModTime()),
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helpers...
|
||||||
|
|
||||||
|
func timeToAge(timestamp time.Time) string {
|
||||||
|
return time.Since(timestamp).String()
|
||||||
|
}
|
||||||
|
|
||||||
|
type ScreenDumper interface {
|
||||||
|
GetFile() os.FileInfo
|
||||||
|
GetDir() string
|
||||||
|
}
|
||||||
|
|
@ -29,7 +29,7 @@ func (Secret) Header(ns string) HeaderRow {
|
||||||
Header{Name: "NAME"},
|
Header{Name: "NAME"},
|
||||||
Header{Name: "TYPE"},
|
Header{Name: "TYPE"},
|
||||||
Header{Name: "DATA", Align: tview.AlignRight},
|
Header{Name: "DATA", Align: tview.AlignRight},
|
||||||
Header{Name: "AGE"},
|
Header{Name: "AGE", Decorator: ageDecorator},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,81 @@
|
||||||
|
package render
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/gdamore/tcell"
|
||||||
|
appsv1 "k8s.io/api/apps/v1"
|
||||||
|
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||||
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
|
)
|
||||||
|
|
||||||
|
// StatefulSet renders a K8s StatefulSet to screen.
|
||||||
|
type StatefulSet struct{}
|
||||||
|
|
||||||
|
// ColorerFunc colors a resource row.
|
||||||
|
func (StatefulSet) ColorerFunc() ColorerFunc {
|
||||||
|
return func(ns string, r RowEvent) tcell.Color {
|
||||||
|
c := DefaultColorer(ns, r)
|
||||||
|
if r.Kind == EventAdd || r.Kind == EventUpdate {
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
readyCol := 2
|
||||||
|
if ns != AllNamespaces {
|
||||||
|
readyCol--
|
||||||
|
}
|
||||||
|
tokens := strings.Split(strings.TrimSpace(r.Row.Fields[readyCol]), "/")
|
||||||
|
curr, des := tokens[0], tokens[1]
|
||||||
|
if curr != des {
|
||||||
|
return ErrColor
|
||||||
|
}
|
||||||
|
|
||||||
|
return StdColor
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Header returns a header row.
|
||||||
|
func (StatefulSet) Header(ns string) HeaderRow {
|
||||||
|
var h HeaderRow
|
||||||
|
if isAllNamespace(ns) {
|
||||||
|
h = append(h, Header{Name: "NAMESPACE"})
|
||||||
|
}
|
||||||
|
|
||||||
|
return append(h,
|
||||||
|
Header{Name: "NAME"},
|
||||||
|
Header{Name: "READY"},
|
||||||
|
Header{Name: "SELECTOR"},
|
||||||
|
Header{Name: "SERVICE"},
|
||||||
|
Header{Name: "AGE", Decorator: ageDecorator},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render renders a K8s resource to screen.
|
||||||
|
func (s StatefulSet) Render(o interface{}, ns string, r *Row) error {
|
||||||
|
raw, ok := o.(*unstructured.Unstructured)
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("Expected StatefulSet, but got %T", o)
|
||||||
|
}
|
||||||
|
var sts appsv1.StatefulSet
|
||||||
|
err := runtime.DefaultUnstructuredConverter.FromUnstructured(raw.Object, &sts)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
r.ID = MetaFQN(sts.ObjectMeta)
|
||||||
|
r.Fields = make(Fields, 0, len(s.Header(ns)))
|
||||||
|
if isAllNamespace(ns) {
|
||||||
|
r.Fields = append(r.Fields, sts.Namespace)
|
||||||
|
}
|
||||||
|
r.Fields = append(r.Fields,
|
||||||
|
sts.Name,
|
||||||
|
strconv.Itoa(int(sts.Status.Replicas))+"/"+strconv.Itoa(int(*sts.Spec.Replicas)),
|
||||||
|
asSelector(sts.Spec.Selector),
|
||||||
|
na(sts.Spec.ServiceName),
|
||||||
|
toAge(sts.ObjectMeta.CreationTimestamp),
|
||||||
|
)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,30 @@
|
||||||
|
package render
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/gdamore/tcell"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Subject renders a rbac to screen.
|
||||||
|
type Subject struct{}
|
||||||
|
|
||||||
|
// ColorerFunc colors a resource row.
|
||||||
|
func (Subject) ColorerFunc() ColorerFunc {
|
||||||
|
return func(ns string, re RowEvent) tcell.Color {
|
||||||
|
return tcell.ColorMediumSpringGreen
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Header returns a header row.
|
||||||
|
func (Subject) Header(ns string) HeaderRow {
|
||||||
|
return HeaderRow{
|
||||||
|
Header{Name: "NAME"},
|
||||||
|
Header{Name: "KIND"},
|
||||||
|
Header{Name: "FIRST LOCATION"},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render renders a K8s resource to screen.
|
||||||
|
func (Subject) Render(o interface{}, gvr string, r *Row) error {
|
||||||
|
panic("NYI")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
@ -33,7 +33,7 @@ func (Service) Header(ns string) HeaderRow {
|
||||||
Header{Name: "EXTERNAL-IP"},
|
Header{Name: "EXTERNAL-IP"},
|
||||||
Header{Name: "SELECTOR"},
|
Header{Name: "SELECTOR"},
|
||||||
Header{Name: "PORTS"},
|
Header{Name: "PORTS"},
|
||||||
Header{Name: "AGE"},
|
Header{Name: "AGE", Decorator: ageDecorator},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
package render
|
||||||
|
|
||||||
|
// TableData tracks a K8s resource for tabular display.
|
||||||
|
type TableData struct {
|
||||||
|
Header HeaderRow
|
||||||
|
RowEvents RowEvents
|
||||||
|
Namespace string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t TableData) Clone() TableData {
|
||||||
|
return TableData{
|
||||||
|
Header: t.Header,
|
||||||
|
RowEvents: t.RowEvents.Clone(),
|
||||||
|
Namespace: t.Namespace,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,35 @@
|
||||||
|
package render
|
||||||
|
|
||||||
|
const (
|
||||||
|
// AllNamespaces represents all namespaces.
|
||||||
|
AllNamespaces = ""
|
||||||
|
|
||||||
|
// NamespaceAll represent the all namespace.
|
||||||
|
NamespaceAll = "all"
|
||||||
|
|
||||||
|
// ClusterWide represents a cluster resources.
|
||||||
|
ClusterWide = "-"
|
||||||
|
|
||||||
|
// NonResource represents a custom resource.
|
||||||
|
NonResource = "*"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// Terminating represents a pod terminating status.
|
||||||
|
Terminating = "Terminating"
|
||||||
|
|
||||||
|
// Running represents a pod running status.
|
||||||
|
Running = "Running"
|
||||||
|
|
||||||
|
// Initialized represents a pod intialized status.
|
||||||
|
Initialized = "Initialized"
|
||||||
|
|
||||||
|
// Completed represents a pod completed status.
|
||||||
|
Completed = "Completed"
|
||||||
|
|
||||||
|
// ContainerCreating represents a pod container status.
|
||||||
|
ContainerCreating = "ContainerCreating"
|
||||||
|
|
||||||
|
// PodInitializing represents a pod initializing status.
|
||||||
|
PodInitializing = "PodInitializing"
|
||||||
|
)
|
||||||
|
|
@ -4,8 +4,10 @@ import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
"path"
|
"path"
|
||||||
|
|
||||||
|
"github.com/derailed/k9s/internal"
|
||||||
"github.com/derailed/k9s/internal/k8s"
|
"github.com/derailed/k9s/internal/k8s"
|
||||||
"github.com/derailed/k9s/internal/watch"
|
"github.com/derailed/k9s/internal/watch"
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
|
|
@ -162,7 +164,10 @@ func (*Base) marshalObject(o runtime.Object) (string, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *Base) podLogs(ctx context.Context, c chan<- string, sel map[string]string, opts LogOptions) error {
|
func (b *Base) podLogs(ctx context.Context, c chan<- string, sel map[string]string, opts LogOptions) error {
|
||||||
f := ctx.Value(IKey("factory")).(*watch.Factory)
|
f, ok := ctx.Value(internal.KeyFactory).(*watch.Factory)
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("no factory in context for pod logs")
|
||||||
|
}
|
||||||
|
|
||||||
ls, err := metav1.ParseToLabelSelector(toSelector(sel))
|
ls, err := metav1.ParseToLabelSelector(toSelector(sel))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
||||||
|
|
@ -81,6 +81,7 @@ func (r *Custom) New(i interface{}) (Columnar, error) {
|
||||||
|
|
||||||
// Marshal resource to yaml.
|
// Marshal resource to yaml.
|
||||||
func (r *Custom) Marshal(path string) (string, error) {
|
func (r *Custom) Marshal(path string) (string, error) {
|
||||||
|
panic("NYI")
|
||||||
ns, n := Namespaced(path)
|
ns, n := Namespaced(path)
|
||||||
i, err := r.Resource.Get(ns, n)
|
i, err := r.Resource.Get(ns, n)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
||||||
|
|
@ -44,18 +44,19 @@ func TestCustomFields(t *testing.T) {
|
||||||
assert.Equal(t, "a", r[0])
|
assert.Equal(t, "a", r[0])
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestCustomMarshal(t *testing.T) {
|
// BOZO!!
|
||||||
mc := NewMockConnection()
|
// func TestCustomMarshal(t *testing.T) {
|
||||||
mr := NewMockCruder()
|
// mc := NewMockConnection()
|
||||||
m.When(mr.Get("blee", "fred")).ThenReturn(k8sCustomTable(), nil)
|
// mr := NewMockCruder()
|
||||||
|
// m.When(mr.Get("blee", "fred")).ThenReturn(k8sCustomTable(), nil)
|
||||||
|
|
||||||
cm := NewCustomWithArgs(mc, mr)
|
// cm := NewCustomWithArgs(mc, mr)
|
||||||
ma, err := cm.Marshal("blee/fred")
|
// ma, err := cm.Marshal("blee/fred")
|
||||||
mr.VerifyWasCalledOnce().Get("blee", "fred")
|
// mr.VerifyWasCalledOnce().Get("blee", "fred")
|
||||||
|
|
||||||
assert.Nil(t, err)
|
// assert.Nil(t, err)
|
||||||
assert.Equal(t, customYaml(), ma)
|
// assert.Equal(t, customYaml(), ma)
|
||||||
}
|
// }
|
||||||
|
|
||||||
func TestCustomMarshalWithUnstructured(t *testing.T) {
|
func TestCustomMarshalWithUnstructured(t *testing.T) {
|
||||||
mc := NewMockConnection()
|
mc := NewMockConnection()
|
||||||
|
|
|
||||||
|
|
@ -1,128 +1,140 @@
|
||||||
package resource
|
package resource
|
||||||
|
|
||||||
import (
|
// import (
|
||||||
"context"
|
// "context"
|
||||||
"errors"
|
// "errors"
|
||||||
"fmt"
|
// "fmt"
|
||||||
"strconv"
|
// "strconv"
|
||||||
|
|
||||||
"github.com/derailed/k9s/internal/k8s"
|
// "github.com/derailed/k9s/internal"
|
||||||
appsv1 "k8s.io/api/apps/v1"
|
// "github.com/derailed/k9s/internal/k8s"
|
||||||
)
|
// "github.com/derailed/k9s/internal/watch"
|
||||||
|
// appsv1 "k8s.io/api/apps/v1"
|
||||||
|
// "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||||
|
// "k8s.io/apimachinery/pkg/labels"
|
||||||
|
// "k8s.io/apimachinery/pkg/runtime"
|
||||||
|
// )
|
||||||
|
|
||||||
// Compile time checks to ensure type satisfies interface
|
// // Compile time checks to ensure type satisfies interface
|
||||||
var _ Restartable = (*DaemonSet)(nil)
|
// var _ Restartable = (*DaemonSet)(nil)
|
||||||
|
|
||||||
// DaemonSet tracks a kubernetes resource.
|
// // DaemonSet tracks a kubernetes resource.
|
||||||
type DaemonSet struct {
|
// type DaemonSet struct {
|
||||||
*Base
|
// *Base
|
||||||
instance *appsv1.DaemonSet
|
// instance *appsv1.DaemonSet
|
||||||
}
|
// }
|
||||||
|
|
||||||
// NewDaemonSetList returns a new resource list.
|
// // NewDaemonSetList returns a new resource list.
|
||||||
func NewDaemonSetList(c Connection, ns string) List {
|
// func NewDaemonSetList(c Connection, ns string) List {
|
||||||
return NewList(
|
// return NewList(
|
||||||
ns,
|
// ns,
|
||||||
"ds",
|
// "ds",
|
||||||
NewDaemonSet(c),
|
// NewDaemonSet(c),
|
||||||
AllVerbsAccess|DescribeAccess,
|
// AllVerbsAccess|DescribeAccess,
|
||||||
)
|
// )
|
||||||
}
|
// }
|
||||||
|
|
||||||
// NewDaemonSet instantiates a new DaemonSet.
|
// // NewDaemonSet instantiates a new DaemonSet.
|
||||||
func NewDaemonSet(c Connection) *DaemonSet {
|
// func NewDaemonSet(c Connection) *DaemonSet {
|
||||||
ds := &DaemonSet{&Base{Connection: c, Resource: k8s.NewDaemonSet(c)}, nil}
|
// ds := &DaemonSet{&Base{Connection: c, Resource: k8s.NewDaemonSet(c)}, nil}
|
||||||
ds.Factory = ds
|
// ds.Factory = ds
|
||||||
|
|
||||||
return ds
|
// return ds
|
||||||
}
|
// }
|
||||||
|
|
||||||
// New builds a new DaemonSet instance from a k8s resource.
|
// // New builds a new DaemonSet instance from a k8s resource.
|
||||||
func (r *DaemonSet) New(i interface{}) (Columnar, error) {
|
// func (r *DaemonSet) New(i interface{}) (Columnar, error) {
|
||||||
c := NewDaemonSet(r.Connection)
|
// c := NewDaemonSet(r.Connection)
|
||||||
switch instance := i.(type) {
|
// switch instance := i.(type) {
|
||||||
case *appsv1.DaemonSet:
|
// case *appsv1.DaemonSet:
|
||||||
c.instance = instance
|
// c.instance = instance
|
||||||
case appsv1.DaemonSet:
|
// case appsv1.DaemonSet:
|
||||||
c.instance = &instance
|
// c.instance = &instance
|
||||||
default:
|
// default:
|
||||||
return nil, fmt.Errorf("Expecting DaemonSet but got %T", instance)
|
// return nil, fmt.Errorf("Expecting DaemonSet but got %T", instance)
|
||||||
}
|
// }
|
||||||
c.path = c.namespacedName(c.instance.ObjectMeta)
|
// c.path = c.namespacedName(c.instance.ObjectMeta)
|
||||||
|
|
||||||
return c, nil
|
// return c, nil
|
||||||
}
|
// }
|
||||||
|
|
||||||
// Marshal resource to yaml.
|
// // Marshal resource to yaml.
|
||||||
func (r *DaemonSet) Marshal(path string) (string, error) {
|
// func (r *DaemonSet) Marshal(path string) (string, error) {
|
||||||
ns, n := Namespaced(path)
|
// ns, n := Namespaced(path)
|
||||||
i, err := r.Resource.Get(ns, n)
|
// i, err := r.Resource.Get(ns, n)
|
||||||
if err != nil {
|
// if err != nil {
|
||||||
return "", err
|
// return "", err
|
||||||
}
|
// }
|
||||||
|
|
||||||
ds, ok := i.(*appsv1.DaemonSet)
|
// ds, ok := i.(*appsv1.DaemonSet)
|
||||||
if !ok {
|
// if !ok {
|
||||||
return "", errors.New("expecting ds resource")
|
// return "", errors.New("expecting ds resource")
|
||||||
}
|
// }
|
||||||
ds.TypeMeta.APIVersion = "apps/v1"
|
// ds.TypeMeta.APIVersion = "apps/v1"
|
||||||
ds.TypeMeta.Kind = "DaemonSet"
|
// ds.TypeMeta.Kind = "DaemonSet"
|
||||||
|
|
||||||
return r.marshalObject(ds)
|
// return r.marshalObject(ds)
|
||||||
}
|
// }
|
||||||
|
|
||||||
// Logs tail logs for all pods represented by this DaemonSet.
|
// // Logs tail logs for all pods represented by this DaemonSet.
|
||||||
func (r *DaemonSet) Logs(ctx context.Context, c chan<- string, opts LogOptions) error {
|
// func (r *DaemonSet) Logs(ctx context.Context, c chan<- string, opts LogOptions) error {
|
||||||
instance, err := r.Resource.Get(opts.Namespace, opts.Name)
|
// f, ok := ctx.Value(internal.KeyFactory).(*watch.Factory)
|
||||||
if err != nil {
|
// if !ok {
|
||||||
return err
|
// return errors.New("no factory in context for pod logs")
|
||||||
}
|
// }
|
||||||
|
|
||||||
ds, ok := instance.(*appsv1.DaemonSet)
|
// o, err := f.Get(opts.Namespace, "apps/v1/daemonsets", opts.Name, labels.Everything())
|
||||||
if !ok {
|
// if err != nil {
|
||||||
return errors.New("expecting ds resource")
|
// return err
|
||||||
}
|
// }
|
||||||
if ds.Spec.Selector == nil || len(ds.Spec.Selector.MatchLabels) == 0 {
|
|
||||||
return fmt.Errorf("No valid selector found on daemonset %s", opts.FQN())
|
|
||||||
}
|
|
||||||
|
|
||||||
return r.podLogs(ctx, c, ds.Spec.Selector.MatchLabels, opts)
|
// var ds appsv1.DaemonSet
|
||||||
}
|
// err = runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &ds)
|
||||||
|
// if err != nil {
|
||||||
|
// return errors.New("expecting daemonset resource")
|
||||||
|
// }
|
||||||
|
|
||||||
// Header return resource header.
|
// if ds.Spec.Selector == nil || len(ds.Spec.Selector.MatchLabels) == 0 {
|
||||||
func (*DaemonSet) Header(ns string) Row {
|
// return fmt.Errorf("No valid selector found on daemonset %s", opts.FQN())
|
||||||
hh := Row{}
|
// }
|
||||||
if ns == AllNamespaces {
|
|
||||||
hh = append(hh, "NAMESPACE")
|
|
||||||
}
|
|
||||||
hh = append(hh, "NAME", "DESIRED", "CURRENT", "READY", "UP-TO-DATE")
|
|
||||||
hh = append(hh, "AVAILABLE", "NODE_SELECTOR", "AGE")
|
|
||||||
|
|
||||||
return hh
|
// return r.podLogs(ctx, c, ds.Spec.Selector.MatchLabels, opts)
|
||||||
}
|
// }
|
||||||
|
|
||||||
// Fields retrieves displayable fields.
|
// // Header return resource header.
|
||||||
func (r *DaemonSet) Fields(ns string) Row {
|
// func (*DaemonSet) Header(ns string) Row {
|
||||||
ff := make([]string, 0, len(r.Header(ns)))
|
// hh := Row{}
|
||||||
|
// if ns == AllNamespaces {
|
||||||
|
// hh = append(hh, "NAMESPACE")
|
||||||
|
// }
|
||||||
|
// hh = append(hh, "NAME", "DESIRED", "CURRENT", "READY", "UP-TO-DATE")
|
||||||
|
// hh = append(hh, "AVAILABLE", "NODE_SELECTOR", "AGE")
|
||||||
|
|
||||||
i := r.instance
|
// return hh
|
||||||
if ns == AllNamespaces {
|
// }
|
||||||
ff = append(ff, i.Namespace)
|
|
||||||
}
|
|
||||||
|
|
||||||
return append(ff,
|
// // Fields retrieves displayable fields.
|
||||||
i.Name,
|
// func (r *DaemonSet) Fields(ns string) Row {
|
||||||
strconv.Itoa(int(i.Status.DesiredNumberScheduled)),
|
// ff := make([]string, 0, len(r.Header(ns)))
|
||||||
strconv.Itoa(int(i.Status.CurrentNumberScheduled)),
|
|
||||||
strconv.Itoa(int(i.Status.NumberReady)),
|
|
||||||
strconv.Itoa(int(i.Status.UpdatedNumberScheduled)),
|
|
||||||
strconv.Itoa(int(i.Status.NumberAvailable)),
|
|
||||||
mapToStr(i.Spec.Template.Spec.NodeSelector),
|
|
||||||
toAge(i.ObjectMeta.CreationTimestamp),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Restart the rollout of the specified resource.
|
// i := r.instance
|
||||||
func (r *DaemonSet) Restart(ns, n string) error {
|
// if ns == AllNamespaces {
|
||||||
return r.Resource.(Restartable).Restart(ns, n)
|
// ff = append(ff, i.Namespace)
|
||||||
}
|
// }
|
||||||
|
|
||||||
|
// return append(ff,
|
||||||
|
// i.Name,
|
||||||
|
// strconv.Itoa(int(i.Status.DesiredNumberScheduled)),
|
||||||
|
// strconv.Itoa(int(i.Status.CurrentNumberScheduled)),
|
||||||
|
// strconv.Itoa(int(i.Status.NumberReady)),
|
||||||
|
// strconv.Itoa(int(i.Status.UpdatedNumberScheduled)),
|
||||||
|
// strconv.Itoa(int(i.Status.NumberAvailable)),
|
||||||
|
// mapToStr(i.Spec.Template.Spec.NodeSelector),
|
||||||
|
// toAge(i.ObjectMeta.CreationTimestamp),
|
||||||
|
// )
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // Restart the rollout of the specified resource.
|
||||||
|
// func (r *DaemonSet) Restart(ns, n string) error {
|
||||||
|
// return r.Resource.(Restartable).Restart(ns, n)
|
||||||
|
// }
|
||||||
|
|
|
||||||
|
|
@ -2,14 +2,9 @@ package resource
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"reflect"
|
"reflect"
|
||||||
|
|
||||||
"github.com/derailed/k9s/internal/model"
|
|
||||||
"github.com/derailed/k9s/internal/render"
|
"github.com/derailed/k9s/internal/render"
|
||||||
w "github.com/derailed/k9s/internal/watch"
|
|
||||||
"github.com/rs/zerolog/log"
|
|
||||||
"k8s.io/apimachinery/pkg/watch"
|
"k8s.io/apimachinery/pkg/watch"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -67,6 +62,11 @@ func (l *list) Namespaced() bool {
|
||||||
return l.namespace != NotNamespaced
|
return l.namespace != NotNamespaced
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IsClusterWide returns true if the resource is cluster scoped.
|
||||||
|
func (l *list) IsCluterWide() bool {
|
||||||
|
return l.namespace == render.ClusterWide
|
||||||
|
}
|
||||||
|
|
||||||
// AllNamespaces checks if this resource spans all namespaces.
|
// AllNamespaces checks if this resource spans all namespaces.
|
||||||
func (l *list) AllNamespaces() bool {
|
func (l *list) AllNamespaces() bool {
|
||||||
return l.namespace == AllNamespaces
|
return l.namespace == AllNamespaces
|
||||||
|
|
@ -114,14 +114,15 @@ func (l *list) Resource() Resource {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cache tracks previous resource state.
|
// Cache tracks previous resource state.
|
||||||
func (l *list) Data() TableData {
|
func (l *list) Data() render.TableData {
|
||||||
return TableData{
|
return render.TableData{
|
||||||
Header: l.header,
|
Header: l.header,
|
||||||
RowEvents: l.cache,
|
RowEvents: l.cache,
|
||||||
Namespace: l.namespace,
|
Namespace: l.namespace,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// BOZO!!
|
||||||
// func (l *list) load(informer *wa.Informer, ns string) (Columnars, error) {
|
// func (l *list) load(informer *wa.Informer, ns string) (Columnars, error) {
|
||||||
// rr, err := informer.List(l.name, ns, metav1.ListOptions{
|
// rr, err := informer.List(l.name, ns, metav1.ListOptions{
|
||||||
// FieldSelector: l.fieldSelector,
|
// FieldSelector: l.fieldSelector,
|
||||||
|
|
@ -178,39 +179,53 @@ func (l *list) Data() TableData {
|
||||||
// return res, nil
|
// return res, nil
|
||||||
// }
|
// }
|
||||||
|
|
||||||
type ContextKey string
|
|
||||||
|
|
||||||
const KeyFactory ContextKey = "factory"
|
|
||||||
|
|
||||||
// Reconcile previous vs current state and emits delta events.
|
// Reconcile previous vs current state and emits delta events.
|
||||||
func (l *list) Reconcile(ctx context.Context, gvr, path string) error {
|
func (l *list) Reconcile(ctx context.Context, gvr string) error {
|
||||||
log.Debug().Msgf("Reconcile %q in path %q", gvr, path)
|
panic("NYI")
|
||||||
ns := l.namespace
|
// path := ctx.Value(internal.KeySelection).(string)
|
||||||
if path != "" {
|
|
||||||
ns = path
|
|
||||||
}
|
|
||||||
|
|
||||||
factory, ok := ctx.Value(KeyFactory).(*w.Factory)
|
// log.Debug().Msgf("Reconcile %q in path %q", gvr, path)
|
||||||
if !ok {
|
// ns := l.namespace
|
||||||
return errors.New("no factory found in context")
|
// if path != "" {
|
||||||
}
|
// ns = path
|
||||||
m, ok := model.Registry[gvr]
|
// }
|
||||||
if !ok {
|
|
||||||
panic(fmt.Errorf("no model registered for %q", gvr))
|
|
||||||
}
|
|
||||||
m.Model.Init(ns, gvr, factory)
|
|
||||||
oo, err := m.Model.List(path)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
items := make(render.Rows, cap(oo))
|
|
||||||
if err := m.Model.Hydrate(oo, items, m.Renderer); err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
l.update(ns, items)
|
|
||||||
l.header = m.Renderer.Header(ns)
|
|
||||||
|
|
||||||
return nil
|
// factory, ok := ctx.Value(internal.KeyFactory).(*w.Factory)
|
||||||
|
// if !ok {
|
||||||
|
// return errors.New("no factory found in context")
|
||||||
|
// }
|
||||||
|
// m, ok := model.Registry[gvr]
|
||||||
|
// if !ok {
|
||||||
|
// log.Warn().Msgf("Resource %s not found in registry. Going generic!", gvr)
|
||||||
|
// m = model.ResourceMeta{
|
||||||
|
// Model: &model.Generic{},
|
||||||
|
// Renderer: &render.Generic{},
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// if m.Model == nil {
|
||||||
|
// m.Model = &model.Resource{}
|
||||||
|
// }
|
||||||
|
// m.Model.Init(ns, gvr, factory)
|
||||||
|
|
||||||
|
// if l.labelSelector != "" {
|
||||||
|
// ctx = context.WithValue(ctx, internal.KeyLabels, l.labelSelector)
|
||||||
|
// }
|
||||||
|
// if l.fieldSelector != "" {
|
||||||
|
// ctx = context.WithValue(ctx, internal.KeyFields, l.fieldSelector)
|
||||||
|
// }
|
||||||
|
// oo, err := m.Model.List(ctx)
|
||||||
|
// if err != nil {
|
||||||
|
// panic(err)
|
||||||
|
// }
|
||||||
|
// log.Debug().Msgf("Model returned [%d] items", len(oo))
|
||||||
|
// rows := make(render.Rows, len(oo))
|
||||||
|
// if err := m.Model.Hydrate(oo, rows, m.Renderer); err != nil {
|
||||||
|
// panic(err)
|
||||||
|
// }
|
||||||
|
// l.update(ns, rows)
|
||||||
|
// l.header = m.Renderer.Header(ns)
|
||||||
|
|
||||||
|
// return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *list) update(ns string, rows render.Rows) {
|
func (l *list) update(ns string, rows render.Rows) {
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue