This is an automated email from the ASF dual-hosted git repository.

AlinsRan pushed a commit to branch feat/gateway-api-1.6.0
in repository https://gitbox.apache.org/repos/asf/apisix-ingress-controller.git


The following commit(s) were added to refs/heads/feat/gateway-api-1.6.0 by this 
push:
     new d7a05234 fix: address maintainer review on Gateway API 1.6 frontend 
mTLS and route attachment
d7a05234 is described below

commit d7a0523423432f4877d2d03aec01fab95f740320
Author: AlinsRan <[email protected]>
AuthorDate: Thu Jul 23 12:12:35 2026 +0800

    fix: address maintainer review on Gateway API 1.6 frontend mTLS and route 
attachment
    
    - frontendValidation now resolves for HTTPS listeners only 
(spec.tls.frontend
      applies to HTTPS in Gateway API v1.6), in both the translator and status 
paths,
      so a TLS listener no longer gets downstream mTLS attached.
    - Reject frontendValidation mode AllowInsecureFallback: APISIX can only 
enforce
      strict client-cert verification, so program an error instead of silently 
doing
      the opposite (strict) behaviour.
    - Frontend CA validation now sets Accepted=False/NoValidCACertificate when 
no CA
      ref resolves, and uses InvalidCACertificateRef/InvalidCACertificateKind 
reasons;
      a single valid ref keeps the listener Accepted.
    - Route attachment: listeners on a port with a conflicting tls.mode are now
      ineligible in ParseRouteParentRefs and report zero attached routes, so a 
route
      cannot be programmed on a listener that reports 
Accepted=False/Programmed=False.
    - ParseRouteParentRefs selects the parent reason after evaluating all 
listeners,
      so the result is independent of listener order (a hostname mismatch on a
      compatible listener is preserved over an unrelated incompatible listener).
---
 internal/adc/translator/gateway.go           |  13 ++-
 internal/adc/translator/gateway_test.go      |  33 +++++++
 internal/controller/gateway_controller.go    |   6 +-
 internal/controller/utils.go                 |  83 ++++++++++++----
 internal/controller/utils_frontendca_test.go | 107 ++++++++++++++++++++
 internal/controller/utils_parentref_test.go  | 142 +++++++++++++++++++++++++++
 6 files changed, 358 insertions(+), 26 deletions(-)

