From d1c62d8c7b30408267ab1359a4fa4f32ffb1ce4e Mon Sep 17 00:00:00 2001 From: Nithin0620 Date: Sat, 9 May 2026 10:26:35 +0530 Subject: [PATCH 1/2] feat(api): add health check validation and observedGeneration to TargetGroupConfiguration --- apis/gateway/v1beta1/targetgroupconfig_types.go | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/apis/gateway/v1beta1/targetgroupconfig_types.go b/apis/gateway/v1beta1/targetgroupconfig_types.go index 6ea860e94..6d11a50be 100644 --- a/apis/gateway/v1beta1/targetgroupconfig_types.go +++ b/apis/gateway/v1beta1/targetgroupconfig_types.go @@ -41,8 +41,8 @@ type Reference struct { Name string `json:"name"` } -// TODO: Add a validation in the admission webhook to check if only one of HTTPCode or GRPCCode is set. // Information to use when checking for a successful response from a target. +// +kubebuilder:validation:XValidation:rule="has(self.httpCode) != has(self.grpcCode)",message="Only one of httpCode or grpcCode must be specified" type HealthCheckMatcher struct { // The HTTP codes. HTTPCode *string `json:"httpCode,omitempty"` @@ -241,10 +241,11 @@ type TargetGroupAttribute struct { Value string `json:"value"` } -// TODO -- these can be used to set what generation the gateway is currently on to track progress on reconcile. - // TargetGroupConfigurationStatus defines the observed state of TargetGroupConfiguration type TargetGroupConfigurationStatus struct { + // The generation of the TargetGroupConfiguration object observed by the controller. + // +optional + ObservedGeneration *int64 `json:"observedGeneration,omitempty"` // The generation of the Gateway Configuration attached to the Gateway object. // +optional ObservedGatewayConfigurationGeneration *int64 `json:"observedGatewayConfigurationGeneration,omitempty"` From 67ea87e5cf38c8c4d1bbfbeec854722676ae8e5b Mon Sep 17 00:00:00 2001 From: Nithin0620 Date: Sat, 9 May 2026 11:23:23 +0530 Subject: [PATCH 2/2] feat(controller): implement ObservedGeneration update and add unit tests --- .../targetgroup_configuration_controller.go | 29 ++++++++-- ...rgetgroup_configuration_controller_test.go | 54 +++++++++++++++++++ 2 files changed, 80 insertions(+), 3 deletions(-) create mode 100644 controllers/gateway/targetgroup_configuration_controller_test.go diff --git a/controllers/gateway/targetgroup_configuration_controller.go b/controllers/gateway/targetgroup_configuration_controller.go index d83e97b3b..9a1d6ea2b 100644 --- a/controllers/gateway/targetgroup_configuration_controller.go +++ b/controllers/gateway/targetgroup_configuration_controller.go @@ -82,10 +82,12 @@ func (r *targetgroupConfigurationReconciler) reconcile(ctx context.Context, req } func (r *targetgroupConfigurationReconciler) handleUpdate(tgConf *elbv2gw.TargetGroupConfiguration) error { - if k8s.HasFinalizer(tgConf, shared_constants.TargetGroupConfigurationFinalizer) { - return nil + if !k8s.HasFinalizer(tgConf, shared_constants.TargetGroupConfigurationFinalizer) { + if err := r.finalizerManager.AddFinalizers(context.Background(), tgConf, shared_constants.TargetGroupConfigurationFinalizer); err != nil { + return err + } } - return r.finalizerManager.AddFinalizers(context.Background(), tgConf, shared_constants.TargetGroupConfigurationFinalizer) + return r.updateStatus(context.Background(), tgConf) } func (r *targetgroupConfigurationReconciler) handleDelete(tgConf *elbv2gw.TargetGroupConfiguration) error { @@ -154,6 +156,27 @@ func (r *targetgroupConfigurationReconciler) handleDelete(tgConf *elbv2gw.Target return r.finalizerManager.RemoveFinalizers(context.Background(), tgConf, shared_constants.TargetGroupConfigurationFinalizer) } +// updateStatus updates the TargetGroupConfiguration status with the current observed generation +func (r *targetgroupConfigurationReconciler) updateStatus(ctx context.Context, tgConf *elbv2gw.TargetGroupConfiguration) error { + // Check if status actually needs updating + if tgConf.Status.ObservedGeneration != nil && + *tgConf.Status.ObservedGeneration == tgConf.Generation { + return nil // No update needed + } + + tgConfOld := tgConf.DeepCopy() + + // Update status fields + tgConf.Status.ObservedGeneration = &tgConf.Generation + + // Patch the status + if err := r.k8sClient.Status().Patch(ctx, tgConf, client.MergeFrom(tgConfOld)); err != nil { + return fmt.Errorf("failed to update TargetGroupConfiguration status: %w", err) + } + + return nil +} + // isDefaultTGCInUse checks if any LoadBalancerConfiguration in the same namespace references // this TGC as its defaultTargetGroupConfiguration, and if that LBC is in use by any Gateway or GatewayClass. // Returns the namespaced names of all in-use LBCs found, or empty string if none. diff --git a/controllers/gateway/targetgroup_configuration_controller_test.go b/controllers/gateway/targetgroup_configuration_controller_test.go new file mode 100644 index 000000000..51d45824d --- /dev/null +++ b/controllers/gateway/targetgroup_configuration_controller_test.go @@ -0,0 +1,54 @@ +package gateway + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" + clientgoscheme "k8s.io/client-go/kubernetes/scheme" + elbv2gw "sigs.k8s.io/aws-load-balancer-controller/apis/gateway/v1beta1" + testclient "sigs.k8s.io/controller-runtime/pkg/client/fake" +) + +func Test_targetgroupConfigurationReconciler_updateStatus(t *testing.T) { + k8sSchema := runtime.NewScheme() + _ = clientgoscheme.AddToScheme(k8sSchema) + _ = elbv2gw.AddToScheme(k8sSchema) + + tgConf := &elbv2gw.TargetGroupConfiguration{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-tgc", + Namespace: "default", + Generation: 10, + }, + } + + k8sClient := testclient.NewClientBuilder(). + WithScheme(k8sSchema). + WithStatusSubresource(&elbv2gw.TargetGroupConfiguration{}). + WithObjects(tgConf). + Build() + + r := &targetgroupConfigurationReconciler{ + k8sClient: k8sClient, + } + + ctx := context.Background() + // Initial update + err := r.updateStatus(ctx, tgConf) + assert.NoError(t, err) + + updatedTgConf := &elbv2gw.TargetGroupConfiguration{} + err = k8sClient.Get(ctx, types.NamespacedName{Name: "test-tgc", Namespace: "default"}, updatedTgConf) + assert.NoError(t, err) + + assert.NotNil(t, updatedTgConf.Status.ObservedGeneration) + assert.Equal(t, int64(10), *updatedTgConf.Status.ObservedGeneration) + + // Update again with same generation - should return nil and do nothing + err = r.updateStatus(ctx, updatedTgConf) + assert.NoError(t, err) +}