diff --git a/change_logs/release_0.5.0.md b/change_logs/release_0.5.0.md new file mode 100644 index 00000000..394b16e2 --- /dev/null +++ b/change_logs/release_0.5.0.md @@ -0,0 +1,51 @@ + + +# Release v0.5.0 + +## 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!! + +Also if you dig this tool, please make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer) + +--- + +## Change Logs + +I am super excited about this drop of K9s. Lot's of cool improvements based on K9s friends excellent feedback! + + +### Popeye + +Turns out [Popeye](https://github.com/derailed/popeye) is in too much flux at present, thus I've decided to remove it from K9s for the time being. + +### ContainerView + +Added a container view to list all the containers available on a given pod. On a selected pod, you can now press `` to view all of it's associated containers. Once in container view pressing `` on a selected container, will show the container logs. + +### Resource Traversals + +> Ever wanted to know where your pods originated from? + +Fear not, K9s has got your back! Some folks have expressed desires to navigate from a deployment to it's pods or see which pods are running on a given node. Whether you are starting from a Node, a Deployment, ReplicaSet, DaemonSet or StatefulSet, you can now simply `` of a selected item a view the associated pods. [Issue #149](https://github.com/derailed/k9s/issues/149) + +### RollingBack ReplicaSets + +You can now select a ReplicaSet and rollback your Deployment to that version. Enter the command mode via `:rs` to view ReplicaSets, select the replica you want to rollback to and use `Ctrl-B` to rollback your deployment to that revision. + +--- + +## Resolved Bugs + ++ [Issue #163](https://github.com/derailed/k9s/issues/163) ++ [Issue #162](https://github.com/derailed/k9s/issues/162) ++ [Issue #39](https://github.com/derailed/k9s/issues/39) ++ [Issue #27](https://github.com/derailed/k9s/issues/27) + +--- + + © 2019 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0) diff --git a/go.mod b/go.mod index 414b5c7c..160a7575 100644 --- a/go.mod +++ b/go.mod @@ -13,14 +13,17 @@ require ( github.com/rs/zerolog v1.12.0 github.com/spf13/cobra v0.0.3 github.com/stretchr/testify v1.2.2 + golang.org/x/image v0.0.0-20190321063152-3fc05d484e9f // indirect gopkg.in/yaml.v2 v2.2.2 k8s.io/api v0.0.0-20190202010724-74b699b93c15 k8s.io/apimachinery v0.0.0-20190207091153-095b9d203467 k8s.io/cli-runtime v0.0.0-20190207094101-a32b78e5dd0a k8s.io/client-go v10.0.0+incompatible k8s.io/klog v0.2.0 // indirect + k8s.io/kube-openapi v0.0.0-20190401085232-94e1e7b7574c // indirect k8s.io/kubernetes v1.13.3 k8s.io/metrics v0.0.0-20181121073115-d8618695b08f sigs.k8s.io/structured-merge-diff v0.0.0-20190404181321-646549c5a231 // indirect sigs.k8s.io/yaml v1.1.0 + vbom.ml/util v0.0.0-20180919145318-efcd4e0f9787 ) diff --git a/go.sum b/go.sum index a4e96cf9..5b6aaf3f 100644 --- a/go.sum +++ b/go.sum @@ -240,7 +240,10 @@ golang.org/x/crypto v0.0.0-20181106171534-e4dc69e5b2fd/go.mod h1:6SG95UA2DQfeDnf golang.org/x/crypto v0.0.0-20190211182817-74369b46fc67/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90PveolxSbWFaJdECFbxSq0Mqo2M= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4 h1:c2HOrn5iMezYjSlGPncknSEr/8x5LELb/ilJbXi9DEA= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/image v0.0.0-20190321063152-3fc05d484e9f h1:FO4MZ3N56GnxbqxGKqh+YTzUWQ2sDwtFQEZgLOxh9Jc= +golang.org/x/image v0.0.0-20190321063152-3fc05d484e9f/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20181217174547-8f45f776aaf1/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= @@ -361,6 +364,8 @@ k8s.io/klog v0.2.0 h1:0ElL0OHzF3N+OhoJTL0uca20SxtYt4X4+bzHeqrB83c= k8s.io/klog v0.2.0/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk= k8s.io/kube-openapi v0.0.0-20190215190454-ea82251f3668 h1:M80qeWaBNOX2Uc4plRHcb6k+3YE5VWMaJXKZo+tX9aU= k8s.io/kube-openapi v0.0.0-20190215190454-ea82251f3668/go.mod h1:BXM9ceUBTj2QnfH2MK1odQs778ajze1RxcmP6S8RVVc= +k8s.io/kube-openapi v0.0.0-20190401085232-94e1e7b7574c h1:kJCzg2vGCzah5icgkKR7O1Dzn0NA2iGlym27sb0ZfGE= +k8s.io/kube-openapi v0.0.0-20190401085232-94e1e7b7574c/go.mod h1:BXM9ceUBTj2QnfH2MK1odQs778ajze1RxcmP6S8RVVc= k8s.io/kubernetes v1.13.3 h1:46t44D87wKtdKFgr/lXM60K8xPrW0wO67Woof3Vsv6E= k8s.io/kubernetes v1.13.3/go.mod h1:ocZa8+6APFNC2tX1DZASIbocyYT5jHzqFVsY5aoB7Jk= k8s.io/metrics v0.0.0-20181121073115-d8618695b08f h1:HyUoIBzks9xTaSnMJ6kv/SSmwaQQccokuiriu2cV0aA= @@ -373,3 +378,5 @@ sigs.k8s.io/structured-merge-diff v0.0.0-20190404181321-646549c5a231 h1:jn9MygT5 sigs.k8s.io/structured-merge-diff v0.0.0-20190404181321-646549c5a231/go.mod h1:wWxsB5ozmmv/SG7nM11ayaAW51xMvak/t1r0CSlcokI= sigs.k8s.io/yaml v1.1.0 h1:4A07+ZFc2wgJwo8YNlQpr1rVlgUDlxXHhPJciaPY5gs= sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o= +vbom.ml/util v0.0.0-20180919145318-efcd4e0f9787 h1:O69FD9pJA4WUZlEwYatBEEkRWKQ5cKodWpdKTrCS/iQ= +vbom.ml/util v0.0.0-20180919145318-efcd4e0f9787/go.mod h1:so/NYdZXCz+E3ZpW0uAoCj6uzU2+8OWDFv/HxUSs7kI= diff --git a/internal/k8s/api.go b/internal/k8s/api.go index 558f90fa..5414f774 100644 --- a/internal/k8s/api.go +++ b/internal/k8s/api.go @@ -38,6 +38,8 @@ type ( Get(ns string, name string) (interface{}, error) List(ns string) (Collection, error) Delete(ns string, name string) error + SetFieldSelector(string) + SetLabelSelector(string) } // Connection represents a Kubenetes apiserver connection. @@ -78,8 +80,7 @@ func InitConnectionOrDie(config *Config, logger zerolog.Logger) *APIClient { return &conn } -// ValidNamespaces returns a collection of valid namespaces. -// Bozo!! filter active? +// ValidNamespaces returns all available namespaces. func (a *APIClient) ValidNamespaces() ([]v1.Namespace, error) { nn, err := a.DialOrDie().CoreV1().Namespaces().List(metav1.ListOptions{}) if err != nil { diff --git a/internal/k8s/base.go b/internal/k8s/base.go new file mode 100644 index 00000000..cf849dbe --- /dev/null +++ b/internal/k8s/base.go @@ -0,0 +1,20 @@ +package k8s + +type base struct { + fieldSelector string + labelSelector string +} + +// SetFieldSelector refines query results via selector. +func (b *base) SetFieldSelector(s string) { + b.fieldSelector = s +} + +// SetLabelSelector refines query results via labels. +func (b *base) SetLabelSelector(s string) { + b.labelSelector = s +} + +func (b *base) HasSelectors() bool { + return b.labelSelector != "" || b.fieldSelector != "" +} diff --git a/internal/k8s/cluster_role.go b/internal/k8s/cluster_role.go index ec3eec91..ccd88dc9 100644 --- a/internal/k8s/cluster_role.go +++ b/internal/k8s/cluster_role.go @@ -6,12 +6,13 @@ import ( // ClusterRole represents a Kubernetes ClusterRole type ClusterRole struct { + *base Connection } // NewClusterRole returns a new ClusterRole. -func NewClusterRole(c Connection) Cruder { - return &ClusterRole{c} +func NewClusterRole(c Connection) *ClusterRole { + return &ClusterRole{&base{}, c} } // Get a cluster role. @@ -21,7 +22,11 @@ func (c *ClusterRole) Get(_, n string) (interface{}, error) { // List all ClusterRoles on a cluster. func (c *ClusterRole) List(_ string) (Collection, error) { - rr, err := c.DialOrDie().RbacV1().ClusterRoles().List(metav1.ListOptions{}) + opts := metav1.ListOptions{ + LabelSelector: c.labelSelector, + FieldSelector: c.fieldSelector, + } + rr, err := c.DialOrDie().RbacV1().ClusterRoles().List(opts) if err != nil { return nil, err } diff --git a/internal/k8s/cluster_roleb.go b/internal/k8s/cluster_roleb.go index 153e1ac3..3f4271a1 100644 --- a/internal/k8s/cluster_roleb.go +++ b/internal/k8s/cluster_roleb.go @@ -6,12 +6,13 @@ import ( // ClusterRoleBinding represents a Kubernetes ClusterRoleBinding type ClusterRoleBinding struct { + *base Connection } // NewClusterRoleBinding returns a new ClusterRoleBinding. -func NewClusterRoleBinding(c Connection) Cruder { - return &ClusterRoleBinding{c} +func NewClusterRoleBinding(c Connection) *ClusterRoleBinding { + return &ClusterRoleBinding{&base{}, c} } // Get a service. @@ -21,7 +22,11 @@ func (c *ClusterRoleBinding) Get(_, n string) (interface{}, error) { // List all ClusterRoleBindings on a cluster. func (c *ClusterRoleBinding) List(_ string) (Collection, error) { - rr, err := c.DialOrDie().RbacV1().ClusterRoleBindings().List(metav1.ListOptions{}) + opts := metav1.ListOptions{ + LabelSelector: c.labelSelector, + FieldSelector: c.fieldSelector, + } + rr, err := c.DialOrDie().RbacV1().ClusterRoleBindings().List(opts) if err != nil { return Collection{}, err } diff --git a/internal/k8s/cm.go b/internal/k8s/cm.go index 82166d94..a2688837 100644 --- a/internal/k8s/cm.go +++ b/internal/k8s/cm.go @@ -6,12 +6,13 @@ import ( // ConfigMap represents a Kubernetes ConfigMap type ConfigMap struct { + *base Connection } // NewConfigMap returns a new ConfigMap. -func NewConfigMap(c Connection) Cruder { - return &ConfigMap{c} +func NewConfigMap(c Connection) *ConfigMap { + return &ConfigMap{&base{}, c} } // Get a ConfigMap. @@ -21,7 +22,11 @@ func (c *ConfigMap) Get(ns, n string) (interface{}, error) { // List all ConfigMaps in a given namespace. func (c *ConfigMap) List(ns string) (Collection, error) { - rr, err := c.DialOrDie().CoreV1().ConfigMaps(ns).List(metav1.ListOptions{}) + opts := metav1.ListOptions{ + LabelSelector: c.labelSelector, + FieldSelector: c.fieldSelector, + } + rr, err := c.DialOrDie().CoreV1().ConfigMaps(ns).List(opts) if err != nil { return nil, err } diff --git a/internal/k8s/context.go b/internal/k8s/context.go index 0a2e8906..6fb379e7 100644 --- a/internal/k8s/context.go +++ b/internal/k8s/context.go @@ -32,12 +32,13 @@ func (c *NamedContext) MustCurrentContextName() string { // Context represents a Kubernetes Context. type Context struct { + *base Connection } // NewContext returns a new Context. -func NewContext(c Connection) Cruder { - return &Context{c} +func NewContext(c Connection) *Context { + return &Context{&base{}, c} } // Get a Context. diff --git a/internal/k8s/crd.go b/internal/k8s/crd.go index 91b61b80..93f56346 100644 --- a/internal/k8s/crd.go +++ b/internal/k8s/crd.go @@ -4,24 +4,29 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) -// CRD represents a Kubernetes CRD -type CRD struct { +// CustomResourceDefinition represents a Kubernetes CustomResourceDefinition +type CustomResourceDefinition struct { + *base Connection } -// NewCRD returns a new CRD. -func NewCRD(c Connection) Cruder { - return &CRD{c} +// NewCustomResourceDefinition returns a new CustomResourceDefinition. +func NewCustomResourceDefinition(c Connection) *CustomResourceDefinition { + return &CustomResourceDefinition{&base{}, c} } -// Get a CRD. -func (c *CRD) Get(_, n string) (interface{}, error) { +// Get a CustomResourceDefinition. +func (c *CustomResourceDefinition) Get(_, n string) (interface{}, error) { return c.NSDialOrDie().Get(n, metav1.GetOptions{}) } -// List all CRDs in a given namespace. -func (c *CRD) List(string) (Collection, error) { - rr, err := c.NSDialOrDie().List(metav1.ListOptions{}) +// List all CustomResourceDefinitions in a given namespace. +func (c *CustomResourceDefinition) List(string) (Collection, error) { + opts := metav1.ListOptions{ + LabelSelector: c.labelSelector, + FieldSelector: c.fieldSelector, + } + rr, err := c.NSDialOrDie().List(opts) if err != nil { return nil, err } @@ -33,7 +38,7 @@ func (c *CRD) List(string) (Collection, error) { return cc, nil } -// Delete a CRD. -func (c *CRD) Delete(_, n string) error { +// Delete a CustomResourceDefinition. +func (c *CustomResourceDefinition) Delete(_, n string) error { return c.NSDialOrDie().Delete(n, nil) } diff --git a/internal/k8s/cronjob.go b/internal/k8s/cronjob.go index c0a38880..b88bd46e 100644 --- a/internal/k8s/cronjob.go +++ b/internal/k8s/cronjob.go @@ -11,12 +11,13 @@ const maxJobNameSize = 42 // CronJob represents a Kubernetes CronJob. type CronJob struct { + *base Connection } // NewCronJob returns a new CronJob. -func NewCronJob(c Connection) Cruder { - return &CronJob{c} +func NewCronJob(c Connection) *CronJob { + return &CronJob{&base{}, c} } // Get a CronJob. @@ -26,7 +27,11 @@ func (c *CronJob) Get(ns, n string) (interface{}, error) { // List all CronJobs in a given namespace. func (c *CronJob) List(ns string) (Collection, error) { - rr, err := c.DialOrDie().BatchV1beta1().CronJobs(ns).List(metav1.ListOptions{}) + opts := metav1.ListOptions{ + LabelSelector: c.labelSelector, + FieldSelector: c.fieldSelector, + } + rr, err := c.DialOrDie().BatchV1beta1().CronJobs(ns).List(opts) if err != nil { return nil, err } diff --git a/internal/k8s/dp.go b/internal/k8s/dp.go index 00ea93c8..7b1eb21c 100644 --- a/internal/k8s/dp.go +++ b/internal/k8s/dp.go @@ -6,12 +6,13 @@ import ( // Deployment represents a Kubernetes Deployment. type Deployment struct { + *base Connection } // NewDeployment returns a new Deployment. -func NewDeployment(c Connection) Cruder { - return &Deployment{c} +func NewDeployment(c Connection) *Deployment { + return &Deployment{&base{}, c} } // Get a deployment. @@ -21,7 +22,11 @@ func (d *Deployment) Get(ns, n string) (interface{}, error) { // List all Deployments in a given namespace. func (d *Deployment) List(ns string) (Collection, error) { - rr, err := d.DialOrDie().Apps().Deployments(ns).List(metav1.ListOptions{}) + opts := metav1.ListOptions{ + LabelSelector: d.labelSelector, + FieldSelector: d.fieldSelector, + } + rr, err := d.DialOrDie().Apps().Deployments(ns).List(opts) if err != nil { return nil, err } diff --git a/internal/k8s/ds.go b/internal/k8s/ds.go index a8cafb65..b39f2739 100644 --- a/internal/k8s/ds.go +++ b/internal/k8s/ds.go @@ -6,12 +6,13 @@ import ( // DaemonSet represents a Kubernetes DaemonSet type DaemonSet struct { + *base Connection } // NewDaemonSet returns a new DaemonSet. -func NewDaemonSet(c Connection) Cruder { - return &DaemonSet{c} +func NewDaemonSet(c Connection) *DaemonSet { + return &DaemonSet{&base{}, c} } // Get a DaemonSet. @@ -21,7 +22,11 @@ func (d *DaemonSet) Get(ns, n string) (interface{}, error) { // List all DaemonSets in a given namespace. func (d *DaemonSet) List(ns string) (Collection, error) { - rr, err := d.DialOrDie().ExtensionsV1beta1().DaemonSets(ns).List(metav1.ListOptions{}) + opts := metav1.ListOptions{ + LabelSelector: d.labelSelector, + FieldSelector: d.fieldSelector, + } + rr, err := d.DialOrDie().ExtensionsV1beta1().DaemonSets(ns).List(opts) if err != nil { return nil, err } diff --git a/internal/k8s/ep.go b/internal/k8s/ep.go index d974ce11..d7e36b8d 100644 --- a/internal/k8s/ep.go +++ b/internal/k8s/ep.go @@ -6,12 +6,13 @@ import ( // Endpoints represents a Kubernetes Endpoints. type Endpoints struct { + *base Connection } // NewEndpoints returns a new Endpoints. -func NewEndpoints(c Connection) Cruder { - return &Endpoints{c} +func NewEndpoints(c Connection) *Endpoints { + return &Endpoints{&base{}, c} } // Get a Endpoint. @@ -21,7 +22,11 @@ func (e *Endpoints) Get(ns, n string) (interface{}, error) { // List all Endpoints in a given namespace. func (e *Endpoints) List(ns string) (Collection, error) { - rr, err := e.DialOrDie().CoreV1().Endpoints(ns).List(metav1.ListOptions{}) + opts := metav1.ListOptions{ + LabelSelector: e.labelSelector, + FieldSelector: e.fieldSelector, + } + rr, err := e.DialOrDie().CoreV1().Endpoints(ns).List(opts) if err != nil { return nil, err } diff --git a/internal/k8s/evt.go b/internal/k8s/evt.go index ba51c7f3..805537c3 100644 --- a/internal/k8s/evt.go +++ b/internal/k8s/evt.go @@ -6,12 +6,13 @@ import ( // Event represents a Kubernetes Event. type Event struct { + *base Connection } // NewEvent returns a new Event. -func NewEvent(c Connection) Cruder { - return &Event{c} +func NewEvent(c Connection) *Event { + return &Event{&base{}, c} } // Get a Event. @@ -21,7 +22,11 @@ func (e *Event) Get(ns, n string) (interface{}, error) { // List all Events in a given namespace. func (e *Event) List(ns string) (Collection, error) { - rr, err := e.DialOrDie().CoreV1().Events(ns).List(metav1.ListOptions{}) + opts := metav1.ListOptions{ + LabelSelector: e.labelSelector, + FieldSelector: e.fieldSelector, + } + rr, err := e.DialOrDie().CoreV1().Events(ns).List(opts) if err != nil { return nil, err } diff --git a/internal/k8s/helpers.go b/internal/k8s/helpers.go index c6ed23a4..44c46100 100644 --- a/internal/k8s/helpers.go +++ b/internal/k8s/helpers.go @@ -6,7 +6,8 @@ import ( const megaByte = 1024 * 1024 -func asMi(v int64) float64 { +// ToMB converts bytes to megabytes. +func ToMB(v int64) float64 { return float64(v) / megaByte } diff --git a/internal/k8s/helpers_test.go b/internal/k8s/helpers_test.go index 3c20f1c9..df159933 100644 --- a/internal/k8s/helpers_test.go +++ b/internal/k8s/helpers_test.go @@ -20,7 +20,7 @@ func TestToPerc(t *testing.T) { } } -func TestAsMi(t *testing.T) { +func TestToMB(t *testing.T) { uu := []struct { v int64 e float64 @@ -31,6 +31,6 @@ func TestAsMi(t *testing.T) { } for _, u := range uu { - assert.Equal(t, u.e, asMi(u.v)) + assert.Equal(t, u.e, ToMB(u.v)) } } diff --git a/internal/k8s/hpa_v1.go b/internal/k8s/hpa_v1.go index c64f882a..6a61f689 100644 --- a/internal/k8s/hpa_v1.go +++ b/internal/k8s/hpa_v1.go @@ -4,24 +4,29 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) -// HPAV1 represents am HorizontalPodAutoscaler. -type HPAV1 struct { +// HorizontalPodAutoscalerV1 represents am HorizontalPodAutoscaler. +type HorizontalPodAutoscalerV1 struct { + *base Connection } -// NewHPAV1 returns a new HPA. -func NewHPAV1(c Connection) Cruder { - return &HPAV1{c} +// NewHorizontalPodAutoscalerV1 returns a new HorizontalPodAutoscaler. +func NewHorizontalPodAutoscalerV1(c Connection) *HorizontalPodAutoscalerV1 { + return &HorizontalPodAutoscalerV1{&base{}, c} } -// Get a HPA. -func (h *HPAV1) Get(ns, n string) (interface{}, error) { +// Get a HorizontalPodAutoscaler. +func (h *HorizontalPodAutoscalerV1) 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{}) +// List all HorizontalPodAutoscalers in a given namespace. +func (h *HorizontalPodAutoscalerV1) List(ns string) (Collection, error) { + opts := metav1.ListOptions{ + LabelSelector: h.labelSelector, + FieldSelector: h.fieldSelector, + } + rr, err := h.DialOrDie().AutoscalingV1().HorizontalPodAutoscalers(ns).List(opts) if err != nil { return nil, err } @@ -32,7 +37,7 @@ func (h *HPAV1) List(ns string) (Collection, error) { return cc, nil } -// Delete a HPA. -func (h *HPAV1) Delete(ns, n string) error { +// Delete a HorizontalPodAutoscaler. +func (h *HorizontalPodAutoscalerV1) Delete(ns, n string) error { return h.DialOrDie().AutoscalingV1().HorizontalPodAutoscalers(ns).Delete(n, nil) } diff --git a/internal/k8s/hpa_v2beta1.go b/internal/k8s/hpa_v2beta1.go index 18a3e9c2..3908ab96 100644 --- a/internal/k8s/hpa_v2beta1.go +++ b/internal/k8s/hpa_v2beta1.go @@ -4,24 +4,29 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) -// HPAV2Beta1 represents am HorizontalPodAutoscaler. -type HPAV2Beta1 struct { +// HorizontalPodAutoscalerV2Beta1 represents am HorizontalPodAutoscaler. +type HorizontalPodAutoscalerV2Beta1 struct { + *base Connection } -// NewHPAV2Beta1 returns a new HPA. -func NewHPAV2Beta1(c Connection) Cruder { - return &HPAV2Beta1{c} +// NewHorizontalPodAutoscalerV2Beta1 returns a new HorizontalPodAutoscaler. +func NewHorizontalPodAutoscalerV2Beta1(c Connection) *HorizontalPodAutoscalerV2Beta1 { + return &HorizontalPodAutoscalerV2Beta1{&base{}, c} } -// Get a HPA. -func (h *HPAV2Beta1) Get(ns, n string) (interface{}, error) { +// Get a HorizontalPodAutoscaler. +func (h *HorizontalPodAutoscalerV2Beta1) 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 *HPAV2Beta1) List(ns string) (Collection, error) { - rr, err := h.DialOrDie().AutoscalingV2beta2().HorizontalPodAutoscalers(ns).List(metav1.ListOptions{}) +// List all HorizontalPodAutoscalers in a given namespace. +func (h *HorizontalPodAutoscalerV2Beta1) List(ns string) (Collection, error) { + opts := metav1.ListOptions{ + LabelSelector: h.labelSelector, + FieldSelector: h.fieldSelector, + } + rr, err := h.DialOrDie().AutoscalingV2beta2().HorizontalPodAutoscalers(ns).List(opts) if err != nil { return nil, err } @@ -32,7 +37,7 @@ func (h *HPAV2Beta1) List(ns string) (Collection, error) { return cc, nil } -// Delete a HPA. -func (h *HPAV2Beta1) Delete(ns, n string) error { +// Delete a HorizontalPodAutoscaler. +func (h *HorizontalPodAutoscalerV2Beta1) Delete(ns, n string) error { return h.DialOrDie().AutoscalingV2beta1().HorizontalPodAutoscalers(ns).Delete(n, nil) } diff --git a/internal/k8s/hpa_v2beta2.go b/internal/k8s/hpa_v2beta2.go index b2ad9c10..17b2a930 100644 --- a/internal/k8s/hpa_v2beta2.go +++ b/internal/k8s/hpa_v2beta2.go @@ -6,24 +6,29 @@ import ( var supportedAutoScalingAPIVersions = []string{"v2beta2", "v2beta1", "v1"} -// HPAV2Beta2 represents am HorizontalPodAutoscaler. -type HPAV2Beta2 struct { +// HorizontalPodAutoscalerV2Beta2 represents am HorizontalPodAutoscaler. +type HorizontalPodAutoscalerV2Beta2 struct { + *base Connection } -// NewHPAV2Beta2 returns a new HPAV2Beta2. -func NewHPAV2Beta2(c Connection) Cruder { - return &HPAV2Beta2{c} +// NewHorizontalPodAutoscalerV2Beta2 returns a new HorizontalPodAutoscalerV2Beta2. +func NewHorizontalPodAutoscalerV2Beta2(c Connection) *HorizontalPodAutoscalerV2Beta2 { + return &HorizontalPodAutoscalerV2Beta2{&base{}, c} } -// Get a HPAV2Beta2. -func (h *HPAV2Beta2) Get(ns, n string) (interface{}, error) { +// Get a HorizontalPodAutoscalerV2Beta2. +func (h *HorizontalPodAutoscalerV2Beta2) 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) { - rr, err := h.DialOrDie().AutoscalingV2beta2().HorizontalPodAutoscalers(ns).List(metav1.ListOptions{}) +// List all HorizontalPodAutoscalerV2Beta2s in a given namespace. +func (h *HorizontalPodAutoscalerV2Beta2) List(ns string) (Collection, error) { + opts := metav1.ListOptions{ + LabelSelector: h.labelSelector, + FieldSelector: h.fieldSelector, + } + rr, err := h.DialOrDie().AutoscalingV2beta2().HorizontalPodAutoscalers(ns).List(opts) if err != nil { return nil, err } @@ -34,7 +39,7 @@ func (h *HPAV2Beta2) List(ns string) (Collection, error) { return cc, nil } -// Delete a HPAV2Beta2. -func (h *HPAV2Beta2) Delete(ns, n string) error { +// Delete a HorizontalPodAutoscalerV2Beta2. +func (h *HorizontalPodAutoscalerV2Beta2) Delete(ns, n string) error { return h.DialOrDie().AutoscalingV2beta2().HorizontalPodAutoscalers(ns).Delete(n, nil) } diff --git a/internal/k8s/ing.go b/internal/k8s/ing.go index 32b11f5f..3fe0f4ba 100644 --- a/internal/k8s/ing.go +++ b/internal/k8s/ing.go @@ -6,12 +6,13 @@ import ( // Ingress represents a Kubernetes Ingress. type Ingress struct { + *base Connection } // NewIngress returns a new Ingress. -func NewIngress(c Connection) Cruder { - return &Ingress{c} +func NewIngress(c Connection) *Ingress { + return &Ingress{&base{}, c} } // Get a Ingress. @@ -21,7 +22,11 @@ func (i *Ingress) Get(ns, n string) (interface{}, error) { // List all Ingresss in a given namespace. func (i *Ingress) List(ns string) (Collection, error) { - rr, err := i.DialOrDie().ExtensionsV1beta1().Ingresses(ns).List(metav1.ListOptions{}) + opts := metav1.ListOptions{ + LabelSelector: i.labelSelector, + FieldSelector: i.fieldSelector, + } + rr, err := i.DialOrDie().ExtensionsV1beta1().Ingresses(ns).List(opts) if err != nil { return nil, err } diff --git a/internal/k8s/job.go b/internal/k8s/job.go index 5916807e..c2c133fe 100644 --- a/internal/k8s/job.go +++ b/internal/k8s/job.go @@ -12,6 +12,7 @@ import ( type ( // Job represents a Kubernetes Job. Job struct { + *base Connection } @@ -23,8 +24,8 @@ type ( ) // NewJob returns a new Job. -func NewJob(c Connection) Cruder { - return &Job{c} +func NewJob(c Connection) *Job { + return &Job{&base{}, c} } // Get a Job. @@ -34,7 +35,11 @@ func (j *Job) Get(ns, n string) (interface{}, error) { // List all Jobs in a given namespace. func (j *Job) List(ns string) (Collection, error) { - rr, err := j.DialOrDie().BatchV1().Jobs(ns).List(metav1.ListOptions{}) + opts := metav1.ListOptions{ + LabelSelector: j.labelSelector, + FieldSelector: j.fieldSelector, + } + rr, err := j.DialOrDie().BatchV1().Jobs(ns).List(opts) if err != nil { return nil, err } @@ -57,7 +62,7 @@ func (j *Job) Containers(ns, n string, includeInit bool) ([]string, error) { if err != nil { return nil, err } - return NewPod(j).(Loggable).Containers(ns, pod, includeInit) + return NewPod(j).Containers(ns, pod, includeInit) } // Logs fetch container logs for a given job and container. @@ -66,7 +71,7 @@ func (j *Job) Logs(ns, n, co string, lines int64, prev bool) *restclient.Request if err != nil { return nil } - return NewPod(j).(Loggable).Logs(ns, pod, co, lines, prev) + return NewPod(j).Logs(ns, pod, co, lines, prev) } // Events retrieved jobs events. diff --git a/internal/k8s/metrics.go b/internal/k8s/metrics.go index d476b275..cc9fa1d9 100644 --- a/internal/k8s/metrics.go +++ b/internal/k8s/metrics.go @@ -9,6 +9,7 @@ import ( type ( // MetricsServer serves cluster metrics for nodes and pods. MetricsServer struct { + *base Connection } @@ -43,7 +44,7 @@ type ( // NewMetricsServer return a metric server instance. func NewMetricsServer(c Connection) *MetricsServer { - return &MetricsServer{c} + return &MetricsServer{&base{}, c} } // NodesMetrics retrieves metrics for a given set of nodes. @@ -51,16 +52,16 @@ func (m *MetricsServer) NodesMetrics(nodes []v1.Node, metrics []mv1beta1.NodeMet for _, n := range nodes { mmx[n.Name] = NodeMetrics{ AvailCPU: n.Status.Allocatable.Cpu().MilliValue(), - AvailMEM: asMi(n.Status.Allocatable.Memory().Value()), + AvailMEM: ToMB(n.Status.Allocatable.Memory().Value()), TotalCPU: n.Status.Capacity.Cpu().MilliValue(), - TotalMEM: asMi(n.Status.Capacity.Memory().Value()), + TotalMEM: ToMB(n.Status.Capacity.Memory().Value()), } } for _, c := range metrics { if mx, ok := mmx[c.Name]; ok { mx.CurrentCPU = c.Usage.Cpu().MilliValue() - mx.CurrentMEM = asMi(c.Usage.Memory().Value()) + mx.CurrentMEM = ToMB(c.Usage.Memory().Value()) mmx[c.Name] = mx } } @@ -73,16 +74,16 @@ func (m *MetricsServer) ClusterLoad(nodes []v1.Node, metrics []mv1beta1.NodeMetr for _, n := range nodes { nodeMetrics[n.Name] = NodeMetrics{ AvailCPU: n.Status.Allocatable.Cpu().MilliValue(), - AvailMEM: asMi(n.Status.Allocatable.Memory().Value()), + AvailMEM: ToMB(n.Status.Allocatable.Memory().Value()), TotalCPU: n.Status.Capacity.Cpu().MilliValue(), - TotalMEM: asMi(n.Status.Capacity.Memory().Value()), + TotalMEM: ToMB(n.Status.Capacity.Memory().Value()), } } for _, mx := range metrics { if m, ok := nodeMetrics[mx.Name]; ok { m.CurrentCPU = mx.Usage.Cpu().MilliValue() - m.CurrentMEM = asMi(mx.Usage.Memory().Value()) + m.CurrentMEM = ToMB(mx.Usage.Memory().Value()) nodeMetrics[mx.Name] = m } } @@ -133,7 +134,7 @@ func (m *MetricsServer) PodsMetrics(pods []mv1beta1.PodMetrics, mmx PodsMetrics) var mx PodMetrics for _, c := range p.Containers { mx.CurrentCPU += c.Usage.Cpu().MilliValue() - mx.CurrentMEM += asMi(c.Usage.Memory().Value()) + mx.CurrentMEM += ToMB(c.Usage.Memory().Value()) } mmx[p.Namespace+"/"+p.Name] = mx } diff --git a/internal/k8s/no.go b/internal/k8s/no.go index dc5c48ee..6d41de61 100644 --- a/internal/k8s/no.go +++ b/internal/k8s/no.go @@ -6,12 +6,13 @@ import ( // Node represents a Kubernetes node. type Node struct { + *base Connection } // NewNode returns a new Node. -func NewNode(c Connection) Cruder { - return &Node{c} +func NewNode(c Connection) *Node { + return &Node{&base{}, c} } // Get a node. @@ -21,7 +22,11 @@ func (n *Node) Get(_, name string) (interface{}, error) { // List all nodes on the cluster. func (n *Node) List(_ string) (Collection, error) { - rr, err := n.DialOrDie().CoreV1().Nodes().List(metav1.ListOptions{}) + opts := metav1.ListOptions{ + LabelSelector: n.labelSelector, + FieldSelector: n.fieldSelector, + } + rr, err := n.DialOrDie().CoreV1().Nodes().List(opts) if err != nil { return nil, err } diff --git a/internal/k8s/ns.go b/internal/k8s/ns.go index 340496aa..f83b0b54 100644 --- a/internal/k8s/ns.go +++ b/internal/k8s/ns.go @@ -6,12 +6,13 @@ import ( // Namespace represents a Kubernetes namespace. type Namespace struct { + *base Connection } // NewNamespace returns a new Namespace. -func NewNamespace(c Connection) Cruder { - return &Namespace{c} +func NewNamespace(c Connection) *Namespace { + return &Namespace{&base{}, c} } // Get a active namespace. @@ -21,7 +22,11 @@ func (n *Namespace) Get(_, name string) (interface{}, error) { // List all active namespaces on the cluster. func (n *Namespace) List(_ string) (Collection, error) { - rr, err := n.DialOrDie().CoreV1().Namespaces().List(metav1.ListOptions{}) + opts := metav1.ListOptions{ + LabelSelector: n.labelSelector, + FieldSelector: n.fieldSelector, + } + rr, err := n.DialOrDie().CoreV1().Namespaces().List(opts) if err != nil { return nil, err } diff --git a/internal/k8s/pdb.go b/internal/k8s/pdb.go index 20a0474f..b224c169 100644 --- a/internal/k8s/pdb.go +++ b/internal/k8s/pdb.go @@ -4,14 +4,15 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) -// PodDisruptionBudget represents a PodDisruptionBudget Kubernetes resource. +// PodDisruptionBudget represents a Kubernetes PodDisruptionBudget. type PodDisruptionBudget struct { + *base Connection } // NewPodDisruptionBudget returns a new PodDisruptionBudget. -func NewPodDisruptionBudget(c Connection) Cruder { - return &PodDisruptionBudget{c} +func NewPodDisruptionBudget(c Connection) *PodDisruptionBudget { + return &PodDisruptionBudget{&base{}, c} } // Get a pdb. @@ -21,7 +22,11 @@ func (p *PodDisruptionBudget) Get(ns, n string) (interface{}, error) { // List all pdbs in a given namespace. func (p *PodDisruptionBudget) List(ns string) (Collection, error) { - rr, err := p.DialOrDie().PolicyV1beta1().PodDisruptionBudgets(ns).List(metav1.ListOptions{}) + opts := metav1.ListOptions{ + LabelSelector: p.labelSelector, + FieldSelector: p.fieldSelector, + } + rr, err := p.DialOrDie().PolicyV1beta1().PodDisruptionBudgets(ns).List(opts) if err != nil { return nil, err } diff --git a/internal/k8s/pod.go b/internal/k8s/pod.go index 37bb7206..ad3cd4f6 100644 --- a/internal/k8s/pod.go +++ b/internal/k8s/pod.go @@ -8,14 +8,15 @@ import ( const defaultKillGrace int64 = 5 -// Pod represents a Kubernetes resource. +// Pod represents a Kubernetes Pod. type Pod struct { + *base Connection } // NewPod returns a new Pod. -func NewPod(c Connection) Cruder { - return &Pod{c} +func NewPod(c Connection) *Pod { + return &Pod{base: &base{}, Connection: c} } // Get a pod. @@ -25,7 +26,12 @@ func (p *Pod) Get(ns, name string) (interface{}, error) { // List all pods in a given namespace. func (p *Pod) List(ns string) (Collection, error) { - rr, err := p.DialOrDie().CoreV1().Pods(ns).List(metav1.ListOptions{}) + opts := metav1.ListOptions{ + LabelSelector: p.labelSelector, + FieldSelector: p.fieldSelector, + } + + rr, err := p.DialOrDie().CoreV1().Pods(ns).List(opts) if err != nil { return nil, err } diff --git a/internal/k8s/pv.go b/internal/k8s/pv.go index d28ae1ee..936c82fd 100644 --- a/internal/k8s/pv.go +++ b/internal/k8s/pv.go @@ -4,24 +4,29 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) -// PV represents a Kubernetes PersistentVolume. -type PV struct { +// PeristentVolume represents a Kubernetes PersistentVolume. +type PeristentVolume struct { + *base Connection } -// NewPV returns a new PV. -func NewPV(c Connection) Cruder { - return &PV{c} +// NewPersistentVolume returns a new PeristentVolume. +func NewPersistentVolume(c Connection) *PeristentVolume { + return &PeristentVolume{&base{}, c} } -// Get a PV. -func (p *PV) Get(_, n string) (interface{}, error) { +// Get a PeristentVolume. +func (p *PeristentVolume) Get(_, n string) (interface{}, error) { return p.DialOrDie().CoreV1().PersistentVolumes().Get(n, metav1.GetOptions{}) } -// List all PVs in a given namespace. -func (p *PV) List(_ string) (Collection, error) { - rr, err := p.DialOrDie().CoreV1().PersistentVolumes().List(metav1.ListOptions{}) +// List all PeristentVolumes in a given namespace. +func (p *PeristentVolume) List(_ string) (Collection, error) { + opts := metav1.ListOptions{ + LabelSelector: p.labelSelector, + FieldSelector: p.fieldSelector, + } + rr, err := p.DialOrDie().CoreV1().PersistentVolumes().List(opts) if err != nil { return nil, err } @@ -34,7 +39,7 @@ func (p *PV) List(_ string) (Collection, error) { return cc, nil } -// Delete a PV. -func (p *PV) Delete(_, n string) error { +// Delete a PeristentVolume. +func (p *PeristentVolume) Delete(_, n string) error { return p.DialOrDie().CoreV1().PersistentVolumes().Delete(n, nil) } diff --git a/internal/k8s/pvc.go b/internal/k8s/pvc.go index a4506fd5..bd140a64 100644 --- a/internal/k8s/pvc.go +++ b/internal/k8s/pvc.go @@ -4,24 +4,29 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) -// PVC represents a Kubernetes service. -type PVC struct { +// PersistentVolumeClaim represents a Kubernetes PersistentVolumeClaim. +type PersistentVolumeClaim struct { + *base Connection } -// NewPVC returns a new PVC. -func NewPVC(c Connection) Cruder { - return &PVC{c} +// NewPersistentVolumeClaim returns a new PersistentVolumeClaim. +func NewPersistentVolumeClaim(c Connection) *PersistentVolumeClaim { + return &PersistentVolumeClaim{&base{}, c} } -// Get a PVC. -func (p *PVC) Get(ns, n string) (interface{}, error) { +// Get a PersistentVolumeClaim. +func (p *PersistentVolumeClaim) Get(ns, n string) (interface{}, error) { return p.DialOrDie().CoreV1().PersistentVolumeClaims(ns).Get(n, metav1.GetOptions{}) } -// List all PVCs in a given namespace. -func (p *PVC) List(ns string) (Collection, error) { - rr, err := p.DialOrDie().CoreV1().PersistentVolumeClaims(ns).List(metav1.ListOptions{}) +// List all PersistentVolumeClaims in a given namespace. +func (p *PersistentVolumeClaim) List(ns string) (Collection, error) { + opts := metav1.ListOptions{ + LabelSelector: p.labelSelector, + FieldSelector: p.fieldSelector, + } + rr, err := p.DialOrDie().CoreV1().PersistentVolumeClaims(ns).List(opts) if err != nil { return nil, err } @@ -33,7 +38,7 @@ func (p *PVC) List(ns string) (Collection, error) { return cc, nil } -// Delete a PVC. -func (p *PVC) Delete(ns, n string) error { +// Delete a PersistentVolumeClaim. +func (p *PersistentVolumeClaim) Delete(ns, n string) error { return p.DialOrDie().CoreV1().PersistentVolumeClaims(ns).Delete(n, nil) } diff --git a/internal/k8s/rc.go b/internal/k8s/rc.go index 96b7d285..ab5cc4fc 100644 --- a/internal/k8s/rc.go +++ b/internal/k8s/rc.go @@ -4,14 +4,15 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) -// ReplicationController represents a Kubernetes service. +// ReplicationController represents a Kubernetes ReplicationController. type ReplicationController struct { + *base Connection } // NewReplicationController returns a new ReplicationController. -func NewReplicationController(c Connection) Cruder { - return &ReplicationController{c} +func NewReplicationController(c Connection) *ReplicationController { + return &ReplicationController{&base{}, c} } // Get a RC. @@ -21,7 +22,11 @@ func (r *ReplicationController) Get(ns, n string) (interface{}, error) { // List all RCs in a given namespace. func (r *ReplicationController) List(ns string) (Collection, error) { - rr, err := r.DialOrDie().Core().ReplicationControllers(ns).List(metav1.ListOptions{}) + opts := metav1.ListOptions{ + LabelSelector: r.labelSelector, + FieldSelector: r.fieldSelector, + } + rr, err := r.DialOrDie().Core().ReplicationControllers(ns).List(opts) if err != nil { return nil, err } diff --git a/internal/k8s/resource.go b/internal/k8s/resource.go index 2650a163..64e31790 100644 --- a/internal/k8s/resource.go +++ b/internal/k8s/resource.go @@ -14,14 +14,15 @@ import ( // Resource represents a Kubernetes Resource type Resource struct { + *base Connection group, version, name string } // NewResource returns a new Resource. -func NewResource(c Connection, group, version, name string) Cruder { - return &Resource{Connection: c, group: group, version: version, name: name} +func NewResource(c Connection, group, version, name string) *Resource { + return &Resource{base: &base{}, Connection: c, group: group, version: version, name: name} } // GetInfo returns info about apigroup. @@ -29,7 +30,7 @@ func (r *Resource) GetInfo() (string, string, string) { return r.group, r.version, r.name } -func (r *Resource) base() dynamic.NamespaceableResourceInterface { +func (r *Resource) nsRes() dynamic.NamespaceableResourceInterface { g := schema.GroupVersionResource{ Group: r.group, Version: r.version, @@ -40,7 +41,7 @@ func (r *Resource) base() dynamic.NamespaceableResourceInterface { // Get a Resource. func (r *Resource) Get(ns, n string) (interface{}, error) { - return r.base().Namespace(ns).Get(n, metav1.GetOptions{}) + return r.nsRes().Namespace(ns).Get(n, metav1.GetOptions{}) } // List all Resources in a given namespace. @@ -54,7 +55,7 @@ func (r *Resource) List(ns string) (Collection, error) { // Delete a Resource. func (r *Resource) Delete(ns, n string) error { - return r.base().Namespace(ns).Delete(n, nil) + return r.nsRes().Namespace(ns).Delete(n, nil) } // ---------------------------------------------------------------------------- diff --git a/internal/k8s/role.go b/internal/k8s/role.go index d2448b7d..7b905174 100644 --- a/internal/k8s/role.go +++ b/internal/k8s/role.go @@ -7,12 +7,13 @@ import ( // Role represents a Kubernetes Role. type Role struct { + *base Connection } // NewRole returns a new Role. -func NewRole(c Connection) Cruder { - return &Role{c} +func NewRole(c Connection) *Role { + return &Role{&base{}, c} } // Get a Role. @@ -22,7 +23,11 @@ func (r *Role) Get(ns, n string) (interface{}, error) { // List all Roles in a given namespace. func (r *Role) List(ns string) (Collection, error) { - rr, err := r.DialOrDie().RbacV1().Roles(ns).List(metav1.ListOptions{}) + opts := metav1.ListOptions{ + LabelSelector: r.labelSelector, + FieldSelector: r.fieldSelector, + } + rr, err := r.DialOrDie().RbacV1().Roles(ns).List(opts) if err != nil { return nil, err } diff --git a/internal/k8s/role_binding.go b/internal/k8s/role_binding.go index eaf46987..cc81da4b 100644 --- a/internal/k8s/role_binding.go +++ b/internal/k8s/role_binding.go @@ -4,12 +4,13 @@ import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" // RoleBinding represents a Kubernetes RoleBinding. type RoleBinding struct { + *base Connection } // NewRoleBinding returns a new RoleBinding. -func NewRoleBinding(c Connection) Cruder { - return &RoleBinding{c} +func NewRoleBinding(c Connection) *RoleBinding { + return &RoleBinding{&base{}, c} } // Get a RoleBinding. @@ -19,7 +20,11 @@ func (r *RoleBinding) Get(ns, n string) (interface{}, error) { // List all RoleBindings in a given namespace. func (r *RoleBinding) List(ns string) (Collection, error) { - rr, err := r.DialOrDie().RbacV1().RoleBindings(ns).List(metav1.ListOptions{}) + opts := metav1.ListOptions{ + LabelSelector: r.labelSelector, + FieldSelector: r.fieldSelector, + } + rr, err := r.DialOrDie().RbacV1().RoleBindings(ns).List(opts) if err != nil { return nil, err } diff --git a/internal/k8s/rs.go b/internal/k8s/rs.go index c82e0b0d..bd6f53d7 100644 --- a/internal/k8s/rs.go +++ b/internal/k8s/rs.go @@ -6,12 +6,13 @@ import ( // ReplicaSet represents a Kubernetes ReplicaSet. type ReplicaSet struct { + *base Connection } // NewReplicaSet returns a new ReplicaSet. -func NewReplicaSet(c Connection) Cruder { - return &ReplicaSet{c} +func NewReplicaSet(c Connection) *ReplicaSet { + return &ReplicaSet{&base{}, c} } // Get a ReplicaSet. @@ -21,7 +22,11 @@ func (r *ReplicaSet) Get(ns, n string) (interface{}, error) { // List all ReplicaSets in a given namespace. func (r *ReplicaSet) List(ns string) (Collection, error) { - rr, err := r.DialOrDie().Apps().ReplicaSets(ns).List(metav1.ListOptions{}) + opts := metav1.ListOptions{ + LabelSelector: r.labelSelector, + FieldSelector: r.fieldSelector, + } + rr, err := r.DialOrDie().Apps().ReplicaSets(ns).List(opts) if err != nil { return nil, err } diff --git a/internal/k8s/sa.go b/internal/k8s/sa.go index f3d02ffd..b36dce70 100644 --- a/internal/k8s/sa.go +++ b/internal/k8s/sa.go @@ -6,12 +6,13 @@ import ( // ServiceAccount manages a Kubernetes ServiceAccount. type ServiceAccount struct { + *base Connection } // NewServiceAccount instantiates a new ServiceAccount. -func NewServiceAccount(c Connection) Cruder { - return &ServiceAccount{c} +func NewServiceAccount(c Connection) *ServiceAccount { + return &ServiceAccount{&base{}, c} } // Get a ServiceAccount. @@ -21,7 +22,11 @@ func (s *ServiceAccount) Get(ns, n string) (interface{}, error) { // List all ServiceAccounts in a given namespace. func (s *ServiceAccount) List(ns string) (Collection, error) { - rr, err := s.DialOrDie().CoreV1().ServiceAccounts(ns).List(metav1.ListOptions{}) + opts := metav1.ListOptions{ + LabelSelector: s.labelSelector, + FieldSelector: s.fieldSelector, + } + rr, err := s.DialOrDie().CoreV1().ServiceAccounts(ns).List(opts) if err != nil { return nil, err } diff --git a/internal/k8s/secret.go b/internal/k8s/secret.go index f5d48637..bb84a701 100644 --- a/internal/k8s/secret.go +++ b/internal/k8s/secret.go @@ -6,12 +6,13 @@ import ( // Secret represents a Kubernetes Secret. type Secret struct { + *base Connection } // NewSecret returns a new Secret. -func NewSecret(c Connection) Cruder { - return &Secret{c} +func NewSecret(c Connection) *Secret { + return &Secret{&base{}, c} } // Get a Secret. @@ -21,7 +22,11 @@ func (s *Secret) Get(ns, n string) (interface{}, error) { // List all Secrets in a given namespace. func (s *Secret) List(ns string) (Collection, error) { - rr, err := s.DialOrDie().CoreV1().Secrets(ns).List(metav1.ListOptions{}) + opts := metav1.ListOptions{ + LabelSelector: s.labelSelector, + FieldSelector: s.fieldSelector, + } + rr, err := s.DialOrDie().CoreV1().Secrets(ns).List(opts) if err != nil { return nil, err } diff --git a/internal/k8s/sts.go b/internal/k8s/sts.go index 612e0722..3eba3969 100644 --- a/internal/k8s/sts.go +++ b/internal/k8s/sts.go @@ -6,12 +6,13 @@ import ( // StatefulSet manages a Kubernetes StatefulSet. type StatefulSet struct { + *base Connection } // NewStatefulSet instantiates a new StatefulSet. -func NewStatefulSet(c Connection) Cruder { - return &StatefulSet{c} +func NewStatefulSet(c Connection) *StatefulSet { + return &StatefulSet{&base{}, c} } // Get a StatefulSet. @@ -21,7 +22,11 @@ func (s *StatefulSet) Get(ns, n string) (interface{}, error) { // List all StatefulSets in a given namespace. func (s *StatefulSet) List(ns string) (Collection, error) { - rr, err := s.DialOrDie().AppsV1().StatefulSets(ns).List(metav1.ListOptions{}) + opts := metav1.ListOptions{ + LabelSelector: s.labelSelector, + FieldSelector: s.fieldSelector, + } + rr, err := s.DialOrDie().AppsV1().StatefulSets(ns).List(opts) if err != nil { return nil, err } diff --git a/internal/k8s/svc.go b/internal/k8s/svc.go index 0d54c703..28418e4d 100644 --- a/internal/k8s/svc.go +++ b/internal/k8s/svc.go @@ -6,12 +6,13 @@ import ( // Service represents a Kubernetes Service. type Service struct { + *base Connection } // NewService returns a new Service. -func NewService(c Connection) Cruder { - return &Service{c} +func NewService(c Connection) *Service { + return &Service{&base{}, c} } // Get a service. @@ -21,7 +22,11 @@ func (s *Service) Get(ns, n string) (interface{}, error) { // List all Services in a given namespace. func (s *Service) List(ns string) (Collection, error) { - rr, err := s.DialOrDie().CoreV1().Services(ns).List(metav1.ListOptions{}) + opts := metav1.ListOptions{ + LabelSelector: s.labelSelector, + FieldSelector: s.fieldSelector, + } + rr, err := s.DialOrDie().CoreV1().Services(ns).List(opts) if err != nil { return nil, err } diff --git a/internal/resource/base.go b/internal/resource/base.go index 23e0064f..65f9d901 100644 --- a/internal/resource/base.go +++ b/internal/resource/base.go @@ -20,6 +20,9 @@ type ( Get(ns string, name string) (interface{}, error) List(ns string) (k8s.Collection, error) Delete(ns string, name string) error + SetLabelSelector(string) + SetFieldSelector(string) + HasSelectors() bool } // Connection represents a Kubenetes apiserver connection. @@ -45,6 +48,21 @@ func NewBase(c Connection, r Cruder) *Base { return &Base{Connection: c, Resource: r} } +// HasSelectors returns true if field or label selectors are set. +func (b *Base) HasSelectors() bool { + return b.Resource.HasSelectors() +} + +// SetFieldSelector refines query results via selector. +func (b *Base) SetFieldSelector(s string) { + b.Resource.SetFieldSelector(s) +} + +// SetLabelSelector refines query results via labels. +func (b *Base) SetLabelSelector(s string) { + b.Resource.SetLabelSelector(s) +} + // Name returns the resource namespaced name. func (b *Base) Name() string { return b.path @@ -87,6 +105,7 @@ func (b *Base) Describe(kind, pa string, flags *genericclioptions.ConfigFlags) ( mapping, err := k8s.RestMapping.Find(kind) if err != nil { + log.Debug().Msgf("Unable to find mapper for %s %s", kind, pa) return "", err } diff --git a/internal/resource/container.go b/internal/resource/container.go new file mode 100644 index 00000000..a187c8e4 --- /dev/null +++ b/internal/resource/container.go @@ -0,0 +1,262 @@ +package resource + +import ( + "bufio" + "context" + "fmt" + "strconv" + "time" + + "github.com/derailed/k9s/internal/k8s" + "github.com/rs/zerolog/log" + v1 "k8s.io/api/core/v1" +) + +type ( + // Container represents a container on a pod. + Container struct { + *Base + + pod *v1.Pod + isInit bool + instance v1.Container + MetricsServer MetricsServer + metrics k8s.PodMetrics + } +) + +// NewContainerList returns a collection of container. +func NewContainerList(c Connection, mx MetricsServer, pod *v1.Pod) List { + return NewList( + "", + "co", + NewContainer(c, mx, pod), + 0, + ) +} + +// NewContainer returns a new set of containers. +func NewContainer(c Connection, mx MetricsServer, pod *v1.Pod) *Container { + co := Container{ + Base: &Base{Connection: c, Resource: k8s.NewPod(c)}, + pod: pod, + MetricsServer: mx, + metrics: k8s.PodMetrics{}, + } + co.Factory = &co + + return &co +} + +// New builds a new Container instance from a k8s resource. +func (r *Container) New(i interface{}) Columnar { + co := NewContainer(r.Connection, r.MetricsServer, r.pod) + co.instance = i.(v1.Container) + co.path = r.namespacedName(r.pod.ObjectMeta) + ":" + co.instance.Name + + return co +} + +// Metrics retrieves cpu/mem resource consumption on associated pod. +func (r *Container) Metrics() k8s.PodMetrics { + return r.metrics +} + +// SetMetrics set the current k8s resource metrics on associated pod. +func (r *Container) SetMetrics(m k8s.PodMetrics) { + r.metrics = m +} + +// Marshal resource to yaml. +func (r *Container) Marshal(path string) (string, error) { + return "", nil +} + +// Logs tails a given container logs +func (r *Container) Logs(c chan<- string, ns, n, co string, lines int64, prev bool) (context.CancelFunc, error) { + req := r.Resource.(k8s.Loggable).Logs(ns, n, co, lines, prev) + ctx, cancel := context.WithCancel(context.TODO()) + req.Context(ctx) + + blocked := true + go func() { + select { + case <-time.After(defaultTimeout): + if blocked { + close(c) + cancel() + } + } + }() + // This call will block if nothing is in the stream!! + stream, err := req.Stream() + blocked = false + if err != nil { + log.Error().Msgf("Tail logs failed `%s/%s:%s -- %v", ns, n, co, err) + return cancel, fmt.Errorf("%v", err) + } + + go func() { + defer func() { + stream.Close() + cancel() + close(c) + }() + + scanner := bufio.NewScanner(stream) + for scanner.Scan() { + c <- scanner.Text() + } + }() + + return cancel, nil +} + +// List resources for a given namespace. +func (r *Container) List(ns string) (Columnars, error) { + icos := r.pod.Spec.InitContainers + cos := r.pod.Spec.Containers + + cc := make(Columnars, 0, len(icos)+len(cos)) + for _, co := range icos { + ci := r.New(co) + ci.(*Container).isInit = true + cc = append(cc, ci) + } + for _, co := range cos { + cc = append(cc, r.New(co)) + } + + return cc, nil +} + +// Header return resource header. +func (*Container) Header(ns string) Row { + hh := Row{} + + return append(hh, + "NAME", + "IMAGE", + "READY", + "STATE", + "RS", + "LPROB", + "RPROB", + "CPU", + "MEM", + "RCPU", + "RMEM", + "AGE", + ) +} + +// Fields retrieves displayable fields. +func (r *Container) Fields(ns string) Row { + ff := make(Row, 0, len(r.Header(ns))) + i := r.instance + + mxs, _ := r.MetricsServer.FetchPodsMetrics(r.pod.Namespace) + + var cpu, mem string + for _, mx := range mxs { + if mx.Name != r.pod.Name { + continue + } + for _, co := range mx.Containers { + if co.Name != i.Name { + continue + } + cpu, mem = toRes(co.Usage) + } + } + + rcpu, rmem := resources(i) + + var cs *v1.ContainerStatus + for _, c := range r.pod.Status.ContainerStatuses { + if c.Name != i.Name { + continue + } + cs = &c + } + if cs == nil { + for _, c := range r.pod.Status.InitContainerStatuses { + if c.Name != i.Name { + continue + } + cs = &c + } + } + + return append(ff, + i.Name, + i.Image, + boolToStr(cs.Ready), + toState(cs.State), + strconv.Itoa(int(cs.RestartCount)), + probe(i.LivenessProbe), + probe(i.ReadinessProbe), + cpu, + mem, + rcpu, + rmem, + toAge(r.pod.CreationTimestamp), + ) +} + +// ---------------------------------------------------------------------------- +// Helpers... + +func toState(s v1.ContainerState) string { + switch { + case s.Waiting != nil: + if s.Waiting.Reason != "" { + return s.Waiting.Reason + } + return "Waiting" + + case s.Terminated != nil: + if s.Terminated.Reason != "" { + return s.Terminated.Reason + } + return "Terminated" + case s.Running != nil: + return "Running" + default: + return MissingValue + } +} + +func toRes(r v1.ResourceList) (string, string) { + cpu, mem := r[v1.ResourceCPU], r[v1.ResourceMemory] + + return ToMillicore(cpu.MilliValue()), ToMi(k8s.ToMB(mem.Value())) +} + +func resources(c v1.Container) (cpu, mem string) { + req, lim := c.Resources.Requests, c.Resources.Limits + + if len(req) == 0 { + if len(lim) != 0 { + return toRes(lim) + } + } else { + return toRes(req) + } + + return "0", "0" +} + +func probe(p *v1.Probe) string { + if p == nil { + return "no" + } + + return "yes" +} + +func asMi(v int64) float64 { + const megaByte = 1024 * 1024 + + return float64(v) / megaByte +} diff --git a/internal/resource/crd.go b/internal/resource/crd.go index a559b75e..fcdd9bd2 100644 --- a/internal/resource/crd.go +++ b/internal/resource/crd.go @@ -10,40 +10,40 @@ import ( "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" ) -// CRD tracks a kubernetes resource. -type CRD struct { +// CustomResourceDefinition tracks a kubernetes resource. +type CustomResourceDefinition struct { *Base instance *unstructured.Unstructured } -// NewCRDList returns a new resource list. -func NewCRDList(c Connection, ns string) List { +// NewCustomResourceDefinitionList returns a new resource list. +func NewCustomResourceDefinitionList(c Connection, ns string) List { return NewList( NotNamespaced, "crd", - NewCRD(c), + NewCustomResourceDefinition(c), CRUDAccess|DescribeAccess, ) } -// NewCRD instantiates a new CRD. -func NewCRD(c Connection) *CRD { - crd := &CRD{&Base{Connection: c, Resource: k8s.NewCRD(c)}, nil} +// NewCustomResourceDefinition instantiates a new CustomResourceDefinition. +func NewCustomResourceDefinition(c Connection) *CustomResourceDefinition { + crd := &CustomResourceDefinition{&Base{Connection: c, Resource: k8s.NewCustomResourceDefinition(c)}, nil} crd.Factory = crd return crd } -// New builds a new CRD instance from a k8s resource. -func (r *CRD) New(i interface{}) Columnar { - c := NewCRD(r.Connection) +// New builds a new CustomResourceDefinition instance from a k8s resource. +func (r *CustomResourceDefinition) New(i interface{}) Columnar { + c := NewCustomResourceDefinition(r.Connection) switch instance := i.(type) { case *unstructured.Unstructured: c.instance = instance case unstructured.Unstructured: c.instance = &instance default: - log.Fatal().Msgf("unknown CRD type %#v", i) + log.Fatal().Msgf("unknown CustomResourceDefinition type %#v", i) } meta := c.instance.Object["metadata"].(map[string]interface{}) c.path = meta["name"].(string) @@ -52,7 +52,7 @@ func (r *CRD) New(i interface{}) Columnar { } // Marshal a resource. -func (r *CRD) Marshal(path string) (string, error) { +func (r *CustomResourceDefinition) Marshal(path string) (string, error) { ns, n := namespaced(path) i, err := r.Resource.Get(ns, n) if err != nil { @@ -70,12 +70,12 @@ func (r *CRD) Marshal(path string) (string, error) { } // Header return the resource header. -func (*CRD) Header(ns string) Row { +func (*CustomResourceDefinition) Header(ns string) Row { return Row{"NAME", "AGE"} } // Fields retrieves displayable fields. -func (r *CRD) Fields(ns string) Row { +func (r *CustomResourceDefinition) Fields(ns string) Row { ff := make(Row, 0, len(r.Header(ns))) i := r.instance @@ -89,7 +89,7 @@ func (r *CRD) Fields(ns string) Row { } // ExtFields returns extended fields. -func (r *CRD) ExtFields() Properties { +func (r *CustomResourceDefinition) ExtFields() Properties { var ( pp = Properties{} i = r.instance diff --git a/internal/resource/crd_test.go b/internal/resource/crd_test.go index 9d3e13e7..bd4e5851 100644 --- a/internal/resource/crd_test.go +++ b/internal/resource/crd_test.go @@ -10,12 +10,12 @@ import ( "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" ) -func NewCRDListWithArgs(ns string, r *resource.CRD) resource.List { +func NewCRDListWithArgs(ns string, r *resource.CustomResourceDefinition) resource.List { return resource.NewList("-", "crd", r, resource.CRUDAccess|resource.DescribeAccess) } -func NewCRDWithArgs(conn k8s.Connection, res resource.Cruder) *resource.CRD { - r := &resource.CRD{Base: resource.NewBase(conn, res)} +func NewCRDWithArgs(conn k8s.Connection, res resource.Cruder) *resource.CustomResourceDefinition { + r := &resource.CustomResourceDefinition{Base: resource.NewBase(conn, res)} r.Factory = r return r @@ -130,12 +130,12 @@ func k8sCRDFull() *unstructured.Unstructured { func newCRDFull() resource.Columnar { mc := NewMockConnection() - return resource.NewCRD(mc).New(k8sCRDFull()) + return resource.NewCustomResourceDefinition(mc).New(k8sCRDFull()) } func newCRD() resource.Columnar { mc := NewMockConnection() - return resource.NewCRD(mc).New(k8sCRD()) + return resource.NewCustomResourceDefinition(mc).New(k8sCRD()) } func crdYaml() string { diff --git a/internal/resource/hpa_test.go b/internal/resource/hpa_test.go index e9a35eba..62b3fc39 100644 --- a/internal/resource/hpa_test.go +++ b/internal/resource/hpa_test.go @@ -13,12 +13,12 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) -func NewHPAListWithArgs(ns string, r *resource.HPA) resource.List { +func NewHPAListWithArgs(ns string, r *resource.HorizontalPodAutoscaler) resource.List { return resource.NewList(ns, "hpa", r, resource.AllVerbsAccess|resource.DescribeAccess) } -func NewHPAWithArgs(conn k8s.Connection, res resource.Cruder) *resource.HPA { - r := &resource.HPA{Base: resource.NewBase(conn, res)} +func NewHPAWithArgs(conn k8s.Connection, res resource.Cruder) *resource.HorizontalPodAutoscaler { + r := &resource.HorizontalPodAutoscaler{Base: resource.NewBase(conn, res)} r.Factory = r return r } @@ -127,7 +127,7 @@ func k8sHPA() *autoscalingv2beta2.HorizontalPodAutoscaler { func newHPA() resource.Columnar { mc := NewMockConnection() - return resource.NewHPA(mc).New(k8sHPA()) + return resource.NewHorizontalPodAutoscaler(mc).New(k8sHPA()) } func hpaYaml() string { diff --git a/internal/resource/hpa_v1.go b/internal/resource/hpa_v1.go index 45325abf..fe8ba712 100644 --- a/internal/resource/hpa_v1.go +++ b/internal/resource/hpa_v1.go @@ -8,40 +8,40 @@ import ( autoscalingv1 "k8s.io/api/autoscaling/v1" ) -// HPAV1 tracks a kubernetes resource. -type HPAV1 struct { +// HorizontalPodAutoscalerV1 tracks a kubernetes resource. +type HorizontalPodAutoscalerV1 struct { *Base instance *autoscalingv1.HorizontalPodAutoscaler } -// NewHPAV1List returns a new resource list. -func NewHPAV1List(c Connection, ns string) List { +// NewHorizontalPodAutoscalerV1List returns a new resource list. +func NewHorizontalPodAutoscalerV1List(c Connection, ns string) List { return NewList( ns, "hpa", - NewHPAV1(c), + NewHorizontalPodAutoscalerV1(c), AllVerbsAccess|DescribeAccess, ) } -// NewHPAV1 instantiates a new HPAV1. -func NewHPAV1(c Connection) *HPAV1 { - hpa := &HPAV1{&Base{Connection: c, Resource: k8s.NewHPAV1(c)}, nil} +// NewHorizontalPodAutoscalerV1 instantiates a new HorizontalPodAutoscalerV1. +func NewHorizontalPodAutoscalerV1(c Connection) *HorizontalPodAutoscalerV1 { + hpa := &HorizontalPodAutoscalerV1{&Base{Connection: c, Resource: k8s.NewHorizontalPodAutoscalerV1(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) +// New builds a new HorizontalPodAutoscalerV1 instance from a k8s resource. +func (r *HorizontalPodAutoscalerV1) New(i interface{}) Columnar { + c := NewHorizontalPodAutoscalerV1(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) + log.Fatal().Msgf("unknown HorizontalPodAutoscalerV1 type %#v", i) } c.path = c.namespacedName(c.instance.ObjectMeta) @@ -49,7 +49,7 @@ func (r *HPAV1) New(i interface{}) Columnar { } // Marshal resource to yaml. -func (r *HPAV1) Marshal(path string) (string, error) { +func (r *HorizontalPodAutoscalerV1) Marshal(path string) (string, error) { ns, n := namespaced(path) i, err := r.Resource.Get(ns, n) if err != nil { @@ -64,7 +64,7 @@ func (r *HPAV1) Marshal(path string) (string, error) { } // Header return resource header. -func (*HPAV1) Header(ns string) Row { +func (*HorizontalPodAutoscalerV1) Header(ns string) Row { hh := Row{} if ns == AllNamespaces { hh = append(hh, "NAMESPACE") @@ -81,7 +81,7 @@ func (*HPAV1) Header(ns string) Row { } // Fields retrieves displayable fields. -func (r *HPAV1) Fields(ns string) Row { +func (r *HorizontalPodAutoscalerV1) Fields(ns string) Row { ff := make(Row, 0, len(r.Header(ns))) i := r.instance @@ -103,7 +103,7 @@ func (r *HPAV1) Fields(ns string) Row { // ---------------------------------------------------------------------------- // Helpers... -func (r *HPAV1) toMetrics(spec autoscalingv1.HorizontalPodAutoscalerSpec, status autoscalingv1.HorizontalPodAutoscalerStatus) string { +func (r *HorizontalPodAutoscalerV1) toMetrics(spec autoscalingv1.HorizontalPodAutoscalerSpec, status autoscalingv1.HorizontalPodAutoscalerStatus) string { current := "" if status.CurrentCPUUtilizationPercentage != nil { current = strconv.Itoa(int(*status.CurrentCPUUtilizationPercentage)) + "%" diff --git a/internal/resource/hpa_v2beta1.go b/internal/resource/hpa_v2beta1.go index d02627ac..a20de73c 100644 --- a/internal/resource/hpa_v2beta1.go +++ b/internal/resource/hpa_v2beta1.go @@ -10,40 +10,40 @@ import ( autoscalingv2beta1 "k8s.io/api/autoscaling/v2beta1" ) -// HPAV2Beta1 tracks a kubernetes resource. -type HPAV2Beta1 struct { +// HorizontalPodAutoscalerV2Beta1 tracks a kubernetes resource. +type HorizontalPodAutoscalerV2Beta1 struct { *Base instance *autoscalingv2beta1.HorizontalPodAutoscaler } -// NewHPAV2Beta1List returns a new resource list. -func NewHPAV2Beta1List(c Connection, ns string) List { +// NewHorizontalPodAutoscalerV2Beta1List returns a new resource list. +func NewHorizontalPodAutoscalerV2Beta1List(c Connection, ns string) List { return NewList( ns, "hpa", - NewHPAV2Beta1(c), + NewHorizontalPodAutoscalerV2Beta1(c), AllVerbsAccess|DescribeAccess, ) } -// NewHPAV2Beta1 instantiates a new HPAV2Beta1. -func NewHPAV2Beta1(c Connection) *HPAV2Beta1 { - hpa := &HPAV2Beta1{&Base{Connection: c, Resource: k8s.NewHPAV2Beta1(c)}, nil} +// NewHorizontalPodAutoscalerV2Beta1 instantiates a new HorizontalPodAutoscalerV2Beta1. +func NewHorizontalPodAutoscalerV2Beta1(c Connection) *HorizontalPodAutoscalerV2Beta1 { + hpa := &HorizontalPodAutoscalerV2Beta1{&Base{Connection: c, Resource: k8s.NewHorizontalPodAutoscalerV2Beta1(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) +// New builds a new HorizontalPodAutoscalerV2Beta1 instance from a k8s resource. +func (r *HorizontalPodAutoscalerV2Beta1) New(i interface{}) Columnar { + c := NewHorizontalPodAutoscalerV2Beta1(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) + log.Fatal().Msgf("unknown HorizontalPodAutoscalerV2Beta1 type %#v", i) } c.path = c.namespacedName(c.instance.ObjectMeta) @@ -51,7 +51,7 @@ func (r *HPAV2Beta1) New(i interface{}) Columnar { } // Marshal resource to yaml. -func (r *HPAV2Beta1) Marshal(path string) (string, error) { +func (r *HorizontalPodAutoscalerV2Beta1) Marshal(path string) (string, error) { ns, n := namespaced(path) i, err := r.Resource.Get(ns, n) if err != nil { @@ -66,7 +66,7 @@ func (r *HPAV2Beta1) Marshal(path string) (string, error) { } // Header return resource header. -func (*HPAV2Beta1) Header(ns string) Row { +func (*HorizontalPodAutoscalerV2Beta1) Header(ns string) Row { hh := Row{} if ns == AllNamespaces { hh = append(hh, "NAMESPACE") @@ -83,7 +83,7 @@ func (*HPAV2Beta1) Header(ns string) Row { } // Fields retrieves displayable fields. -func (r *HPAV2Beta1) Fields(ns string) Row { +func (r *HorizontalPodAutoscalerV2Beta1) Fields(ns string) Row { ff := make(Row, 0, len(r.Header(ns))) i := r.instance @@ -105,7 +105,7 @@ func (r *HPAV2Beta1) Fields(ns string) Row { // ---------------------------------------------------------------------------- // Helpers... -func (r *HPAV2Beta1) toMetrics(specs []autoscalingv2beta1.MetricSpec, statuses []autoscalingv2beta1.MetricStatus) string { +func (r *HorizontalPodAutoscalerV2Beta1) toMetrics(specs []autoscalingv2beta1.MetricSpec, statuses []autoscalingv2beta1.MetricStatus) string { if len(specs) == 0 { return "" } @@ -147,7 +147,7 @@ func (r *HPAV2Beta1) toMetrics(specs []autoscalingv2beta1.MetricSpec, statuses [ return ret } -func (*HPAV2Beta1) externalMetrics(i int, spec autoscalingv2beta1.MetricSpec, statuses []autoscalingv2beta1.MetricStatus) string { +func (*HorizontalPodAutoscalerV2Beta1) externalMetrics(i int, spec autoscalingv2beta1.MetricSpec, statuses []autoscalingv2beta1.MetricStatus) string { current := "" if spec.External.TargetAverageValue != nil { @@ -163,7 +163,7 @@ func (*HPAV2Beta1) externalMetrics(i int, spec autoscalingv2beta1.MetricSpec, st return fmt.Sprintf("%s/%s", current, spec.External.TargetValue.String()) } -func (*HPAV2Beta1) resourceMetrics(i int, spec autoscalingv2beta1.MetricSpec, statuses []autoscalingv2beta1.MetricStatus) string { +func (*HorizontalPodAutoscalerV2Beta1) resourceMetrics(i int, spec autoscalingv2beta1.MetricSpec, statuses []autoscalingv2beta1.MetricStatus) string { current := "" if spec.Resource.TargetAverageValue != nil { diff --git a/internal/resource/hpa_v2beta2.go b/internal/resource/hpa_v2beta2.go index 930f5e22..3f54965e 100644 --- a/internal/resource/hpa_v2beta2.go +++ b/internal/resource/hpa_v2beta2.go @@ -10,40 +10,40 @@ import ( autoscalingv2beta2 "k8s.io/api/autoscaling/v2beta2" ) -// HPA tracks a kubernetes resource. -type HPA struct { +// HorizontalPodAutoscaler tracks a kubernetes resource. +type HorizontalPodAutoscaler struct { *Base instance *autoscalingv2beta2.HorizontalPodAutoscaler } -// NewHPAList returns a new resource list. -func NewHPAList(c Connection, ns string) List { +// NewHorizontalPodAutoscalerList returns a new resource list. +func NewHorizontalPodAutoscalerList(c Connection, ns string) List { return NewList( ns, "hpa", - NewHPA(c), + NewHorizontalPodAutoscaler(c), AllVerbsAccess|DescribeAccess, ) } -// NewHPA instantiates a new HPA. -func NewHPA(c Connection) *HPA { - hpa := &HPA{&Base{Connection: c, Resource: k8s.NewHPAV2Beta2(c)}, nil} +// NewHorizontalPodAutoscaler instantiates a new HorizontalPodAutoscaler. +func NewHorizontalPodAutoscaler(c Connection) *HorizontalPodAutoscaler { + hpa := &HorizontalPodAutoscaler{&Base{Connection: c, Resource: k8s.NewHorizontalPodAutoscalerV2Beta2(c)}, nil} hpa.Factory = hpa return hpa } -// New builds a new HPA instance from a k8s resource. -func (r *HPA) New(i interface{}) Columnar { - c := NewHPA(r.Connection) +// New builds a new HorizontalPodAutoscaler instance from a k8s resource. +func (r *HorizontalPodAutoscaler) New(i interface{}) Columnar { + c := NewHorizontalPodAutoscaler(r.Connection) switch instance := i.(type) { case *autoscalingv2beta2.HorizontalPodAutoscaler: c.instance = instance case autoscalingv2beta2.HorizontalPodAutoscaler: c.instance = &instance default: - log.Fatal().Msgf("unknown HPA type %#v", i) + log.Fatal().Msgf("unknown HorizontalPodAutoscaler type %#v", i) } c.path = c.namespacedName(c.instance.ObjectMeta) @@ -51,7 +51,7 @@ func (r *HPA) New(i interface{}) Columnar { } // Marshal resource to yaml. -func (r *HPA) Marshal(path string) (string, error) { +func (r *HorizontalPodAutoscaler) Marshal(path string) (string, error) { ns, n := namespaced(path) i, err := r.Resource.Get(ns, n) if err != nil { @@ -66,7 +66,7 @@ func (r *HPA) Marshal(path string) (string, error) { } // Header return resource header. -func (*HPA) Header(ns string) Row { +func (*HorizontalPodAutoscaler) Header(ns string) Row { hh := Row{} if ns == AllNamespaces { hh = append(hh, "NAMESPACE") @@ -83,7 +83,7 @@ func (*HPA) Header(ns string) Row { } // Fields retrieves displayable fields. -func (r *HPA) Fields(ns string) Row { +func (r *HorizontalPodAutoscaler) Fields(ns string) Row { ff := make(Row, 0, len(r.Header(ns))) i := r.instance diff --git a/internal/resource/list.go b/internal/resource/list.go index c3de5a51..89ab91e0 100644 --- a/internal/resource/list.go +++ b/internal/resource/list.go @@ -65,6 +65,11 @@ type ( Reconcile() error GetName() string Access(flag int) bool + GetAccess() int + SetAccess(int) + SetFieldSelector(string) + SetLabelSelector(string) + HasSelectors() bool } // Columnar tracks resources that can be diplayed in a tabular fashion. @@ -84,7 +89,7 @@ type ( // Columnars a collection of columnars. Columnars []Columnar - // Resource tracks generic Kubernetes resources. + // Resource represents a tabular Kubernetes resource. Resource interface { New(interface{}) Columnar Get(path string) (Columnar, error) @@ -93,6 +98,9 @@ type ( Describe(kind, pa string, flags *genericclioptions.ConfigFlags) (string, error) Marshal(pa string) (string, error) Header(ns string) Row + SetFieldSelector(string) + SetLabelSelector(string) + HasSelectors() bool } list struct { @@ -118,11 +126,35 @@ func NewList(ns, name string, res Resource, verbs int) *list { } } +func (l *list) HasSelectors() bool { + return l.resource.HasSelectors() +} + +// SetFieldSelector narrows down resource query given fields selection. +func (l *list) SetFieldSelector(s string) { + l.resource.SetFieldSelector(s) +} + +// SetLabelSelector narrows down resource query via labels selections. +func (l *list) SetLabelSelector(s string) { + l.resource.SetLabelSelector(s) +} + // Access check access control on a given resource. func (l *list) Access(f int) bool { return l.verbs&f == f } +// Access check access control on a given resource. +func (l *list) GetAccess() int { + return l.verbs +} + +// Access check access control on a given resource. +func (l *list) SetAccess(f int) { + l.verbs = f +} + // Namespaced checks if k8s resource is namespaced. func (l *list) Namespaced() bool { return l.namespace != NotNamespaced diff --git a/internal/resource/mock_cruder_test.go b/internal/resource/mock_cruder_test.go index fffbed9f..0e2bbb0d 100644 --- a/internal/resource/mock_cruder_test.go +++ b/internal/resource/mock_cruder_test.go @@ -52,6 +52,21 @@ func (mock *MockCruder) Get(_param0 string, _param1 string) (interface{}, error) return ret0, ret1 } +func (mock *MockCruder) HasSelectors() bool { + if mock == nil { + panic("mock must not be nil. Use myMock := NewMockCruder().") + } + params := []pegomock.Param{} + result := pegomock.GetGenericMockFrom(mock).Invoke("HasSelectors", params, []reflect.Type{reflect.TypeOf((*bool)(nil)).Elem()}) + var ret0 bool + if len(result) != 0 { + if result[0] != nil { + ret0 = result[0].(bool) + } + } + return ret0 +} + func (mock *MockCruder) List(_param0 string) (k8s.Collection, error) { if mock == nil { panic("mock must not be nil. Use myMock := NewMockCruder().") @@ -71,6 +86,22 @@ func (mock *MockCruder) List(_param0 string) (k8s.Collection, error) { return ret0, ret1 } +func (mock *MockCruder) SetFieldSelector(_param0 string) { + if mock == nil { + panic("mock must not be nil. Use myMock := NewMockCruder().") + } + params := []pegomock.Param{_param0} + pegomock.GetGenericMockFrom(mock).Invoke("SetFieldSelector", params, []reflect.Type{}) +} + +func (mock *MockCruder) SetLabelSelector(_param0 string) { + if mock == nil { + panic("mock must not be nil. Use myMock := NewMockCruder().") + } + params := []pegomock.Param{_param0} + pegomock.GetGenericMockFrom(mock).Invoke("SetLabelSelector", params, []reflect.Type{}) +} + func (mock *MockCruder) VerifyWasCalledOnce() *VerifierCruder { return &VerifierCruder{ mock: mock, @@ -170,6 +201,23 @@ func (c *Cruder_Get_OngoingVerification) GetAllCapturedArguments() (_param0 []st return } +func (verifier *VerifierCruder) HasSelectors() *Cruder_HasSelectors_OngoingVerification { + params := []pegomock.Param{} + methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "HasSelectors", params, verifier.timeout) + return &Cruder_HasSelectors_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} +} + +type Cruder_HasSelectors_OngoingVerification struct { + mock *MockCruder + methodInvocations []pegomock.MethodInvocation +} + +func (c *Cruder_HasSelectors_OngoingVerification) GetCapturedArguments() { +} + +func (c *Cruder_HasSelectors_OngoingVerification) GetAllCapturedArguments() { +} + func (verifier *VerifierCruder) List(_param0 string) *Cruder_List_OngoingVerification { params := []pegomock.Param{_param0} methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "List", params, verifier.timeout) @@ -196,3 +244,57 @@ func (c *Cruder_List_OngoingVerification) GetAllCapturedArguments() (_param0 []s } return } + +func (verifier *VerifierCruder) SetFieldSelector(_param0 string) *Cruder_SetFieldSelector_OngoingVerification { + params := []pegomock.Param{_param0} + methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "SetFieldSelector", params, verifier.timeout) + return &Cruder_SetFieldSelector_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} +} + +type Cruder_SetFieldSelector_OngoingVerification struct { + mock *MockCruder + methodInvocations []pegomock.MethodInvocation +} + +func (c *Cruder_SetFieldSelector_OngoingVerification) GetCapturedArguments() string { + _param0 := c.GetAllCapturedArguments() + return _param0[len(_param0)-1] +} + +func (c *Cruder_SetFieldSelector_OngoingVerification) GetAllCapturedArguments() (_param0 []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) + } + } + return +} + +func (verifier *VerifierCruder) SetLabelSelector(_param0 string) *Cruder_SetLabelSelector_OngoingVerification { + params := []pegomock.Param{_param0} + methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "SetLabelSelector", params, verifier.timeout) + return &Cruder_SetLabelSelector_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} +} + +type Cruder_SetLabelSelector_OngoingVerification struct { + mock *MockCruder + methodInvocations []pegomock.MethodInvocation +} + +func (c *Cruder_SetLabelSelector_OngoingVerification) GetCapturedArguments() string { + _param0 := c.GetAllCapturedArguments() + return _param0[len(_param0)-1] +} + +func (c *Cruder_SetLabelSelector_OngoingVerification) GetAllCapturedArguments() (_param0 []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) + } + } + return +} diff --git a/internal/resource/mock_switchablecruder_test.go b/internal/resource/mock_switchablecruder_test.go index e49e245e..917c28d1 100644 --- a/internal/resource/mock_switchablecruder_test.go +++ b/internal/resource/mock_switchablecruder_test.go @@ -52,6 +52,21 @@ func (mock *MockSwitchableCruder) Get(_param0 string, _param1 string) (interface return ret0, ret1 } +func (mock *MockSwitchableCruder) HasSelectors() bool { + if mock == nil { + panic("mock must not be nil. Use myMock := NewMockSwitchableCruder().") + } + params := []pegomock.Param{} + result := pegomock.GetGenericMockFrom(mock).Invoke("HasSelectors", params, []reflect.Type{reflect.TypeOf((*bool)(nil)).Elem()}) + var ret0 bool + if len(result) != 0 { + if result[0] != nil { + ret0 = result[0].(bool) + } + } + return ret0 +} + func (mock *MockSwitchableCruder) List(_param0 string) (k8s.Collection, error) { if mock == nil { panic("mock must not be nil. Use myMock := NewMockSwitchableCruder().") @@ -86,6 +101,22 @@ func (mock *MockSwitchableCruder) MustCurrentContextName() string { return ret0 } +func (mock *MockSwitchableCruder) SetFieldSelector(_param0 string) { + if mock == nil { + panic("mock must not be nil. Use myMock := NewMockSwitchableCruder().") + } + params := []pegomock.Param{_param0} + pegomock.GetGenericMockFrom(mock).Invoke("SetFieldSelector", params, []reflect.Type{}) +} + +func (mock *MockSwitchableCruder) SetLabelSelector(_param0 string) { + if mock == nil { + panic("mock must not be nil. Use myMock := NewMockSwitchableCruder().") + } + params := []pegomock.Param{_param0} + pegomock.GetGenericMockFrom(mock).Invoke("SetLabelSelector", params, []reflect.Type{}) +} + func (mock *MockSwitchableCruder) Switch(_param0 string) error { if mock == nil { panic("mock must not be nil. Use myMock := NewMockSwitchableCruder().") @@ -200,6 +231,23 @@ func (c *SwitchableCruder_Get_OngoingVerification) GetAllCapturedArguments() (_p return } +func (verifier *VerifierSwitchableCruder) HasSelectors() *SwitchableCruder_HasSelectors_OngoingVerification { + params := []pegomock.Param{} + methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "HasSelectors", params, verifier.timeout) + return &SwitchableCruder_HasSelectors_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} +} + +type SwitchableCruder_HasSelectors_OngoingVerification struct { + mock *MockSwitchableCruder + methodInvocations []pegomock.MethodInvocation +} + +func (c *SwitchableCruder_HasSelectors_OngoingVerification) GetCapturedArguments() { +} + +func (c *SwitchableCruder_HasSelectors_OngoingVerification) GetAllCapturedArguments() { +} + func (verifier *VerifierSwitchableCruder) List(_param0 string) *SwitchableCruder_List_OngoingVerification { params := []pegomock.Param{_param0} methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "List", params, verifier.timeout) @@ -244,6 +292,60 @@ func (c *SwitchableCruder_MustCurrentContextName_OngoingVerification) GetCapture func (c *SwitchableCruder_MustCurrentContextName_OngoingVerification) GetAllCapturedArguments() { } +func (verifier *VerifierSwitchableCruder) SetFieldSelector(_param0 string) *SwitchableCruder_SetFieldSelector_OngoingVerification { + params := []pegomock.Param{_param0} + methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "SetFieldSelector", params, verifier.timeout) + return &SwitchableCruder_SetFieldSelector_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} +} + +type SwitchableCruder_SetFieldSelector_OngoingVerification struct { + mock *MockSwitchableCruder + methodInvocations []pegomock.MethodInvocation +} + +func (c *SwitchableCruder_SetFieldSelector_OngoingVerification) GetCapturedArguments() string { + _param0 := c.GetAllCapturedArguments() + return _param0[len(_param0)-1] +} + +func (c *SwitchableCruder_SetFieldSelector_OngoingVerification) GetAllCapturedArguments() (_param0 []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) + } + } + return +} + +func (verifier *VerifierSwitchableCruder) SetLabelSelector(_param0 string) *SwitchableCruder_SetLabelSelector_OngoingVerification { + params := []pegomock.Param{_param0} + methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "SetLabelSelector", params, verifier.timeout) + return &SwitchableCruder_SetLabelSelector_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} +} + +type SwitchableCruder_SetLabelSelector_OngoingVerification struct { + mock *MockSwitchableCruder + methodInvocations []pegomock.MethodInvocation +} + +func (c *SwitchableCruder_SetLabelSelector_OngoingVerification) GetCapturedArguments() string { + _param0 := c.GetAllCapturedArguments() + return _param0[len(_param0)-1] +} + +func (c *SwitchableCruder_SetLabelSelector_OngoingVerification) GetAllCapturedArguments() (_param0 []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) + } + } + return +} + func (verifier *VerifierSwitchableCruder) Switch(_param0 string) *SwitchableCruder_Switch_OngoingVerification { params := []pegomock.Param{_param0} methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "Switch", params, verifier.timeout) diff --git a/internal/resource/no.go b/internal/resource/no.go index 954a97ce..c7f00d97 100644 --- a/internal/resource/no.go +++ b/internal/resource/no.go @@ -107,14 +107,11 @@ func (*Node) Header(ns string) Row { return Row{ "NAME", "STATUS", + "ROLE", "VERSION", "KERNEL", "INTERNAL-IP", "EXTERNAL-IP", - "CPU%", - "MEM%", - "RCPU%", - "RMEM%", "CPU", "MEM", "RCPU", @@ -143,40 +140,59 @@ func (r *Node) Fields(ns string) Row { rcpu, rmem := reqs["cpu"], reqs["memory"] - var pcpur float64 - if r.metrics.AvailCPU > 0 { - pcpur = toPerc(float64(rcpu.MilliValue()), float64(r.metrics.AvailCPU)) - } - - var pmemr float64 - if r.metrics.AvailMEM > 0 { - pmemr = toPerc(float64(rmem.Value()/(1024*1024)), float64(r.metrics.AvailMEM)) - } + pcpur := toPerc(float64(rcpu.MilliValue()), float64(r.metrics.AvailCPU)) + pmemr := toPerc(k8s.ToMB(rmem.Value()), float64(r.metrics.AvailMEM)) return append(ff, i.Name, r.status(i), + r.nodeRoles(i), i.Status.NodeInfo.KubeletVersion, i.Status.NodeInfo.KernelVersion, iIP, eIP, - asPerc(toPerc(float64(r.metrics.CurrentCPU), float64(r.metrics.AvailCPU))), - asPerc(toPerc(r.metrics.CurrentMEM, r.metrics.AvailMEM)), - asPerc(pcpur), - asPerc(pmemr), - ToMillicore(r.metrics.CurrentCPU), - ToMi(r.metrics.CurrentMEM), + withPerc(ToMillicore(r.metrics.CurrentCPU), asPerc(toPerc(float64(r.metrics.CurrentCPU), float64(r.metrics.AvailCPU)))), + withPerc(ToMi(r.metrics.CurrentMEM), asPerc(toPerc(r.metrics.CurrentMEM, r.metrics.AvailMEM))), + withPerc(rcpu.String(), asPerc(pcpur)), + withPerc(rmem.String(), asPerc(pmemr)), ToMillicore(r.metrics.AvailCPU), ToMi(r.metrics.AvailMEM), - rcpu.String(), - rmem.String(), toAge(i.ObjectMeta.CreationTimestamp), ) } +func withPerc(v, p string) string { + return v + " (" + p + ")" +} + // ---------------------------------------------------------------------------- // Helpers... +func (*Node) nodeRoles(node *v1.Node) string { + const ( + labelNodeRolePrefix = "node-role.kubernetes.io/" + nodeLabelRole = "kubernetes.io/role" + ) + + roles := sets.NewString() + for k, v := range node.Labels { + switch { + case strings.HasPrefix(k, labelNodeRolePrefix): + if role := strings.TrimPrefix(k, labelNodeRolePrefix); len(role) > 0 { + roles.Insert(role) + } + + case k == nodeLabelRole && v != "": + roles.Insert(v) + } + } + + if len(roles) == 0 { + return MissingValue + } + return strings.Join(roles.List(), ",") +} + func (*Node) getIPs(addrs []v1.NodeAddress) (iIP, eIP string) { for _, a := range addrs { switch a.Type { diff --git a/internal/resource/no_test.go b/internal/resource/no_test.go index f4fabb1d..fe19330b 100644 --- a/internal/resource/no_test.go +++ b/internal/resource/no_test.go @@ -81,7 +81,7 @@ func TestNodeListData(t *testing.T) { assert.Equal(t, resource.NotNamespaced, l.GetNamespace()) row, ok := td.Rows["fred"] assert.True(t, ok) - assert.Equal(t, 17, len(row.Deltas)) + assert.Equal(t, 14, len(row.Deltas)) for _, d := range row.Deltas { assert.Equal(t, "", d) } diff --git a/internal/resource/pod.go b/internal/resource/pod.go index d898b126..218c8b64 100644 --- a/internal/resource/pod.go +++ b/internal/resource/pod.go @@ -17,8 +17,8 @@ const ( ) type ( - // Container represents a resource that encompass multiple containers. - Container interface { + // Containers represents a resource that supports containers. + Containers interface { Containers(path string, includeInit bool) ([]string, error) } diff --git a/internal/resource/pv.go b/internal/resource/pv.go index dba824e6..8defcdf8 100644 --- a/internal/resource/pv.go +++ b/internal/resource/pv.go @@ -9,40 +9,40 @@ import ( v1 "k8s.io/api/core/v1" ) -// PV tracks a kubernetes resource. -type PV struct { +// PersistentVolume tracks a kubernetes resource. +type PersistentVolume struct { *Base instance *v1.PersistentVolume } -// NewPVList returns a new resource list. -func NewPVList(c Connection, ns string) List { +// NewPersistentVolumeList returns a new resource list. +func NewPersistentVolumeList(c Connection, ns string) List { return NewList( NotNamespaced, "pv", - NewPV(c), + NewPersistentVolume(c), CRUDAccess|DescribeAccess, ) } -// NewPV instantiates a new PV. -func NewPV(c Connection) *PV { - p := &PV{&Base{Connection: c, Resource: k8s.NewPV(c)}, nil} +// NewPersistentVolume instantiates a new PersistentVolume. +func NewPersistentVolume(c Connection) *PersistentVolume { + p := &PersistentVolume{&Base{Connection: c, Resource: k8s.NewPersistentVolume(c)}, nil} p.Factory = p return p } -// New builds a new PV instance from a k8s resource. -func (r *PV) New(i interface{}) Columnar { - c := NewPV(r.Connection) +// New builds a new PersistentVolume instance from a k8s resource. +func (r *PersistentVolume) New(i interface{}) Columnar { + c := NewPersistentVolume(r.Connection) switch instance := i.(type) { case *v1.PersistentVolume: c.instance = instance case v1.PersistentVolume: c.instance = &instance default: - log.Fatal().Msgf("unknown PV type %#v", i) + log.Fatal().Msgf("unknown PersistentVolume type %#v", i) } c.path = c.namespacedName(c.instance.ObjectMeta) @@ -50,7 +50,7 @@ func (r *PV) New(i interface{}) Columnar { } // Marshal resource to yaml. -func (r *PV) Marshal(path string) (string, error) { +func (r *PersistentVolume) Marshal(path string) (string, error) { ns, n := namespaced(path) i, err := r.Resource.Get(ns, n) if err != nil { @@ -65,7 +65,7 @@ func (r *PV) Marshal(path string) (string, error) { } // Header return resource header. -func (*PV) Header(ns string) Row { +func (*PersistentVolume) Header(ns string) Row { hh := Row{} if ns == AllNamespaces { hh = append(hh, "NAMESPACE") @@ -75,7 +75,7 @@ func (*PV) Header(ns string) Row { } // Fields retrieves displayable fields. -func (r *PV) Fields(ns string) Row { +func (r *PersistentVolume) Fields(ns string) Row { ff := make(Row, 0, len(r.Header(ns))) i := r.instance if ns == AllNamespaces { @@ -115,7 +115,7 @@ func (r *PV) Fields(ns string) Row { // ---------------------------------------------------------------------------- // Helpers... -func (r *PV) accessMode(aa []v1.PersistentVolumeAccessMode) string { +func (r *PersistentVolume) accessMode(aa []v1.PersistentVolumeAccessMode) string { dd := r.accessDedup(aa) s := make([]string, 0, len(dd)) for i := 0; i < len(aa); i++ { @@ -132,7 +132,7 @@ func (r *PV) accessMode(aa []v1.PersistentVolumeAccessMode) string { return strings.Join(s, ",") } -func (r *PV) accessContains(cc []v1.PersistentVolumeAccessMode, a v1.PersistentVolumeAccessMode) bool { +func (r *PersistentVolume) accessContains(cc []v1.PersistentVolumeAccessMode, a v1.PersistentVolumeAccessMode) bool { for _, c := range cc { if c == a { return true @@ -142,7 +142,7 @@ func (r *PV) accessContains(cc []v1.PersistentVolumeAccessMode, a v1.PersistentV return false } -func (r *PV) accessDedup(cc []v1.PersistentVolumeAccessMode) []v1.PersistentVolumeAccessMode { +func (r *PersistentVolume) accessDedup(cc []v1.PersistentVolumeAccessMode) []v1.PersistentVolumeAccessMode { set := []v1.PersistentVolumeAccessMode{} for _, c := range cc { if !r.accessContains(set, c) { diff --git a/internal/resource/pv_test.go b/internal/resource/pv_test.go index cccf0c0a..7bba6b9a 100644 --- a/internal/resource/pv_test.go +++ b/internal/resource/pv_test.go @@ -11,12 +11,12 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) -func NewPVListWithArgs(ns string, r *resource.PV) resource.List { +func NewPVListWithArgs(ns string, r *resource.PersistentVolume) resource.List { return resource.NewList(resource.NotNamespaced, "pv", r, resource.CRUDAccess|resource.DescribeAccess) } -func NewPVWithArgs(conn k8s.Connection, res resource.Cruder) *resource.PV { - r := &resource.PV{Base: resource.NewBase(conn, res)} +func NewPVWithArgs(conn k8s.Connection, res resource.Cruder) *resource.PersistentVolume { + r := &resource.PersistentVolume{Base: resource.NewBase(conn, res)} r.Factory = r return r } @@ -92,7 +92,7 @@ func k8sPV() *v1.PersistentVolume { func newPV() resource.Columnar { mc := NewMockConnection() - return resource.NewPV(mc).New(k8sPV()) + return resource.NewPersistentVolume(mc).New(k8sPV()) } func pvYaml() string { diff --git a/internal/resource/pvc.go b/internal/resource/pvc.go index 5d3a9265..0dcb4893 100644 --- a/internal/resource/pvc.go +++ b/internal/resource/pvc.go @@ -6,40 +6,40 @@ import ( v1 "k8s.io/api/core/v1" ) -// PVC tracks a kubernetes resource. -type PVC struct { +// PersistentVolumeClaim tracks a kubernetes resource. +type PersistentVolumeClaim struct { *Base instance *v1.PersistentVolumeClaim } -// NewPVCList returns a new resource list. -func NewPVCList(c Connection, ns string) List { +// NewPersistentVolumeClaimList returns a new resource list. +func NewPersistentVolumeClaimList(c Connection, ns string) List { return NewList( ns, "pvc", - NewPVC(c), + NewPersistentVolumeClaim(c), AllVerbsAccess|DescribeAccess, ) } -// NewPVC instantiates a new PVC. -func NewPVC(c Connection) *PVC { - p := &PVC{&Base{Connection: c, Resource: k8s.NewPVC(c)}, nil} +// NewPersistentVolumeClaim instantiates a new PersistentVolumeClaim. +func NewPersistentVolumeClaim(c Connection) *PersistentVolumeClaim { + p := &PersistentVolumeClaim{&Base{Connection: c, Resource: k8s.NewPersistentVolumeClaim(c)}, nil} p.Factory = p return p } -// New builds a new PVC instance from a k8s resource. -func (r *PVC) New(i interface{}) Columnar { - c := NewPVC(r.Connection) +// New builds a new PersistentVolumeClaim instance from a k8s resource. +func (r *PersistentVolumeClaim) New(i interface{}) Columnar { + c := NewPersistentVolumeClaim(r.Connection) switch instance := i.(type) { case *v1.PersistentVolumeClaim: c.instance = instance case v1.PersistentVolumeClaim: c.instance = &instance default: - log.Fatal().Msgf("unknown PVC type %#v", i) + log.Fatal().Msgf("unknown PersistentVolumeClaim type %#v", i) } c.path = c.namespacedName(c.instance.ObjectMeta) @@ -47,7 +47,7 @@ func (r *PVC) New(i interface{}) Columnar { } // Marshal resource to yaml. -func (r *PVC) Marshal(path string) (string, error) { +func (r *PersistentVolumeClaim) Marshal(path string) (string, error) { ns, n := namespaced(path) i, err := r.Resource.Get(ns, n) if err != nil { @@ -62,7 +62,7 @@ func (r *PVC) Marshal(path string) (string, error) { } // Header return resource header. -func (*PVC) Header(ns string) Row { +func (*PersistentVolumeClaim) Header(ns string) Row { hh := Row{} if ns == AllNamespaces { hh = append(hh, "NAMESPACE") @@ -72,7 +72,7 @@ func (*PVC) Header(ns string) Row { } // Fields retrieves displayable fields. -func (r *PVC) Fields(ns string) Row { +func (r *PersistentVolumeClaim) Fields(ns string) Row { ff := make(Row, 0, len(r.Header(ns))) i := r.instance if ns == AllNamespaces { @@ -84,7 +84,7 @@ func (r *PVC) Fields(ns string) Row { phase = "Terminating" } - pv := PV{} + var pv PersistentVolume storage := i.Spec.Resources.Requests[v1.ResourceStorage] var capacity, accessModes string if i.Spec.VolumeName != "" { diff --git a/internal/resource/pvc_test.go b/internal/resource/pvc_test.go index 44c7643e..cefd3210 100644 --- a/internal/resource/pvc_test.go +++ b/internal/resource/pvc_test.go @@ -12,12 +12,12 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) -func NewPVCListWithArgs(ns string, r *resource.PVC) resource.List { +func NewPVCListWithArgs(ns string, r *resource.PersistentVolumeClaim) resource.List { return resource.NewList(ns, "pvc", r, resource.AllVerbsAccess|resource.DescribeAccess) } -func NewPVCWithArgs(conn k8s.Connection, res resource.Cruder) *resource.PVC { - r := &resource.PVC{Base: resource.NewBase(conn, res)} +func NewPVCWithArgs(conn k8s.Connection, res resource.Cruder) *resource.PersistentVolumeClaim { + r := &resource.PersistentVolumeClaim{Base: resource.NewBase(conn, res)} r.Factory = r return r } @@ -100,7 +100,7 @@ func k8sPVC() *v1.PersistentVolumeClaim { func newPVC() resource.Columnar { mc := NewMockConnection() - return resource.NewPVC(mc).New(k8sPVC()) + return resource.NewPersistentVolumeClaim(mc).New(k8sPVC()) } func pvcYaml() string { diff --git a/internal/views/app.go b/internal/views/app.go index c2d2e795..0e5bfe49 100644 --- a/internal/views/app.go +++ b/internal/views/app.go @@ -29,12 +29,15 @@ type ( keyboard(evt *tcell.EventKey) *tcell.EventKey } + actionsFn func(keyActions) + resourceViewer interface { igniter setEnterFn(enterFn) setColorerFn(colorerFn) setDecorateFn(decorateFn) + setExtraActionsFn(actionsFn) } appView struct { @@ -264,7 +267,7 @@ func (a *appView) showPage(p string) { a.pages.SwitchToPage(p) } -func (a *appView) inject(p igniter) { +func (a *appView) inject(i igniter) { if a.cancel != nil { a.cancel() } @@ -273,14 +276,14 @@ func (a *appView) inject(p igniter) { var ctx context.Context { ctx, a.cancel = context.WithCancel(context.Background()) - p.init(ctx, a.config.ActiveNamespace()) + i.init(ctx, a.config.ActiveNamespace()) } - a.content.AddPage("main", p, true, true) + a.content.AddPage("main", i, true, true) - a.focusGroup = append([]tview.Primitive{}, p) + a.focusGroup = append([]tview.Primitive{}, i) a.focusCurrent = 0 - a.fireFocusChanged(p) - a.SetFocus(p) + a.fireFocusChanged(i) + a.SetFocus(i) } func (a *appView) cmdMode() bool { diff --git a/internal/views/command.go b/internal/views/command.go index 4ed065f0..4ee0cb43 100644 --- a/internal/views/command.go +++ b/internal/views/command.go @@ -62,9 +62,6 @@ func (c *command) run(cmd string) bool { case cmd == "alias": c.app.inject(newAliasView(c.app)) return true - case cmd == "popeye": - c.app.inject(newPopeyeView(c.app)) - return true case policyMatcher.MatchString(cmd): tokens := policyMatcher.FindAllStringSubmatch(cmd, -1) if len(tokens) == 1 && len(tokens[0]) == 3 { diff --git a/internal/views/container.go b/internal/views/container.go new file mode 100644 index 00000000..0d91ece2 --- /dev/null +++ b/internal/views/container.go @@ -0,0 +1,125 @@ +package views + +import ( + "github.com/derailed/k9s/internal/resource" + "github.com/gdamore/tcell" + "github.com/rs/zerolog/log" +) + +type containerView struct { + *resourceView + + current igniter + path string +} + +func newContainerView(t string, app *appView, list resource.List, path string) resourceViewer { + v := containerView{resourceView: newResourceView(t, app, list).(*resourceView)} + { + v.path = path + v.extraActionsFn = v.extraActions + v.current = app.content.GetPrimitive("main").(igniter) + } + v.AddPage("logs", newLogsView(list.GetName(), &v), true, false) + v.switchPage("co") + + return &v +} + +// Protocol... + +func (v *containerView) backFn() actionHandler { + return v.backCmd +} + +func (v *containerView) appView() *appView { + return v.app +} + +func (v *containerView) getList() resource.List { + return v.list +} + +func (v *containerView) getSelection() string { + return v.path +} + +// Handlers... + +func (v *containerView) logsCmd(evt *tcell.EventKey) *tcell.EventKey { + if !v.rowSelected() { + return evt + } + v.showLogs(v.selectedItem, v.list.GetName(), v, false) + + return nil +} + +func (v *containerView) prevLogsCmd(evt *tcell.EventKey) *tcell.EventKey { + if !v.rowSelected() { + return evt + } + v.showLogs(v.selectedItem, v.list.GetName(), v, true) + + return nil +} + +func (v *containerView) showLogs(co, view string, parent loggable, prev bool) { + l := v.GetPrimitive("logs").(*logsView) + l.reload(co, parent, view, prev) + v.switchPage("logs") +} + +func (v *containerView) shellCmd(evt *tcell.EventKey) *tcell.EventKey { + if !v.rowSelected() { + return evt + } + log.Debug().Msgf("Selected %s", v.selectedItem) + + v.shellIn(v.path, v.selectedItem) + return nil + + return evt +} + +func (v *containerView) shellIn(path, co string) { + ns, po := namespaced(path) + args := make([]string, 0, 12) + args = append(args, "exec", "-it") + args = append(args, "--context", v.app.config.K9s.CurrentContext) + args = append(args, "-n", ns) + args = append(args, po) + if len(co) != 0 { + args = append(args, "-c", co) + } + args = append(args, "--", "sh") + log.Debug().Msgf("Shell args %v", args) + runK(true, v.app, args...) +} + +func (v *containerView) extraActions(aa keyActions) { + aa[KeyL] = newKeyAction("Logs", v.logsCmd, true) + aa[KeyShiftL] = newKeyAction("Previous Logs", v.prevLogsCmd, true) + aa[KeyS] = newKeyAction("Shell", v.shellCmd, true) + aa[tcell.KeyEscape] = newKeyAction("Back", v.backCmd, false) + aa[KeyP] = newKeyAction("Previous", v.backCmd, false) + aa[tcell.KeyEnter] = newKeyAction("View Logs", v.logsCmd, false) + aa[KeyShiftC] = newKeyAction("Sort CPU", v.sortColCmd(7, false), true) + aa[KeyShiftM] = newKeyAction("Sort MEM", v.sortColCmd(8, false), true) +} + +func (v *containerView) sortColCmd(col int, asc bool) func(evt *tcell.EventKey) *tcell.EventKey { + return func(evt *tcell.EventKey) *tcell.EventKey { + t := v.getTV() + t.sortCol.index, t.sortCol.asc = t.nameColIndex()+col, asc + t.refresh() + + return nil + } +} + +func (v *containerView) backCmd(evt *tcell.EventKey) *tcell.EventKey { + v.app.inject(v.current) + + return nil +} diff --git a/internal/views/dp.go b/internal/views/dp.go new file mode 100644 index 00000000..e5f93af9 --- /dev/null +++ b/internal/views/dp.go @@ -0,0 +1,72 @@ +package views + +import ( + "github.com/derailed/k9s/internal/k8s" + "github.com/derailed/k9s/internal/resource" + "github.com/gdamore/tcell" + "github.com/rs/zerolog/log" + v1 "k8s.io/api/apps/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +type deployView struct { + *resourceView +} + +func newDeployView(t string, app *appView, list resource.List) resourceViewer { + v := deployView{newResourceView(t, app, list).(*resourceView)} + { + v.extraActionsFn = v.extraActions + v.switchPage("deploy") + } + + return &v +} + +func (v *deployView) extraActions(aa keyActions) { + aa[KeyShiftD] = newKeyAction("Sort Desired", v.sortColCmd(2, false), true) + aa[KeyShiftC] = newKeyAction("Sort Current", v.sortColCmd(3, false), true) + aa[tcell.KeyEnter] = newKeyAction("View Pods", v.showPodsCmd, true) +} + +func (v *deployView) sortColCmd(col int, asc bool) func(evt *tcell.EventKey) *tcell.EventKey { + return func(evt *tcell.EventKey) *tcell.EventKey { + t := v.getTV() + t.sortCol.index, t.sortCol.asc = t.nameColIndex()+col, asc + t.refresh() + + return nil + } +} + +func (v *deployView) showPodsCmd(evt *tcell.EventKey) *tcell.EventKey { + if !v.rowSelected() { + return evt + } + + ns, n := namespaced(v.selectedItem) + d := k8s.NewDeployment(v.app.conn()) + dep, err := d.Get(ns, n) + if err != nil { + log.Error().Err(err).Msgf("Fetching Deployment %s", v.selectedItem) + v.app.flash(flashErr, err.Error()) + return evt + } + dp := dep.(*v1.Deployment) + + sel, err := metav1.LabelSelectorAsSelector(dp.Spec.Selector) + if err != nil { + log.Error().Err(err).Msgf("Converting selector for Deployment %s", v.selectedItem) + v.app.flash(flashErr, err.Error()) + return evt + } + showPods(v.app, "", "Deployment", v.selectedItem, sel.String(), "", v.backCmd) + + return nil +} + +func (v *deployView) backCmd(evt *tcell.EventKey) *tcell.EventKey { + v.app.inject(v) + + return nil +} diff --git a/internal/views/ds.go b/internal/views/ds.go new file mode 100644 index 00000000..fcd10924 --- /dev/null +++ b/internal/views/ds.go @@ -0,0 +1,72 @@ +package views + +import ( + "github.com/derailed/k9s/internal/k8s" + "github.com/derailed/k9s/internal/resource" + "github.com/gdamore/tcell" + "github.com/rs/zerolog/log" + extv1beta1 "k8s.io/api/extensions/v1beta1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +type daemonSetView struct { + *resourceView +} + +func newDaemonSetView(t string, app *appView, list resource.List) resourceViewer { + v := daemonSetView{newResourceView(t, app, list).(*resourceView)} + { + v.extraActionsFn = v.extraActions + v.switchPage("ds") + } + + return &v +} + +func (v *daemonSetView) extraActions(aa keyActions) { + aa[KeyShiftD] = newKeyAction("Sort Desired", v.sortColCmd(2, false), true) + aa[KeyShiftC] = newKeyAction("Sort Current", v.sortColCmd(3, false), true) + aa[tcell.KeyEnter] = newKeyAction("View Pods", v.showPodsCmd, true) +} + +func (v *daemonSetView) sortColCmd(col int, asc bool) func(evt *tcell.EventKey) *tcell.EventKey { + return func(evt *tcell.EventKey) *tcell.EventKey { + t := v.getTV() + t.sortCol.index, t.sortCol.asc = t.nameColIndex()+col, asc + t.refresh() + + return nil + } +} + +func (v *daemonSetView) showPodsCmd(evt *tcell.EventKey) *tcell.EventKey { + if !v.rowSelected() { + return evt + } + + ns, n := namespaced(v.selectedItem) + d := k8s.NewDaemonSet(v.app.conn()) + dset, err := d.Get(ns, n) + if err != nil { + log.Error().Err(err).Msgf("Fetching DeaemonSet %s", v.selectedItem) + v.app.flash(flashErr, err.Error()) + return evt + } + ds := dset.(*extv1beta1.DaemonSet) + + sel, err := metav1.LabelSelectorAsSelector(ds.Spec.Selector) + if err != nil { + log.Error().Err(err).Msgf("Converting selector for DaemonSet %s", v.selectedItem) + v.app.flash(flashErr, err.Error()) + return evt + } + showPods(v.app, "", "DaemonSet", v.selectedItem, sel.String(), "", v.backCmd) + + return nil +} + +func (v *daemonSetView) backCmd(evt *tcell.EventKey) *tcell.EventKey { + v.app.inject(v) + + return nil +} diff --git a/internal/views/exec.go b/internal/views/exec.go index c1e9e8cd..8201177c 100644 --- a/internal/views/exec.go +++ b/internal/views/exec.go @@ -13,7 +13,7 @@ import ( "github.com/rs/zerolog/log" ) -func runK(app *appView, args ...string) bool { +func runK(clear bool, app *appView, args ...string) bool { bin, err := exec.LookPath("kubectl") if err != nil { log.Error().Msgf("Unable to find kubeclt command in path %v", err) @@ -24,21 +24,23 @@ func runK(app *appView, args ...string) bool { last := len(args) - 1 if args[last] == "sh" { args[last] = "bash" - if err := execute(bin, args...); err != nil { + if err := execute(clear, bin, args...); err != nil { args[last] = "sh" } else { return } } - if err := execute(bin, args...); err != nil { + if err := execute(clear, bin, args...); err != nil { log.Error().Msgf("Command exited: %T %v %v", err, err, args) app.flash(flashErr, "Command exited:", err.Error()) } }) } -func execute(bin string, args ...string) error { - clearScreen() +func execute(clear bool, bin string, args ...string) error { + if clear { + clearScreen() + } log.Debug().Msgf("Running command > %s %s", bin, strings.Join(args, " ")) ctx, cancel := context.WithCancel(context.Background()) defer cancel() @@ -64,5 +66,6 @@ func execute(bin string, args ...string) error { } func clearScreen() { + log.Debug().Msg("Clearing screen...") fmt.Print("\033[H\033[2J") } diff --git a/internal/views/help.go b/internal/views/help.go index de9b48fe..6127f235 100644 --- a/internal/views/help.go +++ b/internal/views/help.go @@ -70,6 +70,7 @@ func (v *helpView) init(_ context.Context, _ string) { {"tab", "Next term match"}, {"backtab", "Previous term match"}, {"Ctrl-r", "Refresh"}, + {"Shift-i", "Invert Sort"}, {"p", "Previous resource view"}, {"q", "Quit"}, } @@ -105,7 +106,7 @@ func (v *helpView) init(_ context.Context, _ string) { } func (v *helpView) printHelp(key, desc string) { - fmt.Fprintf(v, "[pink::b]%9s [white::]%s\n", key, desc) + fmt.Fprintf(v, "[dodgerblue::b]%9s [white::]%s\n", key, desc) } func (v *helpView) hints() hints { diff --git a/internal/views/job.go b/internal/views/job.go index be2047ca..647e8bde 100644 --- a/internal/views/job.go +++ b/internal/views/job.go @@ -1,9 +1,12 @@ package views import ( + "github.com/derailed/k9s/internal/k8s" "github.com/derailed/k9s/internal/resource" "github.com/gdamore/tcell" "github.com/rs/zerolog/log" + batchv1 "k8s.io/api/batch/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) type jobView struct { @@ -11,7 +14,7 @@ type jobView struct { } func newJobView(t string, app *appView, list resource.List) resourceViewer { - v := jobView{newResourceView(t, app, list).(*resourceView)} + v := jobView{resourceView: newResourceView(t, app, list).(*resourceView)} { v.extraActionsFn = v.extraActions v.AddPage("logs", newLogsView(list.GetName(), &v), true, false) @@ -31,6 +34,9 @@ func newJobView(t string, app *appView, list resource.List) resourceViewer { // Protocol... +func (v *jobView) setExtraActionsFn(f actionsFn) { +} + func (v *jobView) appView() *appView { return v.app } @@ -50,42 +56,86 @@ func (v *jobView) getSelection() string { // Handlers... func (v *jobView) logsCmd(evt *tcell.EventKey) *tcell.EventKey { + if v.viewLogs(false) { + return nil + } + return evt +} + +func (v *jobView) prevLogsCmd(evt *tcell.EventKey) *tcell.EventKey { + if v.viewLogs(true) { + return nil + } + return evt +} + +func (v *jobView) viewLogs(previous bool) bool { if !v.rowSelected() { - return evt + return false } cc, err := fetchContainers(v.list, v.selectedItem, true) if err != nil { v.app.flash(flashErr, err.Error()) log.Error().Err(err).Msgf("Unable to fetch containers for %s", v.selectedItem) - return evt + return false } if len(cc) == 1 { - v.showLogs(v.selectedItem, cc[0], v.list.GetName(), v) - return nil + v.showLogs(v.selectedItem, cc[0], v.list.GetName(), v, previous) + return true } picker := v.GetPrimitive("picker").(*selectList) picker.populate(cc) picker.SetSelectedFunc(func(i int, t, d string, r rune) { - v.showLogs(v.selectedItem, t, "picker", picker) + v.showLogs(v.selectedItem, t, "picker", picker, previous) }) v.switchPage("picker") - return nil + return true } -func (v *jobView) showLogs(path, co, view string, parent loggable) { +func (v *jobView) showLogs(path, co, view string, parent loggable, prev bool) { l := v.GetPrimitive("logs").(*logsView) - l.parent = parent - l.parentView = view - l.deleteAllPages() - l.addContainer(co) + l.reload(co, parent, view, prev) v.switchPage("logs") - l.init() } func (v *jobView) extraActions(aa keyActions) { aa[KeyL] = newKeyAction("Logs", v.logsCmd, true) + aa[KeyShiftL] = newKeyAction("Previous Logs", v.prevLogsCmd, true) + aa[tcell.KeyEnter] = newKeyAction("View Pods", v.showPodsCmd, true) +} + +func (v *jobView) showPodsCmd(evt *tcell.EventKey) *tcell.EventKey { + if !v.rowSelected() { + return evt + } + + ns, n := namespaced(v.selectedItem) + j := k8s.NewJob(v.app.conn()) + job, err := j.Get(ns, n) + if err != nil { + log.Error().Err(err).Msgf("Fetching Job %s", v.selectedItem) + v.app.flash(flashErr, err.Error()) + return evt + } + jo := job.(*batchv1.Job) + + sel, err := metav1.LabelSelectorAsSelector(jo.Spec.Selector) + if err != nil { + log.Error().Err(err).Msgf("Converting selector for Job %s", v.selectedItem) + v.app.flash(flashErr, err.Error()) + return evt + } + showPods(v.app, "", "Job", v.selectedItem, sel.String(), "", v.backCmd) + + return nil +} + +func (v *jobView) backCmd(evt *tcell.EventKey) *tcell.EventKey { + v.app.inject(v) + + return nil } diff --git a/internal/views/logs.go b/internal/views/logs.go index 17d2dc75..004d1f17 100644 --- a/internal/views/logs.go +++ b/internal/views/logs.go @@ -21,12 +21,13 @@ const ( type logsView struct { *tview.Pages - parentView string - parent loggable - containers []string - actions keyActions - cancelFunc context.CancelFunc - autoScroll bool + parentView string + parent loggable + containers []string + actions keyActions + cancelFunc context.CancelFunc + autoScroll bool + showPrevious bool } func newLogsView(pview string, parent loggable) *logsView { @@ -53,7 +54,10 @@ func newLogsView(pview string, parent loggable) *logsView { // Protocol... -func (v *logsView) init() { +func (v *logsView) reload(co string, parent loggable, view string, prevLogs bool) { + v.parent, v.parentView, v.showPrevious = parent, view, prevLogs + v.deleteAllPages() + v.addContainer(co) v.load(0) } @@ -171,7 +175,7 @@ func (v *logsView) doLoad(path, co string) error { return fmt.Errorf("Resource %T is not tailable", v.parent.getList().Resource) } maxBuff := int64(v.parent.appView().config.K9s.LogRequestSize) - cancelFn, err := res.Logs(c, ns, po, co, maxBuff, false) + cancelFn, err := res.Logs(c, ns, po, co, maxBuff, v.showPrevious) if err != nil { cancelFn() return err diff --git a/internal/views/no.go b/internal/views/no.go index 4650a731..0f1d7294 100644 --- a/internal/views/no.go +++ b/internal/views/no.go @@ -1,6 +1,9 @@ package views import ( + "fmt" + + "github.com/derailed/k9s/internal/k8s" "github.com/derailed/k9s/internal/resource" "github.com/gdamore/tcell" ) @@ -20,12 +23,9 @@ func newNodeView(t string, app *appView, list resource.List) resourceViewer { } func (v *nodeView) extraActions(aa keyActions) { - aa[KeyShiftQ] = newKeyAction("Sort CPU%", v.sortColCmd(6, false), true) - aa[KeyShiftW] = newKeyAction("Sort MEM%", v.sortColCmd(7, false), true) - aa[KeyShiftA] = newKeyAction("Sort RCPU%", v.sortColCmd(8, false), true) - aa[KeyShiftS] = newKeyAction("Sort RMEM%", v.sortColCmd(9, false), true) - aa[KeyShiftC] = newKeyAction("Sort CPU", v.sortColCmd(10, false), true) - aa[KeyShiftM] = newKeyAction("Sort MEM", v.sortColCmd(11, false), true) + aa[KeyShiftC] = newKeyAction("Sort CPU", v.sortColCmd(7, false), true) + aa[KeyShiftM] = newKeyAction("Sort MEM", v.sortColCmd(8, false), true) + aa[tcell.KeyEnter] = newKeyAction("View Pods", v.showPodsCmd, true) } func (v *nodeView) sortColCmd(col int, asc bool) func(evt *tcell.EventKey) *tcell.EventKey { @@ -37,3 +37,34 @@ func (v *nodeView) sortColCmd(col int, asc bool) func(evt *tcell.EventKey) *tcel return nil } } + +func (v *nodeView) showPodsCmd(evt *tcell.EventKey) *tcell.EventKey { + if !v.rowSelected() { + return evt + } + + showPods(v.app, "", "Node", v.selectedItem, "", "spec.nodeName="+v.selectedItem, v.backCmd) + + return nil +} + +func (v *nodeView) backCmd(evt *tcell.EventKey) *tcell.EventKey { + v.app.inject(v) + + return nil +} + +func showPods(app *appView, ns, res, selected, labelSel, fieldSel string, b actionHandler) { + mx := k8s.NewMetricsServer(app.conn()) + list := resource.NewPodList(app.conn(), mx, ns) + + list.SetLabelSelector(labelSel) + list.SetFieldSelector(fieldSel) + + title := fmt.Sprintf("%s:%s Pods", res, selected) + pv := newPodView(title, app, list) + pv.setExtraActionsFn(func(aa keyActions) { + aa[tcell.KeyEsc] = newKeyAction("Back", b, true) + }) + app.inject(pv) +} diff --git a/internal/views/pod.go b/internal/views/pod.go index e21a3bde..9c46015a 100644 --- a/internal/views/pod.go +++ b/internal/views/pod.go @@ -1,11 +1,17 @@ package views import ( + "fmt" + + "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" ) +const fmat = "[aqua::b]%s([fuchsia::b]%s[aqua::-])" + type podView struct { *resourceView } @@ -22,6 +28,7 @@ func newPodView(t string, app *appView, list resource.List) resourceViewer { v := podView{newResourceView(t, app, list).(*resourceView)} { v.extraActionsFn = v.extraActions + v.enterFn = v.listContainers } picker := newSelectList(&v) @@ -38,6 +45,20 @@ func newPodView(t string, app *appView, list resource.List) resourceViewer { return &v } +func (v *podView) listContainers(app *appView, _, res, sel string) { + ns, n := namespaced(sel) + po, err := app.conn().DialOrDie().CoreV1().Pods(ns).Get(n, metav1.GetOptions{}) + if err != nil { + log.Error().Err(err).Msgf("Unable to retrieve pod %s", sel) + app.flash(flashErr, err.Error()) + return + } + + mx := k8s.NewMetricsServer(app.conn()) + list := resource.NewContainerList(app.conn(), mx, po) + app.inject(newContainerView(fmt.Sprintf(fmat, "Containers", sel), app, list, namespacedName(po.Namespace, po.Name))) +} + // Protocol... func (v *podView) backFn() actionHandler { @@ -59,40 +80,51 @@ func (v *podView) getSelection() string { // Handlers... func (v *podView) logsCmd(evt *tcell.EventKey) *tcell.EventKey { + if v.viewLogs(false) { + return nil + } + return evt +} + +func (v *podView) prevLogsCmd(evt *tcell.EventKey) *tcell.EventKey { + if v.viewLogs(true) { + return nil + } + return evt +} + +func (v *podView) viewLogs(prev bool) bool { if !v.rowSelected() { - return evt + return false } cc, err := fetchContainers(v.list, v.selectedItem, true) if err != nil { v.app.flash(flashErr, err.Error()) log.Error().Err(err) - return evt + return false } if len(cc) == 1 { - v.showLogs(v.selectedItem, cc[0], v.list.GetName(), v) - return nil + v.showLogs(v.selectedItem, cc[0], v.list.GetName(), v, prev) + return true } picker := v.GetPrimitive("picker").(*selectList) picker.populate(cc) picker.SetSelectedFunc(func(i int, t, d string, r rune) { - v.showLogs(v.selectedItem, t, "picker", picker) + v.showLogs(v.selectedItem, t, "picker", picker, prev) }) v.switchPage("picker") - return nil + return true } -func (v *podView) showLogs(path, co, view string, parent loggable) { +func (v *podView) showLogs(path, co, view string, parent loggable, prev bool) { l := v.GetPrimitive("logs").(*logsView) - l.parent = parent - l.parentView = view - l.deleteAllPages() - l.addContainer(co) + l.reload(co, parent, view, prev) + v.switchPage("logs") - l.init() } func (v *podView) shellCmd(evt *tcell.EventKey) *tcell.EventKey { @@ -134,11 +166,12 @@ func (v *podView) shellIn(path, co string) { } args = append(args, "--", "sh") log.Debug().Msgf("Shell args %v", args) - runK(v.app, args...) + runK(true, v.app, args...) } func (v *podView) extraActions(aa keyActions) { aa[KeyL] = newKeyAction("Logs", v.logsCmd, true) + aa[KeyShiftL] = newKeyAction("Logs", v.prevLogsCmd, true) aa[KeyS] = newKeyAction("Shell", v.shellCmd, true) aa[KeyShiftR] = newKeyAction("Sort Ready", v.sortColCmd(1, false), true) aa[KeyShiftS] = newKeyAction("Sort Status", v.sortColCmd(2, true), true) @@ -146,7 +179,6 @@ func (v *podView) extraActions(aa keyActions) { aa[KeyShiftC] = newKeyAction("Sort CPU", v.sortColCmd(4, false), true) aa[KeyShiftM] = newKeyAction("Sort MEM", v.sortColCmd(5, false), true) aa[KeyShiftO] = newKeyAction("Sort Node", v.sortColCmd(7, true), true) - aa[KeyShiftQ] = newKeyAction("Sort QOS", v.sortColCmd(8, true), true) } func (v *podView) sortColCmd(col int, asc bool) func(evt *tcell.EventKey) *tcell.EventKey { @@ -163,5 +195,5 @@ func fetchContainers(l resource.List, po string, includeInit bool) ([]string, er if len(po) == 0 { return []string{}, nil } - return l.Resource().(resource.Container).Containers(po, includeInit) + return l.Resource().(resource.Containers).Containers(po, includeInit) } diff --git a/internal/views/popeye.go b/internal/views/popeye.go index 6cbb4225..86c02f4d 100644 --- a/internal/views/popeye.go +++ b/internal/views/popeye.go @@ -1,77 +1,77 @@ package views -import ( - "context" - "fmt" - "io" - "os" - "path/filepath" +// import ( +// "context" +// "fmt" +// "io" +// "os" +// "path/filepath" - "github.com/derailed/k9s/internal/config" - "github.com/derailed/popeye/pkg" - cfg "github.com/derailed/popeye/pkg/config" - "github.com/derailed/tview" - "github.com/gdamore/tcell" - "github.com/rs/zerolog/log" -) +// "github.com/derailed/k9s/internal/config" +// "github.com/derailed/popeye/pkg" +// cfg "github.com/derailed/popeye/pkg/config" +// "github.com/derailed/tview" +// "github.com/gdamore/tcell" +// "github.com/rs/zerolog/log" +// ) -type popeyeView struct { - *detailsView +// type popeyeView struct { +// *detailsView - current igniter - ansiWriter io.Writer -} +// current igniter +// ansiWriter io.Writer +// } -func newPopeyeView(app *appView) *popeyeView { - v := popeyeView{} - { - v.detailsView = newDetailsView(app, v.backCmd) - v.SetBorderPadding(0, 0, 1, 1) - v.current = app.content.GetPrimitive("main").(igniter) - v.SetDynamicColors(true) - v.SetWrap(true) - v.setTitle("Popeye") - v.ansiWriter = tview.ANSIWriter(v) - } - v.actions[KeyP] = newKeyAction("Previous", v.app.prevCmd, false) +// func newPopeyeView(app *appView) *popeyeView { +// v := popeyeView{} +// { +// v.detailsView = newDetailsView(app, v.backCmd) +// v.SetBorderPadding(0, 0, 1, 1) +// v.current = app.content.GetPrimitive("main").(igniter) +// v.SetDynamicColors(true) +// v.SetWrap(true) +// v.setTitle("Popeye") +// v.ansiWriter = tview.ANSIWriter(v) +// } +// v.actions[KeyP] = newKeyAction("Previous", v.app.prevCmd, false) - return &v -} +// return &v +// } -func (v *popeyeView) init(ctx context.Context, ns string) { - defer func() { - if err := recover(); err != nil { - v.app.flash(flashErr, fmt.Sprintf("%v", err)) - } - }() +// func (v *popeyeView) init(ctx context.Context, ns string) { +// defer func() { +// if err := recover(); err != nil { +// v.app.flash(flashErr, fmt.Sprintf("%v", err)) +// } +// }() - c := cfg.New() +// c := cfg.New() - spinach := filepath.Join(config.K9sHome, "spinach.yml") +// spinach := filepath.Join(config.K9sHome, "spinach.yml") - if _, err := os.Stat(spinach); err == nil { - c.Spinach = spinach - } +// if _, err := os.Stat(spinach); err == nil { +// c.Spinach = spinach +// } - if v.app.config.K9s.CurrentContext != "" { - v.app.flags.Context = &v.app.config.K9s.CurrentContext - } +// if v.app.config.K9s.CurrentContext != "" { +// v.app.flags.Context = &v.app.config.K9s.CurrentContext +// } - if err := c.Init(v.app.flags); err != nil { - log.Error().Err(err).Msg("Unable to load spinach config") - } +// if err := c.Init(v.app.flags); err != nil { +// log.Error().Err(err).Msg("Unable to load spinach config") +// } - p := pkg.NewPopeye(c, &log.Logger, v.ansiWriter) - p.Sanitize(false) -} +// p := pkg.NewPopeye(c, &log.Logger, v.ansiWriter) +// p.Sanitize(false) +// } -func (v *popeyeView) getTitle() string { - return "Popeye" -} +// func (v *popeyeView) getTitle() string { +// return "Popeye" +// } -func (v *popeyeView) backCmd(evt *tcell.EventKey) *tcell.EventKey { - v.app.command.previousCmd() - v.app.inject(v.current) +// func (v *popeyeView) backCmd(evt *tcell.EventKey) *tcell.EventKey { +// v.app.command.previousCmd() +// v.app.inject(v.current) - return nil -} +// return nil +// } diff --git a/internal/views/registrar.go b/internal/views/registrar.go index 3bfd075e..570def4c 100644 --- a/internal/views/registrar.go +++ b/internal/views/registrar.go @@ -44,8 +44,7 @@ func helpCmds(c k8s.Connection) map[string]resCmd { func allCRDs(c k8s.Connection) map[string]k8s.APIGroup { m := map[string]k8s.APIGroup{} - crds, _ := resource. - NewCRDList(c, resource.AllNamespaces). + crds, _ := resource.NewCustomResourceDefinitionList(c, resource.AllNamespaces). Resource(). List(resource.AllNamespaces) @@ -136,7 +135,7 @@ func resourceViews(c k8s.Connection) map[string]resCmd { title: "CustomResourceDefinitions", api: "apiextensions.k8s.io", viewFn: newResourceView, - listFn: resource.NewCRDList, + listFn: resource.NewCustomResourceDefinitionList, }, "cj": { title: "CronJobs", @@ -154,14 +153,14 @@ func resourceViews(c k8s.Connection) map[string]resCmd { "ds": { title: "DaemonSets", api: "", - viewFn: newResourceView, + viewFn: newDaemonSetView, listFn: resource.NewDaemonSetList, colorerFn: dpColorer, }, "dp": { title: "Deployments", api: "apps", - viewFn: newResourceView, + viewFn: newDeployView, listFn: resource.NewDeploymentList, colorerFn: dpColorer, }, @@ -222,14 +221,14 @@ func resourceViews(c k8s.Connection) map[string]resCmd { title: "PersistentVolumes", api: "", viewFn: newResourceView, - listFn: resource.NewPVList, + listFn: resource.NewPersistentVolumeList, colorerFn: pvColorer, }, "pvc": { title: "PersistentVolumeClaims", api: "", viewFn: newResourceView, - listFn: resource.NewPVCList, + listFn: resource.NewPersistentVolumeClaimList, colorerFn: pvcColorer, }, "rb": { @@ -256,7 +255,7 @@ func resourceViews(c k8s.Connection) map[string]resCmd { "rs": { title: "ReplicaSets", api: "apps", - viewFn: newResourceView, + viewFn: newReplicaSetView, listFn: resource.NewReplicaSetList, colorerFn: rsColorer, }, @@ -276,7 +275,7 @@ func resourceViews(c k8s.Connection) map[string]resCmd { "sts": { title: "StatefulSets", api: "apps", - viewFn: newResourceView, + viewFn: newStatefulSetView, listFn: resource.NewStatefulSetList, colorerFn: stsColorer, }, @@ -310,7 +309,7 @@ func resourceViews(c k8s.Connection) map[string]resCmd { title: "HorizontalPodAutoscalers", api: "autoscaling", viewFn: newResourceView, - listFn: resource.NewHPAV1List, + listFn: resource.NewHorizontalPodAutoscalerV1List, } case "v2beta1": log.Debug().Msg("Using HPA V2Beta1!") @@ -318,7 +317,7 @@ func resourceViews(c k8s.Connection) map[string]resCmd { title: "HorizontalPodAutoscalers", api: "autoscaling", viewFn: newResourceView, - listFn: resource.NewHPAV2Beta1List, + listFn: resource.NewHorizontalPodAutoscalerV2Beta1List, } case "v2beta2": log.Debug().Msg("Using HPA V2Beta2!") @@ -326,7 +325,7 @@ func resourceViews(c k8s.Connection) map[string]resCmd { title: "HorizontalPodAutoscalers", api: "autoscaling", viewFn: newResourceView, - listFn: resource.NewHPAList, + listFn: resource.NewHorizontalPodAutoscalerList, } default: log.Panic().Msgf("K9s does not currently support HPA version `%s`", rev) diff --git a/internal/views/resource.go b/internal/views/resource.go index 35135e9d..c6fef247 100644 --- a/internal/views/resource.go +++ b/internal/views/resource.go @@ -47,6 +47,7 @@ type ( selectedFn func() string decorateFn decorateFn colorerFn colorerFn + actions keyActions } ) @@ -54,6 +55,7 @@ func newResourceView(title string, app *appView, list resource.List) resourceVie v := resourceView{ app: app, title: title, + actions: make(keyActions), list: list, selectedNS: list.GetNamespace(), Pages: tview.NewPages(), @@ -103,6 +105,10 @@ func (v *resourceView) init(ctx context.Context, ns string) { } } +func (v *resourceView) setExtraActionsFn(f actionsFn) { + f(v.actions) +} + func (v *resourceView) getTitle() string { return v.title } @@ -251,7 +257,7 @@ func (v *resourceView) editCmd(evt *tcell.EventKey) *tcell.EventKey { args = append(args, "-n", ns) args = append(args, "--context", v.app.config.K9s.CurrentContext) args = append(args, po) - runK(v.app, args...) + runK(true, v.app, args...) return evt } @@ -369,8 +375,7 @@ func (v *resourceView) refreshActions() { } var nn []interface{} - aa := make(keyActions) - if k8s.CanIAccess(v.app.conn().Config(), log.Logger, "", "list", "namespaces", "namespace.v1") { + if !v.list.HasSelectors() && k8s.CanIAccess(v.app.conn().Config(), log.Logger, "", "list", "namespaces", "namespace.v1") { var err error nn, err = k8s.NewNamespace(v.app.conn()).List(resource.AllNamespaces) if err != nil { @@ -387,35 +392,35 @@ func (v *resourceView) refreshActions() { if v.list.Access(resource.NamespaceAccess) { v.namespaces = make(map[int]string, config.MaxFavoritesNS) for i, n := range v.app.config.FavNamespaces() { - aa[tcell.Key(numKeys[i])] = newKeyAction(n, v.switchNamespaceCmd, true) + v.actions[tcell.Key(numKeys[i])] = newKeyAction(n, v.switchNamespaceCmd, true) v.namespaces[i] = n } } } - aa[tcell.KeyEnter] = newKeyAction("Enter", v.enterCmd, false) + v.actions[tcell.KeyEnter] = newKeyAction("Enter", v.enterCmd, false) - aa[tcell.KeyCtrlR] = newKeyAction("Refresh", v.refreshCmd, false) - aa[KeyHelp] = newKeyAction("Help", v.app.noopCmd, false) - aa[KeyP] = newKeyAction("Previous", v.app.prevCmd, false) + v.actions[tcell.KeyCtrlR] = newKeyAction("Refresh", v.refreshCmd, false) + v.actions[KeyHelp] = newKeyAction("Help", v.app.noopCmd, false) + v.actions[KeyP] = newKeyAction("Previous", v.app.prevCmd, false) if v.list.Access(resource.EditAccess) { - aa[KeyE] = newKeyAction("Edit", v.editCmd, true) + v.actions[KeyE] = newKeyAction("Edit", v.editCmd, true) } if v.list.Access(resource.DeleteAccess) { - aa[tcell.KeyCtrlD] = newKeyAction("Delete", v.deleteCmd, true) + v.actions[tcell.KeyCtrlD] = newKeyAction("Delete", v.deleteCmd, true) } if v.list.Access(resource.ViewAccess) { - aa[KeyY] = newKeyAction("YAML", v.viewCmd, true) + v.actions[KeyY] = newKeyAction("YAML", v.viewCmd, true) } if v.list.Access(resource.DescribeAccess) { - aa[KeyD] = newKeyAction("Describe", v.describeCmd, true) + v.actions[KeyD] = newKeyAction("Describe", v.describeCmd, true) } if v.extraActionsFn != nil { - v.extraActionsFn(aa) + v.extraActionsFn(v.actions) } t := v.getTV() - t.setActions(aa) + t.setActions(v.actions) v.app.setHints(t.hints()) } diff --git a/internal/views/rs.go b/internal/views/rs.go new file mode 100644 index 00000000..a8b69cab --- /dev/null +++ b/internal/views/rs.go @@ -0,0 +1,167 @@ +package views + +import ( + "fmt" + "strconv" + "strings" + + "github.com/derailed/k9s/internal/k8s" + "github.com/derailed/k9s/internal/resource" + "github.com/derailed/tview" + "github.com/gdamore/tcell" + "github.com/rs/zerolog/log" + appsv1 "k8s.io/api/apps/v1" + v1 "k8s.io/api/apps/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/kubernetes/pkg/kubectl" +) + +type replicaSetView struct { + *resourceView +} + +func newReplicaSetView(t string, app *appView, list resource.List) resourceViewer { + v := replicaSetView{newResourceView(t, app, list).(*resourceView)} + { + v.extraActionsFn = v.extraActions + v.switchPage("rs") + } + + return &v +} + +func (v *replicaSetView) extraActions(aa keyActions) { + aa[KeyShiftD] = newKeyAction("Sort Desired", v.sortColCmd(2, false), true) + aa[KeyShiftC] = newKeyAction("Sort Current", v.sortColCmd(3, false), true) + aa[tcell.KeyCtrlB] = newKeyAction("Rollback", v.rollbackCmd, true) + aa[tcell.KeyEnter] = newKeyAction("View Pods", v.showPodsCmd, true) +} + +func (v *replicaSetView) sortColCmd(col int, asc bool) func(evt *tcell.EventKey) *tcell.EventKey { + return func(evt *tcell.EventKey) *tcell.EventKey { + t := v.getTV() + t.sortCol.index, t.sortCol.asc = t.nameColIndex()+col, asc + t.refresh() + + return nil + } +} + +func (v *replicaSetView) showPodsCmd(evt *tcell.EventKey) *tcell.EventKey { + if !v.rowSelected() { + return evt + } + + ns, n := namespaced(v.selectedItem) + rset := k8s.NewReplicaSet(v.app.conn()) + r, err := rset.Get(ns, n) + if err != nil { + log.Error().Err(err).Msgf("Fetching ReplicaSet %s", v.selectedItem) + v.app.flash(flashErr, err.Error()) + return evt + } + rs := r.(*v1.ReplicaSet) + + sel, err := metav1.LabelSelectorAsSelector(rs.Spec.Selector) + if err != nil { + log.Error().Err(err).Msgf("Converting selector for ReplicaSet %s", v.selectedItem) + v.app.flash(flashErr, err.Error()) + return evt + } + showPods(v.app, "", "ReplicaSet", v.selectedItem, sel.String(), "", v.backCmd) + + return nil +} + +func (v *replicaSetView) backCmd(evt *tcell.EventKey) *tcell.EventKey { + v.app.inject(v) + + return nil +} + +func (v *replicaSetView) rollbackCmd(evt *tcell.EventKey) *tcell.EventKey { + if !v.rowSelected() { + return evt + } + + confirm := v.GetPrimitive("confirm").(*tview.Modal) + confirm.SetText(fmt.Sprintf("Rollback %s %s?", v.list.GetName(), v.selectedItem)) + confirm.SetDoneFunc(func(_ int, button string) { + if button == "OK" { + v.app.flash(flashInfo, fmt.Sprintf("Rolling back %s %s", v.list.GetName(), v.selectedItem)) + rollback(v.app, v.selectedItem) + v.refresh() + } + v.switchPage(v.list.GetName()) + }) + v.SwitchToPage("confirm") + + return nil +} + +func rollback(app *appView, selectedItem string) bool { + ns, n := namespaced(selectedItem) + rset := k8s.NewReplicaSet(app.conn()) + r, err := rset.Get(ns, n) + if err != nil { + log.Error().Err(err).Msgf("Fetching ReplicaSet %s", selectedItem) + app.flash(flashErr, err.Error()) + return false + } + rs := r.(*v1.ReplicaSet) + + var ctrlName, ctrlKind, ctrlAPI string + for _, ref := range rs.ObjectMeta.OwnerReferences { + if ref.Controller != nil && *ref.Controller { + ctrlAPI, ctrlKind, ctrlName = ref.APIVersion, ref.Kind, ref.Name + break + } + } + if ctrlName == "" || ctrlKind == "" || ctrlAPI == "" { + app.flash(flashErr, "Unable to find controller for ReplicaSet %s", selectedItem) + return false + } + + revision := rs.ObjectMeta.Annotations["deployment.kubernetes.io/revision"] + if rs.Status.Replicas != 0 { + app.flash(flashWarn, "Can not rollback the current replica!") + return false + } + + dpr := k8s.NewDeployment(app.conn()) + dep, err := dpr.Get(ns, ctrlName) + if err != nil { + log.Error().Err(err).Msgf("Fetching Deployment %s", selectedItem) + app.flash(flashErr, err.Error()) + return false + } + dp := dep.(*appsv1.Deployment) + + vers, err := strconv.Atoi(revision) + if err != nil { + log.Error().Err(err).Msg("Revision conversion failed") + return false + } + + tokens := strings.Split(ctrlAPI, "/") + group := ctrlAPI + if len(tokens) == 2 { + group = tokens[0] + } + rb, err := kubectl.RollbackerFor(schema.GroupKind{group, ctrlKind}, app.conn().DialOrDie()) + if err != nil { + log.Error().Err(err).Msg("No rollbacker") + return false + } + + res, err := rb.Rollback(dp, map[string]string{}, int64(vers), false) + if err != nil { + log.Error().Err(err).Msg("Rollback failed") + return false + } + log.Debug().Msgf("Version %s %s", revision, res) + app.flash(flashInfo, fmt.Sprintf("Version %s %s", revision, res)) + + return true +} diff --git a/internal/views/sorter.go b/internal/views/sorter.go index 7d7f5dd6..3aec9e62 100644 --- a/internal/views/sorter.go +++ b/internal/views/sorter.go @@ -3,11 +3,11 @@ package views import ( "regexp" "strconv" - "strings" "time" "github.com/derailed/k9s/internal/resource" res "k8s.io/apimachinery/pkg/api/resource" + "vbom.ml/util/sortorder" ) type rowSorter struct { @@ -63,11 +63,11 @@ func less(asc bool, c1, c2 string) bool { return o } - c := strings.Compare(c1, c2) + b := sortorder.NaturalLess(c1, c2) if asc { - return c < 0 + return b } - return c > 0 + return !b } func isDurationSort(asc bool, c1, c2 string) (bool, bool) { @@ -118,18 +118,6 @@ func isMetric(s string) (string, bool) { } func isDuration(s string) (time.Duration, bool) { - // rx := regexp.MustCompile(`(\d+)([h|d])`) - // mm := rx.FindStringSubmatch(s) - // if len(mm) == 3 { - // n, _ := strconv.Atoi(mm[1]) - // switch mm[2] { - // case "d": - // s = fmt.Sprintf("%dh", n*24) - // case "y": - // s = fmt.Sprintf("%dh", n*365*24) - // } - // } - d, err := time.ParseDuration(s) if err != nil { return d, false diff --git a/internal/views/sorter_test.go b/internal/views/sorter_test.go index bec39090..fd68267b 100644 --- a/internal/views/sorter_test.go +++ b/internal/views/sorter_test.go @@ -29,6 +29,8 @@ func TestGroupSort(t *testing.T) { {true, []string{"95d", "93d"}, []string{"93d", "95d"}}, {true, []string{"1h10m", "59m"}, []string{"59m", "1h10m"}}, {true, []string{"95m", "1h30m"}, []string{"1h30m", "95m"}}, + {true, []string{"b-21", "b-2"}, []string{"b-2", "b-21"}}, + {false, []string{"b-21", "b-2"}, []string{"b-21", "b-2"}}, } for _, u := range uu { diff --git a/internal/views/sts.go b/internal/views/sts.go new file mode 100644 index 00000000..889c3b00 --- /dev/null +++ b/internal/views/sts.go @@ -0,0 +1,72 @@ +package views + +import ( + "github.com/derailed/k9s/internal/k8s" + "github.com/derailed/k9s/internal/resource" + "github.com/gdamore/tcell" + "github.com/rs/zerolog/log" + v1 "k8s.io/api/apps/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +type statefulSetView struct { + *resourceView +} + +func newStatefulSetView(t string, app *appView, list resource.List) resourceViewer { + v := statefulSetView{newResourceView(t, app, list).(*resourceView)} + { + v.extraActionsFn = v.extraActions + v.switchPage("sts") + } + + return &v +} + +func (v *statefulSetView) extraActions(aa keyActions) { + aa[KeyShiftD] = newKeyAction("Sort Desired", v.sortColCmd(1, false), true) + aa[KeyShiftC] = newKeyAction("Sort Current", v.sortColCmd(2, false), true) + aa[tcell.KeyEnter] = newKeyAction("View Pods", v.showPodsCmd, true) +} + +func (v *statefulSetView) sortColCmd(col int, asc bool) func(evt *tcell.EventKey) *tcell.EventKey { + return func(evt *tcell.EventKey) *tcell.EventKey { + t := v.getTV() + t.sortCol.index, t.sortCol.asc = t.nameColIndex()+col, asc + t.refresh() + + return nil + } +} + +func (v *statefulSetView) showPodsCmd(evt *tcell.EventKey) *tcell.EventKey { + if !v.rowSelected() { + return evt + } + + ns, n := namespaced(v.selectedItem) + d := k8s.NewStatefulSet(v.app.conn()) + s, err := d.Get(ns, n) + if err != nil { + log.Error().Err(err).Msgf("Fetching StatefulSet %s", v.selectedItem) + v.app.flash(flashErr, err.Error()) + return evt + } + sts := s.(*v1.StatefulSet) + + sel, err := metav1.LabelSelectorAsSelector(sts.Spec.Selector) + if err != nil { + log.Error().Err(err).Msgf("Converting selector for StatefulSet %s", v.selectedItem) + v.app.flash(flashErr, err.Error()) + return evt + } + showPods(v.app, "", "StatefulSet", v.selectedItem, sel.String(), "", v.backCmd) + + return nil +} + +func (v *statefulSetView) backCmd(evt *tcell.EventKey) *tcell.EventKey { + v.app.inject(v) + + return nil +} diff --git a/internal/views/subject.go b/internal/views/subject.go index 8288f40b..882ec356 100644 --- a/internal/views/subject.go +++ b/internal/views/subject.go @@ -83,6 +83,9 @@ func (v *subjectView) init(c context.Context, _ string) { v.app.SetFocus(v) } +func (v *subjectView) setExtraActionsFn(f actionsFn) { +} + func (v *subjectView) setColorerFn(f colorerFn) {} func (v *subjectView) setEnterFn(f enterFn) {} func (v *subjectView) setDecorateFn(f decorateFn) {} diff --git a/internal/views/table.go b/internal/views/table.go index 0a0469e0..608bf7b0 100644 --- a/internal/views/table.go +++ b/internal/views/table.go @@ -74,7 +74,7 @@ func newTableView(app *appView, title string) *tableView { } func (v *tableView) bindKeys() { - v.actions[KeyShiftI] = newKeyAction("Invert", v.sortInvertCmd, true) + v.actions[KeyShiftI] = newKeyAction("Invert", v.sortInvertCmd, false) v.actions[KeyShiftN] = newKeyAction("Sort Name", v.sortColCmd(0), true) v.actions[KeyShiftA] = newKeyAction("Sort Age", v.sortColCmd(-1), true)