feat(podspec): add possibility to set container image in Deployment
parent
cc0e8e88d3
commit
c534fbb75e
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,9 +21,11 @@ func NewDeploy(gvr client.GVR) ResourceViewer {
|
|||
ResourceViewer: NewPortForwardExtender(
|
||||
NewRestartExtender(
|
||||
NewScaleExtender(
|
||||
NewLogsExtender(
|
||||
NewBrowser(gvr),
|
||||
nil,
|
||||
NewSetImageExtender(
|
||||
NewLogsExtender(
|
||||
NewBrowser(gvr),
|
||||
nil,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -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()))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
Loading…
Reference in New Issue