Skip to content

Commit 2707785

Browse files
Merge pull request #787 from Nordix/replace-requeafterError-type/sunnat
🌱 Introduce ReconcileError with Transient and Terminal Error type
2 parents f4eeb18 + fa6581d commit 2707785

9 files changed

+171
-110
lines changed

controllers/ippool_controller.go

+15-6
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ import (
3434
"sigs.k8s.io/controller-runtime/pkg/client"
3535
"sigs.k8s.io/controller-runtime/pkg/controller"
3636
"sigs.k8s.io/controller-runtime/pkg/handler"
37+
"sigs.k8s.io/controller-runtime/pkg/reconcile"
3738
)
3839

3940
const (
@@ -147,7 +148,7 @@ func (r *IPPoolReconciler) reconcileNormal(ctx context.Context,
147148

148149
_, err := ipPoolMgr.UpdateAddresses(ctx)
149150
if err != nil {
150-
return checkRequeueError(err, "Failed to create the missing data")
151+
return checkReconcileError(err, "Failed to create the missing data")
151152
}
152153

153154
return ctrl.Result{}, nil
@@ -158,7 +159,7 @@ func (r *IPPoolReconciler) reconcileDelete(ctx context.Context,
158159
) (ctrl.Result, error) {
159160
allocationsNb, err := ipPoolMgr.UpdateAddresses(ctx)
160161
if err != nil {
161-
return checkRequeueError(err, "Failed to delete the old secrets")
162+
return checkReconcileError(err, "Failed to delete the old secrets")
162163
}
163164

164165
if allocationsNb == 0 {
@@ -206,13 +207,21 @@ func (r *IPPoolReconciler) IPClaimToIPPool(_ context.Context, obj client.Object)
206207
return []ctrl.Request{}
207208
}
208209

209-
func checkRequeueError(err error, errMessage string) (ctrl.Result, error) {
210+
// checkReconcileError checks if the error is a transient or terminal error.
211+
// If it is transient, it returns a Result with Requeue set to true.
212+
// Non-reconcile errors are returned as-is.
213+
func checkReconcileError(err error, errMessage string) (ctrl.Result, error) {
210214
if err == nil {
211215
return ctrl.Result{}, nil
212216
}
213-
var requeueErr ipam.HasRequeueAfterError
214-
if ok := errors.As(err, &requeueErr); ok {
215-
return ctrl.Result{Requeue: true, RequeueAfter: requeueErr.GetRequeueAfter()}, nil
217+
var reconcileError ipam.ReconcileError
218+
if errors.As(err, &reconcileError) {
219+
if reconcileError.IsTransient() {
220+
return reconcile.Result{Requeue: true, RequeueAfter: reconcileError.GetRequeueAfter()}, nil
221+
}
222+
if reconcileError.IsTerminal() {
223+
return reconcile.Result{}, nil
224+
}
216225
}
217226
return ctrl.Result{}, errors.Wrap(err, errMessage)
218227
}

ipam/ippool_manager.go

+3-3
Original file line numberDiff line numberDiff line change
@@ -397,11 +397,11 @@ func (m *IPPoolManager) createAddress(ctx context.Context,
397397
}
398398

399399
// Create the IPAddress object. If we get a conflict (that will set
400-
// HasRequeueAfterError), then requeue to retrigger the reconciliation with
400+
// Transient error), then requeue to retrigger the reconciliation with
401401
// the new state
402402
if err := createObject(ctx, m.client, addressObject); err != nil {
403-
var reqAfter *RequeueAfterError
404-
if ok := errors.As(err, &reqAfter); !ok {
403+
var reconcileError ReconcileError
404+
if !errors.As(err, &reconcileError) {
405405
addressClaim.Status.ErrorMessage = ptr.To("Failed to create associated IPAddress object")
406406
}
407407
return addresses, err

ipam/ippool_manager_test.go

+4-4
Original file line numberDiff line numberDiff line change
@@ -334,9 +334,9 @@ var _ = Describe("IPPool manager", func() {
334334
if tc.expectRequeue || tc.expectError {
335335
Expect(err).To(HaveOccurred())
336336
if tc.expectRequeue {
337-
Expect(err).To(BeAssignableToTypeOf(&RequeueAfterError{}))
337+
Expect(err).To(BeAssignableToTypeOf(ReconcileError{}))
338338
} else {
339-
Expect(err).NotTo(BeAssignableToTypeOf(&RequeueAfterError{}))
339+
Expect(err).NotTo(BeAssignableToTypeOf(ReconcileError{}))
340340
}
341341
} else {
342342
Expect(err).NotTo(HaveOccurred())
@@ -596,9 +596,9 @@ var _ = Describe("IPPool manager", func() {
596596
if tc.expectRequeue || tc.expectError {
597597
Expect(err).To(HaveOccurred())
598598
if tc.expectRequeue {
599-
Expect(err).To(BeAssignableToTypeOf(&RequeueAfterError{}))
599+
Expect(err).To(BeAssignableToTypeOf(ReconcileError{}))
600600
} else {
601-
Expect(err).NotTo(BeAssignableToTypeOf(&RequeueAfterError{}))
601+
Expect(err).NotTo(BeAssignableToTypeOf(ReconcileError{}))
602602
}
603603
} else {
604604
Expect(err).NotTo(HaveOccurred())

ipam/reconcile_error.go

+85
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
/*
2+
Copyright 2024 The Metal3 Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package ipam
18+
19+
import (
20+
"fmt"
21+
"time"
22+
)
23+
24+
// ReconcileError represents an generic error of Reconcile loop. ErrorType indicates what type
25+
// of action is required to recover. It can take two values:
26+
// 1. `Transient` - Can be recovered , will be requeued after.
27+
// 2. `Terminal` - Cannot be recovered, will not be requeued.
28+
29+
type ReconcileError struct {
30+
error
31+
errorType ReconcileErrorType
32+
RequeueAfter time.Duration
33+
}
34+
35+
// ReconcileErrorType represents the type of a ReconcileError.
36+
type ReconcileErrorType string
37+
38+
const (
39+
// TransientErrorType can be recovered, will be requeued after a configured time interval.
40+
TransientErrorType ReconcileErrorType = "Transient"
41+
// TerminalErrorType cannot be recovered, will not be requeued.
42+
TerminalErrorType ReconcileErrorType = "Terminal"
43+
)
44+
45+
// Error returns the error message for a ReconcileError.
46+
func (e ReconcileError) Error() string {
47+
var errStr string
48+
if e.error != nil {
49+
errStr = e.error.Error()
50+
}
51+
switch e.errorType {
52+
case TransientErrorType:
53+
return fmt.Sprintf("%s. Object will be requeued after %s", errStr, e.GetRequeueAfter())
54+
case TerminalErrorType:
55+
return fmt.Sprintf("reconcile error that cannot be recovered occurred: %s. Object will not be requeued", errStr)
56+
default:
57+
return fmt.Sprintf("reconcile error occurred with unknown recovery type. The actual error is: %s", errStr)
58+
}
59+
}
60+
61+
// GetRequeueAfter gets the duration to wait until the managed object is
62+
// requeued for further processing.
63+
func (e ReconcileError) GetRequeueAfter() time.Duration {
64+
return e.RequeueAfter
65+
}
66+
67+
// IsTransient returns if the ReconcileError is recoverable.
68+
func (e ReconcileError) IsTransient() bool {
69+
return e.errorType == TransientErrorType
70+
}
71+
72+
// IsTerminal returns if the ReconcileError is non recoverable.
73+
func (e ReconcileError) IsTerminal() bool {
74+
return e.errorType == TerminalErrorType
75+
}
76+
77+
// WithTransientError wraps the error in a ReconcileError with errorType as `Transient`.
78+
func WithTransientError(err error, requeueAfter time.Duration) ReconcileError {
79+
return ReconcileError{error: err, errorType: TransientErrorType, RequeueAfter: requeueAfter}
80+
}
81+
82+
// WithTerminalError wraps the error in a ReconcileError with errorType as `Terminal`.
83+
func WithTerminalError(err error) ReconcileError {
84+
return ReconcileError{error: err, errorType: TerminalErrorType}
85+
}

ipam/reconcile_error_test.go

+56
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
/*
2+
Copyright 2024 The Metal3 Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package ipam
18+
19+
import (
20+
"errors"
21+
"fmt"
22+
"time"
23+
24+
. "github.com/onsi/ginkgo/v2"
25+
. "github.com/onsi/gomega"
26+
)
27+
28+
const (
29+
duration = 50 * time.Second
30+
)
31+
32+
var _ = Describe("Reconcile Error testing", func() {
33+
34+
It("Returns correct values for Transient Error", func() {
35+
36+
err := WithTransientError(errors.New("Transient Error"), duration)
37+
Expect(err.GetRequeueAfter()).To(Equal(duration))
38+
Expect(err.IsTransient()).To(BeTrue())
39+
Expect(err.IsTerminal()).To(BeFalse())
40+
Expect(err.Error()).To(Equal(fmt.Sprintf("%s. Object will be requeued after %s", "Transient Error", duration)))
41+
})
42+
43+
It("Returns correct values for Terminal Error", func() {
44+
err := WithTerminalError(errors.New("Terminal Error"))
45+
Expect(err.IsTransient()).To(BeFalse())
46+
Expect(err.IsTerminal()).To(BeTrue())
47+
Expect(err.Error()).To(Equal(fmt.Sprintf("reconcile error that cannot be recovered occurred: %s. Object will not be requeued", "Terminal Error")))
48+
})
49+
50+
It("Returns correct values for Unknown ReconcileError type", func() {
51+
err := ReconcileError{errors.New("Unknown Error"), "unknownErrorType", 0 * time.Second}
52+
Expect(err.IsTerminal()).To(BeFalse())
53+
Expect(err.IsTransient()).To(BeFalse())
54+
Expect(err.Error()).To(Equal(fmt.Sprintf("reconcile error occurred with unknown recovery type. The actual error is: %s", "Unknown Error")))
55+
})
56+
})

ipam/requeue_error.go

-49
This file was deleted.

ipam/requeue_error_test.go

-43
This file was deleted.

ipam/utils.go

+5-2
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ package ipam
1818

1919
import (
2020
"context"
21+
"fmt"
22+
"time"
2123

2224
"github.com/pkg/errors"
2325
apierrors "k8s.io/apimachinery/pkg/api/errors"
@@ -61,15 +63,16 @@ func (e *NotFoundError) Error() string {
6163
func updateObject(ctx context.Context, cl client.Client, obj client.Object, opts ...client.UpdateOption) error {
6264
err := cl.Update(ctx, obj.DeepCopyObject().(client.Object), opts...)
6365
if apierrors.IsConflict(err) {
64-
return &RequeueAfterError{}
66+
return WithTransientError(errors.New("Updating object failed"), 0*time.Second)
6567
}
6668
return err
6769
}
6870

6971
func createObject(ctx context.Context, cl client.Client, obj client.Object, opts ...client.CreateOption) error {
7072
err := cl.Create(ctx, obj.DeepCopyObject().(client.Object), opts...)
7173
if apierrors.IsAlreadyExists(err) {
72-
return &RequeueAfterError{}
74+
fmt.Printf("I am inside IsAlreadyExists")
75+
return WithTransientError(errors.New("Object already exists"), 0*time.Second)
7376
}
7477
return err
7578
}

ipam/utils_test.go

+3-3
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,7 @@ var _ = Describe("Metal3 manager utils", func() {
130130
err := updateObject(context.TODO(), c, obj)
131131
if tc.ExpectedError {
132132
Expect(err).To(HaveOccurred())
133-
Expect(err).NotTo(BeAssignableToTypeOf(&RequeueAfterError{}))
133+
Expect(err).NotTo(BeAssignableToTypeOf(ReconcileError{}))
134134
} else {
135135
Expect(err).NotTo(HaveOccurred())
136136
Expect(obj.Spec).To(Equal(tc.TestObject.Spec))
@@ -148,7 +148,7 @@ var _ = Describe("Metal3 manager utils", func() {
148148
Expect(savedObject.ResourceVersion).NotTo(Equal(tc.TestObject.ResourceVersion))
149149
err := updateObject(context.TODO(), c, obj)
150150
Expect(err).To(HaveOccurred())
151-
Expect(err).To(BeAssignableToTypeOf(&RequeueAfterError{}))
151+
Expect(err).To(BeAssignableToTypeOf(ReconcileError{}))
152152
}
153153
err = c.Delete(context.TODO(), tc.TestObject)
154154
if err != nil {
@@ -211,7 +211,7 @@ var _ = Describe("Metal3 manager utils", func() {
211211
err := createObject(context.TODO(), c, obj)
212212
if tc.ExpectedError {
213213
Expect(err).To(HaveOccurred())
214-
Expect(err).To(BeAssignableToTypeOf(&RequeueAfterError{}))
214+
Expect(err).To(BeAssignableToTypeOf(ReconcileError{}))
215215
} else {
216216
Expect(err).NotTo(HaveOccurred())
217217
Expect(obj.Spec).To(Equal(tc.TestObject.Spec))

0 commit comments

Comments
 (0)