added subject view + v1 hpa support
parent
06c66a7d15
commit
a0763f920d
|
|
@ -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.
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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 + "%"
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@ func TestMaxColumn(t *testing.T) {
|
|||
},
|
||||
},
|
||||
0,
|
||||
maxyPad{3, 5},
|
||||
maxyPad{28, 5},
|
||||
},
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in New Issue