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)
+}