Skip to content

Commit

Permalink
feat: add pause and resume cmds
Browse files Browse the repository at this point in the history
Signed-off-by: Thorben Below <[email protected]>
  • Loading branch information
thorbenbelow committed Nov 10, 2024
1 parent 99d47ab commit 3c0a834
Show file tree
Hide file tree
Showing 6 changed files with 219 additions and 3 deletions.
24 changes: 24 additions & 0 deletions internal/dao/dp.go
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,30 @@ func (d *Deployment) Restart(ctx context.Context, path string) error {
return err
}

// Pause a Deployment
func (d *Deployment) Pause(ctx context.Context, path string) error {
ns, n := client.Namespaced(path)
dial, err := d.Client().Dial()
if err != nil {
return err
}
_, err = dial.AppsV1().Deployments(ns).Patch(ctx, n, types.MergePatchType, []byte(`{"spec": {"paused": true}}`), metav1.PatchOptions{})

return err
}

// Resume a paused Deployment
func (d *Deployment) Resume(ctx context.Context, path string) error {
ns, n := client.Namespaced(path)
dial, err := d.Client().Dial()
if err != nil {
return err
}
_, err = dial.AppsV1().Deployments(ns).Patch(ctx, n, types.MergePatchType, []byte(`{"spec": {"paused": false}}`), metav1.PatchOptions{})

return err
}

