parent
0af1c7e0d2
commit
4d37d2449c
|
|
@ -64,7 +64,7 @@ func makeSAR(ns, gvr string) *authorizationv1.SelfSubjectAccessReview {
|
|||
ns = ""
|
||||
}
|
||||
spec := NewGVR(gvr)
|
||||
res := spec.AsGVR()
|
||||
res := spec.GVR()
|
||||
return &authorizationv1.SelfSubjectAccessReview{
|
||||
Spec: authorizationv1.SelfSubjectAccessReviewSpec{
|
||||
ResourceAttributes: &authorizationv1.ResourceAttributes{
|
||||
|
|
|
|||
|
|
@ -72,7 +72,7 @@ func (g GVR) String() string {
|
|||
}
|
||||
|
||||
// AsGV returns the group version scheme representation.
|
||||
func (g GVR) AsGV() schema.GroupVersion {
|
||||
func (g GVR) GV() schema.GroupVersion {
|
||||
return schema.GroupVersion{
|
||||
Group: g.g,
|
||||
Version: g.v,
|
||||
|
|
@ -80,39 +80,39 @@ func (g GVR) AsGV() schema.GroupVersion {
|
|||
}
|
||||
|
||||
// AsGVR returns a a full schema representation.
|
||||
func (g GVR) AsGVR() schema.GroupVersionResource {
|
||||
func (g GVR) GVR() schema.GroupVersionResource {
|
||||
return schema.GroupVersionResource{
|
||||
Group: g.ToG(),
|
||||
Version: g.ToV(),
|
||||
Resource: g.ToR(),
|
||||
Group: g.G(),
|
||||
Version: g.V(),
|
||||
Resource: g.R(),
|
||||
}
|
||||
}
|
||||
|
||||
// AsGR returns a a full schema representation.
|
||||
func (g GVR) AsGR() *schema.GroupResource {
|
||||
func (g GVR) GR() *schema.GroupResource {
|
||||
return &schema.GroupResource{
|
||||
Group: g.ToG(),
|
||||
Resource: g.ToR(),
|
||||
Group: g.G(),
|
||||
Resource: g.R(),
|
||||
}
|
||||
}
|
||||
|
||||
// ToV returns the resource version.
|
||||
func (g GVR) ToV() string {
|
||||
func (g GVR) V() string {
|
||||
return g.v
|
||||
}
|
||||
|
||||
// ToRAndG returns the resource and group.
|
||||
func (g GVR) ToRAndG() (string, string) {
|
||||
func (g GVR) RG() (string, string) {
|
||||
return g.r, g.g
|
||||
}
|
||||
|
||||
// ToR returns the resource name.
|
||||
func (g GVR) ToR() string {
|
||||
func (g GVR) R() string {
|
||||
return g.r
|
||||
}
|
||||
|
||||
// ToG returns the resource group name.
|
||||
func (g GVR) ToG() string {
|
||||
func (g GVR) G() string {
|
||||
return g.g
|
||||
}
|
||||
|
||||
|
|
@ -131,7 +131,7 @@ func (g GVRs) Swap(i, j int) {
|
|||
|
||||
// Less returns true if i < j.
|
||||
func (g GVRs) Less(i, j int) bool {
|
||||
g1, g2 := g[i].ToG(), g[j].ToG()
|
||||
g1, g2 := g[i].G(), g[j].G()
|
||||
|
||||
return sortorder.NaturalLess(g1, g2)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -59,7 +59,7 @@ func TestAsGVR(t *testing.T) {
|
|||
for k := range uu {
|
||||
u := uu[k]
|
||||
t.Run(k, func(t *testing.T) {
|
||||
assert.Equal(t, u.e, client.NewGVR(u.gvr).AsGVR())
|
||||
assert.Equal(t, u.e, client.NewGVR(u.gvr).GVR())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -77,7 +77,7 @@ func TestAsGV(t *testing.T) {
|
|||
for k := range uu {
|
||||
u := uu[k]
|
||||
t.Run(k, func(t *testing.T) {
|
||||
assert.Equal(t, u.e, client.NewGVR(u.gvr).AsGV())
|
||||
assert.Equal(t, u.e, client.NewGVR(u.gvr).GV())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -132,7 +132,7 @@ func TestToR(t *testing.T) {
|
|||
for k := range uu {
|
||||
u := uu[k]
|
||||
t.Run(k, func(t *testing.T) {
|
||||
assert.Equal(t, u.e, client.NewGVR(u.gvr).ToR())
|
||||
assert.Equal(t, u.e, client.NewGVR(u.gvr).R())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -151,7 +151,7 @@ func TestToG(t *testing.T) {
|
|||
for k := range uu {
|
||||
u := uu[k]
|
||||
t.Run(k, func(t *testing.T) {
|
||||
assert.Equal(t, u.e, client.NewGVR(u.gvr).ToG())
|
||||
assert.Equal(t, u.e, client.NewGVR(u.gvr).G())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -170,7 +170,7 @@ func TestToV(t *testing.T) {
|
|||
for k := range uu {
|
||||
u := uu[k]
|
||||
t.Run(k, func(t *testing.T) {
|
||||
assert.Equal(t, u.e, client.NewGVR(u.gvr).ToV())
|
||||
assert.Equal(t, u.e, client.NewGVR(u.gvr).V())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ func Describe(c client.Connection, gvr client.GVR, path string) (string, error)
|
|||
return "", err
|
||||
}
|
||||
|
||||
gvk, err := m.KindFor(gvr.AsGVR())
|
||||
gvk, err := m.KindFor(gvr.GVR())
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msgf("No GVK for resource %s", gvr)
|
||||
return "", err
|
||||
|
|
|
|||
|
|
@ -26,7 +26,6 @@ type Generic struct {
|
|||
// List returns a collection of resources.
|
||||
// BOZO!! no auth check??
|
||||
func (g *Generic) List(ctx context.Context, ns string) ([]runtime.Object, error) {
|
||||
log.Debug().Msgf("GENERIC-LIST %q:%q", ns, g.gvr)
|
||||
labelSel, ok := ctx.Value(internal.KeyLabels).(string)
|
||||
if !ok {
|
||||
log.Warn().Msgf("No label selector found in context. Listing all resources")
|
||||
|
|
@ -116,5 +115,5 @@ func (g *Generic) Delete(path string, cascade, force bool) error {
|
|||
}
|
||||
|
||||
func (g *Generic) dynClient() dynamic.NamespaceableResourceInterface {
|
||||
return g.Client().DynDialOrDie().Resource(g.gvr.AsGVR())
|
||||
return g.Client().DynDialOrDie().Resource(g.gvr.GVR())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@ func (r *Rbac) List(ctx context.Context, ns string) ([]runtime.Object, error) {
|
|||
}
|
||||
|
||||
res := client.NewGVR(gvr)
|
||||
switch res.ToR() {
|
||||
switch res.R() {
|
||||
case "clusterrolebindings":
|
||||
return r.loadClusterRoleBinding(path)
|
||||
case "rolebindings":
|
||||
|
|
@ -52,7 +52,7 @@ func (r *Rbac) List(ctx context.Context, ns string) ([]runtime.Object, error) {
|
|||
case "roles":
|
||||
return r.loadRole(path)
|
||||
default:
|
||||
return nil, fmt.Errorf("expecting clusterrole/role but found %s", res.ToR())
|
||||
return nil, fmt.Errorf("expecting clusterrole/role but found %s", res.R())
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package dao
|
|||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/derailed/k9s/internal/client"
|
||||
"github.com/rs/zerolog/log"
|
||||
|
|
@ -115,22 +116,26 @@ func loadK9s(m ResourceMetas) {
|
|||
m[client.NewGVR("xrays")] = metav1.APIResource{
|
||||
Name: "xray",
|
||||
Kind: "XRays",
|
||||
SingularName: "xray",
|
||||
Categories: []string{"k9s"},
|
||||
}
|
||||
m[client.NewGVR("aliases")] = metav1.APIResource{
|
||||
Name: "aliases",
|
||||
Kind: "Aliases",
|
||||
SingularName: "alias",
|
||||
Categories: []string{"k9s"},
|
||||
}
|
||||
m[client.NewGVR("contexts")] = metav1.APIResource{
|
||||
Name: "contexts",
|
||||
Kind: "Contexts",
|
||||
SingularName: "context",
|
||||
ShortNames: []string{"ctx"},
|
||||
Categories: []string{"k9s"},
|
||||
}
|
||||
m[client.NewGVR("screendumps")] = metav1.APIResource{
|
||||
Name: "screendumps",
|
||||
Kind: "ScreenDumps",
|
||||
SingularName: "screendump",
|
||||
ShortNames: []string{"sd"},
|
||||
Verbs: []string{"delete"},
|
||||
Categories: []string{"k9s"},
|
||||
|
|
@ -138,6 +143,7 @@ func loadK9s(m ResourceMetas) {
|
|||
m[client.NewGVR("benchmarks")] = metav1.APIResource{
|
||||
Name: "benchmarks",
|
||||
Kind: "Benchmarks",
|
||||
SingularName: "benchmark",
|
||||
ShortNames: []string{"be"},
|
||||
Verbs: []string{"delete"},
|
||||
Categories: []string{"k9s"},
|
||||
|
|
@ -146,6 +152,7 @@ func loadK9s(m ResourceMetas) {
|
|||
Name: "portforwards",
|
||||
Namespaced: true,
|
||||
Kind: "PortForwards",
|
||||
SingularName: "portforward",
|
||||
ShortNames: []string{"pf"},
|
||||
Verbs: []string{"delete"},
|
||||
Categories: []string{"k9s"},
|
||||
|
|
@ -153,6 +160,7 @@ func loadK9s(m ResourceMetas) {
|
|||
m[client.NewGVR("containers")] = metav1.APIResource{
|
||||
Name: "containers",
|
||||
Kind: "Containers",
|
||||
SingularName: "container",
|
||||
Categories: []string{"k9s"},
|
||||
}
|
||||
}
|
||||
|
|
@ -199,7 +207,10 @@ func loadPreferred(f Factory, m ResourceMetas) error {
|
|||
for _, r := range rr {
|
||||
for _, res := range r.APIResources {
|
||||
gvr := client.FromGVAndR(r.GroupVersion, res.Name)
|
||||
res.Group, res.Version = gvr.ToG(), gvr.ToV()
|
||||
res.Group, res.Version = gvr.G(), gvr.V()
|
||||
if res.SingularName == "" {
|
||||
res.SingularName = strings.ToLower(res.Kind)
|
||||
}
|
||||
m[gvr] = res
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ func (t *Table) Get(ctx context.Context, path string) (runtime.Object, error) {
|
|||
SetHeader("Accept", a).
|
||||
Namespace(ns).
|
||||
Name(n).
|
||||
Resource(t.gvr.ToR()).
|
||||
Resource(t.gvr.R()).
|
||||
VersionedParams(&metav1beta1.TableOptions{}, codec).
|
||||
Do().Get()
|
||||
if err != nil {
|
||||
|
|
@ -57,7 +57,7 @@ func (t *Table) List(ctx context.Context, ns string) ([]runtime.Object, error) {
|
|||
o, err := c.Get().
|
||||
SetHeader("Accept", a).
|
||||
Namespace(ns).
|
||||
Resource(t.gvr.ToR()).
|
||||
Resource(t.gvr.R()).
|
||||
VersionedParams(&metav1beta1.TableOptions{}, codec).
|
||||
Do().Get()
|
||||
if err != nil {
|
||||
|
|
@ -74,10 +74,10 @@ const gvFmt = "application/json;as=Table;v=%s;g=%s, application/json"
|
|||
|
||||
func (t *Table) getClient() (*rest.RESTClient, error) {
|
||||
crConfig := t.Client().RestConfigOrDie()
|
||||
gv := t.gvr.AsGV()
|
||||
gv := t.gvr.GV()
|
||||
crConfig.GroupVersion = &gv
|
||||
crConfig.APIPath = "/apis"
|
||||
if t.gvr.ToG() == "" {
|
||||
if t.gvr.G() == "" {
|
||||
crConfig.APIPath = "/api"
|
||||
}
|
||||
codec, _ := t.codec()
|
||||
|
|
@ -92,7 +92,7 @@ func (t *Table) getClient() (*rest.RESTClient, error) {
|
|||
|
||||
func (t *Table) codec() (serializer.CodecFactory, runtime.ParameterCodec) {
|
||||
scheme := runtime.NewScheme()
|
||||
gv := t.gvr.AsGV()
|
||||
gv := t.gvr.GV()
|
||||
metav1.AddToGroupVersion(scheme, gv)
|
||||
scheme.AddKnownTypes(gv, &metav1beta1.Table{}, &metav1beta1.TableOptions{})
|
||||
scheme.AddKnownTypes(metav1beta1.SchemeGroupVersion, &metav1beta1.Table{}, &metav1beta1.TableOptions{})
|
||||
|
|
|
|||
|
|
@ -98,6 +98,7 @@ var Registry = map[string]ResourceMeta{
|
|||
},
|
||||
"apps/v1/replicasets": {
|
||||
Renderer: &render.ReplicaSet{},
|
||||
TreeRenderer: &xray.ReplicaSet{},
|
||||
},
|
||||
"apps/v1/statefulsets": {
|
||||
DAO: &dao.StatefulSet{},
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ import (
|
|||
"k8s.io/apimachinery/pkg/runtime"
|
||||
)
|
||||
|
||||
const iniRefreshRate = 300 * time.Millisecond
|
||||
const initRefreshRate = 300 * time.Millisecond
|
||||
|
||||
// TableListener represents a table model listener.
|
||||
type TableListener interface {
|
||||
|
|
@ -176,7 +176,7 @@ func (t *Table) Peek() render.TableData {
|
|||
func (t *Table) updater(ctx context.Context) {
|
||||
defer log.Debug().Msgf("Model canceled -- %q", t.gvr)
|
||||
|
||||
rate := iniRefreshRate
|
||||
rate := initRefreshRate
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
|
|
|
|||
|
|
@ -18,6 +18,8 @@ import (
|
|||
"k8s.io/apimachinery/pkg/runtime"
|
||||
)
|
||||
|
||||
const initTreeRefreshRate = 500 * time.Millisecond
|
||||
|
||||
// TreeListener represents a tree model listener.
|
||||
type TreeListener interface {
|
||||
// TreeChanged notifies the model data changed.
|
||||
|
|
@ -159,7 +161,7 @@ func (t *Tree) ToYAML(ctx context.Context, gvr, path string) (string, error) {
|
|||
func (t *Tree) updater(ctx context.Context) {
|
||||
defer log.Debug().Msgf("Tree-model canceled -- %q", t.gvr)
|
||||
|
||||
rate := iniRefreshRate
|
||||
rate := initTreeRefreshRate
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
|
|
@ -204,7 +206,8 @@ func (t *Tree) reconcile(ctx context.Context) error {
|
|||
}
|
||||
|
||||
ns := client.CleanseNamespace(t.namespace)
|
||||
root := xray.NewTreeNode("root", client.NewGVR(t.gvr).ToR())
|
||||
res := client.NewGVR(t.gvr).R()
|
||||
root := xray.NewTreeNode(res, res)
|
||||
ctx = context.WithValue(ctx, xray.KeyParent, root)
|
||||
if _, ok := meta.TreeRenderer.(*xray.Generic); ok {
|
||||
table, ok := oo[0].(*metav1beta1.Table)
|
||||
|
|
@ -287,6 +290,9 @@ func rxFilter(q, path string) bool {
|
|||
}
|
||||
|
||||
func treeHydrate(ctx context.Context, ns string, oo []runtime.Object, re TreeRenderer) error {
|
||||
if re == nil {
|
||||
return fmt.Errorf("no tree renderer defined for this resource")
|
||||
}
|
||||
for _, o := range oo {
|
||||
if err := re.Render(ctx, ns, o); err != nil {
|
||||
return err
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@ func (Alias) Render(o interface{}, ns string, r *Row) error {
|
|||
|
||||
r.ID = a.GVR
|
||||
gvr := client.NewGVR(a.GVR)
|
||||
res, grp := gvr.ToRAndG()
|
||||
res, grp := gvr.RG()
|
||||
r.Fields = append(r.Fields,
|
||||
res,
|
||||
strings.Join(a.Aliases, ","),
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ const (
|
|||
// FlashFatal represents an fatal message.
|
||||
FlashFatal
|
||||
|
||||
flashDelay = 3
|
||||
flashDelay = 3 * time.Second
|
||||
|
||||
emoDoh = "😗"
|
||||
emoRed = "😡"
|
||||
|
|
@ -41,21 +41,30 @@ type (
|
|||
|
||||
cancel context.CancelFunc
|
||||
app *App
|
||||
flushNow bool
|
||||
}
|
||||
)
|
||||
|
||||
// NewFlash returns a new flash view.
|
||||
func NewFlash(app *App, m string) *Flash {
|
||||
f := Flash{app: app, TextView: tview.NewTextView()}
|
||||
f := Flash{
|
||||
app: app,
|
||||
TextView: tview.NewTextView(),
|
||||
}
|
||||
f.SetTextColor(tcell.ColorAqua)
|
||||
f.SetTextAlign(tview.AlignLeft)
|
||||
f.SetBorderPadding(0, 0, 1, 1)
|
||||
f.SetText("")
|
||||
f.SetText(m)
|
||||
f.app.Styles.AddListener(&f)
|
||||
|
||||
return &f
|
||||
}
|
||||
|
||||
// TestMode for testing...
|
||||
func (f *Flash) TestMode() {
|
||||
f.flushNow = true
|
||||
}
|
||||
|
||||
// StylesChanged notifies listener the skin changed.
|
||||
func (f *Flash) StylesChanged(s *config.Styles) {
|
||||
f.SetBackgroundColor(s.BgColor())
|
||||
|
|
@ -64,6 +73,7 @@ func (f *Flash) StylesChanged(s *config.Styles) {
|
|||
|
||||
// Info displays an info flash message.
|
||||
func (f *Flash) Info(msg string) {
|
||||
log.Info().Msg(msg)
|
||||
f.SetMessage(FlashInfo, msg)
|
||||
}
|
||||
|
||||
|
|
@ -74,6 +84,7 @@ func (f *Flash) Infof(fmat string, args ...interface{}) {
|
|||
|
||||
// Warn displays a warning flash message.
|
||||
func (f *Flash) Warn(msg string) {
|
||||
log.Warn().Msg(msg)
|
||||
f.SetMessage(FlashWarn, msg)
|
||||
}
|
||||
|
||||
|
|
@ -84,7 +95,7 @@ func (f *Flash) Warnf(fmat string, args ...interface{}) {
|
|||
|
||||
// Err displays an error flash message.
|
||||
func (f *Flash) Err(err error) {
|
||||
log.Error().Err(err).Msgf("%v", err)
|
||||
log.Error().Msg(err.Error())
|
||||
f.SetMessage(FlashErr, err.Error())
|
||||
}
|
||||
|
||||
|
|
@ -106,35 +117,35 @@ func (f *Flash) SetMessage(level FlashLevel, msg ...string) {
|
|||
if f.cancel != nil {
|
||||
f.cancel()
|
||||
}
|
||||
var ctx1, ctx2 context.Context
|
||||
{
|
||||
var timerCancel context.CancelFunc
|
||||
ctx1, f.cancel = context.WithCancel(context.TODO())
|
||||
ctx2, timerCancel = context.WithTimeout(context.TODO(), flashDelay*time.Second)
|
||||
go f.refresh(ctx1, ctx2, timerCancel)
|
||||
}
|
||||
|
||||
_, _, width, _ := f.GetRect()
|
||||
if width <= 15 {
|
||||
width = 100
|
||||
}
|
||||
m := strings.Join(msg, " ")
|
||||
if f.flushNow {
|
||||
f.SetTextColor(flashColor(level))
|
||||
f.SetText(render.Truncate(flashEmoji(level)+" "+m, width-3))
|
||||
} else {
|
||||
f.app.QueueUpdateDraw(func() {
|
||||
log.Debug().Msgf("FLASH %q", m)
|
||||
f.SetTextColor(flashColor(level))
|
||||
f.SetText(render.Truncate(flashEmoji(level)+" "+m, width-3))
|
||||
})
|
||||
}
|
||||
|
||||
var ctx context.Context
|
||||
ctx, f.cancel = context.WithCancel(context.TODO())
|
||||
ctx, f.cancel = context.WithTimeout(ctx, flashDelay)
|
||||
go f.refresh(ctx)
|
||||
}
|
||||
|
||||
func (f *Flash) refresh(ctx1, ctx2 context.Context, cancel context.CancelFunc) {
|
||||
defer cancel()
|
||||
for {
|
||||
select {
|
||||
case <-ctx1.Done():
|
||||
return
|
||||
case <-ctx2.Done():
|
||||
func (f *Flash) refresh(ctx context.Context) {
|
||||
<-ctx.Done()
|
||||
f.app.QueueUpdateDraw(func() {
|
||||
log.Debug().Msgf("FLASH-CLEAR %q", f.GetText(true))
|
||||
f.Clear()
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func flashEmoji(l FlashLevel) string {
|
||||
|
|
|
|||
|
|
@ -9,32 +9,37 @@ import (
|
|||
)
|
||||
|
||||
func TestFlashInfo(t *testing.T) {
|
||||
f := ui.NewFlash(ui.NewApp(""), "YO!")
|
||||
|
||||
f := newFlash()
|
||||
f.Info("Blee")
|
||||
assert.Equal(t, "😎 Blee\n", f.GetText(false))
|
||||
|
||||
assert.Equal(t, "😎 Blee\n", f.GetText(false))
|
||||
f.Infof("Blee %s", "duh")
|
||||
assert.Equal(t, "😎 Blee duh\n", f.GetText(false))
|
||||
}
|
||||
|
||||
func TestFlashWarn(t *testing.T) {
|
||||
f := ui.NewFlash(ui.NewApp(""), "YO!")
|
||||
|
||||
f := newFlash()
|
||||
f.Warn("Blee")
|
||||
assert.Equal(t, "😗 Blee\n", f.GetText(false))
|
||||
|
||||
assert.Equal(t, "😗 Blee\n", f.GetText(false))
|
||||
f.Warnf("Blee %s", "duh")
|
||||
assert.Equal(t, "😗 Blee duh\n", f.GetText(false))
|
||||
}
|
||||
|
||||
func TestFlashErr(t *testing.T) {
|
||||
f := ui.NewFlash(ui.NewApp(""), "YO!")
|
||||
f := newFlash()
|
||||
|
||||
f.Err(errors.New("Blee"))
|
||||
assert.Equal(t, "😡 Blee\n", f.GetText(false))
|
||||
|
||||
f.Errf("Blee %s", "duh")
|
||||
assert.Equal(t, "😡 Blee duh\n", f.GetText(false))
|
||||
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Helpers...
|
||||
|
||||
func newFlash() *ui.Flash {
|
||||
f := ui.NewFlash(ui.NewApp(""), "YO!")
|
||||
f.TestMode()
|
||||
return f
|
||||
}
|
||||
|
|
|
|||
|
|
@ -314,10 +314,11 @@ func (a *App) Status(l ui.FlashLevel, msg string) {
|
|||
a.Draw()
|
||||
}
|
||||
|
||||
// ClearStatus reset log back to normal.
|
||||
// ClearStatus reset logo back to normal.
|
||||
func (a *App) ClearStatus(flash bool) {
|
||||
a.Logo().Reset()
|
||||
if flash {
|
||||
log.Debug().Msgf("FLASH CLEARED!!")
|
||||
a.Flash().Clear()
|
||||
}
|
||||
a.Draw()
|
||||
|
|
|
|||
|
|
@ -92,7 +92,6 @@ func (b *Browser) SetInstance(path string) {
|
|||
func (b *Browser) Start() {
|
||||
b.Stop()
|
||||
|
||||
b.App().Status(ui.FlashInfo, "Loading...")
|
||||
b.Table.Start()
|
||||
ctx := b.defaultContext()
|
||||
ctx, b.cancelFn = context.WithCancel(ctx)
|
||||
|
|
@ -141,7 +140,7 @@ func (b *Browser) TableDataChanged(data render.TableData) {
|
|||
b.app.QueueUpdateDraw(func() {
|
||||
b.refreshActions()
|
||||
b.Update(data)
|
||||
b.App().ClearStatus(true)
|
||||
b.App().ClearStatus(false)
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -244,7 +243,7 @@ func (b *Browser) deleteCmd(evt *tcell.EventKey) *tcell.EventKey {
|
|||
b.Stop()
|
||||
defer b.Start()
|
||||
{
|
||||
msg := fmt.Sprintf("Delete %s %s?", b.gvr.ToR(), selections[0])
|
||||
msg := fmt.Sprintf("Delete %s %s?", b.gvr.R(), selections[0])
|
||||
if len(selections) > 1 {
|
||||
msg = fmt.Sprintf("Delete %d marked %s?", len(selections), b.gvr)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -43,13 +43,32 @@ func (c *Command) Init() error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func allowedXRay(gvr client.GVR) bool {
|
||||
gg := []string{
|
||||
"v1/pods",
|
||||
"v1/services",
|
||||
"apps/v1/deployments",
|
||||
"apps/v1/daemonsets",
|
||||
"apps/v1/statefulsets",
|
||||
"apps/v1/replicasets",
|
||||
}
|
||||
|
||||
for _, g := range gg {
|
||||
if g == gvr.String() {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (c *Command) xrayCmd(cmd string) error {
|
||||
tokens := strings.Split(cmd, " ")
|
||||
if len(tokens) < 2 {
|
||||
return errors.New("You must specify a resource")
|
||||
}
|
||||
gvr, ok := c.alias.AsGVR(tokens[1])
|
||||
if !ok {
|
||||
if !ok || !allowedXRay(gvr) {
|
||||
return fmt.Errorf("Huh? `%s` Command not found", cmd)
|
||||
}
|
||||
return c.exec(cmd, "xrays", NewXray(gvr), true)
|
||||
|
|
@ -172,8 +191,7 @@ func (c *Command) exec(cmd, gvr string, comp model.Component, clearStack bool) e
|
|||
if comp == nil {
|
||||
return fmt.Errorf("No component given for %s", gvr)
|
||||
}
|
||||
|
||||
c.app.Flash().Infof("Running command %s", cmd)
|
||||
c.app.Flash().Infof("Viewing %s...", client.NewGVR(gvr).R())
|
||||
c.app.Config.SetActiveView(cmd)
|
||||
if err := c.app.Config.Save(); err != nil {
|
||||
log.Error().Err(err).Msg("Config save failed!")
|
||||
|
|
|
|||
|
|
@ -65,7 +65,7 @@ func (n *Node) viewCmd(evt *tcell.EventKey) *tcell.EventKey {
|
|||
}
|
||||
|
||||
sel := n.GetTable().GetSelectedItem()
|
||||
gvr := client.NewGVR(n.GVR()).AsGVR()
|
||||
gvr := client.NewGVR(n.GVR()).GVR()
|
||||
o, err := n.App().factory.Client().DynDialOrDie().Resource(gvr).Get(sel, metav1.GetOptions{})
|
||||
if err != nil {
|
||||
n.App().Flash().Errf("Unable to get resource %q -- %s", n.GVR(), err)
|
||||
|
|
|
|||
|
|
@ -77,9 +77,9 @@ func (x *Xray) Init(ctx context.Context) error {
|
|||
x.SetBackgroundColor(config.AsColor(x.app.Styles.GetTable().BgColor))
|
||||
x.SetBorderColor(config.AsColor(x.app.Styles.GetTable().FgColor))
|
||||
x.SetBorderFocusColor(config.AsColor(x.app.Styles.Frame().Border.FocusColor))
|
||||
x.SetTitle(fmt.Sprintf(" %s-%s ", xrayTitle, strings.Title(x.gvr.ToR())))
|
||||
x.SetTitle(fmt.Sprintf(" %s-%s ", xrayTitle, strings.Title(x.gvr.R())))
|
||||
x.SetGraphics(true)
|
||||
x.SetGraphicsColor(tcell.ColorDimGray)
|
||||
x.SetGraphicsColor(tcell.ColorFloralWhite)
|
||||
x.SetInputCapture(x.keyboard)
|
||||
|
||||
x.model.SetRefreshRate(time.Duration(x.app.Config.K9s.GetRefreshRate()) * time.Second)
|
||||
|
|
@ -370,7 +370,7 @@ func (x *Xray) editCmd(evt *tcell.EventKey) *tcell.EventKey {
|
|||
ns, n := client.Namespaced(ref.Path)
|
||||
args := make([]string, 0, 10)
|
||||
args = append(args, "edit")
|
||||
args = append(args, client.NewGVR(ref.GVR).ToR())
|
||||
args = append(args, client.NewGVR(ref.GVR).R())
|
||||
args = append(args, "-n", ns)
|
||||
args = append(args, "--context", x.app.Config.K9s.CurrentContext)
|
||||
if cfg := x.app.Conn().Config().Flags().KubeConfig; cfg != nil && *cfg != "" {
|
||||
|
|
@ -450,7 +450,7 @@ func (x *Xray) gotoCmd(evt *tcell.EventKey) *tcell.EventKey {
|
|||
if len(strings.Split(ref.Path, "/")) == 1 {
|
||||
return nil
|
||||
}
|
||||
if err := x.app.viewResource(client.NewGVR(ref.GVR).ToR(), ref.Path, false); err != nil {
|
||||
if err := x.app.viewResource(client.NewGVR(ref.GVR).R(), ref.Path, false); err != nil {
|
||||
x.app.Flash().Err(err)
|
||||
}
|
||||
|
||||
|
|
@ -632,11 +632,14 @@ func (x *Xray) App() *App {
|
|||
|
||||
// UpdateTitle updates the view title.
|
||||
func (x *Xray) UpdateTitle() {
|
||||
x.SetTitle(x.styleTitle())
|
||||
t := x.styleTitle()
|
||||
x.app.QueueUpdateDraw(func() {
|
||||
x.SetTitle(t)
|
||||
})
|
||||
}
|
||||
|
||||
func (x *Xray) styleTitle() string {
|
||||
base := fmt.Sprintf("%s-%s", xrayTitle, strings.Title(x.gvr.ToR()))
|
||||
base := fmt.Sprintf("%s-%s", xrayTitle, strings.Title(x.gvr.R()))
|
||||
ns := x.model.GetNamespace()
|
||||
if client.IsAllNamespaces(ns) {
|
||||
ns = client.NamespaceAll
|
||||
|
|
|
|||
|
|
@ -38,6 +38,9 @@ func NewFactory(client client.Connection) *Factory {
|
|||
|
||||
// Start initializes the informers until caller cancels the context.
|
||||
func (f *Factory) Start(ns string) {
|
||||
f.mx.Lock()
|
||||
defer f.mx.Unlock()
|
||||
|
||||
log.Debug().Msgf("Factory START with ns `%q", ns)
|
||||
f.stopChan = make(chan struct{})
|
||||
for ns, fac := range f.factories {
|
||||
|
|
@ -48,13 +51,13 @@ func (f *Factory) Start(ns string) {
|
|||
|
||||
// Terminate terminates all watchers and forwards.
|
||||
func (f *Factory) Terminate() {
|
||||
f.mx.Lock()
|
||||
defer f.mx.Unlock()
|
||||
|
||||
if f.stopChan != nil {
|
||||
close(f.stopChan)
|
||||
f.stopChan = nil
|
||||
}
|
||||
|
||||
f.mx.Lock()
|
||||
defer f.mx.Unlock()
|
||||
for k := range f.factories {
|
||||
delete(f.factories, k)
|
||||
}
|
||||
|
|
@ -179,6 +182,9 @@ func (f *Factory) ForResource(ns, gvr string) informers.GenericInformer {
|
|||
log.Error().Err(fmt.Errorf("MEOW! No informer for %q:%q", ns, gvr))
|
||||
return inf
|
||||
}
|
||||
|
||||
f.mx.RLock()
|
||||
defer f.mx.RUnlock()
|
||||
fact.Start(f.stopChan)
|
||||
|
||||
return inf
|
||||
|
|
@ -193,7 +199,6 @@ func (f *Factory) ensureFactory(ns string) di.DynamicSharedInformerFactory {
|
|||
if fac, ok := f.factories[ns]; ok {
|
||||
return fac
|
||||
}
|
||||
log.Debug().Msgf("FACTORY_CREATE for ns %q", ns)
|
||||
f.factories[ns] = di.NewFilteredDynamicSharedInformerFactory(
|
||||
f.client.DynDialOrDie(),
|
||||
defaultResync,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,80 @@
|
|||
package xray
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/derailed/k9s/internal/client"
|
||||
"github.com/derailed/k9s/internal/render"
|
||||
appsv1 "k8s.io/api/apps/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
)
|
||||
|
||||
// ReplicaSet represents an xray renderer.
|
||||
type ReplicaSet struct{}
|
||||
|
||||
// Render renders an xray node.
|
||||
func (r *ReplicaSet) Render(ctx context.Context, ns string, o interface{}) error {
|
||||
raw, ok := o.(*unstructured.Unstructured)
|
||||
if !ok {
|
||||
return fmt.Errorf("Expected Unstructured, but got %T", o)
|
||||
}
|
||||
var rs appsv1.ReplicaSet
|
||||
err := runtime.DefaultUnstructuredConverter.FromUnstructured(raw.Object, &rs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
parent, ok := ctx.Value(KeyParent).(*TreeNode)
|
||||
if !ok {
|
||||
return fmt.Errorf("Expecting a TreeNode but got %T", ctx.Value(KeyParent))
|
||||
}
|
||||
|
||||
root := NewTreeNode("apps/v1/replicasets", client.FQN(rs.Namespace, rs.Name))
|
||||
oo, err := locatePods(ctx, rs.Namespace, rs.Spec.Selector)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ctx = context.WithValue(ctx, KeyParent, root)
|
||||
var re Pod
|
||||
for _, o := range oo {
|
||||
p, ok := o.(*unstructured.Unstructured)
|
||||
if !ok {
|
||||
return fmt.Errorf("expecting *Unstructured but got %T", o)
|
||||
}
|
||||
if err := re.Render(ctx, ns, &render.PodWithMetrics{Raw: p}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if root.IsLeaf() {
|
||||
return nil
|
||||
}
|
||||
|
||||
gvr, nsID := "v1/namespaces", client.FQN(client.ClusterScope, rs.Namespace)
|
||||
nsn := parent.Find(gvr, nsID)
|
||||
if nsn == nil {
|
||||
nsn = NewTreeNode(gvr, nsID)
|
||||
parent.Add(nsn)
|
||||
}
|
||||
nsn.Add(root)
|
||||
|
||||
return r.validate(root, rs)
|
||||
}
|
||||
|
||||
func (*ReplicaSet) validate(root *TreeNode, rs appsv1.ReplicaSet) error {
|
||||
root.Extras[StatusKey] = OkStatus
|
||||
var r int32
|
||||
if rs.Spec.Replicas != nil {
|
||||
r = int32(*rs.Spec.Replicas)
|
||||
}
|
||||
a := rs.Status.Replicas
|
||||
if a != r {
|
||||
root.Extras[StatusKey] = ToastStatus
|
||||
}
|
||||
root.Extras[InfoKey] = fmt.Sprintf("%d/%d", a, r)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
package xray_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/derailed/k9s/internal"
|
||||
"github.com/derailed/k9s/internal/xray"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
)
|
||||
|
||||
func TestReplicaSetRender(t *testing.T) {
|
||||
uu := map[string]struct {
|
||||
file string
|
||||
level1, level2 int
|
||||
status string
|
||||
}{
|
||||
"plain": {
|
||||
file: "rs",
|
||||
level1: 1,
|
||||
level2: 1,
|
||||
status: xray.OkStatus,
|
||||
},
|
||||
}
|
||||
|
||||
var re xray.ReplicaSet
|
||||
for k := range uu {
|
||||
u := uu[k]
|
||||
t.Run(k, func(t *testing.T) {
|
||||
f := makeFactory()
|
||||
f.rows = map[string][]runtime.Object{"v1/pods": {load(t, "po")}}
|
||||
|
||||
o := load(t, u.file)
|
||||
root := xray.NewTreeNode("replicasets", "replicasets")
|
||||
ctx := context.WithValue(context.Background(), xray.KeyParent, root)
|
||||
ctx = context.WithValue(ctx, internal.KeyFactory, f)
|
||||
|
||||
assert.Nil(t, re.Render(ctx, "", o))
|
||||
assert.Equal(t, u.level1, root.CountChildren())
|
||||
assert.Equal(t, u.level2, root.Children[0].CountChildren())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -37,7 +37,7 @@ func (s *Service) Render(ctx context.Context, ns string, o interface{}) error {
|
|||
return fmt.Errorf("Expecting a TreeNode but got %T", ctx.Value(KeyParent))
|
||||
}
|
||||
|
||||
root := NewTreeNode("apps/v1/services", client.FQN(svc.Namespace, svc.Name))
|
||||
root := NewTreeNode("v1/services", client.FQN(svc.Namespace, svc.Name))
|
||||
oo, err := s.locatePods(ctx, svc.Namespace, svc.Spec.Selector)
|
||||
if err != nil {
|
||||
return err
|
||||
|
|
|
|||
|
|
@ -0,0 +1,122 @@
|
|||
{
|
||||
"apiVersion": "apps/v1",
|
||||
"kind": "ReplicaSet",
|
||||
"metadata": {
|
||||
"annotations": {
|
||||
"deployment.kubernetes.io/desired-replicas": "1",
|
||||
"deployment.kubernetes.io/max-replicas": "2",
|
||||
"deployment.kubernetes.io/revision": "2"
|
||||
},
|
||||
"creationTimestamp": "2020-01-20T01:34:11Z",
|
||||
"generation": 1,
|
||||
"labels": {
|
||||
"app": "nginx-pv",
|
||||
"pod-template-hash": "6476d7d5c8"
|
||||
},
|
||||
"name": "nginx-pv-6476d7d5c8",
|
||||
"namespace": "default",
|
||||
"ownerReferences": [
|
||||
{
|
||||
"apiVersion": "apps/v1",
|
||||
"blockOwnerDeletion": true,
|
||||
"controller": true,
|
||||
"kind": "Deployment",
|
||||
"name": "nginx-pv",
|
||||
"uid": "68aa70ff-ff7c-4a67-8d4f-fc31ef27ec35"
|
||||
}
|
||||
],
|
||||
"resourceVersion": "3743997",
|
||||
"selfLink": "/apis/apps/v1/namespaces/default/replicasets/nginx-pv-6476d7d5c8",
|
||||
"uid": "547a036d-94d9-4818-bd9e-ec2939019471"
|
||||
},
|
||||
"spec": {
|
||||
"replicas": 1,
|
||||
"selector": {
|
||||
"matchLabels": {
|
||||
"app": "nginx-pv",
|
||||
"pod-template-hash": "6476d7d5c8"
|
||||
}
|
||||
},
|
||||
"template": {
|
||||
"metadata": {
|
||||
"creationTimestamp": null,
|
||||
"labels": {
|
||||
"app": "nginx-pv",
|
||||
"pod-template-hash": "6476d7d5c8"
|
||||
}
|
||||
},
|
||||
"spec": {
|
||||
"automountServiceAccountToken": true,
|
||||
"containers": [
|
||||
{
|
||||
"env": [
|
||||
{
|
||||
"name": "FRED",
|
||||
"valueFrom": {
|
||||
"configMapKeyRef": {
|
||||
"key": "fred",
|
||||
"name": "busy"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "PROPS",
|
||||
"valueFrom": {
|
||||
"configMapKeyRef": {
|
||||
"key": "props",
|
||||
"name": "busy"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"image": "k8s.gcr.io/nginx-slim:0.8",
|
||||
"imagePullPolicy": "IfNotPresent",
|
||||
"name": "nginx",
|
||||
"ports": [
|
||||
{
|
||||
"containerPort": 80,
|
||||
"protocol": "TCP"
|
||||
}
|
||||
],
|
||||
"resources": {
|
||||
"limits": {
|
||||
"cpu": "100m",
|
||||
"memory": "200Mi"
|
||||
}
|
||||
},
|
||||
"terminationMessagePath": "/dev/termination-log",
|
||||
"terminationMessagePolicy": "File",
|
||||
"volumeMounts": [
|
||||
{
|
||||
"mountPath": "/usr/share/nginx/html",
|
||||
"name": "index"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"dnsPolicy": "ClusterFirst",
|
||||
"restartPolicy": "Always",
|
||||
"schedulerName": "default-scheduler",
|
||||
"securityContext": {},
|
||||
"serviceAccount": "zorg",
|
||||
"serviceAccountName": "zorg",
|
||||
"terminationGracePeriodSeconds": 30,
|
||||
"volumes": [
|
||||
{
|
||||
"name": "index",
|
||||
"persistentVolumeClaim": {
|
||||
"claimName": "web"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"status": {
|
||||
"availableReplicas": 1,
|
||||
"fullyLabeledReplicas": 1,
|
||||
"observedGeneration": 1,
|
||||
"readyReplicas": 1,
|
||||
"replicas": 1
|
||||
}
|
||||
}
|
||||
|
|
@ -7,6 +7,7 @@ import (
|
|||
"strings"
|
||||
|
||||
"github.com/derailed/k9s/internal/client"
|
||||
"github.com/derailed/k9s/internal/dao"
|
||||
"github.com/rs/zerolog/log"
|
||||
"vbom.ml/util/sortorder"
|
||||
)
|
||||
|
|
@ -295,15 +296,7 @@ func (t *TreeNode) Find(gvr, id string) *TreeNode {
|
|||
|
||||
// Title computes the node title.
|
||||
func (t *TreeNode) Title() string {
|
||||
const withNS = "[white::b]%s[-::d]"
|
||||
|
||||
title := fmt.Sprintf(withNS, t.AsString())
|
||||
|
||||
if t.CountChildren() > 0 {
|
||||
title += fmt.Sprintf("([white::d]%d[-::d])[-::-]", t.CountChildren())
|
||||
}
|
||||
|
||||
return title
|
||||
return t.toTitle()
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
|
@ -341,53 +334,55 @@ func dumpStdOut(n *TreeNode, level int) {
|
|||
}
|
||||
}
|
||||
|
||||
func toEmoji(gvr string) string {
|
||||
switch gvr {
|
||||
case "v1/pods":
|
||||
return "🚛"
|
||||
case "apps/v1/deployments":
|
||||
return "🪂"
|
||||
case "apps/v1/statefulset":
|
||||
return "🎎"
|
||||
case "apps/v1/daemonsets":
|
||||
return "😈"
|
||||
case "containers":
|
||||
return "🐳"
|
||||
case "v1/serviceaccounts":
|
||||
return "💁♀️"
|
||||
case "v1/persistentvolumes":
|
||||
return "📚"
|
||||
case "v1/persistentvolumeclaims":
|
||||
return "🎟"
|
||||
case "v1/secrets":
|
||||
return "🔒"
|
||||
case "v1/configmaps":
|
||||
return "🔑"
|
||||
default:
|
||||
return "📎"
|
||||
func category(gvr string) string {
|
||||
meta, err := dao.MetaFor(client.NewGVR(gvr))
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
return meta.SingularName
|
||||
}
|
||||
|
||||
// AsString transforms a node as a string for viewing.
|
||||
func (t TreeNode) AsString() string {
|
||||
const colorFmt = "%s [gray::-][%s[gray::-]] [%s::b]%s[::]"
|
||||
const (
|
||||
titleFmt = " [gray::-]%s/[white::b][%s::b]%s[::]"
|
||||
topTitleFmt = " [white::b][%s::b]%s[::]"
|
||||
toast = "TOAST"
|
||||
)
|
||||
|
||||
func (t TreeNode) toTitle() (title string) {
|
||||
_, n := client.Namespaced(t.ID)
|
||||
color, flag := "white", "[green::b]OK"
|
||||
color, status := "white", "OK"
|
||||
if v, ok := t.Extras[StatusKey]; ok {
|
||||
switch v {
|
||||
case ToastStatus:
|
||||
color, flag = "orangered", "[red::b]TOAST"
|
||||
color, status = "orangered", toast
|
||||
case MissingRefStatus:
|
||||
color, flag = "orange", "[orange::b]TOAST_REF"
|
||||
color, status = "orange", toast+"_REF"
|
||||
}
|
||||
}
|
||||
str := fmt.Sprintf(colorFmt, toEmoji(t.GVR), flag, color, n)
|
||||
|
||||
i, ok := t.Extras[InfoKey]
|
||||
defer func() {
|
||||
if status != "OK" {
|
||||
title += fmt.Sprintf(" [gray::-][[%s::b]%s[gray::-]]", color, status)
|
||||
}
|
||||
}()
|
||||
|
||||
categ := category(t.GVR)
|
||||
if categ == "" {
|
||||
title = fmt.Sprintf(topTitleFmt, color, n)
|
||||
} else {
|
||||
title = fmt.Sprintf(titleFmt, categ, color, n)
|
||||
}
|
||||
|
||||
if !t.IsLeaf() {
|
||||
title += fmt.Sprintf("[white::d](%d[-::d])[-::-]", t.CountChildren())
|
||||
}
|
||||
|
||||
info, ok := t.Extras[InfoKey]
|
||||
if !ok {
|
||||
return str
|
||||
return
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%s [antiquewhite::][%s][::] ", str, i)
|
||||
title += fmt.Sprintf(" [antiquewhite::][%s][::]", info)
|
||||
return
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue