Skip to content

Commit

Permalink
feat: Support EventRegarding & EventRelated relationship
Browse files Browse the repository at this point in the history
Signed-off-by: Justin Toh <tohjustin@hotmail.com>
  • Loading branch information
tohjustin committed Sep 26, 2021
1 parent 700e645 commit 950aa42
Show file tree
Hide file tree
Showing 4 changed files with 152 additions and 13 deletions.
2 changes: 2 additions & 0 deletions .golangci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ linters:
- sqlclosecheck # exclude 'sql' preset
- exhaustivestruct
- gochecknoglobals
- goconst
- godox
- goerr113
- gomnd
- gomoddirectives
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ List of supported relationships used for discovering dependent objects:

- Kubernetes
- [Controller References](https://github.com/kubernetes/community/blob/master/contributors/design-proposals/api-machinery/controller-ref.md) & [Owner References](https://kubernetes.io/docs/concepts/overview/working-with-objects/owners-dependents/)
- [Event Regarding & Related References](https://kubernetes.io/docs/reference/kubernetes-api/cluster-resources/event-v1/)
- Helm (Coming Soon)

## Installation
Expand Down
119 changes: 107 additions & 12 deletions pkg/cmd/lineage/graph.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,11 @@ package lineage
import (
"sort"

corev1 "k8s.io/api/core/v1"
eventsv1 "k8s.io/api/events/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
unstructuredv1 "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types"
"k8s.io/klog/v2"
)
Expand All @@ -15,7 +18,7 @@ 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.
// Relationship represents a relationship type between two Kubernetes objects.
type Relationship string

// RelationshipSet contains a set of relationships.
Expand Down Expand Up @@ -47,28 +50,50 @@ type Node struct {
type NodeMap map[types.UID]*Node

const (
// Kubernetes Event relationships.
RelationshipEventRegarding Relationship = "EventRegarding"
RelationshipEventRelated Relationship = "EventRelated"

// Kubernetes Owner-Dependent relationships.
RelationshipControllerRef Relationship = "ControllerReference"
RelationshipOwnerRef Relationship = "OwnerReference"
)

// resolveDependents resolves all dependents of the provided root object and
// returns a relationship tree.
//nolint:funlen,gocognit
//nolint:funlen,gocognit,gocyclo
func resolveDependents(objects []unstructuredv1.Unstructured, rootUID types.UID) NodeMap {
// Create global node map of all objects
globalMap := NodeMap{}
for ix, o := range objects {
gvk := o.GroupVersionKind()
node := Node{
Unstructured: &objects[ix],
UID: o.GetUID(),
Name: o.GetName(),
Namespace: o.GetNamespace(),
Group: gvk.Group,
Kind: gvk.Kind,
OwnerReferences: o.GetOwnerReferences(),
Dependents: map[types.UID]RelationshipSet{},
}
globalMap[node.UID] = &node

if node.Group == "" && node.Kind == "Node" {
// Node events sent by the Kubelet uses the node's name as the
// ObjectReference UID, so we include them as keys in our global map to
// support lookup by nodename
globalMap[types.UID(node.Name)] = &node
// Node events sent by the kube-proxy uses the node's hostname as the
// ObjectReference UID, so we include them as keys in our global map to
// support lookup by hostname
if hostname, ok := o.GetLabels()["kubernetes.io/hostname"]; ok {
globalMap[types.UID(hostname)] = &node
}
}
}

// Populate dependents data for every node
// Populate dependents based on ControllerRef & OwnerRef relationships
for _, node := range globalMap {
uid, ownerRefs := node.UID, node.OwnerReferences
for _, ref := range ownerRefs {
Expand All @@ -86,6 +111,53 @@ func resolveDependents(objects []unstructuredv1.Unstructured, rootUID types.UID)
}
}

// Populate dependents based on EventRegarding & EventRelated relationships
// TODO: It's possible to have events to be in a different namespace from the
// its referenced object, so update the resource fetching logic to
// always try to fetch events at the cluster scope for event resources
var evUID, regUID, relUID types.UID
var err error
for _, node := range globalMap {
switch {
case node.Group == "" && node.Kind == "Event":
evUID = node.UID
regUID, err = getEventCoreReferenceUID(node.Unstructured)
if err != nil || len(regUID) == 0 {
klog.V(4).Infof("Failed to get object reference for event named \"%s\" in namespace \"%s\"", node.Name, node.Namespace)
continue
}
if ref, ok := globalMap[regUID]; ok {
if _, ok := ref.Dependents[evUID]; !ok {
ref.Dependents[evUID] = RelationshipSet{}
}
ref.Dependents[evUID][RelationshipEventRegarding] = struct{}{}
}
case node.Group == "events.k8s.io" && node.Kind == "Event":
evUID = node.UID
regUID, relUID, err = getEventReferenceUIDs(node.Unstructured)
if err != nil || len(regUID) == 0 {
klog.V(4).Infof("Failed to get object reference for event.events.k8s.io named \"%s\" in namespace \"%s\"", node.Name, node.Namespace)
continue
}
if ref, ok := globalMap[regUID]; ok {
if _, ok := ref.Dependents[evUID]; !ok {
ref.Dependents[evUID] = RelationshipSet{}
}
ref.Dependents[evUID][RelationshipEventRegarding] = struct{}{}
}
if len(relUID) > 0 {
if ref, ok := globalMap[relUID]; ok {
if _, ok := ref.Dependents[evUID]; !ok {
ref.Dependents[evUID] = RelationshipSet{}
}
ref.Dependents[evUID][RelationshipEventRelated] = struct{}{}
}
}
default:
continue
}
}

// Create submap of the root node & its dependents from the global map
nodeMap, uidQueue, uidSet := NodeMap{}, []types.UID{}, map[types.UID]struct{}{}
if node := globalMap[rootUID]; node != nil {
Expand Down Expand Up @@ -117,15 +189,38 @@ func resolveDependents(objects []unstructuredv1.Unstructured, rootUID types.UID)
}
}

// Populate remaining data for submap nodes
for _, o := range nodeMap {
gvk := o.GroupVersionKind()
o.Group = gvk.Group
o.Kind = gvk.Kind
o.Name = o.GetName()
o.Namespace = o.GetNamespace()
}

klog.V(4).Infof("Resolved %d dependents for root object (uid: %s)", len(nodeMap)-1, rootUID)
return nodeMap
}

// getEventCoreReferenceUID returns the UID of the object this Event is about.
// The UID will be an empty string if the reference doesn't exist.
func getEventCoreReferenceUID(u *unstructuredv1.Unstructured) (types.UID, error) {
var regUID types.UID
var ev corev1.Event
err := runtime.DefaultUnstructuredConverter.FromUnstructured(u.UnstructuredContent(), &ev)
if err != nil {
return "", err
}
regUID = ev.InvolvedObject.UID

return regUID, nil
}

// getEventReferenceUIDs returns the UID of the object this Event.events.k8s.io
// is about & the UID of a secondary object if it exist. The UID will be an
// empty string if the reference doesn't exist.
func getEventReferenceUIDs(u *unstructuredv1.Unstructured) (types.UID, types.UID, error) {
var regUID, relUID types.UID
var ev eventsv1.Event
err := runtime.DefaultUnstructuredConverter.FromUnstructured(u.UnstructuredContent(), &ev)
if err != nil {
return "", "", err
}
regUID = ev.Regarding.UID
if ev.Related != nil {
relUID = ev.Related.UID
}

return regUID, relUID, nil
}
43 changes: 42 additions & 1 deletion pkg/cmd/lineage/printer_table.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (

appsv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
eventsv1 "k8s.io/api/events/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
unstructuredv1 "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
Expand Down Expand Up @@ -141,6 +142,42 @@ func getDeploymentReadyStatus(u *unstructuredv1.Unstructured) (string, string, e
return ready, "", nil
}

// getEventCoreReadyStatus returns the ready & status value of a Event.
//nolint:unparam
func getEventCoreReadyStatus(u *unstructuredv1.Unstructured) (string, string, error) {
var status string
var ev corev1.Event
err := runtime.DefaultUnstructuredConverter.FromUnstructured(u.UnstructuredContent(), &ev)
if err != nil {
return "", "", err
}
if ev.Count > 1 {
status = fmt.Sprintf("%s: %s (x%d)", ev.Reason, ev.Message, ev.Count)
} else {
status = fmt.Sprintf("%s: %s", ev.Reason, ev.Message)
}

return "", status, nil
}

// getEventReadyStatus returns the ready & status value of a Event.events.k8s.io.
//nolint:unparam
func getEventReadyStatus(u *unstructuredv1.Unstructured) (string, string, error) {
var status string
var ev eventsv1.Event
err := runtime.DefaultUnstructuredConverter.FromUnstructured(u.UnstructuredContent(), &ev)
if err != nil {
return "", "", err
}
if ev.DeprecatedCount > 1 {
status = fmt.Sprintf("%s: %s (x%d)", ev.Reason, ev.Note, ev.DeprecatedCount)
} else {
status = fmt.Sprintf("%s: %s", ev.Reason, ev.Note)
}

return "", status, nil
}

// getPodReadyStatus returns the ready & status value of a Pod which is based
// off the table cell values computed by printPod from
// https://github.com/kubernetes/kubernetes/blob/v1.22.1/pkg/printers/internalversion/printers.go.
Expand Down Expand Up @@ -275,7 +312,7 @@ func getStatefulSetReadyStatus(u *unstructuredv1.Unstructured) (string, string,
}

// nodeToTableRow converts the provided node into a table row.
//nolint:goconst
//nolint:gocognit,goconst
func nodeToTableRow(node *Node, rset RelationshipSet, namePrefix string, showGroupFn func(kind string) bool) metav1.TableRow {
var name, ready, status, age string
var relationships interface{}
Expand All @@ -286,6 +323,8 @@ func nodeToTableRow(node *Node, rset RelationshipSet, namePrefix string, showGro
name = fmt.Sprintf("%s%s/%s", namePrefix, node.Kind, node.Name)
}
switch {
case node.Group == "" && node.Kind == "Event":
ready, status, _ = getEventCoreReadyStatus(node.Unstructured)
case node.Group == "" && node.Kind == "Pod":
ready, status, _ = getPodReadyStatus(node.Unstructured)
case node.Group == "" && node.Kind == "ReplicationController":
Expand All @@ -298,6 +337,8 @@ func nodeToTableRow(node *Node, rset RelationshipSet, namePrefix string, showGro
ready, status, _ = getReplicaSetReadyStatus(node.Unstructured)
case node.Group == "apps" && node.Kind == "StatefulSet":
ready, status, _ = getStatefulSetReadyStatus(node.Unstructured)
case node.Group == "events.k8s.io" && node.Kind == "Event":
ready, status, _ = getEventReadyStatus(node.Unstructured)
default:
ready, status, _ = getObjectReadyStatus(node.Unstructured)
}
Expand Down

0 comments on commit 950aa42

Please sign in to comment.