Skip to content

Commit

Permalink
feat: Add optional relationships column in table output
Browse files Browse the repository at this point in the history
Add optional relationships column to display the relationships the
object has with its parent.

Users can view this optional column by simply including the `-o wide`
flag in the command.

Signed-off-by: Justin Toh <tohjustin@hotmail.com>
  • Loading branch information
tohjustin committed Sep 21, 2021
1 parent 9faaea9 commit b1f3581
Show file tree
Hide file tree
Showing 2 changed files with 69 additions and 22 deletions.
53 changes: 44 additions & 9 deletions pkg/cmd/lineage/graph.go
Original file line number Diff line number Diff line change
@@ -1,17 +1,37 @@
package lineage

import (
"sort"

metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
unstructuredv1 "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/types"
"k8s.io/klog/v2"
)

// NodeMap represents an owner-dependent relationship tree stored as a map of
// nodes.
type NodeMap map[types.UID]*Node
type sortableStringSlice []string

func (s sortableStringSlice) Len() int { return len(s) }
func (s sortableStringSlice) Less(i, j int) bool { return s[i] < s[j] }
func (s sortableStringSlice) Swap(i, j int) { s[i], s[j] = s[j], s[i] }

// Relationship represents a relationship type.
type Relationship string

// RelationshipSet contains a set of relationships.
type RelationshipSet map[Relationship]struct{}

// List returns the contents as a sorted string slice.
func (s RelationshipSet) List() []string {
res := make(sortableStringSlice, 0, len(s))
for key := range s {
res = append(res, string(key))
}
sort.Sort(res)
return []string(res)
}

// Node represents a Kubernetes object in an owner-dependent relationship tree.
// Node represents a Kubernetes object in an relationship tree.
type Node struct {
*unstructuredv1.Unstructured
UID types.UID
Expand All @@ -20,11 +40,19 @@ type Node struct {
Group string
Kind string
OwnerReferences []metav1.OwnerReference
Dependents []types.UID
Dependents map[types.UID]RelationshipSet
}

// NodeMap contains a relationship tree stored as a map of nodes.
type NodeMap map[types.UID]*Node

const (
RelationshipOwnerRef Relationship = "OwnerReference"
)

// resolveDependents resolves all dependents of the provided root object and
// returns an owner-dependent relationship tree.
// returns a relationship tree.
//nolint:funlen
func resolveDependents(objects []unstructuredv1.Unstructured, rootUID types.UID) NodeMap {
// Create global node map of all objects
globalMap := NodeMap{}
Expand All @@ -33,6 +61,7 @@ func resolveDependents(objects []unstructuredv1.Unstructured, rootUID types.UID)
Unstructured: &objects[ix],
UID: o.GetUID(),
OwnerReferences: o.GetOwnerReferences(),
Dependents: map[types.UID]RelationshipSet{},
}
globalMap[node.UID] = &node
}
Expand All @@ -42,7 +71,10 @@ func resolveDependents(objects []unstructuredv1.Unstructured, rootUID types.UID)
uid, ownerRefs := node.UID, node.OwnerReferences
for _, ref := range ownerRefs {
if owner, ok := globalMap[ref.UID]; ok {
owner.Dependents = append(owner.Dependents, uid)
if _, ok := owner.Dependents[uid]; !ok {
owner.Dependents[uid] = RelationshipSet{}
}
owner.Dependents[uid][RelationshipOwnerRef] = struct{}{}
}
}
}
Expand All @@ -68,10 +100,13 @@ func resolveDependents(objects []unstructuredv1.Unstructured, rootUID types.UID)
}

if node := nodeMap[uid]; node != nil {
for _, dUID := range node.Dependents {
dependents, ix := make([]types.UID, len(node.Dependents)), 0
for dUID := range node.Dependents {
nodeMap[dUID] = globalMap[dUID]
dependents[ix] = dUID
ix++
}
uidQueue = append(uidQueue[1:], node.Dependents...)
uidQueue = append(uidQueue[1:], dependents...)
}
}

Expand Down
38 changes: 25 additions & 13 deletions pkg/cmd/lineage/printer_table.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ var (
{Name: "Ready", Type: "string", Description: "The readiness state of this object."},
{Name: "Status", Type: "string", Description: "The status of this object."},
{Name: "Age", Type: "string", Description: metav1.ObjectMeta{}.SwaggerDoc()["creationTimestamp"]},
{Name: "Relationships", Type: "array", Description: "The relationships this object has with its parent.", Priority: -1},
}
// objectReadyReasonJSONPath is the JSON path to get a Kubernetes object's
// "Ready" condition reason.
Expand All @@ -38,8 +39,7 @@ var (
objectReadyStatusJSONPath = newJSONPath("status", "{.status.conditions[?(@.type==\"Ready\")].status}")
)