// TailLogs tail logs for all pods represented by this Deployment.
func (d *Deployment) TailLogs(ctx context.Context, opts *LogOptions) ([]LogChan, error) {
dp, err := d.GetInstance(opts.Path)
Expand Down
6 changes: 6 additions & 0 deletions internal/dao/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,12 @@ type Scalable interface {
Scale(ctx context.Context, path string, replicas int32) error
}

// Pausable represents resources that can be paused/resumed
type Pausable interface {
Pause(ctx context.Context, path string) error
Resume(ctx context.Context, path string) error
}

// Controller represents a pod controller.
type Controller interface {
// Pod returns a pod instance matching the selector.
Expand Down
2 changes: 2 additions & 0 deletions internal/render/dp.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ func (Deployment) Header(ns string) model1.Header {
model1.HeaderColumn{Name: "READY", Align: tview.AlignRight},
model1.HeaderColumn{Name: "UP-TO-DATE", Align: tview.AlignRight},
model1.HeaderColumn{Name: "AVAILABLE", Align: tview.AlignRight},
model1.HeaderColumn{Name: "PAUSED"},
model1.HeaderColumn{Name: "LABELS", Wide: true},
model1.HeaderColumn{Name: "VALID", Wide: true},
model1.HeaderColumn{Name: "AGE", Time: true},
Expand Down Expand Up @@ -77,6 +78,7 @@ func (d Deployment) Render(o interface{}, ns string, r *model1.Row) error {
strconv.Itoa(int(dp.Status.AvailableReplicas)) + "/" + strconv.Itoa(int(dp.Status.Replicas)),
strconv.Itoa(int(dp.Status.UpdatedReplicas)),
strconv.Itoa(int(dp.Status.AvailableReplicas)),
strconv.FormatBool(dp.Spec.Paused),
mapToStr(dp.Labels),
AsStatus(d.diagnose(dp.Status.Replicas, dp.Status.AvailableReplicas)),
ToAge(dp.GetCreationTimestamp()),
Expand Down
8 changes: 5 additions & 3 deletions internal/view/dp.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,11 @@ func NewDeploy(gvr client.GVR) ResourceViewer {
NewVulnerabilityExtender(
NewRestartExtender(
NewScaleExtender(
NewImageExtender(
NewOwnerExtender(
NewLogsExtender(NewBrowser(gvr), d.logOptions),
NewPauseExtender(
NewImageExtender(
NewOwnerExtender(
NewLogsExtender(NewBrowser(gvr), d.logOptions),
),
),
),
),
Expand Down
181 changes: 181 additions & 0 deletions internal/view/pause_extender.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright Authors of K9s

package view

import (
"context"
"fmt"

"github.com/derailed/k9s/internal/config"

"github.com/derailed/k9s/internal/dao"
"github.com/derailed/k9s/internal/ui"
"github.com/derailed/tcell/v2"
"github.com/derailed/tview"
"github.com/rs/zerolog/log"
)

// PauseExtender adds pausing extensions.
type PauseExtender struct {
ResourceViewer
}

// NewPauseExtender returns a new extender.
func NewPauseExtender(r ResourceViewer) ResourceViewer {
p := PauseExtender{ResourceViewer: r}
p.AddBindKeysFn(p.bindKeys)

return &p
}

const (
PAUSE = "Pause"
RESUME = "Resume"
PAUSE_RESUME = "Pause/Resume"
)

func (p *PauseExtender) bindKeys(aa *ui.KeyActions) {
if p.App().Config.K9s.IsReadOnly() {
return
}

aa.Add(ui.KeyZ, ui.NewKeyActionWithOpts(PAUSE_RESUME, p.togglePauseCmd,
ui.ActionOpts{
Visible: true,
Dangerous: true,
},
))
}

func (p *PauseExtender) togglePauseCmd(evt *tcell.EventKey) *tcell.EventKey {
paths := p.GetTable().GetSelectedItems()
if len(paths) == 0 {
return nil
}

p.Stop()
defer p.Start()

styles := p.App().Styles.Dialog()
form := p.makeStyledForm(styles)

action := PAUSE
if len(paths) == 1 {
isPaused, err := p.valueOf("PAUSED")
if err != nil {
log.Error().Err(err).Msg("Reading 'PAUSED' state failed")
p.App().Flash().Err(err)
return nil
}

if isPaused == "true" {
action = RESUME
}
}

if len(paths) > 1 {
form.AddDropDown("Action:", []string{PAUSE, RESUME}, 0, func(option string, optionIndex int) {
action = option
})
}

form.AddButton("OK", func() {
defer p.dismissDialog()

ctx, cancel := context.WithTimeout(context.Background(), p.App().Conn().Config().CallTimeout())
defer cancel()

for _, sel := range paths {
if err := p.togglePause(ctx, sel, action); err != nil {
log.Error().Err(err).Msgf("DP %s pausing failed", sel)
p.App().Flash().Err(err)
return
}
}

if len(paths) == 1 {
p.App().Flash().Infof("[%d] %s paused successfully", len(paths), singularize(p.GVR().R()))
} else {
p.App().Flash().Infof("%s %s paused successfully", p.GVR().R(), paths[0])
}
})

form.AddButton("Cancel", func() {
p.dismissDialog()
})
for i := 0; i < 2; i++ {
if b := form.GetButton(i); b != nil {
b.SetBackgroundColorActivated(styles.ButtonFocusBgColor.Color())
b.SetLabelColorActivated(styles.ButtonFocusFgColor.Color())
}
}

confirm := tview.NewModalForm("Pause/Resume", form)
msg := fmt.Sprintf("%s %s %s?", action, singularize(p.GVR().R()), paths[0])
if len(paths) > 1 {
msg = fmt.Sprintf("Pause/Resume [%d] %s?", len(paths), p.GVR().R())
}
confirm.SetText(msg)
confirm.SetDoneFunc(func(int, string) {
p.dismissDialog()
})
p.App().Content.AddPage(pauseDialogKey, confirm, false, false)
p.App().Content.ShowPage(pauseDialogKey)

return nil
}

func (p *PauseExtender) togglePause(ctx context.Context, path string, action string) error {
res, err := dao.AccessorFor(p.App().factory, p.GVR())
if err != nil {
p.App().Flash().Err(err)
return nil
}
pauser, ok := res.(dao.Pausable)
if !ok {
p.App().Flash().Err(fmt.Errorf("expecting a pausable resource for %q", p.GVR()))
return nil
}

if action == PAUSE {
err = pauser.Pause(ctx, path)
} else if action == RESUME {
err = pauser.Resume(ctx, path)
} else {
p.App().Flash().Err(fmt.Errorf("failed to identify action; must be '%s' or '%s' but is: '%s'", PAUSE, RESUME, action))
return nil
}

if err != nil {
p.App().Flash().Err(fmt.Errorf("failed to %s: %q", action, err))
}

return nil
}

func (p *PauseExtender) valueOf(col string) (string, error) {
colIdx, ok := p.GetTable().HeaderIndex(col)
if !ok {
return "", fmt.Errorf("no column index for %s", col)
}
return p.GetTable().GetSelectedCell(colIdx), nil
}

const pauseDialogKey = "pause"

func (p *PauseExtender) dismissDialog() {
p.App().Content.RemovePage(pauseDialogKey)
}

func (p *PauseExtender) makeStyledForm(styles config.Dialog) *tview.Form {
f := tview.NewForm()
f.SetItemPadding(0)
f.SetButtonsAlign(tview.AlignCenter).
SetButtonBackgroundColor(styles.ButtonBgColor.Color()).
SetButtonTextColor(styles.ButtonBgColor.Color()).
SetLabelColor(styles.LabelFgColor.Color()).
SetFieldTextColor(styles.FieldFgColor.Color())

return f
}
1 change: 1 addition & 0 deletions internal/view/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ const (
uptodateCol = "UP-TO-DATE"
readyCol = "READY"
availCol = "AVAILABLE"
pausedCol = "PAUSED"
)

type (
Expand Down

0 comments on commit 3c0a834

Please sign in to comment.