added pdb + fix ev view + misc bugs

mine
derailed 2019-03-21 23:27:48 -06:00
parent fde9b0e981
commit 9138ec06b0
15 changed files with 478 additions and 43 deletions

View File

@ -272,5 +272,5 @@ to make this project a reality!
---
<img src="assets/imhotep_logo.png" width="32" height="auto"/> © 2018 Imhotep Software LLC.
<img src="assets/imhotep_logo.png" width="32" height="auto"/> © 2019 Imhotep Software LLC.
All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)

42
internal/k8s/pdb.go Normal file
View File

@ -0,0 +1,42 @@
package k8s
import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
// PodDisruptionBudget represents a PodDisruptionBudget Kubernetes resource.
type PodDisruptionBudget struct{}
// NewPodDisruptionBudget returns a new PodDisruptionBudget.
func NewPodDisruptionBudget() Res {
return &PodDisruptionBudget{}
}
// Get a pdb.
func (*PodDisruptionBudget) Get(ns, n string) (interface{}, error) {
opts := metav1.GetOptions{}
return conn.dialOrDie().PolicyV1beta1().PodDisruptionBudgets(ns).Get(n, opts)
}
// List all pdbs in a given namespace.
func (*PodDisruptionBudget) List(ns string) (Collection, error) {
opts := metav1.ListOptions{}
rr, err := conn.dialOrDie().PolicyV1beta1().PodDisruptionBudgets(ns).List(opts)
if err != nil {
return Collection{}, err
}
cc := make(Collection, len(rr.Items))
for i, r := range rr.Items {
cc[i] = r
}
return cc, nil
}
// Delete a pdb.
func (*PodDisruptionBudget) Delete(ns, n string) error {
opts := metav1.DeleteOptions{}
return conn.dialOrDie().PolicyV1beta1().PodDisruptionBudgets(ns).Delete(n, &opts)
}

View File

