8000 fix udp/http listener validation logic (#6406) · nginx/kubernetes-ingress@5e96dd2 · GitHub
[go: up one dir, main page]

Skip to content

Commit 5e96dd2

Browse files
Jim Ryanpre-commit-ci[bot]jjngx
authored
fix udp/http listener validation logic (#6406)
* fix udp/http listener validation logic * lint and change field name * fix listener validation algo * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * remove values.yaml change * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * fix test * add more tests * remove commented out listener * fix test * delete comment * improve error message for duplicate ip port protocol combination --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Jakub Jarosz <99677300+jjngx@users.noreply.github.com>
1 parent 1dbc8eb commit 5e96dd2

File tree

7 files changed

+218
-37
lines changed

7 files changed

+218
-37
lines changed

pkg/apis/configuration/validation/globalconfiguration.go

Lines changed: 23 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -50,38 +50,33 @@ func (gcv *GlobalConfigurationValidator) validateGlobalConfigurationSpec(spec *c
5050

5151
func (gcv *GlobalConfigurationValidator) getValidListeners(listeners []conf_v1.Listener, fieldPath *field.Path) ([]conf_v1.Listener, field.ErrorList) {
5252
allErrs := field.ErrorList{}
53-
5453
listenerNames := sets.Set[string]{}
55-
ipv4PortProtocolCombinations := make(map[string]map[int]string) // map[IP]map[Port]Protocol
56-
ipv6PortProtocolCombinations := make(map[string]map[int]string)
54+
ipv4PortProtocolCombinations := make(map[string]map[int][]string) // map[IP]map[Port][]Protocol
55+
ipv6PortProtocolCombinations := make(map[string]map[int][]string)
5756
var validListeners []conf_v1.Listener
58-
5957
for i, l := range listeners {
6058
idxPath := fieldPath.Index(i)
6159
listenerErrs := gcv.validateListener(l, idxPath)
6260
if len(listenerErrs) > 0 {
6361
allErrs = append(allErrs, listenerErrs...)
6462
continue
6563
}
66-
6764
if err := gcv.checkForDuplicateName(listenerNames, l, idxPath); err != nil {
6865
allErrs = append(allErrs, err)
6966
continue
7067
}
71-
7268
if err := gcv.checkIPPortProtocolConflicts(ipv4PortProtocolCombinations, ipv4, l, fieldPath); err != nil {
7369
allErrs = append(allErrs, err)
70+
gcv.updatePortProtocolCombinations(ipv4PortProtocolCombinations, ipv4, l)
7471
continue
7572
}
76-
7773
if err := gcv.checkIPPortProtocolConflicts(ipv6PortProtocolCombinations, ipv6, l, fieldPath); err != nil {
7874
allErrs = append(allErrs, err)
75+
gcv.updatePortProtocolCombinations(ipv6PortProtocolCombinations, ipv6, l)
7976
continue
8077
}
81-
8278
gcv.updatePortProtocolCombinations(ipv4PortProtocolCombinations, ipv4, l)
8379
gcv.updatePortProtocolCombinations(ipv6PortProtocolCombinations, ipv6, l)
84-
8580
validListeners = append(validListeners, l)
8681
}
8782
return validListeners, allErrs
@@ -97,33 +92,37 @@ func (gcv *GlobalConfigurationValidator) checkForDuplicateName(listenerNames set
9792
}
9893

9994
// checkIPPortProtocolConflicts ensures no duplicate or conflicting port/protocol combinations exist.
100-
func (gcv *GlobalConfigurationValidator) checkIPPortProtocolConflicts(combinations map[string]map[int]string, ipType ipType, listener conf_v1.Listener, fieldPath *field.Path) *field.Error {
95+
func (gcv *GlobalConfigurationValidator) checkIPPortProtocolConflicts(combinations map[string]map[int][]string, ipType ipType, listener conf_v1.Listener, fieldPath *field.Path) *field.Error {
10196
ip := getIP(ipType, listener)
102-
10397
if combinations[ip] == nil {
104-
combinations[ip] = make(map[int]string) // map[ip]map[port]protocol
98+
combinations[ip] = make(map[int][]string) // map[ip]map[port][]protocol
10599
}
106-
107-
existingProtocol, exists := combinations[ip][listener.Port]
108-
if exists {
109-
if existingProtocol == listener.Protocol {
110-
return field.Duplicate(fieldPath, fmt.Sprintf("Listener %s: Duplicated port/protocol combination %d/%s", listener.Name, listener.Port, listener.Protocol))
111-
} else if listener.Protocol == "HTTP" || existingProtocol == "HTTP" {
112-
return field.Invalid(fieldPath.Child("port"), listener.Port, fmt.Sprintf("Listener %s: Port %d is used with a different protocol (current: %s, new: %s)", listener.Name, listener.Port, existingProtocol, listener.Protocol))
100+
existingProtocols, exists := combinations[ip][listener.Port]
101+
if !exists {
102+
return nil
103+
}
104+
for _, existingProtocol := range existingProtocols {
105+
switch listener.Protocol {
106+
case "HTTP", "TCP":
107+
if existingProtocol == "HTTP" || existingProtocol == "TCP" {
108+
return field.Invalid(fieldPath.Child("protocol"), listener.Protocol, fmt.Sprintf("Listener %s: Duplicated ip:port protocol combination %s:%d %s", listener.Name, ip, listener.Port, listener.Protocol))
109+
}
110+
case "UDP":
111+
if existingProtocol == "UDP" {
112+
return field.Invalid(fieldPath.Child("protocol"), listener.Protocol, fmt.Sprintf("Listener %s: Duplicated ip:port protocol combination %s:%d %s", listener.Name, ip, listener.Port, listener.Protocol))
113+
}
113114
}
114115
}
115-
116116
return nil
117117
}
118118

119119
// updatePortProtocolCombinations updates the port/protocol combinations map with the given listener's details for both IPv4 and IPv6.
120-
func (gcv *GlobalConfigurationValidator) updatePortProtocolCombinations(combinations map[string]map[int]string, ipType ipType, listener conf_v1.Listener) {
120+
func (gcv *GlobalConfigurationValidator) updatePortProtocolCombinations(combinations map[string]map[int][]string, ipType ipType, listener conf_v1.Listener) {
121121
ip := getIP(ipType, listener)
122-
123122
if combinations[ip] == nil {
124-
combinations[ip] = make(map[int]string)
123+
combinations[ip] = make(map[int][]string)
125124
}
126-
combinations[ip][listener.Port] = listener.Protocol
125+
combinations[ip][listener.Port] = append(combinations[ip][listener.Port], listener.Protocol)
127126
}
128127

129128
// getIP returns the appropriate IP address for the given ipType and listener.

pkg/apis/configuration/validation/globalconfiguration_test.go

Lines changed: 40 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -234,6 +234,27 @@ func TestValidateListeners_PassesOnValidIPListeners(t *testing.T) {
234234
{Name: "listener-2", IPv6IP: "2001:0db8:85a3:0000:0000:8a2e:0370:7334", Port: 8080, Protocol: "HTTP"},
235235
},
236236
},
237+
{
238+
name: "UDP and HTTP Listeners with Same Port",
239+
listeners: []conf_v1.Listener{
240+
{Name: "listener-1", IPv4IP: "127.0.0.1", Port: 8080, Protocol: "UDP"},
241+
{Name: "listener-2", IPv4IP: "127.0.0.1", Port: 8080, Protocol: "HTTP"},
242+
},
243+
},
244+
{
245+
name: "HTTP Listeners with Same Port but different IPv4 and IPv6 ip addresses",
246+
listeners: []conf_v1.Listener{
247+
{Name: "listener-1", IPv4IP: "127.0.0.2", IPv6IP: "::1", Port: 8080, Protocol: "HTTP"},
248+
{Name: "listener-2", IPv4IP: "127.0.0.1", Port: 8080, Protocol: "HTTP"},
249+
},
250+
},
251+
{
252+
name: "UDP and TCP Listeners with Same Port",
253+
listeners: []conf_v1.Listener{
254+
{Name: "listener-1", IPv4IP: "127.0.0.1", Port: 8080, Protocol: "UDP"},
255+
{Name: "listener-2", IPv4IP: "127.0.0.1", Port: 8080, Protocol: "TCP"},
256+
},
257+
},
237258
}
238259

239260
gcv := createGlobalConfigurationValidator()
@@ -587,7 +608,7 @@ func TestValidateListenerProtocol_FailsOnHttpListenerUsingSamePortAsTCPListener(
587608
}
588609
}
589610

590-
func TestValidateListenerProtocol_FailsOnHttpListenerUsingSamePortAsUDPListener(t *testing.T) {
611+
func TestValidateListenerProtocol_PassesOnHttpListenerUsingSamePortAsUDPListener(t *testing.T) {
591612
t.Parallel()
592613
listeners := []conf_v1.Listener{
593614
{
@@ -607,18 +628,23 @@ func TestValidateListenerProtocol_FailsOnHttpListenerUsingSamePortAsUDPListener(
607628
Port: 53,
608629
Protocol: "UDP",
609630
},
631+
{
632+
Name: "http-listener",
633+
Port: 53,
634+
Protocol: "HTTP",
635+
},
610636
}
611637
gcv := createGlobalConfigurationValidator()
612638
listeners, allErrs := gcv.getValidListeners(listeners, field.NewPath("listeners"))
613639
if diff := cmp.Diff(listeners, wantListeners); diff != "" {
614640
t.Errorf("getValidListeners() returned unexpected result: (-want +got):\n%s", diff)
615641
}
616-
if len(allErrs) == 0 {
617-
t.Errorf("validateListeners() returned no errors %v for invalid input", allErrs)
642+
if len(allErrs) != 0 {
643+
t.Errorf("validateListeners() returned errors %v invalid input", allErrs)
618644
}
619645
}
620646

621-
func TestValidateListenerProtocol_FailsOnHttpListenerUsingSamePortAsTCPAndUDPListener(t *testing.T) {
647+
func TestValidateListenerProtocol_FailsOnHttpListenerUsingSamePortAsTCP(t *testing.T) {
622648
t.Parallel()
623649
listeners := []conf_v1.Listener{
624650
{
@@ -649,15 +675,13 @@ func TestValidateListenerProtocol_FailsOnHttpListenerUsingSamePortAsTCPAndUDPLis
649675
Protocol: "UDP",
650676
},
651677
}
652-
653678
gcv := createGlobalConfigurationValidator()
654-
655679
listeners, allErrs := gcv.getValidListeners(listeners, field.NewPath("listeners"))
656680
if diff := cmp.Diff(listeners, wantListeners); diff != "" {
657681
t.Errorf("getValidListeners() returned unexpected result: (-want +got):\n%s", diff)
658682
}
659-
if len(allErrs) == 0 {
660-
t.Errorf("validateListeners() returned no errors %v for invalid input", allErrs)
683+
if len(allErrs) != 1 {
684+
t.Errorf("getValidListeners() returned unexpected number of errors. Got %d, want 1", len(allErrs))
661685
}
662686
}
663687

@@ -694,7 +718,7 @@ func TestValidateListenerProtocol_FailsOnTCPListenerUsingSamePortAsHTTPListener(
694718
}
695719
}
696720

697-
func TestValidateListenerProtocol_FailsOnUDPListenerUsingSamePortAsHTTPListener(t *testing.T) {
721+
func TestValidateListenerProtocol_PassesOnUDPListenerUsingSamePortAsHTTPListener(t *testing.T) {
698722
t.Parallel()
699723
listeners := []conf_v1.Listener{
700724
{
@@ -714,6 +738,11 @@ func TestValidateListenerProtocol_FailsOnUDPListenerUsingSamePortAsHTTPListener(
714738
Port: 53,
715739
Protocol: "HTTP",
716740
},
741+
{
742+
Name: "udp-listener",
743+
Port: 53,
744+
Protocol: "UDP",
745+
},
717746
}
718747

719748
gcv := createGlobalConfigurationValidator()
@@ -722,7 +751,7 @@ func TestValidateListenerProtocol_FailsOnUDPListenerUsingSamePortAsHTTPListener(
722751
if diff := cmp.Diff(listeners, wantListeners); diff != "" {
723752
t.Errorf("getValidListeners() returned unexpected result: (-want +got):\n%s", diff)
724753
}
725-
if len(allErrs) == 0 {
726-
t.Errorf("validateListeners() returned no errors %v for invalid input", allErrs)
754+
if len(allErrs) != 0 {
755+
t.Errorf("validateListeners() returned errors %v for valid input", allErrs)
727756
}
728757
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
apiVersion: k8s.nginx.org/v1
2+
kind: GlobalConfiguration
3+
metadata:
4+
name: nginx-configuration
5+
namespace: nginx-ingress
6+
spec:
7+
listeners:
8+
- name: udp-listener
9+
port: 5454
10+
protocol: UDP
11+
- name: http-listener
12+
port: 5454
13+
protocol: HTTP
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
apiVersion: k8s.nginx.org/v1
2+
kind: TransportServer
3+
metadata:
4+
name: transport-server
5+
spec:
6+
listener:
7+
name: udp-listener
8+
protocol: UDP
9+
upstreams:
10+
- name: dns-app
11+
service: coredns
12+
port: 5353
13+
action:
14+
pass: dns-app
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
apiVersion: k8s.nginx.org/v1
2+
kind: VirtualServer
3+
metadata:
4+
name: virtual-server-status
5+
spec:
6+
listener:
7+
http: http-listener
8+
host: virtual-server-status.example.com
9+
upstreams:
10+
- name: backend2
11+
service: backend2-svc
12+
port: 80
13+
- name: backend1
14+
service: backend1-svc
15+
port: 80
16+
routes:
17+
- path: /backend1
18+
action:
19+
pass: backend1
20+
- path: /backend2
21+
action:
22+
pass: backend2
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
import time
2+
3+
import pytest
4+
from settings import TEST_DATA
5+
from suite.utils.custom_resources_utils import (
6+
create_ts_from_yaml,
7+
delete_ts,
8+
patch_gc_from_yaml,
9+
read_custom_resource,
10+
read_ts,
11+
)
12+
from suite.utils.resources_utils import get_first_pod_name, get_ts_nginx_template_conf, wait_before_test
13+
from suite.utils.vs_vsr_resources_utils import get_vs_nginx_template_conf, patch_virtual_server_from_yaml
14+
15+
16+
@pytest.mark.vs
17+
@pytest.mark.ts
18+
@pytest.mark.parametrize(
19+
"crd_ingress_controller, virtual_server_setup, transport_server_setup",
20+
[
21+
(
22+
{
23+
"type": "complete",
24+
"extra_args": [
25+
"-enable-custom-resources",
26+
"-global-configuration=nginx-ingress/nginx-configuration",
27+
],
28+
},
29+
{"example": "virtual-server-status", "app_type": "simple"},
30+
{"example": "transport-server-status", "app_type": "simple"},
31+
)
32+
],
33+
indirect=True,
34+
)
35+
class TestUDPandHTTPListenersTogether:
36+
def test_udp_and_http_listeners_together(
37+
self,
38+
kube_apis,
39+
ingress_controller_prerequisites,
40+
crd_ingress_controller,
41+
virtual_server_setup,
42+
transport_server_setup,
43+
):
44+
45+
wait_before_test()
46+
existing_ts = read_ts(kube_apis.custom_objects, transport_server_setup.namespace, transport_server_setup.name)
47+
delete_ts(kube_apis.custom_objects, existing_ts, transport_server_setup.namespace)
48+
49+
global_config_file = f"{TEST_DATA}/udp-http-listeners-together/global-configuration.yaml"
50+
transport_server_file = f"{TEST_DATA}/udp-http-listeners-together/transport-server.yaml"
51+
virtual_server_file = f"{TEST_DATA}/udp-http-listeners-together/virtual-server.yaml"
52+
gc_resource_name = "nginx-configuration"
53+
gc_namespace = "nginx-ingress"
54+
55+
patch_gc_from_yaml(kube_apis.custom_objects, gc_resource_name, global_config_file, gc_namespace)
56+
create_ts_from_yaml(kube_apis.custom_objects, transport_server_file, transport_server_setup.namespace)
57+
patch_virtual_server_from_yaml(
58+
kube_apis.custom_objects, "virtual-server-status", virtual_server_file, virtual_server_setup.namespace
59+
)
60+
wait_before_test()
61+
62+
ic_pod_name = get_first_pod_name(kube_apis.v1, ingress_controller_prerequisites.namespace)
63+
ts_config = get_ts_nginx_template_conf(
64+
kube_apis.v1,
65+
transport_server_setup.namespace,
66+
transport_server_setup.name,
67+
ic_pod_name,
68+
ingress_controller_prerequisites.namespace,
69+
)
70+
vs_config = get_vs_nginx_template_conf(
71+
kube_apis.v1,
72+
virtual_server_setup.namespace,
73+
virtual_server_setup.vs_name,
74+
ic_pod_name,
75+
ingress_controller_prerequisites.namespace,
76+
)
77+
assert "listen 5454;" in vs_config
78+
assert "listen 5454 udp;" in ts_config
79+
80+
for _ in range(30):
81+
transport_server_response = read_custom_resource(
82+
kube_apis.custom_objects,
83+
transport_server_setup.namespace,
84+
"transportservers",
85+
"transport-server",
86+
)
87+
if "status" in transport_server_response and transport_server_response["status"]["state"] == "Valid":
88+
break
89+
time.sleep(1)
90+
else:
91+
pytest.fail("TransportServer status did not become 'Valid' within the timeout period")
92+
93+
for _ in range(30):
94+
virtual_server_response = read_custom_resource(
95+
kube_apis.custom_objects,
96+
virtual_server_setup.namespace,
97+
"virtualservers",
98+
"virtual-server-status",
99+
)
100+
if "status" in virtual_server_response and virtual_server_response["status"]["state"] == "Valid":
101+
break
102+
time.sleep(1)
103+
else:
104+
pytest.fail("VirtualServer status did not become 'Valid' within the timeout period")

tests/suite/test_virtual_server_custom_listeners.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -158,7 +158,7 @@ class TestVirtualServerCustomListeners:
158158
"https_listener_in_config": True,
159159
"expected_response_codes": [404, 404, 0, 200],
160160
"expected_vs_error_msg": "Listener http-8085 is not defined in GlobalConfiguration",
161-
"expected_gc_error_msg": "Listener http-8085: Duplicated port/protocol combination 8085/HTTP",
161+
"expected_gc_error_msg": "Listener http-8085: Duplicated ip:port protocol combination 0.0.0.0:8085 HTTP",
162162
},
163163
{
164164
"gc_yaml": "global-configuration-forbidden-port-http",
@@ -339,7 +339,7 @@ def test_custom_listeners(
339339
"https_listener_in_config": True,
340340
"expected_response_codes": [404, 404, 0, 200],
341341
"expected_vs_error_msg": "Listener http-8085 is not defined in GlobalConfiguration",
342-
"expected_gc_error_msg": "Listener http-8085: Duplicated port/protocol combination 8085/HTTP",
342+
"expected_gc_error_msg": "Listener http-8085: Duplicated ip:port protocol combination 0.0.0.0:8085 HTTP",
343343
},
344344
{
345345
"gc_yaml": "global-configuration-forbidden-port-http",

0 commit comments

Comments
 (0)
0