Skip to content

Commit fcc36d6

Browse files
committed
feature: Enhance view resource kubectl output with conditions and metadata
Add richer kubectl get output for view resources with heuristic-based columns: - NAMESPACE column for namespaced resources (replaces AGE which was unpopulated) - CONDITIONS column showing status.conditions with prioritization (Ready, Available, Progressing, Degraded shown first) - LABELS and ANNOTATIONS columns in wide mode (-o wide)
1 parent 765cacc commit fcc36d6

1 file changed

Lines changed: 169 additions & 29 deletions

File tree

pkg/apiserver/storage.go

Lines changed: 169 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package apiserver
33
import (
44
"context"
55
"fmt"
6+
"strings"
67

78
"github.com/go-logr/logr"
89
apierrors "k8s.io/apimachinery/pkg/api/errors"
@@ -11,7 +12,6 @@ import (
1112
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
1213
"k8s.io/apimachinery/pkg/runtime"
1314
"k8s.io/apimachinery/pkg/runtime/schema"
14-
"k8s.io/apimachinery/pkg/util/duration"
1515
"k8s.io/apimachinery/pkg/watch"
1616
genericapirequest "k8s.io/apiserver/pkg/endpoints/request"
1717
"k8s.io/apiserver/pkg/registry/rest"
@@ -352,20 +352,7 @@ func (s *ClientDelegatedStorage) ConvertToTable(ctx context.Context, object runt
352352
APIVersion: metav1.SchemeGroupVersion.String(),
353353
Kind: "Table",
354354
},
355-
ColumnDefinitions: []metav1.TableColumnDefinition{
356-
{
357-
Name: "Name",
358-
Type: "string",
359-
Format: "name",
360-
Description: "Name of the resource",
361-
},
362-
{
363-
Name: "Age",
364-
Type: "string",
365-
Format: "date",
366-
Description: "Age of the resource",
367-
},
368-
},
355+
ColumnDefinitions: s.getColumnDefinitions(),
369356
}
370357

371358
// Handle both single objects and lists.
@@ -396,33 +383,186 @@ func (s *ClientDelegatedStorage) ConvertToTable(ctx context.Context, object runt
396383
return table, nil
397384
}
398385

386+
// getColumnDefinitions returns the column definitions for table output.
387+
// Priority 0 columns are shown by default, priority 1 columns are shown with -o wide.
388+
func (s *ClientDelegatedStorage) getColumnDefinitions() []metav1.TableColumnDefinition {
389+
columns := []metav1.TableColumnDefinition{
390+
{
391+
Name: "Name",
392+
Type: "string",
393+
Format: "name",
394+
Description: "Name of the resource",
395+
Priority: 0,
396+
},
397+
{
398+
Name: "Conditions",
399+
Type: "string",
400+
Description: "Status conditions summary",
401+
Priority: 0,
402+
},
403+
{
404+
Name: "Labels",
405+
Type: "string",
406+
Description: "Resource labels",
407+
Priority: 1,
408+
},
409+
{
410+
Name: "Annotations",
411+
Type: "string",
412+
Description: "Number of annotations",
413+
Priority: 1,
414+
},
415+
}
416+
417+
// Add namespace column at the beginning for namespaced resources.
418+
if s.NamespaceScoped() {
419+
columns = append([]metav1.TableColumnDefinition{
420+
{
421+
Name: "Namespace",
422+
Type: "string",
423+
Description: "Namespace of the resource",
424+
Priority: 0,
425+
},
426+
}, columns...)
427+
}
428+
429+
return columns
430+
}
431+
399432
// objectToTableRow converts a single unstructured object to a table row.
400433
func (s *ClientDelegatedStorage) objectToTableRow(obj *unstructured.Unstructured) (metav1.TableRow, error) { //nolint:unparam
401434
name := obj.GetName()
402-
creationTime := obj.GetCreationTimestamp()
435+
namespace := obj.GetNamespace()
403436

404-
// Calculate age
405-
age := ""
406-
if !creationTime.IsZero() {
407-
age = translateTimestampSince(creationTime)
437+
// Extract conditions summary.
438+
conditions := extractConditionsSummary(obj)
439+
440+
// Format labels and annotations.
441+
labels := formatLabels(obj.GetLabels())
442+
annotations := formatAnnotations(obj.GetAnnotations())
443+
444+
// Build cells - namespace comes first for namespaced resources.
445+
var cells []interface{}
446+
if s.NamespaceScoped() {
447+
cells = []interface{}{namespace, name, conditions, labels, annotations}
448+
} else {
449+
cells = []interface{}{name, conditions, labels, annotations}
408450
}
409451

410452
row := metav1.TableRow{
411-
Cells: []interface{}{name, age},
453+
Cells: cells,
412454
Object: runtime.RawExtension{Object: obj},
413455
}
414456

415457
return row, nil
416458
}
417459

418-
// translateTimestampSince returns a human-readable approximation of how long ago a timestamp
419-
// occurred (similar to kubectl's age column).
420-
func translateTimestampSince(timestamp metav1.Time) string {
421-
if timestamp.IsZero() {
422-
return "<unknown>"
460+
// extractConditionsSummary extracts and formats status.conditions from an unstructured object.
461+
// It looks for status.conditions as a list of maps with "type" and "status" fields.
462+
// Returns a comma-separated list of "Type:Status" pairs, or "<none>" if no conditions exist.
463+
// Well-known condition types (Ready, Available, Progressing) are prioritized and shown first.
464+
func extractConditionsSummary(obj *unstructured.Unstructured) string {
465+
// Try to extract status.conditions.
466+
status, found, err := unstructured.NestedFieldNoCopy(obj.Object, "status", "conditions")
467+
if !found || err != nil {
468+
return "<none>"
469+
}
470+
471+
// status.conditions should be a slice of maps.
472+
conditions, ok := status.([]interface{})
473+
if !ok || len(conditions) == 0 {
474+
return "<none>"
475+
}
476+
477+
// Extract type:status pairs and prioritize well-known types.
478+
priorityConditions := []string{} // Ready, Available, Progressing, etc.
479+
otherConditions := []string{}
480+
481+
// Define well-known condition types in priority order.
482+
wellKnownTypes := map[string]int{
483+
"Ready": 1,
484+
"Available": 2,
485+
"Progressing": 3,
486+
"Degraded": 4,
487+
}
488+
489+
for _, cond := range conditions {
490+
condMap, ok := cond.(map[string]interface{})
491+
if !ok {
492+
continue
493+
}
494+
495+
condType, typeOk := condMap["type"].(string)
496+
condStatus, statusOk := condMap["status"].(string)
497+
if typeOk && statusOk {
498+
pair := fmt.Sprintf("%s:%s", condType, condStatus)
499+
if priority, isWellKnown := wellKnownTypes[condType]; isWellKnown {
500+
// Insert in priority order.
501+
inserted := false
502+
for i, existing := range priorityConditions {
503+
existingType := strings.Split(existing, ":")[0]
504+
if wellKnownTypes[existingType] > priority {
505+
priorityConditions = append(priorityConditions[:i], append([]string{pair}, priorityConditions[i:]...)...)
506+
inserted = true
507+
break
508+
}
509+
}
510+
if !inserted {
511+
priorityConditions = append(priorityConditions, pair)
512+
}
513+
} else {
514+
otherConditions = append(otherConditions, pair)
515+
}
516+
}
517+
}
518+
519+
// Combine priority conditions first, then others.
520+
allPairs := append(priorityConditions, otherConditions...)
521+
522+
if len(allPairs) == 0 {
523+
return "<none>"
524+
}
525+
526+
// Limit to first 3 conditions to avoid excessive width.
527+
if len(allPairs) > 3 {
528+
remaining := len(allPairs) - 3
529+
pairs := allPairs[:3]
530+
return fmt.Sprintf("%s +%d more", strings.Join(pairs, ","), remaining)
531+
}
532+
533+
return strings.Join(allPairs, ",")
534+
}
535+
536+
// formatLabels formats labels as a comma-separated list of key=value pairs.
537+
// Returns "<none>" if no labels exist.
538+
func formatLabels(labels map[string]string) string {
539+
if len(labels) == 0 {
540+
return "<none>"
541+
}
542+
543+
// Convert to key=value pairs.
544+
pairs := make([]string, 0, len(labels))
545+
for k, v := range labels {
546+
pairs = append(pairs, fmt.Sprintf("%s=%s", k, v))
547+
}
548+
549+
// Limit display to avoid excessive width.
550+
if len(pairs) > 3 {
551+
remaining := len(pairs) - 3
552+
pairs = pairs[:3]
553+
return fmt.Sprintf("%s +%d more", strings.Join(pairs, ","), remaining)
554+
}
555+
556+
return strings.Join(pairs, ",")
557+
}
558+
559+
// formatAnnotations formats annotations count or key annotations.
560+
// Returns the count as "N annotations" or "<none>" if no annotations exist.
561+
func formatAnnotations(annotations map[string]string) string {
562+
if len(annotations) == 0 {
563+
return "<none>"
423564
}
424565

425-
// Use Kubernetes' duration formatting
426-
d := metav1.Now().Sub(timestamp.Time)
427-
return duration.ShortHumanDuration(d)
566+
// Just show the count for annotations as they can be verbose.
567+
return fmt.Sprintf("%d", len(annotations))
428568
}

0 commit comments

Comments
 (0)