Skip to content

Commit b51ce8a

Browse files
committed
clean up retryable/non-retryable errors in controllers
Use a custom internal error type to allow reconcile helpers to indicate when they encounter errors that are non-retryable. Signed-off-by: Blaine Gardner <blaine.gardner@ibm.com>
1 parent 66761e6 commit b51ce8a

File tree

3 files changed

+73
-36
lines changed

3 files changed

+73
-36
lines changed

controller/internal/reconciler/bucketclaim.go

Lines changed: 22 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ package reconciler
1818

1919
import (
2020
"context"
21+
"errors"
2122
"fmt"
2223
"time"
2324

@@ -33,6 +34,7 @@ import (
3334
"sigs.k8s.io/controller-runtime/pkg/reconcile"
3435

3536
cosiapi "sigs.k8s.io/container-object-storage-interface/client/apis/objectstorage/v1alpha2"
37+
cosierr "sigs.k8s.io/container-object-storage-interface/internal/errors"
3638
cosipredicate "sigs.k8s.io/container-object-storage-interface/internal/predicate"
3739
)
3840

@@ -63,7 +65,7 @@ func (r *BucketClaimReconciler) Reconcile(ctx context.Context, req ctrl.Request)
6365
return ctrl.Result{}, err
6466
}
6567

66-
retryError, err := r.reconcile(ctx, logger, claim)
68+
err := r.reconcile(ctx, logger, claim)
6769
if err != nil {
6870
// Record any error as a timestamped error in the status.
6971
claim.Status.Error = cosiapi.NewTimestampedError(time.Now(), err.Error())
@@ -74,7 +76,7 @@ func (r *BucketClaimReconciler) Reconcile(ctx context.Context, req ctrl.Request)
7476
return reconcile.Result{}, err
7577
}
7678

