@@ -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
1819const (
@@ -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.
3356func 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.
146219func 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
0 commit comments