diff --git a/charts/nginx-ingress/values.schema.json b/charts/nginx-ingress/values.schema.json index 4679145193..423dd014c5 100644 --- a/charts/nginx-ingress/values.schema.json +++ b/charts/nginx-ingress/values.schema.json @@ -995,7 +995,7 @@ "dns-tcp" ] }, - "ipv4ip": { + "ipv4": { "type": "string", "default": "", "title": "The ipv4 ip", @@ -1003,7 +1003,7 @@ "127.0.0.1" ] }, - "ipv6ip": { + "ipv6": { "type": "string", "default": "", "title": "The ipv6 ip", diff --git a/docs/content/configuration/global-configuration/globalconfiguration-resource.md b/docs/content/configuration/global-configuration/globalconfiguration-resource.md index 66cf33aa27..352b2b4f78 100644 --- a/docs/content/configuration/global-configuration/globalconfiguration-resource.md +++ b/docs/content/configuration/global-configuration/globalconfiguration-resource.md @@ -74,8 +74,8 @@ The `listeners:` key defines a listener (a combination of a protocol and a port) | *port* | The port of the listener. The port must fall into the range ``1..65535`` with the following exceptions: ``80``, ``443``, the [status port](/nginx-ingress-controller/logging-and-monitoring/status-page), the [Prometheus metrics port](/nginx-ingress-controller/logging-and-monitoring/prometheus). Among all listeners, only a single combination of a port-protocol is allowed. | *int* | Yes | | *protocol* | The protocol of the listener. Supported values: ``TCP``, ``UDP`` and ``HTTP``. | *string* | Yes | | *ssl* | Configures the listener with SSL. This is currently only supported for ``HTTP`` listeners. Default value is ``false`` | *bool* | No | -| *ipv4* | Specifies the IPv4 address to listen on. This is currently only supported for ``HTTP`` or ``HTTPS`` listeners. | *string* | No | -| *ipv6* | Specifies the IPv6 address to listen on. This is currently only supported for ``HTTP`` or ``HTTPS`` listeners. | *string* | No | +| *ipv4* | Specifies the IPv4 address to listen on. | *string* | No | +| *ipv6* | Specifies the IPv6 address to listen on. | *string* | No | {{}} diff --git a/examples/custom-resources/custom-ip-listeners/transportserver/README.md b/examples/custom-resources/custom-ip-listeners/transportserver/README.md new file mode 100644 index 0000000000..83268398cc --- /dev/null +++ b/examples/custom-resources/custom-ip-listeners/transportserver/README.md @@ -0,0 +1,215 @@ +# Custom IPv4 and IPv6 Address Listeners + +In this example, we will configure a TransportServer resource with custom IPv4 and IPv6 Address using TCP/UDP listeners. + +## Prerequisites + +1. Follow the [installation](https://docs.nginx.com/nginx-ingress-controller/installation/installation-with-manifests/) + instructions to deploy the Ingress Controller with custom resources enabled. +2. Ensure the Ingress Controller is configured with the `-global-configuration` argument: + + ```console + args: + - -global-configuration=$(POD_NAMESPACE)/nginx-configuration + ``` + +**Note:** + +- **No Updates for GC:** If a GlobalConfiguration resource already exists, delete the previous one before applying the new configuration. +- **Single Replica:** Only one replica is allowed when using this configuration. + +## Step 1 - Deploy the GlobalConfiguration resource + +Similar to how listeners are configured in our [custom-listeners](../../custom-listeners) examples, +here we deploy a GlobalConfiguration resource with the listeners we want to use in our VirtualServer. + + ```yaml +apiVersion: k8s.nginx.org/v1 +kind: GlobalConfiguration +metadata: + name: nginx-configuration + namespace: nginx-ingress +spec: + listeners: + - name: tcp-ip-dns-listener + port: 5353 + protocol: TCP + ipv4: 127.0.0.1 + - name: udp-ip-dns-listener + port: 5252 + protocol: UDP + ipv4: 127.0.0.2 + ipv6: ::1 + ``` + + ```console + kubectl create -f global-configuration.yaml + ``` + +## Step 2 - Deploy the DNS Application + +Create the dns deployment and service: + + ```console + kubectl create -f dns.yaml + ``` + +## Step 3 - Deploy the TransportServers with custom listeners + +The first TransportServer is set to use the udp listener defined in the GlobalConfiguration resource +that was deployed in Step 1. Below is the yaml of this example TransportServer: + +```yaml +apiVersion: k8s.nginx.org/v1 +kind: TransportServer +metadata: + name: dns-udp +spec: + listener: + name: udp-ip-dns-listener + protocol: UDP + upstreams: + - name: dns-app + service: coredns + port: 5252 + upstreamParameters: + udpRequests: 1 + udpResponses: 1 + action: + pass: dns-app +``` + +Create the TransportServer resource: + +```console +kubectl create -f udp-transport-server.yaml +``` + +The second TransportServer is set to use the tcp listener defined in the GlobalConfiguration resource. + +```yaml +apiVersion: k8s.nginx.org/v1 +kind: TransportServer +metadata: + name: tcp-dns +spec: + listener: + name: tcp-ip-dns-listener + protocol: TCP + upstreams: + - name: dns-app + service: coredns + port: 5353 + action: + pass: dns-app +``` + +Create the TransportServer resource: + +```console +kubectl create -f tcp-transport-server.yaml +``` + +## Step 4 - Test the Configuration + +1. Check that the configuration has been successfully applied by inspecting the events of the TransportServer and the GlobalConfiguration: + + ```console + kubectl describe ts udp-dns + ``` + + Below you will see the events as well as the new `Listeners` field + + ```console + . . . + Spec: + Listener: + name: udp-ip-dns-listener + protocol: UDP + + . . . + Routes: + . . . + Events: + Type Reason Age From Message + ---- ------ ---- ---- ------- + Normal AddedOrUpdated 1s nginx-ingress-controller Configuration for default/udp-dns was added or updated + ``` + + ```console + kubectl describe globalconfiguration nginx-configuration -n nginx-ingress + ``` + + ```console + . . . + Spec: + Listeners: + ipv4: 127.0.0.1 + Name: tcp-ip-dns-listener + Port: 5353 + Protocol: TCP + ipv4: 127.0.0.2 + ipv6: ::1 + Name: udp-ip-dns-listener + Port: 5252 + Protocol: UDP + + Events: + Type Reason Age From Message + ---- ------ ---- ---- ------- + Normal Updated 10s nginx-ingress-controller GlobalConfiguration nginx-ingress/nginx-configuration was added or updated + ``` + +2. Since the deployed TransportServer is using port `5252` this example. you can see that the specific ips and ports +are set and listening by using the below commands: + + Access the NGINX Pod: + + ```console + kubectl get pods -n nginx-ingress + ``` + + ```text + NAME READY STATUS RESTARTS AGE + nginx-ingress-5cc9c8f66-4dg2t 1/1 Running 0 50s + ``` + + ```console + kubectl debug -it nginx-ingress-5cc9c8f66-4dg2t --image=busybox:1.28 --target=nginx-ingress + ``` + + ```console + / # netstat -tulpn + Active Internet connections (only servers) + Proto Recv-Q Send-Q Local Address Foreign Address State PID/Program name + tcp 0 0 0.0.0.0:80 0.0.0.0:* LISTEN - + tcp 0 0 127.0.0.1:5353 0.0.0.0:* LISTEN - + tcp 0 0 0.0.0.0:443 0.0.0.0:* LISTEN - + tcp 0 0 0.0.0.0:8080 0.0.0.0:* LISTEN - + tcp 0 0 :::9113 :::* LISTEN - + tcp 0 0 :::80 :::* LISTEN - + tcp 0 0 :::443 :::* LISTEN - + tcp 0 0 :::8080 :::* LISTEN - + tcp 0 0 :::8081 :::* LISTEN - + udp 0 0 127.0.0.2:5252 0.0.0.0:* - + udp 0 0 ::1:5252 :::* - + / # + ``` + + We can see here that the two IPv4 addresses (`127.0.0.1:5353` and `127.0.0.2:5252`) and the one IPv6 address (`::1:5252`) are listed. + +3. Examine the NGINX config using the following command: + + ```console + kubectl exec -it nginx-ingress-5cc9c8f66-4dg2t -n nginx-ingress -- cat /etc/nginx/stream-conf.d/ts_default_dns-udp.conf + ``` + + ```console + ... + server { + listen 127.0.0.2:5252 udp; + listen [::1]:5252 udp; + + ... + } + ``` diff --git a/examples/custom-resources/custom-ip-listeners/transportserver/dns.yaml b/examples/custom-resources/custom-ip-listeners/transportserver/dns.yaml new file mode 100644 index 0000000000..0f6cc049f8 --- /dev/null +++ b/examples/custom-resources/custom-ip-listeners/transportserver/dns.yaml @@ -0,0 +1,64 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: coredns +data: + Corefile: | + .:5353 { + forward . 8.8.8.8:53 + log + } +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: coredns +spec: + replicas: 2 + selector: + matchLabels: + app: coredns + template: + metadata: + labels: + app: coredns + spec: + containers: + - name: coredns + image: coredns/coredns:1.10.0 + args: [ "-conf", "/etc/coredns/Corefile" ] + volumeMounts: + - name: config-volume + mountPath: /etc/coredns + readOnly: true + ports: + - containerPort: 5353 + name: dns + protocol: UDP + - containerPort: 5353 + name: dns-tcp + protocol: TCP + securityContext: + readOnlyRootFilesystem: true + volumes: + - name: config-volume + configMap: + name: coredns + items: + - key: Corefile + path: Corefile +--- +apiVersion: v1 +kind: Service +metadata: + name: coredns +spec: + selector: + app: coredns + ports: + - name: dns + port: 5353 + protocol: UDP + - name: dns-tcp + port: 5353 + protocol: TCP diff --git a/examples/custom-resources/custom-ip-listeners/transportserver/global-configuration.yaml b/examples/custom-resources/custom-ip-listeners/transportserver/global-configuration.yaml new file mode 100644 index 0000000000..0208ba02af --- /dev/null +++ b/examples/custom-resources/custom-ip-listeners/transportserver/global-configuration.yaml @@ -0,0 +1,16 @@ +apiVersion: k8s.nginx.org/v1 +kind: GlobalConfiguration +metadata: + name: my-release-nginx-ingress-controller +spec: + listeners: + - name: tcp-ip-dns-listener + port: 5353 + protocol: TCP + ipv4: 127.0.0.1 + ipv6: ::1 + - name: udp-ip-dns-listener + port: 5252 + protocol: UDP + ipv4: 127.0.0.2 + ipv6: ::1 diff --git a/examples/custom-resources/custom-ip-listeners/transportserver/tcp-transport-server.yaml b/examples/custom-resources/custom-ip-listeners/transportserver/tcp-transport-server.yaml new file mode 100644 index 0000000000..86cfcf014c --- /dev/null +++ b/examples/custom-resources/custom-ip-listeners/transportserver/tcp-transport-server.yaml @@ -0,0 +1,14 @@ +apiVersion: k8s.nginx.org/v1 +kind: TransportServer +metadata: + name: tcp-dns +spec: + listener: + name: tcp-ip-dns-listener + protocol: TCP + upstreams: + - name: dns-app + service: coredns + port: 5353 + action: + pass: dns-app diff --git a/examples/custom-resources/custom-ip-listeners/transportserver/udp-transport-server.yaml b/examples/custom-resources/custom-ip-listeners/transportserver/udp-transport-server.yaml new file mode 100644 index 0000000000..85887e93e8 --- /dev/null +++ b/examples/custom-resources/custom-ip-listeners/transportserver/udp-transport-server.yaml @@ -0,0 +1,17 @@ +apiVersion: k8s.nginx.org/v1 +kind: TransportServer +metadata: + name: dns-udp +spec: + listener: + name: udp-ip-dns-listener + protocol: UDP + upstreams: + - name: dns-app + service: coredns + port: 5353 + upstreamParameters: + udpRequests: 1 + udpResponses: 1 + action: + pass: dns-app diff --git a/internal/configs/transportserver.go b/internal/configs/transportserver.go index 03e0357742..c78db4e217 100644 --- a/internal/configs/transportserver.go +++ b/internal/configs/transportserver.go @@ -23,6 +23,8 @@ type TransportServerEx struct { ExternalNameSvcs map[string]bool DisableIPV6 bool SecretRefs map[string]*secrets.SecretReference + IPv4 string + IPv6 string } func (tsEx *TransportServerEx) String() string { @@ -118,6 +120,8 @@ func generateTransportServerConfig(p transportServerConfigParams) (*version2.Tra ServerSnippets: serverSnippets, DisableIPV6: p.transportServerEx.DisableIPV6, SSL: sslConfig, + IPv4: p.transportServerEx.IPv4, + IPv6: p.transportServerEx.IPv6, }, Match: match, Upstreams: upstreams, diff --git a/internal/configs/version2/__snapshots__/templates_test.snap b/internal/configs/version2/__snapshots__/templates_test.snap index 2f66b55e17..c2a797fd0d 100644 --- a/internal/configs/version2/__snapshots__/templates_test.snap +++ b/internal/configs/version2/__snapshots__/templates_test.snap @@ -80,6 +80,96 @@ server { --- +[TestExecuteTemplateForTransportServerWithTCPIPListener - 1] + +upstream udp-upstream { + zone udp-upstream 512k; + server 10.0.0.20:5001 max_fails=0 fail_timeout= max_conns=0; +} + + +match match_udp-upstream { + + send "GET / HTTP/1.0\r\nHost: localhost\r\n\r\n"; + + + + expect ~* "200 OK"; + +} +server { + listen 127.0.0.1:1234 ssl udp; + listen [::1]:1234 ssl udp; + + ssl_certificate cafe-secret.pem; + ssl_certificate_key cafe-secret.pem; + + status_zone udp-app; + proxy_requests 1; + proxy_responses 2; + + proxy_pass udp-upstream; + + + health_check interval=5s port=8080 + passes=1 jitter=0 fails=1 udp match=match_udp-upstream; + health_check_timeout 5s; + + + proxy_timeout 10s; + proxy_connect_timeout 10s; + proxy_next_upstream on; + proxy_next_upstream_timeout 10s; + proxy_next_upstream_tries 5; +} + +--- + +[TestExecuteTemplateForTransportServerWithUDPIPListener - 1] + +upstream udp-upstream { + zone udp-upstream 512k; + server 10.0.0.20:5001 max_fails=0 fail_timeout= max_conns=0; +} + + +match match_udp-upstream { + + send "GET / HTTP/1.0\r\nHost: localhost\r\n\r\n"; + + + + expect ~* "200 OK"; + +} +server { + listen 127.0.0.1:1234 ssl; + listen [::1]:1234 ssl; + + ssl_certificate cafe-secret.pem; + ssl_certificate_key cafe-secret.pem; + + status_zone udp-app; + proxy_requests 1; + proxy_responses 2; + + proxy_pass udp-upstream; + + + health_check interval=5s port=8080 + passes=1 jitter=0 fails=1 match=match_udp-upstream; + health_check_timeout 5s; + + + proxy_timeout 10s; + proxy_connect_timeout 10s; + proxy_next_upstream on; + proxy_next_upstream_timeout 10s; + proxy_next_upstream_tries 5; +} + +--- + [TestExecuteVirtualServerTemplateWithAPIKeyPolicyNGINXPlus - 1] upstream test-upstream { diff --git a/internal/configs/version2/stream.go b/internal/configs/version2/stream.go index 8662df9509..ef2e67daba 100644 --- a/internal/configs/version2/stream.go +++ b/internal/configs/version2/stream.go @@ -56,6 +56,8 @@ type StreamServer struct { ServerSnippets []string DisableIPV6 bool SSL *StreamSSL + IPv4 string + IPv6 string } // StreamSSL defines SSL configuration for a server. diff --git a/internal/configs/version2/template_helper.go b/internal/configs/version2/template_helper.go index 65b674f679..95e8f59d78 100644 --- a/internal/configs/version2/template_helper.go +++ b/internal/configs/version2/template_helper.go @@ -184,7 +184,7 @@ func makeTransportListener(s StreamServer) string { port := strconv.Itoa(s.Port) directives += buildListenDirective(listen{ - ipAddress: "", + ipAddress: s.IPv4, port: port, tls: s.SSL.Enabled, proxyProtocol: false, @@ -195,7 +195,7 @@ func makeTransportListener(s StreamServer) string { if !s.DisableIPV6 { directives += spacing directives += buildListenDirective(listen{ - ipAddress: "", + ipAddress: s.IPv6, port: port, tls: s.SSL.Enabled, proxyProtocol: false, diff --git a/internal/configs/version2/template_helper_test.go b/internal/configs/version2/template_helper_test.go index de394cd8ce..8c2ea4cb9c 100644 --- a/internal/configs/version2/template_helper_test.go +++ b/internal/configs/version2/template_helper_test.go @@ -539,6 +539,80 @@ func TestMakeTransportListener(t *testing.T) { } } +func TestMakeTransportIPListener(t *testing.T) { + t.Parallel() + + testCases := []struct { + server StreamServer + expected string + }{ + {server: StreamServer{ + UDP: false, + SSL: &StreamSSL{ + Enabled: false, + }, + DisableIPV6: true, + Port: 5353, + IPv4: "127.0.0.1", + }, expected: "listen 127.0.0.1:5353;\n"}, + {server: StreamServer{ + UDP: true, + SSL: &StreamSSL{ + Enabled: false, + }, + DisableIPV6: true, + Port: 5353, + IPv4: "127.0.0.1", + }, expected: "listen 127.0.0.1:5353 udp;\n"}, + {server: StreamServer{ + UDP: true, + SSL: &StreamSSL{ + Enabled: true, + }, + DisableIPV6: true, + Port: 5353, + IPv4: "127.0.0.1", + }, expected: "listen 127.0.0.1:5353 ssl udp;\n"}, + + {server: StreamServer{ + UDP: false, + SSL: &StreamSSL{ + Enabled: false, + }, + DisableIPV6: false, + Port: 5353, + IPv4: "127.0.0.1", + IPv6: "::1", + }, expected: "listen 127.0.0.1:5353;\n listen [::1]:5353;\n"}, + {server: StreamServer{ + UDP: true, + SSL: &StreamSSL{ + Enabled: false, + }, + DisableIPV6: false, + Port: 5353, + IPv4: "127.0.0.1", + IPv6: "::1", + }, expected: "listen 127.0.0.1:5353 udp;\n listen [::1]:5353 udp;\n"}, + {server: StreamServer{ + UDP: true, + SSL: &StreamSSL{ + Enabled: true, + }, + DisableIPV6: false, + Port: 5353, + IPv6: "::1", + }, expected: "listen 5353 ssl udp;\n listen [::1]:5353 ssl udp;\n"}, + } + + for _, tc := range testCases { + got := makeTransportListener(tc.server) + if got != tc.expected { + t.Errorf("Function generated wrong config, got %q but expected %q.", got, tc.expected) + } + } +} + func newContainsTemplate(t *testing.T) *template.Template { t.Helper() tmpl, err := template.New("testTemplate").Funcs(helperFunctions).Parse(`{{contains .InputString .Substring}}`) diff --git a/internal/configs/version2/templates_test.go b/internal/configs/version2/templates_test.go index 9b31c52133..aa0f276c68 100644 --- a/internal/configs/version2/templates_test.go +++ b/internal/configs/version2/templates_test.go @@ -497,6 +497,55 @@ func TestTransportServerForNginx(t *testing.T) { t.Log(string(data)) } +func TestExecuteTemplateForTransportServerWithTCPIPListener(t *testing.T) { + t.Parallel() + executor := newTmplExecutorNGINXPlus(t) + customIPListenerTransportServerCfg := transportServerCfgWithSSL + customIPListenerTransportServerCfg.Server.IPv4 = "127.0.0.1" + customIPListenerTransportServerCfg.Server.IPv6 = "::1" + + got, err := executor.ExecuteTransportServerTemplate(&customIPListenerTransportServerCfg) + if err != nil { + t.Error(err) + } + wantStrings := []string{ + "listen 127.0.0.1:1234 ssl udp;", + "listen [::1]:1234 ssl udp;", + } + for _, want := range wantStrings { + if !bytes.Contains(got, []byte(want)) { + t.Errorf("want `%s` in generated template", want) + } + } + snaps.MatchSnapshot(t, string(got)) + t.Log(string(got)) +} + +func TestExecuteTemplateForTransportServerWithUDPIPListener(t *testing.T) { + t.Parallel() + executor := newTmplExecutorNGINXPlus(t) + customIPListenerTransportServerCfg := transportServerCfgWithSSL + customIPListenerTransportServerCfg.Server.IPv4 = "127.0.0.1" + customIPListenerTransportServerCfg.Server.IPv6 = "::1" + customIPListenerTransportServerCfg.Server.UDP = false + + got, err := executor.ExecuteTransportServerTemplate(&customIPListenerTransportServerCfg) + if err != nil { + t.Error(err) + } + wantStrings := []string{ + "listen 127.0.0.1:1234 ssl;", + "listen [::1]:1234 ssl;", + } + for _, want := range wantStrings { + if !bytes.Contains(got, []byte(want)) { + t.Errorf("want `%s` in generated template", want) + } + } + snaps.MatchSnapshot(t, string(got)) + t.Log(string(got)) +} + func tsConfig() TransportServerConfig { return TransportServerConfig{ Upstreams: []StreamUpstream{ diff --git a/internal/k8s/configuration.go b/internal/k8s/configuration.go index 950460566e..b600213754 100644 --- a/internal/k8s/configuration.go +++ b/internal/k8s/configuration.go @@ -264,6 +264,8 @@ func (vsc *VirtualServerConfiguration) IsEqual(resource Resource) bool { // TransportServerConfiguration holds a TransportServer resource. type TransportServerConfiguration struct { ListenerPort int + IPv4 string + IPv6 string TransportServer *conf_v1.TransportServer Warnings []string } @@ -795,6 +797,8 @@ func (c *Configuration) buildListenersAndTSConfigurations() (newListeners map[st } tsc.ListenerPort = listener.Port + tsc.IPv4 = listener.IPv4 + tsc.IPv6 = listener.IPv6 holder, exists := newListeners[listener.Name] if !exists { @@ -824,8 +828,8 @@ func (c *Configuration) buildListenersForVSConfiguration(vsc *VirtualServerConfi assignListener := func(listenerName string, isSSL bool, port *int, ipv4 *string, ipv6 *string) { if gcListener, ok := c.listenerMap[listenerName]; ok && gcListener.Protocol == conf_v1.HTTPProtocol && gcListener.Ssl == isSSL { *port = gcListener.Port - *ipv4 = gcListener.IPv4IP - *ipv6 = gcListener.IPv6IP + *ipv4 = gcListener.IPv4 + *ipv6 = gcListener.IPv6 } } diff --git a/internal/k8s/controller.go b/internal/k8s/controller.go index e6f01ccd36..f8216fd1c3 100644 --- a/internal/k8s/controller.go +++ b/internal/k8s/controller.go @@ -824,7 +824,7 @@ func (lbc *LoadBalancerController) createExtendedResources(resources []Resource) result.IngressExes = append(result.IngressExes, ingEx) } case *TransportServerConfiguration: - tsEx := lbc.createTransportServerEx(impl.TransportServer, impl.ListenerPort) + tsEx := lbc.createTransportServerEx(impl.TransportServer, impl.ListenerPort, impl.IPv4, impl.IPv6) result.TransportServerExes = append(result.TransportServerExes, tsEx) } } @@ -1179,8 +1179,7 @@ func (lbc *LoadBalancerController) processChanges(changes []ResourceChange) { lbc.updateRegularIngressStatusAndEvents(impl, warnings, addOrUpdateErr) } case *TransportServerConfiguration: - tsEx := lbc.createTransportServerEx(impl.TransportServer, impl.ListenerPort) - + tsEx := lbc.createTransportServerEx(impl.TransportServer, impl.ListenerPort, impl.IPv4, impl.IPv6) warnings, addOrUpdateErr := lbc.configurator.AddOrUpdateTransportServer(tsEx) lbc.updateTransportServerStatusAndEvents(impl, warnings, addOrUpdateErr) } diff --git a/internal/k8s/global_configuration.go b/internal/k8s/global_configuration.go index 0541022c51..7d64c86a78 100644 --- a/internal/k8s/global_configuration.go +++ b/internal/k8s/global_configuration.go @@ -135,7 +135,7 @@ func (lbc *LoadBalancerController) processChangesFromGlobalConfiguration(changes } case *TransportServerConfiguration: if c.Op == AddOrUpdate { - tsEx := lbc.createTransportServerEx(impl.TransportServer, impl.ListenerPort) + tsEx := lbc.createTransportServerEx(impl.TransportServer, impl.ListenerPort, impl.IPv4, impl.IPv6) updatedTSExes = append(updatedTSExes, tsEx) updatedResources = append(updatedResources, impl) diff --git a/internal/k8s/transport_server.go b/internal/k8s/transport_server.go index 946a635acc..ac6e0ef2e4 100644 --- a/internal/k8s/transport_server.go +++ b/internal/k8s/transport_server.go @@ -202,7 +202,7 @@ func (lbc *LoadBalancerController) updateTransportServersStatusFromEvents() erro return nil } -func (lbc *LoadBalancerController) createTransportServerEx(transportServer *conf_v1.TransportServer, listenerPort int) *configs.TransportServerEx { +func (lbc *LoadBalancerController) createTransportServerEx(transportServer *conf_v1.TransportServer, listenerPort int, ipv4 string, ipv6 string) *configs.TransportServerEx { endpoints := make(map[string][]string) externalNameSvcs := make(map[string]bool) podsByIP := make(map[string]string) @@ -250,6 +250,8 @@ func (lbc *LoadBalancerController) createTransportServerEx(transportServer *conf return &configs.TransportServerEx{ ListenerPort: listenerPort, + IPv4: ipv4, + IPv6: ipv6, TransportServer: transportServer, Endpoints: endpoints, PodsByIP: podsByIP, diff --git a/pkg/apis/configuration/v1/types.go b/pkg/apis/configuration/v1/types.go index 76c5261980..cac87569ab 100644 --- a/pkg/apis/configuration/v1/types.go +++ b/pkg/apis/configuration/v1/types.go @@ -411,8 +411,8 @@ type GlobalConfigurationSpec struct { type Listener struct { Name string `json:"name"` Port int `json:"port"` - IPv4IP string `json:"ipv4"` - IPv6IP string `json:"ipv6"` + IPv4 string `json:"ipv4"` + IPv6 string `json:"ipv6"` Protocol string `json:"protocol"` Ssl bool `json:"ssl"` } diff --git a/pkg/apis/configuration/validation/globalconfiguration.go b/pkg/apis/configuration/validation/globalconfiguration.go index 6045508fac..f020252d19 100644 --- a/pkg/apis/configuration/validation/globalconfiguration.go +++ b/pkg/apis/configuration/validation/globalconfiguration.go @@ -128,15 +128,15 @@ func (gcv *GlobalConfigurationValidator) updatePortProtocolCombinations(combinat // getIP returns the appropriate IP address for the given ipType and listener. func getIP(ipType ipType, listener conf_v1.Listener) string { if ipType == ipv4 { - if listener.IPv4IP == "" { + if listener.IPv4 == "" { return "0.0.0.0" } - return listener.IPv4IP + return listener.IPv4 } - if listener.IPv6IP == "" { + if listener.IPv6 == "" { return "::" } - return listener.IPv6IP + return listener.IPv6 } func generatePortProtocolKey(port int, protocol string) string { @@ -147,8 +147,8 @@ func (gcv *GlobalConfigurationValidator) validateListener(listener conf_v1.Liste allErrs := validateGlobalConfigurationListenerName(listener.Name, fieldPath.Child("name")) allErrs = append(allErrs, gcv.validateListenerPort(listener.Name, listener.Port, fieldPath.Child("port"))...) allErrs = append(allErrs, validateListenerProtocol(listener.Protocol, fieldPath.Child("protocol"))...) - allErrs = append(allErrs, validateListenerIPv4IP(listener.IPv4IP, fieldPath.Child("ipv4ip"))...) - allErrs = append(allErrs, validateListenerIPv6IP(listener.IPv6IP, fieldPath.Child("ipv6ip"))...) + allErrs = append(allErrs, validateListenerIPv4(listener.IPv4, fieldPath.Child("ipv4"))...) + allErrs = append(allErrs, validateListenerIPv6(listener.IPv6, fieldPath.Child("ipv6"))...) return allErrs } @@ -184,16 +184,16 @@ func validateListenerProtocol(protocol string, fieldPath *field.Path) field.Erro } } -func validateListenerIPv4IP(ipv4ip string, fieldPath *field.Path) field.ErrorList { - if ipv4ip != "" { - return validation.IsValidIPv4Address(fieldPath, ipv4ip) +func validateListenerIPv4(ipv4 string, fieldPath *field.Path) field.ErrorList { + if ipv4 != "" { + return validation.IsValidIPv4Address(fieldPath, ipv4) } return field.ErrorList{} } -func validateListenerIPv6IP(ipv6ip string, fieldPath *field.Path) field.ErrorList { - if ipv6ip != "" { - return validation.IsValidIPv6Address(fieldPath, ipv6ip) +func validateListenerIPv6(ipv6 string, fieldPath *field.Path) field.ErrorList { + if ipv6 != "" { + return validation.IsValidIPv6Address(fieldPath, ipv6) } return field.ErrorList{} } diff --git a/pkg/apis/configuration/validation/globalconfiguration_test.go b/pkg/apis/configuration/validation/globalconfiguration_test.go index 2acd448397..04bc764ae2 100644 --- a/pkg/apis/configuration/validation/globalconfiguration_test.go +++ b/pkg/apis/configuration/validation/globalconfiguration_test.go @@ -76,8 +76,8 @@ func TestValidateListeners(t *testing.T) { }, { Name: "test-listener-ip", - IPv4IP: "127.0.0.1", - IPv6IP: "::1", + IPv4: "127.0.0.1", + IPv6: "::1", Port: 8080, Protocol: "HTTP", }, @@ -101,38 +101,38 @@ func TestValidateListeners_FailsOnInvalidIP(t *testing.T) { { name: "Invalid IPv4 IP", listeners: []conf_v1.Listener{ - {Name: "test-listener-1", IPv4IP: "267.0.0.1", Port: 8082, Protocol: "HTTP"}, + {Name: "test-listener-1", IPv4: "267.0.0.1", Port: 8082, Protocol: "HTTP"}, }, }, { name: "Invalid IPv4 IP with missing octet", listeners: []conf_v1.Listener{ - {Name: "test-listener-2", IPv4IP: "127.0.0", Port: 8080, Protocol: "HTTP"}, + {Name: "test-listener-2", IPv4: "127.0.0", Port: 8080, Protocol: "HTTP"}, }, }, { name: "Invalid IPv6 IP", listeners: []conf_v1.Listener{ - {Name: "test-listener-3", IPv6IP: "1200::AB00::1234", Port: 8080, Protocol: "HTTP"}, + {Name: "test-listener-3", IPv6: "1200::AB00::1234", Port: 8080, Protocol: "HTTP"}, }, }, { name: "Valid and invalid IPs", listeners: []conf_v1.Listener{ - {Name: "test-listener-4", IPv4IP: "192.168.1.1", IPv6IP: "2001:0db1234123123", Port: 8080, Protocol: "HTTP"}, - {Name: "test-listener-5", IPv4IP: "256.256.256.256", IPv6IP: "2001:0db8:85a3:0000:0000:8a2e:0370:7334", Port: 8081, Protocol: "HTTP"}, + {Name: "test-listener-4", IPv4: "192.168.1.1", IPv6: "2001:0db1234123123", Port: 8080, Protocol: "HTTP"}, + {Name: "test-listener-5", IPv4: "256.256.256.256", IPv6: "2001:0db8:85a3:0000:0000:8a2e:0370:7334", Port: 8081, Protocol: "HTTP"}, }, }, { name: "Valid IPv4 and Invalid IPv6", listeners: []conf_v1.Listener{ - {Name: "test-listener-6", IPv4IP: "192.168.1.1", IPv6IP: "2001::85a3::8a2e:370:7334", Port: 8080, Protocol: "HTTP"}, + {Name: "test-listener-6", IPv4: "192.168.1.1", IPv6: "2001::85a3::8a2e:370:7334", Port: 8080, Protocol: "HTTP"}, }, }, { name: "Invalid IPv4 and Valid IPv6", listeners: []conf_v1.Listener{ - {Name: "test-listener-8", IPv4IP: "300.168.1.1", IPv6IP: "2001:0db8:85a3:0000:0000:8a2e:0370:7334", Port: 8080, Protocol: "HTTP"}, + {Name: "test-listener-8", IPv4: "300.168.1.1", IPv6: "2001:0db8:85a3:0000:0000:8a2e:0370:7334", Port: 8080, Protocol: "HTTP"}, }, }, } @@ -163,29 +163,29 @@ func TestValidateListeners_FailsOnPortProtocolConflictsSameIP(t *testing.T) { { name: "Same port used with the same protocol", listeners: []conf_v1.Listener{ - {Name: "listener-1", IPv4IP: "192.168.1.1", IPv6IP: "::1", Port: 8080, Protocol: "HTTP"}, - {Name: "listener-2", IPv4IP: "192.168.1.1", IPv6IP: "::1", Port: 8080, Protocol: "HTTP"}, + {Name: "listener-1", IPv4: "192.168.1.1", IPv6: "::1", Port: 8080, Protocol: "HTTP"}, + {Name: "listener-2", IPv4: "192.168.1.1", IPv6: "::1", Port: 8080, Protocol: "HTTP"}, }, }, { name: "Same port used with different protocols", listeners: []conf_v1.Listener{ - {Name: "listener-1", IPv4IP: "192.168.1.1", IPv6IP: "::1", Port: 8080, Protocol: "HTTP"}, - {Name: "listener-2", IPv4IP: "192.168.1.1", Port: 8080, Protocol: "TCP"}, + {Name: "listener-1", IPv4: "192.168.1.1", IPv6: "::1", Port: 8080, Protocol: "HTTP"}, + {Name: "listener-2", IPv4: "192.168.1.1", Port: 8080, Protocol: "TCP"}, }, }, { name: "Same port used with the same protocol (IPv6)", listeners: []conf_v1.Listener{ - {Name: "listener-1", IPv4IP: "192.168.1.1", IPv6IP: "2001:0db8:85a3:0000:0000:8a2e:0370:7334", Port: 8080, Protocol: "HTTP"}, - {Name: "listener-2", IPv6IP: "2001:0db8:85a3:0000:0000:8a2e:0370:7334", Port: 8080, Protocol: "HTTP"}, + {Name: "listener-1", IPv4: "192.168.1.1", IPv6: "2001:0db8:85a3:0000:0000:8a2e:0370:7334", Port: 8080, Protocol: "HTTP"}, + {Name: "listener-2", IPv6: "2001:0db8:85a3:0000:0000:8a2e:0370:7334", Port: 8080, Protocol: "HTTP"}, }, }, { name: "Same port used with different protocols (IPv6)", listeners: []conf_v1.Listener{ - {Name: "listener-1", IPv6IP: "2001:0db8:85a3:0000:0000:8a2e:0370:7334", Port: 8080, Protocol: "HTTP"}, - {Name: "listener-2", IPv4IP: "192.168.1.1", IPv6IP: "2001:0db8:85a3:0000:0000:8a2e:0370:7334", Port: 8080, Protocol: "TCP"}, + {Name: "listener-1", IPv6: "2001:0db8:85a3:0000:0000:8a2e:0370:7334", Port: 8080, Protocol: "HTTP"}, + {Name: "listener-2", IPv4: "192.168.1.1", IPv6: "2001:0db8:85a3:0000:0000:8a2e:0370:7334", Port: 8080, Protocol: "TCP"}, }, }, } @@ -216,43 +216,43 @@ func TestValidateListeners_PassesOnValidIPListeners(t *testing.T) { { name: "Different Ports and IPs", listeners: []conf_v1.Listener{ - {Name: "listener-1", IPv4IP: "192.168.1.1", IPv6IP: "2001:0db8:85a3:0000:0000:8a2e:0370:7334", Port: 8080, Protocol: "HTTP"}, - {Name: "listener-2", IPv4IP: "192.168.1.2", IPv6IP: "::1", Port: 9090, Protocol: "HTTP"}, + {Name: "listener-1", IPv4: "192.168.1.1", IPv6: "2001:0db8:85a3:0000:0000:8a2e:0370:7334", Port: 8080, Protocol: "HTTP"}, + {Name: "listener-2", IPv4: "192.168.1.2", IPv6: "::1", Port: 9090, Protocol: "HTTP"}, }, }, { name: "Same IPs, Same Protocol and Different Port", listeners: []conf_v1.Listener{ - {Name: "listener-1", IPv4IP: "192.168.1.1", IPv6IP: "2001:0db8:85a3:0000:0000:8a2e:0370:7334", Port: 8080, Protocol: "HTTP"}, - {Name: "listener-2", IPv4IP: "192.168.1.1", IPv6IP: "2001:0db8:85a3:0000:0000:8a2e:0370:7334", Port: 9090, Protocol: "HTTP"}, + {Name: "listener-1", IPv4: "192.168.1.1", IPv6: "2001:0db8:85a3:0000:0000:8a2e:0370:7334", Port: 8080, Protocol: "HTTP"}, + {Name: "listener-2", IPv4: "192.168.1.1", IPv6: "2001:0db8:85a3:0000:0000:8a2e:0370:7334", Port: 9090, Protocol: "HTTP"}, }, }, { name: "Different Types of IPs", listeners: []conf_v1.Listener{ - {Name: "listener-1", IPv4IP: "192.168.1.1", Port: 8080, Protocol: "HTTP"}, - {Name: "listener-2", IPv6IP: "2001:0db8:85a3:0000:0000:8a2e:0370:7334", Port: 8080, Protocol: "HTTP"}, + {Name: "listener-1", IPv4: "192.168.1.1", Port: 8080, Protocol: "HTTP"}, + {Name: "listener-2", IPv6: "2001:0db8:85a3:0000:0000:8a2e:0370:7334", Port: 8080, Protocol: "HTTP"}, }, }, { name: "UDP and HTTP Listeners with Same Port", listeners: []conf_v1.Listener{ - {Name: "listener-1", IPv4IP: "127.0.0.1", Port: 8080, Protocol: "UDP"}, - {Name: "listener-2", IPv4IP: "127.0.0.1", Port: 8080, Protocol: "HTTP"}, + {Name: "listener-1", IPv4: "127.0.0.1", Port: 8080, Protocol: "UDP"}, + {Name: "listener-2", IPv4: "127.0.0.1", Port: 8080, Protocol: "HTTP"}, }, }, { name: "HTTP Listeners with Same Port but different IPv4 and IPv6 ip addresses", listeners: []conf_v1.Listener{ - {Name: "listener-1", IPv4IP: "127.0.0.2", IPv6IP: "::1", Port: 8080, Protocol: "HTTP"}, - {Name: "listener-2", IPv4IP: "127.0.0.1", Port: 8080, Protocol: "HTTP"}, + {Name: "listener-1", IPv4: "127.0.0.2", IPv6: "::1", Port: 8080, Protocol: "HTTP"}, + {Name: "listener-2", IPv4: "127.0.0.1", Port: 8080, Protocol: "HTTP"}, }, }, { name: "UDP and TCP Listeners with Same Port", listeners: []conf_v1.Listener{ - {Name: "listener-1", IPv4IP: "127.0.0.1", Port: 8080, Protocol: "UDP"}, - {Name: "listener-2", IPv4IP: "127.0.0.1", Port: 8080, Protocol: "TCP"}, + {Name: "listener-1", IPv4: "127.0.0.1", Port: 8080, Protocol: "UDP"}, + {Name: "listener-2", IPv4: "127.0.0.1", Port: 8080, Protocol: "TCP"}, }, }, } @@ -279,15 +279,15 @@ func TestValidateListeners_FailsOnMixedInvalidIPs(t *testing.T) { { name: "Valid IPv4 and Invalid IPv6", listeners: []conf_v1.Listener{ - {Name: "listener-1", IPv4IP: "192.168.1.1", Port: 8080, Protocol: "HTTP"}, - {Name: "listener-2", IPv6IP: "2001::85a3::8a2e:370:7334", Port: 9090, Protocol: "TCP"}, + {Name: "listener-1", IPv4: "192.168.1.1", Port: 8080, Protocol: "HTTP"}, + {Name: "listener-2", IPv6: "2001::85a3::8a2e:370:7334", Port: 9090, Protocol: "TCP"}, }, }, { name: "Invalid IPv4 and Valid IPv6", listeners: []conf_v1.Listener{ - {Name: "listener-1", IPv4IP: "300.168.1.1", Port: 8080, Protocol: "HTTP"}, - {Name: "listener-2", IPv6IP: "2001:0db8:85a3:0000:0000:8a2e:0370:7334", Port: 9090, Protocol: "TCP"}, + {Name: "listener-1", IPv4: "300.168.1.1", Port: 8080, Protocol: "HTTP"}, + {Name: "listener-2", IPv6: "2001:0db8:85a3:0000:0000:8a2e:0370:7334", Port: 9090, Protocol: "TCP"}, }, }, } diff --git a/tests/data/transport-server-custom-ip-listener/global-configuration.yaml b/tests/data/transport-server-custom-ip-listener/global-configuration.yaml new file mode 100644 index 0000000000..569df6eefc --- /dev/null +++ b/tests/data/transport-server-custom-ip-listener/global-configuration.yaml @@ -0,0 +1,12 @@ +apiVersion: k8s.nginx.org/v1 +kind: GlobalConfiguration +metadata: + name: nginx-configuration + namespace: nginx-ingress +spec: + listeners: + - name: dns-tcp-ip + port: 5353 + protocol: TCP + ipv4: 127.0.0.1 + ipv6: ::1 diff --git a/tests/data/transport-server-custom-ip-listener/transport-server.yaml b/tests/data/transport-server-custom-ip-listener/transport-server.yaml new file mode 100644 index 0000000000..42e5fb5c35 --- /dev/null +++ b/tests/data/transport-server-custom-ip-listener/transport-server.yaml @@ -0,0 +1,14 @@ +apiVersion: k8s.nginx.org/v1 +kind: TransportServer +metadata: + name: transport-server +spec: + listener: + name: dns-tcp-ip + protocol: TCP + upstreams: + - name: dns-app + service: coredns + port: 5353 + action: + pass: dns-app diff --git a/tests/suite/test_transport_server_custom_ip_listener.py b/tests/suite/test_transport_server_custom_ip_listener.py new file mode 100644 index 0000000000..dd6983472b --- /dev/null +++ b/tests/suite/test_transport_server_custom_ip_listener.py @@ -0,0 +1,65 @@ +import pytest +from settings import TEST_DATA +from suite.utils.custom_resources_utils import patch_gc_from_yaml, patch_ts_from_yaml +from suite.utils.resources_utils import get_events_for_object, get_ts_nginx_template_conf, wait_before_test + + +@pytest.mark.ts +@pytest.mark.parametrize( + "crd_ingress_controller, transport_server_setup", + [ + ( + { + "type": "complete", + "extra_args": [ + "-global-configuration=nginx-ingress/nginx-configuration", + "-enable-leader-election=false", + ], + }, + {"example": "transport-server-status"}, + ) + ], + indirect=True, +) +class TestTransportServerCustomIPListener: + def test_ts_custom_ip_listener( + self, kube_apis, crd_ingress_controller, transport_server_setup, ingress_controller_prerequisites + ): + """ + Test transport server with custom IP listener + """ + + global_config_file = f"{TEST_DATA}/transport-server-custom-ip-listener/global-configuration.yaml" + patch_gc_from_yaml(kube_apis.custom_objects, "nginx-configuration", global_config_file, "nginx-ingress") + + patch_src = f"{TEST_DATA}/transport-server-custom-ip-listener/transport-server.yaml" + patch_ts_from_yaml( + kube_apis.custom_objects, + transport_server_setup.name, + patch_src, + transport_server_setup.namespace, + ) + wait_before_test() + + conf = get_ts_nginx_template_conf( + kube_apis.v1, + transport_server_setup.namespace, + transport_server_setup.name, + transport_server_setup.ingress_pod_name, + ingress_controller_prerequisites.namespace, + ) + print(conf) + + conf_lines = [line.strip() for line in conf.split("\n")] + assert "listen 127.0.0.1:5353;" in conf_lines + assert "listen [::1]:5353;" in conf_lines + + gc_events = get_events_for_object(kube_apis.v1, "nginx-ingress", "nginx-configuration") + gc_event_latest = gc_events[-1] + print(gc_event_latest) + + assert ( + gc_event_latest.reason == "Updated" + and gc_event_latest.type == "Normal" + and "GlobalConfiguration nginx-ingress/nginx-configuration was added or updated" in gc_event_latest.message + )