Skip to content

Commit 74f9038

Browse files
authored
feat(hybridgateway): rework generated resources lifecycle (#2656)
Signed-off-by: Francesco Giudici <[email protected]>
1 parent d76b029 commit 74f9038

27 files changed

+2632
-233
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,10 @@
135135
[GEP-1867](https://gateway-api.sigs.k8s.io/geps/gep-1867/) via
136136
`GatewayConfiguration` CRD.
137137
[#2653](https://github.com/Kong/kong-operator/pull/2653)
138+
- HybridGateway: reworked generated resources lifecycle management. HTTPRoute ownership on the resources
139+
is now tracked through the `gateway-operator.konghq.com/hybrid-routes` annotation. The same generated
140+
resource can now be shared among different HTTPRoutes.
141+
[#2656](https://github.com/Kong/kong-operator/pull/2656)
138142

139143
### Changed
140144

controller/hybridgateway/const/route/common.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,6 @@ package route
22

33
// HTTPRouteKey identifies HTTPRoute resources.
44
const HTTPRouteKey = "HTTPRoute"
5+
6+
// RouteFinalizer is the finalizer added to Route objects to manage cleanup of generated resources.
7+
const RouteFinalizer = "gateway-operator.konghq.com/route-cleanup"

controller/hybridgateway/controller.go

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,13 @@ import (
44
"context"
55
"fmt"
66

7+
"github.com/go-logr/logr"
78
corev1 "k8s.io/api/core/v1"
89
k8serrors "k8s.io/apimachinery/pkg/api/errors"
910
"k8s.io/client-go/tools/record"
1011
ctrl "sigs.k8s.io/controller-runtime"
1112
"sigs.k8s.io/controller-runtime/pkg/client"
13+
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
1214
"sigs.k8s.io/controller-runtime/pkg/event"
1315
"sigs.k8s.io/controller-runtime/pkg/handler"
1416
ctrllog "sigs.k8s.io/controller-runtime/pkg/log"
@@ -17,6 +19,7 @@ import (
1719
routeconst "github.com/kong/kong-operator/controller/hybridgateway/const/route"
1820
"github.com/kong/kong-operator/controller/hybridgateway/converter"
1921
"github.com/kong/kong-operator/controller/hybridgateway/watch"
22+
"github.com/kong/kong-operator/controller/pkg/finalizer"
2023
"github.com/kong/kong-operator/controller/pkg/log"
2124
)
2225

@@ -129,6 +132,23 @@ func (r *HybridGatewayReconciler[t, tPtr]) Reconcile(ctx context.Context, req ct
129132
gvk := obj.GetObjectKind().GroupVersionKind()
130133
log.Debug(logger, "Reconciling object", "Group", gvk.Group, "Kind", gvk.Kind)
131134

135+
// Handle deletion and finalizer cleanup
136+
if obj.GetDeletionTimestamp() != nil {
137+
return r.handleDeletion(ctx, logger, obj, rootObj)
138+
}
139+
140+
// Ensure finalizer is present
141+
if !controllerutil.ContainsFinalizer(obj, routeconst.RouteFinalizer) {
142+
log.Debug(logger, "Adding finalizer")
143+
old := obj.DeepCopyObject().(tPtr)
144+
controllerutil.AddFinalizer(obj, routeconst.RouteFinalizer)
145+
if err := r.Patch(ctx, obj, client.MergeFrom(old)); err != nil {
146+
log.Error(logger, err, "Failed to add finalizer")
147+
return finalizer.HandlePatchOrUpdateError(err, logger)
148+
}
149+
return ctrl.Result{Requeue: true}, nil
150+
}
151+
132152
conv, err := converter.NewConverter(rootObj, r.Client, r.referenceGrantEnabled, r.fqdnMode, r.clusterDomain)
133153
if err != nil {
134154
return ctrl.Result{}, err
@@ -234,3 +254,60 @@ func (r *HybridGatewayReconciler[t, tPtr]) Reconcile(ctx context.Context, req ct
234254

235255
return ctrl.Result{}, nil
236256
}
257+
258+
// handleDeletion handles the deletion of a Route object by cleaning up generated resources
259+
// and removing the finalizer. This ensures that all Kong resources generated from the Route
260+
// are properly cleaned up before the Route is deleted from the cluster.
261+
func (r *HybridGatewayReconciler[t, tPtr]) handleDeletion(ctx context.Context, logger logr.Logger, obj tPtr, rootObj t) (ctrl.Result, error) {
262+
log.Debug(logger, "Handling Route deletion")
263+
264+
// Create converter to get the cleanup logic
265+
conv, err := converter.NewConverter(rootObj, r.Client, r.referenceGrantEnabled, r.fqdnMode, r.clusterDomain)
266+
if err != nil {
267+
return ctrl.Result{}, fmt.Errorf("failed to create converter for cleanup: %w", err)
268+
}
269+
270+
// Clean up all generated resources by calling the same cleanup logic as orphan cleanup
271+
// but with no desired resources (simulating what cleanOrphanedResources does when desiredObjects is empty)
272+
orphansDeleted, err := r.cleanupGeneratedResources(ctx, logger, conv)
273+
if err != nil {
274+
// Record cleanup failure event
275+
r.eventRecorder.Event(
276+
obj,
277+
corev1.EventTypeWarning,
278+
routeconst.EventReasonOrphanCleanupFailed,
279+
fmt.Sprintf("Route deletion cleanup failed: %v", err),
280+
)
281+
return ctrl.Result{}, fmt.Errorf("failed to cleanup generated resources: %w", err)
282+
}
283+
284+
// Record successful cleanup event if resources were deleted
285+
if orphansDeleted {
286+
r.eventRecorder.Event(
287+
obj,
288+
corev1.EventTypeNormal,
289+
routeconst.EventReasonOrphanCleanupSucceeded,
290+
"Route deletion cleanup completed successfully",
291+
)
292+
}
293+
294+
// Remove finalizer using patch for safer concurrent updates
295+
old := obj.DeepCopyObject().(tPtr)
296+
if controllerutil.RemoveFinalizer(obj, routeconst.RouteFinalizer) {
297+
if err := r.Patch(ctx, obj, client.MergeFrom(old)); err != nil {
298+
return finalizer.HandlePatchOrUpdateError(err, logger)
299+
}
300+
}
301+
302+
log.Debug(logger, "Route deletion completed successfully")
303+
return ctrl.Result{}, nil
304+
}
305+
306+
// cleanupGeneratedResources deletes all resources generated by the converter.
307+
// This is similar to cleanOrphanedResources but treats all owned resources as orphans
308+
// since we want to delete everything when the Route is being deleted.
309+
func (r *HybridGatewayReconciler[t, tPtr]) cleanupGeneratedResources(ctx context.Context, logger logr.Logger, conv converter.APIConverter[t]) (bool, error) {
310+
// Use the existing cleanup logic but with an empty desired set,
311+
// which will cause all owned resources to be considered orphans and deleted
312+
return cleanOrphanedResources[t, tPtr](ctx, r.Client, logger, conv)
313+
}

controller/hybridgateway/converter/http_route.go

Lines changed: 53 additions & 112 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,15 @@ import (
1414
commonv1alpha1 "github.com/kong/kong-operator/api/common/v1alpha1"
1515
configurationv1 "github.com/kong/kong-operator/api/configuration/v1"
1616
configurationv1alpha1 "github.com/kong/kong-operator/api/configuration/v1alpha1"
17-
"github.com/kong/kong-operator/controller/hybridgateway/builder"
1817
hybridgatewayerrors "github.com/kong/kong-operator/controller/hybridgateway/errors"
19-
"github.com/kong/kong-operator/controller/hybridgateway/metadata"
20-
"github.com/kong/kong-operator/controller/hybridgateway/namegen"
18+
"github.com/kong/kong-operator/controller/hybridgateway/kongroute"
19+
"github.com/kong/kong-operator/controller/hybridgateway/plugin"
20+
"github.com/kong/kong-operator/controller/hybridgateway/pluginbinding"
2121
"github.com/kong/kong-operator/controller/hybridgateway/refs"
2222
"github.com/kong/kong-operator/controller/hybridgateway/route"
23+
"github.com/kong/kong-operator/controller/hybridgateway/service"
2324
"github.com/kong/kong-operator/controller/hybridgateway/target"
25+
"github.com/kong/kong-operator/controller/hybridgateway/upstream"
2426
"github.com/kong/kong-operator/controller/hybridgateway/utils"
2527
"github.com/kong/kong-operator/controller/pkg/log"
2628
gwtypes "github.com/kong/kong-operator/internal/types"
@@ -334,31 +336,19 @@ func (c *httpRouteConverter) translate(ctx context.Context, logger logr.Logger)
334336
"filterCount", len(rule.Filters))
335337

336338
// Build the KongUpstream resource.
337-
upstreamName := namegen.NewKongUpstreamName(cp, rule)
338-
log.Trace(logger, "Building KongUpstream resource",
339-
"upstream", upstreamName,
340-
"controlPlane", cp.KonnectNamespacedRef)
341-
342-
upstream, err := builder.NewKongUpstream().
343-
WithName(upstreamName).
344-
WithNamespace(c.route.Namespace).
345-
WithLabels(c.route, &pRef).
346-
WithAnnotations(c.route, &pRef).
347-
WithSpecName(upstreamName).
348-
WithControlPlaneRef(*cp).
349-
WithOwner(c.route).Build()
339+
upstreamPtr, err := upstream.UpstreamForRule(ctx, logger, c.Client, c.route, rule, &pRef, cp)
350340
if err != nil {
351-
log.Error(logger, err, "Failed to build KongUpstream resource, skipping rule",
352-
"upstream", upstreamName,
341+
log.Error(logger, err, "Failed to translate KongUpstream resource for rule, skipping rule",
353342
"controlPlane", cp.KonnectNamespacedRef)
354-
translationErrors = append(translationErrors, fmt.Errorf("failed to build KongUpstream %s: %w", upstreamName, err))
343+
translationErrors = append(translationErrors, fmt.Errorf("failed to translate KongUpstream resource: %w", err))
355344
continue
356345
}
357-
c.outputStore = append(c.outputStore, &upstream)
358-
log.Debug(logger, "Successfully built KongUpstream resource",
346+
upstreamName := upstreamPtr.Name
347+
c.outputStore = append(c.outputStore, upstreamPtr)
348+
log.Debug(logger, "Successfully translated KongUpstream resource",
359349
"upstream", upstreamName)
360350

361-
// Build the KongTarget resources using the new rule-based approach.
351+
// Build the KongTarget resources.
362352
targets, err := target.TargetsForBackendRefs(
363353
ctx,
364354
logger.WithValues("upstream", upstreamName),
@@ -372,135 +362,86 @@ func (c *httpRouteConverter) translate(ctx context.Context, logger logr.Logger)
372362
c.clusterDomain,
373363
)
374364
if err != nil {
375-
log.Error(logger, err, "Failed to create KongTarget resources for rule, skipping rule",
365+
log.Error(logger, err, "Failed to translate KongTarget resources for rule, skipping rule",
376366
"upstream", upstreamName,
377367
"backendRefs", rule.BackendRefs,
378368
"parentRef", pRef)
379-
translationErrors = append(translationErrors, fmt.Errorf("failed to create KongTarget resources for upstream %s: %w", upstreamName, err))
369+
translationErrors = append(translationErrors, fmt.Errorf("failed to translate KongTarget resources for upstream %s: %w", upstreamName, err))
380370
continue
381371
}
382-
log.Debug(logger, "Successfully created KongTarget resources",
372+
log.Debug(logger, "Successfully translated KongTarget resources",
383373
"upstream", upstreamName,
384374
"targetCount", len(targets))
385375
for _, tgt := range targets {
386376
c.outputStore = append(c.outputStore, &tgt)
387377
}
388378

389379
// Build the KongService resource.
390-
serviceName := namegen.NewKongServiceName(cp, rule)
391-
log.Trace(logger, "Building KongService resource",
392-
"service", serviceName,
393-
"upstream", upstreamName)
394-
395-
service, err := builder.NewKongService().
396-
WithName(serviceName).
397-
WithNamespace(c.route.Namespace).
398-
WithLabels(c.route, &pRef).
399-
WithAnnotations(c.route, &pRef).
400-
WithSpecName(serviceName).
401-
WithSpecHost(upstreamName).
402-
WithControlPlaneRef(*cp).
403-
WithOwner(c.route).Build()
380+
servicePtr, err := service.ServiceForRule(ctx, logger, c.Client, c.route, rule, &pRef, cp, upstreamName)
404381
if err != nil {
405-
log.Error(logger, err, "Failed to build KongService resource, skipping rule",
406-
"service", serviceName,
382+
log.Error(logger, err, "Failed to translate KongService resource, skipping rule",
383+
"controlPlane", cp.KonnectNamespacedRef,
407384
"upstream", upstreamName)
408-
translationErrors = append(translationErrors, fmt.Errorf("failed to build KongService %s: %w", serviceName, err))
385+
translationErrors = append(translationErrors, fmt.Errorf("failed to translate KongService for rule: %w", err))
409386
continue
410387
}
411-
c.outputStore = append(c.outputStore, &service)
412-
log.Debug(logger, "Successfully built KongService resource",
388+
serviceName := servicePtr.Name
389+
c.outputStore = append(c.outputStore, servicePtr)
390+
log.Debug(logger, "Successfully translated KongService resource",
413391
"service", serviceName)
414392

415-
// Build the kong route resource.
416-
routeName := namegen.NewKongRouteName(c.route, cp, rule)
417-
log.Trace(logger, "Building KongRoute resource",
418-
"kongRoute", routeName,
419-
"service", serviceName,
420-
"hostnames", hostnames,
421-
"matchCount", len(rule.Matches))
422-
423-
routeBuilder := builder.NewKongRoute().
424-
WithName(routeName).
425-
WithNamespace(c.route.Namespace).
426-
WithLabels(c.route, &pRef).
427-
WithAnnotations(c.route, &pRef).
428-
WithSpecName(routeName).
429-
WithHosts(hostnames).
430-
WithStripPath(metadata.ExtractStripPath(c.route.Annotations)).
431-
WithKongService(serviceName).
432-
WithOwner(c.route)
433-
for _, match := range rule.Matches {
434-
routeBuilder = routeBuilder.WithHTTPRouteMatch(match)
435-
}
436-
route, err := routeBuilder.Build()
393+
// Build the KongRoute resource.
394+
routePtr, err := kongroute.RouteForRule(ctx, logger, c.Client, c.route, rule, &pRef, cp, serviceName, hostnames)
437395
if err != nil {
438-
log.Error(logger, err, "Failed to build KongRoute resource, skipping rule",
439-
"kongRoute", routeName,
396+
log.Error(logger, err, "Failed to translate KongRoute resource, skipping rule",
440397
"service", serviceName,
441398
"hostnames", hostnames)
442-
translationErrors = append(translationErrors, fmt.Errorf("failed to build KongRoute %s: %w", routeName, err))
399+
translationErrors = append(translationErrors, fmt.Errorf("failed to translate KongRoute for rule: %w", err))
443400
continue
444401
}
445-
c.outputStore = append(c.outputStore, &route)
446-
log.Debug(logger, "Successfully built KongRoute resource",
447-
"kongRoute", routeName)
402+
routeName := routePtr.Name
403+
c.outputStore = append(c.outputStore, routePtr)
404+
log.Debug(logger, "Successfully translated KongRoute resource",
405+
"route", routeName)
448406

449-
// Build the kong plugin and kong plugin binding resources.
407+
// Build the KongPlugin and KongPluginBinding resources.
450408
log.Debug(logger, "Processing filters for rule",
451409
"kongRoute", routeName,
452410
"filterCount", len(rule.Filters))
453411

454412
for _, filter := range rule.Filters {
455-
pluginName := namegen.NewKongPluginName(filter)
456-
457-
log.Trace(logger, "Building KongPlugin resource",
458-
"plugin", pluginName,
459-
"filterType", filter.Type)
460-
461-
plugin, err := builder.NewKongPlugin().
462-
WithName(pluginName).
463-
WithNamespace(c.route.Namespace).
464-
WithLabels(c.route, &pRef).
465-
WithAnnotations(c.route, &pRef).
466-
WithFilter(filter).
467-
WithOwner(c.route).Build()
413+
pluginPtr, err := plugin.PluginForFilter(ctx, logger, c.Client, c.route, filter, &pRef)
468414
if err != nil {
469-
log.Error(logger, err, "Failed to build KongPlugin resource, skipping filter",
470-
"plugin", pluginName,
471-
"filterType", filter.Type)
472-
translationErrors = append(translationErrors, fmt.Errorf("failed to build KongPlugin %s: %w", pluginName, err))
415+
log.Error(logger, err, "Failed to translate KongPlugin resource, skipping filter",
416+
"filter", filter.Type)
417+
translationErrors = append(translationErrors, fmt.Errorf("failed to translate KongPlugin for filter: %w", err))
473418
continue
474419
}
475-
c.outputStore = append(c.outputStore, &plugin)
476-
477-
// Create a KongPluginBinding to bind the KongPlugin to each rule match.
478-
bindingName := namegen.NewKongPluginBindingName(routeName, pluginName)
479-
log.Trace(logger, "Building KongPluginBinding resource",
480-
"binding", bindingName,
481-
"plugin", pluginName,
482-
"kongRoute", routeName)
483-
484-
binding, err := builder.NewKongPluginBinding().
485-
WithName(bindingName).
486-
WithNamespace(c.route.Namespace).
487-
WithLabels(c.route, &pRef).
488-
WithAnnotations(c.route, &pRef).
489-
WithPluginRef(pluginName).
490-
WithControlPlaneRef(*cp).
491-
WithOwner(c.route).
492-
WithRouteRef(routeName).Build()
420+
pluginName := pluginPtr.Name
421+
c.outputStore = append(c.outputStore, pluginPtr)
422+
423+
// Create a KongPluginBinding to bind the KongPlugin to each KongRoute.
424+
bindingPtr, err := pluginbinding.BindingForPluginAndRoute(
425+
ctx,
426+
logger,
427+
c.Client,
428+
c.route,
429+
&pRef,
430+
cp,
431+
pluginName,
432+
routeName,
433+
)
493434
if err != nil {
494435
log.Error(logger, err, "Failed to build KongPluginBinding resource, skipping binding",
495-
"binding", bindingName,
496436
"plugin", pluginName,
497437
"kongRoute", routeName)
498-
translationErrors = append(translationErrors, fmt.Errorf("failed to build KongPluginBinding %s: %w", bindingName, err))
438+
translationErrors = append(translationErrors, fmt.Errorf("failed to build KongPluginBinding for plugin %s: %w", pluginName, err))
499439
continue
500440
}
501-
c.outputStore = append(c.outputStore, &binding)
441+
bindingName := bindingPtr.Name
442+
c.outputStore = append(c.outputStore, bindingPtr)
502443

503-
log.Debug(logger, "Successfully built KongPlugin and KongPluginBinding resources",
444+
log.Debug(logger, "Successfully translated KongPlugin and KongPluginBinding resources",
504445
"plugin", pluginName,
505446
"binding", bindingName)
506447
}

0 commit comments

Comments
 (0)