diff --git a/controllers/gateway/targetgroup_configuration_controller.go b/controllers/gateway/targetgroup_configuration_controller.go index d83e97b3b..cc4822a76 100644 --- a/controllers/gateway/targetgroup_configuration_controller.go +++ b/controllers/gateway/targetgroup_configuration_controller.go @@ -26,6 +26,12 @@ import ( "sigs.k8s.io/controller-runtime/pkg/reconcile" "sigs.k8s.io/controller-runtime/pkg/source" gwv1 "sigs.k8s.io/gateway-api/apis/v1" + gwalpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2" +) + +const ( + targetReferenceKindService = "Service" + targetReferenceKindGateway = "Gateway" ) // NewTargetGroupConfigurationReconciler constructs a reconciler that responds to targetgroup configuration changes @@ -126,6 +132,26 @@ func (r *targetgroupConfigurationReconciler) handleDelete(tgConf *elbv2gw.Target return r.finalizerManager.RemoveFinalizers(context.Background(), tgConf, shared_constants.TargetGroupConfigurationFinalizer) } + targetKind := targetReferenceKindService + if tgConf.Spec.TargetReference.Kind != nil { + targetKind = *tgConf.Spec.TargetReference.Kind + } + + if targetKind == targetReferenceKindGateway { + inUseRoutes, err := r.isGatewayTargetTGCInUse(context.Background(), tgConf) + if err != nil { + return err + } + if inUseRoutes != "" { + return fmt.Errorf("targetgroup configuration [%+v] is still in use by TCPRoutes [%s]", k8s.NamespacedName(tgConf), inUseRoutes) + } + return r.finalizerManager.RemoveFinalizers(context.Background(), tgConf, shared_constants.TargetGroupConfigurationFinalizer) + } + + if targetKind != targetReferenceKindService { + return fmt.Errorf("targetgroup configuration [%+v] has unsupported targetReference kind %q", k8s.NamespacedName(tgConf), targetKind) + } + svcReference := types.NamespacedName{ Namespace: tgConf.Namespace, Name: tgConf.Spec.TargetReference.Name, @@ -184,6 +210,40 @@ func (r *targetgroupConfigurationReconciler) isDefaultTGCInUse(ctx context.Conte return "", nil } +func (r *targetgroupConfigurationReconciler) isGatewayTargetTGCInUse(ctx context.Context, tgConf *elbv2gw.TargetGroupConfiguration) (string, error) { + tcpRouteList := &gwalpha2.TCPRouteList{} + if err := r.k8sClient.List(ctx, tcpRouteList); err != nil { + return "", err + } + + var inUseRoutes []string + for i := range tcpRouteList.Items { + route := &tcpRouteList.Items[i] + for _, rule := range route.Spec.Rules { + for _, beRef := range rule.BackendRefs { + if beRef.Kind == nil || *beRef.Kind != targetReferenceKindGateway { + continue + } + + routeTargetNamespace := route.Namespace + if beRef.Namespace != nil { + routeTargetNamespace = string(*beRef.Namespace) + } + + if routeTargetNamespace == tgConf.Namespace && string(beRef.Name) == tgConf.Spec.TargetReference.Name { + inUseRoutes = append(inUseRoutes, k8s.NamespacedName(route).String()) + break + } + } + } + } + + if len(inUseRoutes) > 0 { + return strings.Join(inUseRoutes, ", "), nil + } + return "", nil +} + func (r *targetgroupConfigurationReconciler) SetupWithManager(_ context.Context, mgr ctrl.Manager) (controller.Controller, error) { return controller.New(constants.TargetGroupConfigurationController, mgr, controller.Options{ MaxConcurrentReconciles: r.workers, diff --git a/controllers/gateway/targetgroup_configuration_controller_test.go b/controllers/gateway/targetgroup_configuration_controller_test.go new file mode 100644 index 000000000..ff285af25 --- /dev/null +++ b/controllers/gateway/targetgroup_configuration_controller_test.go @@ -0,0 +1,118 @@ +package gateway + +import ( + "context" + "testing" + + "github.com/go-logr/logr" + "github.com/golang/mock/gomock" + "github.com/stretchr/testify/assert" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + elbv2gw "sigs.k8s.io/aws-load-balancer-controller/apis/gateway/v1beta1" + "sigs.k8s.io/aws-load-balancer-controller/pkg/k8s" + "sigs.k8s.io/aws-load-balancer-controller/pkg/shared_constants" + "sigs.k8s.io/aws-load-balancer-controller/pkg/testutils" + "sigs.k8s.io/controller-runtime/pkg/client" + gwv1 "sigs.k8s.io/gateway-api/apis/v1" + gwalpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2" +) + +func TestTargetGroupConfigurationReconciler_handleDelete_GatewayTargetStillInUse(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + k8sClient := testutils.GenerateTestClient() + finalizerManager := k8s.NewMockFinalizerManager(ctrl) + + targetKind := targetReferenceKindGateway + tgConf := &elbv2gw.TargetGroupConfiguration{ + ObjectMeta: metav1.ObjectMeta{ + Name: "gateway-tgc", + Namespace: "test-ns", + Finalizers: []string{shared_constants.TargetGroupConfigurationFinalizer}, + }, + Spec: elbv2gw.TargetGroupConfigurationSpec{ + TargetReference: &elbv2gw.Reference{ + Name: "chained-gateway", + Kind: &targetKind, + }, + }, + } + routeKind := gwalpha2.Kind(targetReferenceKindGateway) + route := &gwalpha2.TCPRoute{ + ObjectMeta: metav1.ObjectMeta{ + Name: "tcp-route", + Namespace: "test-ns", + }, + Spec: gwalpha2.TCPRouteSpec{ + Rules: []gwalpha2.TCPRouteRule{ + { + BackendRefs: []gwalpha2.BackendRef{ + { + BackendObjectReference: gwalpha2.BackendObjectReference{ + Name: "chained-gateway", + Kind: &routeKind, + }, + }, + }, + }, + }, + }, + } + + assert.NoError(t, k8sClient.Create(context.Background(), tgConf)) + assert.NoError(t, k8sClient.Create(context.Background(), route)) + + r := &targetgroupConfigurationReconciler{ + k8sClient: k8sClient, + logger: logr.Discard(), + finalizerManager: finalizerManager, + gwRetrieveFn: func(ctx context.Context, k8sClient client.Client, gwController string) ([]*gwv1.Gateway, error) { + return nil, nil + }, + } + + err := r.handleDelete(tgConf) + assert.EqualError(t, err, "targetgroup configuration [test-ns/gateway-tgc] is still in use by TCPRoutes [test-ns/tcp-route]") +} + +func TestTargetGroupConfigurationReconciler_handleDelete_GatewayTargetNotInUse(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + k8sClient := testutils.GenerateTestClient() + finalizerManager := k8s.NewMockFinalizerManager(ctrl) + + targetKind := targetReferenceKindGateway + tgConf := &elbv2gw.TargetGroupConfiguration{ + ObjectMeta: metav1.ObjectMeta{ + Name: "gateway-tgc", + Namespace: "test-ns", + Finalizers: []string{shared_constants.TargetGroupConfigurationFinalizer}, + }, + Spec: elbv2gw.TargetGroupConfigurationSpec{ + TargetReference: &elbv2gw.Reference{ + Name: "chained-gateway", + Kind: &targetKind, + }, + }, + } + + assert.NoError(t, k8sClient.Create(context.Background(), tgConf)) + + finalizerManager.EXPECT(). + RemoveFinalizers(context.Background(), tgConf, shared_constants.TargetGroupConfigurationFinalizer). + Return(nil) + + r := &targetgroupConfigurationReconciler{ + k8sClient: k8sClient, + logger: logr.Discard(), + finalizerManager: finalizerManager, + gwRetrieveFn: func(ctx context.Context, k8sClient client.Client, gwController string) ([]*gwv1.Gateway, error) { + return nil, nil + }, + } + + err := r.handleDelete(tgConf) + assert.NoError(t, err) +}