diff --git a/internal/dao/registry.go b/internal/dao/registry.go index e65858e65d..0296b04fd9 100644 --- a/internal/dao/registry.go +++ b/internal/dao/registry.go @@ -5,17 +5,20 @@ package dao import ( "fmt" + "slices" "sort" "strings" "sync" - "github.com/derailed/k9s/internal/client" "github.com/rs/zerolog/log" + apiext "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" + + "github.com/derailed/k9s/internal/client" ) const ( @@ -95,10 +98,7 @@ func AccessorFor(f Factory, gvr client.GVR) (Accessor, error) { r, ok := m[gvr] if !ok { - r = new(Generic) - if MetaAccess.IsScalable(gvr) { - r = new(Scaler) - } + r = new(Scaler) log.Debug().Msgf("No DAO registry entry for %q. Using generics!", gvr) } r.Init(f, gvr) @@ -144,12 +144,7 @@ func (m *Meta) GVK2GVR(gv schema.GroupVersion, kind string) (client.GVR, bool, b // IsCRD checks if resource represents a CRD func IsCRD(r metav1.APIResource) bool { - for _, c := range r.Categories { - if c == crdCat { - return true - } - } - return false + return slices.Contains(r.Categories, crdCat) } // MetaFor returns a resource metadata for a given gvr. @@ -166,24 +161,19 @@ func (m *Meta) MetaFor(gvr client.GVR) (metav1.APIResource, error) { // IsK8sMeta checks for non resource meta. func IsK8sMeta(m metav1.APIResource) bool { - for _, c := range m.Categories { - if c == k9sCat || c == helmCat { - return false - } - } - - return true + return !slices.ContainsFunc(m.Categories, func(category string) bool { + return category == k9sCat || category == helmCat + }) } // IsK9sMeta checks for non resource meta. func IsK9sMeta(m metav1.APIResource) bool { - for _, c := range m.Categories { - if c == k9sCat { - return true - } - } + return slices.Contains(m.Categories, k9sCat) +} - return false +// IsScalable check if the resource can be scaled +func IsScalable(m metav1.APIResource) bool { + return slices.Contains(m.Categories, scaleCat) } // LoadResources hydrates server preferred+CRDs resource metadata. @@ -196,24 +186,14 @@ func (m *Meta) LoadResources(f Factory) error { return err } loadNonResource(m.resMetas) + + // We've actually loaded all the CRDs in loadPreferred, and we're now adding + // some additional CRD properties on top of that. loadCRDs(f, m.resMetas) return nil } -// IsScalable check if the resource can be scaled -func (m *Meta) IsScalable(gvr client.GVR) bool { - if meta, ok := m.resMetas[gvr]; ok { - for _, c := range meta.Categories { - if c == scaleCat { - return true - } - } - } - - return false -} - // BOZO!! Need countermeasures for direct commands! func loadNonResource(m ResourceMetas) { loadK9s(m) @@ -419,11 +399,13 @@ func isDeprecated(gvr client.GVR) bool { return ok } +// loadCRDs Wait for the cache to synced and then add some additional properties to CRD. func loadCRDs(f Factory, m ResourceMetas) { if f.Client() == nil || !f.Client().ConnectionOK() { return } - oo, err := f.List(crdGVR, client.ClusterScope, false, labels.Everything()) + + oo, err := f.List(crdGVR, client.ClusterScope, true, labels.Everything()) if err != nil { log.Warn().Err(err).Msgf("Fail CRDs load") return @@ -437,29 +419,29 @@ func loadCRDs(f Factory, m ResourceMetas) { continue } - var meta metav1.APIResource - meta.Kind = crd.Spec.Names.Kind - meta.Group = crd.Spec.Group - meta.Name = crd.Name - meta.SingularName = crd.Spec.Names.Singular - meta.ShortNames = crd.Spec.Names.ShortNames - meta.Namespaced = crd.Spec.Scope == apiext.NamespaceScoped - for _, v := range crd.Spec.Versions { - if v.Served && !v.Deprecated { - meta.Version = v.Name - break + if gvr, ok := newGVRFromCRD(&crd); ok { + if meta, ok := m[gvr]; ok { + if !slices.Contains(meta.Categories, scaleCat) { + meta.Categories = append(meta.Categories, scaleCat) + m[gvr] = meta + } } } + } +} - // meta, errs := extractMeta(o) - // if len(errs) > 0 { - // log.Error().Err(errs[0]).Msgf("Fail to extract CRD meta (%d) errors", len(errs)) - // continue - // } - meta.Categories = append(meta.Categories, crdCat) - gvr := client.NewGVRFromMeta(meta) - m[gvr] = meta +func newGVRFromCRD(crd *apiext.CustomResourceDefinition) (client.GVR, bool) { + for _, v := range crd.Spec.Versions { + if v.Served && !v.Deprecated { + return client.NewGVRFromMeta(metav1.APIResource{ + Group: crd.Spec.Group, + Name: crd.Spec.Names.Plural, + Version: v.Name, + }), true + } } + + return client.GVR{}, false } func extractMeta(o runtime.Object) (metav1.APIResource, []error) { diff --git a/internal/dao/scalable.go b/internal/dao/scalable.go index 6c13f45d3c..7664b5355d 100644 --- a/internal/dao/scalable.go +++ b/internal/dao/scalable.go @@ -1,14 +1,18 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package dao import ( "context" - "github.com/derailed/k9s/internal/client" "github.com/rs/zerolog/log" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/dynamic" "k8s.io/client-go/restmapper" "k8s.io/client-go/scale" + + "github.com/derailed/k9s/internal/client" ) var _ Scalable = (*Scaler)(nil) diff --git a/internal/dao/types.go b/internal/dao/types.go index 014c668e4c..8e7602f294 100644 --- a/internal/dao/types.go +++ b/internal/dao/types.go @@ -8,14 +8,15 @@ import ( "io" "time" - "github.com/derailed/k9s/internal/client" - "github.com/derailed/k9s/internal/watch" v1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/runtime" "k8s.io/client-go/informers" restclient "k8s.io/client-go/rest" + + "github.com/derailed/k9s/internal/client" + "github.com/derailed/k9s/internal/watch" ) // ResourceMetas represents a collection of resource metadata. diff --git a/internal/view/command.go b/internal/view/command.go index abdf09ab26..bf42956eb4 100644 --- a/internal/view/command.go +++ b/internal/view/command.go @@ -11,11 +11,12 @@ import ( "strings" "sync" + "github.com/rs/zerolog/log" + "github.com/derailed/k9s/internal/client" "github.com/derailed/k9s/internal/dao" "github.com/derailed/k9s/internal/model" "github.com/derailed/k9s/internal/view/cmd" - "github.com/rs/zerolog/log" ) var ( @@ -288,12 +289,7 @@ func (c *Command) viewMetaFor(p *cmd.Interpreter) (client.GVR, *MetaViewer, erro v := MetaViewer{ viewerFn: func(gvr client.GVR) ResourceViewer { - viewer := NewOwnerExtender(NewBrowser(gvr)) - if dao.MetaAccess.IsScalable(gvr) { - viewer = NewScaleExtender(viewer) - } - - return viewer + return NewScaleExtender(NewOwnerExtender(NewBrowser(gvr))) }, } if mv, ok := customViewers[gvr]; ok { diff --git a/internal/view/dp_test.go b/internal/view/dp_test.go index a3e8a51fd4..1e2074dc72 100644 --- a/internal/view/dp_test.go +++ b/internal/view/dp_test.go @@ -6,9 +6,10 @@ package view_test import ( "testing" + "github.com/stretchr/testify/assert" + "github.com/derailed/k9s/internal/client" "github.com/derailed/k9s/internal/view" - "github.com/stretchr/testify/assert" ) func TestDeploy(t *testing.T) { diff --git a/internal/view/scale_extender.go b/internal/view/scale_extender.go index 45fddaf9ba..e572e8cc79 100644 --- a/internal/view/scale_extender.go +++ b/internal/view/scale_extender.go @@ -9,13 +9,13 @@ import ( "strconv" "strings" - "github.com/derailed/k9s/internal/config" - - "github.com/derailed/k9s/internal/dao" - "github.com/derailed/k9s/internal/ui" "github.com/derailed/tcell/v2" "github.com/derailed/tview" "github.com/rs/zerolog/log" + + "github.com/derailed/k9s/internal/config" + "github.com/derailed/k9s/internal/dao" + "github.com/derailed/k9s/internal/ui" ) // ScaleExtender adds scaling extensions. @@ -35,12 +35,21 @@ func (s *ScaleExtender) bindKeys(aa *ui.KeyActions) { if s.App().Config.K9s.IsReadOnly() { return } - aa.Add(ui.KeyS, ui.NewKeyActionWithOpts("Scale", s.scaleCmd, - ui.ActionOpts{ - Visible: true, - Dangerous: true, - }, - )) + + meta, err := dao.MetaAccess.MetaFor(s.GVR()) + if err != nil { + log.Error().Err(err).Msgf("Unable to retrieve meta information for %s", s.GVR()) + return + } + + if !dao.IsCRD(meta) || dao.IsScalable(meta) { + aa.Add(ui.KeyS, ui.NewKeyActionWithOpts("Scale", s.scaleCmd, + ui.ActionOpts{ + Visible: true, + Dangerous: true, + }, + )) + } } func (s *ScaleExtender) scaleCmd(evt *tcell.EventKey) *tcell.EventKey { @@ -127,7 +136,7 @@ func (s *ScaleExtender) makeScaleForm(sels []string) (*tview.Form, error) { if len(sels) == 1 { // If the CRD resource supports scaling, then first try to // read the replicas directly from the CRD. - if dao.MetaAccess.IsScalable(s.GVR()) { + if meta, _ := dao.MetaAccess.MetaFor(s.GVR()); dao.IsScalable(meta) { replicas, err := s.replicasFromScaleSubresource(sels[0]) if err == nil && len(replicas) != 0 { factor = replicas @@ -169,7 +178,7 @@ func (s *ScaleExtender) makeScaleForm(sels []string) (*tview.Form, error) { return } } - if len(sels) == 1 { + if len(sels) != 1 { s.App().Flash().Infof("[%d] %s scaled successfully", len(sels), singularize(s.GVR().R())) } else { s.App().Flash().Infof("%s %s scaled successfully", s.GVR().R(), sels[0]) diff --git a/internal/view/sts_test.go b/internal/view/sts_test.go index 4455194528..256587c63e 100644 --- a/internal/view/sts_test.go +++ b/internal/view/sts_test.go @@ -6,9 +6,10 @@ package view_test import ( "testing" + "github.com/stretchr/testify/assert" + "github.com/derailed/k9s/internal/client" "github.com/derailed/k9s/internal/view" - "github.com/stretchr/testify/assert" ) func TestStatefulSetNew(t *testing.T) { diff --git a/internal/watch/factory.go b/internal/watch/factory.go index 698b17a458..e1a7df7a48 100644 --- a/internal/watch/factory.go +++ b/internal/watch/factory.go @@ -9,7 +9,6 @@ import ( "sync" "time" - "github.com/derailed/k9s/internal/client" "github.com/rs/zerolog/log" v1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" @@ -17,10 +16,13 @@ import ( "k8s.io/apimachinery/pkg/runtime" di "k8s.io/client-go/dynamic/dynamicinformer" "k8s.io/client-go/informers" + + "github.com/derailed/k9s/internal/client" ) const ( - defaultResync = 10 * time.Minute + defaultResync = 10 * time.Minute + defaultWaitTime = 250 * time.Millisecond ) // Factory tracks various resource informers. @@ -142,8 +144,13 @@ func (f *Factory) waitForCacheSync(ns string) { return } - // we must block until all started informers' caches were synced - _ = fac.WaitForCacheSync(f.stopChan) + // Hang for a sec for the cache to refresh if still not done bail out! + c := make(chan struct{}) + go func(c chan struct{}) { + <-time.After(defaultWaitTime) + close(c) + }(c) + _ = fac.WaitForCacheSync(c) } // WaitForCacheSync waits for all factories to update their cache.