Rel v0.50.5 (#3332)
* update pulse view * fix #3301 - pf delete msg * clean * short header styles * fix #3294 event time cols sorting * fix #3309 label selector fault * multi arch build * fix #3328 init co count * rel notesmine
parent
f6383dfa3d
commit
ccebaa604e
|
|
@ -1,7 +1,11 @@
|
|||
# -----------------------------------------------------------------------------
|
||||
# The base image for building the k9s binary
|
||||
FROM --platform=$BUILDPLATFORM golang:1.24.2-alpine3.21 AS build
|
||||
|
||||
FROM golang:1.24.2-alpine3.21 AS build
|
||||
ARG TARGETOS
|
||||
ARG TARGETARCH
|
||||
ENV GOOS=$TARGETOS
|
||||
ENV GOARCH=$TARGETARCH
|
||||
|
||||
WORKDIR /k9s
|
||||
COPY go.mod go.sum main.go Makefile ./
|
||||
|
|
@ -12,8 +16,7 @@ RUN apk --no-cache add --update make libx11-dev git gcc libc-dev curl \
|
|||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Build the final Docker image
|
||||
|
||||
FROM alpine:3.21.3
|
||||
FROM --platform=$BUILDPLATFORM alpine:3.21.3
|
||||
ARG KUBECTL_VERSION="v1.32.2"
|
||||
|
||||
COPY --from=build /k9s/execs/k9s /bin/k9s
|
||||
|
|
|
|||
24
Makefile
24
Makefile
|
|
@ -1,19 +1,22 @@
|
|||
NAME := k9s
|
||||
VERSION ?= v0.50.5
|
||||
PACKAGE := github.com/derailed/$(NAME)
|
||||
OUTPUT_BIN ?= execs/${NAME}
|
||||
GO_FLAGS ?=
|
||||
GO_TAGS ?= netgo
|
||||
CGO_ENABLED?=0
|
||||
OUTPUT_BIN ?= execs/${NAME}
|
||||
PACKAGE := github.com/derailed/$(NAME)
|
||||
CGO_ENABLED ?=0
|
||||
GIT_REV ?= $(shell git rev-parse --short HEAD)
|
||||
|
||||
IMG_NAME := derailed/k9s
|
||||
IMAGE := ${IMG_NAME}:${VERSION}
|
||||
BUILD_PLATFORMS ?= linux/amd64,linux/arm64
|
||||
|
||||
SOURCE_DATE_EPOCH ?= $(shell date +%s)
|
||||
ifeq ($(shell uname), Darwin)
|
||||
DATE ?= $(shell TZ=UTC /bin/date -j -f "%s" ${SOURCE_DATE_EPOCH} +"%Y-%m-%dT%H:%M:%SZ")
|
||||
DATE ?= $(shell TZ=UTC date -j -f "%s" ${SOURCE_DATE_EPOCH} +"%Y-%m-%dT%H:%M:%SZ")
|
||||
else
|
||||
DATE ?= $(shell date -u -d @${SOURCE_DATE_EPOCH} +"%Y-%m-%dT%H:%M:%SZ")
|
||||
endif
|
||||
VERSION ?= v0.50.4
|
||||
IMG_NAME := derailed/k9s
|
||||
IMAGE := ${IMG_NAME}:${VERSION}
|
||||
|
||||
default: help
|
||||
|
||||
|
|
@ -32,8 +35,11 @@ build: ## Builds the CLI
|
|||
kubectl-stable-version: ## Get kubectl latest stable version
|
||||
@curl -s https://storage.googleapis.com/kubernetes-release/release/stable.txt
|
||||
|
||||
img: ## Build Docker Image
|
||||
@docker build --rm -t ${IMAGE} .
|
||||
imgx: ## Build Docker Image
|
||||
@docker buildx build --platform ${BUILD_PLATFORMS} --rm -t ${IMAGE} --load .
|
||||
|
||||
pushx: ## Push Docker image to registry
|
||||
@docker buildx build --platform ${BUILD_PLATFORMS} --rm -t ${IMAGE} --push .
|
||||
|
||||
help:
|
||||
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":[^:]*?## "}; {printf "\033[38;5;69m%-30s\033[38;5;38m %s\033[0m\n", $$1, $$2}'
|
||||
|
|
|
|||
|
|
@ -0,0 +1,45 @@
|
|||
<img src="https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s.png" align="center" width="800" height="auto"/>
|
||||
|
||||
# Release v0.50.5
|
||||
|
||||
## 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/zt-3360a389v-ElLHrb0Dp1kAXqYUItSAFA)
|
||||
|
||||
## Maintenance Release!
|
||||
|
||||
---
|
||||
|
||||
## Resolved Issues
|
||||
|
||||
* [#3328](https://github.com/derailed/k9s/issues/3328) Pod overview shows wrong number of running containers with sidecar init-container
|
||||
* [#3309](https://github.com/derailed/k9s/issues/3309) [0.50.4] k9s crashes when attempting to load logs
|
||||
* [#3301](https://github.com/derailed/k9s/issues/3301) Port Forward deleted without UI notification when forwarding to wrong port
|
||||
* [#3294](https://github.com/derailed/k9s/issues/3294) [0.50.4] k9s crashes when filtering based on labels
|
||||
* [#3278](https://github.com/derailed/k9s/issues/3278) k9s doesn't honor the --namespace parameter
|
||||
|
||||
---
|
||||
|
||||
## 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!!
|
||||
|
||||
* [#3311](https://github.com/derailed/k9s/pull/3311) Fix concurrent read writes
|
||||
* [#3310](https://github.com/derailed/k9s/pull/3310) fix: use full path of date to avoid conflict
|
||||
* [#3308](https://github.com/derailed/k9s/pull/3308) Show replicasets from deployment view
|
||||
* [#3300](https://github.com/derailed/k9s/pull/3300) fix: truncate label selector input to max length
|
||||
* [#3296](https://github.com/derailed/k9s/pull/3296) fix: update time format in logging to 24-hour format
|
||||
|
||||
---
|
||||
<img src="https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png" width="32" height="auto"/> © 2025 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)
|
||||
37
go.mod
37
go.mod
|
|
@ -26,6 +26,7 @@ require (
|
|||
github.com/spf13/cobra v1.9.1
|
||||
github.com/stretchr/testify v1.10.0
|
||||
github.com/xeipuuv/gojsonschema v1.2.0
|
||||
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394
|
||||
golang.org/x/text v0.24.0
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
helm.sh/helm/v3 v3.17.3
|
||||
|
|
@ -36,12 +37,13 @@ require (
|
|||
k8s.io/client-go v0.32.3
|
||||
k8s.io/klog/v2 v2.130.1
|
||||
k8s.io/kubectl v0.32.3
|
||||
k8s.io/kubernetes v1.33.0
|
||||
k8s.io/metrics v0.32.3
|
||||
sigs.k8s.io/yaml v1.4.0
|
||||
)
|
||||
|
||||
require (
|
||||
cel.dev/expr v0.18.0 // indirect
|
||||
cel.dev/expr v0.19.1 // indirect
|
||||
cloud.google.com/go v0.116.0 // indirect
|
||||
cloud.google.com/go/auth v0.13.0 // indirect
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.6 // indirect
|
||||
|
|
@ -116,13 +118,13 @@ require (
|
|||
github.com/containerd/containerd v1.7.27 // indirect
|
||||
github.com/containerd/containerd/api v1.8.0 // indirect
|
||||
github.com/containerd/continuity v0.4.4 // indirect
|
||||
github.com/containerd/errdefs v0.3.0 // indirect
|
||||
github.com/containerd/errdefs v1.0.0 // indirect
|
||||
github.com/containerd/fifo v1.1.0 // indirect
|
||||
github.com/containerd/log v0.1.0 // indirect
|
||||
github.com/containerd/platforms v0.2.1 // indirect
|
||||
github.com/containerd/stargz-snapshotter/estargz v0.16.3 // indirect
|
||||
github.com/containerd/ttrpc v1.2.7 // indirect
|
||||
github.com/containerd/typeurl/v2 v2.1.1 // indirect
|
||||
github.com/containerd/typeurl/v2 v2.2.2 // indirect
|
||||
github.com/cyphar/filepath-securejoin v0.4.1 // indirect
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||
github.com/deitch/magic v0.0.0-20230404182410-1ff89d7342da // indirect
|
||||
|
|
@ -171,7 +173,7 @@ require (
|
|||
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
|
||||
github.com/golang/protobuf v1.5.4 // indirect
|
||||
github.com/golang/snappy v0.0.4 // indirect
|
||||
github.com/google/btree v1.0.1 // indirect
|
||||
github.com/google/btree v1.1.3 // indirect
|
||||
github.com/google/gnostic-models v0.6.9 // indirect
|
||||
github.com/google/go-cmp v0.7.0 // indirect
|
||||
github.com/google/go-containerregistry v0.20.3 // indirect
|
||||
|
|
@ -184,7 +186,7 @@ require (
|
|||
github.com/googleapis/gax-go/v2 v2.14.1 // indirect
|
||||
github.com/gookit/color v1.5.4 // indirect
|
||||
github.com/gorilla/mux v1.8.0 // indirect
|
||||
github.com/gorilla/websocket v1.5.0 // indirect
|
||||
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 // indirect
|
||||
github.com/gosuri/uitable v0.0.4 // indirect
|
||||
github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 // indirect
|
||||
github.com/hako/durafmt v0.0.0-20210608085754-5c1018a4e16b // indirect
|
||||
|
|
@ -215,7 +217,6 @@ require (
|
|||
github.com/knqyf263/go-apk-version v0.0.0-20200609155635-041fdbb8563f // indirect
|
||||
github.com/knqyf263/go-deb-version v0.0.0-20190517075300-09fca494f03d // indirect
|
||||
github.com/knqyf263/go-rpmdb v0.1.1 // indirect
|
||||
github.com/kylelemons/godebug v1.1.0 // indirect
|
||||
github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 // indirect
|
||||
github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect
|
||||
github.com/lib/pq v1.10.9 // indirect
|
||||
|
|
@ -254,8 +255,8 @@ require (
|
|||
github.com/onsi/gomega v1.35.1 // indirect
|
||||
github.com/opencontainers/go-digest v1.0.0 // indirect
|
||||
github.com/opencontainers/image-spec v1.1.1 // indirect
|
||||
github.com/opencontainers/runtime-spec v1.1.0 // indirect
|
||||
github.com/opencontainers/selinux v1.11.0 // indirect
|
||||
github.com/opencontainers/runtime-spec v1.2.0 // indirect
|
||||
github.com/opencontainers/selinux v1.11.1 // indirect
|
||||
github.com/openvex/go-vex v0.2.5 // indirect
|
||||
github.com/owenrumney/go-sarif v1.1.2-0.20231003122901-1000f5e05554 // indirect
|
||||
github.com/package-url/packageurl-go v0.1.1 // indirect
|
||||
|
|
@ -270,9 +271,9 @@ require (
|
|||
github.com/pkg/profile v1.7.0 // indirect
|
||||
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||
github.com/prometheus/client_golang v1.19.1 // indirect
|
||||
github.com/prometheus/client_golang v1.22.0 // indirect
|
||||
github.com/prometheus/client_model v0.6.1 // indirect
|
||||
github.com/prometheus/common v0.55.0 // indirect
|
||||
github.com/prometheus/common v0.62.0 // indirect
|
||||
github.com/prometheus/procfs v0.15.1 // indirect
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||
github.com/rivo/uniseg v0.4.7 // indirect
|
||||
|
|
@ -316,21 +317,20 @@ require (
|
|||
github.com/zclconf/go-cty v1.14.0 // indirect
|
||||
go.opencensus.io v0.24.0 // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
|
||||
go.opentelemetry.io/contrib/detectors/gcp v1.29.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.54.0 // indirect
|
||||
go.opentelemetry.io/contrib/detectors/gcp v1.31.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.58.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0 // indirect
|
||||
go.opentelemetry.io/otel v1.33.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.33.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk v1.33.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk/metric v1.29.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk/metric v1.31.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.33.0 // indirect
|
||||
go.uber.org/multierr v1.11.0 // indirect
|
||||
go4.org v0.0.0-20230225012048-214862532bf5 // indirect
|
||||
golang.org/x/crypto v0.37.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 // indirect
|
||||
golang.org/x/mod v0.24.0 // indirect
|
||||
golang.org/x/net v0.39.0 // indirect
|
||||
golang.org/x/oauth2 v0.25.0 // indirect
|
||||
golang.org/x/oauth2 v0.27.0 // indirect
|
||||
golang.org/x/sync v0.13.0 // indirect
|
||||
golang.org/x/sys v0.32.0 // indirect
|
||||
golang.org/x/term v0.31.0 // indirect
|
||||
|
|
@ -341,7 +341,8 @@ require (
|
|||
google.golang.org/genproto v0.0.0-20241118233622-e639e219e697 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20241223144023-3abc09e42ca8 // indirect
|
||||
google.golang.org/grpc v1.67.3 // indirect
|
||||
google.golang.org/grpc v1.68.1 // indirect
|
||||
google.golang.org/grpc/stats/opentelemetry v0.0.0-20241028142157-ada6787961b3 // indirect
|
||||
google.golang.org/protobuf v1.36.5 // indirect
|
||||
gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect
|
||||
gopkg.in/inf.v0 v0.9.1 // indirect
|
||||
|
|
@ -357,8 +358,8 @@ require (
|
|||
modernc.org/sqlite v1.37.0 // indirect
|
||||
oras.land/oras-go v1.2.5 // indirect
|
||||
sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 // indirect
|
||||
sigs.k8s.io/kustomize/api v0.18.0 // indirect
|
||||
sigs.k8s.io/kustomize/kyaml v0.18.1 // indirect
|
||||
sigs.k8s.io/kustomize/api v0.19.0 // indirect
|
||||
sigs.k8s.io/kustomize/kyaml v0.19.0 // indirect
|
||||
sigs.k8s.io/randfill v1.0.0 // indirect
|
||||
sigs.k8s.io/structured-merge-diff/v4 v4.6.0 // indirect
|
||||
)
|
||||
|
|
|
|||
80
go.sum
80
go.sum
|
|
@ -1,5 +1,5 @@
|
|||
cel.dev/expr v0.18.0 h1:CJ6drgk+Hf96lkLikr4rFf19WrU0BOWEihyZnI2TAzo=
|
||||
cel.dev/expr v0.18.0/go.mod h1:MrpN08Q+lEBs+bGYdLxxHkZoUSsCp0nSKTs0nTymJgw=
|
||||
cel.dev/expr v0.19.1 h1:NciYrtDRIR0lNCnH1LFJegdjspNx9fI59O7TWcua/W4=
|
||||
cel.dev/expr v0.19.1/go.mod h1:MrpN08Q+lEBs+bGYdLxxHkZoUSsCp0nSKTs0nTymJgw=
|
||||
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
|
||||
|
|
@ -858,8 +858,8 @@ github.com/containerd/containerd/api v1.8.0 h1:hVTNJKR8fMc/2Tiw60ZRijntNMd1U+JVM
|
|||
github.com/containerd/containerd/api v1.8.0/go.mod h1:dFv4lt6S20wTu/hMcP4350RL87qPWLVa/OHOwmmdnYc=
|
||||
github.com/containerd/continuity v0.4.4 h1:/fNVfTJ7wIl/YPMHjf+5H32uFhl63JucB34PlCpMKII=
|
||||
github.com/containerd/continuity v0.4.4/go.mod h1:/lNJvtJKUQStBzpVQ1+rasXO1LAWtUQssk28EZvJ3nE=
|
||||
github.com/containerd/errdefs v0.3.0 h1:FSZgGOeK4yuT/+DnF07/Olde/q4KBoMsaamhXxIMDp4=
|
||||
github.com/containerd/errdefs v0.3.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M=
|
||||
github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI=
|
||||
github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M=
|
||||
github.com/containerd/fifo v1.1.0 h1:4I2mbh5stb1u6ycIABlBw9zgtlK8viPI9QkQNRQEEmY=
|
||||
github.com/containerd/fifo v1.1.0/go.mod h1:bmC4NWMbXlt2EZ0Hc7Fx7QzTFxgPID13eH0Qu+MAb2o=
|
||||
github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=
|
||||
|
|
@ -870,8 +870,8 @@ github.com/containerd/stargz-snapshotter/estargz v0.16.3 h1:7evrXtoh1mSbGj/pfRcc
|
|||
github.com/containerd/stargz-snapshotter/estargz v0.16.3/go.mod h1:uyr4BfYfOj3G9WBVE8cOlQmXAbPN9VEQpBBeJIuOipU=
|
||||
github.com/containerd/ttrpc v1.2.7 h1:qIrroQvuOL9HQ1X6KHe2ohc7p+HP/0VE6XPU7elJRqQ=
|
||||
github.com/containerd/ttrpc v1.2.7/go.mod h1:YCXHsb32f+Sq5/72xHubdiJRQY9inL4a4ZQrAbN1q9o=
|
||||
github.com/containerd/typeurl/v2 v2.1.1 h1:3Q4Pt7i8nYwy2KmQWIw2+1hTvwTE/6w9FqcttATPO/4=
|
||||
github.com/containerd/typeurl/v2 v2.1.1/go.mod h1:IDp2JFvbwZ31H8dQbEIY7sDl2L3o3HZj1hsSQlywkQ0=
|
||||
github.com/containerd/typeurl/v2 v2.2.2 h1:3jN/k2ysKuPCsln5Qv8bzR9cxal8XjkxPogJfSNO31k=
|
||||
github.com/containerd/typeurl/v2 v2.2.2/go.mod h1:95ljDnPfD3bAbDJRugOiShd/DlAAsxGtUBhJxIn7SCk=
|
||||
github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
|
||||
github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.1/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||
|
|
@ -1122,8 +1122,8 @@ github.com/gomodule/redigo v1.8.2 h1:H5XSIre1MB5NbPYFp+i1NBbb5qN1W8Y8YAQoAYbkm8k
|
|||
github.com/gomodule/redigo v1.8.2/go.mod h1:P9dn9mFrCBvWhGE1wpxx6fgq7BAeLBk+UUUzlpkBYO0=
|
||||
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||
github.com/google/btree v1.0.1 h1:gK4Kx5IaGY9CD5sPJ36FHiBJ6ZXl0kilRiiCj+jdYp4=
|
||||
github.com/google/btree v1.0.1/go.mod h1:xXMiIv4Fb/0kKde4SpL7qlzvu5cMJDRkFDxJfI9uaxA=
|
||||
github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg=
|
||||
github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4=
|
||||
github.com/google/flatbuffers v2.0.8+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8=
|
||||
github.com/google/gnostic-models v0.6.9 h1:MU/8wDLif2qCXZmzncUQ/BOfxWfthHi63KqpoNbWqVw=
|
||||
github.com/google/gnostic-models v0.6.9/go.mod h1:CiWsm0s6BSQd1hRn8/QmxqB6BesYcbSZxsz9b0KuDBw=
|
||||
|
|
@ -1217,8 +1217,8 @@ github.com/gorilla/handlers v1.5.1 h1:9lRY6j8DEeeBT10CvO9hGW0gmky0BprnvDI5vfhUHH
|
|||
github.com/gorilla/handlers v1.5.1/go.mod h1:t8XrUpc4KVXb7HGyJ4/cEnwQiaxrX/hz1Zv/4g96P1Q=
|
||||
github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
|
||||
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
|
||||
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
|
||||
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 h1:JeSE6pjso5THxAzdVpqr6/geYxZytqFMBCOtn/ujyeo=
|
||||
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674/go.mod h1:r4w70xmWCQKmi1ONH4KIaBptdivuRPyosB9RmPlGEwA=
|
||||
github.com/gosuri/uitable v0.0.4 h1:IG2xLKRvErL3uhY6e1BylFzG+aJiwQviDDTfOKeKTpY=
|
||||
github.com/gosuri/uitable v0.0.4/go.mod h1:tKR86bXuXPZazfOTG1FIzvjIdXzd0mo4Vtn16vt0PJo=
|
||||
github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 h1:+ngKgrYPPJrOjhax5N+uePQ0Fh1Z7PheYoUI/0nzkPA=
|
||||
|
|
@ -1227,8 +1227,8 @@ github.com/grpc-ecosystem/grpc-gateway v1.16.0 h1:gmcG1KaJ57LophUzW0Hy8NmPhnMZb4
|
|||
github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.7.0/go.mod h1:hgWBS7lorOAVIJEQMi4ZsPv9hVvWI6+ch50m39Pf2Ks=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.11.3/go.mod h1:o//XUCC/F+yRGJoPO/VU0GSB0f8Nhgmxx0VIRUvaC0w=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 h1:bkypFPDjIYGfCYD5mRBvpqxfYX1YCS1PXdKYWi8FsN0=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0/go.mod h1:P+Lt/0by1T8bfcF3z737NnSbmxQAppXMRziHUxPOC8k=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.24.0 h1:TmHmbvxPmaegwhDubVz0lICL0J5Ka2vwTzhoePEXsGE=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.24.0/go.mod h1:qztMSjm835F2bXf+5HKAPIS5qsmQDqZna/PgVt4rWtI=
|
||||
github.com/hako/durafmt v0.0.0-20210608085754-5c1018a4e16b h1:wDUNC2eKiL35DbLvsDhiblTUXHxcOPwQSCzi7xpQUN4=
|
||||
github.com/hako/durafmt v0.0.0-20210608085754-5c1018a4e16b/go.mod h1:VzxiSdG6j1pi7rwGm/xYI5RbtpBgM8sARDXlvEvxlu0=
|
||||
github.com/hashicorp/consul/api v1.11.0/go.mod h1:XjsvQN+RJGWI2TWy1/kqaE16HrR2J/FWgkYjdZQsX9M=
|
||||
|
|
@ -1512,10 +1512,10 @@ github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8
|
|||
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
|
||||
github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040=
|
||||
github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M=
|
||||
github.com/opencontainers/runtime-spec v1.1.0 h1:HHUyrt9mwHUjtasSbXSMvs4cyFxh+Bll4AjJ9odEGpg=
|
||||
github.com/opencontainers/runtime-spec v1.1.0/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0=
|
||||
github.com/opencontainers/selinux v1.11.0 h1:+5Zbo97w3Lbmb3PeqQtpmTkMwsW5nRI3YaLpt7tQ7oU=
|
||||
github.com/opencontainers/selinux v1.11.0/go.mod h1:E5dMC3VPuVvVHDYmi78qvhJp8+M586T4DlDRYpFkyec=
|
||||
github.com/opencontainers/runtime-spec v1.2.0 h1:z97+pHb3uELt/yiAWD691HNHQIF07bE7dzrbT927iTk=
|
||||
github.com/opencontainers/runtime-spec v1.2.0/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0=
|
||||
github.com/opencontainers/selinux v1.11.1 h1:nHFvthhM0qY8/m+vfhJylliSshm8G1jJ2jDMcgULaH8=
|
||||
github.com/opencontainers/selinux v1.11.1/go.mod h1:E5dMC3VPuVvVHDYmi78qvhJp8+M586T4DlDRYpFkyec=
|
||||
github.com/openvex/go-vex v0.2.5 h1:41utdp2rHgAGCsG+UbjmfMG5CWQxs15nGqir1eRgSrQ=
|
||||
github.com/openvex/go-vex v0.2.5/go.mod h1:j+oadBxSUELkrKh4NfNb+BPo77U3q7gdKME88IO/0Wo=
|
||||
github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde/go.mod h1:nZgzbfBr3hhjoZnS66nKrHmduYNpc34ny7RK4z5/HM0=
|
||||
|
|
@ -1570,8 +1570,8 @@ github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXP
|
|||
github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
|
||||
github.com/prometheus/client_golang v1.1.0/go.mod h1:I1FGZT9+L76gKKOs5djB6ezCbFQP1xR9D75/vuwEF3g=
|
||||
github.com/prometheus/client_golang v1.4.0/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU=
|
||||
github.com/prometheus/client_golang v1.19.1 h1:wZWJDwK+NameRJuPGDhlnFgx8e8HN3XHQeLaYJFJBOE=
|
||||
github.com/prometheus/client_golang v1.19.1/go.mod h1:mP78NwGzrVks5S2H6ab8+ZZGJLZUq1hoULYBAYBw1Ho=
|
||||
github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q=
|
||||
github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0=
|
||||
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
|
||||
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
|
|
@ -1582,8 +1582,8 @@ github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQy
|
|||
github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
|
||||
github.com/prometheus/common v0.6.0/go.mod h1:eBmuwkDJBwy6iBfxCBob6t6dR6ENT/y+J+Zk0j9GMYc=
|
||||
github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4=
|
||||
github.com/prometheus/common v0.55.0 h1:KEi6DK7lXW/m7Ig5i47x0vRzuBsHuvJdi5ee6Y3G1dc=
|
||||
github.com/prometheus/common v0.55.0/go.mod h1:2SECS4xJG1kd8XF9IcM1gMX6510RAEL65zxzNImwdc8=
|
||||
github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ2Io=
|
||||
github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I=
|
||||
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
|
||||
github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
|
||||
github.com/prometheus/procfs v0.0.3/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ=
|
||||
|
|
@ -1782,16 +1782,16 @@ go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
|
|||
go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
|
||||
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
|
||||
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
|
||||
go.opentelemetry.io/contrib/detectors/gcp v1.29.0 h1:TiaiXB4DpGD3sdzNlYQxruQngn5Apwzi1X0DRhuGvDQ=
|
||||
go.opentelemetry.io/contrib/detectors/gcp v1.29.0/go.mod h1:GW2aWZNwR2ZxDLdv8OyC2G8zkRoQBuURgV7RPQgcPoU=
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.54.0 h1:r6I7RJCN86bpD/FQwedZ0vSixDpwuWREjW9oRMsmqDc=
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.54.0/go.mod h1:B9yO6b04uB80CzjedvewuqDhxJxi11s7/GtiGa8bAjI=
|
||||
go.opentelemetry.io/contrib/detectors/gcp v1.31.0 h1:G1JQOreVrfhRkner+l4mrGxmfqYCAuy76asTDAo0xsA=
|
||||
go.opentelemetry.io/contrib/detectors/gcp v1.31.0/go.mod h1:tzQL6E1l+iV44YFTkcAeNQqzXUiekSYP9jjJjXwEd00=
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.58.0 h1:PS8wXpbyaDJQ2VDHHncMe9Vct0Zn1fEjpsjrLxGJoSc=
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.58.0/go.mod h1:HDBUsEjOuRC0EzKZ1bSaRGZWUBAzo+MhAcUUORSr4D0=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0 h1:yd02MEjBdJkG3uabWP9apV+OuWRIXGDuJEUJbOHmCFU=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0/go.mod h1:umTcuxiv1n/s/S6/c2AT/g2CQ7u5C59sHDNmfSwgz7Q=
|
||||
go.opentelemetry.io/otel v1.33.0 h1:/FerN9bax5LoK51X/sI0SVYrjSE0/yUL7DpxW4K3FWw=
|
||||
go.opentelemetry.io/otel v1.33.0/go.mod h1:SUUkR6csvUQl+yjReHu5uM3EtVV7MBm5FHKRlNx4I8I=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0 h1:3Q/xZUyC1BBkualc9ROb4G8qkH90LXEIICcs5zv1OYY=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0/go.mod h1:s75jGIWA9OfCMzF0xr+ZgfrB5FEbbV7UuYo32ahUiFI=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.33.0 h1:Vh5HayB/0HHfOQA7Ctx69E/Y/DcQSMPpKANYVMQ7fBA=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.33.0/go.mod h1:cpgtDBaqD/6ok/UG0jT15/uKjAY8mRA53diogHBg3UI=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.33.0 h1:wpMfgF8E1rkrT1Z6meFh1NDtownE9Ii3n3X2GJYjsaU=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.33.0/go.mod h1:wAy0T/dUbs468uOlkT31xjvqQgEVXv58BRFWEgn5v/0=
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.29.0 h1:WDdP9acbMYjbKIyJUhTvtzj601sVJOqgWdUxSdR/Ysc=
|
||||
|
|
@ -1800,15 +1800,15 @@ go.opentelemetry.io/otel/metric v1.33.0 h1:r+JOocAyeRVXD8lZpjdQjzMadVZp2M4WmQ+5W
|
|||
go.opentelemetry.io/otel/metric v1.33.0/go.mod h1:L9+Fyctbp6HFTddIxClbQkjtubW6O9QS3Ann/M82u6M=
|
||||
go.opentelemetry.io/otel/sdk v1.33.0 h1:iax7M131HuAm9QkZotNHEfstof92xM+N8sr3uHXc2IM=
|
||||
go.opentelemetry.io/otel/sdk v1.33.0/go.mod h1:A1Q5oi7/9XaMlIWzPSxLRWOI8nG3FnzHJNbiENQuihM=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.29.0 h1:K2CfmJohnRgvZ9UAj2/FhIf/okdWcNdBwe1m8xFXiSY=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.29.0/go.mod h1:6zZLdCl2fkauYoZIOn/soQIDSWFmNSRcICarHfuhNJQ=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.31.0 h1:i9hxxLJF/9kkvfHppyLL55aW7iIJz4JjxTeYusH7zMc=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.31.0/go.mod h1:CRInTMVvNhUKgSAMbKyTMxqOBC0zgyxzW55lZzX43Y8=
|
||||
go.opentelemetry.io/otel/trace v1.33.0 h1:cCJuF7LRjUFso9LPnEAHJDB2pqzp+hbO8eu1qqW2d/s=
|
||||
go.opentelemetry.io/otel/trace v1.33.0/go.mod h1:uIcdVUZMpTAmz0tI1z04GoVSezK37CbGV4fr1f2nBck=
|
||||
go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI=
|
||||
go.opentelemetry.io/proto/otlp v0.15.0/go.mod h1:H7XAot3MsfNsj7EXtrA2q5xSNQ10UqI405h3+duxN4U=
|
||||
go.opentelemetry.io/proto/otlp v0.19.0/go.mod h1:H7XAot3MsfNsj7EXtrA2q5xSNQ10UqI405h3+duxN4U=
|
||||
go.opentelemetry.io/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeXrui0=
|
||||
go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8=
|
||||
go.opentelemetry.io/proto/otlp v1.4.0 h1:TA9WRvW6zMwP+Ssb6fLoUIuirti1gGbP28GcKG1jgeg=
|
||||
go.opentelemetry.io/proto/otlp v1.4.0/go.mod h1:PPBWZIP98o2ElSqI35IHfu7hIhSwvc5N38Jw8pXuGFY=
|
||||
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
|
||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||
|
|
@ -2006,8 +2006,8 @@ golang.org/x/oauth2 v0.4.0/go.mod h1:RznEsdpjGAINPTOF0UH/t+xJ75L18YO3Ho6Pyn+uRec
|
|||
golang.org/x/oauth2 v0.5.0/go.mod h1:9/XBHVqLaWO3/BRHs5jbpYCnOZVjj5V0ndyaAM7KB4I=
|
||||
golang.org/x/oauth2 v0.6.0/go.mod h1:ycmewcwgD4Rpr3eZJLSB4Kyyljb3qDh40vJ8STE5HKw=
|
||||
golang.org/x/oauth2 v0.7.0/go.mod h1:hPLQkd9LyjfXTiRohC/41GhcFqxisoUQ99sCUOHO9x4=
|
||||
golang.org/x/oauth2 v0.25.0 h1:CY4y7XT9v0cRI9oupztF8AgiIu99L/ksR/Xp/6jrZ70=
|
||||
golang.org/x/oauth2 v0.25.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
|
||||
golang.org/x/oauth2 v0.27.0 h1:da9Vo7/tDv5RH/7nZDz1eMGS/q1Vv1N/7FCrBhI9I3M=
|
||||
golang.org/x/oauth2 v0.27.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
|
|
@ -2526,9 +2526,11 @@ google.golang.org/grpc v1.52.3/go.mod h1:pu6fVzoFb+NBYNAvQL08ic+lvB2IojljRYuun5v
|
|||
google.golang.org/grpc v1.53.0/go.mod h1:OnIrk0ipVdj4N5d9IUoFUx72/VlD7+jUsHwZgwSMQpw=
|
||||
google.golang.org/grpc v1.54.0/go.mod h1:PUSEXI6iWghWaB6lXM4knEgpJNu2qUcKfDtNci3EC2g=
|
||||
google.golang.org/grpc v1.56.3/go.mod h1:I9bI3vqKfayGqPUAwGdOSu7kt6oIJLixfffKrpXqQ9s=
|
||||
google.golang.org/grpc v1.67.3 h1:OgPcDAFKHnH8X3O4WcO4XUc8GRDeKsKReqbQtiCj7N8=
|
||||
google.golang.org/grpc v1.67.3/go.mod h1:YGaHCc6Oap+FzBJTZLBzkGSYt/cvGPFTPxkn7QfSU8s=
|
||||
google.golang.org/grpc v1.68.1 h1:oI5oTa11+ng8r8XMMN7jAOmWfPZWbYpCFaMUTACxkM0=
|
||||
google.golang.org/grpc v1.68.1/go.mod h1:+q1XYFJjShcqn0QZHvCyeR4CXPA+llXIeUIfIe00waw=
|
||||
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw=
|
||||
google.golang.org/grpc/stats/opentelemetry v0.0.0-20241028142157-ada6787961b3 h1:hUfOButuEtpc0UvYiaYRbNwxVYr0mQQOWq6X8beJ9Gc=
|
||||
google.golang.org/grpc/stats/opentelemetry v0.0.0-20241028142157-ada6787961b3/go.mod h1:jzYlkSMbKypzuu6xoAEijsNVo9ZeDF1u/zCfFgsx7jg=
|
||||
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
||||
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
||||
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
|
||||
|
|
@ -2614,6 +2616,8 @@ k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff h1:/usPimJzUKKu+m+TE36gUy
|
|||
k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff/go.mod h1:5jIi+8yX4RIb8wk3XwBo5Pq2ccx4FP10ohkbSKCZoK8=
|
||||
k8s.io/kubectl v0.32.3 h1:VMi584rbboso+yjfv0d8uBHwwxbC438LKq+dXd5tOAI=
|
||||
k8s.io/kubectl v0.32.3/go.mod h1:6Euv2aso5GKzo/UVMacV6C7miuyevpfI91SvBvV9Zdg=
|
||||
k8s.io/kubernetes v1.33.0 h1:BP5Y5yIzUZVeBuE/ESZvnw6TNxjXbLsCckIkljE+R0U=
|
||||
k8s.io/kubernetes v1.33.0/go.mod h1:2nWuPk0seE4+6sd0x60wQ6rYEXcV7SoeMbU0YbFm/5k=
|
||||
k8s.io/metrics v0.32.3 h1:2vsBvw0v8rIIlczZ/lZ8Kcqk9tR6Fks9h+dtFNbc2a4=
|
||||
k8s.io/metrics v0.32.3/go.mod h1:9R1Wk5cb+qJpCQon9h52mgkVCcFeYxcY+YkumfwHVCU=
|
||||
k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 h1:M3sRQVHv7vB20Xc2ybTt7ODCeFj6JSWYFzOFnYeS6Ro=
|
||||
|
|
@ -2684,10 +2688,10 @@ rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
|
|||
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
|
||||
sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 h1:/Rv+M11QRah1itp8VhT6HoVx1Ray9eB4DBr+K+/sCJ8=
|
||||
sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3/go.mod h1:18nIHnGi6636UCz6m8i4DhaJ65T6EruyzmoQqI2BVDo=
|
||||
sigs.k8s.io/kustomize/api v0.18.0 h1:hTzp67k+3NEVInwz5BHyzc9rGxIauoXferXyjv5lWPo=
|
||||
sigs.k8s.io/kustomize/api v0.18.0/go.mod h1:f8isXnX+8b+SGLHQ6yO4JG1rdkZlvhaCf/uZbLVMb0U=
|
||||
sigs.k8s.io/kustomize/kyaml v0.18.1 h1:WvBo56Wzw3fjS+7vBjN6TeivvpbW9GmRaWZ9CIVmt4E=
|
||||
sigs.k8s.io/kustomize/kyaml v0.18.1/go.mod h1:C3L2BFVU1jgcddNBE1TxuVLgS46TjObMwW5FT9FcjYo=
|
||||
sigs.k8s.io/kustomize/api v0.19.0 h1:F+2HB2mU1MSiR9Hp1NEgoU2q9ItNOaBJl0I4Dlus5SQ=
|
||||
sigs.k8s.io/kustomize/api v0.19.0/go.mod h1:/BbwnivGVcBh1r+8m3tH1VNxJmHSk1PzP5fkP6lbL1o=
|
||||
sigs.k8s.io/kustomize/kyaml v0.19.0 h1:RFge5qsO1uHhwJsu3ipV7RNolC7Uozc0jUBC/61XSlA=
|
||||
sigs.k8s.io/kustomize/kyaml v0.19.0/go.mod h1:FeKD5jEOH+FbZPpqUghBP8mrLjJ3+zD3/rf9NNu1cwY=
|
||||
sigs.k8s.io/randfill v0.0.0-20250304075658-069ef1bbf016/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY=
|
||||
sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU=
|
||||
sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY=
|
||||
|
|
|
|||
|
|
@ -6,14 +6,12 @@ package client
|
|||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/derailed/k9s/internal/slogs"
|
||||
"k8s.io/cli-runtime/pkg/genericclioptions"
|
||||
restclient "k8s.io/client-go/rest"
|
||||
"k8s.io/client-go/tools/clientcmd"
|
||||
|
|
@ -51,7 +49,6 @@ func (c *Config) CallTimeout() time.Duration {
|
|||
if err != nil {
|
||||
return DefaultCallTimeoutDuration
|
||||
}
|
||||
slog.Debug("APIServer timeout", slogs.Duration, dur)
|
||||
|
||||
return dur
|
||||
}
|
||||
|
|
|
|||
|
|
@ -37,6 +37,8 @@ var (
|
|||
PdbGVR = NewGVR("policy/v1/poddisruptionbudgets")
|
||||
PspGVR = NewGVR("policy/v1beta1/podsecuritypolicies")
|
||||
|
||||
IngGVR = NewGVR("networking.k8s.io/v1/ingresses")
|
||||
|
||||
// Metrics...
|
||||
NmxGVR = NewGVR("metrics.k8s.io/v1beta1/nodes")
|
||||
PmxGVR = NewGVR("metrics.k8s.io/v1beta1/pods")
|
||||
|
|
|
|||
|
|
@ -28,7 +28,10 @@
|
|||
"type": "object",
|
||||
"properties": {
|
||||
"fgColor": {"type": "string"},
|
||||
"sectionColor": {"type": "string"}
|
||||
"sectionColor": {"type": "string"},
|
||||
"k9sRevColor": {"type": "string"},
|
||||
"cpuColor": {"type": "string"},
|
||||
"memColor": {"type": "string"}
|
||||
}
|
||||
},
|
||||
"help": {
|
||||
|
|
|
|||
|
|
@ -185,6 +185,9 @@ type (
|
|||
Info struct {
|
||||
SectionColor Color `json:"sectionColor" yaml:"sectionColor"`
|
||||
FgColor Color `json:"fgColor" yaml:"fgColor"`
|
||||
CPUColor Color `json:"cpuColor" yaml:"cpuColor"`
|
||||
MEMColor Color `json:"memColor" yaml:"memColor"`
|
||||
K9sRevColor Color `json:"k9sRevColor" yaml:"k9sRevColor"`
|
||||
}
|
||||
|
||||
// Border tracks border styles.
|
||||
|
|
@ -242,6 +245,8 @@ type (
|
|||
DefaultDialColors Colors `json:"defaultDialColors" yaml:"defaultDialColors"`
|
||||
DefaultChartColors Colors `json:"defaultChartColors" yaml:"defaultChartColors"`
|
||||
ResourceColors map[string]Colors `json:"resourceColors" yaml:"resourceColors"`
|
||||
FocusFgColor Color `yaml:"focusFgColor"`
|
||||
FocusBgColor Color `yaml:"focusBgColor"`
|
||||
}
|
||||
)
|
||||
|
||||
|
|
@ -293,6 +298,8 @@ func newCharts() Charts {
|
|||
CPU: {Color("dodgerblue"), Color("darkslateblue")},
|
||||
MEM: {Color("yellow"), Color("goldenrod")},
|
||||
},
|
||||
FocusFgColor: "white",
|
||||
FocusBgColor: "aqua",
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -399,6 +406,9 @@ func newInfo() Info {
|
|||
return Info{
|
||||
SectionColor: "white",
|
||||
FgColor: "orange",
|
||||
CPUColor: "lawngreen",
|
||||
MEMColor: "darkturquoise",
|
||||
K9sRevColor: "aqua",
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,300 @@
|
|||
package dao
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/derailed/k9s/internal"
|
||||
"github.com/derailed/k9s/internal/client"
|
||||
"github.com/derailed/k9s/internal/slogs"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/labels"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/util/cache"
|
||||
)
|
||||
|
||||
var MxRecorder *Recorder
|
||||
|
||||
const (
|
||||
seriesCacheSize = 600
|
||||
seriesCacheExpiry = 3 * time.Hour
|
||||
seriesRecordRate = 1 * time.Minute
|
||||
nodeMetrics = "node"
|
||||
podMetrics = "pod"
|
||||
)
|
||||
|
||||
type MetricsChan chan TimeSeries
|
||||
|
||||
type TimeSeries []Point
|
||||
|
||||
type Point struct {
|
||||
Time time.Time
|
||||
Tags map[string]string
|
||||
Value client.NodeMetrics
|
||||
}
|
||||
|
||||
type Recorder struct {
|
||||
conn client.Connection
|
||||
series *cache.LRUExpireCache
|
||||
mxChan MetricsChan
|
||||
mx sync.RWMutex
|
||||
}
|
||||
|
||||
func DialRecorder(c client.Connection) *Recorder {
|
||||
if MxRecorder != nil {
|
||||
return MxRecorder
|
||||
}
|
||||
MxRecorder = &Recorder{
|
||||
conn: c,
|
||||
series: cache.NewLRUExpireCache(seriesCacheSize),
|
||||
}
|
||||
|
||||
return MxRecorder
|
||||
}
|
||||
|
||||
func ResetRecorder(c client.Connection) {
|
||||
MxRecorder = nil
|
||||
DialRecorder(c)
|
||||
}
|
||||
|
||||
func (r *Recorder) Clear() {
|
||||
r.mx.Lock()
|
||||
defer r.mx.Unlock()
|
||||
|
||||
kk := r.series.Keys()
|
||||
for _, k := range kk {
|
||||
r.series.Remove(k)
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Recorder) dispatchSeries(kind, ns string) {
|
||||
if r.mxChan == nil {
|
||||
return
|
||||
}
|
||||
kk := r.series.Keys()
|
||||
hour := time.Now().Add(-1 * time.Hour)
|
||||
ts := make(TimeSeries, 0, len(kk))
|
||||
for _, k := range kk {
|
||||
if v, ok := r.series.Get(k); ok {
|
||||
if pt, cool := v.(Point); cool {
|
||||
if pt.Tags["type"] != kind || pt.Time.Sub(hour) < 0 {
|
||||
continue
|
||||
}
|
||||
switch kind {
|
||||
case nodeMetrics:
|
||||
ts = append(ts, pt)
|
||||
case podMetrics:
|
||||
if client.IsAllNamespaces(ns) || pt.Tags["namespace"] == ns {
|
||||
ts = append(ts, pt)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(ts) > 0 {
|
||||
r.mxChan <- ts
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Recorder) Watch(ctx context.Context, ns string) MetricsChan {
|
||||
r.mx.Lock()
|
||||
if r.mxChan != nil {
|
||||
close(r.mxChan)
|
||||
r.mxChan = nil
|
||||
}
|
||||
r.mxChan = make(MetricsChan, 2)
|
||||
r.mx.Unlock()
|
||||
|
||||
go func() {
|
||||
kind := podMetrics
|
||||
if client.IsAllNamespaces(ns) {
|
||||
kind = nodeMetrics
|
||||
}
|
||||
switch kind {
|
||||
case podMetrics:
|
||||
if err := r.recordPodMetrics(ctx, ns); err != nil {
|
||||
slog.Error("Record pod metrics failed", slogs.Error, err)
|
||||
}
|
||||
case nodeMetrics:
|
||||
if err := r.recordNodeMetrics(ctx); err != nil {
|
||||
slog.Error("Record node metrics failed", slogs.Error, err)
|
||||
}
|
||||
}
|
||||
r.dispatchSeries(kind, ns)
|
||||
<-ctx.Done()
|
||||
r.mx.Lock()
|
||||
if r.mxChan != nil {
|
||||
close(r.mxChan)
|
||||
r.mxChan = nil
|
||||
}
|
||||
r.mx.Unlock()
|
||||
}()
|
||||
|
||||
return r.mxChan
|
||||
}
|
||||
|
||||
func (r *Recorder) Record(ctx context.Context) error {
|
||||
if err := r.recordNodeMetrics(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
return r.recordPodMetrics(ctx, client.NamespaceAll)
|
||||
}
|
||||
|
||||
func (r *Recorder) recordNodeMetrics(ctx context.Context) error {
|
||||
f, ok := ctx.Value(internal.KeyFactory).(Factory)
|
||||
if !ok {
|
||||
return errors.New("expecting factory in context")
|
||||
}
|
||||
nn, err := FetchNodes(ctx, f, "")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
go func() {
|
||||
r.recordClusterMetrics(ctx, nn)
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-time.After(seriesRecordRate):
|
||||
r.recordClusterMetrics(ctx, nn)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Recorder) recordClusterMetrics(ctx context.Context, nn *v1.NodeList) {
|
||||
dial := client.DialMetrics(r.conn)
|
||||
nmx, err := dial.FetchNodesMetrics(ctx)
|
||||
if err != nil {
|
||||
slog.Error("Fetch node metrics failed", slogs.Error, err)
|
||||
return
|
||||
}
|
||||
|
||||
mx := make(client.NodesMetrics, len(nn.Items))
|
||||
dial.NodesMetrics(nn, nmx, mx)
|
||||
var cmx client.NodeMetrics
|
||||
for _, m := range mx {
|
||||
cmx.CurrentCPU += m.CurrentCPU
|
||||
cmx.CurrentMEM += m.CurrentMEM
|
||||
cmx.AllocatableCPU += m.AllocatableCPU
|
||||
cmx.AllocatableMEM += m.AllocatableMEM
|
||||
cmx.TotalCPU += m.TotalCPU
|
||||
cmx.TotalMEM += m.TotalMEM
|
||||
}
|
||||
pt := Point{
|
||||
Time: time.Now(),
|
||||
Value: cmx,
|
||||
Tags: map[string]string{
|
||||
"type": nodeMetrics,
|
||||
},
|
||||
}
|
||||
if len(nn.Items) > 0 {
|
||||
r.series.Add(pt.Time, pt, seriesCacheExpiry)
|
||||
}
|
||||
r.mx.Lock()
|
||||
defer r.mx.Unlock()
|
||||
if r.mxChan != nil {
|
||||
r.mxChan <- TimeSeries{pt}
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Recorder) recordPodMetrics(ctx context.Context, ns string) error {
|
||||
go func() {
|
||||
if err := r.recordPodsMetrics(ctx, ns); err != nil {
|
||||
slog.Error("Record pod metrics failed", slogs.Error, err)
|
||||
}
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-time.After(seriesRecordRate):
|
||||
// case <-time.After(5 * time.Second):
|
||||
if err := r.recordPodsMetrics(ctx, ns); err != nil {
|
||||
slog.Error("Record pod metrics failed", slogs.Error, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Recorder) recordPodsMetrics(ctx context.Context, ns string) error {
|
||||
f, ok := ctx.Value(internal.KeyFactory).(Factory)
|
||||
if !ok {
|
||||
return errors.New("expecting factory in context")
|
||||
}
|
||||
pp, err := FetchPods(ctx, f, ns)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
pt := Point{
|
||||
Time: time.Now(),
|
||||
Value: client.NodeMetrics{},
|
||||
Tags: map[string]string{
|
||||
"namespace": ns,
|
||||
"type": podMetrics,
|
||||
},
|
||||
}
|
||||
dial := client.DialMetrics(r.conn)
|
||||
for i := range pp.Items {
|
||||
p := pp.Items[i]
|
||||
fqn := client.FQN(p.Namespace, p.Name)
|
||||
pmx, err := dial.FetchPodMetrics(ctx, fqn)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
for _, c := range pmx.Containers {
|
||||
pt.Value.CurrentCPU += c.Usage.Cpu().MilliValue()
|
||||
pt.Value.CurrentMEM += client.ToMB(c.Usage.Memory().Value())
|
||||
}
|
||||
}
|
||||
if len(pp.Items) > 0 {
|
||||
pt.Value.AllocatableCPU = pt.Value.CurrentCPU
|
||||
pt.Value.AllocatableMEM = pt.Value.CurrentMEM
|
||||
r.series.Add(pt.Time, pt, seriesCacheExpiry)
|
||||
r.mx.Lock()
|
||||
defer r.mx.Unlock()
|
||||
if r.mxChan != nil {
|
||||
r.mxChan <- TimeSeries{pt}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// FetchPods retrieves all pods in a given namespace.
|
||||
func FetchPods(_ context.Context, f Factory, ns string) (*v1.PodList, error) {
|
||||
auth, err := f.Client().CanI(ns, client.PodGVR, "pods", []string{client.ListVerb})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !auth {
|
||||
return nil, fmt.Errorf("user is not authorized to list pods")
|
||||
}
|
||||
|
||||
oo, err := f.List(client.PodGVR, ns, false, labels.Everything())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
pp := make([]v1.Pod, 0, len(oo))
|
||||
for _, o := range oo {
|
||||
var pod v1.Pod
|
||||
err = runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &pod)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
pp = append(pp, pod)
|
||||
}
|
||||
|
||||
return &v1.PodList{Items: pp}, nil
|
||||
}
|
||||
|
|
@ -98,7 +98,7 @@ func (f *Flash) Warnf(fmat string, args ...any) {
|
|||
|
||||
// Err displays an error flash message.
|
||||
func (f *Flash) Err(err error) {
|
||||
slog.Error("Flash failed", slogs.Error, err)
|
||||
slog.Error("Flash error", slogs.Error, err)
|
||||
f.SetMessage(FlashErr, err.Error())
|
||||
}
|
||||
|
||||
|
|
@ -110,7 +110,7 @@ func (f *Flash) Errf(fmat string, args ...any) {
|
|||
err = e
|
||||
}
|
||||
}
|
||||
slog.Error("Flashing error",
|
||||
slog.Error("Flash error",
|
||||
slogs.Error, err,
|
||||
slogs.Message, fmt.Sprintf(fmat, args...),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,23 +1,17 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright Authors of K9s
|
||||
|
||||
package model
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/derailed/k9s/internal"
|
||||
"github.com/derailed/k9s/internal/client"
|
||||
"github.com/derailed/k9s/internal/dao"
|
||||
"github.com/derailed/k9s/internal/health"
|
||||
"github.com/derailed/k9s/internal/slogs"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
)
|
||||
|
||||
const defaultRefreshRate = 5 * time.Second
|
||||
const defaultRefreshRate = 1 * time.Minute
|
||||
|
||||
// PulseListener represents a health model listener.
|
||||
type PulseListener interface {
|
||||
|
|
@ -26,100 +20,48 @@ type PulseListener interface {
|
|||
|
||||
// TreeFailed notifies the health check failed.
|
||||
PulseFailed(error)
|
||||
|
||||
// MetricsChanged update metrics time series.
|
||||
MetricsChanged(dao.TimeSeries)
|
||||
}
|
||||
|
||||
// Pulse tracks multiple resources health.
|
||||
type Pulse struct {
|
||||
gvr string
|
||||
gvr *client.GVR
|
||||
namespace string
|
||||
inUpdate int32
|
||||
listeners []PulseListener
|
||||
refreshRate time.Duration
|
||||
health *PulseHealth
|
||||
data health.Checks
|
||||
}
|
||||
|
||||
// NewPulse returns a new pulse.
|
||||
func NewPulse(gvr string) *Pulse {
|
||||
func NewPulse(gvr *client.GVR) *Pulse {
|
||||
return &Pulse{
|
||||
gvr: gvr,
|
||||
refreshRate: defaultRefreshRate,
|
||||
}
|
||||
}
|
||||
|
||||
type HealthChan chan HealthPoint
|
||||
|
||||
// Watch monitors pulses.
|
||||
func (p *Pulse) Watch(ctx context.Context) {
|
||||
p.Refresh(ctx)
|
||||
go p.updater(ctx)
|
||||
}
|
||||
|
||||
func (p *Pulse) updater(ctx context.Context) {
|
||||
defer slog.Debug("Pulse canceled", slogs.GVR, p.gvr)
|
||||
|
||||
rate := initRefreshRate
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-time.After(rate):
|
||||
rate = p.refreshRate
|
||||
p.refresh(ctx)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Refresh update the model now.
|
||||
func (p *Pulse) Refresh(ctx context.Context) {
|
||||
for _, d := range p.data {
|
||||
p.firePulseChanged(d)
|
||||
}
|
||||
p.refresh(ctx)
|
||||
}
|
||||
|
||||
func (p *Pulse) refresh(ctx context.Context) {
|
||||
if !atomic.CompareAndSwapInt32(&p.inUpdate, 0, 1) {
|
||||
slog.Debug("Dropping update...")
|
||||
return
|
||||
}
|
||||
defer atomic.StoreInt32(&p.inUpdate, 0)
|
||||
|
||||
if err := p.reconcile(ctx); err != nil {
|
||||
slog.Error("Reconcile failed", slogs.Error, err)
|
||||
p.firePulseFailed(err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Pulse) list(ctx context.Context) ([]runtime.Object, error) {
|
||||
func (p *Pulse) Watch(ctx context.Context) (HealthChan, dao.MetricsChan, error) {
|
||||
f, ok := ctx.Value(internal.KeyFactory).(dao.Factory)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("expected Factory in context but got %T", ctx.Value(internal.KeyFactory))
|
||||
return nil, nil, fmt.Errorf("expected Factory in context but got %T", ctx.Value(internal.KeyFactory))
|
||||
}
|
||||
if p.health == nil {
|
||||
p.health = NewPulseHealth(f)
|
||||
}
|
||||
ctx = context.WithValue(ctx, internal.KeyFields, "")
|
||||
ctx = context.WithValue(ctx, internal.KeyWithMetrics, false)
|
||||
return p.health.List(ctx, p.namespace)
|
||||
|
||||
healthChan := p.health.Watch(ctx, p.namespace)
|
||||
metricsChan := dao.DialRecorder(f.Client()).Watch(ctx, p.namespace)
|
||||
|
||||
return healthChan, metricsChan, nil
|
||||
}
|
||||
|
||||
func (p *Pulse) reconcile(ctx context.Context) error {
|
||||
oo, err := p.list(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
p.data = health.Checks{}
|
||||
for _, o := range oo {
|
||||
c, ok := o.(*health.Check)
|
||||
if !ok {
|
||||
return fmt.Errorf("expecting health check but got %T", o)
|
||||
}
|
||||
p.data = append(p.data, c)
|
||||
p.firePulseChanged(c)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
// Refresh update the model now.
|
||||
func (*Pulse) Refresh(context.Context) {}
|
||||
|
||||
// GetNamespace returns the model namespace.
|
||||
func (p *Pulse) GetNamespace() string {
|
||||
|
|
@ -128,6 +70,9 @@ func (p *Pulse) GetNamespace() string {
|
|||
|
||||
// SetNamespace sets up model namespace.
|
||||
func (p *Pulse) SetNamespace(ns string) {
|
||||
if client.IsAllNamespaces(ns) {
|
||||
ns = client.BlankNamespace
|
||||
}
|
||||
p.namespace = ns
|
||||
}
|
||||
|
||||
|
|
@ -150,15 +95,3 @@ func (p *Pulse) RemoveListener(l PulseListener) {
|
|||
p.listeners = append(p.listeners[:victim], p.listeners[victim+1:]...)
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Pulse) firePulseChanged(check *health.Check) {
|
||||
for _, l := range p.listeners {
|
||||
l.PulseChanged(check)
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Pulse) firePulseFailed(err error) {
|
||||
for _, l := range p.listeners {
|
||||
l.PulseFailed(err)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,23 +1,66 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright Authors of K9s
|
||||
|
||||
package model
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"time"
|
||||
|
||||
"github.com/derailed/k9s/internal"
|
||||
"github.com/derailed/k9s/internal/client"
|
||||
"github.com/derailed/k9s/internal/dao"
|
||||
"github.com/derailed/k9s/internal/health"
|
||||
"github.com/derailed/k9s/internal/model1"
|
||||
"github.com/derailed/k9s/internal/render"
|
||||
"github.com/derailed/k9s/internal/slogs"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
)
|
||||
|
||||
const pulseRate = 15 * time.Second
|
||||
|
||||
type HealthPoint struct {
|
||||
GVR *client.GVR
|
||||
Total, Faults int
|
||||
}
|
||||
|
||||
type GVRs []*client.GVR
|
||||
|
||||
var PulseGVRs = client.GVRs{
|
||||
client.NodeGVR,
|
||||
client.NsGVR,
|
||||
client.SvcGVR,
|
||||
client.EvGVR,
|
||||
|
||||
client.PodGVR,
|
||||
client.DpGVR,
|
||||
client.StsGVR,
|
||||
client.DsGVR,
|
||||
|
||||
client.JobGVR,
|
||||
client.CjGVR,
|
||||
client.PvGVR,
|
||||
client.PvcGVR,
|
||||
|
||||
client.HpaGVR,
|
||||
client.IngGVR,
|
||||
client.NpGVR,
|
||||
client.SaGVR,
|
||||
}
|
||||
|
||||
func (g GVRs) First() *client.GVR {
|
||||
return g[0]
|
||||
}
|
||||
|
||||
func (g GVRs) Last() *client.GVR {
|
||||
return g[len(g)-1]
|
||||
}
|
||||
|
||||
func (g GVRs) Index(gvr *client.GVR) int {
|
||||
for i := range g {
|
||||
if g[i] == gvr {
|
||||
return i
|
||||
}
|
||||
}
|
||||
|
||||
return -1
|
||||
}
|
||||
|
||||
// PulseHealth tracks resources health.
|
||||
type PulseHealth struct {
|
||||
factory dao.Factory
|
||||
|
|
@ -25,88 +68,50 @@ type PulseHealth struct {
|
|||
|
||||
// NewPulseHealth returns a new instance.
|
||||
func NewPulseHealth(f dao.Factory) *PulseHealth {
|
||||
return &PulseHealth{
|
||||
factory: f,
|
||||
}
|
||||
return &PulseHealth{factory: f}
|
||||
}
|
||||
|
||||
// List returns a canned collection of resources health.
|
||||
func (h *PulseHealth) List(ctx context.Context, ns string) ([]runtime.Object, error) {
|
||||
gvrs := []*client.GVR{
|
||||
client.PodGVR,
|
||||
client.EvGVR,
|
||||
client.RsGVR,
|
||||
client.DpGVR,
|
||||
client.StsGVR,
|
||||
client.DsGVR,
|
||||
client.CjGVR,
|
||||
client.PcGVR,
|
||||
}
|
||||
func (h *PulseHealth) Watch(ctx context.Context, ns string) HealthChan {
|
||||
c := make(HealthChan, 2)
|
||||
ctx = context.WithValue(ctx, internal.KeyWithMetrics, false)
|
||||
|
||||
hh := make([]runtime.Object, 0, 10)
|
||||
for _, gvr := range gvrs {
|
||||
c, err := h.check(ctx, ns, gvr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
go func(ctx context.Context, ns string, c HealthChan) {
|
||||
if err := h.checkPulse(ctx, ns, c); err != nil {
|
||||
slog.Error("Pulse check failed", slogs.Error, err)
|
||||
}
|
||||
hh = append(hh, c)
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
close(c)
|
||||
return
|
||||
case <-time.After(pulseRate):
|
||||
if err := h.checkPulse(ctx, ns, c); err != nil {
|
||||
slog.Error("Pulse check failed", slogs.Error, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}(ctx, ns, c)
|
||||
|
||||
mm, err := h.checkMetrics(ctx)
|
||||
if err != nil {
|
||||
return hh, err
|
||||
}
|
||||
for _, m := range mm {
|
||||
hh = append(hh, m)
|
||||
}
|
||||
|
||||
return hh, nil
|
||||
return c
|
||||
}
|
||||
|
||||
func (h *PulseHealth) checkMetrics(ctx context.Context) (health.Checks, error) {
|
||||
dial := client.DialMetrics(h.factory.Client())
|
||||
|
||||
nn, err := dao.FetchNodes(ctx, h.factory, "")
|
||||
func (h *PulseHealth) checkPulse(ctx context.Context, ns string, c HealthChan) error {
|
||||
for _, gvr := range PulseGVRs {
|
||||
check, err := h.check(ctx, ns, gvr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return err
|
||||
}
|
||||
|
||||
nmx, err := dial.FetchNodesMetrics(ctx)
|
||||
if err != nil {
|
||||
slog.Error("Fetching metrics", slogs.Error, err)
|
||||
return nil, err
|
||||
c <- check
|
||||
}
|
||||
|
||||
mx := make(client.NodesMetrics, len(nn.Items))
|
||||
dial.NodesMetrics(nn, nmx, mx)
|
||||
|
||||
var ccpu, cmem, acpu, amem, tcpu, tmem int64
|
||||
for _, m := range mx {
|
||||
ccpu += m.CurrentCPU
|
||||
cmem += m.CurrentMEM
|
||||
acpu += m.AllocatableCPU
|
||||
amem += m.AllocatableMEM
|
||||
tcpu += m.TotalCPU
|
||||
tmem += m.TotalMEM
|
||||
}
|
||||
c1 := health.NewCheck(client.CpuGVR)
|
||||
c1.Set(health.S1, ccpu)
|
||||
c1.Set(health.S2, acpu)
|
||||
c1.Set(health.S3, tcpu)
|
||||
c2 := health.NewCheck(client.MemGVR)
|
||||
c2.Set(health.S1, cmem)
|
||||
c2.Set(health.S2, amem)
|
||||
c2.Set(health.S3, tmem)
|
||||
|
||||
return health.Checks{c1, c2}, nil
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *PulseHealth) check(ctx context.Context, ns string, gvr *client.GVR) (*health.Check, error) {
|
||||
func (h *PulseHealth) check(ctx context.Context, ns string, gvr *client.GVR) (HealthPoint, error) {
|
||||
meta, ok := Registry[gvr]
|
||||
if !ok {
|
||||
meta = ResourceMeta{
|
||||
DAO: new(dao.Table),
|
||||
Renderer: new(render.Table),
|
||||
DAO: &dao.Table{},
|
||||
Renderer: &render.Generic{},
|
||||
}
|
||||
}
|
||||
if meta.DAO == nil {
|
||||
|
|
@ -116,42 +121,13 @@ func (h *PulseHealth) check(ctx context.Context, ns string, gvr *client.GVR) (*h
|
|||
meta.DAO.Init(h.factory, gvr)
|
||||
oo, err := meta.DAO.List(ctx, ns)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return HealthPoint{}, err
|
||||
}
|
||||
c := health.NewCheck(gvr)
|
||||
|
||||
if meta.Renderer.IsGeneric() {
|
||||
table, ok := oo[0].(*metav1.Table)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("expecting a meta table but got %T", oo[0])
|
||||
c := HealthPoint{GVR: gvr, Total: len(oo)}
|
||||
for _, o := range oo {
|
||||
if err := meta.Renderer.Healthy(ctx, o); err != nil {
|
||||
c.Faults++
|
||||
}
|
||||
rows := make(model1.Rows, len(table.Rows))
|
||||
re, _ := meta.Renderer.(model1.Generic)
|
||||
re.SetTable(ns, table)
|
||||
for i, row := range table.Rows {
|
||||
if err := re.Render(row, ns, &rows[i]); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !model1.IsValid(ns, re.Header(ns), rows[i]) {
|
||||
c.Inc(health.S2)
|
||||
continue
|
||||
}
|
||||
c.Inc(health.S1)
|
||||
}
|
||||
c.Total(int64(len(table.Rows)))
|
||||
return c, nil
|
||||
}
|
||||
c.Total(int64(len(oo)))
|
||||
rr, re := make(model1.Rows, len(oo)), meta.Renderer
|
||||
for i, o := range oo {
|
||||
if err := re.Render(o, ns, &rr[i]); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !model1.IsValid(ns, re.Header(ns), rr[i]) {
|
||||
c.Inc(health.S2)
|
||||
continue
|
||||
}
|
||||
c.Inc(health.S1)
|
||||
}
|
||||
|
||||
return c, nil
|
||||
|
|
|
|||
|
|
@ -123,6 +123,9 @@ var Registry = map[*client.GVR]ResourceMeta{
|
|||
client.PvcGVR: {
|
||||
Renderer: new(render.PersistentVolumeClaim),
|
||||
},
|
||||
client.EvGVR: {
|
||||
Renderer: new(render.Event),
|
||||
},
|
||||
|
||||
// Apps...
|
||||
client.DpGVR: {
|
||||
|
|
|
|||
|
|
@ -4,6 +4,8 @@
|
|||
package model1
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/derailed/k9s/internal/config"
|
||||
"github.com/derailed/tcell/v2"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
|
|
@ -50,6 +52,9 @@ type Renderer interface {
|
|||
|
||||
// SetViewSetting sets custom view settings if any.
|
||||
SetViewSetting(vs *config.ViewSetting)
|
||||
|
||||
// Healthy checks if the resource is healthy.
|
||||
Healthy(ctx context.Context, o any) error
|
||||
}
|
||||
|
||||
// Generic represents a generic resource.
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
package render
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log/slog"
|
||||
|
||||
"github.com/derailed/k9s/internal/config"
|
||||
|
|
@ -64,3 +65,8 @@ func (*Base) ColorerFunc() model1.ColorerFunc {
|
|||
func (*Base) Happy(string, *model1.Row) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// Healthy checks if the resource is healthy.
|
||||
func (*Base) Healthy(context.Context, any) error {
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
package render
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
|
|
@ -22,6 +23,11 @@ func (Dir) IsGeneric() bool {
|
|||
return false
|
||||
}
|
||||
|
||||
// Healthy checks if the resource is healthy.
|
||||
func (Dir) Healthy(context.Context, any) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// ColorerFunc colors a resource row.
|
||||
func (Dir) ColorerFunc() model1.ColorerFunc {
|
||||
return func(string, model1.Header, *model1.RowEvent) tcell.Color {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,39 @@
|
|||
// Copyright Authors of K9s
|
||||
|
||||
package render
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
|
||||
"github.com/derailed/k9s/internal/slogs"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
api "k8s.io/kubernetes/pkg/apis/core"
|
||||
)
|
||||
|
||||
// Event renders a event resource to screen.
|
||||
type Event struct {
|
||||
Table
|
||||
}
|
||||
|
||||
// Healthy checks component health.
|
||||
func (*Event) Healthy(_ context.Context, o any) error {
|
||||
u, ok := o.(*unstructured.Unstructured)
|
||||
if !ok {
|
||||
slog.Error("Expected Unstructured", slogs.Type, fmt.Sprintf("%T", o))
|
||||
return nil
|
||||
}
|
||||
var ev api.Event
|
||||
err := runtime.DefaultUnstructuredConverter.FromUnstructured(u.Object, &ev)
|
||||
if err != nil {
|
||||
slog.Error("Failed to convert unstructured to Node", slogs.Error, err)
|
||||
return nil
|
||||
}
|
||||
if ev.Type != "Normal" {
|
||||
return fmt.Errorf("event is not normal: %s", ev.Type)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
@ -4,13 +4,16 @@
|
|||
package helm
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"strconv"
|
||||
|
||||
"github.com/derailed/k9s/internal/client"
|
||||
"github.com/derailed/k9s/internal/config"
|
||||
"github.com/derailed/k9s/internal/model1"
|
||||
"github.com/derailed/k9s/internal/render"
|
||||
"github.com/derailed/k9s/internal/slogs"
|
||||
"helm.sh/helm/v3/pkg/release"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
|
|
@ -68,6 +71,16 @@ func (c Chart) Render(o any, _ string, r *model1.Row) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// Healthy checks component health.
|
||||
func (c Chart) Healthy(_ context.Context, o any) error {
|
||||
h, ok := o.(*ReleaseRes)
|
||||
if !ok {
|
||||
slog.Error("Expected *ReleaseRes, but got", slogs.Type, fmt.Sprintf("%T", o))
|
||||
}
|
||||
|
||||
return c.diagnose(h.Release.Info.Status.String())
|
||||
}
|
||||
|
||||
func (Chart) diagnose(s string) error {
|
||||
if s != "deployed" {
|
||||
return fmt.Errorf("chart is in an invalid state")
|
||||
|
|
|
|||
|
|
@ -17,11 +17,6 @@ import (
|
|||
// History renders a History chart to screen.
|
||||
type History struct{}
|
||||
|
||||
// Healthy checks component health.
|
||||
func (History) Healthy(context.Context, any) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (History) SetViewSetting(*config.ViewSetting) {}
|
||||
|
||||
// IsGeneric identifies a generic handler.
|
||||
|
|
@ -67,6 +62,11 @@ func (c History) Render(o any, _ string, r *model1.Row) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// Healthy checks component health.
|
||||
func (History) Healthy(context.Context, any) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (History) diagnose(string) error {
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,14 +4,17 @@
|
|||
package render
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/derailed/k9s/internal/client"
|
||||
"github.com/derailed/k9s/internal/model1"
|
||||
"github.com/derailed/k9s/internal/slogs"
|
||||
"github.com/derailed/tview"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
|
|
@ -130,6 +133,25 @@ func (n Node) defaultRow(nwm *NodeWithMetrics, r *model1.Row) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// Healthy checks component health.
|
||||
func (n Node) Healthy(_ context.Context, o any) error {
|
||||
nwm, ok := o.(*NodeWithMetrics)
|
||||
if !ok {
|
||||
slog.Error("Expected *NodeWithMetrics", slogs.Type, fmt.Sprintf("%T", o))
|
||||
return nil
|
||||
}
|
||||
var no v1.Node
|
||||
err := runtime.DefaultUnstructuredConverter.FromUnstructured(nwm.Raw.Object, &no)
|
||||
if err != nil {
|
||||
slog.Error("Failed to convert unstructured to Node", slogs.Error, err)
|
||||
return nil
|
||||
}
|
||||
ss := make([]string, 10)
|
||||
status(no.Status.Conditions, no.Spec.Unschedulable, ss)
|
||||
|
||||
return n.diagnose(ss)
|
||||
}
|
||||
|
||||
func (Node) diagnose(ss []string) error {
|
||||
if len(ss) == 0 {
|
||||
return nil
|
||||
|
|
|
|||
|
|
@ -4,13 +4,16 @@
|
|||
package render
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/derailed/k9s/internal/client"
|
||||
"github.com/derailed/k9s/internal/model1"
|
||||
"github.com/derailed/k9s/internal/slogs"
|
||||
"github.com/derailed/tcell/v2"
|
||||
"golang.org/x/exp/slog"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
|
|
@ -93,6 +96,23 @@ func (n Namespace) defaultRow(raw *unstructured.Unstructured, r *model1.Row) err
|
|||
return nil
|
||||
}
|
||||
|
||||
// Healthy checks component health.
|
||||
func (n Namespace) Healthy(_ context.Context, o any) error {
|
||||
res, ok := o.(*unstructured.Unstructured)
|
||||
if !ok {
|
||||
slog.Error("Expected *Unstructured, but got", slogs.Type, fmt.Sprintf("%T", o))
|
||||
return nil
|
||||
}
|
||||
var ns v1.Namespace
|
||||
err := runtime.DefaultUnstructuredConverter.FromUnstructured(res.Object, &ns)
|
||||
if err != nil {
|
||||
slog.Error("Failed to convert Unstructured to Namespace", slogs.Type, fmt.Sprintf("%T", o), slog.String("error", err.Error()))
|
||||
return nil
|
||||
}
|
||||
|
||||
return n.diagnose(ns.Status.Phase)
|
||||
}
|
||||
|
||||
func (Namespace) diagnose(phase v1.NamespacePhase) error {
|
||||
if phase != v1.NamespaceActive && phase != v1.NamespaceTerminating {
|
||||
return errors.New("namespace not ready")
|
||||
|
|
|
|||
|
|
@ -4,12 +4,15 @@
|
|||
package render
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/derailed/k9s/internal/client"
|
||||
"github.com/derailed/k9s/internal/model1"
|
||||
"github.com/derailed/k9s/internal/slogs"
|
||||
"github.com/derailed/tcell/v2"
|
||||
"github.com/derailed/tview"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
|
|
@ -157,6 +160,10 @@ func (p *Pod) defaultRow(pwm *PodWithMetrics, row *model1.Row) error {
|
|||
_, _, irc, _ := p.Statuses(st.InitContainerStatuses)
|
||||
cr, _, rc, lr := p.Statuses(st.ContainerStatuses)
|
||||
|
||||
rcr, rcc := p.initContainerCounts(spec.InitContainers, st.InitContainerStatuses)
|
||||
cr += rcr
|
||||
cc := len(spec.Containers) + rcc
|
||||
|
||||
var ccmx []mv1beta1.ContainerMetrics
|
||||
if pwm.MX != nil {
|
||||
ccmx = pwm.MX.Containers
|
||||
|
|
@ -172,7 +179,7 @@ func (p *Pod) defaultRow(pwm *PodWithMetrics, row *model1.Row) error {
|
|||
n,
|
||||
computeVulScore(ns, pwm.Raw.GetLabels(), spec),
|
||||
"●",
|
||||
strconv.Itoa(cr) + "/" + strconv.Itoa(len(spec.Containers)),
|
||||
strconv.Itoa(cr) + "/" + strconv.Itoa(cc),
|
||||
phase,
|
||||
strconv.Itoa(rc + irc),
|
||||
ToAge(lr),
|
||||
|
|
@ -191,13 +198,41 @@ func (p *Pod) defaultRow(pwm *PodWithMetrics, row *model1.Row) error {
|
|||
asReadinessGate(spec, &st),
|
||||
p.mapQOS(st.QOSClass),
|
||||
mapToStr(pwm.Raw.GetLabels()),
|
||||
AsStatus(p.diagnose(phase, cr, len(st.ContainerStatuses))),
|
||||
AsStatus(p.diagnose(phase, cr, cc)),
|
||||
ToAge(pwm.Raw.GetCreationTimestamp()),
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Healthy checks component health.
|
||||
func (p Pod) Healthy(_ context.Context, o any) error {
|
||||
pwm, ok := o.(*PodWithMetrics)
|
||||
if !ok {
|
||||
slog.Error("Expected *PodWithMetrics", slogs.Type, fmt.Sprintf("%T", o))
|
||||
return nil
|
||||
}
|
||||
var st v1.PodStatus
|
||||
if err := runtime.DefaultUnstructuredConverter.FromUnstructured(pwm.Raw.Object["status"].(map[string]any), &st); err != nil {
|
||||
slog.Error("Failed to convert unstructured to PodState", slogs.Error, err)
|
||||
return nil
|
||||
}
|
||||
spec := new(v1.PodSpec)
|
||||
if err := runtime.DefaultUnstructuredConverter.FromUnstructured(pwm.Raw.Object["spec"].(map[string]any), spec); err != nil {
|
||||
slog.Error("Failed to convert unstructured to PodSpec", slogs.Error, err)
|
||||
return nil
|
||||
}
|
||||
dt := pwm.Raw.GetDeletionTimestamp()
|
||||
phase := p.Phase(dt, spec, &st)
|
||||
cr, _, _, _ := p.Statuses(st.ContainerStatuses)
|
||||
|
||||
rcr, rcc := p.initContainerCounts(spec.InitContainers, st.InitContainerStatuses)
|
||||
cr += rcr
|
||||
cc := len(spec.Containers) + rcc
|
||||
|
||||
return p.diagnose(phase, cr, cc)
|
||||
}
|
||||
|
||||
func (*Pod) diagnose(phase string, cr, ct int) error {
|
||||
if phase == Completed {
|
||||
return nil
|
||||
|
|
@ -358,6 +393,19 @@ func (*Pod) Statuses(cc []v1.ContainerStatus) (cr, ct, rc int, latest metav1.Tim
|
|||
return
|
||||
}
|
||||
|
||||
func (*Pod) initContainerCounts(cc []v1.Container, cos []v1.ContainerStatus) (ready, total int) {
|
||||
for i := range cos {
|
||||
if !restartableInitCO(cc[i].RestartPolicy) {
|
||||
continue
|
||||
}
|
||||
total++
|
||||
if cos[i].Ready {
|
||||
ready++
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Phase reports the given pod phase.
|
||||
func (p *Pod) Phase(dt *metav1.Time, spec *v1.PodSpec, st *v1.PodStatus) string {
|
||||
status := string(st.Phase)
|
||||
|
|
|
|||
|
|
@ -211,7 +211,7 @@ func TestPodSidecarRender(t *testing.T) {
|
|||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, "default/sleep", r.ID)
|
||||
e := model1.Fields{"default", "sleep", "0", "●", "1/1", "Running", "0", "<unknown>", "100", "40", "50:250", "50:80", "200", "40", "80", "50", "10.244.0.8", "kind-control-plane", "default", "<none>"}
|
||||
e := model1.Fields{"default", "sleep", "0", "●", "2/2", "Running", "0", "<unknown>", "100", "40", "50:250", "50:80", "200", "40", "80", "50", "10.244.0.8", "kind-control-plane", "default", "<none>"}
|
||||
assert.Equal(t, e, r.Fields[:20])
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -14,10 +14,13 @@ import (
|
|||
"github.com/derailed/k9s/internal/model1"
|
||||
"k8s.io/apimachinery/pkg/api/meta"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/util/sets"
|
||||
)
|
||||
|
||||
const ageTableCol = "Age"
|
||||
|
||||
var ageCols = sets.New("Last Seen", "First Seen", "Age")
|
||||
|
||||
// Table renders a tabular resource to screen.
|
||||
type Table struct {
|
||||
Base
|
||||
|
|
@ -70,7 +73,8 @@ func (t *Table) defaultHeader() model1.Header {
|
|||
t.setAgeIndex(i)
|
||||
continue
|
||||
}
|
||||
h = append(h, model1.HeaderColumn{Name: strings.ToUpper(c.Name)})
|
||||
timeCol := ageCols.Has(c.Name)
|
||||
h = append(h, model1.HeaderColumn{Name: strings.ToUpper(c.Name), Attrs: model1.Attrs{Time: timeCol}})
|
||||
}
|
||||
if t.getAgeIndex() > 0 {
|
||||
h = append(h, model1.HeaderColumn{Name: "AGE", Attrs: model1.Attrs{Time: true}})
|
||||
|
|
|
|||
|
|
@ -0,0 +1,72 @@
|
|||
package render
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/derailed/k9s/internal/model1"
|
||||
"github.com/stretchr/testify/assert"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
func Test_defaultHeader(t *testing.T) {
|
||||
uu := map[string]struct {
|
||||
cdefs []metav1.TableColumnDefinition
|
||||
e model1.Header
|
||||
}{
|
||||
"empty": {
|
||||
e: make(model1.Header, 0),
|
||||
},
|
||||
|
||||
"plain": {
|
||||
cdefs: []metav1.TableColumnDefinition{
|
||||
{Name: "A"},
|
||||
{Name: "B"},
|
||||
{Name: "C"},
|
||||
},
|
||||
e: model1.Header{
|
||||
model1.HeaderColumn{Name: "A"},
|
||||
model1.HeaderColumn{Name: "B"},
|
||||
model1.HeaderColumn{Name: "C"},
|
||||
},
|
||||
},
|
||||
|
||||
"age": {
|
||||
cdefs: []metav1.TableColumnDefinition{
|
||||
{Name: "Fred"},
|
||||
{Name: "Blee"},
|
||||
{Name: "Age"},
|
||||
},
|
||||
e: model1.Header{
|
||||
model1.HeaderColumn{Name: "FRED"},
|
||||
model1.HeaderColumn{Name: "BLEE"},
|
||||
model1.HeaderColumn{Name: "AGE", Attrs: model1.Attrs{Time: true}},
|
||||
},
|
||||
},
|
||||
|
||||
"time-cols": {
|
||||
cdefs: []metav1.TableColumnDefinition{
|
||||
{Name: "Last Seen"},
|
||||
{Name: "Fred"},
|
||||
{Name: "Blee"},
|
||||
{Name: "Age"},
|
||||
{Name: "First Seen"},
|
||||
},
|
||||
e: model1.Header{
|
||||
model1.HeaderColumn{Name: "LAST SEEN", Attrs: model1.Attrs{Time: true}},
|
||||
model1.HeaderColumn{Name: "FRED"},
|
||||
model1.HeaderColumn{Name: "BLEE"},
|
||||
model1.HeaderColumn{Name: "FIRST SEEN", Attrs: model1.Attrs{Time: true}},
|
||||
model1.HeaderColumn{Name: "AGE", Attrs: model1.Attrs{Time: true}},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for k := range uu {
|
||||
u := uu[k]
|
||||
t.Run(k, func(t *testing.T) {
|
||||
var ta Table
|
||||
ta.SetTable("ns-1", &metav1.Table{ColumnDefinitions: u.cdefs})
|
||||
assert.Equal(t, u.e, ta.defaultHeader())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -218,4 +218,7 @@ const (
|
|||
|
||||
// Duration tracks a duration logger key.
|
||||
Duration = "duration"
|
||||
|
||||
// Type tracks a type logger key.
|
||||
Type = "type"
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,6 +1,3 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright Authors of K9s
|
||||
|
||||
package tchart
|
||||
|
||||
import (
|
||||
|
|
@ -11,11 +8,6 @@ import (
|
|||
"github.com/derailed/tview"
|
||||
)
|
||||
|
||||
const (
|
||||
okColor, faultColor = tcell.ColorPaleGreen, tcell.ColorOrangeRed
|
||||
okColorName, faultColorName = "palegreen", "orangered"
|
||||
)
|
||||
|
||||
// Component represents a graphic component.
|
||||
type Component struct {
|
||||
*tview.Box
|
||||
|
|
@ -24,8 +16,7 @@ type Component struct {
|
|||
focusFgColor, focusBgColor string
|
||||
seriesColors []tcell.Color
|
||||
dimmed tcell.Style
|
||||
id string
|
||||
legend string
|
||||
id, legend string
|
||||
blur func(tcell.Key)
|
||||
mx sync.RWMutex
|
||||
}
|
||||
|
|
@ -36,7 +27,11 @@ func NewComponent(id string) *Component {
|
|||
Box: tview.NewBox(),
|
||||
id: id,
|
||||
noColor: tcell.ColorDefault,
|
||||
seriesColors: []tcell.Color{tview.Styles.PrimaryTextColor, tview.Styles.FocusColor},
|
||||
seriesColors: []tcell.Color{
|
||||
tcell.ColorGreen,
|
||||
tcell.ColorOrange,
|
||||
tcell.ColorOrangeRed,
|
||||
},
|
||||
dimmed: tcell.StyleDefault.Background(tview.Styles.PrimitiveBackgroundColor).Foreground(tcell.ColorGray).Dim(true),
|
||||
}
|
||||
}
|
||||
|
|
@ -48,6 +43,8 @@ func (c *Component) SetFocusColorNames(fg, bg string) {
|
|||
|
||||
// SetBackgroundColor sets the graph bg color.
|
||||
func (c *Component) SetBackgroundColor(color tcell.Color) {
|
||||
c.mx.Lock()
|
||||
defer c.mx.Unlock()
|
||||
c.Box.SetBackgroundColor(color)
|
||||
c.bgColor = color
|
||||
c.dimmed = c.dimmed.Background(color)
|
||||
|
|
@ -68,7 +65,6 @@ func (c *Component) SetLegend(l string) {
|
|||
// InputHandler returns the handler for this primitive.
|
||||
func (c *Component) InputHandler() func(event *tcell.EventKey, setFocus func(p tview.Primitive)) {
|
||||
return c.WrapInputHandler(func(event *tcell.EventKey, setFocus func(p tview.Primitive)) {
|
||||
//nolint:exhaustive
|
||||
switch key := event.Key(); key {
|
||||
case tcell.KeyEnter:
|
||||
case tcell.KeyBacktab, tcell.KeyTab:
|
||||
|
|
@ -80,7 +76,7 @@ func (c *Component) InputHandler() func(event *tcell.EventKey, setFocus func(p t
|
|||
})
|
||||
}
|
||||
|
||||
// IsDial returns true if chart is a dial.
|
||||
// IsDial returns true if chart is a dial
|
||||
func (*Component) IsDial() bool {
|
||||
return false
|
||||
}
|
||||
|
|
@ -103,6 +99,9 @@ func (c *Component) GetSeriesColorNames() []string {
|
|||
c.mx.RLock()
|
||||
defer c.mx.RUnlock()
|
||||
|
||||
if len(c.seriesColors) < 3 {
|
||||
return []string{"green", "orange", "red"}
|
||||
}
|
||||
nn := make([]string, 0, len(c.seriesColors))
|
||||
for _, color := range c.seriesColors {
|
||||
for name, co := range tcell.ColorNames {
|
||||
|
|
@ -111,21 +110,14 @@ func (c *Component) GetSeriesColorNames() []string {
|
|||
}
|
||||
}
|
||||
}
|
||||
if len(nn) < 2 {
|
||||
nn = append(nn, okColorName, faultColorName)
|
||||
}
|
||||
|
||||
return nn
|
||||
}
|
||||
|
||||
func (c *Component) colorForSeries() (cool, fault tcell.Color) {
|
||||
func (c *Component) colorForSeries() []tcell.Color {
|
||||
c.mx.RLock()
|
||||
defer c.mx.RUnlock()
|
||||
if len(c.seriesColors) == 2 {
|
||||
return c.seriesColors[0], c.seriesColors[1]
|
||||
}
|
||||
|
||||
return okColor, faultColor
|
||||
return c.seriesColors
|
||||
}
|
||||
|
||||
func (c *Component) asRect() image.Rectangle {
|
||||
|
|
|
|||
|
|
@ -1,13 +1,10 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright Authors of K9s
|
||||
|
||||
package tchart
|
||||
|
||||
import (
|
||||
"image"
|
||||
"testing"
|
||||
|
||||
"github.com/derailed/tview"
|
||||
"github.com/derailed/tcell/v2"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
|
|
@ -20,9 +17,11 @@ func TestComponentAsRect(t *testing.T) {
|
|||
|
||||
func TestComponentColorForSeries(t *testing.T) {
|
||||
c := NewComponent("fred")
|
||||
okC, errC := c.colorForSeries()
|
||||
cc := c.colorForSeries()
|
||||
|
||||
assert.Equal(t, tview.Styles.PrimaryTextColor, okC)
|
||||
assert.Equal(t, tview.Styles.FocusColor, errC)
|
||||
assert.Equal(t, []string{"white", "green"}, c.GetSeriesColorNames())
|
||||
assert.Len(t, cc, 3)
|
||||
assert.Equal(t, tcell.ColorGreen, cc[0])
|
||||
assert.Equal(t, tcell.ColorOrange, cc[1])
|
||||
assert.Equal(t, tcell.ColorOrangeRed, cc[2])
|
||||
assert.Equal(t, []string{"green", "orange", "orangered"}, c.GetSeriesColorNames())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,3 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright Authors of K9s
|
||||
|
||||
package tchart_test
|
||||
|
||||
import (
|
||||
|
|
@ -14,15 +11,7 @@ import (
|
|||
func TestCoSeriesColorNames(t *testing.T) {
|
||||
c := tchart.NewComponent("fred")
|
||||
|
||||
c.SetSeriesColors(tcell.ColorGreen, tcell.ColorBlue)
|
||||
c.SetSeriesColors(tcell.ColorGreen, tcell.ColorBlue, tcell.ColorRed)
|
||||
|
||||
assert.Equal(t, []string{"green", "blue"}, c.GetSeriesColorNames())
|
||||
}
|
||||
|
||||
func TestComponentAsRect(t *testing.T) {
|
||||
c := tchart.NewComponent("fred")
|
||||
|
||||
c.SetSeriesColors(tcell.ColorGreen, tcell.ColorBlue)
|
||||
|
||||
assert.Equal(t, []string{"green", "blue"}, c.GetSeriesColorNames())
|
||||
assert.Equal(t, []string{"green", "blue", "red"}, c.GetSeriesColorNames())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,3 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright Authors of K9s
|
||||
|
||||
package tchart_test
|
||||
|
||||
import (
|
||||
|
|
|
|||
|
|
@ -1,11 +1,9 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright Authors of K9s
|
||||
|
||||
package tchart
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"image"
|
||||
"time"
|
||||
|
||||
"github.com/derailed/tcell/v2"
|
||||
"github.com/derailed/tview"
|
||||
|
|
@ -20,19 +18,21 @@ const (
|
|||
|
||||
// DeltaLess represents a lower value.
|
||||
DeltaLess
|
||||
|
||||
gaugeFmt = "0%dd"
|
||||
)
|
||||
|
||||
type State struct {
|
||||
OK, Fault int
|
||||
}
|
||||
|
||||
type delta int
|
||||
|
||||
// Gauge represents a gauge component.
|
||||
type Gauge struct {
|
||||
*Component
|
||||
|
||||
data Metric
|
||||
state State
|
||||
resolution int
|
||||
deltaOk, deltaS2 delta
|
||||
deltaOK, deltaFault delta
|
||||
}
|
||||
|
||||
// NewGauge returns a new gauge.
|
||||
|
|
@ -47,23 +47,30 @@ func (g *Gauge) SetResolution(n int) {
|
|||
g.resolution = n
|
||||
}
|
||||
|
||||
// IsDial returns true if chart is a dial.
|
||||
// IsDial returns true if chart is a dial
|
||||
func (*Gauge) IsDial() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (*Gauge) SetColorIndex(int) {}
|
||||
func (*Gauge) SetMax(float64) {}
|
||||
func (*Gauge) GetMax() float64 { return 0 }
|
||||
|
||||
// Add adds a metric.
|
||||
func (*Gauge) AddMetric(time.Time, float64) {}
|
||||
|
||||
// Add adds a new metric.
|
||||
func (g *Gauge) Add(m Metric) {
|
||||
func (g *Gauge) Add(ok, fault int) {
|
||||
g.mx.Lock()
|
||||
defer g.mx.Unlock()
|
||||
|
||||
g.deltaOk, g.deltaS2 = computeDelta(g.data.S1, m.S1), computeDelta(g.data.S2, m.S2)
|
||||
g.data = m
|
||||
g.deltaOK, g.deltaFault = computeDelta(g.state.OK, ok), computeDelta(g.state.Fault, fault)
|
||||
g.state = State{OK: ok, Fault: fault}
|
||||
}
|
||||
|
||||
type number struct {
|
||||
ok bool
|
||||
val int64
|
||||
val int
|
||||
str string
|
||||
delta delta
|
||||
}
|
||||
|
|
@ -77,26 +84,24 @@ func (g *Gauge) Draw(sc tcell.Screen) {
|
|||
|
||||
rect := g.asRect()
|
||||
mid := image.Point{X: rect.Min.X + rect.Dx()/2, Y: rect.Min.Y + rect.Dy()/2 - 1}
|
||||
style := tcell.StyleDefault.Background(g.bgColor)
|
||||
style = style.Foreground(tcell.ColorYellow)
|
||||
sc.SetContent(mid.X, mid.Y, '⠔', nil, style)
|
||||
|
||||
maxD := g.data.MaxDigits()
|
||||
if maxD < g.resolution {
|
||||
maxD = g.resolution
|
||||
}
|
||||
var (
|
||||
fmat = "%" + fmt.Sprintf(gaugeFmt, maxD)
|
||||
o = image.Point{X: mid.X, Y: mid.Y - 1}
|
||||
fmat = "%d"
|
||||
)
|
||||
d1, d2 := fmt.Sprintf(fmat, g.state.OK), fmt.Sprintf(fmat, g.state.Fault)
|
||||
|
||||
s1C, s2C := g.colorForSeries()
|
||||
d1, d2 := fmt.Sprintf(fmat, g.data.S1), fmt.Sprintf(fmat, g.data.S2)
|
||||
o.X -= len(d1) * 3
|
||||
g.drawNum(sc, o, number{ok: true, val: g.data.S1, delta: g.deltaOk, str: d1}, style.Foreground(s1C).Dim(false))
|
||||
style := tcell.StyleDefault.Background(g.bgColor)
|
||||
|
||||
o.X = mid.X + 1
|
||||
g.drawNum(sc, o, number{ok: false, val: g.data.S2, delta: g.deltaS2, str: d2}, style.Foreground(s2C).Dim(false))
|
||||
total := len(d1)*3 + len(d2)*3 + 1
|
||||
colors := g.colorForSeries()
|
||||
o := image.Point{X: mid.X, Y: mid.Y - 1}
|
||||
o.X -= total / 2
|
||||
g.drawNum(sc, o, number{ok: true, val: g.state.OK, delta: g.deltaOK, str: d1}, style.Foreground(colors[0]).Dim(false))
|
||||
|
||||
o.X, o.Y = o.X+len(d1)*3, mid.Y
|
||||
sc.SetContent(o.X, o.Y, '⠔', nil, style)
|
||||
|
||||
o.X, o.Y = o.X+1, mid.Y-1
|
||||
g.drawNum(sc, o, number{ok: false, val: g.state.Fault, delta: g.deltaFault, str: d2}, style.Foreground(colors[1]).Dim(false))
|
||||
|
||||
if rect.Dx() > 0 && rect.Dy() > 0 && g.legend != "" {
|
||||
legend := g.legend
|
||||
|
|
@ -108,9 +113,9 @@ func (g *Gauge) Draw(sc tcell.Screen) {
|
|||
}
|
||||
|
||||
func (g *Gauge) drawNum(sc tcell.Screen, o image.Point, n number, style tcell.Style) {
|
||||
c1, _ := g.colorForSeries()
|
||||
colors := g.colorForSeries()
|
||||
if n.ok {
|
||||
style = style.Foreground(c1)
|
||||
style = style.Foreground(colors[0])
|
||||
printDelta(sc, n.delta, o, style)
|
||||
}
|
||||
|
||||
|
|
@ -133,15 +138,15 @@ func (g *Gauge) drawNum(sc tcell.Screen, o image.Point, n number, style tcell.St
|
|||
}
|
||||
}
|
||||
|
||||
func (g *Gauge) drawDial(sc tcell.Screen, m Matrix, o image.Point, style tcell.Style) {
|
||||
func (*Gauge) drawDial(sc tcell.Screen, m Matrix, o image.Point, style tcell.Style) {
|
||||
for r := range m {
|
||||
for c := range m[r] {
|
||||
var c int
|
||||
for c < len(m[r]) {
|
||||
dot := m[r][c]
|
||||
if dot == dots[0] {
|
||||
sc.SetContent(o.X+c, o.Y+r, dots[1], nil, g.dimmed)
|
||||
} else {
|
||||
if dot != dots[0] {
|
||||
sc.SetContent(o.X+c, o.Y+r, dot, nil, style)
|
||||
}
|
||||
c++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -149,7 +154,7 @@ func (g *Gauge) drawDial(sc tcell.Screen, m Matrix, o image.Point, style tcell.S
|
|||
// ----------------------------------------------------------------------------
|
||||
// Helpers...
|
||||
|
||||
func computeDelta(d1, d2 int64) delta {
|
||||
func computeDelta(d1, d2 int) delta {
|
||||
if d2 == 0 {
|
||||
return DeltaSame
|
||||
}
|
||||
|
|
@ -167,7 +172,6 @@ func computeDelta(d1, d2 int64) delta {
|
|||
|
||||
func printDelta(sc tcell.Screen, d delta, o image.Point, s tcell.Style) {
|
||||
s = s.Dim(false)
|
||||
//nolint:exhaustive
|
||||
switch d {
|
||||
case DeltaLess:
|
||||
sc.SetContent(o.X-1, o.Y+1, '↓', nil, s)
|
||||
|
|
|
|||
|
|
@ -1,6 +1,3 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright Authors of K9s
|
||||
|
||||
package tchart
|
||||
|
||||
import (
|
||||
|
|
@ -11,7 +8,7 @@ import (
|
|||
|
||||
func TestComputeDeltas(t *testing.T) {
|
||||
uu := map[string]struct {
|
||||
d1, d2 int64
|
||||
d1, d2 int
|
||||
e delta
|
||||
}{
|
||||
"same": {
|
||||
|
|
|
|||
|
|
@ -1,59 +1,56 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright Authors of K9s
|
||||
|
||||
package tchart_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
// import (
|
||||
// "testing"
|
||||
|
||||
"github.com/derailed/k9s/internal/tchart"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
// "github.com/imhotepio/tchart"
|
||||
// "github.com/stretchr/testify/assert"
|
||||
// )
|
||||
|
||||
func TestMetricsMaxDigits(t *testing.T) {
|
||||
uu := map[string]struct {
|
||||
m tchart.Metric
|
||||
e int
|
||||
}{
|
||||
"empty": {
|
||||
e: 1,
|
||||
},
|
||||
"oks": {
|
||||
m: tchart.Metric{S1: 100, S2: 10},
|
||||
e: 3,
|
||||
},
|
||||
"errs": {
|
||||
m: tchart.Metric{S1: 10, S2: 1000},
|
||||
e: 4,
|
||||
},
|
||||
}
|
||||
// func TestMetricsMaxDigits(t *testing.T) {
|
||||
// uu := map[string]struct {
|
||||
// m tchart.State
|
||||
// e int
|
||||
// }{
|
||||
// "empty": {
|
||||
// e: 1,
|
||||
// },
|
||||
// "oks": {
|
||||
// m: tchart.State{OK: 100, Fault: 10},
|
||||
// e: 3,
|
||||
// },
|
||||
// "errs": {
|
||||
// m: tchart.State{OK: 10, Fault: 1000},
|
||||
// e: 4,
|
||||
// },
|
||||
// }
|
||||
|
||||
for k := range uu {
|
||||
u := uu[k]
|
||||
t.Run(k, func(t *testing.T) {
|
||||
assert.Equal(t, u.e, u.m.MaxDigits())
|
||||
})
|
||||
}
|
||||
}
|
||||
// for k := range uu {
|
||||
// u := uu[k]
|
||||
// t.Run(k, func(t *testing.T) {
|
||||
// assert.Equal(t, u.e, u.m.MaxDigits())
|
||||
// })
|
||||
// }
|
||||
// }
|
||||
|
||||
func TestMetricsMax(t *testing.T) {
|
||||
uu := map[string]struct {
|
||||
m tchart.Metric
|
||||
e int64
|
||||
}{
|
||||
"empty": {
|
||||
e: 0,
|
||||
},
|
||||
"max_ok": {
|
||||
m: tchart.Metric{S1: 100, S2: 10},
|
||||
e: 100,
|
||||
},
|
||||
}
|
||||
// func TestMetricsMax(t *testing.T) {
|
||||
// uu := map[string]struct {
|
||||
// m tchart.Metric
|
||||
// e int64
|
||||
// }{
|
||||
// "empty": {
|
||||
// e: 0,
|
||||
// },
|
||||
// "max_ok": {
|
||||
// m: tchart.Metric{S1: 100, S2: 10},
|
||||
// e: 100,
|
||||
// },
|
||||
// }
|
||||
|
||||
for k := range uu {
|
||||
u := uu[k]
|
||||
t.Run(k, func(t *testing.T) {
|
||||
assert.Equal(t, u.e, u.m.Max())
|
||||
})
|
||||
}
|
||||
}
|
||||
// for k := range uu {
|
||||
// u := uu[k]
|
||||
// t.Run(k, func(t *testing.T) {
|
||||
// assert.Equal(t, u.e, u.m.Max())
|
||||
// })
|
||||
// }
|
||||
// }
|
||||
|
|
|
|||
|
|
@ -0,0 +1,77 @@
|
|||
package tchart
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"sort"
|
||||
"time"
|
||||
)
|
||||
|
||||
type MetricSeries map[time.Time]float64
|
||||
|
||||
type Times []time.Time
|
||||
|
||||
func (tt Times) Len() int {
|
||||
return len(tt)
|
||||
}
|
||||
|
||||
func (tt Times) Swap(i, j int) {
|
||||
tt[i], tt[j] = tt[j], tt[i]
|
||||
}
|
||||
|
||||
func (tt Times) Less(i, j int) bool {
|
||||
return tt[i].Sub(tt[j]) <= 0
|
||||
}
|
||||
|
||||
func (tt Times) Includes(ti time.Time) bool {
|
||||
for _, t := range tt {
|
||||
if t.Equal(ti) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (mm MetricSeries) Empty() bool {
|
||||
return len(mm) == 0
|
||||
}
|
||||
|
||||
func (mm MetricSeries) Merge(metrics MetricSeries) {
|
||||
for k, v := range metrics {
|
||||
mm[k] = v
|
||||
}
|
||||
}
|
||||
|
||||
func (mm MetricSeries) Dump() {
|
||||
slog.Debug("METRICS")
|
||||
for _, k := range mm.Keys() {
|
||||
slog.Debug(fmt.Sprintf("%v: %f", k, mm[k]))
|
||||
}
|
||||
}
|
||||
|
||||
func (mm MetricSeries) Add(t time.Time, f float64) {
|
||||
if _, ok := mm[t]; !ok {
|
||||
mm[t] = f
|
||||
}
|
||||
}
|
||||
|
||||
func (mm MetricSeries) Keys() Times {
|
||||
kk := make(Times, 0, len(mm))
|
||||
for k := range mm {
|
||||
kk = append(kk, k)
|
||||
}
|
||||
sort.Sort(kk)
|
||||
|
||||
return kk
|
||||
}
|
||||
|
||||
func (mm MetricSeries) Truncate(size int) {
|
||||
kk := mm.Keys()
|
||||
kk = kk[0 : len(kk)-size]
|
||||
for t := range mm {
|
||||
if kk.Includes(t) {
|
||||
continue
|
||||
}
|
||||
delete(mm, kk[0])
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,70 @@
|
|||
package tchart_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/derailed/k9s/internal/tchart"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestSeriesAdd(t *testing.T) {
|
||||
type tuple struct {
|
||||
time.Time
|
||||
float64
|
||||
}
|
||||
uu := map[string]struct {
|
||||
tt []tuple
|
||||
e int
|
||||
}{
|
||||
"one": {
|
||||
tt: []tuple{
|
||||
{time.Now(), 1000},
|
||||
},
|
||||
e: 6,
|
||||
},
|
||||
}
|
||||
|
||||
for k := range uu {
|
||||
u := uu[k]
|
||||
t.Run(k, func(t *testing.T) {
|
||||
ss := makeSeries()
|
||||
for _, tu := range u.tt {
|
||||
ss.Add(tu.Time, tu.float64)
|
||||
}
|
||||
assert.Len(t, ss, u.e)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSeriesTruncate(t *testing.T) {
|
||||
uu := map[string]struct {
|
||||
n, e int
|
||||
}{
|
||||
"one": {
|
||||
n: 1,
|
||||
e: 4,
|
||||
},
|
||||
}
|
||||
|
||||
for k := range uu {
|
||||
u := uu[k]
|
||||
t.Run(k, func(t *testing.T) {
|
||||
ss := makeSeries()
|
||||
ss.Truncate(u.n)
|
||||
assert.Len(t, ss, u.e)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Helpers...
|
||||
|
||||
func makeSeries() tchart.MetricSeries {
|
||||
return tchart.MetricSeries{
|
||||
time.Now(): -100,
|
||||
time.Now(): 0,
|
||||
time.Now(): 100,
|
||||
time.Now(): 50,
|
||||
time.Now(): 10,
|
||||
}
|
||||
}
|
||||
|
|
@ -1,12 +1,10 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright Authors of K9s
|
||||
|
||||
package tchart
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"image"
|
||||
"math"
|
||||
"time"
|
||||
|
||||
"github.com/derailed/tcell/v2"
|
||||
"github.com/derailed/tview"
|
||||
|
|
@ -14,63 +12,106 @@ import (
|
|||
|
||||
var sparks = []rune{'▁', '▂', '▃', '▄', '▅', '▆', '▇', '█'}
|
||||
|
||||
const axisColor = "#ff0066"
|
||||
|
||||
type block struct {
|
||||
full int
|
||||
partial rune
|
||||
}
|
||||
|
||||
type blocks struct {
|
||||
s1, s2 block
|
||||
}
|
||||
|
||||
// Metric tracks two series.
|
||||
type Metric struct {
|
||||
S1, S2 int64
|
||||
}
|
||||
|
||||
// MaxDigits returns the max series number of digits.
|
||||
func (m Metric) MaxDigits() int {
|
||||
s := fmt.Sprintf("%d", m.Max())
|
||||
|
||||
return len(s)
|
||||
}
|
||||
|
||||
// Max returns the max of the series.
|
||||
func (m Metric) Max() int64 {
|
||||
return int64(math.Max(float64(m.S1), float64(m.S2)))
|
||||
}
|
||||
|
||||
// Sum returns the sum of series.
|
||||
func (m Metric) Sum() int64 {
|
||||
return m.S1 + m.S2
|
||||
}
|
||||
|
||||
// SparkLine represents a sparkline component.
|
||||
type SparkLine struct {
|
||||
*Component
|
||||
|
||||
data []Metric
|
||||
multiSeries bool
|
||||
series MetricSeries
|
||||
max float64
|
||||
unit string
|
||||
colorIndex int
|
||||
}
|
||||
|
||||
// NewSparkLine returns a new graph.
|
||||
func NewSparkLine(id string) *SparkLine {
|
||||
func NewSparkLine(id, unit string) *SparkLine {
|
||||
return &SparkLine{
|
||||
Component: NewComponent(id),
|
||||
multiSeries: true,
|
||||
series: make(MetricSeries),
|
||||
unit: unit,
|
||||
}
|
||||
}
|
||||
|
||||
// SetMultiSeries indicates if multi series are in effect or not.
|
||||
func (s *SparkLine) SetMultiSeries(b bool) {
|
||||
s.multiSeries = b
|
||||
// GetSeriesColorNames returns series colors by name.
|
||||
func (s *SparkLine) GetSeriesColorNames() []string {
|
||||
s.mx.RLock()
|
||||
defer s.mx.RUnlock()
|
||||
|
||||
nn := make([]string, 0, len(s.seriesColors))
|
||||
for _, color := range s.seriesColors {
|
||||
for name, co := range tcell.ColorNames {
|
||||
if co == color {
|
||||
nn = append(nn, name)
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(nn) < 3 {
|
||||
nn = append(nn, "green", "orange", "orangered")
|
||||
}
|
||||
|
||||
return nn
|
||||
}
|
||||
|
||||
func (s *SparkLine) SetColorIndex(n int) {
|
||||
s.colorIndex = n
|
||||
}
|
||||
|
||||
func (s *SparkLine) SetMax(m float64) {
|
||||
if m > s.max {
|
||||
s.max = m
|
||||
}
|
||||
}
|
||||
|
||||
func (s *SparkLine) GetMax() float64 {
|
||||
return s.max
|
||||
}
|
||||
|
||||
func (*SparkLine) Add(int, int) {}
|
||||
|
||||
// Add adds a metric.
|
||||
func (s *SparkLine) Add(m Metric) {
|
||||
func (s *SparkLine) AddMetric(t time.Time, f float64) {
|
||||
s.mx.Lock()
|
||||
defer s.mx.Unlock()
|
||||
s.data = append(s.data, m)
|
||||
s.series.Add(t, f)
|
||||
}
|
||||
|
||||
func (s *SparkLine) printYAxis(screen tcell.Screen, rect image.Rectangle) {
|
||||
style := tcell.StyleDefault.Foreground(tcell.GetColor(axisColor)).Background(s.bgColor)
|
||||
for y := range rect.Dy() - 3 {
|
||||
screen.SetContent(rect.Min.X, rect.Min.Y+y, tview.BoxDrawingsLightVertical, nil, style)
|
||||
}
|
||||
screen.SetContent(rect.Min.X, rect.Min.Y+rect.Dy()-3, tview.BoxDrawingsLightUpAndRight, nil, style)
|
||||
}
|
||||
|
||||
func (s *SparkLine) printXAxis(screen tcell.Screen, rect image.Rectangle) time.Time {
|
||||
dx, t := rect.Dx()-1, time.Now()
|
||||
vals := make([]string, 0, dx)
|
||||
for i := dx; i > 0; i -= 10 {
|
||||
label := fmt.Sprintf("%02d:%02d", t.Hour(), t.Minute())
|
||||
vals = append(vals, label)
|
||||
t = t.Add(-(10 * time.Minute))
|
||||
}
|
||||
|
||||
y := rect.Max.Y - 2
|
||||
for _, v := range vals {
|
||||
if dx <= 2 {
|
||||
break
|
||||
}
|
||||
tview.Print(screen, v, rect.Min.X+dx-5, y, 6, tview.AlignCenter, tcell.ColorOrange)
|
||||
dx -= 10
|
||||
}
|
||||
style := tcell.StyleDefault.Foreground(tcell.GetColor(axisColor)).Background(s.bgColor)
|
||||
for x := 1; x < rect.Dx()-1; x++ {
|
||||
screen.SetContent(rect.Min.X+x, rect.Max.Y-3, tview.BoxDrawingsLightHorizontal, nil, style)
|
||||
}
|
||||
|
||||
return t
|
||||
}
|
||||
|
||||
// Draw draws the graph.
|
||||
|
|
@ -80,54 +121,50 @@ func (s *SparkLine) Draw(screen tcell.Screen) {
|
|||
s.mx.RLock()
|
||||
defer s.mx.RUnlock()
|
||||
|
||||
if len(s.data) == 0 {
|
||||
return
|
||||
rect := s.asRect()
|
||||
s.printXAxis(screen, rect)
|
||||
|
||||
padX := 1
|
||||
s.cutSet(rect.Dx() - padX)
|
||||
var cX int
|
||||
if len(s.series) < rect.Dx() {
|
||||
cX = rect.Max.X - len(s.series) - 1
|
||||
} else {
|
||||
cX = rect.Min.X + padX
|
||||
}
|
||||
|
||||
pad := 0
|
||||
pad := 2
|
||||
if s.legend != "" {
|
||||
pad++
|
||||
}
|
||||
|
||||
rect := s.asRect()
|
||||
s.cutSet(rect.Dx())
|
||||
maxVal := s.computeMax()
|
||||
|
||||
cX, idx := rect.Min.X+1, 0
|
||||
if len(s.data)*2 < rect.Dx() {
|
||||
cX = rect.Max.X - len(s.data)*2
|
||||
} else {
|
||||
idx = len(s.data) - rect.Dx()/2
|
||||
}
|
||||
|
||||
scale := float64(len(sparks)*(rect.Dy()-pad)) / float64(maxVal)
|
||||
c1, c2 := s.colorForSeries()
|
||||
for _, d := range s.data[idx:] {
|
||||
b := toBlocks(d, scale)
|
||||
cY := rect.Max.Y - pad
|
||||
s.drawBlock(rect, screen, cX, cY, b.s1, c1)
|
||||
cX++
|
||||
s.drawBlock(rect, screen, cX, cY, b.s2, c2)
|
||||
scale := float64(len(sparks)*(rect.Dy()-pad)) / float64(s.max)
|
||||
colors := s.colorForSeries()
|
||||
cY := rect.Max.Y - pad - 1
|
||||
for _, t := range s.series.Keys() {
|
||||
b := s.makeBlock(s.series[t], scale)
|
||||
s.drawBlock(rect, screen, cX, cY, b, colors[s.colorIndex%len(colors)])
|
||||
cX++
|
||||
}
|
||||
|
||||
s.printYAxis(screen, rect)
|
||||
|
||||
if rect.Dx() > 0 && rect.Dy() > 0 && s.legend != "" {
|
||||
legend := s.legend
|
||||
if s.HasFocus() {
|
||||
legend = fmt.Sprintf("[%s:%s:]", s.focusFgColor, s.focusBgColor) + s.legend + "[::]"
|
||||
}
|
||||
tview.Print(screen, legend, rect.Min.X, rect.Max.Y, rect.Dx(), tview.AlignCenter, tcell.ColorWhite)
|
||||
tview.Print(screen, legend, rect.Min.X, rect.Max.Y-1, rect.Dx(), tview.AlignCenter, tcell.ColorWhite)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *SparkLine) drawBlock(r image.Rectangle, screen tcell.Screen, x, y int, b block, c tcell.Color) {
|
||||
style := tcell.StyleDefault.Foreground(c).Background(s.bgColor)
|
||||
|
||||
zeroY := r.Max.Y - r.Dy()
|
||||
zeroY, full := r.Min.Y, sparks[len(sparks)-1]
|
||||
for range b.full {
|
||||
screen.SetContent(x, y, sparks[len(sparks)-1], nil, style)
|
||||
screen.SetContent(x, y, full, nil, style)
|
||||
y--
|
||||
if y <= zeroY {
|
||||
if y < zeroY {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
|
@ -137,41 +174,22 @@ func (s *SparkLine) drawBlock(r image.Rectangle, screen tcell.Screen, x, y int,
|
|||
}
|
||||
|
||||
func (s *SparkLine) cutSet(width int) {
|
||||
if width <= 0 || len(s.data) == 0 {
|
||||
if width <= 0 || s.series.Empty() {
|
||||
return
|
||||
}
|
||||
|
||||
if len(s.data) >= width*2 {
|
||||
s.data = s.data[len(s.data)-width:]
|
||||
if len(s.series) > width {
|
||||
s.series.Truncate(width)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *SparkLine) computeMax() int64 {
|
||||
var maxVal int64
|
||||
for _, d := range s.data {
|
||||
m := d.Max()
|
||||
if maxVal < m {
|
||||
maxVal = m
|
||||
func (*SparkLine) makeBlock(v, scale float64) block {
|
||||
sc := (v * scale)
|
||||
scaled := math.Round(sc)
|
||||
p, b := int(scaled)%len(sparks), block{full: int(scaled / float64(len(sparks)))}
|
||||
if v < 0 {
|
||||
return b
|
||||
}
|
||||
}
|
||||
|
||||
return maxVal
|
||||
}
|
||||
|
||||
func toBlocks(m Metric, scale float64) blocks {
|
||||
if m.Sum() <= 0 {
|
||||
return blocks{}
|
||||
}
|
||||
return blocks{s1: makeBlocks(m.S1, scale), s2: makeBlocks(m.S2, scale)}
|
||||
}
|
||||
|
||||
func makeBlocks(v int64, scale float64) block {
|
||||
scaled := int(math.Round(float64(v) * scale))
|
||||
p, b := scaled%len(sparks), block{full: scaled / len(sparks)}
|
||||
if b.full == 0 && v > 0 && p == 0 {
|
||||
p = 4
|
||||
}
|
||||
if v > 0 && p >= 0 && p < len(sparks) {
|
||||
if p > 0 && p < len(sparks) {
|
||||
b.partial = sparks[p]
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,134 +0,0 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright Authors of K9s
|
||||
|
||||
package tchart
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestCutSet(t *testing.T) {
|
||||
uu := map[string]struct {
|
||||
mm []Metric
|
||||
w, e int
|
||||
}{
|
||||
"empty": {
|
||||
w: 10,
|
||||
e: 0,
|
||||
},
|
||||
"at": {
|
||||
mm: make([]Metric, 10),
|
||||
w: 10,
|
||||
e: 10,
|
||||
},
|
||||
"under": {
|
||||
mm: make([]Metric, 5),
|
||||
w: 10,
|
||||
e: 5,
|
||||
},
|
||||
"over": {
|
||||
mm: make([]Metric, 10),
|
||||
w: 5,
|
||||
e: 5,
|
||||
},
|
||||
}
|
||||
|
||||
for k := range uu {
|
||||
u := uu[k]
|
||||
t.Run(k, func(t *testing.T) {
|
||||
s := NewSparkLine("s")
|
||||
assert.False(t, s.IsDial())
|
||||
|
||||
for _, m := range u.mm {
|
||||
s.Add(m)
|
||||
}
|
||||
s.cutSet(u.w)
|
||||
assert.Len(t, s.data, u.e)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestToBlocks(t *testing.T) {
|
||||
uu := map[string]struct {
|
||||
m Metric
|
||||
s float64
|
||||
e blocks
|
||||
}{
|
||||
"empty": {
|
||||
e: blocks{},
|
||||
},
|
||||
"max_ok": {
|
||||
m: Metric{S1: 100, S2: 10},
|
||||
s: 0.5,
|
||||
e: blocks{
|
||||
s1: block{full: 6, partial: sparks[2]},
|
||||
s2: block{full: 0, partial: sparks[5]},
|
||||
},
|
||||
},
|
||||
"max_fault": {
|
||||
m: Metric{S1: 10, S2: 100},
|
||||
s: 0.5,
|
||||
e: blocks{
|
||||
s1: block{full: 0, partial: sparks[5]},
|
||||
s2: block{full: 6, partial: sparks[2]},
|
||||
},
|
||||
},
|
||||
"over": {
|
||||
m: Metric{S1: 22, S2: 999},
|
||||
s: float64(8*20) / float64(999),
|
||||
e: blocks{
|
||||
s1: block{full: 0, partial: sparks[4]},
|
||||
s2: block{full: 20, partial: sparks[0]},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for k := range uu {
|
||||
u := uu[k]
|
||||
t.Run(k, func(t *testing.T) {
|
||||
assert.Equal(t, u.e, toBlocks(u.m, u.s))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestComputeMax(t *testing.T) {
|
||||
uu := map[string]struct {
|
||||
mm []Metric
|
||||
e int64
|
||||
}{
|
||||
"empty": {
|
||||
e: 0,
|
||||
},
|
||||
"max_ok": {
|
||||
mm: []Metric{{S1: 100, S2: 10}},
|
||||
e: 100,
|
||||
},
|
||||
"max_fault": {
|
||||
mm: []Metric{{S1: 100, S2: 1000}},
|
||||
e: 1000,
|
||||
},
|
||||
"many": {
|
||||
mm: []Metric{
|
||||
{S1: 100, S2: 1000},
|
||||
{S1: 110, S2: 1010},
|
||||
{S1: 120, S2: 1020},
|
||||
{S1: 130, S2: 1030},
|
||||
{S1: 140, S2: 1040},
|
||||
},
|
||||
e: 1040,
|
||||
},
|
||||
}
|
||||
|
||||
for k := range uu {
|
||||
u := uu[k]
|
||||
s := NewSparkLine("s")
|
||||
for _, m := range u.mm {
|
||||
s.Add(m)
|
||||
}
|
||||
t.Run(k, func(t *testing.T) {
|
||||
assert.Equal(t, u.e, s.computeMax())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -11,7 +11,6 @@ import (
|
|||
"github.com/derailed/k9s/internal/config"
|
||||
"github.com/derailed/k9s/internal/model"
|
||||
"github.com/derailed/k9s/internal/render"
|
||||
"github.com/derailed/tcell/v2"
|
||||
"github.com/derailed/tview"
|
||||
)
|
||||
|
||||
|
|
@ -33,7 +32,7 @@ func NewStatusIndicator(app *App, styles *config.Styles) *StatusIndicator {
|
|||
styles: styles,
|
||||
}
|
||||
s.SetTextAlign(tview.AlignCenter)
|
||||
s.SetTextColor(tcell.ColorWhite)
|
||||
s.SetTextColor(styles.FgColor())
|
||||
s.SetBackgroundColor(styles.BgColor())
|
||||
s.SetDynamicColors(true)
|
||||
styles.AddListener(&s)
|
||||
|
|
@ -48,18 +47,24 @@ func (s *StatusIndicator) StylesChanged(styles *config.Styles) {
|
|||
s.SetTextColor(styles.FgColor())
|
||||
}
|
||||
|
||||
const statusIndicatorFmt = "[orange::b]K9s [aqua::]%s [white::]%s:%s:%s [lawngreen::]%s[white::]::[darkturquoise::]%s"
|
||||
const statusIndicatorFmt = "[%s::b]K9s [%s::]%s [%s::]%s:%s:%s [%s::]%s[%s::]::[%s::]%s"
|
||||
|
||||
// ClusterInfoUpdated notifies the cluster meta was updated.
|
||||
func (s *StatusIndicator) ClusterInfoUpdated(data *model.ClusterMeta) {
|
||||
s.app.QueueUpdateDraw(func() {
|
||||
s.SetPermanent(fmt.Sprintf(
|
||||
statusIndicatorFmt,
|
||||
s.styles.Body().LogoColor.String(),
|
||||
s.styles.K9s.Info.K9sRevColor.String(),
|
||||
data.K9sVer,
|
||||
s.styles.K9s.Info.FgColor.String(),
|
||||
data.Context,
|
||||
data.Cluster,
|
||||
data.K8sVer,
|
||||
s.styles.K9s.Info.CPUColor.String(),
|
||||
render.PrintPerc(data.Cpu),
|
||||
s.styles.Body().FgColor.String(),
|
||||
s.styles.K9s.Info.MEMColor.String(),
|
||||
render.PrintPerc(data.Mem),
|
||||
))
|
||||
})
|
||||
|
|
@ -73,11 +78,17 @@ func (s *StatusIndicator) ClusterInfoChanged(prev, cur *model.ClusterMeta) {
|
|||
s.app.QueueUpdateDraw(func() {
|
||||
s.SetPermanent(fmt.Sprintf(
|
||||
statusIndicatorFmt,
|
||||
s.styles.Body().LogoColor.String(),
|
||||
s.styles.K9s.Info.K9sRevColor.String(),
|
||||
cur.K9sVer,
|
||||
s.styles.K9s.Info.FgColor.String(),
|
||||
cur.Context,
|
||||
cur.Cluster,
|
||||
cur.K8sVer,
|
||||
s.styles.K9s.Info.CPUColor.String(),
|
||||
AsPercDelta(prev.Cpu, cur.Cpu),
|
||||
s.styles.Body().FgColor.String(),
|
||||
s.styles.K9s.Info.MEMColor.String(),
|
||||
AsPercDelta(prev.Cpu, cur.Mem),
|
||||
))
|
||||
})
|
||||
|
|
|
|||
|
|
@ -560,10 +560,7 @@ func (t *Table) styleTitle() string {
|
|||
|
||||
buff := t.cmdBuff.GetText()
|
||||
if internal.IsLabelSelector(buff) {
|
||||
sel, err := TrimLabelSelector(buff)
|
||||
if err != nil {
|
||||
buff = render.Truncate(buff, maxTruncate)
|
||||
} else if sel != nil {
|
||||
if sel, err := TrimLabelSelector(buff); err == nil {
|
||||
buff = render.Truncate(sel.String(), maxTruncate)
|
||||
}
|
||||
} else if l := t.GetModel().GetLabelSelector(); l != nil && !l.Empty() {
|
||||
|
|
@ -571,7 +568,6 @@ func (t *Table) styleTitle() string {
|
|||
} else if buff != "" {
|
||||
buff = render.Truncate(buff, maxTruncate)
|
||||
}
|
||||
|
||||
if buff == "" {
|
||||
return title
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,8 +17,8 @@ import (
|
|||
)
|
||||
|
||||
const (
|
||||
unlockedIC = "🖍"
|
||||
lockedIC = "🔑"
|
||||
unlockedIC = "[RW]"
|
||||
lockedIC = "[R]"
|
||||
)
|
||||
|
||||
// Namespaceable tracks namespaces.
|
||||
|
|
|
|||
|
|
@ -214,9 +214,12 @@ func pluginAction(r Runner, p *config.Plugin) ui.ActionHandler {
|
|||
errs = errors.Join(errs, e)
|
||||
}
|
||||
if errs != nil {
|
||||
if !strings.Contains(errs.Error(), "signal: interrupt") {
|
||||
slog.Error("Plugin command failed", slogs.Error, errs)
|
||||
r.App().cowCmd(errs.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
go func() {
|
||||
for st := range statusChan {
|
||||
if !p.OverwriteOutput {
|
||||
|
|
|
|||
|
|
@ -27,9 +27,6 @@ func newArgs(p *Interpreter, aa []string) args {
|
|||
for i := 0; i < len(aa); i++ {
|
||||
a := strings.TrimSpace(aa[i])
|
||||
switch {
|
||||
case strings.Index(a, contextFlag) == 0:
|
||||
arguments[contextKey] = a[1:]
|
||||
|
||||
case strings.Index(a, fuzzyFlag) == 0:
|
||||
if a == fuzzyFlag {
|
||||
i++
|
||||
|
|
@ -54,20 +51,26 @@ func newArgs(p *Interpreter, aa []string) args {
|
|||
arguments[labelKey] = strings.ToLower(a)
|
||||
}
|
||||
|
||||
case strings.Index(a, contextFlag) == 0:
|
||||
arguments[contextKey] = a[1:]
|
||||
|
||||
default:
|
||||
switch {
|
||||
case p.IsContextCmd():
|
||||
arguments[contextKey] = a
|
||||
|
||||
case p.IsDirCmd():
|
||||
if _, ok := arguments[topicKey]; !ok {
|
||||
arguments[topicKey] = a
|
||||
}
|
||||
|
||||
case p.IsXrayCmd():
|
||||
if _, ok := arguments[topicKey]; ok {
|
||||
arguments[nsKey] = strings.ToLower(a)
|
||||
} else {
|
||||
arguments[topicKey] = strings.ToLower(a)
|
||||
}
|
||||
|
||||
default:
|
||||
arguments[nsKey] = strings.ToLower(a)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,61 +19,73 @@ func TestFlagsNew(t *testing.T) {
|
|||
i: NewInterpreter("po"),
|
||||
ll: make(args),
|
||||
},
|
||||
|
||||
"ns": {
|
||||
i: NewInterpreter("po"),
|
||||
aa: []string{"ns1"},
|
||||
ll: args{nsKey: "ns1"},
|
||||
},
|
||||
|
||||
"ns+spaces": {
|
||||
i: NewInterpreter("po"),
|
||||
aa: []string{" ns1 "},
|
||||
ll: args{nsKey: "ns1"},
|
||||
},
|
||||
|
||||
"filter": {
|
||||
i: NewInterpreter("po"),
|
||||
aa: []string{"/fred"},
|
||||
ll: args{filterKey: "fred"},
|
||||
},
|
||||
|
||||
"inverse-filter": {
|
||||
i: NewInterpreter("po"),
|
||||
aa: []string{"/!fred"},
|
||||
ll: args{filterKey: "!fred"},
|
||||
},
|
||||
|
||||
"fuzzy-filter": {
|
||||
i: NewInterpreter("po"),
|
||||
aa: []string{"-f", "fred"},
|
||||
ll: args{fuzzyKey: "fred"},
|
||||
},
|
||||
|
||||
"fuzzy-filter-nospace": {
|
||||
i: NewInterpreter("po"),
|
||||
aa: []string{"-ffred"},
|
||||
ll: args{fuzzyKey: "fred"},
|
||||
},
|
||||
|
||||
"filter+ns": {
|
||||
i: NewInterpreter("po"),
|
||||
aa: []string{"/fred", " ns1 "},
|
||||
ll: args{nsKey: "ns1", filterKey: "fred"},
|
||||
},
|
||||
|
||||
"label": {
|
||||
i: NewInterpreter("po"),
|
||||
aa: []string{"app=fred"},
|
||||
ll: args{labelKey: "app=fred"},
|
||||
},
|
||||
|
||||
"label-toast": {
|
||||
i: NewInterpreter("po"),
|
||||
aa: []string{"="},
|
||||
ll: make(args),
|
||||
},
|
||||
|
||||
"multi-labels": {
|
||||
i: NewInterpreter("po"),
|
||||
aa: []string{"app=fred,blee=duh"},
|
||||
ll: args{labelKey: "app=fred,blee=duh"},
|
||||
},
|
||||
|
||||
"label+ns": {
|
||||
i: NewInterpreter("po"),
|
||||
aa: []string{"a=b,c=d", " ns1 "},
|
||||
ll: args{labelKey: "a=b,c=d", nsKey: "ns1"},
|
||||
},
|
||||
|
||||
"full-monty": {
|
||||
i: NewInterpreter("po"),
|
||||
aa: []string{"app=fred", "ns1", "-f", "blee", "/zorg"},
|
||||
|
|
@ -84,6 +96,7 @@ func TestFlagsNew(t *testing.T) {
|
|||
nsKey: "ns1",
|
||||
},
|
||||
},
|
||||
|
||||
"full-monty+ctx": {
|
||||
i: NewInterpreter("po"),
|
||||
aa: []string{"app=fred", "ns1", "-f", "blee", "/zorg", "@ctx1"},
|
||||
|
|
@ -95,6 +108,43 @@ func TestFlagsNew(t *testing.T) {
|
|||
contextKey: "ctx1",
|
||||
},
|
||||
},
|
||||
|
||||
"full-monty+ctx-with-space": {
|
||||
i: NewInterpreter("po"),
|
||||
aa: []string{"app=fred", "ns1", "-f", "blee", "/zorg", "@zorg fred"},
|
||||
ll: args{
|
||||
filterKey: "zorg",
|
||||
fuzzyKey: "blee",
|
||||
labelKey: "app=fred",
|
||||
nsKey: "ns1",
|
||||
contextKey: "zorg fred",
|
||||
},
|
||||
},
|
||||
|
||||
"full-monty+ctx-first": {
|
||||
i: NewInterpreter("po"),
|
||||
aa: []string{"@ctx1", "app=fred", "ns1", "-f", "blee", "/zorg"},
|
||||
ll: args{
|
||||
filterKey: "zorg",
|
||||
fuzzyKey: "blee",
|
||||
labelKey: "app=fred",
|
||||
nsKey: "ns1",
|
||||
contextKey: "ctx1",
|
||||
},
|
||||
},
|
||||
|
||||
"full-monty+ctx-with-space-middle": {
|
||||
i: NewInterpreter("po"),
|
||||
aa: []string{"app=fred", "@ctx1", "ns1", "-f", "blee", "/zorg"},
|
||||
ll: args{
|
||||
filterKey: "zorg",
|
||||
fuzzyKey: "blee",
|
||||
labelKey: "app=fred",
|
||||
nsKey: "ns1",
|
||||
contextKey: "ctx1",
|
||||
},
|
||||
},
|
||||
|
||||
"caps": {
|
||||
i: NewInterpreter("po"),
|
||||
aa: []string{"app=fred", "ns1", "-f", "blee", "/zorg", "@Dev"},
|
||||
|
|
@ -106,12 +156,14 @@ func TestFlagsNew(t *testing.T) {
|
|||
contextKey: "Dev",
|
||||
},
|
||||
},
|
||||
|
||||
"ctx": {
|
||||
i: NewInterpreter("ctx"),
|
||||
aa: []string{"Dev"},
|
||||
ll: args{contextKey: "Dev"},
|
||||
},
|
||||
"bork": {
|
||||
|
||||
"toast": {
|
||||
i: NewInterpreter("apply -f"),
|
||||
ll: args{},
|
||||
},
|
||||
|
|
|
|||
|
|
@ -130,11 +130,11 @@ func (c *Interpreter) IsRBACCmd() bool {
|
|||
|
||||
// ContextArg returns context cmd arg.
|
||||
func (c *Interpreter) ContextArg() (string, bool) {
|
||||
if !c.IsContextCmd() {
|
||||
return "", false
|
||||
if c.IsContextCmd() || strings.Contains(c.line, contextFlag) {
|
||||
return c.args[contextKey], true
|
||||
}
|
||||
|
||||
return c.args[contextKey], true
|
||||
return "", false
|
||||
}
|
||||
|
||||
// ResetContextArg deletes context arg.
|
||||
|
|
|
|||
|
|
@ -475,3 +475,31 @@ func TestCowCmd(t *testing.T) {
|
|||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestArgs(t *testing.T) {
|
||||
uu := map[string]struct {
|
||||
cmd string
|
||||
ok bool
|
||||
ctx string
|
||||
}{
|
||||
"empty": {},
|
||||
|
||||
"with-plain-context": {
|
||||
cmd: "po @fred",
|
||||
ok: true,
|
||||
ctx: "fred",
|
||||
},
|
||||
}
|
||||
|
||||
for k := range uu {
|
||||
u := uu[k]
|
||||
t.Run(k, func(t *testing.T) {
|
||||
p := cmd.NewInterpreter(u.cmd)
|
||||
ctx, ok := p.ContextArg()
|
||||
assert.Equal(t, u.ok, ok)
|
||||
if u.ok {
|
||||
assert.Equal(t, u.ctx, ctx)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -137,7 +137,7 @@ func runForward(v ResourceViewer, pf watch.Forwarder, f *portforward.PortForward
|
|||
|
||||
pf.SetActive(true)
|
||||
if err := f.ForwardPorts(); err != nil {
|
||||
v.App().Flash().Err(err)
|
||||
v.App().Flash().Warnf("PortForward failed for %s: %s. Deleting!", pf.ID(), err)
|
||||
}
|
||||
v.App().QueueUpdateDraw(func() {
|
||||
v.App().factory.DeleteForwarder(pf.ID())
|
||||
|
|
|
|||
|
|
@ -1,20 +1,19 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright Authors of K9s
|
||||
|
||||
package view
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"image"
|
||||
"strings"
|
||||
"log/slog"
|
||||
"time"
|
||||
|
||||
"github.com/derailed/k9s/internal"
|
||||
"github.com/derailed/k9s/internal/client"
|
||||
"github.com/derailed/k9s/internal/config"
|
||||
"github.com/derailed/k9s/internal/health"
|
||||
"github.com/derailed/k9s/internal/dao"
|
||||
"github.com/derailed/k9s/internal/model"
|
||||
"github.com/derailed/k9s/internal/render"
|
||||
"github.com/derailed/k9s/internal/slogs"
|
||||
"github.com/derailed/k9s/internal/tchart"
|
||||
"github.com/derailed/k9s/internal/ui"
|
||||
"github.com/derailed/k9s/internal/view/cmd"
|
||||
|
|
@ -25,6 +24,22 @@ import (
|
|||
"k8s.io/apimachinery/pkg/labels"
|
||||
)
|
||||
|
||||
const (
|
||||
cpuFmt = " %s [%s::b]%s[white::-]([%s::]%sm[white::]/[%s::]%sm[-::])"
|
||||
memFmt = " %s [%s::b]%s[white::-]([%s::]%sMi[white::]/[%s::]%sMi[-::])"
|
||||
pulseTitle = "Pulses"
|
||||
NSTitleFmt = "[fg:bg:b] %s([hilite:bg:b]%s[fg:bg:-])[fg:bg:-] "
|
||||
dirLeft = 1
|
||||
dirRight = -dirLeft
|
||||
dirDown = 4
|
||||
dirUp = -dirDown
|
||||
grayC = "gray"
|
||||
)
|
||||
|
||||
var corpusGVRs = append(model.PulseGVRs, client.CpuGVR, client.MemGVR)
|
||||
|
||||
type Charts map[*client.GVR]Graphable
|
||||
|
||||
// Graphable represents a graphic component.
|
||||
type Graphable interface {
|
||||
tview.Primitive
|
||||
|
|
@ -33,11 +48,18 @@ type Graphable interface {
|
|||
ID() string
|
||||
|
||||
// Add adds a metric
|
||||
Add(tchart.Metric)
|
||||
Add(ok, fault int)
|
||||
|
||||
AddMetric(time.Time, float64)
|
||||
|
||||
// SetLegend sets the graph legend
|
||||
SetLegend(string)
|
||||
|
||||
SetColorIndex(int)
|
||||
|
||||
SetMax(float64)
|
||||
GetMax() float64
|
||||
|
||||
// SetSeriesColors sets charts series colors.
|
||||
SetSeriesColors(...tcell.Color)
|
||||
|
||||
|
|
@ -50,14 +72,12 @@ type Graphable interface {
|
|||
// SetBackgroundColor sets chart bg color.
|
||||
SetBackgroundColor(tcell.Color)
|
||||
|
||||
SetBorderColor(tcell.Color) *tview.Box
|
||||
|
||||
// IsDial returns true if chart is a dial
|
||||
IsDial() bool
|
||||
}
|
||||
|
||||
const pulseTitle = "Pulses"
|
||||
|
||||
var _ ResourceViewer = (*Pulse)(nil)
|
||||
|
||||
// Pulse represents a command health view.
|
||||
type Pulse struct {
|
||||
*tview.Grid
|
||||
|
|
@ -67,54 +87,64 @@ type Pulse struct {
|
|||
model *model.Pulse
|
||||
cancelFn context.CancelFunc
|
||||
actions *ui.KeyActions
|
||||
charts []Graphable
|
||||
charts Charts
|
||||
prevFocusIndex int
|
||||
chartGVRs client.GVRs
|
||||
}
|
||||
|
||||
// NewPulse returns a new alias view.
|
||||
func NewPulse(gvr *client.GVR) ResourceViewer {
|
||||
return &Pulse{
|
||||
Grid: tview.NewGrid(),
|
||||
model: model.NewPulse(gvr.String()),
|
||||
model: model.NewPulse(gvr),
|
||||
actions: ui.NewKeyActions(),
|
||||
prevFocusIndex: -1,
|
||||
}
|
||||
}
|
||||
|
||||
func (*Pulse) SetCommand(*cmd.Interpreter) {}
|
||||
func (*Pulse) SetFilter(string) {}
|
||||
func (*Pulse) SetLabelSelector(labels.Selector) {}
|
||||
|
||||
// Init initializes the view.
|
||||
func (p *Pulse) Init(ctx context.Context) error {
|
||||
p.SetBorder(true)
|
||||
p.SetTitle(fmt.Sprintf(" %s ", pulseTitle))
|
||||
p.SetGap(1, 1)
|
||||
p.SetGap(0, 0)
|
||||
p.SetBorderPadding(0, 0, 1, 1)
|
||||
var err error
|
||||
if p.app, err = extractApp(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
p.charts = []Graphable{
|
||||
p.makeGA(image.Point{X: 0, Y: 0}, image.Point{X: 2, Y: 2}, client.DpGVR),
|
||||
p.makeGA(image.Point{X: 0, Y: 2}, image.Point{X: 2, Y: 2}, client.RsGVR),
|
||||
p.makeGA(image.Point{X: 0, Y: 4}, image.Point{X: 2, Y: 2}, client.StsGVR),
|
||||
p.makeGA(image.Point{X: 0, Y: 6}, image.Point{X: 2, Y: 2}, client.DsGVR),
|
||||
p.makeSP(image.Point{X: 2, Y: 0}, image.Point{X: 3, Y: 2}, client.PodGVR),
|
||||
p.makeSP(image.Point{X: 2, Y: 2}, image.Point{X: 3, Y: 2}, client.EvGVR),
|
||||
p.makeSP(image.Point{X: 2, Y: 4}, image.Point{X: 3, Y: 2}, client.JobGVR),
|
||||
p.makeSP(image.Point{X: 2, Y: 6}, image.Point{X: 3, Y: 2}, client.PvGVR),
|
||||
ns := p.app.Config.ActiveNamespace()
|
||||
frame := p.app.Styles.Frame()
|
||||
p.SetTitle(ui.SkinTitle(fmt.Sprintf(NSTitleFmt, pulseTitle, ns), &frame))
|
||||
|
||||
index, chartRow := 4, 6
|
||||
if client.IsAllNamespace(ns) {
|
||||
index, chartRow = 0, 8
|
||||
}
|
||||
p.chartGVRs = corpusGVRs[index:]
|
||||
|
||||
p.charts = make(Charts, len(p.chartGVRs))
|
||||
var x, y, col int
|
||||
for _, gvr := range p.chartGVRs[:len(p.chartGVRs)-2] {
|
||||
p.charts[gvr] = p.makeGA(image.Point{X: x, Y: y}, image.Point{X: 2, Y: 2}, gvr)
|
||||
col, y = col+1, y+2
|
||||
if y > 6 {
|
||||
y = 0
|
||||
}
|
||||
if col >= 4 {
|
||||
col, x = 0, x+2
|
||||
}
|
||||
}
|
||||
if p.app.Conn().HasMetrics() {
|
||||
p.charts = append(p.charts,
|
||||
p.makeSP(image.Point{X: 5, Y: 0}, image.Point{X: 2, Y: 4}, client.CpuGVR),
|
||||
p.makeSP(image.Point{X: 5, Y: 4}, image.Point{X: 2, Y: 4}, client.MemGVR),
|
||||
)
|
||||
p.charts[client.CpuGVR] = p.makeSP(image.Point{X: chartRow, Y: 0}, image.Point{X: 2, Y: 4}, client.CpuGVR, "c")
|
||||
p.charts[client.MemGVR] = p.makeSP(image.Point{X: chartRow, Y: 4}, image.Point{X: 2, Y: 4}, client.MemGVR, "Gi")
|
||||
}
|
||||
p.GetItem(0).Focus = true
|
||||
p.app.SetFocus(p.charts[p.chartGVRs[0]])
|
||||
|
||||
p.bindKeys()
|
||||
p.model.AddListener(p)
|
||||
p.app.SetFocus(p.charts[0])
|
||||
p.app.Styles.AddListener(p)
|
||||
p.StylesChanged(p.app.Styles)
|
||||
p.model.SetNamespace(ns)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
@ -124,11 +154,15 @@ func (*Pulse) InCmdMode() bool {
|
|||
return false
|
||||
}
|
||||
|
||||
func (*Pulse) SetCommand(*cmd.Interpreter) {}
|
||||
func (*Pulse) SetFilter(string) {}
|
||||
func (*Pulse) SetLabelSelector(labels.Selector) {}
|
||||
|
||||
// StylesChanged notifies the skin changed.
|
||||
func (p *Pulse) StylesChanged(s *config.Styles) {
|
||||
p.SetBackgroundColor(s.Charts().BgColor.Color())
|
||||
for _, c := range p.charts {
|
||||
c.SetFocusColorNames(s.Table().BgColor.String(), s.Table().CursorBgColor.String())
|
||||
c.SetFocusColorNames(s.Charts().FocusFgColor.String(), s.Charts().FocusBgColor.String())
|
||||
if c.IsDial() {
|
||||
c.SetBackgroundColor(s.Charts().DialBgColor.Color())
|
||||
c.SetSeriesColors(s.Charts().DefaultDialColors.Colors()...)
|
||||
|
|
@ -142,65 +176,93 @@ func (p *Pulse) StylesChanged(s *config.Styles) {
|
|||
}
|
||||
}
|
||||
|
||||
const (
|
||||
genFmat = " %s([%s::]%d[white::]:[%s::b]%d[-::])"
|
||||
cpuFmt = " %s [%s::b]%s[white::-]([%s::]%sm[white::]/[%s::]%sm[-::])"
|
||||
memFmt = " %s [%s::b]%s[white::-]([%s::]%sMi[white::]/[%s::]%sMi[-::])"
|
||||
)
|
||||
// SeriesChanged update cluster time series.
|
||||
func (p *Pulse) SeriesChanged(tt dao.TimeSeries) {
|
||||
if len(tt) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
// PulseChanged notifies the model data changed.
|
||||
func (p *Pulse) PulseChanged(c *health.Check) {
|
||||
index, ok := findIndexGVR(p.charts, c.GVR)
|
||||
cpu, ok := p.charts[client.CpuGVR]
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
mem := p.charts[client.MemGVR]
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
v, ok := p.GetItem(index).Item.(Graphable)
|
||||
for i := range tt {
|
||||
t := tt[i]
|
||||
cpu.SetMax(float64(t.Value.AllocatableCPU))
|
||||
mem.SetMax(float64(t.Value.AllocatableMEM))
|
||||
cpu.AddMetric(t.Time, float64(t.Value.CurrentCPU))
|
||||
mem.AddMetric(t.Time, float64(t.Value.CurrentMEM))
|
||||
}
|
||||
|
||||
last := tt[len(tt)-1]
|
||||
perc := client.ToPercentage(last.Value.CurrentCPU, int64(cpu.GetMax()))
|
||||
index := int(p.app.Config.K9s.Thresholds.LevelFor("cpu", perc))
|
||||
cpu.SetColorIndex(int(p.app.Config.K9s.Thresholds.LevelFor("cpu", perc)))
|
||||
nn := cpu.GetSeriesColorNames()
|
||||
if last.Value.CurrentCPU == 0 {
|
||||
nn[0] = grayC
|
||||
}
|
||||
if last.Value.AllocatableCPU == 0 {
|
||||
nn[1] = grayC
|
||||
}
|
||||
cpu.SetLegend(fmt.Sprintf(cpuFmt,
|
||||
cases.Title(language.English).String(client.CpuGVR.R()),
|
||||
p.app.Config.K9s.Thresholds.SeverityColor("cpu", perc),
|
||||
render.PrintPerc(perc),
|
||||
nn[index],
|
||||
render.AsThousands(last.Value.CurrentCPU),
|
||||
"white",
|
||||
render.AsThousands(int64(cpu.GetMax())),
|
||||
))
|
||||
|
||||
nn = mem.GetSeriesColorNames()
|
||||
if last.Value.CurrentMEM == 0 {
|
||||
nn[0] = grayC
|
||||
}
|
||||
if last.Value.AllocatableMEM == 0 {
|
||||
nn[1] = grayC
|
||||
}
|
||||
perc = client.ToPercentage(last.Value.CurrentMEM, int64(mem.GetMax()))
|
||||
index = int(p.app.Config.K9s.Thresholds.LevelFor("memory", perc))
|
||||
mem.SetColorIndex(index)
|
||||
mem.SetLegend(fmt.Sprintf(memFmt,
|
||||
cases.Title(language.English).String(client.MemGVR.R()),
|
||||
p.app.Config.K9s.Thresholds.SeverityColor("memory", perc),
|
||||
render.PrintPerc(perc),
|
||||
nn[index],
|
||||
render.AsThousands(last.Value.CurrentMEM),
|
||||
"white",
|
||||
render.AsThousands(int64(mem.GetMax())),
|
||||
))
|
||||
}
|
||||
|
||||
// PulseChanged notifies the model data changed.
|
||||
func (p *Pulse) PulseChanged(pt model.HealthPoint) {
|
||||
v, ok := p.charts[pt.GVR]
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
nn := v.GetSeriesColorNames()
|
||||
if c.Tally(health.S1) == 0 {
|
||||
nn[0] = "gray"
|
||||
if pt.Total == 0 {
|
||||
nn[0] = grayC
|
||||
}
|
||||
if c.Tally(health.S2) == 0 {
|
||||
nn[1] = "gray"
|
||||
if pt.Faults == 0 {
|
||||
nn[1] = grayC
|
||||
}
|
||||
|
||||
switch c.GVR {
|
||||
case client.CpuGVR:
|
||||
perc := client.ToPercentage(c.Tally(health.S1), c.Tally(health.S2))
|
||||
v.SetLegend(fmt.Sprintf(cpuFmt,
|
||||
cases.Title(language.Und, cases.NoLower).String(c.GVR.R()),
|
||||
p.app.Config.K9s.Thresholds.SeverityColor(config.CPU, perc),
|
||||
render.PrintPerc(perc),
|
||||
nn[0],
|
||||
render.AsThousands(c.Tally(health.S1)),
|
||||
nn[1],
|
||||
render.AsThousands(c.Tally(health.S2)),
|
||||
))
|
||||
case client.MemGVR:
|
||||
perc := client.ToPercentage(c.Tally(health.S1), c.Tally(health.S2))
|
||||
v.SetLegend(fmt.Sprintf(memFmt,
|
||||
cases.Title(language.Und, cases.NoLower).String(c.GVR.R()),
|
||||
p.app.Config.K9s.Thresholds.SeverityColor(config.MEM, perc),
|
||||
render.PrintPerc(perc),
|
||||
nn[0],
|
||||
render.AsThousands(c.Tally(health.S1)),
|
||||
nn[1],
|
||||
render.AsThousands(c.Tally(health.S2)),
|
||||
))
|
||||
default:
|
||||
v.SetLegend(fmt.Sprintf(genFmat,
|
||||
cases.Title(language.Und, cases.NoLower).String(c.GVR.R()),
|
||||
nn[0],
|
||||
c.Tally(health.S1),
|
||||
nn[1],
|
||||
c.Tally(health.S2),
|
||||
))
|
||||
v.SetLegend(cases.Title(language.English).String(pt.GVR.R()))
|
||||
if pt.Faults > 0 {
|
||||
v.SetBorderColor(tcell.ColorDarkRed)
|
||||
} else {
|
||||
v.SetBorderColor(tcell.ColorDarkOliveGreen)
|
||||
}
|
||||
v.Add(tchart.Metric{S1: c.Tally(health.S1), S2: c.Tally(health.S2)})
|
||||
v.Add(pt.Total, pt.Faults)
|
||||
}
|
||||
|
||||
// PulseFailed notifies the load failed.
|
||||
|
|
@ -211,15 +273,13 @@ func (p *Pulse) PulseFailed(err error) {
|
|||
func (p *Pulse) bindKeys() {
|
||||
p.actions.Merge(ui.NewKeyActionsFromMap(ui.KeyMap{
|
||||
tcell.KeyEnter: ui.NewKeyAction("Goto", p.enterCmd, true),
|
||||
tcell.KeyTab: ui.NewKeyAction("Next", p.nextFocusCmd(1), true),
|
||||
tcell.KeyBacktab: ui.NewKeyAction("Prev", p.nextFocusCmd(-1), true),
|
||||
tcell.KeyTab: ui.NewKeyAction("Next", p.nextFocusCmd(dirLeft), true),
|
||||
tcell.KeyBacktab: ui.NewKeyAction("Prev", p.nextFocusCmd(dirRight), true),
|
||||
tcell.KeyDown: ui.NewKeyAction("Next", p.nextFocusCmd(dirDown), false),
|
||||
tcell.KeyUp: ui.NewKeyAction("Prev", p.nextFocusCmd(dirUp), false),
|
||||
tcell.KeyRight: ui.NewKeyAction("Next", p.nextFocusCmd(dirLeft), false),
|
||||
tcell.KeyLeft: ui.NewKeyAction("Next", p.nextFocusCmd(dirRight), false),
|
||||
}))
|
||||
|
||||
for i, v := range p.charts {
|
||||
tt := strings.Split(v.ID(), "/")
|
||||
t := cases.Title(language.Und, cases.NoLower).String(tt[len(tt)-1])
|
||||
p.actions.Add(ui.NumKeys[i], ui.NewKeyAction(t, p.sparkFocusCmd(i), true))
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Pulse) keyboard(evt *tcell.EventKey) *tcell.EventKey {
|
||||
|
|
@ -238,13 +298,40 @@ func (p *Pulse) defaultContext() context.Context {
|
|||
return context.WithValue(context.Background(), internal.KeyFactory, p.app.factory)
|
||||
}
|
||||
|
||||
func (*Pulse) Restart() {}
|
||||
|
||||
// Start initializes resource watch loop.
|
||||
func (p *Pulse) Start() {
|
||||
p.Stop()
|
||||
|
||||
ctx := p.defaultContext()
|
||||
ctx, p.cancelFn = context.WithCancel(ctx)
|
||||
p.model.Watch(ctx)
|
||||
gaugeChan, metricsChan, err := p.model.Watch(ctx)
|
||||
if err != nil {
|
||||
slog.Error("Pulse watch failed", slogs.Error, err)
|
||||
return
|
||||
}
|
||||
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case check, ok := <-gaugeChan:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
p.app.QueueUpdateDraw(func() {
|
||||
p.PulseChanged(check)
|
||||
})
|
||||
case mx, ok := <-metricsChan:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
p.app.QueueUpdateDraw(func() {
|
||||
p.SeriesChanged(mx)
|
||||
})
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// Stop terminates watch loop.
|
||||
|
|
@ -256,7 +343,7 @@ func (p *Pulse) Stop() {
|
|||
p.cancelFn = nil
|
||||
}
|
||||
|
||||
// Refresh updates the view.
|
||||
// Refresh updates the view
|
||||
func (*Pulse) Refresh() {}
|
||||
|
||||
// GVR returns a resource descriptor.
|
||||
|
|
@ -286,6 +373,8 @@ func (*Pulse) AddBindKeysFn(BindKeysFunc) {}
|
|||
// SetContextFn sets custom context.
|
||||
func (*Pulse) SetContextFn(ContextFunc) {}
|
||||
|
||||
func (*Pulse) GetContextFn() ContextFunc { return nil }
|
||||
|
||||
// GetTable return the view table if any.
|
||||
func (*Pulse) GetTable() *Table {
|
||||
return nil
|
||||
|
|
@ -306,24 +395,33 @@ func (*Pulse) ExtraHints() map[string]string {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (p *Pulse) sparkFocusCmd(i int) func(evt *tcell.EventKey) *tcell.EventKey {
|
||||
return func(*tcell.EventKey) *tcell.EventKey {
|
||||
p.app.SetFocus(p.charts[i])
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Pulse) enterCmd(*tcell.EventKey) *tcell.EventKey {
|
||||
v := p.App().GetFocus()
|
||||
s, ok := v.(Graphable)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
res := s.ID()
|
||||
if res == client.CpuGVR.String() || res == client.MemGVR.String() {
|
||||
res = client.PodGVR.String()
|
||||
g, ok := v.(Graphable)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
p.App().gotoResource(res+" all", "", false, true)
|
||||
p.prevFocusIndex = p.findIndex(g)
|
||||
for i := range len(p.charts) {
|
||||
gi := p.GetItem(i)
|
||||
if i == p.prevFocusIndex {
|
||||
gi.Focus = true
|
||||
} else {
|
||||
gi.Focus = false
|
||||
}
|
||||
}
|
||||
|
||||
p.Stop()
|
||||
res := client.NewGVR(s.ID()).R()
|
||||
if res == "cpu" || res == "mem" {
|
||||
res = p.app.Config.K9s.DefaultView
|
||||
}
|
||||
p.App().SetFocus(p.App().Main)
|
||||
p.App().gotoResource(res+" "+p.model.GetNamespace(), "", false, true)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
@ -331,10 +429,59 @@ func (p *Pulse) enterCmd(*tcell.EventKey) *tcell.EventKey {
|
|||
func (p *Pulse) nextFocusCmd(direction int) func(evt *tcell.EventKey) *tcell.EventKey {
|
||||
return func(*tcell.EventKey) *tcell.EventKey {
|
||||
v := p.app.GetFocus()
|
||||
index := findIndex(p.charts, v)
|
||||
p.GetItem(index).Focus = false
|
||||
p.GetItem(index).Item.Blur()
|
||||
i, v := nextFocus(p.charts, index+direction)
|
||||
g, ok := v.(Graphable)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
currentIndex := p.findIndex(g)
|
||||
nextIndex, total := currentIndex+direction, len(p.charts)
|
||||
if nextIndex < 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
switch direction {
|
||||
case dirLeft:
|
||||
if nextIndex >= total {
|
||||
return nil
|
||||
}
|
||||
p.prevFocusIndex = -1
|
||||
case dirRight:
|
||||
p.prevFocusIndex = -1
|
||||
case dirUp:
|
||||
if p.app.Conn().HasMetrics() {
|
||||
if currentIndex >= total-2 {
|
||||
if p.prevFocusIndex >= 0 && p.prevFocusIndex != currentIndex {
|
||||
nextIndex = p.prevFocusIndex
|
||||
} else if currentIndex == p.chartGVRs.Len()-1 {
|
||||
nextIndex += 1
|
||||
}
|
||||
} else {
|
||||
p.prevFocusIndex = currentIndex
|
||||
}
|
||||
}
|
||||
case dirDown:
|
||||
if p.app.Conn().HasMetrics() {
|
||||
if currentIndex >= total-6 && currentIndex < total-2 {
|
||||
switch {
|
||||
case (currentIndex % 4) <= 1:
|
||||
p.prevFocusIndex, nextIndex = currentIndex, total-2
|
||||
case (currentIndex % 4) <= 3:
|
||||
p.prevFocusIndex, nextIndex = currentIndex, total-1
|
||||
}
|
||||
} else if currentIndex >= total-2 {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
if nextIndex < 0 {
|
||||
nextIndex = 0
|
||||
} else if nextIndex > total-1 {
|
||||
nextIndex = currentIndex
|
||||
}
|
||||
p.GetItem(nextIndex).Focus = false
|
||||
p.GetItem(nextIndex).Item.Blur()
|
||||
i, v := p.nextFocus(nextIndex)
|
||||
p.GetItem(i).Focus = true
|
||||
p.app.SetFocus(v)
|
||||
|
||||
|
|
@ -342,34 +489,33 @@ func (p *Pulse) nextFocusCmd(direction int) func(evt *tcell.EventKey) *tcell.Eve
|
|||
}
|
||||
}
|
||||
|
||||
func (p *Pulse) makeSP(loc, span image.Point, gvr *client.GVR) *tchart.SparkLine {
|
||||
s := tchart.NewSparkLine(gvr.String())
|
||||
func (p *Pulse) makeSP(loc, span image.Point, gvr *client.GVR, unit string) *tchart.SparkLine {
|
||||
s := tchart.NewSparkLine(gvr.String(), unit)
|
||||
s.SetBackgroundColor(p.app.Styles.Charts().BgColor.Color())
|
||||
s.SetBorderPadding(0, 1, 0, 1)
|
||||
if cc, ok := p.app.Styles.Charts().ResourceColors[gvr.String()]; ok {
|
||||
s.SetSeriesColors(cc.Colors()...)
|
||||
} else {
|
||||
s.SetSeriesColors(p.app.Styles.Charts().DefaultChartColors.Colors()...)
|
||||
}
|
||||
s.SetLegend(fmt.Sprintf(" %s ", cases.Title(language.Und, cases.NoLower).String(gvr.R())))
|
||||
s.SetLegend(fmt.Sprintf(" %s ", cases.Title(language.English).String(gvr.R())))
|
||||
s.SetInputCapture(p.keyboard)
|
||||
s.SetMultiSeries(true)
|
||||
p.AddItem(s, loc.X, loc.Y, span.X, span.Y, 0, 0, true)
|
||||
p.AddItem(s, loc.X, loc.Y, span.X, span.Y, 0, 0, false)
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
func (p *Pulse) makeGA(loc, span image.Point, gvr *client.GVR) *tchart.Gauge {
|
||||
g := tchart.NewGauge(gvr.String())
|
||||
g.SetBorder(true)
|
||||
g.SetBackgroundColor(p.app.Styles.Charts().BgColor.Color())
|
||||
if cc, ok := p.app.Styles.Charts().ResourceColors[gvr.String()]; ok {
|
||||
g.SetSeriesColors(cc.Colors()...)
|
||||
} else {
|
||||
g.SetSeriesColors(p.app.Styles.Charts().DefaultDialColors.Colors()...)
|
||||
}
|
||||
g.SetLegend(fmt.Sprintf(" %s ", cases.Title(language.Und, cases.NoLower).String(gvr.R())))
|
||||
g.SetLegend(fmt.Sprintf(" %s ", cases.Title(language.English).String(gvr.R())))
|
||||
g.SetInputCapture(p.keyboard)
|
||||
p.AddItem(g, loc.X, loc.Y, span.X, span.Y, 0, 0, true)
|
||||
p.AddItem(g, loc.X, loc.Y, span.X, span.Y, 0, 0, false)
|
||||
|
||||
return g
|
||||
}
|
||||
|
|
@ -377,32 +523,23 @@ func (p *Pulse) makeGA(loc, span image.Point, gvr *client.GVR) *tchart.Gauge {
|
|||
// ----------------------------------------------------------------------------
|
||||
// Helpers
|
||||
|
||||
func nextFocus(pp []Graphable, index int) (int, tview.Primitive) {
|
||||
if index >= len(pp) {
|
||||
return 0, pp[0]
|
||||
func (p *Pulse) nextFocus(index int) (int, tview.Primitive) {
|
||||
if index >= len(p.chartGVRs) {
|
||||
return 0, p.charts[p.chartGVRs[0]]
|
||||
}
|
||||
|
||||
if index < 0 {
|
||||
return len(pp) - 1, pp[len(pp)-1]
|
||||
return len(p.chartGVRs) - 1, p.charts[p.chartGVRs[len(p.chartGVRs)-1]]
|
||||
}
|
||||
|
||||
return index, pp[index]
|
||||
return index, p.charts[p.chartGVRs[index]]
|
||||
}
|
||||
|
||||
func findIndex(pp []Graphable, p tview.Primitive) int {
|
||||
for i, v := range pp {
|
||||
if v == p {
|
||||
func (p *Pulse) findIndex(g Graphable) int {
|
||||
for i, gvr := range p.chartGVRs {
|
||||
if gvr.String() == g.ID() {
|
||||
return i
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func findIndexGVR(pp []Graphable, gvr *client.GVR) (int, bool) {
|
||||
for i, v := range pp {
|
||||
if v.ID() == gvr.String() {
|
||||
return i, true
|
||||
}
|
||||
}
|
||||
return 0, false
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
name: k9s
|
||||
base: core22
|
||||
version: 'v0.50.4'
|
||||
version: 'v0.50.5'
|
||||
summary: K9s is a CLI to view and manage your Kubernetes clusters.
|
||||
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.
|
||||
|
|
|
|||
Loading…
Reference in New Issue