@@ -3,6 +3,7 @@ package apiserver
33import (
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.
400433func (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