diff --git a/README.md b/README.md index 0ff57f2..0f9bef7 100644 --- a/README.md +++ b/README.md @@ -33,7 +33,8 @@ 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/) + - [Event References](https://kubernetes.io/docs/reference/kubernetes-api/cluster-resources/event-v1/) + - [Pod References](https://kubernetes.io/docs/reference/kubernetes-api/workload-resources/pod-v1/) - Helm (Coming Soon) ## Installation diff --git a/pkg/cmd/lineage/graph.go b/pkg/cmd/lineage/graph.go index 432cda0..bc16e16 100644 --- a/pkg/cmd/lineage/graph.go +++ b/pkg/cmd/lineage/graph.go @@ -1,10 +1,13 @@ package lineage import ( + "fmt" "sort" + corev1 "k8s.io/api/core/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" ) @@ -58,6 +61,12 @@ func (n *Node) GetNestedString(fields ...string) string { return val } +// Key returns a key composed of the node's group, kind, namespace & name which +// can be used to identify or reference a node. +func (n *Node) Key() string { + return fmt.Sprintf("%s/%s/%s/%s", n.Group, n.Kind, n.Namespace, n.Name) +} + // NodeMap contains a relationship tree stored as a map of nodes. type NodeMap map[types.UID]*Node @@ -69,14 +78,25 @@ const ( // Kubernetes Owner-Dependent relationships. RelationshipControllerRef Relationship = "ControllerReference" RelationshipOwnerRef Relationship = "OwnerReference" + + // Kubernetes Pod relationships. + RelationshipPodContainerEnv Relationship = "PodContainerEnvironment" + RelationshipPodImagePullSecret Relationship = "PodImagePullSecret" //nolint:gosec + RelationshipPodNode Relationship = "PodNode" + RelationshipPodPriorityClass Relationship = "PodPriorityClass" + RelationshipPodRuntimeClass Relationship = "PodRuntimeClass" + RelationshipPodServiceAccount Relationship = "PodServiceAccount" + RelationshipPodVolume Relationship = "PodVolume" ) // resolveDependents resolves all dependents of the provided root object and // returns a relationship tree. //nolint:funlen,gocognit func resolveDependents(objects []unstructuredv1.Unstructured, rootUID types.UID) NodeMap { - // Create global node map of all objects + // Create global node maps of all objects, one mapped by node UIDs & the other + // mapped by node keys globalMap := NodeMap{} + globalMapByKey := map[string]*Node{} for ix, o := range objects { gvk := o.GroupVersionKind() node := Node{ @@ -90,6 +110,7 @@ func resolveDependents(objects []unstructuredv1.Unstructured, rootUID types.UID) Dependents: map[types.UID]RelationshipSet{}, } globalMap[node.UID] = &node + globalMapByKey[node.Key()] = &node if node.Group == "" && node.Kind == "Node" { // Node events sent by the Kubelet uses the node's name as the @@ -105,7 +126,7 @@ func resolveDependents(objects []unstructuredv1.Unstructured, rootUID types.UID) } } - // Populate dependents based on ControllerRef & OwnerRef relationships + // Populate dependents based on Owner-Dependent relationships for _, node := range globalMap { for _, ref := range node.OwnerReferences { if n, ok := globalMap[ref.UID]; ok { @@ -117,7 +138,7 @@ func resolveDependents(objects []unstructuredv1.Unstructured, rootUID types.UID) } } - // Populate dependents based on EventRegarding & EventRelated relationships + // Populate dependents based on Event 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 @@ -137,6 +158,24 @@ func resolveDependents(objects []unstructuredv1.Unstructured, rootUID types.UID) } } + // Populate dependents based on Pod relationships + for _, node := range globalMap { + if node.Group == "" && node.Kind == "Pod" { + keyToRsetMap, err := getPodRelationships(node) + if err != nil { + klog.V(4).Infof("Failed to get object references for pod named \"%s\" in namespace \"%s\": %s", node.Name, node.Namespace, err) + continue + } + for k, rset := range keyToRsetMap { + if n, ok := globalMapByKey[k]; ok { + for r := range rset { + n.AddDependent(node.UID, r) + } + } + } + } + } + // 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 { @@ -186,3 +225,109 @@ func getEventReferenceUIDs(n *Node) (types.UID, types.UID) { } return types.UID(regUID), types.UID(relUID) } + +// getPodRelationships returns returns a map of relationships (keyed by the +// nodes representing each object reference) that this Pod has. +//nolint:funlen,gocognit +func getPodRelationships(n *Node) (map[string]RelationshipSet, error) { + var pod corev1.Pod + err := runtime.DefaultUnstructuredConverter.FromUnstructured(n.UnstructuredContent(), &pod) + if err != nil { + return nil, err + } + + var node Node + ns := pod.Namespace + result := map[string]RelationshipSet{} + addRelationship := func(r Relationship, n Node) { + k := n.Key() + if _, ok := result[k]; !ok { + result[k] = RelationshipSet{r: {}} + } + result[k][r] = struct{}{} + } + + // RelationshipPodContainerEnv + var cList []corev1.Container + cList = append(cList, pod.Spec.InitContainers...) + cList = append(cList, pod.Spec.Containers...) + for _, c := range cList { + for _, env := range c.EnvFrom { + switch { + case env.ConfigMapRef != nil: + node = Node{Kind: "ConfigMap", Name: env.ConfigMapRef.Name, Namespace: ns} + addRelationship(RelationshipPodContainerEnv, node) + case env.SecretRef != nil: + node = Node{Kind: "Secret", Name: env.SecretRef.Name, Namespace: ns} + addRelationship(RelationshipPodContainerEnv, node) + } + } + for _, env := range c.Env { + if env.ValueFrom == nil { + continue + } + switch { + case env.ValueFrom.ConfigMapKeyRef != nil: + node = Node{Kind: "ConfigMap", Name: env.ValueFrom.ConfigMapKeyRef.Name, Namespace: ns} + addRelationship(RelationshipPodContainerEnv, node) + case env.ValueFrom.SecretKeyRef != nil: + node = Node{Kind: "Secret", Name: env.ValueFrom.SecretKeyRef.Name, Namespace: ns} + addRelationship(RelationshipPodContainerEnv, node) + } + } + } + + // RelationshipPodImagePullSecret + for _, ips := range pod.Spec.ImagePullSecrets { + node = Node{Kind: "Secret", Name: ips.Name, Namespace: ns} + addRelationship(RelationshipPodImagePullSecret, node) + } + + // RelationshipPodNode + node = Node{Kind: "Node", Name: pod.Spec.NodeName} + addRelationship(RelationshipPodNode, node) + + // RelationshipPodPriorityClass + if pc := pod.Spec.PriorityClassName; len(pc) != 0 { + node = Node{Group: "scheduling.k8s.io", Kind: "PriorityClass", Name: pc} + addRelationship(RelationshipPodPriorityClass, node) + } + + // RelationshipPodRuntimeClass + if rc := pod.Spec.RuntimeClassName; rc != nil && len(*rc) != 0 { + node = Node{Group: "node.k8s.io", Kind: "RuntimeClass", Name: *rc} + addRelationship(RelationshipPodRuntimeClass, node) + } + + // RelationshipPodServiceAccount + node = Node{Kind: "ServiceAccount", Name: pod.Spec.ServiceAccountName, Namespace: ns} + addRelationship(RelationshipPodServiceAccount, node) + + // RelationshipPodVolume + for _, v := range pod.Spec.Volumes { + switch { + case v.ConfigMap != nil: + node = Node{Kind: "ConfigMap", Name: v.ConfigMap.Name, Namespace: ns} + addRelationship(RelationshipPodVolume, node) + case v.PersistentVolumeClaim != nil: + node = Node{Kind: "PersistentVolumeClaim", Name: v.PersistentVolumeClaim.ClaimName, Namespace: ns} + addRelationship(RelationshipPodVolume, node) + case v.Secret != nil: + node = Node{Kind: "Secret", Name: v.Secret.SecretName, Namespace: ns} + addRelationship(RelationshipPodVolume, node) + case v.Projected != nil: + for _, src := range v.Projected.Sources { + switch { + case src.ConfigMap != nil: + node = Node{Kind: "ConfigMap", Name: src.ConfigMap.Name, Namespace: ns} + addRelationship(RelationshipPodVolume, node) + case src.Secret != nil: + node = Node{Kind: "Secret", Name: src.Secret.Name, Namespace: ns} + addRelationship(RelationshipPodVolume, node) + } + } + } + } + + return result, nil +}