checkpoint

mine
derailed 2019-12-02 16:11:39 -07:00
parent 7a7d66564d
commit 4c2c4793dc
156 changed files with 5506 additions and 3334 deletions

5
go.mod
View File

@ -2,6 +2,8 @@ module github.com/derailed/k9s
go 1.13
replace github.com/derailed/tview => /Users/fernand/go_wk/derailed/src/github.com/derailed/tview
replace (
k8s.io/api => k8s.io/api v0.0.0-20190918155943-95b840bb6a1f
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/ghodss/yaml v1.0.0 // 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/googleapis/gnostic v0.2.0 // 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/petergtz/pegomock v2.6.0+incompatible
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/spf13/cobra v0.0.5
github.com/stretchr/testify v1.3.0

6
go.sum
View File

@ -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-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.2.0 h1:28o5sBqPkBsMGnC6b4MvE2TzSr5/AT4c/1fLqVGIwlk=
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 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/zerolog v1.14.3 h1:4EGfSkR2hJDB0s3oFfrlPqjU1e4WLncergLil3nEKW0=
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/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=
@ -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-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-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/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
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-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-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/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=

View File

@ -14,6 +14,9 @@ var K9sAlias = filepath.Join(K9sHome, "alias.yml")
// Alias tracks shortname to GVR mappings.
type Alias map[string]string
// ShortNames represents a collection of shortnames for aliases.
type ShortNames map[string][]string
// Aliases represents a collection of aliases.
type Aliases struct {
Alias Alias `yaml:"alias"`
@ -88,13 +91,13 @@ func (a Aliases) Get(k string) (string, bool) {
}
// Define declares a new alias.
func (a Aliases) Define(command, alias string) {
if _, ok := a.Alias[alias]; ok {
// Don't override aliases. Take order of alias registration as precedence.
return
func (a Aliases) Define(gvr string, aliases ...string) {
for _, alias := range aliases {
if _, ok := a.Alias[alias]; ok {
continue
}
a.Alias[alias] = gvr
}
a.Alias[alias] = command
}
// LoadAliases loads alias from a given file.

View File

@ -11,7 +11,6 @@ import (
"path/filepath"
"github.com/derailed/k9s/internal/k8s"
"github.com/derailed/k9s/internal/resource"
"github.com/rs/zerolog/log"
"gopkg.in/yaml.v2"
v1 "k8s.io/api/core/v1"
@ -123,7 +122,7 @@ func (c *Config) ActiveNamespace() string {
return cl.Namespace.Active
}
}
return resource.DefaultNamespace
return ""
}
// FavNamespaces returns fav namespaces in the current cluster.

View File

@ -216,7 +216,7 @@ func newTable() Table {
FgColor: "aqua",
BgColor: "black",
CursorColor: "aqua",
MarkColor: "khaki",
MarkColor: "violet",
Header: newTableHeader(),
}
}

122
internal/dao/context.go Normal file
View File

@ -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
}

37
internal/dao/describe.go Normal file
View File

@ -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})
}

80
internal/dao/dp.go Normal file
View File

@ -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)
}

122
internal/dao/ds.go Normal file
View File

@ -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, ",")
}

124
internal/dao/gvr.go Normal file
View File

@ -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)
}
}

104
internal/dao/log_options.go Normal file
View File

@ -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
}

1
internal/dao/logger.go Normal file
View File

@ -0,0 +1 @@
package dao

196
internal/dao/pod.go Normal file
View File

@ -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
}

100
internal/dao/reconcile.go Normal file
View File

@ -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)
}
}
}

213
internal/dao/registry.go Normal file
View File

@ -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
}

32
internal/dao/resource.go Normal file
View File

@ -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())
}

View File

@ -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))
}

80
internal/dao/sts.go Normal file
View File

@ -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)
}

64
internal/dao/types.go Normal file
View File

@ -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
}

View File

@ -6,8 +6,6 @@ import (
"sync"
"time"
"k8s.io/client-go/discovery/cached/disk"
"github.com/rs/zerolog/log"
authorizationv1 "k8s.io/api/authorization/v1"
v1 "k8s.io/api/core/v1"
@ -15,6 +13,7 @@ import (
"k8s.io/apimachinery/pkg/fields"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/version"
"k8s.io/client-go/discovery/cached/disk"
"k8s.io/client-go/dynamic"
"k8s.io/client-go/kubernetes"
restclient "k8s.io/client-go/rest"
@ -107,7 +106,6 @@ func (a *APIClient) CheckNSAccess(n string) error {
func makeSAR(ns, gvr string) *authorizationv1.SelfSubjectAccessReview {
res := GVR(gvr).AsGVR()
log.Debug().Msgf("GVR for %s -- %#v", gvr, res)
return &authorizationv1.SelfSubjectAccessReview{
Spec: authorizationv1.SelfSubjectAccessReviewSpec{
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.
func (a *APIClient) NodePods(node string) (*v1.PodList, error) {
panic("NYI")
const selFmt = "spec.nodeName=%s,status.phase!=%s,status.phase!=%s"
fieldSelector, err := fields.ParseSelector(fmt.Sprintf(selFmt, node, v1.PodSucceeded, v1.PodFailed))
if err != nil {

View File

@ -17,11 +17,13 @@ func NewClusterRole(c Connection) *ClusterRole {
// Get a cluster role.
func (c *ClusterRole) Get(_, n string) (interface{}, error) {
panic("NYI")
return c.DialOrDie().RbacV1().ClusterRoles().Get(n, metav1.GetOptions{})
}
// List all ClusterRoles on a cluster.
func (c *ClusterRole) List(ns string, opts metav1.ListOptions) (Collection, error) {
panic("NYI")
rr, err := c.DialOrDie().RbacV1().ClusterRoles().List(opts)
if err != nil {
return nil, err

View File

@ -17,11 +17,13 @@ func NewClusterRoleBinding(c Connection) *ClusterRoleBinding {
// Get a service.
func (c *ClusterRoleBinding) Get(_, n string) (interface{}, error) {
panic("NYI")
return c.DialOrDie().RbacV1().ClusterRoleBindings().Get(n, metav1.GetOptions{})
}
// List all ClusterRoleBindings on a cluster.
func (c *ClusterRoleBinding) List(ns string, opts metav1.ListOptions) (Collection, error) {
panic("NYI")
rr, err := c.DialOrDie().RbacV1().ClusterRoleBindings().List(opts)
if err != nil {
return Collection{}, err

View File

@ -5,7 +5,6 @@ import (
"github.com/rs/zerolog/log"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/tools/clientcmd"
"k8s.io/client-go/tools/clientcmd/api"
)

View File

@ -19,11 +19,13 @@ func NewDeployment(c Connection) *Deployment {
// Get a deployment.
func (d *Deployment) Get(ns, n string) (interface{}, error) {
panic("NYI")
return d.DialOrDie().AppsV1().Deployments(ns).Get(n, metav1.GetOptions{})
}
// List all Deployments in a given namespace.
func (d *Deployment) List(ns string, opts metav1.ListOptions) (Collection, error) {
panic("NYI")
rr, err := d.DialOrDie().AppsV1().Deployments(ns).List(opts)
if err != nil {
return nil, err

View File

@ -1,64 +1,73 @@
package k8s
import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
"k8s.io/kubectl/pkg/polymorphichelpers"
)
// BOZO!!
// import (
// metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
// "k8s.io/apimachinery/pkg/types"
// "k8s.io/kubectl/pkg/polymorphichelpers"
// )
// DaemonSet represents a Kubernetes DaemonSet
type DaemonSet struct {
*base
Connection
}
// // DaemonSet represents a Kubernetes DaemonSet
// type DaemonSet struct {
// *base
// Connection
// }
// NewDaemonSet returns a new DaemonSet.
func NewDaemonSet(c Connection) *DaemonSet {
return &DaemonSet{&base{}, c}
}
// // NewDaemonSet returns a new DaemonSet.
// func NewDaemonSet(c Connection) *DaemonSet {
// return &DaemonSet{&base{}, c}
// }
// Get a DaemonSet.
func (d *DaemonSet) Get(ns, n string) (interface{}, error) {
return d.DialOrDie().AppsV1().DaemonSets(ns).Get(n, metav1.GetOptions{})
}
// // Get a DaemonSet.
// func (d *DaemonSet) Get(ns, n string) (interface{}, error) {
// panic("NYI")
// return d.DialOrDie().AppsV1().DaemonSets(ns).Get(n, metav1.GetOptions{})
// }
// List all DaemonSets in a given namespace.
func (d *DaemonSet) List(ns string, opts metav1.ListOptions) (Collection, error) {
rr, err := d.DialOrDie().AppsV1().DaemonSets(ns).List(opts)
if err != nil {
return nil, err
}
cc := make(Collection, len(rr.Items))
for i, r := range rr.Items {
cc[i] = r
}
// // List all DaemonSets in a given namespace.
// func (d *DaemonSet) List(ns string, opts metav1.ListOptions) (Collection, error) {
// panic("NYI")
// rr, err := d.DialOrDie().AppsV1().DaemonSets(ns).List(opts)
// if err != nil {
// return nil, err
// }
// cc := make(Collection, len(rr.Items))
// for i, r := range rr.Items {
// cc[i] = r
// }
return cc, nil
}
// return cc, nil
// }
// Delete a DaemonSet.
func (d *DaemonSet) Delete(ns, n string, cascade, force bool) error {
p := metav1.DeletePropagationOrphan
if cascade {
p = metav1.DeletePropagationBackground
}
return d.DialOrDie().AppsV1().DaemonSets(ns).Delete(n, &metav1.DeleteOptions{
PropagationPolicy: &p,
})
}
// // Delete a DaemonSet.
// func (d *DaemonSet) Delete(ns, n string, cascade, force bool) error {
// p := metav1.DeletePropagationOrphan
// if cascade {
// p = metav1.DeletePropagationBackground
// }
// return d.DialOrDie().AppsV1().DaemonSets(ns).Delete(n, &metav1.DeleteOptions{
// PropagationPolicy: &p,
// })
// }
// Restart a DaemonSet rollout.
func (d *DaemonSet) Restart(ns, n string) error {
// // Restart a DaemonSet rollout.
// 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{})
if err != nil {
return err
}
update, err := polymorphichelpers.ObjectRestarterFn(ds)
if err != nil {
return err
}
// var ds appsv1.DaemonSet
// err = runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &ds)
// if err != nil {
// return err
// }
_, err = d.DialOrDie().AppsV1().DaemonSets(ns).Patch(ds.Name, types.StrategicMergePatchType, update)
return err
}
// update, err := polymorphichelpers.ObjectRestarterFn(ds)
// if err != nil {
// return err
// }
// _, err = f.Client().DialOrDie().AppsV1().DaemonSets(ns).Patch(ds.Name, types.StrategicMergePatchType, update)
// return err
// }

View File

@ -25,8 +25,8 @@ func (g GVR) ResName() string {
return g.ToR() + "." + g.ToV() + "." + g.ToG()
}
// AsGR returns the group version.
func (g GVR) AsGR() schema.GroupVersion {
// AsGV returns the group version.
func (g GVR) AsGV() schema.GroupVersion {
return schema.GroupVersion{
Group: g.ToG(),
Version: g.ToV(),

View File

@ -8,7 +8,7 @@ import (
"k8s.io/apimachinery/pkg/runtime/schema"
)
func TestAsGR(t *testing.T) {
func TestAsGV(t *testing.T) {
uu := map[string]struct {
gvr string
e schema.GroupVersion
@ -21,7 +21,7 @@ func TestAsGR(t *testing.T) {
for k := range uu {
u := uu[k]
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())
})
}
}

View File

@ -73,7 +73,6 @@ func (r *RestMapper) resourceFor(resourceArg string) (schema.GroupVersionResourc
}
fullGVR, gr := schema.ParseResourceArg(strings.ToLower(resourceArg))
log.Debug().Msgf("GVR %#v -- %#v", fullGVR, gr)
if fullGVR != nil {
return mapper.ResourceFor(*fullGVR)
}

View File

@ -17,11 +17,13 @@ func NewNamespace(c Connection) *Namespace {
// Get a active namespace.
func (n *Namespace) Get(_, name string) (interface{}, error) {
panic("NYI")
return n.DialOrDie().CoreV1().Namespaces().Get(name, metav1.GetOptions{})
}
// List all active namespaces on the cluster.
func (n *Namespace) List(ns string, opts metav1.ListOptions) (Collection, error) {
panic("NYI")
rr, err := n.DialOrDie().CoreV1().Namespaces().List(opts)
if err != nil {
return nil, err

View File

@ -22,11 +22,14 @@ func NewPod(c Connection) *Pod {
// Get a pod.
func (p *Pod) Get(ns, name string) (interface{}, error) {
panic("POd GEt")
return p.DialOrDie().CoreV1().Pods(ns).Get(name, metav1.GetOptions{})
}
// List all pods in a given namespace.
func (p *Pod) List(ns string, opts metav1.ListOptions) (Collection, error) {
panic("POd List")
rr, err := p.DialOrDie().CoreV1().Pods(ns).List(opts)
if err != nil {
return nil, err

View File

@ -76,7 +76,7 @@ func (r *Resource) listAll(ns, n string) (runtime.Object, error) {
func (r *Resource) getClient() (*rest.RESTClient, error) {
crConfig := r.RestConfigOrDie()
gv := r.gvr.AsGR()
gv := r.gvr.AsGV()
crConfig.GroupVersion = &gv
crConfig.APIPath = "/apis"
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) {
scheme := runtime.NewScheme()
gv := r.gvr.AsGR()
gv := r.gvr.AsGV()
metav1.AddToGroupVersion(scheme, gv)
scheme.AddKnownTypes(gv, &metav1beta1.Table{}, &metav1beta1.TableOptions{})
scheme.AddKnownTypes(metav1beta1.SchemeGroupVersion, &metav1beta1.Table{}, &metav1beta1.TableOptions{})

View File

@ -18,11 +18,13 @@ func NewRole(c Connection) *Role {
// Get a Role.
func (r *Role) Get(ns, n string) (interface{}, error) {
panic("NYI")
return r.DialOrDie().RbacV1().Roles(ns).Get(n, metav1.GetOptions{})
}
// List all Roles in a given namespace.
func (r *Role) List(ns string, opts metav1.ListOptions) (Collection, error) {
panic("NYI")
rr, err := r.DialOrDie().RbacV1().Roles(ns).List(opts)
if err != nil {
return nil, err

View File

@ -15,11 +15,13 @@ func NewRoleBinding(c Connection) *RoleBinding {
// Get a RoleBinding.
func (r *RoleBinding) Get(ns, n string) (interface{}, error) {
panic("NYI")
return r.DialOrDie().RbacV1().RoleBindings(ns).Get(n, metav1.GetOptions{})
}
// List all RoleBindings in a given namespace.
func (r *RoleBinding) List(ns string, opts metav1.ListOptions) (Collection, error) {
panic("NYI")
rr, err := r.DialOrDie().RbacV1().RoleBindings(ns).List(opts)
if err != nil {
return nil, err

View File

@ -17,11 +17,13 @@ func NewService(c Connection) *Service {
// Get a service.
func (s *Service) Get(ns, n string) (interface{}, error) {
panic("NYI")
return s.DialOrDie().CoreV1().Services(ns).Get(n, metav1.GetOptions{})
}
// List all Services in a given namespace.
func (s *Service) List(ns string, opts metav1.ListOptions) (Collection, error) {
panic("NYI")
rr, err := s.DialOrDie().CoreV1().Services(ns).List(opts)
if err != nil {
return nil, err

14
internal/keys.go Normal file
View File

@ -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"
)

View File

@ -1,6 +1,9 @@
package model
import (
"context"
"github.com/derailed/k9s/internal"
"github.com/derailed/k9s/internal/k8s"
"github.com/derailed/k9s/internal/render"
"github.com/rs/zerolog/log"
@ -9,6 +12,7 @@ import (
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
mv1beta1 "k8s.io/metrics/pkg/apis/metrics/v1beta1"
)
@ -16,18 +20,15 @@ var _ render.ContainerWithMetrics = &ContainerWithMetrics{}
// Container represents a container model.
type Container struct {
*Resource
}
Resource
// NewContainer returns a new container model
func NewContainer() *Container {
return &Container{
Resource: NewResource(),
}
pod *v1.Pod
}
// 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)
c.namespace = ns
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 {
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
}
// Hydrate returns a pod as container rows.
func (c *Container) Hydrate(cc []runtime.Object, rr render.Rows, re Renderer) error {
po := cc[0].(*v1.Pod)
func (c *Container) Hydrate(oo []runtime.Object, rr render.Rows, re Renderer) error {
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 {
return err
log.Warn().Err(err).Msgf("No metrics found for pod %q:%q", c.namespace, c.pod.Name)
}
var index int
size := len(re.Header(c.namespace))
for _, co := range po.Spec.InitContainers {
row, err := renderCoRow(co.Name, index, size, coMetricsFor(co, po, mmx, true), re)
for _, o := range oo {
co := o.(ContainerRes)
row, err := renderCoRow(co.Container.Name, index, coMetricsFor(co.Container, c.pod, mmx, true), re)
if err != nil {
return err
}
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++
}
return nil
}
func renderCoRow(n string, index, size int, pmx *ContainerWithMetrics, re Renderer) (render.Row, error) {
row := render.Row{Fields: make([]string, size)}
func renderCoRow(n string, index int, pmx *ContainerWithMetrics, re Renderer) (render.Row, error) {
var row render.Row
if err := re.Render(pmx, n, &row); err != nil {
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 {
pmx := mx.(*mv1beta1.PodMetrics)
log.Debug().Msgf("CO MX fo %s", n)
for _, m := range pmx.Containers {
log.Debug().Msgf("Container Metrics %#v", m)
if m.Name == n {
return &m
}
@ -155,3 +151,20 @@ func (c *ContainerWithMetrics) Metrics() *mv1beta1.ContainerMetrics {
func (c *ContainerWithMetrics) Age() metav1.Time {
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
}

49
internal/model/context.go Normal file
View File

@ -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
}

130
internal/model/generic.go Normal file
View File

@ -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
}

View File

@ -1,9 +1,10 @@
package model
import (
"context"
"github.com/derailed/k9s/internal/k8s"
"github.com/derailed/k9s/internal/render"
"github.com/derailed/k9s/internal/watch"
"github.com/rs/zerolog/log"
v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@ -17,16 +18,11 @@ var _ render.NodeWithMetrics = &NodeWithMetrics{}
// Node represents a node model.
type Node struct {
*Resource
}
// NewNode returns a new node model.
func NewNode() *Node {
return &Node{Resource: NewResource()}
Resource
}
// 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{})
if err != nil {
return nil, err
@ -52,19 +48,17 @@ func (n *Node) Hydrate(oo []runtime.Object, rr render.Rows, re Renderer) error {
}
var index int
size := len(re.Header(""))
for _, no := range oo {
o := no.(*unstructured.Unstructured)
pods, err := n.nodePods(n.factory, o.Object["metadata"].(map[string]interface{})["name"].(string))
if err != nil {
panic(err)
}
row := render.Row{Fields: make([]string, size)}
nmx := NodeWithMetrics{
o,
nodeMetricsFor(o, mmx),
pods,
return err
}
var (
row render.Row
nmx = NodeWithMetrics{object: o, mx: nodeMetricsFor(o, mmx), pods: pods}
)
if err := re.Render(&nmx, "", &row); err != nil {
return err
}
@ -85,7 +79,7 @@ func nodeMetricsFor(o runtime.Object, mmx *mv1beta1.NodeMetricsList) *mv1beta1.N
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())
if err != nil {
return nil, err

View File

@ -1,9 +1,13 @@
package model
import (
"context"
"fmt"
"github.com/derailed/k9s/internal"
"github.com/derailed/k9s/internal/k8s"
"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/labels"
"k8s.io/apimachinery/pkg/runtime"
@ -12,37 +16,42 @@ import (
// Pod represents a pod model.
type Pod struct {
*Resource
Resource
}
// NewPod returns a new pod model.
func NewPod() *Pod {
return &Pod{NewResource()}
}
// List returns a collection of nodes.
func (p *Pod) List(ctx context.Context) ([]runtime.Object, error) {
oo, err := p.Resource.List(ctx)
if err != nil {
return oo, err
}
func (p *Pod) FetchContainers(sel string, includeInit bool) ([]string, error) {
o, err := p.factory.Get(p.namespace, p.gvr, sel, labels.Everything())
fieldSel, ok := ctx.Value(internal.KeyFields).(string)
if !ok {
return oo, nil
}
sel, err := labels.ConvertSelectorToLabelsMap(fieldSel)
if err != nil {
return nil, err
}
var po v1.Pod
if runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &po); err != nil {
return nil, err
nodeName, ok := sel["spec.nodeName"]
if !ok {
return nil, fmt.Errorf("NYI field selector %q", nodeName)
}
cc := make([]string, 0, len(po.Spec.Containers))
for _, c := range po.Spec.Containers {
cc = append(cc, c.Name)
}
if includeInit {
for _, c := range po.Spec.InitContainers {
cc = append(cc, c.Name)
var res []runtime.Object
for _, o := range oo {
u := o.(*unstructured.Unstructured)
spec := u.Object["spec"].(map[string]interface{})
log.Debug().Msgf("Spec node %q -- %q", nodeName, spec["nodeName"])
if spec["nodeName"] == nodeName {
res = append(res, o)
}
}
return cc, nil
return res, nil
}
// 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))
mmx, err := mx.FetchPodsMetrics(p.namespace)
if err != nil {
return err
log.Warn().Err(err).Msgf("No metrics found for pod")
}
var index int
size := len(re.Header(p.namespace))
for _, o := range oo {
row := render.Row{Fields: make([]string, size)}
pmx := PodWithMetrics{o, podMetricsFor(o, mmx)}
var (
row render.Row
pmx = PodWithMetrics{object: o, mx: podMetricsFor(o, mmx)}
)
if err := re.Render(&pmx, p.namespace, &row); err != nil {
return err
}

View File

@ -2,51 +2,81 @@ package model
import (
"github.com/derailed/k9s/internal/render"
"github.com/derailed/k9s/internal/watch"
"k8s.io/apimachinery/pkg/runtime"
)
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() 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
}
// BOZO!! Break up deps and merge into single registrar
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{
Model: NewPod(),
Model: &Pod{},
Renderer: &render.Pod{},
},
"v1/nodes": ResourceMeta{
Model: NewNode(),
Model: &Node{},
Renderer: &render.Node{},
},
"v1/configmaps": ResourceMeta{
Model: NewResource(),
Renderer: &render.ConfigMap{},
"v1/namespaces": ResourceMeta{
Renderer: &render.Namespace{},
},
"containers": ResourceMeta{
Model: NewContainer(),
Renderer: &render.Container{},
"apps/v1/deployments": ResourceMeta{
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{},
},
}

View File

@ -1,8 +1,11 @@
package model
import (
"context"
"github.com/derailed/k9s/internal"
"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/labels"
"k8s.io/apimachinery/pkg/runtime"
@ -11,30 +14,35 @@ import (
// Resource represents a generic resource model.
type Resource struct {
namespace, gvr string
factory *watch.Factory
factory Factory
}
func NewResource() *Resource {
return &Resource{}
}
// NewResource returns a new model.
func (r *Resource) Init(ns, gvr string, f *watch.Factory) {
func (r *Resource) Init(ns, gvr string, f Factory) {
r.namespace, r.gvr, r.factory = ns, gvr, f
}
// List returns a collection of nodes.
func (r *Resource) List(_ string) ([]runtime.Object, error) {
return r.factory.List(r.namespace, r.gvr, labels.Everything())
func (r *Resource) List(ctx context.Context) ([]runtime.Object, error) {
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.
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
size := len(re.Header(r.namespace))
for _, o := range oo {
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 {
return err
}

View File

@ -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
}

View File

@ -3,7 +3,13 @@ package model
import (
"context"
"github.com/derailed/k9s/internal/k8s"
"github.com/derailed/k9s/internal/render"
"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.
@ -20,6 +26,7 @@ type Igniter interface {
// Hinter represent a menu mnemonic provider.
type Hinter interface {
// Hints returns a collection of menu hints.
Hints() MenuHints
}
@ -37,3 +44,74 @@ type Component interface {
Igniter
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
}

64
internal/render/alias.go Normal file
View File

@ -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))
}

158
internal/render/bench.go Normal file
View File

@ -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)
}

View File

@ -30,7 +30,7 @@ func (CronJob) Header(ns string) HeaderRow {
Header{Name: "SUSPEND"},
Header{Name: "ACTIVE"},
Header{Name: "LAST_SCHEDULE"},
Header{Name: "AGE"},
Header{Name: "AGE", Decorator: ageDecorator},
)
}

View File

@ -28,7 +28,7 @@ func (ConfigMap) Header(ns string) HeaderRow {
return append(h,
Header{Name: "NAME"},
Header{Name: "DATA", Align: tview.AlignRight},
Header{Name: "AGE"},
Header{Name: "AGE", Decorator: ageDecorator},
)
}

View File

@ -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))
// }
// }

View File

@ -7,6 +7,7 @@ import (
"github.com/derailed/k9s/internal/k8s"
"github.com/derailed/tview"
"github.com/gdamore/tcell"
v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
mv1beta1 "k8s.io/metrics/pkg/apis/metrics/v1beta1"
@ -35,7 +36,29 @@ type Container struct{}
// ColorerFunc colors a resource row.
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.
@ -53,12 +76,12 @@ func (Container) Header(ns string) HeaderRow {
Header{Name: "%CPU", Align: tview.AlignRight},
Header{Name: "%MEM", Align: tview.AlignRight},
Header{Name: "PORTS"},
Header{Name: "AGE"},
Header{Name: "AGE", Decorator: ageDecorator},
}
}
// 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)
if !ok {
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()
c, p := gatherMetrics(co, oo.Metrics())
cur, perc := gatherMetrics(co, oo.Metrics())
ready, state, restarts := "false", MissingValue, "0"
if cs != nil {
ready, state, restarts = boolToStr(cs.Ready), toState(cs.State), strconv.Itoa(int(cs.RestartCount))
}
fields := make(Fields, 0, len(r.Fields))
fields = append(fields,
r.ID = co.Name
r.Fields = make(Fields, 0, len(c.Header(AllNamespaces)))
r.Fields = append(r.Fields,
co.Name,
co.Image,
ready,
@ -81,14 +105,13 @@ func (Container) Render(o interface{}, name string, r *Row) error {
boolToStr(oo.IsInit()),
restarts,
probe(co.LivenessProbe)+":"+probe(co.ReadinessProbe),
c.cpu,
c.mem,
p.cpu,
p.mem,
cur.cpu,
cur.mem,
perc.cpu,
perc.mem,
toStrPorts(co.Ports),
toAge(oo.Age()),
)
r.ID, r.Fields = co.Name, fields
return nil
}
@ -96,21 +119,6 @@ func (Container) Render(o interface{}, name string, r *Row) error {
// ----------------------------------------------------------------------------
// 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) {
c, p = noMetric(), noMetric()
if mx == nil {

View File

@ -2,8 +2,14 @@ package render
import (
"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.
@ -11,7 +17,17 @@ type Context struct{}
// ColorerFunc colors a resource row.
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.
@ -25,16 +41,58 @@ func (Context) Header(ns string) HeaderRow {
}
// Render renders a K8s resource to screen.
func (Context) Render(o interface{}, _ string, r *Row) error {
i, ok := o.(*api.Context)
func (c Context) Render(o interface{}, _ string, r *Row) error {
ctx, ok := o.(*NamedContext)
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
r.Fields[1] = i.Cluster
r.Fields[2] = i.AuthInfo
r.Fields[3] = i.Namespace
name := ctx.Name
if ctx.IsCurrentContext(ctx.Name) {
name += "(*)"
}
r.ID = ctx.Name
r.Fields = Fields{
name,
ctx.Context.Cluster,
ctx.Context.AuthInfo,
ctx.Context.Namespace,
}
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
}

View File

@ -20,7 +20,7 @@ func (ClusterRole) ColorerFunc() ColorerFunc {
func (ClusterRole) Header(string) HeaderRow {
return HeaderRow{
Header{Name: "NAME"},
Header{Name: "AGE"},
Header{Name: "AGE", Decorator: ageDecorator},
}
}

View File

@ -23,7 +23,7 @@ func (ClusterRoleBinding) Header(string) HeaderRow {
Header{Name: "ROLE"},
Header{Name: "KIND"},
Header{Name: "SUBJECTS"},
Header{Name: "AGE"},
Header{Name: "AGE", Decorator: ageDecorator},
}
}

View File

@ -1,7 +1,6 @@
package render
import (
"errors"
"fmt"
"time"
@ -22,7 +21,7 @@ func (CustomResourceDefinition) ColorerFunc() ColorerFunc {
func (CustomResourceDefinition) Header(string) HeaderRow {
return HeaderRow{
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
}
// TypeMeta represents resource type meta data.
type TypeMeta struct {
Name string
Namespaced bool
Group string
Version string
Kind string
Singular string
Plural string
ShortNames []string
}
// BOZO!!
// // TypeMeta represents resource type meta data.
// type TypeMeta struct {
// Name string
// Namespaced bool
// Group string
// Version string
// Kind string
// Singular string
// Plural string
// ShortNames []string
// }
func (CustomResourceDefinition) Meta(o interface{}) (TypeMeta, error) {
var m TypeMeta
// func (CustomResourceDefinition) Meta(o interface{}) (TypeMeta, error) {
// var m TypeMeta
crd, ok := o.(*unstructured.Unstructured)
if !ok {
return m, fmt.Errorf("Expected CustomResourceDefinition, but got %T", o)
}
// crd, ok := o.(*unstructured.Unstructured)
// if !ok {
// return m, fmt.Errorf("Expected CustomResourceDefinition, but got %T", o)
// }
spec, ok := crd.Object["spec"].(map[string]interface{})
if !ok {
return m, errors.New("missing crd specs")
}
// spec, ok := crd.Object["spec"].(map[string]interface{})
// if !ok {
// return m, errors.New("missing crd specs")
// }
if meta, ok := crd.Object["metadata"].(map[string]interface{}); ok {
m.Name = meta["name"].(string)
}
m.Group, m.Version = spec["group"].(string), spec["version"].(string)
m.Namespaced = isNamespaced(spec["scope"].(string))
names, ok := spec["names"].(map[string]interface{})
if !ok {
return m, errors.New("missing crd names")
}
m.Kind = names["kind"].(string)
m.Singular, m.Plural = names["singular"].(string), names["plural"].(string)
if names["shortNames"] != nil {
for _, s := range names["shortNames"].([]interface{}) {
m.ShortNames = append(m.ShortNames, s.(string))
}
} else {
m.ShortNames = nil
}
return m, nil
}
// if meta, ok := crd.Object["metadata"].(map[string]interface{}); ok {
// m.Name = meta["name"].(string)
// }
// m.Group, m.Version = spec["group"].(string), spec["version"].(string)
// m.Namespaced = isNamespaced(spec["scope"].(string))
// names, ok := spec["names"].(map[string]interface{})
// if !ok {
// return m, errors.New("missing crd names")
// }
// m.Kind = names["kind"].(string)
// m.Singular, m.Plural = names["singular"].(string), names["plural"].(string)
// if names["shortNames"] != nil {
// for _, s := range names["shortNames"].([]interface{}) {
// m.ShortNames = append(m.ShortNames, s.(string))
// }
// } else {
// m.ShortNames = nil
// }
// return m, nil
// }
func isNamespaced(scope string) bool {
return scope == "Namespaced"
}
// func isNamespaced(scope string) bool {
// return scope == "Namespaced"
// }

View File

@ -1,5 +1,7 @@
package render
import "github.com/rs/zerolog/log"
// DeltaRow represents a collection of row detlas between old and new row.
type DeltaRow []string
@ -7,10 +9,11 @@ type DeltaRow []string
func NewDeltaRow(o, n Row) DeltaRow {
deltas := make(DeltaRow, len(o.Fields))
// Exclude age col
fields := o.Fields[:len(o.Fields)-1]
for i, v := range fields {
if v != "" && n.Fields[i] != v {
deltas[i] = v
oldFields := o.Fields[:len(o.Fields)-1]
for i, old := range oldFields {
if old != "" && old != n.Fields[i] {
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
}
// 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
}

View File

@ -3,9 +3,13 @@ package render
import (
"fmt"
"strconv"
"strings"
"github.com/derailed/tview"
"github.com/gdamore/tcell"
"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/runtime"
)
@ -19,7 +23,23 @@ func isAllNamespace(ns string) bool {
// ColorerFunc colors a resource row.
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.
@ -31,16 +51,16 @@ func (Deployment) Header(ns string) HeaderRow {
return append(h,
Header{Name: "NAME"},
Header{Name: "DESIRED", Align: tview.AlignRight},
Header{Name: "CURRENT", Align: tview.AlignRight},
Header{Name: "READY"},
Header{Name: "UP-TO-DATE", 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.
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)
if !ok {
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
}
fields := make(Fields, 0, len(r.Fields))
r.ID = MetaFQN(dp.ObjectMeta)
r.Fields = make(Fields, 0, len(d.Header(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,
strconv.Itoa(int(*dp.Spec.Replicas)),
strconv.Itoa(int(dp.Status.Replicas)),
strconv.Itoa(int(dp.Status.AvailableReplicas))+"/"+strconv.Itoa(int(*dp.Spec.Replicas)),
strconv.Itoa(int(dp.Status.UpdatedReplicas)),
strconv.Itoa(int(dp.Status.AvailableReplicas)),
asSelector(dp.Spec.Selector),
toAge(dp.ObjectMeta.CreationTimestamp),
)
r.ID, r.Fields = MetaFQN(dp.ObjectMeta), fields
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()
}

View File

@ -13,5 +13,5 @@ func TestDeploymentRender(t *testing.T) {
c.Render(load(t, "dp"), "", &r)
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])
}

View File

@ -3,8 +3,10 @@ package render
import (
"fmt"
"strconv"
"strings"
"github.com/derailed/tview"
"github.com/gdamore/tcell"
appsv1 "k8s.io/api/apps/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
@ -15,7 +17,22 @@ type DaemonSet struct{}
// ColorerFunc colors a resource row.
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.
@ -33,12 +50,12 @@ func (DaemonSet) Header(ns string) HeaderRow {
Header{Name: "UP-TO-DATE", Align: tview.AlignRight},
Header{Name: "AVAILABLE", Align: tview.AlignRight},
Header{Name: "NODE_SELECTOR"},
Header{Name: "AGE"},
Header{Name: "AGE", Decorator: ageDecorator},
)
}
// 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)
if !ok {
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
}
fields := make(Fields, 0, len(r.Fields))
r.ID = MetaFQN(ds.ObjectMeta)
r.Fields = make(Fields, 0, len(d.Header(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,
strconv.Itoa(int(ds.Status.DesiredNumberScheduled)),
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),
toAge(ds.ObjectMeta.CreationTimestamp),
)
r.ID, r.Fields = MetaFQN(ds.ObjectMeta), fields
return nil
}

View File

@ -28,7 +28,7 @@ func (Endpoints) Header(ns string) HeaderRow {
return append(h,
Header{Name: "NAME"},
Header{Name: "ENDPOINTS"},
Header{Name: "AGE"},
Header{Name: "AGE", Decorator: ageDecorator},
)
}

View File

@ -3,8 +3,10 @@ package render
import (
"fmt"
"strconv"
"strings"
"github.com/derailed/tview"
"github.com/gdamore/tcell"
v1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
@ -15,7 +17,22 @@ type Event struct{}
// ColorerFunc colors a resource row.
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.
@ -31,7 +48,7 @@ func (Event) Header(ns string) HeaderRow {
Header{Name: "SOURCE"},
Header{Name: "COUNT", Align: tview.AlignRight},
Header{Name: "MESSAGE"},
Header{Name: "AGE"},
Header{Name: "AGE", Decorator: ageDecorator},
)
}

View File

@ -1,9 +1,11 @@
package render
import (
"fmt"
"sort"
"github.com/gdamore/tcell"
"github.com/rs/zerolog/log"
)
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.
func (re RowEvents) Delete(id string) RowEvents {
idx, ok := re.FindIndex(id)
func (rr RowEvents) Delete(id string) RowEvents {
idx, ok := rr.FindIndex(id)
if !ok {
return re
return rr
}
if idx == 0 {
return re[1:]
return rr[1:]
}
if idx == len(re)-1 {
return re[:len(re)-1]
if idx == len(rr)-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.
func (re RowEvents) FindIndex(id string) (int, bool) {
for i, e := range re {
func (rr RowEvents) FindIndex(id string) (int, bool) {
for i, e := range rr {
if e.Row.ID == id {
return i, true
}
@ -82,9 +121,28 @@ func (re RowEvents) FindIndex(id string) (int, bool) {
}
// Sort rows based on column index and order.
func (re RowEvents) Sort(ns string, col int, asc bool) {
t := RowEventSorter{NS: ns, Events: re, Index: col, Asc: asc}
func (rr RowEvents) Sort(ns string, col int, asc bool) {
t := RowEventSorter{NS: ns, Events: rr, Index: col, Asc: asc}
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 {
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.
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.
func DefaultColorer(ns string, evt ResEvent, r Row) tcell.Color {
switch evt {
func DefaultColorer(ns string, evt RowEvent) tcell.Color {
switch evt.Kind {
case EventAdd:
return AddColor
case EventUpdate:
@ -155,3 +235,25 @@ func DefaultColorer(ns string, evt ResEvent, r Row) tcell.Color {
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
}

View File

@ -52,7 +52,7 @@ func TestDefaultColorer(t *testing.T) {
for k, u := range uu {
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{}))
})
}
}

110
internal/render/forward.go Normal file
View File

@ -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
}

View File

@ -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
}

View File

@ -152,13 +152,13 @@ func boolToStr(b bool) 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 {
d, err := time.ParseDuration(s)
if err != nil {
return "<unknown>"
return NAValue
}
return duration.HumanDuration(d)

View File

@ -32,7 +32,7 @@ func (HorizontalPodAutoscaler) Header(ns string) HeaderRow {
Header{Name: "MINPODS", Align: tview.AlignRight},
Header{Name: "MAXPODS", Align: tview.AlignRight},
Header{Name: "REPLICAS", Align: tview.AlignRight},
Header{Name: "AGE"},
Header{Name: "AGE", Decorator: ageDecorator},
)
}

View File

@ -30,7 +30,7 @@ func (Ingress) Header(ns string) HeaderRow {
Header{Name: "HOSTS"},
Header{Name: "ADDRESS"},
Header{Name: "PORT"},
Header{Name: "AGE"},
Header{Name: "AGE", Decorator: ageDecorator},
)
}

View File

@ -34,7 +34,7 @@ func (Job) Header(ns string) HeaderRow {
Header{Name: "DURATION"},
Header{Name: "CONTAINERS"},
Header{Name: "IMAGES"},
Header{Name: "AGE"},
Header{Name: "AGE", Decorator: ageDecorator},
)
}

View File

@ -50,7 +50,7 @@ func (Node) Header(_ string) HeaderRow {
Header{Name: "%MEM", Align: tview.AlignRight},
Header{Name: "ACPU", Align: tview.AlignRight},
Header{Name: "AMEM", Align: tview.AlignRight},
Header{Name: "AGE"},
Header{Name: "AGE", Decorator: ageDecorator},
}
}

View File

@ -33,7 +33,7 @@ func (NetworkPolicy) Header(ns string) HeaderRow {
Header{Name: "EGR-SELECTOR"},
Header{Name: "EGR-PORTS"},
Header{Name: "EGR-BLOCK"},
Header{Name: "AGE"},
Header{Name: "AGE", Decorator: ageDecorator},
)
}

View File

@ -2,7 +2,9 @@ package render
import (
"fmt"
"strings"
"github.com/gdamore/tcell"
v1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
@ -13,7 +15,22 @@ type Namespace struct{}
// ColorerFunc colors a resource row.
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.
@ -21,7 +38,7 @@ func (Namespace) Header(string) HeaderRow {
return HeaderRow{
Header{Name: "NAME"},
Header{Name: "STATUS"},
Header{Name: "AGE"},
Header{Name: "AGE", Decorator: ageDecorator},
}
}

View File

@ -3,8 +3,10 @@ package render
import (
"fmt"
"strconv"
"strings"
"github.com/derailed/tview"
"github.com/gdamore/tcell"
v1beta1 "k8s.io/api/policy/v1beta1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
@ -16,7 +18,23 @@ type PodDisruptionBudget struct{}
// ColorerFunc colors a resource row.
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.
@ -34,7 +52,7 @@ func (PodDisruptionBudget) Header(ns string) HeaderRow {
Header{Name: "CURRENT", Align: tview.AlignRight},
Header{Name: "DESIRED", Align: tview.AlignRight},
Header{Name: "EXPECTED", Align: tview.AlignRight},
Header{Name: "AGE"},
Header{Name: "AGE", Decorator: ageDecorator},
)
}

View File

@ -28,9 +28,9 @@ type PodWithMetrics interface {
type Pod struct{}
// ColorerFunc colors a resource row.
func (Pod) ColorerFunc() ColorerFunc {
return func(ns string, evt ResEvent, r Row) tcell.Color {
c := DefaultColorer(ns, evt, r)
func (p Pod) ColorerFunc() ColorerFunc {
return func(ns string, re RowEvent) tcell.Color {
c := DefaultColorer(ns, re)
readyCol := 2
if len(ns) != 0 {
@ -38,29 +38,39 @@ func (Pod) ColorerFunc() ColorerFunc {
}
statusCol := readyCol + 1
tokens := strings.Split(strings.TrimSpace(r.Fields[readyCol]), "/")
if len(tokens) == 2 && (tokens[0] == "0" || tokens[0] != tokens[1]) {
if strings.TrimSpace(r.Fields[statusCol]) != "Completed" {
c = ErrColor
}
}
ready, status := strings.TrimSpace(re.Row.Fields[readyCol]), strings.TrimSpace(re.Row.Fields[statusCol])
c = p.checkReadyCol(ready, status, c)
switch strings.TrimSpace(r.Fields[statusCol]) {
case "ContainerCreating", "PodInitializing":
switch status {
case ContainerCreating, PodInitializing:
return AddColor
case "Terminating", "Initialized":
case Initialized:
return HighlightColor
case "Completed":
case Completed:
return CompletedColor
case "Running":
case Running:
case Terminating:
return KillColor
default:
c = ErrColor
return ErrColor
}
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.
func (Pod) Header(ns string) HeaderRow {
var h HeaderRow
@ -80,7 +90,7 @@ func (Pod) Header(ns string) HeaderRow {
Header{Name: "IP"},
Header{Name: "NODE"},
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
err := runtime.DefaultUnstructuredConverter.FromUnstructured(oo.Object().(*unstructured.Unstructured).Object, &po)
if err != nil {
log.Error().Err(err).Msg("Converting Pod")
log.Error().Err(err).Msg("Expecting a pod resource")
return err
}
@ -102,11 +112,12 @@ func (p Pod) Render(o interface{}, ns string, r *Row) error {
cr, _, rc := p.statuses(ss)
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) {
fields = append(fields, po.Namespace)
r.Fields = append(r.Fields, po.Namespace)
}
fields = append(fields,
r.Fields = append(r.Fields,
po.ObjectMeta.Name,
strconv.Itoa(cr)+"/"+strconv.Itoa(len(ss)),
p.phase(&po),
@ -120,11 +131,8 @@ func (p Pod) Render(o interface{}, ns string, r *Row) error {
p.mapQOS(po.Status.QOSClass),
toAge(po.ObjectMeta.CreationTimestamp),
)
r.ID = MetaFQN(po.ObjectMeta)
r.Fields = fields
return nil
}
// ----------------------------------------------------------------------------

49
internal/render/policy.go Normal file
View File

@ -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...

View File

@ -5,6 +5,7 @@ import (
"path"
"strings"
"github.com/gdamore/tcell"
v1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
@ -15,7 +16,25 @@ type PersistentVolume struct{}
// ColorerFunc colors a resource row.
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.
@ -29,7 +48,7 @@ func (PersistentVolume) Header(string) HeaderRow {
Header{Name: "CLAIM"},
Header{Name: "STORAGECLASS"},
Header{Name: "REASON"},
Header{Name: "AGE"},
Header{Name: "AGE", Decorator: ageDecorator},
}
}

View File

@ -2,7 +2,9 @@ package render
import (
"fmt"
"strings"
"github.com/gdamore/tcell"
v1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
@ -13,7 +15,24 @@ type PersistentVolumeClaim struct{}
// ColorerFunc colors a resource row.
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.
@ -30,7 +49,7 @@ func (PersistentVolumeClaim) Header(ns string) HeaderRow {
Header{Name: "CAPACITY"},
Header{Name: "ACCESS MODES"},
Header{Name: "STORAGECLASS"},
Header{Name: "AGE"},
Header{Name: "AGE", Decorator: ageDecorator},
)
}

View File

@ -29,7 +29,7 @@ func (RoleBinding) Header(ns string) HeaderRow {
Header{Name: "ROLE"},
Header{Name: "KIND"},
Header{Name: "SUBJECTS"},
Header{Name: "AGE"},
Header{Name: "AGE", Decorator: ageDecorator},
)
}

33
internal/render/rbac.go Normal file
View File

@ -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...

View File

@ -25,7 +25,7 @@ func (Role) Header(ns string) HeaderRow {
return append(h,
Header{Name: "NAME"},
Header{Name: "AGE"},
Header{Name: "AGE", Decorator: ageDecorator},
)
}

View File

@ -21,13 +21,18 @@ type Rows []Row
// Header represent a table header
type Header struct {
Name string
Align int
Name string
Align int
Decorator DecoratorFunc
}
// HeaderRow represents a table header.
type HeaderRow []Header
func (h HeaderRow) AgeCol(col int) bool {
return col == len(h)-1
}
// RowSorter sorts rows.
type RowSorter struct {
Rows Rows
@ -35,6 +40,22 @@ type RowSorter struct {
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.
func (rr Rows) Delete(id string) Rows {
idx, ok := rr.Find(id)
@ -57,6 +78,16 @@ func NewRow(cols int) Row {
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.
func (rr Rows) Find(id string) (int, bool) {
for i, r := range rr {

View File

@ -3,8 +3,10 @@ package render
import (
"fmt"
"strconv"
"strings"
"github.com/derailed/tview"
"github.com/gdamore/tcell"
appsv1 "k8s.io/api/apps/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
@ -15,7 +17,23 @@ type ReplicaSet struct{}
// ColorerFunc colors a resource row.
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.
@ -30,7 +48,7 @@ func (ReplicaSet) Header(ns string) HeaderRow {
Header{Name: "DESIRED", Align: tview.AlignRight},
Header{Name: "CURRENT", Align: tview.AlignRight},
Header{Name: "READY", Align: tview.AlignRight},
Header{Name: "AGE"},
Header{Name: "AGE", Decorator: ageDecorator},
)
}

View File

@ -27,7 +27,7 @@ func (ServiceAccount) Header(ns string) HeaderRow {
return append(h,
Header{Name: "NAME"},
Header{Name: "SECRET"},
Header{Name: "AGE"},
Header{Name: "AGE", Decorator: ageDecorator},
)
}

View File

@ -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
}

View File

@ -29,7 +29,7 @@ func (Secret) Header(ns string) HeaderRow {
Header{Name: "NAME"},
Header{Name: "TYPE"},
Header{Name: "DATA", Align: tview.AlignRight},
Header{Name: "AGE"},
Header{Name: "AGE", Decorator: ageDecorator},
)
}

81
internal/render/sts.go Normal file
View File

@ -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
}

View File

@ -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
}

View File

@ -33,7 +33,7 @@ func (Service) Header(ns string) HeaderRow {
Header{Name: "EXTERNAL-IP"},
Header{Name: "SELECTOR"},
Header{Name: "PORTS"},
Header{Name: "AGE"},
Header{Name: "AGE", Decorator: ageDecorator},
)
}

16
internal/render/table.go Normal file
View File

@ -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,
}
}

35
internal/render/types.go Normal file
View File

@ -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"
)

View File

@ -4,8 +4,10 @@ import (
"bytes"
"context"
"errors"
"fmt"
"path"
"github.com/derailed/k9s/internal"
"github.com/derailed/k9s/internal/k8s"
"github.com/derailed/k9s/internal/watch"
"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 {
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))
if err != nil {

View File

@ -81,6 +81,7 @@ func (r *Custom) New(i interface{}) (Columnar, error) {
// Marshal resource to yaml.
func (r *Custom) Marshal(path string) (string, error) {
panic("NYI")
ns, n := Namespaced(path)
i, err := r.Resource.Get(ns, n)
if err != nil {

View File

@ -44,18 +44,19 @@ func TestCustomFields(t *testing.T) {
assert.Equal(t, "a", r[0])
}
func TestCustomMarshal(t *testing.T) {
mc := NewMockConnection()
mr := NewMockCruder()
m.When(mr.Get("blee", "fred")).ThenReturn(k8sCustomTable(), nil)
// BOZO!!
// func TestCustomMarshal(t *testing.T) {
// mc := NewMockConnection()
// mr := NewMockCruder()
// m.When(mr.Get("blee", "fred")).ThenReturn(k8sCustomTable(), nil)
cm := NewCustomWithArgs(mc, mr)
ma, err := cm.Marshal("blee/fred")
mr.VerifyWasCalledOnce().Get("blee", "fred")
// cm := NewCustomWithArgs(mc, mr)
// ma, err := cm.Marshal("blee/fred")
// mr.VerifyWasCalledOnce().Get("blee", "fred")
assert.Nil(t, err)
assert.Equal(t, customYaml(), ma)
}
// assert.Nil(t, err)
// assert.Equal(t, customYaml(), ma)
// }
func TestCustomMarshalWithUnstructured(t *testing.T) {
mc := NewMockConnection()

View File

@ -1,128 +1,140 @@
package resource
import (
"context"
"errors"
"fmt"
"strconv"
// import (
// "context"
// "errors"
// "fmt"
// "strconv"
"github.com/derailed/k9s/internal/k8s"
appsv1 "k8s.io/api/apps/v1"
)
// "github.com/derailed/k9s/internal"
// "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
var _ Restartable = (*DaemonSet)(nil)
// // Compile time checks to ensure type satisfies interface
// var _ Restartable = (*DaemonSet)(nil)
// DaemonSet tracks a kubernetes resource.
type DaemonSet struct {
*Base
instance *appsv1.DaemonSet
}
// // DaemonSet tracks a kubernetes resource.
// type DaemonSet struct {
// *Base
// instance *appsv1.DaemonSet
// }
// NewDaemonSetList returns a new resource list.
func NewDaemonSetList(c Connection, ns string) List {
return NewList(
ns,
"ds",
NewDaemonSet(c),
AllVerbsAccess|DescribeAccess,
)
}
// // NewDaemonSetList returns a new resource list.
// func NewDaemonSetList(c Connection, ns string) List {
// return NewList(
// ns,
// "ds",
// NewDaemonSet(c),
// AllVerbsAccess|DescribeAccess,
// )
// }
// NewDaemonSet instantiates a new DaemonSet.
func NewDaemonSet(c Connection) *DaemonSet {
ds := &DaemonSet{&Base{Connection: c, Resource: k8s.NewDaemonSet(c)}, nil}
ds.Factory = ds
// // NewDaemonSet instantiates a new DaemonSet.
// func NewDaemonSet(c Connection) *DaemonSet {
// ds := &DaemonSet{&Base{Connection: c, Resource: k8s.NewDaemonSet(c)}, nil}
// ds.Factory = ds
return ds
}
// return ds
// }
// New builds a new DaemonSet instance from a k8s resource.
func (r *DaemonSet) New(i interface{}) (Columnar, error) {
c := NewDaemonSet(r.Connection)
switch instance := i.(type) {
case *appsv1.DaemonSet:
c.instance = instance
case appsv1.DaemonSet:
c.instance = &instance
default:
return nil, fmt.Errorf("Expecting DaemonSet but got %T", instance)
}
c.path = c.namespacedName(c.instance.ObjectMeta)
// // New builds a new DaemonSet instance from a k8s resource.
// func (r *DaemonSet) New(i interface{}) (Columnar, error) {
// c := NewDaemonSet(r.Connection)
// switch instance := i.(type) {
// case *appsv1.DaemonSet:
// c.instance = instance
// case appsv1.DaemonSet:
// c.instance = &instance
// default:
// return nil, fmt.Errorf("Expecting DaemonSet but got %T", instance)
// }
// c.path = c.namespacedName(c.instance.ObjectMeta)
return c, nil
}
// return c, nil
// }
// Marshal resource to yaml.
func (r *DaemonSet) Marshal(path string) (string, error) {
ns, n := Namespaced(path)
i, err := r.Resource.Get(ns, n)
if err != nil {
return "", err
}
// // Marshal resource to yaml.
// func (r *DaemonSet) Marshal(path string) (string, error) {
// ns, n := Namespaced(path)
// i, err := r.Resource.Get(ns, n)
// if err != nil {
// return "", err
// }
ds, ok := i.(*appsv1.DaemonSet)
if !ok {
return "", errors.New("expecting ds resource")
}
ds.TypeMeta.APIVersion = "apps/v1"
ds.TypeMeta.Kind = "DaemonSet"
// ds, ok := i.(*appsv1.DaemonSet)
// if !ok {
// return "", errors.New("expecting ds resource")
// }
// ds.TypeMeta.APIVersion = "apps/v1"
// ds.TypeMeta.Kind = "DaemonSet"
return r.marshalObject(ds)
}
// return r.marshalObject(ds)
// }
// Logs tail logs for all pods represented by this DaemonSet.
func (r *DaemonSet) Logs(ctx context.Context, c chan<- string, opts LogOptions) error {
instance, err := r.Resource.Get(opts.Namespace, opts.Name)
if err != nil {
return err
}
// // Logs tail logs for all pods represented by this DaemonSet.
// func (r *DaemonSet) Logs(ctx context.Context, c chan<- string, opts LogOptions) error {
// f, ok := ctx.Value(internal.KeyFactory).(*watch.Factory)
// if !ok {
// return errors.New("no factory in context for pod logs")
// }
ds, ok := instance.(*appsv1.DaemonSet)
if !ok {
return errors.New("expecting ds resource")
}
if ds.Spec.Selector == nil || len(ds.Spec.Selector.MatchLabels) == 0 {
return fmt.Errorf("No valid selector found on daemonset %s", opts.FQN())
}
// o, err := f.Get(opts.Namespace, "apps/v1/daemonsets", opts.Name, labels.Everything())
// if err != nil {
// return err
// }
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.
func (*DaemonSet) Header(ns string) Row {
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")
// if ds.Spec.Selector == nil || len(ds.Spec.Selector.MatchLabels) == 0 {
// return fmt.Errorf("No valid selector found on daemonset %s", opts.FQN())
// }
return hh
}
// return r.podLogs(ctx, c, ds.Spec.Selector.MatchLabels, opts)
// }
// Fields retrieves displayable fields.
func (r *DaemonSet) Fields(ns string) Row {
ff := make([]string, 0, len(r.Header(ns)))
// // Header return resource header.
// func (*DaemonSet) Header(ns string) Row {
// 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
if ns == AllNamespaces {
ff = append(ff, i.Namespace)
}
// return hh
// }
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),
)
}
// // Fields retrieves displayable fields.
// func (r *DaemonSet) Fields(ns string) Row {
// ff := make([]string, 0, len(r.Header(ns)))
// Restart the rollout of the specified resource.
func (r *DaemonSet) Restart(ns, n string) error {
return r.Resource.(Restartable).Restart(ns, n)
}
// i := r.instance
// if ns == AllNamespaces {
// 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)
// }

View File

@ -2,14 +2,9 @@ package resource
import (
"context"
"errors"
"fmt"
"reflect"
"github.com/derailed/k9s/internal/model"
"github.com/derailed/k9s/internal/render"
w "github.com/derailed/k9s/internal/watch"
"github.com/rs/zerolog/log"
"k8s.io/apimachinery/pkg/watch"
)
@ -67,6 +62,11 @@ func (l *list) Namespaced() bool {
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.
func (l *list) AllNamespaces() bool {
return l.namespace == AllNamespaces
@ -114,14 +114,15 @@ func (l *list) Resource() Resource {
}
// Cache tracks previous resource state.
func (l *list) Data() TableData {
return TableData{
func (l *list) Data() render.TableData {
return render.TableData{
Header: l.header,
RowEvents: l.cache,
Namespace: l.namespace,
}
}
// BOZO!!
// func (l *list) load(informer *wa.Informer, ns string) (Columnars, error) {
// rr, err := informer.List(l.name, ns, metav1.ListOptions{
// FieldSelector: l.fieldSelector,
@ -178,39 +179,53 @@ func (l *list) Data() TableData {
// return res, nil
// }
type ContextKey string
const KeyFactory ContextKey = "factory"
// Reconcile previous vs current state and emits delta events.
func (l *list) Reconcile(ctx context.Context, gvr, path string) error {
log.Debug().Msgf("Reconcile %q in path %q", gvr, path)
ns := l.namespace
if path != "" {
ns = path
}
func (l *list) Reconcile(ctx context.Context, gvr string) error {
panic("NYI")
// path := ctx.Value(internal.KeySelection).(string)
factory, ok := ctx.Value(KeyFactory).(*w.Factory)
if !ok {
return errors.New("no factory found in context")
}
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)
// log.Debug().Msgf("Reconcile %q in path %q", gvr, path)
// ns := l.namespace
// if path != "" {
// ns = path
// }
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) {

Some files were not shown because too many files have changed in this diff Show More