diff --git a/cmd/kubectl-tree/rootcmd.go b/cmd/kubectl-tree/rootcmd.go index 4a29586..ad55040 100644 --- a/cmd/kubectl-tree/rootcmd.go +++ b/cmd/kubectl-tree/rootcmd.go @@ -35,11 +35,15 @@ import ( ) const ( - allNamespacesFlag = "all-namespaces" - colorFlag = "color" + allNamespacesFlag = "all-namespaces" + colorFlag = "color" + conditionTypesFlag = "condition-types" ) -var cf *genericclioptions.ConfigFlags +var ( + cf *genericclioptions.ConfigFlags + conditionTypes []string +) // This variable is populated by goreleaser var version string @@ -84,6 +88,15 @@ func run(command *cobra.Command, args []string) error { return errors.Errorf("invalid value for --%s", colorFlag) } + conditionTypes, err = command.Flags().GetStringSlice(conditionTypesFlag) + if err != nil { + return err + } + if len(conditionTypes) == 0 { + // Default to "Ready" if not specified + conditionTypes = []string{"Ready"} + } + restConfig, err := cf.ToRESTConfig() if err != nil { return err @@ -160,7 +173,7 @@ func run(command *cobra.Command, args []string) error { fmt.Println("No resources are owned by this object through ownerReferences.") return nil } - treeView(os.Stderr, objs, *obj) + treeView(os.Stderr, objs, *obj, conditionTypes) klog.V(2).Infof("done printing tree view") return nil } @@ -180,6 +193,7 @@ func init() { rootCmd.Flags().BoolP(allNamespacesFlag, "A", false, "query all objects in all API groups, both namespaced and non-namespaced") rootCmd.Flags().StringP(colorFlag, "c", "auto", "Enable or disable color output. This can be 'always', 'never', or 'auto' (default = use color only if using tty). The flag is overridden by the NO_COLOR env variable if set.") + rootCmd.Flags().StringSlice(conditionTypesFlag, []string{}, "Comma-separated list of condition types to check (default: Ready). Example: Ready,Processed,Scheduled") cf.AddFlags(rootCmd.Flags()) if err := flag.Set("logtostderr", "true"); err != nil { diff --git a/cmd/kubectl-tree/status.go b/cmd/kubectl-tree/status.go index 101bb64..f572831 100644 --- a/cmd/kubectl-tree/status.go +++ b/cmd/kubectl-tree/status.go @@ -11,7 +11,7 @@ import ( type ReadyStatus string // True False Unknown or "" type Reason string -func extractStatus(obj unstructured.Unstructured) (ReadyStatus, Reason, status.Status) { +func extractStatus(obj unstructured.Unstructured, conditionTypes []string) (ReadyStatus, Reason, status.Status) { jsonVal, _ := json.Marshal(obj.Object["status"]) klog.V(6).Infof("status for object=%s/%s: %s", obj.GetKind(), obj.GetName(), string(jsonVal)) result, err := status.Compute(&obj) @@ -35,19 +35,22 @@ func extractStatus(obj unstructured.Unstructured) (ReadyStatus, Reason, status.S return "", "", "" } - for _, cond := range conditionsV { - condM, ok := cond.(map[string]interface{}) - if !ok { - return "", "", "" - } - condType, ok := condM["type"].(string) - if !ok { - return "", "", "" - } - if condType == "Ready" { - condStatus, _ := condM["status"].(string) - condReason, _ := condM["reason"].(string) - return ReadyStatus(condStatus), Reason(condReason), status.Status(result.Status.String()) + // Check each requested condition type in order + for _, targetCondType := range conditionTypes { + for _, cond := range conditionsV { + condM, ok := cond.(map[string]interface{}) + if !ok { + continue + } + condType, ok := condM["type"].(string) + if !ok { + continue + } + if condType == targetCondType { + condStatus, _ := condM["status"].(string) + condReason, _ := condM["reason"].(string) + return ReadyStatus(condStatus), Reason(condReason), status.Status(result.Status.String()) + } } } return "", "", "" diff --git a/cmd/kubectl-tree/status_test.go b/cmd/kubectl-tree/status_test.go new file mode 100644 index 0000000..b148122 --- /dev/null +++ b/cmd/kubectl-tree/status_test.go @@ -0,0 +1,120 @@ +package main + +import ( + "testing" + + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" +) + +func TestExtractStatusWithMultipleConditionTypes(t *testing.T) { + tests := []struct { + name string + obj unstructured.Unstructured + conditionTypes []string + wantReady ReadyStatus + wantReason Reason + }{ + { + name: "finds Ready condition", + obj: unstructured.Unstructured{ + Object: map[string]interface{}{ + "status": map[string]interface{}{ + "conditions": []interface{}{ + map[string]interface{}{ + "type": "Ready", + "status": "True", + "reason": "AllGood", + }, + }, + }, + }, + }, + conditionTypes: []string{"Ready"}, + wantReady: "True", + wantReason: "AllGood", + }, + { + name: "finds Processed condition", + obj: unstructured.Unstructured{ + Object: map[string]interface{}{ + "status": map[string]interface{}{ + "conditions": []interface{}{ + map[string]interface{}{ + "type": "Processed", + "status": "True", + "reason": "ProcessingComplete", + }, + }, + }, + }, + }, + conditionTypes: []string{"Processed"}, + wantReady: "True", + wantReason: "ProcessingComplete", + }, + { + name: "finds first matching condition from multiple types", + obj: unstructured.Unstructured{ + Object: map[string]interface{}{ + "status": map[string]interface{}{ + "conditions": []interface{}{ + map[string]interface{}{ + "type": "Scheduled", + "status": "True", + "reason": "ScheduledOK", + }, + map[string]interface{}{ + "type": "Processed", + "status": "False", + "reason": "ProcessingFailed", + }, + }, + }, + }, + }, + conditionTypes: []string{"Ready", "Processed", "Scheduled"}, + wantReady: "False", + wantReason: "ProcessingFailed", + }, + { + name: "returns empty when condition type not found", + obj: unstructured.Unstructured{ + Object: map[string]interface{}{ + "status": map[string]interface{}{ + "conditions": []interface{}{ + map[string]interface{}{ + "type": "Ready", + "status": "True", + "reason": "AllGood", + }, + }, + }, + }, + }, + conditionTypes: []string{"NonExistent"}, + wantReady: "", + wantReason: "", + }, + { + name: "handles object without status", + obj: unstructured.Unstructured{ + Object: map[string]interface{}{}, + }, + conditionTypes: []string{"Ready"}, + wantReady: "", + wantReason: "", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + gotReady, gotReason, _ := extractStatus(tt.obj, tt.conditionTypes) + if gotReady != tt.wantReady { + t.Errorf("extractStatus() gotReady = %v, want %v", gotReady, tt.wantReady) + } + if gotReason != tt.wantReason { + t.Errorf("extractStatus() gotReason = %v, want %v", gotReason, tt.wantReason) + } + }) + } +} diff --git a/cmd/kubectl-tree/tree.go b/cmd/kubectl-tree/tree.go index 3988e08..dee7111 100644 --- a/cmd/kubectl-tree/tree.go +++ b/cmd/kubectl-tree/tree.go @@ -28,16 +28,16 @@ var ( ) // treeView prints object hierarchy to out stream. -func treeView(out io.Writer, objs objectDirectory, obj unstructured.Unstructured) { +func treeView(out io.Writer, objs objectDirectory, obj unstructured.Unstructured, conditionTypes []string) { tbl := uitable.New() tbl.Separator = " " tbl.AddRow("NAMESPACE", "NAME", "READY", "REASON", "STATUS", "AGE") - treeViewInner("", tbl, objs, obj) + treeViewInner("", tbl, objs, obj, conditionTypes) fmt.Fprintln(color.Output, tbl) } -func treeViewInner(prefix string, tbl *uitable.Table, objs objectDirectory, obj unstructured.Unstructured) { - ready, reason, kstatus := extractStatus(obj) +func treeViewInner(prefix string, tbl *uitable.Table, objs objectDirectory, obj unstructured.Unstructured, conditionTypes []string) { + ready, reason, kstatus := extractStatus(obj, conditionTypes) var readyColor *color.Color switch ready { @@ -90,7 +90,7 @@ func treeViewInner(prefix string, tbl *uitable.Table, objs objectDirectory, obj default: p = prefix + firstElemPrefix } - treeViewInner(p, tbl, objs, child) + treeViewInner(p, tbl, objs, child, conditionTypes) } }