Skip to content

Commit 0484630

Browse files
committed
working version with colors
Signed-off-by: Ahmet Alp Balkan <[email protected]>
1 parent 1b7829c commit 0484630

File tree

7 files changed

+152
-20
lines changed

7 files changed

+152
-20
lines changed

cmd/kubectl-tree/query.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@ import (
88
"sync"
99
)
1010

11-
func QueryResources(client dynamic.Interface, apis []apiResource) ([]unstructured.Unstructured, error) {
11+
// getAllResources finds all API objects in specified API resources in all namespaces (or non-namespaced).
12+
func getAllResources(client dynamic.Interface, apis []apiResource) ([]unstructured.Unstructured, error) {
1213
var mu sync.Mutex
1314
var wg sync.WaitGroup
1415
var out []unstructured.Unstructured
@@ -46,7 +47,6 @@ func queryAPI(client dynamic.Interface, api apiResource) ([]unstructured.Unstruc
4647
}
4748
out = append(out, resp.Items...)
4849

49-
fmt.Printf("found %d objects in %s (next=%s)\n", len(resp.Items), api.GroupVersionResource(), resp.GetContinue())
5050
next = resp.GetContinue()
5151
if next == "" {
5252
break

cmd/kubectl-tree/relationship.go

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,16 @@ package main
33
import (
44
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
55
"k8s.io/apimachinery/pkg/types"
6+
"sort"
67
)
78

9+
// objectDirectory stores objects and owner relationships between them.
810
type objectDirectory struct {
911
items map[types.UID]unstructured.Unstructured
1012
ownership map[types.UID]map[types.UID]bool
1113
}
1214

15+
// newObjectDirectory builds object lookup and hierarchy.
1316
func newObjectDirectory(objs []unstructured.Unstructured) objectDirectory {
1417
v := objectDirectory{
1518
items: make(map[types.UID]unstructured.Unstructured),
@@ -27,12 +30,33 @@ func newObjectDirectory(objs []unstructured.Unstructured) objectDirectory {
2730
return v
2831
}
2932

33+
// getObject finds object by ID, since objectDirectory is built with specified objects, id should exist in there.
3034
func (od objectDirectory) getObject(id types.UID) unstructured.Unstructured { return od.items[id] }
3135

32-
func (od objectDirectory) ownedBy(ownerID types.UID) []types.UID {
33-
var out []types.UID
34-
for k := range od.ownership[ownerID] {
35-
out = append(out, k)
36+
// ownedBy returns objects directly owned by specified id, sorted by Kind, then by Name, then by Namespace.
37+
func (od objectDirectory) ownedBy(id types.UID) []unstructured.Unstructured {
38+
var out sortedObjects
39+
for k := range od.ownership[id] {
40+
out = append(out, od.getObject(k))
3641
}
42+
sort.Sort(out)
3743
return out
3844
}
45+
46+
// sortedObjects sorts objects by Kind, then by Name, then by Namespace.
47+
type sortedObjects []unstructured.Unstructured
48+
49+
func (s sortedObjects) Len() int { return len(s) }
50+
51+
func (s sortedObjects) Less(i, j int) bool {
52+
a, b := s[i], s[j]
53+
if a.GetKind() != b.GetKind() {
54+
return a.GetKind() < b.GetKind()
55+
}
56+
if a.GetName() != b.GetName() {
57+
return a.GetName() < b.GetName()
58+
}
59+
return a.GetNamespace() < b.GetNamespace()
60+
}
61+
62+
func (s sortedObjects) Swap(i, j int) { s[i], s[j] = s[j], s[i] }

cmd/kubectl-tree/rootcmd.go

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,8 @@ var cf *genericclioptions.ConfigFlags
3030
var rootCmd = &cobra.Command{
3131
Use: "kubectl tree",
3232
Short: "Show sub-resources of the Kubernetes object",
33-
Example: " kubectl tree deployment my-app", // TODO add more examples about disambiguation etc
33+
Example: " kubectl tree deployment my-app\n" +
34+
" kubectl tree kservice.v1.serving.knative.dev my-app", // TODO add more examples about disambiguation etc
3435
Args: cobra.MinimumNArgs(2),
3536
RunE: run,
3637
}
@@ -68,21 +69,15 @@ func run(cmd *cobra.Command, args []string) error {
6869
strings.Join(names, ", "))
6970
}
7071

71-
fmt.Printf("kind=%#v name=%s\n", apiRes[0], name)
72-
if *cf.Namespace == "" {
73-
*cf.Namespace = "default" // TODO(ahmetb) figure out how to have this auto-set by kubeconfig w/ cli override
74-
}
75-
7672
obj, err := dyn.Resource(apiRes[0].GroupVersionResource()).Namespace(*cf.Namespace).Get(name, metav1.GetOptions{})
7773
if err != nil {
7874
return fmt.Errorf("failed to get: %w", err)
7975
}
8076

81-
apiObjects, err := QueryResources(dyn, apis.resources())
77+
apiObjects, err := getAllResources(dyn, apis.resources())
8278
if err != nil {
8379
return fmt.Errorf("error while querying api objects: %w", err)
8480
}
85-
fmt.Printf("%d api objects found\n", len(apiObjects))
8681

8782
objs := newObjectDirectory(apiObjects)
8883
if len(objs.ownership[obj.GetUID()]) == 0 {

cmd/kubectl-tree/status.go

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
package main
2+
3+
import (
4+
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
5+
)
6+
7+
type ReadyStatus string // True False Unknown or ""
8+
type Reason string
9+
10+
func extractStatus(obj unstructured.Unstructured) (ReadyStatus, Reason) {
11+
statusF, ok := obj.Object["status"]
12+
if !ok {
13+
return "", ""
14+
}
15+
statusV, ok := statusF.(map[string]interface{})
16+
if !ok {
17+
return "", ""
18+
}
19+
conditionsF, ok := statusV["conditions"]
20+
if !ok {
21+
return "", ""
22+
}
23+
conditionsV, ok := conditionsF.([]interface{})
24+
if !ok {
25+
return "", ""
26+
}
27+
28+
for _, cond := range conditionsV {
29+
condM, ok := cond.(map[string]interface{})
30+
if !ok {
31+
return "", ""
32+
}
33+
condType, ok := condM["type"].(string)
34+
if !ok {
35+
return "", ""
36+
}
37+
if condType == "Ready" {
38+
condStatus, _ := condM["status"].(string)
39+
condReason, _ := condM["reason"].(string)
40+
return ReadyStatus(condStatus), Reason(condReason)
41+
}
42+
}
43+
return "", ""
44+
}

cmd/kubectl-tree/tree.go

Lines changed: 68 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,80 @@ package main
22

33
import (
44
"fmt"
5+
"github.com/fatih/color"
6+
"github.com/gosuri/uitable"
57
"io"
68
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
9+
"strings"
710
)
811

12+
const (
13+
firstElemPrefix = `├─`
14+
lastElemPrefix = `└─`
15+
indent = " "
16+
pipe = `│ `
17+
)
18+
19+
var (
20+
gray = color.New(color.FgHiBlack)
21+
red = color.New(color.FgRed)
22+
green = color.New(color.FgGreen)
23+
)
24+
25+
// treeView prints object hierarchy to out stream.
926
func treeView(out io.Writer, objs objectDirectory, obj unstructured.Unstructured) {
10-
treeViewInner("", out, objs, obj)
27+
tbl := uitable.New()
28+
tbl.AddRow("NAMESPACE", "NAME", "READY", "REASON")
29+
treeViewInner("", tbl, objs, obj)
30+
fmt.Fprintln(out, tbl)
1131
}
1232

13-
func treeViewInner(prefix string, out io.Writer, objs objectDirectory, obj unstructured.Unstructured) {
14-
fmt.Fprintf(out, prefix+"%s/%s (#%s#)\n", obj.GetKind(), obj.GetName(),obj.GetUID())
15-
for _, child := range objs.ownedBy(obj.GetUID()) {
16-
treeViewInner(prefix+" ", out, objs, objs.getObject(child))
33+
func treeViewInner(prefix string, tbl *uitable.Table, objs objectDirectory, obj unstructured.Unstructured) {
34+
ready, reason := extractStatus(obj)
35+
36+
var readyColor *color.Color
37+
switch ready {
38+
case "True":
39+
readyColor = green
40+
case "False", "Unknown":
41+
readyColor = red
42+
default:
43+
readyColor = gray
44+
}
45+
if ready == "" {
46+
ready = "-"
47+
}
48+
49+
tbl.AddRow(obj.GetNamespace(), fmt.Sprintf("%s%s/%s",
50+
gray.Sprint(printPrefix(prefix)),
51+
gray.Sprint(obj.GetKind()),
52+
color.New(color.Bold).Sprint(obj.GetName())),
53+
readyColor.Sprint(ready),
54+
readyColor.Sprint(reason))
55+
chs := objs.ownedBy(obj.GetUID())
56+
for i, child := range chs {
57+
var p string
58+
switch i {
59+
case len(chs) - 1:
60+
p = prefix + lastElemPrefix
61+
default:
62+
p = prefix + firstElemPrefix
63+
}
64+
treeViewInner(p, tbl, objs, child)
65+
}
66+
}
67+
68+
func printPrefix(p string) string {
69+
if strings.HasSuffix(p, firstElemPrefix) {
70+
p = strings.Replace(p, firstElemPrefix, pipe, strings.Count(p, firstElemPrefix)-1)
71+
} else {
72+
p = strings.ReplaceAll(p, firstElemPrefix, pipe)
73+
}
74+
75+
if strings.HasSuffix(p, lastElemPrefix) {
76+
p = strings.Replace(p, lastElemPrefix, strings.Repeat(" ", len([]rune(lastElemPrefix))), strings.Count(p, lastElemPrefix)-1)
77+
} else {
78+
p = strings.ReplaceAll(p, lastElemPrefix, strings.Repeat(" ", len([]rune(lastElemPrefix))))
1779
}
80+
return p
1881
}

go.mod

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,15 @@ require (
77
github.com/cppforlife/go-cli-ui v0.0.0-20181113222104-5a69326440e8 // indirect
88
github.com/emicklei/go-restful v2.9.6+incompatible // indirect
99
github.com/evanphx/json-patch v4.5.0+incompatible // indirect
10-
github.com/fatih/color v1.7.0 // indirect
10+
github.com/fatih/color v1.7.0
1111
github.com/googleapis/gnostic v0.3.0 // indirect
12+
github.com/gosuri/uitable v0.0.4
1213
github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 // indirect
1314
github.com/imdario/mergo v0.3.7 // indirect
1415
github.com/k14s/kapp v0.16.0 // indirect
1516
github.com/mattn/go-colorable v0.1.4 // indirect
1617
github.com/mattn/go-isatty v0.0.11 // indirect
18+
github.com/mattn/go-runewidth v0.0.7 // indirect
1719
github.com/spf13/cobra v0.0.5
1820
github.com/vito/go-interact v1.0.0 // indirect
1921
golang.org/x/crypto v0.0.0-20191206172530-e9b2fee46413 // indirect

go.sum

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,8 @@ github.com/googleapis/gnostic v0.0.0-20170729233727-0c5108395e2d/go.mod h1:sJBsC
100100
github.com/googleapis/gnostic v0.3.0 h1:CcQijm0XKekKjP/YCz28LXVSpgguuB+nCxaSjCe09y0=
101101
github.com/googleapis/gnostic v0.3.0/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY=
102102
github.com/gophercloud/gophercloud v0.1.0/go.mod h1:vxM41WHh5uqHVBMZHzuwNOHh8XEoIEcSTewFxm1c5g8=
103+
github.com/gosuri/uitable v0.0.4 h1:IG2xLKRvErL3uhY6e1BylFzG+aJiwQviDDTfOKeKTpY=
104+
github.com/gosuri/uitable v0.0.4/go.mod h1:tKR86bXuXPZazfOTG1FIzvjIdXzd0mo4Vtn16vt0PJo=
103105
github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA=
104106
github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 h1:+ngKgrYPPJrOjhax5N+uePQ0Fh1Z7PheYoUI/0nzkPA=
105107
github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA=
@@ -142,6 +144,8 @@ github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVc
142144
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
143145
github.com/mattn/go-isatty v0.0.11 h1:FxPOTFNqGkuDUGi3H/qkUbQO4ZiBa2brKq5r0l8TGeM=
144146
github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE=
147+
github.com/mattn/go-runewidth v0.0.7 h1:Ei8KR0497xHyKJPAv59M1dkC+rOZCMBJ+t3fZ+twI54=
148+
github.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
145149
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
146150
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
147151
github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE=

0 commit comments

Comments
 (0)