Merge pull request #847 from Ameausoone/feat/setImageFeature

Feat/set image feature
mine
Fernand Galiana 2020-10-23 06:02:52 -06:00 committed by GitHub
commit ca51ce547b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 560 additions and 34 deletions

2
go.mod
View File

@ -24,7 +24,7 @@ require (
github.com/ryanuber/go-glob v1.0.0 // indirect
github.com/sahilm/fuzzy v0.1.0
github.com/spf13/cobra v1.0.0
github.com/stretchr/testify v1.5.1
github.com/stretchr/testify v1.6.1
golang.org/x/net v0.0.0-20200519113804-d87ec0cfa476 // indirect
golang.org/x/sys v0.0.0-20200519105757-fe76b779f299 // indirect
golang.org/x/text v0.3.2

4
go.sum
View File

@ -613,6 +613,8 @@ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UV
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/syndtr/gocapability v0.0.0-20170704070218-db04d3cc01c8/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww=
github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
@ -837,6 +839,8 @@ gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo=
gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw=
helm.sh/helm/v3 v3.2.0 h1:V12EGAmr2DJ/fWrPo2fPdXWSIXvlXm51vGkQIXMeymE=

View File

@ -4,7 +4,6 @@ import (
"context"
"errors"
"fmt"
"github.com/derailed/k9s/internal/client"
"github.com/rs/zerolog/log"
appsv1 "k8s.io/api/apps/v1"
@ -18,12 +17,13 @@ import (
)
var (
_ Accessor = (*Deployment)(nil)
_ Nuker = (*Deployment)(nil)
_ Loggable = (*Deployment)(nil)
_ Restartable = (*Deployment)(nil)
_ Scalable = (*Deployment)(nil)
_ Controller = (*Deployment)(nil)
_ Accessor = (*Deployment)(nil)
_ Nuker = (*Deployment)(nil)
_ Loggable = (*Deployment)(nil)
_ Restartable = (*Deployment)(nil)
_ Scalable = (*Deployment)(nil)
_ Controller = (*Deployment)(nil)
_ ContainsPodSpec = (*Deployment)(nil)
)
// Deployment represents a deployment K8s resource.
@ -211,6 +211,42 @@ func (d *Deployment) Scan(ctx context.Context, gvr, fqn string, wait bool) (Refs
return refs, nil
}
func (d *Deployment) GetPodSpec(path string) (*v1.PodSpec, error) {
dp, err := d.Load(d.Factory, path)
if err != nil {
return nil, err
}
podSpec := dp.Spec.Template.Spec
return &podSpec, nil
}
func (d *Deployment) SetImages(ctx context.Context, path string, imageSpecs ImageSpecs) error {
ns, n := client.Namespaced(path)
auth, err := d.Client().CanI(ns, "apps/v1/deployments", []string{client.PatchVerb})
if err != nil {
return err
}
if !auth {
return fmt.Errorf("user is not authorized to patch a deployment")
}
jsonPatch, err := GetTemplateJsonPatch(imageSpecs)
if err != nil {
return err
}
dial, err := d.Client().Dial()
if err != nil {
return err
}
_, err = dial.AppsV1().Deployments(ns).Patch(
ctx,
n,
types.StrategicMergePatchType,
jsonPatch,
metav1.PatchOptions{},
)
return err
}
func hasPVC(spec *v1.PodSpec, name string) bool {
for _, v := range spec.Volumes {
if v.PersistentVolumeClaim != nil && v.PersistentVolumeClaim.ClaimName == name {

View File

@ -21,11 +21,12 @@ import (
)
var (
_ Accessor = (*DaemonSet)(nil)
_ Nuker = (*DaemonSet)(nil)
_ Loggable = (*DaemonSet)(nil)
_ Restartable = (*DaemonSet)(nil)
_ Controller = (*DaemonSet)(nil)
_ Accessor = (*DaemonSet)(nil)
_ Nuker = (*DaemonSet)(nil)
_ Loggable = (*DaemonSet)(nil)
_ Restartable = (*DaemonSet)(nil)
_ Controller = (*DaemonSet)(nil)
_ ContainsPodSpec = (*DaemonSet)(nil)
)
// DaemonSet represents a K8s daemonset.
@ -225,6 +226,42 @@ func (d *DaemonSet) Scan(ctx context.Context, gvr, fqn string, wait bool) (Refs,
return refs, nil
}
func (d *DaemonSet) GetPodSpec(path string) (*v1.PodSpec, error) {
ds, err := d.GetInstance(path)
if err != nil {
return nil, err
}
podSpec := ds.Spec.Template.Spec
return &podSpec, nil
}
func (d *DaemonSet) SetImages(ctx context.Context, path string, imageSpecs ImageSpecs) error {
ns, n := client.Namespaced(path)
auth, err := d.Client().CanI(ns, "apps/v1/daemonset", []string{client.PatchVerb})
if err != nil {
return err
}
if !auth {
return fmt.Errorf("user is not authorized to patch a daemonset")
}
jsonPatch, err := GetTemplateJsonPatch(imageSpecs)
if err != nil {
return err
}
dial, err := d.Client().Dial()
if err != nil {
return err
}
_, err = dial.AppsV1().DaemonSets(ns).Patch(
ctx,
n,
types.StrategicMergePatchType,
jsonPatch,
metav1.PatchOptions{},
)
return err
}
// ----------------------------------------------------------------------------
// Helpers...

78
internal/dao/patch.go Normal file
View File

@ -0,0 +1,78 @@
package dao
import (
"encoding/json"
)
type ImageSpec struct {
Index int
Name, DockerImage string
Init bool
}
type ImageSpecs []ImageSpec
type JsonPatch struct {
Spec Spec `json:"spec"`
}
type Spec struct {
Template PodSpec `json:"template"`
}
type PodSpec struct {
Spec ImagesSpec `json:"spec"`
}
type ImagesSpec struct {
SetElementOrderContainers []Element `json:"$setElementOrder/containers,omitempty"`
SetElementOrderInitContainers []Element `json:"$setElementOrder/initContainers,omitempty"`
Containers []Element `json:"containers,omitempty"`
InitContainers []Element `json:"initContainers,omitempty"`
}
type Element struct {
Image string `json:"image,omitempty"`
Name string `json:"name"`
}
// Build a json patch string to update PodSpec images
func GetTemplateJsonPatch(imageSpecs ImageSpecs) ([]byte, error) {
jsonPatch := JsonPatch{
Spec: Spec{
Template: getPatchPodSpec(imageSpecs),
},
}
return json.Marshal(jsonPatch)
}
func GetJsonPatch(imageSpecs ImageSpecs) ([]byte, error) {
podSpec := getPatchPodSpec(imageSpecs)
return json.Marshal(podSpec)
}
func getPatchPodSpec(imageSpecs ImageSpecs) PodSpec {
initElementsOrders, initElements, elementsOrders, elements := extractElements(imageSpecs)
podSpec := PodSpec{
Spec: ImagesSpec{
SetElementOrderInitContainers: initElementsOrders,
InitContainers: initElements,
SetElementOrderContainers: elementsOrders,
Containers: elements,
},
}
return podSpec
}
func extractElements(imageSpecs ImageSpecs) (initElementsOrders []Element, initElements []Element, elementsOrders []Element, elements []Element) {
for _, spec := range imageSpecs {
if spec.Init {
initElementsOrders = append(initElementsOrders, Element{Name: spec.Name})
initElements = append(initElements, Element{Name: spec.Name, Image: spec.DockerImage})
} else {
elementsOrders = append(elementsOrders, Element{Name: spec.Name})
elements = append(elements, Element{Name: spec.Name, Image: spec.DockerImage})
}
}
return initElementsOrders, initElements, elementsOrders, elements
}

View File

@ -0,0 +1,90 @@
package dao
import (
"github.com/stretchr/testify/require"
"testing"
)
func TestGetTemplateJsonPatch(t *testing.T) {
type args struct {
imageSpecs ImageSpecs
}
tests := map[string]struct {
args args
want string
wantErr bool
}{
"simple": {
args: args{
imageSpecs: ImageSpecs{
ImageSpec{
Index: 0,
Name: "init",
DockerImage: "busybox:latest",
Init: true,
},
ImageSpec{
Index: 0,
Name: "nginx",
DockerImage: "nginx:latest",
Init: false,
},
},
},
want: `{"spec":{"template":{"spec":{"$setElementOrder/initContainers":[{"name":"init"}],"$setElementOrder/containers":[{"name":"nginx"}],"initContainers":[{"image":"busybox:latest","name":"init"}],"containers":[{"image":"nginx:latest","name":"nginx"}]}}}}`,
wantErr: false,
},
}
for name, tt := range tests {
t.Run(name, func(t *testing.T) {
got, err := GetTemplateJsonPatch(tt.args.imageSpecs)
if (err != nil) != tt.wantErr {
t.Errorf("GetTemplateJsonPatch() error = %v, wantErr %v", err, tt.wantErr)
return
}
require.JSONEq(t, tt.want, string(got), "Json strings should be equal")
})
}
}
func TestGetJsonPatch(t *testing.T) {
type args struct {
imageSpecs ImageSpecs
}
tests := map[string]struct {
args args
want string
wantErr bool
}{
"simple": {
args: args{
imageSpecs: ImageSpecs{
ImageSpec{
Index: 0,
Name: "init",
DockerImage: "busybox:latest",
Init: true,
},
ImageSpec{
Index: 0,
Name: "nginx",
DockerImage: "nginx:latest",
Init: false,
},
},
},
want: `{"spec":{"$setElementOrder/initContainers":[{"name":"init"}],"initContainers":[{"image":"busybox:latest","name":"init"}],"$setElementOrder/containers":[{"name":"nginx"}],"containers":[{"image":"nginx:latest","name":"nginx"}]}}`,
wantErr: false,
},
}
for name, tt := range tests {
t.Run(name, func(t *testing.T) {
got, err := GetJsonPatch(tt.args.imageSpecs)
if (err != nil) != tt.wantErr {
t.Errorf("GetTemplateJsonPatch() error = %v, wantErr %v", err, tt.wantErr)
return
}
require.JSONEq(t, tt.want, string(got), "Json strings should be equal")
})
}
}

View File

@ -18,15 +18,17 @@ import (
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types"
restclient "k8s.io/client-go/rest"
mv1beta1 "k8s.io/metrics/pkg/apis/metrics/v1beta1"
)
var (
_ Accessor = (*Pod)(nil)
_ Nuker = (*Pod)(nil)
_ Loggable = (*Pod)(nil)
_ Controller = (*Pod)(nil)
_ Accessor = (*Pod)(nil)
_ Nuker = (*Pod)(nil)
_ Loggable = (*Pod)(nil)
_ Controller = (*Pod)(nil)
_ ContainsPodSpec = (*Pod)(nil)
)
const (
@ -455,3 +457,57 @@ func in(ll []string, s string) bool {
}
return false
}
func (p *Pod) GetPodSpec(path string) (*v1.PodSpec, error) {
pod, err := p.GetInstance(path)
if err != nil {
return nil, err
}
podSpec := pod.Spec
return &podSpec, nil
}
func (p *Pod) SetImages(ctx context.Context, path string, imageSpecs ImageSpecs) error {
ns, n := client.Namespaced(path)
auth, err := p.Client().CanI(ns, "v1/pod", []string{client.PatchVerb})
if err != nil {
return err
}
if !auth {
return fmt.Errorf("user is not authorized to patch a deployment")
}
if manager, isManaged, err := p.isControlled(path); isManaged {
if err != nil {
return err
}
return fmt.Errorf("Unable to set image. This pod is managed by %s. Please set the image on the controller", manager)
}
jsonPatch, err := GetJsonPatch(imageSpecs)
if err != nil {
return err
}
dial, err := p.Client().Dial()
if err != nil {
return err
}
_, err = dial.CoreV1().Pods(ns).Patch(
ctx,
n,
types.StrategicMergePatchType,
jsonPatch,
metav1.PatchOptions{},
)
return err
}
func (p *Pod) isControlled(path string) (string, bool, error) {
pod, err := p.GetInstance(path)
if err != nil {
return "", false, err
}
references := pod.GetObjectMeta().GetOwnerReferences()
if len(references) != 0 && *references[0].Controller == true {
return fmt.Sprintf("%s/%s", references[0].Kind, references[0].Name), true, nil
}
return "", false, nil
}

View File

@ -9,6 +9,7 @@ import (
"github.com/derailed/k9s/internal/client"
"github.com/rs/zerolog/log"
appsv1 "k8s.io/api/apps/v1"
v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/labels"
@ -18,12 +19,13 @@ import (
)
var (
_ Accessor = (*StatefulSet)(nil)
_ Nuker = (*StatefulSet)(nil)
_ Loggable = (*StatefulSet)(nil)
_ Restartable = (*StatefulSet)(nil)
_ Scalable = (*StatefulSet)(nil)
_ Controller = (*StatefulSet)(nil)
_ Accessor = (*StatefulSet)(nil)
_ Nuker = (*StatefulSet)(nil)
_ Loggable = (*StatefulSet)(nil)
_ Restartable = (*StatefulSet)(nil)
_ Scalable = (*StatefulSet)(nil)
_ Controller = (*StatefulSet)(nil)
_ ContainsPodSpec = (*StatefulSet)(nil)
)
// StatefulSet represents a K8s sts.
@ -219,3 +221,39 @@ func (s *StatefulSet) Scan(ctx context.Context, gvr, fqn string, wait bool) (Ref
return refs, nil
}
func (s *StatefulSet) GetPodSpec(path string) (*v1.PodSpec, error) {
sts, err := s.getStatefulSet(path)
if err != nil {
return nil, err
}
podSpec := sts.Spec.Template.Spec
return &podSpec, nil
}
func (s *StatefulSet) SetImages(ctx context.Context, path string, imageSpecs ImageSpecs) error {
ns, n := client.Namespaced(path)
auth, err := s.Client().CanI(ns, "apps/v1/statefulset", []string{client.PatchVerb})
if err != nil {
return err
}
if !auth {
return fmt.Errorf("user is not authorized to patch a statefulset")
}
jsonPatch, err := GetTemplateJsonPatch(imageSpecs)
if err != nil {
return err
}
dial, err := s.Client().Dial()
if err != nil {
return err
}
_, err = dial.AppsV1().StatefulSets(ns).Patch(
ctx,
n,
types.StrategicMergePatchType,
jsonPatch,
metav1.PatchOptions{},
)
return err
}

View File

@ -146,3 +146,11 @@ type Logger interface {
// Logs tails a resource logs.
Logs(path string, opts *v1.PodLogOptions) (*restclient.Request, error)
}
type ContainsPodSpec interface {
// Get PodSpec of a resource
GetPodSpec(path string) (*v1.PodSpec, error)
// Set Images for a resource
SetImages(ctx context.Context, path string, imageSpecs ImageSpecs) error
}

View File

@ -21,9 +21,11 @@ func NewDeploy(gvr client.GVR) ResourceViewer {
ResourceViewer: NewPortForwardExtender(
NewRestartExtender(
NewScaleExtender(
NewLogsExtender(
NewBrowser(gvr),
nil,
NewSetImageExtender(
NewLogsExtender(
NewBrowser(gvr),
nil,
),
),
),
),

View File

@ -13,5 +13,5 @@ func TestDeploy(t *testing.T) {
assert.Nil(t, v.Init(makeCtx()))
assert.Equal(t, "Deployments", v.Name())
assert.Equal(t, 12, len(v.Hints()))
assert.Equal(t, 13, len(v.Hints()))
}

View File

@ -17,7 +17,9 @@ func NewDaemonSet(gvr client.GVR) ResourceViewer {
d := DaemonSet{
ResourceViewer: NewPortForwardExtender(
NewRestartExtender(
NewLogsExtender(NewBrowser(gvr), nil),
NewSetImageExtender(
NewLogsExtender(NewBrowser(gvr), nil),
),
),
),
}

View File

@ -13,5 +13,5 @@ func TestDaemonSet(t *testing.T) {
assert.Nil(t, v.Init(makeCtx()))
assert.Equal(t, "DaemonSets", v.Name())
assert.Equal(t, 13, len(v.Hints()))
assert.Equal(t, 14, len(v.Hints()))
}

View File

@ -21,7 +21,7 @@ func TestHelp(t *testing.T) {
v := view.NewHelp()
assert.Nil(t, v.Init(ctx))
assert.Equal(t, 24, v.GetRowCount())
assert.Equal(t, 25, v.GetRowCount())
assert.Equal(t, 8, v.GetColumnCount())
assert.Equal(t, "<a>", strings.TrimSpace(v.GetCell(1, 0).Text))
assert.Equal(t, "Attach", strings.TrimSpace(v.GetCell(1, 1).Text))

View File

@ -29,7 +29,9 @@ type Pod struct {
func NewPod(gvr client.GVR) ResourceViewer {
p := Pod{}
p.ResourceViewer = NewPortForwardExtender(
NewLogsExtender(NewBrowser(gvr), p.selectedContainer),
NewSetImageExtender(
NewLogsExtender(NewBrowser(gvr), p.selectedContainer),
),
)
p.SetBindKeysFn(p.bindKeys)
p.GetTable().SetEnterFn(p.showContainers)

View File

@ -16,7 +16,7 @@ func TestPodNew(t *testing.T) {
assert.Nil(t, po.Init(makeCtx()))
assert.Equal(t, "Pods", po.Name())
assert.Equal(t, 23, len(po.Hints()))
assert.Equal(t, 24, len(po.Hints()))
}
// Helpers...

View File

@ -0,0 +1,171 @@
package view
import (
"context"
"fmt"
"github.com/derailed/k9s/internal/dao"
"github.com/derailed/k9s/internal/ui"
"github.com/derailed/tview"
"github.com/gdamore/tcell"
"github.com/rs/zerolog/log"
corev1 "k8s.io/api/core/v1"
"strings"
)
const setImageKey = "setImage"
// SetImageExtender adds set image extensions
type SetImageExtender struct {
ResourceViewer
}
type imageFormSpec struct {
name, dockerImage, newDockerImage string
init bool
}
func (m *imageFormSpec) modified() bool {
var newDockerImage = strings.TrimSpace(m.newDockerImage)
return newDockerImage != "" && m.dockerImage != newDockerImage
}
func (m *imageFormSpec) imageSpec() dao.ImageSpec {
var ret = dao.ImageSpec{
Name: m.name,
Init: m.init,
}
if m.modified() {
ret.DockerImage = strings.TrimSpace(m.newDockerImage)
} else {
ret.DockerImage = m.dockerImage
}
return ret
}
func NewSetImageExtender(r ResourceViewer) ResourceViewer {
s := SetImageExtender{ResourceViewer: r}
s.bindKeys(s.Actions())
return &s
}
func (s *SetImageExtender) bindKeys(aa ui.KeyActions) {
aa.Add(ui.KeyActions{
ui.KeyI: ui.NewKeyAction("SetImage", s.setImageCmd, true),
})
}
func (s *SetImageExtender) setImageCmd(evt *tcell.EventKey) *tcell.EventKey {
path := s.GetTable().GetSelectedItem()
if path == "" {
return nil
}
s.Stop()
defer s.Start()
s.showSetImageDialog(path)
return nil
}
func (s *SetImageExtender) showSetImageDialog(path string) {
confirm := tview.NewModalForm("<Set image>", s.makeSetImageForm(path))
confirm.SetText(fmt.Sprintf("Set image %s %s", s.GVR(), path))
confirm.SetDoneFunc(func(int, string) {
s.dismissDialog()
})
s.App().Content.AddPage(setImageKey, confirm, false, false)
s.App().Content.ShowPage(setImageKey)
}
func (s *SetImageExtender) makeSetImageForm(sel string) *tview.Form {
f := s.makeStyledForm()
podSpec, err := s.getPodSpec(sel)
if err != nil {
s.App().Flash().Err(err)
return nil
}
var formContainerLines []imageFormSpec
for _, spec := range podSpec.InitContainers {
formContainerLines = append(formContainerLines, imageFormSpec{init: true, name: spec.Name, dockerImage: spec.Image})
}
for _, spec := range podSpec.Containers {
formContainerLines = append(formContainerLines, imageFormSpec{init: false, name: spec.Name, dockerImage: spec.Image})
}
for _, ctn := range formContainerLines {
ctnCopy := ctn
f.AddInputField(ctn.name, ctn.dockerImage, 0, nil, func(changed string) {
ctnCopy.newDockerImage = changed
})
}
f.AddButton("OK", func() {
defer s.dismissDialog()
if err != nil {
s.App().Flash().Err(err)
return
}
ctx, cancel := context.WithTimeout(context.Background(), s.App().Conn().Config().CallTimeout())
defer cancel()
var imageSpecsModified dao.ImageSpecs
for _, v := range formContainerLines {
if v.modified() {
imageSpecsModified = append(imageSpecsModified, v.imageSpec())
}
}
if err := s.setImages(ctx, sel, imageSpecsModified); err != nil {
log.Error().Err(err).Msgf("PodSpec %s image update failed", sel)
s.App().Flash().Err(err)
} else {
s.App().Flash().Infof("Resource %s:%s image updated successfully", s.GVR(), sel)
}
})
f.AddButton("Cancel", func() {
s.dismissDialog()
})
return f
}
func (s *SetImageExtender) dismissDialog() {
s.App().Content.RemovePage(setImageKey)
}
func (s *SetImageExtender) makeStyledForm() *tview.Form {
f := tview.NewForm()
f.SetItemPadding(0)
f.SetButtonsAlign(tview.AlignCenter).
SetButtonBackgroundColor(tview.Styles.PrimitiveBackgroundColor).
SetButtonTextColor(tview.Styles.PrimaryTextColor).
SetLabelColor(tcell.ColorAqua).
SetFieldTextColor(tcell.ColorOrange)
return f
}
func (s *SetImageExtender) getPodSpec(path string) (*corev1.PodSpec, error) {
res, err := dao.AccessorFor(s.App().factory, s.GVR())
if err != nil {
return nil, err
}
resourceWPodSpec, ok := res.(dao.ContainsPodSpec)
if !ok {
return nil, fmt.Errorf("expecting a resourceWPodSpec resource for %q", s.GVR())
}
return resourceWPodSpec.GetPodSpec(path)
}
func (s *SetImageExtender) setImages(ctx context.Context, path string, imageSpecs dao.ImageSpecs) error {
res, err := dao.AccessorFor(s.App().factory, s.GVR())
if err != nil {
return err
}
resourceWPodSpec, ok := res.(dao.ContainsPodSpec)
if !ok {
return fmt.Errorf("expecting a scalable resource for %q", s.GVR())
}
return resourceWPodSpec.SetImages(ctx, path, imageSpecs)
}

View File

@ -21,7 +21,9 @@ func NewStatefulSet(gvr client.GVR) ResourceViewer {
ResourceViewer: NewPortForwardExtender(
NewRestartExtender(
NewScaleExtender(
NewLogsExtender(NewBrowser(gvr), nil),
NewSetImageExtender(
NewLogsExtender(NewBrowser(gvr), nil),
),
),
),
),

View File

@ -13,5 +13,5 @@ func TestStatefulSetNew(t *testing.T) {
assert.Nil(t, s.Init(makeCtx()))
assert.Equal(t, "StatefulSets", s.Name())
assert.Equal(t, 11, len(s.Hints()))
assert.Equal(t, 12, len(s.Hints()))
}