Release v0.32.6 (#2955)

* update releaser

* fix err messages

* make pf display the correct address

* fix regex filters

* fix cust res loading

* add container index

* update deps + clean up

* release v0.32.6
mine
Fernand Galiana 2024-11-10 10:08:54 -07:00 committed by GitHub
parent 99d47ab7e7
commit 9984e3f4bf
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
52 changed files with 318 additions and 138 deletions

View File

@ -53,10 +53,8 @@ linters-settings:
- nilness - nilness
goimports: goimports:
local-prefixes: github.com/cilium/cilium local-prefixes: github.com/cilium/cilium
staticcheck:
go: "1.20"
unused: unused:
go: "1.20" go: "1.23"
goheader: goheader:
values: values:
regexp: regexp:
@ -93,7 +91,7 @@ issues:
text: "SA9003: empty branch" text: "SA9003: empty branch"
- linters: [staticcheck] - linters: [staticcheck]
text: "SA2001: empty critical section" text: "SA2001: empty critical section"
- linters: [goerr113] - linters: [err113]
text: "do not define dynamic errors, use wrapped static errors instead" # This rule to avoid opinionated check fmt.Errorf("text") text: "do not define dynamic errors, use wrapped static errors instead" # This rule to avoid opinionated check fmt.Errorf("text")
# Skip goimports check on generated files # Skip goimports check on generated files
- path: \\.(generated\\.deepcopy|pb)\\.go$ - path: \\.(generated\\.deepcopy|pb)\\.go$
@ -107,13 +105,12 @@ issues:
linters: linters:
disable-all: true disable-all: true
enable: enable:
- goerr113 - err113
- gofmt - gofmt
- goimports - goimports
- govet - govet
- ineffassign - ineffassign
- misspell - misspell
- staticcheck
- unused - unused
- goheader - goheader
- gosec - gosec

View File

@ -1,3 +1,5 @@
version: 2
project_name: k9s project_name: k9s
before: before:
@ -90,7 +92,7 @@ brews:
commit_author: commit_author:
name: derailed name: derailed
email: fernand@imhotep.io email: fernand@imhotep.io
folder: Formula directory: Formula
homepage: https://k9scli.io/ homepage: https://k9scli.io/
description: Kubernetes CLI To Manage Your Clusters In Style! description: Kubernetes CLI To Manage Your Clusters In Style!
test: | test: |

View File

@ -1,7 +1,7 @@
language: go language: go
go_import_path: github.com/derailed/k9s go_import_path: github.com/derailed/k9s
go: go:
- "1.15" - "1.23"
jobs: jobs:
include: include:

View File

@ -11,7 +11,7 @@ DATE ?= $(shell TZ=UTC date -j -f "%s" ${SOURCE_DATE_EPOCH} +"%Y-%m-%dT%H:
else else
DATE ?= $(shell date -u -d @${SOURCE_DATE_EPOCH} +"%Y-%m-%dT%H:%M:%SZ") DATE ?= $(shell date -u -d @${SOURCE_DATE_EPOCH} +"%Y-%m-%dT%H:%M:%SZ")
endif endif
VERSION ?= v0.32.5 VERSION ?= v0.32.6
IMG_NAME := derailed/k9s IMG_NAME := derailed/k9s
IMAGE := ${IMG_NAME}:${VERSION} IMAGE := ${IMG_NAME}:${VERSION}

View File

@ -0,0 +1,97 @@
<img src="https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s.png" align="center" width="800" height="auto"/>
# Release v0.32.6
## 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!
Also big thanks to all that have allocated their own time to help others on both slack and on this repo!!
As you may know, K9s is not pimped out by corps with deep pockets, thus 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)
## Maintenance Release!
---
## Videos Are In The Can!
Please dial [K9s Channel](https://www.youtube.com/channel/UC897uwPygni4QIjkPCpgjmw) for up coming content...
* [K9s v0.31.0 Configs+Sneak peek](https://youtu.be/X3444KfjguE)
* [K9s v0.30.0 Sneak peek](https://youtu.be/mVBc1XneRJ4)
* [Vulnerability Scans](https://youtu.be/ULkl0MsaidU)
---
## Resolved Issues
* [#2947](https://github.com/derailed/k9s/issues/2947) CTRL+Z causes k9s to crash
* [#2938](https://github.com/derailed/k9s/issues/2938) Critical Vulnerability CVE-2024-41110 in v26.0.1 of docker included in k9s
* [#2929](https://github.com/derailed/k9s/issues/2929) conflicting plugins shortcuts
* [#2896](https://github.com/derailed/k9s/issues/2896) Add a plugin to disable/enable a keda ScaledObject
* [#2811](https://github.com/derailed/k9s/issues/2811) Dockerfile build step fails due to misaligned Go versions (1.21.5 vs 1.22.0)
* [#2767](https://github.com/derailed/k9s/issues/2767) Manually triggered jobs don't get automatically cleaned up
* [#2761](https://github.com/derailed/k9s/issues/2761) Enable "jump to owner" for more kinds
* [#2754](https://github.com/derailed/k9s/issues/2754) Plugins not loaded/shown in UI
* [#2747](https://github.com/derailed/k9s/issues/2747) Combining context and namespace switching only works sporadically (e.g. ":pod foo-ns @ctx-dev")
* [#2746](https://github.com/derailed/k9s/issues/2746) k9s does not display "[::]" string in its logs
* [#2738](https://github.com/derailed/k9s/issues/2738) "Faults" view should show all Terminating pods
---
## Contributed PRs
Please be sure to give `Big Thanks!` and `ATTA Girls/Boys!` to all the fine contributors for making K9s better for all of us!!
* [#2937](https://github.com/derailed/k9s/pull/2937) Adding Argo Rollouts plugin version for PowerShell
* [#2935](https://github.com/derailed/k9s/pull/2935) fix: show all terminating pods in Faults view (#2738)
* [#2933](https://github.com/derailed/k9s/pull/2933) chore: broken url in build-status tag in the readme.md
* [#2932](https://github.com/derailed/k9s/pull/2932) fix: add kubeconfig if k9s is launched with --kubeconfig
* [#2930](https://github.com/derailed/k9s/pull/2930) fixed conflicting plugin shortcuts, and added 2 new plugins
* [#2927](https://github.com/derailed/k9s/pull/2927) Fix "Mark Range": reduce maximum namespaces in favorites, fix shadowing of ctrl+space
* [#2926](https://github.com/derailed/k9s/pull/2926) chore(plugins,remove-finalizers): make sure the resources api group is respected
* [#2921](https://github.com/derailed/k9s/pull/2921) feat: Add plugins for kubectl node-shell
* [#2920](https://github.com/derailed/k9s/pull/2920) eat: added StartupProbes status (S) to the PROBES column in the container render
* [#2914](https://github.com/derailed/k9s/pull/2914) Adding eks-node-viewer plugin
* [#2898](https://github.com/derailed/k9s/pull/2898) Add argocd plugin to community plugins
* [#2896](https://github.com/derailed/k9s/pull/2896) feat(2896): Add toggle keda plugin
* [#2890](https://github.com/derailed/k9s/pull/2890) Update README.md
* [#2881](https://github.com/derailed/k9s/pull/2881) Fix Mark-Range command: ensure that NS Favorite doesn't exceed the limit
* [#2861](https://github.com/derailed/k9s/pull/2861) chore: fix function name
* [#2856](https://github.com/derailed/k9s/pull/2856) fix internal/render/hpa.go merge issue
* [#2848](https://github.com/derailed/k9s/pull/2848) Include sidecar containers requests and limits
* [#2844](https://github.com/derailed/k9s/pull/2844) Update README GO Version Required
* [#2830](https://github.com/derailed/k9s/pull/2830) update tview to fix log escaping problem completely
* [#2822](https://github.com/derailed/k9s/pull/2822) Adding HolmesGPT plugin
* [#2821](https://github.com/derailed/k9s/pull/2821) Add a spark-operator plugin
* [#2817](https://github.com/derailed/k9s/pull/2817) Add comment about Escape keybinding
* [#2812](https://github.com/derailed/k9s/pull/2812) fix: align build image Go version with go.mod
* [#2795](https://github.com/derailed/k9s/pull/2795) add new plugin current-ctx-terminal
* [#2791](https://github.com/derailed/k9s/pull/2791) Add leading space to Kubernetes context suggestions
* [#2789](https://github.com/derailed/k9s/pull/2789) Create kubectl-get-in-shell.yaml
* [#2788](https://github.com/derailed/k9s/pull/2788) Update README.md plugin format
* [#2787](https://github.com/derailed/k9s/pull/2787) Update helm-purge.yaml
* [#2786](https://github.com/derailed/k9s/pull/2786) Update README.md with plugin dangerous field
* [#2780](https://github.com/derailed/k9s/pull/2780) install copyright file into correct location
* [#2775](https://github.com/derailed/k9s/pull/2775) fix freebsd build failure
* [#2780](https://github.com/derailed/k9s/pull/2780) install copyright file into correct location
* [#2772](https://github.com/derailed/k9s/pull/2772) proper handle OwnerReference for manually created job
* [#2771](https://github.com/derailed/k9s/pull/2771) feat: add duplik8s plugin
* [#2770](https://github.com/derailed/k9s/pull/2770) feat: allow plugins block in plugin files
* [#2765](https://github.com/derailed/k9s/pull/2765) fix: Shellin -> ShellIn
* [#2763](https://github.com/derailed/k9s/pull/2763) enable "jump to owner" for more kinds
* [#2755](https://github.com/derailed/k9s/pull/2755) Loki plugin
* [#2751](https://github.com/derailed/k9s/pull/2751) container logs should be escaped when printed
* [#2750](https://github.com/derailed/k9s/pull/2750) fix: should switching ctx before ns
---
<img src="https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png" width="32" height="auto"/> © 2024 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)

6
go.mod
View File

@ -1,6 +1,6 @@
module github.com/derailed/k9s module github.com/derailed/k9s
go 1.22.0 go 1.23.0
require ( require (
github.com/adrg/xdg v0.5.0 github.com/adrg/xdg v0.5.0
@ -314,8 +314,8 @@ require (
gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/warnings.v0 v0.1.2 // indirect gopkg.in/warnings.v0 v0.1.2 // indirect
gorm.io/gorm v1.25.9 // indirect gorm.io/gorm v1.25.9 // indirect
k8s.io/apiserver v0.31.1 // indirect k8s.io/apiserver v0.31.2 // indirect
k8s.io/component-base v0.31.1 // indirect k8s.io/component-base v0.31.2 // indirect
k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340 // indirect k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340 // indirect
k8s.io/utils v0.0.0-20240711033017-18e509b52bc8 // indirect k8s.io/utils v0.0.0-20240711033017-18e509b52bc8 // indirect
modernc.org/libc v1.41.0 // indirect modernc.org/libc v1.41.0 // indirect

8
go.sum
View File

@ -1864,14 +1864,14 @@ k8s.io/apiextensions-apiserver v0.31.1 h1:L+hwULvXx+nvTYX/MKM3kKMZyei+UiSXQWciX/
k8s.io/apiextensions-apiserver v0.31.1/go.mod h1:tWMPR3sgW+jsl2xm9v7lAyRF1rYEK71i9G5dRtkknoQ= k8s.io/apiextensions-apiserver v0.31.1/go.mod h1:tWMPR3sgW+jsl2xm9v7lAyRF1rYEK71i9G5dRtkknoQ=
k8s.io/apimachinery v0.31.2 h1:i4vUt2hPK56W6mlT7Ry+AO8eEsyxMD1U44NR22CLTYw= k8s.io/apimachinery v0.31.2 h1:i4vUt2hPK56W6mlT7Ry+AO8eEsyxMD1U44NR22CLTYw=
k8s.io/apimachinery v0.31.2/go.mod h1:rsPdaZJfTfLsNJSQzNHQvYoTmxhoOEofxtOsF3rtsMo= k8s.io/apimachinery v0.31.2/go.mod h1:rsPdaZJfTfLsNJSQzNHQvYoTmxhoOEofxtOsF3rtsMo=
k8s.io/apiserver v0.31.1 h1:Sars5ejQDCRBY5f7R3QFHdqN3s61nhkpaX8/k1iEw1c= k8s.io/apiserver v0.31.2 h1:VUzOEUGRCDi6kX1OyQ801m4A7AUPglpsmGvdsekmcI4=
k8s.io/apiserver v0.31.1/go.mod h1:lzDhpeToamVZJmmFlaLwdYZwd7zB+WYRYIboqA1kGxM= k8s.io/apiserver v0.31.2/go.mod h1:o3nKZR7lPlJqkU5I3Ove+Zx3JuoFjQobGX1Gctw6XuE=
k8s.io/cli-runtime v0.31.1 h1:/ZmKhmZ6hNqDM+yf9s3Y4KEYakNXUn5sod2LWGGwCuk= k8s.io/cli-runtime v0.31.1 h1:/ZmKhmZ6hNqDM+yf9s3Y4KEYakNXUn5sod2LWGGwCuk=
k8s.io/cli-runtime v0.31.1/go.mod h1:pKv1cDIaq7ehWGuXQ+A//1OIF+7DI+xudXtExMCbe9U= k8s.io/cli-runtime v0.31.1/go.mod h1:pKv1cDIaq7ehWGuXQ+A//1OIF+7DI+xudXtExMCbe9U=
k8s.io/client-go v0.31.2 h1:Y2F4dxU5d3AQj+ybwSMqQnpZH9F30//1ObxOKlTI9yc= k8s.io/client-go v0.31.2 h1:Y2F4dxU5d3AQj+ybwSMqQnpZH9F30//1ObxOKlTI9yc=
k8s.io/client-go v0.31.2/go.mod h1:NPa74jSVR/+eez2dFsEIHNa+3o09vtNaWwWwb1qSxSs= k8s.io/client-go v0.31.2/go.mod h1:NPa74jSVR/+eez2dFsEIHNa+3o09vtNaWwWwb1qSxSs=
k8s.io/component-base v0.31.1 h1:UpOepcrX3rQ3ab5NB6g5iP0tvsgJWzxTyAo20sgYSy8= k8s.io/component-base v0.31.2 h1:Z1J1LIaC0AV+nzcPRFqfK09af6bZ4D1nAOpWsy9owlA=
k8s.io/component-base v0.31.1/go.mod h1:WGeaw7t/kTsqpVTaCoVEtillbqAhF2/JgvO0LDOMa0w= k8s.io/component-base v0.31.2/go.mod h1:9PeyyFN/drHjtJZMCTkSpQJS3U9OXORnHQqMLDz0sUQ=
k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk=
k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE=
k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340 h1:BZqlfIlq5YbRMFko6/PM7FjZpUb45WallggurYhKGag= k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340 h1:BZqlfIlq5YbRMFko6/PM7FjZpUb45WallggurYhKGag=

View File

@ -98,7 +98,7 @@ func (m *MetricsServer) checkAccess(ns, gvr, msg string) error {
return err return err
} }
if !auth { if !auth {
return fmt.Errorf(msg) return errors.New(msg)
} }
return nil return nil
} }

View File

@ -72,7 +72,7 @@ func (n *Namespace) Validate(c client.Connection) {
if len(n.Favorites) > MaxFavoritesNS { if len(n.Favorites) > MaxFavoritesNS {
log.Debug().Msgf("[Namespace] Number of favorite exceeds hard limit of %v. Trimming.", MaxFavoritesNS) log.Debug().Msgf("[Namespace] Number of favorite exceeds hard limit of %v. Trimming.", MaxFavoritesNS)
for _, ns := range n.Favorites[MaxFavoritesNS:] { for _, ns := range n.Favorites[MaxFavoritesNS:] {
n.rmFavNS(ns) n.rmFavNS(ns)
} }
} }
} }

View File

@ -36,14 +36,12 @@ func TestNSValidateNoNS(t *testing.T) {
} }
func TestNsValidateMaxNS(t *testing.T) { func TestNsValidateMaxNS(t *testing.T) {
allNS := []string{"ns9","ns8","ns7","ns6","ns5","ns4", "ns3", "ns2", "ns1", "all", "default"} allNS := []string{"ns9", "ns8", "ns7", "ns6", "ns5", "ns4", "ns3", "ns2", "ns1", "all", "default"}
ns := data.NewNamespace() ns := data.NewNamespace()
ns.Favorites = allNS
ns.Favorites = allNS ns.Validate(mock.NewMockConnection())
assert.Equal(t, data.MaxFavoritesNS, len(ns.Favorites))
ns.Validate(mock.NewMockConnection())
assert.Equal(t, data.MaxFavoritesNS, len(ns.Favorites))
} }
func TestNSSetActive(t *testing.T) { func TestNSSetActive(t *testing.T) {

View File

@ -1,14 +0,0 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright Authors of K9s
package config
// // FeatureGates represents K9s opt-in features.
// type FeatureGates struct {
// NodeShell bool `yaml:"nodeShell"`
// }
// // NewFeatureGates returns a new feature gate.
// func NewFeatureGates() *FeatureGates {
// return &FeatureGates{}
// }

View File

@ -47,7 +47,7 @@ func (v *ViewSetting) SortCol() (string, bool, error) {
return "", false, fmt.Errorf("invalid sort column spec: %q. must be col-name:asc|desc", v.SortColumn) return "", false, fmt.Errorf("invalid sort column spec: %q. must be col-name:asc|desc", v.SortColumn)
} }
return tt[0], tt[1] == "desc", nil return tt[0], tt[1] == "asc", nil
} }
func (v *ViewSetting) Equals(vs *ViewSetting) bool { func (v *ViewSetting) Equals(vs *ViewSetting) bool {
@ -116,8 +116,8 @@ func (v *CustomView) RemoveListener(gvr string) {
func (v *CustomView) fireConfigChanged() { func (v *CustomView) fireConfigChanged() {
for gvr, list := range v.listeners { for gvr, list := range v.listeners {
if v, ok := v.Views[gvr]; ok { if view, ok := v.Views[gvr]; ok {
list.ViewSettingsChanged(v) list.ViewSettingsChanged(view)
} else { } else {
list.ViewSettingsChanged(ViewSetting{}) list.ViewSettingsChanged(ViewSetting{})
} }

View File

@ -84,7 +84,7 @@ func (a *Alias) AsGVR(c string) (client.GVR, string, bool) {
// Get fetch a resource. // Get fetch a resource.
func (a *Alias) Get(_ context.Context, _ string) (runtime.Object, error) { func (a *Alias) Get(_ context.Context, _ string) (runtime.Object, error) {
return nil, errors.New("NYI!!") return nil, errors.New("nyi")
} }
// Ensure makes sure alias are loaded. // Ensure makes sure alias are loaded.

View File

@ -6,6 +6,7 @@ package dao
import ( import (
"context" "context"
"fmt" "fmt"
"strconv"
"github.com/derailed/k9s/internal" "github.com/derailed/k9s/internal"
"github.com/derailed/k9s/internal/client" "github.com/derailed/k9s/internal/client"
@ -22,6 +23,12 @@ var (
_ Loggable = (*Container)(nil) _ Loggable = (*Container)(nil)
) )
const (
initIDX = "I"
mainIDX = "M"
ephIDX = "E"
)
// Container represents a pod's container dao. // Container represents a pod's container dao.
type Container struct { type Container struct {
NonResource NonResource
@ -46,12 +53,15 @@ func (c *Container) List(ctx context.Context, _ string) ([]runtime.Object, error
if err != nil { if err != nil {
return nil, err return nil, err
} }
res := make([]runtime.Object, 0, len(po.Spec.InitContainers)+len(po.Spec.Containers)) res := make([]runtime.Object, 0, len(po.Spec.InitContainers)+len(po.Spec.Containers)+len(po.Spec.EphemeralContainers))
for _, co := range po.Spec.InitContainers { for i, co := range po.Spec.InitContainers {
res = append(res, makeContainerRes(co, po, cmx[co.Name], true)) res = append(res, makeContainerRes(initIDX, i, co, po, cmx[co.Name]))
} }
for _, co := range po.Spec.Containers { for i, co := range po.Spec.Containers {
res = append(res, makeContainerRes(co, po, cmx[co.Name], false)) res = append(res, makeContainerRes(mainIDX, i, co, po, cmx[co.Name]))
}
for i, co := range po.Spec.EphemeralContainers {
res = append(res, makeContainerRes(ephIDX, i, v1.Container(co.EphemeralContainerCommon), po, cmx[co.Name]))
} }
return res, nil return res, nil
@ -68,25 +78,29 @@ func (c *Container) TailLogs(ctx context.Context, opts *LogOptions) ([]LogChan,
// ---------------------------------------------------------------------------- // ----------------------------------------------------------------------------
// Helpers... // Helpers...
func makeContainerRes(co v1.Container, po *v1.Pod, cmx *mv1beta1.ContainerMetrics, isInit bool) render.ContainerRes { func makeContainerRes(kind string, idx int, co v1.Container, po *v1.Pod, cmx *mv1beta1.ContainerMetrics) render.ContainerRes {
return render.ContainerRes{ return render.ContainerRes{
Idx: kind + strconv.Itoa(idx+1),
Container: &co, Container: &co,
Status: getContainerStatus(co.Name, po.Status), Status: getContainerStatus(kind, idx, po.Status),
MX: cmx, MX: cmx,
IsInit: isInit,
Age: po.GetCreationTimestamp(), Age: po.GetCreationTimestamp(),
} }
} }
func getContainerStatus(co string, status v1.PodStatus) *v1.ContainerStatus { func getContainerStatus(kind string, idx int, status v1.PodStatus) *v1.ContainerStatus {
for _, c := range status.ContainerStatuses { switch kind {
if c.Name == co { case mainIDX:
return &c if idx < len(status.ContainerStatuses) {
return &status.ContainerStatuses[idx]
} }
} case initIDX:
for _, c := range status.InitContainerStatuses { if idx < len(status.InitContainerStatuses) {
if c.Name == co { return &status.InitContainerStatuses[idx]
return &c }
case ephIDX:
if idx < len(status.EphemeralContainerStatuses) {
return &status.EphemeralContainerStatuses[idx]
} }
} }

View File

@ -37,7 +37,7 @@ var yamlRX = regexp.MustCompile(`.*\.(yml|yaml|json)`)
func (a *Dir) List(ctx context.Context, _ string) ([]runtime.Object, error) { func (a *Dir) List(ctx context.Context, _ string) ([]runtime.Object, error) {
dir, ok := ctx.Value(internal.KeyPath).(string) dir, ok := ctx.Value(internal.KeyPath).(string)
if !ok { if !ok {
return nil, errors.New("No dir in context") return nil, errors.New("no dir in context")
} }
files, err := os.ReadDir(dir) files, err := os.ReadDir(dir)
@ -61,5 +61,5 @@ func (a *Dir) List(ctx context.Context, _ string) ([]runtime.Object, error) {
// Get fetch a resource. // Get fetch a resource.
func (a *Dir) Get(_ context.Context, _ string) (runtime.Object, error) { func (a *Dir) Get(_ context.Context, _ string) (runtime.Object, error) {
return nil, errors.New("NYI!!") return nil, errors.New("nyi")
} }

View File

@ -130,7 +130,7 @@ func (d *Deployment) TailLogs(ctx context.Context, opts *LogOptions) ([]LogChan,
return nil, err return nil, err
} }
if dp.Spec.Selector == nil || len(dp.Spec.Selector.MatchLabels) == 0 { if dp.Spec.Selector == nil || len(dp.Spec.Selector.MatchLabels) == 0 {
return nil, fmt.Errorf("No valid selector found on Deployment %s", opts.Path) return nil, fmt.Errorf("no valid selector found on deployment: %s", opts.Path)
} }
return podLogs(ctx, dp.Spec.Selector.MatchLabels, opts) return podLogs(ctx, dp.Spec.Selector.MatchLabels, opts)

View File

@ -85,7 +85,7 @@ func (j *Job) TailLogs(ctx context.Context, opts *LogOptions) ([]LogChan, error)
} }
if job.Spec.Selector == nil || len(job.Spec.Selector.MatchLabels) == 0 { if job.Spec.Selector == nil || len(job.Spec.Selector.MatchLabels) == 0 {
return nil, fmt.Errorf("No valid selector found on Job %s", opts.Path) return nil, fmt.Errorf("no valid selector found for job: %s", opts.Path)
} }
return podLogs(ctx, job.Spec.Selector.MatchLabels, opts) return podLogs(ctx, job.Spec.Selector.MatchLabels, opts)

View File

@ -53,5 +53,5 @@ func (n *NonResource) GVR() string {
// Get returns the given resource. // Get returns the given resource.
func (n *NonResource) Get(context.Context, string) (runtime.Object, error) { func (n *NonResource) Get(context.Context, string) (runtime.Object, error) {
return nil, fmt.Errorf("NYI!") return nil, fmt.Errorf("nyi")
} }

View File

@ -385,11 +385,11 @@ func readLogs(ctx context.Context, wg *sync.WaitGroup, stream io.ReadCloser, out
item = opts.ToLogItem(tview.EscapeBytes(bytes)) item = opts.ToLogItem(tview.EscapeBytes(bytes))
} else { } else {
if errors.Is(err, io.EOF) { if errors.Is(err, io.EOF) {
e := fmt.Errorf("Stream closed %w for %s", err, opts.Info()) e := fmt.Errorf("stream closed %w for %s", err, opts.Info())
item = opts.ToErrLogItem(e) item = opts.ToErrLogItem(e)
log.Warn().Err(e).Msg("log-reader EOF") log.Warn().Err(e).Msg("log-reader EOF")
} else { } else {
e := fmt.Errorf("Stream canceled %w for %s", err, opts.Info()) e := fmt.Errorf("stream canceled %w for %s", err, opts.Info())
item = opts.ToErrLogItem(e) item = opts.ToErrLogItem(e)
log.Warn().Err(e).Msg("log-reader canceled") log.Warn().Err(e).Msg("log-reader canceled")
} }
@ -439,7 +439,7 @@ func (p *Pod) SetImages(ctx context.Context, path string, imageSpecs ImageSpecs)
return err return err
} }
if isManaged { if isManaged {
return fmt.Errorf("Unable to set image. This pod is managed by %s. Please set the image on the controller", manager) return fmt.Errorf("unable to set image. This pod is managed by %s. Please set the image on the controller", manager)
} }
jsonPatch, err := GetJsonPatch(imageSpecs) jsonPatch, err := GetJsonPatch(imageSpecs)
if err != nil { if err != nil {

View File

@ -72,6 +72,11 @@ func (p *PortForwarder) Port() string {
return p.tunnel.PortMap() return p.tunnel.PortMap()
} }
// Address returns the port Address.
func (p *PortForwarder) Address() string {
return p.tunnel.Address
}
// ContainerPort returns the container port. // ContainerPort returns the container port.
func (p *PortForwarder) ContainerPort() string { func (p *PortForwarder) ContainerPort() string {
return p.tunnel.ContainerPort return p.tunnel.ContainerPort

View File

@ -24,7 +24,7 @@ type Reference struct {
func (r *Reference) List(ctx context.Context, ns string) ([]runtime.Object, error) { func (r *Reference) List(ctx context.Context, ns string) ([]runtime.Object, error) {
gvr, ok := ctx.Value(internal.KeyGVR).(client.GVR) gvr, ok := ctx.Value(internal.KeyGVR).(client.GVR)
if !ok { if !ok {
return nil, errors.New("No context GVR found") return nil, errors.New("no context for gvr found")
} }
switch gvr { switch gvr {
case SaGVR: case SaGVR:

View File

@ -11,6 +11,7 @@ import (
"github.com/derailed/k9s/internal/client" "github.com/derailed/k9s/internal/client"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
apiext "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/labels"
@ -413,11 +414,32 @@ func loadCRDs(f Factory, m ResourceMetas) {
} }
for _, o := range oo { for _, o := range oo {
meta, errs := extractMeta(o) var crd apiext.CustomResourceDefinition
if len(errs) > 0 { err = runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &crd)
log.Error().Err(errs[0]).Msgf("Fail to extract CRD meta (%d) errors", len(errs)) if err != nil {
log.Err(err).Msg("boom")
continue continue
} }
var meta metav1.APIResource
meta.Kind = crd.Spec.Names.Kind
meta.Group = crd.Spec.Group
meta.Name = crd.Name
meta.SingularName = crd.Spec.Names.Singular
meta.ShortNames = crd.Spec.Names.ShortNames
meta.Namespaced = crd.Spec.Scope == apiext.NamespaceScoped
for _, v := range crd.Spec.Versions {
if v.Served && !v.Deprecated {
meta.Version = v.Name
break
}
}
// meta, errs := extractMeta(o)
// if len(errs) > 0 {
// log.Error().Err(errs[0]).Msgf("Fail to extract CRD meta (%d) errors", len(errs))
// continue
// }
meta.Categories = append(meta.Categories, crdCat) meta.Categories = append(meta.Categories, crdCat)
gvr := client.NewGVRFromMeta(meta) gvr := client.NewGVRFromMeta(meta)
m[gvr] = meta m[gvr] = meta

View File

@ -80,7 +80,7 @@ func controllerInfo(rs *appsv1.ReplicaSet) (string, string, string, error) {
} }
return ref.Name, ref.Kind, group, nil return ref.Name, ref.Kind, group, nil
} }
return "", "", "", fmt.Errorf("Unable to find controller for ReplicaSet %s", rs.ObjectMeta.Name) return "", "", "", fmt.Errorf("unable to find controller for replicaset: %s", rs.ObjectMeta.Name)
} }
// Rollback reverses the last deployment. // Rollback reverses the last deployment.

View File

@ -152,7 +152,7 @@ func (s *StatefulSet) TailLogs(ctx context.Context, opts *LogOptions) ([]LogChan
return nil, errors.New("expecting StatefulSet resource") return nil, errors.New("expecting StatefulSet resource")
} }
if sts.Spec.Selector == nil || len(sts.Spec.Selector.MatchLabels) == 0 { if sts.Spec.Selector == nil || len(sts.Spec.Selector.MatchLabels) == 0 {
return nil, fmt.Errorf("No valid selector found on StatefulSet %s", opts.Path) return nil, fmt.Errorf("no valid selector found on statefulset: %s", opts.Path)
} }
return podLogs(ctx, sts.Spec.Selector.MatchLabels, opts) return podLogs(ctx, sts.Spec.Selector.MatchLabels, opts)

View File

@ -32,7 +32,7 @@ func (s *Service) TailLogs(ctx context.Context, opts *LogOptions) ([]LogChan, er
if err != nil { if err != nil {
return nil, err return nil, err
} }
if svc.Spec.Selector == nil || len(svc.Spec.Selector) == 0 { if len(svc.Spec.Selector) == 0 {
return nil, fmt.Errorf("no valid selector found on Service %s", opts.Path) return nil, fmt.Errorf("no valid selector found on Service %s", opts.Path)
} }

View File

@ -10,7 +10,6 @@ import (
"github.com/derailed/k9s/internal" "github.com/derailed/k9s/internal"
"github.com/derailed/k9s/internal/client" "github.com/derailed/k9s/internal/client"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/serializer" "k8s.io/apimachinery/pkg/runtime/serializer"
"k8s.io/client-go/rest" "k8s.io/client-go/rest"
@ -66,7 +65,7 @@ func (t *Table) List(ctx context.Context, ns string) ([]runtime.Object, error) {
LabelSelector: labelSel, LabelSelector: labelSel,
FieldSelector: fieldSel, FieldSelector: fieldSel,
ResourceVersion: "0", ResourceVersion: "0",
ResourceVersionMatch: v1.ResourceVersionMatchNotOlderThan, ResourceVersionMatch: metav1.ResourceVersionMatchNotOlderThan,
}, p). }, p).
Do(ctx).Get() Do(ctx).Get()
if err != nil { if err != nil {
@ -102,7 +101,7 @@ func (t *Table) getClient(f serializer.CodecFactory) (*rest.RESTClient, error) {
func (t *Table) codec() (serializer.CodecFactory, runtime.ParameterCodec) { func (t *Table) codec() (serializer.CodecFactory, runtime.ParameterCodec) {
var tt metav1.Table var tt metav1.Table
opts := metav1.TableOptions{IncludeObject: v1.IncludeObject} opts := metav1.TableOptions{IncludeObject: metav1.IncludeObject}
gv := t.gvr.GV() gv := t.gvr.GV()
metav1.AddToGroupVersion(genScheme, gv) metav1.AddToGroupVersion(genScheme, gv)
genScheme.AddKnownTypes(gv, &tt, &opts) genScheme.AddKnownTypes(gv, &tt, &opts)

View File

@ -36,6 +36,33 @@ func Test_rxFilter(t *testing.T) {
}, },
}, },
}, },
"start-rx-match": {
q: "(?i)^foo",
lines: []string{"foo", "fob", "barfoo"},
e: fuzzy.Matches{
{
Str: "(?i)^foo",
Index: 0,
MatchedIndexes: []int{0, 1, 2},
},
},
},
"end-rx-match": {
q: "foo$",
lines: []string{"foo", "fob", "barfoo"},
e: fuzzy.Matches{
{
Str: "foo$",
Index: 0,
MatchedIndexes: []int{0, 1, 2},
},
{
Str: "foo$",
Index: 2,
MatchedIndexes: []int{3, 4, 5},
},
},
},
"multiple-matches": { "multiple-matches": {
q: "foo", q: "foo",
lines: []string{"foo", "bar", "foo bar foo", "baz"}, lines: []string{"foo", "bar", "foo bar foo", "baz"},
@ -58,6 +85,7 @@ func Test_rxFilter(t *testing.T) {
}, },
}, },
} }
for k := range uu { for k := range uu {
u := uu[k] u := uu[k]
t.Run(k, func(t *testing.T) { t.Run(k, func(t *testing.T) {

View File

@ -16,7 +16,6 @@ type HeaderColumn struct {
Name string Name string
Align int Align int
Decorator DecoratorFunc Decorator DecoratorFunc
Hide bool
Wide bool Wide bool
MX bool MX bool
Time bool Time bool

View File

@ -255,7 +255,7 @@ func (r *RowEvents) FindIndex(id string) (int, bool) {
// Sort rows based on column index and order. // Sort rows based on column index and order.
func (r *RowEvents) Sort(ns string, sortCol int, isDuration, numCol, isCapacity, asc bool) { func (r *RowEvents) Sort(ns string, sortCol int, isDuration, numCol, isCapacity, asc bool) {
if sortCol == -1 { if sortCol == -1 || r == nil {
return return
} }
@ -290,6 +290,7 @@ func (r RowEventSorter) Len() int {
} }
func (r RowEventSorter) Swap(i, j int) { func (r RowEventSorter) Swap(i, j int) {
r.Events.events[i], r.Events.events[j] = r.Events.events[j], r.Events.events[i] r.Events.events[i], r.Events.events[j] = r.Events.events[j], r.Events.events[i]
} }

View File

@ -167,19 +167,22 @@ func (t *TableData) rxFilter(q string, inverse bool) (*RowEvents, error) {
return nil, fmt.Errorf("invalid rx filter %q: %w", q, err) return nil, fmt.Errorf("invalid rx filter %q: %w", q, err)
} }
ageIndex, ok := t.header.IndexOf("AGE", true) var startIndex int
if _, ok := t.header.IndexOf("NAMESPACE", true); ok && client.IsNamespaced(t.namespace) {
rr := NewRowEvents(t.RowCount() / 2) startIndex = 1
}
rr := NewRowEvents(50)
ageIndex, _ := t.header.IndexOf("AGE", true)
t.rowEvents.Range(func(_ int, re RowEvent) bool { t.rowEvents.Range(func(_ int, re RowEvent) bool {
ff := re.Row.Fields ff := re.Row.Fields[startIndex:]
if ok && ageIndex+1 <= len(ff) { if ageIndex >= 0 && ageIndex+1 <= len(ff) {
ff = append(ff[0:ageIndex], ff[ageIndex+1:]...) ff = append(ff[0:ageIndex], ff[ageIndex+1:]...)
} }
fields := strings.Join(ff, spacer) match := rx.MatchString(strings.Join(ff, spacer))
if (inverse && !rx.MatchString(fields)) || if (inverse && !match) || (!inverse && match) {
((!inverse) && rx.MatchString(fields)) {
rr.Add(re) rr.Add(re)
} }
return true return true
}) })

View File

@ -72,12 +72,12 @@ func (c Container) ColorerFunc() model1.ColorerFunc {
// Header returns a header row. // Header returns a header row.
func (Container) Header(ns string) model1.Header { func (Container) Header(ns string) model1.Header {
return model1.Header{ return model1.Header{
model1.HeaderColumn{Name: "IDX"},
model1.HeaderColumn{Name: "NAME"}, model1.HeaderColumn{Name: "NAME"},
model1.HeaderColumn{Name: "PF"}, model1.HeaderColumn{Name: "PF"},
model1.HeaderColumn{Name: "IMAGE"}, model1.HeaderColumn{Name: "IMAGE"},
model1.HeaderColumn{Name: "READY"}, model1.HeaderColumn{Name: "READY"},
model1.HeaderColumn{Name: "STATE"}, model1.HeaderColumn{Name: "STATE"},
model1.HeaderColumn{Name: "INIT"},
model1.HeaderColumn{Name: "RESTARTS", Align: tview.AlignRight}, model1.HeaderColumn{Name: "RESTARTS", Align: tview.AlignRight},
model1.HeaderColumn{Name: "PROBES(L:R:S)"}, model1.HeaderColumn{Name: "PROBES(L:R:S)"},
model1.HeaderColumn{Name: "CPU", Align: tview.AlignRight, MX: true}, model1.HeaderColumn{Name: "CPU", Align: tview.AlignRight, MX: true},
@ -109,12 +109,12 @@ func (c Container) Render(o interface{}, name string, r *model1.Row) error {
r.ID = co.Container.Name r.ID = co.Container.Name
r.Fields = model1.Fields{ r.Fields = model1.Fields{
co.Idx,
co.Container.Name, co.Container.Name,
"●", "●",
co.Container.Image, co.Container.Image,
ready, ready,
state, state,
boolToStr(co.IsInit),
restarts, restarts,
probe(co.Container.LivenessProbe) + ":" + probe(co.Container.ReadinessProbe) + ":" + probe(co.Container.StartupProbe), probe(co.Container.LivenessProbe) + ":" + probe(co.Container.ReadinessProbe) + ":" + probe(co.Container.StartupProbe),
toMc(cur.cpu), toMc(cur.cpu),
@ -241,7 +241,7 @@ type ContainerRes struct {
Container *v1.Container Container *v1.Container
Status *v1.ContainerStatus Status *v1.ContainerStatus
MX *mv1beta1.ContainerMetrics MX *mv1beta1.ContainerMetrics
IsInit bool Idx string
Age metav1.Time Age metav1.Time
} }

View File

@ -24,19 +24,18 @@ func TestContainer(t *testing.T) {
Container: makeContainer(), Container: makeContainer(),
Status: makeContainerStatus(), Status: makeContainerStatus(),
MX: makeContainerMetrics(), MX: makeContainerMetrics(),
IsInit: false,
Age: makeAge(), Age: makeAge(),
} }
var r model1.Row var r model1.Row
assert.Nil(t, c.Render(cres, "blee", &r)) assert.Nil(t, c.Render(cres, "blee", &r))
assert.Equal(t, "fred", r.ID) assert.Equal(t, "fred", r.ID)
assert.Equal(t, model1.Fields{ assert.Equal(t, model1.Fields{
"",
"fred", "fred",
"●", "●",
"img", "img",
"false", "false",
"Running", "Running",
"false",
"0", "0",
"off:off:off", "off:off:off",
"10", "10",
@ -61,7 +60,6 @@ func BenchmarkContainerRender(b *testing.B) {
Container: makeContainer(), Container: makeContainer(),
Status: makeContainerStatus(), Status: makeContainerStatus(),
MX: makeContainerMetrics(), MX: makeContainerMetrics(),
IsInit: false,
Age: makeAge(), Age: makeAge(),
} }
var r model1.Row var r model1.Row

View File

@ -39,6 +39,7 @@ func (Node) Header(string) model1.Header {
model1.HeaderColumn{Name: "ARCH", Wide: true}, model1.HeaderColumn{Name: "ARCH", Wide: true},
model1.HeaderColumn{Name: "TAINTS"}, model1.HeaderColumn{Name: "TAINTS"},
model1.HeaderColumn{Name: "VERSION"}, model1.HeaderColumn{Name: "VERSION"},
model1.HeaderColumn{Name: "OS-IMAGE", Wide: true},
model1.HeaderColumn{Name: "KERNEL", Wide: true}, model1.HeaderColumn{Name: "KERNEL", Wide: true},
model1.HeaderColumn{Name: "INTERNAL-IP", Wide: true}, model1.HeaderColumn{Name: "INTERNAL-IP", Wide: true},
model1.HeaderColumn{Name: "EXTERNAL-IP", Wide: true}, model1.HeaderColumn{Name: "EXTERNAL-IP", Wide: true},
@ -95,6 +96,7 @@ func (n Node) Render(o interface{}, ns string, r *model1.Row) error {
no.Status.NodeInfo.Architecture, no.Status.NodeInfo.Architecture,
strconv.Itoa(len(no.Spec.Taints)), strconv.Itoa(len(no.Spec.Taints)),
no.Status.NodeInfo.KubeletVersion, no.Status.NodeInfo.KubeletVersion,
no.Status.NodeInfo.OSImage,
no.Status.NodeInfo.KernelVersion, no.Status.NodeInfo.KernelVersion,
iIP, iIP,
eIP, eIP,

View File

@ -25,8 +25,8 @@ func TestNodeRender(t *testing.T) {
assert.Nil(t, err) assert.Nil(t, err)
assert.Equal(t, "minikube", r.ID) assert.Equal(t, "minikube", r.ID)
e := model1.Fields{"minikube", "Ready", "master", "amd64", "0", "v1.15.2", "4.15.0", "192.168.64.107", "<none>", "0", "10", "20", "0", "0", "4000", "7874"} e := model1.Fields{"minikube", "Ready", "master", "amd64", "0", "v1.15.2", "Buildroot 2018.05.3", "4.15.0", "192.168.64.107", "<none>", "0", "10", "20", "0", "0", "4000", "7874"}
assert.Equal(t, e, r.Fields[:16]) assert.Equal(t, e, r.Fields[:17])
} }
func BenchmarkNodeRender(b *testing.B) { func BenchmarkNodeRender(b *testing.B) {

View File

@ -194,7 +194,7 @@ func asReadinessGate(pod v1.Pod) string {
return MissingValue return MissingValue
} }
trueConditions := 0 var trueConditions int
for _, readinessGate := range pod.Spec.ReadinessGates { for _, readinessGate := range pod.Spec.ReadinessGates {
conditionType := readinessGate.ConditionType conditionType := readinessGate.ConditionType
for _, condition := range pod.Status.Conditions { for _, condition := range pod.Status.Conditions {
@ -228,7 +228,7 @@ func (p *PodWithMetrics) DeepCopyObject() runtime.Object {
func gatherCoMX(spec *v1.PodSpec, ccmx []mv1beta1.ContainerMetrics) (c, r metric) { func gatherCoMX(spec *v1.PodSpec, ccmx []mv1beta1.ContainerMetrics) (c, r metric) {
cc := make([]v1.Container, 0, len(spec.InitContainers)+len(spec.Containers)) cc := make([]v1.Container, 0, len(spec.InitContainers)+len(spec.Containers))
cc = append(cc, filterRestartableInitCO(spec.InitContainers)...) cc = append(cc, filterSidecarCO(spec.InitContainers)...)
cc = append(cc, spec.Containers...) cc = append(cc, spec.Containers...)
rcpu, rmem := cosRequests(cc) rcpu, rmem := cosRequests(cc)
@ -498,12 +498,13 @@ func restartableInitCO(p *v1.ContainerRestartPolicy) bool {
return p != nil && *p == v1.ContainerRestartPolicyAlways return p != nil && *p == v1.ContainerRestartPolicyAlways
} }
func filterRestartableInitCO(cc []v1.Container) []v1.Container { func filterSidecarCO(cc []v1.Container) []v1.Container {
rcc := make([]v1.Container, 0, len(cc)) rcc := make([]v1.Container, 0, len(cc))
for _, c := range cc { for _, c := range cc {
if c.RestartPolicy != nil && *c.RestartPolicy == v1.ContainerRestartPolicyAlways { if c.RestartPolicy != nil && *c.RestartPolicy == v1.ContainerRestartPolicyAlways {
rcc = append(rcc, c) rcc = append(rcc, c)
} }
} }
return rcc return rcc
} }

View File

@ -295,12 +295,11 @@ func Test_restartableInitCO(t *testing.T) {
} }
} }
func Test_filterRestartableInitCO(t *testing.T) { func Test_filterSidecarCO(t *testing.T) {
always := v1.ContainerRestartPolicyAlways always := v1.ContainerRestartPolicyAlways
uu := map[string]struct { uu := map[string]struct {
cc []v1.Container cc, ecc []v1.Container
ecc []v1.Container
}{ }{
"empty": { "empty": {
cc: []v1.Container{}, cc: []v1.Container{},
@ -350,7 +349,7 @@ func Test_filterRestartableInitCO(t *testing.T) {
for k := range uu { for k := range uu {
u := uu[k] u := uu[k]
t.Run(k, func(t *testing.T) { t.Run(k, func(t *testing.T) {
assert.Equal(t, u.ecc, filterRestartableInitCO(u.cc)) assert.Equal(t, u.ecc, filterSidecarCO(u.cc))
}) })
} }
} }

View File

@ -66,3 +66,7 @@ func (f fwd) Active() bool {
func (f fwd) Age() time.Time { func (f fwd) Age() time.Time {
return testTime() return testTime()
} }
func (f fwd) Address() string {
return ""
}

View File

@ -24,9 +24,12 @@ type Forwarder interface {
// Container returns a container name. // Container returns a container name.
Container() string Container() string
// Ports returns container exposed ports. // Port returns container exposed port.
Port() string Port() string
// Address returns the host address.
Address() string
// Active returns forwarder current state. // Active returns forwarder current state.
Active() bool Active() bool
@ -77,7 +80,7 @@ func (f PortForward) Render(o interface{}, gvr string, r *model1.Row) error {
trimContainer(n), trimContainer(n),
pf.Container(), pf.Container(),
pf.Port(), pf.Port(),
UrlFor(pf.Config.Host, pf.Config.Path, ports[0]), UrlFor(pf.Config.Host, pf.Config.Path, ports[0], pf.Address()),
AsThousands(int64(pf.Config.C)), AsThousands(int64(pf.Config.C)),
AsThousands(int64(pf.Config.N)), AsThousands(int64(pf.Config.N)),
"", "",
@ -100,9 +103,9 @@ func trimContainer(n string) string {
} }
// UrlFor computes fq url for a given benchmark configuration. // UrlFor computes fq url for a given benchmark configuration.
func UrlFor(host, path, port string) string { func UrlFor(host, path, port, address string) string {
if host == "" { if host == "" {
host = "localhost" host = address
} }
if path == "" { if path == "" {
path = "/" path = "/"

View File

@ -1,3 +1,6 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright Authors of K9s
package dialog package dialog
import ( import (

View File

@ -1,3 +1,6 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright Authors of K9s
package ui package ui
import ( import (

View File

@ -5,6 +5,7 @@ package ui
import ( import (
"fmt" "fmt"
"github.com/derailed/k9s/internal/model" "github.com/derailed/k9s/internal/model"
"github.com/derailed/tview" "github.com/derailed/tview"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"

View File

@ -33,6 +33,7 @@ func NewContainer(gvr client.GVR) ResourceViewer {
c.SetEnvFn(c.k9sEnv) c.SetEnvFn(c.k9sEnv)
c.GetTable().SetEnterFn(c.viewLogs) c.GetTable().SetEnterFn(c.viewLogs)
c.GetTable().SetDecorateFn(c.decorateRows) c.GetTable().SetDecorateFn(c.decorateRows)
c.GetTable().SetSortCol("IDX", true)
c.AddBindKeysFn(c.bindKeys) c.AddBindKeysFn(c.bindKeys)
c.GetTable().SetDecorateFn(c.portForwardIndicator) c.GetTable().SetDecorateFn(c.portForwardIndicator)
@ -90,6 +91,7 @@ func (c *Container) bindKeys(aa *ui.KeyActions) {
ui.KeyF: ui.NewKeyAction("Show PortForward", c.showPFCmd, true), ui.KeyF: ui.NewKeyAction("Show PortForward", c.showPFCmd, true),
ui.KeyShiftF: ui.NewKeyAction("PortForward", c.portFwdCmd, true), ui.KeyShiftF: ui.NewKeyAction("PortForward", c.portFwdCmd, true),
ui.KeyShiftT: ui.NewKeyAction("Sort Restart", c.GetTable().SortColCmd("RESTARTS", false), false), ui.KeyShiftT: ui.NewKeyAction("Sort Restart", c.GetTable().SortColCmd("RESTARTS", false), false),
ui.KeyShiftI: ui.NewKeyAction("Sort Idx", c.GetTable().SortColCmd("IDX", true), false),
}) })
aa.Merge(resourceSorters(c.GetTable())) aa.Merge(resourceSorters(c.GetTable()))
} }

View File

@ -16,5 +16,5 @@ func TestContainerNew(t *testing.T) {
assert.Nil(t, c.Init(makeCtx())) assert.Nil(t, c.Init(makeCtx()))
assert.Equal(t, "Containers", c.Name()) assert.Equal(t, "Containers", c.Name())
assert.Equal(t, 18, len(c.Hints())) assert.Equal(t, 19, len(c.Hints()))
} }

View File

@ -153,7 +153,7 @@ func showPods(app *App, path, labelSel, fieldSel string) {
} }
} }
func podCtx(app *App, path, fieldSel string) ContextFunc { func podCtx(_ *App, path, fieldSel string) ContextFunc {
return func(ctx context.Context) context.Context { return func(ctx context.Context) context.Context {
ctx = context.WithValue(ctx, internal.KeyPath, path) ctx = context.WithValue(ctx, internal.KeyPath, path)
return context.WithValue(ctx, internal.KeyFields, fieldSel) return context.WithValue(ctx, internal.KeyFields, fieldSel)

View File

@ -1,23 +1,25 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright Authors of K9s
package view package view
import ( import (
"context" "context"
"fmt" "fmt"
"github.com/derailed/k9s/internal/ui/dialog"
"github.com/rs/zerolog/log"
"github.com/derailed/tcell/v2"
"github.com/go-errors/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"github.com/derailed/k9s/internal" "github.com/derailed/k9s/internal"
"github.com/derailed/k9s/internal/client" "github.com/derailed/k9s/internal/client"
"github.com/derailed/k9s/internal/dao" "github.com/derailed/k9s/internal/dao"
"github.com/derailed/k9s/internal/render" "github.com/derailed/k9s/internal/render"
"github.com/derailed/k9s/internal/ui" "github.com/derailed/k9s/internal/ui"
"github.com/derailed/k9s/internal/ui/dialog"
"github.com/derailed/tcell/v2"
"github.com/go-errors/errors"
"github.com/rs/zerolog/log"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
) )
// OwnerExtender adds owner actions to a given viewer. // OwnerExtender adds owner actions to a given viewer.

View File

@ -54,8 +54,9 @@ func (r *Reference) gotoCmd(evt *tcell.EventKey) *tcell.EventKey {
} }
path := r.GetTable().GetSelectedItem() path := r.GetTable().GetSelectedItem()
ns, _ := client.Namespaced(path)
gvr := ui.TrimCell(r.GetTable().SelectTable, row, 2) gvr := ui.TrimCell(r.GetTable().SelectTable, row, 2)
r.App().gotoResource(client.NewGVR(gvr).R(), path, false) r.App().gotoResource(client.NewGVR(gvr).R()+" "+ns, path, false)
return evt return evt
} }

View File

@ -11,6 +11,7 @@ import (
"github.com/derailed/k9s/internal" "github.com/derailed/k9s/internal"
"github.com/derailed/k9s/internal/client" "github.com/derailed/k9s/internal/client"
"github.com/derailed/k9s/internal/config"
"github.com/derailed/k9s/internal/model" "github.com/derailed/k9s/internal/model"
"github.com/derailed/k9s/internal/render" "github.com/derailed/k9s/internal/render"
"github.com/derailed/k9s/internal/ui" "github.com/derailed/k9s/internal/ui"
@ -46,16 +47,16 @@ func (t *Table) Init(ctx context.Context) (err error) {
if t.app.Conn() != nil { if t.app.Conn() != nil {
ctx = context.WithValue(ctx, internal.KeyHasMetrics, t.app.Conn().HasMetrics()) ctx = context.WithValue(ctx, internal.KeyHasMetrics, t.app.Conn().HasMetrics())
} }
t.app.CustomView = config.NewCustomView()
ctx = context.WithValue(ctx, internal.KeyStyles, t.app.Styles) ctx = context.WithValue(ctx, internal.KeyStyles, t.app.Styles)
ctx = context.WithValue(ctx, internal.KeyViewConfig, t.app.CustomView)
t.Table.Init(ctx)
if !t.app.Config.K9s.UI.Reactive { if !t.app.Config.K9s.UI.Reactive {
if err := t.app.RefreshCustomViews(); err != nil { if err := t.app.RefreshCustomViews(); err != nil {
log.Warn().Err(err).Msg("CustomViews load failed") log.Warn().Err(err).Msg("CustomViews load failed")
t.app.Logo().Warn("Views load failed!") t.app.Logo().Warn("Views load failed!")
} }
} }
ctx = context.WithValue(ctx, internal.KeyViewConfig, t.app.CustomView)
t.Table.Init(ctx)
t.SetInputCapture(t.keyboard) t.SetInputCapture(t.keyboard)
t.bindKeys() t.bindKeys()
t.GetModel().SetRefreshRate(time.Duration(t.app.Config.K9s.GetRefreshRate()) * time.Second) t.GetModel().SetRefreshRate(time.Duration(t.app.Config.K9s.GetRefreshRate()) * time.Second)

View File

@ -79,7 +79,7 @@ func parsePath(path string) (client.GVR, string, bool) {
func (w *Workload) showRes(app *App, _ ui.Tabular, _ client.GVR, path string) { func (w *Workload) showRes(app *App, _ ui.Tabular, _ client.GVR, path string) {
gvr, fqn, ok := parsePath(path) gvr, fqn, ok := parsePath(path)
if !ok { if !ok {
app.Flash().Err(fmt.Errorf("Unable to parse path: %q", path)) app.Flash().Err(fmt.Errorf("unable to parse path: %q", path))
return return
} }
app.gotoResource(gvr.R(), fqn, false) app.gotoResource(gvr.R(), fqn, false)
@ -130,7 +130,7 @@ func (w *Workload) resourceDelete(selections []string, msg string) {
for _, sel := range selections { for _, sel := range selections {
gvr, fqn, ok := parsePath(sel) gvr, fqn, ok := parsePath(sel)
if !ok { if !ok {
w.App().Flash().Err(fmt.Errorf("Unable to parse path: %q", sel)) w.App().Flash().Err(fmt.Errorf("unable to parse path: %q", sel))
return return
} }
@ -157,7 +157,7 @@ func (w *Workload) describeCmd(evt *tcell.EventKey) *tcell.EventKey {
} }
gvr, fqn, ok := parsePath(path) gvr, fqn, ok := parsePath(path)
if !ok { if !ok {
w.App().Flash().Err(fmt.Errorf("Unable to parse path: %q", path)) w.App().Flash().Err(fmt.Errorf("unable to parse path: %q", path))
return evt return evt
} }
@ -173,7 +173,7 @@ func (w *Workload) editCmd(evt *tcell.EventKey) *tcell.EventKey {
} }
gvr, fqn, ok := parsePath(path) gvr, fqn, ok := parsePath(path)
if !ok { if !ok {
w.App().Flash().Err(fmt.Errorf("Unable to parse path: %q", path)) w.App().Flash().Err(fmt.Errorf("unable to parse path: %q", path))
return evt return evt
} }
@ -193,7 +193,7 @@ func (w *Workload) yamlCmd(evt *tcell.EventKey) *tcell.EventKey {
} }
gvr, fqn, ok := parsePath(path) gvr, fqn, ok := parsePath(path)
if !ok { if !ok {
w.App().Flash().Err(fmt.Errorf("Unable to parse path: %q", path)) w.App().Flash().Err(fmt.Errorf("unable to parse path: %q", path))
return evt return evt
} }

View File

@ -29,6 +29,9 @@ type Forwarder interface {
// Port returns the port mapping. // Port returns the port mapping.
Port() string Port() string
// Address returns the host address.
Address() string
// FQN returns the full port-forward name. // FQN returns the full port-forward name.
FQN() string FQN() string

View File

@ -173,15 +173,16 @@ func newNoOpForwarder() noOpForwarder {
return noOpForwarder{} return noOpForwarder{}
} }
func (m noOpForwarder) Start(path string, tunnel port.PortTunnel) (*portforward.PortForwarder, error) { func (noOpForwarder) Start(path string, tunnel port.PortTunnel) (*portforward.PortForwarder, error) {
return nil, nil return nil, nil
} }
func (m noOpForwarder) Stop() {} func (noOpForwarder) Stop() {}
func (m noOpForwarder) ID() string { return "" } func (noOpForwarder) ID() string { return "" }
func (m noOpForwarder) Container() string { return "" } func (noOpForwarder) Container() string { return "" }
func (m noOpForwarder) Port() string { return "" } func (noOpForwarder) Port() string { return "" }
func (m noOpForwarder) FQN() string { return "" } func (noOpForwarder) FQN() string { return "" }
func (m noOpForwarder) Active() bool { return false } func (noOpForwarder) Active() bool { return false }
func (m noOpForwarder) SetActive(bool) {} func (noOpForwarder) SetActive(bool) {}
func (m noOpForwarder) Age() time.Time { return time.Now() } func (noOpForwarder) Age() time.Time { return time.Now() }
func (m noOpForwarder) HasPortMapping(string) bool { return false } func (noOpForwarder) HasPortMapping(string) bool { return false }
func (noOpForwarder) Address() string { return "" }

View File

@ -41,7 +41,7 @@ func (p *Pod) Render(ctx context.Context, ns string, o interface{}) error {
node := NewTreeNode("v1/pods", client.FQN(po.Namespace, po.Name)) node := NewTreeNode("v1/pods", client.FQN(po.Namespace, po.Name))
parent, ok := ctx.Value(KeyParent).(*TreeNode) parent, ok := ctx.Value(KeyParent).(*TreeNode)
if !ok { if !ok {
return fmt.Errorf("Expecting a TreeNode but got %T", ctx.Value(KeyParent)) return fmt.Errorf("expecting a TreeNode but got %T", ctx.Value(KeyParent))
} }
if err := p.containerRefs(ctx, node, po.Namespace, po.Spec); err != nil { if err := p.containerRefs(ctx, node, po.Namespace, po.Spec); err != nil {
@ -95,6 +95,11 @@ func (*Pod) containerRefs(ctx context.Context, parent *TreeNode, ns string, spec
return err return err
} }
} }
for i := 0; i < len(spec.EphemeralContainers); i++ {
if err := cre.Render(ctx, ns, render.ContainerRes{Container: &spec.Containers[i]}); err != nil {
return err
}
}
return nil return nil
} }

View File

@ -1,6 +1,6 @@
name: k9s name: k9s
base: core22 base: core22
version: 'v0.32.5' version: 'v0.32.6'
summary: K9s is a CLI to view and manage your Kubernetes clusters. summary: K9s is a CLI to view and manage your Kubernetes clusters.
description: | description: |
K9s is a CLI to view and manage your Kubernetes clusters. By leveraging a terminal UI, you can easily traverse Kubernetes resources and view the state of your clusters in a single powerful session. K9s is a CLI to view and manage your Kubernetes clusters. By leveraging a terminal UI, you can easily traverse Kubernetes resources and view the state of your clusters in a single powerful session.