Skip to content

Commit ba4e273

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

File tree

7 files changed

+192
-4
lines changed

7 files changed

+192
-4
lines changed

internal/dao/dp.go

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

126+
func (d *Deployment) TogglePause(ctx context.Context, path string) error {
127+
ns, n := client.Namespaced(path)
128+
auth, err := d.Client().CanI(ns, d.GVR(), n, []string{client.GetVerb, client.UpdateVerb})
129+
if err != nil {
130+
return err
131+
}
132+
if !auth {
133+
return fmt.Errorf("user is not authorized to (un)suspend cronjobs")
134+
}
135+
136+
dial, err := d.Client().Dial()
137+
if err != nil {
138+
return err
139+
}
140+
dp, err := dial.AppsV1().Deployments(ns).Get(ctx, n, metav1.GetOptions{})
141+
if err != nil {
142+
return err
143+
}
144+
dp.Spec.Paused = !dp.Spec.Paused
145+
_, err = dial.AppsV1().Deployments(ns).Update(ctx, dp, metav1.UpdateOptions{})
146+
147+
return err
148+
149+
}
150+
126151
// TailLogs tail logs for all pods represented by this Deployment.
127152
func (d *Deployment) TailLogs(ctx context.Context, opts *LogOptions) ([]LogChan, error) {
128153
dp, err := d.GetInstance(opts.Path)

internal/dao/types.go

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

124+
// Pausable represents resources that can be paused/resumed
125+
type Pausable interface {
126+
TogglePause(ctx context.Context, path string) error
127+
}
128+
124129
// Controller represents a pod controller.
125130
type Controller interface {
126131
// 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/dp_test.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -16,5 +16,5 @@ func TestDeploy(t *testing.T) {
1616

1717
assert.Nil(t, v.Init(makeCtx()))
1818
assert.Equal(t, "Deployments", v.Name())
19-
assert.Equal(t, 16, len(v.Hints()))
19+
assert.Equal(t, 17, len(v.Hints()))
2020
}

internal/view/pause_extender.go

+153
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
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+
path := p.GetTable().GetSelectedItem()
53+
54+
p.Stop()
55+
defer p.Start()
56+
57+
styles := p.App().Styles.Dialog()
58+
form := p.makeStyledForm(styles)
59+
60+
action := PAUSE
61+
isPaused, err := p.valueOf("PAUSED")
62+
if err != nil {
63+
log.Error().Err(err).Msg("Reading 'PAUSED' state failed")
64+
p.App().Flash().Err(err)
65+
return nil
66+
}
67+
68+
if isPaused == "true" {
69+
action = RESUME
70+
}
71+
72+
form.AddButton("OK", func() {
73+
defer p.dismissDialog()
74+
75+
ctx, cancel := context.WithTimeout(context.Background(), p.App().Conn().Config().CallTimeout())
76+
defer cancel()
77+
78+
if err := p.togglePause(ctx, path, action); err != nil {
79+
log.Error().Err(err).Msgf("DP %s pausing failed", path)
80+
p.App().Flash().Err(err)
81+
return
82+
}
83+
84+
p.App().Flash().Infof("%s paused successfully", singularize(p.GVR().R()))
85+
})
86+
87+
form.AddButton("Cancel", func() {
88+
p.dismissDialog()
89+
})
90+
for i := 0; i < 2; i++ {
91+
if b := form.GetButton(i); b != nil {
92+
b.SetBackgroundColorActivated(styles.ButtonFocusBgColor.Color())
93+
b.SetLabelColorActivated(styles.ButtonFocusFgColor.Color())
94+
}
95+
}
96+
97+
confirm := tview.NewModalForm("Pause/Resume", form)
98+
msg := fmt.Sprintf("%s %s %s?", action, singularize(p.GVR().R()), path)
99+
100+
confirm.SetText(msg)
101+
confirm.SetDoneFunc(func(int, string) {
102+
p.dismissDialog()
103+
})
104+
p.App().Content.AddPage(pauseDialogKey, confirm, false, false)
105+
p.App().Content.ShowPage(pauseDialogKey)
106+
107+
return nil
108+
}
109+
110+
func (p *PauseExtender) togglePause(ctx context.Context, path string, action string) error {
111+
res, err := dao.AccessorFor(p.App().factory, p.GVR())
112+
if err != nil {
113+
p.App().Flash().Err(err)
114+
return nil
115+
}
116+
pauser, ok := res.(dao.Pausable)
117+
if !ok {
118+
p.App().Flash().Err(fmt.Errorf("expecting a pausable resource for %q", p.GVR()))
119+
return nil
120+
}
121+
122+
if err := pauser.TogglePause(ctx, path); err != nil {
123+
p.App().Flash().Err(fmt.Errorf("failed to %s: %q", action, err))
124+
}
125+
126+
return nil
127+
}
128+
129+
func (p *PauseExtender) valueOf(col string) (string, error) {
130+
colIdx, ok := p.GetTable().HeaderIndex(col)
131+
if !ok {
132+
return "", fmt.Errorf("no column index for %s", col)
133+
}
134+
return p.GetTable().GetSelectedCell(colIdx), nil
135+
}
136+
137+
const pauseDialogKey = "pause"
138+
139+
func (p *PauseExtender) dismissDialog() {
140+
p.App().Content.RemovePage(pauseDialogKey)
141+
}
142+
143+
func (p *PauseExtender) makeStyledForm(styles config.Dialog) *tview.Form {
144+
f := tview.NewForm()
145+
f.SetItemPadding(0)
146+
f.SetButtonsAlign(tview.AlignCenter).
147+
SetButtonBackgroundColor(styles.ButtonBgColor.Color()).
148+
SetButtonTextColor(styles.ButtonBgColor.Color()).
149+
SetLabelColor(styles.LabelFgColor.Color()).
150+
SetFieldTextColor(styles.FieldFgColor.Color())
151+
152+
return f
153+
}

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)