Skip to content

Commit 00922cd

Browse files
alacukufgiudici
andauthored
Standardize logging and enhance error handling in hybridgateway controller (#2571)
Signed-off-by: Aldo Lacuku <[email protected]> Co-authored-by: Francesco Giudici <[email protected]>
1 parent 74d5a44 commit 00922cd

File tree

9 files changed

+429
-94
lines changed

9 files changed

+429
-94
lines changed

controller/hybridgateway/controller.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,5 +143,7 @@ func (r *HybridGatewayReconciler[t, tPtr]) Reconcile(ctx context.Context, req ct
143143
return ctrl.Result{}, err
144144
}
145145

146+
log.Debug(logger, "Object reconciliation completed", "Group", gvk.Group, "Kind", gvk.Kind)
147+
146148
return ctrl.Result{}, nil
147149
}

controller/hybridgateway/converter/converter.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ type APIConverter[t RootObject] interface {
2323
// GetRootObject returns the current root object of type t.
2424
GetRootObject() t
2525
// GetOutputStore returns a slice of unstructured.Unstructured objects representing the current state of the store, using the provided context.
26-
GetOutputStore(ctx context.Context) []unstructured.Unstructured
26+
GetOutputStore(ctx context.Context, logger logr.Logger) ([]unstructured.Unstructured, error)
2727
// UpdateRootObjectStatus updates the status for the root object.
2828
UpdateRootObjectStatus(ctx context.Context, logger logr.Logger) (bool, error)
2929
}

controller/hybridgateway/converter/http_route.go

Lines changed: 259 additions & 35 deletions
Large diffs are not rendered by default.

controller/hybridgateway/converter/http_route_test.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,8 @@ func TestHostnamesIntersection(t *testing.T) {
109109
err := converter.Translate(t.Context(), logr.Discard())
110110
require.NoError(t, err)
111111

112-
output := converter.GetOutputStore(context.TODO())
112+
output, err := converter.GetOutputStore(context.TODO(), logr.Discard())
113+
require.NoError(t, err)
113114

114115
// Extract KongRoute objects from the output
115116
var kongRoutes []*configurationv1alpha1.KongRoute

controller/hybridgateway/reconciler_utils.go

Lines changed: 115 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313
"github.com/kong/kong-operator/controller/hybridgateway/managedfields"
1414
"github.com/kong/kong-operator/controller/hybridgateway/metadata"
1515
"github.com/kong/kong-operator/controller/hybridgateway/utils"
16+
"github.com/kong/kong-operator/controller/pkg/log"
1617
)
1718

