Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@ require (
)

require (
github.com/codeready-toolchain/api v0.0.0-20250916082953-4ecb3a4645e6
github.com/codeready-toolchain/api v0.0.0-20251008084914-06282b83d4cd
github.com/codeready-toolchain/member-operator v0.0.0-20251021123459-0568ed2989cf
github.com/ghodss/yaml v1.0.0
github.com/google/go-cmp v0.7.0
github.com/google/go-github/v52 v52.0.0
Expand Down Expand Up @@ -116,6 +117,7 @@ require (
k8s.io/cli-runtime v0.32.2 // indirect
k8s.io/klog/v2 v2.130.1 // indirect
k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f // indirect
k8s.io/metrics v0.32.2 // indirect
sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 // indirect
sigs.k8s.io/kustomize/api v0.18.0 // indirect
sigs.k8s.io/kustomize/kyaml v0.18.1 // indirect
Expand Down
8 changes: 6 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,10 @@ github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XL
github.com/cloudflare/circl v1.1.0/go.mod h1:prBCrKB9DV4poKZY1l9zBXg2QJY7mvgRvtMxxK7fi4I=
github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0=
github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs=
github.com/codeready-toolchain/api v0.0.0-20250916082953-4ecb3a4645e6 h1:3Z1nQ7CjBCphWyGkEUCSMrd4VfuxzOVYyEi9/7FjLPs=
github.com/codeready-toolchain/api v0.0.0-20250916082953-4ecb3a4645e6/go.mod h1:TiQ/yNv3cGL4nxo3fgRtcHyYYuRf+nAgs6B1IAqvxOU=
github.com/codeready-toolchain/api v0.0.0-20251008084914-06282b83d4cd h1:A6WOUwlUdgOf4PDzzgKvycRj4Pk+1F6npWV4YoPVFcM=
github.com/codeready-toolchain/api v0.0.0-20251008084914-06282b83d4cd/go.mod h1:TiQ/yNv3cGL4nxo3fgRtcHyYYuRf+nAgs6B1IAqvxOU=
github.com/codeready-toolchain/member-operator v0.0.0-20251021123459-0568ed2989cf h1:+Bkr1cce/52pPiNrMr+xGHTmjQsr7CAiO/mZB7ciI5c=
github.com/codeready-toolchain/member-operator v0.0.0-20251021123459-0568ed2989cf/go.mod h1:WLSAIBXKPZG5pRFqzqlh99WlSNgMnXIAlcuUDGCEX0M=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
Expand Down Expand Up @@ -353,6 +355,8 @@ k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f h1:GA7//TjRY9yWGy1poLzYYJ
k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f/go.mod h1:R/HEjbvWI0qdfb8viZUeVZm0X6IZnxAydC7YU42CMw4=
k8s.io/kubectl v0.32.2 h1:TAkag6+XfSBgkqK9I7ZvwtF0WVtUAvK8ZqTt+5zi1Us=
k8s.io/kubectl v0.32.2/go.mod h1:+h/NQFSPxiDZYX/WZaWw9fwYezGLISP0ud8nQKg+3g8=
k8s.io/metrics v0.32.2 h1:7t/rZzTHFrGa9f94XcgLlm3ToAuJtdlHANcJEHlYl9g=
k8s.io/metrics v0.32.2/go.mod h1:VL3nJpzcgB6L5nSljkkzoE0nilZhVgcjCfNRgoylaIQ=
k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 h1:M3sRQVHv7vB20Xc2ybTt7ODCeFj6JSWYFzOFnYeS6Ro=
k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
sigs.k8s.io/controller-runtime v0.20.4 h1:X3c+Odnxz+iPTRobG4tp092+CvBU9UK0t/bRf+n0DGU=
Expand Down
133 changes: 133 additions & 0 deletions pkg/owners/fetcher.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
package owners

import (
"context"
"fmt"

metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/client-go/discovery"
"k8s.io/client-go/dynamic"
)

// OwnerFetcher fetches the owner references of Kubernetes objects by traversing
// the owner reference chain up to the top-level owner.
type OwnerFetcher struct {
resourceLists []*metav1.APIResourceList // All available API in the cluster
discoveryClient discovery.ServerResourcesInterface
dynamicClient dynamic.Interface
}

// NewOwnerFetcher creates a new OwnerFetcher with the provided discovery and dynamic clients.
// The discovery client is used to fetch available API resources, and the dynamic client is used
// to retrieve owner objects from the cluster.
func NewOwnerFetcher(discoveryClient discovery.ServerResourcesInterface, dynamicClient dynamic.Interface) *OwnerFetcher {
return &OwnerFetcher{
discoveryClient: discoveryClient,
dynamicClient: dynamicClient,
}
}

// ObjectWithGVR contains an unstructured Kubernetes object along with its
// GroupVersionResource (GVR) for identifying the resource type.
type ObjectWithGVR struct {
Object *unstructured.Unstructured
GVR *schema.GroupVersionResource
}

// GetOwners recursively retrieves all owner references for the given object, starting from
// the immediate owner up to the top-level owner. It returns a slice of ObjectWithGVR in order
// from top-level owner to immediate owner. Returns nil if the object has no owner.
func (o *OwnerFetcher) GetOwners(ctx context.Context, obj metav1.Object) ([]*ObjectWithGVR, error) {
if o.resourceLists == nil {
// Get all API resources from the cluster using the discovery client. We need it for constructing GVRs for unstructured objects.
// Do it here once, so we do not have to list it multiple times before listing/getting every unstructured resource.
resourceLists, err := o.discoveryClient.ServerPreferredResources()
if err != nil {
return nil, err
}
o.resourceLists = resourceLists
}

// get the controller owner (it's possible to have only one controller owner)
owners := obj.GetOwnerReferences()
var ownerReference metav1.OwnerReference
var nonControllerOwner metav1.OwnerReference
for _, ownerRef := range owners {
// try to get the controller owner as the preferred one
if ownerRef.Controller != nil && *ownerRef.Controller {
ownerReference = ownerRef
break
} else if nonControllerOwner.Name == "" {
// take only the first non-controller owner
nonControllerOwner = ownerRef
}
}
// if no controller owner was found, then use the first non-controller owner (if present)
if ownerReference.Name == "" {
ownerReference = nonControllerOwner
}
if ownerReference.Name == "" {
return nil, nil // No owner
}
// Get the GVR for the owner
gvr, err := gvrForKind(ownerReference.Kind, ownerReference.APIVersion, o.resourceLists)
if err != nil {
return nil, err
}
// Get the owner object
ownerObject, err := o.dynamicClient.Resource(*gvr).Namespace(obj.GetNamespace()).Get(ctx, ownerReference.Name, metav1.GetOptions{})
if err != nil {
return nil, err
}
owner := &ObjectWithGVR{
Object: ownerObject,
GVR: gvr,
}
// Recursively try to find the top owner
ownerOwners, err := o.GetOwners(ctx, ownerObject)
if err != nil || ownerOwners == nil {
return append(ownerOwners, owner), err
}
return append(ownerOwners, owner), nil
}

// gvrForKind returns GVR for the kind, if it's found in the available API list in the cluster
// returns an error if not found or failed to parse the API version
func gvrForKind(kind, apiVersion string, resourceLists []*metav1.APIResourceList) (*schema.GroupVersionResource, error) {
gvr, err := findGVRForKind(kind, apiVersion, resourceLists)
if gvr == nil && err == nil {
return nil, fmt.Errorf("no resource found for kind %s in %s", kind, apiVersion)
}
return gvr, err
}

// findGVRForKind returns GVR for the kind, if it's found in the available API list in the cluster
// if not found then returns nil, nil
// returns nil, error if failed to parse the API version
func findGVRForKind(kind, apiVersion string, resourceLists []*metav1.APIResourceList) (*schema.GroupVersionResource, error) {
// Parse the group and version from the APIVersion (e.g., "apps/v1" -> group: "apps", version: "v1")
gv, err := schema.ParseGroupVersion(apiVersion)
if err != nil {
return nil, fmt.Errorf("failed to parse APIVersion %s: %w", apiVersion, err)
}

// Look for a matching resource
for _, resourceList := range resourceLists {
if resourceList.GroupVersion == apiVersion {
for _, apiResource := range resourceList.APIResources {
if apiResource.Kind == kind {
// Construct the GVR
return &schema.GroupVersionResource{
Group: gv.Group,
Version: gv.Version,
Resource: apiResource.Name,
}, nil
}
}
}
}

return nil, nil
}
Loading
Loading