77-
if !retryError {
79+
if errors.Is(err, cosierr.NonRetryableError(nil)) {
7880
return reconcile.Result{}, reconcile.TerminalError(err)
7981
}
8082
return reconcile.Result{}, err
@@ -112,31 +114,22 @@ func (r *BucketClaimReconciler) SetupWithManager(mgr ctrl.Manager) error {
112114
Complete(r)
113115
}
114116

115-
// Type definition enforces readability. true/false in code is hard to read/review/maintain.
116-
type retryErrorType bool
117-
118-
const (
119-
RetryError retryErrorType = true // DO retry the error.
120-
DoNotRetryError retryErrorType = false // Do NOT retry the error.
121-
NoError retryErrorType = false // No error, don't retry. For readability.
122-
)
123-
124117
func (r *BucketClaimReconciler) reconcile(
125118
ctx context.Context, logger logr.Logger, claim *cosiapi.BucketClaim,
126-
) (retryErrorType, error) {
119+
) error {
127120
bucketName, err := determineBucketName(claim)
128121
if err != nil {
129122
// Opinion: It is best to not apply a missing finalizer when boundBucketName is degraded
130123
// (err returned here). When degraded, the user needs to delete and re-create the
131124
// BucketClaim to fix the degradation, which requires the finalizer be absent.
132125
logger.Error(err, "failed to determine Bucket name for claim")
133-
return DoNotRetryError, err
126+
return cosierr.NonRetryableError(err)
134127
}
135128

136129
logger = logger.WithValues("bucketName", bucketName)
137130

138131
if claim.Spec.ExistingBucketName != "" {
139-
return DoNotRetryError, fmt.Errorf("static provisioning is not yet supported") // TODO
132+
return cosierr.NonRetryableError(fmt.Errorf("static provisioning is not yet supported")) // TODO
140133
}
141134

142135
if !claim.GetDeletionTimestamp().IsZero() {
@@ -147,10 +140,10 @@ func (r *BucketClaimReconciler) reconcile(
147140
ctrlutil.RemoveFinalizer(claim, cosiapi.ProtectionFinalizer)
148141
if err := r.Update(ctx, claim); err != nil {
149142
logger.Error(err, "failed to remove finalizer")
150-
return RetryError, fmt.Errorf("failed to remove finalizer: %w", err)
143+
return fmt.Errorf("failed to remove finalizer: %w", err)
151144
}
152145

153-
return DoNotRetryError, fmt.Errorf("deletion is not yet implemented") // TODO
146+
return cosierr.NonRetryableError(fmt.Errorf("deletion is not yet implemented")) // TODO
154147
}
155148

156149
logger.V(1).Info("reconciling BucketClaim")
@@ -159,7 +152,7 @@ func (r *BucketClaimReconciler) reconcile(
159152
if didAdd {
160153
if err := r.Update(ctx, claim); err != nil {
161154
logger.Error(err, "failed to add protection finalizer")
162-
return RetryError, fmt.Errorf("failed to add protection finalizer: %w", err)
155+
return fmt.Errorf("failed to add protection finalizer: %w", err)
163156
}
164157
}
165158

@@ -168,7 +161,7 @@ func (r *BucketClaimReconciler) reconcile(
168161
claim.Status.BoundBucketName = bucketName
169162
if err := r.Status().Update(ctx, claim); err != nil {
170163
logger.Error(err, "failed to bind BucketClaim to Bucket")
171-
return RetryError, fmt.Errorf("failed to bind BucketClaim to Bucket: %w", err)
164+
return fmt.Errorf("failed to bind BucketClaim to Bucket: %w", err)
172165
}
173166
}
174167

@@ -180,14 +173,14 @@ func (r *BucketClaimReconciler) reconcile(
180173
if err := r.Get(ctx, bucketNsName, bucket); err != nil {
181174
if !kerrors.IsNotFound(err) {
182175
logger.Error(err, "failed to determine if Bucket exists")
183-
return RetryError, err
176+
return err
184177
}
185178

186179
// TODO: static provisioning: don't do this
187180
logger.Info("creating intermediate Bucket")
188-
_, retryErr, err := createIntermediateBucket(ctx, logger, r.Client, claim, bucketName)
181+
_, err := createIntermediateBucket(ctx, logger, r.Client, claim, bucketName)
189182
if err != nil {
190-
return retryErr, err
183+
return err
191184
}
192185
}
193186

@@ -205,7 +198,7 @@ func (r *BucketClaimReconciler) reconcile(
205198
// 1. Supported protocols
206199
// 2. `ReadyToUse`
207200

208-
return NoError, nil
201+
return nil
209202
}
210203

211204
// Determine the bucket name that should go with the claim. No errors can be retried.
@@ -244,11 +237,11 @@ func createIntermediateBucket(
244237
client client.Client,
245238
claim *cosiapi.BucketClaim,
246239
bucketName string,
247-
) (*cosiapi.Bucket, retryErrorType, error) {
240+
) (*cosiapi.Bucket, error) {
248241
className := claim.Spec.BucketClassName
249242
if className == "" {
250243
logger.Error(nil, "BucketClaim cannot have empty bucketClassName")
251-
return nil, DoNotRetryError, fmt.Errorf("BucketClaim cannot have empty bucketClassName")
244+
return nil, cosierr.NonRetryableError(fmt.Errorf("BucketClaim cannot have empty bucketClassName"))
252245
}
253246

254247
logger = logger.WithValues("bucketClassName", className)
@@ -265,10 +258,10 @@ func createIntermediateBucket(
265258
// BucketClass reconciler that enqueues requests for BucketClaims that reference the
266259
// class and don't yet have a bound Bucket.
267260
logger.Error(err, "BucketClass not found")
268-
return nil, RetryError, err
261+
return nil, err
269262
}
270263
logger.Error(err, "failed to get BucketClass")
271-
return nil, RetryError, err
264+
return nil, err
272265
}
273266

274267
logger.V(1).Info("using BucketClass for intermediate Bucket")
@@ -279,13 +272,13 @@ func createIntermediateBucket(
279272
if kerrors.IsAlreadyExists(err) {
280273
// Unlikely race condition. Error to allow the next reconcile to attempt to recover.
281274
logger.Error(err, "intermediate Bucket already exists")
282-
return nil, RetryError, err
275+
return nil, err
283276
}
284277
logger.Error(err, "failed to create intermediate Bucket")
285-
return nil, RetryError, err
278+
return nil, err
286279
}
287280

288-
return bucket, NoError, nil
281+
return bucket, nil
289282
}
290283

291284
func generateIntermediateBucket(

controller/internal/reconciler/bucketclaim_test.go

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ import (
3333
"sigs.k8s.io/controller-runtime/pkg/reconcile"
3434

3535
cosiapi "sigs.k8s.io/container-object-storage-interface/client/apis/objectstorage/v1alpha2"
36+
cosierr "sigs.k8s.io/container-object-storage-interface/internal/errors"
3637
)
3738

3839
func Test_determineBucketName(t *testing.T) {
@@ -162,7 +163,7 @@ func Test_createIntermediateBucket(t *testing.T) {
162163
claim := baseClaim.DeepCopy()
163164
client := newClient(baseClass.DeepCopy())
164165

165-
bucket, _, err := createIntermediateBucket(ctx, nolog, client, claim, "bc-qwerty")
166+
bucket, err := createIntermediateBucket(ctx, nolog, client, claim, "bc-qwerty")
166167
assert.NoError(t, err)
167168

168169
assert.Empty(t, bucket.Finalizers) // NO finalizers pre-applied
@@ -185,10 +186,10 @@ func Test_createIntermediateBucket(t *testing.T) {
185186
claim := baseClaim.DeepCopy()
186187
client := newClient() // no bucketclass exists
187188

188-
bucket, retryErr, err := createIntermediateBucket(ctx, nolog, client, claim, "bc-qwerty")
189+
bucket, err := createIntermediateBucket(ctx, nolog, client, claim, "bc-qwerty")
189190
assert.Error(t, err)
190191
assert.ErrorContains(t, err, "s3-class") // the class name
191-
assert.Equal(t, RetryError, retryErr)
192+
assert.NotErrorIs(t, err, cosierr.NonRetryableError(nil))
192193
assert.Nil(t, bucket)
193194
})
194195

@@ -197,9 +198,9 @@ func Test_createIntermediateBucket(t *testing.T) {
197198
claim.Spec.BucketClassName = ""
198199
client := newClient(baseClass.DeepCopy())
199200

200-
bucket, retryErr, err := createIntermediateBucket(ctx, nolog, client, claim, "bc-qwerty")
201+
bucket, err := createIntermediateBucket(ctx, nolog, client, claim, "bc-qwerty")
201202
assert.Error(t, err)
202-
assert.Equal(t, DoNotRetryError, retryErr)
203+
assert.ErrorIs(t, err, cosierr.NonRetryableError(nil))
203204
assert.Nil(t, bucket)
204205
})
205206

@@ -213,9 +214,9 @@ func Test_createIntermediateBucket(t *testing.T) {
213214
}
214215
client := newClient(baseClass.DeepCopy(), raceBucket)
215216

216-
bucket, retryErr, err := createIntermediateBucket(ctx, nolog, client, claim, "bc-qwerty")
217+
bucket, err := createIntermediateBucket(ctx, nolog, client, claim, "bc-qwerty")
217218
assert.Error(t, err)
218-
assert.Equal(t, RetryError, retryErr)
219+
assert.NotErrorIs(t, err, cosierr.NonRetryableError(nil))
219220
assert.Nil(t, bucket)
220221
})
221222
}

internal/errors/errors.go

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
/*
2+
Copyright 2025 The Kubernetes 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+
// COSI internal error types.
18+
package errors
19+
20+
import "errors"
21+
22+
// NonRetryableError is an error that should not be retried but still treated as a reconcile error.
23+
func NonRetryableError(wrapped error) error {
24+
return &nonRetryableError{err: wrapped}
25+
}
26+
27+
type nonRetryableError struct {
28+
err error
29+
}
30+
31+
func (te *nonRetryableError) Error() string {
32+
return te.err.Error()
33+
}
34+
35+
// This function will return nil if te.err is nil.
36+
func (te *nonRetryableError) Unwrap() error {
37+
return te.err
38+
}
39+
40+
func (te *nonRetryableError) Is(target error) bool {
41+
tp := &nonRetryableError{}
42+
return errors.As(target, &tp)
43+
}

0 commit comments

Comments
 (0)