|
| 1 | +package owners |
| 2 | + |
| 3 | +import ( |
| 4 | + "context" |
| 5 | + "fmt" |
| 6 | + |
| 7 | + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" |
| 8 | + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" |
| 9 | + "k8s.io/apimachinery/pkg/runtime/schema" |
| 10 | + "k8s.io/client-go/discovery" |
| 11 | + "k8s.io/client-go/dynamic" |
| 12 | +) |
| 13 | + |
| 14 | +// OwnerFetcher fetches the owner references of Kubernetes objects by traversing |
| 15 | +// the owner reference chain up to the top-level owner. |
| 16 | +type OwnerFetcher struct { |
| 17 | + resourceLists []*metav1.APIResourceList // All available API in the cluster |
| 18 | + discoveryClient discovery.ServerResourcesInterface |
| 19 | + dynamicClient dynamic.Interface |
| 20 | +} |
| 21 | + |
| 22 | +// NewOwnerFetcher creates a new OwnerFetcher with the provided discovery and dynamic clients. |
| 23 | +// The discovery client is used to fetch available API resources, and the dynamic client is used |
| 24 | +// to retrieve owner objects from the cluster. |
| 25 | +func NewOwnerFetcher(discoveryClient discovery.ServerResourcesInterface, dynamicClient dynamic.Interface) *OwnerFetcher { |
| 26 | + return &OwnerFetcher{ |
| 27 | + discoveryClient: discoveryClient, |
| 28 | + dynamicClient: dynamicClient, |
| 29 | + } |
| 30 | +} |
| 31 | + |
| 32 | +// ObjectWithGVR contains an unstructured Kubernetes object along with its |
| 33 | +// GroupVersionResource (GVR) for identifying the resource type. |
| 34 | +type ObjectWithGVR struct { |
| 35 | + Object *unstructured.Unstructured |
| 36 | + GVR *schema.GroupVersionResource |
| 37 | +} |
| 38 | + |
| 39 | +// GetOwners recursively retrieves all owner references for the given object, starting from |
| 40 | +// the immediate owner up to the top-level owner. It returns a slice of ObjectWithGVR in order |
| 41 | +// from top-level owner to immediate owner. Returns nil if the object has no owner. |
| 42 | +func (o *OwnerFetcher) GetOwners(ctx context.Context, obj metav1.Object) ([]*ObjectWithGVR, error) { |
| 43 | + if o.resourceLists == nil { |
| 44 | + // Get all API resources from the cluster using the discovery client. We need it for constructing GVRs for unstructured objects. |
| 45 | + // Do it here once, so we do not have to list it multiple times before listing/getting every unstructured resource. |
| 46 | + resourceLists, err := o.discoveryClient.ServerPreferredResources() |
| 47 | + if err != nil { |
| 48 | + return nil, err |
| 49 | + } |
| 50 | + o.resourceLists = resourceLists |
| 51 | + } |
| 52 | + |
| 53 | + // get the controller owner (it's possible to have only one controller owner) |
| 54 | + owners := obj.GetOwnerReferences() |
| 55 | + var ownerReference metav1.OwnerReference |
| 56 | + var nonControllerOwner metav1.OwnerReference |
| 57 | + for _, ownerRef := range owners { |
| 58 | + // try to get the controller owner as the preferred one |
| 59 | + if ownerRef.Controller != nil && *ownerRef.Controller { |
| 60 | + ownerReference = ownerRef |
| 61 | + break |
| 62 | + } else if nonControllerOwner.Name == "" { |
| 63 | + // take only the first non-controller owner |
| 64 | + nonControllerOwner = ownerRef |
| 65 | + } |
| 66 | + } |
| 67 | + // if no controller owner was found, then use the first non-controller owner (if present) |
| 68 | + if ownerReference.Name == "" { |
| 69 | + ownerReference = nonControllerOwner |
| 70 | + } |
| 71 | + if ownerReference.Name == "" { |
| 72 | + return nil, nil // No owner |
| 73 | + } |
| 74 | + // Get the GVR for the owner |
| 75 | + gvr, err := gvrForKind(ownerReference.Kind, ownerReference.APIVersion, o.resourceLists) |
| 76 | + if err != nil { |
| 77 | + return nil, err |
| 78 | + } |
| 79 | + // Get the owner object |
| 80 | + ownerObject, err := o.dynamicClient.Resource(*gvr).Namespace(obj.GetNamespace()).Get(ctx, ownerReference.Name, metav1.GetOptions{}) |
| 81 | + if err != nil { |
| 82 | + return nil, err |
| 83 | + } |
| 84 | + owner := &ObjectWithGVR{ |
| 85 | + Object: ownerObject, |
| 86 | + GVR: gvr, |
| 87 | + } |
| 88 | + // Recursively try to find the top owner |
| 89 | + ownerOwners, err := o.GetOwners(ctx, ownerObject) |
| 90 | + if err != nil || ownerOwners == nil { |
| 91 | + return append(ownerOwners, owner), err |
| 92 | + } |
| 93 | + return append(ownerOwners, owner), nil |
| 94 | +} |
| 95 | + |
| 96 | +// gvrForKind returns GVR for the kind, if it's found in the available API list in the cluster |
| 97 | +// returns an error if not found or failed to parse the API version |
| 98 | +func gvrForKind(kind, apiVersion string, resourceLists []*metav1.APIResourceList) (*schema.GroupVersionResource, error) { |
| 99 | + gvr, err := findGVRForKind(kind, apiVersion, resourceLists) |
| 100 | + if gvr == nil && err == nil { |
| 101 | + return nil, fmt.Errorf("no resource found for kind %s in %s", kind, apiVersion) |
| 102 | + } |
| 103 | + return gvr, err |
| 104 | +} |
| 105 | + |
| 106 | +// findGVRForKind returns GVR for the kind, if it's found in the available API list in the cluster |
| 107 | +// if not found then returns nil, nil |
| 108 | +// returns nil, error if failed to parse the API version |
| 109 | +func findGVRForKind(kind, apiVersion string, resourceLists []*metav1.APIResourceList) (*schema.GroupVersionResource, error) { |
| 110 | + // Parse the group and version from the APIVersion (e.g., "apps/v1" -> group: "apps", version: "v1") |
| 111 | + gv, err := schema.ParseGroupVersion(apiVersion) |
| 112 | + if err != nil { |
| 113 | + return nil, fmt.Errorf("failed to parse APIVersion %s: %w", apiVersion, err) |
| 114 | + } |
| 115 | + |
| 116 | + // Look for a matching resource |
| 117 | + for _, resourceList := range resourceLists { |
| 118 | + if resourceList.GroupVersion == apiVersion { |
| 119 | + for _, apiResource := range resourceList.APIResources { |
| 120 | + if apiResource.Kind == kind { |
| 121 | + // Construct the GVR |
| 122 | + return &schema.GroupVersionResource{ |
| 123 | + Group: gv.Group, |
| 124 | + Version: gv.Version, |
| 125 | + Resource: apiResource.Name, |
| 126 | + }, nil |
| 127 | + } |
| 128 | + } |
| 129 | + } |
| 130 | + } |
| 131 | + |
| 132 | + return nil, nil |
| 133 | +} |
0 commit comments