Skip to content

Commit 3e8dbc2

Browse files
author
Anand Kumar
committed
feat: add pkg/controller/ctrlutil package
Add controller utility package with client, constants, errors, reconcile_result, utils and fakes. Made-with: Cursor
1 parent 9d55129 commit 3e8dbc2

6 files changed

Lines changed: 1137 additions & 0 deletions

File tree

pkg/controller/ctrlutil/client.go

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
package ctrlutil
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"reflect"
7+
8+
"k8s.io/apimachinery/pkg/api/errors"
9+
"k8s.io/client-go/util/retry"
10+
11+
"sigs.k8s.io/controller-runtime/pkg/client"
12+
"sigs.k8s.io/controller-runtime/pkg/manager"
13+
)
14+
15+
var errFailedToConvertToClientObject = fmt.Errorf("failed to convert to client.Object")
16+
17+
// ctrlClientImpl implements the CtrlClient interface using the manager's client.
18+
type ctrlClientImpl struct {
19+
client.Client
20+
}
21+
22+
// CtrlClient defines the interface for controller client operations.
23+
//
24+
//go:generate go run github.com/maxbrunsfeld/counterfeiter/v6 -generate
25+
//counterfeiter:generate -o fakes . CtrlClient
26+
type CtrlClient interface {
27+
Get(context.Context, client.ObjectKey, client.Object) error
28+
List(context.Context, client.ObjectList, ...client.ListOption) error
29+
StatusUpdate(context.Context, client.Object, ...client.SubResourceUpdateOption) error
30+
Update(context.Context, client.Object, ...client.UpdateOption) error
31+
UpdateWithRetry(context.Context, client.Object, ...client.UpdateOption) error
32+
Create(context.Context, client.Object, ...client.CreateOption) error
33+
Delete(context.Context, client.Object, ...client.DeleteOption) error
34+
Patch(context.Context, client.Object, client.Patch, ...client.PatchOption) error
35+
Exists(context.Context, client.ObjectKey, client.Object) (bool, error)
36+
}
37+
38+
// NewClient creates a new controller client from the manager.
39+
// Use the manager's client directly instead of creating a custom client.
40+
// The manager's client uses the manager's cache, which ensures the reconciler
41+
// reads from the same cache that the controller's watches use, preventing
42+
// cache mismatch issues.
43+
func NewClient(m manager.Manager) (CtrlClient, error) {
44+
return &ctrlClientImpl{
45+
Client: m.GetClient(),
46+
}, nil
47+
}
48+
49+
func (c *ctrlClientImpl) Get(
50+
ctx context.Context, key client.ObjectKey, obj client.Object,
51+
) error {
52+
if err := c.Client.Get(ctx, key, obj); err != nil {
53+
return fmt.Errorf("failed to get object %q: %w", key, err)
54+
}
55+
return nil
56+
}
57+
58+
func (c *ctrlClientImpl) List(
59+
ctx context.Context, list client.ObjectList, opts ...client.ListOption,
60+
) error {
61+
if err := c.Client.List(ctx, list, opts...); err != nil {
62+
return fmt.Errorf("failed to list objects: %w", err)
63+
}
64+
return nil
65+
}
66+
67+
func (c *ctrlClientImpl) Create(
68+
ctx context.Context, obj client.Object, opts ...client.CreateOption,
69+
) error {
70+
key := client.ObjectKeyFromObject(obj)
71+
if err := c.Client.Create(ctx, obj, opts...); err != nil {
72+
return fmt.Errorf("failed to create object %q: %w", key, err)
73+
}
74+
return nil
75+
}
76+
77+
func (c *ctrlClientImpl) Delete(
78+
ctx context.Context, obj client.Object, opts ...client.DeleteOption,
79+
) error {
80+
key := client.ObjectKeyFromObject(obj)
81+
if err := c.Client.Delete(ctx, obj, opts...); err != nil {
82+
return fmt.Errorf("failed to delete object %q: %w", key, err)
83+
}
84+
return nil
85+
}
86+
87+
func (c *ctrlClientImpl) Update(
88+
ctx context.Context, obj client.Object, opts ...client.UpdateOption,
89+
) error {
90+
key := client.ObjectKeyFromObject(obj)
91+
if err := c.Client.Update(ctx, obj, opts...); err != nil {
92+
return fmt.Errorf("failed to update object %q: %w", key, err)
93+
}
94+
return nil
95+
}
96+
97+
func (c *ctrlClientImpl) UpdateWithRetry(
98+
ctx context.Context, obj client.Object, opts ...client.UpdateOption,
99+
) error {
100+
key := client.ObjectKeyFromObject(obj)
101+
if err := retry.RetryOnConflict(retry.DefaultRetry, func() error {
102+
currentInterface := reflect.New(reflect.TypeOf(obj).Elem()).Interface()
103+
current, ok := currentInterface.(client.Object)
104+
if !ok {
105+
return errFailedToConvertToClientObject
106+
}
107+
if err := c.Client.Get(ctx, key, current); err != nil {
108+
return fmt.Errorf("failed to fetch latest %q for update: %w", key, err)
109+
}
110+
obj.SetResourceVersion(current.GetResourceVersion())
111+
if err := c.Client.Update(ctx, obj, opts...); err != nil {
112+
return fmt.Errorf("failed to update %q resource: %w", key, err)
113+
}
114+
return nil
115+
}); err != nil {
116+
return err
117+
}
118+
119+
return nil
120+
}
121+
122+
func (c *ctrlClientImpl) StatusUpdate(
123+
ctx context.Context, obj client.Object, opts ...client.SubResourceUpdateOption,
124+
) error {
125+
key := client.ObjectKeyFromObject(obj)
126+
if err := c.Client.Status().Update(ctx, obj, opts...); err != nil {
127+
return fmt.Errorf("failed to update status for object %q: %w", key, err)
128+
}
129+
return nil
130+
}
131+
132+
func (c *ctrlClientImpl) Patch(
133+
ctx context.Context, obj client.Object, patch client.Patch, opts ...client.PatchOption,
134+
) error {
135+
key := client.ObjectKeyFromObject(obj)
136+
if err := c.Client.Patch(ctx, obj, patch, opts...); err != nil {
137+
return fmt.Errorf("failed to patch object %q: %w", key, err)
138+
}
139+
return nil
140+
}
141+
142+
func (c *ctrlClientImpl) Exists(ctx context.Context, key client.ObjectKey, obj client.Object) (bool, error) {
143+
if err := c.Client.Get(ctx, key, obj); err != nil {
144+
if errors.IsNotFound(err) {
145+
return false, nil
146+
}
147+
return false, err
148+
}
149+
return true, nil
150+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
package ctrlutil
2+
3+
// ManagedResourceLabelKey is the common label key used by all operand controllers
4+
// to identify resources they manage. Each controller uses a different value
5+
// to distinguish its resources.
6+
const ManagedResourceLabelKey = "app"

pkg/controller/ctrlutil/errors.go

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
package ctrlutil
2+
3+
import (
4+
"errors"
5+
"fmt"
6+
7+
apierrors "k8s.io/apimachinery/pkg/api/errors"
8+
)
9+
10+
// ErrorReason represents the reason for a reconciliation error.
11+
type ErrorReason string
12+
13+
const (
14+
// IrrecoverableError indicates an error that cannot be recovered by retrying.
15+
IrrecoverableError ErrorReason = "IrrecoverableError"
16+
17+
// RetryRequiredError indicates an error that may be recovered by retrying.
18+
RetryRequiredError ErrorReason = "RetryRequiredError"
19+
20+
// MultipleInstanceError indicates that multiple singleton instances exist.
21+
MultipleInstanceError ErrorReason = "MultipleInstanceError"
22+
)
23+
24+
// ReconcileError represents an error that occurred during reconciliation.
25+
type ReconcileError struct {
26+
Reason ErrorReason `json:"reason,omitempty"`
27+
Message string `json:"message,omitempty"`
28+
Err error `json:"error,omitempty"`
29+
}
30+
31+
var _ error = &ReconcileError{}
32+
33+
// NewIrrecoverableError creates a new irrecoverable error.
34+
func NewIrrecoverableError(err error, message string, args ...any) *ReconcileError {
35+
if err == nil {
36+
return nil
37+
}
38+
return &ReconcileError{
39+
Reason: IrrecoverableError,
40+
Message: fmt.Sprintf(message, args...),
41+
Err: err,
42+
}
43+
}
44+
45+
// NewMultipleInstanceError creates a new multiple instance error.
46+
func NewMultipleInstanceError(err error) *ReconcileError {
47+
if err == nil {
48+
return nil
49+
}
50+
return &ReconcileError{
51+
Reason: MultipleInstanceError,
52+
Message: fmt.Sprint(err.Error()),
53+
Err: err,
54+
}
55+
}
56+
57+
// NewRetryRequiredError creates a new error that requires retry.
58+
func NewRetryRequiredError(err error, message string, args ...any) *ReconcileError {
59+
if err == nil {
60+
return nil
61+
}
62+
return &ReconcileError{
63+
Reason: RetryRequiredError,
64+
Message: fmt.Sprintf(message, args...),
65+
Err: err,
66+
}
67+
}
68+
69+
// FromClientError creates a ReconcileError from a Kubernetes client error.
70+
func FromClientError(err error, message string, args ...any) *ReconcileError {
71+
if err == nil {
72+
return nil
73+
}
74+
if apierrors.IsUnauthorized(err) || apierrors.IsForbidden(err) || apierrors.IsInvalid(err) ||
75+
apierrors.IsBadRequest(err) || apierrors.IsServiceUnavailable(err) {
76+
return NewIrrecoverableError(err, message, args...)
77+
}
78+
79+
return NewRetryRequiredError(err, message, args...)
80+
}
81+
82+
// FromError creates a ReconcileError from a generic error.
83+
func FromError(err error, message string, args ...any) *ReconcileError {
84+
if err == nil {
85+
return nil
86+
}
87+
if IsIrrecoverableError(err) {
88+
return NewIrrecoverableError(err, message, args...)
89+
}
90+
return NewRetryRequiredError(err, message, args...)
91+
}
92+
93+
// IsIrrecoverableError checks if the error is an irrecoverable error.
94+
func IsIrrecoverableError(err error) bool {
95+
rerr := &ReconcileError{}
96+
if errors.As(err, &rerr) {
97+
return rerr.Reason == IrrecoverableError
98+
}
99+
return false
100+
}
101+
102+
// IsRetryRequiredError checks if the error requires retry.
103+
func IsRetryRequiredError(err error) bool {
104+
rerr := &ReconcileError{}
105+
if errors.As(err, &rerr) {
106+
return rerr.Reason == RetryRequiredError
107+
}
108+
return false
109+
}
110+
111+
// IsMultipleInstanceError checks if the error is a multiple instance error.
112+
func IsMultipleInstanceError(err error) bool {
113+
rerr := &ReconcileError{}
114+
if errors.As(err, &rerr) {
115+
return rerr.Reason == MultipleInstanceError
116+
}
117+
return false
118+
}
119+
120+
// Error implements the error interface.
121+
func (e *ReconcileError) Error() string {
122+
return fmt.Sprintf("%s: %s", e.Message, e.Err)
123+
}

0 commit comments

Comments
 (0)