beefup tests

mine
derailed 2019-03-25 10:28:20 -06:00
parent d71411a788
commit 1562b6255e
19 changed files with 930 additions and 190 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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{}, "<pending>"},
{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►30080UDP",
},
}
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
}