Skip to content

Commit a059991

Browse files
feat: add support to unstructured resources to predicates (opendatahub-io#3121)
1 parent c10f03c commit a059991

File tree

2 files changed

+1280
-63
lines changed

2 files changed

+1280
-63
lines changed

pkg/controller/predicates/resources/resources.go

Lines changed: 148 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -1,46 +1,91 @@
11
package resources
22

33
import (
4+
"math"
45
"reflect"
56
"strings"
67

78
appsv1 "k8s.io/api/apps/v1"
89
corev1 "k8s.io/api/core/v1"
10+
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
11+
"k8s.io/apimachinery/pkg/runtime"
12+
"k8s.io/apimachinery/pkg/runtime/schema"
913
"sigs.k8s.io/controller-runtime/pkg/client"
1014
"sigs.k8s.io/controller-runtime/pkg/event"
1115
"sigs.k8s.io/controller-runtime/pkg/predicate"
1216
gwapiv1 "sigs.k8s.io/gateway-api/apis/v1"
1317

1418
dscv2 "github.com/opendatahub-io/opendatahub-operator/v2/api/datasciencecluster/v2"
15-
dsciv2 "github.com/opendatahub-io/opendatahub-operator/v2/api/dscinitialization/v2"
19+
"github.com/opendatahub-io/opendatahub-operator/v2/pkg/cluster/gvk"
1620
"github.com/opendatahub-io/opendatahub-operator/v2/pkg/resources"
1721
)
1822

1923
var _ predicate.Predicate = DeploymentPredicate{}
2024

25+
// hasGVK checks if an unstructured object matches the expected GroupVersionKind.
26+
func hasGVK(u *unstructured.Unstructured, expected schema.GroupVersionKind) bool {
27+
objGVK := u.GetObjectKind().GroupVersionKind()
28+
return objGVK == expected
29+
}
30+
2131
type DeploymentPredicate struct {
2232
predicate.Funcs
2333
}
2434

2535
// Update implements default UpdateEvent filter for validating generation change.
36+
// Works with both typed *appsv1.Deployment and *unstructured.Unstructured objects.
2637
func (DeploymentPredicate) Update(e event.UpdateEvent) bool {
2738
if e.ObjectOld == nil || e.ObjectNew == nil {
2839
return false
2940
}
3041

31-
oldDeployment, ok := e.ObjectOld.(*appsv1.Deployment)
32-
if !ok {
42+
oldReplicas, oldReadyReplicas, oldOk := getDeploymentStatus(e.ObjectOld)
43+
newReplicas, newReadyReplicas, newOk := getDeploymentStatus(e.ObjectNew)
44+
45+
if !oldOk || !newOk {
3346
return false
3447
}
3548

36-
newDeployment, ok := e.ObjectNew.(*appsv1.Deployment)
37-
if !ok {
38-
return false
49+
return e.ObjectOld.GetGeneration() != e.ObjectNew.GetGeneration() ||
50+
oldReplicas != newReplicas ||
51+
oldReadyReplicas != newReadyReplicas
52+
}
53+
54+
// getDeploymentStatus extracts replicas and readyReplicas from a Deployment object.
55+
// Supports both typed *appsv1.Deployment and *unstructured.Unstructured.
56+
func getDeploymentStatus(obj client.Object) (int32, int32, bool) {
57+
// Try typed Deployment first
58+
if deploy, isTyped := obj.(*appsv1.Deployment); isTyped {
59+
return deploy.Status.Replicas, deploy.Status.ReadyReplicas, true
60+
}
61+
62+
u, ok := obj.(*unstructured.Unstructured)
63+
if !ok || !hasGVK(u, gvk.Deployment) {
64+
return 0, 0, false
65+
}
66+
67+
status, found, err := unstructured.NestedMap(u.Object, "status")
68+
if err != nil || !found {
69+
return 0, 0, true // No status yet, but valid object
3970
}
4071

41-
return oldDeployment.Generation != newDeployment.Generation ||
42-
oldDeployment.Status.Replicas != newDeployment.Status.Replicas ||
43-
oldDeployment.Status.ReadyReplicas != newDeployment.Status.ReadyReplicas
72+
return getInt32Field(status, "replicas"), getInt32Field(status, "readyReplicas"), true
73+
}
74+
75+
func getInt32Field(m map[string]interface{}, field string) int32 {
76+
val, found, err := unstructured.NestedInt64(m, field)
77+
if err != nil || !found {
78+
return 0
79+
}
80+
81+
if val < math.MinInt32 {
82+
return math.MinInt32
83+
}
84+
if val > math.MaxInt32 {
85+
return math.MaxInt32
86+
}
87+
88+
return int32(val)
4489
}
4590

4691
func NewDeploymentPredicate() *DeploymentPredicate {
@@ -64,36 +109,96 @@ func Deleted() predicate.Funcs {
64109
}
65110
}
66111

112+
func getConfigMapData(obj client.Object) (any, bool) {
113+
cm, ok := obj.(*corev1.ConfigMap)
114+
if ok {
115+
return cm.Data, true
116+
}
117+
118+
u, ok := obj.(*unstructured.Unstructured)
119+
if !ok || !hasGVK(u, gvk.ConfigMap) {
120+
return nil, false
121+
}
122+
123+
data, _, err := unstructured.NestedFieldNoCopy(u.Object, "data")
124+
if err != nil {
125+
return nil, false
126+
}
127+
return data, true
128+
}
129+
67130
// Content predicates moved from original controller.
68131
var CMContentChangedPredicate = predicate.Funcs{
69132
UpdateFunc: func(e event.UpdateEvent) bool {
70-
oldCM, _ := e.ObjectOld.(*corev1.ConfigMap)
71-
newCM, _ := e.ObjectNew.(*corev1.ConfigMap)
72-
return !reflect.DeepEqual(oldCM.Data, newCM.Data)
133+
oldData, oldOk := getConfigMapData(e.ObjectOld)
134+
newData, newOk := getConfigMapData(e.ObjectNew)
135+
if !oldOk || !newOk {
136+
return false
137+
}
138+
return !reflect.DeepEqual(oldData, newData)
73139
},
74140
}
75141

142+
func getSecretData(obj client.Object) (any, bool) {
143+
secret, ok := obj.(*corev1.Secret)
144+
if ok {
145+
return secret.Data, true
146+
}
147+
148+
u, ok := obj.(*unstructured.Unstructured)
149+
if !ok || !hasGVK(u, gvk.Secret) {
150+
return nil, false
151+
}
152+
153+
data, _, err := unstructured.NestedFieldNoCopy(u.Object, "data")
154+
if err != nil {
155+
return nil, false
156+
}
157+
return data, true
158+
}
159+
76160
var SecretContentChangedPredicate = predicate.Funcs{
77161
UpdateFunc: func(e event.UpdateEvent) bool {
78-
oldSecret, _ := e.ObjectOld.(*corev1.Secret)
79-
newSecret, _ := e.ObjectNew.(*corev1.Secret)
80-
return !reflect.DeepEqual(oldSecret.Data, newSecret.Data)
162+
oldData, oldOk := getSecretData(e.ObjectOld)
163+
newData, newOk := getSecretData(e.ObjectNew)
164+
if !oldOk || !newOk {
165+
return false
166+
}
167+
return !reflect.DeepEqual(oldData, newData)
81168
},
82169
}
83170

171+
// FIXME: is this function correct? By default CreateFunc and UpdateFunc are true.
84172
var DSCDeletionPredicate = predicate.Funcs{
85173
DeleteFunc: func(e event.DeleteEvent) bool {
86174
return true
87175
},
88176
}
89177

178+
func getDSC(obj client.Object) (*dscv2.DataScienceCluster, bool) {
179+
if dsc, ok := obj.(*dscv2.DataScienceCluster); ok {
180+
return dsc, true
181+
}
182+
183+
u, err := resources.ToUnstructured(obj)
184+
if err != nil {
185+
return nil, false
186+
}
187+
dsc := &dscv2.DataScienceCluster{}
188+
err = runtime.DefaultUnstructuredConverter.FromUnstructured(u.Object, dsc)
189+
if err != nil {
190+
return nil, false
191+
}
192+
return dsc, true
193+
}
194+
90195
var DSCComponentUpdatePredicate = predicate.Funcs{
91196
UpdateFunc: func(e event.UpdateEvent) bool {
92-
oldDSC, ok := e.ObjectOld.(*dscv2.DataScienceCluster)
197+
oldDSC, ok := getDSC(e.ObjectOld)
93198
if !ok {
94199
return false
95200
}
96-
newDSC, ok := e.ObjectNew.(*dscv2.DataScienceCluster)
201+
newDSC, ok := getDSC(e.ObjectNew)
97202
if !ok {
98203
return false
99204
}
@@ -123,30 +228,6 @@ var DSCComponentUpdatePredicate = predicate.Funcs{
123228
},
124229
}
125230

126-
var DSCIReadiness = predicate.Funcs{
127-
UpdateFunc: func(e event.UpdateEvent) bool {
128-
oldObj, ok := e.ObjectOld.(*dsciv2.DSCInitialization)
129-
if !ok {
130-
return false
131-
}
132-
newObj, ok := e.ObjectNew.(*dsciv2.DSCInitialization)
133-
if !ok {
134-
return false
135-
}
136-
137-
return oldObj.Status.Phase != newObj.Status.Phase
138-
},
139-
CreateFunc: func(e event.CreateEvent) bool {
140-
return false
141-
},
142-
DeleteFunc: func(e event.DeleteEvent) bool {
143-
return false
144-
},
145-
GenericFunc: func(e event.GenericEvent) bool {
146-
return false
147-
},
148-
}
149-
150231
func AnnotationChanged(name string) predicate.Funcs {
151232
return predicate.Funcs{
152233
CreateFunc: func(e event.CreateEvent) bool {
@@ -236,7 +317,28 @@ func GatewayStatusChanged() predicate.Predicate {
236317

237318
// HTTPRouteReferencesGateway returns a predicate that filters HTTPRoutes referencing the specified gateway.
238319
func HTTPRouteReferencesGateway(gatewayName, gatewayNamespace string) predicate.Predicate {
239-
referencesGateway := func(httpRoute *gwapiv1.HTTPRoute) bool {
320+
getHTTPRoute := func(obj client.Object) (*gwapiv1.HTTPRoute, bool) {
321+
httpRoute, ok := obj.(*gwapiv1.HTTPRoute)
322+
if ok {
323+
return httpRoute, true
324+
}
325+
u, err := resources.ToUnstructured(obj)
326+
if err != nil {
327+
return nil, false
328+
}
329+
httpRoute = &gwapiv1.HTTPRoute{}
330+
err = runtime.DefaultUnstructuredConverter.FromUnstructured(u.Object, httpRoute)
331+
if err != nil {
332+
return nil, false
333+
}
334+
return httpRoute, true
335+
}
336+
337+
referencesGateway := func(obj client.Object) bool {
338+
httpRoute, ok := getHTTPRoute(obj)
339+
if !ok {
340+
return false
341+
}
240342
for _, ref := range httpRoute.Spec.ParentRefs {
241343
refNamespace := gatewayNamespace
242344
if ref.Namespace != nil {
@@ -251,32 +353,16 @@ func HTTPRouteReferencesGateway(gatewayName, gatewayNamespace string) predicate.
251353

252354
return predicate.Funcs{
253355
CreateFunc: func(e event.CreateEvent) bool {
254-
httpRoute, ok := e.Object.(*gwapiv1.HTTPRoute)
255-
if !ok {
256-
return false
257-
}
258-
return referencesGateway(httpRoute)
356+
return referencesGateway(e.Object)
259357
},
260358
UpdateFunc: func(e event.UpdateEvent) bool {
261-
httpRoute, ok := e.ObjectNew.(*gwapiv1.HTTPRoute)
262-
if !ok {
263-
return false
264-
}
265-
return referencesGateway(httpRoute)
359+
return referencesGateway(e.ObjectNew)
266360
},
267361
DeleteFunc: func(e event.DeleteEvent) bool {
268-
httpRoute, ok := e.Object.(*gwapiv1.HTTPRoute)
269-
if !ok {
270-
return false
271-
}
272-
return referencesGateway(httpRoute)
362+
return referencesGateway(e.Object)
273363
},
274364
GenericFunc: func(e event.GenericEvent) bool {
275-
httpRoute, ok := e.Object.(*gwapiv1.HTTPRoute)
276-
if !ok {
277-
return false
278-
}
279-
return referencesGateway(httpRoute)
365+
return referencesGateway(e.Object)
280366
},
281367
}
282368
}

0 commit comments

Comments
 (0)