added subject view + v1 hpa support

mine
derailed 2019-03-30 17:06:32 -06:00
parent 06c66a7d15
commit a0763f920d
27 changed files with 1189 additions and 251 deletions

View File

@ -29,7 +29,7 @@ K9s is available on Linux, OSX and Windows platforms.
```
* Building from source
K9s was built using go 1.11 or above. In order to build K9 from source you must:
K9s was built using go 1.12 or above. In order to build K9 from source you must:
1. Clone the repo
2. Set env var *GO111MODULE=on*
3. Add the following command in your go.mod file
@ -151,7 +151,7 @@ K9s uses aliases to navigate most K8s resources.
This initial drop is brittle. K9s will most likely blow up...
1. You're running older versions of Kubernetes. K9s works best Kubernetes 1.10+.
1. You're running older versions of Kubernetes. K9s works best Kubernetes 1.12+.
1. You don't have enough RBAC fu to manage your cluster (see RBAC section below).
1. Your cluster does not run a metric server.

View File

@ -0,0 +1,54 @@
# Release v0.4.1
## Notes
Thank you to all that contributed with flushing out issues with K9s! I'll try
to mark some of these issues as fixed. But if you don't mind grab the latest
rev and see if we're happier with some of the fixes!
If you've filed an issue please help me verify and close.
Thank you so much for your support and awesome suggestions to make K9s better!!
---
## Change Logs
### Subject View
You can now view users/groups that are bound by RBAC rules without having to type to full subject name.
To activate use the following command mode
```text
# for users
:usr
# for groups
:grp
```
These commands will pull all the available cluster and role binding associated with these subject types.
You can then select and `<enter>` to see the associated policies.
You can also filter/sort like in any other K9s views with the added bonus of auto updates when new
users/groups binding come into your clusters.
To see ServiceAccount RBAC policies, you can now navigate to the serviceaccount view aka `:sa` and press `<enter>`
to view the associated policy rules.
### Fu View
Has been renamed policy view to see all RBAC policies available on a subject.
You can now use `pol` (instead of `fu`) to list out RBAC policies associated with a
user/group or serviceaccount.
### Enter now has a meaning!
Pressing `<enter>` on most resource views will now describe the resource by default.
---
## Resolved Bugs
+ [Issue #143](https://github.com/derailed/k9s/issues/143)
+ [Issue #140](https://github.com/derailed/k9s/issues/140)
NOTE! Describe on v1 HPA is busted just like it is when running v 1.13 of
kubectl against a v1.12 cluster.

View File

@ -87,9 +87,6 @@ func loadConfiguration() *config.Config {
log.Info().Msg("✅ Kubernetes connectivity")
k9sCfg.Save()
// k8s.NewNode(k9sCfg.GetConnection()).FetchReqLimit("minikube")
// os.Exit(0)
return k9sCfg
}

View File

@ -147,6 +147,25 @@ func (mock *MockConnection) RestConfigOrDie() *rest.Config {
return ret0
}
func (mock *MockConnection) SupportsRes(_param0 string, _param1 []string) (string, bool) {
if mock == nil {
panic("mock must not be nil. Use myMock := NewMockConnection().")
}
params := []pegomock.Param{_param0, _param1}
result := pegomock.GetGenericMockFrom(mock).Invoke("SupportsRes", params, []reflect.Type{reflect.TypeOf((*string)(nil)).Elem(), reflect.TypeOf((*bool)(nil)).Elem()})
var ret0 string
var ret1 bool
if len(result) != 0 {
if result[0] != nil {
ret0 = result[0].(string)
}
if result[1] != nil {
ret1 = result[1].(bool)
}
}
return ret0, ret1
}
func (mock *MockConnection) SupportsResource(_param0 string) bool {
if mock == nil {
panic("mock must not be nil. Use myMock := NewMockConnection().")
@ -391,6 +410,37 @@ func (c *Connection_RestConfigOrDie_OngoingVerification) GetCapturedArguments()
func (c *Connection_RestConfigOrDie_OngoingVerification) GetAllCapturedArguments() {
}
func (verifier *VerifierConnection) SupportsRes(_param0 string, _param1 []string) *Connection_SupportsRes_OngoingVerification {
params := []pegomock.Param{_param0, _param1}
methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "SupportsRes", params, verifier.timeout)
return &Connection_SupportsRes_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations}
}
type Connection_SupportsRes_OngoingVerification struct {
mock *MockConnection
methodInvocations []pegomock.MethodInvocation
}
func (c *Connection_SupportsRes_OngoingVerification) GetCapturedArguments() (string, []string) {
_param0, _param1 := c.GetAllCapturedArguments()
return _param0[len(_param0)-1], _param1[len(_param1)-1]
}
func (c *Connection_SupportsRes_OngoingVerification) GetAllCapturedArguments() (_param0 []string, _param1 [][]string) {
params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations)
if len(params) > 0 {
_param0 = make([]string, len(params[0]))
for u, param := range params[0] {
_param0[u] = param.(string)
}
_param1 = make([][]string, len(params[1]))
for u, param := range params[1] {
_param1[u] = param.([]string)
}
}
return
}
func (verifier *VerifierConnection) SupportsResource(_param0 string) *Connection_SupportsResource_OngoingVerification {
params := []pegomock.Param{_param0}
methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "SupportsResource", params, verifier.timeout)

View File

@ -54,6 +54,7 @@ type (
SupportsResource(group string) bool
ValidNamespaces() ([]v1.Namespace, error)
ValidPods(node string) ([]v1.Pod, error)
SupportsRes(grp string, versions []string) (string, bool)
}
// APIClient represents a Kubernetes api client.
@ -122,6 +123,7 @@ func (a *APIClient) IsNamespaced(res string) bool {
func (a *APIClient) SupportsResource(group string) bool {
list, _ := a.DialOrDie().Discovery().ServerPreferredResources()
for _, l := range list {
log.Debug().Msgf(">>> Group %s", l.GroupVersion)
if l.GroupVersion == group {
return true
}
@ -243,3 +245,28 @@ func (a *APIClient) supportsMxServer() bool {
return false
}
// SupportsRes checks latest supported version.
func (a *APIClient) SupportsRes(group string, versions []string) (string, bool) {
apiGroups, err := a.DialOrDie().Discovery().ServerGroups()
if err != nil {
return "", false
}
for _, grp := range apiGroups.Groups {
if grp.Name != group {
continue
}
return grp.PreferredVersion.Version, true
// for _, version := range grp.Versions {
// for _, supportedVersion := range versions {
// if version.Version == supportedVersion {
// return supportedVersion, true
// }
// }
// }
}
return "", false
}

38
internal/k8s/hpa_v1.go Normal file
View File

@ -0,0 +1,38 @@
package k8s
import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
// HPAV1 represents am HorizontalPodAutoscaler.
type HPAV1 struct {
Connection
}
// NewHPAV1 returns a new HPA.
func NewHPAV1(c Connection) Cruder {
return &HPAV1{c}
}
// Get a HPA.
func (h *HPAV1) Get(ns, n string) (interface{}, error) {
return h.DialOrDie().AutoscalingV1().HorizontalPodAutoscalers(ns).Get(n, metav1.GetOptions{})
}
// List all HPAs in a given namespace.
func (h *HPAV1) List(ns string) (Collection, error) {
rr, err := h.DialOrDie().AutoscalingV1().HorizontalPodAutoscalers(ns).List(metav1.ListOptions{})
if err != nil {
return nil, err
}
cc := make(Collection, len(rr.Items))
for i, r := range rr.Items {
cc[i] = r
}
return cc, nil
}
// Delete a HPA.
func (h *HPAV1) Delete(ns, n string) error {
return h.DialOrDie().AutoscalingV1().HorizontalPodAutoscalers(ns).Delete(n, nil)
}

View File

@ -1,26 +1,29 @@
package k8s
import (
"github.com/rs/zerolog/log"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
// HPA represents am HorizontalPodAutoscaler.
type HPA struct {
// HPAV2Beta1 represents am HorizontalPodAutoscaler.
type HPAV2Beta1 struct {
Connection
}
// NewHPA returns a new HPA.
func NewHPA(c Connection) Cruder {
return &HPA{c}
// NewHPAV2Beta1 returns a new HPA.
func NewHPAV2Beta1(c Connection) Cruder {
return &HPAV2Beta1{c}
}
// Get a HPA.
func (h *HPA) Get(ns, n string) (interface{}, error) {
func (h *HPAV2Beta1) Get(ns, n string) (interface{}, error) {
return h.DialOrDie().AutoscalingV2beta2().HorizontalPodAutoscalers(ns).Get(n, metav1.GetOptions{})
}
// List all HPAs in a given namespace.
func (h *HPA) List(ns string) (Collection, error) {
func (h *HPAV2Beta1) List(ns string) (Collection, error) {
log.Debug().Msg("!!!! YO V2B1")
rr, err := h.DialOrDie().AutoscalingV2beta2().HorizontalPodAutoscalers(ns).List(metav1.ListOptions{})
if err != nil {
return nil, err
@ -33,9 +36,6 @@ func (h *HPA) List(ns string) (Collection, error) {
}
// Delete a HPA.
func (h *HPA) Delete(ns, n string) error {
if h.SupportsResource("autoscaling/v2beta1") {
func (h *HPAV2Beta1) Delete(ns, n string) error {
return h.DialOrDie().AutoscalingV2beta1().HorizontalPodAutoscalers(ns).Delete(n, nil)
}
return h.DialOrDie().AutoscalingV2beta2().HorizontalPodAutoscalers(ns).Delete(n, nil)
}

View File

@ -0,0 +1,42 @@
package k8s
import (
"github.com/rs/zerolog/log"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
var supportedAutoScalingAPIVersions = []string{"v2beta2", "v2beta1", "v1"}
// HPAV2Beta2 represents am HorizontalPodAutoscaler.
type HPAV2Beta2 struct {
Connection
}
// NewHPAV2Beta2 returns a new HPAV2Beta2.
func NewHPAV2Beta2(c Connection) Cruder {
return &HPAV2Beta2{c}
}
// Get a HPAV2Beta2.
func (h *HPAV2Beta2) Get(ns, n string) (interface{}, error) {
return h.DialOrDie().AutoscalingV2beta2().HorizontalPodAutoscalers(ns).Get(n, metav1.GetOptions{})
}
// List all HPAV2Beta2s in a given namespace.
func (h *HPAV2Beta2) List(ns string) (Collection, error) {
log.Debug().Msg("!!!! YO V2B2")
rr, err := h.DialOrDie().AutoscalingV2beta2().HorizontalPodAutoscalers(ns).List(metav1.ListOptions{})
if err != nil {
return nil, err
}
cc := make(Collection, len(rr.Items))
for i, r := range rr.Items {
cc[i] = r
}
return cc, nil
}
// Delete a HPAV2Beta2.
func (h *HPAV2Beta2) Delete(ns, n string) error {
return h.DialOrDie().AutoscalingV2beta2().HorizontalPodAutoscalers(ns).Delete(n, nil)
}

View File

@ -98,11 +98,6 @@ func (m *MetricsServer) ClusterLoad(nodes []v1.Node, metrics []mv1beta1.NodeMetr
return ClusterMetrics{PercCPU: toPerc(cpu, tcpu), PercMEM: toPerc(mem, tmem)}
}
// // HasMetrics check if cluster has a metrics server.
// func (m *MetricsServer) HasMetrics() bool {
// return m.HasMetrics()
// }
// FetchNodesMetrics return all metrics for pods in a given namespace.
func (m *MetricsServer) FetchNodesMetrics() ([]mv1beta1.NodeMetrics, error) {
client, err := m.MXDial()

View File

@ -92,13 +92,13 @@ func (b *Base) Describe(kind, pa string, flags *genericclioptions.ConfigFlags) (
d, err := versioned.Describer(flags, mapping)
if err != nil {
log.Error().Err(err).Msgf("Unable to find describer for %#v", mapping)
return "", err
}
opts := describe.DescriberSettings{
ShowEvents: true,
}
return d.Describe(ns, n, opts)
log.Debug().Msgf("Describer %#v", d)
return d.Describe(ns, n, describe.DescriberSettings{ShowEvents: true})
}
// Delete a resource by name.

118
internal/resource/hpa_v1.go Normal file
View File

@ -0,0 +1,118 @@
package resource
import (
"strconv"
"github.com/derailed/k9s/internal/k8s"
"github.com/rs/zerolog/log"
autoscalingv1 "k8s.io/api/autoscaling/v1"
)
// HPAV1 tracks a kubernetes resource.
type HPAV1 struct {
*Base
instance *autoscalingv1.HorizontalPodAutoscaler
}
// NewHPAV1List returns a new resource list.
func NewHPAV1List(c Connection, ns string) List {
log.Debug().Msg(">>> YO!!!")
return NewList(
ns,
"hpa",
NewHPAV1(c),
AllVerbsAccess|DescribeAccess,
)
}
// NewHPAV1 instantiates a new HPAV1.
func NewHPAV1(c Connection) *HPAV1 {
hpa := &HPAV1{&Base{Connection: c, Resource: k8s.NewHPAV1(c)}, nil}
hpa.Factory = hpa
return hpa
}
// New builds a new HPAV1 instance from a k8s resource.
func (r *HPAV1) New(i interface{}) Columnar {
c := NewHPAV1(r.Connection)
switch instance := i.(type) {
case *autoscalingv1.HorizontalPodAutoscaler:
c.instance = instance
case autoscalingv1.HorizontalPodAutoscaler:
c.instance = &instance
default:
log.Fatal().Msgf("unknown HPAV1 type %#v", i)
}
c.path = c.namespacedName(c.instance.ObjectMeta)
return c
}
// Marshal resource to yaml.
func (r *HPAV1) Marshal(path string) (string, error) {
ns, n := namespaced(path)
i, err := r.Resource.Get(ns, n)
if err != nil {
return "", err
}
hpa := i.(*autoscalingv1.HorizontalPodAutoscaler)
hpa.TypeMeta.APIVersion = "autoscaling/v1"
hpa.TypeMeta.Kind = "HorizontalPodAutoscaler"
return r.marshalObject(hpa)
}
// Header return resource header.
func (*HPAV1) Header(ns string) Row {
hh := Row{}
if ns == AllNamespaces {
hh = append(hh, "NAMESPACE")
}
return append(hh,
"NAME",
"REFERENCE",
"TARGETS",
"MINPODS",
"MAXPODS",
"REPLICAS",
"AGE")
}
// Fields retrieves displayable fields.
func (r *HPAV1) Fields(ns string) Row {
ff := make(Row, 0, len(r.Header(ns)))
i := r.instance
if ns == AllNamespaces {
ff = append(ff, i.Namespace)
}
return append(ff,
i.ObjectMeta.Name,
i.Spec.ScaleTargetRef.Name,
r.toMetrics(i.Spec, i.Status),
strconv.Itoa(int(*i.Spec.MinReplicas)),
strconv.Itoa(int(i.Spec.MaxReplicas)),
strconv.Itoa(int(i.Status.CurrentReplicas)),
toAge(i.ObjectMeta.CreationTimestamp),
)
}
// ----------------------------------------------------------------------------
// Helpers...
func (r *HPAV1) toMetrics(spec autoscalingv1.HorizontalPodAutoscalerSpec, status autoscalingv1.HorizontalPodAutoscalerStatus) string {
current := "<unknown>"
if status.CurrentCPUUtilizationPercentage != nil {
current = strconv.Itoa(int(*status.CurrentCPUUtilizationPercentage)) + "%"
}
target := "<unknown>"
if spec.TargetCPUUtilizationPercentage != nil {
target = strconv.Itoa(int(*spec.TargetCPUUtilizationPercentage))
}
return current + "/" + target + "%"
}

View File

@ -0,0 +1,185 @@
package resource
import (
"fmt"
"strconv"
"strings"
"github.com/derailed/k9s/internal/k8s"
"github.com/rs/zerolog/log"
autoscalingv2beta1 "k8s.io/api/autoscaling/v2beta1"
)
// HPAV2Beta1 tracks a kubernetes resource.
type HPAV2Beta1 struct {
*Base
instance *autoscalingv2beta1.HorizontalPodAutoscaler
}
// NewHPAV2Beta1List returns a new resource list.
func NewHPAV2Beta1List(c Connection, ns string) List {
return NewList(
ns,
"hpa",
NewHPAV2Beta1(c),
AllVerbsAccess|DescribeAccess,
)
}
// NewHPAV2Beta1 instantiates a new HPAV2Beta1.
func NewHPAV2Beta1(c Connection) *HPAV2Beta1 {
hpa := &HPAV2Beta1{&Base{Connection: c, Resource: k8s.NewHPAV2Beta1(c)}, nil}
hpa.Factory = hpa
return hpa
}
// New builds a new HPAV2Beta1 instance from a k8s resource.
func (r *HPAV2Beta1) New(i interface{}) Columnar {
c := NewHPAV2Beta1(r.Connection)
switch instance := i.(type) {
case *autoscalingv2beta1.HorizontalPodAutoscaler:
c.instance = instance
case autoscalingv2beta1.HorizontalPodAutoscaler:
c.instance = &instance
default:
log.Fatal().Msgf("unknown HPAV2Beta1 type %#v", i)
}
c.path = c.namespacedName(c.instance.ObjectMeta)
return c
}
// Marshal resource to yaml.
func (r *HPAV2Beta1) Marshal(path string) (string, error) {
ns, n := namespaced(path)
i, err := r.Resource.Get(ns, n)
if err != nil {
return "", err
}
hpa := i.(*autoscalingv2beta1.HorizontalPodAutoscaler)
hpa.TypeMeta.APIVersion = "autoscaling/v2beta1"
hpa.TypeMeta.Kind = "HorizontalPodAutoscaler"
return r.marshalObject(hpa)
}
// Header return resource header.
func (*HPAV2Beta1) Header(ns string) Row {
hh := Row{}
if ns == AllNamespaces {
hh = append(hh, "NAMESPACE")
}
return append(hh,
"NAME",
"REFERENCE",
"TARGETS",
"MINPODS",
"MAXPODS",
"REPLICAS",
"AGE")
}
// Fields retrieves displayable fields.
func (r *HPAV2Beta1) Fields(ns string) Row {
ff := make(Row, 0, len(r.Header(ns)))
i := r.instance
if ns == AllNamespaces {
ff = append(ff, i.Namespace)
}
return append(ff,
i.ObjectMeta.Name,
i.Spec.ScaleTargetRef.Name,
r.toMetrics(i.Spec.Metrics, i.Status.CurrentMetrics),
strconv.Itoa(int(*i.Spec.MinReplicas)),
strconv.Itoa(int(i.Spec.MaxReplicas)),
strconv.Itoa(int(i.Status.CurrentReplicas)),
toAge(i.ObjectMeta.CreationTimestamp),
)
}
// ----------------------------------------------------------------------------
// Helpers...
func (r *HPAV2Beta1) toMetrics(specs []autoscalingv2beta1.MetricSpec, statuses []autoscalingv2beta1.MetricStatus) string {
if len(specs) == 0 {
return "<none>"
}
list, max, more, count := []string{}, 2, false, 0
for i, spec := range specs {
current := "<unknown>"
switch spec.Type {
case autoscalingv2beta1.ExternalMetricSourceType:
list = append(list, r.externalMetrics(i, spec, statuses))
case autoscalingv2beta1.PodsMetricSourceType:
if len(statuses) > i && statuses[i].Pods != nil {
current = statuses[i].Pods.CurrentAverageValue.String()
}
list = append(list, fmt.Sprintf("%s/%s", current, spec.Pods.TargetAverageValue.String()))
case autoscalingv2beta1.ObjectMetricSourceType:
if len(statuses) > i && statuses[i].Object != nil {
current = statuses[i].Object.CurrentValue.String()
}
list = append(list, fmt.Sprintf("%s/%s", current, spec.Object.TargetValue.String()))
case autoscalingv2beta1.ResourceMetricSourceType:
list = append(list, r.resourceMetrics(i, spec, statuses))
default:
list = append(list, "<unknown type>")
}
count++
}
if count > max {
list, more = list[:max], true
}
ret := strings.Join(list, ", ")
if more {
return fmt.Sprintf("%s + %d more...", ret, count-max)
}
return ret
}
func (*HPAV2Beta1) externalMetrics(i int, spec autoscalingv2beta1.MetricSpec, statuses []autoscalingv2beta1.MetricStatus) string {
current := "<unknown>"
if spec.External.TargetAverageValue != nil {
if len(statuses) > i && statuses[i].External != nil && &statuses[i].External.CurrentAverageValue != nil {
current = statuses[i].External.CurrentAverageValue.String()
}
return fmt.Sprintf("%s/%s (avg)", current, spec.External.TargetAverageValue.String())
}
if len(statuses) > i && statuses[i].External != nil {
current = statuses[i].External.CurrentValue.String()
}
return fmt.Sprintf("%s/%s", current, spec.External.TargetValue.String())
}
func (*HPAV2Beta1) resourceMetrics(i int, spec autoscalingv2beta1.MetricSpec, statuses []autoscalingv2beta1.MetricStatus) string {
current := "<unknown>"
if spec.Resource.TargetAverageValue != nil {
if len(statuses) > i && statuses[i].Resource != nil {
current = statuses[i].Resource.CurrentAverageValue.String()
}
return fmt.Sprintf("%s/%s", current, spec.Resource.TargetAverageValue.String())
}
if len(statuses) > i && statuses[i].Resource != nil && statuses[i].Resource.CurrentAverageUtilization != nil {
current = fmt.Sprintf("%d%%", *statuses[i].Resource.CurrentAverageUtilization)
}
target := "<auto>"
if spec.Resource.TargetAverageUtilization != nil {
target = fmt.Sprintf("%d%%", *spec.Resource.TargetAverageUtilization)
}
return fmt.Sprintf("%s/%s", current, target)
}

View File

@ -28,7 +28,7 @@ func NewHPAList(c Connection, ns string) List {
// NewHPA instantiates a new HPA.
func NewHPA(c Connection) *HPA {
hpa := &HPA{&Base{Connection: c, Resource: k8s.NewHPA(c)}, nil}
hpa := &HPA{&Base{Connection: c, Resource: k8s.NewHPAV2Beta2(c)}, nil}
hpa.Factory = hpa
return hpa

View File

@ -196,6 +196,25 @@ func (mock *MockClusterMeta) RestConfigOrDie() *rest.Config {
return ret0
}
func (mock *MockClusterMeta) SupportsRes(_param0 string, _param1 []string) (string, bool) {
if mock == nil {
panic("mock must not be nil. Use myMock := NewMockClusterMeta().")
}
params := []pegomock.Param{_param0, _param1}
result := pegomock.GetGenericMockFrom(mock).Invoke("SupportsRes", params, []reflect.Type{reflect.TypeOf((*string)(nil)).Elem(), reflect.TypeOf((*bool)(nil)).Elem()})
var ret0 string
var ret1 bool
if len(result) != 0 {
if result[0] != nil {
ret0 = result[0].(string)
}
if result[1] != nil {
ret1 = result[1].(bool)
}
}
return ret0, ret1
}
func (mock *MockClusterMeta) SupportsResource(_param0 string) bool {
if mock == nil {
panic("mock must not be nil. Use myMock := NewMockClusterMeta().")
@ -525,6 +544,37 @@ func (c *ClusterMeta_RestConfigOrDie_OngoingVerification) GetCapturedArguments()
func (c *ClusterMeta_RestConfigOrDie_OngoingVerification) GetAllCapturedArguments() {
}
func (verifier *VerifierClusterMeta) SupportsRes(_param0 string, _param1 []string) *ClusterMeta_SupportsRes_OngoingVerification {
params := []pegomock.Param{_param0, _param1}
methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "SupportsRes", params, verifier.timeout)
return &ClusterMeta_SupportsRes_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations}
}
type ClusterMeta_SupportsRes_OngoingVerification struct {
mock *MockClusterMeta
methodInvocations []pegomock.MethodInvocation
}
func (c *ClusterMeta_SupportsRes_OngoingVerification) GetCapturedArguments() (string, []string) {
_param0, _param1 := c.GetAllCapturedArguments()
return _param0[len(_param0)-1], _param1[len(_param1)-1]
}
func (c *ClusterMeta_SupportsRes_OngoingVerification) GetAllCapturedArguments() (_param0 []string, _param1 [][]string) {
params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations)
if len(params) > 0 {
_param0 = make([]string, len(params[0]))
for u, param := range params[0] {
_param0[u] = param.(string)
}
_param1 = make([][]string, len(params[1]))
for u, param := range params[1] {
_param1[u] = param.([]string)
}
}
return
}
func (verifier *VerifierClusterMeta) SupportsResource(_param0 string) *ClusterMeta_SupportsResource_OngoingVerification {
params := []pegomock.Param{_param0}
methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "SupportsResource", params, verifier.timeout)

View File

@ -147,6 +147,25 @@ func (mock *MockConnection) RestConfigOrDie() *rest.Config {
return ret0
}
func (mock *MockConnection) SupportsRes(_param0 string, _param1 []string) (string, bool) {
if mock == nil {
panic("mock must not be nil. Use myMock := NewMockConnection().")
}
params := []pegomock.Param{_param0, _param1}
result := pegomock.GetGenericMockFrom(mock).Invoke("SupportsRes", params, []reflect.Type{reflect.TypeOf((*string)(nil)).Elem(), reflect.TypeOf((*bool)(nil)).Elem()})
var ret0 string
var ret1 bool
if len(result) != 0 {
if result[0] != nil {
ret0 = result[0].(string)
}
if result[1] != nil {
ret1 = result[1].(bool)
}
}
return ret0, ret1
}
func (mock *MockConnection) SupportsResource(_param0 string) bool {
if mock == nil {
panic("mock must not be nil. Use myMock := NewMockConnection().")
@ -391,6 +410,37 @@ func (c *Connection_RestConfigOrDie_OngoingVerification) GetCapturedArguments()
func (c *Connection_RestConfigOrDie_OngoingVerification) GetAllCapturedArguments() {
}
func (verifier *VerifierConnection) SupportsRes(_param0 string, _param1 []string) *Connection_SupportsRes_OngoingVerification {
params := []pegomock.Param{_param0, _param1}
methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "SupportsRes", params, verifier.timeout)
return &Connection_SupportsRes_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations}
}
type Connection_SupportsRes_OngoingVerification struct {
mock *MockConnection
methodInvocations []pegomock.MethodInvocation
}
func (c *Connection_SupportsRes_OngoingVerification) GetCapturedArguments() (string, []string) {
_param0, _param1 := c.GetAllCapturedArguments()
return _param0[len(_param0)-1], _param1[len(_param1)-1]
}
func (c *Connection_SupportsRes_OngoingVerification) GetAllCapturedArguments() (_param0 []string, _param1 [][]string) {
params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations)
if len(params) > 0 {
_param0 = make([]string, len(params[0]))
for u, param := range params[0] {
_param0[u] = param.(string)
}
_param1 = make([][]string, len(params[1]))
for u, param := range params[1] {
_param1[u] = param.([]string)
}
}
return
}
func (verifier *VerifierConnection) SupportsResource(_param0 string) *Connection_SupportsResource_OngoingVerification {
params := []pegomock.Param{_param0}
methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "SupportsResource", params, verifier.timeout)

View File

@ -21,8 +21,8 @@ type cmdView struct {
func newCmdView(ic rune) *cmdView {
v := cmdView{icon: ic, TextView: tview.NewTextView()}
{
v.SetWordWrap(false)
v.SetWrap(false)
v.SetWordWrap(true)
v.SetWrap(true)
v.SetDynamicColors(true)
v.SetBorderPadding(0, 0, 1, 1)
v.SetTextColor(tcell.ColorAqua)

View File

@ -44,3 +44,7 @@ func (s *cmdStack) top() (string, bool) {
func (s *cmdStack) empty() bool {
return len(s.stack) == 0
}
func (s *cmdStack) last() bool {
return len(s.stack) == 1
}

View File

@ -9,6 +9,12 @@ import (
"github.com/rs/zerolog/log"
)
type subjectViewer interface {
resourceViewer
setSubject(s string)
}
type command struct {
app *appView
history *cmdStack
@ -18,6 +24,10 @@ func newCommand(app *appView) *command {
return &command{app: app, history: newCmdStack()}
}
func (c *command) lastCmd() bool {
return c.history.last()
}
func (c *command) pushCmd(cmd string) {
c.history.push(cmd)
c.app.crumbsView.update(c.history.stack)
@ -37,7 +47,7 @@ func (c *command) defaultCmd() {
// Helpers...
var fuMatcher = regexp.MustCompile(`\Afu\s([u|g|s]):([\w-:]+)\b`)
var policyMatcher = regexp.MustCompile(`\Apol\s([u|g|s]):([\w-:]+)\b`)
// Exec the command by showing associated display.
func (c *command) run(cmd string) bool {
@ -52,21 +62,21 @@ func (c *command) run(cmd string) bool {
case cmd == "alias":
c.app.inject(newAliasView(c.app))
return true
case fuMatcher.MatchString(cmd):
tokens := fuMatcher.FindAllStringSubmatch(cmd, -1)
case policyMatcher.MatchString(cmd):
tokens := policyMatcher.FindAllStringSubmatch(cmd, -1)
if len(tokens) == 1 && len(tokens[0]) == 3 {
c.app.inject(newFuView(c.app, tokens[0][1], tokens[0][2]))
c.app.inject(newPolicyView(c.app, tokens[0][1], tokens[0][2]))
return true
}
default:
if res, ok := resourceViews()[cmd]; ok {
if res, ok := resourceViews(c.app.conn())[cmd]; ok {
var r resource.List
if res.listMxFn != nil {
r = res.listMxFn(c.app.conn(),
k8s.NewMetricsServer(c.app.conn()),
resource.DefaultNamespace,
)
} else {
} else if res.listFn != nil {
r = res.listFn(c.app.conn(), resource.DefaultNamespace)
}
v = res.viewFn(res.title, c.app, r)
@ -79,8 +89,8 @@ func (c *command) run(cmd string) bool {
if res.decorateFn != nil {
v.setDecorateFn(res.decorateFn)
}
const fmat = "Viewing %s in namespace %s..."
c.app.flash(flashInfo, fmt.Sprintf(fmat, res.title, c.app.config.ActiveNamespace()))
const fmat = "Viewing resource %s..."
c.app.flash(flashInfo, fmt.Sprintf(fmat, res.title))
log.Debug().Msgf("Running command %s", cmd)
c.exec(cmd, v)
return true
@ -108,9 +118,11 @@ func (c *command) run(cmd string) bool {
}
func (c *command) exec(cmd string, v igniter) {
if v != nil {
if v == nil {
return
}
c.app.config.SetActiveView(cmd)
c.app.config.Save()
c.app.inject(v)
}
}

View File

@ -12,9 +12,6 @@ func toPerc(f float64) string {
func deltas(c, n string) string {
c, n = strings.TrimSpace(c), strings.TrimSpace(n)
// log.Debug().Msgf("`%s` vs `%s`", c, n)
if c == "n/a" {
return n
}
@ -83,13 +80,13 @@ func delta(s string) string {
}
func plus(s string) string {
return suffix(s, "+")
return suffix(s, "")
}
func minus(s string) string {
return suffix(s, "-")
return suffix(s, "⬇︎")
}
func suffix(s, su string) string {
return s + "(" + su + ")"
return s + su
}

View File

@ -20,7 +20,7 @@ func computeMaxColumns(pads maxyPad, sortCol int, table resource.TableData) {
var row int
for _, rev := range table.Rows {
for index, field := range rev.Fields {
if len(field) > pads[index] && isASCII(field) {
if len(field) > pads[index] {
pads[index] = len([]rune(field))
}
}

View File

@ -44,7 +44,7 @@ func TestMaxColumn(t *testing.T) {
},
},
0,
maxyPad{3, 5},
maxyPad{28, 5},
},
}

View File

@ -3,7 +3,6 @@ package views
import (
"context"
"fmt"
"reflect"
"time"
"github.com/derailed/k9s/internal/resource"
@ -11,12 +10,13 @@ import (
"github.com/rs/zerolog/log"
rbacv1 "k8s.io/api/rbac/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/watch"
)
var fuHeader = append(resource.Row{"NAMESPACE", "NAME", "GROUP", "BINDING"}, rbacHeaderVerbs...)
const policyTitle = "Policy"
type fuView struct {
var policyHeader = append(resource.Row{"NAMESPACE", "NAME", "API GROUP", "BINDING"}, rbacHeaderVerbs...)
type policyView struct {
*tableView
current igniter
@ -26,10 +26,10 @@ type fuView struct {
cache resource.RowEvents
}
func newFuView(app *appView, subject, name string) *fuView {
v := fuView{}
func newPolicyView(app *appView, subject, name string) *policyView {
v := policyView{}
{
v.subjectKind, v.subjectName = v.mapSubject(subject), name
v.subjectKind, v.subjectName = mapSubject(subject), name
v.tableView = newTableView(app, v.getTitle())
v.colorerFn = rbacColorer
v.current = app.content.GetPrimitive("main").(igniter)
@ -40,16 +40,16 @@ func newFuView(app *appView, subject, name string) *fuView {
}
// Init the view.
func (v *fuView) init(_ context.Context, ns string) {
v.sortCol = sortColumn{1, len(rbacHeader), true}
func (v *policyView) init(c context.Context, ns string) {
v.sortCol = sortColumn{1, len(rbacHeader), false}
ctx, cancel := context.WithCancel(context.Background())
ctx, cancel := context.WithCancel(c)
v.cancel = cancel
go func(ctx context.Context) {
for {
select {
case <-ctx.Done():
log.Debug().Msg("FU Watch bailing out!")
log.Debug().Msgf("Policy %s:%s Watch bailing out!", v.subjectKind, v.subjectName)
return
case <-time.After(time.Duration(v.app.config.K9s.RefreshRate) * time.Second):
v.refresh()
@ -62,7 +62,7 @@ func (v *fuView) init(_ context.Context, ns string) {
v.app.SetFocus(v)
}
func (v *fuView) bindKeys() {
func (v *policyView) bindKeys() {
delete(v.actions, KeyShiftA)
v.actions[tcell.KeyEscape] = newKeyAction("Reset", v.resetCmd, false)
@ -75,11 +75,11 @@ func (v *fuView) bindKeys() {
v.actions[KeyShiftB] = newKeyAction("Sort Binding", v.sortColCmd(3), true)
}
func (v *fuView) getTitle() string {
return fmt.Sprintf(rbacTitleFmt, "Fu", v.subjectKind+":"+v.subjectName)
func (v *policyView) getTitle() string {
return fmt.Sprintf(rbacTitleFmt, policyTitle, v.subjectKind+":"+v.subjectName)
}
func (v *fuView) refresh() {
func (v *policyView) refresh() {
data, err := v.reconcile()
if err != nil {
log.Error().Err(err).Msgf("Unable to reconcile for %s:%s", v.subjectKind, v.subjectName)
@ -87,7 +87,7 @@ func (v *fuView) refresh() {
v.update(data)
}
func (v *fuView) resetCmd(evt *tcell.EventKey) *tcell.EventKey {
func (v *policyView) resetCmd(evt *tcell.EventKey) *tcell.EventKey {
if !v.cmdBuff.empty() {
v.cmdBuff.reset()
return nil
@ -96,31 +96,36 @@ func (v *fuView) resetCmd(evt *tcell.EventKey) *tcell.EventKey {
return v.backCmd(evt)
}
func (v *fuView) backCmd(evt *tcell.EventKey) *tcell.EventKey {
func (v *policyView) backCmd(evt *tcell.EventKey) *tcell.EventKey {
if v.cancel != nil {
v.cancel()
}
if v.cmdBuff.isActive() {
v.cmdBuff.reset()
} else {
v.app.prevCmd(evt)
return nil
}
v.app.inject(v.current)
return nil
}
func (v *fuView) hints() hints {
func (v *policyView) hints() hints {
return v.actions.toHints()
}
func (v *fuView) reconcile() (resource.TableData, error) {
func (v *policyView) reconcile() (resource.TableData, error) {
var table resource.TableData
log.Debug().Msgf(">>> Policy %s-%s", v.subjectKind, v.subjectName)
evts, errs := v.clusterPolicies()
if len(errs) > 0 {
for _, err := range errs {
log.Debug().Err(err).Msg("Unable to find cluster policies")
}
return resource.TableData{}, errs[0]
return table, errs[0]
}
nevts, errs := v.namespacePolicies()
@ -128,66 +133,29 @@ func (v *fuView) reconcile() (resource.TableData, error) {
for _, err := range errs {
log.Debug().Err(err).Msg("Unable to find cluster policies")
}
return resource.TableData{}, errs[0]
return table, errs[0]
}
for k, v := range nevts {
evts[k] = v
}
data := resource.TableData{
Header: fuHeader,
Rows: make(resource.RowEvents, len(evts)),
Namespace: "*",
return buildTable(v, evts), nil
}
noDeltas := make(resource.Row, len(fuHeader))
if len(v.cache) == 0 {
for k, ev := range evts {
ev.Action = resource.New
ev.Deltas = noDeltas
data.Rows[k] = ev
func (v *policyView) header() resource.Row {
return policyHeader
}
func (v *policyView) getCache() resource.RowEvents {
return v.cache
}
func (v *policyView) setCache(evts resource.RowEvents) {
v.cache = evts
return data, nil
}
for k, ev := range evts {
data.Rows[k] = ev
newr := ev.Fields
if _, ok := v.cache[k]; !ok {
ev.Action, ev.Deltas = watch.Added, noDeltas
continue
}
oldr := v.cache[k].Fields
deltas := make(resource.Row, len(newr))
if !reflect.DeepEqual(oldr, newr) {
ev.Action = watch.Modified
for i, field := range oldr {
if field != newr[i] {
deltas[i] = field
}
}
ev.Deltas = deltas
} else {
ev.Action = resource.Unchanged
ev.Deltas = noDeltas
}
}
v.cache = evts
for k := range v.cache {
if _, ok := data.Rows[k]; !ok {
delete(v.cache, k)
}
}
return data, nil
}
func (v *fuView) clusterPolicies() (resource.RowEvents, []error) {
func (v *policyView) clusterPolicies() (resource.RowEvents, []error) {
var errs []error
evts := make(resource.RowEvents)
@ -196,22 +164,22 @@ func (v *fuView) clusterPolicies() (resource.RowEvents, []error) {
return evts, errs
}
var roles []string
for _, c := range crbs.Items {
for _, s := range c.Subjects {
var rr []string
for _, crb := range crbs.Items {
for _, s := range crb.Subjects {
if s.Kind == v.subjectKind && s.Name == v.subjectName {
roles = append(roles, c.RoleRef.Name)
rr = append(rr, crb.RoleRef.Name)
}
}
}
log.Debug().Msgf("Matching clusterroles: %#v", rr)
for _, r := range roles {
cr, err := v.app.conn().DialOrDie().Rbac().ClusterRoles().Get(r, metav1.GetOptions{})
for _, r := range rr {
role, err := v.app.conn().DialOrDie().Rbac().ClusterRoles().Get(r, metav1.GetOptions{})
if err != nil {
errs = append(errs, err)
}
e := v.parseRules("*", r, cr.Rules)
for k, v := range e {
for k, v := range v.parseRules("*", "CR:"+r, role.Rules) {
evts[k] = v
}
}
@ -219,7 +187,11 @@ func (v *fuView) clusterPolicies() (resource.RowEvents, []error) {
return evts, errs
}
func (v *fuView) namespacePolicies() (resource.RowEvents, []error) {
type namespacedRole struct {
ns, role string
}
func (v *policyView) namespacePolicies() (resource.RowEvents, []error) {
var errs []error
evts := make(resource.RowEvents)
@ -228,25 +200,22 @@ func (v *fuView) namespacePolicies() (resource.RowEvents, []error) {
return evts, errs
}
type nsRole struct {
ns, role string
}
var roles []nsRole
var rr []namespacedRole
for _, rb := range rbs.Items {
for _, s := range rb.Subjects {
if s.Kind == v.subjectKind && s.Name == v.subjectName {
roles = append(roles, nsRole{rb.Namespace, rb.RoleRef.Name})
rr = append(rr, namespacedRole{rb.Namespace, rb.RoleRef.Name})
}
}
}
log.Debug().Msgf("Matching roles: %#v", rr)
for _, r := range roles {
for _, r := range rr {
cr, err := v.app.conn().DialOrDie().Rbac().Roles(r.ns).Get(r.role, metav1.GetOptions{})
if err != nil {
errs = append(errs, err)
}
e := v.parseRules(r.ns, r.role, cr.Rules)
for k, v := range e {
for k, v := range v.parseRules(r.ns, "RO:"+r.role, cr.Rules) {
evts[k] = v
}
}
@ -254,11 +223,11 @@ func (v *fuView) namespacePolicies() (resource.RowEvents, []error) {
return evts, errs
}
func (v *fuView) namespace(ns, n string) string {
func namespacedName(ns, n string) string {
return ns + "/" + n
}
func (v *fuView) parseRules(ns, binding string, rules []rbacv1.PolicyRule) resource.RowEvents {
func (v *policyView) parseRules(ns, binding string, rules []rbacv1.PolicyRule) resource.RowEvents {
m := make(resource.RowEvents, len(rules))
for _, r := range rules {
for _, grp := range r.APIGroups {
@ -269,11 +238,11 @@ func (v *fuView) parseRules(ns, binding string, rules []rbacv1.PolicyRule) resou
}
for _, na := range r.ResourceNames {
n := k + "/" + na
m[v.namespace(ns, n)] = &resource.RowEvent{
m[namespacedName(ns, n)] = &resource.RowEvent{
Fields: v.prepRow(ns, n, grp, binding, r.Verbs),
}
}
m[v.namespace(ns, k)] = &resource.RowEvent{
m[namespacedName(ns, k)] = &resource.RowEvent{
Fields: v.prepRow(ns, k, grp, binding, r.Verbs),
}
}
@ -282,7 +251,7 @@ func (v *fuView) parseRules(ns, binding string, rules []rbacv1.PolicyRule) resou
if nres[0] != '/' {
nres = "/" + nres
}
m[v.namespace(ns, nres)] = &resource.RowEvent{
m[namespacedName(ns, nres)] = &resource.RowEvent{
Fields: v.prepRow(ns, nres, resource.NAValue, binding, r.Verbs),
}
}
@ -291,13 +260,7 @@ func (v *fuView) parseRules(ns, binding string, rules []rbacv1.PolicyRule) resou
return m
}
func (v *fuView) prepRow(ns, res, grp, binding string, verbs []string) resource.Row {
const (
nameLen = 60
groupLen = 30
nsLen = 30
)
func (v *policyView) prepRow(ns, res, grp, binding string, verbs []string) resource.Row {
if grp != resource.NAValue {
grp = toGroup(grp)
}
@ -305,14 +268,14 @@ func (v *fuView) prepRow(ns, res, grp, binding string, verbs []string) resource.
return v.makeRow(ns, res, grp, binding, asVerbs(verbs...))
}
func (*fuView) makeRow(ns, res, group, binding string, verbs []string) resource.Row {
r := make(resource.Row, 0, len(fuHeader))
func (*policyView) makeRow(ns, res, group, binding string, verbs []string) resource.Row {
r := make(resource.Row, 0, len(policyHeader))
r = append(r, ns, res, group, binding)
return append(r, verbs...)
}
func (v *fuView) mapSubject(subject string) string {
func mapSubject(subject string) string {
switch subject {
case "g":
return "Group"

View File

@ -3,7 +3,6 @@ package views
import (
"context"
"fmt"
"reflect"
"strings"
"time"
@ -12,7 +11,6 @@ import (
"github.com/rs/zerolog/log"
rbacv1 "k8s.io/api/rbac/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/watch"
)
const (
@ -50,7 +48,8 @@ var (
"DELETE",
"EXTRAS",
}
rbacHeader = append(resource.Row{"NAME", "GROUP"}, rbacHeaderVerbs...)
rbacHeader = append(resource.Row{"NAME", "API GROUP"}, rbacHeaderVerbs...)
k8sVerbs = []string{
"get",
@ -93,10 +92,10 @@ func newRBACView(app *appView, ns, name string, kind roleKind) *rbacView {
}
// Init the view.
func (v *rbacView) init(_ context.Context, ns string) {
func (v *rbacView) init(c context.Context, ns string) {
v.sortCol = sortColumn{1, len(rbacHeader), true}
ctx, cancel := context.WithCancel(context.Background())
ctx, cancel := context.WithCancel(c)
v.cancel = cancel
go func(ctx context.Context) {
for {
@ -122,16 +121,11 @@ func (v *rbacView) bindKeys() {
v.actions[KeySlash] = newKeyAction("Filter", v.activateCmd, false)
v.actions[KeyP] = newKeyAction("Previous", v.app.prevCmd, false)
v.actions[KeyShiftO] = newKeyAction("Sort Groups", v.sortColCmd(1), true)
v.actions[KeyShiftO] = newKeyAction("Sort APIGroup", v.sortColCmd(1), true)
}
func (v *rbacView) getTitle() string {
title := "ClusterRole"
if v.roleType == role {
title = "Role"
}
return fmt.Sprintf(rbacTitleFmt, title, v.roleName)
return fmt.Sprintf(rbacTitleFmt, rbacTitle, v.roleName)
}
func (v *rbacView) hints() hints {
@ -139,6 +133,7 @@ func (v *rbacView) hints() hints {
}
func (v *rbacView) refresh() {
log.Debug().Msg("RBAC Watching...")
data, err := v.reconcile(v.currentNS, v.roleName, v.roleType)
if err != nil {
log.Error().Err(err).Msgf("Unable to reconcile for %s:%d", v.roleName, v.roleType)
@ -162,69 +157,35 @@ func (v *rbacView) backCmd(evt *tcell.EventKey) *tcell.EventKey {
if v.cmdBuff.isActive() {
v.cmdBuff.reset()
} else {
v.app.prevCmd(evt)
return nil
}
v.app.inject(v.current)
return nil
}
func (v *rbacView) reconcile(ns, name string, kind roleKind) (resource.TableData, error) {
var table resource.TableData
evts, err := v.rowEvents(ns, name, kind)
if err != nil {
return resource.TableData{}, err
return table, err
}
data := resource.TableData{
Header: rbacHeader,
Rows: make(resource.RowEvents, len(evts)),
Namespace: resource.NotNamespaced,
return buildTable(v, evts), nil
}
noDeltas := make(resource.Row, len(rbacHeader))
if len(v.cache) == 0 {
for k, ev := range evts {
ev.Action = resource.New
ev.Deltas = noDeltas
data.Rows[k] = ev
func (v *rbacView) header() resource.Row {
return rbacHeader
}
func (v *rbacView) getCache() resource.RowEvents {
return v.cache
}
func (v *rbacView) setCache(evts resource.RowEvents) {
v.cache = evts
return data, nil
}
for k, ev := range evts {
data.Rows[k] = ev
newr := ev.Fields
if _, ok := v.cache[k]; !ok {
ev.Action, ev.Deltas = watch.Added, noDeltas
continue
}
oldr := v.cache[k].Fields
deltas := make(resource.Row, len(newr))
if !reflect.DeepEqual(oldr, newr) {
ev.Action = watch.Modified
for i, field := range oldr {
if field != newr[i] {
deltas[i] = field
}
}
ev.Deltas = deltas
} else {
ev.Action = resource.Unchanged
ev.Deltas = noDeltas
}
}
v.cache = evts
for k := range v.cache {
if _, ok := data.Rows[k]; !ok {
delete(v.cache, k)
}
}
return data, nil
}
func (v *rbacView) rowEvents(ns, name string, kind roleKind) (resource.RowEvents, error) {
@ -260,6 +221,7 @@ func (v *rbacView) clusterPolicies(name string) (resource.RowEvents, error) {
func (v *rbacView) namespacedPolicies(path string) (resource.RowEvents, error) {
ns, na := namespaced(path)
log.Debug().Msgf("!!!! YO %s %s", ns, na)
cr, err := v.app.conn().DialOrDie().Rbac().Roles(ns).Get(na, metav1.GetOptions{})
if err != nil {
return nil, err

View File

@ -4,6 +4,8 @@ import (
"github.com/derailed/k9s/internal/k8s"
"github.com/derailed/k9s/internal/resource"
"github.com/gdamore/tcell"
"github.com/rs/zerolog/log"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
type (
@ -27,7 +29,7 @@ type (
)
func helpCmds(c k8s.Connection) map[string]resCmd {
cmdMap := resourceViews()
cmdMap := resourceViews(c)
cmds := make(map[string]resCmd, len(cmdMap))
for k, v := range cmdMap {
cmds[k] = v
@ -81,12 +83,35 @@ func showRBAC(app *appView, ns, resource, selection string) {
if resource == "role" {
kind = role
}
app.command.pushCmd("policies")
app.inject(newRBACView(app, ns, selection, kind))
}
func resourceViews() map[string]resCmd {
return map[string]resCmd{
func showClusterRole(app *appView, ns, resource, selection string) {
crb, err := app.conn().DialOrDie().Rbac().ClusterRoleBindings().Get(selection, metav1.GetOptions{})
if err != nil {
app.flash(flashErr, "Unable to retrieve crb", selection)
return
}
app.inject(newRBACView(app, ns, crb.RoleRef.Name, clusterRole))
}
func showRole(app *appView, _, resource, selection string) {
ns, n := namespaced(selection)
rb, err := app.conn().DialOrDie().Rbac().RoleBindings(ns).Get(n, metav1.GetOptions{})
if err != nil {
app.flash(flashErr, "Unable to retrieve rb", selection)
return
}
app.inject(newRBACView(app, ns, namespacedName(ns, rb.RoleRef.Name), role))
}
func showSAPolicy(app *appView, _, _, selection string) {
_, n := namespaced(selection)
app.inject(newPolicyView(app, mapFuSubject("ServiceAccount"), n))
}
func resourceViews(c k8s.Connection) map[string]resCmd {
cmds := map[string]resCmd{
"cm": {
title: "ConfigMaps",
api: "",
@ -105,7 +130,7 @@ func resourceViews() map[string]resCmd {
api: "rbac.authorization.k8s.io",
viewFn: newResourceView,
listFn: resource.NewClusterRoleBindingList,
// decorateFn: crbDecorator,
enterFn: showClusterRole,
},
"crd": {
title: "CustomResourceDefinitions",
@ -153,12 +178,6 @@ func resourceViews() map[string]resCmd {
listFn: resource.NewEventList,
colorerFn: evColorer,
},
"hpa": {
title: "HorizontalPodAutoscalers",
api: "autoscaling",
viewFn: newResourceView,
listFn: resource.NewHPAList,
},
"ing": {
title: "Ingress",
api: "extensions",
@ -218,6 +237,7 @@ func resourceViews() map[string]resCmd {
api: "rbac.authorization.k8s.io",
viewFn: newResourceView,
listFn: resource.NewRoleBindingList,
enterFn: showRole,
},
"rc": {
title: "ReplicationControllers",
@ -245,6 +265,7 @@ func resourceViews() map[string]resCmd {
api: "",
viewFn: newResourceView,
listFn: resource.NewServiceAccountList,
enterFn: showSAPolicy,
},
"sec": {
title: "Secrets",
@ -264,7 +285,52 @@ func resourceViews() map[string]resCmd {
api: "",
viewFn: newResourceView,
listFn: resource.NewServiceList,
// decorateFn: svcDecorator,
},
"usr": {
title: "Users",
api: "",
viewFn: newSubjectView,
},
"grp": {
title: "Groups",
api: "",
viewFn: newSubjectView,
},
}
rev, ok := c.SupportsRes("autoscaling", []string{"v1", "v2beta1", "v2beta2"})
if !ok {
log.Warn().Msg("HPA are not supported on this cluster")
}
switch rev {
case "v1":
log.Debug().Msg("Using HPA V1!")
cmds["hpa"] = resCmd{
title: "HorizontalPodAutoscalers",
api: "autoscaling",
viewFn: newResourceView,
listFn: resource.NewHPAV1List,
}
case "v2beta1":
log.Debug().Msg("Using HPA V2Beta1!")
cmds["hpa"] = resCmd{
title: "HorizontalPodAutoscalers",
api: "autoscaling",
viewFn: newResourceView,
listFn: resource.NewHPAV2Beta1List,
}
case "v2beta2":
log.Debug().Msg("Using HPA V2Beta2!")
cmds["hpa"] = resCmd{
title: "HorizontalPodAutoscalers",
api: "autoscaling",
viewFn: newResourceView,
listFn: resource.NewHPAList,
}
default:
log.Panic().Msgf("K9s does not currently support HPA version %s", rev)
}
return cmds
}

View File

@ -144,9 +144,10 @@ func (v *resourceView) setDecorateFn(f decorateFn) {
// Actions...
func (v *resourceView) enterCmd(*tcell.EventKey) *tcell.EventKey {
v.app.flash(flashInfo, "Enter pressed...")
if v.enterFn != nil {
v.enterFn(v.app, v.list.GetNamespace(), v.list.GetName(), v.selectedItem)
} else {
v.defaultEnter(v.app, v.list.GetNamespace(), v.list.GetName(), v.selectedItem)
}
return nil
}
@ -187,17 +188,15 @@ func (v *resourceView) deleteCmd(evt *tcell.EventKey) *tcell.EventKey {
return nil
}
func (v *resourceView) describeCmd(evt *tcell.EventKey) *tcell.EventKey {
if !v.rowSelected() {
return evt
}
func (v *resourceView) defaultEnter(app *appView, ns, resource, selection string) {
sel := v.getSelectedItem()
raw, err := v.list.Resource().Describe(v.title, sel, v.app.flags)
if err != nil {
v.app.flash(flashErr, err.Error())
log.Warn().Msgf("Describe %v", err.Error())
return evt
return
}
details := v.GetPrimitive("details").(*detailsView)
{
details.setCategory("Describe")
@ -207,6 +206,15 @@ func (v *resourceView) describeCmd(evt *tcell.EventKey) *tcell.EventKey {
details.ScrollToBeginning()
}
v.switchPage("details")
}
func (v *resourceView) describeCmd(evt *tcell.EventKey) *tcell.EventKey {
if !v.rowSelected() {
return evt
}
v.defaultEnter(v.app, v.list.GetNamespace(), v.list.GetName(), v.selectedItem)
return nil
}
@ -383,7 +391,7 @@ func (v *resourceView) refreshActions() {
}
}
aa[tcell.KeyEnter] = newKeyAction("Enter", v.enterCmd, true)
aa[tcell.KeyEnter] = newKeyAction("Enter", v.enterCmd, false)
aa[tcell.KeyCtrlR] = newKeyAction("Refresh", v.refreshCmd, false)
aa[KeyHelp] = newKeyAction("Help", v.app.noopCmd, false)

320
internal/views/subject.go Normal file
View File

@ -0,0 +1,320 @@
package views
import (
"context"
"fmt"
"reflect"
"strings"
"time"
"github.com/derailed/k9s/internal/resource"
"github.com/gdamore/tcell"
"github.com/rs/zerolog/log"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/watch"
)
const subjectTitleFmt = " [aqua::b]%s([fuchsia::b]%s[aqua::-])"
var subjectHeader = resource.Row{"NAME", "KIND", "FIRST LOCATION"}
type (
cachedEventer interface {
header() resource.Row
getCache() resource.RowEvents
setCache(resource.RowEvents)
}
subjectView struct {
*tableView
current igniter
cancel context.CancelFunc
subjectKind string
selectedItem string
cache resource.RowEvents
}
)
func newSubjectView(ns string, app *appView, list resource.List) resourceViewer {
v := subjectView{}
{
v.tableView = newTableView(app, v.getTitle())
v.tableView.SetSelectionChangedFunc(v.selChanged)
v.colorerFn = rbacColorer
v.bindKeys()
}
if current, ok := app.content.GetPrimitive("main").(igniter); ok {
v.current = current
} else {
v.current = &v
}
return &v
}
// Init the view.
func (v *subjectView) init(c context.Context, _ string) {
if v.cancel != nil {
v.cancel()
}
v.sortCol = sortColumn{1, len(rbacHeader), true}
v.subjectKind = mapCmdSubject(v.app.config.K9s.ActiveCluster().View.Active)
v.baseTitle = v.getTitle()
ctx, cancel := context.WithCancel(c)
v.cancel = cancel
go func(ctx context.Context) {
for {
select {
case <-ctx.Done():
log.Debug().Msgf("Subject:%s Watch bailing out!", v.subjectKind)
return
case <-time.After(time.Duration(v.app.config.K9s.RefreshRate) * time.Second):
v.refresh()
v.app.Draw()
}
}
}(ctx)
v.refresh()
v.app.SetFocus(v)
}
func (v *subjectView) setColorerFn(f colorerFn) {}
func (v *subjectView) setEnterFn(f enterFn) {}
func (v *subjectView) setDecorateFn(f decorateFn) {}
func (v *subjectView) bindKeys() {
// No time data or ns
delete(v.actions, KeyShiftA)
delete(v.actions, KeyShiftP)
v.actions[tcell.KeyEnter] = newKeyAction("RBAC", v.rbackCmd, true)
v.actions[tcell.KeyEscape] = newKeyAction("Reset", v.resetCmd, false)
v.actions[KeySlash] = newKeyAction("Filter", v.activateCmd, false)
v.actions[KeyP] = newKeyAction("Previous", v.app.prevCmd, false)
v.actions[KeyShiftK] = newKeyAction("Sort Kind", v.sortColCmd(1), true)
}
func (v *subjectView) getTitle() string {
return fmt.Sprintf(rbacTitleFmt, "Subject", v.subjectKind)
}
func (v *subjectView) selChanged(r, _ int) {
if r == 0 {
v.selectedItem = ""
return
}
v.selectedItem = strings.TrimSpace(v.GetCell(r, 0).Text)
}
func (v *subjectView) SetSubject(s string) {
v.subjectKind = mapSubject(s)
}
func (v *subjectView) refresh() {
data, err := v.reconcile()
if err != nil {
log.Error().Err(err).Msgf("Unable to reconcile for %s", v.subjectKind)
}
v.update(data)
}
func (v *subjectView) rbackCmd(evt *tcell.EventKey) *tcell.EventKey {
if v.selectedItem == "" {
return evt
}
if v.cancel != nil {
v.cancel()
}
_, n := namespaced(v.selectedItem)
v.app.inject(newPolicyView(v.app, mapFuSubject(v.subjectKind), n))
return nil
}
func (v *subjectView) resetCmd(evt *tcell.EventKey) *tcell.EventKey {
if !v.cmdBuff.empty() {
v.cmdBuff.reset()
return nil
}
return v.backCmd(evt)
}
func (v *subjectView) backCmd(evt *tcell.EventKey) *tcell.EventKey {
if v.cancel != nil {
v.cancel()
}
if v.cmdBuff.isActive() {
v.cmdBuff.reset()
return nil
}
v.app.inject(v.current)
return nil
}
func (v *subjectView) hints() hints {
return v.actions.toHints()
}
func (v *subjectView) reconcile() (resource.TableData, error) {
var table resource.TableData
evts, err := v.clusterSubjects()
if err != nil {
return table, err
}
log.Debug().Msgf("Cluster evts %d", len(evts))
nevts, err := v.namespacedSubjects()
if err != nil {
return table, err
}
log.Debug().Msgf("NS evts %d", len(nevts))
for k, v := range nevts {
evts[k] = v
}
return buildTable(v, evts), nil
}
func (v *subjectView) header() resource.Row {
return subjectHeader
}
func (v *subjectView) getCache() resource.RowEvents {
return v.cache
}
func (v *subjectView) setCache(evts resource.RowEvents) {
v.cache = evts
}
func buildTable(v cachedEventer, evts resource.RowEvents) resource.TableData {
table := resource.TableData{
Header: v.header(),
Rows: make(resource.RowEvents, len(evts)),
Namespace: "*",
}
noDeltas := make(resource.Row, len(v.header()))
cache := v.getCache()
if len(cache) == 0 {
for k, ev := range evts {
ev.Action = resource.New
ev.Deltas = noDeltas
table.Rows[k] = ev
}
v.setCache(evts)
return table
}
for k, ev := range evts {
table.Rows[k] = ev
newr := ev.Fields
if _, ok := cache[k]; !ok {
ev.Action, ev.Deltas = watch.Added, noDeltas
continue
}
oldr := cache[k].Fields
deltas := make(resource.Row, len(newr))
if !reflect.DeepEqual(oldr, newr) {
ev.Action = watch.Modified
for i, field := range oldr {
if field != newr[i] {
deltas[i] = field
}
}
ev.Deltas = deltas
} else {
ev.Action = resource.Unchanged
ev.Deltas = noDeltas
}
}
for k := range evts {
if _, ok := table.Rows[k]; !ok {
delete(evts, k)
}
}
v.setCache(evts)
return table
}
func (v *subjectView) clusterSubjects() (resource.RowEvents, error) {
crbs, err := v.app.conn().DialOrDie().Rbac().ClusterRoleBindings().List(metav1.ListOptions{})
if err != nil {
return nil, err
}
evts := make(resource.RowEvents, len(crbs.Items))
for _, crb := range crbs.Items {
for _, s := range crb.Subjects {
if s.Kind == v.subjectKind {
evts[s.Name] = &resource.RowEvent{
Fields: v.makeRow("*", s.Name, "ClusterRoleBinding", crb.Name),
}
}
}
}
return evts, nil
}
func (v *subjectView) makeRow(_, subject, kind, loc string) resource.Row {
return resource.Row{subject, kind, loc}
}
func (v *subjectView) namespacedSubjects() (resource.RowEvents, error) {
rbs, err := v.app.conn().DialOrDie().Rbac().RoleBindings("").List(metav1.ListOptions{})
if err != nil {
return nil, err
}
evts := make(resource.RowEvents, len(rbs.Items))
for _, rb := range rbs.Items {
for _, s := range rb.Subjects {
if s.Kind == v.subjectKind {
evts[s.Name] = &resource.RowEvent{
Fields: v.makeRow(rb.Namespace, s.Name, "RoleBinding", rb.Name),
}
}
}
}
return evts, nil
}
func mapCmdSubject(subject string) string {
switch subject {
case "grp":
return "Group"
case "sas":
return "ServiceAccount"
default:
return "User"
}
}
func mapFuSubject(subject string) string {
switch subject {
case "Group":
return "g"
case "ServiceAccount":
return "s"
default:
return "u"
}
}

View File

@ -298,10 +298,10 @@ func (v *tableView) sortIndicator(index int, name string) string {
func (v *tableView) doUpdate(data resource.TableData) {
v.currentNS = data.Namespace
if v.currentNS == resource.AllNamespaces || v.currentNS == "*" {
if v.currentNS == resource.AllNamespaces && v.currentNS != "*" {
v.actions[KeyShiftP] = newKeyAction("Sort Namespace", v.sortNamespaceCmd, true)
} else {
delete(v.actions, KeyShiftS)
delete(v.actions, KeyShiftP)
}
v.Clear()
@ -378,7 +378,7 @@ func (v *tableView) addHeaderCell(col int, name string, pads maxyPad) {
func (v *tableView) addBodyCell(row, col int, field, delta string, color tcell.Color, pads maxyPad) {
var pField string
if isASCII(field) {
pField = pad(deltas(delta, field), pads[col])
pField = pad(deltas(delta, field), pads[col]+5)
} else {
pField = deltas(delta, field)
}
@ -420,7 +420,7 @@ func (v *tableView) resetTitle() {
rc--
}
switch v.currentNS {
case resource.NotNamespaced:
case resource.NotNamespaced, "*":
title = fmt.Sprintf(titleFmt, v.baseTitle, rc)
default:
ns := v.currentNS