Skip to content

Commit f53ce3c

Browse files
committed
Add function to check the status of the owner of an object
1 parent 6ba873b commit f53ce3c

2 files changed

Lines changed: 280 additions & 0 deletions

File tree

modules/common/object/metadata.go

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ import (
3131

3232
k8s_errors "k8s.io/apimachinery/pkg/api/errors"
3333
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
34+
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
35+
"k8s.io/apimachinery/pkg/runtime/schema"
3436
)
3537

3638
// CheckOwnerRefExist - returns true if the owner is already in the owner ref list
@@ -114,3 +116,84 @@ func EnsureOwnerRef(
114116

115117
return nil
116118
}
119+
120+
// IsOwnerServiceReady checks if the owner service that owns this object is ready.
121+
// Returns true if the owner is ready, false if not ready, and error only for unexpected failures.
122+
// If there's no owner with controller=true, it returns true (safe to proceed).
123+
func IsOwnerServiceReady(
124+
ctx context.Context,
125+
h *helper.Helper,
126+
obj client.Object,
127+
) (bool, error) {
128+
// Find the controller owner reference (e.g., Cinder, Nova, etc.)
129+
var ownerRef *metav1.OwnerReference
130+
for _, owner := range obj.GetOwnerReferences() {
131+
if owner.Controller != nil && *owner.Controller {
132+
ownerRef = &owner
133+
break
134+
}
135+
}
136+
137+
// If no controlling owner, safe to proceed
138+
if ownerRef == nil {
139+
h.GetLogger().Info("No controller owner found, owner is considered ready")
140+
return true, nil
141+
}
142+
143+
// Parse the APIVersion to extract group and version
144+
gv, err := schema.ParseGroupVersion(ownerRef.APIVersion)
145+
if err != nil {
146+
h.GetLogger().Error(err, "Failed to parse owner APIVersion", "apiVersion", ownerRef.APIVersion)
147+
return false, err
148+
}
149+
150+
// Fetch the owner resource using unstructured client
151+
owner := &unstructured.Unstructured{}
152+
owner.SetGroupVersionKind(schema.GroupVersionKind{
153+
Group: gv.Group,
154+
Version: gv.Version,
155+
Kind: ownerRef.Kind,
156+
})
157+
158+
err = h.GetClient().Get(ctx, types.NamespacedName{
159+
Name: ownerRef.Name,
160+
Namespace: obj.GetNamespace(),
161+
}, owner)
162+
163+
if err != nil {
164+
if k8s_errors.IsNotFound(err) {
165+
// Owner deleted, safe to proceed
166+
h.GetLogger().Info("Owner resource not found, owner is considered ready", "kind", ownerRef.Kind, "name", ownerRef.Name)
167+
return true, nil
168+
}
169+
// Unexpected error, log and return error
170+
h.GetLogger().Error(err, "Failed to fetch owner resource", "kind", ownerRef.Kind, "name", ownerRef.Name)
171+
return false, err
172+
}
173+
174+
// Check status.conditions for Ready condition
175+
conditions, found, err := unstructured.NestedSlice(owner.Object, "status", "conditions")
176+
if err != nil || !found {
177+
h.GetLogger().Info("No conditions found in owner status, waiting", "kind", ownerRef.Kind, "name", ownerRef.Name)
178+
return false, nil
179+
}
180+
181+
// Look for Ready condition with status=True
182+
for _, c := range conditions {
183+
condition, ok := c.(map[string]any)
184+
if !ok {
185+
continue
186+
}
187+
188+
condType, _, _ := unstructured.NestedString(condition, "type")
189+
status, _, _ := unstructured.NestedString(condition, "status")
190+
191+
if condType == "Ready" && status == "True" {
192+
h.GetLogger().Info("Owner service is ready", "kind", ownerRef.Kind, "name", ownerRef.Name)
193+
return true, nil
194+
}
195+
}
196+
197+
h.GetLogger().Info("Owner service not ready, waiting", "kind", ownerRef.Kind, "name", ownerRef.Name)
198+
return false, nil
199+
}

