Skip to content

Commit 4066619

Browse files
move owner fetcher from member-operator (#496)
1 parent 2591ebe commit 4066619

File tree

4 files changed

+493
-3
lines changed

4 files changed

+493
-3
lines changed

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ require (
2626
)
2727

2828
require (
29-
github.com/codeready-toolchain/api v0.0.0-20250916082953-4ecb3a4645e6
29+
github.com/codeready-toolchain/api v0.0.0-20251008084914-06282b83d4cd
3030
github.com/ghodss/yaml v1.0.0
3131
github.com/google/go-cmp v0.7.0
3232
github.com/google/go-github/v52 v52.0.0

go.sum

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,8 @@ github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XL
2020
github.com/cloudflare/circl v1.1.0/go.mod h1:prBCrKB9DV4poKZY1l9zBXg2QJY7mvgRvtMxxK7fi4I=
2121
github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0=
2222
github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs=
23-
github.com/codeready-toolchain/api v0.0.0-20250916082953-4ecb3a4645e6 h1:3Z1nQ7CjBCphWyGkEUCSMrd4VfuxzOVYyEi9/7FjLPs=
24-
github.com/codeready-toolchain/api v0.0.0-20250916082953-4ecb3a4645e6/go.mod h1:TiQ/yNv3cGL4nxo3fgRtcHyYYuRf+nAgs6B1IAqvxOU=
23+
github.com/codeready-toolchain/api v0.0.0-20251008084914-06282b83d4cd h1:A6WOUwlUdgOf4PDzzgKvycRj4Pk+1F6npWV4YoPVFcM=
24+
github.com/codeready-toolchain/api v0.0.0-20251008084914-06282b83d4cd/go.mod h1:TiQ/yNv3cGL4nxo3fgRtcHyYYuRf+nAgs6B1IAqvxOU=
2525
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
2626
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
2727
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=

pkg/owners/fetcher.go

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
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

Comments
 (0)