Skip to content
Open
Show file tree
Hide file tree
Changes from 5 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
2 changes: 2 additions & 0 deletions conformance/conformance.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import (
"sigs.k8s.io/gateway-api/apis/v1alpha2"
"sigs.k8s.io/gateway-api/apis/v1alpha3"
"sigs.k8s.io/gateway-api/apis/v1beta1"
xv1alpha1 "sigs.k8s.io/gateway-api/apisx/v1alpha1"
confv1 "sigs.k8s.io/gateway-api/conformance/apis/v1"
"sigs.k8s.io/gateway-api/conformance/tests"
conformanceconfig "sigs.k8s.io/gateway-api/conformance/utils/config"
Expand Down Expand Up @@ -58,6 +59,7 @@ func DefaultOptions(t *testing.T) suite.ConformanceOptions {
require.NoError(t, v1alpha3.Install(client.Scheme()))
require.NoError(t, v1alpha2.Install(client.Scheme()))
require.NoError(t, v1beta1.Install(client.Scheme()))
require.NoError(t, xv1alpha1.Install(client.Scheme()))
require.NoError(t, v1.Install(client.Scheme()))
require.NoError(t, apiextensionsv1.AddToScheme(client.Scheme()))

Expand Down
151 changes: 151 additions & 0 deletions conformance/tests/listenerset-domain-name-conflict.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
/*
Copyright 2025 The Kubernetes Authors.

Licensed 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 tests

import (
"testing"

"github.com/stretchr/testify/require"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"

gatewayv1 "sigs.k8s.io/gateway-api/apis/v1"
gatewayxv1a1 "sigs.k8s.io/gateway-api/apisx/v1alpha1"
"sigs.k8s.io/gateway-api/conformance/utils/http"
"sigs.k8s.io/gateway-api/conformance/utils/kubernetes"
"sigs.k8s.io/gateway-api/conformance/utils/suite"
"sigs.k8s.io/gateway-api/pkg/features"
)

func init() {
ConformanceTests = append(ConformanceTests, ListenerSetDomainNameConflict)
}

var ListenerSetDomainNameConflict = suite.ConformanceTest{
ShortName: "ListenerSetDomainNameConflict",
Description: "Listener Set listener with domain name conflict with a Gateway listener",
Features: []features.FeatureName{
features.SupportGateway,
features.SupportGatewayListenerSet,
features.SupportHTTPRoute,
},
Manifests: []string{
"tests/listenerset-domain-name-conflict.yaml",
},
Test: func(t *testing.T, suite *suite.ConformanceTestSuite) {
ns := "gateway-conformance-infra"

kubernetes.NamespacesMustBeReady(t, suite.Client, suite.TimeoutConfig, []string{ns})

testCases := []http.ExpectedResponse{
// Requests to the listeners without domain name conflict should work
{
Request: http.Request{Host: "gateway.com", Path: "/gateway-route"},
Backend: "infra-backend-v1",
Namespace: ns,
},
{
Request: http.Request{Host: "listenerset.com", Path: "/gateway-route"},
Backend: "infra-backend-v1",
Namespace: ns,
},
// Requests to the listener with domain name conflict should not work
{
Request: http.Request{Host: "conflict.com", Path: "/gateway-route"},
Response: http.Response{StatusCode: 404},
},
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, I don't know if we were clear enough about this in the GEP, but in this case, because there is a Gateway Listener, it should win over the ListenerSet listener. (see https://gateway-api.sigs.k8s.io/geps/gep-1713/#listener-precedence for the details here)

In this and other conflict cases, there should always be one "winning" Listener from somewhere, and that Listener should end up Accepted, and traffic should flow.

The intent of that is to stop the creation of conflicts from stopping traffic flowing (that's why "oldest first wins" for ListenerSets).

Copy link
Author

@davidjumani davidjumani Aug 21, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm confused since the GEP mentions

Implementations MUST treat the parent Gateways as having the merged list of all listeners from itself and attached ListenerSets and validation of this list of listeners MUST behave the same as if the list were part of a single Gateway with the relaxed listener name constraints.

and so I based the validation tests as though they were defined on a single gateway - My understanding was that the ordering was purely for merging them into a gateway and not validating them any different

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay I see #3978 has been updated. Shall I wait for it to merge before making the changes here ?

}

gwNN := types.NamespacedName{Name: "gateway-with-listenerset-http-listener", Namespace: ns}
gwRoutes := []types.NamespacedName{
{Name: "attaches-to-all-listeners", Namespace: ns},
}

gwAddr, err := kubernetes.WaitForGatewayAddress(t, suite.Client, suite.TimeoutConfig, kubernetes.NewGatewayRef(gwNN))
require.NoErrorf(t, err, "timed out waiting for Gateway address to be assigned")
for _, routeNN := range gwRoutes {
kubernetes.HTTPRouteMustHaveResolvedRefsConditionsTrue(t, suite.Client, suite.TimeoutConfig, routeNN, gwNN)
}

acceptedListenerConditions := []metav1.Condition{
{
Type: string(gatewayv1.ListenerConditionResolvedRefs),
Status: metav1.ConditionTrue,
Reason: "", // any reason
},
{
Type: string(gatewayv1.ListenerConditionAccepted),
Status: metav1.ConditionTrue,
Reason: "", // any reason
},
{
Type: string(gatewayv1.ListenerConditionProgrammed),
Status: metav1.ConditionTrue,
Reason: "", // any reason
},
}
conflictedListenerConditions := []metav1.Condition{
{
Type: string(gatewayv1.ListenerConditionAccepted),
Status: metav1.ConditionFalse,
Reason: string(gatewayv1.ListenerReasonHostnameConflict),
},
{
Type: string(gatewayv1.ListenerConditionProgrammed),
Status: metav1.ConditionFalse,
Reason: string(gatewayv1.ListenerReasonHostnameConflict),
},
{
Type: string(gatewayv1.ListenerConditionConflicted),
Status: metav1.ConditionTrue,
Reason: string(gatewayv1.ListenerReasonHostnameConflict),
},
}

kubernetes.GatewayMustHaveCondition(t, suite.Client, suite.TimeoutConfig, gwNN, metav1.Condition{
Type: string(gatewayv1.GatewayConditionAttachedListenerSets),
Status: metav1.ConditionTrue,
Reason: string(gatewayv1.GatewayReasonListenerSetsAttached),
})
kubernetes.GatewayListenerMustHaveConditions(t, suite.Client, suite.TimeoutConfig, gwNN, "gateway-com", acceptedListenerConditions)
kubernetes.GatewayListenerMustHaveConditions(t, suite.Client, suite.TimeoutConfig, gwNN, "conflict-com", conflictedListenerConditions)

lsNN := types.NamespacedName{Name: "listenerset-with-http-listener", Namespace: ns}
kubernetes.ListenerSetMustHaveCondition(t, suite.Client, suite.TimeoutConfig, lsNN, metav1.Condition{
Type: string(gatewayxv1a1.ListenerSetConditionAccepted),
Status: metav1.ConditionTrue,
Reason: string(gatewayxv1a1.ListenerSetReasonListenersNotValid),
})
kubernetes.ListenerSetMustHaveCondition(t, suite.Client, suite.TimeoutConfig, lsNN, metav1.Condition{
Type: string(gatewayxv1a1.ListenerSetConditionProgrammed),
Status: metav1.ConditionTrue,
Reason: string(gatewayxv1a1.ListenerSetReasonListenersNotValid),
})
kubernetes.ListenerSetListenerMustHaveConditions(t, suite.Client, suite.TimeoutConfig, lsNN, "listenerset-com", acceptedListenerConditions)
kubernetes.ListenerSetListenerMustHaveConditions(t, suite.Client, suite.TimeoutConfig, lsNN, "conflict-com", conflictedListenerConditions)

for i := range testCases {
// Declare tc here to avoid loop variable
// reuse issues across parallel tests.
tc := testCases[i]
t.Run(tc.GetTestCaseName(i), func(t *testing.T) {
t.Parallel()
http.MakeRequestAndExpectEventuallyConsistentResponse(t, suite.RoundTripper, suite.TimeoutConfig, gwAddr, tc)
})
}
},
}
70 changes: 70 additions & 0 deletions conformance/tests/listenerset-domain-name-conflict.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
name: gateway-with-listenerset-http-listener
namespace: gateway-conformance-infra
spec:
gatewayClassName: "{GATEWAY_CLASS_NAME}"
listeners:
- name: gateway-com
port: 80
protocol: HTTP
hostname: "gateway.com"
allowedRoutes:
namespaces:
from: All
- name: conflict-com
port: 80
protocol: HTTP
hostname: "conflict.com"
allowedRoutes:
namespaces:
from: All
allowedListeners:
namespaces:
from: Same
---
apiVersion: gateway.networking.x-k8s.io/v1alpha1
kind: XListenerSet
metadata:
name: listenerset-with-http-listener
namespace: gateway-conformance-infra
spec:
parentRef:
kind: Gateway
group: gateway.networking.k8s.io
name: gateway-with-listenerset-http-listener
namespace: gateway-conformance-infra
listeners:
- name: conflict-com
port: 80
protocol: HTTP
hostname: "conflict.com"
allowedRoutes:
namespaces:
from: All
- name: listenerset-com
port: 80
protocol: HTTP
hostname: "listenerset.com"
allowedRoutes:
namespaces:
from: All
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: attaches-to-all-listeners
namespace: gateway-conformance-infra
spec:
parentRefs:
- name: gateway-with-listenerset-http-listener
namespace: gateway-conformance-infra
rules:
- matches:
- path:
type: PathPrefix
value: /gateway-route
backendRefs:
- name: infra-backend-v1
port: 8080
Loading