// SPDX-License-Identifier: Apache-2.0 // Copyright Authors of K9s package dao import ( "context" "encoding/json" "errors" "fmt" "log/slog" "strconv" "strings" "github.com/derailed/k9s/internal" "github.com/derailed/k9s/internal/client" "github.com/derailed/k9s/internal/render" "github.com/derailed/k9s/internal/slogs" "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" ) const ( StatusOK = "OK" DegradedStatus = "DEGRADED" ) var ( SaGVR = client.NewGVR("v1/serviceaccounts") PvcGVR = client.NewGVR("v1/persistentvolumeclaims") PcGVR = client.NewGVR("scheduling.k8s.io/v1/priorityclasses") CmGVR = client.NewGVR("v1/configmaps") SecGVR = client.NewGVR("v1/secrets") PodGVR = client.NewGVR("v1/pods") SvcGVR = client.NewGVR("v1/services") DsGVR = client.NewGVR("apps/v1/daemonsets") StsGVR = client.NewGVR("apps/v1/statefulSets") DpGVR = client.NewGVR("apps/v1/deployments") RsGVR = client.NewGVR("apps/v1/replicasets") resList = []client.GVR{PodGVR, SvcGVR, DsGVR, StsGVR, DpGVR, RsGVR} ) // Workload tracks a select set of resources in a given namespace. type Workload struct { Table } func (w *Workload) Delete(ctx context.Context, path string, propagation *metav1.DeletionPropagation, grace Grace) error { gvr, _ := ctx.Value(internal.KeyGVR).(client.GVR) ns, n := client.Namespaced(path) auth, err := w.Client().CanI(ns, gvr.String(), n, []string{client.DeleteVerb}) if err != nil { return err } if !auth { return fmt.Errorf("user is not authorized to delete %s", path) } var gracePeriod *int64 if grace != DefaultGrace { gracePeriod = (*int64)(&grace) } opts := metav1.DeleteOptions{ PropagationPolicy: propagation, GracePeriodSeconds: gracePeriod, } ctx, cancel := context.WithTimeout(ctx, w.Client().Config().CallTimeout()) defer cancel() d, err := w.Client().DynDial() if err != nil { return err } dial := d.Resource(gvr.GVR()) if client.IsClusterScoped(ns) { return dial.Delete(ctx, n, opts) } return dial.Namespace(ns).Delete(ctx, n, opts) } func (a *Workload) fetch(ctx context.Context, gvr client.GVR, ns string) (*metav1.Table, error) { a.gvr = gvr oo, err := a.Table.List(ctx, ns) if err != nil { return nil, err } if len(oo) == 0 { return nil, fmt.Errorf("no table found for gvr: %s", gvr) } tt, ok := oo[0].(*metav1.Table) if !ok { return nil, errors.New("not a metav1.Table") } return tt, nil } // List fetch workloads. func (a *Workload) List(ctx context.Context, ns string) ([]runtime.Object, error) { oo := make([]runtime.Object, 0, 100) for _, gvr := range resList { table, err := a.fetch(ctx, gvr, ns) if err != nil { return nil, err } var ( ns string ts metav1.Time ) for _, r := range table.Rows { if obj := r.Object.Object; obj != nil { if m, err := meta.Accessor(obj); err == nil { ns, ts = m.GetNamespace(), m.GetCreationTimestamp() } } else { var m metav1.PartialObjectMetadata if err := json.Unmarshal(r.Object.Raw, &m); err == nil { ns, ts = m.GetNamespace(), m.CreationTimestamp } } stat := status(gvr, r, table.ColumnDefinitions) oo = append(oo, &render.WorkloadRes{Row: metav1.TableRow{Cells: []interface{}{ gvr.String(), ns, r.Cells[indexOf("Name", table.ColumnDefinitions)], stat, readiness(gvr, r, table.ColumnDefinitions), validity(stat), ts, }}}) } } return oo, nil } // Helpers... func readiness(gvr client.GVR, r metav1.TableRow, h []metav1.TableColumnDefinition) string { switch gvr { case PodGVR, DpGVR, StsGVR: return r.Cells[indexOf("Ready", h)].(string) case RsGVR, DsGVR: c := r.Cells[indexOf("Ready", h)].(int64) d := r.Cells[indexOf("Desired", h)].(int64) return fmt.Sprintf("%d/%d", c, d) case SvcGVR: return "" } return render.NAValue } func status(gvr client.GVR, r metav1.TableRow, h []metav1.TableColumnDefinition) string { switch gvr { case PodGVR: if status := r.Cells[indexOf("Status", h)]; status == render.PhaseCompleted { return StatusOK } else if !isReady(r.Cells[indexOf("Ready", h)].(string)) || status != render.PhaseRunning { return DegradedStatus } case DpGVR, StsGVR: if !isReady(r.Cells[indexOf("Ready", h)].(string)) { return DegradedStatus } case RsGVR, DsGVR: rd, ok1 := r.Cells[indexOf("Ready", h)].(int64) de, ok2 := r.Cells[indexOf("Desired", h)].(int64) if ok1 && ok2 { if !isReady(fmt.Sprintf("%d/%d", rd, de)) { return DegradedStatus } break } rds, oks1 := r.Cells[indexOf("Ready", h)].(string) des, oks2 := r.Cells[indexOf("Desired", h)].(string) if oks1 && oks2 { if !isReady(fmt.Sprintf("%s/%s", rds, des)) { return DegradedStatus } } case SvcGVR: default: return render.MissingValue } return StatusOK } func validity(status string) string { if status != "DEGRADED" { return "" } return status } func isReady(s string) bool { tt := strings.Split(s, "/") if len(tt) != 2 { return false } r, err := strconv.Atoi(tt[0]) if err != nil { slog.Error("Invalid ready count", slogs.Error, err, slogs.Count, tt[0], ) return false } c, err := strconv.Atoi(tt[1]) if err != nil { slog.Error("invalid expected count: %q", slogs.Error, err, slogs.Count, tt[1], ) return false } if c == 0 { return true } return r == c } func indexOf(n string, defs []metav1.TableColumnDefinition) int { for i, d := range defs { if d.Name == n { return i } } return -1 }