@ -88,26 +88,26 @@ func (r *Event) Delete(path string) error {
// Header return resource header.
func (*Event) Header(ns string) Row {
ff := Row{""}
var ff Row
if ns == AllNamespaces {
ff = append(ff, "NAMESPACE")
}
return append(ff, "NAME", "REASON", "SOURCE", "COUNT", "MESSAGE", "AGE")
}
var rx = regexp.MustCompile(`(.+)\.(.+)`)
// Fields returns display fields.
func (r *Event) Fields(ns string) Row {
ff := make(Row, 0, len(r.Header(ns)))
i := r.instance
ff = append(ff, r.toEmoji(i.Type, i.Reason))
if ns == AllNamespaces {
ff = append(ff, i.Namespace)
}
rx := regexp.MustCompile(`(.+)\.(.+)`)
return append(ff,
// i.Name,
rx.ReplaceAllString(i.Name, `$1`),
i.Name,
i.Reason,
i.Source.Component,
strconv.Itoa(int(i.Count)),

View File

@ -24,12 +24,12 @@ func TestEventListAccess(t *testing.T) {
}
func TestEventHeader(t *testing.T) {
assert.Equal(t, resource.Row{"", "NAME", "REASON", "SOURCE", "COUNT", "MESSAGE", "AGE"}, newEvent().Header(resource.DefaultNamespace))
assert.Equal(t, resource.Row{"NAME", "REASON", "SOURCE", "COUNT", "MESSAGE", "AGE"}, newEvent().Header(resource.DefaultNamespace))
}
func TestEventFields(t *testing.T) {
r := newEvent().Fields("blee")
assert.Equal(t, resource.Row{"😮", "fred", "blah", "", "1"}, r[:5])
assert.Equal(t, resource.Row{"fred", "blah", "", "1"}, r[:4])
}
func TestEventMarshal(t *testing.T) {
@ -64,11 +64,11 @@ func TestEventListData(t *testing.T) {
assert.Equal(t, resource.NotNamespaced, l.GetNamespace())
assert.False(t, l.HasXRay())
row := td.Rows["blee/fred"]
assert.Equal(t, 7, len(row.Deltas))
assert.Equal(t, 6, len(row.Deltas))
for _, d := range row.Deltas {
assert.Equal(t, "", d)
}
assert.Equal(t, resource.Row{"😮"}, row.Fields[:1])
assert.Equal(t, resource.Row{"fred"}, row.Fields[:1])
}
func TestEventListDescribe(t *testing.T) {

129
internal/resource/pdb.go Normal file
View File

@ -0,0 +1,129 @@
package resource
import (
"strconv"
"github.com/derailed/k9s/internal/k8s"
"github.com/rs/zerolog/log"
v1beta1 "k8s.io/api/policy/v1beta1"
)
// PodDisruptionBudget that can be displayed in a table and interacted with.
type PodDisruptionBudget struct {
*Base
instance *v1beta1.PodDisruptionBudget
}
// NewPDBList returns a new resource list.
func NewPDBList(ns string) List {
return NewPDBListWithArgs(ns, NewPDB())
}
// NewPDBListWithArgs returns a new resource list.
func NewPDBListWithArgs(ns string, res Resource) List {
return newList(ns, "pdb", res, AllVerbsAccess|DescribeAccess)
}
// NewPDB returns a new PodDisruptionBudget instance.
func NewPDB() *PodDisruptionBudget {
return NewPDBWithArgs(k8s.NewPodDisruptionBudget())
}
// NewPDBWithArgs returns a new Pod instance.
func NewPDBWithArgs(r k8s.Res) *PodDisruptionBudget {
p := &PodDisruptionBudget{
Base: &Base{
caller: r,
},
}
p.creator = p
return p
}
// NewInstance builds a new PodDisruptionBudget instance from a k8s resource.
func (r *PodDisruptionBudget) NewInstance(i interface{}) Columnar {
pdb := NewPDB()
switch i.(type) {
case *v1beta1.PodDisruptionBudget:
pdb.instance = i.(*v1beta1.PodDisruptionBudget)
case v1beta1.PodDisruptionBudget:
ii := i.(v1beta1.PodDisruptionBudget)
pdb.instance = &ii
case *interface{}:
ptr := *i.(*interface{})
pdbi := ptr.(v1beta1.PodDisruptionBudget)
pdb.instance = &pdbi
default:
log.Fatal().Msgf("Unknown %#v", i)
}
pdb.path = r.namespacedName(pdb.instance.ObjectMeta)
return pdb
}
// Marshal resource to yaml.
func (r *PodDisruptionBudget) Marshal(path string) (string, error) {
ns, n := namespaced(path)
i, err := r.caller.Get(ns, n)
if err != nil {
return "", err
}
pdb := i.(*v1beta1.PodDisruptionBudget)
pdb.TypeMeta.APIVersion = "v1beta1"
pdb.TypeMeta.Kind = "PodDisruptionBudget"
return r.marshalObject(pdb)
}
// Header return resource header.
func (*PodDisruptionBudget) Header(ns string) Row {
hh := Row{}
if ns == AllNamespaces {
hh = append(hh, "NAMESPACE")
}
return append(hh,
"NAME",
"MIN AVAILABLE",
"MAX_ UNAVAILABLE",
"ALLOWED DISRUPTIONS",
"CURRENT",
"DESIRED",
"EXPECTED",
"AGE",
)
}
// Fields retrieves displayable fields.
func (r *PodDisruptionBudget) Fields(ns string) Row {
ff := make(Row, 0, len(r.Header(ns)))
i := r.instance
if ns == AllNamespaces {
ff = append(ff, i.Namespace)
}
min := NAValue
if i.Spec.MinAvailable != nil {
min = strconv.Itoa(int(i.Spec.MinAvailable.IntVal))
}
max := NAValue
if i.Spec.MaxUnavailable != nil {
max = strconv.Itoa(int(i.Spec.MaxUnavailable.IntVal))
}
return append(ff,
i.Name,
min,
max,
strconv.Itoa(int(i.Status.PodDisruptionsAllowed)),
strconv.Itoa(int(i.Status.CurrentHealthy)),
strconv.Itoa(int(i.Status.DesiredHealthy)),
strconv.Itoa(int(i.Status.ExpectedPods)),
toAge(i.ObjectMeta.CreationTimestamp),
)
}
// ExtFields returns extra info about the resource.
func (r *PodDisruptionBudget) ExtFields() Properties {
return nil
}

View File

@ -0,0 +1,128 @@
package resource_test
import (
"testing"
"github.com/derailed/k9s/internal/k8s"
"github.com/derailed/k9s/internal/resource"
m "github.com/petergtz/pegomock"
"github.com/stretchr/testify/assert"
v1beta1 "k8s.io/api/policy/v1beta1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
func TestPDBListAccess(t *testing.T) {
ns := "blee"
l := resource.NewPDBList(resource.AllNamespaces)
l.SetNamespace(ns)
assert.Equal(t, ns, l.GetNamespace())
assert.Equal(t, "pdb", l.GetName())
for _, a := range []int{resource.GetAccess, resource.ListAccess, resource.DeleteAccess, resource.ViewAccess, resource.EditAccess} {
assert.True(t, l.Access(a))
}
}
func TestPDBHeader(t *testing.T) {
row := resource.Row{
"NAME",
"MIN AVAILABLE",
"MAX_ UNAVAILABLE",
"ALLOWED DISRUPTIONS",
"CURRENT",
"DESIRED",
"EXPECTED",
"AGE",
}
assert.Equal(t, row, newPDB().Header(resource.DefaultNamespace))
}
func TestPDBFields(t *testing.T) {
r := newPDB().Fields("blee")
assert.Equal(t, "fred", r[0])
}
func TestPDBMarshal(t *testing.T) {
setup(t)
ca := NewMockCaller()
m.When(ca.Get("blee", "fred")).ThenReturn(k8sPDB(), nil)
cm := resource.NewPDBWithArgs(ca)
ma, err := cm.Marshal("blee/fred")
ca.VerifyWasCalledOnce().Get("blee", "fred")
assert.Nil(t, err)
assert.Equal(t, pdbYaml(), ma)
}
func TestPDBListData(t *testing.T) {
setup(t)
ca := NewMockCaller()
m.When(ca.List(resource.NotNamespaced)).ThenReturn(k8s.Collection{*k8sPDB()}, nil)
l := resource.NewPDBListWithArgs("-", resource.NewPDBWithArgs(ca))
// Make sure we can get deltas!
for i := 0; i < 2; i++ {
err := l.Reconcile()
assert.Nil(t, err)
}
ca.VerifyWasCalled(m.Times(2)).List(resource.NotNamespaced)
td := l.Data()
assert.Equal(t, 1, len(td.Rows))
assert.Equal(t, resource.NotNamespaced, l.GetNamespace())
assert.False(t, l.HasXRay())
row := td.Rows["blee/fred"]
assert.Equal(t, 8, len(row.Deltas))
for _, d := range row.Deltas {
assert.Equal(t, "", d)
}
assert.Equal(t, resource.Row{"fred"}, row.Fields[:1])
}
func TestPDBListDescribe(t *testing.T) {
setup(t)
ca := NewMockCaller()
m.When(ca.Get("blee", "fred")).ThenReturn(k8sPDB(), nil)
l := resource.NewPDBListWithArgs("blee", resource.NewPDBWithArgs(ca))
props, err := l.Describe("blee/fred")
ca.VerifyWasCalledOnce().Get("blee", "fred")
assert.Nil(t, err)
assert.Equal(t, 0, len(props))
}
// Helpers...
func k8sPDB() *v1beta1.PodDisruptionBudget {
return &v1beta1.PodDisruptionBudget{
ObjectMeta: metav1.ObjectMeta{
Namespace: "blee",
Name: "fred",
CreationTimestamp: metav1.Time{Time: testTime()},
},
Spec: v1beta1.PodDisruptionBudgetSpec{},
}
}
func newPDB() resource.Columnar {
return resource.NewPDB().NewInstance(k8sPDB())
}
func pdbYaml() string {
return `apiVersion: v1beta1
kind: PodDisruptionBudget
metadata:
creationTimestamp: "2018-12-14T17:36:43Z"
name: fred
namespace: blee
spec: {}
status:
currentHealthy: 0
desiredHealthy: 0
disruptionsAllowed: 0
expectedPods: 0
`
}

View File

@ -142,7 +142,8 @@ func (r *Pod) Logs(c chan<- string, ns, n, co string, lines int64, prev bool) (c
stream, err := req.Stream()
blocked = false
if err != nil {
return cancel, fmt.Errorf("Log tail request failed for pod `%s/%s:%s", ns, n, co)
log.Error().Msgf("Tail logs failed `%s/%s:%s -- %v", ns, n, co, err)
return cancel, fmt.Errorf("%v", err)
}
go func() {

View File

@ -84,6 +84,7 @@ func NewApp() *appView {
v.actions[tcell.KeyEnter] = newKeyAction("Goto", v.gotoCmd, false)
v.actions[tcell.KeyBackspace2] = newKeyAction("Erase", v.eraseCmd, false)
v.actions[tcell.KeyBackspace] = newKeyAction("Erase", v.eraseCmd, false)
v.actions[tcell.KeyDelete] = newKeyAction("Erase", v.eraseCmd, false)
v.actions[tcell.KeyTab] = newKeyAction("Focus", v.focusCmd, false)
return &v

View File

@ -106,6 +106,22 @@ func pvcColorer(ns string, r *resource.RowEvent) tcell.Color {
return c
}
func pdbColorer(ns string, r *resource.RowEvent) tcell.Color {
c := defaultColorer(ns, r)
if r.Action == watch.Added || r.Action == watch.Modified {
return c
}
markCol := 5
if ns != resource.AllNamespaces {
markCol = 4
}
if strings.TrimSpace(r.Fields[markCol]) != strings.TrimSpace(r.Fields[markCol+1]) {
return errColor
}
return stdColor
}
func dpColorer(ns string, r *resource.RowEvent) tcell.Color {
c := defaultColorer(ns, r)
if r.Action == watch.Added || r.Action == watch.Modified {

View File

@ -160,6 +160,35 @@ func TestDpColorer(t *testing.T) {
}
}
func TestPdbColorer(t *testing.T) {
var (
ns = resource.Row{"blee", "fred", "1", "1", "1", "1", "1"}
nonNS = ns[1:]
bustNS = resource.Row{"blee", "fred", "1", "1", "1", "1", "2"}
bustNoNS = bustNS[1:]
)
uu := colorerUCs{
// Add AllNS
{"", &resource.RowEvent{Action: watch.Added, Fields: ns}, addColor},
// Add NS
{"blee", &resource.RowEvent{Action: watch.Added, Fields: nonNS}, addColor},
// Mod AllNS
{"", &resource.RowEvent{Action: watch.Modified, Fields: ns}, modColor},
// Mod NS
{"blee", &resource.RowEvent{Action: watch.Modified, Fields: nonNS}, modColor},
// Unchanged cool
{"", &resource.RowEvent{Action: resource.Unchanged, Fields: ns}, stdColor},
// Bust AllNS
{"", &resource.RowEvent{Action: resource.Unchanged, Fields: bustNS}, errColor},
// Bust NS
{"blee", &resource.RowEvent{Action: resource.Unchanged, Fields: bustNoNS}, errColor},
}
for _, u := range uu {
assert.Equal(t, u.e, pdbColorer(u.ns, u.r))
}
}
func TestPVColorer(t *testing.T) {
var (
pv = resource.Row{"blee", "1G", "RO", "Duh", "Bound"}

View File

@ -57,6 +57,7 @@ func newDetailsView(app *appView, backFn actionHandler) *detailsView {
// v.actions[tcell.KeyEnter] = newKeyAction("Search", v.searchCmd)
v.actions[tcell.KeyBackspace2] = newKeyAction("Erase", v.eraseCmd, false)
v.actions[tcell.KeyBackspace] = newKeyAction("Erase", v.eraseCmd, false)
v.actions[tcell.KeyDelete] = newKeyAction("Erase", v.eraseCmd, false)
v.actions[tcell.KeyEscape] = newKeyAction("Back", v.backCmd, true)
v.actions[tcell.KeyTab] = newKeyAction("Next Match", v.nextCmd, false)
v.actions[tcell.KeyBacktab] = newKeyAction("Previous Match", v.prevCmd, false)

View File

@ -176,6 +176,13 @@ func resourceViews() map[string]resCmd {
listFn: resource.NewNamespaceList,
colorerFn: nsColorer,
},
"pdb": {
title: "PodDiscruptionBudgets",
api: "v1.beta1",
viewFn: newResourceView,
listFn: resource.NewPDBList,
colorerFn: pdbColorer,
},
"po": {
title: "Pods",
api: "",

View File

@ -0,0 +1,67 @@
package views
import (
"sort"
"testing"
"github.com/derailed/k9s/internal/resource"
"github.com/stretchr/testify/assert"
)
func TestGroupSort(t *testing.T) {
uu := []struct {
order bool
rows []string
expect []string
}{
{true, []string{"200m", "100m"}, []string{"100m", "200m"}},
{false, []string{"200m", "100m"}, []string{"200m", "100m"}},
{true, []string{"10", "1"}, []string{"1", "10"}},
{false, []string{"10", "1"}, []string{"10", "1"}},
{true, []string{"100Mi", "10Mi"}, []string{"10Mi", "100Mi"}},
{false, []string{"100Mi", "10Mi"}, []string{"100Mi", "10Mi"}},
{true, []string{"xyz", "abc"}, []string{"abc", "xyz"}},
{false, []string{"xyz", "abc"}, []string{"xyz", "abc"}},
}
for _, u := range uu {
g := groupSorter{rows: u.rows, asc: u.order}
sort.Sort(g)
assert.Equal(t, u.expect, g.rows)
}
}
func TestRowSort(t *testing.T) {
uu := []struct {
order bool
rows resource.Rows
expect resource.Rows
}{
{
true,
resource.Rows{resource.Row{"200m"}, resource.Row{"100m"}},
resource.Rows{resource.Row{"100m"}, resource.Row{"200m"}},
},
{
false,
resource.Rows{resource.Row{"200m"}, resource.Row{"100m"}},
resource.Rows{resource.Row{"200m"}, resource.Row{"100m"}},
},
{
true,
resource.Rows{resource.Row{"200Mi"}, resource.Row{"100Mi"}},
resource.Rows{resource.Row{"100Mi"}, resource.Row{"200Mi"}},
},
{
false,
resource.Rows{resource.Row{"200Mi"}, resource.Row{"100Mi"}},
resource.Rows{resource.Row{"200Mi"}, resource.Row{"100Mi"}},
},
}
for _, u := range uu {
r := rowSorter{index: 0, rows: u.rows, asc: u.order}
sort.Sort(r)
assert.Equal(t, u.expect, r.rows)
}
}

View File

@ -17,28 +17,13 @@ type rowSorter struct {
func (s rowSorter) Len() int {
return len(s.rows)
}
func (s rowSorter) Swap(i, j int) {
s.rows[i], s.rows[j] = s.rows[j], s.rows[i]
}
func (s rowSorter) Less(i, j int) bool {
c1 := s.rows[i][s.index]
c2 := s.rows[j][s.index]
if m1, ok := isMetric(c1); ok {
m2, _ := isMetric(c2)
i1, _ := strconv.Atoi(m1)
i2, _ := strconv.Atoi(m2)
if s.asc {
return i1 < i2
}
return i1 > i2
}
c := strings.Compare(c1, c2)
if s.asc {
return c < 0
}
return c > 0
return less(s.asc, s.rows[i][s.index], s.rows[j][s.index])
}
// ----------------------------------------------------------------------------
@ -51,32 +36,59 @@ type groupSorter struct {
func (s groupSorter) Len() int {
return len(s.rows)
}
func (s groupSorter) Swap(i, j int) {
s.rows[i], s.rows[j] = s.rows[j], s.rows[i]
}
func (s groupSorter) Less(i, j int) bool {
c1 := s.rows[i]
c2 := s.rows[j]
if m1, ok := isMetric(c1); ok {
m2, _ := isMetric(c2)
i1, _ := strconv.Atoi(m1)
i2, _ := strconv.Atoi(m2)
if s.asc {
return i1 < i2
}
return i1 > i2
func (s groupSorter) Less(i, j int) bool {
return less(s.asc, s.rows[i], s.rows[j])
}
// ----------------------------------------------------------------------------
// Helpers...
func less(asc bool, c1, c2 string) bool {
if o, ok := isMetricSort(asc, c1, c2); ok {
return o
}
if o, ok := isIntegerSort(asc, c1, c2); ok {
return o
}
c := strings.Compare(c1, c2)
if s.asc {
if asc {
return c < 0
}
return c > 0
}
// ----------------------------------------------------------------------------
// Helpers...
func isMetricSort(asc bool, c1, c2 string) (bool, bool) {
m1, ok := isMetric(c1)
if !ok {
return false, false
}
m2, _ := isMetric(c2)
i1, _ := strconv.Atoi(m1)
i2, _ := strconv.Atoi(m2)
if asc {
return i1 < i2, true
}
return i1 > i2, true
}
func isIntegerSort(asc bool, c1, c2 string) (bool, bool) {
n1, err := strconv.Atoi(c1)
if err != nil {
return false, false
}
n2, _ := strconv.Atoi(c2)
if asc {
return n1 < n2, true
}
return n1 > n2, true
}
var metricRX = regexp.MustCompile(`\A(\d+)(m|Mi)\z`)

View File

@ -74,6 +74,8 @@ func newTableView(app *appView, title string) *tableView {
v.actions[tcell.KeyEnter] = newKeyAction("Filter", v.filterCmd, false)
v.actions[tcell.KeyBackspace2] = newKeyAction("Erase", v.eraseCmd, false)
v.actions[tcell.KeyBackspace] = newKeyAction("Erase", v.eraseCmd, false)
v.actions[tcell.KeyDelete] = newKeyAction("Erase", v.eraseCmd, false)
v.actions[KeyG] = newKeyAction("Top", app.puntCmd, false)
v.actions[KeyShiftG] = newKeyAction("Bottom", app.puntCmd, false)
v.actions[KeyB] = newKeyAction("Down", v.pageDownCmd, false)