checkpoint
parent
7a7d66564d
commit
4c2c4793dc
5
go.mod
5
go.mod
|
|
@ -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
6
go.sum
|
|
@ -176,6 +176,7 @@ github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfU
|
|||
github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/groupcache v0.0.0-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=
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -216,7 +216,7 @@ func newTable() Table {
|
|||
FgColor: "aqua",
|
||||
BgColor: "black",
|
||||
CursorColor: "aqua",
|
||||
MarkColor: "khaki",
|
||||
MarkColor: "violet",
|
||||
Header: newTableHeader(),
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,122 @@
|
|||
package dao
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/derailed/k9s/internal/k8s"
|
||||
"github.com/rs/zerolog/log"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/client-go/tools/clientcmd"
|
||||
"k8s.io/client-go/tools/clientcmd/api"
|
||||
)
|
||||
|
||||
type Context struct {
|
||||
Resource
|
||||
}
|
||||
|
||||
var _ Accessor = &Context{}
|
||||
var _ Switchable = &Context{}
|
||||
|
||||
func (c *Context) config() *k8s.Config {
|
||||
return c.Factory.Client().Config()
|
||||
}
|
||||
|
||||
// Get a Context.
|
||||
func (c *Context) Get(_, n string) (runtime.Object, error) {
|
||||
ctx, err := c.config().GetContext(n)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &NamedContext{Name: n, Context: ctx}, nil
|
||||
}
|
||||
|
||||
// List all Contexts on the current cluster.
|
||||
func (c *Context) List(string, metav1.ListOptions) ([]runtime.Object, error) {
|
||||
ctxs, err := c.config().Contexts()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cc := make([]runtime.Object, 0, len(ctxs))
|
||||
for k, v := range ctxs {
|
||||
cc = append(cc, NewNamedContext(c.config(), k, v))
|
||||
}
|
||||
|
||||
return cc, nil
|
||||
}
|
||||
|
||||
// Delete a Context.
|
||||
func (c *Context) Delete(ns, n string, cascade, force bool) error {
|
||||
ctx, err := c.config().CurrentContextName()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if ctx == n {
|
||||
return fmt.Errorf("trying to delete your current context %s", n)
|
||||
}
|
||||
|
||||
return c.config().DelContext(n)
|
||||
}
|
||||
|
||||
// MustCurrentContextName return the active context name.
|
||||
func (c *Context) MustCurrentContextName() string {
|
||||
cl, err := c.config().CurrentContextName()
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("Fetching current context")
|
||||
}
|
||||
return cl
|
||||
}
|
||||
|
||||
// Switch to another context.
|
||||
func (c *Context) Switch(ctx string) error {
|
||||
c.Factory.Client().SwitchContextOrDie(ctx)
|
||||
return nil
|
||||
}
|
||||
|
||||
// KubeUpdate modifies kubeconfig default context.
|
||||
func (c *Context) KubeUpdate(n string) error {
|
||||
config, err := c.config().RawConfig()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := c.Switch(n); err != nil {
|
||||
return err
|
||||
}
|
||||
return clientcmd.ModifyConfig(
|
||||
clientcmd.NewDefaultPathOptions(), config, true,
|
||||
)
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
// NamedContext represents a named cluster context.
|
||||
type NamedContext struct {
|
||||
Name string
|
||||
Context *api.Context
|
||||
config *k8s.Config
|
||||
}
|
||||
|
||||
// NewNamedContext returns a new named context.
|
||||
func NewNamedContext(c *k8s.Config, n string, ctx *api.Context) *NamedContext {
|
||||
return &NamedContext{Name: n, Context: ctx, config: c}
|
||||
}
|
||||
|
||||
// MustCurrentContextName return the active context name.
|
||||
func (c *NamedContext) MustCurrentContextName() string {
|
||||
cl, err := c.config.CurrentContextName()
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("Fetching current context")
|
||||
}
|
||||
return cl
|
||||
}
|
||||
|
||||
// GetObjectKind returns a schema object.
|
||||
func (c *NamedContext) GetObjectKind() schema.ObjectKind {
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeepCopyObject returns a container copy.
|
||||
func (c *NamedContext) DeepCopyObject() runtime.Object {
|
||||
return c
|
||||
}
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
package dao
|
||||
|
||||
import (
|
||||
"github.com/derailed/k9s/internal/k8s"
|
||||
"github.com/rs/zerolog/log"
|
||||
"k8s.io/kubectl/pkg/describe"
|
||||
"k8s.io/kubectl/pkg/describe/versioned"
|
||||
)
|
||||
|
||||
func Describe(c k8s.Connection, gvr GVR, ns, n string) (string, error) {
|
||||
mapper := k8s.RestMapper{Connection: c}
|
||||
m, err := mapper.ToRESTMapper()
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msgf("No REST mapper for resource %s", gvr)
|
||||
return "", err
|
||||
}
|
||||
|
||||
GVR := k8s.GVR(gvr)
|
||||
gvk, err := m.KindFor(GVR.AsGVR())
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msgf("No GVK for resource %s", gvr)
|
||||
return "", err
|
||||
}
|
||||
|
||||
mapping, err := mapper.ResourceFor(GVR.ResName(), gvk.Kind)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msgf("Unable to find mapper for %s %s", gvr, n)
|
||||
return "", err
|
||||
}
|
||||
d, err := versioned.Describer(c.Config().Flags(), mapping)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msgf("Unable to find describer for %#v", mapping)
|
||||
return "", err
|
||||
}
|
||||
|
||||
return d.Describe(ns, n, describe.DescriberSettings{ShowEvents: true})
|
||||
}
|
||||
|
|
@ -0,0 +1,80 @@
|
|||
package dao
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
appsv1 "k8s.io/api/apps/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/labels"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"k8s.io/kubectl/pkg/polymorphichelpers"
|
||||
)
|
||||
|
||||
type Deployment struct {
|
||||
Resource
|
||||
}
|
||||
|
||||
var _ Accessor = &Deployment{}
|
||||
var _ Loggable = &Deployment{}
|
||||
var _ Restartable = &Deployment{}
|
||||
var _ Scalable = &Deployment{}
|
||||
|
||||
// Scale a Deployment.
|
||||
func (d *Deployment) Scale(ns, n string, replicas int32) error {
|
||||
scale, err := d.Client().DialOrDie().AppsV1().Deployments(ns).GetScale(n, metav1.GetOptions{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
scale.Spec.Replicas = replicas
|
||||
_, err = d.Client().DialOrDie().AppsV1().Deployments(ns).UpdateScale(n, scale)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// Restart a Deployment rollout.
|
||||
func (d *Deployment) Restart(ns, n string) error {
|
||||
o, err := d.Get(ns, string(d.gvr), n, labels.Everything())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var ds appsv1.Deployment
|
||||
err = runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &ds)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
update, err := polymorphichelpers.ObjectRestarterFn(&ds)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = d.Client().DialOrDie().AppsV1().Deployments(ns).Patch(ds.Name, types.StrategicMergePatchType, update)
|
||||
return err
|
||||
}
|
||||
|
||||
// Logs tail logs for all pods represented by this Deployment.
|
||||
func (d *Deployment) TailLogs(ctx context.Context, c chan<- string, opts LogOptions) error {
|
||||
log.Debug().Msgf("Tailing Deployment %q -- %q", opts.Namespace, opts.Name)
|
||||
o, err := d.Get(opts.Namespace, string(d.gvr), opts.Name, labels.Everything())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var dp appsv1.Deployment
|
||||
err = runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &dp)
|
||||
if err != nil {
|
||||
return errors.New("expecting Deployment resource")
|
||||
}
|
||||
|
||||
if dp.Spec.Selector == nil || len(dp.Spec.Selector.MatchLabels) == 0 {
|
||||
return fmt.Errorf("No valid selector found on Deployment %s", opts.FQN())
|
||||
}
|
||||
|
||||
return podLogs(ctx, c, dp.Spec.Selector.MatchLabels, opts)
|
||||
}
|
||||
|
|
@ -0,0 +1,122 @@
|
|||
package dao
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/derailed/k9s/internal"
|
||||
"github.com/derailed/k9s/internal/watch"
|
||||
"github.com/rs/zerolog/log"
|
||||
appsv1 "k8s.io/api/apps/v1"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/labels"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"k8s.io/kubectl/pkg/polymorphichelpers"
|
||||
)
|
||||
|
||||
type DaemonSet struct {
|
||||
Resource
|
||||
}
|
||||
|
||||
var _ Accessor = &DaemonSet{}
|
||||
var _ Loggable = &DaemonSet{}
|
||||
var _ Restartable = &DaemonSet{}
|
||||
|
||||
// Restart a DaemonSet rollout.
|
||||
func (d *DaemonSet) Restart(ns, n string) error {
|
||||
o, err := d.Get(ns, string(d.gvr), n, labels.Everything())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var ds appsv1.DaemonSet
|
||||
err = runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &ds)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
update, err := polymorphichelpers.ObjectRestarterFn(&ds)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = d.Client().DialOrDie().AppsV1().DaemonSets(ns).Patch(ds.Name, types.StrategicMergePatchType, update)
|
||||
return err
|
||||
}
|
||||
|
||||
// Logs tail logs for all pods represented by this DaemonSet.
|
||||
func (d *DaemonSet) TailLogs(ctx context.Context, c chan<- string, opts LogOptions) error {
|
||||
log.Debug().Msgf("Tailing DaemonSet %q -- %q", opts.Namespace, opts.Name)
|
||||
o, err := d.Get(opts.Namespace, "apps/v1/daemonsets", opts.Name, labels.Everything())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var ds appsv1.DaemonSet
|
||||
err = runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &ds)
|
||||
if err != nil {
|
||||
return errors.New("expecting daemonset resource")
|
||||
}
|
||||
|
||||
if ds.Spec.Selector == nil || len(ds.Spec.Selector.MatchLabels) == 0 {
|
||||
return fmt.Errorf("No valid selector found on daemonset %s", opts.FQN())
|
||||
}
|
||||
|
||||
return podLogs(ctx, c, ds.Spec.Selector.MatchLabels, opts)
|
||||
}
|
||||
|
||||
func podLogs(ctx context.Context, c chan<- string, sel map[string]string, opts LogOptions) error {
|
||||
f, ok := ctx.Value(internal.KeyFactory).(*watch.Factory)
|
||||
if !ok {
|
||||
return errors.New("expecting a context factory")
|
||||
}
|
||||
ls, err := metav1.ParseToLabelSelector(toSelector(sel))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
lsel, err := metav1.LabelSelectorAsSelector(ls)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
oo, err := f.List(opts.Namespace, "v1/pods", lsel)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(oo) > 1 {
|
||||
opts.MultiPods = true
|
||||
}
|
||||
|
||||
po := Pod{}
|
||||
for _, o := range oo {
|
||||
var pod v1.Pod
|
||||
err := runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &pod)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if pod.Status.Phase == v1.PodRunning {
|
||||
opts.Namespace, opts.Name = pod.Namespace, pod.Name
|
||||
if err := po.TailLogs(ctx, c, opts); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Helpers...
|
||||
|
||||
func toSelector(m map[string]string) string {
|
||||
s := make([]string, 0, len(m))
|
||||
for k, v := range m {
|
||||
s = append(s, k+"="+v)
|
||||
}
|
||||
|
||||
return strings.Join(s, ",")
|
||||
}
|
||||
|
|
@ -0,0 +1,124 @@
|
|||
package dao
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"vbom.ml/util/sortorder"
|
||||
)
|
||||
|
||||
// GVR represents a kubernetes resource schema as a string.
|
||||
// Format is group/version/resources
|
||||
type GVR string
|
||||
|
||||
// NewGVR builds a new gvr from a group, version, resource.
|
||||
func NewGVR(g, v, r string) GVR {
|
||||
return GVR(path.Join(g, v, r))
|
||||
}
|
||||
|
||||
// FromGVAndR builds a gvr from a group/version and resource.
|
||||
func FromGVAndR(gv, r string) GVR {
|
||||
return GVR(path.Join(gv, r))
|
||||
}
|
||||
|
||||
// ResName returns a resource . separated descriptor in the shape of kind.version.group.
|
||||
func (g GVR) ResName() string {
|
||||
return g.ToR() + "." + g.ToV() + "." + g.ToG()
|
||||
}
|
||||
|
||||
// AsGV returns the group version scheme representation.
|
||||
func (g GVR) AsGV() schema.GroupVersion {
|
||||
return schema.GroupVersion{
|
||||
Group: g.ToG(),
|
||||
Version: g.ToV(),
|
||||
}
|
||||
}
|
||||
|
||||
// AsGVR returns a a full schema representation.
|
||||
func (g GVR) AsGVR() schema.GroupVersionResource {
|
||||
return schema.GroupVersionResource{
|
||||
Group: g.ToG(),
|
||||
Version: g.ToV(),
|
||||
Resource: g.ToR(),
|
||||
}
|
||||
}
|
||||
|
||||
// ToV returns the resource version.
|
||||
func (g GVR) ToV() string {
|
||||
tokens := strings.Split(string(g), "/")
|
||||
if len(tokens) < 2 {
|
||||
return ""
|
||||
}
|
||||
return tokens[len(tokens)-2]
|
||||
}
|
||||
|
||||
// ToR returns the resource name.
|
||||
func (g GVR) ToR() string {
|
||||
tokens := strings.Split(string(g), "/")
|
||||
return tokens[len(tokens)-1]
|
||||
}
|
||||
|
||||
// ToG returns the resource group name.
|
||||
func (g GVR) ToG() string {
|
||||
tokens := strings.Split(string(g), "/")
|
||||
switch len(tokens) {
|
||||
case 3:
|
||||
return tokens[0]
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
type GVRs []GVR
|
||||
|
||||
func (g GVRs) Len() int {
|
||||
return len(g)
|
||||
}
|
||||
|
||||
func (g GVRs) Swap(i, j int) {
|
||||
g[i], g[j] = g[j], g[i]
|
||||
}
|
||||
|
||||
func (g GVRs) Less(i, j int) bool {
|
||||
g1, g2 := g[i].ToG(), g[j].ToG()
|
||||
|
||||
return sortorder.NaturalLess(g1, g2)
|
||||
}
|
||||
|
||||
// Helper...
|
||||
|
||||
// Can determines the available actions for a given resource.
|
||||
func Can(verbs []string, v string) bool {
|
||||
for _, verb := range verbs {
|
||||
candidates, err := mapVerb(v)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msgf("verb mapping failed")
|
||||
return false
|
||||
}
|
||||
for _, c := range candidates {
|
||||
if verb == c {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func mapVerb(v string) ([]string, error) {
|
||||
switch v {
|
||||
case "describe":
|
||||
return []string{"get"}, nil
|
||||
case "view":
|
||||
return []string{"get", "list"}, nil
|
||||
case "delete":
|
||||
return []string{"delete"}, nil
|
||||
case "edit":
|
||||
return []string{"patch", "update"}, nil
|
||||
default:
|
||||
return []string{}, fmt.Errorf("no standard verb for %q", v)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,104 @@
|
|||
package dao
|
||||
|
||||
import (
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
"github.com/derailed/k9s/internal/color"
|
||||
"github.com/derailed/tview"
|
||||
runewidth "github.com/mattn/go-runewidth"
|
||||
)
|
||||
|
||||
type (
|
||||
// Fqn uniquely describes a container
|
||||
Fqn struct {
|
||||
Namespace, Name, Container string
|
||||
}
|
||||
|
||||
// LogOptions represent logger options.
|
||||
LogOptions struct {
|
||||
Fqn
|
||||
|
||||
Lines int64
|
||||
Color color.Paint
|
||||
Previous bool
|
||||
SingleContainer bool
|
||||
MultiPods bool
|
||||
}
|
||||
)
|
||||
|
||||
// HasContainer checks if a container is present.
|
||||
func (o LogOptions) HasContainer() bool {
|
||||
return o.Container != ""
|
||||
}
|
||||
|
||||
// FQN returns resource fully qualified name.
|
||||
func (o LogOptions) FQN() string {
|
||||
return FQN(o.Namespace, o.Name)
|
||||
}
|
||||
|
||||
// Path returns resource descriptor path.
|
||||
func (o LogOptions) Path() string {
|
||||
return o.FQN() + ":" + o.Container
|
||||
}
|
||||
|
||||
// FixedSizeName returns a normalize fixed size pod name if possible.
|
||||
func (o LogOptions) FixedSizeName() string {
|
||||
tokens := strings.Split(o.Name, "-")
|
||||
if len(tokens) < 3 {
|
||||
return o.Name
|
||||
}
|
||||
var s []string
|
||||
for i := 0; i < len(tokens)-1; i++ {
|
||||
s = append(s, tokens[i])
|
||||
}
|
||||
return Truncate(strings.Join(s, "-"), 15) + "-" + tokens[len(tokens)-1]
|
||||
}
|
||||
|
||||
func colorize(c color.Paint, txt string) string {
|
||||
if c == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
return color.Colorize(txt, c)
|
||||
}
|
||||
|
||||
// DecorateLog add a log header to display po/co information along with the log message.
|
||||
func (o LogOptions) DecorateLog(msg string) string {
|
||||
if msg == "" {
|
||||
return msg
|
||||
}
|
||||
|
||||
if o.MultiPods {
|
||||
return colorize(o.Color, o.Name+":"+o.Container+" ") + msg
|
||||
}
|
||||
|
||||
if !o.SingleContainer {
|
||||
return colorize(o.Color, o.Container+" ") + msg
|
||||
}
|
||||
|
||||
return msg
|
||||
}
|
||||
|
||||
// Helpers...
|
||||
|
||||
// BOZO!! Consolidate!!
|
||||
// Truncate a string to the given l and suffix ellipsis if needed.
|
||||
func Truncate(str string, width int) string {
|
||||
return runewidth.Truncate(str, width, string(tview.SemigraphicsHorizontalEllipsis))
|
||||
}
|
||||
|
||||
// Namespaced return a namesapace and a name.
|
||||
func Namespaced(n string) (string, string) {
|
||||
ns, po := path.Split(n)
|
||||
|
||||
return strings.Trim(ns, "/"), po
|
||||
}
|
||||
|
||||
// FQN returns a fully qualified resource name.
|
||||
func FQN(ns, n string) string {
|
||||
if ns == "" {
|
||||
return n
|
||||
}
|
||||
return ns + "/" + n
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
package dao
|
||||
|
|
@ -0,0 +1,196 @@
|
|||
package dao
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/derailed/k9s/internal"
|
||||
"github.com/derailed/k9s/internal/color"
|
||||
"github.com/derailed/k9s/internal/watch"
|
||||
"github.com/rs/zerolog/log"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/labels"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
restclient "k8s.io/client-go/rest"
|
||||
)
|
||||
|
||||
const defaultTimeout = 1 * time.Second
|
||||
|
||||
type Logger interface {
|
||||
Logs(ns, n string, opts *v1.PodLogOptions) *restclient.Request
|
||||
}
|
||||
|
||||
type Pod struct {
|
||||
Resource
|
||||
}
|
||||
|
||||
var _ Accessor = &Pod{}
|
||||
|
||||
// Logs fetch container logs for a given pod and container.
|
||||
func (p *Pod) Logs(ns, n string, opts *v1.PodLogOptions) *restclient.Request {
|
||||
return p.Client().DialOrDie().CoreV1().Pods(ns).GetLogs(n, opts)
|
||||
}
|
||||
|
||||
// Containers returns all container names on pod
|
||||
func (p *Pod) Containers(ns, n string, includeInit bool) ([]string, error) {
|
||||
o, err := p.Get(ns, "v1/pod", n, labels.Everything())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var pod v1.Pod
|
||||
err = runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &pod)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cc := []string{}
|
||||
for _, c := range pod.Spec.Containers {
|
||||
cc = append(cc, c.Name)
|
||||
}
|
||||
|
||||
if includeInit {
|
||||
for _, c := range pod.Spec.InitContainers {
|
||||
cc = append(cc, c.Name)
|
||||
}
|
||||
}
|
||||
|
||||
return cc, nil
|
||||
}
|
||||
|
||||
// Logs tails a given container logs
|
||||
func (p *Pod) TailLogs(ctx context.Context, c chan<- string, opts LogOptions) error {
|
||||
if !opts.HasContainer() {
|
||||
return p.logs(ctx, c, opts)
|
||||
}
|
||||
return tailLogs(ctx, p, c, opts)
|
||||
}
|
||||
|
||||
// PodLogs tail logs for all containers in a running Pod.
|
||||
func (p *Pod) logs(ctx context.Context, c chan<- string, opts LogOptions) error {
|
||||
fac, ok := ctx.Value(internal.KeyFactory).(*watch.Factory)
|
||||
if !ok {
|
||||
return errors.New("Expecting an informer")
|
||||
}
|
||||
ns, n := Namespaced(opts.FQN())
|
||||
o, err := fac.Get(ns, "v1/pods", n, labels.Everything())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var po v1.Pod
|
||||
if runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &po); err != nil {
|
||||
return err
|
||||
}
|
||||
opts.Color = asColor(po.Name)
|
||||
if len(po.Spec.InitContainers)+len(po.Spec.Containers) == 1 {
|
||||
opts.SingleContainer = true
|
||||
}
|
||||
|
||||
for _, co := range po.Spec.InitContainers {
|
||||
opts.Container = co.Name
|
||||
if err := p.TailLogs(ctx, c, opts); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
rcos := loggableContainers(po.Status)
|
||||
for _, co := range po.Spec.Containers {
|
||||
if in(rcos, co.Name) {
|
||||
opts.Container = co.Name
|
||||
if err := p.TailLogs(ctx, c, opts); err != nil {
|
||||
log.Error().Err(err).Msgf("Getting logs for %s failed", co.Name)
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func tailLogs(ctx context.Context, logger Logger, c chan<- string, opts LogOptions) error {
|
||||
log.Debug().Msgf("Tailing logs for %q -- %q -- %q", opts.Namespace, opts.Name, opts.Container)
|
||||
o := v1.PodLogOptions{
|
||||
Container: opts.Container,
|
||||
Follow: true,
|
||||
TailLines: &opts.Lines,
|
||||
Previous: opts.Previous,
|
||||
}
|
||||
req := logger.Logs(opts.Namespace, opts.Name, &o)
|
||||
ctxt, cancelFunc := context.WithCancel(ctx)
|
||||
req.Context(ctxt)
|
||||
|
||||
var blocked int32 = 1
|
||||
go logsTimeout(cancelFunc, &blocked)
|
||||
|
||||
// This call will block if nothing is in the stream!!
|
||||
stream, err := req.Stream()
|
||||
atomic.StoreInt32(&blocked, 0)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msgf("Log stream failed for `%s", opts.Path())
|
||||
return fmt.Errorf("Unable to obtain log stream for %s", opts.Path())
|
||||
}
|
||||
go readLogs(ctx, stream, c, opts)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func logsTimeout(cancel context.CancelFunc, blocked *int32) {
|
||||
<-time.After(defaultTimeout)
|
||||
if atomic.LoadInt32(blocked) == 1 {
|
||||
log.Debug().Msg("Timed out reading the log stream")
|
||||
cancel()
|
||||
}
|
||||
}
|
||||
|
||||
func readLogs(ctx context.Context, stream io.ReadCloser, c chan<- string, opts LogOptions) {
|
||||
defer func() {
|
||||
log.Debug().Msgf(">>> Closing stream `%s", opts.Path())
|
||||
if err := stream.Close(); err != nil {
|
||||
log.Error().Err(err).Msg("Cloing stream")
|
||||
}
|
||||
}()
|
||||
|
||||
scanner := bufio.NewScanner(stream)
|
||||
for scanner.Scan() {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
default:
|
||||
c <- opts.DecorateLog(scanner.Text())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Helpers...
|
||||
|
||||
func loggableContainers(s v1.PodStatus) []string {
|
||||
var rcos []string
|
||||
for _, c := range s.ContainerStatuses {
|
||||
rcos = append(rcos, c.Name)
|
||||
}
|
||||
return rcos
|
||||
}
|
||||
|
||||
func asColor(n string) color.Paint {
|
||||
var sum int
|
||||
for _, r := range n {
|
||||
sum += int(r)
|
||||
}
|
||||
return color.Paint(30 + 2 + sum%6)
|
||||
}
|
||||
|
||||
// Check if string is in a string list.
|
||||
func in(ll []string, s string) bool {
|
||||
for _, l := range ll {
|
||||
if l == s {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
|
@ -0,0 +1,100 @@
|
|||
package dao
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/derailed/k9s/internal"
|
||||
"github.com/derailed/k9s/internal/model"
|
||||
"github.com/derailed/k9s/internal/render"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
// Reconcile previous vs current state and emits delta events.
|
||||
func Reconcile(ctx context.Context, table render.TableData, gvr GVR) (render.TableData, error) {
|
||||
path, ok := ctx.Value(internal.KeySelection).(string)
|
||||
if !ok {
|
||||
return table, fmt.Errorf("no path specified for %s", gvr)
|
||||
}
|
||||
if path != "" {
|
||||
log.Debug().Msgf("########## OVERRIDING NS %q", path)
|
||||
table.Namespace = path
|
||||
}
|
||||
log.Debug().Msgf(" Reconcile %q in ns %q with path %q", gvr, table.Namespace, path)
|
||||
factory, ok := ctx.Value(internal.KeyFactory).(Factory)
|
||||
if !ok {
|
||||
return table, fmt.Errorf("no factory found for %s", gvr)
|
||||
}
|
||||
m, ok := model.Registry[string(gvr)]
|
||||
if !ok {
|
||||
log.Warn().Msgf("Resource %s not found in registry. Going generic!", gvr)
|
||||
m = model.ResourceMeta{
|
||||
Model: &model.Generic{},
|
||||
Renderer: &render.Generic{},
|
||||
}
|
||||
}
|
||||
if m.Model == nil {
|
||||
m.Model = &model.Resource{}
|
||||
}
|
||||
m.Model.Init(table.Namespace, string(gvr), factory)
|
||||
|
||||
table.Header = m.Renderer.Header(table.Namespace)
|
||||
oo, err := m.Model.List(ctx)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
log.Debug().Msgf("Model returned [%d] items", len(oo))
|
||||
rows := make(render.Rows, len(oo))
|
||||
if err := m.Model.Hydrate(oo, rows, m.Renderer); err != nil {
|
||||
return table, err
|
||||
}
|
||||
update(&table, rows)
|
||||
|
||||
log.Debug().Msgf("Table returned [%d] events", len(table.RowEvents))
|
||||
return table, nil
|
||||
}
|
||||
|
||||
func update(table *render.TableData, rows render.Rows) {
|
||||
cacheEmpty := len(table.RowEvents) == 0
|
||||
kk := make([]string, 0, len(rows))
|
||||
var blankDelta render.DeltaRow
|
||||
for _, row := range rows {
|
||||
kk = append(kk, row.ID)
|
||||
if cacheEmpty {
|
||||
table.RowEvents = append(table.RowEvents, render.NewRowEvent(render.EventAdd, row))
|
||||
continue
|
||||
}
|
||||
if index, ok := table.RowEvents.FindIndex(row.ID); ok {
|
||||
delta := render.NewDeltaRow(table.RowEvents[index].Row, row)
|
||||
if delta.IsBlank() {
|
||||
table.RowEvents[index].Kind, table.RowEvents[index].Deltas = render.EventUnchanged, blankDelta
|
||||
} else {
|
||||
table.RowEvents[index] = render.NewDeltaRowEvent(row, delta)
|
||||
}
|
||||
continue
|
||||
}
|
||||
table.RowEvents = append(table.RowEvents, render.NewRowEvent(render.EventAdd, row))
|
||||
}
|
||||
|
||||
if cacheEmpty {
|
||||
return
|
||||
}
|
||||
ensureDeletes(table, kk)
|
||||
}
|
||||
|
||||
// EnsureDeletes delete items in cache that are no longer valid.
|
||||
func ensureDeletes(table *render.TableData, newKeys []string) {
|
||||
for _, re := range table.RowEvents {
|
||||
var found bool
|
||||
for i, key := range newKeys {
|
||||
if key == re.Row.ID {
|
||||
found = true
|
||||
newKeys = append(newKeys[:i], newKeys[i+1:]...)
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
table.RowEvents = table.RowEvents.Delete(re.Row.ID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,213 @@
|
|||
package dao
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
|
||||
"github.com/derailed/k9s/internal/watch"
|
||||
"github.com/rs/zerolog/log"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/labels"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
)
|
||||
|
||||
// MetaViewers represents a collection of meta viewers.
|
||||
type ResourceMetas map[GVR]metav1.APIResource
|
||||
|
||||
var resMetas ResourceMetas
|
||||
|
||||
func AccessorFor(f Factory, gvr GVR) (Accessor, error) {
|
||||
m := map[GVR]Accessor{
|
||||
"contexts": &Context{},
|
||||
"screendumps": &ScreenDump{},
|
||||
"apps/v1/deployments": &Deployment{},
|
||||
"apps/v1/daemonsets": &DaemonSet{},
|
||||
"extensions/v1beta1/daemonsets": &DaemonSet{},
|
||||
"apps/v1/statefulsets": &StatefulSet{},
|
||||
}
|
||||
|
||||
r, ok := m[gvr]
|
||||
if !ok {
|
||||
r = &Resource{}
|
||||
log.Warn().Msgf("No DAO registry entry for %q. Going generic!", gvr)
|
||||
}
|
||||
r.Init(f, gvr)
|
||||
|
||||
return r, nil
|
||||
}
|
||||
|
||||
func AllGVRs() []GVR {
|
||||
kk := make(GVRs, 0, len(resMetas))
|
||||
for k := range resMetas {
|
||||
kk = append(kk, k)
|
||||
}
|
||||
sort.Sort(kk)
|
||||
|
||||
return kk
|
||||
}
|
||||
|
||||
// MetaFor returns a resource metadata for a given gvr.
|
||||
func MetaFor(gvr GVR) (metav1.APIResource, error) {
|
||||
m, ok := resMetas[gvr]
|
||||
if !ok {
|
||||
return metav1.APIResource{}, fmt.Errorf("no resource meta defined for %q", gvr)
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
// Load hydrates server preferred+CRDs resource metadata.
|
||||
func Load(f *watch.Factory) error {
|
||||
resMetas = make(ResourceMetas, 100)
|
||||
if err := loadPreferred(f, resMetas); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := loadNonResource(resMetas); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return loadCRDs(f, resMetas)
|
||||
}
|
||||
|
||||
func loadNonResource(m ResourceMetas) error {
|
||||
m["contexts"] = metav1.APIResource{
|
||||
Name: "contexts",
|
||||
SingularName: "context",
|
||||
Namespaced: false,
|
||||
Kind: "Context",
|
||||
ShortNames: []string{"ctx"},
|
||||
Verbs: []string{},
|
||||
Categories: []string{"K9s"},
|
||||
}
|
||||
m["screendumps"] = metav1.APIResource{
|
||||
Name: "screendumps",
|
||||
SingularName: "screendump",
|
||||
Namespaced: false,
|
||||
Kind: "ScreenDump",
|
||||
ShortNames: []string{"sd"},
|
||||
Verbs: []string{"delete"},
|
||||
Categories: []string{"K9s"},
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func loadPreferred(f *watch.Factory, m ResourceMetas) error {
|
||||
discovery, err := f.Client().CachedDiscovery()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
rr, err := discovery.ServerPreferredResources()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, r := range rr {
|
||||
for _, res := range r.APIResources {
|
||||
gvr := FromGVAndR(r.GroupVersion, res.Name)
|
||||
res.Group, res.Version = gvr.ToG(), gvr.ToV()
|
||||
m[gvr] = res
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func loadCRDs(f *watch.Factory, m ResourceMetas) error {
|
||||
oo, err := f.List("", "apiextensions.k8s.io/v1beta1/customresourcedefinitions", labels.Everything())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
f.WaitForCacheSync()
|
||||
|
||||
for _, o := range oo {
|
||||
meta, errs := extractMeta(o)
|
||||
if len(errs) > 0 {
|
||||
log.Error().Err(errs[0]).Msgf("Fail to extract CRD meta (%d) errors", len(errs))
|
||||
continue
|
||||
}
|
||||
gvr := NewGVR(meta.Group, meta.Version, meta.Name)
|
||||
m[gvr] = meta
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func extractMeta(o runtime.Object) (metav1.APIResource, []error) {
|
||||
var (
|
||||
m metav1.APIResource
|
||||
errs []error
|
||||
)
|
||||
|
||||
crd, ok := o.(*unstructured.Unstructured)
|
||||
if !ok {
|
||||
return m, append(errs, fmt.Errorf("Expected CustomResourceDefinition, but got %T", o))
|
||||
}
|
||||
|
||||
var spec map[string]interface{}
|
||||
spec, errs = extractMap(crd.Object, "spec", errs)
|
||||
|
||||
var meta map[string]interface{}
|
||||
meta, errs = extractMap(crd.Object, "metadata", errs)
|
||||
|
||||
m.Name, errs = extractStr(meta, "name", errs)
|
||||
m.Group, errs = extractStr(spec, "group", errs)
|
||||
m.Version, errs = extractStr(spec, "version", errs)
|
||||
|
||||
var scope string
|
||||
scope, errs = extractStr(spec, "scope", errs)
|
||||
|
||||
m.Namespaced = isNamespaced(scope)
|
||||
|
||||
var names map[string]interface{}
|
||||
names, errs = extractMap(spec, "names", errs)
|
||||
m.Kind, errs = extractStr(names, "kind", errs)
|
||||
m.SingularName, errs = extractStr(names, "singular", errs)
|
||||
m.Name, errs = extractStr(names, "plural", errs)
|
||||
m.ShortNames, errs = extractSlice(names, "shortNames", errs)
|
||||
|
||||
return m, errs
|
||||
}
|
||||
|
||||
func isNamespaced(scope string) bool {
|
||||
return scope == "Namespaced"
|
||||
}
|
||||
|
||||
func extractSlice(m map[string]interface{}, n string, errs []error) ([]string, []error) {
|
||||
if m[n] == nil {
|
||||
return nil, errs
|
||||
}
|
||||
s, ok := m[n].([]string)
|
||||
if ok {
|
||||
return s, errs
|
||||
}
|
||||
|
||||
ii, ok := m[n].([]interface{})
|
||||
if !ok {
|
||||
return s, append(errs, fmt.Errorf("failed to extract slice %s -- %#v", n, m))
|
||||
}
|
||||
|
||||
ss := make([]string, len(ii))
|
||||
for i, name := range ii {
|
||||
ss[i], ok = name.(string)
|
||||
if !ok {
|
||||
return s, append(errs, fmt.Errorf("expecting string shortnames"))
|
||||
}
|
||||
}
|
||||
return s, errs
|
||||
}
|
||||
|
||||
func extractStr(m map[string]interface{}, n string, errs []error) (string, []error) {
|
||||
s, ok := m[n].(string)
|
||||
if !ok {
|
||||
return s, append(errs, fmt.Errorf("failed to extract string %s", n))
|
||||
}
|
||||
return s, errs
|
||||
}
|
||||
|
||||
func extractMap(m map[string]interface{}, n string, errs []error) (map[string]interface{}, []error) {
|
||||
v, ok := m[n].(map[string]interface{})
|
||||
if !ok {
|
||||
return v, append(errs, fmt.Errorf("failed to extract field %s", n))
|
||||
}
|
||||
return v, errs
|
||||
}
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
package dao
|
||||
|
||||
import (
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/client-go/dynamic"
|
||||
)
|
||||
|
||||
type Resource struct {
|
||||
Factory
|
||||
|
||||
gvr GVR
|
||||
}
|
||||
|
||||
func (r *Resource) Init(f Factory, gvr GVR) {
|
||||
r.Factory, r.gvr = f, gvr
|
||||
}
|
||||
|
||||
// Delete a Generic.
|
||||
func (r *Resource) Delete(ns, n string, cascade, force bool) error {
|
||||
p := metav1.DeletePropagationOrphan
|
||||
if cascade {
|
||||
p = metav1.DeletePropagationBackground
|
||||
}
|
||||
|
||||
return r.dynClient().Namespace(ns).Delete(n, &metav1.DeleteOptions{
|
||||
PropagationPolicy: &p,
|
||||
})
|
||||
}
|
||||
|
||||
func (r *Resource) dynClient() dynamic.NamespaceableResourceInterface {
|
||||
return r.Client().DynDialOrDie().Resource(r.gvr.AsGVR())
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
package dao
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
type ScreenDump struct {
|
||||
Resource
|
||||
}
|
||||
|
||||
var _ Accessor = &ScreenDump{}
|
||||
var _ Nuker = &ScreenDump{}
|
||||
|
||||
// Delete a ScreenDump.
|
||||
func (d *ScreenDump) Delete(dir, sel string, cascade, force bool) error {
|
||||
log.Debug().Msgf("ScreenDump DELETE %q:%q", dir, sel)
|
||||
return os.Remove(filepath.Join("/"+dir, sel))
|
||||
}
|
||||
|
|
@ -0,0 +1,80 @@
|
|||
package dao
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
appsv1 "k8s.io/api/apps/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/labels"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"k8s.io/kubectl/pkg/polymorphichelpers"
|
||||
)
|
||||
|
||||
type StatefulSet struct {
|
||||
Resource
|
||||
}
|
||||
|
||||
var _ Accessor = &StatefulSet{}
|
||||
var _ Loggable = &StatefulSet{}
|
||||
var _ Restartable = &StatefulSet{}
|
||||
var _ Scalable = &StatefulSet{}
|
||||
|
||||
// Scale a StatefulSet.
|
||||
func (s *StatefulSet) Scale(ns, n string, replicas int32) error {
|
||||
scale, err := s.Client().DialOrDie().AppsV1().StatefulSets(ns).GetScale(n, metav1.GetOptions{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
scale.Spec.Replicas = replicas
|
||||
_, err = s.Client().DialOrDie().AppsV1().StatefulSets(ns).UpdateScale(n, scale)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// Restart a StatefulSet rollout.
|
||||
func (s *StatefulSet) Restart(ns, n string) error {
|
||||
o, err := s.Get(ns, string(s.gvr), n, labels.Everything())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var ds appsv1.StatefulSet
|
||||
err = runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &ds)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
update, err := polymorphichelpers.ObjectRestarterFn(&ds)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = s.Client().DialOrDie().AppsV1().StatefulSets(ns).Patch(ds.Name, types.StrategicMergePatchType, update)
|
||||
return err
|
||||
}
|
||||
|
||||
// Logs tail logs for all pods represented by this StatefulSet.
|
||||
func (s *StatefulSet) TailLogs(ctx context.Context, c chan<- string, opts LogOptions) error {
|
||||
log.Debug().Msgf("Tailing StatefulSet %q -- %q", opts.Namespace, opts.Name)
|
||||
o, err := s.Get(opts.Namespace, string(s.gvr), opts.Name, labels.Everything())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var dp appsv1.StatefulSet
|
||||
err = runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &dp)
|
||||
if err != nil {
|
||||
return errors.New("expecting StatefulSet resource")
|
||||
}
|
||||
|
||||
if dp.Spec.Selector == nil || len(dp.Spec.Selector.MatchLabels) == 0 {
|
||||
return fmt.Errorf("No valid selector found on StatefulSet %s", opts.FQN())
|
||||
}
|
||||
|
||||
return podLogs(ctx, c, dp.Spec.Selector.MatchLabels, opts)
|
||||
}
|
||||
|
|
@ -0,0 +1,64 @@
|
|||
package dao
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/derailed/k9s/internal/k8s"
|
||||
"k8s.io/apimachinery/pkg/labels"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/client-go/informers"
|
||||
)
|
||||
|
||||
type Factory interface {
|
||||
// Client retrieves an api client.
|
||||
Client() k8s.Connection
|
||||
|
||||
// Get fetch a given resource.
|
||||
Get(ns, gvr, n string, sel labels.Selector) (runtime.Object, error)
|
||||
|
||||
// List fetch a collection of resources.
|
||||
List(ns, gvr string, sel labels.Selector) ([]runtime.Object, error)
|
||||
|
||||
// ForResource fetch an informer for a given resource.
|
||||
ForResource(ns, gvr string) informers.GenericInformer
|
||||
|
||||
// WaitForCacheSync synchronize the cache.
|
||||
WaitForCacheSync() map[schema.GroupVersionResource]bool
|
||||
}
|
||||
|
||||
// Accessor represents an accessible k8s resource.
|
||||
type Accessor interface {
|
||||
Nuker
|
||||
|
||||
// Init the resource with a factory object.
|
||||
Init(Factory, GVR)
|
||||
}
|
||||
|
||||
// Loggable represents resources with logs.
|
||||
type Loggable interface {
|
||||
// TaiLogs streams resource logs.
|
||||
TailLogs(ctx context.Context, c chan<- string, opts LogOptions) error
|
||||
}
|
||||
|
||||
type Scalable interface {
|
||||
Scale(ns, n string, replicas int32) error
|
||||
}
|
||||
|
||||
// Nuker represents a resource deleter.
|
||||
type Nuker interface {
|
||||
// Delete removes a resource from the api server.
|
||||
Delete(ns, n string, cascade, force bool) error
|
||||
}
|
||||
|
||||
// Switchable represents a switchable resource.
|
||||
type Switchable interface {
|
||||
// Switch changes the active context.
|
||||
Switch(ctx string) error
|
||||
}
|
||||
|
||||
// Restartable represents a restartable resource.
|
||||
type Restartable interface {
|
||||
// Restart performs a rollout restart.
|
||||
Restart(ns, n string) error
|
||||
}
|
||||
|
|
@ -6,8 +6,6 @@ import (
|
|||
"sync"
|
||||
"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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
// }
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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{})
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -0,0 +1,14 @@
|
|||
package internal
|
||||
|
||||
// ContextKey represents context key.
|
||||
type ContextKey string
|
||||
|
||||
const (
|
||||
// Factory represents a factory context key.
|
||||
KeyFactory ContextKey = "factory"
|
||||
KeySelection = "selection"
|
||||
KeyLabels = "labels"
|
||||
KeyFields = "fields"
|
||||
KeyTable = "table"
|
||||
KeyDir = "dir"
|
||||
)
|
||||
|
|
@ -1,6 +1,9 @@
|
|||
package model
|
||||
|
||||
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
|
||||
}
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
package model
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/derailed/k9s/internal/render"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
)
|
||||
|
||||
// Context represents a kube context model.
|
||||
type Context struct {
|
||||
Resource
|
||||
}
|
||||
|
||||
// List returns a collection of node resources.
|
||||
func (c *Context) List(_ context.Context) ([]runtime.Object, error) {
|
||||
cfg := c.factory.Client().Config()
|
||||
ctxs, err := cfg.Contexts()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cc := make([]runtime.Object, 0, len(ctxs))
|
||||
for name, ctx := range ctxs {
|
||||
cc = append(cc, render.NewNamedContext(cfg, name, ctx))
|
||||
}
|
||||
|
||||
return cc, nil
|
||||
}
|
||||
|
||||
// Hydrate returns nodes as rows.
|
||||
func (n *Context) Hydrate(oo []runtime.Object, rr render.Rows, re Renderer) error {
|
||||
var index int
|
||||
for _, o := range oo {
|
||||
ctx, ok := o.(*render.NamedContext)
|
||||
if !ok {
|
||||
return fmt.Errorf("expecting named context but got %T", o)
|
||||
}
|
||||
|
||||
var row render.Row
|
||||
if err := re.Render(ctx, "", &row); err != nil {
|
||||
return err
|
||||
}
|
||||
rr[index] = row
|
||||
index++
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
@ -0,0 +1,130 @@
|
|||
package model
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/derailed/k9s/internal/k8s"
|
||||
"github.com/derailed/k9s/internal/render"
|
||||
"github.com/rs/zerolog/log"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
metav1beta1 "k8s.io/apimachinery/pkg/apis/meta/v1beta1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apimachinery/pkg/runtime/serializer"
|
||||
"k8s.io/client-go/rest"
|
||||
)
|
||||
|
||||
const gvFmt = "application/json;as=Table;v=%s;g=%s, application/json"
|
||||
|
||||
// Generic represents a generic model.
|
||||
type Generic struct {
|
||||
Resource
|
||||
|
||||
table *metav1beta1.Table
|
||||
}
|
||||
|
||||
// List returns a collection of node resources.
|
||||
func (g *Generic) List(ctx context.Context) ([]runtime.Object, error) {
|
||||
// Ensures the factory is tracking this resource
|
||||
_ = g.factory.ForResource(g.namespace, g.gvr)
|
||||
|
||||
gvr := k8s.GVR(g.gvr)
|
||||
fcodec, codec := g.codec(gvr.AsGV())
|
||||
|
||||
c, err := g.client(fcodec, gvr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// BOZO!! Need to know if gvr is namespaced or not
|
||||
o, err := c.Get().
|
||||
SetHeader("Accept", fmt.Sprintf(gvFmt, metav1beta1.SchemeGroupVersion.Version, metav1beta1.GroupName)).
|
||||
// Namespace(g.namespace).
|
||||
Resource(gvr.ToR()).
|
||||
VersionedParams(&metav1beta1.TableOptions{}, codec).
|
||||
Do().Get()
|
||||
|
||||
table, ok := o.(*metav1beta1.Table)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("invalid table found on generic %s -- %T", g.gvr, o)
|
||||
}
|
||||
g.table = table
|
||||
res := make([]runtime.Object, len(g.table.Rows))
|
||||
for i := range g.table.Rows {
|
||||
res[i] = RowRes{&g.table.Rows[i]}
|
||||
}
|
||||
|
||||
log.Debug().Msgf("!!!!GENERIC lister returns %d", len(res))
|
||||
return res, err
|
||||
}
|
||||
|
||||
// Hydrate returns nodes as rows.
|
||||
func (g *Generic) Hydrate(oo []runtime.Object, rr render.Rows, re Renderer) error {
|
||||
gr, ok := re.(*render.Generic)
|
||||
if !ok {
|
||||
return fmt.Errorf("expecting generic renderer for %s but got %T", g.gvr, re)
|
||||
}
|
||||
gr.SetTable(g.table)
|
||||
for i, o := range oo {
|
||||
res, ok := o.(RowRes)
|
||||
if !ok {
|
||||
return fmt.Errorf("expecting RowRes but got %#v", o)
|
||||
}
|
||||
count := len(res.Cells)
|
||||
if g.namespace == "" {
|
||||
count++
|
||||
}
|
||||
if err := gr.Render(res.TableRow, g.namespace, &rr[i]); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Helpers...
|
||||
|
||||
func (g *Generic) client(codec serializer.CodecFactory, gvr k8s.GVR) (*rest.RESTClient, error) {
|
||||
crConfig := g.factory.Client().RestConfigOrDie()
|
||||
gv := gvr.AsGV()
|
||||
crConfig.GroupVersion = &gv
|
||||
crConfig.APIPath = "/apis"
|
||||
if len(gvr.ToG()) == 0 {
|
||||
crConfig.APIPath = "/api"
|
||||
}
|
||||
crConfig.NegotiatedSerializer = codec.WithoutConversion()
|
||||
|
||||
crRestClient, err := rest.RESTClientFor(crConfig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return crRestClient, nil
|
||||
}
|
||||
|
||||
func (r *Resource) codec(gv schema.GroupVersion) (serializer.CodecFactory, runtime.ParameterCodec) {
|
||||
scheme := runtime.NewScheme()
|
||||
metav1.AddToGroupVersion(scheme, gv)
|
||||
scheme.AddKnownTypes(gv, &metav1beta1.Table{}, &metav1beta1.TableOptions{})
|
||||
scheme.AddKnownTypes(metav1beta1.SchemeGroupVersion, &metav1beta1.Table{}, &metav1beta1.TableOptions{})
|
||||
|
||||
return serializer.NewCodecFactory(scheme), runtime.NewParameterCodec(scheme)
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
// RowRes represents a table row.
|
||||
type RowRes struct {
|
||||
*metav1beta1.TableRow
|
||||
}
|
||||
|
||||
// GetObjectKind returns a schema object.
|
||||
func (r RowRes) GetObjectKind() schema.ObjectKind {
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeepCopyObject returns a container copy.
|
||||
func (r RowRes) DeepCopyObject() runtime.Object {
|
||||
return r
|
||||
}
|
||||
|
|
@ -1,9 +1,10 @@
|
|||
package model
|
||||
|
||||
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
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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{},
|
||||
},
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,81 @@
|
|||
package model
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
|
||||
"github.com/derailed/k9s/internal"
|
||||
"github.com/derailed/k9s/internal/render"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
)
|
||||
|
||||
// ScreenDump represents a container model.
|
||||
type ScreenDump struct {
|
||||
Resource
|
||||
|
||||
pod *v1.Pod
|
||||
}
|
||||
|
||||
// List returns a collection of containers
|
||||
func (c *ScreenDump) List(ctx context.Context) ([]runtime.Object, error) {
|
||||
dir, ok := ctx.Value(internal.KeyDir).(string)
|
||||
if !ok {
|
||||
return nil, errors.New("no screendump dir found in context")
|
||||
}
|
||||
|
||||
ff, err := ioutil.ReadDir(dir)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
oo := make([]runtime.Object, len(ff))
|
||||
for i, f := range ff {
|
||||
oo[i] = FileRes{file: f, dir: dir}
|
||||
}
|
||||
|
||||
return oo, nil
|
||||
}
|
||||
|
||||
// Hydrate returns a pod as container rows.
|
||||
func (c *ScreenDump) Hydrate(oo []runtime.Object, rr render.Rows, re Renderer) error {
|
||||
for i, o := range oo {
|
||||
res, ok := o.(FileRes)
|
||||
if !ok {
|
||||
return fmt.Errorf("expecting a file resource but got %T", o)
|
||||
}
|
||||
|
||||
if err := re.Render(res, render.NonResource, &rr[i]); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
// FileRes represents a file resource.
|
||||
type FileRes struct {
|
||||
file os.FileInfo
|
||||
dir string
|
||||
}
|
||||
|
||||
func (c FileRes) GetFile() os.FileInfo { return c.file }
|
||||
func (c FileRes) GetDir() string { return c.dir }
|
||||
|
||||
// GetObjectKind returns a schema object.
|
||||
func (c FileRes) GetObjectKind() schema.ObjectKind {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeepCopyObject returns a container copy.
|
||||
func (c FileRes) DeepCopyObject() runtime.Object {
|
||||
|
||||
return c
|
||||
}
|
||||
|
|
@ -3,7 +3,13 @@ package model
|
|||
import (
|
||||
"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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,64 @@
|
|||
package render
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/derailed/k9s/internal/k8s"
|
||||
"github.com/gdamore/tcell"
|
||||
)
|
||||
|
||||
// Alias renders a aliases to screen.
|
||||
type Alias struct{}
|
||||
|
||||
// ColorerFunc colors a resource row.
|
||||
func (Alias) ColorerFunc() ColorerFunc {
|
||||
return func(ns string, re RowEvent) tcell.Color {
|
||||
return tcell.ColorMediumSpringGreen
|
||||
}
|
||||
}
|
||||
|
||||
// Header returns a header row.
|
||||
func (Alias) Header(ns string) HeaderRow {
|
||||
return HeaderRow{
|
||||
Header{Name: "RESOURCE"},
|
||||
Header{Name: "COMMAND"},
|
||||
Header{Name: "APIGROUP"},
|
||||
}
|
||||
}
|
||||
|
||||
// Render renders a K8s resource to screen.
|
||||
func (Alias) Render(o interface{}, gvr string, r *Row) error {
|
||||
aliases, ok := o.([]string)
|
||||
if !ok {
|
||||
return fmt.Errorf("Expected Alias, but got %T", o)
|
||||
}
|
||||
|
||||
g := k8s.GVR(gvr)
|
||||
r.ID = string(gvr)
|
||||
r.Fields = Fields{
|
||||
g.ToR(),
|
||||
strings.Join(aliases, ","),
|
||||
g.ToG(),
|
||||
// Pad(g.ToR(), 30),
|
||||
// Pad(strings.Join(aliases, ","), 70),
|
||||
// Pad(g.ToG(), 30),
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Helpers...
|
||||
|
||||
// Pad a string up to the given length or truncates if greater than length.
|
||||
func Pad(s string, width int) string {
|
||||
if len(s) == width {
|
||||
return s
|
||||
}
|
||||
|
||||
if len(s) > width {
|
||||
return Truncate(s, width)
|
||||
}
|
||||
|
||||
return s + strings.Repeat(" ", width-len(s))
|
||||
}
|
||||
|
|
@ -0,0 +1,158 @@
|
|||
package render
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/derailed/tview"
|
||||
"github.com/gdamore/tcell"
|
||||
"golang.org/x/text/language"
|
||||
"golang.org/x/text/message"
|
||||
)
|
||||
|
||||
var (
|
||||
totalRx = regexp.MustCompile(`Total:\s+([0-9.]+)\ssecs`)
|
||||
reqRx = regexp.MustCompile(`Requests/sec:\s+([0-9.]+)`)
|
||||
okRx = regexp.MustCompile(`\[2\d{2}\]\s+(\d+)\s+responses`)
|
||||
errRx = regexp.MustCompile(`\[[4-5]\d{2}\]\s+(\d+)\s+responses`)
|
||||
toastRx = regexp.MustCompile(`Error distribution`)
|
||||
)
|
||||
|
||||
// BenchInfo represents benchmark run info.
|
||||
type BenchInfo struct {
|
||||
File os.FileInfo
|
||||
Path string
|
||||
}
|
||||
|
||||
// Bench renders a benchmarks to screen.
|
||||
type Bench struct{}
|
||||
|
||||
// ColorerFunc colors a resource row.
|
||||
func (Bench) ColorerFunc() ColorerFunc {
|
||||
return func(ns string, re RowEvent) tcell.Color {
|
||||
c := tcell.ColorPaleGreen
|
||||
statusCol := 2
|
||||
if strings.TrimSpace(re.Row.Fields[statusCol]) != "pass" {
|
||||
c = ErrColor
|
||||
}
|
||||
return c
|
||||
}
|
||||
}
|
||||
|
||||
// Header returns a header row.
|
||||
func (Bench) Header(ns string) HeaderRow {
|
||||
return HeaderRow{
|
||||
Header{Name: "NAMESPACE", Align: tview.AlignLeft},
|
||||
Header{Name: "NAME", Align: tview.AlignLeft},
|
||||
Header{Name: "STATUS", Align: tview.AlignLeft},
|
||||
Header{Name: "TIME", Align: tview.AlignLeft},
|
||||
Header{Name: "REQ/S", Align: tview.AlignRight},
|
||||
Header{Name: "2XX", Align: tview.AlignRight},
|
||||
Header{Name: "4XX/5XX", Align: tview.AlignRight},
|
||||
Header{Name: "REPORT", Align: tview.AlignLeft},
|
||||
Header{Name: "AGE", Align: tview.AlignLeft},
|
||||
}
|
||||
}
|
||||
|
||||
// Render renders a K8s resource to screen.
|
||||
func (b Bench) Render(o interface{}, ns string, r *Row) error {
|
||||
bench, ok := o.(BenchInfo)
|
||||
if !ok {
|
||||
return fmt.Errorf("Expected string, but got %T", o)
|
||||
}
|
||||
|
||||
data, err := b.readFile(bench.Path)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Unable to load bench file %s", bench.Path)
|
||||
}
|
||||
|
||||
r.Fields = make(Fields, len(b.Header(ns)))
|
||||
if err := b.initRow(r.Fields, bench.File); err != nil {
|
||||
return err
|
||||
}
|
||||
b.augmentRow(r.Fields, data)
|
||||
r.ID = bench.Path
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Helpers...
|
||||
|
||||
func (Bench) readFile(file string) (string, error) {
|
||||
data, err := ioutil.ReadFile(file)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(data), nil
|
||||
}
|
||||
|
||||
func (Bench) initRow(row Fields, f os.FileInfo) error {
|
||||
tokens := strings.Split(f.Name(), "_")
|
||||
if len(tokens) < 2 {
|
||||
return fmt.Errorf("Invalid file name %s", f.Name())
|
||||
}
|
||||
row[0] = tokens[0]
|
||||
row[1] = tokens[1]
|
||||
row[7] = f.Name()
|
||||
row[8] = time.Since(f.ModTime()).String()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b Bench) augmentRow(fields Fields, data string) {
|
||||
if len(data) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
col := 2
|
||||
fields[col] = "pass"
|
||||
mf := toastRx.FindAllStringSubmatch(data, 1)
|
||||
if len(mf) > 0 {
|
||||
fields[col] = "fail"
|
||||
}
|
||||
col++
|
||||
|
||||
mt := totalRx.FindAllStringSubmatch(data, 1)
|
||||
if len(mt) > 0 {
|
||||
fields[col] = mt[0][1]
|
||||
}
|
||||
col++
|
||||
|
||||
mr := reqRx.FindAllStringSubmatch(data, 1)
|
||||
if len(mr) > 0 {
|
||||
fields[col] = mr[0][1]
|
||||
}
|
||||
col++
|
||||
|
||||
ms := okRx.FindAllStringSubmatch(data, -1)
|
||||
fields[col] = b.countReq(ms)
|
||||
col++
|
||||
|
||||
me := errRx.FindAllStringSubmatch(data, -1)
|
||||
fields[col] = b.countReq(me)
|
||||
}
|
||||
|
||||
func (Bench) countReq(rr [][]string) string {
|
||||
if len(rr) == 0 {
|
||||
return "0"
|
||||
}
|
||||
|
||||
var sum int
|
||||
for _, m := range rr {
|
||||
if m, err := strconv.Atoi(string(m[1])); err == nil {
|
||||
sum += m
|
||||
}
|
||||
}
|
||||
return asNum(sum)
|
||||
}
|
||||
|
||||
// AsNumb prints a number with thousand separator.
|
||||
func asNum(n int) string {
|
||||
p := message.NewPrinter(language.English)
|
||||
return p.Sprintf("%d", n)
|
||||
}
|
||||
|
|
@ -30,7 +30,7 @@ func (CronJob) Header(ns string) HeaderRow {
|
|||
Header{Name: "SUSPEND"},
|
||||
Header{Name: "ACTIVE"},
|
||||
Header{Name: "LAST_SCHEDULE"},
|
||||
Header{Name: "AGE"},
|
||||
Header{Name: "AGE", Decorator: ageDecorator},
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ func (ConfigMap) Header(ns string) HeaderRow {
|
|||
return append(h,
|
||||
Header{Name: "NAME"},
|
||||
Header{Name: "DATA", Align: tview.AlignRight},
|
||||
Header{Name: "AGE"},
|
||||
Header{Name: "AGE", Decorator: ageDecorator},
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,280 @@
|
|||
package render
|
||||
|
||||
// BOZO!!
|
||||
// type (
|
||||
// colorerUC struct {
|
||||
// ns string
|
||||
// r RowEvent
|
||||
// e tcell.Color
|
||||
// }
|
||||
// colorerUCs []colorerUC
|
||||
// )
|
||||
|
||||
// func TestNSColorer(t *testing.T) {
|
||||
// var (
|
||||
// ns = Row{Fields: Fields{"blee", "Active"}}
|
||||
// term = Row{Fields: Fields{"blee", Terminating}}
|
||||
// dead = Row{Fields: Fields{"blee", "Inactive"}}
|
||||
// )
|
||||
|
||||
// uu := colorerUCs{
|
||||
// // Add AllNS
|
||||
// {"", RowEvent{
|
||||
// Kind: EventAdd,
|
||||
// Row: ns,
|
||||
// },
|
||||
// AddColor},
|
||||
// // Mod AllNS
|
||||
// {"", RowEvent{Kind: EventUpdate, Row: ns}, ModColor},
|
||||
// // MoChange AllNS
|
||||
// {"", RowEvent{Kind: EventUnchanged, Row: ns}, StdColor},
|
||||
// // Bust NS
|
||||
// {"", RowEvent{Kind: EventUnchanged, Row: term}, ErrColor},
|
||||
// // Bust NS
|
||||
// {"", RowEvent{Kind: EventUnchanged, Row: dead}, ErrColor},
|
||||
// }
|
||||
// for _, u := range uu {
|
||||
// assert.Equal(t, u.e, nsColorer(u.ns, u.r))
|
||||
// }
|
||||
// }
|
||||
|
||||
// func TestEvColorer(t *testing.T) {
|
||||
// var (
|
||||
// ns = Row{Fields: Fields{"", "blee", "fred", "Normal"}}
|
||||
// nonNS = Row{Fields: Fields{"", "fred", "Normal"}}
|
||||
// failNS = Row{Fields: Fields{"", "blee", "fred", "Failed"}}
|
||||
// failNoNS = Row{Fields: Fields{"", "fred", "Failed"}}
|
||||
// killNS = Row{Fields: Fields{"", "blee", "fred", "Killing"}}
|
||||
// killNoNS = Row{Fields: Fields{"", "fred", "Killing"}}
|
||||
// )
|
||||
|
||||
// uu := colorerUCs{
|
||||
// // Add AllNS
|
||||
// {"", RowEvent{Kind: EventAdd, Row: ns}, AddColor},
|
||||
// // Add NS
|
||||
// {"blee", RowEvent{Kind: EventAdd, Row: nonNS}, AddColor},
|
||||
// // Mod AllNS
|
||||
// {"", RowEvent{Kind: EventUpdate, Row: ns}, ModColor},
|
||||
// // Mod NS
|
||||
// {"blee", RowEvent{Kind: EventUpdate, Row: nonNS}, ModColor},
|
||||
// // Bust AllNS
|
||||
// {"", RowEvent{Kind: EventUnchanged, Row: failNS}, ErrColor},
|
||||
// // Bust NS
|
||||
// {"blee", RowEvent{Kind: EventUnchanged, Row: failNoNS}, ErrColor},
|
||||
// // Bust AllNS
|
||||
// {"", RowEvent{Kind: EventUnchanged, Row: killNS}, KillColor},
|
||||
// // Bust NS
|
||||
// {"blee", RowEvent{Kind: EventUnchanged, Row: killNoNS}, KillColor},
|
||||
// }
|
||||
// for _, u := range uu {
|
||||
// assert.Equal(t, u.e, evColorer(u.ns, u.r))
|
||||
// }
|
||||
// }
|
||||
|
||||
// func TestRSColorer(t *testing.T) {
|
||||
// var (
|
||||
// ns = Row{Fields: Fields{"blee", "fred", "1", "1"}}
|
||||
// noNs = Row{Fields: Fields{"fred", "1", "1"}}
|
||||
// bustNS = Row{Fields: Fields{"blee", "fred", "1", "0"}}
|
||||
// bustNoNS = Row{Fields: Fields{"fred", "1", "0"}}
|
||||
// )
|
||||
|
||||
// uu := colorerUCs{
|
||||
// // Add AllNS
|
||||
// {"", RowEvent{Kind: EventAdd, Row: ns}, AddColor},
|
||||
// // Add NS
|
||||
// {"blee", RowEvent{Kind: EventAdd, Row: noNs}, AddColor},
|
||||
// // Bust AllNS
|
||||
// {"", RowEvent{Kind: EventUnchanged, Row: bustNS}, ErrColor},
|
||||
// // Bust NS
|
||||
// {"blee", RowEvent{Kind: EventUnchanged, Row: bustNoNS}, ErrColor},
|
||||
// // Nochange AllNS
|
||||
// {"", RowEvent{Kind: EventUnchanged, Row: ns}, StdColor},
|
||||
// // Nochange NS
|
||||
// {"blee", RowEvent{Kind: EventUnchanged, Row: noNs}, StdColor},
|
||||
// }
|
||||
// for _, u := range uu {
|
||||
// assert.Equal(t, u.e, rsColorer(u.ns, u.r))
|
||||
// }
|
||||
// }
|
||||
|
||||
// func TestStsColorer(t *testing.T) {
|
||||
// var (
|
||||
// ns = Row{Fields: Fields{"blee", "fred", "1", "1"}}
|
||||
// nonNS = Row{Fields: Fields{"fred", "1", "1"}}
|
||||
// bustNS = Row{Fields: Fields{"blee", "fred", "2", "1"}}
|
||||
// bustNoNS = Row{Fields: Fields{"fred", "2", "1"}}
|
||||
// )
|
||||
|
||||
// uu := colorerUCs{
|
||||
// // Add AllNS
|
||||
// {"", RowEvent{Kind: EventAdd, Row: ns}, AddColor},
|
||||
// // Add NS
|
||||
// {"blee", RowEvent{Kind: EventAdd, Row: nonNS}, AddColor},
|
||||
// // Mod AllNS
|
||||
// {"", RowEvent{Kind: EventUpdate, Row: ns}, ModColor},
|
||||
// // Mod NS
|
||||
// {"blee", RowEvent{Kind: EventUpdate, Row: nonNS}, ModColor},
|
||||
// // Bust AllNS
|
||||
// {"", RowEvent{Kind: EventUnchanged, Row: bustNS}, ErrColor},
|
||||
// // Bust NS
|
||||
// {"blee", RowEvent{Kind: EventUnchanged, Row: bustNoNS}, ErrColor},
|
||||
// // Unchanged cool AllNS
|
||||
// {"", RowEvent{Kind: EventUnchanged, Row: ns}, StdColor},
|
||||
// }
|
||||
// for _, u := range uu {
|
||||
// assert.Equal(t, u.e, stsColorer(u.ns, u.r))
|
||||
// }
|
||||
// }
|
||||
|
||||
// func TestDpColorer(t *testing.T) {
|
||||
// var (
|
||||
// ns = Row{Fields: Fields{"blee", "fred", "1", "1"}}
|
||||
// nonNS = Row{Fields: Fields{"fred", "1", "1"}}
|
||||
// bustNS = Row{Fields: Fields{"blee", "fred", "2", "1"}}
|
||||
// bustNoNS = Row{Fields: Fields{"fred", "2", "1"}}
|
||||
// )
|
||||
|
||||
// uu := colorerUCs{
|
||||
// // Add AllNS
|
||||
// {"", RowEvent{Kind: EventAdd, Row: ns}, AddColor},
|
||||
// // Add NS
|
||||
// {"blee", RowEvent{Kind: EventAdd, Row: nonNS}, AddColor},
|
||||
// // Mod AllNS
|
||||
// {"", RowEvent{Kind: EventUpdate, Row: ns}, ModColor},
|
||||
// // Mod NS
|
||||
// {"blee", RowEvent{Kind: EventUpdate, Row: nonNS}, ModColor},
|
||||
// // Unchanged cool
|
||||
// {"", RowEvent{Kind: EventUnchanged, Row: ns}, StdColor},
|
||||
// // Bust AllNS
|
||||
// {"", RowEvent{Kind: EventUnchanged, Row: bustNS}, ErrColor},
|
||||
// // Bust NS
|
||||
// {"blee", RowEvent{Kind: EventUnchanged, Row: bustNoNS}, ErrColor},
|
||||
// }
|
||||
// for _, u := range uu {
|
||||
// assert.Equal(t, u.e, dpColorer(u.ns, u.r))
|
||||
// }
|
||||
// }
|
||||
|
||||
// func TestPdbColorer(t *testing.T) {
|
||||
// var (
|
||||
// ns = Row{Fields: Fields{"blee", "fred", "1", "1", "1", "1", "1"}}
|
||||
// nonNS = Row{Fields: Fields{"fred", "1", "1", "1", "1", "1"}}
|
||||
// bustNS = Row{Fields: Fields{"blee", "fred", "1", "1", "1", "1", "2"}}
|
||||
// bustNoNS = Row{Fields: Fields{"fred", "1", "1", "1", "1", "2"}}
|
||||
// )
|
||||
|
||||
// uu := colorerUCs{
|
||||
// // Add AllNS
|
||||
// {"", RowEvent{Kind: EventAdd, Row: ns}, AddColor},
|
||||
// // Add NS
|
||||
// {"blee", RowEvent{Kind: EventAdd, Row: nonNS}, AddColor},
|
||||
// // Mod AllNS
|
||||
// {"", RowEvent{Kind: EventUpdate, Row: ns}, ModColor},
|
||||
// // Mod NS
|
||||
// {"blee", RowEvent{Kind: EventUpdate, Row: nonNS}, ModColor},
|
||||
// // Unchanged cool
|
||||
// {"", RowEvent{Kind: EventUnchanged, Row: ns}, StdColor},
|
||||
// // Bust AllNS
|
||||
// {"", RowEvent{Kind: EventUnchanged, Row: bustNS}, ErrColor},
|
||||
// // Bust NS
|
||||
// {"blee", RowEvent{Kind: EventUnchanged, Row: bustNoNS}, ErrColor},
|
||||
// }
|
||||
// for _, u := range uu {
|
||||
// assert.Equal(t, u.e, pdbColorer(u.ns, u.r))
|
||||
// }
|
||||
// }
|
||||
|
||||
// func TestPVColorer(t *testing.T) {
|
||||
// var (
|
||||
// pv = Row{Fields: Fields{"blee", "1G", "RO", "Duh", "Bound"}}
|
||||
// bustPv = Row{Fields: Fields{"blee", "1G", "RO", "Duh", "UnBound"}}
|
||||
// )
|
||||
|
||||
// uu := colorerUCs{
|
||||
// // Add Normal
|
||||
// {"", RowEvent{Kind: EventAdd, Row: pv}, AddColor},
|
||||
// // Unchanged Bound
|
||||
// {"", RowEvent{Kind: EventUnchanged, Row: pv}, StdColor},
|
||||
// // Unchanged Bound
|
||||
// {"", RowEvent{Kind: EventUnchanged, Row: bustPv}, ErrColor},
|
||||
// }
|
||||
// for _, u := range uu {
|
||||
// assert.Equal(t, u.e, pvColorer(u.ns, u.r))
|
||||
// }
|
||||
// }
|
||||
|
||||
// func TestPVCColorer(t *testing.T) {
|
||||
// var (
|
||||
// pvc = Row{Fields: Fields{"blee", "fred", "Bound"}}
|
||||
// bustPvc = Row{Fields: Fields{"blee", "fred", "UnBound"}}
|
||||
// )
|
||||
|
||||
// uu := colorerUCs{
|
||||
// // Add Normal
|
||||
// {"", RowEvent{Kind: EventAdd, Row: pvc}, AddColor},
|
||||
// // Add Bound
|
||||
// {"", RowEvent{Kind: EventUnchanged, Row: bustPvc}, ErrColor},
|
||||
// }
|
||||
// for _, u := range uu {
|
||||
// assert.Equal(t, u.e, pvcColorer(u.ns, u.r))
|
||||
// }
|
||||
// }
|
||||
|
||||
// func TestCtxColorer(t *testing.T) {
|
||||
// var (
|
||||
// ctx = Row{Fields: Fields{"blee"}}
|
||||
// defCtx = Row{Fields: Fields{"blee*"}}
|
||||
// )
|
||||
|
||||
// uu := colorerUCs{
|
||||
// // Add Normal
|
||||
// {"", RowEvent{Kind: EventAdd, Row: ctx}, AddColor},
|
||||
// // Add Default
|
||||
// {"", RowEvent{Kind: EventAdd, Row: defCtx}, AddColor},
|
||||
// // Mod Normal
|
||||
// {"", RowEvent{Kind: EventUpdate, Row: ctx}, ModColor},
|
||||
// // Mod Default
|
||||
// {"", RowEvent{Kind: EventUpdate, Row: defCtx}, ModColor},
|
||||
// // Unchanged Normal
|
||||
// {"", RowEvent{Kind: EventUnchanged, Row: ctx}, StdColor},
|
||||
// // Unchanged Default
|
||||
// {"", RowEvent{Kind: EventUnchanged, Row: defCtx}, HighlightColor},
|
||||
// }
|
||||
// for _, u := range uu {
|
||||
// assert.Equal(t, u.e, ctxColorer(u.ns, u.r))
|
||||
// }
|
||||
// }
|
||||
|
||||
// func TestPodColorer(t *testing.T) {
|
||||
// var (
|
||||
// nsRow = Row{Fields: Fields{"blee", "fred", "1/1", "Running"}}
|
||||
// toastNS = Row{Fields: Fields{"blee", "fred", "1/1", "Boom"}}
|
||||
// notReadyNS = Row{Fields: Fields{"blee", "fred", "0/1", "Boom"}}
|
||||
// row = Row{Fields: Fields{"fred", "1/1", "Running"}}
|
||||
// toast = Row{Fields: Fields{"fred", "1/1", "Boom"}}
|
||||
// notReady = Row{Fields: Fields{"fred", "0/1", "Boom"}}
|
||||
// )
|
||||
|
||||
// uu := colorerUCs{
|
||||
// // Add allNS
|
||||
// {"", RowEvent{Kind: EventAdd, Row: nsRow}, AddColor},
|
||||
// // Add Namespaced
|
||||
// {"blee", RowEvent{Kind: EventAdd, Row: row}, AddColor},
|
||||
// // Mod AllNS
|
||||
// {"", RowEvent{Kind: EventUpdate, Row: nsRow}, ModColor},
|
||||
// // Mod Namespaced
|
||||
// {"blee", RowEvent{Kind: EventUpdate, Row: row}, ModColor},
|
||||
// // Mod Busted AllNS
|
||||
// {"", RowEvent{Kind: EventUpdate, Row: toastNS}, ErrColor},
|
||||
// // Mod Busted Namespaced
|
||||
// {"blee", RowEvent{Kind: EventUpdate, Row: toast}, ErrColor},
|
||||
// // NotReady AllNS
|
||||
// {"", RowEvent{Kind: EventUpdate, Row: notReadyNS}, ErrColor},
|
||||
// // NotReady Namespaced
|
||||
// {"blee", RowEvent{Kind: EventUpdate, Row: notReady}, ErrColor},
|
||||
// }
|
||||
// for _, u := range uu {
|
||||
// assert.Equal(t, u.e, podColorer(u.ns, u.r))
|
||||
// }
|
||||
// }
|
||||
|
|
@ -7,6 +7,7 @@ import (
|
|||
|
||||
"github.com/derailed/k9s/internal/k8s"
|
||||
"github.com/derailed/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 {
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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},
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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},
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
// }
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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])
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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},
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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},
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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{}))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,110 @@
|
|||
package render
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/gdamore/tcell"
|
||||
)
|
||||
|
||||
// Forwarder represents a port forwarder.
|
||||
type Forwarder interface {
|
||||
// Path returns a resource FQN.
|
||||
Path() string
|
||||
|
||||
// Container returns a container name.
|
||||
Container() string
|
||||
|
||||
// Ports returns container exposed ports.
|
||||
Ports() []string
|
||||
|
||||
// Active returns forwarder current state.
|
||||
Active() bool
|
||||
|
||||
// Age returns forwarder age.
|
||||
Age() string
|
||||
}
|
||||
|
||||
// Forward renders a portforwards to screen.
|
||||
type Forward struct{}
|
||||
|
||||
// ColorerFunc colors a resource row.
|
||||
func (Forward) ColorerFunc() ColorerFunc {
|
||||
return func(ns string, re RowEvent) tcell.Color {
|
||||
return tcell.ColorSkyblue
|
||||
}
|
||||
}
|
||||
|
||||
// Header returns a header row.
|
||||
func (Forward) Header(ns string) HeaderRow {
|
||||
return HeaderRow{
|
||||
Header{Name: "NAMESPACE"},
|
||||
Header{Name: "NAME"},
|
||||
Header{Name: "CONTAINER"},
|
||||
Header{Name: "PORTS"},
|
||||
Header{Name: "URL"},
|
||||
Header{Name: "C"},
|
||||
Header{Name: "N"},
|
||||
Header{Name: "AGE", Decorator: ageDecorator},
|
||||
}
|
||||
}
|
||||
|
||||
// Render renders a K8s resource to screen.
|
||||
func (f Forward) Render(o interface{}, gvr string, r *Row) error {
|
||||
pf, ok := o.(PortForwarder)
|
||||
if !ok {
|
||||
return fmt.Errorf("expecting a portforward but got %T", o)
|
||||
}
|
||||
|
||||
ports := strings.Split(pf.Ports()[0], ":")
|
||||
ns, na := Namespaced(pf.Path())
|
||||
|
||||
r.ID = pf.Path()
|
||||
r.Fields = Fields{
|
||||
ns,
|
||||
na,
|
||||
pf.Container(),
|
||||
strings.Join(pf.Ports(), ","),
|
||||
UrlFor(pf.Host(), pf.HttpPath(), ports[0]),
|
||||
asNum(pf.C()),
|
||||
asNum(pf.N()),
|
||||
pf.Age(),
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Helpers...
|
||||
|
||||
type PortForwarder interface {
|
||||
Forwarder
|
||||
BenchConfigurator
|
||||
}
|
||||
|
||||
type BenchConfigurators map[string]BenchConfigurator
|
||||
|
||||
type BenchConfigurator interface {
|
||||
// C returns the number of concurent connections.
|
||||
C() int
|
||||
|
||||
// N returns the number of requests.
|
||||
N() int
|
||||
|
||||
// Host returns the forward host address.
|
||||
Host() string
|
||||
|
||||
// Path returns the http path.
|
||||
HttpPath() string
|
||||
}
|
||||
|
||||
// UrlFor computes fq url for a given benchmark configuration.
|
||||
func UrlFor(host, path, port string) string {
|
||||
if host == "" {
|
||||
host = "localhost"
|
||||
}
|
||||
if path == "" {
|
||||
path = "/"
|
||||
}
|
||||
|
||||
return "http://" + host + ":" + port + path
|
||||
}
|
||||
|
|
@ -0,0 +1,99 @@
|
|||
package render
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
metav1beta1 "k8s.io/apimachinery/pkg/apis/meta/v1beta1"
|
||||
)
|
||||
|
||||
// Generic renders a generic resource to screen.
|
||||
type Generic struct {
|
||||
table *metav1beta1.Table
|
||||
}
|
||||
|
||||
func (g *Generic) SetTable(t *metav1beta1.Table) {
|
||||
g.table = t
|
||||
}
|
||||
|
||||
// ColorerFunc colors a resource row.
|
||||
func (Generic) ColorerFunc() ColorerFunc {
|
||||
return DefaultColorer
|
||||
}
|
||||
|
||||
// Header returns a header row.
|
||||
func (g *Generic) Header(ns string) HeaderRow {
|
||||
h := make(HeaderRow, 0, len(g.table.ColumnDefinitions))
|
||||
|
||||
if ns == "" {
|
||||
h = append(h, Header{Name: "NAMESPACE"})
|
||||
}
|
||||
for _, c := range g.table.ColumnDefinitions {
|
||||
h = append(h, Header{Name: strings.ToUpper(c.Name)})
|
||||
}
|
||||
|
||||
log.Debug().Msgf("Generic Header %#v", h)
|
||||
return h
|
||||
}
|
||||
|
||||
// Render renders a K8s resource to screen.
|
||||
func (g *Generic) Render(o interface{}, ns string, r *Row) error {
|
||||
row, ok := o.(*metav1beta1.TableRow)
|
||||
if !ok {
|
||||
return fmt.Errorf("expecting a table but got %#v", o)
|
||||
}
|
||||
|
||||
count := len(row.Cells)
|
||||
if ns == AllNamespaces {
|
||||
count++
|
||||
}
|
||||
r.ID, ok = row.Cells[0].(string)
|
||||
if !ok {
|
||||
return fmt.Errorf("expecting row id to be a string but got %#v", row.Cells[0])
|
||||
}
|
||||
r.Fields = make(Fields, count)
|
||||
|
||||
var index int
|
||||
if ns == AllNamespaces {
|
||||
rns, err := extractNamespace(row.Object.Raw)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
r.Fields[index] = rns
|
||||
r.ID = FQN(rns, r.ID)
|
||||
index++
|
||||
}
|
||||
|
||||
for _, c := range row.Cells {
|
||||
r.Fields[index] = fmt.Sprintf("%v", c)
|
||||
index++
|
||||
}
|
||||
log.Debug().Msgf("Generic row %#v", r)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Helpers...
|
||||
|
||||
func extractNamespace(raw []byte) (string, error) {
|
||||
var obj map[string]interface{}
|
||||
err := json.Unmarshal(raw, &obj)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
meta, ok := obj["metadata"].(map[string]interface{})
|
||||
if !ok {
|
||||
return "", errors.New("no metadata found on generic resource")
|
||||
}
|
||||
ns, ok := meta["namespace"].(string)
|
||||
if !ok {
|
||||
return "", errors.New("invalid namespace found on generic metadata")
|
||||
}
|
||||
|
||||
return ns, nil
|
||||
}
|
||||
|
|
@ -152,13 +152,13 @@ func boolToStr(b bool) string {
|
|||
}
|
||||
|
||||
func toAge(timestamp metav1.Time) string {
|
||||
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)
|
||||
|
|
|
|||
|
|
@ -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},
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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},
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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},
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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},
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -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},
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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},
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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},
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
package render
|
||||
|
||||
import (
|
||||
"github.com/gdamore/tcell"
|
||||
)
|
||||
|
||||
func rbacVerbHeader() HeaderRow {
|
||||
return HeaderRow{
|
||||
Header{Name: "GET "},
|
||||
Header{Name: "LIST "},
|
||||
Header{Name: "WATCH "},
|
||||
Header{Name: "CREATE"},
|
||||
Header{Name: "PATCH "},
|
||||
Header{Name: "UPDATE"},
|
||||
Header{Name: "DELETE"},
|
||||
Header{Name: "DLIST "},
|
||||
Header{Name: "EXTRAS"},
|
||||
}
|
||||
}
|
||||
|
||||
// Policy renders a rbac policy to screen.
|
||||
type Policy struct{}
|
||||
|
||||
// ColorerFunc colors a resource row.
|
||||
func (Policy) ColorerFunc() ColorerFunc {
|
||||
return func(ns string, re RowEvent) tcell.Color {
|
||||
return tcell.ColorMediumSpringGreen
|
||||
}
|
||||
}
|
||||
|
||||
// Header returns a header row.
|
||||
func (Policy) Header(ns string) HeaderRow {
|
||||
h := HeaderRow{
|
||||
Header{Name: "NAMESPACE"},
|
||||
Header{Name: "NAME"},
|
||||
Header{Name: "API GROUP"},
|
||||
Header{Name: "BINDING"},
|
||||
}
|
||||
|
||||
return append(h, rbacVerbHeader()...)
|
||||
}
|
||||
|
||||
// Render renders a K8s resource to screen.
|
||||
func (Policy) Render(o interface{}, gvr string, r *Row) error {
|
||||
panic("NYI")
|
||||
return nil
|
||||
}
|
||||
|
||||
// Helpers...
|
||||
|
|
@ -5,6 +5,7 @@ import (
|
|||
"path"
|
||||
"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},
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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},
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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},
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,33 @@
|
|||
package render
|
||||
|
||||
import (
|
||||
"github.com/gdamore/tcell"
|
||||
)
|
||||
|
||||
// Rbac renders a rbac to screen.
|
||||
type Rbac struct{}
|
||||
|
||||
// ColorerFunc colors a resource row.
|
||||
func (Rbac) ColorerFunc() ColorerFunc {
|
||||
return func(ns string, re RowEvent) tcell.Color {
|
||||
return tcell.ColorMediumSpringGreen
|
||||
}
|
||||
}
|
||||
|
||||
// Header returns a header row.
|
||||
func (Rbac) Header(ns string) HeaderRow {
|
||||
h := HeaderRow{
|
||||
Header{Name: "NAME"},
|
||||
Header{Name: "API GROUP"},
|
||||
}
|
||||
|
||||
return append(h, rbacVerbHeader()...)
|
||||
}
|
||||
|
||||
// Render renders a K8s resource to screen.
|
||||
func (Rbac) Render(o interface{}, gvr string, r *Row) error {
|
||||
panic("NYI")
|
||||
return nil
|
||||
}
|
||||
|
||||
// Helpers...
|
||||
|
|
@ -25,7 +25,7 @@ func (Role) Header(ns string) HeaderRow {
|
|||
|
||||
return append(h,
|
||||
Header{Name: "NAME"},
|
||||
Header{Name: "AGE"},
|
||||
Header{Name: "AGE", Decorator: ageDecorator},
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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},
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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},
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,61 @@
|
|||
package render
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/gdamore/tcell"
|
||||
)
|
||||
|
||||
// ScreenDump renders a screendumps to screen.
|
||||
type ScreenDump struct{}
|
||||
|
||||
// ColorerFunc colors a resource row.
|
||||
func (ScreenDump) ColorerFunc() ColorerFunc {
|
||||
return func(ns string, re RowEvent) tcell.Color {
|
||||
return tcell.ColorNavajoWhite
|
||||
}
|
||||
}
|
||||
|
||||
type DecoratorFunc func(string) string
|
||||
|
||||
var ageDecorator = func(a string) string {
|
||||
return toAgeHuman(a)
|
||||
}
|
||||
|
||||
// Header returns a header row.
|
||||
func (ScreenDump) Header(ns string) HeaderRow {
|
||||
return HeaderRow{
|
||||
Header{Name: "NAME"},
|
||||
Header{Name: "AGE", Decorator: ageDecorator},
|
||||
}
|
||||
}
|
||||
|
||||
// Render renders a K8s resource to screen.
|
||||
func (b ScreenDump) Render(o interface{}, ns string, r *Row) error {
|
||||
f, ok := o.(ScreenDumper)
|
||||
if !ok {
|
||||
return fmt.Errorf("Expected string, but got %T", o)
|
||||
}
|
||||
|
||||
r.ID = filepath.Join(f.GetDir(), f.GetFile().Name())
|
||||
r.Fields = Fields{
|
||||
f.GetFile().Name(),
|
||||
timeToAge(f.GetFile().ModTime()),
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Helpers...
|
||||
|
||||
func timeToAge(timestamp time.Time) string {
|
||||
return time.Since(timestamp).String()
|
||||
}
|
||||
|
||||
type ScreenDumper interface {
|
||||
GetFile() os.FileInfo
|
||||
GetDir() string
|
||||
}
|
||||
|
|
@ -29,7 +29,7 @@ func (Secret) Header(ns string) HeaderRow {
|
|||
Header{Name: "NAME"},
|
||||
Header{Name: "TYPE"},
|
||||
Header{Name: "DATA", Align: tview.AlignRight},
|
||||
Header{Name: "AGE"},
|
||||
Header{Name: "AGE", Decorator: ageDecorator},
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,81 @@
|
|||
package render
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/gdamore/tcell"
|
||||
appsv1 "k8s.io/api/apps/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
)
|
||||
|
||||
// StatefulSet renders a K8s StatefulSet to screen.
|
||||
type StatefulSet struct{}
|
||||
|
||||
// ColorerFunc colors a resource row.
|
||||
func (StatefulSet) ColorerFunc() ColorerFunc {
|
||||
return func(ns string, r RowEvent) tcell.Color {
|
||||
c := DefaultColorer(ns, r)
|
||||
if r.Kind == EventAdd || r.Kind == EventUpdate {
|
||||
return c
|
||||
}
|
||||
|
||||
readyCol := 2
|
||||
if ns != AllNamespaces {
|
||||
readyCol--
|
||||
}
|
||||
tokens := strings.Split(strings.TrimSpace(r.Row.Fields[readyCol]), "/")
|
||||
curr, des := tokens[0], tokens[1]
|
||||
if curr != des {
|
||||
return ErrColor
|
||||
}
|
||||
|
||||
return StdColor
|
||||
}
|
||||
}
|
||||
|
||||
// Header returns a header row.
|
||||
func (StatefulSet) Header(ns string) HeaderRow {
|
||||
var h HeaderRow
|
||||
if isAllNamespace(ns) {
|
||||
h = append(h, Header{Name: "NAMESPACE"})
|
||||
}
|
||||
|
||||
return append(h,
|
||||
Header{Name: "NAME"},
|
||||
Header{Name: "READY"},
|
||||
Header{Name: "SELECTOR"},
|
||||
Header{Name: "SERVICE"},
|
||||
Header{Name: "AGE", Decorator: ageDecorator},
|
||||
)
|
||||
}
|
||||
|
||||
// Render renders a K8s resource to screen.
|
||||
func (s StatefulSet) Render(o interface{}, ns string, r *Row) error {
|
||||
raw, ok := o.(*unstructured.Unstructured)
|
||||
if !ok {
|
||||
return fmt.Errorf("Expected StatefulSet, but got %T", o)
|
||||
}
|
||||
var sts appsv1.StatefulSet
|
||||
err := runtime.DefaultUnstructuredConverter.FromUnstructured(raw.Object, &sts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
r.ID = MetaFQN(sts.ObjectMeta)
|
||||
r.Fields = make(Fields, 0, len(s.Header(ns)))
|
||||
if isAllNamespace(ns) {
|
||||
r.Fields = append(r.Fields, sts.Namespace)
|
||||
}
|
||||
r.Fields = append(r.Fields,
|
||||
sts.Name,
|
||||
strconv.Itoa(int(sts.Status.Replicas))+"/"+strconv.Itoa(int(*sts.Spec.Replicas)),
|
||||
asSelector(sts.Spec.Selector),
|
||||
na(sts.Spec.ServiceName),
|
||||
toAge(sts.ObjectMeta.CreationTimestamp),
|
||||
)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
package render
|
||||
|
||||
import (
|
||||
"github.com/gdamore/tcell"
|
||||
)
|
||||
|
||||
// Subject renders a rbac to screen.
|
||||
type Subject struct{}
|
||||
|
||||
// ColorerFunc colors a resource row.
|
||||
func (Subject) ColorerFunc() ColorerFunc {
|
||||
return func(ns string, re RowEvent) tcell.Color {
|
||||
return tcell.ColorMediumSpringGreen
|
||||
}
|
||||
}
|
||||
|
||||
// Header returns a header row.
|
||||
func (Subject) Header(ns string) HeaderRow {
|
||||
return HeaderRow{
|
||||
Header{Name: "NAME"},
|
||||
Header{Name: "KIND"},
|
||||
Header{Name: "FIRST LOCATION"},
|
||||
}
|
||||
}
|
||||
|
||||
// Render renders a K8s resource to screen.
|
||||
func (Subject) Render(o interface{}, gvr string, r *Row) error {
|
||||
panic("NYI")
|
||||
return nil
|
||||
}
|
||||
|
|
@ -33,7 +33,7 @@ func (Service) Header(ns string) HeaderRow {
|
|||
Header{Name: "EXTERNAL-IP"},
|
||||
Header{Name: "SELECTOR"},
|
||||
Header{Name: "PORTS"},
|
||||
Header{Name: "AGE"},
|
||||
Header{Name: "AGE", Decorator: ageDecorator},
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,16 @@
|
|||
package render
|
||||
|
||||
// TableData tracks a K8s resource for tabular display.
|
||||
type TableData struct {
|
||||
Header HeaderRow
|
||||
RowEvents RowEvents
|
||||
Namespace string
|
||||
}
|
||||
|
||||
func (t TableData) Clone() TableData {
|
||||
return TableData{
|
||||
Header: t.Header,
|
||||
RowEvents: t.RowEvents.Clone(),
|
||||
Namespace: t.Namespace,
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
package render
|
||||
|
||||
const (
|
||||
// AllNamespaces represents all namespaces.
|
||||
AllNamespaces = ""
|
||||
|
||||
// NamespaceAll represent the all namespace.
|
||||
NamespaceAll = "all"
|
||||
|
||||
// ClusterWide represents a cluster resources.
|
||||
ClusterWide = "-"
|
||||
|
||||
// NonResource represents a custom resource.
|
||||
NonResource = "*"
|
||||
)
|
||||
|
||||
const (
|
||||
// Terminating represents a pod terminating status.
|
||||
Terminating = "Terminating"
|
||||
|
||||
// Running represents a pod running status.
|
||||
Running = "Running"
|
||||
|
||||
// Initialized represents a pod intialized status.
|
||||
Initialized = "Initialized"
|
||||
|
||||
// Completed represents a pod completed status.
|
||||
Completed = "Completed"
|
||||
|
||||
// ContainerCreating represents a pod container status.
|
||||
ContainerCreating = "ContainerCreating"
|
||||
|
||||
// PodInitializing represents a pod initializing status.
|
||||
PodInitializing = "PodInitializing"
|
||||
)
|
||||
|
|
@ -4,8 +4,10 @@ import (
|
|||
"bytes"
|
||||
"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 {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
// }
|
||||
|
|
|
|||
|
|
@ -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
Loading…
Reference in New Issue