Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions internal/annotations/gateway_annotations.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
5 changes: 5 additions & 0 deletions internal/controller/constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -44,6 +47,8 @@ const (

DefaultHnetIpFamily = "ipv4"

IPv4IpFamily = "ipv4"

// enableClientTrafficPolicyPQCEnvVar toggles ClientTrafficPolicy creation.
enableClientTrafficPolicyPQCEnvVar = "ENABLE_CLIENTTRAFFICPOLICY_PQC"

Expand Down
48 changes: 41 additions & 7 deletions internal/controller/gateway_manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ func (r *HTTPRouteReconciler) ensureGateway(
ipamZone string,
ipFamily string,
clusterIssuer string,
ipamAddresses string,
) error {
log := logf.FromContext(ctx)

Expand All @@ -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
}
Expand All @@ -57,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 {
Expand All @@ -69,6 +71,16 @@ func (r *HTTPRouteReconciler) ensureGateway(
return errors.NewBadRequest("HTTPRoute IPAM ip-family mismatch: Gateway has ip-family '" + string(existingFamily) + "' but HTTPRoute requires '" + ipFamily + "'")
}
}
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
Expand All @@ -93,6 +105,7 @@ func (r *HTTPRouteReconciler) createGateway(
ipamZone string,
ipFamily string,
clusterIssuer string,
ipamAddresses string,
) (*gatewayv1.Gateway, error) {
log := logf.FromContext(ctx)

Expand All @@ -114,10 +127,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),
},
},
}
Expand All @@ -141,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 {
Expand All @@ -153,6 +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 + "'")
}
}
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)
Expand All @@ -173,3 +194,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
}
5 changes: 4 additions & 1 deletion internal/controller/httproute_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down
17 changes: 13 additions & 4 deletions internal/controller/listener_manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -287,10 +287,19 @@ 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 {
// Default to hnet gwclass if infrastructure annotations is set, but not to inet
gw.Spec.GatewayClassName = gatewayv1.ObjectName(hnetGatewayClassName)
}
} else {
// Use Hnet gatewayclass if AnnotationIPAMZone is not InetIPAMZone
gw.Spec.GatewayClassName = gatewayv1.ObjectName(hnetGatewayClassName)
Expand Down
14 changes: 14 additions & 0 deletions internal/webhook/v1/httproute_webhook.go
Original file line number Diff line number Diff line change
Expand Up @@ -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

}
Expand Down Expand Up @@ -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
}

Expand Down
47 changes: 47 additions & 0 deletions internal/webhook/v1/validations/validate_addresses.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
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 !isHTTPRouteAndGatewayAddressesMatching(httprouteAddresses, gateway) {
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)
}

return nil
}

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
}

// 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])
}

// HTTPRoute and Gateway must agree exactly: both empty, or both the same value.
return gatewayAddresses == httprouteAddresses
}
70 changes: 70 additions & 0 deletions internal/webhook/v1/validations/validations_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -210,3 +210,73 @@ 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) {
// 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.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.Error("expected error when route specifies addresses but gateway has no addresses annotation, got nil")
}
}

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")
}
}
Loading