Skip to content

Commit 3c0a834

Browse files
committed
feat: add pause and resume cmds
Signed-off-by: Thorben Below <[email protected]>
1 parent 99d47ab commit 3c0a834

File tree

6 files changed

+219
-3
lines changed

6 files changed

+219
-3
lines changed

internal/dao/dp.go

+24
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,30 @@ func (d *Deployment) Restart(ctx context.Context, path string) error {
123123
return err
124124
}
125125

126+
// Pause a Deployment
127+
func (d *Deployment) Pause(ctx context.Context, path string) error {
128+
ns, n := client.Namespaced(path)
129+
dial, err := d.Client().Dial()
130+
if err != nil {
131+
return err
132+
}
133+
_, err = dial.AppsV1().Deployments(ns).Patch(ctx, n, types.MergePatchType, []byte(`{"spec": {"paused": true}}`), metav1.PatchOptions{})
134+
135+
return err
136+
}
137+
138+
// Resume a paused Deployment
139+
func (d *Deployment) Resume(ctx context.Context, path string) error {
140+
ns, n := client.Namespaced(path)
141+
dial, err := d.Client().Dial()
142+
if err != nil {
143+
return err
144+
}
145+
_, err = dial.AppsV1().Deployments(ns).Patch(ctx, n, types.MergePatchType, []byte(`{"spec": {"paused": false}}`), metav1.PatchOptions{})
146+
147+
return err
148+
}
149+
126150
// TailLogs tail logs for all pods represented by this Deployment.
127151
func (d *Deployment) TailLogs(ctx context.Context, opts *LogOptions) ([]LogChan, error) {
128152
dp, err := d.GetInstance(opts.Path)

internal/dao/types.go

+6
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,12 @@ type Scalable interface {
120120
Scale(ctx context.Context, path string, replicas int32) error
121121
}
122122

123+
// Pausable represents resources that can be paused/resumed
124+
type Pausable interface {
125+
Pause(ctx context.Context, path string) error
126+
Resume(ctx context.Context, path string) error
127+
}
128+
123129
// Controller represents a pod controller.
124130
type Controller interface {
125131
// Pod returns a pod instance matching the selector.

internal/render/dp.go

+2
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ func (Deployment) Header(ns string) model1.Header {
5050
model1.HeaderColumn{Name: "READY", Align: tview.AlignRight},
5151
model1.HeaderColumn{Name: "UP-TO-DATE", Align: tview.AlignRight},
5252
model1.HeaderColumn{Name: "AVAILABLE", Align: tview.AlignRight},
53+
model1.HeaderColumn{Name: "PAUSED"},
5354
model1.HeaderColumn{Name: "LABELS", Wide: true},
5455
model1.HeaderColumn{Name: "VALID", Wide: true},
5556
model1.HeaderColumn{Name: "AGE", Time: true},
@@ -77,6 +78,7 @@ func (d Deployment) Render(o interface{}, ns string, r *model1.Row) error {
7778
strconv.Itoa(int(dp.Status.AvailableReplicas)) + "/" + strconv.Itoa(int(dp.Status.Replicas)),
7879
strconv.Itoa(int(dp.Status.UpdatedReplicas)),
7980
strconv.Itoa(int(dp.Status.AvailableReplicas)),
81+
strconv.FormatBool(dp.Spec.Paused),
8082
mapToStr(dp.Labels),
8183
AsStatus(d.diagnose(dp.Status.Replicas, dp.Status.AvailableReplicas)),
8284
ToAge(dp.GetCreationTimestamp()),

internal/view/dp.go

+5-3
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,11 @@ func NewDeploy(gvr client.GVR) ResourceViewer {
2727
NewVulnerabilityExtender(
2828
NewRestartExtender(
2929
NewScaleExtender(
30-
NewImageExtender(
31-
NewOwnerExtender(
32-
NewLogsExtender(NewBrowser(gvr), d.logOptions),
30+
NewPauseExtender(
31+
NewImageExtender(
32+
NewOwnerExtender(
33+
NewLogsExtender(NewBrowser(gvr), d.logOptions),
34+
),
3335
),
3436
),
3537
),

internal/view/pause_extender.go

+181
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
// Copyright Authors of K9s
3+
4+
package view
5+
6+
import (
7+
"context"
8+
"fmt"
9+
10+
"github.com/derailed/k9s/internal/config"
11+
12+
"github.com/derailed/k9s/internal/dao"
13+
"github.com/derailed/k9s/internal/ui"
14+
"github.com/derailed/tcell/v2"
15+
"github.com/derailed/tview"
16+
"github.com/rs/zerolog/log"
17+
)
18+
19+
// PauseExtender adds pausing extensions.
20+
type PauseExtender struct {
21+
ResourceViewer
22+
}
23+
24+
// NewPauseExtender returns a new extender.
25+
func NewPauseExtender(r ResourceViewer) ResourceViewer {
26+
p := PauseExtender{ResourceViewer: r}
27+
p.AddBindKeysFn(p.bindKeys)
28+
29+
return &p
30+
}
31+
32+
const (
33+
PAUSE = "Pause"
34+
RESUME = "Resume"
35+
PAUSE_RESUME = "Pause/Resume"
36+
)
37+
38+
func (p *PauseExtender) bindKeys(aa *ui.KeyActions) {
39+
if p.App().Config.K9s.IsReadOnly() {
40+
return
41+
}
42+
43+
aa.Add(ui.KeyZ, ui.NewKeyActionWithOpts(PAUSE_RESUME, p.togglePauseCmd,
44+
ui.ActionOpts{
45+
Visible: true,
46+
Dangerous: true,
47+
},
48+
))
49+
}
50+
51+
func (p *PauseExtender) togglePauseCmd(evt *tcell.EventKey) *tcell.EventKey {
52+
paths := p.GetTable().GetSelectedItems()
53+
if len(paths) == 0 {
54+
return nil
55+
}
56+
57+
p.Stop()
58+
defer p.Start()
59+
60+
styles := p.App().Styles.Dialog()
61+
form := p.makeStyledForm(styles)
62+
63+
action := PAUSE
64+
if len(paths) == 1 {
65+
isPaused, err := p.valueOf("PAUSED")
66+
if err != nil {
67+
log.Error().Err(err).Msg("Reading 'PAUSED' state failed")
68+
p.App().Flash().Err(err)
69+
return nil
70+
}
71+
72+
if isPaused == "true" {
73+
action = RESUME
74+
}
75+
}
76+
77+
if len(paths) > 1 {
78+
form.AddDropDown("Action:", []string{PAUSE, RESUME}, 0, func(option string, optionIndex int) {
79+
action = option
80+
})
81+
}
82+
83+
form.AddButton("OK", func() {
84+
defer p.dismissDialog()
85+
86+
ctx, cancel := context.WithTimeout(context.Background(), p.App().Conn().Config().CallTimeout())
87+
defer cancel()
88+
89+
for _, sel := range paths {
90+
if err := p.togglePause(ctx, sel, action); err != nil {
91+
log.Error().Err(err).Msgf("DP %s pausing failed", sel)
92+
p.App().Flash().Err(err)
93+
return
94+
}
95+
}
96+
97+
if len(paths) == 1 {
98+
p.App().Flash().Infof("[%d] %s paused successfully", len(paths), singularize(p.GVR().R()))
99+
} else {
100+
p.App().Flash().Infof("%s %s paused successfully", p.GVR().R(), paths[0])
101+
}
102+
})
103+
104+
form.AddButton("Cancel", func() {
105+
p.dismissDialog()
106+
})
107+
for i := 0; i < 2; i++ {
108+
if b := form.GetButton(i); b != nil {
109+
b.SetBackgroundColorActivated(styles.ButtonFocusBgColor.Color())
110+
b.SetLabelColorActivated(styles.ButtonFocusFgColor.Color())
111+
}
112+
}
113+
114+
confirm := tview.NewModalForm("Pause/Resume", form)
115+
msg := fmt.Sprintf("%s %s %s?", action, singularize(p.GVR().R()), paths[0])
116+
if len(paths) > 1 {
117+
msg = fmt.Sprintf("Pause/Resume [%d] %s?", len(paths), p.GVR().R())
118+
}
119+
confirm.SetText(msg)
120+
confirm.SetDoneFunc(func(int, string) {
121+
p.dismissDialog()
122+
})
123+
p.App().Content.AddPage(pauseDialogKey, confirm, false, false)
124+
p.App().Content.ShowPage(pauseDialogKey)
125+
126+
return nil
127+
}
128+
129+
func (p *PauseExtender) togglePause(ctx context.Context, path string, action string) error {
130+
res, err := dao.AccessorFor(p.App().factory, p.GVR())
131+
if err != nil {
132+
p.App().Flash().Err(err)
133+
return nil
134+
}
135+
pauser, ok := res.(dao.Pausable)
136+
if !ok {
137+
p.App().Flash().Err(fmt.Errorf("expecting a pausable resource for %q", p.GVR()))
138+
return nil
139+
}
140+
141+
if action == PAUSE {
142+
err = pauser.Pause(ctx, path)
143+
} else if action == RESUME {
144+
err = pauser.Resume(ctx, path)
145+
} else {
146+
p.App().Flash().Err(fmt.Errorf("failed to identify action; must be '%s' or '%s' but is: '%s'", PAUSE, RESUME, action))
147+
return nil
148+
}
149+
150+
if err != nil {
151+
p.App().Flash().Err(fmt.Errorf("failed to %s: %q", action, err))
152+
}
153+
154+
return nil
155+
}
156+
157+
func (p *PauseExtender) valueOf(col string) (string, error) {
158+
colIdx, ok := p.GetTable().HeaderIndex(col)
159+
if !ok {
160+
return "", fmt.Errorf("no column index for %s", col)
161+
}
162+
return p.GetTable().GetSelectedCell(colIdx), nil
163+
}
164+
165+
const pauseDialogKey = "pause"
166+
167+
func (p *PauseExtender) dismissDialog() {
168+
p.App().Content.RemovePage(pauseDialogKey)
169+
}
170+
171+
func (p *PauseExtender) makeStyledForm(styles config.Dialog) *tview.Form {
172+
f := tview.NewForm()
173+
f.SetItemPadding(0)
174+
f.SetButtonsAlign(tview.AlignCenter).
175+
SetButtonBackgroundColor(styles.ButtonBgColor.Color()).
176+
SetButtonTextColor(styles.ButtonBgColor.Color()).
177+
SetLabelColor(styles.LabelFgColor.Color()).
178+
SetFieldTextColor(styles.FieldFgColor.Color())
179+
180+
return f
181+
}

internal/view/types.go

+1
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ const (
2121
uptodateCol = "UP-TO-DATE"
2222
readyCol = "READY"
2323
availCol = "AVAILABLE"
24+
pausedCol = "PAUSED"
2425
)
2526

2627
type (

0 commit comments

Comments
 (0)