Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

allow scaling custom resource #2833

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
88 changes: 43 additions & 45 deletions internal/dao/registry.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,25 +5,28 @@ 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 (
crdCat = "crd"
k9sCat = "k9s"
helmCat = "helm"
crdGVR = "apiextensions.k8s.io/v1/customresourcedefinitions"
crdCat = "crd"
k9sCat = "k9s"
helmCat = "helm"
scaleCat = "scale"
crdGVR = "apiextensions.k8s.io/v1/customresourcedefinitions"
)

// MetaAccess tracks resources metadata.
Expand Down Expand Up @@ -95,7 +98,7 @@ func AccessorFor(f Factory, gvr client.GVR) (Accessor, error) {

r, ok := m[gvr]
if !ok {
r = new(Generic)
r = new(Scaler)
log.Debug().Msgf("No DAO registry entry for %q. Using generics!", gvr)
}
r.Init(f, gvr)
Expand Down Expand Up @@ -141,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.
Expand All @@ -163,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.
Expand All @@ -193,6 +186,9 @@ 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
Expand Down Expand Up @@ -403,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
Expand All @@ -421,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, version, ok := newGVRFromCRD(&crd); ok {
if meta, ok := m[gvr]; ok && version.Subresources != nil && version.Subresources.Scale != nil {
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, apiext.CustomResourceDefinitionVersion, 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,
}), v, true
}
}

return client.GVR{}, apiext.CustomResourceDefinitionVersion{}, false
}

func extractMeta(o runtime.Object) (metav1.APIResource, []error) {
Expand Down
81 changes: 81 additions & 0 deletions internal/dao/scalable.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright Authors of K9s

package dao

import (
"context"

"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)
var _ ReplicasGetter = (*Scaler)(nil)

// Scaler represents a generic resource with scaling.
type Scaler struct {
Generic
}

// Replicas returns the number of replicas for the resource located at the given path.
func (s *Scaler) Replicas(ctx context.Context, path string) (int32, error) {
scaleClient, err := s.scaleClient()
if err != nil {
return 0, err
}

ns, name := client.Namespaced(path)
currScale, err := scaleClient.Scales(ns).Get(ctx, *s.gvr.GR(), name, metav1.GetOptions{})
if err != nil {
return 0, err
}

return currScale.Spec.Replicas, nil
}

// Scale modifies the number of replicas for a given resource specified by the path.
func (s *Scaler) Scale(ctx context.Context, path string, replicas int32) error {
ns, name := client.Namespaced(path)

scaleClient, err := s.scaleClient()
if err != nil {
return err
}

currentScale, err := scaleClient.Scales(ns).Get(ctx, *s.gvr.GR(), name, metav1.GetOptions{})
if err != nil {
return err
}

currentScale.Spec.Replicas = replicas
updatedScale, err := scaleClient.Scales(ns).Update(ctx, *s.gvr.GR(), currentScale, metav1.UpdateOptions{})
if err != nil {
return err
}

log.Debug().Msgf("%s scaled to %d", path, updatedScale.Spec.Replicas)
return nil
}

func (s *Scaler) scaleClient() (scale.ScalesGetter, error) {
cfg, err := s.Client().RestConfig()
if err != nil {
return nil, err
}

discoveryClient, err := s.Client().CachedDiscovery()
if err != nil {
return nil, err
}

mapper := restmapper.NewDeferredDiscoveryRESTMapper(discoveryClient)
scaleKindResolver := scale.NewDiscoveryScaleKindResolver(discoveryClient)

return scale.NewForConfig(cfg, mapper, dynamic.LegacyAPIPathResolverFunc, scaleKindResolver)
}
11 changes: 9 additions & 2 deletions internal/dao/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -121,6 +122,12 @@ type Scalable interface {
Scale(ctx context.Context, path string, replicas int32) error
}

// ReplicasGetter represents a resource with replicas.
type ReplicasGetter interface {
// Replicas returns the number of replicas for the resource located at the given path.
Replicas(ctx context.Context, path string) (int32, error)
}

// Controller represents a pod controller.
type Controller interface {
// Pod returns a pod instance matching the selector.
Expand Down
5 changes: 3 additions & 2 deletions internal/view/command.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down Expand Up @@ -288,7 +289,7 @@ func (c *Command) viewMetaFor(p *cmd.Interpreter) (client.GVR, *MetaViewer, erro

v := MetaViewer{
viewerFn: func(gvr client.GVR) ResourceViewer {
return NewOwnerExtender(NewBrowser(gvr))
return NewScaleExtender(NewOwnerExtender(NewBrowser(gvr)))
},
}
if mv, ok := customViewers[gvr]; ok {
Expand Down
3 changes: 2 additions & 1 deletion internal/view/dp_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
Loading
Loading