modules/common/test/functional/object_test.go

Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import (
2121
. "github.com/onsi/gomega" // nolint:revive
2222
"github.com/openstack-k8s-operators/lib-common/modules/common/object"
2323

24+
corev1 "k8s.io/api/core/v1"
2425
"k8s.io/apimachinery/pkg/types"
2526
)
2627

@@ -72,4 +73,200 @@ var _ = Describe("object package", func() {
7273
Expect(err).ShouldNot(HaveOccurred())
7374
Expect(object.CheckOwnerRefExist(ownerCM.GetUID(), cm.GetOwnerReferences())).To(BeTrue())
7475
})
76+
77+
When("checking if owner service is ready", func() {
78+
It("returns true when object has no controller owner", func() {
79+
cmName := types.NamespacedName{
80+
Namespace: namespace,
81+
Name: "test-cm-no-owner",
82+
}
83+
cm := th.CreateConfigMap(cmName, map[string]interface{}{})
84+
85+
ready, err := object.IsOwnerServiceReady(th.Ctx, h, cm)
86+
Expect(err).ShouldNot(HaveOccurred())
87+
Expect(ready).To(BeTrue())
88+
})
89+
90+
It("returns true when controller owner is deleted", func() {
91+
// Create a ConfigMap with an owner reference to a non-existent ConfigMap
92+
rawCM := map[string]interface{}{
93+
"apiVersion": "v1",
94+
"kind": "ConfigMap",
95+
"metadata": map[string]interface{}{
96+
"name": "test-cm-deleted-owner",
97+
"namespace": namespace,
98+
"ownerReferences": []interface{}{
99+
map[string]interface{}{
100+
"apiVersion": "v1",
101+
"kind": "ConfigMap",
102+
"name": "non-existent-configmap",
103+
"uid": "11111111-1111-1111-1111-111111111111",
104+
"controller": true,
105+
},
106+
},
107+
},
108+
}
109+
cm := th.CreateUnstructured(rawCM)
110+
111+
ready, err := object.IsOwnerServiceReady(th.Ctx, h, cm)
112+
Expect(err).ShouldNot(HaveOccurred())
113+
Expect(ready).To(BeTrue())
114+
})
115+
116+
It("returns false when controller owner exists but has no status", func() {
117+
// Create owner ConfigMap (no status.conditions)
118+
ownerCM := th.CreateConfigMap(types.NamespacedName{
119+
Namespace: namespace,
120+
Name: "owner-cm-no-status",
121+
}, map[string]interface{}{})
122+
123+
// Create child ConfigMap with owner reference
124+
rawCM := map[string]interface{}{
125+
"apiVersion": "v1",
126+
"kind": "ConfigMap",
127+
"metadata": map[string]interface{}{
128+
"name": "child-cm-no-status",
129+
"namespace": namespace,
130+
"ownerReferences": []interface{}{
131+
map[string]interface{}{
132+
"apiVersion": "v1",
133+
"kind": "ConfigMap",
134+
"name": ownerCM.GetName(),
135+
"uid": string(ownerCM.GetUID()),
136+
"controller": true,
137+
},
138+
},
139+
},
140+
}
141+
cm := th.CreateUnstructured(rawCM)
142+
143+
ready, err := object.IsOwnerServiceReady(th.Ctx, h, cm)
144+
Expect(err).ShouldNot(HaveOccurred())
145+
// ConfigMaps don't have status.conditions, so should return false
146+
Expect(ready).To(BeFalse())
147+
})
148+
149+
It("returns true when controller owner has Ready condition with status True", func() {
150+
// Create a Pod as the owner (Pods support status updates)
151+
th.CreatePod(types.NamespacedName{
152+
Namespace: namespace,
153+
Name: "owner-pod-ready",
154+
}, map[string]string{}, map[string]interface{}{
155+
"containers": []interface{}{
156+
map[string]interface{}{
157+
"name": "test",
158+
"image": "test:latest",
159+
},
160+
},
161+
})
162+
163+
// Update the Pod status to include a Ready condition
164+
Eventually(func(g Gomega) {
165+
pod := th.GetPod(types.NamespacedName{
166+
Namespace: namespace,
167+
Name: "owner-pod-ready",
168+
})
169+
// Manually set status with Ready condition
170+
pod.Status.Conditions = []corev1.PodCondition{
171+
{
172+
Type: "Ready",
173+
Status: corev1.ConditionTrue,
174+
},
175+
}
176+
err := th.K8sClient.Status().Update(th.Ctx, pod)
177+
g.Expect(err).ShouldNot(HaveOccurred())
178+
}, th.Timeout, th.Interval).Should(Succeed())
179+
180+
// Get the updated pod to get its UID
181+
pod := th.GetPod(types.NamespacedName{
182+
Namespace: namespace,
183+
Name: "owner-pod-ready",
184+
})
185+
186+
// Create a ConfigMap owned by the Pod
187+
rawCM := map[string]interface{}{
188+
"apiVersion": "v1",
189+
"kind": "ConfigMap",
190+
"metadata": map[string]interface{}{
191+
"name": "child-cm-ready-owner",
192+
"namespace": namespace,
193+
"ownerReferences": []interface{}{
194+
map[string]interface{}{
195+
"apiVersion": "v1",
196+
"kind": "Pod",
197+
"name": pod.Name,
198+
"uid": string(pod.GetUID()),
199+
"controller": true,
200+
},
201+
},
202+
},
203+
}
204+
cm := th.CreateUnstructured(rawCM)
205+
206+
ready, err := object.IsOwnerServiceReady(th.Ctx, h, cm)
207+
Expect(err).ShouldNot(HaveOccurred())
208+
Expect(ready).To(BeTrue())
209+
})
210+
211+
It("returns false when controller owner has Ready condition with status False", func() {
212+
// Create a Pod as the owner
213+
th.CreatePod(types.NamespacedName{
214+
Namespace: namespace,
215+
Name: "owner-pod-not-ready",
216+
}, map[string]string{}, map[string]interface{}{
217+
"containers": []interface{}{
218+
map[string]interface{}{
219+
"name": "test",
220+
"image": "test:latest",
221+
},
222+
},
223+
})
224+
225+
// Update the Pod status to include a Ready=False condition
226+
Eventually(func(g Gomega) {
227+
pod := th.GetPod(types.NamespacedName{
228+
Namespace: namespace,
229+
Name: "owner-pod-not-ready",
230+
})
231+
pod.Status.Conditions = []corev1.PodCondition{
232+
{
233+
Type: "Ready",
234+
Status: corev1.ConditionFalse,
235+
},
236+
}
237+
err := th.K8sClient.Status().Update(th.Ctx, pod)
238+
g.Expect(err).ShouldNot(HaveOccurred())
239+
}, th.Timeout, th.Interval).Should(Succeed())
240+
241+
// Get the updated pod
242+
pod := th.GetPod(types.NamespacedName{
243+
Namespace: namespace,
244+
Name: "owner-pod-not-ready",
245+
})
246+
247+
// Create a ConfigMap owned by the Pod
248+
rawCM := map[string]interface{}{
249+
"apiVersion": "v1",
250+
"kind": "ConfigMap",
251+
"metadata": map[string]interface{}{
252+
"name": "child-cm-not-ready-owner",
253+
"namespace": namespace,
254+
"ownerReferences": []interface{}{
255+
map[string]interface{}{
256+
"apiVersion": "v1",
257+
"kind": "Pod",
258+
"name": pod.Name,
259+
"uid": string(pod.GetUID()),
260+
"controller": true,
261+
},
262+
},
263+
},
264+
}
265+
cm := th.CreateUnstructured(rawCM)
266+
267+
ready, err := object.IsOwnerServiceReady(th.Ctx, h, cm)
268+
Expect(err).ShouldNot(HaveOccurred())
269+
Expect(ready).To(BeFalse())
270+
})
271+
})
75272
})

0 commit comments

Comments
 (0)