allow scaling custom resource (#2833)
parent
bd4a8ca1f6
commit
6881892433
|
|
@ -5,11 +5,11 @@ package dao
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"slices"
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/derailed/k9s/internal/client"
|
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
apiext "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
|
apiext "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
|
@ -17,13 +17,16 @@ import (
|
||||||
"k8s.io/apimachinery/pkg/labels"
|
"k8s.io/apimachinery/pkg/labels"
|
||||||
"k8s.io/apimachinery/pkg/runtime"
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||||
|
|
||||||
|
"github.com/derailed/k9s/internal/client"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
crdCat = "crd"
|
crdCat = "crd"
|
||||||
k9sCat = "k9s"
|
k9sCat = "k9s"
|
||||||
helmCat = "helm"
|
helmCat = "helm"
|
||||||
crdGVR = "apiextensions.k8s.io/v1/customresourcedefinitions"
|
scaleCat = "scale"
|
||||||
|
crdGVR = "apiextensions.k8s.io/v1/customresourcedefinitions"
|
||||||
)
|
)
|
||||||
|
|
||||||
// MetaAccess tracks resources metadata.
|
// MetaAccess tracks resources metadata.
|
||||||
|
|
@ -93,7 +96,7 @@ func AccessorFor(f Factory, gvr client.GVR) (Accessor, error) {
|
||||||
|
|
||||||
r, ok := m[gvr]
|
r, ok := m[gvr]
|
||||||
if !ok {
|
if !ok {
|
||||||
r = new(Generic)
|
r = new(Scaler)
|
||||||
log.Debug().Msgf("No DAO registry entry for %q. Using generics!", gvr)
|
log.Debug().Msgf("No DAO registry entry for %q. Using generics!", gvr)
|
||||||
}
|
}
|
||||||
r.Init(f, gvr)
|
r.Init(f, gvr)
|
||||||
|
|
@ -151,34 +154,24 @@ func (m *Meta) MetaFor(gvr client.GVR) (metav1.APIResource, error) {
|
||||||
|
|
||||||
// IsCRD checks if resource represents a CRD
|
// IsCRD checks if resource represents a CRD
|
||||||
func IsCRD(r metav1.APIResource) bool {
|
func IsCRD(r metav1.APIResource) bool {
|
||||||
for _, c := range r.Categories {
|
return slices.Contains(r.Categories, crdCat)
|
||||||
if c == crdCat {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsK8sMeta checks for non resource meta.
|
// IsK8sMeta checks for non resource meta.
|
||||||
func IsK8sMeta(m metav1.APIResource) bool {
|
func IsK8sMeta(m metav1.APIResource) bool {
|
||||||
for _, c := range m.Categories {
|
return !slices.ContainsFunc(m.Categories, func(category string) bool {
|
||||||
if c == k9sCat || c == helmCat {
|
return category == k9sCat || category == helmCat
|
||||||
return false
|
})
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsK9sMeta checks for non resource meta.
|
// IsK9sMeta checks for non resource meta.
|
||||||
func IsK9sMeta(m metav1.APIResource) bool {
|
func IsK9sMeta(m metav1.APIResource) bool {
|
||||||
for _, c := range m.Categories {
|
return slices.Contains(m.Categories, k9sCat)
|
||||||
if c == k9sCat {
|
}
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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.
|
// LoadResources hydrates server preferred+CRDs resource metadata.
|
||||||
|
|
@ -191,6 +184,9 @@ func (m *Meta) LoadResources(f Factory) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
loadNonResource(m.resMetas)
|
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)
|
loadCRDs(f, m.resMetas)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|
@ -401,11 +397,13 @@ func isDeprecated(gvr client.GVR) bool {
|
||||||
return ok
|
return ok
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// loadCRDs Wait for the cache to synced and then add some additional properties to CRD.
|
||||||
func loadCRDs(f Factory, m ResourceMetas) {
|
func loadCRDs(f Factory, m ResourceMetas) {
|
||||||
if f.Client() == nil || !f.Client().ConnectionOK() {
|
if f.Client() == nil || !f.Client().ConnectionOK() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
oo, err := f.List(crdGVR, client.ClusterScope, false, labels.Everything())
|
|
||||||
|
oo, err := f.List(crdGVR, client.ClusterScope, true, labels.Everything())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Warn().Err(err).Msgf("Fail CRDs load")
|
log.Warn().Err(err).Msgf("Fail CRDs load")
|
||||||
return
|
return
|
||||||
|
|
@ -419,34 +417,35 @@ func loadCRDs(f Factory, m ResourceMetas) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
var meta metav1.APIResource
|
if gvr, version, ok := newGVRFromCRD(&crd); ok {
|
||||||
meta.Kind = crd.Spec.Names.Kind
|
if meta, ok := m[gvr]; ok && version.Subresources != nil && version.Subresources.Scale != nil {
|
||||||
meta.Group = crd.Spec.Group
|
if !slices.Contains(meta.Categories, scaleCat) {
|
||||||
// Since CRD names are cluster scoped they need to be unique, however, it is allowed
|
meta.Categories = append(meta.Categories, scaleCat)
|
||||||
// to have the CRDs with the same names in different groups. Because of that, the
|
m[gvr] = meta
|
||||||
// returned `crd.Name` values have the group as a suffix, for example
|
}
|
||||||
// "ciliumnetworkpolicies.cilium.io".
|
|
||||||
//
|
|
||||||
// `Name` field of `meta/v1/APIResource` is supposed to be the plural name of the
|
|
||||||
// resource, without the group. Because of that we need to trim the group suffix.
|
|
||||||
meta.Name = strings.TrimSuffix(crd.Name, "."+meta.Group)
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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{
|
||||||
|
Kind: crd.Spec.Names.Kind,
|
||||||
|
Group: crd.Spec.Group,
|
||||||
|
Name: crd.Spec.Names.Plural,
|
||||||
|
Version: v.Name,
|
||||||
|
ShortNames: crd.Spec.Names.ShortNames,
|
||||||
|
SingularName: crd.Spec.Names.Plural,
|
||||||
|
Namespaced: crd.Spec.Scope == apiext.NamespaceScoped,
|
||||||
|
}), v, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return client.GVR{}, apiext.CustomResourceDefinitionVersion{}, false
|
||||||
|
}
|
||||||
|
|
||||||
func extractMeta(o runtime.Object) (metav1.APIResource, []error) {
|
func extractMeta(o runtime.Object) (metav1.APIResource, []error) {
|
||||||
var (
|
var (
|
||||||
m metav1.APIResource
|
m metav1.APIResource
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
|
|
@ -8,14 +8,15 @@ import (
|
||||||
"io"
|
"io"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/derailed/k9s/internal/client"
|
|
||||||
"github.com/derailed/k9s/internal/watch"
|
|
||||||
v1 "k8s.io/api/core/v1"
|
v1 "k8s.io/api/core/v1"
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
"k8s.io/apimachinery/pkg/labels"
|
"k8s.io/apimachinery/pkg/labels"
|
||||||
"k8s.io/apimachinery/pkg/runtime"
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
"k8s.io/client-go/informers"
|
"k8s.io/client-go/informers"
|
||||||
restclient "k8s.io/client-go/rest"
|
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.
|
// ResourceMetas represents a collection of resource metadata.
|
||||||
|
|
@ -124,6 +125,12 @@ type Scalable interface {
|
||||||
Scale(ctx context.Context, path string, replicas int32) error
|
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.
|
// Controller represents a pod controller.
|
||||||
type Controller interface {
|
type Controller interface {
|
||||||
// Pod returns a pod instance matching the selector.
|
// Pod returns a pod instance matching the selector.
|
||||||
|
|
|
||||||
|
|
@ -11,11 +11,12 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
|
||||||
"github.com/derailed/k9s/internal/client"
|
"github.com/derailed/k9s/internal/client"
|
||||||
"github.com/derailed/k9s/internal/dao"
|
"github.com/derailed/k9s/internal/dao"
|
||||||
"github.com/derailed/k9s/internal/model"
|
"github.com/derailed/k9s/internal/model"
|
||||||
"github.com/derailed/k9s/internal/view/cmd"
|
"github.com/derailed/k9s/internal/view/cmd"
|
||||||
"github.com/rs/zerolog/log"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
|
@ -293,7 +294,7 @@ func (c *Command) viewMetaFor(p *cmd.Interpreter) (client.GVR, *MetaViewer, erro
|
||||||
|
|
||||||
v := MetaViewer{
|
v := MetaViewer{
|
||||||
viewerFn: func(gvr client.GVR) ResourceViewer {
|
viewerFn: func(gvr client.GVR) ResourceViewer {
|
||||||
return NewOwnerExtender(NewBrowser(gvr))
|
return NewScaleExtender(NewOwnerExtender(NewBrowser(gvr)))
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
if mv, ok := customViewers[gvr]; ok {
|
if mv, ok := customViewers[gvr]; ok {
|
||||||
|
|
|
||||||
|
|
@ -6,9 +6,10 @@ package view_test
|
||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
|
||||||
"github.com/derailed/k9s/internal/client"
|
"github.com/derailed/k9s/internal/client"
|
||||||
"github.com/derailed/k9s/internal/view"
|
"github.com/derailed/k9s/internal/view"
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestDeploy(t *testing.T) {
|
func TestDeploy(t *testing.T) {
|
||||||
|
|
|
||||||
|
|
@ -9,11 +9,12 @@ import (
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/derailed/k9s/internal/dao"
|
|
||||||
"github.com/derailed/k9s/internal/ui"
|
|
||||||
"github.com/derailed/tcell/v2"
|
"github.com/derailed/tcell/v2"
|
||||||
"github.com/derailed/tview"
|
"github.com/derailed/tview"
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
|
|
||||||
|
"github.com/derailed/k9s/internal/dao"
|
||||||
|
"github.com/derailed/k9s/internal/ui"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ScaleExtender adds scaling extensions.
|
// ScaleExtender adds scaling extensions.
|
||||||
|
|
@ -33,12 +34,21 @@ func (s *ScaleExtender) bindKeys(aa *ui.KeyActions) {
|
||||||
if s.App().Config.K9s.IsReadOnly() {
|
if s.App().Config.K9s.IsReadOnly() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
aa.Add(ui.KeyS, ui.NewKeyActionWithOpts("Scale", s.scaleCmd,
|
|
||||||
ui.ActionOpts{
|
meta, err := dao.MetaAccess.MetaFor(s.GVR())
|
||||||
Visible: true,
|
if err != nil {
|
||||||
Dangerous: true,
|
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 {
|
func (s *ScaleExtender) scaleCmd(evt *tcell.EventKey) *tcell.EventKey {
|
||||||
|
|
@ -81,18 +91,64 @@ func (s *ScaleExtender) valueOf(col string) (string, error) {
|
||||||
return s.GetTable().GetSelectedCell(colIdx), nil
|
return s.GetTable().GetSelectedCell(colIdx), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *ScaleExtender) replicasFromReady(_ string) (string, error) {
|
||||||
|
replicas, err := s.valueOf("READY")
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
tokens := strings.Split(replicas, "/")
|
||||||
|
if len(tokens) < 2 {
|
||||||
|
return "", fmt.Errorf("unable to locate replicas from %s", replicas)
|
||||||
|
}
|
||||||
|
|
||||||
|
return strings.TrimRight(tokens[1], ui.DeltaSign), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ScaleExtender) replicasFromScaleSubresource(sel string) (string, error) {
|
||||||
|
res, err := dao.AccessorFor(s.App().factory, s.GVR())
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
replicasGetter, ok := res.(dao.ReplicasGetter)
|
||||||
|
if !ok {
|
||||||
|
return "", fmt.Errorf("expecting a replicasGetter resource for %q", s.GVR())
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), s.App().Conn().Config().CallTimeout())
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
replicas, err := replicasGetter.Replicas(ctx, sel)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return strconv.Itoa(int(replicas)), nil
|
||||||
|
}
|
||||||
|
|
||||||
func (s *ScaleExtender) makeScaleForm(sels []string) (*tview.Form, error) {
|
func (s *ScaleExtender) makeScaleForm(sels []string) (*tview.Form, error) {
|
||||||
factor := "0"
|
factor := "0"
|
||||||
if len(sels) == 1 {
|
if len(sels) == 1 {
|
||||||
replicas, err := s.valueOf("READY")
|
// If the CRD resource supports scaling, then first try to
|
||||||
if err != nil {
|
// read the replicas directly from the CRD.
|
||||||
return nil, err
|
if meta, _ := dao.MetaAccess.MetaFor(s.GVR()); dao.IsScalable(meta) {
|
||||||
|
replicas, err := s.replicasFromScaleSubresource(sels[0])
|
||||||
|
if err == nil && len(replicas) != 0 {
|
||||||
|
factor = replicas
|
||||||
|
}
|
||||||
}
|
}
|
||||||
tokens := strings.Split(replicas, "/")
|
|
||||||
if len(tokens) < 2 {
|
// For built-in resources or cases where we can't get the replicas from the CRD, we can
|
||||||
return nil, fmt.Errorf("unable to locate replicas from %s", replicas)
|
// only try to get the number of copies from the READY field.
|
||||||
|
if factor == "0" {
|
||||||
|
replicas, err := s.replicasFromReady(sels[0])
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
factor = replicas
|
||||||
}
|
}
|
||||||
factor = strings.TrimRight(tokens[1], ui.DeltaSign)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
styles := s.App().Styles.Dialog()
|
styles := s.App().Styles.Dialog()
|
||||||
|
|
@ -127,7 +183,7 @@ func (s *ScaleExtender) makeScaleForm(sels []string) (*tview.Form, error) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if len(sels) == 1 {
|
if len(sels) != 1 {
|
||||||
s.App().Flash().Infof("[%d] %s scaled successfully", len(sels), singularize(s.GVR().R()))
|
s.App().Flash().Infof("[%d] %s scaled successfully", len(sels), singularize(s.GVR().R()))
|
||||||
} else {
|
} else {
|
||||||
s.App().Flash().Infof("%s %s scaled successfully", s.GVR().R(), sels[0])
|
s.App().Flash().Infof("%s %s scaled successfully", s.GVR().R(), sels[0])
|
||||||
|
|
|
||||||
|
|
@ -6,9 +6,10 @@ package view_test
|
||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
|
||||||
"github.com/derailed/k9s/internal/client"
|
"github.com/derailed/k9s/internal/client"
|
||||||
"github.com/derailed/k9s/internal/view"
|
"github.com/derailed/k9s/internal/view"
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestStatefulSetNew(t *testing.T) {
|
func TestStatefulSetNew(t *testing.T) {
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,6 @@ import (
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/derailed/k9s/internal/client"
|
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
v1 "k8s.io/api/core/v1"
|
v1 "k8s.io/api/core/v1"
|
||||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||||
|
|
@ -17,6 +16,8 @@ import (
|
||||||
"k8s.io/apimachinery/pkg/runtime"
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
di "k8s.io/client-go/dynamic/dynamicinformer"
|
di "k8s.io/client-go/dynamic/dynamicinformer"
|
||||||
"k8s.io/client-go/informers"
|
"k8s.io/client-go/informers"
|
||||||
|
|
||||||
|
"github.com/derailed/k9s/internal/client"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
|
@ -47,7 +48,7 @@ func (f *Factory) Start(ns string) {
|
||||||
f.mx.Lock()
|
f.mx.Lock()
|
||||||
defer f.mx.Unlock()
|
defer f.mx.Unlock()
|
||||||
|
|
||||||
log.Debug().Msgf("Factory START with ns `%q", ns)
|
log.Debug().Msgf("Factory START with ns %q", ns)
|
||||||
f.stopChan = make(chan struct{})
|
f.stopChan = make(chan struct{})
|
||||||
for ns, fac := range f.factories {
|
for ns, fac := range f.factories {
|
||||||
log.Debug().Msgf("Starting factory in ns %q", ns)
|
log.Debug().Msgf("Starting factory in ns %q", ns)
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue