Skip to content

Commit 0368bf3

Browse files
committed
Propagate more errors
Both in the traits controller as well as in the placement-api calls, some errors were swallowed.
1 parent 05f22f6 commit 0368bf3

4 files changed

Lines changed: 82 additions & 2 deletions

File tree

internal/controller/traits_controller.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ func (tc *TraitsController) Reconcile(ctx context.Context, req ctrl.Request) (ct
106106
base := hv.DeepCopy()
107107
if meta.SetStatusCondition(&hv.Status.Conditions,
108108
getTraitCondition(err, "Failed to get current traits from placement")) {
109-
err = errors.Join(tc.Status().Patch(ctx, hv, k8sclient.MergeFromWithOptions(base,
109+
err = errors.Join(err, tc.Status().Patch(ctx, hv, k8sclient.MergeFromWithOptions(base,
110110
k8sclient.MergeFromWithOptimisticLock{}), k8sclient.FieldOwner(TraitsControllerName)))
111111
}
112112
return ctrl.Result{}, err

internal/controller/traits_controller_test.go

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -266,6 +266,41 @@ var _ = Describe("TraitsController", func() {
266266
})
267267
})
268268

269+
Context("Reconcile when GetTraits returns an error", func() {
270+
BeforeEach(func(ctx SpecContext) {
271+
// Mock resourceproviders.GetTraits to return a server error
272+
fakeServer.Mux.HandleFunc("GET /resource_providers/1234/traits", func(w http.ResponseWriter, r *http.Request) {
273+
w.WriteHeader(http.StatusInternalServerError)
274+
fmt.Fprint(w, `{"error": "Internal Server Error"}`)
275+
})
276+
// UpdateTraits must not be called
277+
fakeServer.Mux.HandleFunc("PUT /resource_providers/1234/traits", func(w http.ResponseWriter, r *http.Request) {
278+
defer GinkgoRecover()
279+
Fail("should not be called")
280+
})
281+
282+
hypervisor := &kvmv1.Hypervisor{}
283+
Expect(k8sClient.Get(ctx, hypervisorName, hypervisor)).To(Succeed())
284+
meta.SetStatusCondition(&hypervisor.Status.Conditions, metav1.Condition{
285+
Type: kvmv1.ConditionTypeOnboarding,
286+
Status: metav1.ConditionFalse,
287+
Reason: "UnitTest",
288+
})
289+
hypervisor.Status.HypervisorID = "1234"
290+
hypervisor.Status.Traits = []string{"CUSTOM_FOO", "HW_CPU_X86_VMX"}
291+
Expect(k8sClient.Status().Update(ctx, hypervisor)).To(Succeed())
292+
})
293+
294+
It("should return the GetTraits error", func(ctx SpecContext) {
295+
req := ctrl.Request{NamespacedName: hypervisorName}
296+
_, err := traitsController.Reconcile(ctx, req)
297+
// Bug: err is reassigned to errors.Join(patchErr) instead of
298+
// errors.Join(originalErr, patchErr), so when the status patch
299+
// succeeds the original GetTraits error is silently dropped.
300+
Expect(err).To(HaveOccurred())
301+
})
302+
})
303+
269304
Context("Reconcile when terminating", func() {
270305
BeforeEach(func(ctx SpecContext) {
271306
// Mock resourceproviders.GetTraits

internal/openstack/placement.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,9 @@ func CleanupResourceProvider(ctx context.Context, client *gophercloud.ServiceCli
151151

152152
// The consumer actually doesn't have *any* allocations, so it is just
153153
// inconsistent, and we can drop them all
154-
deleteConsumerAllocations(ctx, client, consumerID)
154+
if err := deleteConsumerAllocations(ctx, client, consumerID).Err; err != nil {
155+
return err
156+
}
155157
}
156158

157159
// We are done, let's clean it up

internal/openstack/placement_test.go

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -298,6 +298,49 @@ var _ = Describe("Placement API", func() {
298298
})
299299
})
300300

301+
Context("when deleting consumer allocations fails", func() {
302+
BeforeEach(func() {
303+
fakeServer.Mux.HandleFunc(providerAllocsURL, func(w http.ResponseWriter, r *http.Request) {
304+
w.Header().Set("Content-Type", "application/json")
305+
w.WriteHeader(http.StatusOK)
306+
fmt.Fprint(w, `{"allocations": {"consumer-1": {}}}`)
307+
})
308+
309+
fakeServer.Mux.HandleFunc("/allocations/consumer-1", func(w http.ResponseWriter, r *http.Request) {
310+
switch r.Method {
311+
case http.MethodGet:
312+
// Consumer has no allocations of its own — proceed to delete
313+
w.Header().Set("Content-Type", "application/json")
314+
w.WriteHeader(http.StatusOK)
315+
fmt.Fprint(w, `{"allocations": {}, "consumer_generation": 0}`)
316+
case http.MethodDelete:
317+
// Deletion fails
318+
w.WriteHeader(http.StatusInternalServerError)
319+
fmt.Fprint(w, `{"error": "Internal Server Error"}`)
320+
}
321+
})
322+
323+
// The provider delete succeeds, so that it cannot accidentally surface
324+
// the error for us — only the consumer allocation delete error should be returned.
325+
fakeServer.Mux.HandleFunc(deleteProviderURL, func(w http.ResponseWriter, r *http.Request) {
326+
w.WriteHeader(http.StatusNoContent)
327+
})
328+
})
329+
330+
It("should return an error when consumer allocation deletion fails", func() {
331+
provider := &resourceproviders.ResourceProvider{
332+
UUID: providerUUID,
333+
Name: "test-provider",
334+
}
335+
336+
err := CleanupResourceProvider(ctx, client.ServiceClient(fakeServer), provider)
337+
// Bug: deleteConsumerAllocations return value is discarded, so this
338+
// error is silently swallowed and the function proceeds to delete the
339+
// provider and returns nil instead of the deletion error.
340+
Expect(err).To(HaveOccurred())
341+
})
342+
})
343+
301344
Context("when deleting provider fails", func() {
302345
BeforeEach(func() {
303346
fakeServer.Mux.HandleFunc(providerAllocsURL, func(w http.ResponseWriter, r *http.Request) {

0 commit comments

Comments
 (0)