release v0.25.16 (#1400)

mine
Fernand Galiana 2021-12-24 17:41:35 -07:00 committed by GitHub
parent df613ec88d
commit fc8ffe5d37
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 174 additions and 82 deletions

View File

@ -5,7 +5,7 @@ PACKAGE := github.com/derailed/$(NAME)
GIT_REV ?= $(shell git rev-parse --short HEAD)
SOURCE_DATE_EPOCH ?= $(shell date +%s)
DATE ?= $(shell date -u -d @${SOURCE_DATE_EPOCH} +"%Y-%m-%dT%H:%M:%SZ")
VERSION ?= v0.25.15
VERSION ?= v0.25.16
IMG_NAME := derailed/k9s
IMAGE := ${IMG_NAME}:${VERSION}

BIN
assets/k9s-xmas.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 146 KiB

View File

@ -0,0 +1,81 @@
<img src="https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s.png" align="center" width="800" height="auto"/>
# Release v0.25.16
## Notes
Thank you to all that contributed with flushing out issues and enhancements for K9s! I'll try to mark some of these issues as fixed. But if you don't mind grab the latest rev and see if we're happier with some of the fixes! If you've filed an issue please help me verify and close. Your support, kindness and awesome suggestions to make K9s better are as ever very much noted and appreciated!
If you feel K9s is helping your Kubernetes journey, please consider joining our [sponsorship program](https://github.com/sponsors/derailed) and/or make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer)
On Slack? Please join us [K9slackers](https://join.slack.com/t/k9sers/shared_invite/enQtOTA5MDEyNzI5MTU0LWQ1ZGI3MzliYzZhZWEyNzYxYzA3NjE0YTk1YmFmNzViZjIyNzhkZGI0MmJjYzhlNjdlMGJhYzE2ZGU1NjkyNTM)
### A Word From Our Sponsors...
I want to recognize the following folks that have been kind enough to join our sponsorship program and opted to `pay it forward`!
* [Sebastian Racs](https://github.com/sebracs)
* [Timothy C. Arland](https://github.com/tcarland)
* [Julie Ng](https://github.com/julie-ng)
So if you feel K9s is helping with your productivity while administering your Kubernetes clusters, please consider pitching in as it will go a long way in ensuring a thriving environment for this repo and our k9ers community at large.
Also please take some time and give a huge shoot out to all the good folks below that have spent time plowing thru the code to help improve K9s for all of us!
Thank you!!
---
## ♫ Sounds Behind The Release ♭
[Blue Christmas - Fats Domino](https://www.youtube.com/watch?v=7jeo09zAskc)
[Mele Kalikimaka - Bing Crosby](https://www.youtube.com/watch?v=hEvGKUXW0iI)
[Cause - Rodriguez -- Spreading The Holiday Cheer! 🤨](https://www.youtube.com/watch?v=oKFkc19T3Dk)
---
## 🎅🎄 !!Merry Christmas To All!! 🎄🎅
I hope you will take this time of the year to relax, re-source and spend quality time with your loved ones. I know it's been a `tad rocky` of recent ;( as I've gotten seriously slammed with work in the last few months...
The fine folks here on this channel have been nothing but kind, patient and willing to help, this humbles me! I feel truly blessed to be affiliated with our great `k9sers` community!
Next month, we'll celebrate our anniversary as we've started out in this venture back in Jan 2019 (Yikes!) so get crack'in and iron out those bow ties already!!
Best wishes for great health, happiness and continued success for 2022 to you all!!
-Fernand
---
## A Christmas Story...
As of this drop, we've added a new feature to override the sort column and order for a given Kubernetes resource. This feature piggy backs of custom column views and add a new attribute namely `sortColumn`. For example say you'd like to set the default sort for pods to age descending vs name/namespace, you can now do the following in your `views.yml` file in the k9s config directory:
NOTE: This file is live thus you can nav to your fav resource, change the column config and view the resource columns and sort changes... Woot!!
```yaml
k9s:
views:
v1/endpoints:
columns:
- NAME
- NAMESPACE
- ENDPOINTS
- AGE
v1/pods:
sortColumn: AGE:desc # => suffix [:asc|:desc] for ascending or descending order.
v1/services:
...
```
---
## Resolved Issues
* [Issue #1398](https://github.com/derailed/k9s/issues/1398) Pod logs containing brackets not in k9s logs output
* [Issue #1397](https://github.com/derailed/k9s/issues/1397) Regression: k9s no longer starts in current context namespace since v0.25.12
* [Issue #1358](https://github.com/derailed/k9s/issues/1358) Namespaces list is empty
* [Issue #956](https://github.com/derailed/k9s/issues/956) Feature request : Default column sort (by resource view)
---
<img src="https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png" width="32" height="auto"/> © 2021 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)

View File

@ -2,9 +2,6 @@ package cmd
import (
"fmt"
"os"
"runtime/debug"
"github.com/derailed/k9s/internal/client"
"github.com/derailed/k9s/internal/color"
"github.com/derailed/k9s/internal/config"
@ -14,6 +11,8 @@ import (
"github.com/rs/zerolog/log"
"github.com/spf13/cobra"
"k8s.io/cli-runtime/pkg/genericclioptions"
"os"
"runtime/debug"
)
const (
@ -53,6 +52,17 @@ func Execute() {
}
func run(cmd *cobra.Command, args []string) {
config.EnsurePath(*k9sFlags.LogFile, config.DefaultDirMod)
mod := os.O_CREATE | os.O_APPEND | os.O_WRONLY
file, err := os.OpenFile(*k9sFlags.LogFile, mod, config.DefaultFileMod)
if err != nil {
panic(err)
}
defer func() {
if file != nil {
_ = file.Close()
}
}()
defer func() {
if err := recover(); err != nil {
log.Error().Msgf("Boom! %v", err)
@ -63,15 +73,6 @@ func run(cmd *cobra.Command, args []string) {
}
}()
config.EnsurePath(*k9sFlags.LogFile, config.DefaultDirMod)
mod := os.O_CREATE | os.O_APPEND | os.O_WRONLY
file, err := os.OpenFile(*k9sFlags.LogFile, mod, config.DefaultFileMod)
defer func() {
_ = file.Close()
}()
if err != nil {
panic(err)
}
log.Logger = log.Output(zerolog.ConsoleWriter{Out: file})
zerolog.SetGlobalLevel(parseLevel(*k9sFlags.LogLevel))
@ -136,6 +137,8 @@ func loadConfiguration() *config.Config {
func parseLevel(level string) zerolog.Level {
switch level {
case "trace":
return zerolog.TraceLevel
case "debug":
return zerolog.DebugLevel
case "warn":
@ -161,7 +164,7 @@ func initK9sFlags() {
k9sFlags.LogLevel,
"logLevel", "l",
config.DefaultLogLevel,
"Specify a log level (info, warn, debug, error)",
"Specify a log level (info, warn, debug, trace, error)",
)
rootCmd.Flags().StringVarP(
k9sFlags.LogFile,

View File

@ -89,6 +89,7 @@ func makeSAR(ns, gvr string) *authorizationv1.SelfSubjectAccessReview {
ResourceAttributes: &authorizationv1.ResourceAttributes{
Namespace: ns,
Group: res.Group,
Version: res.Version,
Resource: res.Resource,
Subresource: spec.SubResource(),
},
@ -162,6 +163,7 @@ func (a *APIClient) CanI(ns, gvr string, verbs []string) (auth bool, err error)
for _, v := range verbs {
sar.Spec.ResourceAttributes.Verb = v
resp, err := client.Create(ctx, sar, metav1.CreateOptions{})
log.Trace().Msgf("[CAN] %s(%s) %v <<%v>>", gvr, verbs, resp, err)
if err != nil {
log.Warn().Err(err).Msgf(" Dial Failed!")
a.cache.Add(key, false, cacheExpiry)

View File

@ -21,6 +21,9 @@ const (
// AllNamespaces designates all namespaces.
AllNamespaces = ""
// DefaultNamespace designates the default namespace.
DefaultNamespace = "default"
// ClusterScope designates a resource is not namespaced.
ClusterScope = "-"

View File

@ -34,6 +34,9 @@ func (c *Cluster) Validate(conn client.Connection, ks KubeSettings) {
if c.Namespace == nil {
c.Namespace = NewNamespace()
}
if c.Namespace.Active == client.AllNamespaces {
c.Namespace.Active = client.NamespaceAll
}
if c.FeatureGates == nil {
c.FeatureGates = NewFeatureGates()

View File

@ -90,32 +90,24 @@ func (c *Config) Refine(flags *genericclioptions.ConfigFlags, k9sFlags *Flags, c
return fmt.Errorf("The specified context %q does not exists in kubeconfig", c.K9s.CurrentContext)
}
c.K9s.CurrentCluster = context.Cluster
c.K9s.ActivateCluster()
c.K9s.ActivateCluster(context.Namespace)
var cns string
if cl := c.K9s.ActiveCluster(); cl != nil && cl.Namespace != nil {
cns = cl.Namespace.Active
}
var ns string
var ns = client.DefaultNamespace
if k9sFlags != nil && IsBoolSet(k9sFlags.AllNamespaces) {
ns = client.NamespaceAll
} else if isSet(flags.Namespace) {
ns = *flags.Namespace
} else if context.Namespace != "" {
} else if isSet(flags.Context) {
ns = context.Namespace
if cns != "" {
ns = cns
}
} else {
ns = cns
ns = c.K9s.ActiveCluster().Namespace.Active
}
if ns != "" {
if err := c.SetActiveNamespace(ns); err != nil {
return err
}
flags.Namespace = &ns
if err := c.SetActiveNamespace(ns); err != nil {
return err
}
flags.Namespace = &ns
if isSet(flags.ClusterName) {
c.K9s.CurrentCluster = *flags.ClusterName
}
@ -166,9 +158,6 @@ func (c *Config) ActiveNamespace() string {
// ValidateFavorites ensure favorite ns are legit.
func (c *Config) ValidateFavorites() {
cl := c.K9s.ActiveCluster()
if cl == nil {
cl = NewCluster()
}
cl.Validate(c.client, c.settings)
cl.Namespace.Validate(c.client, c.settings)
}
@ -176,16 +165,14 @@ func (c *Config) ValidateFavorites() {
// FavNamespaces returns fav namespaces in the current cluster.
func (c *Config) FavNamespaces() []string {
cl := c.K9s.ActiveCluster()
if cl == nil {
return nil
}
return c.K9s.ActiveCluster().Namespace.Favorites
return cl.Namespace.Favorites
}
// SetActiveNamespace set the active namespace in the current cluster.
func (c *Config) SetActiveNamespace(ns string) error {
if c.K9s.ActiveCluster() != nil {
return c.K9s.ActiveCluster().Namespace.SetActive(ns, c.settings)
if cl := c.K9s.ActiveCluster(); cl != nil {
return cl.Namespace.SetActive(ns, c.settings)
}
err := errors.New("no active cluster. unable to set active namespace")
log.Error().Err(err).Msg("SetActiveNamespace")
@ -195,11 +182,11 @@ func (c *Config) SetActiveNamespace(ns string) error {
// ActiveView returns the active view in the current cluster.
func (c *Config) ActiveView() string {
if c.K9s.ActiveCluster() == nil {
cl := c.K9s.ActiveCluster()
if cl == nil {
return defaultView
}
cmd := c.K9s.ActiveCluster().View.Active
cmd := cl.View.Active
if c.K9s.manualCommand != nil && *c.K9s.manualCommand != "" {
cmd = *c.K9s.manualCommand
}
@ -209,8 +196,7 @@ func (c *Config) ActiveView() string {
// SetActiveView set the currently cluster active view.
func (c *Config) SetActiveView(view string) {
cl := c.K9s.ActiveCluster()
if cl != nil {
if cl := c.K9s.ActiveCluster(); cl != nil {
cl.View.Active = view
}
}

View File

@ -30,7 +30,7 @@ func TestConfigRefine(t *testing.T) {
issue: false,
context: "test1",
cluster: "cluster1",
namespace: "default",
namespace: "ns1",
},
"overrideNS": {
flags: &genericclioptions.ConfigFlags{

View File

@ -47,11 +47,13 @@ func NewK9s() *K9s {
}
// ActivateCluster initializes the active cluster is not present.
func (k *K9s) ActivateCluster() {
func (k *K9s) ActivateCluster(ns string) {
if _, ok := k.Clusters[k.CurrentCluster]; ok {
return
}
k.Clusters[k.CurrentCluster] = NewCluster()
cl := NewCluster()
cl.Namespace.Active = ns
k.Clusters[k.CurrentCluster] = cl
}
// OverrideRefreshRate set the refresh rate manually.
@ -59,17 +61,17 @@ func (k *K9s) OverrideRefreshRate(r int) {
k.manualRefreshRate = r
}
// OverrideHeadless set the headlessness manually.
// OverrideHeadless toggle the header manually.
func (k *K9s) OverrideHeadless(b bool) {
k.manualHeadless = &b
}
// OverrideLogoless set the logolessness manually.
// OverrideLogoless toggle the k9s logo manually.
func (k *K9s) OverrideLogoless(b bool) {
k.manualLogoless = &b
}
// OverrideCrumbsless set the crumbslessness manually.
// OverrideCrumbsless tooh the crumbslessness manually.
func (k *K9s) OverrideCrumbsless(b bool) {
k.manualCrumbsless = &b
}
@ -154,7 +156,6 @@ func (k *K9s) ActiveCluster() *Cluster {
if k.Clusters == nil {
k.Clusters = map[string]*Cluster{}
}
if c, ok := k.Clusters[k.CurrentCluster]; ok {
return c
}

View File

@ -22,7 +22,7 @@ type Namespace struct {
func NewNamespace() *Namespace {
return &Namespace{
Active: defaultNS,
Favorites: []string{"default"},
Favorites: []string{defaultNS},
}
}

View File

@ -18,7 +18,8 @@ type ViewConfigListener interface {
// ViewSetting represents a view configuration.
type ViewSetting struct {
Columns []string `yaml:"columns"`
Columns []string `yaml:"columns"`
SortColumn string `yaml:"sortColumn"`
}
// ViewSettings represent a collection of view configurations.

View File

@ -130,7 +130,7 @@ func (o *LogOptions) ToLogItem(bytes []byte) *LogItem {
func (o *LogOptions) ToErrLogItem(err error) *LogItem {
t := time.Now().UTC().Format(time.RFC3339Nano)
item := NewLogItem([]byte(fmt.Sprintf("%s [red::b]%s\n", t, err)))
item := NewLogItem([]byte(fmt.Sprintf("%s [orange::b]%s[::-]\n", t, err)))
item.IsError = true
return item
}

View File

@ -313,18 +313,14 @@ func (p *Pod) Scan(ctx context.Context, gvr, fqn string, wait bool) (Refs, error
func tailLogs(ctx context.Context, logger Logger, opts *LogOptions) LogChan {
var (
out = make(LogChan, 2)
wg sync.WaitGroup
out = make(LogChan, 2)
wg sync.WaitGroup
)
wg.Add(1)
go func() {
defer func() {
wg.Done()
log.Debug().Msgf("<<< RETRY-TAIL DONE!!! %s", opts.Info())
}()
defer wg.Done()
podOpts := opts.ToPodLogOptions()
log.Debug().Msgf(">>> RETRY-TAIL START %s", opts.Info())
var stream io.ReadCloser
for r := 0; r < logRetryCount; r++ {
var e error
@ -345,7 +341,6 @@ func tailLogs(ctx context.Context, logger Logger, opts *LogOptions) LogChan {
select {
case <-ctx.Done():
log.Debug().Msgf("LOG CANCELED %s", opts.Info())
return
default:
if e != nil {
@ -358,7 +353,6 @@ func tailLogs(ctx context.Context, logger Logger, opts *LogOptions) LogChan {
go func() {
wg.Wait()
close(out)
log.Debug().Msgf("<<< LOG-TAILER %s DONE!!", opts.Info())
}()
return out
@ -369,7 +363,6 @@ func readLogs(ctx context.Context, wg *sync.WaitGroup, stream io.ReadCloser, out
if err := stream.Close(); err != nil {
log.Error().Err(err).Msgf("Fail to close stream %s", opts.Info())
}
log.Debug().Msgf("<<< LOG-READER EXIT!!! %s", opts.Info())
wg.Done()
}()
@ -383,11 +376,11 @@ func readLogs(ctx context.Context, wg *sync.WaitGroup, stream io.ReadCloser, out
if errors.Is(err, io.EOF) {
e := fmt.Errorf("Stream closed %w for %s", err, opts.Info())
item = opts.ToErrLogItem(e)
log.Debug().Err(e).Msg("log-reader EOF")
log.Warn().Err(e).Msg("log-reader EOF")
} else {
e := fmt.Errorf("Stream canceled %w for %s", err, opts.Info())
item = opts.ToErrLogItem(e)
log.Debug().Err(e).Msg("log-reader canceled")
log.Warn().Err(e).Msg("log-reader canceled")
}
}
select {

View File

@ -147,12 +147,15 @@ func (h Header) HasAge() bool {
// IsMetricsCol checks if given column index represents metrics.
func (h Header) IsMetricsCol(col int) bool {
if col < 0 || col >= len(h) {
return false
}
return h[col].MX
}
// IsTimeCol checks if given column index represents a timestamp.
func (h Header) IsTimeCol(col int) bool {
if col >= len(h) {
if col < 0 || col >= len(h) {
return false
}

View File

@ -191,7 +191,9 @@ func (t *Table) Update(data render.TableData, hasMetrics bool) {
func (t *Table) doUpdate(data render.TableData) {
if client.IsAllNamespaces(data.Namespace) {
t.actions[KeyShiftP] = NewKeyAction("Sort Namespace", t.SortColCmd("NAMESPACE", true), false)
t.sortCol.name = "NAMESPACE"
} else {
t.sortCol.name = "NAME"
t.actions.Delete(KeyShiftP)
}
@ -203,16 +205,24 @@ func (t *Table) doUpdate(data render.TableData) {
cols = t.header.Columns(t.wide)
}
custData := data.Customize(cols, t.wide)
if (t.sortCol.name == "" || custData.Header.IndexOf(t.sortCol.name, false) == -1) && len(custData.Header) > 0 && t.sortCol.name != "NONE" {
t.sortCol.name = custData.Header[0].Name
if t.sortCol.name == "NAMESPACE" && !client.IsAllNamespaces(data.Namespace) {
if idx := custData.Header.IndexOf("NAME", false); idx != -1 {
t.sortCol.name = custData.Header[idx].Name
if t.viewSetting != nil && t.viewSetting.SortColumn != "" {
tokens := strings.Split(t.viewSetting.SortColumn, ":")
if custData.Header.IndexOf(tokens[0], false) >= 0 {
t.sortCol.name, t.sortCol.asc = tokens[0], true
if len(tokens) == 2 && tokens[1] == "desc" {
t.sortCol.asc = false
}
}
}
if t.sortCol.name == "NAMESPACE" && !client.IsAllNamespaces(data.Namespace) && len(custData.Header) > 0 {
if idx := custData.Header.IndexOf("NAME", false); idx >= 0 {
t.sortCol.name = custData.Header[idx].Name
} else {
t.sortCol.name = custData.Header[0].Name
}
}
t.Clear()
fg := t.styles.Table().Header.FgColor.Color()
bg := t.styles.Table().Header.BgColor.Color()

View File

@ -140,7 +140,7 @@ func (b *Browser) Start() {
b.Table.Start()
b.CmdBuff().AddListener(b)
if err := b.GetModel().Watch(b.prepareContext()); err != nil {
log.Error().Err(err).Msgf("Watcher failed for %s", b.GVR())
b.App().Flash().Err(fmt.Errorf("Watcher failed for %s -- %w", b.GVR(), err))
}
}

View File

@ -199,14 +199,17 @@ func ssh(a *App, node string) error {
if err := launchShellPod(a, node); err != nil {
return err
}
ns := a.Config.K9s.ActiveCluster().ShellPod.Namespace
cl := a.Config.K9s.ActiveCluster()
ns := cl.ShellPod.Namespace
sshIn(a, client.FQN(ns, k9sShellPodName()), k9sShell)
return nil
}
func sshIn(a *App, fqn, co string) {
cfg := a.Config.K9s.ActiveCluster().ShellPod
cl := a.Config.K9s.ActiveCluster()
cfg := cl.ShellPod
os, err := getPodOS(a.factory, fqn)
if err != nil {
log.Warn().Err(err).Msgf("os detect failed")
@ -232,12 +235,13 @@ func sshIn(a *App, fqn, co string) {
}
func nukeK9sShell(a *App) error {
cl := a.Config.K9s.CurrentCluster
if !a.Config.K9s.Clusters[cl].FeatureGates.NodeShell {
clName := a.Config.K9s.CurrentCluster
if !a.Config.K9s.Clusters[clName].FeatureGates.NodeShell {
return nil
}
ns := a.Config.K9s.ActiveCluster().ShellPod.Namespace
cl := a.Config.K9s.ActiveCluster()
ns := cl.ShellPod.Namespace
ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond)
defer cancel()
@ -256,8 +260,9 @@ func nukeK9sShell(a *App) error {
func launchShellPod(a *App, node string) error {
a.Flash().Infof("Launching node shell on %s...", node)
ns := a.Config.K9s.ActiveCluster().ShellPod.Namespace
spec := k9sShellPod(node, a.Config.K9s.ActiveCluster().ShellPod)
cl := a.Config.K9s.ActiveCluster()
ns := cl.ShellPod.Namespace
spec := k9sShellPod(node, cl.ShellPod)
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()

View File

@ -350,6 +350,7 @@ func (l *Log) Flush(lines [][]byte) {
if l.cancelUpdates {
break
}
log.Debug().Msgf("FLUSH %q", string(lines[i]))
_, _ = l.ansiWriter.Write(lines[i])
}
if l.follow {

View File

@ -36,7 +36,7 @@ func (l *Logger) Init(_ context.Context) error {
if l.title != "" {
l.SetBorder(true)
}
l.SetScrollable(true).SetWrap(true).SetRegions(true)
l.SetScrollable(true).SetWrap(true)
l.SetDynamicColors(true)
l.SetHighlightColor(tcell.ColorOrange)
l.SetTitleColor(tcell.ColorAqua)