From 1562b6255e39415d6dff019d9a8e37330603454b Mon Sep 17 00:00:00 2001 From: derailed Date: Mon, 25 Mar 2019 10:28:20 -0600 Subject: [PATCH] beefup tests --- internal/resource/cluster.go | 24 +- internal/resource/cluster_test.go | 66 +++- internal/resource/context.go | 35 +- internal/resource/context_test.go | 89 +++-- internal/resource/crd.go | 16 +- internal/resource/crd_test.go | 36 ++ internal/resource/cronjob.go | 2 +- internal/resource/custom_test.go | 312 ++++++++++++++++++ internal/resource/helpers.go | 8 + internal/resource/helpers_test.go | 17 + internal/resource/mock_clustermeta_test.go | 37 +++ .../resource/mock_switchableresource_test.go | 32 ++ internal/resource/pod.go | 98 +----- internal/resource/pod_int_test.go | 117 +++++++ internal/resource/rc_test.go | 114 +++++++ internal/resource/ro.go | 12 +- internal/resource/ro_int_test.go | 25 ++ internal/resource/svc.go | 8 +- internal/resource/svc_int_test.go | 72 +++- 19 files changed, 930 insertions(+), 190 deletions(-) create mode 100644 internal/resource/custom_test.go create mode 100644 internal/resource/pod_int_test.go create mode 100644 internal/resource/rc_test.go create mode 100644 internal/resource/ro_int_test.go diff --git a/internal/resource/cluster.go b/internal/resource/cluster.go index 5fd2521a..e8d2f1c9 100644 --- a/internal/resource/cluster.go +++ b/internal/resource/cluster.go @@ -16,6 +16,7 @@ type ( ContextName() string ClusterName() string UserName() string + FetchNodes() ([]v1.Node, error) } // MetricsServer gather metrics information from pods and nodes. @@ -42,8 +43,8 @@ type ( ) // NewCluster returns a new cluster info resource. -func NewCluster(c Connection, log *zerolog.Logger) *Cluster { - return NewClusterWithArgs(k8s.NewCluster(c, log), k8s.NewMetricsServer(c)) +func NewCluster(c Connection, log *zerolog.Logger, mx MetricsServer) *Cluster { + return NewClusterWithArgs(k8s.NewCluster(c, log), mx) } // NewClusterWithArgs for tests only! @@ -81,21 +82,12 @@ func (c *Cluster) Metrics(nodes []v1.Node, nmx []mv1beta1.NodeMetrics) k8s.Clust return c.mx.ClusterLoad(nodes, nmx) } -// GetNodesMetrics fetch all nodes metrics. -func (c *Cluster) GetNodesMetrics() ([]mv1beta1.NodeMetrics, error) { +// FetchNodesMetrics fetch all nodes metrics. +func (c *Cluster) FetchNodesMetrics() ([]mv1beta1.NodeMetrics, error) { return c.mx.FetchNodesMetrics() } -// GetNodes fetch all available nodes. -func (c *Cluster) GetNodes() ([]v1.Node, error) { - nn, err := k8s.NewNode(c.api).List("") - if err != nil { - return nil, err - } - nodes := make([]v1.Node, 0, len(nn)) - for _, n := range nn { - nodes = append(nodes, n.(v1.Node)) - } - - return nodes, nil +// FetchNodes fetch all available nodes. +func (c *Cluster) FetchNodes() ([]v1.Node, error) { + return c.api.FetchNodes() } diff --git a/internal/resource/cluster_test.go b/internal/resource/cluster_test.go index 92258784..0a5feadd 100644 --- a/internal/resource/cluster_test.go +++ b/internal/resource/cluster_test.go @@ -13,37 +13,77 @@ import ( ) func TestClusterVersion(t *testing.T) { - cIfc, mxIfc := NewMockClusterMeta(), NewMockMetricsServer() - m.When(cIfc.Version()).ThenReturn("1.2.3", nil) + mm, mx := NewMockClusterMeta(), NewMockMetricsServer() + m.When(mm.Version()).ThenReturn("1.2.3", nil) - ci := resource.NewClusterWithArgs(cIfc, mxIfc) + ci := resource.NewClusterWithArgs(mm, mx) assert.Equal(t, "1.2.3", ci.Version()) } func TestClusterNoVersion(t *testing.T) { - cIfc, mxIfc := NewMockClusterMeta(), NewMockMetricsServer() - m.When(cIfc.Version()).ThenReturn("bad", fmt.Errorf("No data")) + mm, mx := NewMockClusterMeta(), NewMockMetricsServer() + m.When(mm.Version()).ThenReturn("bad", fmt.Errorf("No data")) - ci := resource.NewClusterWithArgs(cIfc, mxIfc) + ci := resource.NewClusterWithArgs(mm, mx) assert.Equal(t, "n/a", ci.Version()) } func TestClusterName(t *testing.T) { - cIfc, mxIfc := NewMockClusterMeta(), NewMockMetricsServer() - m.When(cIfc.ClusterName()).ThenReturn("fred") + mm, mx := NewMockClusterMeta(), NewMockMetricsServer() + m.When(mm.ClusterName()).ThenReturn("fred") - ci := resource.NewClusterWithArgs(cIfc, mxIfc) + ci := resource.NewClusterWithArgs(mm, mx) assert.Equal(t, "fred", ci.ClusterName()) } -func TestClusterMetrics(t *testing.T) { - cIfc, mxIfc := NewMockClusterMeta(), NewMockMetricsServer() - m.When(mxIfc.ClusterLoad([]v1.Node{}, []mv1beta1.NodeMetrics{})).ThenReturn(clusterMetric()) +func TestContextName(t *testing.T) { + mm, mx := NewMockClusterMeta(), NewMockMetricsServer() + m.When(mm.ContextName()).ThenReturn("fred") - c := resource.NewClusterWithArgs(cIfc, mxIfc) + ci := resource.NewClusterWithArgs(mm, mx) + assert.Equal(t, "fred", ci.ContextName()) +} + +func TestUserName(t *testing.T) { + mm, mx := NewMockClusterMeta(), NewMockMetricsServer() + m.When(mm.UserName()).ThenReturn("fred") + + ci := resource.NewClusterWithArgs(mm, mx) + assert.Equal(t, "fred", ci.UserName()) +} + +func TestClusterMetrics(t *testing.T) { + mm, mx := NewMockClusterMeta(), NewMockMetricsServer() + m.When(mx.ClusterLoad([]v1.Node{}, []mv1beta1.NodeMetrics{})).ThenReturn(clusterMetric()) + + c := resource.NewClusterWithArgs(mm, mx) assert.Equal(t, clusterMetric(), c.Metrics([]v1.Node{}, []mv1beta1.NodeMetrics{})) } +func TestClusterGetNodes(t *testing.T) { + mm, mx := NewMockClusterMeta(), NewMockMetricsServer() + m.When(mm.FetchNodes()).ThenReturn([]v1.Node{*k8sNode()}, nil) + m.When(mx.ClusterLoad([]v1.Node{}, []mv1beta1.NodeMetrics{})).ThenReturn(clusterMetric()) + + c := resource.NewClusterWithArgs(mm, mx) + nodes, err := c.FetchNodes() + + assert.Nil(t, err) + assert.Equal(t, 1, len(nodes)) +} + +func TestClusterFetchNodesMetrics(t *testing.T) { + mm, mx := NewMockClusterMeta(), NewMockMetricsServer() + m.When(mm.FetchNodes()).ThenReturn([]v1.Node{*k8sNode()}, nil) + m.When(mx.FetchNodesMetrics()).ThenReturn([]mv1beta1.NodeMetrics{makeMxNode("fred", "100m", "10Mi")}, nil) + + c := resource.NewClusterWithArgs(mm, mx) + metrics, err := c.FetchNodesMetrics() + + assert.Nil(t, err) + assert.Equal(t, 1, len(metrics)) +} + // Helpers... func TestUsingMocks(t *testing.T) { diff --git a/internal/resource/context.go b/internal/resource/context.go index a44142dd..81fd1268 100644 --- a/internal/resource/context.go +++ b/internal/resource/context.go @@ -5,25 +5,27 @@ import ( "github.com/rs/zerolog/log" ) -// SwitchableResource represents a resource that can be switched. -type SwitchableResource interface { - k8s.Cruder - k8s.Switchable -} +type ( + // SwitchableResource represents a resource that can be switched. + SwitchableResource interface { + Cruder + k8s.Switchable + } -// Context tracks a kubernetes resource. -type Context struct { - *Base - instance *k8s.NamedContext -} + // Context tracks a kubernetes resource. + Context struct { + *Base + instance *k8s.NamedContext + } +) // NewContextList returns a new resource list. -func NewContextList(c k8s.Connection, ns string) List { +func NewContextList(c Connection, ns string) List { return NewList(NotNamespaced, "ctx", NewContext(c), SwitchAccess) } // NewContext instantiates a new Context. -func NewContext(c k8s.Connection) *Context { +func NewContext(c Connection) *Context { ctx := &Context{Base: NewBase(c, k8s.NewContext(c))} ctx.Factory = ctx @@ -64,15 +66,14 @@ func (*Context) Header(string) Row { // Fields retrieves displayable fields. func (r *Context) Fields(ns string) Row { ff := make(Row, 0, len(r.Header(ns))) - i := r.instance - name := i.Name - if i.MustCurrentContextName() == name { - name += "*" + i := r.instance + if i.MustCurrentContextName() == i.Name { + i.Name += "*" } return append(ff, - name, + i.Name, i.Context.Cluster, i.Context.AuthInfo, i.Context.Namespace, diff --git a/internal/resource/context_test.go b/internal/resource/context_test.go index f1ac16ed..75538cef 100644 --- a/internal/resource/context_test.go +++ b/internal/resource/context_test.go @@ -7,6 +7,7 @@ import ( "github.com/derailed/k9s/internal/resource" m "github.com/petergtz/pegomock" "github.com/stretchr/testify/assert" + "k8s.io/cli-runtime/pkg/genericclioptions" api "k8s.io/client-go/tools/clientcmd/api" ) @@ -21,73 +22,103 @@ func NewContextWithArgs(c k8s.Connection, s resource.SwitchableResource) *resour } func TestCTXSwitch(t *testing.T) { - conn := NewMockConnection() - ca := NewMockSwitchableResource() - m.When(ca.Switch("fred")).ThenReturn(nil) + mc := NewMockConnection() + mr := NewMockSwitchableResource() + m.When(mr.Switch("fred")).ThenReturn(nil) - ctx := NewContextWithArgs(conn, ca) + ctx := NewContextWithArgs(mc, mr) err := ctx.Switch("fred") assert.Nil(t, err) - ca.VerifyWasCalledOnce().Switch("fred") + mr.VerifyWasCalledOnce().Switch("fred") } func TestCTXList(t *testing.T) { - conn := NewMockConnection() - ca := NewMockSwitchableResource() - m.When(ca.List("blee")).ThenReturn(k8s.Collection{*k8sNamedCTX()}, nil) + mc := NewMockConnection() + mr := NewMockSwitchableResource() + m.When(mr.List("blee")).ThenReturn(k8s.Collection{*k8sNamedCTX()}, nil) - ctx := NewContextWithArgs(conn, ca) + ctx := NewContextWithArgs(mc, mr) cc, err := ctx.List("blee") assert.Nil(t, err) assert.Equal(t, resource.Columnars{ctx.New(k8sNamedCTX())}, cc) - ca.VerifyWasCalledOnce().List("blee") + mr.VerifyWasCalledOnce().List("blee") } func TestCTXDelete(t *testing.T) { - conn := NewMockConnection() - ca := NewMockSwitchableResource() - m.When(ca.Delete("", "fred")).ThenReturn(nil) + mc := NewMockConnection() + mr := NewMockSwitchableResource() + m.When(mr.Delete("", "fred")).ThenReturn(nil) - ctx := NewContextWithArgs(conn, ca) + ctx := NewContextWithArgs(mc, mr) assert.Nil(t, ctx.Delete("fred")) - ca.VerifyWasCalledOnce().Delete("", "fred") + mr.VerifyWasCalledOnce().Delete("", "fred") } func TestCTXListHasName(t *testing.T) { - conn := NewMockConnection() - ca := NewMockSwitchableResource() + mc := NewMockConnection() + mr := NewMockSwitchableResource() - ctx := NewContextWithArgs(conn, ca) + ctx := NewContextWithArgs(mc, mr) l := NewContextListWithArgs("blee", ctx) assert.Equal(t, "ctx", l.GetName()) } func TestCTXListHasNamespace(t *testing.T) { - conn := NewMockConnection() - ca := NewMockSwitchableResource() + mc := NewMockConnection() + mr := NewMockSwitchableResource() - ctx := NewContextWithArgs(conn, ca) + ctx := NewContextWithArgs(mc, mr) l := NewContextListWithArgs("blee", ctx) assert.Equal(t, resource.NotNamespaced, l.GetNamespace()) } func TestCTXListHasResource(t *testing.T) { - conn := NewMockConnection() - ca := NewMockSwitchableResource() + mc := NewMockConnection() + mr := NewMockSwitchableResource() - ctx := NewContextWithArgs(conn, ca) + ctx := NewContextWithArgs(mc, mr) l := NewContextListWithArgs("blee", ctx) assert.NotNil(t, l.Resource()) } +func TestCTXHeader(t *testing.T) { + mc := NewMockConnection() + mr := NewMockSwitchableResource() + + ctx := NewContextWithArgs(mc, mr) + + assert.Equal(t, 4, len(ctx.Header(""))) +} + +func TestCTXFields(t *testing.T) { + mc := NewMockConnection() + m.When(mc.Config()).ThenReturn(k8sConfig()) + mr := NewMockSwitchableResource() + m.When(mr.MustCurrentContextName()).ThenReturn("test") + + ctx := NewContextWithArgs(mc, mr) + c := ctx.New(k8sNamedCTX()) + + assert.Equal(t, 4, len(c.Fields(""))) + assert.Equal(t, "test*", c.Fields("")[0]) +} + // Helpers... +func k8sConfig() *k8s.Config { + ctx := "test" + f := genericclioptions.ConfigFlags{ + Context: &ctx, + } + return k8s.NewConfig(&f) +} + func k8sCTX() *api.Context { return &api.Context{ LocationOfOrigin: "fred", @@ -97,13 +128,13 @@ func k8sCTX() *api.Context { } func k8sNamedCTX() *k8s.NamedContext { - ctx := k8s.NamedContext{ - Name: "test", - Context: &api.Context{ + return k8s.NewNamedContext( + k8sConfig(), + "test", + &api.Context{ LocationOfOrigin: "fred", Cluster: "blee", AuthInfo: "secret", }, - } - return &ctx + ) } diff --git a/internal/resource/crd.go b/internal/resource/crd.go index e1379f0b..23fd1219 100644 --- a/internal/resource/crd.go +++ b/internal/resource/crd.go @@ -95,15 +95,17 @@ func (r *CRD) ExtFields() Properties { i = r.instance ) - meta := i.Object["metadata"].(map[string]interface{}) - if spec, ok := i.Object["spec"].(map[string]interface{}); ok { - pp["name"] = meta["name"] + if meta, ok := i.Object["metadata"].(map[string]interface{}); ok { + pp["name"] = meta["name"] + } pp["group"], pp["version"] = spec["group"], spec["version"] - names := spec["names"].(map[string]interface{}) - pp["kind"] = names["kind"] - pp["singular"], pp["plural"] = names["singular"], names["plural"] - pp["aliases"] = names["shortNames"] + + if names, ok := spec["names"].(map[string]interface{}); ok { + pp["kind"] = names["kind"] + pp["singular"], pp["plural"] = names["singular"], names["plural"] + pp["aliases"] = names["shortNames"] + } } return pp diff --git a/internal/resource/crd_test.go b/internal/resource/crd_test.go index 93930c0b..9d3e13e7 100644 --- a/internal/resource/crd_test.go +++ b/internal/resource/crd_test.go @@ -17,6 +17,7 @@ func NewCRDListWithArgs(ns string, r *resource.CRD) resource.List { func NewCRDWithArgs(conn k8s.Connection, res resource.Cruder) *resource.CRD { r := &resource.CRD{Base: resource.NewBase(conn, res)} r.Factory = r + return r } @@ -38,11 +39,19 @@ func TestCRDListAccess(t *testing.T) { func TestCRDFields(t *testing.T) { r := newCRD().Fields("blee") + assert.Equal(t, "fred", r[0]) } +func TestCRDExtFields(t *testing.T) { + p := newCRDFull().ExtFields() + + assert.Equal(t, 7, len(p)) +} + func TestCRDFieldsAllNS(t *testing.T) { r := newCRD().Fields(resource.AllNamespaces) + assert.Equal(t, "fred", r[0]) } @@ -97,6 +106,33 @@ func k8sCRD() *unstructured.Unstructured { } } +func k8sCRDFull() *unstructured.Unstructured { + return &unstructured.Unstructured{ + Object: map[string]interface{}{ + "metadata": map[string]interface{}{ + "namespace": "blee", + "name": "fred", + "creationTimestamp": "2018-12-14T10:36:43.326972Z", + }, + "spec": map[string]interface{}{ + "group": "apps", + "version": "v1", + "names": map[string]interface{}{ + "kind": "cool", + "singular": "cool", + "plural": "cools", + "shortNamed": []string{"co", "cos"}, + }, + }, + }, + } +} + +func newCRDFull() resource.Columnar { + mc := NewMockConnection() + return resource.NewCRD(mc).New(k8sCRDFull()) +} + func newCRD() resource.Columnar { mc := NewMockConnection() return resource.NewCRD(mc).New(k8sCRD()) diff --git a/internal/resource/cronjob.go b/internal/resource/cronjob.go index 26b21fbb..839f96a7 100644 --- a/internal/resource/cronjob.go +++ b/internal/resource/cronjob.go @@ -113,7 +113,7 @@ func (r *CronJob) Fields(ns string) Row { return append(ff, i.Name, i.Spec.Schedule, - boolToStr(*i.Spec.Suspend), + boolPtrToStr(i.Spec.Suspend), strconv.Itoa(len(i.Status.Active)), lastScheduled, toAge(i.ObjectMeta.CreationTimestamp), diff --git a/internal/resource/custom_test.go b/internal/resource/custom_test.go new file mode 100644 index 00000000..a2472776 --- /dev/null +++ b/internal/resource/custom_test.go @@ -0,0 +1,312 @@ +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" + metav1beta1 "k8s.io/apimachinery/pkg/apis/meta/v1beta1" + "k8s.io/apimachinery/pkg/runtime" +) + +func NewCustomListWithArgs(ns, name string, r *resource.Custom) resource.List { + return resource.NewList(ns, name, r, resource.AllVerbsAccess) +} + +func NewCustomWithArgs(conn k8s.Connection, res resource.Cruder) *resource.Custom { + r := &resource.Custom{Base: resource.NewBase(conn, res)} + r.Factory = r + return r +} + +func TestCustomListAccess(t *testing.T) { + mc := NewMockConnection() + mr := NewMockCruder() + + ns := "blee" + r := NewCustomWithArgs(mc, mr) + l := NewCustomListWithArgs(resource.AllNamespaces, "fred", r) + l.SetNamespace(ns) + + assert.Equal(t, ns, l.GetNamespace()) + assert.Equal(t, "fred", l.GetName()) + for _, a := range []int{resource.GetAccess, resource.ListAccess, resource.DeleteAccess, resource.ViewAccess, resource.EditAccess} { + assert.True(t, l.Access(a)) + } +} + +func TestCustomFields(t *testing.T) { + r := newCustom().Fields("blee") + assert.Equal(t, "a", r[0]) +} + +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") + + assert.Nil(t, err) + assert.Equal(t, customYaml(), ma) +} + +func TestCustomListData(t *testing.T) { + mc := NewMockConnection() + mr := NewMockCruder() + m.When(mr.List("blee")).ThenReturn(k8s.Collection{k8sCustomTable()}, nil) + + l := NewCustomListWithArgs("blee", "fred", NewCustomWithArgs(mc, mr)) + // Make sure we can get deltas! + for i := 0; i < 2; i++ { + err := l.Reconcile() + assert.Nil(t, err) + } + + mr.VerifyWasCalled(m.Times(2)).List("blee") + td := l.Data() + assert.Equal(t, 1, len(td.Rows)) + assert.Equal(t, "blee", l.GetNamespace()) + row := td.Rows["blee/fred"] + assert.Equal(t, 3, len(row.Deltas)) + for _, d := range row.Deltas { + assert.Equal(t, "", d) + } + assert.Equal(t, resource.Row{"a"}, row.Fields[:1]) +} + +// Helpers... + +func k8sCustomTable() *metav1beta1.Table { + return &metav1beta1.Table{ + ColumnDefinitions: []metav1beta1.TableColumnDefinition{ + {Name: "A"}, + {Name: "B"}, + {Name: "C"}, + }, + Rows: []metav1beta1.TableRow{ + { + Object: runtime.RawExtension{ + Raw: []byte(`{ + "kind": "fred", + "apiVersion": "v1", + "metadata": { + "namespace": "blee", + "name": "fred" + }}`), + }, + Cells: []interface{}{ + "a", + "b", + "c", + }, + }, + }, + } +} + +func k8sCustomRow() *metav1beta1.TableRow { + return &metav1beta1.TableRow{ + Object: runtime.RawExtension{ + Raw: []byte(`{ + "kind": "fred", + "apiVersion": "v1", + "metadata": { + "namespace": "blee", + "name": "fred" + }}`), + }, + Cells: []interface{}{ + "a", + "b", + "c", + }, + } +} + +func newCustom() resource.Columnar { + mc := NewMockConnection() + return resource.NewCustom(mc, "g", "v1", "fred").New(k8sCustomRow()) +} + +func customYaml() string { + return `typemeta: + kind: "" + apiversion: "" +listmeta: + selflink: "" + resourceversion: "" + continue: "" +columndefinitions: +- name: A + type: "" + format: "" + description: "" + priority: 0 +- name: B + type: "" + format: "" + description: "" + priority: 0 +- name: C + type: "" + format: "" + description: "" + priority: 0 +rows: +- cells: + - a + - b + - c + conditions: [] + object: + raw: + - 123 + - 10 + - 32 + - 32 + - 32 + - 32 + - 32 + - 32 + - 32 + - 32 + - 34 + - 107 + - 105 + - 110 + - 100 + - 34 + - 58 + - 32 + - 34 + - 102 + - 114 + - 101 + - 100 + - 34 + - 44 + - 10 + - 32 + - 32 + - 32 + - 32 + - 32 + - 32 + - 32 + - 32 + - 34 + - 97 + - 112 + - 105 + - 86 + - 101 + - 114 + - 115 + - 105 + - 111 + - 110 + - 34 + - 58 + - 32 + - 34 + - 118 + - 49 + - 34 + - 44 + - 10 + - 32 + - 32 + - 32 + - 32 + - 32 + - 32 + - 32 + - 32 + - 34 + - 109 + - 101 + - 116 + - 97 + - 100 + - 97 + - 116 + - 97 + - 34 + - 58 + - 32 + - 123 + - 10 + - 32 + - 32 + - 32 + - 32 + - 32 + - 32 + - 32 + - 32 + - 32 + - 32 + - 34 + - 110 + - 97 + - 109 + - 101 + - 115 + - 112 + - 97 + - 99 + - 101 + - 34 + - 58 + - 32 + - 34 + - 98 + - 108 + - 101 + - 101 + - 34 + - 44 + - 10 + - 32 + - 32 + - 32 + - 32 + - 32 + - 32 + - 32 + - 32 + - 32 + - 32 + - 34 + - 110 + - 97 + - 109 + - 101 + - 34 + - 58 + - 32 + - 34 + - 102 + - 114 + - 101 + - 100 + - 34 + - 10 + - 32 + - 32 + - 32 + - 32 + - 32 + - 32 + - 32 + - 32 + - 125 + - 125 + object: null +` +} diff --git a/internal/resource/helpers.go b/internal/resource/helpers.go index ea46cdd6..e48e45ba 100644 --- a/internal/resource/helpers.go +++ b/internal/resource/helpers.go @@ -121,3 +121,11 @@ func ToMillicore(v int64) string { func ToMi(v float64) string { return strconv.Itoa(int(v)) + "Mi" } + +func boolPtrToStr(b *bool) string { + if b == nil { + return "false" + } + + return boolToStr(*b) +} diff --git a/internal/resource/helpers_test.go b/internal/resource/helpers_test.go index f2fcc1b8..aa8a0cda 100644 --- a/internal/resource/helpers_test.go +++ b/internal/resource/helpers_test.go @@ -6,6 +6,23 @@ import ( "github.com/stretchr/testify/assert" ) +func TestBoolPtrToStr(t *testing.T) { + tv, fv := true, false + + uu := []struct { + p *bool + e string + }{ + {nil, "false"}, + {&tv, "true"}, + {&fv, "false"}, + } + + for _, u := range uu { + assert.Equal(t, u.e, boolPtrToStr(u.p)) + } +} + func TestNamespaced(t *testing.T) { uu := []struct { p, ns, n string diff --git a/internal/resource/mock_clustermeta_test.go b/internal/resource/mock_clustermeta_test.go index 3c4fba7d..f2e17944 100644 --- a/internal/resource/mock_clustermeta_test.go +++ b/internal/resource/mock_clustermeta_test.go @@ -6,6 +6,7 @@ package resource_test import ( k8s "github.com/derailed/k9s/internal/k8s" pegomock "github.com/petergtz/pegomock" + v1 "k8s.io/api/core/v1" dynamic "k8s.io/client-go/dynamic" kubernetes "k8s.io/client-go/kubernetes" rest "k8s.io/client-go/rest" @@ -97,6 +98,25 @@ func (mock *MockClusterMeta) DynDialOrDie() dynamic.Interface { return ret0 } +func (mock *MockClusterMeta) FetchNodes() ([]v1.Node, error) { + if mock == nil { + panic("mock must not be nil. Use myMock := NewMockClusterMeta().") + } + params := []pegomock.Param{} + result := pegomock.GetGenericMockFrom(mock).Invoke("FetchNodes", params, []reflect.Type{reflect.TypeOf((*[]v1.Node)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()}) + var ret0 []v1.Node + var ret1 error + if len(result) != 0 { + if result[0] != nil { + ret0 = result[0].([]v1.Node) + } + if result[1] != nil { + ret1 = result[1].(error) + } + } + return ret0, ret1 +} + func (mock *MockClusterMeta) HasMetrics() bool { if mock == nil { panic("mock must not be nil. Use myMock := NewMockClusterMeta().") @@ -355,6 +375,23 @@ func (c *ClusterMeta_DynDialOrDie_OngoingVerification) GetCapturedArguments() { func (c *ClusterMeta_DynDialOrDie_OngoingVerification) GetAllCapturedArguments() { } +func (verifier *VerifierClusterMeta) FetchNodes() *ClusterMeta_FetchNodes_OngoingVerification { + params := []pegomock.Param{} + methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "FetchNodes", params, verifier.timeout) + return &ClusterMeta_FetchNodes_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} +} + +type ClusterMeta_FetchNodes_OngoingVerification struct { + mock *MockClusterMeta + methodInvocations []pegomock.MethodInvocation +} + +func (c *ClusterMeta_FetchNodes_OngoingVerification) GetCapturedArguments() { +} + +func (c *ClusterMeta_FetchNodes_OngoingVerification) GetAllCapturedArguments() { +} + func (verifier *VerifierClusterMeta) HasMetrics() *ClusterMeta_HasMetrics_OngoingVerification { params := []pegomock.Param{} methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "HasMetrics", params, verifier.timeout) diff --git a/internal/resource/mock_switchableresource_test.go b/internal/resource/mock_switchableresource_test.go index 05482b7f..f0dc7bc8 100644 --- a/internal/resource/mock_switchableresource_test.go +++ b/internal/resource/mock_switchableresource_test.go @@ -71,6 +71,21 @@ func (mock *MockSwitchableResource) List(_param0 string) (k8s.Collection, error) return ret0, ret1 } +func (mock *MockSwitchableResource) MustCurrentContextName() string { + if mock == nil { + panic("mock must not be nil. Use myMock := NewMockSwitchableResource().") + } + params := []pegomock.Param{} + result := pegomock.GetGenericMockFrom(mock).Invoke("MustCurrentContextName", params, []reflect.Type{reflect.TypeOf((*string)(nil)).Elem()}) + var ret0 string + if len(result) != 0 { + if result[0] != nil { + ret0 = result[0].(string) + } + } + return ret0 +} + func (mock *MockSwitchableResource) Switch(_param0 string) error { if mock == nil { panic("mock must not be nil. Use myMock := NewMockSwitchableResource().") @@ -212,6 +227,23 @@ func (c *SwitchableResource_List_OngoingVerification) GetAllCapturedArguments() return } +func (verifier *VerifierSwitchableResource) MustCurrentContextName() *SwitchableResource_MustCurrentContextName_OngoingVerification { + params := []pegomock.Param{} + methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "MustCurrentContextName", params, verifier.timeout) + return &SwitchableResource_MustCurrentContextName_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} +} + +type SwitchableResource_MustCurrentContextName_OngoingVerification struct { + mock *MockSwitchableResource + methodInvocations []pegomock.MethodInvocation +} + +func (c *SwitchableResource_MustCurrentContextName_OngoingVerification) GetCapturedArguments() { +} + +func (c *SwitchableResource_MustCurrentContextName_OngoingVerification) GetAllCapturedArguments() { +} + func (verifier *VerifierSwitchableResource) Switch(_param0 string) *SwitchableResource_Switch_OngoingVerification { params := []pegomock.Param{_param0} methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "Switch", params, verifier.timeout) diff --git a/internal/resource/pod.go b/internal/resource/pod.go index 5a9f467d..3756f396 100644 --- a/internal/resource/pod.go +++ b/internal/resource/pod.go @@ -207,11 +207,12 @@ func (r *Pod) Fields(ns string) Row { ff = append(ff, i.Namespace) } - cr, _, rc, cc := r.statuses() + ss := i.Status.ContainerStatuses + cr, _, rc := r.statuses(ss) return append(ff, Pad(i.ObjectMeta.Name, podNameSize), - strconv.Itoa(cr)+"/"+strconv.Itoa(len(cc)), + strconv.Itoa(cr)+"/"+strconv.Itoa(len(ss)), r.phase(i.Status), strconv.Itoa(rc), ToMillicore(r.metrics.CurrentCPU), @@ -226,97 +227,8 @@ func (r *Pod) Fields(ns string) Row { // ---------------------------------------------------------------------------- // Helpers... -func (r *Pod) toVolumes(vv []v1.Volume) map[string]interface{} { - m := make(map[string]interface{}, len(vv)) - for _, v := range vv { - m[v.Name] = r.toVolume(v) - } - - return m -} - -func (r *Pod) toVolume(v v1.Volume) map[string]interface{} { - switch { - case v.Secret != nil: - return map[string]interface{}{ - "Type": "Secret", - "Name": v.Secret.SecretName, - "Optional": r.boolPtrToStr(v.Secret.Optional), - } - case v.AWSElasticBlockStore != nil: - return map[string]interface{}{ - "Type": v.AWSElasticBlockStore.FSType, - "VolumeID": v.AWSElasticBlockStore.VolumeID, - "Partition": strconv.Itoa(int(v.AWSElasticBlockStore.Partition)), - "ReadOnly": boolToStr(v.AWSElasticBlockStore.ReadOnly), - } - default: - return map[string]interface{}{} - } -} - -func (r *Pod) toContainers(cc []v1.Container) map[string]interface{} { - m := make(map[string]interface{}, len(cc)) - for _, c := range cc { - m[c.Name] = map[string]interface{}{ - "Image": c.Image, - "Environment": r.toEnv(c.Env), - } - } - - return m -} - -func (r *Pod) toEnv(ee []v1.EnvVar) []string { - if len(ee) == 0 { - return []string{MissingValue} - } - - ss := make([]string, len(ee)) - for i, e := range ee { - s := r.toEnvFrom(e.ValueFrom) - if len(s) == 0 { - ss[i] = e.Name + "=" + e.Value - } else { - ss[i] = e.Name + "=" + e.Value + "(" + s + ")" - } - } - - return ss -} - -func (r *Pod) toEnvFrom(e *v1.EnvVarSource) string { - if e == nil { - return MissingValue - } - - var s string - switch { - case e.ConfigMapKeyRef != nil: - f := e.ConfigMapKeyRef - s += f.Name + ":" + f.Key + "(" + r.boolPtrToStr(f.Optional) + ")" - case e.FieldRef != nil: - f := e.FieldRef - s += f.FieldPath + ":" + f.APIVersion - case e.SecretKeyRef != nil: - f := e.SecretKeyRef - s += f.Name + ":" + f.Key + "(" + r.boolPtrToStr(f.Optional) + ")" - } - - return s -} - -func (r *Pod) boolPtrToStr(b *bool) string { - if b == nil { - return "false" - } - - return boolToStr(*b) -} - -func (r *Pod) statuses() (cr, ct, rc int, cc []v1.ContainerStatus) { - cc = r.instance.Status.ContainerStatuses - for _, c := range cc { +func (r *Pod) statuses(ss []v1.ContainerStatus) (cr, ct, rc int) { + for _, c := range ss { if c.State.Terminated != nil { ct++ } diff --git a/internal/resource/pod_int_test.go b/internal/resource/pod_int_test.go new file mode 100644 index 00000000..bab635a1 --- /dev/null +++ b/internal/resource/pod_int_test.go @@ -0,0 +1,117 @@ +package resource + +import ( + "testing" + + "github.com/stretchr/testify/assert" + v1 "k8s.io/api/core/v1" +) + +func TestPodStatuses(t *testing.T) { + type counts struct { + ready, terminated, restarts int + } + + uu := []struct { + s []v1.ContainerStatus + e counts + }{ + { + []v1.ContainerStatus{ + v1.ContainerStatus{ + Name: "c1", + Ready: true, + State: v1.ContainerState{ + Running: &v1.ContainerStateRunning{}, + }, + }, + v1.ContainerStatus{ + Name: "c2", + Ready: false, + RestartCount: 10, + State: v1.ContainerState{ + Terminated: &v1.ContainerStateTerminated{}, + }, + }, + }, + counts{1, 1, 10}, + }, + } + + var p Pod + for _, u := range uu { + cr, ct, cs := p.statuses(u.s) + assert.Equal(t, u.e.ready, cr) + assert.Equal(t, u.e.terminated, ct) + assert.Equal(t, u.e.restarts, cs) + } +} + +func TestPodPhase(t *testing.T) { + uu := []struct { + s v1.PodStatus + e string + }{ + { + v1.PodStatus{ + ContainerStatuses: []v1.ContainerStatus{ + { + Name: "c1", + State: v1.ContainerState{ + Running: &v1.ContainerStateRunning{}, + }, + }, + }, + }, + "Running", + }, + { + v1.PodStatus{ + ContainerStatuses: []v1.ContainerStatus{ + { + Name: "c1", + State: v1.ContainerState{ + Waiting: &v1.ContainerStateWaiting{ + Reason: "blee", + }, + }, + }, + }, + }, + "blee", + }, + { + v1.PodStatus{ + ContainerStatuses: []v1.ContainerStatus{ + { + Name: "c1", + State: v1.ContainerState{ + Terminated: &v1.ContainerStateTerminated{}, + }, + }, + }, + }, + "Terminating", + }, + { + v1.PodStatus{ + ContainerStatuses: []v1.ContainerStatus{ + { + Name: "c1", + State: v1.ContainerState{ + Terminated: &v1.ContainerStateTerminated{ + Reason: "blee", + }, + }, + }, + }, + }, + "blee", + }, + } + + var p Pod + for _, u := range uu { + assert.Equal(t, u.e, p.phase(u.s)) + } +} diff --git a/internal/resource/rc_test.go b/internal/resource/rc_test.go new file mode 100644 index 00000000..17a67bf7 --- /dev/null +++ b/internal/resource/rc_test.go @@ -0,0 +1,114 @@ +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" + v1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func NewRCListWithArgs(ns string, r *resource.ReplicationController) resource.List { + return resource.NewList(ns, "rc", r, resource.AllVerbsAccess|resource.DescribeAccess) +} + +func NewRCWithArgs(conn k8s.Connection, res resource.Cruder) *resource.ReplicationController { + r := &resource.ReplicationController{Base: resource.NewBase(conn, res)} + r.Factory = r + return r +} + +func TestRCListAccess(t *testing.T) { + mc := NewMockConnection() + mr := NewMockCruder() + + ns := "blee" + l := NewRCListWithArgs(resource.AllNamespaces, NewRCWithArgs(mc, mr)) + l.SetNamespace(ns) + + assert.Equal(t, "blee", l.GetNamespace()) + assert.Equal(t, "rc", l.GetName()) + for _, a := range []int{resource.GetAccess, resource.ListAccess, resource.DeleteAccess, resource.ViewAccess, resource.EditAccess} { + assert.True(t, l.Access(a)) + } +} + +func TestRCFields(t *testing.T) { + r := newRC().Fields("blee") + assert.Equal(t, "fred", r[0]) +} + +func TestRCMarshal(t *testing.T) { + mc := NewMockConnection() + mr := NewMockCruder() + m.When(mr.Get("blee", "fred")).ThenReturn(k8sRC(), nil) + + cm := NewRCWithArgs(mc, mr) + ma, err := cm.Marshal("blee/fred") + + mr.VerifyWasCalledOnce().Get("blee", "fred") + assert.Nil(t, err) + assert.Equal(t, rcYaml(), ma) +} + +func TestRCListData(t *testing.T) { + mc := NewMockConnection() + mr := NewMockCruder() + m.When(mr.List("blee")).ThenReturn(k8s.Collection{*k8sRC()}, nil) + + l := NewRCListWithArgs("blee", NewRCWithArgs(mc, mr)) + // Make sure we mrn get deltas! + for i := 0; i < 2; i++ { + err := l.Reconcile() + assert.Nil(t, err) + } + + mr.VerifyWasCalled(m.Times(2)).List("blee") + td := l.Data() + assert.Equal(t, 1, len(td.Rows)) + assert.Equal(t, "blee", l.GetNamespace()) + row := td.Rows["blee/fred"] + assert.Equal(t, 5, len(row.Deltas)) + for _, d := range row.Deltas { + assert.Equal(t, "", d) + } + assert.Equal(t, resource.Row{"fred"}, row.Fields[:1]) +} + +// Helpers... + +func k8sRC() *v1.ReplicationController { + var c int32 = 10 + return &v1.ReplicationController{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "blee", + Name: "fred", + CreationTimestamp: metav1.Time{Time: testTime()}, + }, + Spec: v1.ReplicationControllerSpec{ + Replicas: &c, + }, + } +} + +func newRC() resource.Columnar { + mc := NewMockConnection() + return resource.NewReplicationController(mc).New(k8sRC()) +} + +func rcYaml() string { + return `apiVersion: v1 +kind: ReplicationController +metadata: + creationTimestamp: "2018-12-14T17:36:43Z" + name: fred + namespace: blee +spec: + replicas: 10 +status: + replicas: 0 +` +} diff --git a/internal/resource/ro.go b/internal/resource/ro.go index 4b9f7fa4..7340cdd7 100644 --- a/internal/resource/ro.go +++ b/internal/resource/ro.go @@ -76,6 +76,7 @@ func (*Role) Header(ns string) Row { // Fields retrieves displayable fields. func (r *Role) Fields(ns string) Row { ff := make(Row, 0, len(r.Header(ns))) + i := r.instance if ns == AllNamespaces { ff = append(ff, i.Namespace) @@ -92,12 +93,13 @@ func (r *Role) Fields(ns string) Row { func (r *Role) parseRules(pp []v1.PolicyRule) []Row { acc := make([]Row, len(pp)) + for i, p := range pp { - acc[i] = make(Row, 4) - acc[i][0] = strings.Join(p.Resources, ", ") - acc[i][1] = strings.Join(p.NonResourceURLs, ", ") - acc[i][2] = strings.Join(p.ResourceNames, ", ") - acc[i][3] = strings.Join(p.Verbs, ", ") + acc[i] = make(Row, 0, 4) + acc[i] = append(acc[i], strings.Join(p.Resources, ", ")) + acc[i] = append(acc[i], strings.Join(p.NonResourceURLs, ", ")) + acc[i] = append(acc[i], strings.Join(p.ResourceNames, ", ")) + acc[i] = append(acc[i], strings.Join(p.Verbs, ", ")) } return acc diff --git a/internal/resource/ro_int_test.go b/internal/resource/ro_int_test.go new file mode 100644 index 00000000..8b251030 --- /dev/null +++ b/internal/resource/ro_int_test.go @@ -0,0 +1,25 @@ +package resource + +import ( + "testing" + + "github.com/stretchr/testify/assert" + v1 "k8s.io/api/rbac/v1" +) + +func TestRoleParseRules(t *testing.T) { + rules := []v1.PolicyRule{ + { + Resources: []string{"", "apps"}, + NonResourceURLs: []string{"/fred"}, + ResourceNames: []string{"pods", "deployments"}, + Verbs: []string{"get", "list"}, + }, + } + + var r Role + rows := r.parseRules(rules) + + assert.Equal(t, 1, len(rows)) + assert.Equal(t, 1, len(rows)) +} diff --git a/internal/resource/svc.go b/internal/resource/svc.go index 608408c6..94a9fd33 100644 --- a/internal/resource/svc.go +++ b/internal/resource/svc.go @@ -98,7 +98,7 @@ func (r *Service) Fields(ns string) Row { i.ObjectMeta.Name, string(i.Spec.Type), i.Spec.ClusterIP, - r.toIPs(i.Spec.Type, getSvcExtIPS(i)), + r.toIPs(i.Spec.Type, r.getSvcExtIPS(i)), r.toPorts(i.Spec.Ports), toAge(i.ObjectMeta.CreationTimestamp), ) @@ -107,7 +107,7 @@ func (r *Service) Fields(ns string) Row { // ---------------------------------------------------------------------------- // Helpers... -func getSvcExtIPS(svc *v1.Service) []string { +func (r *Service) getSvcExtIPS(svc *v1.Service) []string { results := []string{} switch svc.Spec.Type { @@ -116,7 +116,7 @@ func getSvcExtIPS(svc *v1.Service) []string { case v1.ServiceTypeNodePort: return svc.Spec.ExternalIPs case v1.ServiceTypeLoadBalancer: - lbIps := lbIngressIP(svc.Status.LoadBalancer) + lbIps := r.lbIngressIP(svc.Status.LoadBalancer) if len(svc.Spec.ExternalIPs) > 0 { if len(lbIps) > 0 { results = append(results, lbIps) @@ -133,7 +133,7 @@ func getSvcExtIPS(svc *v1.Service) []string { return results } -func lbIngressIP(s v1.LoadBalancerStatus) string { +func (*Service) lbIngressIP(s v1.LoadBalancerStatus) string { ingress := s.Ingress result := []string{} for i := range ingress { diff --git a/internal/resource/svc_int_test.go b/internal/resource/svc_int_test.go index 4fdbc2a9..5bc1fdfe 100644 --- a/internal/resource/svc_int_test.go +++ b/internal/resource/svc_int_test.go @@ -1,15 +1,37 @@ package resource import ( + "fmt" "testing" + "time" "github.com/stretchr/testify/assert" v1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) -func TestToIPs(t *testing.T) { - s := Service{} +func TestSvcExtIPs(t *testing.T) { + i := k8sSVCLb() + var s Service + ips := s.getSvcExtIPS(i) + + assert.Equal(t, "10.0.0.0,2.2.2.2", s.toIPs(i.Spec.Type, ips)) +} + +func TestLbIngressIP(t *testing.T) { + lb := v1.LoadBalancerStatus{ + Ingress: []v1.LoadBalancerIngress{ + {"10.0.0.0", "fred"}, + {"10.0.0.1", "blee"}, + }, + } + + var s Service + assert.Equal(t, "10.0.0.0,10.0.0.1", s.lbIngressIP(lb)) +} + +func TestToIPs(t *testing.T) { uu := []struct { t v1.ServiceType ii []string @@ -19,14 +41,14 @@ func TestToIPs(t *testing.T) { {v1.ServiceTypeLoadBalancer, []string{}, ""}, {v1.ServiceTypeClusterIP, []string{}, MissingValue}, } + + var s Service for _, u := range uu { assert.Equal(t, u.e, s.toIPs(u.t, u.ii)) } } func TestToPorts(t *testing.T) { - var s Service - uu := []struct { pp []v1.ServicePort e string @@ -40,13 +62,14 @@ func TestToPorts(t *testing.T) { "80►30080╱UDP", }, } + + var s Service for _, u := range uu { assert.Equal(t, u.e, s.toPorts(u.pp)) } } func BenchmarkToPorts(b *testing.B) { - var s Service sp := []v1.ServicePort{ {Name: "http", Port: 80, NodePort: 90, Protocol: "TCP"}, {Port: 80, NodePort: 90, Protocol: "TCP"}, @@ -55,7 +78,46 @@ func BenchmarkToPorts(b *testing.B) { b.ResetTimer() b.ReportAllocs() + var s Service for i := 0; i < b.N; i++ { s.toPorts(sp) } } + +func k8sSVCLb() *v1.Service { + return &v1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: "fred", + Namespace: "blee", + CreationTimestamp: metav1.Time{testTime()}, + }, + Spec: v1.ServiceSpec{ + Type: v1.ServiceTypeLoadBalancer, + ClusterIP: "1.1.1.1", + ExternalIPs: []string{"2.2.2.2"}, + Selector: map[string]string{"fred": "blee"}, + Ports: []v1.ServicePort{ + { + Name: "http", + Port: 90, + Protocol: "TCP", + }, + }, + }, + Status: v1.ServiceStatus{ + LoadBalancer: v1.LoadBalancerStatus{ + Ingress: []v1.LoadBalancerIngress{ + {IP: "10.0.0.0", Hostname: "fred"}, + }, + }, + }, + } +} + +func testTime() time.Time { + t, err := time.Parse(time.RFC3339, "2018-12-14T10:36:43.326972-07:00") + if err != nil { + fmt.Println("TestTime Failed", err) + } + return t +}