feat(podspec): add possibility to set container image in Deployment

mine
Antoine Meausoone 2020-08-11 12:21:42 +02:00
parent cc0e8e88d3
commit c534fbb75e
5 changed files with 200 additions and 10 deletions

View File

@ -4,6 +4,7 @@ import (
"context"
"errors"
"fmt"
"strings"
"github.com/derailed/k9s/internal/client"
"github.com/rs/zerolog/log"
@ -18,12 +19,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 +213,48 @@ 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, images map[string]string) 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")
}
var nameStrB strings.Builder
var imageStrB strings.Builder
for name, image := range images {
nameStrB.WriteString(fmt.Sprintf(`{"name":"%s"},`, name))
imageStrB.WriteString(fmt.Sprintf(`{"image":"%s","name":"%s"},`, image, name))
}
namesJson := strings.TrimSuffix(nameStrB.String(), ",")
imagesJson := strings.TrimSuffix(imageStrB.String(), ",")
patchJson := `{"spec":{"template":{"spec":{"$setElementOrder/containers":[` + namesJson + `],"containers":[` + imagesJson + `]}}}}`
dial, err := d.Client().Dial()
if err != nil {
return err
}
_, err = dial.AppsV1().Deployments(ns).Patch(
ctx,
n,
types.StrategicMergePatchType,
[]byte(patchJson),
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

@ -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, images map[string]string) 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

@ -0,0 +1,136 @@
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"
)
// SetImageExtender adds set image extensions
type SetImageExtender struct {
ResourceViewer
}
const setImageKey = "setImage"
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()
containers, err := s.getImages(sel)
if err != nil {
s.App().Flash().Err(err)
return nil
}
images := make(map[string]string)
for _, container := range *containers {
f.AddInputField(container.Name, container.Image, 0, nil, func(changed string) {
images[container.Name] = 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()
if err := s.setImages(ctx, sel, images); err != nil {
log.Error().Err(err).Msgf("DP %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) getImages(path string) (*[]corev1.Container, error) {
res, err := dao.AccessorFor(s.App().factory, s.GVR())
if err != nil {
return nil, err
}
podSpecable, ok := res.(dao.ContainsPodSpec)
if !ok {
return nil, fmt.Errorf("expecting a podSpecable resource for %q", s.GVR())
}
podSpec, err := podSpecable.GetPodSpec(path)
containers := podSpec.Containers
return &containers, nil
}
func (s *SetImageExtender) setImages(ctx context.Context, path string, images map[string]string) error {
res, err := dao.AccessorFor(s.App().factory, s.GVR())
if err != nil {
return err
}
deployment, ok := res.(dao.ContainsPodSpec)
if !ok {
return fmt.Errorf("expecting a scalable resource for %q", s.GVR())
}
return deployment.SetImages(ctx, path, images)
}