1819
const (
@@ -30,15 +31,52 @@ func Translate[t converter.RootObject](conv converter.APIConverter[t], ctx conte
3031
// structured merge. The function returns requeue and stop flags to control reconciliation flow, and an error
3132
// for any unrecoverable or transient issues. Resources marked for deletion are skipped. Conflict errors
3233
// trigger a requeue for optimistic concurrency. All other errors are wrapped with resource kind and name for context.
34+
//
35+
// The function performs the following operations:
36+
// 1. Retrieves the desired state from the converter's output store
37+
// 2. For each desired resource, checks if it exists in the cluster
38+
// 3. Creates new resources using server-side apply if they don't exist
39+
// 4. Skips resources that are marked for deletion
40+
// 5. Updates existing resources if changes are detected using managed fields comparison
41+
// 6. Handles conflicts by returning requeue=true for optimistic concurrency
42+
//
43+
// Parameters:
44+
// - ctx: The context for API calls and cancellation
45+
// - cl: The Kubernetes client for CRUD operations
46+
// - logger: Logger for structured logging with state-enforcement phase
47+
// - conv: The APIConverter that provides the desired state
48+
//
49+
// Returns:
50+
// - requeue: true if the reconciliation should be retried due to conflicts
51+
// - stop: true if reconciliation should stop (currently always false)
52+
// - err: Any error that occurred during state enforcement
53+
//
54+
// The function uses server-side apply with the "gateway-operator" field manager to ensure
55+
// proper ownership and conflict resolution when multiple controllers manage the same resources.
3356
func EnforceState[t converter.RootObject](ctx context.Context, cl client.Client, logger logr.Logger, conv converter.APIConverter[t]) (requeue bool, stop bool, err error) {
57+
logger = logger.WithValues("phase", "state-enforcement")
58+
log.Debug(logger, "Starting state enforcement")
59+
3460
// Get the desired state from the converter.
35-
desiredObjects := conv.GetOutputStore(ctx)
61+
desiredObjects, err := conv.GetOutputStore(ctx, logger)
62+
if err != nil {
63+
return false, false, fmt.Errorf("failed to get desired objects from converter: %w", err)
64+
}
3665
if len(desiredObjects) == 0 {
37-
logger.V(1).Info("No desired objects to enforce")
66+
log.Debug(logger, "No desired objects to enforce")
3867
return false, false, nil
3968
}
4069

41-
for _, desired := range desiredObjects {
70+
log.Debug(logger, "Retrieved desired objects for enforcement", "objectCount", len(desiredObjects))
71+
72+
var (
73+
objectsCreated = 0
74+
objectsUpdated = 0
75+
objectsSkipped = 0
76+
)
77+
78+
for i, desired := range desiredObjects {
79+
log.Debug(logger, "Processing desired object", "index", i, "kind", desired.GetKind(), "name", desired.GetName())
4280
// Get the existing object by name from the API server.
4381
existing := &unstructured.Unstructured{}
4482
existing.SetGroupVersionKind(desired.GetObjectKind().GroupVersionKind())
@@ -54,7 +92,7 @@ func EnforceState[t converter.RootObject](ctx context.Context, cl client.Client,
5492
if err != nil {
5593
if errors.IsNotFound(err) {
5694
// Object doesn't exist, create it using server-side apply.
57-
logger.V(1).Info("Creating new object", "kind", desired.GetKind(), "obj", namespacedNameDesired)
95+
log.Debug(logger, "Creating new object", "kind", desired.GetKind(), "obj", namespacedNameDesired)
5896

5997
// Set field manager for server-side apply
6098
if err := cl.Patch(ctx, &desired, client.Apply, client.FieldOwner(FieldManager), client.ForceOwnership); err != nil {
@@ -63,6 +101,8 @@ func EnforceState[t converter.RootObject](ctx context.Context, cl client.Client,
63101
}
64102
return true, false, fmt.Errorf("failed to create object kind %s obj %s: %w", desired.GetKind(), namespacedNameDesired, err)
65103
}
104+
objectsCreated++
105+
log.Debug(logger, "Successfully created object", "kind", desired.GetKind(), "obj", namespacedNameDesired)
66106
continue
67107
} else {
68108
// Other error getting the object.
@@ -72,7 +112,8 @@ func EnforceState[t converter.RootObject](ctx context.Context, cl client.Client,
72112

73113
// Handle the case when resource are marked for deletion.
74114
if !existing.GetDeletionTimestamp().IsZero() {
75-
logger.V(1).Info("Existing object is marked for deletion, will not enforce state", "kind", existing.GetKind(), "obj", namespacedNameDesired)
115+
log.Debug(logger, "Existing object is marked for deletion, will not enforce state", "kind", existing.GetKind(), "obj", namespacedNameDesired)
116+
objectsSkipped++
76117
continue
77118
}
78119

@@ -83,13 +124,15 @@ func EnforceState[t converter.RootObject](ctx context.Context, cl client.Client,
83124
}
84125
if managedFieldsObj == nil {
85126
// No managed fields for our field manager, we should update.
86-
logger.V(1).Info("No managed fields found for our field manager, will apply desired state", "kind", existing.GetKind(), "obj", namespacedNameExisting)
127+
log.Debug(logger, "No managed fields found for our field manager, will apply desired state", "kind", existing.GetKind(), "obj", namespacedNameExisting)
87128
if err := cl.Patch(ctx, &desired, client.Apply, client.FieldOwner(FieldManager), client.ForceOwnership); err != nil {
88129
if errors.IsConflict(err) {
89130
return true, false, fmt.Errorf("conflict during create of object kind %s obj %s: %w", desired.GetKind(), namespacedNameDesired, err)
90131
}
91132
return true, false, fmt.Errorf("failed to create object kind %s obj %s: %w", desired.GetKind(), namespacedNameDesired, err)
92133
}
134+
objectsUpdated++
135+
log.Debug(logger, "Successfully applied desired state (no managed fields)", "kind", existing.GetKind(), "obj", namespacedNameExisting)
93136
continue
94137
}
95138

@@ -106,19 +149,27 @@ func EnforceState[t converter.RootObject](ctx context.Context, cl client.Client,
106149
}
107150

108151
if compare.IsSame() {
109-
logger.V(3).Info("No changes detected for obj", "kind", existing.GetKind(), "obj", namespacedNameExisting)
152+
log.Trace(logger, "No changes detected for obj", "kind", existing.GetKind(), "obj", namespacedNameExisting)
110153
} else {
111-
logger.Info("Changes detected for obj, applying desired state", "kind", existing.GetKind(), "obj", namespacedNameExisting, "changes", compare.String())
154+
log.Info(logger, "Changes detected for obj, applying desired state", "kind", existing.GetKind(), "obj", namespacedNameExisting, "changes", compare.String())
112155
// Changes detected, apply the desired state using server-side apply.
113156
if err := cl.Patch(ctx, &desired, client.Apply, client.FieldOwner(FieldManager), client.ForceOwnership); err != nil {
114157
if errors.IsConflict(err) {
115158
return true, false, fmt.Errorf("conflict during create of object kind %s obj %s: %w", desired.GetKind(), namespacedNameDesired, err)
116159
}
117160
return true, false, fmt.Errorf("failed to update object kind %s obj %s: %w", desired.GetKind(), namespacedNameDesired, err)
118161
}
162+
objectsUpdated++
163+
log.Debug(logger, "Successfully applied changes to object", "kind", existing.GetKind(), "obj", namespacedNameExisting)
119164
}
120165
}
121166

167+
log.Debug(logger, "Finished state enforcement",
168+
"totalObjects", len(desiredObjects),
169+
"created", objectsCreated,
170+
"updated", objectsUpdated,
171+
"skipped", objectsSkipped)
172+
122173
return false, false, nil
123174
}
124175

@@ -143,11 +194,44 @@ func EnforceStatus[t converter.RootObject](ctx context.Context, logger logr.Logg
143194
}
144195

145196
// CleanOrphanedResources deletes resources previously managed by the converter but no longer present in the desired output.
197+
//
198+
// The function performs the following operations:
199+
// 1. Retrieves the current desired state from the converter's output store
200+
// 2. Builds a set of desired resource keys for quick lookup
201+
// 3. For each expected GroupVersionKind, lists existing resources owned by the root object
202+
// 4. Compares existing resources against the desired set and deletes orphans
203+
// 5. Handles deletion errors gracefully, ignoring NotFound errors
204+
//
205+
// This cleanup process ensures that resources that were previously created by the converter
206+
// but are no longer needed (due to configuration changes) are properly removed from the cluster.
207+
//
208+
// Parameters:
209+
// - ctx: The context for API calls and cancellation
210+
// - cl: The Kubernetes client for listing and deleting resources
211+
// - logger: Logger for debugging and status information
212+
// - conv: The APIConverter that manages the root object and its desired state
213+
//
214+
// Returns:
215+
// - error: Any error that occurred during the cleanup process
216+
//
217+
// The function uses ownership labels to identify resources managed by the root object
218+
// and only deletes resources that are no longer present in the converter's desired output.
146219
func CleanOrphanedResources[t converter.RootObject, tPtr converter.RootObjectPtr[t]](ctx context.Context, cl client.Client, logger logr.Logger, conv converter.APIConverter[t]) error {
147-
desiredObjects := conv.GetOutputStore(ctx)
220+
logger = logger.WithValues("phase", "orphan-cleanup")
221+
log.Debug(logger, "Starting orphaned resource cleanup")
222+
223+
desiredObjects, err := conv.GetOutputStore(ctx, logger)
224+
if err != nil {
225+
return fmt.Errorf("failed to get desired objects from converter for cleanup: %w", err)
226+
}
227+
148228
desiredSet := make(map[string]struct{})
149229
expectedGVKs := conv.GetExpectedGVKs()
150230

231+
log.Debug(logger, "Retrieved desired objects and expected GVKs",
232+
"desiredObjectCount", len(desiredObjects),
233+
"expectedGVKCount", len(expectedGVKs))
234+
151235
// Extract the root object for label selector.
152236
rootObj := conv.GetRootObject()
153237
var rootObjPtr tPtr
@@ -159,13 +243,19 @@ func CleanOrphanedResources[t converter.RootObject, tPtr converter.RootObjectPtr
159243
}
160244

161245
// Build a set of desired resource keys.
246+
log.Debug(logger, "Building desired resource key set")
162247
for _, obj := range desiredObjects {
163248
key := fmt.Sprintf("%s/%s/%s", obj.GetNamespace(), obj.GetName(), obj.GetObjectKind().GroupVersionKind().String())
164249
desiredSet[key] = struct{}{}
250+
log.Trace(logger, "Added desired resource key", "key", key, "kind", obj.GetKind(), "name", obj.GetName())
165251
}
252+
log.Debug(logger, "Finished building desired resource key set", "totalKeys", len(desiredSet))
166253

167254
// For each expected GVK, list resources and delete orphans.
255+
totalOrphansDeleted := 0
168256
for _, gvk := range expectedGVKs {
257+
log.Debug(logger, "Processing GVK for orphan cleanup", "gvk", gvk.String())
258+
169259
list := &unstructured.UnstructuredList{}
170260
list.SetGroupVersionKind(gvk)
171261
selector := metadata.LabelSelectorForOwnedResources(rootObjPtr, nil)
@@ -177,17 +267,32 @@ func CleanOrphanedResources[t converter.RootObject, tPtr converter.RootObjectPtr
177267
return fmt.Errorf("unable to list objects with gvk %s in namespace %s: %w", gvk.String(), ns, err)
178268
}
179269

270+
log.Debug(logger, "Found existing resources for GVK", "gvk", gvk.String(), "resourceCount", len(list.Items))
271+
272+
orphansForGVK := 0
180273
for _, item := range list.Items {
181274
key := fmt.Sprintf("%s/%s/%s", item.GetNamespace(), item.GetName(), gvk.String())
182275
if _, found := desiredSet[key]; !found {
183276
// Not in desired output, delete it.
184-
logger.Info("Deleting orphaned resource", "kind", item.GetKind(), "obj", client.ObjectKeyFromObject(&item))
277+
log.Info(logger, "Deleting orphaned resource", "kind", item.GetKind(), "obj", client.ObjectKeyFromObject(&item))
185278
if err := cl.Delete(ctx, &item); err != nil && !errors.IsNotFound(err) {
186279
return fmt.Errorf("failed to delete orphaned resource kind %s obj %s: %w", item.GetKind(), client.ObjectKeyFromObject(&item), err)
187280
}
281+
orphansForGVK++
282+
totalOrphansDeleted++
283+
} else {
284+
log.Trace(logger, "Resource still desired, keeping", "kind", item.GetKind(), "obj", client.ObjectKeyFromObject(&item))
188285
}
189286
}
287+
288+
if orphansForGVK > 0 {
289+
log.Debug(logger, "Deleted orphaned resources for GVK", "gvk", gvk.String(), "orphansDeleted", orphansForGVK)
290+
} else {
291+
log.Debug(logger, "No orphaned resources found for GVK", "gvk", gvk.String())
292+
}
190293
}
294+
295+
log.Debug(logger, "Finished orphaned resource cleanup", "totalOrphansDeleted", totalOrphansDeleted)
191296
return nil
192297
}
193298

controller/hybridgateway/reconciler_utils_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -266,8 +266,8 @@ type fakeHTTPRouteConverter struct {
266266
root gwtypes.HTTPRoute
267267
}
268268

269-
func (f *fakeHTTPRouteConverter) GetOutputStore(ctx context.Context) []unstructured.Unstructured {
270-
return f.desired
269+
func (f *fakeHTTPRouteConverter) GetOutputStore(ctx context.Context, logger logr.Logger) ([]unstructured.Unstructured, error) {
270+
return f.desired, nil
271271
}
272272
func (f *fakeHTTPRouteConverter) GetExpectedGVKs() []schema.GroupVersionKind { return f.gvks }
273273
func (f *fakeHTTPRouteConverter) GetRootObject() gwtypes.HTTPRoute { return f.root }

controller/hybridgateway/refs/get.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"sigs.k8s.io/controller-runtime/pkg/client"
1010

1111
hybridgatewayerrors "github.com/kong/kong-operator/controller/hybridgateway/errors"
12+
"github.com/kong/kong-operator/controller/pkg/log"
1213
gwtypes "github.com/kong/kong-operator/internal/types"
1314
"github.com/kong/kong-operator/pkg/vars"
1415
)
@@ -71,13 +72,13 @@ func GetSupportedGatewayForParentRef(ctx context.Context, logger logr.Logger, cl
7172
routeNamespace string) (*gwtypes.Gateway, error) {
7273
// Only support Gateway kind.
7374
if pRef.Kind != nil && *pRef.Kind != "Gateway" {
74-
logger.V(1).Info("Ignoring ParentRef, unsupported kind", "pRef", pRef, "kind", *pRef.Kind)
75+
log.Debug(logger, "Ignoring ParentRef, unsupported kind", "pRef", pRef, "kind", *pRef.Kind)
7576
return nil, nil
7677
}
7778

7879
// Only support gateway.networking.k8s.io group (or empty group which defaults to this).
7980
if pRef.Group != nil && *pRef.Group != "gateway.networking.k8s.io" {
80-
logger.V(1).Info("Ignoring ParentRef, unsupported group", "pRef", pRef, "group", *pRef.Group)
81+
log.Debug(logger, "Ignoring ParentRef, unsupported group", "pRef", pRef, "group", *pRef.Group)
8182
return nil, nil
8283
}
8384

0 commit comments

Comments
 (0)