From 98778b9315b4bf7b4fa7e96dc09aa39c72f5eedd Mon Sep 17 00:00:00 2001 From: Haakon Reppen <56997877+haahaakon@users.noreply.github.com> Date: Tue, 14 Jul 2026 11:18:53 +0200 Subject: [PATCH 1/8] Initial commit --- internal/annotations/gateway_annotations.go | 4 +++ internal/controller/gateway_manager.go | 29 +++++++++++++++++---- internal/controller/httproute_controller.go | 5 +++- 3 files changed, 32 insertions(+), 6 deletions(-) diff --git a/internal/annotations/gateway_annotations.go b/internal/annotations/gateway_annotations.go index 3c29623..8cdcb3a 100644 --- a/internal/annotations/gateway_annotations.go +++ b/internal/annotations/gateway_annotations.go @@ -23,4 +23,8 @@ const ( AnnotationCertManagerClusterIssuer = "cert-manager.io/cluster-issuer" AnnotationIpFamily = "ipam.vitistack.io/ip-family" + + // AnnotationIPAMAddresses specifies specific IP addresses to assign via IPAM + // Value type: string + AnnotationIPAMAddresses = "ipam.vitistack.io/addresses" ) diff --git a/internal/controller/gateway_manager.go b/internal/controller/gateway_manager.go index f8a7158..8a0969a 100644 --- a/internal/controller/gateway_manager.go +++ b/internal/controller/gateway_manager.go @@ -19,6 +19,7 @@ func (r *HTTPRouteReconciler) ensureGateway( ipamZone string, ipFamily string, clusterIssuer string, + ipamAddresses string, ) error { log := logf.FromContext(ctx) @@ -33,7 +34,7 @@ func (r *HTTPRouteReconciler) ensureGateway( if errors.IsNotFound(err) { // Gateway doesn't exist, create it log.Info("Creating new Gateway", "gateway", gatewayName, "namespace", gatewayNamespace) - created, err := r.createGateway(ctx, gatewayName, gatewayNamespace, ipamZone, ipFamily, clusterIssuer) + created, err := r.createGateway(ctx, gatewayName, gatewayNamespace, ipamZone, ipFamily, clusterIssuer, ipamAddresses) if err != nil { return err } @@ -93,6 +94,7 @@ func (r *HTTPRouteReconciler) createGateway( ipamZone string, ipFamily string, clusterIssuer string, + ipamAddresses string, ) (*gatewayv1.Gateway, error) { log := logf.FromContext(ctx) @@ -114,10 +116,7 @@ func (r *HTTPRouteReconciler) createGateway( Spec: gatewayv1.GatewaySpec{ Listeners: listeners, Infrastructure: &gatewayv1.GatewayInfrastructure{ - Annotations: map[gatewayv1.AnnotationKey]gatewayv1.AnnotationValue{ - annotations.AnnotationIPAMZone: gatewayv1.AnnotationValue(ipamZone), - annotations.AnnotationIpFamily: gatewayv1.AnnotationValue(ipFamily), - }, + Annotations: buildInfrastructureAnnotations(ipamZone, ipFamily, ipamAddresses), }, }, } @@ -153,6 +152,13 @@ func (r *HTTPRouteReconciler) createGateway( return nil, errors.NewBadRequest("HTTPRoute IPAM ip-family mismatch: Gateway has ip-family '" + string(existingFamily) + "' but HTTPRoute requires '" + ipFamily + "'") } } + if ipamAddresses != "" { + if existingAddresses, exists := existing.Spec.Infrastructure.Annotations[annotations.AnnotationIPAMAddresses]; exists { + if string(existingAddresses) != ipamAddresses { + return nil, errors.NewBadRequest("HTTPRoute IPAM addresses mismatch: Gateway has addresses '" + string(existingAddresses) + "' but HTTPRoute requires '" + ipamAddresses + "'") + } + } + } } deleted, err := r.updateGatewayListeners(ctx, existing, gatewayNamespace) @@ -173,3 +179,16 @@ func (r *HTTPRouteReconciler) createGateway( log.Info("Successfully created Gateway", "gateway", gatewayName, "namespace", gatewayNamespace, "listeners", len(listeners)) return newGateway, nil } + +// buildInfrastructureAnnotations constructs the Gateway.Spec.Infrastructure.Annotations map. +// ipamAddresses is optional; it is omitted when empty. +func buildInfrastructureAnnotations(ipamZone, ipFamily, ipamAddresses string) map[gatewayv1.AnnotationKey]gatewayv1.AnnotationValue { + m := map[gatewayv1.AnnotationKey]gatewayv1.AnnotationValue{ + annotations.AnnotationIPAMZone: gatewayv1.AnnotationValue(ipamZone), + annotations.AnnotationIpFamily: gatewayv1.AnnotationValue(ipFamily), + } + if ipamAddresses != "" { + m[annotations.AnnotationIPAMAddresses] = gatewayv1.AnnotationValue(ipamAddresses) + } + return m +} diff --git a/internal/controller/httproute_controller.go b/internal/controller/httproute_controller.go index ee15d6c..c37ef65 100644 --- a/internal/controller/httproute_controller.go +++ b/internal/controller/httproute_controller.go @@ -213,8 +213,11 @@ func (r *HTTPRouteReconciler) Reconcile(ctx context.Context, req ctrl.Request) ( log.V(1).Info("No cluster issuer annotation found, using default", "clusterIssuer", clusterIssuer) } + // Get IPAM addresses from annotation (optional, no default) + ipamAddresses := httpRoute.Annotations[annotations.AnnotationIPAMAddresses] + // Ensure the Gateway exists and has correct listeners - if err := r.ensureGateway(ctx, gatewayName, gatewayNamespace, ipamZone, ipFamily, clusterIssuer); err != nil { + if err := r.ensureGateway(ctx, gatewayName, gatewayNamespace, ipamZone, ipFamily, clusterIssuer, ipamAddresses); err != nil { return ctrl.Result{}, err } From b17697efada9ae4b9afdf24a58afaf37780d81a3 Mon Sep 17 00:00:00 2001 From: Haakon Reppen <56997877+haahaakon@users.noreply.github.com> Date: Tue, 14 Jul 2026 11:23:21 +0200 Subject: [PATCH 2/8] Add webhook validation --- internal/webhook/v1/httproute_webhook.go | 14 ++++++ .../v1/validations/validate_addresses.go | 44 +++++++++++++++++++ 2 files changed, 58 insertions(+) create mode 100644 internal/webhook/v1/validations/validate_addresses.go diff --git a/internal/webhook/v1/httproute_webhook.go b/internal/webhook/v1/httproute_webhook.go index c35ed48..5609f46 100644 --- a/internal/webhook/v1/httproute_webhook.go +++ b/internal/webhook/v1/httproute_webhook.go @@ -88,6 +88,13 @@ func (v *HTTPRouteCustomValidator) ValidateCreate(ctx context.Context, httproute return nil, err } + // Validate that addresses are the same on HTTPRoute and Gateway + err = validations.ValidateAddresses(httproute, referredGateway) + if err != nil { + httproutelog.Info("Rejecting HTTPRoute creation", "name", httproute.GetName(), "reason", err.Error()) + return nil, err + } + return nil, nil } @@ -129,6 +136,13 @@ func (v *HTTPRouteCustomValidator) ValidateUpdate(ctx context.Context, _, httpro return nil, err } + // Validate that addresses are the same on HTTPRoute and Gateway + err = validations.ValidateAddresses(httproute, referredGateway) + if err != nil { + httproutelog.Info("Rejecting HTTPRoute update", "name", httproute.GetName(), "reason", err.Error()) + return nil, err + } + return nil, nil } diff --git a/internal/webhook/v1/validations/validate_addresses.go b/internal/webhook/v1/validations/validate_addresses.go new file mode 100644 index 0000000..e496b8e --- /dev/null +++ b/internal/webhook/v1/validations/validate_addresses.go @@ -0,0 +1,44 @@ +package validations + +import ( + "fmt" + + "github.com/NorskHelsenett/gatewayapi-operator/internal/annotations" + gatewayv1 "sigs.k8s.io/gateway-api/apis/v1" +) + +func ValidateAddresses(httproute *gatewayv1.HTTPRoute, gateway *gatewayv1.Gateway) error { + httprouteAddresses := httproute.GetAnnotations()[annotations.AnnotationIPAMAddresses] + + // If the HTTPRoute does not specify addresses there is nothing to conflict with. + if httprouteAddresses == "" { + return nil + } + + if !isHTTPRouteAndGatewayAddressesMatching(httprouteAddresses, gateway) { + gatewayAddresses := string(gateway.Spec.Infrastructure.Annotations[annotations.AnnotationIPAMAddresses]) + return fmt.Errorf("HTTPRoute %s annotation %q conflicts with existing Gateway %s/%s (has %q)", + annotations.AnnotationIPAMAddresses, httprouteAddresses, gateway.Namespace, gateway.Name, gatewayAddresses) + } + + return nil +} + +func isHTTPRouteAndGatewayAddressesMatching(httprouteAddresses string, gateway *gatewayv1.Gateway) bool { + if gateway == nil { + return true + } + + if gateway.Spec.Infrastructure == nil { + return true + } + + gatewayAddresses := string(gateway.Spec.Infrastructure.Annotations[annotations.AnnotationIPAMAddresses]) + + // Addresses on Gateway is not set. No conflict possible. + if gatewayAddresses == "" { + return true + } + + return gatewayAddresses == httprouteAddresses +} From 000751b2b5d6d8f071a5c29a232a38dee2d33042 Mon Sep 17 00:00:00 2001 From: Haakon Reppen <56997877+haahaakon@users.noreply.github.com> Date: Tue, 14 Jul 2026 11:23:38 +0200 Subject: [PATCH 3/8] Ensure check for Address is done in ensureGateway --- internal/controller/gateway_manager.go | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/internal/controller/gateway_manager.go b/internal/controller/gateway_manager.go index 8a0969a..bc311c2 100644 --- a/internal/controller/gateway_manager.go +++ b/internal/controller/gateway_manager.go @@ -70,6 +70,13 @@ func (r *HTTPRouteReconciler) ensureGateway( return errors.NewBadRequest("HTTPRoute IPAM ip-family mismatch: Gateway has ip-family '" + string(existingFamily) + "' but HTTPRoute requires '" + ipFamily + "'") } } + if ipamAddresses != "" { + if existingAddresses, exists := gateway.Spec.Infrastructure.Annotations[annotations.AnnotationIPAMAddresses]; exists { + if string(existingAddresses) != ipamAddresses { + return errors.NewBadRequest("HTTPRoute IPAM addresses mismatch: Gateway has addresses '" + string(existingAddresses) + "' but HTTPRoute requires '" + ipamAddresses + "'") + } + } + } } // Gateway exists and configuration matches, update listeners From 6338074dcdc31dea4b6a5a31a87e61f9119a0f0f Mon Sep 17 00:00:00 2001 From: Haakon Reppen <56997877+haahaakon@users.noreply.github.com> Date: Tue, 14 Jul 2026 11:32:23 +0200 Subject: [PATCH 4/8] Make sure all HTTPRoutes have the annotation set for a GW annotated with the adresses annotation --- internal/controller/gateway_manager.go | 16 ++--- .../v1/validations/validate_addresses.go | 8 +-- .../v1/validations/validations_test.go | 68 +++++++++++++++++++ 3 files changed, 76 insertions(+), 16 deletions(-) diff --git a/internal/controller/gateway_manager.go b/internal/controller/gateway_manager.go index bc311c2..6055520 100644 --- a/internal/controller/gateway_manager.go +++ b/internal/controller/gateway_manager.go @@ -70,11 +70,9 @@ func (r *HTTPRouteReconciler) ensureGateway( return errors.NewBadRequest("HTTPRoute IPAM ip-family mismatch: Gateway has ip-family '" + string(existingFamily) + "' but HTTPRoute requires '" + ipFamily + "'") } } - if ipamAddresses != "" { - if existingAddresses, exists := gateway.Spec.Infrastructure.Annotations[annotations.AnnotationIPAMAddresses]; exists { - if string(existingAddresses) != ipamAddresses { - return errors.NewBadRequest("HTTPRoute IPAM addresses mismatch: Gateway has addresses '" + string(existingAddresses) + "' but HTTPRoute requires '" + ipamAddresses + "'") - } + if existingAddresses, exists := gateway.Spec.Infrastructure.Annotations[annotations.AnnotationIPAMAddresses]; exists && existingAddresses != "" { + if ipamAddresses != string(existingAddresses) { + return errors.NewBadRequest("HTTPRoute IPAM addresses mismatch: Gateway has addresses '" + string(existingAddresses) + "' but HTTPRoute requires '" + ipamAddresses + "'") } } } @@ -159,11 +157,9 @@ func (r *HTTPRouteReconciler) createGateway( return nil, errors.NewBadRequest("HTTPRoute IPAM ip-family mismatch: Gateway has ip-family '" + string(existingFamily) + "' but HTTPRoute requires '" + ipFamily + "'") } } - if ipamAddresses != "" { - if existingAddresses, exists := existing.Spec.Infrastructure.Annotations[annotations.AnnotationIPAMAddresses]; exists { - if string(existingAddresses) != ipamAddresses { - return nil, errors.NewBadRequest("HTTPRoute IPAM addresses mismatch: Gateway has addresses '" + string(existingAddresses) + "' but HTTPRoute requires '" + ipamAddresses + "'") - } + if existingAddresses, exists := existing.Spec.Infrastructure.Annotations[annotations.AnnotationIPAMAddresses]; exists && existingAddresses != "" { + if ipamAddresses != string(existingAddresses) { + return nil, errors.NewBadRequest("HTTPRoute IPAM addresses mismatch: Gateway has addresses '" + string(existingAddresses) + "' but HTTPRoute requires '" + ipamAddresses + "'") } } } diff --git a/internal/webhook/v1/validations/validate_addresses.go b/internal/webhook/v1/validations/validate_addresses.go index e496b8e..e961dd0 100644 --- a/internal/webhook/v1/validations/validate_addresses.go +++ b/internal/webhook/v1/validations/validate_addresses.go @@ -10,11 +10,6 @@ import ( func ValidateAddresses(httproute *gatewayv1.HTTPRoute, gateway *gatewayv1.Gateway) error { httprouteAddresses := httproute.GetAnnotations()[annotations.AnnotationIPAMAddresses] - // If the HTTPRoute does not specify addresses there is nothing to conflict with. - if httprouteAddresses == "" { - return nil - } - if !isHTTPRouteAndGatewayAddressesMatching(httprouteAddresses, gateway) { gatewayAddresses := string(gateway.Spec.Infrastructure.Annotations[annotations.AnnotationIPAMAddresses]) return fmt.Errorf("HTTPRoute %s annotation %q conflicts with existing Gateway %s/%s (has %q)", @@ -35,10 +30,11 @@ func isHTTPRouteAndGatewayAddressesMatching(httprouteAddresses string, gateway * gatewayAddresses := string(gateway.Spec.Infrastructure.Annotations[annotations.AnnotationIPAMAddresses]) - // Addresses on Gateway is not set. No conflict possible. + // Gateway has no addresses annotation. No conflict possible. if gatewayAddresses == "" { return true } + // Gateway has addresses set; HTTPRoute must specify the same value. return gatewayAddresses == httprouteAddresses } diff --git a/internal/webhook/v1/validations/validations_test.go b/internal/webhook/v1/validations/validations_test.go index 26cd266..9f79b0b 100644 --- a/internal/webhook/v1/validations/validations_test.go +++ b/internal/webhook/v1/validations/validations_test.go @@ -210,3 +210,71 @@ func TestValidateIpfamily_Mismatch(t *testing.T) { t.Error("expected error for ip-family mismatch, got nil") } } + +// ---- ValidateAddresses ---- + +func TestValidateAddresses_NoAnnotationOnRoute(t *testing.T) { + // Gateway has addresses set; route without the annotation must be rejected + route := newHTTProute(nil) + gw := newGatewayWithInfraAnn(map[gatewayv1.AnnotationKey]gatewayv1.AnnotationValue{ + annotations.AnnotationIPAMAddresses: "192.168.1.100", + }) + if err := validations.ValidateAddresses(route, gw); err == nil { + t.Error("expected error when gateway has addresses but route does not, got nil") + } +} + +func TestValidateAddresses_NoAnnotationOnRouteNoGatewayAnnotation(t *testing.T) { + // Neither route nor gateway has addresses set -> no conflict + route := newHTTProute(nil) + gw := newGatewayWithInfraAnn(nil) + if err := validations.ValidateAddresses(route, gw); err != nil { + t.Errorf("expected nil error when neither route nor gateway has addresses, got %v", err) + } +} + +func TestValidateAddresses_NoGateway(t *testing.T) { + route := newHTTProute(map[string]string{annotations.AnnotationIPAMAddresses: "192.168.1.100"}) + if err := validations.ValidateAddresses(route, nil); err != nil { + t.Errorf("expected nil error with no gateway, got %v", err) + } +} + +func TestValidateAddresses_GatewayNoInfrastructure(t *testing.T) { + route := newHTTProute(map[string]string{annotations.AnnotationIPAMAddresses: "192.168.1.100"}) + gw := &gatewayv1.Gateway{ + ObjectMeta: metav1.ObjectMeta{Name: "gw", Namespace: "default"}, + Spec: gatewayv1.GatewaySpec{GatewayClassName: "eg"}, + } + if err := validations.ValidateAddresses(route, gw); err != nil { + t.Errorf("expected nil error when gateway has no infrastructure, got %v", err) + } +} + +func TestValidateAddresses_GatewayNoAddressesAnnotation(t *testing.T) { + route := newHTTProute(map[string]string{annotations.AnnotationIPAMAddresses: "192.168.1.100"}) + gw := newGatewayWithInfraAnn(nil) + if err := validations.ValidateAddresses(route, gw); err != nil { + t.Errorf("expected nil error when gateway has no addresses annotation, got %v", err) + } +} + +func TestValidateAddresses_Matching(t *testing.T) { + route := newHTTProute(map[string]string{annotations.AnnotationIPAMAddresses: "192.168.1.100"}) + gw := newGatewayWithInfraAnn(map[gatewayv1.AnnotationKey]gatewayv1.AnnotationValue{ + annotations.AnnotationIPAMAddresses: "192.168.1.100", + }) + if err := validations.ValidateAddresses(route, gw); err != nil { + t.Errorf("expected nil error for matching addresses, got %v", err) + } +} + +func TestValidateAddresses_Mismatch(t *testing.T) { + route := newHTTProute(map[string]string{annotations.AnnotationIPAMAddresses: "192.168.1.100"}) + gw := newGatewayWithInfraAnn(map[gatewayv1.AnnotationKey]gatewayv1.AnnotationValue{ + annotations.AnnotationIPAMAddresses: "10.0.0.5", + }) + if err := validations.ValidateAddresses(route, gw); err == nil { + t.Error("expected error for addresses mismatch, got nil") + } +} From 0df1d39d14f312d0a696f82dae850019895ce93e Mon Sep 17 00:00:00 2001 From: Haakon Reppen <56997877+haahaakon@users.noreply.github.com> Date: Tue, 14 Jul 2026 11:42:54 +0200 Subject: [PATCH 5/8] Throw error if HTTPRoute specifies addressess annotation when referencing a gw without it --- internal/controller/gateway_manager.go | 32 +++++++++++++------ .../v1/validations/validate_addresses.go | 29 ++++++++++------- .../v1/validations/validations_test.go | 10 +++--- 3 files changed, 46 insertions(+), 25 deletions(-) diff --git a/internal/controller/gateway_manager.go b/internal/controller/gateway_manager.go index 6055520..0d3698f 100644 --- a/internal/controller/gateway_manager.go +++ b/internal/controller/gateway_manager.go @@ -58,7 +58,8 @@ func (r *HTTPRouteReconciler) ensureGateway( return errors.NewBadRequest("HTTPRoute cluster issuer mismatch: Gateway has issuer '" + existingIssuer + "' but HTTPRoute requires '" + clusterIssuer + "'") } - // Gateway exists, validate IPAM zone and ip-family match if set + // Gateway exists, validate IPAM settings match + gatewayInfraAddresses := "" if gateway.Spec.Infrastructure != nil && gateway.Spec.Infrastructure.Annotations != nil { if existingZone, exists := gateway.Spec.Infrastructure.Annotations[annotations.AnnotationIPAMZone]; exists { if string(existingZone) != ipamZone { @@ -70,11 +71,16 @@ func (r *HTTPRouteReconciler) ensureGateway( return errors.NewBadRequest("HTTPRoute IPAM ip-family mismatch: Gateway has ip-family '" + string(existingFamily) + "' but HTTPRoute requires '" + ipFamily + "'") } } - if existingAddresses, exists := gateway.Spec.Infrastructure.Annotations[annotations.AnnotationIPAMAddresses]; exists && existingAddresses != "" { - if ipamAddresses != string(existingAddresses) { - return errors.NewBadRequest("HTTPRoute IPAM addresses mismatch: Gateway has addresses '" + string(existingAddresses) + "' but HTTPRoute requires '" + ipamAddresses + "'") - } + gatewayInfraAddresses = string(gateway.Spec.Infrastructure.Annotations[annotations.AnnotationIPAMAddresses]) + } + if ipamAddresses != gatewayInfraAddresses { + if gatewayInfraAddresses == "" { + return errors.NewBadRequest("HTTPRoute specifies IPAM addresses '" + ipamAddresses + "' but Gateway was not created with an addresses annotation; all routes on this gateway must omit it") + } + if ipamAddresses == "" { + return errors.NewBadRequest("Gateway has IPAM addresses '" + gatewayInfraAddresses + "'; all routes on this gateway must set annotation " + string(annotations.AnnotationIPAMAddresses)) } + return errors.NewBadRequest("HTTPRoute IPAM addresses mismatch: Gateway has addresses '" + gatewayInfraAddresses + "' but HTTPRoute requires '" + ipamAddresses + "'") } // Gateway exists and configuration matches, update listeners @@ -145,7 +151,8 @@ func (r *HTTPRouteReconciler) createGateway( return nil, errors.NewBadRequest("HTTPRoute cluster issuer mismatch: Gateway has issuer '" + existingIssuer + "' but HTTPRoute requires '" + clusterIssuer + "'") } - // Validate IPAM zone and ip-family match if set + // Validate IPAM zone, ip-family, and addresses match + concurrentGatewayAddresses := "" if existing.Spec.Infrastructure != nil && existing.Spec.Infrastructure.Annotations != nil { if existingZone, exists := existing.Spec.Infrastructure.Annotations[annotations.AnnotationIPAMZone]; exists { if string(existingZone) != ipamZone { @@ -157,11 +164,16 @@ func (r *HTTPRouteReconciler) createGateway( return nil, errors.NewBadRequest("HTTPRoute IPAM ip-family mismatch: Gateway has ip-family '" + string(existingFamily) + "' but HTTPRoute requires '" + ipFamily + "'") } } - if existingAddresses, exists := existing.Spec.Infrastructure.Annotations[annotations.AnnotationIPAMAddresses]; exists && existingAddresses != "" { - if ipamAddresses != string(existingAddresses) { - return nil, errors.NewBadRequest("HTTPRoute IPAM addresses mismatch: Gateway has addresses '" + string(existingAddresses) + "' but HTTPRoute requires '" + ipamAddresses + "'") - } + concurrentGatewayAddresses = string(existing.Spec.Infrastructure.Annotations[annotations.AnnotationIPAMAddresses]) + } + if ipamAddresses != concurrentGatewayAddresses { + if concurrentGatewayAddresses == "" { + return nil, errors.NewBadRequest("HTTPRoute specifies IPAM addresses '" + ipamAddresses + "' but Gateway was not created with an addresses annotation; all routes on this gateway must omit it") + } + if ipamAddresses == "" { + return nil, errors.NewBadRequest("Gateway has IPAM addresses '" + concurrentGatewayAddresses + "'; all routes on this gateway must set annotation " + string(annotations.AnnotationIPAMAddresses)) } + return nil, errors.NewBadRequest("HTTPRoute IPAM addresses mismatch: Gateway has addresses '" + concurrentGatewayAddresses + "' but HTTPRoute requires '" + ipamAddresses + "'") } deleted, err := r.updateGatewayListeners(ctx, existing, gatewayNamespace) diff --git a/internal/webhook/v1/validations/validate_addresses.go b/internal/webhook/v1/validations/validate_addresses.go index e961dd0..3209f9d 100644 --- a/internal/webhook/v1/validations/validate_addresses.go +++ b/internal/webhook/v1/validations/validate_addresses.go @@ -11,7 +11,18 @@ func ValidateAddresses(httproute *gatewayv1.HTTPRoute, gateway *gatewayv1.Gatewa httprouteAddresses := httproute.GetAnnotations()[annotations.AnnotationIPAMAddresses] if !isHTTPRouteAndGatewayAddressesMatching(httprouteAddresses, gateway) { - gatewayAddresses := string(gateway.Spec.Infrastructure.Annotations[annotations.AnnotationIPAMAddresses]) + gatewayAddresses := "" + if gateway.Spec.Infrastructure != nil && gateway.Spec.Infrastructure.Annotations != nil { + gatewayAddresses = string(gateway.Spec.Infrastructure.Annotations[annotations.AnnotationIPAMAddresses]) + } + if gatewayAddresses == "" { + return fmt.Errorf("HTTPRoute specifies %s %q but Gateway %s/%s was not created with an addresses annotation; all routes on this gateway must omit it", + annotations.AnnotationIPAMAddresses, httprouteAddresses, gateway.Namespace, gateway.Name) + } + if httprouteAddresses == "" { + return fmt.Errorf("Gateway %s/%s requires %s %q; all routes on this gateway must set it", + gateway.Namespace, gateway.Name, annotations.AnnotationIPAMAddresses, gatewayAddresses) + } return fmt.Errorf("HTTPRoute %s annotation %q conflicts with existing Gateway %s/%s (has %q)", annotations.AnnotationIPAMAddresses, httprouteAddresses, gateway.Namespace, gateway.Name, gatewayAddresses) } @@ -20,21 +31,17 @@ func ValidateAddresses(httproute *gatewayv1.HTTPRoute, gateway *gatewayv1.Gatewa } func isHTTPRouteAndGatewayAddressesMatching(httprouteAddresses string, gateway *gatewayv1.Gateway) bool { + // Gateway doesn't exist yet — any value on the HTTPRoute is fine, it will be set at creation. if gateway == nil { return true } - if gateway.Spec.Infrastructure == nil { - return true - } - - gatewayAddresses := string(gateway.Spec.Infrastructure.Annotations[annotations.AnnotationIPAMAddresses]) - - // Gateway has no addresses annotation. No conflict possible. - if gatewayAddresses == "" { - return true + // Gateway exists — extract its addresses (empty string if unset). + gatewayAddresses := "" + if gateway.Spec.Infrastructure != nil && gateway.Spec.Infrastructure.Annotations != nil { + gatewayAddresses = string(gateway.Spec.Infrastructure.Annotations[annotations.AnnotationIPAMAddresses]) } - // Gateway has addresses set; HTTPRoute must specify the same value. + // HTTPRoute and Gateway must agree exactly: both empty, or both the same value. return gatewayAddresses == httprouteAddresses } diff --git a/internal/webhook/v1/validations/validations_test.go b/internal/webhook/v1/validations/validations_test.go index 9f79b0b..7c43ca9 100644 --- a/internal/webhook/v1/validations/validations_test.go +++ b/internal/webhook/v1/validations/validations_test.go @@ -241,21 +241,23 @@ func TestValidateAddresses_NoGateway(t *testing.T) { } func TestValidateAddresses_GatewayNoInfrastructure(t *testing.T) { + // HTTPRoute specifies addresses but the gateway has no infrastructure block -> error route := newHTTProute(map[string]string{annotations.AnnotationIPAMAddresses: "192.168.1.100"}) gw := &gatewayv1.Gateway{ ObjectMeta: metav1.ObjectMeta{Name: "gw", Namespace: "default"}, Spec: gatewayv1.GatewaySpec{GatewayClassName: "eg"}, } - if err := validations.ValidateAddresses(route, gw); err != nil { - t.Errorf("expected nil error when gateway has no infrastructure, got %v", err) + if err := validations.ValidateAddresses(route, gw); err == nil { + t.Error("expected error when route specifies addresses but gateway has no infrastructure, got nil") } } func TestValidateAddresses_GatewayNoAddressesAnnotation(t *testing.T) { + // HTTPRoute specifies addresses but the gateway has no addresses annotation -> error route := newHTTProute(map[string]string{annotations.AnnotationIPAMAddresses: "192.168.1.100"}) gw := newGatewayWithInfraAnn(nil) - if err := validations.ValidateAddresses(route, gw); err != nil { - t.Errorf("expected nil error when gateway has no addresses annotation, got %v", err) + if err := validations.ValidateAddresses(route, gw); err == nil { + t.Error("expected error when route specifies addresses but gateway has no addresses annotation, got nil") } } From a17a9b1bddb8bc92264467364f6ef643ab695c36 Mon Sep 17 00:00:00 2001 From: Haakon Reppen <56997877+haahaakon@users.noreply.github.com> Date: Wed, 12 Aug 2026 11:26:51 +0200 Subject: [PATCH 6/8] add support for eg-inet-ipv4 gatewayclass --- internal/controller/constants.go | 3 +++ internal/controller/listener_manager.go | 6 +++++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/internal/controller/constants.go b/internal/controller/constants.go index ee3bbfe..a8caf2a 100644 --- a/internal/controller/constants.go +++ b/internal/controller/constants.go @@ -26,6 +26,9 @@ const ( // Internet gateway class inetGatewayClassName = "eg-inet" + // Internet gateway class - ipv4 only + inetIpv4GatewayClassName = "eg-inet-ipv4" + // httpsPort is the default HTTPS port httpsPort = 443 diff --git a/internal/controller/listener_manager.go b/internal/controller/listener_manager.go index bdb7283..117a409 100644 --- a/internal/controller/listener_manager.go +++ b/internal/controller/listener_manager.go @@ -287,8 +287,12 @@ func UpdateGatewayAnnotations(ctx context.Context, gw *gatewayv1.Gateway, ignore func UpdateGatewayClass(gw *gatewayv1.Gateway) { // only update GatewayClass when no Class is set or when old default "eg" is used. if gw.Spec.GatewayClassName == "" || gw.Spec.GatewayClassName == legacyGatewayClassName { - // If gateway is in InetIPAMZone we use the Inet GatewayClass + // set ipv4 only gatewayclass if ipv4 is specified on inet gateway if gw.Spec.Infrastructure != nil && gw.Spec.Infrastructure.Annotations != nil && + gw.Spec.Infrastructure.Annotations[annotations.AnnotationIPAMZone] == InetIPAMZone && + gw.Spec.Infrastructure.Annotations[annotations.AnnotationIpFamily] == "ipv4" { + gw.Spec.GatewayClassName = gatewayv1.ObjectName(inetIpv4GatewayClassName) + } else if gw.Spec.Infrastructure != nil && gw.Spec.Infrastructure.Annotations != nil && gw.Spec.Infrastructure.Annotations[annotations.AnnotationIPAMZone] == InetIPAMZone { gw.Spec.GatewayClassName = gatewayv1.ObjectName(inetGatewayClassName) } else { From fec46e7b613893047d3b2a30909fab5e8807ccef Mon Sep 17 00:00:00 2001 From: Haakon Reppen <56997877+haahaakon@users.noreply.github.com> Date: Wed, 12 Aug 2026 13:51:07 +0200 Subject: [PATCH 7/8] add support for eg-inet-ipv4 gatewayclass --- internal/controller/constants.go | 5 +++++ internal/controller/listener_manager.go | 16 ++++++++++++---- 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/internal/controller/constants.go b/internal/controller/constants.go index ee3bbfe..6524581 100644 --- a/internal/controller/constants.go +++ b/internal/controller/constants.go @@ -26,6 +26,9 @@ const ( // Internet gateway class inetGatewayClassName = "eg-inet" + // Internet gateway class - ipv4 only + inetIpv4GatewayClassName = "eg-inet-ipv4" + // httpsPort is the default HTTPS port httpsPort = 443 @@ -44,6 +47,8 @@ const ( DefaultHnetIpFamily = "ipv4" + IPv4IpFamily = "ipv4" + // enableClientTrafficPolicyPQCEnvVar toggles ClientTrafficPolicy creation. enableClientTrafficPolicyPQCEnvVar = "ENABLE_CLIENTTRAFFICPOLICY_PQC" diff --git a/internal/controller/listener_manager.go b/internal/controller/listener_manager.go index bdb7283..3ea1bcd 100644 --- a/internal/controller/listener_manager.go +++ b/internal/controller/listener_manager.go @@ -287,10 +287,18 @@ func UpdateGatewayAnnotations(ctx context.Context, gw *gatewayv1.Gateway, ignore func UpdateGatewayClass(gw *gatewayv1.Gateway) { // only update GatewayClass when no Class is set or when old default "eg" is used. if gw.Spec.GatewayClassName == "" || gw.Spec.GatewayClassName == legacyGatewayClassName { - // If gateway is in InetIPAMZone we use the Inet GatewayClass - if gw.Spec.Infrastructure != nil && gw.Spec.Infrastructure.Annotations != nil && - gw.Spec.Infrastructure.Annotations[annotations.AnnotationIPAMZone] == InetIPAMZone { - gw.Spec.GatewayClassName = gatewayv1.ObjectName(inetGatewayClassName) + // set ipv4 only gatewayclass if ipv4 is specified on inet gateway + if gw.Spec.Infrastructure != nil && gw.Spec.Infrastructure.Annotations != nil { + if gw.Spec.Infrastructure.Annotations[annotations.AnnotationIPAMZone] == InetIPAMZone && + gw.Spec.Infrastructure.Annotations[annotations.AnnotationIpFamily] == IPv4IpFamily { + gw.Spec.GatewayClassName = gatewayv1.ObjectName(inetIpv4GatewayClassName) + + } else if gw.Spec.Infrastructure.Annotations[annotations.AnnotationIPAMZone] == InetIPAMZone { + gw.Spec.GatewayClassName = gatewayv1.ObjectName(inetGatewayClassName) + + } else { + gw.Spec.GatewayClassName = gatewayv1.ObjectName(hnetGatewayClassName) // ← missing + } } else { // Use Hnet gatewayclass if AnnotationIPAMZone is not InetIPAMZone gw.Spec.GatewayClassName = gatewayv1.ObjectName(hnetGatewayClassName) From b9e69a31da73b1e230210165954a74fb4cc28caf Mon Sep 17 00:00:00 2001 From: Haakon Reppen <56997877+haahaakon@users.noreply.github.com> Date: Wed, 12 Aug 2026 14:05:52 +0200 Subject: [PATCH 8/8] Correct comment --- internal/controller/listener_manager.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/controller/listener_manager.go b/internal/controller/listener_manager.go index 5ac445d..043447b 100644 --- a/internal/controller/listener_manager.go +++ b/internal/controller/listener_manager.go @@ -297,7 +297,7 @@ func UpdateGatewayClass(gw *gatewayv1.Gateway) { gw.Spec.GatewayClassName = gatewayv1.ObjectName(inetGatewayClassName) } else { - // Default to hnet gwclass if zone is set, but not inetIPamZone + // Default to hnet gwclass if infrastructure annotations is set, but not to inet gw.Spec.GatewayClassName = gatewayv1.ObjectName(hnetGatewayClassName) } } else {