diff --git a/internal/adc/translator/gateway.go 
b/internal/adc/translator/gateway.go
index 2ec64946..fb659f3b 100644
--- a/internal/adc/translator/gateway.go
+++ b/internal/adc/translator/gateway.go
@@ -159,9 +159,9 @@ func (t *Translator) translateSecret(tctx 
*provider.TranslateContext, listener g
 // spec.tls.frontend: Default applies to all HTTPS listeners, and a PerPort 
entry
 // overrides it for listeners on the matching port.
 func frontendTLSValidation(obj *gatewayv1.Gateway, listener 
gatewayv1.Listener) *gatewayv1.FrontendTLSValidation {
-       // Downstream mTLS only applies where the Gateway terminates TLS 
(HTTPS/TLS
-       // listeners); never enable client-cert validation on plaintext 
listeners.
-       if listener.Protocol != gatewayv1.HTTPSProtocolType && 
listener.Protocol != gatewayv1.TLSProtocolType {
+       // In Gateway API v1.6 spec.tls.frontend applies only to HTTPS 
listeners, so
+       // never resolve downstream mTLS for any other protocol (including TLS).
+       if listener.Protocol != gatewayv1.HTTPSProtocolType {
                return nil
        }
        if obj.Spec.TLS == nil || obj.Spec.TLS.Frontend == nil {
@@ -185,6 +185,13 @@ func (t *Translator) translateFrontendValidation(tctx 
*provider.TranslateContext
        if validation == nil || len(validation.CACertificateRefs) == 0 {
                return nil, nil
        }
+       // APISIX can only enforce strict client-certificate verification 
(setting ca
+       // turns on ssl_verify_client). AllowInsecureFallback ("accept even if 
the
+       // client cert is missing or fails verification") cannot be expressed, 
so
+       // reject it instead of silently programming the opposite, strict 
behaviour.
+       if validation.Mode == gatewayv1.AllowInsecureFallback {
+               return nil, fmt.Errorf("unsupported frontendValidation mode %q 
in listener %s: APISIX cannot make client certificate verification optional", 
validation.Mode, listener.Name)
+       }
 
        cas := make([]string, 0, len(validation.CACertificateRefs))
        for _, ref := range validation.CACertificateRefs {
diff --git a/internal/adc/translator/gateway_test.go 
b/internal/adc/translator/gateway_test.go
index ec1d7ee0..65644f7b 100644
--- a/internal/adc/translator/gateway_test.go
+++ b/internal/adc/translator/gateway_test.go
@@ -187,6 +187,39 @@ func TestTranslateSecret_FrontendValidation(t *testing.T) {
                assert.Nil(t, sslObjs[0].Client)
        })
 
+       t.Run("AllowInsecureFallback mode is rejected", func(t *testing.T) {
+               tr := &Translator{Log: logr.Discard()}
+               gateway := newTLSGateway(&gatewayv1.FrontendTLSValidation{
+                       Mode: gatewayv1.AllowInsecureFallback,
+                       CACertificateRefs: []gatewayv1.ObjectReference{
+                               {Group: "", Kind: "ConfigMap", Name: "ca-cm"},
+                       },
+               })
+               tctx := newTranslateContextWithTLS()
+
+               _, err := tr.translateSecret(tctx, gateway.Spec.Listeners[0], 
gateway)
+               require.Error(t, err)
+               assert.Contains(t, err.Error(), "AllowInsecureFallback")
+       })
+
+       t.Run("frontendValidation is ignored on a non-HTTPS listener", func(t 
*testing.T) {
+               // spec.tls.frontend applies only to HTTPS listeners in Gateway 
API v1.6, so a
+               // TLS (Terminate) listener must not get downstream mTLS 
attached.
+               tr := &Translator{Log: logr.Discard()}
+               gateway := newTLSGateway(&gatewayv1.FrontendTLSValidation{
+                       CACertificateRefs: []gatewayv1.ObjectReference{
+                               {Group: "", Kind: "ConfigMap", Name: "ca-cm"},
+                       },
+               })
+               gateway.Spec.Listeners[0].Protocol = gatewayv1.TLSProtocolType
+               tctx := newTranslateContextWithTLS()
+
+               sslObjs, err := tr.translateSecret(tctx, 
gateway.Spec.Listeners[0], gateway)
+               require.NoError(t, err)
+               require.Len(t, sslObjs, 1)
+               assert.Nil(t, sslObjs[0].Client, "client mTLS must not be set 
for a non-HTTPS listener")
+       })
+
        t.Run("missing CA ConfigMap returns error", func(t *testing.T) {
                tr := &Translator{Log: logr.Discard()}
                gateway := newTLSGateway(&gatewayv1.FrontendTLSValidation{
diff --git a/internal/controller/gateway_controller.go 
b/internal/controller/gateway_controller.go
index f1a4135d..ff1743e5 100644
--- a/internal/controller/gateway_controller.go
+++ b/internal/controller/gateway_controller.go
@@ -476,9 +476,9 @@ func (r *GatewayReconciler) processInfrastructure(tctx 
*provider.TranslateContex
 // validation (Gateway API v1.6 spec.tls.frontend) that applies to the given 
HTTPS
 // listener: a PerPort entry matching the listener's port overrides the 
Default.
 func frontendTLSValidationForListener(gateway *gatewayv1.Gateway, listener 
gatewayv1.Listener) *gatewayv1.FrontendTLSValidation {
-       // Downstream mTLS only applies where the Gateway terminates TLS 
(HTTPS/TLS
-       // listeners); never enable client-cert validation on plaintext 
listeners.
-       if listener.Protocol != gatewayv1.HTTPSProtocolType && 
listener.Protocol != gatewayv1.TLSProtocolType {
+       // In Gateway API v1.6 spec.tls.frontend applies only to HTTPS 
listeners, so
+       // never resolve downstream mTLS for any other protocol (including TLS).
+       if listener.Protocol != gatewayv1.HTTPSProtocolType {
                return nil
        }
        if gateway.Spec.TLS == nil || gateway.Spec.TLS.Frontend == nil {
diff --git a/internal/controller/utils.go b/internal/controller/utils.go
index 4ac80679..734c3ac5 100644
--- a/internal/controller/utils.go
+++ b/internal/controller/utils.go
@@ -365,6 +365,15 @@ func ParseRouteParentRefs(
                var listenerName string
                var matchedListener gatewayv1.Listener
                var matchedListeners []gatewayv1.Listener
+               // Aggregate the reasons listeners were rejected so the final 
parent reason
+               // does not depend on listener order: a hostname mismatch on an 
otherwise
+               // compatible listener is preserved over an unrelated 
incompatible listener.
+               var hostnameMismatch, notAllowed bool
+
+               // A TLS listener on a port whose tls.mode conflicts cannot be 
programmed, so
+               // routes must not attach to it; otherwise the route would be 
translated and
+               // served even though the listener reports 
Accepted=False/Programmed=False.
+               tlsConflictPorts := portsWithConflictingTLSMode(&gateway)
 
                // Track if sectionName was explicitly specified
                sectionNameSpecified := parentRef.SectionName != nil && 
*parentRef.SectionName != ""
@@ -382,16 +391,20 @@ func ParseRouteParentRefs(
                                }
                        }
 
+                       if listener.Protocol == gatewayv1.TLSProtocolType && 
tlsConflictPorts[listener.Port] {
+                               notAllowed = true
+                               continue
+                       }
+
                        if ok, _ := routeMatchesListenerType(route, listener); 
!ok {
                                // The listener exists but its protocol cannot 
carry this route kind,
-                               // which the spec reports as 
NotAllowedByListeners. A parentRef that
-                               // matches no listener at all stays 
NoMatchingParent.
-                               reason = 
gatewayv1.RouteReasonNotAllowedByListeners
+                               // which the spec reports as 
NotAllowedByListeners.
+                               notAllowed = true
                                continue
                        }
 
                        if !routeHostnamesIntersectsWithListenerHostname(route, 
listener) {
-                               reason = 
gatewayv1.RouteReasonNoMatchingListenerHostname
+                               hostnameMismatch = true
                                continue
                        }
 
@@ -403,7 +416,7 @@ func ParseRouteParentRefs(
                                        "gateway", gateway.Name)
                        }
                        if !ok {
-                               reason = 
gatewayv1.RouteReasonNotAllowedByListeners
+                               notAllowed = true
                                continue
                        }
 
@@ -428,6 +441,19 @@ func ParseRouteParentRefs(
                        }
                }
 
+               // Select the parent reason after evaluating every listener so 
the outcome is
+               // independent of listener order. A hostname mismatch on an 
otherwise
+               // compatible listener is more specific than a generic 
NotAllowedByListeners;
+               // a parentRef that matches no listener at all stays 
NoMatchingParent.
+               if !matched {
+                       switch {
+                       case hostnameMismatch:
+                               reason = 
gatewayv1.RouteReasonNoMatchingListenerHostname
+                       case notAllowed:
+                               reason = 
gatewayv1.RouteReasonNotAllowedByListeners
+                       }
+               }
+
                if matched {
                        gateways = append(gateways, RouteParentRefContext{
                                Gateway:      &gateway,
@@ -765,6 +791,12 @@ func routeMatchesListenerType(route client.Object, 
listener gatewayv1.Listener)
 }
 
 func getAttachedRoutesForListener(ctx context.Context, mgrc client.Client, 
gateway gatewayv1.Gateway, listener gatewayv1.Listener) (int32, error) {
+       // A TLS listener on a port with a conflicting tls.mode is not 
programmable, so
+       // no route attaches to it; report zero attached routes to match that.
+       if listener.Protocol == gatewayv1.TLSProtocolType && 
portsWithConflictingTLSMode(&gateway)[listener.Port] {
+               return 0, nil
+       }
+
        routes := []types.RouteAdapter{}
        routeList := []client.ObjectList{}
 
@@ -1017,7 +1049,7 @@ func getListenerStatus(
                        // In Gateway API v1.6 it is declared at the Gateway 
level (spec.tls.frontend).
                        if validation := 
frontendTLSValidationForListener(gateway, listener); validation != nil &&
                                (listener.TLS.Mode == nil || *listener.TLS.Mode 
== gatewayv1.TLSModeTerminate) {
-                               validateListenerFrontendValidation(ctx, mrgc, 
gateway, validation, &conditionResolvedRefs, &conditionProgrammed)
+                               validateListenerFrontendValidation(ctx, mrgc, 
gateway, validation, &conditionResolvedRefs, &conditionProgrammed, 
&conditionAccepted)
                        }
                }
 
@@ -1070,7 +1102,7 @@ func validateListenerFrontendValidation(
        mrgc client.Client,
        gateway *gatewayv1.Gateway,
        frontendValidation *gatewayv1.FrontendTLSValidation,
-       conditionResolvedRefs, conditionProgrammed *metav1.Condition,
+       conditionResolvedRefs, conditionProgrammed, conditionAccepted 
*metav1.Condition,
 ) {
        setInvalid := func(reason gatewayv1.ListenerConditionReason, message 
string) {
                conditionResolvedRefs.Status = metav1.ConditionFalse
@@ -1080,20 +1112,24 @@ func validateListenerFrontendValidation(
                conditionProgrammed.Reason = 
string(gatewayv1.ListenerReasonInvalid)
        }
 
+       // Count the CA references that resolve to a usable certificate. Any 
invalid
+       // ref makes ResolvedRefs=False; only when none remain valid is the 
listener
+       // Accepted=False with NoValidCACertificate (Gateway API v1.6 
semantics).
+       valid := 0
        for _, ref := range frontendValidation.CACertificateRefs {
                if ref.Group != "" && string(ref.Group) != corev1.GroupName {
-                       
setInvalid(gatewayv1.ListenerReasonInvalidCertificateRef,
+                       
setInvalid(gatewayv1.ListenerReasonInvalidCACertificateKind,
                                fmt.Sprintf(`Invalid Group for 
caCertificateRef, expect "", got "%s"`, ref.Group))
-                       return
+                       continue
                }
                kind := KindConfigMap
                if ref.Kind != "" {
                        kind = string(ref.Kind)
                }
                if kind != KindConfigMap && kind != KindSecret {
-                       
setInvalid(gatewayv1.ListenerReasonInvalidCertificateRef,
+                       
setInvalid(gatewayv1.ListenerReasonInvalidCACertificateKind,
                                fmt.Sprintf(`Invalid Kind for caCertificateRef, 
expect "ConfigMap" or "Secret", got "%s"`, ref.Kind))
-                       return
+                       continue
                }
                if permitted := checkReferenceGrant(ctx,
                        mrgc,
@@ -1110,7 +1146,7 @@ func validateListenerFrontendValidation(
                        },
                ); !permitted {
                        setInvalid(gatewayv1.ListenerReasonRefNotPermitted, 
"caCertificateRefs cross namespaces is not permitted")
-                       return
+                       continue
                }
                nn := k8stypes.NamespacedName{
                        Namespace: string(*cmp.Or(ref.Namespace, 
(*gatewayv1.Namespace)(&gateway.Namespace))),
@@ -1120,26 +1156,33 @@ func validateListenerFrontendValidation(
                case KindConfigMap:
                        var configMap corev1.ConfigMap
                        if err := mrgc.Get(ctx, nn, &configMap); err != nil {
-                               
setInvalid(gatewayv1.ListenerReasonInvalidCertificateRef, err.Error())
-                               return
+                               
setInvalid(gatewayv1.ListenerReasonInvalidCACertificateRef, err.Error())
+                               continue
                        }
                        if _, err := 
sslutils.ExtractCAFromConfigMap(&configMap); err != nil {
-                               
setInvalid(gatewayv1.ListenerReasonInvalidCertificateRef,
+                               
setInvalid(gatewayv1.ListenerReasonInvalidCACertificateRef,
                                        fmt.Sprintf("Malformed CA ConfigMap 
referenced: %s", err.Error()))
-                               return
+                               continue
                        }
                case KindSecret:
                        var secret corev1.Secret
                        if err := mrgc.Get(ctx, nn, &secret); err != nil {
-                               
setInvalid(gatewayv1.ListenerReasonInvalidCertificateRef, err.Error())
-                               return
+                               
setInvalid(gatewayv1.ListenerReasonInvalidCACertificateRef, err.Error())
+                               continue
                        }
                        if _, err := sslutils.ExtractCAFromSecret(&secret); err 
!= nil {
-                               
setInvalid(gatewayv1.ListenerReasonInvalidCertificateRef,
+                               
setInvalid(gatewayv1.ListenerReasonInvalidCACertificateRef,
                                        fmt.Sprintf("Malformed CA Secret 
referenced: %s", err.Error()))
-                               return
+                               continue
                        }
                }
+               valid++
+       }
+
+       if valid == 0 {
+               conditionAccepted.Status = metav1.ConditionFalse
+               conditionAccepted.Reason = 
string(gatewayv1.ListenerReasonNoValidCACertificate)
+               conditionAccepted.Message = "no valid CA certificate for 
frontend client validation"
        }
 }
 
diff --git a/internal/controller/utils_frontendca_test.go 
b/internal/controller/utils_frontendca_test.go
new file mode 100644
index 00000000..4c2fd8b0
--- /dev/null
+++ b/internal/controller/utils_frontendca_test.go
@@ -0,0 +1,107 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+package controller
+
+import (
+       "context"
+       "testing"
+
+       "github.com/stretchr/testify/assert"
+       "github.com/stretchr/testify/require"
+       corev1 "k8s.io/api/core/v1"
+       metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+       "k8s.io/apimachinery/pkg/runtime"
+       clientgoscheme "k8s.io/client-go/kubernetes/scheme"
+       "sigs.k8s.io/controller-runtime/pkg/client/fake"
+       gatewayv1 "sigs.k8s.io/gateway-api/apis/v1"
+)
+
+const frontendCACert = `-----BEGIN CERTIFICATE-----
+MIIBQzCB6qADAgECAgEBMAoGCCqGSM49BAMCMBIxEDAOBgNVBAMTB3Rlc3QtY2Ew
+HhcNNzAwMTAxMDAwMDAwWhcNMzgwMTE5MDMxNDA4WjASMRAwDgYDVQQDEwd0ZXN0
+LWNhMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEJo4AsM30ZHN+mYeHjqwceGBz
+V2bMz1+OyNXuaPYVrSF7HShZhanOYNHb6QLNhjGxMsBDQHVLolPjyTQJp9R5GqMx
+MC8wDgYDVR0PAQH/BAQDAgIEMB0GA1UdDgQWBBRzjh0YVmnpN/cFJziO0aYySuti
+4DAKBggqhkjOPQQDAgNIADBFAiEA7fEGiQA7wX0LrrkRH4KplAPOgVV5Kvm/1dv1
+3TLq9ssCIHKkv2dhydRvv36KC1WsRDcrl7W+7YmEnCS9PZfb8agM
+-----END CERTIFICATE-----`
+
+func newFrontendConditions() (resolvedRefs, programmed, accepted 
metav1.Condition) {
+       mk := func(t gatewayv1.ListenerConditionType, r 
gatewayv1.ListenerConditionReason) metav1.Condition {
+               return metav1.Condition{Type: string(t), Status: 
metav1.ConditionTrue, Reason: string(r)}
+       }
+       return mk(gatewayv1.ListenerConditionResolvedRefs, 
gatewayv1.ListenerReasonResolvedRefs),
+               mk(gatewayv1.ListenerConditionProgrammed, 
gatewayv1.ListenerReasonProgrammed),
+               mk(gatewayv1.ListenerConditionAccepted, 
gatewayv1.ListenerReasonAccepted)
+}
+
+// TestValidateListenerFrontendValidation checks the listener conditions 
produced
+// for Gateway API v1.6 frontend client-cert CA validation.
+func TestValidateListenerFrontendValidation(t *testing.T) {
+       scheme := runtime.NewScheme()
+       require.NoError(t, clientgoscheme.AddToScheme(scheme))
+       require.NoError(t, gatewayv1.Install(scheme))
+
+       gateway := &gatewayv1.Gateway{ObjectMeta: metav1.ObjectMeta{Namespace: 
"default", Name: "gw"}}
+       ref := func(kind, name string) gatewayv1.ObjectReference {
+               return gatewayv1.ObjectReference{Group: "", Kind: 
gatewayv1.Kind(kind), Name: gatewayv1.ObjectName(name)}
+       }
+
+       t.Run("missing CA ConfigMap: ResolvedRefs and Accepted both False", 
func(t *testing.T) {
+               cli := fake.NewClientBuilder().WithScheme(scheme).Build()
+               resolvedRefs, programmed, accepted := newFrontendConditions()
+               validateListenerFrontendValidation(context.Background(), cli, 
gateway,
+                       &gatewayv1.FrontendTLSValidation{CACertificateRefs: 
[]gatewayv1.ObjectReference{ref("ConfigMap", "missing")}},
+                       &resolvedRefs, &programmed, &accepted)
+
+               assert.Equal(t, metav1.ConditionFalse, resolvedRefs.Status)
+               assert.Equal(t, 
string(gatewayv1.ListenerReasonInvalidCACertificateRef), resolvedRefs.Reason)
+               assert.Equal(t, metav1.ConditionFalse, accepted.Status)
+               assert.Equal(t, 
string(gatewayv1.ListenerReasonNoValidCACertificate), accepted.Reason)
+       })
+
+       t.Run("unsupported Kind: InvalidCACertificateKind", func(t *testing.T) {
+               cli := fake.NewClientBuilder().WithScheme(scheme).Build()
+               resolvedRefs, programmed, accepted := newFrontendConditions()
+               validateListenerFrontendValidation(context.Background(), cli, 
gateway,
+                       &gatewayv1.FrontendTLSValidation{CACertificateRefs: 
[]gatewayv1.ObjectReference{ref("Pod", "x")}},
+                       &resolvedRefs, &programmed, &accepted)
+
+               assert.Equal(t, 
string(gatewayv1.ListenerReasonInvalidCACertificateKind), resolvedRefs.Reason)
+               assert.Equal(t, metav1.ConditionFalse, accepted.Status)
+               assert.Equal(t, 
string(gatewayv1.ListenerReasonNoValidCACertificate), accepted.Reason)
+       })
+
+       t.Run("one valid ref keeps Accepted True while ResolvedRefs stays 
False", func(t *testing.T) {
+               validCM := &corev1.ConfigMap{
+                       ObjectMeta: metav1.ObjectMeta{Namespace: "default", 
Name: "ca"},
+                       Data:       
map[string]string{corev1.ServiceAccountRootCAKey: frontendCACert},
+               }
+               cli := 
fake.NewClientBuilder().WithScheme(scheme).WithObjects(validCM).Build()
+               resolvedRefs, programmed, accepted := newFrontendConditions()
+               validateListenerFrontendValidation(context.Background(), cli, 
gateway,
+                       &gatewayv1.FrontendTLSValidation{CACertificateRefs: 
[]gatewayv1.ObjectReference{
+                               ref("ConfigMap", "ca"),
+                               ref("ConfigMap", "missing"),
+                       }},
+                       &resolvedRefs, &programmed, &accepted)
+
+               assert.Equal(t, metav1.ConditionFalse, resolvedRefs.Status, "an 
invalid ref still fails ResolvedRefs")
+               assert.Equal(t, metav1.ConditionTrue, accepted.Status, 
"Accepted stays True while at least one CA is valid")
+       })
+}
diff --git a/internal/controller/utils_parentref_test.go 
b/internal/controller/utils_parentref_test.go
new file mode 100644
index 00000000..3c915c58
--- /dev/null
+++ b/internal/controller/utils_parentref_test.go
@@ -0,0 +1,142 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+package controller
+
+import (
+       "context"
+       "testing"
+
+       "github.com/go-logr/logr"
+       "github.com/stretchr/testify/assert"
+       "github.com/stretchr/testify/require"
+       metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+       "k8s.io/apimachinery/pkg/runtime"
+       "k8s.io/utils/ptr"
+       "sigs.k8s.io/controller-runtime/pkg/client/fake"
+       gatewayv1 "sigs.k8s.io/gateway-api/apis/v1"
+       gatewayv1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2"
+
+       "github.com/apache/apisix-ingress-controller/internal/controller/config"
+)
+
+func parentRefTestScheme(t *testing.T) *runtime.Scheme {
+       t.Helper()
+       scheme := runtime.NewScheme()
+       require.NoError(t, gatewayv1.Install(scheme))
+       require.NoError(t, gatewayv1alpha2.Install(scheme))
+       return scheme
+}
+
+func newParentRefGatewayClass() *gatewayv1.GatewayClass {
+       return &gatewayv1.GatewayClass{
+               ObjectMeta: metav1.ObjectMeta{Name: "apisix"},
+               Spec: gatewayv1.GatewayClassSpec{
+                       ControllerName: 
gatewayv1.GatewayController(config.ControllerConfig.ControllerName),
+               },
+       }
+}
+
+// TestParseRouteParentRefs_ReasonOrderIndependent verifies the parent 
condition
+// reason does not depend on the order of gateway.spec.listeners. A
+// protocol-compatible listener that only fails hostname intersection must 
yield
+// NoMatchingListenerHostname regardless of where an unrelated incompatible
+// listener sits in the list.
+func TestParseRouteParentRefs_ReasonOrderIndependent(t *testing.T) {
+       scheme := parentRefTestScheme(t)
+
+       httpListener := gatewayv1.Listener{
+               Name:     "http",
+               Port:     80,
+               Protocol: gatewayv1.HTTPProtocolType,
+               Hostname: ptr.To(gatewayv1.Hostname("foo.example.com")),
+       }
+       tcpListener := gatewayv1.Listener{
+               Name:     "tcp",
+               Port:     9000,
+               Protocol: gatewayv1.TCPProtocolType,
+       }
+       route := &gatewayv1.HTTPRoute{
+               ObjectMeta: metav1.ObjectMeta{Namespace: "default", Name: "r"},
+               Spec: gatewayv1.HTTPRouteSpec{
+                       Hostnames: []gatewayv1.Hostname{"bar.com"},
+               },
+       }
+
+       for _, tc := range []struct {
+               name      string
+               listeners []gatewayv1.Listener
+       }{
+               {"compatible-first", []gatewayv1.Listener{httpListener, 
tcpListener}},
+               {"incompatible-first", []gatewayv1.Listener{tcpListener, 
httpListener}},
+       } {
+               t.Run(tc.name, func(t *testing.T) {
+                       gw := &gatewayv1.Gateway{
+                               ObjectMeta: metav1.ObjectMeta{Namespace: 
"default", Name: "gw"},
+                               Spec: gatewayv1.GatewaySpec{
+                                       GatewayClassName: "apisix",
+                                       Listeners:        tc.listeners,
+                               },
+                       }
+                       cli := fake.NewClientBuilder().WithScheme(scheme).
+                               WithObjects(newParentRefGatewayClass(), 
gw).Build()
+
+                       got, err := ParseRouteParentRefs(context.Background(), 
cli, logr.Discard(), route,
+                               []gatewayv1.ParentReference{{Name: "gw"}})
+                       require.NoError(t, err)
+                       require.Len(t, got, 1)
+                       cond := got[0].Conditions[0]
+                       assert.Equal(t, metav1.ConditionFalse, cond.Status)
+                       assert.Equal(t, 
string(gatewayv1.RouteReasonNoMatchingListenerHostname), cond.Reason,
+                               "hostname mismatch on the compatible listener 
must win regardless of listener order")
+               })
+       }
+}
+
+// TestParseRouteParentRefs_ConflictingTLSModePort verifies a route does not
+// attach to a listener whose port carries a conflicting tls.mode: such a 
listener
+// is not programmable, so the route must not be Accepted.
+func TestParseRouteParentRefs_ConflictingTLSModePort(t *testing.T) {
+       scheme := parentRefTestScheme(t)
+       terminate := gatewayv1.TLSModeTerminate
+       passthrough := gatewayv1.TLSModePassthrough
+
+       gw := &gatewayv1.Gateway{
+               ObjectMeta: metav1.ObjectMeta{Namespace: "default", Name: "gw"},
+               Spec: gatewayv1.GatewaySpec{
+                       GatewayClassName: "apisix",
+                       Listeners: []gatewayv1.Listener{
+                               {Name: "tls-a", Port: 443, Protocol: 
gatewayv1.TLSProtocolType, TLS: &gatewayv1.ListenerTLSConfig{Mode: &terminate}},
+                               {Name: "tls-b", Port: 443, Protocol: 
gatewayv1.TLSProtocolType, TLS: &gatewayv1.ListenerTLSConfig{Mode: 
&passthrough}},
+                       },
+               },
+       }
+       route := &gatewayv1alpha2.TLSRoute{
+               ObjectMeta: metav1.ObjectMeta{Namespace: "default", Name: "tr"},
+       }
+       cli := fake.NewClientBuilder().WithScheme(scheme).
+               WithObjects(newParentRefGatewayClass(), gw).Build()
+
+       got, err := ParseRouteParentRefs(context.Background(), cli, 
logr.Discard(), route,
+               []gatewayv1.ParentReference{{Name: "gw"}})
+       require.NoError(t, err)
+       require.Len(t, got, 1)
+       cond := got[0].Conditions[0]
+       assert.Equal(t, metav1.ConditionFalse, cond.Status,
+               "route must not attach to a conflicting-tls-mode port")
+       assert.Equal(t, string(gatewayv1.RouteReasonNotAllowedByListeners), 
cond.Reason)
+}

Reply via email to