Skip to content

Commit

Permalink
Add sanitize command (derailed#2286)
Browse files Browse the repository at this point in the history
- Sanitize provides for clearing out pods in either completed/failed
  state derailed#2277
  • Loading branch information
derailed committed Nov 11, 2023
1 parent 19952cd commit 5540b5a
Show file tree
Hide file tree
Showing 23 changed files with 745 additions and 44 deletions.
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ DATE ?= $(shell TZ=UTC date -j -f "%s" ${SOURCE_DATE_EPOCH} +"%Y-%m-%dT%H:
else
DATE ?= $(shell date -u -d @${SOURCE_DATE_EPOCH} +"%Y-%m-%dT%H:%M:%SZ")
endif
VERSION ?= v0.28.0
VERSION ?= v0.28.1
IMG_NAME := derailed/k9s
IMAGE := ${IMG_NAME}:${VERSION}

Expand Down
63 changes: 63 additions & 0 deletions change_logs/release_v0.28.1.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
<img src="https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s.png" align="center" width="800" height="auto"/>

# Release v0.28.1

## Notes

Thank you to all that contributed with flushing out issues and enhancements for K9s! I'll try to mark some of these issues as fixed. But if you don't mind grab the latest rev and see if we're happier with some of the fixes! If you've filed an issue please help me verify and close. Your support, kindness and awesome suggestions to make K9s better are, as ever, very much noted and appreciated! Also big thanks to all that have allocated their own time to help others on both slack and on this repo!!

As you may know, K9s is not pimped out by corps with deep pockets, thus if you feel K9s is helping your Kubernetes journey, please consider joining our [sponsorship program](https://github.com/sponsors/derailed) and/or make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer)

On Slack? Please join us [K9slackers](https://join.slack.com/t/k9sers/shared_invite/enQtOTA5MDEyNzI5MTU0LWQ1ZGI3MzliYzZhZWEyNzYxYzA3NjE0YTk1YmFmNzViZjIyNzhkZGI0MmJjYzhlNjdlMGJhYzE2ZGU1NjkyNTM)

---

## ♫ Sounds Behind The Release ♭

* [If Trouble Was Money - Albert Collins](https://www.youtube.com/watch?v=cz6LbWWqX-g)
* [Old Love - Eric Clapton](https://www.youtube.com/watch?v=EklciRHZnUQ)
* [Touch And GO - The Cars](https://www.youtube.com/watch?v=L7Gpr_Auz8Y)

---

## A Word From Our Sponsors...

To all the good folks below that opted to `pay it forward` and join our sponsorship program, I salute you!!

* [Bradley Heilbrun](https://github.com/bheilbrun)

> Sponsorship cancellations since the last release: `2` ;(
---

## Feature Release

### Sanitize Me!

Over time, you might end up with a lot of pod cruft on your cluster. Pods that might be completed, erroring out, etc... Once you've completed your pod analysis it could be useful to clear out these pods from your cluster.

In this drop, we introduce a new command `sanitize` aka `z` available on pod views otherwise known as `The Axe!`. This command performs a clean up of all pods that are in either in completed, crashloopBackoff or failed state. This could be especially handy if you run workflows jobs or commands on your cluster that might leave lots of `turd` pods. Tho this has a `phat` fail safe dialog please be careful with this one as it is a blunt tool!

---

## Resolved Issues

* [Issue #2281](https://github.com/derailed/k9s/issues/2281) Can't run Node shell
* [Issue #2277](https://github.com/derailed/k9s/issues/2277) bulk actions applied to power filters
* [Issue #2273](https://github.com/derailed/k9s/issues/2273) Error when draining node that is cordoned bug
* [Issue #2233](https://github.com/derailed/k9s/issues/2233) Invalid port-forwarding status displayed over the k9s UI

---

## 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!!

* [PR #2280](https://github.com/derailed/k9s/pull/2280) chore: replace github.com/ghodss/yaml with sigs.k8s.
* [PR #2278](https://github.com/derailed/k9s/pull/2278) README.md: fix typo in netshoot URL
* [PR #2275](https://github.com/derailed/k9s/pull/2275) check if the Node already cordoned when executing Drain
* [PR #2247](https://github.com/derailed/k9s/pull/2247) Delete port forwards when pods get deleted

---

<img src="https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png" width="32" height="auto"/> © 2023 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ require (
k8s.io/client-go v0.28.3
k8s.io/klog/v2 v2.100.1
k8s.io/kubectl v0.28.3
k8s.io/kubernetes v1.28.3
k8s.io/metrics v0.28.3
sigs.k8s.io/yaml v1.3.0
)
Expand Down
6 changes: 4 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -245,8 +245,8 @@ github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/u
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
github.com/karrick/godirwalk v1.16.1 h1:DynhcF+bztK8gooS0+NDJFrdNZjJ3gzVzC545UNA9iw=
github.com/karrick/godirwalk v1.16.1/go.mod h1:j4mkqPuvaLI8mp1DroR3P6ad7cyYd4c1qeJ3RV7ULlk=
github.com/karrick/godirwalk v1.17.0 h1:b4kY7nqDdioR/6qnbHQyDvmA17u5G1cZ6J+CZXwSWoI=
github.com/karrick/godirwalk v1.17.0/go.mod h1:j4mkqPuvaLI8mp1DroR3P6ad7cyYd4c1qeJ3RV7ULlk=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/compress v1.16.0 h1:iULayQNOReoYUe+1qtKOqw9CwJv3aNQu8ivo7lw1HU4=
Expand Down Expand Up @@ -628,6 +628,8 @@ k8s.io/kube-openapi v0.0.0-20230717233707-2695361300d9 h1:LyMgNKD2P8Wn1iAwQU5Ohx
k8s.io/kube-openapi v0.0.0-20230717233707-2695361300d9/go.mod h1:wZK2AVp1uHCp4VamDVgBP2COHZjqD1T68Rf0CM3YjSM=
k8s.io/kubectl v0.28.3 h1:H1Peu1O3EbN9zHkJCcvhiJ4NUj6lb88sGPO5wrWIM6k=
k8s.io/kubectl v0.28.3/go.mod h1:RDAudrth/2wQ3Sg46fbKKl4/g+XImzvbsSRZdP2RiyE=
k8s.io/kubernetes v1.28.3 h1:XTci6gzk+JR51UZuZQCFJ4CsyUkfivSjLI4O1P9z6LY=
k8s.io/kubernetes v1.28.3/go.mod h1:NhAysZWvHtNcJFFHic87ofxQN7loylCQwg3ZvXVDbag=
k8s.io/metrics v0.28.3 h1:w2s3kVi7HulXqCVDFkF4hN/OsL1tXTTb4Biif995h/g=
k8s.io/metrics v0.28.3/go.mod h1:OZZ23AHFojPzU6r3xoHGRUcV3I9pauLua+07sAUbwLc=
k8s.io/utils v0.0.0-20230406110748-d93618cff8a2 h1:qY1Ad8PODbnymg2pRbkyMT/ylpTrCM8P2RJ0yroCyIk=
Expand Down
10 changes: 5 additions & 5 deletions internal/dao/dp.go
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ func (d *Deployment) Restart(ctx context.Context, path string) error {

// TailLogs tail logs for all pods represented by this Deployment.
func (d *Deployment) TailLogs(ctx context.Context, opts *LogOptions) ([]LogChan, error) {
dp, err := d.Load(d.Factory, opts.Path)
dp, err := d.GetInstance(d.Factory, opts.Path)
if err != nil {
return nil, err
}
Expand All @@ -128,16 +128,16 @@ func (d *Deployment) TailLogs(ctx context.Context, opts *LogOptions) ([]LogChan,

// Pod returns a pod victim by name.
func (d *Deployment) Pod(fqn string) (string, error) {
dp, err := d.Load(d.Factory, fqn)
dp, err := d.GetInstance(d.Factory, fqn)
if err != nil {
return "", err
}

return podFromSelector(d.Factory, dp.Namespace, dp.Spec.Selector.MatchLabels)
}

// Load returns a deployment instance.
func (*Deployment) Load(f Factory, fqn string) (*appsv1.Deployment, error) {
// GetInstance fetch a matching deployment.
func (*Deployment) GetInstance(f Factory, fqn string) (*appsv1.Deployment, error) {
o, err := f.Get("apps/v1/deployments", fqn, true, labels.Everything())
if err != nil {
return nil, err
Expand Down Expand Up @@ -240,7 +240,7 @@ func (d *Deployment) Scan(ctx context.Context, gvr, fqn string, wait bool) (Refs

// GetPodSpec returns a pod spec given a resource.
func (d *Deployment) GetPodSpec(path string) (*v1.PodSpec, error) {
dp, err := d.Load(d.Factory, path)
dp, err := d.GetInstance(d.Factory, path)
if err != nil {
return nil, err
}
Expand Down
34 changes: 34 additions & 0 deletions internal/dao/pod.go
Original file line number Diff line number Diff line change
Expand Up @@ -500,3 +500,37 @@ func GetDefaultContainer(m metav1.ObjectMeta, spec v1.PodSpec) (string, bool) {

return "", false
}

func (p *Pod) Sanitize(ctx context.Context, ns string) (int, error) {
oo, err := p.Resource.List(ctx, ns)
if err != nil {
return 0, err
}

var count int
for _, o := range oo {
u, ok := o.(*unstructured.Unstructured)
if !ok {
continue
}
var pod v1.Pod
err = runtime.DefaultUnstructuredConverter.FromUnstructured(u.Object, &pod)
if err != nil {
continue
}
log.Debug().Msgf("Pod status: %q", render.PodStatus(&pod))
switch render.PodStatus(&pod) {
case render.PhaseCompleted, render.PhaseCrashLoop, render.PhaseError, render.PhaseImagePullBackOff, render.PhaseOOMKilled:
log.Debug().Msgf("Sanitizing %s:%s", pod.Namespace, pod.Name)
fqn := client.FQN(pod.Namespace, pod.Name)
if err := p.Resource.Delete(ctx, fqn, nil, NowGrace); err != nil {
log.Warn().Err(err).Msgf("Pod %s deletion failed", fqn)
continue
}
count++
}
}
log.Debug().Msgf("Sanitizer deleted %d pods", count)

return count, nil
}
5 changes: 5 additions & 0 deletions internal/dao/port_forwarder.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,11 @@ func NewPortForwarder(f Factory) *PortForwarder {
}
}

// String dumps as string.
func (p *PortForwarder) String() string {
return fmt.Sprintf("%s|%s", p.path, p.tunnel)
}

// Age returns the port forward age.
func (p *PortForwarder) Age() string {
return time.Since(p.age).String()
Expand Down
2 changes: 1 addition & 1 deletion internal/dao/rs.go
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ func (r *ReplicaSet) Rollback(fqn string) error {
}

var ddp Deployment
dp, err := ddp.Load(r.Factory, client.FQN(rs.Namespace, name))
dp, err := ddp.GetInstance(r.Factory, client.FQN(rs.Namespace, name))
if err != nil {
return err
}
Expand Down
41 changes: 34 additions & 7 deletions internal/dao/sts.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,15 +67,19 @@ func (s *StatefulSet) Scale(ctx context.Context, path string, replicas int32) er

// Restart a StatefulSet rollout.
func (s *StatefulSet) Restart(ctx context.Context, path string) error {
o, err := s.GetFactory().Get("apps/v1/statefulsets", path, true, labels.Everything())
sts, err := s.GetInstance(s.Factory, path)
if err != nil {
return err
}
var sts appsv1.StatefulSet
err = runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &sts)

ns, _ := client.Namespaced(path)
pp, err := podsFromSelector(s.Factory, ns, sts.Spec.Selector.MatchLabels)
if err != nil {
return err
}
for _, p := range pp {
s.Forwarders().Kill(client.FQN(p.Namespace, p.Name))
}

auth, err := s.Client().CanI(sts.Namespace, "apps/v1/statefulsets", []string{client.PatchVerb})
if err != nil {
Expand All @@ -90,12 +94,12 @@ func (s *StatefulSet) Restart(ctx context.Context, path string) error {
return err
}

before, err := runtime.Encode(scheme.Codecs.LegacyCodec(appsv1.SchemeGroupVersion), &sts)
before, err := runtime.Encode(scheme.Codecs.LegacyCodec(appsv1.SchemeGroupVersion), sts)
if err != nil {
return err
}

after, err := polymorphichelpers.ObjectRestarterFn(&sts)
after, err := polymorphichelpers.ObjectRestarterFn(sts)
if err != nil {
return err
}
Expand All @@ -115,8 +119,8 @@ func (s *StatefulSet) Restart(ctx context.Context, path string) error {

}

// Load returns a statefulset instance.
func (*StatefulSet) Load(f Factory, fqn string) (*appsv1.StatefulSet, error) {
// GetInstance returns a statefulset instance.
func (*StatefulSet) GetInstance(f Factory, fqn string) (*appsv1.StatefulSet, error) {
o, err := f.Get("apps/v1/statefulsets", fqn, true, labels.Everything())
if err != nil {
return nil, err
Expand Down Expand Up @@ -301,3 +305,26 @@ func (s *StatefulSet) SetImages(ctx context.Context, path string, imageSpecs Ima
)
return err
}

func podsFromSelector(f Factory, ns string, sel map[string]string) ([]*v1.Pod, error) {
oo, err := f.List("v1/pods", ns, true, labels.Set(sel).AsSelector())
if err != nil {
return nil, err
}

if len(oo) == 0 {
return nil, fmt.Errorf("no matching pods for %v", sel)
}

pp := make([]*v1.Pod, 0, len(oo))
for _, o := range oo {
pod := new(v1.Pod)
err = runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, pod)
if err != nil {
return nil, err
}
pp = append(pp, pod)
}

return pp, nil
}
6 changes: 6 additions & 0 deletions internal/dao/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -155,3 +155,9 @@ type ContainsPodSpec interface {
// Set Images for a resource
SetImages(ctx context.Context, path string, imageSpecs ImageSpecs) error
}

// Sanitizer represents a resource sanitizer.
type Sanitizer interface {
// Sanitize nukes all resources in unhappy state.
Sanitize(context.Context, string) (int, error)
}
5 changes: 5 additions & 0 deletions internal/port/tunnel.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,11 @@ func NewPortTunnel(a, co, lp, cp string) PortTunnel {
}
}

// String dumps as string.
func (t PortTunnel) String() string {
return fmt.Sprintf("%s|%s|%s:%s", t.Address, t.Container, t.LocalPort, t.ContainerPort)
}

// PortMap returns a port mapping.
func (t PortTunnel) PortMap() string {
if t.LocalPort == "" {
Expand Down
Loading

0 comments on commit 5540b5a

Please sign in to comment.