node view tlc. add cordon/uncordon/drain options
parent
98fea647b9
commit
bdd4ecff20
20
cmd/root.go
20
cmd/root.go
|
|
@ -28,6 +28,7 @@ var (
|
|||
version, commit, date = "dev", "dev", "n/a"
|
||||
k9sFlags *config.Flags
|
||||
k8sFlags *genericclioptions.ConfigFlags
|
||||
demoMode = new(bool)
|
||||
|
||||
rootCmd = &cobra.Command{
|
||||
Use: appName,
|
||||
|
|
@ -38,8 +39,8 @@ var (
|
|||
)
|
||||
|
||||
func init() {
|
||||
const falseFlag = "false"
|
||||
rootCmd.AddCommand(versionCmd(), infoCmd())
|
||||
initTransientFlags()
|
||||
initK9sFlags()
|
||||
initK8sFlags()
|
||||
|
||||
|
|
@ -51,10 +52,10 @@ func init() {
|
|||
if err := flag.Set("stderrthreshold", "fatal"); err != nil {
|
||||
log.Error().Err(err)
|
||||
}
|
||||
if err := flag.Set("alsologtostderr", falseFlag); err != nil {
|
||||
if err := flag.Set("alsologtostderr", "false"); err != nil {
|
||||
log.Error().Err(err)
|
||||
}
|
||||
if err := flag.Set("logtostderr", falseFlag); err != nil {
|
||||
if err := flag.Set("logtostderr", "false"); err != nil {
|
||||
log.Error().Err(err)
|
||||
}
|
||||
}
|
||||
|
|
@ -105,6 +106,10 @@ func loadConfiguration() *config.Config {
|
|||
log.Warn().Msg("Unable to locate K9s config. Generating new configuration...")
|
||||
}
|
||||
|
||||
log.Debug().Msgf("DEMO MODE %#v", demoMode)
|
||||
if demoMode != nil {
|
||||
k9sCfg.SetDemoMode(*demoMode)
|
||||
}
|
||||
if *k9sFlags.RefreshRate != config.DefaultRefreshRate {
|
||||
k9sCfg.K9s.OverrideRefreshRate(*k9sFlags.RefreshRate)
|
||||
}
|
||||
|
|
@ -161,6 +166,15 @@ func parseLevel(level string) zerolog.Level {
|
|||
}
|
||||
}
|
||||
|
||||
func initTransientFlags() {
|
||||
rootCmd.Flags().BoolVar(
|
||||
demoMode,
|
||||
"demo",
|
||||
false,
|
||||
"Enable demo mode to show keyboard commands",
|
||||
)
|
||||
}
|
||||
|
||||
func initK9sFlags() {
|
||||
k9sFlags = config.NewFlags()
|
||||
rootCmd.Flags().IntVarP(
|
||||
|
|
|
|||
|
|
@ -79,7 +79,16 @@ func (g GVR) GV() schema.GroupVersion {
|
|||
}
|
||||
}
|
||||
|
||||
// GVR returns a a full schema representation.
|
||||
// GVK returns a full schema representation.
|
||||
func (g GVR) GVK() schema.GroupVersionKind {
|
||||
return schema.GroupVersionKind{
|
||||
Group: g.G(),
|
||||
Version: g.V(),
|
||||
Kind: g.R(),
|
||||
}
|
||||
}
|
||||
|
||||
// GVR returns a full schema representation.
|
||||
func (g GVR) GVR() schema.GroupVersionResource {
|
||||
return schema.GroupVersionResource{
|
||||
Group: g.G(),
|
||||
|
|
@ -88,7 +97,7 @@ func (g GVR) GVR() schema.GroupVersionResource {
|
|||
}
|
||||
}
|
||||
|
||||
// GR returns a a full schema representation.
|
||||
// GR returns a full schema representation.
|
||||
func (g GVR) GR() *schema.GroupResource {
|
||||
return &schema.GroupResource{
|
||||
Group: g.G(),
|
||||
|
|
|
|||
|
|
@ -206,15 +206,12 @@ func (m *MetricsServer) FetchPodMetrics(fqn string) (*mv1beta1.PodMetrics, error
|
|||
return mx, err
|
||||
}
|
||||
|
||||
var key = FQN(ns, "pods")
|
||||
if entry, ok := m.cache.Get(key); ok {
|
||||
if list, ok := entry.(*mv1beta1.PodMetricsList); ok && list != nil {
|
||||
for _, m := range list.Items {
|
||||
if FQN(m.Namespace, m.Name) == fqn {
|
||||
return &m, nil
|
||||
}
|
||||
}
|
||||
if entry, ok := m.cache.Get(fqn); ok {
|
||||
pmx, ok := entry.(*mv1beta1.PodMetrics)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("expecting podmetrics but got %T", entry)
|
||||
}
|
||||
return pmx, nil
|
||||
}
|
||||
|
||||
client, err := m.MXDial()
|
||||
|
|
@ -225,7 +222,7 @@ func (m *MetricsServer) FetchPodMetrics(fqn string) (*mv1beta1.PodMetrics, error
|
|||
if err != nil {
|
||||
return mx, err
|
||||
}
|
||||
m.cache.Add(key, mx, mxCacheExpiry)
|
||||
m.cache.Add(fqn, mx, mxCacheExpiry)
|
||||
|
||||
return mx, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -49,6 +49,7 @@ type (
|
|||
K9s *K9s `yaml:"k9s"`
|
||||
client client.Connection
|
||||
settings KubeSettings
|
||||
demoMode bool
|
||||
}
|
||||
)
|
||||
|
||||
|
|
@ -57,6 +58,16 @@ func NewConfig(ks KubeSettings) *Config {
|
|||
return &Config{K9s: NewK9s(), settings: ks}
|
||||
}
|
||||
|
||||
// DemoMode returns true if demo mode is active, false otherwise.
|
||||
func (c *Config) DemoMode() bool {
|
||||
return c.demoMode
|
||||
}
|
||||
|
||||
// SetDemoMode sets the demo mode.
|
||||
func (c *Config) SetDemoMode(b bool) {
|
||||
c.demoMode = b
|
||||
}
|
||||
|
||||
// Refine the configuration based on cli args.
|
||||
func (c *Config) Refine(flags *genericclioptions.ConfigFlags) error {
|
||||
cfg, err := flags.ToRawKubeConfigLoader().RawConfig()
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package dao
|
|||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"github.com/derailed/k9s/internal"
|
||||
"github.com/derailed/k9s/internal/client"
|
||||
|
|
@ -12,11 +13,15 @@ import (
|
|||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
"k8s.io/kubectl/pkg/drain"
|
||||
"k8s.io/kubectl/pkg/scheme"
|
||||
mv1beta1 "k8s.io/metrics/pkg/apis/metrics/v1beta1"
|
||||
)
|
||||
|
||||
var (
|
||||
_ Accessor = (*Node)(nil)
|
||||
_ NodeMaintainer = (*Node)(nil)
|
||||
)
|
||||
|
||||
// NodeMetricsFunc retrieves node metrics.
|
||||
|
|
@ -27,6 +32,75 @@ type Node struct {
|
|||
Resource
|
||||
}
|
||||
|
||||
// ToggleCordon toggles cordon/uncordon a node.
|
||||
func (n *Node) ToggleCordon(path string, cordon bool) error {
|
||||
o, err := n.Get(context.Background(), path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
h, err := drain.NewCordonHelperFromRuntimeObject(o, scheme.Scheme, n.gvr.GVK())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !h.UpdateIfRequired(cordon) {
|
||||
if cordon {
|
||||
return fmt.Errorf("node is already cordoned")
|
||||
}
|
||||
return fmt.Errorf("node is already uncordoned")
|
||||
}
|
||||
err, patchErr := h.PatchOrReplace(n.Factory.Client().DialOrDie())
|
||||
if patchErr != nil {
|
||||
return patchErr
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (o DrainOptions) toDrainHelper(k kubernetes.Interface, w io.Writer) drain.Helper {
|
||||
return drain.Helper{
|
||||
Client: k,
|
||||
GracePeriodSeconds: o.GracePeriodSeconds,
|
||||
Timeout: o.Timeout,
|
||||
DeleteLocalData: o.DeleteLocalData,
|
||||
IgnoreAllDaemonSets: o.IgnoreAllDaemonSets,
|
||||
Out: w,
|
||||
ErrOut: w,
|
||||
}
|
||||
}
|
||||
|
||||
// Drain drains a node.
|
||||
func (n *Node) Drain(path string, opts DrainOptions, w io.Writer) error {
|
||||
_ = n.ToggleCordon(path, true)
|
||||
|
||||
h := opts.toDrainHelper(n.Factory.Client().DialOrDie(), w)
|
||||
dd, errs := h.GetPodsForDeletion(path)
|
||||
if len(errs) != 0 {
|
||||
for _, e := range errs {
|
||||
if _, err := h.ErrOut.Write([]byte(e.Error() + "\n")); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return errs[0]
|
||||
}
|
||||
|
||||
if err := h.DeleteOrEvictPods(dd.Pods()); err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Fprintf(h.Out, "Node %s drained!", path)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Get returns a node resource.
|
||||
func (n *Node) Get(_ context.Context, path string) (runtime.Object, error) {
|
||||
return FetchNode(n.Factory, path)
|
||||
}
|
||||
|
||||
// List returns a collection of node resources.
|
||||
func (n *Node) List(ctx context.Context, ns string) ([]runtime.Object, error) {
|
||||
labels, ok := ctx.Value(internal.KeyLabels).(string)
|
||||
|
|
@ -66,15 +140,27 @@ func (n *Node) List(ctx context.Context, ns string) ([]runtime.Object, error) {
|
|||
// ----------------------------------------------------------------------------
|
||||
// Helpers...
|
||||
|
||||
// FetchNodes retrieves all nodes.
|
||||
func FetchNodes(f Factory, labelsSel string) (*v1.NodeList, error) {
|
||||
var list v1.NodeList
|
||||
auth, err := f.Client().CanI("", "v1/nodes", []string{client.ListVerb})
|
||||
// FetchNode retrieves a node.
|
||||
func FetchNode(f Factory, path string) (*v1.Node, error) {
|
||||
auth, err := f.Client().CanI("", "v1/nodes", []string{"get"})
|
||||
if err != nil {
|
||||
return &list, err
|
||||
return nil, err
|
||||
}
|
||||
if !auth {
|
||||
return &list, fmt.Errorf("user is not authorized to list nodes")
|
||||
return nil, fmt.Errorf("user is not authorized to list nodes")
|
||||
}
|
||||
|
||||
return f.Client().DialOrDie().CoreV1().Nodes().Get(path, metav1.GetOptions{})
|
||||
}
|
||||
|
||||
// FetchNodes retrieves all nodes.
|
||||
func FetchNodes(f Factory, labelsSel string) (*v1.NodeList, error) {
|
||||
auth, err := f.Client().CanI("", "v1/nodes", []string{client.ListVerb})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !auth {
|
||||
return nil, fmt.Errorf("user is not authorized to list nodes")
|
||||
}
|
||||
|
||||
return f.Client().DialOrDie().CoreV1().Nodes().List(metav1.ListOptions{
|
||||
|
|
|
|||
|
|
@ -227,7 +227,7 @@ func tailLogs(ctx context.Context, logger Logger, c chan<- []byte, opts LogOptio
|
|||
Container: opts.Container,
|
||||
Follow: true,
|
||||
TailLines: &opts.Lines,
|
||||
Timestamps: opts.ShowTimestamp,
|
||||
Timestamps: false,
|
||||
Previous: opts.Previous,
|
||||
}
|
||||
req, err := logger.Logs(opts.Path, &o)
|
||||
|
|
|
|||
|
|
@ -40,6 +40,7 @@ func AccessorFor(f Factory, gvr client.GVR) (Accessor, error) {
|
|||
client.NewGVR("portforwards"): &PortForward{},
|
||||
client.NewGVR("v1/services"): &Service{},
|
||||
client.NewGVR("v1/pods"): &Pod{},
|
||||
client.NewGVR("v1/nodes"): &Node{},
|
||||
client.NewGVR("apps/v1/deployments"): &Deployment{},
|
||||
client.NewGVR("apps/v1/daemonsets"): &DaemonSet{},
|
||||
client.NewGVR("extensions/v1beta1/daemonsets"): &DaemonSet{},
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@ package dao
|
|||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"time"
|
||||
|
||||
"github.com/derailed/k9s/internal/client"
|
||||
"github.com/derailed/k9s/internal/watch"
|
||||
|
|
@ -70,6 +72,24 @@ type Accessor interface {
|
|||
GVR() string
|
||||
}
|
||||
|
||||
// DrainOptions tracks drain attributes.
|
||||
type DrainOptions struct {
|
||||
GracePeriodSeconds int
|
||||
Timeout time.Duration
|
||||
IgnoreAllDaemonSets bool
|
||||
DeleteLocalData bool
|
||||
Force bool
|
||||
}
|
||||
|
||||
// NodeMaintainer performs node maintenance operations.
|
||||
type NodeMaintainer interface {
|
||||
// ToggleCordon toggles cordon/uncordon a node.
|
||||
ToggleCordon(path string, cordon bool) error
|
||||
|
||||
// Drain drains the given node.
|
||||
Drain(path string, opts DrainOptions, w io.Writer) error
|
||||
}
|
||||
|
||||
// Loggable represents resources with logs.
|
||||
type Loggable interface {
|
||||
// TaiLogs streams resource logs.
|
||||
|
|
|
|||
|
|
@ -275,7 +275,6 @@ func applyFilter(q string, lines []string) ([]string, error) {
|
|||
}
|
||||
|
||||
func (l *Log) fireLogBuffChanged(lines []string) {
|
||||
log.Debug().Msgf("FIRE-BUFF-CHNGED")
|
||||
filtered, err := applyFilter(l.filter, lines)
|
||||
if err != nil {
|
||||
l.fireLogError(err)
|
||||
|
|
@ -307,7 +306,7 @@ func (l *Log) fireLogCleared() {
|
|||
// ----------------------------------------------------------------------------
|
||||
// Helpers...
|
||||
|
||||
// BOZO!! Log timestamps.
|
||||
// BOZO!!
|
||||
// func showTimes(lines []string, show bool) []string {
|
||||
// filtered := make([]string, 0, len(lines))
|
||||
// for _, l := range lines {
|
||||
|
|
|
|||
|
|
@ -224,6 +224,10 @@ func (t *Table) list(ctx context.Context, a dao.Accessor) ([]runtime.Object, err
|
|||
}
|
||||
|
||||
func (t *Table) reconcile(ctx context.Context) error {
|
||||
defer func(ti time.Time) {
|
||||
log.Debug().Msgf("Elapsed %v %v", t.gvr, time.Since(ti))
|
||||
}(time.Now())
|
||||
|
||||
meta := t.resourceMeta()
|
||||
var (
|
||||
oo []runtime.Object
|
||||
|
|
|
|||
|
|
@ -107,13 +107,25 @@ func (Node) diagnose(ss []string) error {
|
|||
if len(ss) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
var ready bool
|
||||
for _, s := range ss {
|
||||
if s == "" {
|
||||
continue
|
||||
}
|
||||
if s == "SchedulingDisabled" {
|
||||
return errors.New("node is cordoned")
|
||||
}
|
||||
if s == "Ready" {
|
||||
return nil
|
||||
ready = true
|
||||
}
|
||||
}
|
||||
|
||||
if !ready {
|
||||
return errors.New("node is not ready")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ func ShowConfirm(pages *ui.Pages, title, msg string, ack confirmFunc, cancel can
|
|||
cancel()
|
||||
})
|
||||
|
||||
modal := tview.NewModalForm(" <"+title+"> ", f)
|
||||
modal := tview.NewModalForm("<"+title+">", f)
|
||||
modal.SetText(msg)
|
||||
modal.SetDoneFunc(func(int, string) {
|
||||
dismissConfirm(pages)
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ func NewFlash(app *App) *Flash {
|
|||
TextView: tview.NewTextView(),
|
||||
}
|
||||
f.SetTextColor(tcell.ColorAqua)
|
||||
f.SetDynamicColors(true)
|
||||
f.SetTextAlign(tview.AlignCenter)
|
||||
f.SetBorderPadding(0, 0, 1, 1)
|
||||
f.app.Styles.AddListener(&f)
|
||||
|
|
|
|||
|
|
@ -104,9 +104,9 @@ func (a *App) Init(version string, rate int) error {
|
|||
|
||||
main := tview.NewFlex().SetDirection(tview.FlexRow)
|
||||
main.AddItem(a.statusIndicator(), 1, 1, false)
|
||||
main.AddItem(flash, 1, 1, false)
|
||||
main.AddItem(a.Content, 0, 10, true)
|
||||
main.AddItem(a.Crumbs(), 1, 1, false)
|
||||
main.AddItem(flash, 1, 1, false)
|
||||
|
||||
a.Main.AddPage("main", main, true, false)
|
||||
a.Main.AddPage("splash", ui.NewSplash(a.Styles, version), true, true)
|
||||
|
|
@ -116,6 +116,7 @@ func (a *App) Init(version string, rate int) error {
|
|||
}
|
||||
|
||||
func (a *App) keyboard(evt *tcell.EventKey) *tcell.EventKey {
|
||||
displayKey(a, a.InCmdMode(), evt)
|
||||
key := evt.Key()
|
||||
if key == tcell.KeyRune {
|
||||
if a.CmdBuff().IsActive() && evt.Modifiers() == tcell.ModNone {
|
||||
|
|
@ -154,7 +155,7 @@ func (a *App) toggleHeader(flag bool) {
|
|||
}
|
||||
if a.showHeader {
|
||||
flex.RemoveItemAtIndex(0)
|
||||
flex.AddItemAtIndex(0, a.buildHeader(), 8, 1, false)
|
||||
flex.AddItemAtIndex(0, a.buildHeader(), 7, 1, false)
|
||||
} else {
|
||||
flex.RemoveItemAtIndex(0)
|
||||
flex.AddItemAtIndex(0, a.statusIndicator(), 1, 1, false)
|
||||
|
|
|
|||
|
|
@ -192,7 +192,6 @@ func (b *Browser) resetCmd(evt *tcell.EventKey) *tcell.EventKey {
|
|||
}
|
||||
|
||||
cmd := b.SearchBuff().String()
|
||||
b.App().Flash().Info("Clearing filter...")
|
||||
b.SearchBuff().Reset()
|
||||
|
||||
if ui.IsLabelSelector(cmd) {
|
||||
|
|
|
|||
|
|
@ -128,6 +128,7 @@ func (d *Details) bindKeys() {
|
|||
}
|
||||
|
||||
func (d *Details) keyboard(evt *tcell.EventKey) *tcell.EventKey {
|
||||
displayKey(d.app, d.cmdBuff.InCmdMode(), evt)
|
||||
key := evt.Key()
|
||||
if key == tcell.KeyUp || key == tcell.KeyDown {
|
||||
return evt
|
||||
|
|
@ -248,7 +249,6 @@ func (d *Details) activateCmd(evt *tcell.EventKey) *tcell.EventKey {
|
|||
if d.app.InCmdMode() {
|
||||
return evt
|
||||
}
|
||||
d.app.Flash().Info("Filter mode activated.")
|
||||
d.cmdBuff.SetActive(true)
|
||||
|
||||
return nil
|
||||
|
|
@ -281,7 +281,6 @@ func (d *Details) resetCmd(evt *tcell.EventKey) *tcell.EventKey {
|
|||
if d.cmdBuff.String() != "" {
|
||||
d.model.ClearFilter()
|
||||
}
|
||||
d.app.Flash().Info("Clearing filter...")
|
||||
d.cmdBuff.SetActive(false)
|
||||
d.cmdBuff.Reset()
|
||||
d.updateTitle()
|
||||
|
|
|
|||
|
|
@ -0,0 +1,103 @@
|
|||
package view
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/derailed/k9s/internal/dao"
|
||||
"github.com/derailed/k9s/internal/ui"
|
||||
"github.com/derailed/tview"
|
||||
)
|
||||
|
||||
const drainKey = "drain"
|
||||
|
||||
// DrainFunc represents a drain callback function.
|
||||
type DrainFunc func(v ResourceViewer, path string, opts dao.DrainOptions)
|
||||
|
||||
// ShowDrain pops a node drain dialog.
|
||||
func ShowDrain(view ResourceViewer, path string, defaults dao.DrainOptions, okFn DrainFunc) {
|
||||
styles := view.App().Styles
|
||||
|
||||
f := tview.NewForm()
|
||||
f.SetItemPadding(0)
|
||||
f.SetButtonsAlign(tview.AlignCenter).
|
||||
SetButtonBackgroundColor(styles.BgColor()).
|
||||
SetButtonTextColor(styles.FgColor()).
|
||||
SetLabelColor(styles.K9s.Info.FgColor.Color()).
|
||||
SetFieldTextColor(styles.K9s.Info.SectionColor.Color())
|
||||
|
||||
var opts dao.DrainOptions
|
||||
f.AddInputField("GracePeriod:", strconv.Itoa(defaults.GracePeriodSeconds), 0, nil, func(v string) {
|
||||
a, err := asIntOpt(v)
|
||||
if err != nil {
|
||||
view.App().Flash().Err(err)
|
||||
return
|
||||
}
|
||||
view.App().Flash().Clear()
|
||||
opts.GracePeriodSeconds = a
|
||||
})
|
||||
f.AddInputField("Timeout:", defaults.Timeout.String(), 0, nil, func(v string) {
|
||||
a, err := asDurOpt(v)
|
||||
if err != nil {
|
||||
view.App().Flash().Err(err)
|
||||
return
|
||||
}
|
||||
view.App().Flash().Clear()
|
||||
opts.Timeout = a
|
||||
})
|
||||
f.AddCheckbox("Ignore DaemonSets:", defaults.IgnoreAllDaemonSets, func(v bool) {
|
||||
opts.IgnoreAllDaemonSets = v
|
||||
})
|
||||
f.AddCheckbox("Delete Local Data:", defaults.DeleteLocalData, func(v bool) {
|
||||
opts.DeleteLocalData = v
|
||||
})
|
||||
f.AddCheckbox("Force:", defaults.Force, func(v bool) {
|
||||
opts.Force = v
|
||||
})
|
||||
|
||||
pages := view.App().Content.Pages
|
||||
f.AddButton("Cancel", func() {
|
||||
DismissDrain(view, pages)
|
||||
})
|
||||
f.AddButton("OK", func() {
|
||||
DismissDrain(view, pages)
|
||||
okFn(view, path, opts)
|
||||
})
|
||||
|
||||
modal := tview.NewModalForm("<Drain>", f)
|
||||
modal.SetText(path)
|
||||
modal.SetDoneFunc(func(_ int, b string) {
|
||||
DismissDrain(view, pages)
|
||||
})
|
||||
|
||||
pages.AddPage(drainKey, modal, false, true)
|
||||
pages.ShowPage(drainKey)
|
||||
view.App().SetFocus(pages.GetPrimitive(drainKey))
|
||||
}
|
||||
|
||||
// DismissDrain dismiss the port forward dialog.
|
||||
func DismissDrain(v ResourceViewer, p *ui.Pages) {
|
||||
p.RemovePage(drainKey)
|
||||
v.App().SetFocus(p.CurrentPage().Item)
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Helpers...
|
||||
|
||||
func asDurOpt(v string) (time.Duration, error) {
|
||||
d, err := time.ParseDuration(v)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return d, nil
|
||||
}
|
||||
|
||||
func asIntOpt(v string) (int, error) {
|
||||
i, err := strconv.Atoi(v)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return i, nil
|
||||
}
|
||||
|
|
@ -50,6 +50,7 @@ func generalEnv(a *App) K9sEnv {
|
|||
}
|
||||
|
||||
func defaultK9sEnv(a *App, sel string, row render.Row) K9sEnv {
|
||||
log.Debug().Msgf("ROW %#v", row)
|
||||
ns, n := client.Namespaced(sel)
|
||||
|
||||
env := generalEnv(a)
|
||||
|
|
|
|||
|
|
@ -185,6 +185,7 @@ func (l *Log) bindKeys() {
|
|||
}
|
||||
|
||||
func (l *Log) keyboard(evt *tcell.EventKey) *tcell.EventKey {
|
||||
displayKey(l.app, l.cmdBuff.InCmdMode(), evt)
|
||||
key := evt.Key()
|
||||
if key == tcell.KeyUp || key == tcell.KeyDown {
|
||||
return evt
|
||||
|
|
@ -265,7 +266,6 @@ func (l *Log) activateCmd(evt *tcell.EventKey) *tcell.EventKey {
|
|||
if l.app.InCmdMode() {
|
||||
return evt
|
||||
}
|
||||
l.app.Flash().Info("Filter mode activated.")
|
||||
l.cmdBuff.SetActive(true)
|
||||
|
||||
return nil
|
||||
|
|
@ -293,7 +293,6 @@ func (l *Log) resetCmd(evt *tcell.EventKey) *tcell.EventKey {
|
|||
if l.cmdBuff.String() != "" {
|
||||
l.model.ClearFilter()
|
||||
}
|
||||
l.app.Flash().Info("Clearing filter...")
|
||||
l.cmdBuff.SetActive(false)
|
||||
l.cmdBuff.Reset()
|
||||
l.updateTitle()
|
||||
|
|
@ -330,6 +329,7 @@ func saveData(cluster, name, data string) (string, error) {
|
|||
if err != nil {
|
||||
log.Error().Err(err).Msgf("LogFile create %s", path)
|
||||
return "", nil
|
||||
|
||||
}
|
||||
defer func() {
|
||||
if err := file.Close(); err != nil {
|
||||
|
|
@ -344,7 +344,6 @@ func saveData(cluster, name, data string) (string, error) {
|
|||
}
|
||||
|
||||
func (l *Log) clearCmd(*tcell.EventKey) *tcell.EventKey {
|
||||
l.app.Flash().Info("Clearing logs...")
|
||||
l.model.Clear()
|
||||
return nil
|
||||
}
|
||||
|
|
@ -355,7 +354,7 @@ func (l *Log) textWrapCmd(*tcell.EventKey) *tcell.EventKey {
|
|||
return nil
|
||||
}
|
||||
|
||||
// BOZO! Log timestamps.
|
||||
// BOZO!! Log timestamps.
|
||||
// func (l *Log) toggleTimestampCmd(evt *tcell.EventKey) *tcell.EventKey {
|
||||
// l.model.Clear()
|
||||
// l.indicator.ToggleTimestamp()
|
||||
|
|
|
|||
|
|
@ -16,8 +16,7 @@ type LogIndicator struct {
|
|||
scrollStatus int32
|
||||
fullScreen bool
|
||||
textWrap bool
|
||||
// BOZO!! timestamp
|
||||
// showTime bool
|
||||
showTime bool
|
||||
}
|
||||
|
||||
// NewLogIndicator returns a new indicator.
|
||||
|
|
@ -40,11 +39,10 @@ func (l *LogIndicator) AutoScroll() bool {
|
|||
return atomic.LoadInt32(&l.scrollStatus) == 1
|
||||
}
|
||||
|
||||
// BOZO!! Timestamp
|
||||
// // Timestamp reports the current timestamp mode.
|
||||
// func (l *LogIndicator) Timestamp() bool {
|
||||
// return l.showTime
|
||||
// }
|
||||
// Timestamp reports the current timestamp mode.
|
||||
func (l *LogIndicator) Timestamp() bool {
|
||||
return l.showTime
|
||||
}
|
||||
|
||||
// TextWrap reports the current wrap mode.
|
||||
func (l *LogIndicator) TextWrap() bool {
|
||||
|
|
@ -56,11 +54,10 @@ func (l *LogIndicator) FullScreen() bool {
|
|||
return l.fullScreen
|
||||
}
|
||||
|
||||
// BOZO!! Timestamp
|
||||
// // TextWrap reports the current wrap mode.
|
||||
// func (l *LogIndicator) ToggleTimestamp() {
|
||||
// l.showTime = !l.showTime
|
||||
// }
|
||||
// TextWrap reports the current wrap mode.
|
||||
func (l *LogIndicator) ToggleTimestamp() {
|
||||
l.showTime = !l.showTime
|
||||
}
|
||||
|
||||
// ToggleFullScreen toggles the screen mode.
|
||||
func (l *LogIndicator) ToggleFullScreen() {
|
||||
|
|
|
|||
|
|
@ -1,9 +1,15 @@
|
|||
package view
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/derailed/k9s/internal/client"
|
||||
"github.com/derailed/k9s/internal/dao"
|
||||
"github.com/derailed/k9s/internal/ui"
|
||||
"github.com/derailed/k9s/internal/ui/dialog"
|
||||
"github.com/gdamore/tcell"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
|
@ -28,6 +34,9 @@ func (n *Node) bindKeys(aa ui.KeyActions) {
|
|||
aa.Delete(ui.KeySpace, tcell.KeyCtrlSpace, tcell.KeyCtrlD)
|
||||
aa.Add(ui.KeyActions{
|
||||
ui.KeyY: ui.NewKeyAction("YAML", n.viewCmd, true),
|
||||
ui.KeyC: ui.NewKeyAction("Cordon", n.toggleCordonCmd(true), true),
|
||||
ui.KeyU: ui.NewKeyAction("Uncordon", n.toggleCordonCmd(false), true),
|
||||
ui.KeyD: ui.NewKeyAction("Drain", n.drainCmd, true),
|
||||
ui.KeyShiftC: ui.NewKeyAction("Sort CPU", n.GetTable().SortColCmd(cpuCol, false), false),
|
||||
ui.KeyShiftM: ui.NewKeyAction("Sort MEM", n.GetTable().SortColCmd(memCol, false), false),
|
||||
ui.KeyShiftX: ui.NewKeyAction("Sort CPU%", n.GetTable().SortColCmd("%CPU", false), false),
|
||||
|
|
@ -39,6 +48,84 @@ func (n *Node) showPods(app *App, _ ui.Tabular, _, path string) {
|
|||
showPods(app, n.GetTable().GetSelectedItem(), "", "spec.nodeName="+path)
|
||||
}
|
||||
|
||||
func (n *Node) drainCmd(evt *tcell.EventKey) *tcell.EventKey {
|
||||
path := n.GetTable().GetSelectedItem()
|
||||
if path == "" {
|
||||
return evt
|
||||
}
|
||||
|
||||
defaults := dao.DrainOptions{
|
||||
GracePeriodSeconds: -1,
|
||||
Timeout: 5 * time.Second,
|
||||
DeleteLocalData: false,
|
||||
IgnoreAllDaemonSets: false,
|
||||
}
|
||||
ShowDrain(n, path, defaults, drainNode)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func drainNode(v ResourceViewer, path string, opts dao.DrainOptions) {
|
||||
res, err := dao.AccessorFor(v.App().factory, v.GVR())
|
||||
if err != nil {
|
||||
v.App().Flash().Err(err)
|
||||
return
|
||||
}
|
||||
m, ok := res.(dao.NodeMaintainer)
|
||||
if !ok {
|
||||
v.App().Flash().Err(fmt.Errorf("expecting a maintainer for %q", v.GVR()))
|
||||
return
|
||||
}
|
||||
|
||||
buff := bytes.NewBufferString("")
|
||||
if err := m.Drain(path, opts, buff); err != nil {
|
||||
v.App().Flash().Err(err)
|
||||
return
|
||||
}
|
||||
lines := strings.Split(buff.String(), "\n")
|
||||
for _, l := range lines {
|
||||
if len(l) > 0 {
|
||||
v.App().Flash().Info(l)
|
||||
}
|
||||
}
|
||||
v.Refresh()
|
||||
}
|
||||
|
||||
func (n *Node) toggleCordonCmd(cordon bool) func(evt *tcell.EventKey) *tcell.EventKey {
|
||||
return func(evt *tcell.EventKey) *tcell.EventKey {
|
||||
path := n.GetTable().GetSelectedItem()
|
||||
if path == "" {
|
||||
return evt
|
||||
}
|
||||
|
||||
title, msg := "Confirm ", ""
|
||||
if cordon {
|
||||
title, msg = title+"Cordon", "Cordon "
|
||||
} else {
|
||||
title, msg = title+"Uncordon", "Uncordon "
|
||||
}
|
||||
msg += path + "?"
|
||||
dialog.ShowConfirm(n.App().Content.Pages, title, msg, func() {
|
||||
res, err := dao.AccessorFor(n.App().factory, n.GVR())
|
||||
if err != nil {
|
||||
n.App().Flash().Err(err)
|
||||
return
|
||||
}
|
||||
m, ok := res.(dao.NodeMaintainer)
|
||||
if !ok {
|
||||
n.App().Flash().Err(fmt.Errorf("expecting a maintainer for %q", n.GVR()))
|
||||
return
|
||||
}
|
||||
if err := m.ToggleCordon(path, cordon); err != nil {
|
||||
n.App().Flash().Err(err)
|
||||
}
|
||||
n.Refresh()
|
||||
}, func() {})
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (n *Node) viewCmd(evt *tcell.EventKey) *tcell.EventKey {
|
||||
path := n.GetTable().GetSelectedItem()
|
||||
if path == "" {
|
||||
|
|
|
|||
|
|
@ -208,6 +208,7 @@ func (p *Pulse) bindKeys() {
|
|||
}
|
||||
|
||||
func (p *Pulse) keyboard(evt *tcell.EventKey) *tcell.EventKey {
|
||||
displayKey(p.app, false, evt)
|
||||
key := evt.Key()
|
||||
if key == tcell.KeyRune {
|
||||
key = tcell.Key(evt.Rune())
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@ func (r *RestartExtender) restartCmd(evt *tcell.EventKey) *tcell.EventKey {
|
|||
if len(paths) > 1 {
|
||||
msg = fmt.Sprintf("Restart %d deployments?", len(paths))
|
||||
}
|
||||
dialog.ShowConfirm(r.App().Content.Pages, "<Confirm Restart>", msg, func() {
|
||||
dialog.ShowConfirm(r.App().Content.Pages, "Confirm Restart", msg, func() {
|
||||
for _, path := range paths {
|
||||
if err := r.restartRollout(path); err != nil {
|
||||
r.App().Flash().Err(err)
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package view
|
|||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/atotto/clipboard"
|
||||
|
|
@ -55,7 +56,25 @@ func (t *Table) SendKey(evt *tcell.EventKey) {
|
|||
t.keyboard(evt)
|
||||
}
|
||||
|
||||
func displayKey(a *App, isCmd bool, evt *tcell.EventKey) {
|
||||
if !a.Config.DemoMode() || a.InCmdMode() || isCmd {
|
||||
a.Flash().Clear()
|
||||
return
|
||||
}
|
||||
a.Flash().Clear()
|
||||
|
||||
key, ok := tcell.KeyNames[evt.Key()]
|
||||
if !ok {
|
||||
key = string(evt.Rune())
|
||||
}
|
||||
if evt.Modifiers() == tcell.ModCtrl {
|
||||
key = "⌃" + strings.Replace(key, "Ctrl-", "", 1)
|
||||
}
|
||||
a.Flash().Infof("Pressed[:springgreen:b]%s", key)
|
||||
}
|
||||
|
||||
func (t *Table) keyboard(evt *tcell.EventKey) *tcell.EventKey {
|
||||
displayKey(t.app, t.SearchBuff().InCmdMode(), evt)
|
||||
key := evt.Key()
|
||||
if key == tcell.KeyUp || key == tcell.KeyDown {
|
||||
return evt
|
||||
|
|
@ -233,7 +252,6 @@ func (t *Table) activateCmd(evt *tcell.EventKey) *tcell.EventKey {
|
|||
if t.app.InCmdMode() {
|
||||
return evt
|
||||
}
|
||||
t.app.Flash().Info("Filter mode activated.")
|
||||
t.SearchBuff().SetActive(true)
|
||||
|
||||
return nil
|
||||
|
|
|
|||
|
|
@ -417,7 +417,6 @@ func (x *Xray) activateCmd(evt *tcell.EventKey) *tcell.EventKey {
|
|||
if x.app.InCmdMode() {
|
||||
return evt
|
||||
}
|
||||
x.app.Flash().Info("Filter mode activated.")
|
||||
x.CmdBuff().SetActive(true)
|
||||
|
||||
return nil
|
||||
|
|
@ -448,8 +447,6 @@ func (x *Xray) resetCmd(evt *tcell.EventKey) *tcell.EventKey {
|
|||
x.CmdBuff().Reset()
|
||||
return x.app.PrevCmd(evt)
|
||||
}
|
||||
|
||||
x.app.Flash().Info("Clearing filter...")
|
||||
x.CmdBuff().Reset()
|
||||
x.model.ClearFilter()
|
||||
x.Start()
|
||||
|
|
|
|||
Loading…
Reference in New Issue