// NodeList represents an owner-dependent relationship tree stored as flat list
// of nodes.
// NodeList contains a list of nodes.
type NodeList []*Node

func (n NodeList) Len() int {
Expand Down Expand Up @@ -275,8 +275,9 @@ func getStatefulSetReadyStatus(u *unstructuredv1.Unstructured) (string, string,

// nodeToTableRow converts the provided node into a table row.
//nolint:goconst
func nodeToTableRow(node *Node, namePrefix string, showGroupFn func(kind string) bool) metav1.TableRow {
func nodeToTableRow(node *Node, rset RelationshipSet, namePrefix string, showGroupFn func(kind string) bool) metav1.TableRow {
var name, ready, status, age string
var relationships interface{}

if showGroupFn(node.Kind) && len(node.Group) > 0 {
name = fmt.Sprintf("%s%s.%s/%s", namePrefix, node.Kind, node.Group, node.Name)
Expand All @@ -303,6 +304,11 @@ func nodeToTableRow(node *Node, namePrefix string, showGroupFn func(kind string)
ready = cellNotApplicable
}
age = translateTimestampSince(node.GetCreationTimestamp())
if r := rset.List(); len(r) > 0 {
relationships = r
} else {
relationships = cellNotApplicable
}

return metav1.TableRow{
Object: runtime.RawExtension{Object: node.DeepCopyObject()},
Expand All @@ -311,6 +317,7 @@ func nodeToTableRow(node *Node, namePrefix string, showGroupFn func(kind string)
ready,
status,
age,
relationships,
},
}
}
Expand All @@ -335,23 +342,24 @@ func printNode(nodeMap NodeMap, root *Node, withGroup bool) ([]metav1.TableRow,
}
// Sorts the list of UIDs based on the underlying object in following order:
// Namespace, Kind, Group, Name
sortUIDsFn := func(uids []types.UID) []types.UID {
nodes := make(NodeList, len(uids))
for ix, uid := range uids {
sortDependentsFn := func(d map[types.UID]RelationshipSet) []types.UID {
nodes, ix := make(NodeList, len(d)), 0
for uid := range d {
nodes[ix] = nodeMap[uid]
ix++
}
sort.Sort(nodes)
sortedUIDs := make([]types.UID, len(uids))
sortedUIDs := make([]types.UID, len(d))
for ix, node := range nodes {
sortedUIDs[ix] = node.UID
}
return sortedUIDs
}

var rows []metav1.TableRow
row := nodeToTableRow(root, "", showGroupFn)
row := nodeToTableRow(root, nil, "", showGroupFn)
uidSet := map[types.UID]struct{}{}
dependentRows, err := printNodeDependents(nodeMap, uidSet, root, "", sortUIDsFn, showGroupFn)
dependentRows, err := printNodeDependents(nodeMap, uidSet, root, "", sortDependentsFn, showGroupFn)
if err != nil {
return nil, err
}
Expand All @@ -367,7 +375,7 @@ func printNodeDependents(
uidSet map[types.UID]struct{},
node *Node,
prefix string,
sortUIDsFn func(uids []types.UID) []types.UID,
sortDependentsFn func(d map[types.UID]RelationshipSet) []types.UID,
showGroupFn func(kind string) bool) ([]metav1.TableRow, error) {
rows := make([]metav1.TableRow, 0, len(nodeMap))

Expand All @@ -377,7 +385,7 @@ func printNodeDependents(
}
uidSet[node.UID] = struct{}{}

dependents := sortUIDsFn(node.Dependents)
dependents := sortDependentsFn(node.Dependents)
lastIx := len(dependents) - 1
for ix, childUID := range dependents {
var childPrefix, dependentPrefix string
Expand All @@ -391,8 +399,12 @@ func printNodeDependents(
if !ok {
return nil, fmt.Errorf("dependent object (uid: %s) not found in list of fetched objects", childUID)
}
row := nodeToTableRow(child, childPrefix, showGroupFn)
dependentRows, err := printNodeDependents(nodeMap, uidSet, child, dependentPrefix, sortUIDsFn, showGroupFn)
rset, ok := node.Dependents[childUID]
if !ok {
return nil, fmt.Errorf("dependent object (uid: %s) not found", childUID)
}
row := nodeToTableRow(child, rset, childPrefix, showGroupFn)
dependentRows, err := printNodeDependents(nodeMap, uidSet, child, dependentPrefix, sortDependentsFn, showGroupFn)
if err != nil {
return nil, err
}
Expand Down

0 comments on commit b1f3581

Please sign in to comment.