From ed55fd3b0e3471c36a63df80d4464052197e49ae Mon Sep 17 00:00:00 2001 From: Jakub Jarosz Date: Wed, 3 May 2023 17:53:04 +0100 Subject: [PATCH 1/3] Fix gunzip support for VS and python tests --- .../crds/k8s.nginx.org_virtualservers.yaml | 2 +- .../crds/k8s.nginx.org_virtualservers.yaml | 2 +- ...server-and-virtualserverroute-resources.md | 2 +- examples/custom-resources/jwt/README.md | 18 + internal/configs/version2/http.go | 2 +- .../version2/nginx-plus.virtualserver.tmpl | 2 +- .../configs/version2/nginx.virtualserver.tmpl | 2 +- internal/configs/version2/templates_test.go | 32 +- internal/configs/virtualserver.go | 1 + internal/configs/virtualserver_test.go | 11759 +++++++++------- pkg/apis/configuration/v1/types.go | 2 +- .../configuration/validation/virtualserver.go | 10 - .../validation/virtualserver_test.go | 229 - .../virtual-server/virtual-server-gunzip.yaml | 21 + tests/suite/test_virtual_server.py | 42 + 15 files changed, 6663 insertions(+), 5463 deletions(-) create mode 100644 tests/data/virtual-server/virtual-server-gunzip.yaml diff --git a/deployments/common/crds/k8s.nginx.org_virtualservers.yaml b/deployments/common/crds/k8s.nginx.org_virtualservers.yaml index c5015904fd..b288111874 100644 --- a/deployments/common/crds/k8s.nginx.org_virtualservers.yaml +++ b/deployments/common/crds/k8s.nginx.org_virtualservers.yaml @@ -87,7 +87,7 @@ spec: recordType: type: string gunzip: - type: string + type: boolean host: type: string http-snippets: diff --git a/deployments/helm-chart/crds/k8s.nginx.org_virtualservers.yaml b/deployments/helm-chart/crds/k8s.nginx.org_virtualservers.yaml index c5015904fd..b288111874 100644 --- a/deployments/helm-chart/crds/k8s.nginx.org_virtualservers.yaml +++ b/deployments/helm-chart/crds/k8s.nginx.org_virtualservers.yaml @@ -87,7 +87,7 @@ spec: recordType: type: string gunzip: - type: string + type: boolean host: type: string http-snippets: diff --git a/docs/content/configuration/virtualserver-and-virtualserverroute-resources.md b/docs/content/configuration/virtualserver-and-virtualserverroute-resources.md index 412cf848e6..2bbbfd0473 100644 --- a/docs/content/configuration/virtualserver-and-virtualserverroute-resources.md +++ b/docs/content/configuration/virtualserver-and-virtualserverroute-resources.md @@ -54,7 +54,7 @@ spec: | ---| ---| ---| --- | |``host`` | The host (domain name) of the server. Must be a valid subdomain as defined in RFC 1123, such as ``my-app`` or ``hello.example.com``. When using a wildcard domain like ``*.example.com`` the domain must be contained in double quotes. The ``host`` value needs to be unique among all Ingress and VirtualServer resources. See also [Handling Host and Listener Collisions](/nginx-ingress-controller/configuration/handling-host-and-listener-collisions). | ``string`` | Yes | |``tls`` | The TLS termination configuration. | [tls](#virtualservertls) | No | -|``gunzip`` | Enables or disables [decompression](https://docs.nginx.com/nginx/admin-guide/web-server/compression/) of gzipped responses for clients. Allowed values are: "on" or "off". If the ``gunzip`` value is not set, it defaults to ``off``. | ``string`` | No | +|``gunzip`` | Enables or disables [decompression](https://docs.nginx.com/nginx/admin-guide/web-server/compression/) of gzipped responses for clients. Allowed values are: "on" or "off". If the ``gunzip`` value is not set, it defaults to ``off``. | ``boolean`` | No | |``externalDNS`` | The externalDNS configuration for a VirtualServer. | [externalDNS](#virtualserverexternaldns) | No | |``dos`` | A reference to a DosProtectedResource, setting this enables DOS protection of the VirtualServer. | ``string`` | No | |``policies`` | A list of policies. | [[]policy](#virtualserverpolicy) | No | diff --git a/examples/custom-resources/jwt/README.md b/examples/custom-resources/jwt/README.md index 8466ceb334..e812b64cd6 100644 --- a/examples/custom-resources/jwt/README.md +++ b/examples/custom-resources/jwt/README.md @@ -67,3 +67,21 @@ Date: 10/Sep/2020:18:20:03 +0000 URI: / Request ID: db2c07ce640755ccbe9f666d16f85620 ``` + +> **Note**:
+You can add a ``gunzip`` option to the VirtualServer spec. Adding the ``gunzip`` allows NIC to decompress responses where an item +like a JWT token is compressed by the IdP.
+If an IdP compresses a JWT token and NIC is not configured to decompress responses (``gunzip`` not set to ``on``), the error "invalid JWK set while sending to client" is generated by NIC.
+When the ``gunzip`` value is set to ``on``, NIC automatically decompresses responses with “Content-Encoding: gzip” header. + +Example: +```yaml +apiVersion: k8s.nginx.org/v1 +kind: VirtualServer +metadata: + name: webapp +spec: + host: webapp.example.com + gunzip: on + ... +``` diff --git a/internal/configs/version2/http.go b/internal/configs/version2/http.go index 3152d51e2d..f129205e8a 100644 --- a/internal/configs/version2/http.go +++ b/internal/configs/version2/http.go @@ -79,7 +79,7 @@ type Server struct { VSNamespace string VSName string DisableIPV6 bool - Gunzip string + Gunzip bool } // SSL defines SSL configuration for a server. diff --git a/internal/configs/version2/nginx-plus.virtualserver.tmpl b/internal/configs/version2/nginx-plus.virtualserver.tmpl index 973b4a5852..ccb078bdf6 100644 --- a/internal/configs/version2/nginx-plus.virtualserver.tmpl +++ b/internal/configs/version2/nginx-plus.virtualserver.tmpl @@ -68,7 +68,7 @@ match {{ $m.Name }} { {{ end }} server { - {{ if (eq $s.Gunzip "on") }}gunzip {{ $s.Gunzip }};{{end}} + {{ if $s.Gunzip }}gunzip on;{{end}} listen 80{{ if $s.ProxyProtocol }} proxy_protocol{{ end }}; {{ if not $s.DisableIPV6 }}listen [::]:80{{ if $s.ProxyProtocol }} proxy_protocol{{ end }};{{ end }} diff --git a/internal/configs/version2/nginx.virtualserver.tmpl b/internal/configs/version2/nginx.virtualserver.tmpl index 1b03b44df5..bd032e9c4e 100644 --- a/internal/configs/version2/nginx.virtualserver.tmpl +++ b/internal/configs/version2/nginx.virtualserver.tmpl @@ -40,7 +40,7 @@ limit_req_zone {{ $z.Key }} zone={{ $z.ZoneName }}:{{ $z.ZoneSize }} rate={{ $z. {{ $s := .Server }} server { - {{ if (eq $s.Gunzip "on") }}gunzip {{ $s.Gunzip }};{{end}} + {{ if $s.Gunzip }}gunzip on;{{end}} listen 80{{ if $s.ProxyProtocol }} proxy_protocol{{ end }}; {{ if not $s.DisableIPV6 }}listen [::]:80{{ if $s.ProxyProtocol }} proxy_protocol{{ end }};{{ end }} diff --git a/internal/configs/version2/templates_test.go b/internal/configs/version2/templates_test.go index e2af009e90..f56315880d 100644 --- a/internal/configs/version2/templates_test.go +++ b/internal/configs/version2/templates_test.go @@ -1,6 +1,7 @@ package version2 import ( + "bytes" "testing" ) @@ -38,6 +39,9 @@ func TestExecuteVirtualServerTemplate_RendersTemplateWithServerGunzipOn(t *testi if err != nil { t.Error(err) } + if !bytes.Contains(got, []byte("gunzip on;")) { + t.Error("want `gunzip on` directive, got no directive") + } t.Log(string(got)) } @@ -51,32 +55,25 @@ func TestExecuteVirtualServerTemplate_RendersTemplateWithServerGunzipOff(t *test if err != nil { t.Error(err) } - t.Log(string(got)) -} - -func TestExecuteVirtualServerTemplate_RendersTemplateWithServerGunzipEmpty(t *testing.T) { - t.Parallel() - executor, err := NewTemplateExecutor(nginxPlusVirtualServerTmpl, nginxPlusTransportServerTmpl) - if err != nil { - t.Fatal(err) - } - got, err := executor.ExecuteVirtualServerTemplate(&virtualServerCfgWithEmptyGunzip) - if err != nil { - t.Error(err) + if bytes.Contains(got, []byte("gunzip on;")) { + t.Error("want no directive, got `gunzip on`") } t.Log(string(got)) } -func TestExecuteVirtualServerTemplate_RendersTemplateWithoutServerGunzip(t *testing.T) { +func TestExecuteVirtualServerTemplate_RendersTemplateWithServerGunzipNotSet(t *testing.T) { t.Parallel() executor, err := NewTemplateExecutor(nginxPlusVirtualServerTmpl, nginxPlusTransportServerTmpl) if err != nil { t.Fatal(err) } - got, err := executor.ExecuteVirtualServerTemplate(&virtualServerCfg) + got, err := executor.ExecuteVirtualServerTemplate(&virtualServerCfgWithGunzipNotSet) if err != nil { t.Error(err) } + if bytes.Contains(got, []byte("gunzip on;")) { + t.Error("want no directive, got `gunzip on` directive") + } t.Log(string(got)) } @@ -853,7 +850,7 @@ var ( }, }, }, - Gunzip: "on", + Gunzip: true, }, } @@ -1199,11 +1196,11 @@ var ( }, }, }, - Gunzip: "off", + Gunzip: false, }, } - virtualServerCfgWithEmptyGunzip = VirtualServerConfig{ + virtualServerCfgWithGunzipNotSet = VirtualServerConfig{ LimitReqZones: []LimitReqZone{ { ZoneName: "pol_rl_test_test_test", Rate: "10r/s", ZoneSize: "10m", Key: "$url", @@ -1545,7 +1542,6 @@ var ( }, }, }, - Gunzip: "", }, } diff --git a/internal/configs/virtualserver.go b/internal/configs/virtualserver.go index bd28697689..bba32f5ca3 100644 --- a/internal/configs/virtualserver.go +++ b/internal/configs/virtualserver.go @@ -654,6 +654,7 @@ func (vsc *virtualServerConfigurator) GenerateVirtualServerConfig( HTTPSnippets: httpSnippets, Server: version2.Server{ ServerName: vsEx.VirtualServer.Spec.Host, + Gunzip: vsEx.VirtualServer.Spec.Gunzip, StatusZone: vsEx.VirtualServer.Spec.Host, ProxyProtocol: vsc.cfgParams.ProxyProtocol, SSL: sslConfig, diff --git a/internal/configs/virtualserver_test.go b/internal/configs/virtualserver_test.go index 2aa856eac9..3811d7efe4 100644 --- a/internal/configs/virtualserver_test.go +++ b/internal/configs/virtualserver_test.go @@ -197,210 +197,12 @@ func TestVariableNamer(t *testing.T) { } } -func TestGenerateVirtualServerConfig(t *testing.T) { +func TestGenerateVSConfig_GeneratesConfigWithGunzipOn(t *testing.T) { t.Parallel() - virtualServerEx := VirtualServerEx{ - VirtualServer: &conf_v1.VirtualServer{ - ObjectMeta: meta_v1.ObjectMeta{ - Name: "cafe", - Namespace: "default", - }, - Spec: conf_v1.VirtualServerSpec{ - Host: "cafe.example.com", - Upstreams: []conf_v1.Upstream{ - { - Name: "tea", - Service: "tea-svc", - Port: 80, - }, - { - Name: "tea-latest", - Service: "tea-svc", - Subselector: map[string]string{"version": "v1"}, - Port: 80, - }, - { - Name: "coffee", - Service: "coffee-svc", - Port: 80, - }, - }, - Routes: []conf_v1.Route{ - { - Path: "/tea", - Action: &conf_v1.Action{ - Pass: "tea", - }, - }, - { - Path: "/tea-latest", - Action: &conf_v1.Action{ - Pass: "tea-latest", - }, - }, - { - Path: "/coffee", - Route: "default/coffee", - }, - { - Path: "/subtea", - Route: "default/subtea", - }, - { - Path: "/coffee-errorpage", - Action: &conf_v1.Action{ - Pass: "coffee", - }, - ErrorPages: []conf_v1.ErrorPage{ - { - Codes: []int{401, 403}, - Redirect: &conf_v1.ErrorPageRedirect{ - ActionRedirect: conf_v1.ActionRedirect{ - URL: "http://nginx.com", - Code: 301, - }, - }, - }, - }, - }, - { - Path: "/coffee-errorpage-subroute", - Route: "default/subcoffee", - ErrorPages: []conf_v1.ErrorPage{ - { - Codes: []int{401, 403}, - Redirect: &conf_v1.ErrorPageRedirect{ - ActionRedirect: conf_v1.ActionRedirect{ - URL: "http://nginx.com", - Code: 301, - }, - }, - }, - }, - }, - }, - }, - }, - Endpoints: map[string][]string{ - "default/tea-svc:80": { - "10.0.0.20:80", - }, - "default/tea-svc_version=v1:80": { - "10.0.0.30:80", - }, - "default/coffee-svc:80": { - "10.0.0.40:80", - }, - "default/sub-tea-svc_version=v1:80": { - "10.0.0.50:80", - }, - }, - VirtualServerRoutes: []*conf_v1.VirtualServerRoute{ - { - ObjectMeta: meta_v1.ObjectMeta{ - Name: "coffee", - Namespace: "default", - }, - Spec: conf_v1.VirtualServerRouteSpec{ - Host: "cafe.example.com", - Upstreams: []conf_v1.Upstream{ - { - Name: "coffee", - Service: "coffee-svc", - Port: 80, - }, - }, - Subroutes: []conf_v1.Route{ - { - Path: "/coffee", - Action: &conf_v1.Action{ - Pass: "coffee", - }, - }, - }, - }, - }, - { - ObjectMeta: meta_v1.ObjectMeta{ - Name: "subtea", - Namespace: "default", - }, - Spec: conf_v1.VirtualServerRouteSpec{ - Host: "cafe.example.com", - Upstreams: []conf_v1.Upstream{ - { - Name: "subtea", - Service: "sub-tea-svc", - Port: 80, - Subselector: map[string]string{"version": "v1"}, - }, - }, - Subroutes: []conf_v1.Route{ - { - Path: "/subtea", - Action: &conf_v1.Action{ - Pass: "subtea", - }, - }, - }, - }, - }, - { - ObjectMeta: meta_v1.ObjectMeta{ - Name: "subcoffee", - Namespace: "default", - }, - Spec: conf_v1.VirtualServerRouteSpec{ - Host: "cafe.example.com", - Upstreams: []conf_v1.Upstream{ - { - Name: "coffee", - Service: "coffee-svc", - Port: 80, - }, - }, - Subroutes: []conf_v1.Route{ - { - Path: "/coffee-errorpage-subroute", - Action: &conf_v1.Action{ - Pass: "coffee", - }, - }, - { - Path: "/coffee-errorpage-subroute-defined", - Action: &conf_v1.Action{ - Pass: "coffee", - }, - ErrorPages: []conf_v1.ErrorPage{ - { - Codes: []int{502, 503}, - Return: &conf_v1.ErrorPageReturn{ - ActionReturn: conf_v1.ActionReturn{ - Code: 200, - Type: "text/plain", - Body: "All Good", - }, - }, - }, - }, - }, - }, - }, - }, - }, - } - baseCfgParams := ConfigParams{ - ServerTokens: "off", - Keepalive: 16, - ServerSnippets: []string{"# server snippet"}, - ProxyProtocol: true, - SetRealIPFrom: []string{"0.0.0.0/0"}, - RealIPHeader: "X-Real-IP", - RealIPRecursive: true, - } + vsc := newVirtualServerConfigurator(&baseCfgParams, true, false, &StaticConfigParams{TLSPassthrough: true}, false) - expected := version2.VirtualServerConfig{ + want := version2.VirtualServerConfig{ Upstreams: []version2.Upstream{ { UpstreamLabels: version2.UpstreamLabels{ @@ -497,6 +299,7 @@ func TestGenerateVirtualServerConfig(t *testing.T) { LimitReqZones: []version2.LimitReqZone{}, Server: version2.Server{ ServerName: "cafe.example.com", + Gunzip: true, StatusZone: "cafe.example.com", VSNamespace: "default", VSName: "cafe", @@ -643,79 +446,37 @@ func TestGenerateVirtualServerConfig(t *testing.T) { }, } - isPlus := false - isResolverConfigured := false - isWildcardEnabled := false - vsc := newVirtualServerConfigurator( - &baseCfgParams, - isPlus, - isResolverConfigured, - &StaticConfigParams{TLSPassthrough: true}, - isWildcardEnabled, - ) - - result, warnings := vsc.GenerateVirtualServerConfig(&virtualServerEx, nil, nil) - if diff := cmp.Diff(expected, result); diff != "" { - t.Errorf("GenerateVirtualServerConfig() mismatch (-want +got):\n%s", diff) + got, warnings := vsc.GenerateVirtualServerConfig(&virtualServerExWithGunzipOn, nil, nil) + if len(warnings) > 0 { + t.Fatalf("want no warnings, got: %v", vsc.warnings) } - - if len(warnings) != 0 { - t.Errorf("GenerateVirtualServerConfig returned warnings: %v", vsc.warnings) + if !cmp.Equal(want, got) { + t.Error(cmp.Diff(want, got)) } } -func TestGenerateVirtualServerConfigIPV6Disabled(t *testing.T) { +func TestGenerateVSConfig_GeneratesConfigWithGunzipOff(t *testing.T) { t.Parallel() - virtualServerEx := VirtualServerEx{ - VirtualServer: &conf_v1.VirtualServer{ - ObjectMeta: meta_v1.ObjectMeta{ - Name: "cafe", - Namespace: "default", - }, - Spec: conf_v1.VirtualServerSpec{ - Host: "cafe.example.com", - Upstreams: []conf_v1.Upstream{ - { - Name: "tea", - Service: "tea-svc", - Port: 80, - }, + + vsc := newVirtualServerConfigurator(&baseCfgParams, true, false, &StaticConfigParams{TLSPassthrough: true}, false) + + want := version2.VirtualServerConfig{ + Upstreams: []version2.Upstream{ + { + UpstreamLabels: version2.UpstreamLabels{ + Service: "tea-svc", + ResourceType: "virtualserver", + ResourceName: "cafe", + ResourceNamespace: "default", + }, + Name: "vs_default_cafe_tea", + Servers: []version2.UpstreamServer{ { - Name: "coffee", - Service: "coffee-svc", - Port: 80, + Address: "10.0.0.20:80", }, }, - Routes: []conf_v1.Route{ - { - Path: "/tea", - Action: &conf_v1.Action{ - Pass: "tea", - }, - }, - { - Path: "/coffee", - Action: &conf_v1.Action{ - Pass: "coffee", - }, - }, - }, - }, - }, - Endpoints: map[string][]string{ - "default/tea-svc:80": { - "10.0.0.20:80", - }, - "default/coffee-svc:80": { - "10.0.0.40:80", + Keepalive: 16, }, - }, - } - - baseCfgParams := ConfigParams{} - - expected := version2.VirtualServerConfig{ - Upstreams: []version2.Upstream{ { UpstreamLabels: version2.UpstreamLabels{ Service: "tea-svc", @@ -723,12 +484,13 @@ func TestGenerateVirtualServerConfigIPV6Disabled(t *testing.T) { ResourceName: "cafe", ResourceNamespace: "default", }, - Name: "vs_default_cafe_tea", + Name: "vs_default_cafe_tea-latest", Servers: []version2.UpstreamServer{ { - Address: "10.0.0.20:80", + Address: "10.0.0.30:80", }, }, + Keepalive: 16, }, { UpstreamLabels: version2.UpstreamLabels{ @@ -743,429 +505,473 @@ func TestGenerateVirtualServerConfigIPV6Disabled(t *testing.T) { Address: "10.0.0.40:80", }, }, + Keepalive: 16, + }, + { + UpstreamLabels: version2.UpstreamLabels{ + Service: "coffee-svc", + ResourceType: "virtualserverroute", + ResourceName: "coffee", + ResourceNamespace: "default", + }, + Name: "vs_default_cafe_vsr_default_coffee_coffee", + Servers: []version2.UpstreamServer{ + { + Address: "10.0.0.40:80", + }, + }, + Keepalive: 16, + }, + { + UpstreamLabels: version2.UpstreamLabels{ + Service: "sub-tea-svc", + ResourceType: "virtualserverroute", + ResourceName: "subtea", + ResourceNamespace: "default", + }, + Name: "vs_default_cafe_vsr_default_subtea_subtea", + Servers: []version2.UpstreamServer{ + { + Address: "10.0.0.50:80", + }, + }, + Keepalive: 16, + }, + { + UpstreamLabels: version2.UpstreamLabels{ + Service: "coffee-svc", + ResourceType: "virtualserverroute", + ResourceName: "subcoffee", + ResourceNamespace: "default", + }, + Name: "vs_default_cafe_vsr_default_subcoffee_coffee", + Servers: []version2.UpstreamServer{ + { + Address: "10.0.0.40:80", + }, + }, + Keepalive: 16, }, }, HTTPSnippets: []string{}, LimitReqZones: []version2.LimitReqZone{}, Server: version2.Server{ - ServerName: "cafe.example.com", - StatusZone: "cafe.example.com", - VSNamespace: "default", - VSName: "cafe", - DisableIPV6: true, + ServerName: "cafe.example.com", + Gunzip: false, + StatusZone: "cafe.example.com", + VSNamespace: "default", + VSName: "cafe", + ProxyProtocol: true, + ServerTokens: "off", + SetRealIPFrom: []string{"0.0.0.0/0"}, + RealIPHeader: "X-Real-IP", + RealIPRecursive: true, + Snippets: []string{"# server snippet"}, + TLSPassthrough: true, Locations: []version2.Location{ { Path: "/tea", ProxyPass: "http://vs_default_cafe_tea", ProxyNextUpstream: "error timeout", ProxyNextUpstreamTimeout: "0s", + ProxyNextUpstreamTries: 0, + HasKeepalive: true, ProxySSLName: "tea-svc.default.svc", ProxyPassRequestHeaders: true, ProxySetHeaders: []version2.Header{{Name: "Host", Value: "$host"}}, ServiceName: "tea-svc", }, { - Path: "/coffee", + Path: "/tea-latest", + ProxyPass: "http://vs_default_cafe_tea-latest", + ProxyNextUpstream: "error timeout", + ProxyNextUpstreamTimeout: "0s", + ProxyNextUpstreamTries: 0, + HasKeepalive: true, + ProxySSLName: "tea-svc.default.svc", + ProxyPassRequestHeaders: true, + ProxySetHeaders: []version2.Header{{Name: "Host", Value: "$host"}}, + ServiceName: "tea-svc", + }, + // Order changes here because we generate first all the VS Routes and then all the VSR Subroutes (separated for loops) + { + Path: "/coffee-errorpage", ProxyPass: "http://vs_default_cafe_coffee", ProxyNextUpstream: "error timeout", ProxyNextUpstreamTimeout: "0s", + ProxyNextUpstreamTries: 0, + HasKeepalive: true, + ProxyInterceptErrors: true, + ErrorPages: []version2.ErrorPage{ + { + Name: "http://nginx.com", + Codes: "401 403", + ResponseCode: 301, + }, + }, + ProxySSLName: "coffee-svc.default.svc", + ProxyPassRequestHeaders: true, + ProxySetHeaders: []version2.Header{{Name: "Host", Value: "$host"}}, + ServiceName: "coffee-svc", + }, + { + Path: "/coffee", + ProxyPass: "http://vs_default_cafe_vsr_default_coffee_coffee", + ProxyNextUpstream: "error timeout", + ProxyNextUpstreamTimeout: "0s", + ProxyNextUpstreamTries: 0, + HasKeepalive: true, ProxySSLName: "coffee-svc.default.svc", ProxyPassRequestHeaders: true, ProxySetHeaders: []version2.Header{{Name: "Host", Value: "$host"}}, ServiceName: "coffee-svc", + IsVSR: true, + VSRName: "coffee", + VSRNamespace: "default", + }, + { + Path: "/subtea", + ProxyPass: "http://vs_default_cafe_vsr_default_subtea_subtea", + ProxyNextUpstream: "error timeout", + ProxyNextUpstreamTimeout: "0s", + ProxyNextUpstreamTries: 0, + HasKeepalive: true, + ProxySSLName: "sub-tea-svc.default.svc", + ProxyPassRequestHeaders: true, + ProxySetHeaders: []version2.Header{{Name: "Host", Value: "$host"}}, + ServiceName: "sub-tea-svc", + IsVSR: true, + VSRName: "subtea", + VSRNamespace: "default", + }, + + { + Path: "/coffee-errorpage-subroute", + ProxyPass: "http://vs_default_cafe_vsr_default_subcoffee_coffee", + ProxyNextUpstream: "error timeout", + ProxyNextUpstreamTimeout: "0s", + ProxyNextUpstreamTries: 0, + HasKeepalive: true, + ProxyInterceptErrors: true, + ErrorPages: []version2.ErrorPage{ + { + Name: "http://nginx.com", + Codes: "401 403", + ResponseCode: 301, + }, + }, + ProxySSLName: "coffee-svc.default.svc", + ProxyPassRequestHeaders: true, + ProxySetHeaders: []version2.Header{{Name: "Host", Value: "$host"}}, + ServiceName: "coffee-svc", + IsVSR: true, + VSRName: "subcoffee", + VSRNamespace: "default", + }, + { + Path: "/coffee-errorpage-subroute-defined", + ProxyPass: "http://vs_default_cafe_vsr_default_subcoffee_coffee", + ProxyNextUpstream: "error timeout", + ProxyNextUpstreamTimeout: "0s", + ProxyNextUpstreamTries: 0, + HasKeepalive: true, + ProxyInterceptErrors: true, + ErrorPages: []version2.ErrorPage{ + { + Name: "@error_page_0_0", + Codes: "502 503", + ResponseCode: 200, + }, + }, + ProxySSLName: "coffee-svc.default.svc", + ProxyPassRequestHeaders: true, + ProxySetHeaders: []version2.Header{{Name: "Host", Value: "$host"}}, + ServiceName: "coffee-svc", + IsVSR: true, + VSRName: "subcoffee", + VSRNamespace: "default", + }, + }, + ErrorPageLocations: []version2.ErrorPageLocation{ + { + Name: "@error_page_0_0", + DefaultType: "text/plain", + Return: &version2.Return{ + Text: "All Good", + }, }, }, }, } - isPlus := false - isResolverConfigured := false - isWildcardEnabled := false - vsc := newVirtualServerConfigurator( - &baseCfgParams, - isPlus, - isResolverConfigured, - &StaticConfigParams{DisableIPV6: true}, - isWildcardEnabled, - ) - - result, warnings := vsc.GenerateVirtualServerConfig(&virtualServerEx, nil, nil) - if diff := cmp.Diff(expected, result); diff != "" { - t.Errorf("GenerateVirtualServerConfig() mismatch (-want +got):\n%s", diff) + got, warnings := vsc.GenerateVirtualServerConfig(&virtualServerExWithGunzipOff, nil, nil) + if len(warnings) > 0 { + t.Fatalf("want no warnings, got: %v", vsc.warnings) } - - if len(warnings) != 0 { - t.Errorf("GenerateVirtualServerConfig returned warnings: %v", vsc.warnings) + if !cmp.Equal(want, got) { + t.Error(cmp.Diff(want, got)) } } -func TestGenerateVirtualServerConfigGrpcErrorPageWarning(t *testing.T) { +func TestGenerateVSConfig_GeneratesConfigWithNoGunzip(t *testing.T) { t.Parallel() - virtualServerEx := VirtualServerEx{ - VirtualServer: &conf_v1.VirtualServer{ - ObjectMeta: meta_v1.ObjectMeta{ - Name: "cafe", - Namespace: "default", - }, - Spec: conf_v1.VirtualServerSpec{ - Host: "cafe.example.com", - TLS: &conf_v1.TLS{ - Secret: "", + + vsc := newVirtualServerConfigurator(&baseCfgParams, true, false, &StaticConfigParams{TLSPassthrough: true}, false) + + want := version2.VirtualServerConfig{ + Upstreams: []version2.Upstream{ + { + UpstreamLabels: version2.UpstreamLabels{ + Service: "tea-svc", + ResourceType: "virtualserver", + ResourceName: "cafe", + ResourceNamespace: "default", }, - Upstreams: []conf_v1.Upstream{ + Name: "vs_default_cafe_tea", + Servers: []version2.UpstreamServer{ { - Name: "grpc-app-1", - Service: "grpc-svc", - Port: 50051, - Type: "grpc", - TLS: conf_v1.UpstreamTLS{ - Enable: true, - }, - }, - { - Name: "grpc-app-2", - Service: "grpc-svc2", - Port: 50052, - Type: "grpc", - TLS: conf_v1.UpstreamTLS{ - Enable: true, - }, - }, - { - Name: "tea", - Service: "tea-svc", - Port: 80, - }, - }, - Routes: []conf_v1.Route{ - { - Path: "/grpc-errorpage", - Action: &conf_v1.Action{ - Pass: "grpc-app-1", - }, - ErrorPages: []conf_v1.ErrorPage{ - { - Codes: []int{404, 405}, - Return: &conf_v1.ErrorPageReturn{ - ActionReturn: conf_v1.ActionReturn{ - Code: 200, - Type: "text/plain", - Body: "All Good", - }, - }, - }, - }, - }, - { - Path: "/grpc-matches", - Matches: []conf_v1.Match{ - { - Conditions: []conf_v1.Condition{ - { - Variable: "$request_method", - Value: "POST", - }, - }, - Action: &conf_v1.Action{ - Pass: "grpc-app-2", - }, - }, - }, - Action: &conf_v1.Action{ - Pass: "tea", - }, - ErrorPages: []conf_v1.ErrorPage{ - { - Codes: []int{404}, - Return: &conf_v1.ErrorPageReturn{ - ActionReturn: conf_v1.ActionReturn{ - Code: 200, - Type: "text/plain", - Body: "Original resource not found, but success!", - }, - }, - }, - }, - }, - { - Path: "/grpc-splits", - Splits: []conf_v1.Split{ - { - Weight: 90, - Action: &conf_v1.Action{ - Pass: "grpc-app-1", - }, - }, - { - Weight: 10, - Action: &conf_v1.Action{ - Pass: "grpc-app-2", - }, - }, - }, - ErrorPages: []conf_v1.ErrorPage{ - { - Codes: []int{404, 405}, - Return: &conf_v1.ErrorPageReturn{ - ActionReturn: conf_v1.ActionReturn{ - Code: 200, - Type: "text/plain", - Body: "All Good", - }, - }, - }, - }, + Address: "10.0.0.20:80", }, }, + Keepalive: 16, }, - }, - Endpoints: map[string][]string{ - "default/grpc-svc:50051": { - "10.0.0.20:80", - }, - }, - } - - baseCfgParams := ConfigParams{ - HTTP2: true, - } - - expected := version2.VirtualServerConfig{ - Upstreams: []version2.Upstream{ { UpstreamLabels: version2.UpstreamLabels{ - Service: "grpc-svc", + Service: "tea-svc", ResourceType: "virtualserver", ResourceName: "cafe", ResourceNamespace: "default", }, - Name: "vs_default_cafe_grpc-app-1", + Name: "vs_default_cafe_tea-latest", Servers: []version2.UpstreamServer{ { - Address: "10.0.0.20:80", + Address: "10.0.0.30:80", }, }, + Keepalive: 16, }, { - Name: "vs_default_cafe_grpc-app-2", UpstreamLabels: version2.UpstreamLabels{ - Service: "grpc-svc2", + Service: "coffee-svc", ResourceType: "virtualserver", ResourceName: "cafe", ResourceNamespace: "default", }, + Name: "vs_default_cafe_coffee", Servers: []version2.UpstreamServer{ { - Address: "unix:/var/lib/nginx/nginx-502-server.sock", + Address: "10.0.0.40:80", }, }, + Keepalive: 16, }, { - Name: "vs_default_cafe_tea", UpstreamLabels: version2.UpstreamLabels{ - Service: "tea-svc", - ResourceType: "virtualserver", - ResourceName: "cafe", + Service: "coffee-svc", + ResourceType: "virtualserverroute", + ResourceName: "coffee", ResourceNamespace: "default", }, + Name: "vs_default_cafe_vsr_default_coffee_coffee", Servers: []version2.UpstreamServer{ { - Address: "unix:/var/lib/nginx/nginx-502-server.sock", + Address: "10.0.0.40:80", }, }, + Keepalive: 16, }, - }, - HTTPSnippets: []string{}, - LimitReqZones: []version2.LimitReqZone{}, - Maps: []version2.Map{ { - Source: "$request_method", - Variable: "$vs_default_cafe_matches_0_match_0_cond_0", - Parameters: []version2.Parameter{ - { - Value: `"POST"`, - Result: "1", - }, + UpstreamLabels: version2.UpstreamLabels{ + Service: "sub-tea-svc", + ResourceType: "virtualserverroute", + ResourceName: "subtea", + ResourceNamespace: "default", + }, + Name: "vs_default_cafe_vsr_default_subtea_subtea", + Servers: []version2.UpstreamServer{ { - Value: "default", - Result: "0", + Address: "10.0.0.50:80", }, }, + Keepalive: 16, }, { - Source: "$vs_default_cafe_matches_0_match_0_cond_0", - Variable: "$vs_default_cafe_matches_0", - Parameters: []version2.Parameter{ - { - Value: "~^1", - Result: "/internal_location_matches_0_match_0", - }, + UpstreamLabels: version2.UpstreamLabels{ + Service: "coffee-svc", + ResourceType: "virtualserverroute", + ResourceName: "subcoffee", + ResourceNamespace: "default", + }, + Name: "vs_default_cafe_vsr_default_subcoffee_coffee", + Servers: []version2.UpstreamServer{ { - Value: "default", - Result: "/internal_location_matches_0_default", + Address: "10.0.0.40:80", }, }, + Keepalive: 16, }, }, + HTTPSnippets: []string{}, + LimitReqZones: []version2.LimitReqZone{}, Server: version2.Server{ - ServerName: "cafe.example.com", - StatusZone: "cafe.example.com", - VSNamespace: "default", - VSName: "cafe", - SSL: &version2.SSL{ - HTTP2: true, - Certificate: "/etc/nginx/secrets/wildcard", - CertificateKey: "/etc/nginx/secrets/wildcard", - }, - InternalRedirectLocations: []version2.InternalRedirectLocation{ - { - Path: "/grpc-matches", - Destination: "$vs_default_cafe_matches_0", - }, - { - Path: "/grpc-splits", - Destination: "$vs_default_cafe_splits_0", - }, - }, + ServerName: "cafe.example.com", + Gunzip: false, + StatusZone: "cafe.example.com", + VSNamespace: "default", + VSName: "cafe", + ProxyProtocol: true, + ServerTokens: "off", + SetRealIPFrom: []string{"0.0.0.0/0"}, + RealIPHeader: "X-Real-IP", + RealIPRecursive: true, + Snippets: []string{"# server snippet"}, + TLSPassthrough: true, Locations: []version2.Location{ { - Path: "/grpc-errorpage", - ProxyPass: "https://vs_default_cafe_grpc-app-1", - ProxyNextUpstream: "error timeout", - ProxyNextUpstreamTimeout: "0s", - ProxyNextUpstreamTries: 0, - ErrorPages: []version2.ErrorPage{{Name: "@error_page_0_0", Codes: "404 405", ResponseCode: 200}}, - ProxyInterceptErrors: true, - ProxySSLName: "grpc-svc.default.svc", - ProxyPassRequestHeaders: true, - ProxySetHeaders: []version2.Header{{Name: "Host", Value: "$host"}}, - ServiceName: "grpc-svc", - GRPCPass: "grpcs://vs_default_cafe_grpc-app-1", - }, - { - Path: "/internal_location_matches_0_match_0", - Internal: true, - ProxyPass: "https://vs_default_cafe_grpc-app-2$request_uri", + Path: "/tea", + ProxyPass: "http://vs_default_cafe_tea", ProxyNextUpstream: "error timeout", ProxyNextUpstreamTimeout: "0s", ProxyNextUpstreamTries: 0, - Rewrites: []string{"^ $request_uri break"}, - ErrorPages: []version2.ErrorPage{{Name: "@error_page_1_0", Codes: "404", ResponseCode: 200}}, - ProxyInterceptErrors: true, - ProxySSLName: "grpc-svc2.default.svc", + HasKeepalive: true, + ProxySSLName: "tea-svc.default.svc", ProxyPassRequestHeaders: true, ProxySetHeaders: []version2.Header{{Name: "Host", Value: "$host"}}, - ServiceName: "grpc-svc2", - GRPCPass: "grpcs://vs_default_cafe_grpc-app-2", + ServiceName: "tea-svc", }, { - Path: "/internal_location_matches_0_default", - Internal: true, - ProxyPass: "http://vs_default_cafe_tea$request_uri", + Path: "/tea-latest", + ProxyPass: "http://vs_default_cafe_tea-latest", ProxyNextUpstream: "error timeout", ProxyNextUpstreamTimeout: "0s", ProxyNextUpstreamTries: 0, - ErrorPages: []version2.ErrorPage{{Name: "@error_page_1_0", Codes: "404", ResponseCode: 200}}, - ProxyInterceptErrors: true, + HasKeepalive: true, ProxySSLName: "tea-svc.default.svc", ProxyPassRequestHeaders: true, ProxySetHeaders: []version2.Header{{Name: "Host", Value: "$host"}}, ServiceName: "tea-svc", }, + // Order changes here because we generate first all the VS Routes and then all the VSR Subroutes (separated for loops) { - Path: "/internal_location_splits_0_split_0", - Internal: true, - ProxyPass: "https://vs_default_cafe_grpc-app-1$request_uri", + Path: "/coffee-errorpage", + ProxyPass: "http://vs_default_cafe_coffee", ProxyNextUpstream: "error timeout", ProxyNextUpstreamTimeout: "0s", ProxyNextUpstreamTries: 0, - HasKeepalive: false, - ErrorPages: []version2.ErrorPage{{Name: "@error_page_2_0", Codes: "404 405", ResponseCode: 200}}, + HasKeepalive: true, ProxyInterceptErrors: true, - Rewrites: []string{"^ $request_uri break"}, - ProxySSLName: "grpc-svc.default.svc", - ProxyPassRequestHeaders: true, - ProxySetHeaders: []version2.Header{{Name: "Host", Value: "$host"}}, - ServiceName: "grpc-svc", - GRPCPass: "grpcs://vs_default_cafe_grpc-app-1", + ErrorPages: []version2.ErrorPage{ + { + Name: "http://nginx.com", + Codes: "401 403", + ResponseCode: 301, + }, + }, + ProxySSLName: "coffee-svc.default.svc", + ProxyPassRequestHeaders: true, + ProxySetHeaders: []version2.Header{{Name: "Host", Value: "$host"}}, + ServiceName: "coffee-svc", }, { - Path: "/internal_location_splits_0_split_1", - Internal: true, - ProxyPass: "https://vs_default_cafe_grpc-app-2$request_uri", + Path: "/coffee", + ProxyPass: "http://vs_default_cafe_vsr_default_coffee_coffee", ProxyNextUpstream: "error timeout", ProxyNextUpstreamTimeout: "0s", ProxyNextUpstreamTries: 0, - HasKeepalive: false, - ErrorPages: []version2.ErrorPage{{Name: "@error_page_2_0", Codes: "404 405", ResponseCode: 200}}, - ProxyInterceptErrors: true, - Rewrites: []string{"^ $request_uri break"}, - ProxySSLName: "grpc-svc2.default.svc", + HasKeepalive: true, + ProxySSLName: "coffee-svc.default.svc", ProxyPassRequestHeaders: true, ProxySetHeaders: []version2.Header{{Name: "Host", Value: "$host"}}, - ServiceName: "grpc-svc2", - GRPCPass: "grpcs://vs_default_cafe_grpc-app-2", + ServiceName: "coffee-svc", + IsVSR: true, + VSRName: "coffee", + VSRNamespace: "default", }, - }, - ErrorPageLocations: []version2.ErrorPageLocation{ { - Name: "@error_page_0_0", - DefaultType: "text/plain", - Return: &version2.Return{Text: "All Good"}, + Path: "/subtea", + ProxyPass: "http://vs_default_cafe_vsr_default_subtea_subtea", + ProxyNextUpstream: "error timeout", + ProxyNextUpstreamTimeout: "0s", + ProxyNextUpstreamTries: 0, + HasKeepalive: true, + ProxySSLName: "sub-tea-svc.default.svc", + ProxyPassRequestHeaders: true, + ProxySetHeaders: []version2.Header{{Name: "Host", Value: "$host"}}, + ServiceName: "sub-tea-svc", + IsVSR: true, + VSRName: "subtea", + VSRNamespace: "default", }, + { - Name: "@error_page_1_0", - DefaultType: "text/plain", - Return: &version2.Return{Text: "Original resource not found, but success!"}, + Path: "/coffee-errorpage-subroute", + ProxyPass: "http://vs_default_cafe_vsr_default_subcoffee_coffee", + ProxyNextUpstream: "error timeout", + ProxyNextUpstreamTimeout: "0s", + ProxyNextUpstreamTries: 0, + HasKeepalive: true, + ProxyInterceptErrors: true, + ErrorPages: []version2.ErrorPage{ + { + Name: "http://nginx.com", + Codes: "401 403", + ResponseCode: 301, + }, + }, + ProxySSLName: "coffee-svc.default.svc", + ProxyPassRequestHeaders: true, + ProxySetHeaders: []version2.Header{{Name: "Host", Value: "$host"}}, + ServiceName: "coffee-svc", + IsVSR: true, + VSRName: "subcoffee", + VSRNamespace: "default", }, { - Name: "@error_page_2_0", - DefaultType: "text/plain", - Return: &version2.Return{Text: "All Good"}, + Path: "/coffee-errorpage-subroute-defined", + ProxyPass: "http://vs_default_cafe_vsr_default_subcoffee_coffee", + ProxyNextUpstream: "error timeout", + ProxyNextUpstreamTimeout: "0s", + ProxyNextUpstreamTries: 0, + HasKeepalive: true, + ProxyInterceptErrors: true, + ErrorPages: []version2.ErrorPage{ + { + Name: "@error_page_0_0", + Codes: "502 503", + ResponseCode: 200, + }, + }, + ProxySSLName: "coffee-svc.default.svc", + ProxyPassRequestHeaders: true, + ProxySetHeaders: []version2.Header{{Name: "Host", Value: "$host"}}, + ServiceName: "coffee-svc", + IsVSR: true, + VSRName: "subcoffee", + VSRNamespace: "default", }, }, - }, - SplitClients: []version2.SplitClient{ - { - Source: "$request_id", - Variable: "$vs_default_cafe_splits_0", - Distributions: []version2.Distribution{ - { - Weight: "90%", - Value: "/internal_location_splits_0_split_0", - }, - { - Weight: "10%", - Value: "/internal_location_splits_0_split_1", + ErrorPageLocations: []version2.ErrorPageLocation{ + { + Name: "@error_page_0_0", + DefaultType: "text/plain", + Return: &version2.Return{ + Text: "All Good", }, }, }, }, } - expectedWarnings := Warnings{ - virtualServerEx.VirtualServer: { - `The error page configuration for the upstream grpc-app-1 is ignored for status code(s) [404 405], which cannot be used for GRPC upstreams.`, - `The error page configuration for the upstream grpc-app-2 is ignored for status code(s) [404], which cannot be used for GRPC upstreams.`, - `The error page configuration for the upstream grpc-app-1 is ignored for status code(s) [404 405], which cannot be used for GRPC upstreams.`, - `The error page configuration for the upstream grpc-app-2 is ignored for status code(s) [404 405], which cannot be used for GRPC upstreams.`, - }, - } - isPlus := false - isResolverConfigured := false - isWildcardEnabled := true - vsc := newVirtualServerConfigurator(&baseCfgParams, isPlus, isResolverConfigured, &StaticConfigParams{}, isWildcardEnabled) - result, warnings := vsc.GenerateVirtualServerConfig(&virtualServerEx, nil, nil) - if diff := cmp.Diff(expected, result); diff != "" { - t.Errorf("TestGenerateVirtualServerConfigGrpcErrorPageWarning() mismatch (-want +got):\n%s", diff) + got, warnings := vsc.GenerateVirtualServerConfig(&virtualServerExWithNoGunzip, nil, nil) + if len(warnings) > 0 { + t.Fatalf("want no warnings, got: %v", vsc.warnings) } - - if !reflect.DeepEqual(vsc.warnings, expectedWarnings) { - t.Errorf("GenerateVirtualServerConfig() returned warnings of \n%v but expected \n%v", warnings, expectedWarnings) + if !cmp.Equal(want, got) { + t.Error(cmp.Diff(want, got)) } } -func TestGenerateVirtualServerConfigWithSpiffeCerts(t *testing.T) { +func TestGenerateVirtualServerConfig(t *testing.T) { t.Parallel() virtualServerEx := VirtualServerEx{ VirtualServer: &conf_v1.VirtualServer{ @@ -1181,6 +987,17 @@ func TestGenerateVirtualServerConfigWithSpiffeCerts(t *testing.T) { Service: "tea-svc", Port: 80, }, + { + Name: "tea-latest", + Service: "tea-svc", + Subselector: map[string]string{"version": "v1"}, + Port: 80, + }, + { + Name: "coffee", + Service: "coffee-svc", + Port: 80, + }, }, Routes: []conf_v1.Route{ { @@ -1189,6 +1006,52 @@ func TestGenerateVirtualServerConfigWithSpiffeCerts(t *testing.T) { Pass: "tea", }, }, + { + Path: "/tea-latest", + Action: &conf_v1.Action{ + Pass: "tea-latest", + }, + }, + { + Path: "/coffee", + Route: "default/coffee", + }, + { + Path: "/subtea", + Route: "default/subtea", + }, + { + Path: "/coffee-errorpage", + Action: &conf_v1.Action{ + Pass: "coffee", + }, + ErrorPages: []conf_v1.ErrorPage{ + { + Codes: []int{401, 403}, + Redirect: &conf_v1.ErrorPageRedirect{ + ActionRedirect: conf_v1.ActionRedirect{ + URL: "http://nginx.com", + Code: 301, + }, + }, + }, + }, + }, + { + Path: "/coffee-errorpage-subroute", + Route: "default/subcoffee", + ErrorPages: []conf_v1.ErrorPage{ + { + Codes: []int{401, 403}, + Redirect: &conf_v1.ErrorPageRedirect{ + ActionRedirect: conf_v1.ActionRedirect{ + URL: "http://nginx.com", + Code: 301, + }, + }, + }, + }, + }, }, }, }, @@ -1196,117 +1059,107 @@ func TestGenerateVirtualServerConfigWithSpiffeCerts(t *testing.T) { "default/tea-svc:80": { "10.0.0.20:80", }, + "default/tea-svc_version=v1:80": { + "10.0.0.30:80", + }, + "default/coffee-svc:80": { + "10.0.0.40:80", + }, + "default/sub-tea-svc_version=v1:80": { + "10.0.0.50:80", + }, }, - } - - baseCfgParams := ConfigParams{ - ServerTokens: "off", - Keepalive: 16, - ServerSnippets: []string{"# server snippet"}, - ProxyProtocol: true, - SetRealIPFrom: []string{"0.0.0.0/0"}, - RealIPHeader: "X-Real-IP", - RealIPRecursive: true, - } - - expected := version2.VirtualServerConfig{ - Upstreams: []version2.Upstream{ + VirtualServerRoutes: []*conf_v1.VirtualServerRoute{ { - UpstreamLabels: version2.UpstreamLabels{ - Service: "tea-svc", - ResourceType: "virtualserver", - ResourceName: "cafe", - ResourceNamespace: "default", + ObjectMeta: meta_v1.ObjectMeta{ + Name: "coffee", + Namespace: "default", }, - Name: "vs_default_cafe_tea", - Servers: []version2.UpstreamServer{ - { - Address: "10.0.0.20:80", + Spec: conf_v1.VirtualServerRouteSpec{ + Host: "cafe.example.com", + Upstreams: []conf_v1.Upstream{ + { + Name: "coffee", + Service: "coffee-svc", + Port: 80, + }, + }, + Subroutes: []conf_v1.Route{ + { + Path: "/coffee", + Action: &conf_v1.Action{ + Pass: "coffee", + }, + }, }, }, - Keepalive: 16, }, - }, - HTTPSnippets: []string{}, - LimitReqZones: []version2.LimitReqZone{}, - Server: version2.Server{ - ServerName: "cafe.example.com", - StatusZone: "cafe.example.com", - VSNamespace: "default", - VSName: "cafe", - ProxyProtocol: true, - ServerTokens: "off", - SetRealIPFrom: []string{"0.0.0.0/0"}, - RealIPHeader: "X-Real-IP", - RealIPRecursive: true, - Snippets: []string{"# server snippet"}, - TLSPassthrough: true, - Locations: []version2.Location{ - { - Path: "/tea", - ProxyPass: "https://vs_default_cafe_tea", - ProxyNextUpstream: "error timeout", - ProxyNextUpstreamTimeout: "0s", - ProxyNextUpstreamTries: 0, - HasKeepalive: true, - ProxySSLName: "tea-svc.default.svc", - ProxyPassRequestHeaders: true, - ProxySetHeaders: []version2.Header{{Name: "Host", Value: "$host"}}, - ServiceName: "tea-svc", + { + ObjectMeta: meta_v1.ObjectMeta{ + Name: "subtea", + Namespace: "default", }, - }, - }, - SpiffeClientCerts: true, - } - - isPlus := false - isResolverConfigured := false - staticConfigParams := &StaticConfigParams{TLSPassthrough: true, NginxServiceMesh: true} - isWildcardEnabled := false - vsc := newVirtualServerConfigurator(&baseCfgParams, isPlus, isResolverConfigured, staticConfigParams, isWildcardEnabled) - - result, warnings := vsc.GenerateVirtualServerConfig(&virtualServerEx, nil, nil) - if diff := cmp.Diff(expected, result); diff != "" { - t.Errorf("GenerateVirtualServerConfig() mismatch (-want +got):\n%s", diff) - } - - if len(warnings) != 0 { - t.Errorf("GenerateVirtualServerConfig returned warnings: %v", vsc.warnings) - } -} - -func TestGenerateVirtualServerConfigWithInternalRoutes(t *testing.T) { - t.Parallel() - virtualServerEx := VirtualServerEx{ - VirtualServer: &conf_v1.VirtualServer{ - ObjectMeta: meta_v1.ObjectMeta{ - Name: "cafe", - Namespace: "default", - }, - Spec: conf_v1.VirtualServerSpec{ - Host: "cafe.example.com", - Upstreams: []conf_v1.Upstream{ - { - Name: "tea", - Service: "tea-svc", - Port: 80, - TLS: conf_v1.UpstreamTLS{Enable: false}, + Spec: conf_v1.VirtualServerRouteSpec{ + Host: "cafe.example.com", + Upstreams: []conf_v1.Upstream{ + { + Name: "subtea", + Service: "sub-tea-svc", + Port: 80, + Subselector: map[string]string{"version": "v1"}, + }, }, - }, - Routes: []conf_v1.Route{ - { - Path: "/", - Action: &conf_v1.Action{ - Pass: "tea", + Subroutes: []conf_v1.Route{ + { + Path: "/subtea", + Action: &conf_v1.Action{ + Pass: "subtea", + }, }, }, }, - InternalRoute: true, }, - }, - Endpoints: map[string][]string{ - "default/tea-svc:80": { - "10.0.0.20:80", + { + ObjectMeta: meta_v1.ObjectMeta{ + Name: "subcoffee", + Namespace: "default", + }, + Spec: conf_v1.VirtualServerRouteSpec{ + Host: "cafe.example.com", + Upstreams: []conf_v1.Upstream{ + { + Name: "coffee", + Service: "coffee-svc", + Port: 80, + }, + }, + Subroutes: []conf_v1.Route{ + { + Path: "/coffee-errorpage-subroute", + Action: &conf_v1.Action{ + Pass: "coffee", + }, + }, + { + Path: "/coffee-errorpage-subroute-defined", + Action: &conf_v1.Action{ + Pass: "coffee", + }, + ErrorPages: []conf_v1.ErrorPage{ + { + Codes: []int{502, 503}, + Return: &conf_v1.ErrorPageReturn{ + ActionReturn: conf_v1.ActionReturn{ + Code: 200, + Type: "text/plain", + Body: "All Good", + }, + }, + }, + }, + }, + }, + }, }, }, } @@ -1338,6 +1191,81 @@ func TestGenerateVirtualServerConfigWithInternalRoutes(t *testing.T) { }, Keepalive: 16, }, + { + UpstreamLabels: version2.UpstreamLabels{ + Service: "tea-svc", + ResourceType: "virtualserver", + ResourceName: "cafe", + ResourceNamespace: "default", + }, + Name: "vs_default_cafe_tea-latest", + Servers: []version2.UpstreamServer{ + { + Address: "10.0.0.30:80", + }, + }, + Keepalive: 16, + }, + { + UpstreamLabels: version2.UpstreamLabels{ + Service: "coffee-svc", + ResourceType: "virtualserver", + ResourceName: "cafe", + ResourceNamespace: "default", + }, + Name: "vs_default_cafe_coffee", + Servers: []version2.UpstreamServer{ + { + Address: "10.0.0.40:80", + }, + }, + Keepalive: 16, + }, + { + UpstreamLabels: version2.UpstreamLabels{ + Service: "coffee-svc", + ResourceType: "virtualserverroute", + ResourceName: "coffee", + ResourceNamespace: "default", + }, + Name: "vs_default_cafe_vsr_default_coffee_coffee", + Servers: []version2.UpstreamServer{ + { + Address: "10.0.0.40:80", + }, + }, + Keepalive: 16, + }, + { + UpstreamLabels: version2.UpstreamLabels{ + Service: "sub-tea-svc", + ResourceType: "virtualserverroute", + ResourceName: "subtea", + ResourceNamespace: "default", + }, + Name: "vs_default_cafe_vsr_default_subtea_subtea", + Servers: []version2.UpstreamServer{ + { + Address: "10.0.0.50:80", + }, + }, + Keepalive: 16, + }, + { + UpstreamLabels: version2.UpstreamLabels{ + Service: "coffee-svc", + ResourceType: "virtualserverroute", + ResourceName: "subcoffee", + ResourceNamespace: "default", + }, + Name: "vs_default_cafe_vsr_default_subcoffee_coffee", + Servers: []version2.UpstreamServer{ + { + Address: "10.0.0.40:80", + }, + }, + Keepalive: 16, + }, }, HTTPSnippets: []string{}, LimitReqZones: []version2.LimitReqZone{}, @@ -1355,7 +1283,7 @@ func TestGenerateVirtualServerConfigWithInternalRoutes(t *testing.T) { TLSPassthrough: true, Locations: []version2.Location{ { - Path: "/", + Path: "/tea", ProxyPass: "http://vs_default_cafe_tea", ProxyNextUpstream: "error timeout", ProxyNextUpstreamTimeout: "0s", @@ -1366,73 +1294,199 @@ func TestGenerateVirtualServerConfigWithInternalRoutes(t *testing.T) { ProxySetHeaders: []version2.Header{{Name: "Host", Value: "$host"}}, ServiceName: "tea-svc", }, - }, - }, - SpiffeCerts: true, - SpiffeClientCerts: false, - } - - isPlus := false - isResolverConfigured := false - staticConfigParams := &StaticConfigParams{TLSPassthrough: true, NginxServiceMesh: true, EnableInternalRoutes: true} - isWildcardEnabled := false - vsc := newVirtualServerConfigurator(&baseCfgParams, isPlus, isResolverConfigured, staticConfigParams, isWildcardEnabled) - - result, warnings := vsc.GenerateVirtualServerConfig(&virtualServerEx, nil, nil) - if diff := cmp.Diff(expected, result); diff != "" { - t.Errorf("GenerateVirtualServerConfig() mismatch (-want +got):\n%s", diff) - } - - if len(warnings) != 0 { - t.Errorf("GenerateVirtualServerConfig returned warnings: %v", vsc.warnings) - } -} - -func TestGenerateVirtualServerConfigWithInternalRoutesWarning(t *testing.T) { - t.Parallel() - virtualServerEx := VirtualServerEx{ - VirtualServer: &conf_v1.VirtualServer{ - ObjectMeta: meta_v1.ObjectMeta{ - Name: "cafe", - Namespace: "default", - }, - Spec: conf_v1.VirtualServerSpec{ - Host: "cafe.example.com", - Upstreams: []conf_v1.Upstream{ - { - Name: "tea", - Service: "tea-svc", - Port: 80, - TLS: conf_v1.UpstreamTLS{Enable: false}, - }, + { + Path: "/tea-latest", + ProxyPass: "http://vs_default_cafe_tea-latest", + ProxyNextUpstream: "error timeout", + ProxyNextUpstreamTimeout: "0s", + ProxyNextUpstreamTries: 0, + HasKeepalive: true, + ProxySSLName: "tea-svc.default.svc", + ProxyPassRequestHeaders: true, + ProxySetHeaders: []version2.Header{{Name: "Host", Value: "$host"}}, + ServiceName: "tea-svc", }, - Routes: []conf_v1.Route{ + // Order changes here because we generate first all the VS Routes and then all the VSR Subroutes (separated for loops) + { + Path: "/coffee-errorpage", + ProxyPass: "http://vs_default_cafe_coffee", + ProxyNextUpstream: "error timeout", + ProxyNextUpstreamTimeout: "0s", + ProxyNextUpstreamTries: 0, + HasKeepalive: true, + ProxyInterceptErrors: true, + ErrorPages: []version2.ErrorPage{ + { + Name: "http://nginx.com", + Codes: "401 403", + ResponseCode: 301, + }, + }, + ProxySSLName: "coffee-svc.default.svc", + ProxyPassRequestHeaders: true, + ProxySetHeaders: []version2.Header{{Name: "Host", Value: "$host"}}, + ServiceName: "coffee-svc", + }, + { + Path: "/coffee", + ProxyPass: "http://vs_default_cafe_vsr_default_coffee_coffee", + ProxyNextUpstream: "error timeout", + ProxyNextUpstreamTimeout: "0s", + ProxyNextUpstreamTries: 0, + HasKeepalive: true, + ProxySSLName: "coffee-svc.default.svc", + ProxyPassRequestHeaders: true, + ProxySetHeaders: []version2.Header{{Name: "Host", Value: "$host"}}, + ServiceName: "coffee-svc", + IsVSR: true, + VSRName: "coffee", + VSRNamespace: "default", + }, + { + Path: "/subtea", + ProxyPass: "http://vs_default_cafe_vsr_default_subtea_subtea", + ProxyNextUpstream: "error timeout", + ProxyNextUpstreamTimeout: "0s", + ProxyNextUpstreamTries: 0, + HasKeepalive: true, + ProxySSLName: "sub-tea-svc.default.svc", + ProxyPassRequestHeaders: true, + ProxySetHeaders: []version2.Header{{Name: "Host", Value: "$host"}}, + ServiceName: "sub-tea-svc", + IsVSR: true, + VSRName: "subtea", + VSRNamespace: "default", + }, + + { + Path: "/coffee-errorpage-subroute", + ProxyPass: "http://vs_default_cafe_vsr_default_subcoffee_coffee", + ProxyNextUpstream: "error timeout", + ProxyNextUpstreamTimeout: "0s", + ProxyNextUpstreamTries: 0, + HasKeepalive: true, + ProxyInterceptErrors: true, + ErrorPages: []version2.ErrorPage{ + { + Name: "http://nginx.com", + Codes: "401 403", + ResponseCode: 301, + }, + }, + ProxySSLName: "coffee-svc.default.svc", + ProxyPassRequestHeaders: true, + ProxySetHeaders: []version2.Header{{Name: "Host", Value: "$host"}}, + ServiceName: "coffee-svc", + IsVSR: true, + VSRName: "subcoffee", + VSRNamespace: "default", + }, + { + Path: "/coffee-errorpage-subroute-defined", + ProxyPass: "http://vs_default_cafe_vsr_default_subcoffee_coffee", + ProxyNextUpstream: "error timeout", + ProxyNextUpstreamTimeout: "0s", + ProxyNextUpstreamTries: 0, + HasKeepalive: true, + ProxyInterceptErrors: true, + ErrorPages: []version2.ErrorPage{ + { + Name: "@error_page_0_0", + Codes: "502 503", + ResponseCode: 200, + }, + }, + ProxySSLName: "coffee-svc.default.svc", + ProxyPassRequestHeaders: true, + ProxySetHeaders: []version2.Header{{Name: "Host", Value: "$host"}}, + ServiceName: "coffee-svc", + IsVSR: true, + VSRName: "subcoffee", + VSRNamespace: "default", + }, + }, + ErrorPageLocations: []version2.ErrorPageLocation{ + { + Name: "@error_page_0_0", + DefaultType: "text/plain", + Return: &version2.Return{ + Text: "All Good", + }, + }, + }, + }, + } + + isPlus := false + isResolverConfigured := false + isWildcardEnabled := false + vsc := newVirtualServerConfigurator( + &baseCfgParams, + isPlus, + isResolverConfigured, + &StaticConfigParams{TLSPassthrough: true}, + isWildcardEnabled, + ) + + result, warnings := vsc.GenerateVirtualServerConfig(&virtualServerEx, nil, nil) + if diff := cmp.Diff(expected, result); diff != "" { + t.Errorf("GenerateVirtualServerConfig() mismatch (-want +got):\n%s", diff) + } + + if len(warnings) != 0 { + t.Errorf("GenerateVirtualServerConfig returned warnings: %v", vsc.warnings) + } +} + +func TestGenerateVirtualServerConfigIPV6Disabled(t *testing.T) { + t.Parallel() + virtualServerEx := VirtualServerEx{ + VirtualServer: &conf_v1.VirtualServer{ + ObjectMeta: meta_v1.ObjectMeta{ + Name: "cafe", + Namespace: "default", + }, + Spec: conf_v1.VirtualServerSpec{ + Host: "cafe.example.com", + Upstreams: []conf_v1.Upstream{ { - Path: "/", + Name: "tea", + Service: "tea-svc", + Port: 80, + }, + { + Name: "coffee", + Service: "coffee-svc", + Port: 80, + }, + }, + Routes: []conf_v1.Route{ + { + Path: "/tea", Action: &conf_v1.Action{ Pass: "tea", }, }, + { + Path: "/coffee", + Action: &conf_v1.Action{ + Pass: "coffee", + }, + }, }, - InternalRoute: true, }, }, Endpoints: map[string][]string{ "default/tea-svc:80": { "10.0.0.20:80", }, + "default/coffee-svc:80": { + "10.0.0.40:80", + }, }, } - baseCfgParams := ConfigParams{ - ServerTokens: "off", - Keepalive: 16, - ServerSnippets: []string{"# server snippet"}, - ProxyProtocol: true, - SetRealIPFrom: []string{"0.0.0.0/0"}, - RealIPHeader: "X-Real-IP", - RealIPRecursive: true, - } + baseCfgParams := ConfigParams{} expected := version2.VirtualServerConfig{ Upstreams: []version2.Upstream{ @@ -1449,59 +1503,77 @@ func TestGenerateVirtualServerConfigWithInternalRoutesWarning(t *testing.T) { Address: "10.0.0.20:80", }, }, - Keepalive: 16, + }, + { + UpstreamLabels: version2.UpstreamLabels{ + Service: "coffee-svc", + ResourceType: "virtualserver", + ResourceName: "cafe", + ResourceNamespace: "default", + }, + Name: "vs_default_cafe_coffee", + Servers: []version2.UpstreamServer{ + { + Address: "10.0.0.40:80", + }, + }, }, }, HTTPSnippets: []string{}, LimitReqZones: []version2.LimitReqZone{}, Server: version2.Server{ - ServerName: "cafe.example.com", - StatusZone: "cafe.example.com", - VSNamespace: "default", - VSName: "cafe", - ProxyProtocol: true, - ServerTokens: "off", - SetRealIPFrom: []string{"0.0.0.0/0"}, - RealIPHeader: "X-Real-IP", - RealIPRecursive: true, - Snippets: []string{"# server snippet"}, - TLSPassthrough: true, + ServerName: "cafe.example.com", + StatusZone: "cafe.example.com", + VSNamespace: "default", + VSName: "cafe", + DisableIPV6: true, Locations: []version2.Location{ { - Path: "/", + Path: "/tea", ProxyPass: "http://vs_default_cafe_tea", ProxyNextUpstream: "error timeout", ProxyNextUpstreamTimeout: "0s", - ProxyNextUpstreamTries: 0, - HasKeepalive: true, ProxySSLName: "tea-svc.default.svc", ProxyPassRequestHeaders: true, ProxySetHeaders: []version2.Header{{Name: "Host", Value: "$host"}}, ServiceName: "tea-svc", }, + { + Path: "/coffee", + ProxyPass: "http://vs_default_cafe_coffee", + ProxyNextUpstream: "error timeout", + ProxyNextUpstreamTimeout: "0s", + ProxySSLName: "coffee-svc.default.svc", + ProxyPassRequestHeaders: true, + ProxySetHeaders: []version2.Header{{Name: "Host", Value: "$host"}}, + ServiceName: "coffee-svc", + }, }, }, - SpiffeCerts: true, - SpiffeClientCerts: true, } isPlus := false isResolverConfigured := false - staticConfigParams := &StaticConfigParams{TLSPassthrough: true, NginxServiceMesh: true, EnableInternalRoutes: false} isWildcardEnabled := false - vsc := newVirtualServerConfigurator(&baseCfgParams, isPlus, isResolverConfigured, staticConfigParams, isWildcardEnabled) + vsc := newVirtualServerConfigurator( + &baseCfgParams, + isPlus, + isResolverConfigured, + &StaticConfigParams{DisableIPV6: true}, + isWildcardEnabled, + ) result, warnings := vsc.GenerateVirtualServerConfig(&virtualServerEx, nil, nil) - if diff := cmp.Diff(expected, result); diff == "" { - t.Errorf("GenerateVirtualServerConfig() should not configure internal route") + if diff := cmp.Diff(expected, result); diff != "" { + t.Errorf("GenerateVirtualServerConfig() mismatch (-want +got):\n%s", diff) } - if len(warnings) != 1 { - t.Errorf("GenerateVirtualServerConfig should return warning to enable internal routing") + if len(warnings) != 0 { + t.Errorf("GenerateVirtualServerConfig returned warnings: %v", vsc.warnings) } } -func TestGenerateVirtualServerConfigForVirtualServerWithSplits(t *testing.T) { +func TestGenerateVirtualServerConfigGrpcErrorPageWarning(t *testing.T) { t.Parallel() virtualServerEx := VirtualServerEx{ VirtualServer: &conf_v1.VirtualServer{ @@ -1511,91 +1583,108 @@ func TestGenerateVirtualServerConfigForVirtualServerWithSplits(t *testing.T) { }, Spec: conf_v1.VirtualServerSpec{ Host: "cafe.example.com", + TLS: &conf_v1.TLS{ + Secret: "", + }, Upstreams: []conf_v1.Upstream{ { - Name: "tea-v1", - Service: "tea-svc-v1", - Port: 80, + Name: "grpc-app-1", + Service: "grpc-svc", + Port: 50051, + Type: "grpc", + TLS: conf_v1.UpstreamTLS{ + Enable: true, + }, }, { - Name: "tea-v2", - Service: "tea-svc-v2", + Name: "grpc-app-2", + Service: "grpc-svc2", + Port: 50052, + Type: "grpc", + TLS: conf_v1.UpstreamTLS{ + Enable: true, + }, + }, + { + Name: "tea", + Service: "tea-svc", Port: 80, }, }, Routes: []conf_v1.Route{ { - Path: "/tea", - Splits: []conf_v1.Split{ + Path: "/grpc-errorpage", + Action: &conf_v1.Action{ + Pass: "grpc-app-1", + }, + ErrorPages: []conf_v1.ErrorPage{ { - Weight: 90, - Action: &conf_v1.Action{ - Pass: "tea-v1", + Codes: []int{404, 405}, + Return: &conf_v1.ErrorPageReturn{ + ActionReturn: conf_v1.ActionReturn{ + Code: 200, + Type: "text/plain", + Body: "All Good", + }, }, }, + }, + }, + { + Path: "/grpc-matches", + Matches: []conf_v1.Match{ { - Weight: 10, + Conditions: []conf_v1.Condition{ + { + Variable: "$request_method", + Value: "POST", + }, + }, Action: &conf_v1.Action{ - Pass: "tea-v2", + Pass: "grpc-app-2", }, }, }, - }, - { - Path: "/coffee", - Route: "default/coffee", - }, - }, - }, - }, - Endpoints: map[string][]string{ - "default/tea-svc-v1:80": { - "10.0.0.20:80", - }, - "default/tea-svc-v2:80": { - "10.0.0.21:80", - }, - "default/coffee-svc-v1:80": { - "10.0.0.30:80", - }, - "default/coffee-svc-v2:80": { - "10.0.0.31:80", - }, - }, - VirtualServerRoutes: []*conf_v1.VirtualServerRoute{ - { - ObjectMeta: meta_v1.ObjectMeta{ - Name: "coffee", - Namespace: "default", - }, - Spec: conf_v1.VirtualServerRouteSpec{ - Host: "cafe.example.com", - Upstreams: []conf_v1.Upstream{ - { - Name: "coffee-v1", - Service: "coffee-svc-v1", - Port: 80, + Action: &conf_v1.Action{ + Pass: "tea", }, - { - Name: "coffee-v2", - Service: "coffee-svc-v2", - Port: 80, + ErrorPages: []conf_v1.ErrorPage{ + { + Codes: []int{404}, + Return: &conf_v1.ErrorPageReturn{ + ActionReturn: conf_v1.ActionReturn{ + Code: 200, + Type: "text/plain", + Body: "Original resource not found, but success!", + }, + }, + }, }, }, - Subroutes: []conf_v1.Route{ - { - Path: "/coffee", - Splits: []conf_v1.Split{ - { - Weight: 40, - Action: &conf_v1.Action{ - Pass: "coffee-v1", - }, + { + Path: "/grpc-splits", + Splits: []conf_v1.Split{ + { + Weight: 90, + Action: &conf_v1.Action{ + Pass: "grpc-app-1", }, - { - Weight: 60, - Action: &conf_v1.Action{ - Pass: "coffee-v2", + }, + { + Weight: 10, + Action: &conf_v1.Action{ + Pass: "grpc-app-2", + }, + }, + }, + ErrorPages: []conf_v1.ErrorPage{ + { + Codes: []int{404, 405}, + Return: &conf_v1.ErrorPageReturn{ + ActionReturn: conf_v1.ActionReturn{ + Code: 200, + Type: "text/plain", + Body: "All Good", }, }, }, @@ -1604,20 +1693,27 @@ func TestGenerateVirtualServerConfigForVirtualServerWithSplits(t *testing.T) { }, }, }, + Endpoints: map[string][]string{ + "default/grpc-svc:50051": { + "10.0.0.20:80", + }, + }, } - baseCfgParams := ConfigParams{} + baseCfgParams := ConfigParams{ + HTTP2: true, + } expected := version2.VirtualServerConfig{ Upstreams: []version2.Upstream{ { - Name: "vs_default_cafe_tea-v1", UpstreamLabels: version2.UpstreamLabels{ - Service: "tea-svc-v1", + Service: "grpc-svc", ResourceType: "virtualserver", ResourceName: "cafe", ResourceNamespace: "default", }, + Name: "vs_default_cafe_grpc-app-1", Servers: []version2.UpstreamServer{ { Address: "10.0.0.20:80", @@ -1625,170 +1721,225 @@ func TestGenerateVirtualServerConfigForVirtualServerWithSplits(t *testing.T) { }, }, { - Name: "vs_default_cafe_tea-v2", + Name: "vs_default_cafe_grpc-app-2", UpstreamLabels: version2.UpstreamLabels{ - Service: "tea-svc-v2", + Service: "grpc-svc2", ResourceType: "virtualserver", ResourceName: "cafe", ResourceNamespace: "default", }, Servers: []version2.UpstreamServer{ { - Address: "10.0.0.21:80", - }, - }, - }, - { - Name: "vs_default_cafe_vsr_default_coffee_coffee-v1", - UpstreamLabels: version2.UpstreamLabels{ - Service: "coffee-svc-v1", - ResourceType: "virtualserverroute", - ResourceName: "coffee", - ResourceNamespace: "default", - }, - Servers: []version2.UpstreamServer{ - { - Address: "10.0.0.30:80", + Address: "unix:/var/lib/nginx/nginx-502-server.sock", }, }, }, { - Name: "vs_default_cafe_vsr_default_coffee_coffee-v2", + Name: "vs_default_cafe_tea", UpstreamLabels: version2.UpstreamLabels{ - Service: "coffee-svc-v2", - ResourceType: "virtualserverroute", - ResourceName: "coffee", + Service: "tea-svc", + ResourceType: "virtualserver", + ResourceName: "cafe", ResourceNamespace: "default", }, Servers: []version2.UpstreamServer{ { - Address: "10.0.0.31:80", + Address: "unix:/var/lib/nginx/nginx-502-server.sock", }, }, }, }, - SplitClients: []version2.SplitClient{ + HTTPSnippets: []string{}, + LimitReqZones: []version2.LimitReqZone{}, + Maps: []version2.Map{ { - Source: "$request_id", - Variable: "$vs_default_cafe_splits_0", - Distributions: []version2.Distribution{ + Source: "$request_method", + Variable: "$vs_default_cafe_matches_0_match_0_cond_0", + Parameters: []version2.Parameter{ { - Weight: "90%", - Value: "/internal_location_splits_0_split_0", + Value: `"POST"`, + Result: "1", }, { - Weight: "10%", - Value: "/internal_location_splits_0_split_1", + Value: "default", + Result: "0", }, }, }, { - Source: "$request_id", - Variable: "$vs_default_cafe_splits_1", - Distributions: []version2.Distribution{ + Source: "$vs_default_cafe_matches_0_match_0_cond_0", + Variable: "$vs_default_cafe_matches_0", + Parameters: []version2.Parameter{ { - Weight: "40%", - Value: "/internal_location_splits_1_split_0", + Value: "~^1", + Result: "/internal_location_matches_0_match_0", }, { - Weight: "60%", - Value: "/internal_location_splits_1_split_1", + Value: "default", + Result: "/internal_location_matches_0_default", }, }, }, }, - HTTPSnippets: []string{}, - LimitReqZones: []version2.LimitReqZone{}, Server: version2.Server{ ServerName: "cafe.example.com", StatusZone: "cafe.example.com", VSNamespace: "default", VSName: "cafe", + SSL: &version2.SSL{ + HTTP2: true, + Certificate: "/etc/nginx/secrets/wildcard", + CertificateKey: "/etc/nginx/secrets/wildcard", + }, InternalRedirectLocations: []version2.InternalRedirectLocation{ { - Path: "/tea", - Destination: "$vs_default_cafe_splits_0", + Path: "/grpc-matches", + Destination: "$vs_default_cafe_matches_0", }, { - Path: "/coffee", - Destination: "$vs_default_cafe_splits_1", + Path: "/grpc-splits", + Destination: "$vs_default_cafe_splits_0", }, }, Locations: []version2.Location{ { - Path: "/internal_location_splits_0_split_0", - ProxyPass: "http://vs_default_cafe_tea-v1$request_uri", + Path: "/grpc-errorpage", + ProxyPass: "https://vs_default_cafe_grpc-app-1", ProxyNextUpstream: "error timeout", ProxyNextUpstreamTimeout: "0s", ProxyNextUpstreamTries: 0, - Internal: true, - ProxySSLName: "tea-svc-v1.default.svc", + ErrorPages: []version2.ErrorPage{{Name: "@error_page_0_0", Codes: "404 405", ResponseCode: 200}}, + ProxyInterceptErrors: true, + ProxySSLName: "grpc-svc.default.svc", ProxyPassRequestHeaders: true, ProxySetHeaders: []version2.Header{{Name: "Host", Value: "$host"}}, - ServiceName: "tea-svc-v1", + ServiceName: "grpc-svc", + GRPCPass: "grpcs://vs_default_cafe_grpc-app-1", }, { - Path: "/internal_location_splits_0_split_1", - ProxyPass: "http://vs_default_cafe_tea-v2$request_uri", + Path: "/internal_location_matches_0_match_0", + Internal: true, + ProxyPass: "https://vs_default_cafe_grpc-app-2$request_uri", ProxyNextUpstream: "error timeout", ProxyNextUpstreamTimeout: "0s", ProxyNextUpstreamTries: 0, - Internal: true, - ProxySSLName: "tea-svc-v2.default.svc", + Rewrites: []string{"^ $request_uri break"}, + ErrorPages: []version2.ErrorPage{{Name: "@error_page_1_0", Codes: "404", ResponseCode: 200}}, + ProxyInterceptErrors: true, + ProxySSLName: "grpc-svc2.default.svc", ProxyPassRequestHeaders: true, ProxySetHeaders: []version2.Header{{Name: "Host", Value: "$host"}}, - ServiceName: "tea-svc-v2", + ServiceName: "grpc-svc2", + GRPCPass: "grpcs://vs_default_cafe_grpc-app-2", }, { - Path: "/internal_location_splits_1_split_0", - ProxyPass: "http://vs_default_cafe_vsr_default_coffee_coffee-v1$request_uri", + Path: "/internal_location_matches_0_default", + Internal: true, + ProxyPass: "http://vs_default_cafe_tea$request_uri", ProxyNextUpstream: "error timeout", ProxyNextUpstreamTimeout: "0s", ProxyNextUpstreamTries: 0, - Internal: true, - ProxySSLName: "coffee-svc-v1.default.svc", - ProxyPassRequestHeaders: true, + ErrorPages: []version2.ErrorPage{{Name: "@error_page_1_0", Codes: "404", ResponseCode: 200}}, + ProxyInterceptErrors: true, + ProxySSLName: "tea-svc.default.svc", + ProxyPassRequestHeaders: true, ProxySetHeaders: []version2.Header{{Name: "Host", Value: "$host"}}, - ServiceName: "coffee-svc-v1", - IsVSR: true, - VSRName: "coffee", - VSRNamespace: "default", + ServiceName: "tea-svc", }, { - Path: "/internal_location_splits_1_split_1", - ProxyPass: "http://vs_default_cafe_vsr_default_coffee_coffee-v2$request_uri", + Path: "/internal_location_splits_0_split_0", + Internal: true, + ProxyPass: "https://vs_default_cafe_grpc-app-1$request_uri", ProxyNextUpstream: "error timeout", ProxyNextUpstreamTimeout: "0s", ProxyNextUpstreamTries: 0, + HasKeepalive: false, + ErrorPages: []version2.ErrorPage{{Name: "@error_page_2_0", Codes: "404 405", ResponseCode: 200}}, + ProxyInterceptErrors: true, + Rewrites: []string{"^ $request_uri break"}, + ProxySSLName: "grpc-svc.default.svc", + ProxyPassRequestHeaders: true, + ProxySetHeaders: []version2.Header{{Name: "Host", Value: "$host"}}, + ServiceName: "grpc-svc", + GRPCPass: "grpcs://vs_default_cafe_grpc-app-1", + }, + { + Path: "/internal_location_splits_0_split_1", Internal: true, - ProxySSLName: "coffee-svc-v2.default.svc", + ProxyPass: "https://vs_default_cafe_grpc-app-2$request_uri", + ProxyNextUpstream: "error timeout", + ProxyNextUpstreamTimeout: "0s", + ProxyNextUpstreamTries: 0, + HasKeepalive: false, + ErrorPages: []version2.ErrorPage{{Name: "@error_page_2_0", Codes: "404 405", ResponseCode: 200}}, + ProxyInterceptErrors: true, + Rewrites: []string{"^ $request_uri break"}, + ProxySSLName: "grpc-svc2.default.svc", ProxyPassRequestHeaders: true, ProxySetHeaders: []version2.Header{{Name: "Host", Value: "$host"}}, - ServiceName: "coffee-svc-v2", - IsVSR: true, - VSRName: "coffee", - VSRNamespace: "default", + ServiceName: "grpc-svc2", + GRPCPass: "grpcs://vs_default_cafe_grpc-app-2", + }, + }, + ErrorPageLocations: []version2.ErrorPageLocation{ + { + Name: "@error_page_0_0", + DefaultType: "text/plain", + Return: &version2.Return{Text: "All Good"}, + }, + { + Name: "@error_page_1_0", + DefaultType: "text/plain", + Return: &version2.Return{Text: "Original resource not found, but success!"}, + }, + { + Name: "@error_page_2_0", + DefaultType: "text/plain", + Return: &version2.Return{Text: "All Good"}, + }, + }, + }, + SplitClients: []version2.SplitClient{ + { + Source: "$request_id", + Variable: "$vs_default_cafe_splits_0", + Distributions: []version2.Distribution{ + { + Weight: "90%", + Value: "/internal_location_splits_0_split_0", + }, + { + Weight: "10%", + Value: "/internal_location_splits_0_split_1", + }, }, }, }, } - + expectedWarnings := Warnings{ + virtualServerEx.VirtualServer: { + `The error page configuration for the upstream grpc-app-1 is ignored for status code(s) [404 405], which cannot be used for GRPC upstreams.`, + `The error page configuration for the upstream grpc-app-2 is ignored for status code(s) [404], which cannot be used for GRPC upstreams.`, + `The error page configuration for the upstream grpc-app-1 is ignored for status code(s) [404 405], which cannot be used for GRPC upstreams.`, + `The error page configuration for the upstream grpc-app-2 is ignored for status code(s) [404 405], which cannot be used for GRPC upstreams.`, + }, + } isPlus := false isResolverConfigured := false - isWildcardEnabled := false + isWildcardEnabled := true vsc := newVirtualServerConfigurator(&baseCfgParams, isPlus, isResolverConfigured, &StaticConfigParams{}, isWildcardEnabled) result, warnings := vsc.GenerateVirtualServerConfig(&virtualServerEx, nil, nil) if diff := cmp.Diff(expected, result); diff != "" { - t.Errorf("GenerateVirtualServerConfig() mismatch (-want +got):\n%s", diff) + t.Errorf("TestGenerateVirtualServerConfigGrpcErrorPageWarning() mismatch (-want +got):\n%s", diff) } - if len(warnings) != 0 { - t.Errorf("GenerateVirtualServerConfig returned warnings: %v", vsc.warnings) + if !reflect.DeepEqual(vsc.warnings, expectedWarnings) { + t.Errorf("GenerateVirtualServerConfig() returned warnings of \n%v but expected \n%v", warnings, expectedWarnings) } } -func TestGenerateVirtualServerConfigForVirtualServerWithMatches(t *testing.T) { +func TestGenerateVirtualServerConfigWithSpiffeCerts(t *testing.T) { t.Parallel() virtualServerEx := VirtualServerEx{ VirtualServer: &conf_v1.VirtualServer{ @@ -1800,302 +1951,206 @@ func TestGenerateVirtualServerConfigForVirtualServerWithMatches(t *testing.T) { Host: "cafe.example.com", Upstreams: []conf_v1.Upstream{ { - Name: "tea-v1", - Service: "tea-svc-v1", - Port: 80, - }, - { - Name: "tea-v2", - Service: "tea-svc-v2", + Name: "tea", + Service: "tea-svc", Port: 80, }, }, Routes: []conf_v1.Route{ { Path: "/tea", - Matches: []conf_v1.Match{ - { - Conditions: []conf_v1.Condition{ - { - Header: "x-version", - Value: "v2", - }, - }, - Action: &conf_v1.Action{ - Pass: "tea-v2", - }, - }, - }, Action: &conf_v1.Action{ - Pass: "tea-v1", + Pass: "tea", }, }, - { - Path: "/coffee", - Route: "default/coffee", - }, }, }, }, Endpoints: map[string][]string{ - "default/tea-svc-v1:80": { + "default/tea-svc:80": { "10.0.0.20:80", }, - "default/tea-svc-v2:80": { - "10.0.0.21:80", - }, - "default/coffee-svc-v1:80": { - "10.0.0.30:80", - }, - "default/coffee-svc-v2:80": { - "10.0.0.31:80", - }, - }, - VirtualServerRoutes: []*conf_v1.VirtualServerRoute{ - { - ObjectMeta: meta_v1.ObjectMeta{ - Name: "coffee", - Namespace: "default", - }, - Spec: conf_v1.VirtualServerRouteSpec{ - Host: "cafe.example.com", - Upstreams: []conf_v1.Upstream{ - { - Name: "coffee-v1", - Service: "coffee-svc-v1", - Port: 80, - }, - { - Name: "coffee-v2", - Service: "coffee-svc-v2", - Port: 80, - }, - }, - Subroutes: []conf_v1.Route{ - { - Path: "/coffee", - Matches: []conf_v1.Match{ - { - Conditions: []conf_v1.Condition{ - { - Argument: "version", - Value: "v2", - }, - }, - Action: &conf_v1.Action{ - Pass: "coffee-v2", - }, - }, - }, - Action: &conf_v1.Action{ - Pass: "coffee-v1", - }, - }, - }, - }, - }, }, } - baseCfgParams := ConfigParams{} + baseCfgParams := ConfigParams{ + ServerTokens: "off", + Keepalive: 16, + ServerSnippets: []string{"# server snippet"}, + ProxyProtocol: true, + SetRealIPFrom: []string{"0.0.0.0/0"}, + RealIPHeader: "X-Real-IP", + RealIPRecursive: true, + } expected := version2.VirtualServerConfig{ Upstreams: []version2.Upstream{ { UpstreamLabels: version2.UpstreamLabels{ - Service: "tea-svc-v1", + Service: "tea-svc", ResourceType: "virtualserver", ResourceName: "cafe", ResourceNamespace: "default", }, - Name: "vs_default_cafe_tea-v1", + Name: "vs_default_cafe_tea", Servers: []version2.UpstreamServer{ { Address: "10.0.0.20:80", }, }, + Keepalive: 16, }, - { - Name: "vs_default_cafe_tea-v2", - UpstreamLabels: version2.UpstreamLabels{ - Service: "tea-svc-v2", - ResourceType: "virtualserver", - ResourceName: "cafe", - ResourceNamespace: "default", - }, - Servers: []version2.UpstreamServer{ - { - Address: "10.0.0.21:80", - }, - }, - }, - { - Name: "vs_default_cafe_vsr_default_coffee_coffee-v1", - UpstreamLabels: version2.UpstreamLabels{ - Service: "coffee-svc-v1", - ResourceType: "virtualserverroute", - ResourceName: "coffee", - ResourceNamespace: "default", - }, - Servers: []version2.UpstreamServer{ - { - Address: "10.0.0.30:80", - }, - }, - }, - { - Name: "vs_default_cafe_vsr_default_coffee_coffee-v2", - UpstreamLabels: version2.UpstreamLabels{ - Service: "coffee-svc-v2", - ResourceType: "virtualserverroute", - ResourceName: "coffee", - ResourceNamespace: "default", - }, - Servers: []version2.UpstreamServer{ - { - Address: "10.0.0.31:80", - }, + }, + HTTPSnippets: []string{}, + LimitReqZones: []version2.LimitReqZone{}, + Server: version2.Server{ + ServerName: "cafe.example.com", + StatusZone: "cafe.example.com", + VSNamespace: "default", + VSName: "cafe", + ProxyProtocol: true, + ServerTokens: "off", + SetRealIPFrom: []string{"0.0.0.0/0"}, + RealIPHeader: "X-Real-IP", + RealIPRecursive: true, + Snippets: []string{"# server snippet"}, + TLSPassthrough: true, + Locations: []version2.Location{ + { + Path: "/tea", + ProxyPass: "https://vs_default_cafe_tea", + ProxyNextUpstream: "error timeout", + ProxyNextUpstreamTimeout: "0s", + ProxyNextUpstreamTries: 0, + HasKeepalive: true, + ProxySSLName: "tea-svc.default.svc", + ProxyPassRequestHeaders: true, + ProxySetHeaders: []version2.Header{{Name: "Host", Value: "$host"}}, + ServiceName: "tea-svc", }, }, }, - Maps: []version2.Map{ - { - Source: "$http_x_version", - Variable: "$vs_default_cafe_matches_0_match_0_cond_0", - Parameters: []version2.Parameter{ - { - Value: `"v2"`, - Result: "1", - }, - { - Value: "default", - Result: "0", - }, - }, + SpiffeClientCerts: true, + } + + isPlus := false + isResolverConfigured := false + staticConfigParams := &StaticConfigParams{TLSPassthrough: true, NginxServiceMesh: true} + isWildcardEnabled := false + vsc := newVirtualServerConfigurator(&baseCfgParams, isPlus, isResolverConfigured, staticConfigParams, isWildcardEnabled) + + result, warnings := vsc.GenerateVirtualServerConfig(&virtualServerEx, nil, nil) + if diff := cmp.Diff(expected, result); diff != "" { + t.Errorf("GenerateVirtualServerConfig() mismatch (-want +got):\n%s", diff) + } + + if len(warnings) != 0 { + t.Errorf("GenerateVirtualServerConfig returned warnings: %v", vsc.warnings) + } +} + +func TestGenerateVirtualServerConfigWithInternalRoutes(t *testing.T) { + t.Parallel() + virtualServerEx := VirtualServerEx{ + VirtualServer: &conf_v1.VirtualServer{ + ObjectMeta: meta_v1.ObjectMeta{ + Name: "cafe", + Namespace: "default", }, - { - Source: "$vs_default_cafe_matches_0_match_0_cond_0", - Variable: "$vs_default_cafe_matches_0", - Parameters: []version2.Parameter{ - { - Value: "~^1", - Result: "/internal_location_matches_0_match_0", - }, + Spec: conf_v1.VirtualServerSpec{ + Host: "cafe.example.com", + Upstreams: []conf_v1.Upstream{ { - Value: "default", - Result: "/internal_location_matches_0_default", + Name: "tea", + Service: "tea-svc", + Port: 80, + TLS: conf_v1.UpstreamTLS{Enable: false}, }, }, - }, - { - Source: "$arg_version", - Variable: "$vs_default_cafe_matches_1_match_0_cond_0", - Parameters: []version2.Parameter{ - { - Value: `"v2"`, - Result: "1", - }, + Routes: []conf_v1.Route{ { - Value: "default", - Result: "0", + Path: "/", + Action: &conf_v1.Action{ + Pass: "tea", + }, }, }, + InternalRoute: true, + }, + }, + Endpoints: map[string][]string{ + "default/tea-svc:80": { + "10.0.0.20:80", }, + }, + } + + baseCfgParams := ConfigParams{ + ServerTokens: "off", + Keepalive: 16, + ServerSnippets: []string{"# server snippet"}, + ProxyProtocol: true, + SetRealIPFrom: []string{"0.0.0.0/0"}, + RealIPHeader: "X-Real-IP", + RealIPRecursive: true, + } + + expected := version2.VirtualServerConfig{ + Upstreams: []version2.Upstream{ { - Source: "$vs_default_cafe_matches_1_match_0_cond_0", - Variable: "$vs_default_cafe_matches_1", - Parameters: []version2.Parameter{ - { - Value: "~^1", - Result: "/internal_location_matches_1_match_0", - }, + UpstreamLabels: version2.UpstreamLabels{ + Service: "tea-svc", + ResourceType: "virtualserver", + ResourceName: "cafe", + ResourceNamespace: "default", + }, + Name: "vs_default_cafe_tea", + Servers: []version2.UpstreamServer{ { - Value: "default", - Result: "/internal_location_matches_1_default", + Address: "10.0.0.20:80", }, }, + Keepalive: 16, }, }, HTTPSnippets: []string{}, LimitReqZones: []version2.LimitReqZone{}, Server: version2.Server{ - ServerName: "cafe.example.com", - StatusZone: "cafe.example.com", - VSNamespace: "default", - VSName: "cafe", - InternalRedirectLocations: []version2.InternalRedirectLocation{ - { - Path: "/tea", - Destination: "$vs_default_cafe_matches_0", - }, - { - Path: "/coffee", - Destination: "$vs_default_cafe_matches_1", - }, - }, + ServerName: "cafe.example.com", + StatusZone: "cafe.example.com", + VSNamespace: "default", + VSName: "cafe", + ProxyProtocol: true, + ServerTokens: "off", + SetRealIPFrom: []string{"0.0.0.0/0"}, + RealIPHeader: "X-Real-IP", + RealIPRecursive: true, + Snippets: []string{"# server snippet"}, + TLSPassthrough: true, Locations: []version2.Location{ { - Path: "/internal_location_matches_0_match_0", - ProxyPass: "http://vs_default_cafe_tea-v2$request_uri", - ProxyNextUpstream: "error timeout", - ProxyNextUpstreamTimeout: "0s", - ProxyNextUpstreamTries: 0, - Internal: true, - ProxySSLName: "tea-svc-v2.default.svc", - ProxyPassRequestHeaders: true, - ProxySetHeaders: []version2.Header{{Name: "Host", Value: "$host"}}, - ServiceName: "tea-svc-v2", - }, - { - Path: "/internal_location_matches_0_default", - ProxyPass: "http://vs_default_cafe_tea-v1$request_uri", - ProxyNextUpstream: "error timeout", - ProxyNextUpstreamTimeout: "0s", - ProxyNextUpstreamTries: 0, - Internal: true, - ProxySSLName: "tea-svc-v1.default.svc", - ProxyPassRequestHeaders: true, - ProxySetHeaders: []version2.Header{{Name: "Host", Value: "$host"}}, - ServiceName: "tea-svc-v1", - }, - { - Path: "/internal_location_matches_1_match_0", - ProxyPass: "http://vs_default_cafe_vsr_default_coffee_coffee-v2$request_uri", - ProxyNextUpstream: "error timeout", - ProxyNextUpstreamTimeout: "0s", - ProxyNextUpstreamTries: 0, - Internal: true, - ProxySSLName: "coffee-svc-v2.default.svc", - ProxyPassRequestHeaders: true, - ProxySetHeaders: []version2.Header{{Name: "Host", Value: "$host"}}, - ServiceName: "coffee-svc-v2", - IsVSR: true, - VSRName: "coffee", - VSRNamespace: "default", - }, - { - Path: "/internal_location_matches_1_default", - ProxyPass: "http://vs_default_cafe_vsr_default_coffee_coffee-v1$request_uri", + Path: "/", + ProxyPass: "http://vs_default_cafe_tea", ProxyNextUpstream: "error timeout", ProxyNextUpstreamTimeout: "0s", ProxyNextUpstreamTries: 0, - Internal: true, - ProxySSLName: "coffee-svc-v1.default.svc", + HasKeepalive: true, + ProxySSLName: "tea-svc.default.svc", ProxyPassRequestHeaders: true, ProxySetHeaders: []version2.Header{{Name: "Host", Value: "$host"}}, - ServiceName: "coffee-svc-v1", - IsVSR: true, - VSRName: "coffee", - VSRNamespace: "default", + ServiceName: "tea-svc", }, }, }, + SpiffeCerts: true, + SpiffeClientCerts: false, } isPlus := false isResolverConfigured := false + staticConfigParams := &StaticConfigParams{TLSPassthrough: true, NginxServiceMesh: true, EnableInternalRoutes: true} isWildcardEnabled := false - vsc := newVirtualServerConfigurator(&baseCfgParams, isPlus, isResolverConfigured, &StaticConfigParams{}, isWildcardEnabled) + vsc := newVirtualServerConfigurator(&baseCfgParams, isPlus, isResolverConfigured, staticConfigParams, isWildcardEnabled) result, warnings := vsc.GenerateVirtualServerConfig(&virtualServerEx, nil, nil) if diff := cmp.Diff(expected, result); diff != "" { @@ -2107,44 +2162,8 @@ func TestGenerateVirtualServerConfigForVirtualServerWithMatches(t *testing.T) { } } -func TestGenerateVirtualServerConfigForVirtualServerRoutesWithDos(t *testing.T) { +func TestGenerateVirtualServerConfigWithInternalRoutesWarning(t *testing.T) { t.Parallel() - dosResources := map[string]*appProtectDosResource{ - "/coffee": { - AppProtectDosEnable: "on", - AppProtectDosLogEnable: false, - AppProtectDosMonitorURI: "test.example.com", - AppProtectDosMonitorProtocol: "http", - AppProtectDosMonitorTimeout: 0, - AppProtectDosName: "my-dos-coffee", - AppProtectDosAccessLogDst: "svc.dns.com:123", - AppProtectDosPolicyFile: "", - AppProtectDosLogConfFile: "", - }, - "/tea": { - AppProtectDosEnable: "on", - AppProtectDosLogEnable: false, - AppProtectDosMonitorURI: "test.example.com", - AppProtectDosMonitorProtocol: "http", - AppProtectDosMonitorTimeout: 0, - AppProtectDosName: "my-dos-tea", - AppProtectDosAccessLogDst: "svc.dns.com:123", - AppProtectDosPolicyFile: "", - AppProtectDosLogConfFile: "", - }, - "/juice": { - AppProtectDosEnable: "on", - AppProtectDosLogEnable: false, - AppProtectDosMonitorURI: "test.example.com", - AppProtectDosMonitorProtocol: "http", - AppProtectDosMonitorTimeout: 0, - AppProtectDosName: "my-dos-juice", - AppProtectDosAccessLogDst: "svc.dns.com:123", - AppProtectDosPolicyFile: "", - AppProtectDosLogConfFile: "", - }, - } - virtualServerEx := VirtualServerEx{ VirtualServer: &conf_v1.VirtualServer{ ObjectMeta: meta_v1.ObjectMeta{ @@ -2153,18 +2172,152 @@ func TestGenerateVirtualServerConfigForVirtualServerRoutesWithDos(t *testing.T) }, Spec: conf_v1.VirtualServerSpec{ Host: "cafe.example.com", - Routes: []conf_v1.Route{ - { - Path: "/coffee", - Route: "default/coffee", - }, + Upstreams: []conf_v1.Upstream{ { - Path: "/tea", - Route: "default/tea", + Name: "tea", + Service: "tea-svc", + Port: 80, + TLS: conf_v1.UpstreamTLS{Enable: false}, }, + }, + Routes: []conf_v1.Route{ { - Path: "/juice", - Route: "default/juice", + Path: "/", + Action: &conf_v1.Action{ + Pass: "tea", + }, + }, + }, + InternalRoute: true, + }, + }, + Endpoints: map[string][]string{ + "default/tea-svc:80": { + "10.0.0.20:80", + }, + }, + } + + baseCfgParams := ConfigParams{ + ServerTokens: "off", + Keepalive: 16, + ServerSnippets: []string{"# server snippet"}, + ProxyProtocol: true, + SetRealIPFrom: []string{"0.0.0.0/0"}, + RealIPHeader: "X-Real-IP", + RealIPRecursive: true, + } + + expected := version2.VirtualServerConfig{ + Upstreams: []version2.Upstream{ + { + UpstreamLabels: version2.UpstreamLabels{ + Service: "tea-svc", + ResourceType: "virtualserver", + ResourceName: "cafe", + ResourceNamespace: "default", + }, + Name: "vs_default_cafe_tea", + Servers: []version2.UpstreamServer{ + { + Address: "10.0.0.20:80", + }, + }, + Keepalive: 16, + }, + }, + HTTPSnippets: []string{}, + LimitReqZones: []version2.LimitReqZone{}, + Server: version2.Server{ + ServerName: "cafe.example.com", + StatusZone: "cafe.example.com", + VSNamespace: "default", + VSName: "cafe", + ProxyProtocol: true, + ServerTokens: "off", + SetRealIPFrom: []string{"0.0.0.0/0"}, + RealIPHeader: "X-Real-IP", + RealIPRecursive: true, + Snippets: []string{"# server snippet"}, + TLSPassthrough: true, + Locations: []version2.Location{ + { + Path: "/", + ProxyPass: "http://vs_default_cafe_tea", + ProxyNextUpstream: "error timeout", + ProxyNextUpstreamTimeout: "0s", + ProxyNextUpstreamTries: 0, + HasKeepalive: true, + ProxySSLName: "tea-svc.default.svc", + ProxyPassRequestHeaders: true, + ProxySetHeaders: []version2.Header{{Name: "Host", Value: "$host"}}, + ServiceName: "tea-svc", + }, + }, + }, + SpiffeCerts: true, + SpiffeClientCerts: true, + } + + isPlus := false + isResolverConfigured := false + staticConfigParams := &StaticConfigParams{TLSPassthrough: true, NginxServiceMesh: true, EnableInternalRoutes: false} + isWildcardEnabled := false + vsc := newVirtualServerConfigurator(&baseCfgParams, isPlus, isResolverConfigured, staticConfigParams, isWildcardEnabled) + + result, warnings := vsc.GenerateVirtualServerConfig(&virtualServerEx, nil, nil) + if diff := cmp.Diff(expected, result); diff == "" { + t.Errorf("GenerateVirtualServerConfig() should not configure internal route") + } + + if len(warnings) != 1 { + t.Errorf("GenerateVirtualServerConfig should return warning to enable internal routing") + } +} + +func TestGenerateVirtualServerConfigForVirtualServerWithSplits(t *testing.T) { + t.Parallel() + virtualServerEx := VirtualServerEx{ + VirtualServer: &conf_v1.VirtualServer{ + ObjectMeta: meta_v1.ObjectMeta{ + Name: "cafe", + Namespace: "default", + }, + Spec: conf_v1.VirtualServerSpec{ + Host: "cafe.example.com", + Upstreams: []conf_v1.Upstream{ + { + Name: "tea-v1", + Service: "tea-svc-v1", + Port: 80, + }, + { + Name: "tea-v2", + Service: "tea-svc-v2", + Port: 80, + }, + }, + Routes: []conf_v1.Route{ + { + Path: "/tea", + Splits: []conf_v1.Split{ + { + Weight: 90, + Action: &conf_v1.Action{ + Pass: "tea-v1", + }, + }, + { + Weight: 10, + Action: &conf_v1.Action{ + Pass: "tea-v2", + }, + }, + }, + }, + { + Path: "/coffee", + Route: "default/coffee", }, }, }, @@ -2182,12 +2335,6 @@ func TestGenerateVirtualServerConfigForVirtualServerRoutesWithDos(t *testing.T) "default/coffee-svc-v2:80": { "10.0.0.31:80", }, - "default/juice-svc-v1:80": { - "10.0.0.33:80", - }, - "default/juice-svc-v2:80": { - "10.0.0.34:80", - }, }, VirtualServerRoutes: []*conf_v1.VirtualServerRoute{ { @@ -2212,220 +2359,201 @@ func TestGenerateVirtualServerConfigForVirtualServerRoutesWithDos(t *testing.T) Subroutes: []conf_v1.Route{ { Path: "/coffee", - Matches: []conf_v1.Match{ + Splits: []conf_v1.Split{ { - Conditions: []conf_v1.Condition{ - { - Argument: "version", - Value: "v2", - }, + Weight: 40, + Action: &conf_v1.Action{ + Pass: "coffee-v1", }, + }, + { + Weight: 60, Action: &conf_v1.Action{ Pass: "coffee-v2", }, }, }, - Dos: "test_ns/dos_protected", - Action: &conf_v1.Action{ - Pass: "coffee-v1", - }, }, }, }, }, + }, + } + + baseCfgParams := ConfigParams{} + + expected := version2.VirtualServerConfig{ + Upstreams: []version2.Upstream{ { - ObjectMeta: meta_v1.ObjectMeta{ - Name: "tea", - Namespace: "default", + Name: "vs_default_cafe_tea-v1", + UpstreamLabels: version2.UpstreamLabels{ + Service: "tea-svc-v1", + ResourceType: "virtualserver", + ResourceName: "cafe", + ResourceNamespace: "default", }, - Spec: conf_v1.VirtualServerRouteSpec{ - Host: "cafe.example.com", - Upstreams: []conf_v1.Upstream{ - { - Name: "tea-v1", - Service: "tea-svc-v1", - Port: 80, - }, - { - Name: "tea-v2", - Service: "tea-svc-v2", - Port: 80, - }, - }, - Subroutes: []conf_v1.Route{ - { - Path: "/tea", - Dos: "test_ns/dos_protected", - Action: &conf_v1.Action{ - Pass: "tea-v1", - }, - }, + Servers: []version2.UpstreamServer{ + { + Address: "10.0.0.20:80", }, }, }, { - ObjectMeta: meta_v1.ObjectMeta{ - Name: "juice", - Namespace: "default", + Name: "vs_default_cafe_tea-v2", + UpstreamLabels: version2.UpstreamLabels{ + Service: "tea-svc-v2", + ResourceType: "virtualserver", + ResourceName: "cafe", + ResourceNamespace: "default", }, - Spec: conf_v1.VirtualServerRouteSpec{ - Host: "cafe.example.com", - Upstreams: []conf_v1.Upstream{ - { - Name: "juice-v1", - Service: "juice-svc-v1", - Port: 80, - }, - { - Name: "juice-v2", - Service: "juice-svc-v2", - Port: 80, - }, + Servers: []version2.UpstreamServer{ + { + Address: "10.0.0.21:80", }, - Subroutes: []conf_v1.Route{ - { - Path: "/juice", - Dos: "test_ns/dos_protected", - Splits: []conf_v1.Split{ - { - Weight: 80, - Action: &conf_v1.Action{ - Pass: "juice-v1", - }, - }, - { - Weight: 20, - Action: &conf_v1.Action{ - Pass: "juice-v2", - }, - }, - }, - }, + }, + }, + { + Name: "vs_default_cafe_vsr_default_coffee_coffee-v1", + UpstreamLabels: version2.UpstreamLabels{ + Service: "coffee-svc-v1", + ResourceType: "virtualserverroute", + ResourceName: "coffee", + ResourceNamespace: "default", + }, + Servers: []version2.UpstreamServer{ + { + Address: "10.0.0.30:80", }, }, }, - }, - } - - baseCfgParams := ConfigParams{} - - expected := []version2.Location{ - { - Path: "/internal_location_matches_0_match_0", - ProxyPass: "http://vs_default_cafe_vsr_default_coffee_coffee-v2$request_uri", - ProxyNextUpstream: "error timeout", - ProxyNextUpstreamTimeout: "0s", - ProxyNextUpstreamTries: 0, - Internal: true, - ProxySSLName: "coffee-svc-v2.default.svc", - ProxyPassRequestHeaders: true, - ProxySetHeaders: []version2.Header{{Name: "Host", Value: "$host"}}, - ServiceName: "coffee-svc-v2", - IsVSR: true, - VSRName: "coffee", - VSRNamespace: "default", - Dos: &version2.Dos{ - Enable: "on", - Name: "my-dos-coffee", - ApDosMonitorURI: "test.example.com", - ApDosMonitorProtocol: "http", - ApDosAccessLogDest: "svc.dns.com:123", + { + Name: "vs_default_cafe_vsr_default_coffee_coffee-v2", + UpstreamLabels: version2.UpstreamLabels{ + Service: "coffee-svc-v2", + ResourceType: "virtualserverroute", + ResourceName: "coffee", + ResourceNamespace: "default", + }, + Servers: []version2.UpstreamServer{ + { + Address: "10.0.0.31:80", + }, + }, }, }, - { - Path: "/internal_location_matches_0_default", - ProxyPass: "http://vs_default_cafe_vsr_default_coffee_coffee-v1$request_uri", - ProxyNextUpstream: "error timeout", - ProxyNextUpstreamTimeout: "0s", - ProxyNextUpstreamTries: 0, - Internal: true, - ProxySSLName: "coffee-svc-v1.default.svc", - ProxyPassRequestHeaders: true, - ProxySetHeaders: []version2.Header{{Name: "Host", Value: "$host"}}, - ServiceName: "coffee-svc-v1", - IsVSR: true, - VSRName: "coffee", - VSRNamespace: "default", - Dos: &version2.Dos{ - Enable: "on", - Name: "my-dos-coffee", - ApDosMonitorURI: "test.example.com", - ApDosMonitorProtocol: "http", - ApDosAccessLogDest: "svc.dns.com:123", + SplitClients: []version2.SplitClient{ + { + Source: "$request_id", + Variable: "$vs_default_cafe_splits_0", + Distributions: []version2.Distribution{ + { + Weight: "90%", + Value: "/internal_location_splits_0_split_0", + }, + { + Weight: "10%", + Value: "/internal_location_splits_0_split_1", + }, + }, }, - }, - { - Path: "/tea", - ProxyPass: "http://vs_default_cafe_vsr_default_tea_tea-v1", - ProxyNextUpstream: "error timeout", - ProxyNextUpstreamTimeout: "0s", - ProxyNextUpstreamTries: 0, - Internal: false, - ProxySSLName: "tea-svc-v1.default.svc", - ProxyPassRequestHeaders: true, - ProxySetHeaders: []version2.Header{{Name: "Host", Value: "$host"}}, - ServiceName: "tea-svc-v1", - IsVSR: true, - VSRName: "tea", - VSRNamespace: "default", - Dos: &version2.Dos{ - Enable: "on", - Name: "my-dos-tea", - ApDosMonitorURI: "test.example.com", - ApDosMonitorProtocol: "http", - ApDosAccessLogDest: "svc.dns.com:123", + { + Source: "$request_id", + Variable: "$vs_default_cafe_splits_1", + Distributions: []version2.Distribution{ + { + Weight: "40%", + Value: "/internal_location_splits_1_split_0", + }, + { + Weight: "60%", + Value: "/internal_location_splits_1_split_1", + }, + }, }, }, - { - Path: "/internal_location_splits_0_split_0", - Internal: true, - ProxyPass: "http://vs_default_cafe_vsr_default_juice_juice-v1$request_uri", - ProxyNextUpstream: "error timeout", - ProxyNextUpstreamTimeout: "0s", - ProxyPassRequestHeaders: true, - ProxySetHeaders: []version2.Header{{Name: "Host", Value: "$host"}}, - ProxySSLName: "juice-svc-v1.default.svc", - Dos: &version2.Dos{ - Enable: "on", - Name: "my-dos-juice", - ApDosMonitorURI: "test.example.com", - ApDosMonitorProtocol: "http", - ApDosAccessLogDest: "svc.dns.com:123", + HTTPSnippets: []string{}, + LimitReqZones: []version2.LimitReqZone{}, + Server: version2.Server{ + ServerName: "cafe.example.com", + StatusZone: "cafe.example.com", + VSNamespace: "default", + VSName: "cafe", + InternalRedirectLocations: []version2.InternalRedirectLocation{ + { + Path: "/tea", + Destination: "$vs_default_cafe_splits_0", + }, + { + Path: "/coffee", + Destination: "$vs_default_cafe_splits_1", + }, }, - ServiceName: "juice-svc-v1", - IsVSR: true, - VSRName: "juice", - VSRNamespace: "default", - }, - { - Path: "/internal_location_splits_0_split_1", - Internal: true, - ProxyPass: "http://vs_default_cafe_vsr_default_juice_juice-v2$request_uri", - ProxyNextUpstream: "error timeout", - ProxyNextUpstreamTimeout: "0s", - ProxyPassRequestHeaders: true, - ProxySetHeaders: []version2.Header{{Name: "Host", Value: "$host"}}, - ProxySSLName: "juice-svc-v2.default.svc", - Dos: &version2.Dos{ - Enable: "on", - Name: "my-dos-juice", - ApDosMonitorURI: "test.example.com", - ApDosMonitorProtocol: "http", - ApDosAccessLogDest: "svc.dns.com:123", + Locations: []version2.Location{ + { + Path: "/internal_location_splits_0_split_0", + ProxyPass: "http://vs_default_cafe_tea-v1$request_uri", + ProxyNextUpstream: "error timeout", + ProxyNextUpstreamTimeout: "0s", + ProxyNextUpstreamTries: 0, + Internal: true, + ProxySSLName: "tea-svc-v1.default.svc", + ProxyPassRequestHeaders: true, + ProxySetHeaders: []version2.Header{{Name: "Host", Value: "$host"}}, + ServiceName: "tea-svc-v1", + }, + { + Path: "/internal_location_splits_0_split_1", + ProxyPass: "http://vs_default_cafe_tea-v2$request_uri", + ProxyNextUpstream: "error timeout", + ProxyNextUpstreamTimeout: "0s", + ProxyNextUpstreamTries: 0, + Internal: true, + ProxySSLName: "tea-svc-v2.default.svc", + ProxyPassRequestHeaders: true, + ProxySetHeaders: []version2.Header{{Name: "Host", Value: "$host"}}, + ServiceName: "tea-svc-v2", + }, + { + Path: "/internal_location_splits_1_split_0", + ProxyPass: "http://vs_default_cafe_vsr_default_coffee_coffee-v1$request_uri", + ProxyNextUpstream: "error timeout", + ProxyNextUpstreamTimeout: "0s", + ProxyNextUpstreamTries: 0, + Internal: true, + ProxySSLName: "coffee-svc-v1.default.svc", + ProxyPassRequestHeaders: true, + ProxySetHeaders: []version2.Header{{Name: "Host", Value: "$host"}}, + ServiceName: "coffee-svc-v1", + IsVSR: true, + VSRName: "coffee", + VSRNamespace: "default", + }, + { + Path: "/internal_location_splits_1_split_1", + ProxyPass: "http://vs_default_cafe_vsr_default_coffee_coffee-v2$request_uri", + ProxyNextUpstream: "error timeout", + ProxyNextUpstreamTimeout: "0s", + ProxyNextUpstreamTries: 0, + Internal: true, + ProxySSLName: "coffee-svc-v2.default.svc", + ProxyPassRequestHeaders: true, + ProxySetHeaders: []version2.Header{{Name: "Host", Value: "$host"}}, + ServiceName: "coffee-svc-v2", + IsVSR: true, + VSRName: "coffee", + VSRNamespace: "default", + }, }, - ServiceName: "juice-svc-v2", - IsVSR: true, - VSRName: "juice", - VSRNamespace: "default", }, } isPlus := false isResolverConfigured := false - vsc := newVirtualServerConfigurator(&baseCfgParams, isPlus, isResolverConfigured, &StaticConfigParams{MainAppProtectDosLoadModule: true}, false) + isWildcardEnabled := false + vsc := newVirtualServerConfigurator(&baseCfgParams, isPlus, isResolverConfigured, &StaticConfigParams{}, isWildcardEnabled) - result, warnings := vsc.GenerateVirtualServerConfig(&virtualServerEx, nil, dosResources) - if diff := cmp.Diff(expected, result.Server.Locations); diff != "" { + result, warnings := vsc.GenerateVirtualServerConfig(&virtualServerEx, nil, nil) + if diff := cmp.Diff(expected, result); diff != "" { t.Errorf("GenerateVirtualServerConfig() mismatch (-want +got):\n%s", diff) } @@ -2434,135 +2562,107 @@ func TestGenerateVirtualServerConfigForVirtualServerRoutesWithDos(t *testing.T) } } -func TestGenerateVirtualServerConfigForVirtualServerWithReturns(t *testing.T) { +func TestGenerateVirtualServerConfigForVirtualServerWithMatches(t *testing.T) { t.Parallel() virtualServerEx := VirtualServerEx{ VirtualServer: &conf_v1.VirtualServer{ ObjectMeta: meta_v1.ObjectMeta{ - Name: "returns", + Name: "cafe", Namespace: "default", }, Spec: conf_v1.VirtualServerSpec{ - Host: "example.com", - Routes: []conf_v1.Route{ + Host: "cafe.example.com", + Upstreams: []conf_v1.Upstream{ { - Path: "/return", - Action: &conf_v1.Action{ - Return: &conf_v1.ActionReturn{ - Body: "hello 0", - }, - }, + Name: "tea-v1", + Service: "tea-svc-v1", + Port: 80, }, { - Path: "/splits-with-return", - Splits: []conf_v1.Split{ + Name: "tea-v2", + Service: "tea-svc-v2", + Port: 80, + }, + }, + Routes: []conf_v1.Route{ + { + Path: "/tea", + Matches: []conf_v1.Match{ { - Weight: 90, + Conditions: []conf_v1.Condition{ + { + Header: "x-version", + Value: "v2", + }, + }, Action: &conf_v1.Action{ - Return: &conf_v1.ActionReturn{ - Body: "hello 1", - }, - }, - }, - { - Weight: 10, - Action: &conf_v1.Action{ - Return: &conf_v1.ActionReturn{ - Body: "hello 2", - }, - }, - }, - }, - }, - { - Path: "/matches-with-return", - Matches: []conf_v1.Match{ - { - Conditions: []conf_v1.Condition{ - { - Header: "x-version", - Value: "v2", - }, - }, - Action: &conf_v1.Action{ - Return: &conf_v1.ActionReturn{ - Body: "hello 3", - }, + Pass: "tea-v2", }, }, }, Action: &conf_v1.Action{ - Return: &conf_v1.ActionReturn{ - Body: "hello 4", - }, + Pass: "tea-v1", }, }, { - Path: "/more", - Route: "default/more-returns", + Path: "/coffee", + Route: "default/coffee", }, }, }, }, + Endpoints: map[string][]string{ + "default/tea-svc-v1:80": { + "10.0.0.20:80", + }, + "default/tea-svc-v2:80": { + "10.0.0.21:80", + }, + "default/coffee-svc-v1:80": { + "10.0.0.30:80", + }, + "default/coffee-svc-v2:80": { + "10.0.0.31:80", + }, + }, VirtualServerRoutes: []*conf_v1.VirtualServerRoute{ { ObjectMeta: meta_v1.ObjectMeta{ - Name: "more-returns", + Name: "coffee", Namespace: "default", }, Spec: conf_v1.VirtualServerRouteSpec{ - Host: "example.com", - Subroutes: []conf_v1.Route{ + Host: "cafe.example.com", + Upstreams: []conf_v1.Upstream{ { - Path: "/more/return", - Action: &conf_v1.Action{ - Return: &conf_v1.ActionReturn{ - Body: "hello 5", - }, - }, + Name: "coffee-v1", + Service: "coffee-svc-v1", + Port: 80, }, { - Path: "/more/splits-with-return", - Splits: []conf_v1.Split{ - { - Weight: 90, - Action: &conf_v1.Action{ - Return: &conf_v1.ActionReturn{ - Body: "hello 6", - }, - }, - }, - { - Weight: 10, - Action: &conf_v1.Action{ - Return: &conf_v1.ActionReturn{ - Body: "hello 7", - }, - }, - }, - }, + Name: "coffee-v2", + Service: "coffee-svc-v2", + Port: 80, }, + }, + Subroutes: []conf_v1.Route{ { - Path: "/more/matches-with-return", + Path: "/coffee", Matches: []conf_v1.Match{ { Conditions: []conf_v1.Condition{ { - Header: "x-version", - Value: "v2", + Argument: "version", + Value: "v2", }, }, Action: &conf_v1.Action{ - Return: &conf_v1.ActionReturn{ - Body: "hello 8", - }, + Pass: "coffee-v2", }, }, }, Action: &conf_v1.Action{ - Return: &conf_v1.ActionReturn{ - Body: "hello 9", - }, + Pass: "coffee-v1", }, }, }, @@ -2574,38 +2674,68 @@ func TestGenerateVirtualServerConfigForVirtualServerWithReturns(t *testing.T) { baseCfgParams := ConfigParams{} expected := version2.VirtualServerConfig{ - Maps: []version2.Map{ + Upstreams: []version2.Upstream{ { - Source: "$http_x_version", - Variable: "$vs_default_returns_matches_0_match_0_cond_0", - Parameters: []version2.Parameter{ + UpstreamLabels: version2.UpstreamLabels{ + Service: "tea-svc-v1", + ResourceType: "virtualserver", + ResourceName: "cafe", + ResourceNamespace: "default", + }, + Name: "vs_default_cafe_tea-v1", + Servers: []version2.UpstreamServer{ { - Value: `"v2"`, - Result: "1", + Address: "10.0.0.20:80", }, + }, + }, + { + Name: "vs_default_cafe_tea-v2", + UpstreamLabels: version2.UpstreamLabels{ + Service: "tea-svc-v2", + ResourceType: "virtualserver", + ResourceName: "cafe", + ResourceNamespace: "default", + }, + Servers: []version2.UpstreamServer{ { - Value: "default", - Result: "0", + Address: "10.0.0.21:80", }, }, }, { - Source: "$vs_default_returns_matches_0_match_0_cond_0", - Variable: "$vs_default_returns_matches_0", - Parameters: []version2.Parameter{ + Name: "vs_default_cafe_vsr_default_coffee_coffee-v1", + UpstreamLabels: version2.UpstreamLabels{ + Service: "coffee-svc-v1", + ResourceType: "virtualserverroute", + ResourceName: "coffee", + ResourceNamespace: "default", + }, + Servers: []version2.UpstreamServer{ { - Value: "~^1", - Result: "/internal_location_matches_0_match_0", + Address: "10.0.0.30:80", }, + }, + }, + { + Name: "vs_default_cafe_vsr_default_coffee_coffee-v2", + UpstreamLabels: version2.UpstreamLabels{ + Service: "coffee-svc-v2", + ResourceType: "virtualserverroute", + ResourceName: "coffee", + ResourceNamespace: "default", + }, + Servers: []version2.UpstreamServer{ { - Value: "default", - Result: "/internal_location_matches_0_default", + Address: "10.0.0.31:80", }, }, }, + }, + Maps: []version2.Map{ { Source: "$http_x_version", - Variable: "$vs_default_returns_matches_1_match_0_cond_0", + Variable: "$vs_default_cafe_matches_0_match_0_cond_0", Parameters: []version2.Parameter{ { Value: `"v2"`, @@ -2618,46 +2748,44 @@ func TestGenerateVirtualServerConfigForVirtualServerWithReturns(t *testing.T) { }, }, { - Source: "$vs_default_returns_matches_1_match_0_cond_0", - Variable: "$vs_default_returns_matches_1", + Source: "$vs_default_cafe_matches_0_match_0_cond_0", + Variable: "$vs_default_cafe_matches_0", Parameters: []version2.Parameter{ { Value: "~^1", - Result: "/internal_location_matches_1_match_0", + Result: "/internal_location_matches_0_match_0", }, { Value: "default", - Result: "/internal_location_matches_1_default", + Result: "/internal_location_matches_0_default", }, }, }, - }, - SplitClients: []version2.SplitClient{ { - Source: "$request_id", - Variable: "$vs_default_returns_splits_0", - Distributions: []version2.Distribution{ + Source: "$arg_version", + Variable: "$vs_default_cafe_matches_1_match_0_cond_0", + Parameters: []version2.Parameter{ { - Weight: "90%", - Value: "/internal_location_splits_0_split_0", + Value: `"v2"`, + Result: "1", }, { - Weight: "10%", - Value: "/internal_location_splits_0_split_1", + Value: "default", + Result: "0", }, }, }, { - Source: "$request_id", - Variable: "$vs_default_returns_splits_1", - Distributions: []version2.Distribution{ + Source: "$vs_default_cafe_matches_1_match_0_cond_0", + Variable: "$vs_default_cafe_matches_1", + Parameters: []version2.Parameter{ { - Weight: "90%", - Value: "/internal_location_splits_1_split_0", + Value: "~^1", + Result: "/internal_location_matches_1_match_0", }, { - Weight: "10%", - Value: "/internal_location_splits_1_split_1", + Value: "default", + Result: "/internal_location_matches_1_default", }, }, }, @@ -2665,230 +2793,74 @@ func TestGenerateVirtualServerConfigForVirtualServerWithReturns(t *testing.T) { HTTPSnippets: []string{}, LimitReqZones: []version2.LimitReqZone{}, Server: version2.Server{ - ServerName: "example.com", - StatusZone: "example.com", + ServerName: "cafe.example.com", + StatusZone: "cafe.example.com", VSNamespace: "default", - VSName: "returns", + VSName: "cafe", InternalRedirectLocations: []version2.InternalRedirectLocation{ { - Path: "/splits-with-return", - Destination: "$vs_default_returns_splits_0", - }, - { - Path: "/matches-with-return", - Destination: "$vs_default_returns_matches_0", + Path: "/tea", + Destination: "$vs_default_cafe_matches_0", }, { - Path: "/more/splits-with-return", - Destination: "$vs_default_returns_splits_1", + Path: "/coffee", + Destination: "$vs_default_cafe_matches_1", }, + }, + Locations: []version2.Location{ { - Path: "/more/matches-with-return", - Destination: "$vs_default_returns_matches_1", + Path: "/internal_location_matches_0_match_0", + ProxyPass: "http://vs_default_cafe_tea-v2$request_uri", + ProxyNextUpstream: "error timeout", + ProxyNextUpstreamTimeout: "0s", + ProxyNextUpstreamTries: 0, + Internal: true, + ProxySSLName: "tea-svc-v2.default.svc", + ProxyPassRequestHeaders: true, + ProxySetHeaders: []version2.Header{{Name: "Host", Value: "$host"}}, + ServiceName: "tea-svc-v2", }, - }, - ReturnLocations: []version2.ReturnLocation{ { - Name: "@return_0", - DefaultType: "text/plain", - Return: version2.Return{ - Code: 0, - Text: "hello 0", - }, + Path: "/internal_location_matches_0_default", + ProxyPass: "http://vs_default_cafe_tea-v1$request_uri", + ProxyNextUpstream: "error timeout", + ProxyNextUpstreamTimeout: "0s", + ProxyNextUpstreamTries: 0, + Internal: true, + ProxySSLName: "tea-svc-v1.default.svc", + ProxyPassRequestHeaders: true, + ProxySetHeaders: []version2.Header{{Name: "Host", Value: "$host"}}, + ServiceName: "tea-svc-v1", }, { - Name: "@return_1", - DefaultType: "text/plain", - Return: version2.Return{ - Code: 0, - Text: "hello 1", - }, - }, - { - Name: "@return_2", - DefaultType: "text/plain", - Return: version2.Return{ - Code: 0, - Text: "hello 2", - }, - }, - { - Name: "@return_3", - DefaultType: "text/plain", - Return: version2.Return{ - Code: 0, - Text: "hello 3", - }, - }, - { - Name: "@return_4", - DefaultType: "text/plain", - Return: version2.Return{ - Code: 0, - Text: "hello 4", - }, - }, - { - Name: "@return_5", - DefaultType: "text/plain", - Return: version2.Return{ - Code: 0, - Text: "hello 5", - }, - }, - { - Name: "@return_6", - DefaultType: "text/plain", - Return: version2.Return{ - Code: 0, - Text: "hello 6", - }, - }, - { - Name: "@return_7", - DefaultType: "text/plain", - Return: version2.Return{ - Code: 0, - Text: "hello 7", - }, - }, - { - Name: "@return_8", - DefaultType: "text/plain", - Return: version2.Return{ - Code: 0, - Text: "hello 8", - }, - }, - { - Name: "@return_9", - DefaultType: "text/plain", - Return: version2.Return{ - Code: 0, - Text: "hello 9", - }, - }, - }, - Locations: []version2.Location{ - { - Path: "/return", - ProxyInterceptErrors: true, - ErrorPages: []version2.ErrorPage{ - { - Name: "@return_0", - Codes: "418", - ResponseCode: 200, - }, - }, - InternalProxyPass: "http://unix:/var/lib/nginx/nginx-418-server.sock", - }, - { - Path: "/internal_location_splits_0_split_0", - ProxyInterceptErrors: true, - ErrorPages: []version2.ErrorPage{ - { - Name: "@return_1", - Codes: "418", - ResponseCode: 200, - }, - }, - InternalProxyPass: "http://unix:/var/lib/nginx/nginx-418-server.sock", - }, - { - Path: "/internal_location_splits_0_split_1", - ProxyInterceptErrors: true, - ErrorPages: []version2.ErrorPage{ - { - Name: "@return_2", - Codes: "418", - ResponseCode: 200, - }, - }, - InternalProxyPass: "http://unix:/var/lib/nginx/nginx-418-server.sock", - }, - { - Path: "/internal_location_matches_0_match_0", - ProxyInterceptErrors: true, - ErrorPages: []version2.ErrorPage{ - { - Name: "@return_3", - Codes: "418", - ResponseCode: 200, - }, - }, - InternalProxyPass: "http://unix:/var/lib/nginx/nginx-418-server.sock", - }, - { - Path: "/internal_location_matches_0_default", - ProxyInterceptErrors: true, - ErrorPages: []version2.ErrorPage{ - { - Name: "@return_4", - Codes: "418", - ResponseCode: 200, - }, - }, - InternalProxyPass: "http://unix:/var/lib/nginx/nginx-418-server.sock", - }, - { - Path: "/more/return", - ProxyInterceptErrors: true, - ErrorPages: []version2.ErrorPage{ - { - Name: "@return_5", - Codes: "418", - ResponseCode: 200, - }, - }, - InternalProxyPass: "http://unix:/var/lib/nginx/nginx-418-server.sock", - }, - { - Path: "/internal_location_splits_1_split_0", - ProxyInterceptErrors: true, - ErrorPages: []version2.ErrorPage{ - { - Name: "@return_6", - Codes: "418", - ResponseCode: 200, - }, - }, - InternalProxyPass: "http://unix:/var/lib/nginx/nginx-418-server.sock", - }, - { - Path: "/internal_location_splits_1_split_1", - ProxyInterceptErrors: true, - ErrorPages: []version2.ErrorPage{ - { - Name: "@return_7", - Codes: "418", - ResponseCode: 200, - }, - }, - InternalProxyPass: "http://unix:/var/lib/nginx/nginx-418-server.sock", - }, - { - Path: "/internal_location_matches_1_match_0", - ProxyInterceptErrors: true, - ErrorPages: []version2.ErrorPage{ - { - Name: "@return_8", - Codes: "418", - ResponseCode: 200, - }, - }, - InternalProxyPass: "http://unix:/var/lib/nginx/nginx-418-server.sock", + Path: "/internal_location_matches_1_match_0", + ProxyPass: "http://vs_default_cafe_vsr_default_coffee_coffee-v2$request_uri", + ProxyNextUpstream: "error timeout", + ProxyNextUpstreamTimeout: "0s", + ProxyNextUpstreamTries: 0, + Internal: true, + ProxySSLName: "coffee-svc-v2.default.svc", + ProxyPassRequestHeaders: true, + ProxySetHeaders: []version2.Header{{Name: "Host", Value: "$host"}}, + ServiceName: "coffee-svc-v2", + IsVSR: true, + VSRName: "coffee", + VSRNamespace: "default", }, { - Path: "/internal_location_matches_1_default", - ProxyInterceptErrors: true, - ErrorPages: []version2.ErrorPage{ - { - Name: "@return_9", - Codes: "418", - ResponseCode: 200, - }, - }, - InternalProxyPass: "http://unix:/var/lib/nginx/nginx-418-server.sock", + Path: "/internal_location_matches_1_default", + ProxyPass: "http://vs_default_cafe_vsr_default_coffee_coffee-v1$request_uri", + ProxyNextUpstream: "error timeout", + ProxyNextUpstreamTimeout: "0s", + ProxyNextUpstreamTries: 0, + Internal: true, + ProxySSLName: "coffee-svc-v1.default.svc", + ProxyPassRequestHeaders: true, + ProxySetHeaders: []version2.Header{{Name: "Host", Value: "$host"}}, + ServiceName: "coffee-svc-v1", + IsVSR: true, + VSRName: "coffee", + VSRNamespace: "default", }, }, }, @@ -2900,8 +2872,8 @@ func TestGenerateVirtualServerConfigForVirtualServerWithReturns(t *testing.T) { vsc := newVirtualServerConfigurator(&baseCfgParams, isPlus, isResolverConfigured, &StaticConfigParams{}, isWildcardEnabled) result, warnings := vsc.GenerateVirtualServerConfig(&virtualServerEx, nil, nil) - if !reflect.DeepEqual(result, expected) { - t.Errorf("GenerateVirtualServerConfig returned \n%+v but expected \n%+v", result, expected) + if diff := cmp.Diff(expected, result); diff != "" { + t.Errorf("GenerateVirtualServerConfig() mismatch (-want +got):\n%s", diff) } if len(warnings) != 0 { @@ -2909,777 +2881,1003 @@ func TestGenerateVirtualServerConfigForVirtualServerWithReturns(t *testing.T) { } } -func TestGeneratePolicies(t *testing.T) { +func TestGenerateVirtualServerConfigForVirtualServerRoutesWithDos(t *testing.T) { t.Parallel() - ownerDetails := policyOwnerDetails{ - owner: nil, // nil is OK for the unit test - ownerNamespace: "default", - vsNamespace: "default", - vsName: "test", - } - mTLSCertPath := "/etc/nginx/secrets/default-ingress-mtls-secret-ca.crt" - mTLSCrlPath := "/etc/nginx/secrets/default-ingress-mtls-secret-ca.crl" - mTLSCertAndCrlPath := fmt.Sprintf("%s %s", mTLSCertPath, mTLSCrlPath) - policyOpts := policyOptions{ - tls: true, - secretRefs: map[string]*secrets.SecretReference{ - "default/ingress-mtls-secret": { - Secret: &api_v1.Secret{ - Type: secrets.SecretTypeCA, - }, - Path: mTLSCertPath, - }, - "default/ingress-mtls-secret-crl": { - Secret: &api_v1.Secret{ - Type: secrets.SecretTypeCA, - Data: map[string][]byte{ - "ca.crl": []byte("base64crl"), - }, - }, - Path: mTLSCertAndCrlPath, - }, - "default/egress-mtls-secret": { - Secret: &api_v1.Secret{ - Type: api_v1.SecretTypeTLS, - }, - Path: "/etc/nginx/secrets/default-egress-mtls-secret", - }, - "default/egress-trusted-ca-secret": { - Secret: &api_v1.Secret{ - Type: secrets.SecretTypeCA, - }, - Path: "/etc/nginx/secrets/default-egress-trusted-ca-secret", + dosResources := map[string]*appProtectDosResource{ + "/coffee": { + AppProtectDosEnable: "on", + AppProtectDosLogEnable: false, + AppProtectDosMonitorURI: "test.example.com", + AppProtectDosMonitorProtocol: "http", + AppProtectDosMonitorTimeout: 0, + AppProtectDosName: "my-dos-coffee", + AppProtectDosAccessLogDst: "svc.dns.com:123", + AppProtectDosPolicyFile: "", + AppProtectDosLogConfFile: "", + }, + "/tea": { + AppProtectDosEnable: "on", + AppProtectDosLogEnable: false, + AppProtectDosMonitorURI: "test.example.com", + AppProtectDosMonitorProtocol: "http", + AppProtectDosMonitorTimeout: 0, + AppProtectDosName: "my-dos-tea", + AppProtectDosAccessLogDst: "svc.dns.com:123", + AppProtectDosPolicyFile: "", + AppProtectDosLogConfFile: "", + }, + "/juice": { + AppProtectDosEnable: "on", + AppProtectDosLogEnable: false, + AppProtectDosMonitorURI: "test.example.com", + AppProtectDosMonitorProtocol: "http", + AppProtectDosMonitorTimeout: 0, + AppProtectDosName: "my-dos-juice", + AppProtectDosAccessLogDst: "svc.dns.com:123", + AppProtectDosPolicyFile: "", + AppProtectDosLogConfFile: "", + }, + } + + virtualServerEx := VirtualServerEx{ + VirtualServer: &conf_v1.VirtualServer{ + ObjectMeta: meta_v1.ObjectMeta{ + Name: "cafe", + Namespace: "default", }, - "default/egress-trusted-ca-secret-crl": { - Secret: &api_v1.Secret{ - Type: secrets.SecretTypeCA, + Spec: conf_v1.VirtualServerSpec{ + Host: "cafe.example.com", + Routes: []conf_v1.Route{ + { + Path: "/coffee", + Route: "default/coffee", + }, + { + Path: "/tea", + Route: "default/tea", + }, + { + Path: "/juice", + Route: "default/juice", + }, }, - Path: mTLSCertAndCrlPath, }, - "default/jwt-secret": { - Secret: &api_v1.Secret{ - Type: secrets.SecretTypeJWK, - }, - Path: "/etc/nginx/secrets/default-jwt-secret", + }, + Endpoints: map[string][]string{ + "default/tea-svc-v1:80": { + "10.0.0.20:80", }, - "default/htpasswd-secret": { - Secret: &api_v1.Secret{ - Type: secrets.SecretTypeHtpasswd, - }, - Path: "/etc/nginx/secrets/default-htpasswd-secret", + "default/tea-svc-v2:80": { + "10.0.0.21:80", }, - "default/oidc-secret": { - Secret: &api_v1.Secret{ - Type: secrets.SecretTypeOIDC, - Data: map[string][]byte{ - "client-secret": []byte("super_secret_123"), - }, - }, + "default/coffee-svc-v1:80": { + "10.0.0.30:80", }, - }, - apResources: &appProtectResourcesForVS{ - Policies: map[string]string{ - "default/dataguard-alarm": "/etc/nginx/waf/nac-policies/default-dataguard-alarm", + "default/coffee-svc-v2:80": { + "10.0.0.31:80", }, - LogConfs: map[string]string{ - "default/logconf": "/etc/nginx/waf/nac-logconfs/default-logconf", + "default/juice-svc-v1:80": { + "10.0.0.33:80", + }, + "default/juice-svc-v2:80": { + "10.0.0.34:80", }, }, - } - - tests := []struct { - policyRefs []conf_v1.PolicyReference - policies map[string]*conf_v1.Policy - context string - expected policiesCfg - msg string - }{ - { - policyRefs: []conf_v1.PolicyReference{ - { - Name: "allow-policy", + VirtualServerRoutes: []*conf_v1.VirtualServerRoute{ + { + ObjectMeta: meta_v1.ObjectMeta{ + Name: "coffee", Namespace: "default", }, - }, - policies: map[string]*conf_v1.Policy{ - "default/allow-policy": { - Spec: conf_v1.PolicySpec{ - AccessControl: &conf_v1.AccessControl{ - Allow: []string{"127.0.0.1"}, + Spec: conf_v1.VirtualServerRouteSpec{ + Host: "cafe.example.com", + Upstreams: []conf_v1.Upstream{ + { + Name: "coffee-v1", + Service: "coffee-svc-v1", + Port: 80, + }, + { + Name: "coffee-v2", + Service: "coffee-svc-v2", + Port: 80, }, }, - }, - }, - expected: policiesCfg{ - Allow: []string{"127.0.0.1"}, - }, - msg: "explicit reference", - }, - { - policyRefs: []conf_v1.PolicyReference{ - { - Name: "allow-policy", - }, - }, - policies: map[string]*conf_v1.Policy{ - "default/allow-policy": { - Spec: conf_v1.PolicySpec{ - AccessControl: &conf_v1.AccessControl{ - Allow: []string{"127.0.0.1"}, + Subroutes: []conf_v1.Route{ + { + Path: "/coffee", + Matches: []conf_v1.Match{ + { + Conditions: []conf_v1.Condition{ + { + Argument: "version", + Value: "v2", + }, + }, + Action: &conf_v1.Action{ + Pass: "coffee-v2", + }, + }, + }, + Dos: "test_ns/dos_protected", + Action: &conf_v1.Action{ + Pass: "coffee-v1", + }, }, }, }, }, - expected: policiesCfg{ - Allow: []string{"127.0.0.1"}, - }, - msg: "implicit reference", - }, - { - policyRefs: []conf_v1.PolicyReference{ - { - Name: "allow-policy-1", - }, - { - Name: "allow-policy-2", + { + ObjectMeta: meta_v1.ObjectMeta{ + Name: "tea", + Namespace: "default", }, - }, - policies: map[string]*conf_v1.Policy{ - "default/allow-policy-1": { - Spec: conf_v1.PolicySpec{ - AccessControl: &conf_v1.AccessControl{ - Allow: []string{"127.0.0.1"}, + Spec: conf_v1.VirtualServerRouteSpec{ + Host: "cafe.example.com", + Upstreams: []conf_v1.Upstream{ + { + Name: "tea-v1", + Service: "tea-svc-v1", + Port: 80, + }, + { + Name: "tea-v2", + Service: "tea-svc-v2", + Port: 80, }, }, - }, - "default/allow-policy-2": { - Spec: conf_v1.PolicySpec{ - AccessControl: &conf_v1.AccessControl{ - Allow: []string{"127.0.0.2"}, + Subroutes: []conf_v1.Route{ + { + Path: "/tea", + Dos: "test_ns/dos_protected", + Action: &conf_v1.Action{ + Pass: "tea-v1", + }, }, }, }, }, - expected: policiesCfg{ - Allow: []string{"127.0.0.1", "127.0.0.2"}, - }, - msg: "merging", - }, - { - policyRefs: []conf_v1.PolicyReference{ - { - Name: "rateLimit-policy", + { + ObjectMeta: meta_v1.ObjectMeta{ + Name: "juice", Namespace: "default", }, - }, - policies: map[string]*conf_v1.Policy{ - "default/rateLimit-policy": { - Spec: conf_v1.PolicySpec{ - RateLimit: &conf_v1.RateLimit{ - Key: "test", - ZoneSize: "10M", - Rate: "10r/s", - LogLevel: "notice", + Spec: conf_v1.VirtualServerRouteSpec{ + Host: "cafe.example.com", + Upstreams: []conf_v1.Upstream{ + { + Name: "juice-v1", + Service: "juice-svc-v1", + Port: 80, + }, + { + Name: "juice-v2", + Service: "juice-svc-v2", + Port: 80, }, }, - }, - }, - expected: policiesCfg{ - LimitReqZones: []version2.LimitReqZone{ - { - Key: "test", - ZoneSize: "10M", - Rate: "10r/s", - ZoneName: "pol_rl_default_rateLimit-policy_default_test", - }, - }, - LimitReqOptions: version2.LimitReqOptions{ - LogLevel: "notice", - RejectCode: 503, - }, - LimitReqs: []version2.LimitReq{ - { - ZoneName: "pol_rl_default_rateLimit-policy_default_test", + Subroutes: []conf_v1.Route{ + { + Path: "/juice", + Dos: "test_ns/dos_protected", + Splits: []conf_v1.Split{ + { + Weight: 80, + Action: &conf_v1.Action{ + Pass: "juice-v1", + }, + }, + { + Weight: 20, + Action: &conf_v1.Action{ + Pass: "juice-v2", + }, + }, + }, + }, }, }, }, - msg: "rate limit reference", }, + } + + baseCfgParams := ConfigParams{} + + expected := []version2.Location{ { - policyRefs: []conf_v1.PolicyReference{ - { - Name: "rateLimit-policy", - Namespace: "default", - }, - { - Name: "rateLimit-policy2", - Namespace: "default", - }, - }, - policies: map[string]*conf_v1.Policy{ - "default/rateLimit-policy": { - Spec: conf_v1.PolicySpec{ - RateLimit: &conf_v1.RateLimit{ - Key: "test", - ZoneSize: "10M", - Rate: "10r/s", - }, - }, - }, - "default/rateLimit-policy2": { - Spec: conf_v1.PolicySpec{ - RateLimit: &conf_v1.RateLimit{ - Key: "test2", - ZoneSize: "20M", - Rate: "20r/s", - }, - }, - }, - }, - expected: policiesCfg{ - LimitReqZones: []version2.LimitReqZone{ - { - Key: "test", - ZoneSize: "10M", - Rate: "10r/s", - ZoneName: "pol_rl_default_rateLimit-policy_default_test", - }, - { - Key: "test2", - ZoneSize: "20M", - Rate: "20r/s", - ZoneName: "pol_rl_default_rateLimit-policy2_default_test", - }, - }, - LimitReqOptions: version2.LimitReqOptions{ - LogLevel: "error", - RejectCode: 503, - }, - LimitReqs: []version2.LimitReq{ - { - ZoneName: "pol_rl_default_rateLimit-policy_default_test", - }, - { - ZoneName: "pol_rl_default_rateLimit-policy2_default_test", - }, - }, + Path: "/internal_location_matches_0_match_0", + ProxyPass: "http://vs_default_cafe_vsr_default_coffee_coffee-v2$request_uri", + ProxyNextUpstream: "error timeout", + ProxyNextUpstreamTimeout: "0s", + ProxyNextUpstreamTries: 0, + Internal: true, + ProxySSLName: "coffee-svc-v2.default.svc", + ProxyPassRequestHeaders: true, + ProxySetHeaders: []version2.Header{{Name: "Host", Value: "$host"}}, + ServiceName: "coffee-svc-v2", + IsVSR: true, + VSRName: "coffee", + VSRNamespace: "default", + Dos: &version2.Dos{ + Enable: "on", + Name: "my-dos-coffee", + ApDosMonitorURI: "test.example.com", + ApDosMonitorProtocol: "http", + ApDosAccessLogDest: "svc.dns.com:123", }, - msg: "multi rate limit reference", }, { - policyRefs: []conf_v1.PolicyReference{ - { - Name: "jwt-policy", - Namespace: "default", - }, - }, - policies: map[string]*conf_v1.Policy{ - "default/jwt-policy": { - ObjectMeta: meta_v1.ObjectMeta{ - Name: "jwt-policy", - Namespace: "default", - }, - Spec: conf_v1.PolicySpec{ - JWTAuth: &conf_v1.JWTAuth{ - Realm: "My Test API", - Secret: "jwt-secret", - }, - }, - }, - }, - expected: policiesCfg{ - JWTAuth: &version2.JWTAuth{ - Secret: "/etc/nginx/secrets/default-jwt-secret", - Realm: "My Test API", - }, + Path: "/internal_location_matches_0_default", + ProxyPass: "http://vs_default_cafe_vsr_default_coffee_coffee-v1$request_uri", + ProxyNextUpstream: "error timeout", + ProxyNextUpstreamTimeout: "0s", + ProxyNextUpstreamTries: 0, + Internal: true, + ProxySSLName: "coffee-svc-v1.default.svc", + ProxyPassRequestHeaders: true, + ProxySetHeaders: []version2.Header{{Name: "Host", Value: "$host"}}, + ServiceName: "coffee-svc-v1", + IsVSR: true, + VSRName: "coffee", + VSRNamespace: "default", + Dos: &version2.Dos{ + Enable: "on", + Name: "my-dos-coffee", + ApDosMonitorURI: "test.example.com", + ApDosMonitorProtocol: "http", + ApDosAccessLogDest: "svc.dns.com:123", }, - msg: "jwt reference", }, { - policyRefs: []conf_v1.PolicyReference{ - { - Name: "jwt-policy-2", - Namespace: "default", - }, - }, - policies: map[string]*conf_v1.Policy{ - "default/jwt-policy-2": { - ObjectMeta: meta_v1.ObjectMeta{ - Name: "jwt-policy", - Namespace: "default", - }, - Spec: conf_v1.PolicySpec{ - JWTAuth: &conf_v1.JWTAuth{ - Realm: "My Test API", - JwksURI: "https://idp.example.com:443/keys", - KeyCache: "1h", - }, - }, - }, + Path: "/tea", + ProxyPass: "http://vs_default_cafe_vsr_default_tea_tea-v1", + ProxyNextUpstream: "error timeout", + ProxyNextUpstreamTimeout: "0s", + ProxyNextUpstreamTries: 0, + Internal: false, + ProxySSLName: "tea-svc-v1.default.svc", + ProxyPassRequestHeaders: true, + ProxySetHeaders: []version2.Header{{Name: "Host", Value: "$host"}}, + ServiceName: "tea-svc-v1", + IsVSR: true, + VSRName: "tea", + VSRNamespace: "default", + Dos: &version2.Dos{ + Enable: "on", + Name: "my-dos-tea", + ApDosMonitorURI: "test.example.com", + ApDosMonitorProtocol: "http", + ApDosAccessLogDest: "svc.dns.com:123", }, - expected: policiesCfg{ - JWTAuth: &version2.JWTAuth{ - Realm: "My Test API", - JwksURI: version2.JwksURI{ - JwksScheme: "https", - JwksHost: "idp.example.com", - JwksPort: "443", - JwksPath: "/keys", - }, - KeyCache: "1h", - }, + }, + { + Path: "/internal_location_splits_0_split_0", + Internal: true, + ProxyPass: "http://vs_default_cafe_vsr_default_juice_juice-v1$request_uri", + ProxyNextUpstream: "error timeout", + ProxyNextUpstreamTimeout: "0s", + ProxyPassRequestHeaders: true, + ProxySetHeaders: []version2.Header{{Name: "Host", Value: "$host"}}, + ProxySSLName: "juice-svc-v1.default.svc", + Dos: &version2.Dos{ + Enable: "on", + Name: "my-dos-juice", + ApDosMonitorURI: "test.example.com", + ApDosMonitorProtocol: "http", + ApDosAccessLogDest: "svc.dns.com:123", }, - msg: "Basic jwks example", + ServiceName: "juice-svc-v1", + IsVSR: true, + VSRName: "juice", + VSRNamespace: "default", }, { - policyRefs: []conf_v1.PolicyReference{ - { - Name: "jwt-policy-2", - Namespace: "default", - }, + Path: "/internal_location_splits_0_split_1", + Internal: true, + ProxyPass: "http://vs_default_cafe_vsr_default_juice_juice-v2$request_uri", + ProxyNextUpstream: "error timeout", + ProxyNextUpstreamTimeout: "0s", + ProxyPassRequestHeaders: true, + ProxySetHeaders: []version2.Header{{Name: "Host", Value: "$host"}}, + ProxySSLName: "juice-svc-v2.default.svc", + Dos: &version2.Dos{ + Enable: "on", + Name: "my-dos-juice", + ApDosMonitorURI: "test.example.com", + ApDosMonitorProtocol: "http", + ApDosAccessLogDest: "svc.dns.com:123", }, - policies: map[string]*conf_v1.Policy{ - "default/jwt-policy-2": { - ObjectMeta: meta_v1.ObjectMeta{ - Name: "jwt-policy", - Namespace: "default", + ServiceName: "juice-svc-v2", + IsVSR: true, + VSRName: "juice", + VSRNamespace: "default", + }, + } + + isPlus := false + isResolverConfigured := false + vsc := newVirtualServerConfigurator(&baseCfgParams, isPlus, isResolverConfigured, &StaticConfigParams{MainAppProtectDosLoadModule: true}, false) + + result, warnings := vsc.GenerateVirtualServerConfig(&virtualServerEx, nil, dosResources) + if diff := cmp.Diff(expected, result.Server.Locations); diff != "" { + t.Errorf("GenerateVirtualServerConfig() mismatch (-want +got):\n%s", diff) + } + + if len(warnings) != 0 { + t.Errorf("GenerateVirtualServerConfig returned warnings: %v", vsc.warnings) + } +} + +func TestGenerateVirtualServerConfigForVirtualServerWithReturns(t *testing.T) { + t.Parallel() + virtualServerEx := VirtualServerEx{ + VirtualServer: &conf_v1.VirtualServer{ + ObjectMeta: meta_v1.ObjectMeta{ + Name: "returns", + Namespace: "default", + }, + Spec: conf_v1.VirtualServerSpec{ + Host: "example.com", + Routes: []conf_v1.Route{ + { + Path: "/return", + Action: &conf_v1.Action{ + Return: &conf_v1.ActionReturn{ + Body: "hello 0", + }, + }, }, - Spec: conf_v1.PolicySpec{ - JWTAuth: &conf_v1.JWTAuth{ - Realm: "My Test API", - JwksURI: "https://idp.example.com/keys", - KeyCache: "1h", + { + Path: "/splits-with-return", + Splits: []conf_v1.Split{ + { + Weight: 90, + Action: &conf_v1.Action{ + Return: &conf_v1.ActionReturn{ + Body: "hello 1", + }, + }, + }, + { + Weight: 10, + Action: &conf_v1.Action{ + Return: &conf_v1.ActionReturn{ + Body: "hello 2", + }, + }, + }, }, }, - }, - }, - expected: policiesCfg{ - JWTAuth: &version2.JWTAuth{ - Realm: "My Test API", - JwksURI: version2.JwksURI{ - JwksScheme: "https", - JwksHost: "idp.example.com", - JwksPort: "", - JwksPath: "/keys", + { + Path: "/matches-with-return", + Matches: []conf_v1.Match{ + { + Conditions: []conf_v1.Condition{ + { + Header: "x-version", + Value: "v2", + }, + }, + Action: &conf_v1.Action{ + Return: &conf_v1.ActionReturn{ + Body: "hello 3", + }, + }, + }, + }, + Action: &conf_v1.Action{ + Return: &conf_v1.ActionReturn{ + Body: "hello 4", + }, + }, + }, + { + Path: "/more", + Route: "default/more-returns", }, - KeyCache: "1h", }, }, - msg: "Basic jwks example, no port in JwksURI", }, - { - policyRefs: []conf_v1.PolicyReference{ - { - Name: "basic-auth-policy", + VirtualServerRoutes: []*conf_v1.VirtualServerRoute{ + { + ObjectMeta: meta_v1.ObjectMeta{ + Name: "more-returns", Namespace: "default", }, - }, - policies: map[string]*conf_v1.Policy{ - "default/basic-auth-policy": { - ObjectMeta: meta_v1.ObjectMeta{ - Name: "basic-auth-policy", - Namespace: "default", - }, - Spec: conf_v1.PolicySpec{ - BasicAuth: &conf_v1.BasicAuth{ - Realm: "My Test API", - Secret: "htpasswd-secret", + Spec: conf_v1.VirtualServerRouteSpec{ + Host: "example.com", + Subroutes: []conf_v1.Route{ + { + Path: "/more/return", + Action: &conf_v1.Action{ + Return: &conf_v1.ActionReturn{ + Body: "hello 5", + }, + }, + }, + { + Path: "/more/splits-with-return", + Splits: []conf_v1.Split{ + { + Weight: 90, + Action: &conf_v1.Action{ + Return: &conf_v1.ActionReturn{ + Body: "hello 6", + }, + }, + }, + { + Weight: 10, + Action: &conf_v1.Action{ + Return: &conf_v1.ActionReturn{ + Body: "hello 7", + }, + }, + }, + }, + }, + { + Path: "/more/matches-with-return", + Matches: []conf_v1.Match{ + { + Conditions: []conf_v1.Condition{ + { + Header: "x-version", + Value: "v2", + }, + }, + Action: &conf_v1.Action{ + Return: &conf_v1.ActionReturn{ + Body: "hello 8", + }, + }, + }, + }, + Action: &conf_v1.Action{ + Return: &conf_v1.ActionReturn{ + Body: "hello 9", + }, + }, }, }, }, }, - expected: policiesCfg{ - BasicAuth: &version2.BasicAuth{ - Secret: "/etc/nginx/secrets/default-htpasswd-secret", - Realm: "My Test API", + }, + } + + baseCfgParams := ConfigParams{} + + expected := version2.VirtualServerConfig{ + Maps: []version2.Map{ + { + Source: "$http_x_version", + Variable: "$vs_default_returns_matches_0_match_0_cond_0", + Parameters: []version2.Parameter{ + { + Value: `"v2"`, + Result: "1", + }, + { + Value: "default", + Result: "0", + }, }, }, - msg: "basic auth reference", - }, - { - policyRefs: []conf_v1.PolicyReference{ - { - Name: "ingress-mtls-policy", - Namespace: "default", + { + Source: "$vs_default_returns_matches_0_match_0_cond_0", + Variable: "$vs_default_returns_matches_0", + Parameters: []version2.Parameter{ + { + Value: "~^1", + Result: "/internal_location_matches_0_match_0", + }, + { + Value: "default", + Result: "/internal_location_matches_0_default", + }, }, }, - policies: map[string]*conf_v1.Policy{ - "default/ingress-mtls-policy": { - ObjectMeta: meta_v1.ObjectMeta{ - Name: "ingress-mtls-policy", - Namespace: "default", + { + Source: "$http_x_version", + Variable: "$vs_default_returns_matches_1_match_0_cond_0", + Parameters: []version2.Parameter{ + { + Value: `"v2"`, + Result: "1", }, - Spec: conf_v1.PolicySpec{ - IngressMTLS: &conf_v1.IngressMTLS{ - ClientCertSecret: "ingress-mtls-secret", - VerifyClient: "off", - }, + { + Value: "default", + Result: "0", }, }, }, - context: "spec", - expected: policiesCfg{ - IngressMTLS: &version2.IngressMTLS{ - ClientCert: mTLSCertPath, - VerifyClient: "off", - VerifyDepth: 1, + { + Source: "$vs_default_returns_matches_1_match_0_cond_0", + Variable: "$vs_default_returns_matches_1", + Parameters: []version2.Parameter{ + { + Value: "~^1", + Result: "/internal_location_matches_1_match_0", + }, + { + Value: "default", + Result: "/internal_location_matches_1_default", + }, }, }, - msg: "ingressMTLS reference", }, - { - policyRefs: []conf_v1.PolicyReference{ - { - Name: "ingress-mtls-policy-crl", - Namespace: "default", - }, - }, - policies: map[string]*conf_v1.Policy{ - "default/ingress-mtls-policy-crl": { - ObjectMeta: meta_v1.ObjectMeta{ - Name: "ingress-mtls-policy-crl", - Namespace: "default", + SplitClients: []version2.SplitClient{ + { + Source: "$request_id", + Variable: "$vs_default_returns_splits_0", + Distributions: []version2.Distribution{ + { + Weight: "90%", + Value: "/internal_location_splits_0_split_0", }, - Spec: conf_v1.PolicySpec{ - IngressMTLS: &conf_v1.IngressMTLS{ - ClientCertSecret: "ingress-mtls-secret-crl", - VerifyClient: "off", - }, + { + Weight: "10%", + Value: "/internal_location_splits_0_split_1", }, }, }, - context: "spec", - expected: policiesCfg{ - IngressMTLS: &version2.IngressMTLS{ - ClientCert: mTLSCertPath, - ClientCrl: mTLSCrlPath, - VerifyClient: "off", - VerifyDepth: 1, + { + Source: "$request_id", + Variable: "$vs_default_returns_splits_1", + Distributions: []version2.Distribution{ + { + Weight: "90%", + Value: "/internal_location_splits_1_split_0", + }, + { + Weight: "10%", + Value: "/internal_location_splits_1_split_1", + }, }, }, - msg: "ingressMTLS reference with ca.crl field in secret", }, - { - policyRefs: []conf_v1.PolicyReference{ + HTTPSnippets: []string{}, + LimitReqZones: []version2.LimitReqZone{}, + Server: version2.Server{ + ServerName: "example.com", + StatusZone: "example.com", + VSNamespace: "default", + VSName: "returns", + InternalRedirectLocations: []version2.InternalRedirectLocation{ { - Name: "ingress-mtls-policy-crl", - Namespace: "default", + Path: "/splits-with-return", + Destination: "$vs_default_returns_splits_0", }, - }, - policies: map[string]*conf_v1.Policy{ - "default/ingress-mtls-policy-crl": { - ObjectMeta: meta_v1.ObjectMeta{ - Name: "ingress-mtls-policy-crl", - Namespace: "default", - }, - Spec: conf_v1.PolicySpec{ - IngressMTLS: &conf_v1.IngressMTLS{ - ClientCertSecret: "ingress-mtls-secret", - CrlFileName: "default-ingress-mtls-secret-ca.crl", - VerifyClient: "off", - }, - }, + { + Path: "/matches-with-return", + Destination: "$vs_default_returns_matches_0", }, - }, - context: "spec", - expected: policiesCfg{ - IngressMTLS: &version2.IngressMTLS{ - ClientCert: mTLSCertPath, - ClientCrl: mTLSCrlPath, - VerifyClient: "off", - VerifyDepth: 1, + { + Path: "/more/splits-with-return", + Destination: "$vs_default_returns_splits_1", }, - }, - msg: "ingressMTLS reference with crl field in policy", - }, - { - policyRefs: []conf_v1.PolicyReference{ { - Name: "egress-mtls-policy", - Namespace: "default", + Path: "/more/matches-with-return", + Destination: "$vs_default_returns_matches_1", }, }, - policies: map[string]*conf_v1.Policy{ - "default/egress-mtls-policy": { - Spec: conf_v1.PolicySpec{ - EgressMTLS: &conf_v1.EgressMTLS{ - TLSSecret: "egress-mtls-secret", - ServerName: true, - SessionReuse: createPointerFromBool(false), - TrustedCertSecret: "egress-trusted-ca-secret", - }, + ReturnLocations: []version2.ReturnLocation{ + { + Name: "@return_0", + DefaultType: "text/plain", + Return: version2.Return{ + Code: 0, + Text: "hello 0", }, }, - }, - context: "route", - expected: policiesCfg{ - EgressMTLS: &version2.EgressMTLS{ - Certificate: "/etc/nginx/secrets/default-egress-mtls-secret", - CertificateKey: "/etc/nginx/secrets/default-egress-mtls-secret", - Ciphers: "DEFAULT", - Protocols: "TLSv1 TLSv1.1 TLSv1.2", - ServerName: true, - SessionReuse: false, - VerifyDepth: 1, - VerifyServer: false, - TrustedCert: "/etc/nginx/secrets/default-egress-trusted-ca-secret", - SSLName: "$proxy_host", + { + Name: "@return_1", + DefaultType: "text/plain", + Return: version2.Return{ + Code: 0, + Text: "hello 1", + }, }, - }, - msg: "egressMTLS reference", - }, - { - policyRefs: []conf_v1.PolicyReference{ { - Name: "egress-mtls-policy", - Namespace: "default", + Name: "@return_2", + DefaultType: "text/plain", + Return: version2.Return{ + Code: 0, + Text: "hello 2", + }, }, - }, - policies: map[string]*conf_v1.Policy{ - "default/egress-mtls-policy": { - Spec: conf_v1.PolicySpec{ - EgressMTLS: &conf_v1.EgressMTLS{ - TLSSecret: "egress-mtls-secret", - ServerName: true, - SessionReuse: createPointerFromBool(false), - TrustedCertSecret: "egress-trusted-ca-secret-crl", - }, + { + Name: "@return_3", + DefaultType: "text/plain", + Return: version2.Return{ + Code: 0, + Text: "hello 3", }, }, - }, - context: "route", - expected: policiesCfg{ - EgressMTLS: &version2.EgressMTLS{ - Certificate: "/etc/nginx/secrets/default-egress-mtls-secret", - CertificateKey: "/etc/nginx/secrets/default-egress-mtls-secret", - Ciphers: "DEFAULT", - Protocols: "TLSv1 TLSv1.1 TLSv1.2", - ServerName: true, - SessionReuse: false, - VerifyDepth: 1, - VerifyServer: false, - TrustedCert: mTLSCertPath, - SSLName: "$proxy_host", + { + Name: "@return_4", + DefaultType: "text/plain", + Return: version2.Return{ + Code: 0, + Text: "hello 4", + }, }, - }, - msg: "egressMTLS with crt and crl", - }, - { - policyRefs: []conf_v1.PolicyReference{ { - Name: "oidc-policy", - Namespace: "default", + Name: "@return_5", + DefaultType: "text/plain", + Return: version2.Return{ + Code: 0, + Text: "hello 5", + }, }, - }, - policies: map[string]*conf_v1.Policy{ - "default/oidc-policy": { - ObjectMeta: meta_v1.ObjectMeta{ - Name: "oidc-policy", - Namespace: "default", + { + Name: "@return_6", + DefaultType: "text/plain", + Return: version2.Return{ + Code: 0, + Text: "hello 6", }, - Spec: conf_v1.PolicySpec{ - OIDC: &conf_v1.OIDC{ - AuthEndpoint: "http://example.com/auth", - TokenEndpoint: "http://example.com/token", - JWKSURI: "http://example.com/jwks", - ClientID: "client-id", - ClientSecret: "oidc-secret", - Scope: "scope", - RedirectURI: "/redirect", - ZoneSyncLeeway: createPointerFromInt(20), - AccessTokenEnable: true, - }, + }, + { + Name: "@return_7", + DefaultType: "text/plain", + Return: version2.Return{ + Code: 0, + Text: "hello 7", }, }, - }, - expected: policiesCfg{ - OIDC: true, - }, - msg: "oidc reference", - }, - { - policyRefs: []conf_v1.PolicyReference{ { - Name: "waf-policy", - Namespace: "default", + Name: "@return_8", + DefaultType: "text/plain", + Return: version2.Return{ + Code: 0, + Text: "hello 8", + }, + }, + { + Name: "@return_9", + DefaultType: "text/plain", + Return: version2.Return{ + Code: 0, + Text: "hello 9", + }, }, }, - policies: map[string]*conf_v1.Policy{ - "default/waf-policy": { - Spec: conf_v1.PolicySpec{ - WAF: &conf_v1.WAF{ - Enable: true, - ApPolicy: "default/dataguard-alarm", - SecurityLog: &conf_v1.SecurityLog{ - Enable: true, - ApLogConf: "default/logconf", - LogDest: "syslog:server=127.0.0.1:514", - }, + Locations: []version2.Location{ + { + Path: "/return", + ProxyInterceptErrors: true, + ErrorPages: []version2.ErrorPage{ + { + Name: "@return_0", + Codes: "418", + ResponseCode: 200, }, }, + InternalProxyPass: "http://unix:/var/lib/nginx/nginx-418-server.sock", }, - }, - context: "route", - expected: policiesCfg{ - WAF: &version2.WAF{ - Enable: "on", - ApPolicy: "/etc/nginx/waf/nac-policies/default-dataguard-alarm", - ApSecurityLogEnable: true, - ApLogConf: []string{"/etc/nginx/waf/nac-logconfs/default-logconf syslog:server=127.0.0.1:514"}, + { + Path: "/internal_location_splits_0_split_0", + ProxyInterceptErrors: true, + ErrorPages: []version2.ErrorPage{ + { + Name: "@return_1", + Codes: "418", + ResponseCode: 200, + }, + }, + InternalProxyPass: "http://unix:/var/lib/nginx/nginx-418-server.sock", + }, + { + Path: "/internal_location_splits_0_split_1", + ProxyInterceptErrors: true, + ErrorPages: []version2.ErrorPage{ + { + Name: "@return_2", + Codes: "418", + ResponseCode: 200, + }, + }, + InternalProxyPass: "http://unix:/var/lib/nginx/nginx-418-server.sock", + }, + { + Path: "/internal_location_matches_0_match_0", + ProxyInterceptErrors: true, + ErrorPages: []version2.ErrorPage{ + { + Name: "@return_3", + Codes: "418", + ResponseCode: 200, + }, + }, + InternalProxyPass: "http://unix:/var/lib/nginx/nginx-418-server.sock", + }, + { + Path: "/internal_location_matches_0_default", + ProxyInterceptErrors: true, + ErrorPages: []version2.ErrorPage{ + { + Name: "@return_4", + Codes: "418", + ResponseCode: 200, + }, + }, + InternalProxyPass: "http://unix:/var/lib/nginx/nginx-418-server.sock", + }, + { + Path: "/more/return", + ProxyInterceptErrors: true, + ErrorPages: []version2.ErrorPage{ + { + Name: "@return_5", + Codes: "418", + ResponseCode: 200, + }, + }, + InternalProxyPass: "http://unix:/var/lib/nginx/nginx-418-server.sock", + }, + { + Path: "/internal_location_splits_1_split_0", + ProxyInterceptErrors: true, + ErrorPages: []version2.ErrorPage{ + { + Name: "@return_6", + Codes: "418", + ResponseCode: 200, + }, + }, + InternalProxyPass: "http://unix:/var/lib/nginx/nginx-418-server.sock", + }, + { + Path: "/internal_location_splits_1_split_1", + ProxyInterceptErrors: true, + ErrorPages: []version2.ErrorPage{ + { + Name: "@return_7", + Codes: "418", + ResponseCode: 200, + }, + }, + InternalProxyPass: "http://unix:/var/lib/nginx/nginx-418-server.sock", + }, + { + Path: "/internal_location_matches_1_match_0", + ProxyInterceptErrors: true, + ErrorPages: []version2.ErrorPage{ + { + Name: "@return_8", + Codes: "418", + ResponseCode: 200, + }, + }, + InternalProxyPass: "http://unix:/var/lib/nginx/nginx-418-server.sock", + }, + { + Path: "/internal_location_matches_1_default", + ProxyInterceptErrors: true, + ErrorPages: []version2.ErrorPage{ + { + Name: "@return_9", + Codes: "418", + ResponseCode: 200, + }, + }, + InternalProxyPass: "http://unix:/var/lib/nginx/nginx-418-server.sock", }, }, - msg: "WAF reference", }, } - vsc := newVirtualServerConfigurator(&ConfigParams{}, false, false, &StaticConfigParams{}, false) + isPlus := false + isResolverConfigured := false + isWildcardEnabled := false + vsc := newVirtualServerConfigurator(&baseCfgParams, isPlus, isResolverConfigured, &StaticConfigParams{}, isWildcardEnabled) - for _, test := range tests { - result := vsc.generatePolicies(ownerDetails, test.policyRefs, test.policies, test.context, policyOpts) - if diff := cmp.Diff(test.expected, result); diff != "" { - t.Errorf("generatePolicies() '%v' mismatch (-want +got):\n%s", test.msg, diff) - } - if len(vsc.warnings) > 0 { - t.Errorf("generatePolicies() returned unexpected warnings %v for the case of %s", vsc.warnings, test.msg) - } + result, warnings := vsc.GenerateVirtualServerConfig(&virtualServerEx, nil, nil) + if !reflect.DeepEqual(result, expected) { + t.Errorf("GenerateVirtualServerConfig returned \n%+v but expected \n%+v", result, expected) + } + + if len(warnings) != 0 { + t.Errorf("GenerateVirtualServerConfig returned warnings: %v", vsc.warnings) } } -func TestGeneratePolicies_GeneratesWAFPolicyOnValidApBundle(t *testing.T) { +func TestGeneratePolicies(t *testing.T) { t.Parallel() - ownerDetails := policyOwnerDetails{ owner: nil, // nil is OK for the unit test ownerNamespace: "default", vsNamespace: "default", vsName: "test", } + mTLSCertPath := "/etc/nginx/secrets/default-ingress-mtls-secret-ca.crt" + mTLSCrlPath := "/etc/nginx/secrets/default-ingress-mtls-secret-ca.crl" + mTLSCertAndCrlPath := fmt.Sprintf("%s %s", mTLSCertPath, mTLSCrlPath) + policyOpts := policyOptions{ + tls: true, + secretRefs: map[string]*secrets.SecretReference{ + "default/ingress-mtls-secret": { + Secret: &api_v1.Secret{ + Type: secrets.SecretTypeCA, + }, + Path: mTLSCertPath, + }, + "default/ingress-mtls-secret-crl": { + Secret: &api_v1.Secret{ + Type: secrets.SecretTypeCA, + Data: map[string][]byte{ + "ca.crl": []byte("base64crl"), + }, + }, + Path: mTLSCertAndCrlPath, + }, + "default/egress-mtls-secret": { + Secret: &api_v1.Secret{ + Type: api_v1.SecretTypeTLS, + }, + Path: "/etc/nginx/secrets/default-egress-mtls-secret", + }, + "default/egress-trusted-ca-secret": { + Secret: &api_v1.Secret{ + Type: secrets.SecretTypeCA, + }, + Path: "/etc/nginx/secrets/default-egress-trusted-ca-secret", + }, + "default/egress-trusted-ca-secret-crl": { + Secret: &api_v1.Secret{ + Type: secrets.SecretTypeCA, + }, + Path: mTLSCertAndCrlPath, + }, + "default/jwt-secret": { + Secret: &api_v1.Secret{ + Type: secrets.SecretTypeJWK, + }, + Path: "/etc/nginx/secrets/default-jwt-secret", + }, + "default/htpasswd-secret": { + Secret: &api_v1.Secret{ + Type: secrets.SecretTypeHtpasswd, + }, + Path: "/etc/nginx/secrets/default-htpasswd-secret", + }, + "default/oidc-secret": { + Secret: &api_v1.Secret{ + Type: secrets.SecretTypeOIDC, + Data: map[string][]byte{ + "client-secret": []byte("super_secret_123"), + }, + }, + }, + }, + apResources: &appProtectResourcesForVS{ + Policies: map[string]string{ + "default/dataguard-alarm": "/etc/nginx/waf/nac-policies/default-dataguard-alarm", + }, + LogConfs: map[string]string{ + "default/logconf": "/etc/nginx/waf/nac-logconfs/default-logconf", + }, + }, + } - test := struct { + tests := []struct { policyRefs []conf_v1.PolicyReference policies map[string]*conf_v1.Policy - policyOpts policyOptions context string - want policiesCfg + expected policiesCfg + msg string }{ - policyRefs: []conf_v1.PolicyReference{ - { - Name: "waf-bundle", - Namespace: "default", + { + policyRefs: []conf_v1.PolicyReference{ + { + Name: "allow-policy", + Namespace: "default", + }, }, - }, - policies: map[string]*conf_v1.Policy{ - "default/waf-bundle": { - Spec: conf_v1.PolicySpec{ - WAF: &conf_v1.WAF{ - Enable: true, - ApBundle: "testWAFPolicyBundle.tgz", + policies: map[string]*conf_v1.Policy{ + "default/allow-policy": { + Spec: conf_v1.PolicySpec{ + AccessControl: &conf_v1.AccessControl{ + Allow: []string{"127.0.0.1"}, + }, }, }, }, + expected: policiesCfg{ + Allow: []string{"127.0.0.1"}, + }, + msg: "explicit reference", }, - context: "route", - } - - vsc := newVirtualServerConfigurator(&ConfigParams{}, false, false, &StaticConfigParams{}, false) - want := policiesCfg{ - WAF: &version2.WAF{ - Enable: "on", - ApBundle: "/etc/nginx/waf/bundles/testWAFPolicyBundle.tgz", - }, - } - got := vsc.generatePolicies(ownerDetails, test.policyRefs, test.policies, test.context, policyOptions{}) - if !cmp.Equal(want, got) { - t.Error(cmp.Diff(want, got)) - } -} - -func TestGeneratePoliciesFails(t *testing.T) { - t.Parallel() - ownerDetails := policyOwnerDetails{ - owner: nil, // nil is OK for the unit test - ownerNamespace: "default", - vsNamespace: "default", - vsName: "test", - } - - dryRunOverride := true - rejectCodeOverride := 505 - - ingressMTLSCertPath := "/etc/nginx/secrets/default-ingress-mtls-secret-ca.crt" - ingressMTLSCrlPath := "/etc/nginx/secrets/default-ingress-mtls-secret-ca.crl" - - tests := []struct { - policyRefs []conf_v1.PolicyReference - policies map[string]*conf_v1.Policy - policyOpts policyOptions - trustedCAFileName string - context string - oidcPolCfg *oidcPolicyCfg - expected policiesCfg - expectedWarnings Warnings - expectedOidc *oidcPolicyCfg - msg string - }{ { policyRefs: []conf_v1.PolicyReference{ { - Name: "allow-policy", - Namespace: "default", + Name: "allow-policy", }, }, - policies: map[string]*conf_v1.Policy{}, - policyOpts: policyOptions{}, - expected: policiesCfg{ - ErrorReturn: &version2.Return{ - Code: 500, + policies: map[string]*conf_v1.Policy{ + "default/allow-policy": { + Spec: conf_v1.PolicySpec{ + AccessControl: &conf_v1.AccessControl{ + Allow: []string{"127.0.0.1"}, + }, + }, }, }, - expectedWarnings: Warnings{ - nil: { - "Policy default/allow-policy is missing or invalid", - }, + expected: policiesCfg{ + Allow: []string{"127.0.0.1"}, }, - expectedOidc: &oidcPolicyCfg{}, - msg: "missing policy", + msg: "implicit reference", }, { policyRefs: []conf_v1.PolicyReference{ { - Name: "allow-policy", + Name: "allow-policy-1", }, { - Name: "deny-policy", + Name: "allow-policy-2", }, }, policies: map[string]*conf_v1.Policy{ - "default/allow-policy": { + "default/allow-policy-1": { Spec: conf_v1.PolicySpec{ AccessControl: &conf_v1.AccessControl{ Allow: []string{"127.0.0.1"}, }, }, }, - "default/deny-policy": { + "default/allow-policy-2": { Spec: conf_v1.PolicySpec{ AccessControl: &conf_v1.AccessControl{ - Deny: []string{"127.0.0.2"}, + Allow: []string{"127.0.0.2"}, }, }, }, }, - policyOpts: policyOptions{}, expected: policiesCfg{ - Allow: []string{"127.0.0.1"}, - Deny: []string{"127.0.0.2"}, + Allow: []string{"127.0.0.1", "127.0.0.2"}, }, - expectedWarnings: Warnings{ - nil: { - "AccessControl policy (or policies) with deny rules is overridden by policy (or policies) with allow rules", + msg: "merging", + }, + { + policyRefs: []conf_v1.PolicyReference{ + { + Name: "rateLimit-policy", + Namespace: "default", }, }, - expectedOidc: &oidcPolicyCfg{}, - msg: "conflicting policies", + policies: map[string]*conf_v1.Policy{ + "default/rateLimit-policy": { + Spec: conf_v1.PolicySpec{ + RateLimit: &conf_v1.RateLimit{ + Key: "test", + ZoneSize: "10M", + Rate: "10r/s", + LogLevel: "notice", + }, + }, + }, + }, + expected: policiesCfg{ + LimitReqZones: []version2.LimitReqZone{ + { + Key: "test", + ZoneSize: "10M", + Rate: "10r/s", + ZoneName: "pol_rl_default_rateLimit-policy_default_test", + }, + }, + LimitReqOptions: version2.LimitReqOptions{ + LogLevel: "notice", + RejectCode: 503, + }, + LimitReqs: []version2.LimitReq{ + { + ZoneName: "pol_rl_default_rateLimit-policy_default_test", + }, + }, + }, + msg: "rate limit reference", }, { policyRefs: []conf_v1.PolicyReference{ @@ -3705,17 +3903,13 @@ func TestGeneratePoliciesFails(t *testing.T) { "default/rateLimit-policy2": { Spec: conf_v1.PolicySpec{ RateLimit: &conf_v1.RateLimit{ - Key: "test2", - ZoneSize: "20M", - Rate: "20r/s", - DryRun: &dryRunOverride, - LogLevel: "info", - RejectCode: &rejectCodeOverride, + Key: "test2", + ZoneSize: "20M", + Rate: "20r/s", }, }, }, }, - policyOpts: policyOptions{}, expected: policiesCfg{ LimitReqZones: []version2.LimitReqZone{ { @@ -3744,15 +3938,7 @@ func TestGeneratePoliciesFails(t *testing.T) { }, }, }, - expectedWarnings: Warnings{ - nil: { - `RateLimit policy default/rateLimit-policy2 with limit request option dryRun='true' is overridden to dryRun='false' by the first policy reference in this context`, - `RateLimit policy default/rateLimit-policy2 with limit request option logLevel='info' is overridden to logLevel='error' by the first policy reference in this context`, - `RateLimit policy default/rateLimit-policy2 with limit request option rejectCode='505' is overridden to rejectCode='503' by the first policy reference in this context`, - }, - }, - expectedOidc: &oidcPolicyCfg{}, - msg: "rate limit policy limit request option override", + msg: "multi rate limit reference", }, { policyRefs: []conf_v1.PolicyReference{ @@ -3769,475 +3955,455 @@ func TestGeneratePoliciesFails(t *testing.T) { }, Spec: conf_v1.PolicySpec{ JWTAuth: &conf_v1.JWTAuth{ - Realm: "test", + Realm: "My Test API", Secret: "jwt-secret", }, }, }, }, - policyOpts: policyOptions{ - secretRefs: map[string]*secrets.SecretReference{ - "default/jwt-secret": { - Secret: &api_v1.Secret{ - Type: secrets.SecretTypeJWK, - }, - Error: errors.New("secret is invalid"), - }, - }, - }, expected: policiesCfg{ - ErrorReturn: &version2.Return{ - Code: 500, - }, - }, - expectedWarnings: Warnings{ - nil: { - `JWT policy default/jwt-policy references an invalid secret default/jwt-secret: secret is invalid`, + JWTAuth: &version2.JWTAuth{ + Secret: "/etc/nginx/secrets/default-jwt-secret", + Realm: "My Test API", }, }, - expectedOidc: &oidcPolicyCfg{}, - msg: "jwt reference missing secret", + msg: "jwt reference", }, { policyRefs: []conf_v1.PolicyReference{ { - Name: "jwt-policy", + Name: "jwt-policy-2", Namespace: "default", }, }, policies: map[string]*conf_v1.Policy{ - "default/jwt-policy": { + "default/jwt-policy-2": { ObjectMeta: meta_v1.ObjectMeta{ Name: "jwt-policy", Namespace: "default", }, Spec: conf_v1.PolicySpec{ JWTAuth: &conf_v1.JWTAuth{ - Realm: "test", - Secret: "jwt-secret", - }, - }, - }, - }, - policyOpts: policyOptions{ - secretRefs: map[string]*secrets.SecretReference{ - "default/jwt-secret": { - Secret: &api_v1.Secret{ - Type: secrets.SecretTypeCA, + Realm: "My Test API", + JwksURI: "https://idp.example.com:443/keys", + KeyCache: "1h", }, }, }, }, expected: policiesCfg{ - ErrorReturn: &version2.Return{ - Code: 500, - }, - }, - expectedWarnings: Warnings{ - nil: { - `JWT policy default/jwt-policy references a secret default/jwt-secret of a wrong type 'nginx.org/ca', must be 'nginx.org/jwk'`, + JWTAuth: &version2.JWTAuth{ + Realm: "My Test API", + JwksURI: version2.JwksURI{ + JwksScheme: "https", + JwksHost: "idp.example.com", + JwksPort: "443", + JwksPath: "/keys", + }, + KeyCache: "1h", }, }, - expectedOidc: &oidcPolicyCfg{}, - msg: "jwt references wrong secret type", + msg: "Basic jwks example", }, { policyRefs: []conf_v1.PolicyReference{ { - Name: "jwt-policy", - Namespace: "default", - }, - { - Name: "jwt-policy2", + Name: "jwt-policy-2", Namespace: "default", }, }, policies: map[string]*conf_v1.Policy{ - "default/jwt-policy": { + "default/jwt-policy-2": { ObjectMeta: meta_v1.ObjectMeta{ Name: "jwt-policy", Namespace: "default", }, Spec: conf_v1.PolicySpec{ JWTAuth: &conf_v1.JWTAuth{ - Realm: "test", - Secret: "jwt-secret", + Realm: "My Test API", + JwksURI: "https://idp.example.com/keys", + KeyCache: "1h", }, }, }, - "default/jwt-policy2": { + }, + expected: policiesCfg{ + JWTAuth: &version2.JWTAuth{ + Realm: "My Test API", + JwksURI: version2.JwksURI{ + JwksScheme: "https", + JwksHost: "idp.example.com", + JwksPort: "", + JwksPath: "/keys", + }, + KeyCache: "1h", + }, + }, + msg: "Basic jwks example, no port in JwksURI", + }, + { + policyRefs: []conf_v1.PolicyReference{ + { + Name: "basic-auth-policy", + Namespace: "default", + }, + }, + policies: map[string]*conf_v1.Policy{ + "default/basic-auth-policy": { ObjectMeta: meta_v1.ObjectMeta{ - Name: "jwt-policy2", + Name: "basic-auth-policy", Namespace: "default", }, Spec: conf_v1.PolicySpec{ - JWTAuth: &conf_v1.JWTAuth{ - Realm: "test", - Secret: "jwt-secret2", - }, - }, - }, - }, - policyOpts: policyOptions{ - secretRefs: map[string]*secrets.SecretReference{ - "default/jwt-secret": { - Secret: &api_v1.Secret{ - Type: secrets.SecretTypeJWK, - }, - Path: "/etc/nginx/secrets/default-jwt-secret", - }, - "default/jwt-secret2": { - Secret: &api_v1.Secret{ - Type: secrets.SecretTypeJWK, + BasicAuth: &conf_v1.BasicAuth{ + Realm: "My Test API", + Secret: "htpasswd-secret", }, - Path: "/etc/nginx/secrets/default-jwt-secret2", }, }, }, expected: policiesCfg{ - JWTAuth: &version2.JWTAuth{ - Secret: "/etc/nginx/secrets/default-jwt-secret", - Realm: "test", - }, - }, - expectedWarnings: Warnings{ - nil: { - `Multiple jwt policies in the same context is not valid. JWT policy default/jwt-policy2 will be ignored`, + BasicAuth: &version2.BasicAuth{ + Secret: "/etc/nginx/secrets/default-htpasswd-secret", + Realm: "My Test API", }, }, - expectedOidc: &oidcPolicyCfg{}, - msg: "multi jwt reference", + msg: "basic auth reference", }, { policyRefs: []conf_v1.PolicyReference{ { - Name: "basic-auth-policy", + Name: "ingress-mtls-policy", Namespace: "default", }, }, policies: map[string]*conf_v1.Policy{ - "default/basic-auth-policy": { + "default/ingress-mtls-policy": { ObjectMeta: meta_v1.ObjectMeta{ - Name: "basic-auth-policy", + Name: "ingress-mtls-policy", Namespace: "default", }, Spec: conf_v1.PolicySpec{ - BasicAuth: &conf_v1.BasicAuth{ - Realm: "test", - Secret: "htpasswd-secret", - }, - }, - }, - }, - policyOpts: policyOptions{ - secretRefs: map[string]*secrets.SecretReference{ - "default/htpasswd-secret": { - Secret: &api_v1.Secret{ - Type: secrets.SecretTypeHtpasswd, + IngressMTLS: &conf_v1.IngressMTLS{ + ClientCertSecret: "ingress-mtls-secret", + VerifyClient: "off", }, - Error: errors.New("secret is invalid"), }, }, }, + context: "spec", expected: policiesCfg{ - ErrorReturn: &version2.Return{ - Code: 500, - }, - }, - expectedWarnings: Warnings{ - nil: { - `Basic Auth policy default/basic-auth-policy references an invalid secret default/htpasswd-secret: secret is invalid`, + IngressMTLS: &version2.IngressMTLS{ + ClientCert: mTLSCertPath, + VerifyClient: "off", + VerifyDepth: 1, }, }, - expectedOidc: &oidcPolicyCfg{}, - msg: "basic auth reference missing secret", + msg: "ingressMTLS reference", }, { policyRefs: []conf_v1.PolicyReference{ { - Name: "basic-auth-policy", + Name: "ingress-mtls-policy-crl", Namespace: "default", }, }, policies: map[string]*conf_v1.Policy{ - "default/basic-auth-policy": { + "default/ingress-mtls-policy-crl": { ObjectMeta: meta_v1.ObjectMeta{ - Name: "basic-auth-policy", + Name: "ingress-mtls-policy-crl", Namespace: "default", }, Spec: conf_v1.PolicySpec{ - BasicAuth: &conf_v1.BasicAuth{ - Realm: "test", - Secret: "htpasswd-secret", - }, - }, - }, - }, - policyOpts: policyOptions{ - secretRefs: map[string]*secrets.SecretReference{ - "default/htpasswd-secret": { - Secret: &api_v1.Secret{ - Type: secrets.SecretTypeCA, + IngressMTLS: &conf_v1.IngressMTLS{ + ClientCertSecret: "ingress-mtls-secret-crl", + VerifyClient: "off", }, }, }, }, + context: "spec", expected: policiesCfg{ - ErrorReturn: &version2.Return{ - Code: 500, - }, - }, - expectedWarnings: Warnings{ - nil: { - `Basic Auth policy default/basic-auth-policy references a secret default/htpasswd-secret of a wrong type 'nginx.org/ca', must be 'nginx.org/htpasswd'`, + IngressMTLS: &version2.IngressMTLS{ + ClientCert: mTLSCertPath, + ClientCrl: mTLSCrlPath, + VerifyClient: "off", + VerifyDepth: 1, }, }, - expectedOidc: &oidcPolicyCfg{}, - msg: "basic auth references wrong secret type", + msg: "ingressMTLS reference with ca.crl field in secret", }, { policyRefs: []conf_v1.PolicyReference{ { - Name: "basic-auth-policy", - Namespace: "default", - }, - { - Name: "basic-auth-policy2", + Name: "ingress-mtls-policy-crl", Namespace: "default", }, }, policies: map[string]*conf_v1.Policy{ - "default/basic-auth-policy": { + "default/ingress-mtls-policy-crl": { ObjectMeta: meta_v1.ObjectMeta{ - Name: "basic-auth-policy", + Name: "ingress-mtls-policy-crl", Namespace: "default", }, Spec: conf_v1.PolicySpec{ - BasicAuth: &conf_v1.BasicAuth{ - Realm: "test", - Secret: "htpasswd-secret", + IngressMTLS: &conf_v1.IngressMTLS{ + ClientCertSecret: "ingress-mtls-secret", + CrlFileName: "default-ingress-mtls-secret-ca.crl", + VerifyClient: "off", }, }, }, - "default/basic-auth-policy2": { - ObjectMeta: meta_v1.ObjectMeta{ - Name: "basic-auth-policy2", - Namespace: "default", - }, - Spec: conf_v1.PolicySpec{ - BasicAuth: &conf_v1.BasicAuth{ - Realm: "test", - Secret: "htpasswd-secret2", - }, - }, + }, + context: "spec", + expected: policiesCfg{ + IngressMTLS: &version2.IngressMTLS{ + ClientCert: mTLSCertPath, + ClientCrl: mTLSCrlPath, + VerifyClient: "off", + VerifyDepth: 1, }, }, - policyOpts: policyOptions{ - secretRefs: map[string]*secrets.SecretReference{ - "default/htpasswd-secret": { - Secret: &api_v1.Secret{ - Type: secrets.SecretTypeHtpasswd, - }, - Path: "/etc/nginx/secrets/default-htpasswd-secret", - }, - "default/htpasswd-secret2": { - Secret: &api_v1.Secret{ - Type: secrets.SecretTypeHtpasswd, + msg: "ingressMTLS reference with crl field in policy", + }, + { + policyRefs: []conf_v1.PolicyReference{ + { + Name: "egress-mtls-policy", + Namespace: "default", + }, + }, + policies: map[string]*conf_v1.Policy{ + "default/egress-mtls-policy": { + Spec: conf_v1.PolicySpec{ + EgressMTLS: &conf_v1.EgressMTLS{ + TLSSecret: "egress-mtls-secret", + ServerName: true, + SessionReuse: createPointerFromBool(false), + TrustedCertSecret: "egress-trusted-ca-secret", }, - Path: "/etc/nginx/secrets/default-htpasswd-secret2", }, }, }, + context: "route", expected: policiesCfg{ - BasicAuth: &version2.BasicAuth{ - Secret: "/etc/nginx/secrets/default-htpasswd-secret", - Realm: "test", - }, - }, - expectedWarnings: Warnings{ - nil: { - `Multiple basic auth policies in the same context is not valid. Basic auth policy default/basic-auth-policy2 will be ignored`, + EgressMTLS: &version2.EgressMTLS{ + Certificate: "/etc/nginx/secrets/default-egress-mtls-secret", + CertificateKey: "/etc/nginx/secrets/default-egress-mtls-secret", + Ciphers: "DEFAULT", + Protocols: "TLSv1 TLSv1.1 TLSv1.2", + ServerName: true, + SessionReuse: false, + VerifyDepth: 1, + VerifyServer: false, + TrustedCert: "/etc/nginx/secrets/default-egress-trusted-ca-secret", + SSLName: "$proxy_host", }, }, - expectedOidc: &oidcPolicyCfg{}, - msg: "multi basic auth reference", + msg: "egressMTLS reference", }, { policyRefs: []conf_v1.PolicyReference{ { - Name: "ingress-mtls-policy", + Name: "egress-mtls-policy", Namespace: "default", }, }, policies: map[string]*conf_v1.Policy{ - "default/ingress-mtls-policy": { - ObjectMeta: meta_v1.ObjectMeta{ - Name: "ingress-mtls-policy", - Namespace: "default", - }, + "default/egress-mtls-policy": { Spec: conf_v1.PolicySpec{ - IngressMTLS: &conf_v1.IngressMTLS{ - ClientCertSecret: "ingress-mtls-secret", + EgressMTLS: &conf_v1.EgressMTLS{ + TLSSecret: "egress-mtls-secret", + ServerName: true, + SessionReuse: createPointerFromBool(false), + TrustedCertSecret: "egress-trusted-ca-secret-crl", }, }, }, }, - policyOpts: policyOptions{ - tls: true, - secretRefs: map[string]*secrets.SecretReference{ - "default/ingress-mtls-secret": { - Error: errors.New("secret is invalid"), - }, - }, - }, - context: "spec", + context: "route", expected: policiesCfg{ - ErrorReturn: &version2.Return{ - Code: 500, - }, - }, - expectedWarnings: Warnings{ - nil: { - `IngressMTLS policy "default/ingress-mtls-policy" references an invalid secret default/ingress-mtls-secret: secret is invalid`, + EgressMTLS: &version2.EgressMTLS{ + Certificate: "/etc/nginx/secrets/default-egress-mtls-secret", + CertificateKey: "/etc/nginx/secrets/default-egress-mtls-secret", + Ciphers: "DEFAULT", + Protocols: "TLSv1 TLSv1.1 TLSv1.2", + ServerName: true, + SessionReuse: false, + VerifyDepth: 1, + VerifyServer: false, + TrustedCert: mTLSCertPath, + SSLName: "$proxy_host", }, }, - expectedOidc: &oidcPolicyCfg{}, - msg: "ingress mtls reference an invalid secret", + msg: "egressMTLS with crt and crl", }, { policyRefs: []conf_v1.PolicyReference{ { - Name: "ingress-mtls-policy", + Name: "oidc-policy", Namespace: "default", }, }, policies: map[string]*conf_v1.Policy{ - "default/ingress-mtls-policy": { + "default/oidc-policy": { ObjectMeta: meta_v1.ObjectMeta{ - Name: "ingress-mtls-policy", + Name: "oidc-policy", Namespace: "default", }, Spec: conf_v1.PolicySpec{ - IngressMTLS: &conf_v1.IngressMTLS{ - ClientCertSecret: "ingress-mtls-secret", - }, - }, - }, - }, - policyOpts: policyOptions{ - tls: true, - secretRefs: map[string]*secrets.SecretReference{ - "default/ingress-mtls-secret": { - Secret: &api_v1.Secret{ - Type: api_v1.SecretTypeTLS, + OIDC: &conf_v1.OIDC{ + AuthEndpoint: "http://example.com/auth", + TokenEndpoint: "http://example.com/token", + JWKSURI: "http://example.com/jwks", + ClientID: "client-id", + ClientSecret: "oidc-secret", + Scope: "scope", + RedirectURI: "/redirect", + ZoneSyncLeeway: createPointerFromInt(20), + AccessTokenEnable: true, }, }, }, }, - context: "spec", expected: policiesCfg{ - ErrorReturn: &version2.Return{ - Code: 500, - }, - }, - expectedWarnings: Warnings{ - nil: { - `IngressMTLS policy default/ingress-mtls-policy references a secret default/ingress-mtls-secret of a wrong type 'kubernetes.io/tls', must be 'nginx.org/ca'`, - }, + OIDC: true, }, - expectedOidc: &oidcPolicyCfg{}, - msg: "ingress mtls references wrong secret type", + msg: "oidc reference", }, { policyRefs: []conf_v1.PolicyReference{ { - Name: "ingress-mtls-policy", - Namespace: "default", - }, - { - Name: "ingress-mtls-policy2", + Name: "waf-policy", Namespace: "default", }, }, policies: map[string]*conf_v1.Policy{ - "default/ingress-mtls-policy": { - ObjectMeta: meta_v1.ObjectMeta{ - Name: "ingress-mtls-policy", - Namespace: "default", - }, - Spec: conf_v1.PolicySpec{ - IngressMTLS: &conf_v1.IngressMTLS{ - ClientCertSecret: "ingress-mtls-secret", - }, - }, - }, - "default/ingress-mtls-policy2": { + "default/waf-policy": { Spec: conf_v1.PolicySpec{ - IngressMTLS: &conf_v1.IngressMTLS{ - ClientCertSecret: "ingress-mtls-secret2", - }, - }, - }, - }, - policyOpts: policyOptions{ - tls: true, - secretRefs: map[string]*secrets.SecretReference{ - "default/ingress-mtls-secret": { - Secret: &api_v1.Secret{ - Type: secrets.SecretTypeCA, + WAF: &conf_v1.WAF{ + Enable: true, + ApPolicy: "default/dataguard-alarm", + SecurityLog: &conf_v1.SecurityLog{ + Enable: true, + ApLogConf: "default/logconf", + LogDest: "syslog:server=127.0.0.1:514", + }, }, - Path: ingressMTLSCertPath, }, }, }, - context: "spec", + context: "route", expected: policiesCfg{ - IngressMTLS: &version2.IngressMTLS{ - ClientCert: ingressMTLSCertPath, - VerifyClient: "on", - VerifyDepth: 1, + WAF: &version2.WAF{ + Enable: "on", + ApPolicy: "/etc/nginx/waf/nac-policies/default-dataguard-alarm", + ApSecurityLogEnable: true, + ApLogConf: []string{"/etc/nginx/waf/nac-logconfs/default-logconf syslog:server=127.0.0.1:514"}, }, }, - expectedWarnings: Warnings{ - nil: { - `Multiple ingressMTLS policies are not allowed. IngressMTLS policy default/ingress-mtls-policy2 will be ignored`, + msg: "WAF reference", + }, + } + + vsc := newVirtualServerConfigurator(&ConfigParams{}, false, false, &StaticConfigParams{}, false) + + for _, test := range tests { + result := vsc.generatePolicies(ownerDetails, test.policyRefs, test.policies, test.context, policyOpts) + if diff := cmp.Diff(test.expected, result); diff != "" { + t.Errorf("generatePolicies() '%v' mismatch (-want +got):\n%s", test.msg, diff) + } + if len(vsc.warnings) > 0 { + t.Errorf("generatePolicies() returned unexpected warnings %v for the case of %s", vsc.warnings, test.msg) + } + } +} + +func TestGeneratePolicies_GeneratesWAFPolicyOnValidApBundle(t *testing.T) { + t.Parallel() + + ownerDetails := policyOwnerDetails{ + owner: nil, // nil is OK for the unit test + ownerNamespace: "default", + vsNamespace: "default", + vsName: "test", + } + + test := struct { + policyRefs []conf_v1.PolicyReference + policies map[string]*conf_v1.Policy + policyOpts policyOptions + context string + want policiesCfg + }{ + policyRefs: []conf_v1.PolicyReference{ + { + Name: "waf-bundle", + Namespace: "default", + }, + }, + policies: map[string]*conf_v1.Policy{ + "default/waf-bundle": { + Spec: conf_v1.PolicySpec{ + WAF: &conf_v1.WAF{ + Enable: true, + ApBundle: "testWAFPolicyBundle.tgz", + }, }, }, - expectedOidc: &oidcPolicyCfg{}, - msg: "multi ingress mtls", }, + context: "route", + } + + vsc := newVirtualServerConfigurator(&ConfigParams{}, false, false, &StaticConfigParams{}, false) + want := policiesCfg{ + WAF: &version2.WAF{ + Enable: "on", + ApBundle: "/etc/nginx/waf/bundles/testWAFPolicyBundle.tgz", + }, + } + got := vsc.generatePolicies(ownerDetails, test.policyRefs, test.policies, test.context, policyOptions{}) + if !cmp.Equal(want, got) { + t.Error(cmp.Diff(want, got)) + } +} + +func TestGeneratePoliciesFails(t *testing.T) { + t.Parallel() + ownerDetails := policyOwnerDetails{ + owner: nil, // nil is OK for the unit test + ownerNamespace: "default", + vsNamespace: "default", + vsName: "test", + } + + dryRunOverride := true + rejectCodeOverride := 505 + + ingressMTLSCertPath := "/etc/nginx/secrets/default-ingress-mtls-secret-ca.crt" + ingressMTLSCrlPath := "/etc/nginx/secrets/default-ingress-mtls-secret-ca.crl" + + tests := []struct { + policyRefs []conf_v1.PolicyReference + policies map[string]*conf_v1.Policy + policyOpts policyOptions + trustedCAFileName string + context string + oidcPolCfg *oidcPolicyCfg + expected policiesCfg + expectedWarnings Warnings + expectedOidc *oidcPolicyCfg + msg string + }{ { policyRefs: []conf_v1.PolicyReference{ { - Name: "ingress-mtls-policy", + Name: "allow-policy", Namespace: "default", }, }, - policies: map[string]*conf_v1.Policy{ - "default/ingress-mtls-policy": { - ObjectMeta: meta_v1.ObjectMeta{ - Name: "ingress-mtls-policy", - Namespace: "default", - }, - Spec: conf_v1.PolicySpec{ - IngressMTLS: &conf_v1.IngressMTLS{ - ClientCertSecret: "ingress-mtls-secret", - }, - }, - }, - }, - policyOpts: policyOptions{ - tls: true, - secretRefs: map[string]*secrets.SecretReference{ - "default/ingress-mtls-secret": { - Secret: &api_v1.Secret{ - Type: secrets.SecretTypeCA, - }, - Path: ingressMTLSCertPath, - }, - }, - }, - context: "route", + policies: map[string]*conf_v1.Policy{}, + policyOpts: policyOptions{}, expected: policiesCfg{ ErrorReturn: &version2.Return{ Code: 500, @@ -4245,208 +4411,197 @@ func TestGeneratePoliciesFails(t *testing.T) { }, expectedWarnings: Warnings{ nil: { - `IngressMTLS policy default/ingress-mtls-policy is not allowed in the route context`, + "Policy default/allow-policy is missing or invalid", }, }, expectedOidc: &oidcPolicyCfg{}, - msg: "ingress mtls in the wrong context", + msg: "missing policy", }, { policyRefs: []conf_v1.PolicyReference{ { - Name: "ingress-mtls-policy", - Namespace: "default", + Name: "allow-policy", + }, + { + Name: "deny-policy", }, }, policies: map[string]*conf_v1.Policy{ - "default/ingress-mtls-policy": { - ObjectMeta: meta_v1.ObjectMeta{ - Name: "ingress-mtls-policy", - Namespace: "default", - }, + "default/allow-policy": { Spec: conf_v1.PolicySpec{ - IngressMTLS: &conf_v1.IngressMTLS{ - ClientCertSecret: "ingress-mtls-secret", + AccessControl: &conf_v1.AccessControl{ + Allow: []string{"127.0.0.1"}, }, }, }, - }, - policyOpts: policyOptions{ - tls: false, - secretRefs: map[string]*secrets.SecretReference{ - "default/ingress-mtls-secret": { - Secret: &api_v1.Secret{ - Type: secrets.SecretTypeCA, + "default/deny-policy": { + Spec: conf_v1.PolicySpec{ + AccessControl: &conf_v1.AccessControl{ + Deny: []string{"127.0.0.2"}, }, - Path: ingressMTLSCertPath, }, }, }, - context: "route", + policyOpts: policyOptions{}, expected: policiesCfg{ - ErrorReturn: &version2.Return{ - Code: 500, - }, + Allow: []string{"127.0.0.1"}, + Deny: []string{"127.0.0.2"}, }, expectedWarnings: Warnings{ nil: { - `TLS must be enabled in VirtualServer for IngressMTLS policy default/ingress-mtls-policy`, + "AccessControl policy (or policies) with deny rules is overridden by policy (or policies) with allow rules", }, }, expectedOidc: &oidcPolicyCfg{}, - msg: "ingress mtls missing TLS config", + msg: "conflicting policies", }, { policyRefs: []conf_v1.PolicyReference{ { - Name: "ingress-mtls-policy", + Name: "rateLimit-policy", + Namespace: "default", + }, + { + Name: "rateLimit-policy2", Namespace: "default", }, }, policies: map[string]*conf_v1.Policy{ - "default/ingress-mtls-policy": { - ObjectMeta: meta_v1.ObjectMeta{ - Name: "ingress-mtls-policy", - Namespace: "default", - }, + "default/rateLimit-policy": { Spec: conf_v1.PolicySpec{ - IngressMTLS: &conf_v1.IngressMTLS{ - ClientCertSecret: "ingress-mtls-secret", - CrlFileName: "default-ingress-mtls-secret-ca.crl", + RateLimit: &conf_v1.RateLimit{ + Key: "test", + ZoneSize: "10M", + Rate: "10r/s", }, }, }, - }, - policyOpts: policyOptions{ - tls: true, - secretRefs: map[string]*secrets.SecretReference{ - "default/ingress-mtls-secret": { - Secret: &api_v1.Secret{ - Type: secrets.SecretTypeCA, - Data: map[string][]byte{ - "ca.crl": []byte("base64crl"), - }, + "default/rateLimit-policy2": { + Spec: conf_v1.PolicySpec{ + RateLimit: &conf_v1.RateLimit{ + Key: "test2", + ZoneSize: "20M", + Rate: "20r/s", + DryRun: &dryRunOverride, + LogLevel: "info", + RejectCode: &rejectCodeOverride, }, - Path: ingressMTLSCertPath, }, }, }, - context: "spec", + policyOpts: policyOptions{}, expected: policiesCfg{ - IngressMTLS: &version2.IngressMTLS{ - ClientCert: ingressMTLSCertPath, - ClientCrl: ingressMTLSCrlPath, - VerifyClient: "on", - VerifyDepth: 1, + LimitReqZones: []version2.LimitReqZone{ + { + Key: "test", + ZoneSize: "10M", + Rate: "10r/s", + ZoneName: "pol_rl_default_rateLimit-policy_default_test", + }, + { + Key: "test2", + ZoneSize: "20M", + Rate: "20r/s", + ZoneName: "pol_rl_default_rateLimit-policy2_default_test", + }, + }, + LimitReqOptions: version2.LimitReqOptions{ + LogLevel: "error", + RejectCode: 503, + }, + LimitReqs: []version2.LimitReq{ + { + ZoneName: "pol_rl_default_rateLimit-policy_default_test", + }, + { + ZoneName: "pol_rl_default_rateLimit-policy2_default_test", + }, }, - ErrorReturn: nil, }, expectedWarnings: Warnings{ nil: { - `Both ca.crl in the Secret and ingressMTLS.crlFileName fields cannot be used. ca.crl in default/ingress-mtls-secret will be ignored and default/ingress-mtls-policy will be applied`, + `RateLimit policy default/rateLimit-policy2 with limit request option dryRun='true' is overridden to dryRun='false' by the first policy reference in this context`, + `RateLimit policy default/rateLimit-policy2 with limit request option logLevel='info' is overridden to logLevel='error' by the first policy reference in this context`, + `RateLimit policy default/rateLimit-policy2 with limit request option rejectCode='505' is overridden to rejectCode='503' by the first policy reference in this context`, }, }, expectedOidc: &oidcPolicyCfg{}, - msg: "ingress mtls ca.crl and ingressMTLS.Crl set", + msg: "rate limit policy limit request option override", }, { policyRefs: []conf_v1.PolicyReference{ { - Name: "egress-mtls-policy", - Namespace: "default", - }, - { - Name: "egress-mtls-policy2", + Name: "jwt-policy", Namespace: "default", }, }, policies: map[string]*conf_v1.Policy{ - "default/egress-mtls-policy": { - ObjectMeta: meta_v1.ObjectMeta{ - Name: "egress-mtls-policy", - Namespace: "default", - }, - Spec: conf_v1.PolicySpec{ - EgressMTLS: &conf_v1.EgressMTLS{ - TLSSecret: "egress-mtls-secret", - }, - }, - }, - "default/egress-mtls-policy2": { + "default/jwt-policy": { ObjectMeta: meta_v1.ObjectMeta{ - Name: "egress-mtls-policy2", + Name: "jwt-policy", Namespace: "default", }, Spec: conf_v1.PolicySpec{ - EgressMTLS: &conf_v1.EgressMTLS{ - TLSSecret: "egress-mtls-secret2", + JWTAuth: &conf_v1.JWTAuth{ + Realm: "test", + Secret: "jwt-secret", }, }, }, }, policyOpts: policyOptions{ secretRefs: map[string]*secrets.SecretReference{ - "default/egress-mtls-secret": { + "default/jwt-secret": { Secret: &api_v1.Secret{ - Type: api_v1.SecretTypeTLS, + Type: secrets.SecretTypeJWK, }, - Path: "/etc/nginx/secrets/default-egress-mtls-secret", + Error: errors.New("secret is invalid"), }, }, }, - context: "route", expected: policiesCfg{ - EgressMTLS: &version2.EgressMTLS{ - Certificate: "/etc/nginx/secrets/default-egress-mtls-secret", - CertificateKey: "/etc/nginx/secrets/default-egress-mtls-secret", - VerifyServer: false, - VerifyDepth: 1, - Ciphers: "DEFAULT", - Protocols: "TLSv1 TLSv1.1 TLSv1.2", - SessionReuse: true, - SSLName: "$proxy_host", + ErrorReturn: &version2.Return{ + Code: 500, }, }, expectedWarnings: Warnings{ nil: { - `Multiple egressMTLS policies in the same context is not valid. EgressMTLS policy default/egress-mtls-policy2 will be ignored`, + `JWT policy default/jwt-policy references an invalid secret default/jwt-secret: secret is invalid`, }, }, expectedOidc: &oidcPolicyCfg{}, - msg: "multi egress mtls", + msg: "jwt reference missing secret", }, { policyRefs: []conf_v1.PolicyReference{ { - Name: "egress-mtls-policy", + Name: "jwt-policy", Namespace: "default", }, }, policies: map[string]*conf_v1.Policy{ - "default/egress-mtls-policy": { + "default/jwt-policy": { ObjectMeta: meta_v1.ObjectMeta{ - Name: "egress-mtls-policy", + Name: "jwt-policy", Namespace: "default", }, Spec: conf_v1.PolicySpec{ - EgressMTLS: &conf_v1.EgressMTLS{ - TrustedCertSecret: "egress-trusted-secret", - SSLName: "foo.com", + JWTAuth: &conf_v1.JWTAuth{ + Realm: "test", + Secret: "jwt-secret", }, }, }, }, policyOpts: policyOptions{ secretRefs: map[string]*secrets.SecretReference{ - "default/egress-trusted-secret": { + "default/jwt-secret": { Secret: &api_v1.Secret{ Type: secrets.SecretTypeCA, }, - Error: errors.New("secret is invalid"), }, }, }, - context: "route", expected: policiesCfg{ ErrorReturn: &version2.Return{ Code: 500, @@ -4454,87 +4609,110 @@ func TestGeneratePoliciesFails(t *testing.T) { }, expectedWarnings: Warnings{ nil: { - `EgressMTLS policy default/egress-mtls-policy references an invalid secret default/egress-trusted-secret: secret is invalid`, + `JWT policy default/jwt-policy references a secret default/jwt-secret of a wrong type 'nginx.org/ca', must be 'nginx.org/jwk'`, }, }, expectedOidc: &oidcPolicyCfg{}, - msg: "egress mtls referencing an invalid CA secret", + msg: "jwt references wrong secret type", }, { policyRefs: []conf_v1.PolicyReference{ { - Name: "egress-mtls-policy", + Name: "jwt-policy", + Namespace: "default", + }, + { + Name: "jwt-policy2", Namespace: "default", }, }, policies: map[string]*conf_v1.Policy{ - "default/egress-mtls-policy": { + "default/jwt-policy": { ObjectMeta: meta_v1.ObjectMeta{ - Name: "egress-mtls-policy", + Name: "jwt-policy", Namespace: "default", }, Spec: conf_v1.PolicySpec{ - EgressMTLS: &conf_v1.EgressMTLS{ - TLSSecret: "egress-mtls-secret", - SSLName: "foo.com", + JWTAuth: &conf_v1.JWTAuth{ + Realm: "test", + Secret: "jwt-secret", + }, + }, + }, + "default/jwt-policy2": { + ObjectMeta: meta_v1.ObjectMeta{ + Name: "jwt-policy2", + Namespace: "default", + }, + Spec: conf_v1.PolicySpec{ + JWTAuth: &conf_v1.JWTAuth{ + Realm: "test", + Secret: "jwt-secret2", }, }, }, }, policyOpts: policyOptions{ secretRefs: map[string]*secrets.SecretReference{ - "default/egress-mtls-secret": { + "default/jwt-secret": { Secret: &api_v1.Secret{ - Type: secrets.SecretTypeCA, + Type: secrets.SecretTypeJWK, + }, + Path: "/etc/nginx/secrets/default-jwt-secret", + }, + "default/jwt-secret2": { + Secret: &api_v1.Secret{ + Type: secrets.SecretTypeJWK, }, + Path: "/etc/nginx/secrets/default-jwt-secret2", }, }, }, - context: "route", expected: policiesCfg{ - ErrorReturn: &version2.Return{ - Code: 500, + JWTAuth: &version2.JWTAuth{ + Secret: "/etc/nginx/secrets/default-jwt-secret", + Realm: "test", }, }, expectedWarnings: Warnings{ nil: { - `EgressMTLS policy default/egress-mtls-policy references a secret default/egress-mtls-secret of a wrong type 'nginx.org/ca', must be 'kubernetes.io/tls'`, + `Multiple jwt policies in the same context is not valid. JWT policy default/jwt-policy2 will be ignored`, }, }, expectedOidc: &oidcPolicyCfg{}, - msg: "egress mtls referencing wrong secret type", + msg: "multi jwt reference", }, { policyRefs: []conf_v1.PolicyReference{ { - Name: "egress-mtls-policy", + Name: "basic-auth-policy", Namespace: "default", }, }, policies: map[string]*conf_v1.Policy{ - "default/egress-mtls-policy": { + "default/basic-auth-policy": { ObjectMeta: meta_v1.ObjectMeta{ - Name: "egress-mtls-policy", + Name: "basic-auth-policy", Namespace: "default", }, Spec: conf_v1.PolicySpec{ - EgressMTLS: &conf_v1.EgressMTLS{ - TrustedCertSecret: "egress-trusted-secret", - SSLName: "foo.com", + BasicAuth: &conf_v1.BasicAuth{ + Realm: "test", + Secret: "htpasswd-secret", }, }, }, }, policyOpts: policyOptions{ secretRefs: map[string]*secrets.SecretReference{ - "default/egress-trusted-secret": { + "default/htpasswd-secret": { Secret: &api_v1.Secret{ - Type: api_v1.SecretTypeTLS, + Type: secrets.SecretTypeHtpasswd, }, + Error: errors.New("secret is invalid"), }, }, }, - context: "route", expected: policiesCfg{ ErrorReturn: &version2.Return{ Code: 500, @@ -4542,44 +4720,42 @@ func TestGeneratePoliciesFails(t *testing.T) { }, expectedWarnings: Warnings{ nil: { - `EgressMTLS policy default/egress-mtls-policy references a secret default/egress-trusted-secret of a wrong type 'kubernetes.io/tls', must be 'nginx.org/ca'`, + `Basic Auth policy default/basic-auth-policy references an invalid secret default/htpasswd-secret: secret is invalid`, }, }, expectedOidc: &oidcPolicyCfg{}, - msg: "egress trusted secret referencing wrong secret type", + msg: "basic auth reference missing secret", }, { policyRefs: []conf_v1.PolicyReference{ { - Name: "egress-mtls-policy", + Name: "basic-auth-policy", Namespace: "default", }, }, policies: map[string]*conf_v1.Policy{ - "default/egress-mtls-policy": { + "default/basic-auth-policy": { ObjectMeta: meta_v1.ObjectMeta{ - Name: "egress-mtls-policy", + Name: "basic-auth-policy", Namespace: "default", }, Spec: conf_v1.PolicySpec{ - EgressMTLS: &conf_v1.EgressMTLS{ - TLSSecret: "egress-mtls-secret", - SSLName: "foo.com", + BasicAuth: &conf_v1.BasicAuth{ + Realm: "test", + Secret: "htpasswd-secret", }, }, }, }, policyOpts: policyOptions{ secretRefs: map[string]*secrets.SecretReference{ - "default/egress-mtls-secret": { + "default/htpasswd-secret": { Secret: &api_v1.Secret{ - Type: api_v1.SecretTypeTLS, + Type: secrets.SecretTypeCA, }, - Error: errors.New("secret is invalid"), }, }, }, - context: "route", expected: policiesCfg{ ErrorReturn: &version2.Return{ Code: 500, @@ -4587,86 +4763,104 @@ func TestGeneratePoliciesFails(t *testing.T) { }, expectedWarnings: Warnings{ nil: { - `EgressMTLS policy default/egress-mtls-policy references an invalid secret default/egress-mtls-secret: secret is invalid`, + `Basic Auth policy default/basic-auth-policy references a secret default/htpasswd-secret of a wrong type 'nginx.org/ca', must be 'nginx.org/htpasswd'`, }, }, expectedOidc: &oidcPolicyCfg{}, - msg: "egress mtls referencing missing tls secret", + msg: "basic auth references wrong secret type", }, { policyRefs: []conf_v1.PolicyReference{ { - Name: "oidc-policy", + Name: "basic-auth-policy", + Namespace: "default", + }, + { + Name: "basic-auth-policy2", Namespace: "default", }, }, policies: map[string]*conf_v1.Policy{ - "default/oidc-policy": { + "default/basic-auth-policy": { ObjectMeta: meta_v1.ObjectMeta{ - Name: "oidc-policy", + Name: "basic-auth-policy", Namespace: "default", }, Spec: conf_v1.PolicySpec{ - OIDC: &conf_v1.OIDC{ - ClientSecret: "oidc-secret", + BasicAuth: &conf_v1.BasicAuth{ + Realm: "test", + Secret: "htpasswd-secret", + }, + }, + }, + "default/basic-auth-policy2": { + ObjectMeta: meta_v1.ObjectMeta{ + Name: "basic-auth-policy2", + Namespace: "default", + }, + Spec: conf_v1.PolicySpec{ + BasicAuth: &conf_v1.BasicAuth{ + Realm: "test", + Secret: "htpasswd-secret2", }, }, }, }, policyOpts: policyOptions{ secretRefs: map[string]*secrets.SecretReference{ - "default/oidc-secret": { + "default/htpasswd-secret": { Secret: &api_v1.Secret{ - Type: secrets.SecretTypeOIDC, + Type: secrets.SecretTypeHtpasswd, }, - Error: errors.New("secret is invalid"), + Path: "/etc/nginx/secrets/default-htpasswd-secret", + }, + "default/htpasswd-secret2": { + Secret: &api_v1.Secret{ + Type: secrets.SecretTypeHtpasswd, + }, + Path: "/etc/nginx/secrets/default-htpasswd-secret2", }, }, }, - context: "route", expected: policiesCfg{ - ErrorReturn: &version2.Return{ - Code: 500, + BasicAuth: &version2.BasicAuth{ + Secret: "/etc/nginx/secrets/default-htpasswd-secret", + Realm: "test", }, }, expectedWarnings: Warnings{ nil: { - `OIDC policy default/oidc-policy references an invalid secret default/oidc-secret: secret is invalid`, + `Multiple basic auth policies in the same context is not valid. Basic auth policy default/basic-auth-policy2 will be ignored`, }, }, expectedOidc: &oidcPolicyCfg{}, - msg: "oidc referencing missing oidc secret", + msg: "multi basic auth reference", }, { policyRefs: []conf_v1.PolicyReference{ { - Name: "oidc-policy", + Name: "ingress-mtls-policy", Namespace: "default", }, }, policies: map[string]*conf_v1.Policy{ - "default/oidc-policy": { + "default/ingress-mtls-policy": { ObjectMeta: meta_v1.ObjectMeta{ - Name: "oidc-policy", + Name: "ingress-mtls-policy", Namespace: "default", }, Spec: conf_v1.PolicySpec{ - OIDC: &conf_v1.OIDC{ - ClientSecret: "oidc-secret", - AuthEndpoint: "http://foo.com/bar", - TokenEndpoint: "http://foo.com/bar", - JWKSURI: "http://foo.com/bar", - AccessTokenEnable: true, + IngressMTLS: &conf_v1.IngressMTLS{ + ClientCertSecret: "ingress-mtls-secret", }, }, }, }, policyOpts: policyOptions{ + tls: true, secretRefs: map[string]*secrets.SecretReference{ - "default/oidc-secret": { - Secret: &api_v1.Secret{ - Type: api_v1.SecretTypeTLS, - }, + "default/ingress-mtls-secret": { + Error: errors.New("secret is invalid"), }, }, }, @@ -4678,80 +4872,43 @@ func TestGeneratePoliciesFails(t *testing.T) { }, expectedWarnings: Warnings{ nil: { - `OIDC policy default/oidc-policy references a secret default/oidc-secret of a wrong type 'kubernetes.io/tls', must be 'nginx.org/oidc'`, + `IngressMTLS policy "default/ingress-mtls-policy" references an invalid secret default/ingress-mtls-secret: secret is invalid`, }, }, expectedOidc: &oidcPolicyCfg{}, - msg: "oidc secret referencing wrong secret type", + msg: "ingress mtls reference an invalid secret", }, { policyRefs: []conf_v1.PolicyReference{ { - Name: "oidc-policy-2", + Name: "ingress-mtls-policy", Namespace: "default", }, }, policies: map[string]*conf_v1.Policy{ - "default/oidc-policy-1": { - ObjectMeta: meta_v1.ObjectMeta{ - Name: "oidc-policy-1", - Namespace: "default", - }, - Spec: conf_v1.PolicySpec{ - OIDC: &conf_v1.OIDC{ - ClientID: "foo", - ClientSecret: "oidc-secret", - AuthEndpoint: "https://foo.com/auth", - TokenEndpoint: "https://foo.com/token", - JWKSURI: "https://foo.com/certs", - AccessTokenEnable: true, - }, - }, - }, - "default/oidc-policy-2": { + "default/ingress-mtls-policy": { ObjectMeta: meta_v1.ObjectMeta{ - Name: "oidc-policy-2", + Name: "ingress-mtls-policy", Namespace: "default", }, Spec: conf_v1.PolicySpec{ - OIDC: &conf_v1.OIDC{ - ClientID: "foo", - ClientSecret: "oidc-secret", - AuthEndpoint: "https://bar.com/auth", - TokenEndpoint: "https://bar.com/token", - JWKSURI: "https://bar.com/certs", - AccessTokenEnable: true, + IngressMTLS: &conf_v1.IngressMTLS{ + ClientCertSecret: "ingress-mtls-secret", }, }, }, }, policyOpts: policyOptions{ + tls: true, secretRefs: map[string]*secrets.SecretReference{ - "default/oidc-secret": { + "default/ingress-mtls-secret": { Secret: &api_v1.Secret{ - Type: secrets.SecretTypeOIDC, - Data: map[string][]byte{ - "client-secret": []byte("super_secret_123"), - }, + Type: api_v1.SecretTypeTLS, }, }, }, }, - context: "route", - oidcPolCfg: &oidcPolicyCfg{ - oidc: &version2.OIDC{ - AuthEndpoint: "https://foo.com/auth", - TokenEndpoint: "https://foo.com/token", - JwksURI: "https://foo.com/certs", - ClientID: "foo", - ClientSecret: "super_secret_123", - RedirectURI: "/_codexch", - Scope: "openid", - ZoneSyncLeeway: 0, - AccessTokenEnable: true, - }, - key: "default/oidc-policy-1", - }, + context: "spec", expected: policiesCfg{ ErrorReturn: &version2.Return{ Code: 500, @@ -4759,387 +4916,1004 @@ func TestGeneratePoliciesFails(t *testing.T) { }, expectedWarnings: Warnings{ nil: { - `Only one oidc policy is allowed in a VirtualServer and its VirtualServerRoutes. Can't use default/oidc-policy-2. Use default/oidc-policy-1`, - }, - }, - expectedOidc: &oidcPolicyCfg{ - oidc: &version2.OIDC{ - AuthEndpoint: "https://foo.com/auth", - TokenEndpoint: "https://foo.com/token", - JwksURI: "https://foo.com/certs", - ClientID: "foo", - ClientSecret: "super_secret_123", - RedirectURI: "/_codexch", - Scope: "openid", - AccessTokenEnable: true, + `IngressMTLS policy default/ingress-mtls-policy references a secret default/ingress-mtls-secret of a wrong type 'kubernetes.io/tls', must be 'nginx.org/ca'`, }, - key: "default/oidc-policy-1", }, - msg: "multiple oidc policies", + expectedOidc: &oidcPolicyCfg{}, + msg: "ingress mtls references wrong secret type", }, { policyRefs: []conf_v1.PolicyReference{ { - Name: "oidc-policy", + Name: "ingress-mtls-policy", Namespace: "default", }, { - Name: "oidc-policy2", + Name: "ingress-mtls-policy2", Namespace: "default", }, }, policies: map[string]*conf_v1.Policy{ - "default/oidc-policy": { + "default/ingress-mtls-policy": { ObjectMeta: meta_v1.ObjectMeta{ - Name: "oidc-policy", + Name: "ingress-mtls-policy", Namespace: "default", }, Spec: conf_v1.PolicySpec{ - OIDC: &conf_v1.OIDC{ - ClientSecret: "oidc-secret", - AuthEndpoint: "https://foo.com/auth", - TokenEndpoint: "https://foo.com/token", - JWKSURI: "https://foo.com/certs", - ClientID: "foo", - AccessTokenEnable: true, + IngressMTLS: &conf_v1.IngressMTLS{ + ClientCertSecret: "ingress-mtls-secret", }, }, }, - "default/oidc-policy2": { - ObjectMeta: meta_v1.ObjectMeta{ - Name: "oidc-policy2", - Namespace: "default", - }, + "default/ingress-mtls-policy2": { Spec: conf_v1.PolicySpec{ - OIDC: &conf_v1.OIDC{ - ClientSecret: "oidc-secret", - AuthEndpoint: "https://bar.com/auth", - TokenEndpoint: "https://bar.com/token", - JWKSURI: "https://bar.com/certs", - ClientID: "bar", - AccessTokenEnable: true, + IngressMTLS: &conf_v1.IngressMTLS{ + ClientCertSecret: "ingress-mtls-secret2", }, }, }, }, policyOpts: policyOptions{ + tls: true, secretRefs: map[string]*secrets.SecretReference{ - "default/oidc-secret": { + "default/ingress-mtls-secret": { Secret: &api_v1.Secret{ - Type: secrets.SecretTypeOIDC, - Data: map[string][]byte{ - "client-secret": []byte("super_secret_123"), - }, + Type: secrets.SecretTypeCA, }, + Path: ingressMTLSCertPath, }, }, }, - context: "route", + context: "spec", expected: policiesCfg{ - OIDC: true, + IngressMTLS: &version2.IngressMTLS{ + ClientCert: ingressMTLSCertPath, + VerifyClient: "on", + VerifyDepth: 1, + }, }, expectedWarnings: Warnings{ nil: { - `Multiple oidc policies in the same context is not valid. OIDC policy default/oidc-policy2 will be ignored`, - }, - }, - expectedOidc: &oidcPolicyCfg{ - &version2.OIDC{ - AuthEndpoint: "https://foo.com/auth", - TokenEndpoint: "https://foo.com/token", - JwksURI: "https://foo.com/certs", - ClientID: "foo", - ClientSecret: "super_secret_123", - RedirectURI: "/_codexch", - Scope: "openid", - ZoneSyncLeeway: 200, - AccessTokenEnable: true, + `Multiple ingressMTLS policies are not allowed. IngressMTLS policy default/ingress-mtls-policy2 will be ignored`, }, - "default/oidc-policy", }, - msg: "multi oidc", + expectedOidc: &oidcPolicyCfg{}, + msg: "multi ingress mtls", }, { policyRefs: []conf_v1.PolicyReference{ { - Name: "waf-policy", - Namespace: "default", - }, - { - Name: "waf-policy2", + Name: "ingress-mtls-policy", Namespace: "default", }, }, policies: map[string]*conf_v1.Policy{ - "default/waf-policy": { - ObjectMeta: meta_v1.ObjectMeta{ - Name: "waf-policy", - Namespace: "default", - }, - Spec: conf_v1.PolicySpec{ - WAF: &conf_v1.WAF{ - Enable: true, - ApPolicy: "default/dataguard-alarm", - }, - }, - }, - "default/waf-policy2": { + "default/ingress-mtls-policy": { ObjectMeta: meta_v1.ObjectMeta{ - Name: "waf-policy2", + Name: "ingress-mtls-policy", Namespace: "default", }, Spec: conf_v1.PolicySpec{ - WAF: &conf_v1.WAF{ - Enable: true, - ApPolicy: "default/dataguard-alarm", + IngressMTLS: &conf_v1.IngressMTLS{ + ClientCertSecret: "ingress-mtls-secret", }, }, }, }, policyOpts: policyOptions{ - apResources: &appProtectResourcesForVS{ - Policies: map[string]string{ - "default/dataguard-alarm": "/etc/nginx/waf/nac-policies/default-dataguard-alarm", - }, - LogConfs: map[string]string{ - "default/logconf": "/etc/nginx/waf/nac-logconfs/default-logconf", + tls: true, + secretRefs: map[string]*secrets.SecretReference{ + "default/ingress-mtls-secret": { + Secret: &api_v1.Secret{ + Type: secrets.SecretTypeCA, + }, + Path: ingressMTLSCertPath, }, }, }, context: "route", expected: policiesCfg{ - WAF: &version2.WAF{ - Enable: "on", - ApPolicy: "/etc/nginx/waf/nac-policies/default-dataguard-alarm", + ErrorReturn: &version2.Return{ + Code: 500, }, }, expectedWarnings: Warnings{ nil: { - `Multiple WAF policies in the same context is not valid. WAF policy default/waf-policy2 will be ignored`, + `IngressMTLS policy default/ingress-mtls-policy is not allowed in the route context`, }, }, expectedOidc: &oidcPolicyCfg{}, - msg: "multi waf", + msg: "ingress mtls in the wrong context", }, - } - - for _, test := range tests { - vsc := newVirtualServerConfigurator(&ConfigParams{}, false, false, &StaticConfigParams{}, false) - - if test.oidcPolCfg != nil { - vsc.oidcPolCfg = test.oidcPolCfg - } - - result := vsc.generatePolicies(ownerDetails, test.policyRefs, test.policies, test.context, test.policyOpts) - if diff := cmp.Diff(test.expected, result); diff != "" { - t.Errorf("generatePolicies() '%v' mismatch (-want +got):\n%s", test.msg, diff) - } - if !reflect.DeepEqual(vsc.warnings, test.expectedWarnings) { - t.Errorf( - "generatePolicies() returned warnings of \n%v but expected \n%v for the case of %s", - vsc.warnings, - test.expectedWarnings, - test.msg, - ) - } - if diff := cmp.Diff(test.expectedOidc.oidc, vsc.oidcPolCfg.oidc); diff != "" { - t.Errorf("generatePolicies() '%v' mismatch (-want +got):\n%s", test.msg, diff) - } - if diff := cmp.Diff(test.expectedOidc.key, vsc.oidcPolCfg.key); diff != "" { - t.Errorf("generatePolicies() '%v' mismatch (-want +got):\n%s", test.msg, diff) - } - } -} - -func TestRemoveDuplicates(t *testing.T) { - t.Parallel() - tests := []struct { - rlz []version2.LimitReqZone - expected []version2.LimitReqZone - }{ { - rlz: []version2.LimitReqZone{ - {ZoneName: "test"}, - {ZoneName: "test"}, - {ZoneName: "test2"}, - {ZoneName: "test3"}, - }, - expected: []version2.LimitReqZone{ - {ZoneName: "test"}, - {ZoneName: "test2"}, - {ZoneName: "test3"}, + policyRefs: []conf_v1.PolicyReference{ + { + Name: "ingress-mtls-policy", + Namespace: "default", + }, }, - }, - { - rlz: []version2.LimitReqZone{ - {ZoneName: "test"}, - {ZoneName: "test"}, - {ZoneName: "test2"}, - {ZoneName: "test3"}, - {ZoneName: "test3"}, + policies: map[string]*conf_v1.Policy{ + "default/ingress-mtls-policy": { + ObjectMeta: meta_v1.ObjectMeta{ + Name: "ingress-mtls-policy", + Namespace: "default", + }, + Spec: conf_v1.PolicySpec{ + IngressMTLS: &conf_v1.IngressMTLS{ + ClientCertSecret: "ingress-mtls-secret", + }, + }, + }, }, - expected: []version2.LimitReqZone{ - {ZoneName: "test"}, - {ZoneName: "test2"}, - {ZoneName: "test3"}, + policyOpts: policyOptions{ + tls: false, + secretRefs: map[string]*secrets.SecretReference{ + "default/ingress-mtls-secret": { + Secret: &api_v1.Secret{ + Type: secrets.SecretTypeCA, + }, + Path: ingressMTLSCertPath, + }, + }, }, - }, - } - for _, test := range tests { - result := removeDuplicateLimitReqZones(test.rlz) - if !reflect.DeepEqual(result, test.expected) { - t.Errorf("removeDuplicateLimitReqZones() returned \n%v, but expected \n%v", result, test.expected) - } - } -} - -func TestAddPoliciesCfgToLocations(t *testing.T) { - t.Parallel() - cfg := policiesCfg{ - Allow: []string{"127.0.0.1"}, - Deny: []string{"127.0.0.2"}, - ErrorReturn: &version2.Return{ - Code: 400, - }, - } - - locations := []version2.Location{ - { - Path: "/", - }, - } - - expectedLocations := []version2.Location{ - { - Path: "/", - Allow: []string{"127.0.0.1"}, - Deny: []string{"127.0.0.2"}, - PoliciesErrorReturn: &version2.Return{ - Code: 400, + context: "route", + expected: policiesCfg{ + ErrorReturn: &version2.Return{ + Code: 500, + }, }, - }, - } - - addPoliciesCfgToLocations(cfg, locations) - if !reflect.DeepEqual(locations, expectedLocations) { - t.Errorf("addPoliciesCfgToLocations() returned \n%+v but expected \n%+v", locations, expectedLocations) - } -} - -func TestGenerateUpstream(t *testing.T) { - t.Parallel() - name := "test-upstream" - upstream := conf_v1.Upstream{Service: name, Port: 80} - endpoints := []string{ - "192.168.10.10:8080", - } - cfgParams := ConfigParams{ - LBMethod: "random", - MaxFails: 1, - MaxConns: 0, - FailTimeout: "10s", - Keepalive: 21, - UpstreamZoneSize: "256k", - } - - expected := version2.Upstream{ - Name: "test-upstream", - UpstreamLabels: version2.UpstreamLabels{ - Service: "test-upstream", - }, - Servers: []version2.UpstreamServer{ - { - Address: "192.168.10.10:8080", + expectedWarnings: Warnings{ + nil: { + `TLS must be enabled in VirtualServer for IngressMTLS policy default/ingress-mtls-policy`, + }, }, + expectedOidc: &oidcPolicyCfg{}, + msg: "ingress mtls missing TLS config", }, - MaxFails: 1, - MaxConns: 0, - FailTimeout: "10s", - LBMethod: "random", - Keepalive: 21, - UpstreamZoneSize: "256k", - } - - vsc := newVirtualServerConfigurator(&cfgParams, false, false, &StaticConfigParams{}, false) - result := vsc.generateUpstream(nil, name, upstream, false, endpoints) - if !reflect.DeepEqual(result, expected) { - t.Errorf("generateUpstream() returned %v but expected %v", result, expected) - } - - if len(vsc.warnings) != 0 { - t.Errorf("generateUpstream returned warnings for %v", upstream) - } -} - -func TestGenerateUpstreamWithKeepalive(t *testing.T) { - t.Parallel() - name := "test-upstream" - noKeepalive := 0 - keepalive := 32 - endpoints := []string{ - "192.168.10.10:8080", - } - - tests := []struct { - upstream conf_v1.Upstream - cfgParams *ConfigParams - expected version2.Upstream - msg string - }{ { - conf_v1.Upstream{Keepalive: &keepalive, Service: name, Port: 80}, - &ConfigParams{Keepalive: 21}, - version2.Upstream{ - Name: "test-upstream", - UpstreamLabels: version2.UpstreamLabels{ - Service: "test-upstream", + policyRefs: []conf_v1.PolicyReference{ + { + Name: "ingress-mtls-policy", + Namespace: "default", }, - Servers: []version2.UpstreamServer{ - { - Address: "192.168.10.10:8080", + }, + policies: map[string]*conf_v1.Policy{ + "default/ingress-mtls-policy": { + ObjectMeta: meta_v1.ObjectMeta{ + Name: "ingress-mtls-policy", + Namespace: "default", + }, + Spec: conf_v1.PolicySpec{ + IngressMTLS: &conf_v1.IngressMTLS{ + ClientCertSecret: "ingress-mtls-secret", + CrlFileName: "default-ingress-mtls-secret-ca.crl", + }, }, }, - Keepalive: 32, }, - "upstream keepalive set, configparam set", - }, - { - conf_v1.Upstream{Service: name, Port: 80}, - &ConfigParams{Keepalive: 21}, - version2.Upstream{ - Name: "test-upstream", - UpstreamLabels: version2.UpstreamLabels{ - Service: "test-upstream", - }, - Servers: []version2.UpstreamServer{ - { - Address: "192.168.10.10:8080", + policyOpts: policyOptions{ + tls: true, + secretRefs: map[string]*secrets.SecretReference{ + "default/ingress-mtls-secret": { + Secret: &api_v1.Secret{ + Type: secrets.SecretTypeCA, + Data: map[string][]byte{ + "ca.crl": []byte("base64crl"), + }, + }, + Path: ingressMTLSCertPath, }, }, - Keepalive: 21, }, - "upstream keepalive not set, configparam set", - }, - { - conf_v1.Upstream{Keepalive: &noKeepalive, Service: name, Port: 80}, - &ConfigParams{Keepalive: 21}, - version2.Upstream{ - Name: "test-upstream", - UpstreamLabels: version2.UpstreamLabels{ - Service: "test-upstream", + context: "spec", + expected: policiesCfg{ + IngressMTLS: &version2.IngressMTLS{ + ClientCert: ingressMTLSCertPath, + ClientCrl: ingressMTLSCrlPath, + VerifyClient: "on", + VerifyDepth: 1, }, - Servers: []version2.UpstreamServer{ - { - Address: "192.168.10.10:8080", - }, + ErrorReturn: nil, + }, + expectedWarnings: Warnings{ + nil: { + `Both ca.crl in the Secret and ingressMTLS.crlFileName fields cannot be used. ca.crl in default/ingress-mtls-secret will be ignored and default/ingress-mtls-policy will be applied`, }, }, - "upstream keepalive set to 0, configparam set", + expectedOidc: &oidcPolicyCfg{}, + msg: "ingress mtls ca.crl and ingressMTLS.Crl set", }, - } - - for _, test := range tests { - vsc := newVirtualServerConfigurator(test.cfgParams, false, false, &StaticConfigParams{}, false) + { + policyRefs: []conf_v1.PolicyReference{ + { + Name: "egress-mtls-policy", + Namespace: "default", + }, + { + Name: "egress-mtls-policy2", + Namespace: "default", + }, + }, + policies: map[string]*conf_v1.Policy{ + "default/egress-mtls-policy": { + ObjectMeta: meta_v1.ObjectMeta{ + Name: "egress-mtls-policy", + Namespace: "default", + }, + Spec: conf_v1.PolicySpec{ + EgressMTLS: &conf_v1.EgressMTLS{ + TLSSecret: "egress-mtls-secret", + }, + }, + }, + "default/egress-mtls-policy2": { + ObjectMeta: meta_v1.ObjectMeta{ + Name: "egress-mtls-policy2", + Namespace: "default", + }, + Spec: conf_v1.PolicySpec{ + EgressMTLS: &conf_v1.EgressMTLS{ + TLSSecret: "egress-mtls-secret2", + }, + }, + }, + }, + policyOpts: policyOptions{ + secretRefs: map[string]*secrets.SecretReference{ + "default/egress-mtls-secret": { + Secret: &api_v1.Secret{ + Type: api_v1.SecretTypeTLS, + }, + Path: "/etc/nginx/secrets/default-egress-mtls-secret", + }, + }, + }, + context: "route", + expected: policiesCfg{ + EgressMTLS: &version2.EgressMTLS{ + Certificate: "/etc/nginx/secrets/default-egress-mtls-secret", + CertificateKey: "/etc/nginx/secrets/default-egress-mtls-secret", + VerifyServer: false, + VerifyDepth: 1, + Ciphers: "DEFAULT", + Protocols: "TLSv1 TLSv1.1 TLSv1.2", + SessionReuse: true, + SSLName: "$proxy_host", + }, + }, + expectedWarnings: Warnings{ + nil: { + `Multiple egressMTLS policies in the same context is not valid. EgressMTLS policy default/egress-mtls-policy2 will be ignored`, + }, + }, + expectedOidc: &oidcPolicyCfg{}, + msg: "multi egress mtls", + }, + { + policyRefs: []conf_v1.PolicyReference{ + { + Name: "egress-mtls-policy", + Namespace: "default", + }, + }, + policies: map[string]*conf_v1.Policy{ + "default/egress-mtls-policy": { + ObjectMeta: meta_v1.ObjectMeta{ + Name: "egress-mtls-policy", + Namespace: "default", + }, + Spec: conf_v1.PolicySpec{ + EgressMTLS: &conf_v1.EgressMTLS{ + TrustedCertSecret: "egress-trusted-secret", + SSLName: "foo.com", + }, + }, + }, + }, + policyOpts: policyOptions{ + secretRefs: map[string]*secrets.SecretReference{ + "default/egress-trusted-secret": { + Secret: &api_v1.Secret{ + Type: secrets.SecretTypeCA, + }, + Error: errors.New("secret is invalid"), + }, + }, + }, + context: "route", + expected: policiesCfg{ + ErrorReturn: &version2.Return{ + Code: 500, + }, + }, + expectedWarnings: Warnings{ + nil: { + `EgressMTLS policy default/egress-mtls-policy references an invalid secret default/egress-trusted-secret: secret is invalid`, + }, + }, + expectedOidc: &oidcPolicyCfg{}, + msg: "egress mtls referencing an invalid CA secret", + }, + { + policyRefs: []conf_v1.PolicyReference{ + { + Name: "egress-mtls-policy", + Namespace: "default", + }, + }, + policies: map[string]*conf_v1.Policy{ + "default/egress-mtls-policy": { + ObjectMeta: meta_v1.ObjectMeta{ + Name: "egress-mtls-policy", + Namespace: "default", + }, + Spec: conf_v1.PolicySpec{ + EgressMTLS: &conf_v1.EgressMTLS{ + TLSSecret: "egress-mtls-secret", + SSLName: "foo.com", + }, + }, + }, + }, + policyOpts: policyOptions{ + secretRefs: map[string]*secrets.SecretReference{ + "default/egress-mtls-secret": { + Secret: &api_v1.Secret{ + Type: secrets.SecretTypeCA, + }, + }, + }, + }, + context: "route", + expected: policiesCfg{ + ErrorReturn: &version2.Return{ + Code: 500, + }, + }, + expectedWarnings: Warnings{ + nil: { + `EgressMTLS policy default/egress-mtls-policy references a secret default/egress-mtls-secret of a wrong type 'nginx.org/ca', must be 'kubernetes.io/tls'`, + }, + }, + expectedOidc: &oidcPolicyCfg{}, + msg: "egress mtls referencing wrong secret type", + }, + { + policyRefs: []conf_v1.PolicyReference{ + { + Name: "egress-mtls-policy", + Namespace: "default", + }, + }, + policies: map[string]*conf_v1.Policy{ + "default/egress-mtls-policy": { + ObjectMeta: meta_v1.ObjectMeta{ + Name: "egress-mtls-policy", + Namespace: "default", + }, + Spec: conf_v1.PolicySpec{ + EgressMTLS: &conf_v1.EgressMTLS{ + TrustedCertSecret: "egress-trusted-secret", + SSLName: "foo.com", + }, + }, + }, + }, + policyOpts: policyOptions{ + secretRefs: map[string]*secrets.SecretReference{ + "default/egress-trusted-secret": { + Secret: &api_v1.Secret{ + Type: api_v1.SecretTypeTLS, + }, + }, + }, + }, + context: "route", + expected: policiesCfg{ + ErrorReturn: &version2.Return{ + Code: 500, + }, + }, + expectedWarnings: Warnings{ + nil: { + `EgressMTLS policy default/egress-mtls-policy references a secret default/egress-trusted-secret of a wrong type 'kubernetes.io/tls', must be 'nginx.org/ca'`, + }, + }, + expectedOidc: &oidcPolicyCfg{}, + msg: "egress trusted secret referencing wrong secret type", + }, + { + policyRefs: []conf_v1.PolicyReference{ + { + Name: "egress-mtls-policy", + Namespace: "default", + }, + }, + policies: map[string]*conf_v1.Policy{ + "default/egress-mtls-policy": { + ObjectMeta: meta_v1.ObjectMeta{ + Name: "egress-mtls-policy", + Namespace: "default", + }, + Spec: conf_v1.PolicySpec{ + EgressMTLS: &conf_v1.EgressMTLS{ + TLSSecret: "egress-mtls-secret", + SSLName: "foo.com", + }, + }, + }, + }, + policyOpts: policyOptions{ + secretRefs: map[string]*secrets.SecretReference{ + "default/egress-mtls-secret": { + Secret: &api_v1.Secret{ + Type: api_v1.SecretTypeTLS, + }, + Error: errors.New("secret is invalid"), + }, + }, + }, + context: "route", + expected: policiesCfg{ + ErrorReturn: &version2.Return{ + Code: 500, + }, + }, + expectedWarnings: Warnings{ + nil: { + `EgressMTLS policy default/egress-mtls-policy references an invalid secret default/egress-mtls-secret: secret is invalid`, + }, + }, + expectedOidc: &oidcPolicyCfg{}, + msg: "egress mtls referencing missing tls secret", + }, + { + policyRefs: []conf_v1.PolicyReference{ + { + Name: "oidc-policy", + Namespace: "default", + }, + }, + policies: map[string]*conf_v1.Policy{ + "default/oidc-policy": { + ObjectMeta: meta_v1.ObjectMeta{ + Name: "oidc-policy", + Namespace: "default", + }, + Spec: conf_v1.PolicySpec{ + OIDC: &conf_v1.OIDC{ + ClientSecret: "oidc-secret", + }, + }, + }, + }, + policyOpts: policyOptions{ + secretRefs: map[string]*secrets.SecretReference{ + "default/oidc-secret": { + Secret: &api_v1.Secret{ + Type: secrets.SecretTypeOIDC, + }, + Error: errors.New("secret is invalid"), + }, + }, + }, + context: "route", + expected: policiesCfg{ + ErrorReturn: &version2.Return{ + Code: 500, + }, + }, + expectedWarnings: Warnings{ + nil: { + `OIDC policy default/oidc-policy references an invalid secret default/oidc-secret: secret is invalid`, + }, + }, + expectedOidc: &oidcPolicyCfg{}, + msg: "oidc referencing missing oidc secret", + }, + { + policyRefs: []conf_v1.PolicyReference{ + { + Name: "oidc-policy", + Namespace: "default", + }, + }, + policies: map[string]*conf_v1.Policy{ + "default/oidc-policy": { + ObjectMeta: meta_v1.ObjectMeta{ + Name: "oidc-policy", + Namespace: "default", + }, + Spec: conf_v1.PolicySpec{ + OIDC: &conf_v1.OIDC{ + ClientSecret: "oidc-secret", + AuthEndpoint: "http://foo.com/bar", + TokenEndpoint: "http://foo.com/bar", + JWKSURI: "http://foo.com/bar", + AccessTokenEnable: true, + }, + }, + }, + }, + policyOpts: policyOptions{ + secretRefs: map[string]*secrets.SecretReference{ + "default/oidc-secret": { + Secret: &api_v1.Secret{ + Type: api_v1.SecretTypeTLS, + }, + }, + }, + }, + context: "spec", + expected: policiesCfg{ + ErrorReturn: &version2.Return{ + Code: 500, + }, + }, + expectedWarnings: Warnings{ + nil: { + `OIDC policy default/oidc-policy references a secret default/oidc-secret of a wrong type 'kubernetes.io/tls', must be 'nginx.org/oidc'`, + }, + }, + expectedOidc: &oidcPolicyCfg{}, + msg: "oidc secret referencing wrong secret type", + }, + { + policyRefs: []conf_v1.PolicyReference{ + { + Name: "oidc-policy-2", + Namespace: "default", + }, + }, + policies: map[string]*conf_v1.Policy{ + "default/oidc-policy-1": { + ObjectMeta: meta_v1.ObjectMeta{ + Name: "oidc-policy-1", + Namespace: "default", + }, + Spec: conf_v1.PolicySpec{ + OIDC: &conf_v1.OIDC{ + ClientID: "foo", + ClientSecret: "oidc-secret", + AuthEndpoint: "https://foo.com/auth", + TokenEndpoint: "https://foo.com/token", + JWKSURI: "https://foo.com/certs", + AccessTokenEnable: true, + }, + }, + }, + "default/oidc-policy-2": { + ObjectMeta: meta_v1.ObjectMeta{ + Name: "oidc-policy-2", + Namespace: "default", + }, + Spec: conf_v1.PolicySpec{ + OIDC: &conf_v1.OIDC{ + ClientID: "foo", + ClientSecret: "oidc-secret", + AuthEndpoint: "https://bar.com/auth", + TokenEndpoint: "https://bar.com/token", + JWKSURI: "https://bar.com/certs", + AccessTokenEnable: true, + }, + }, + }, + }, + policyOpts: policyOptions{ + secretRefs: map[string]*secrets.SecretReference{ + "default/oidc-secret": { + Secret: &api_v1.Secret{ + Type: secrets.SecretTypeOIDC, + Data: map[string][]byte{ + "client-secret": []byte("super_secret_123"), + }, + }, + }, + }, + }, + context: "route", + oidcPolCfg: &oidcPolicyCfg{ + oidc: &version2.OIDC{ + AuthEndpoint: "https://foo.com/auth", + TokenEndpoint: "https://foo.com/token", + JwksURI: "https://foo.com/certs", + ClientID: "foo", + ClientSecret: "super_secret_123", + RedirectURI: "/_codexch", + Scope: "openid", + ZoneSyncLeeway: 0, + AccessTokenEnable: true, + }, + key: "default/oidc-policy-1", + }, + expected: policiesCfg{ + ErrorReturn: &version2.Return{ + Code: 500, + }, + }, + expectedWarnings: Warnings{ + nil: { + `Only one oidc policy is allowed in a VirtualServer and its VirtualServerRoutes. Can't use default/oidc-policy-2. Use default/oidc-policy-1`, + }, + }, + expectedOidc: &oidcPolicyCfg{ + oidc: &version2.OIDC{ + AuthEndpoint: "https://foo.com/auth", + TokenEndpoint: "https://foo.com/token", + JwksURI: "https://foo.com/certs", + ClientID: "foo", + ClientSecret: "super_secret_123", + RedirectURI: "/_codexch", + Scope: "openid", + AccessTokenEnable: true, + }, + key: "default/oidc-policy-1", + }, + msg: "multiple oidc policies", + }, + { + policyRefs: []conf_v1.PolicyReference{ + { + Name: "oidc-policy", + Namespace: "default", + }, + { + Name: "oidc-policy2", + Namespace: "default", + }, + }, + policies: map[string]*conf_v1.Policy{ + "default/oidc-policy": { + ObjectMeta: meta_v1.ObjectMeta{ + Name: "oidc-policy", + Namespace: "default", + }, + Spec: conf_v1.PolicySpec{ + OIDC: &conf_v1.OIDC{ + ClientSecret: "oidc-secret", + AuthEndpoint: "https://foo.com/auth", + TokenEndpoint: "https://foo.com/token", + JWKSURI: "https://foo.com/certs", + ClientID: "foo", + AccessTokenEnable: true, + }, + }, + }, + "default/oidc-policy2": { + ObjectMeta: meta_v1.ObjectMeta{ + Name: "oidc-policy2", + Namespace: "default", + }, + Spec: conf_v1.PolicySpec{ + OIDC: &conf_v1.OIDC{ + ClientSecret: "oidc-secret", + AuthEndpoint: "https://bar.com/auth", + TokenEndpoint: "https://bar.com/token", + JWKSURI: "https://bar.com/certs", + ClientID: "bar", + AccessTokenEnable: true, + }, + }, + }, + }, + policyOpts: policyOptions{ + secretRefs: map[string]*secrets.SecretReference{ + "default/oidc-secret": { + Secret: &api_v1.Secret{ + Type: secrets.SecretTypeOIDC, + Data: map[string][]byte{ + "client-secret": []byte("super_secret_123"), + }, + }, + }, + }, + }, + context: "route", + expected: policiesCfg{ + OIDC: true, + }, + expectedWarnings: Warnings{ + nil: { + `Multiple oidc policies in the same context is not valid. OIDC policy default/oidc-policy2 will be ignored`, + }, + }, + expectedOidc: &oidcPolicyCfg{ + &version2.OIDC{ + AuthEndpoint: "https://foo.com/auth", + TokenEndpoint: "https://foo.com/token", + JwksURI: "https://foo.com/certs", + ClientID: "foo", + ClientSecret: "super_secret_123", + RedirectURI: "/_codexch", + Scope: "openid", + ZoneSyncLeeway: 200, + AccessTokenEnable: true, + }, + "default/oidc-policy", + }, + msg: "multi oidc", + }, + { + policyRefs: []conf_v1.PolicyReference{ + { + Name: "waf-policy", + Namespace: "default", + }, + { + Name: "waf-policy2", + Namespace: "default", + }, + }, + policies: map[string]*conf_v1.Policy{ + "default/waf-policy": { + ObjectMeta: meta_v1.ObjectMeta{ + Name: "waf-policy", + Namespace: "default", + }, + Spec: conf_v1.PolicySpec{ + WAF: &conf_v1.WAF{ + Enable: true, + ApPolicy: "default/dataguard-alarm", + }, + }, + }, + "default/waf-policy2": { + ObjectMeta: meta_v1.ObjectMeta{ + Name: "waf-policy2", + Namespace: "default", + }, + Spec: conf_v1.PolicySpec{ + WAF: &conf_v1.WAF{ + Enable: true, + ApPolicy: "default/dataguard-alarm", + }, + }, + }, + }, + policyOpts: policyOptions{ + apResources: &appProtectResourcesForVS{ + Policies: map[string]string{ + "default/dataguard-alarm": "/etc/nginx/waf/nac-policies/default-dataguard-alarm", + }, + LogConfs: map[string]string{ + "default/logconf": "/etc/nginx/waf/nac-logconfs/default-logconf", + }, + }, + }, + context: "route", + expected: policiesCfg{ + WAF: &version2.WAF{ + Enable: "on", + ApPolicy: "/etc/nginx/waf/nac-policies/default-dataguard-alarm", + }, + }, + expectedWarnings: Warnings{ + nil: { + `Multiple WAF policies in the same context is not valid. WAF policy default/waf-policy2 will be ignored`, + }, + }, + expectedOidc: &oidcPolicyCfg{}, + msg: "multi waf", + }, + } + + for _, test := range tests { + vsc := newVirtualServerConfigurator(&ConfigParams{}, false, false, &StaticConfigParams{}, false) + + if test.oidcPolCfg != nil { + vsc.oidcPolCfg = test.oidcPolCfg + } + + result := vsc.generatePolicies(ownerDetails, test.policyRefs, test.policies, test.context, test.policyOpts) + if diff := cmp.Diff(test.expected, result); diff != "" { + t.Errorf("generatePolicies() '%v' mismatch (-want +got):\n%s", test.msg, diff) + } + if !reflect.DeepEqual(vsc.warnings, test.expectedWarnings) { + t.Errorf( + "generatePolicies() returned warnings of \n%v but expected \n%v for the case of %s", + vsc.warnings, + test.expectedWarnings, + test.msg, + ) + } + if diff := cmp.Diff(test.expectedOidc.oidc, vsc.oidcPolCfg.oidc); diff != "" { + t.Errorf("generatePolicies() '%v' mismatch (-want +got):\n%s", test.msg, diff) + } + if diff := cmp.Diff(test.expectedOidc.key, vsc.oidcPolCfg.key); diff != "" { + t.Errorf("generatePolicies() '%v' mismatch (-want +got):\n%s", test.msg, diff) + } + } +} + +func TestRemoveDuplicates(t *testing.T) { + t.Parallel() + tests := []struct { + rlz []version2.LimitReqZone + expected []version2.LimitReqZone + }{ + { + rlz: []version2.LimitReqZone{ + {ZoneName: "test"}, + {ZoneName: "test"}, + {ZoneName: "test2"}, + {ZoneName: "test3"}, + }, + expected: []version2.LimitReqZone{ + {ZoneName: "test"}, + {ZoneName: "test2"}, + {ZoneName: "test3"}, + }, + }, + { + rlz: []version2.LimitReqZone{ + {ZoneName: "test"}, + {ZoneName: "test"}, + {ZoneName: "test2"}, + {ZoneName: "test3"}, + {ZoneName: "test3"}, + }, + expected: []version2.LimitReqZone{ + {ZoneName: "test"}, + {ZoneName: "test2"}, + {ZoneName: "test3"}, + }, + }, + } + for _, test := range tests { + result := removeDuplicateLimitReqZones(test.rlz) + if !reflect.DeepEqual(result, test.expected) { + t.Errorf("removeDuplicateLimitReqZones() returned \n%v, but expected \n%v", result, test.expected) + } + } +} + +func TestAddPoliciesCfgToLocations(t *testing.T) { + t.Parallel() + cfg := policiesCfg{ + Allow: []string{"127.0.0.1"}, + Deny: []string{"127.0.0.2"}, + ErrorReturn: &version2.Return{ + Code: 400, + }, + } + + locations := []version2.Location{ + { + Path: "/", + }, + } + + expectedLocations := []version2.Location{ + { + Path: "/", + Allow: []string{"127.0.0.1"}, + Deny: []string{"127.0.0.2"}, + PoliciesErrorReturn: &version2.Return{ + Code: 400, + }, + }, + } + + addPoliciesCfgToLocations(cfg, locations) + if !reflect.DeepEqual(locations, expectedLocations) { + t.Errorf("addPoliciesCfgToLocations() returned \n%+v but expected \n%+v", locations, expectedLocations) + } +} + +func TestGenerateUpstream(t *testing.T) { + t.Parallel() + name := "test-upstream" + upstream := conf_v1.Upstream{Service: name, Port: 80} + endpoints := []string{ + "192.168.10.10:8080", + } + cfgParams := ConfigParams{ + LBMethod: "random", + MaxFails: 1, + MaxConns: 0, + FailTimeout: "10s", + Keepalive: 21, + UpstreamZoneSize: "256k", + } + + expected := version2.Upstream{ + Name: "test-upstream", + UpstreamLabels: version2.UpstreamLabels{ + Service: "test-upstream", + }, + Servers: []version2.UpstreamServer{ + { + Address: "192.168.10.10:8080", + }, + }, + MaxFails: 1, + MaxConns: 0, + FailTimeout: "10s", + LBMethod: "random", + Keepalive: 21, + UpstreamZoneSize: "256k", + } + + vsc := newVirtualServerConfigurator(&cfgParams, false, false, &StaticConfigParams{}, false) + result := vsc.generateUpstream(nil, name, upstream, false, endpoints) + if !reflect.DeepEqual(result, expected) { + t.Errorf("generateUpstream() returned %v but expected %v", result, expected) + } + + if len(vsc.warnings) != 0 { + t.Errorf("generateUpstream returned warnings for %v", upstream) + } +} + +func TestGenerateUpstreamWithKeepalive(t *testing.T) { + t.Parallel() + name := "test-upstream" + noKeepalive := 0 + keepalive := 32 + endpoints := []string{ + "192.168.10.10:8080", + } + + tests := []struct { + upstream conf_v1.Upstream + cfgParams *ConfigParams + expected version2.Upstream + msg string + }{ + { + conf_v1.Upstream{Keepalive: &keepalive, Service: name, Port: 80}, + &ConfigParams{Keepalive: 21}, + version2.Upstream{ + Name: "test-upstream", + UpstreamLabels: version2.UpstreamLabels{ + Service: "test-upstream", + }, + Servers: []version2.UpstreamServer{ + { + Address: "192.168.10.10:8080", + }, + }, + Keepalive: 32, + }, + "upstream keepalive set, configparam set", + }, + { + conf_v1.Upstream{Service: name, Port: 80}, + &ConfigParams{Keepalive: 21}, + version2.Upstream{ + Name: "test-upstream", + UpstreamLabels: version2.UpstreamLabels{ + Service: "test-upstream", + }, + Servers: []version2.UpstreamServer{ + { + Address: "192.168.10.10:8080", + }, + }, + Keepalive: 21, + }, + "upstream keepalive not set, configparam set", + }, + { + conf_v1.Upstream{Keepalive: &noKeepalive, Service: name, Port: 80}, + &ConfigParams{Keepalive: 21}, + version2.Upstream{ + Name: "test-upstream", + UpstreamLabels: version2.UpstreamLabels{ + Service: "test-upstream", + }, + Servers: []version2.UpstreamServer{ + { + Address: "192.168.10.10:8080", + }, + }, + }, + "upstream keepalive set to 0, configparam set", + }, + } + + for _, test := range tests { + vsc := newVirtualServerConfigurator(test.cfgParams, false, false, &StaticConfigParams{}, false) result := vsc.generateUpstream(nil, name, test.upstream, false, endpoints) if !reflect.DeepEqual(result, test.expected) { t.Errorf("generateUpstream() returned %v but expected %v for the case of %v", result, test.expected, test.msg) @@ -6409,97 +7183,510 @@ func TestGenerateDefaultSplitsConfig(t *testing.T) { }, }, } - virtualServer := conf_v1.VirtualServer{ - ObjectMeta: meta_v1.ObjectMeta{ - Name: "cafe", - Namespace: "default", - }, - } + virtualServer := conf_v1.VirtualServer{ + ObjectMeta: meta_v1.ObjectMeta{ + Name: "cafe", + Namespace: "default", + }, + } + upstreamNamer := NewUpstreamNamerForVirtualServer(&virtualServer) + variableNamer := newVariableNamer(&virtualServer) + index := 1 + + expected := routingCfg{ + SplitClients: []version2.SplitClient{ + { + Source: "$request_id", + Variable: "$vs_default_cafe_splits_1", + Distributions: []version2.Distribution{ + { + Weight: "90%", + Value: "/internal_location_splits_1_split_0", + }, + { + Weight: "10%", + Value: "/internal_location_splits_1_split_1", + }, + }, + }, + }, + Locations: []version2.Location{ + { + Path: "/internal_location_splits_1_split_0", + ProxyPass: "http://vs_default_cafe_coffee-v1$request_uri", + ProxyNextUpstream: "error timeout", + ProxyNextUpstreamTimeout: "0s", + ProxyNextUpstreamTries: 0, + Internal: true, + ProxySSLName: "coffee-v1.default.svc", + ProxyPassRequestHeaders: true, + ProxySetHeaders: []version2.Header{{Name: "Host", Value: "$host"}}, + ServiceName: "coffee-v1", + IsVSR: true, + VSRName: "coffee", + VSRNamespace: "default", + }, + { + Path: "/internal_location_splits_1_split_1", + ProxyPass: "http://vs_default_cafe_coffee-v2$request_uri", + ProxyNextUpstream: "error timeout", + ProxyNextUpstreamTimeout: "0s", + ProxyNextUpstreamTries: 0, + Internal: true, + ProxySSLName: "coffee-v2.default.svc", + ProxyPassRequestHeaders: true, + ProxySetHeaders: []version2.Header{{Name: "Host", Value: "$host"}}, + ServiceName: "coffee-v2", + IsVSR: true, + VSRName: "coffee", + VSRNamespace: "default", + }, + }, + InternalRedirectLocation: version2.InternalRedirectLocation{ + Path: "/", + Destination: "$vs_default_cafe_splits_1", + }, + } + + cfgParams := ConfigParams{} + locSnippet := "" + enableSnippets := false + crUpstreams := map[string]conf_v1.Upstream{ + "vs_default_cafe_coffee-v1": { + Service: "coffee-v1", + }, + "vs_default_cafe_coffee-v2": { + Service: "coffee-v2", + }, + } + + errorPageDetails := errorPageDetails{ + pages: route.ErrorPages, + index: 0, + owner: nil, + } + + result := generateDefaultSplitsConfig(route, upstreamNamer, crUpstreams, variableNamer, index, &cfgParams, + errorPageDetails, "", locSnippet, enableSnippets, 0, true, "coffee", "default", Warnings{}) + if !reflect.DeepEqual(result, expected) { + t.Errorf("generateDefaultSplitsConfig() returned \n%+v but expected \n%+v", result, expected) + } +} + +func TestGenerateMatchesConfig(t *testing.T) { + t.Parallel() + route := conf_v1.Route{ + Path: "/", + Matches: []conf_v1.Match{ + { + Conditions: []conf_v1.Condition{ + { + Header: "x-version", + Value: "v1", + }, + { + Cookie: "user", + Value: "john", + }, + { + Argument: "answer", + Value: "yes", + }, + { + Variable: "$request_method", + Value: "GET", + }, + }, + Action: &conf_v1.Action{ + Pass: "coffee-v1", + }, + }, + { + Conditions: []conf_v1.Condition{ + { + Header: "x-version", + Value: "v2", + }, + { + Cookie: "user", + Value: "paul", + }, + { + Argument: "answer", + Value: "no", + }, + { + Variable: "$request_method", + Value: "POST", + }, + }, + Splits: []conf_v1.Split{ + { + Weight: 90, + Action: &conf_v1.Action{ + Pass: "coffee-v1", + }, + }, + { + Weight: 10, + Action: &conf_v1.Action{ + Pass: "coffee-v2", + }, + }, + }, + }, + }, + Action: &conf_v1.Action{ + Pass: "tea", + }, + } + virtualServer := conf_v1.VirtualServer{ + ObjectMeta: meta_v1.ObjectMeta{ + Name: "cafe", + Namespace: "default", + }, + } + errorPages := []conf_v1.ErrorPage{ + { + Codes: []int{400, 500}, + Return: &conf_v1.ErrorPageReturn{ + ActionReturn: conf_v1.ActionReturn{ + Code: 200, + Type: "application/json", + Body: `{\"message\": \"ok\"}`, + }, + Headers: []conf_v1.Header{ + { + Name: "Set-Cookie", + Value: "cookie1=value", + }, + }, + }, + Redirect: nil, + }, + { + Codes: []int{500, 502}, + Return: nil, + Redirect: &conf_v1.ErrorPageRedirect{ + ActionRedirect: conf_v1.ActionRedirect{ + URL: "http://nginx.com", + Code: 301, + }, + }, + }, + } upstreamNamer := NewUpstreamNamerForVirtualServer(&virtualServer) variableNamer := newVariableNamer(&virtualServer) index := 1 + scIndex := 2 expected := routingCfg{ - SplitClients: []version2.SplitClient{ + Maps: []version2.Map{ { - Source: "$request_id", - Variable: "$vs_default_cafe_splits_1", - Distributions: []version2.Distribution{ + Source: "$http_x_version", + Variable: "$vs_default_cafe_matches_1_match_0_cond_0", + Parameters: []version2.Parameter{ { - Weight: "90%", - Value: "/internal_location_splits_1_split_0", + Value: `"v1"`, + Result: "$vs_default_cafe_matches_1_match_0_cond_1", }, { - Weight: "10%", - Value: "/internal_location_splits_1_split_1", + Value: "default", + Result: "0", + }, + }, + }, + { + Source: "$cookie_user", + Variable: "$vs_default_cafe_matches_1_match_0_cond_1", + Parameters: []version2.Parameter{ + { + Value: `"john"`, + Result: "$vs_default_cafe_matches_1_match_0_cond_2", + }, + { + Value: "default", + Result: "0", + }, + }, + }, + { + Source: "$arg_answer", + Variable: "$vs_default_cafe_matches_1_match_0_cond_2", + Parameters: []version2.Parameter{ + { + Value: `"yes"`, + Result: "$vs_default_cafe_matches_1_match_0_cond_3", + }, + { + Value: "default", + Result: "0", + }, + }, + }, + { + Source: "$request_method", + Variable: "$vs_default_cafe_matches_1_match_0_cond_3", + Parameters: []version2.Parameter{ + { + Value: `"GET"`, + Result: "1", + }, + { + Value: "default", + Result: "0", + }, + }, + }, + { + Source: "$http_x_version", + Variable: "$vs_default_cafe_matches_1_match_1_cond_0", + Parameters: []version2.Parameter{ + { + Value: `"v2"`, + Result: "$vs_default_cafe_matches_1_match_1_cond_1", + }, + { + Value: "default", + Result: "0", + }, + }, + }, + { + Source: "$cookie_user", + Variable: "$vs_default_cafe_matches_1_match_1_cond_1", + Parameters: []version2.Parameter{ + { + Value: `"paul"`, + Result: "$vs_default_cafe_matches_1_match_1_cond_2", + }, + { + Value: "default", + Result: "0", + }, + }, + }, + { + Source: "$arg_answer", + Variable: "$vs_default_cafe_matches_1_match_1_cond_2", + Parameters: []version2.Parameter{ + { + Value: `"no"`, + Result: "$vs_default_cafe_matches_1_match_1_cond_3", + }, + { + Value: "default", + Result: "0", + }, + }, + }, + { + Source: "$request_method", + Variable: "$vs_default_cafe_matches_1_match_1_cond_3", + Parameters: []version2.Parameter{ + { + Value: `"POST"`, + Result: "1", + }, + { + Value: "default", + Result: "0", + }, + }, + }, + { + Source: "$vs_default_cafe_matches_1_match_0_cond_0$vs_default_cafe_matches_1_match_1_cond_0", + Variable: "$vs_default_cafe_matches_1", + Parameters: []version2.Parameter{ + { + Value: "~^1", + Result: "/internal_location_matches_1_match_0", + }, + { + Value: "~^01", + Result: "$vs_default_cafe_splits_2", + }, + { + Value: "default", + Result: "/internal_location_matches_1_default", }, }, }, }, Locations: []version2.Location{ { - Path: "/internal_location_splits_1_split_0", + Path: "/internal_location_matches_1_match_0", ProxyPass: "http://vs_default_cafe_coffee-v1$request_uri", ProxyNextUpstream: "error timeout", ProxyNextUpstreamTimeout: "0s", ProxyNextUpstreamTries: 0, + ProxyInterceptErrors: true, Internal: true, - ProxySSLName: "coffee-v1.default.svc", - ProxyPassRequestHeaders: true, - ProxySetHeaders: []version2.Header{{Name: "Host", Value: "$host"}}, - ServiceName: "coffee-v1", - IsVSR: true, - VSRName: "coffee", - VSRNamespace: "default", + ErrorPages: []version2.ErrorPage{ + { + Name: "@error_page_2_0", + Codes: "400 500", + ResponseCode: 200, + }, + { + Name: "http://nginx.com", + Codes: "500 502", + ResponseCode: 301, + }, + }, + ProxySSLName: "coffee-v1.default.svc", + ProxyPassRequestHeaders: true, + ProxySetHeaders: []version2.Header{{Name: "Host", Value: "$host"}}, + ServiceName: "coffee-v1", + IsVSR: false, + VSRName: "", + VSRNamespace: "", }, { - Path: "/internal_location_splits_1_split_1", + Path: "/internal_location_splits_2_split_0", + ProxyPass: "http://vs_default_cafe_coffee-v1$request_uri", + ProxyNextUpstream: "error timeout", + ProxyNextUpstreamTimeout: "0s", + ProxyNextUpstreamTries: 0, + ProxyInterceptErrors: true, + Internal: true, + ErrorPages: []version2.ErrorPage{ + { + Name: "@error_page_2_0", + Codes: "400 500", + ResponseCode: 200, + }, + { + Name: "http://nginx.com", + Codes: "500 502", + ResponseCode: 301, + }, + }, + ProxySSLName: "coffee-v1.default.svc", + ProxyPassRequestHeaders: true, + ProxySetHeaders: []version2.Header{{Name: "Host", Value: "$host"}}, + ServiceName: "coffee-v1", + IsVSR: false, + VSRName: "", + VSRNamespace: "", + }, + { + Path: "/internal_location_splits_2_split_1", ProxyPass: "http://vs_default_cafe_coffee-v2$request_uri", ProxyNextUpstream: "error timeout", ProxyNextUpstreamTimeout: "0s", ProxyNextUpstreamTries: 0, + ProxyInterceptErrors: true, Internal: true, - ProxySSLName: "coffee-v2.default.svc", - ProxyPassRequestHeaders: true, - ProxySetHeaders: []version2.Header{{Name: "Host", Value: "$host"}}, - ServiceName: "coffee-v2", - IsVSR: true, - VSRName: "coffee", - VSRNamespace: "default", + ErrorPages: []version2.ErrorPage{ + { + Name: "@error_page_2_0", + Codes: "400 500", + ResponseCode: 200, + }, + { + Name: "http://nginx.com", + Codes: "500 502", + ResponseCode: 301, + }, + }, + ProxySSLName: "coffee-v2.default.svc", + ProxyPassRequestHeaders: true, + ProxySetHeaders: []version2.Header{{Name: "Host", Value: "$host"}}, + ServiceName: "coffee-v2", + IsVSR: false, + VSRName: "", + VSRNamespace: "", + }, + { + Path: "/internal_location_matches_1_default", + ProxyPass: "http://vs_default_cafe_tea$request_uri", + ProxyNextUpstream: "error timeout", + ProxyNextUpstreamTimeout: "0s", + ProxyNextUpstreamTries: 0, + ProxyInterceptErrors: true, + Internal: true, + ErrorPages: []version2.ErrorPage{ + { + Name: "@error_page_2_0", + Codes: "400 500", + ResponseCode: 200, + }, + { + Name: "http://nginx.com", + Codes: "500 502", + ResponseCode: 301, + }, + }, + ProxySSLName: "tea.default.svc", + ProxyPassRequestHeaders: true, + ProxySetHeaders: []version2.Header{{Name: "Host", Value: "$host"}}, + ServiceName: "tea", + IsVSR: false, + VSRName: "", + VSRNamespace: "", }, }, InternalRedirectLocation: version2.InternalRedirectLocation{ Path: "/", - Destination: "$vs_default_cafe_splits_1", + Destination: "$vs_default_cafe_matches_1", + }, + SplitClients: []version2.SplitClient{ + { + Source: "$request_id", + Variable: "$vs_default_cafe_splits_2", + Distributions: []version2.Distribution{ + { + Weight: "90%", + Value: "/internal_location_splits_2_split_0", + }, + { + Weight: "10%", + Value: "/internal_location_splits_2_split_1", + }, + }, + }, }, } cfgParams := ConfigParams{} - locSnippet := "" enableSnippets := false + locSnippets := "" crUpstreams := map[string]conf_v1.Upstream{ - "vs_default_cafe_coffee-v1": { - Service: "coffee-v1", - }, - "vs_default_cafe_coffee-v2": { - Service: "coffee-v2", - }, + "vs_default_cafe_coffee-v1": {Service: "coffee-v1"}, + "vs_default_cafe_coffee-v2": {Service: "coffee-v2"}, + "vs_default_cafe_tea": {Service: "tea"}, } errorPageDetails := errorPageDetails{ - pages: route.ErrorPages, - index: 0, + pages: errorPages, + index: 2, owner: nil, } - result := generateDefaultSplitsConfig(route, upstreamNamer, crUpstreams, variableNamer, index, &cfgParams, - errorPageDetails, "", locSnippet, enableSnippets, 0, true, "coffee", "default", Warnings{}) + result := generateMatchesConfig( + route, + upstreamNamer, + crUpstreams, + variableNamer, + index, + scIndex, + &cfgParams, + errorPageDetails, + locSnippets, + enableSnippets, + 0, + false, + "", + "", + Warnings{}, + ) if !reflect.DeepEqual(result, expected) { - t.Errorf("generateDefaultSplitsConfig() returned \n%+v but expected \n%+v", result, expected) + t.Errorf("generateMatchesConfig() returned \n%+v but expected \n%+v", result, expected) } } -func TestGenerateMatchesConfig(t *testing.T) { +func TestGenerateMatchesConfigWithMultipleSplits(t *testing.T) { t.Parallel() route := conf_v1.Route{ Path: "/", @@ -6510,22 +7697,21 @@ func TestGenerateMatchesConfig(t *testing.T) { Header: "x-version", Value: "v1", }, + }, + Splits: []conf_v1.Split{ { - Cookie: "user", - Value: "john", - }, - { - Argument: "answer", - Value: "yes", + Weight: 30, + Action: &conf_v1.Action{ + Pass: "coffee-v1", + }, }, { - Variable: "$request_method", - Value: "GET", + Weight: 70, + Action: &conf_v1.Action{ + Pass: "coffee-v2", + }, }, }, - Action: &conf_v1.Action{ - Pass: "coffee-v1", - }, }, { Conditions: []conf_v1.Condition{ @@ -6533,37 +7719,36 @@ func TestGenerateMatchesConfig(t *testing.T) { Header: "x-version", Value: "v2", }, - { - Cookie: "user", - Value: "paul", - }, - { - Argument: "answer", - Value: "no", - }, - { - Variable: "$request_method", - Value: "POST", - }, }, Splits: []conf_v1.Split{ { Weight: 90, Action: &conf_v1.Action{ - Pass: "coffee-v1", + Pass: "coffee-v2", }, }, { Weight: 10, Action: &conf_v1.Action{ - Pass: "coffee-v2", + Pass: "coffee-v1", }, }, }, }, }, - Action: &conf_v1.Action{ - Pass: "tea", + Splits: []conf_v1.Split{ + { + Weight: 99, + Action: &conf_v1.Action{ + Pass: "coffee-v1", + }, + }, + { + Weight: 1, + Action: &conf_v1.Action{ + Pass: "coffee-v2", + }, + }, }, } virtualServer := conf_v1.VirtualServer{ @@ -6572,6 +7757,10 @@ func TestGenerateMatchesConfig(t *testing.T) { Namespace: "default", }, } + upstreamNamer := NewUpstreamNamerForVirtualServer(&virtualServer) + variableNamer := newVariableNamer(&virtualServer) + index := 1 + scIndex := 2 errorPages := []conf_v1.ErrorPage{ { Codes: []int{400, 500}, @@ -6601,10 +7790,6 @@ func TestGenerateMatchesConfig(t *testing.T) { }, }, } - upstreamNamer := NewUpstreamNamerForVirtualServer(&virtualServer) - variableNamer := newVariableNamer(&virtualServer) - index := 1 - scIndex := 2 expected := routingCfg{ Maps: []version2.Map{ @@ -6614,48 +7799,6 @@ func TestGenerateMatchesConfig(t *testing.T) { Parameters: []version2.Parameter{ { Value: `"v1"`, - Result: "$vs_default_cafe_matches_1_match_0_cond_1", - }, - { - Value: "default", - Result: "0", - }, - }, - }, - { - Source: "$cookie_user", - Variable: "$vs_default_cafe_matches_1_match_0_cond_1", - Parameters: []version2.Parameter{ - { - Value: `"john"`, - Result: "$vs_default_cafe_matches_1_match_0_cond_2", - }, - { - Value: "default", - Result: "0", - }, - }, - }, - { - Source: "$arg_answer", - Variable: "$vs_default_cafe_matches_1_match_0_cond_2", - Parameters: []version2.Parameter{ - { - Value: `"yes"`, - Result: "$vs_default_cafe_matches_1_match_0_cond_3", - }, - { - Value: "default", - Result: "0", - }, - }, - }, - { - Source: "$request_method", - Variable: "$vs_default_cafe_matches_1_match_0_cond_3", - Parameters: []version2.Parameter{ - { - Value: `"GET"`, Result: "1", }, { @@ -6670,7 +7813,7 @@ func TestGenerateMatchesConfig(t *testing.T) { Parameters: []version2.Parameter{ { Value: `"v2"`, - Result: "$vs_default_cafe_matches_1_match_1_cond_1", + Result: "1", }, { Value: "default", @@ -6679,78 +7822,91 @@ func TestGenerateMatchesConfig(t *testing.T) { }, }, { - Source: "$cookie_user", - Variable: "$vs_default_cafe_matches_1_match_1_cond_1", + Source: "$vs_default_cafe_matches_1_match_0_cond_0$vs_default_cafe_matches_1_match_1_cond_0", + Variable: "$vs_default_cafe_matches_1", Parameters: []version2.Parameter{ { - Value: `"paul"`, - Result: "$vs_default_cafe_matches_1_match_1_cond_2", - }, - { - Value: "default", - Result: "0", + Value: "~^1", + Result: "$vs_default_cafe_splits_2", }, - }, - }, - { - Source: "$arg_answer", - Variable: "$vs_default_cafe_matches_1_match_1_cond_2", - Parameters: []version2.Parameter{ { - Value: `"no"`, - Result: "$vs_default_cafe_matches_1_match_1_cond_3", + Value: "~^01", + Result: "$vs_default_cafe_splits_3", }, { Value: "default", - Result: "0", + Result: "$vs_default_cafe_splits_4", }, }, }, + }, + Locations: []version2.Location{ { - Source: "$request_method", - Variable: "$vs_default_cafe_matches_1_match_1_cond_3", - Parameters: []version2.Parameter{ + Path: "/internal_location_splits_2_split_0", + ProxyPass: "http://vs_default_cafe_coffee-v1$request_uri", + ProxyNextUpstream: "error timeout", + ProxyNextUpstreamTimeout: "0s", + ProxyNextUpstreamTries: 0, + Internal: true, + ErrorPages: []version2.ErrorPage{ { - Value: `"POST"`, - Result: "1", + Name: "@error_page_0_0", + Codes: "400 500", + ResponseCode: 200, }, { - Value: "default", - Result: "0", + Name: "http://nginx.com", + Codes: "500 502", + ResponseCode: 301, }, }, + ProxyInterceptErrors: true, + ProxySSLName: "coffee-v1.default.svc", + ProxyPassRequestHeaders: true, + ProxySetHeaders: []version2.Header{{Name: "Host", Value: "$host"}}, + ServiceName: "coffee-v1", + IsVSR: true, + VSRName: "coffee", + VSRNamespace: "default", }, { - Source: "$vs_default_cafe_matches_1_match_0_cond_0$vs_default_cafe_matches_1_match_1_cond_0", - Variable: "$vs_default_cafe_matches_1", - Parameters: []version2.Parameter{ - { - Value: "~^1", - Result: "/internal_location_matches_1_match_0", - }, + Path: "/internal_location_splits_2_split_1", + ProxyPass: "http://vs_default_cafe_coffee-v2$request_uri", + ProxyNextUpstream: "error timeout", + ProxyNextUpstreamTimeout: "0s", + ProxyNextUpstreamTries: 0, + Internal: true, + ErrorPages: []version2.ErrorPage{ { - Value: "~^01", - Result: "$vs_default_cafe_splits_2", + Name: "@error_page_0_0", + Codes: "400 500", + ResponseCode: 200, }, { - Value: "default", - Result: "/internal_location_matches_1_default", + Name: "http://nginx.com", + Codes: "500 502", + ResponseCode: 301, }, }, + ProxyInterceptErrors: true, + ProxySSLName: "coffee-v2.default.svc", + ProxyPassRequestHeaders: true, + ProxySetHeaders: []version2.Header{{Name: "Host", Value: "$host"}}, + ServiceName: "coffee-v2", + IsVSR: true, + VSRName: "coffee", + VSRNamespace: "default", }, - }, - Locations: []version2.Location{ { - Path: "/internal_location_matches_1_match_0", - ProxyPass: "http://vs_default_cafe_coffee-v1$request_uri", + Path: "/internal_location_splits_3_split_0", + ProxyPass: "http://vs_default_cafe_coffee-v2$request_uri", ProxyNextUpstream: "error timeout", ProxyNextUpstreamTimeout: "0s", ProxyNextUpstreamTries: 0, - ProxyInterceptErrors: true, Internal: true, ErrorPages: []version2.ErrorPage{ { - Name: "@error_page_2_0", + Name: "@error_page_0_0", Codes: "400 500", ResponseCode: 200, }, @@ -6760,25 +7916,25 @@ func TestGenerateMatchesConfig(t *testing.T) { ResponseCode: 301, }, }, - ProxySSLName: "coffee-v1.default.svc", + ProxyInterceptErrors: true, + ProxySSLName: "coffee-v2.default.svc", ProxyPassRequestHeaders: true, ProxySetHeaders: []version2.Header{{Name: "Host", Value: "$host"}}, - ServiceName: "coffee-v1", - IsVSR: false, - VSRName: "", - VSRNamespace: "", + ServiceName: "coffee-v2", + IsVSR: true, + VSRName: "coffee", + VSRNamespace: "default", }, { - Path: "/internal_location_splits_2_split_0", + Path: "/internal_location_splits_3_split_1", ProxyPass: "http://vs_default_cafe_coffee-v1$request_uri", ProxyNextUpstream: "error timeout", ProxyNextUpstreamTimeout: "0s", ProxyNextUpstreamTries: 0, - ProxyInterceptErrors: true, Internal: true, ErrorPages: []version2.ErrorPage{ { - Name: "@error_page_2_0", + Name: "@error_page_0_0", Codes: "400 500", ResponseCode: 200, }, @@ -6788,25 +7944,25 @@ func TestGenerateMatchesConfig(t *testing.T) { ResponseCode: 301, }, }, + ProxyInterceptErrors: true, ProxySSLName: "coffee-v1.default.svc", ProxyPassRequestHeaders: true, ProxySetHeaders: []version2.Header{{Name: "Host", Value: "$host"}}, ServiceName: "coffee-v1", - IsVSR: false, - VSRName: "", - VSRNamespace: "", + IsVSR: true, + VSRName: "coffee", + VSRNamespace: "default", }, { - Path: "/internal_location_splits_2_split_1", - ProxyPass: "http://vs_default_cafe_coffee-v2$request_uri", + Path: "/internal_location_splits_4_split_0", + ProxyPass: "http://vs_default_cafe_coffee-v1$request_uri", ProxyNextUpstream: "error timeout", ProxyNextUpstreamTimeout: "0s", ProxyNextUpstreamTries: 0, - ProxyInterceptErrors: true, Internal: true, ErrorPages: []version2.ErrorPage{ { - Name: "@error_page_2_0", + Name: "@error_page_0_0", Codes: "400 500", ResponseCode: 200, }, @@ -6816,25 +7972,25 @@ func TestGenerateMatchesConfig(t *testing.T) { ResponseCode: 301, }, }, - ProxySSLName: "coffee-v2.default.svc", + ProxyInterceptErrors: true, + ProxySSLName: "coffee-v1.default.svc", ProxyPassRequestHeaders: true, ProxySetHeaders: []version2.Header{{Name: "Host", Value: "$host"}}, - ServiceName: "coffee-v2", - IsVSR: false, - VSRName: "", - VSRNamespace: "", + ServiceName: "coffee-v1", + IsVSR: true, + VSRName: "coffee", + VSRNamespace: "default", }, { - Path: "/internal_location_matches_1_default", - ProxyPass: "http://vs_default_cafe_tea$request_uri", + Path: "/internal_location_splits_4_split_1", + ProxyPass: "http://vs_default_cafe_coffee-v2$request_uri", ProxyNextUpstream: "error timeout", ProxyNextUpstreamTimeout: "0s", ProxyNextUpstreamTries: 0, - ProxyInterceptErrors: true, Internal: true, ErrorPages: []version2.ErrorPage{ { - Name: "@error_page_2_0", + Name: "@error_page_0_0", Codes: "400 500", ResponseCode: 200, }, @@ -6844,13 +8000,14 @@ func TestGenerateMatchesConfig(t *testing.T) { ResponseCode: 301, }, }, - ProxySSLName: "tea.default.svc", + ProxyInterceptErrors: true, + ProxySSLName: "coffee-v2.default.svc", ProxyPassRequestHeaders: true, ProxySetHeaders: []version2.Header{{Name: "Host", Value: "$host"}}, - ServiceName: "tea", - IsVSR: false, - VSRName: "", - VSRNamespace: "", + ServiceName: "coffee-v2", + IsVSR: true, + VSRName: "coffee", + VSRNamespace: "default", }, }, InternalRedirectLocation: version2.InternalRedirectLocation{ @@ -6863,15 +8020,43 @@ func TestGenerateMatchesConfig(t *testing.T) { Variable: "$vs_default_cafe_splits_2", Distributions: []version2.Distribution{ { - Weight: "90%", + Weight: "30%", Value: "/internal_location_splits_2_split_0", }, { - Weight: "10%", + Weight: "70%", Value: "/internal_location_splits_2_split_1", }, }, }, + { + Source: "$request_id", + Variable: "$vs_default_cafe_splits_3", + Distributions: []version2.Distribution{ + { + Weight: "90%", + Value: "/internal_location_splits_3_split_0", + }, + { + Weight: "10%", + Value: "/internal_location_splits_3_split_1", + }, + }, + }, + { + Source: "$request_id", + Variable: "$vs_default_cafe_splits_4", + Distributions: []version2.Distribution{ + { + Weight: "99%", + Value: "/internal_location_splits_4_split_0", + }, + { + Weight: "1%", + Value: "/internal_location_splits_4_split_1", + }, + }, + }, }, } @@ -6881,12 +8066,11 @@ func TestGenerateMatchesConfig(t *testing.T) { crUpstreams := map[string]conf_v1.Upstream{ "vs_default_cafe_coffee-v1": {Service: "coffee-v1"}, "vs_default_cafe_coffee-v2": {Service: "coffee-v2"}, - "vs_default_cafe_tea": {Service: "tea"}, } errorPageDetails := errorPageDetails{ pages: errorPages, - index: 2, + index: 0, owner: nil, } @@ -6902,9 +8086,9 @@ func TestGenerateMatchesConfig(t *testing.T) { locSnippets, enableSnippets, 0, - false, - "", - "", + true, + "coffee", + "default", Warnings{}, ) if !reflect.DeepEqual(result, expected) { @@ -6912,2470 +8096,2647 @@ func TestGenerateMatchesConfig(t *testing.T) { } } -func TestGenerateMatchesConfigWithMultipleSplits(t *testing.T) { +func TestGenerateValueForMatchesRouteMap(t *testing.T) { t.Parallel() - route := conf_v1.Route{ - Path: "/", - Matches: []conf_v1.Match{ - { - Conditions: []conf_v1.Condition{ - { - Header: "x-version", - Value: "v1", - }, + tests := []struct { + input string + expectedValue string + expectedIsNegative bool + }{ + { + input: "default", + expectedValue: `\default`, + expectedIsNegative: false, + }, + { + input: "!default", + expectedValue: `\default`, + expectedIsNegative: true, + }, + { + input: "hostnames", + expectedValue: `\hostnames`, + expectedIsNegative: false, + }, + { + input: "include", + expectedValue: `\include`, + expectedIsNegative: false, + }, + { + input: "volatile", + expectedValue: `\volatile`, + expectedIsNegative: false, + }, + { + input: "abc", + expectedValue: `"abc"`, + expectedIsNegative: false, + }, + { + input: "!abc", + expectedValue: `"abc"`, + expectedIsNegative: true, + }, + { + input: "", + expectedValue: `""`, + expectedIsNegative: false, + }, + { + input: "!", + expectedValue: `""`, + expectedIsNegative: true, + }, + } + + for _, test := range tests { + resultValue, resultIsNegative := generateValueForMatchesRouteMap(test.input) + if resultValue != test.expectedValue { + t.Errorf("generateValueForMatchesRouteMap(%q) returned %q but expected %q as the value", test.input, resultValue, test.expectedValue) + } + if resultIsNegative != test.expectedIsNegative { + t.Errorf("generateValueForMatchesRouteMap(%q) returned %v but expected %v as the isNegative", test.input, resultIsNegative, test.expectedIsNegative) + } + } +} + +func TestGenerateParametersForMatchesRouteMap(t *testing.T) { + t.Parallel() + tests := []struct { + inputMatchedValue string + inputSuccessfulResult string + expected []version2.Parameter + }{ + { + inputMatchedValue: "abc", + inputSuccessfulResult: "1", + expected: []version2.Parameter{ + { + Value: `"abc"`, + Result: "1", }, - Splits: []conf_v1.Split{ - { - Weight: 30, - Action: &conf_v1.Action{ - Pass: "coffee-v1", - }, - }, - { - Weight: 70, - Action: &conf_v1.Action{ - Pass: "coffee-v2", - }, - }, + { + Value: "default", + Result: "0", }, }, - { - Conditions: []conf_v1.Condition{ - { - Header: "x-version", - Value: "v2", - }, + }, + { + inputMatchedValue: "!abc", + inputSuccessfulResult: "1", + expected: []version2.Parameter{ + { + Value: `"abc"`, + Result: "0", }, - Splits: []conf_v1.Split{ - { - Weight: 90, - Action: &conf_v1.Action{ - Pass: "coffee-v2", - }, - }, - { - Weight: 10, - Action: &conf_v1.Action{ - Pass: "coffee-v1", - }, - }, + { + Value: "default", + Result: "1", }, }, }, - Splits: []conf_v1.Split{ - { - Weight: 99, - Action: &conf_v1.Action{ - Pass: "coffee-v1", - }, + } + + for _, test := range tests { + result := generateParametersForMatchesRouteMap(test.inputMatchedValue, test.inputSuccessfulResult) + if !reflect.DeepEqual(result, test.expected) { + t.Errorf("generateParametersForMatchesRouteMap(%q, %q) returned %v but expected %v", test.inputMatchedValue, test.inputSuccessfulResult, result, test.expected) + } + } +} + +func TestGetNameForSourceForMatchesRouteMapFromCondition(t *testing.T) { + t.Parallel() + tests := []struct { + input conf_v1.Condition + expected string + }{ + { + input: conf_v1.Condition{ + Header: "x-version", + }, + expected: "$http_x_version", + }, + { + input: conf_v1.Condition{ + Cookie: "mycookie", }, - { - Weight: 1, - Action: &conf_v1.Action{ - Pass: "coffee-v2", - }, + expected: "$cookie_mycookie", + }, + { + input: conf_v1.Condition{ + Argument: "arg", }, + expected: "$arg_arg", }, - } - virtualServer := conf_v1.VirtualServer{ - ObjectMeta: meta_v1.ObjectMeta{ - Name: "cafe", - Namespace: "default", + { + input: conf_v1.Condition{ + Variable: "$request_method", + }, + expected: "$request_method", }, } - upstreamNamer := NewUpstreamNamerForVirtualServer(&virtualServer) - variableNamer := newVariableNamer(&virtualServer) - index := 1 - scIndex := 2 - errorPages := []conf_v1.ErrorPage{ + + for _, test := range tests { + result := getNameForSourceForMatchesRouteMapFromCondition(test.input) + if result != test.expected { + t.Errorf("getNameForSourceForMatchesRouteMapFromCondition() returned %q but expected %q for input %v", result, test.expected, test.input) + } + } +} + +func TestGenerateLBMethod(t *testing.T) { + t.Parallel() + defaultMethod := "random two least_conn" + + tests := []struct { + input string + expected string + }{ { - Codes: []int{400, 500}, - Return: &conf_v1.ErrorPageReturn{ - ActionReturn: conf_v1.ActionReturn{ - Code: 200, - Type: "application/json", - Body: `{\"message\": \"ok\"}`, - }, - Headers: []conf_v1.Header{ - { - Name: "Set-Cookie", - Value: "cookie1=value", - }, - }, - }, - Redirect: nil, + input: "", + expected: defaultMethod, }, { - Codes: []int{500, 502}, - Return: nil, - Redirect: &conf_v1.ErrorPageRedirect{ - ActionRedirect: conf_v1.ActionRedirect{ - URL: "http://nginx.com", - Code: 301, - }, - }, + input: "round_robin", + expected: "", + }, + { + input: "random", + expected: "random", }, } + for _, test := range tests { + result := generateLBMethod(test.input, defaultMethod) + if result != test.expected { + t.Errorf("generateLBMethod() returned %q but expected %q for input '%v'", result, test.expected, test.input) + } + } +} - expected := routingCfg{ - Maps: []version2.Map{ - { - Source: "$http_x_version", - Variable: "$vs_default_cafe_matches_1_match_0_cond_0", - Parameters: []version2.Parameter{ - { - Value: `"v1"`, - Result: "1", - }, - { - Value: "default", - Result: "0", - }, - }, - }, - { - Source: "$http_x_version", - Variable: "$vs_default_cafe_matches_1_match_1_cond_0", - Parameters: []version2.Parameter{ - { - Value: `"v2"`, - Result: "1", - }, - { - Value: "default", - Result: "0", - }, - }, - }, - { - Source: "$vs_default_cafe_matches_1_match_0_cond_0$vs_default_cafe_matches_1_match_1_cond_0", - Variable: "$vs_default_cafe_matches_1", - Parameters: []version2.Parameter{ - { - Value: "~^1", - Result: "$vs_default_cafe_splits_2", - }, - { - Value: "~^01", - Result: "$vs_default_cafe_splits_3", - }, - { - Value: "default", - Result: "$vs_default_cafe_splits_4", - }, - }, - }, +func TestUpstreamHasKeepalive(t *testing.T) { + t.Parallel() + noKeepalive := 0 + keepalive := 32 + + tests := []struct { + upstream conf_v1.Upstream + cfgParams *ConfigParams + expected bool + msg string + }{ + { + conf_v1.Upstream{}, + &ConfigParams{Keepalive: keepalive}, + true, + "upstream keepalive not set, configparam keepalive set", }, - Locations: []version2.Location{ - { - Path: "/internal_location_splits_2_split_0", - ProxyPass: "http://vs_default_cafe_coffee-v1$request_uri", - ProxyNextUpstream: "error timeout", - ProxyNextUpstreamTimeout: "0s", - ProxyNextUpstreamTries: 0, - Internal: true, - ErrorPages: []version2.ErrorPage{ - { - Name: "@error_page_0_0", - Codes: "400 500", - ResponseCode: 200, - }, - { - Name: "http://nginx.com", - Codes: "500 502", - ResponseCode: 301, + { + conf_v1.Upstream{Keepalive: &noKeepalive}, + &ConfigParams{Keepalive: keepalive}, + false, + "upstream keepalive set to 0, configparam keepalive set", + }, + { + conf_v1.Upstream{Keepalive: &keepalive}, + &ConfigParams{Keepalive: noKeepalive}, + true, + "upstream keepalive set, configparam keepalive set to 0", + }, + } + + for _, test := range tests { + result := upstreamHasKeepalive(test.upstream, test.cfgParams) + if result != test.expected { + t.Errorf("upstreamHasKeepalive() returned %v, but expected %v for the case of %v", result, test.expected, test.msg) + } + } +} + +func TestNewHealthCheckWithDefaults(t *testing.T) { + t.Parallel() + upstreamName := "test-upstream" + baseCfgParams := &ConfigParams{ + ProxySendTimeout: "5s", + ProxyReadTimeout: "5s", + ProxyConnectTimeout: "5s", + } + expected := &version2.HealthCheck{ + Name: upstreamName, + ProxySendTimeout: "5s", + ProxyReadTimeout: "5s", + ProxyConnectTimeout: "5s", + ProxyPass: fmt.Sprintf("http://%v", upstreamName), + URI: "/", + Interval: "5s", + Jitter: "0s", + KeepaliveTime: "60s", + Fails: 1, + Passes: 1, + Headers: make(map[string]string), + } + + result := newHealthCheckWithDefaults(conf_v1.Upstream{}, upstreamName, baseCfgParams) + + if !reflect.DeepEqual(result, expected) { + t.Errorf("newHealthCheckWithDefaults returned \n%v but expected \n%v", result, expected) + } +} + +func TestGenerateHealthCheck(t *testing.T) { + t.Parallel() + upstreamName := "test-upstream" + tests := []struct { + upstream conf_v1.Upstream + upstreamName string + expected *version2.HealthCheck + msg string + }{ + { + upstream: conf_v1.Upstream{ + HealthCheck: &conf_v1.HealthCheck{ + Enable: true, + Path: "/healthz", + Interval: "5s", + Jitter: "2s", + KeepaliveTime: "120s", + Fails: 3, + Passes: 2, + Port: 8080, + ConnectTimeout: "20s", + SendTimeout: "20s", + ReadTimeout: "20s", + Headers: []conf_v1.Header{ + { + Name: "Host", + Value: "my.service", + }, + { + Name: "User-Agent", + Value: "nginx", + }, }, + StatusMatch: "! 500", }, - ProxyInterceptErrors: true, - ProxySSLName: "coffee-v1.default.svc", - ProxyPassRequestHeaders: true, - ProxySetHeaders: []version2.Header{{Name: "Host", Value: "$host"}}, - ServiceName: "coffee-v1", - IsVSR: true, - VSRName: "coffee", - VSRNamespace: "default", }, - { - Path: "/internal_location_splits_2_split_1", - ProxyPass: "http://vs_default_cafe_coffee-v2$request_uri", - ProxyNextUpstream: "error timeout", - ProxyNextUpstreamTimeout: "0s", - ProxyNextUpstreamTries: 0, - Internal: true, - ErrorPages: []version2.ErrorPage{ - { - Name: "@error_page_0_0", - Codes: "400 500", - ResponseCode: 200, - }, - { - Name: "http://nginx.com", - Codes: "500 502", - ResponseCode: 301, - }, + upstreamName: upstreamName, + expected: &version2.HealthCheck{ + Name: upstreamName, + ProxyConnectTimeout: "20s", + ProxySendTimeout: "20s", + ProxyReadTimeout: "20s", + ProxyPass: fmt.Sprintf("http://%v", upstreamName), + URI: "/healthz", + Interval: "5s", + Jitter: "2s", + KeepaliveTime: "120s", + Fails: 3, + Passes: 2, + Port: 8080, + Headers: map[string]string{ + "Host": "my.service", + "User-Agent": "nginx", }, - ProxyInterceptErrors: true, - ProxySSLName: "coffee-v2.default.svc", - ProxyPassRequestHeaders: true, - ProxySetHeaders: []version2.Header{{Name: "Host", Value: "$host"}}, - ServiceName: "coffee-v2", - IsVSR: true, - VSRName: "coffee", - VSRNamespace: "default", + Match: fmt.Sprintf("%v_match", upstreamName), }, - { - Path: "/internal_location_splits_3_split_0", - ProxyPass: "http://vs_default_cafe_coffee-v2$request_uri", - ProxyNextUpstream: "error timeout", - ProxyNextUpstreamTimeout: "0s", - ProxyNextUpstreamTries: 0, - Internal: true, - ErrorPages: []version2.ErrorPage{ - { - Name: "@error_page_0_0", - Codes: "400 500", - ResponseCode: 200, - }, - { - Name: "http://nginx.com", - Codes: "500 502", - ResponseCode: 301, - }, + msg: "HealthCheck with changed parameters", + }, + { + upstream: conf_v1.Upstream{ + HealthCheck: &conf_v1.HealthCheck{ + Enable: true, }, - ProxyInterceptErrors: true, - ProxySSLName: "coffee-v2.default.svc", - ProxyPassRequestHeaders: true, - ProxySetHeaders: []version2.Header{{Name: "Host", Value: "$host"}}, - ServiceName: "coffee-v2", - IsVSR: true, - VSRName: "coffee", - VSRNamespace: "default", + ProxyConnectTimeout: "30s", + ProxyReadTimeout: "30s", + ProxySendTimeout: "30s", }, - { - Path: "/internal_location_splits_3_split_1", - ProxyPass: "http://vs_default_cafe_coffee-v1$request_uri", - ProxyNextUpstream: "error timeout", - ProxyNextUpstreamTimeout: "0s", - ProxyNextUpstreamTries: 0, - Internal: true, - ErrorPages: []version2.ErrorPage{ - { - Name: "@error_page_0_0", - Codes: "400 500", - ResponseCode: 200, - }, - { - Name: "http://nginx.com", - Codes: "500 502", - ResponseCode: 301, - }, - }, - ProxyInterceptErrors: true, - ProxySSLName: "coffee-v1.default.svc", - ProxyPassRequestHeaders: true, - ProxySetHeaders: []version2.Header{{Name: "Host", Value: "$host"}}, - ServiceName: "coffee-v1", - IsVSR: true, - VSRName: "coffee", - VSRNamespace: "default", + upstreamName: upstreamName, + expected: &version2.HealthCheck{ + Name: upstreamName, + ProxyConnectTimeout: "30s", + ProxyReadTimeout: "30s", + ProxySendTimeout: "30s", + ProxyPass: fmt.Sprintf("http://%v", upstreamName), + URI: "/", + Interval: "5s", + Jitter: "0s", + KeepaliveTime: "60s", + Fails: 1, + Passes: 1, + Headers: make(map[string]string), }, - { - Path: "/internal_location_splits_4_split_0", - ProxyPass: "http://vs_default_cafe_coffee-v1$request_uri", - ProxyNextUpstream: "error timeout", - ProxyNextUpstreamTimeout: "0s", - ProxyNextUpstreamTries: 0, - Internal: true, - ErrorPages: []version2.ErrorPage{ - { - Name: "@error_page_0_0", - Codes: "400 500", - ResponseCode: 200, - }, - { - Name: "http://nginx.com", - Codes: "500 502", - ResponseCode: 301, - }, + msg: "HealthCheck with default parameters from Upstream", + }, + { + upstream: conf_v1.Upstream{ + HealthCheck: &conf_v1.HealthCheck{ + Enable: true, }, - ProxyInterceptErrors: true, - ProxySSLName: "coffee-v1.default.svc", - ProxyPassRequestHeaders: true, - ProxySetHeaders: []version2.Header{{Name: "Host", Value: "$host"}}, - ServiceName: "coffee-v1", - IsVSR: true, - VSRName: "coffee", - VSRNamespace: "default", }, - { - Path: "/internal_location_splits_4_split_1", - ProxyPass: "http://vs_default_cafe_coffee-v2$request_uri", - ProxyNextUpstream: "error timeout", - ProxyNextUpstreamTimeout: "0s", - ProxyNextUpstreamTries: 0, - Internal: true, - ErrorPages: []version2.ErrorPage{ - { - Name: "@error_page_0_0", - Codes: "400 500", - ResponseCode: 200, - }, - { - Name: "http://nginx.com", - Codes: "500 502", - ResponseCode: 301, - }, - }, - ProxyInterceptErrors: true, - ProxySSLName: "coffee-v2.default.svc", - ProxyPassRequestHeaders: true, - ProxySetHeaders: []version2.Header{{Name: "Host", Value: "$host"}}, - ServiceName: "coffee-v2", - IsVSR: true, - VSRName: "coffee", - VSRNamespace: "default", + upstreamName: upstreamName, + expected: &version2.HealthCheck{ + Name: upstreamName, + ProxyConnectTimeout: "5s", + ProxyReadTimeout: "5s", + ProxySendTimeout: "5s", + ProxyPass: fmt.Sprintf("http://%v", upstreamName), + URI: "/", + Interval: "5s", + Jitter: "0s", + KeepaliveTime: "60s", + Fails: 1, + Passes: 1, + Headers: make(map[string]string), }, + msg: "HealthCheck with default parameters from ConfigMap (not defined in Upstream)", }, - InternalRedirectLocation: version2.InternalRedirectLocation{ - Path: "/", - Destination: "$vs_default_cafe_matches_1", + { + upstream: conf_v1.Upstream{}, + upstreamName: upstreamName, + expected: nil, + msg: "HealthCheck not enabled", }, - SplitClients: []version2.SplitClient{ - { - Source: "$request_id", - Variable: "$vs_default_cafe_splits_2", - Distributions: []version2.Distribution{ - { - Weight: "30%", - Value: "/internal_location_splits_2_split_0", - }, - { - Weight: "70%", - Value: "/internal_location_splits_2_split_1", - }, + { + upstream: conf_v1.Upstream{ + HealthCheck: &conf_v1.HealthCheck{ + Enable: true, + Interval: "1m 5s", + Jitter: "2m 3s", + KeepaliveTime: "1m 6s", + ConnectTimeout: "1m 10s", + SendTimeout: "1m 20s", + ReadTimeout: "1m 30s", }, }, - { - Source: "$request_id", - Variable: "$vs_default_cafe_splits_3", - Distributions: []version2.Distribution{ - { - Weight: "90%", - Value: "/internal_location_splits_3_split_0", - }, - { - Weight: "10%", - Value: "/internal_location_splits_3_split_1", - }, - }, + upstreamName: upstreamName, + expected: &version2.HealthCheck{ + Name: upstreamName, + ProxyConnectTimeout: "1m10s", + ProxySendTimeout: "1m20s", + ProxyReadTimeout: "1m30s", + ProxyPass: fmt.Sprintf("http://%v", upstreamName), + URI: "/", + Interval: "1m5s", + Jitter: "2m3s", + KeepaliveTime: "1m6s", + Fails: 1, + Passes: 1, + Headers: make(map[string]string), }, - { - Source: "$request_id", - Variable: "$vs_default_cafe_splits_4", - Distributions: []version2.Distribution{ - { - Weight: "99%", - Value: "/internal_location_splits_4_split_0", - }, - { - Weight: "1%", - Value: "/internal_location_splits_4_split_1", - }, + msg: "HealthCheck with time parameters have correct format", + }, + { + upstream: conf_v1.Upstream{ + HealthCheck: &conf_v1.HealthCheck{ + Enable: true, + Mandatory: true, + Persistent: true, }, + ProxyConnectTimeout: "30s", + ProxyReadTimeout: "30s", + ProxySendTimeout: "30s", + }, + upstreamName: upstreamName, + expected: &version2.HealthCheck{ + Name: upstreamName, + ProxyConnectTimeout: "30s", + ProxyReadTimeout: "30s", + ProxySendTimeout: "30s", + ProxyPass: fmt.Sprintf("http://%v", upstreamName), + URI: "/", + Interval: "5s", + Jitter: "0s", + KeepaliveTime: "60s", + Fails: 1, + Passes: 1, + Headers: make(map[string]string), + Mandatory: true, + Persistent: true, }, + msg: "HealthCheck with mandatory and persistent set", }, } - cfgParams := ConfigParams{} - enableSnippets := false - locSnippets := "" - crUpstreams := map[string]conf_v1.Upstream{ - "vs_default_cafe_coffee-v1": {Service: "coffee-v1"}, - "vs_default_cafe_coffee-v2": {Service: "coffee-v2"}, - } - - errorPageDetails := errorPageDetails{ - pages: errorPages, - index: 0, - owner: nil, + baseCfgParams := &ConfigParams{ + ProxySendTimeout: "5s", + ProxyReadTimeout: "5s", + ProxyConnectTimeout: "5s", } - result := generateMatchesConfig( - route, - upstreamNamer, - crUpstreams, - variableNamer, - index, - scIndex, - &cfgParams, - errorPageDetails, - locSnippets, - enableSnippets, - 0, - true, - "coffee", - "default", - Warnings{}, - ) - if !reflect.DeepEqual(result, expected) { - t.Errorf("generateMatchesConfig() returned \n%+v but expected \n%+v", result, expected) + for _, test := range tests { + result := generateHealthCheck(test.upstream, test.upstreamName, baseCfgParams) + if !reflect.DeepEqual(result, test.expected) { + t.Errorf("generateHealthCheck returned \n%v but expected \n%v \n for case: %v", result, test.expected, test.msg) + } } } -func TestGenerateValueForMatchesRouteMap(t *testing.T) { +func TestGenerateGrpcHealthCheck(t *testing.T) { t.Parallel() + upstreamName := "test-upstream" tests := []struct { - input string - expectedValue string - expectedIsNegative bool + upstream conf_v1.Upstream + upstreamName string + expected *version2.HealthCheck + msg string }{ { - input: "default", - expectedValue: `\default`, - expectedIsNegative: false, - }, - { - input: "!default", - expectedValue: `\default`, - expectedIsNegative: true, - }, - { - input: "hostnames", - expectedValue: `\hostnames`, - expectedIsNegative: false, - }, - { - input: "include", - expectedValue: `\include`, - expectedIsNegative: false, - }, - { - input: "volatile", - expectedValue: `\volatile`, - expectedIsNegative: false, - }, - { - input: "abc", - expectedValue: `"abc"`, - expectedIsNegative: false, - }, - { - input: "!abc", - expectedValue: `"abc"`, - expectedIsNegative: true, - }, - { - input: "", - expectedValue: `""`, - expectedIsNegative: false, + upstream: conf_v1.Upstream{ + HealthCheck: &conf_v1.HealthCheck{ + Enable: true, + Interval: "5s", + Jitter: "2s", + KeepaliveTime: "120s", + Fails: 3, + Passes: 2, + Port: 50051, + ConnectTimeout: "20s", + SendTimeout: "20s", + ReadTimeout: "20s", + GRPCStatus: createPointerFromInt(12), + GRPCService: "grpc-service", + Headers: []conf_v1.Header{ + { + Name: "Host", + Value: "my.service", + }, + { + Name: "User-Agent", + Value: "nginx", + }, + }, + }, + Type: "grpc", + }, + upstreamName: upstreamName, + expected: &version2.HealthCheck{ + Name: upstreamName, + ProxyConnectTimeout: "20s", + ProxySendTimeout: "20s", + ProxyReadTimeout: "20s", + ProxyPass: fmt.Sprintf("http://%v", upstreamName), + GRPCPass: fmt.Sprintf("grpc://%v", upstreamName), + Interval: "5s", + Jitter: "2s", + KeepaliveTime: "120s", + Fails: 3, + Passes: 2, + Port: 50051, + GRPCStatus: createPointerFromInt(12), + GRPCService: "grpc-service", + Headers: map[string]string{ + "Host": "my.service", + "User-Agent": "nginx", + }, + }, + msg: "HealthCheck with changed parameters", }, { - input: "!", - expectedValue: `""`, - expectedIsNegative: true, + upstream: conf_v1.Upstream{ + HealthCheck: &conf_v1.HealthCheck{ + Enable: true, + }, + ProxyConnectTimeout: "30s", + ProxyReadTimeout: "30s", + ProxySendTimeout: "30s", + Type: "grpc", + }, + upstreamName: upstreamName, + expected: &version2.HealthCheck{ + Name: upstreamName, + ProxyConnectTimeout: "30s", + ProxyReadTimeout: "30s", + ProxySendTimeout: "30s", + ProxyPass: fmt.Sprintf("http://%v", upstreamName), + GRPCPass: fmt.Sprintf("grpc://%v", upstreamName), + Interval: "5s", + Jitter: "0s", + KeepaliveTime: "60s", + Fails: 1, + Passes: 1, + Headers: make(map[string]string), + }, + msg: "HealthCheck with default parameters from Upstream", }, } + baseCfgParams := &ConfigParams{ + ProxySendTimeout: "5s", + ProxyReadTimeout: "5s", + ProxyConnectTimeout: "5s", + } + for _, test := range tests { - resultValue, resultIsNegative := generateValueForMatchesRouteMap(test.input) - if resultValue != test.expectedValue { - t.Errorf("generateValueForMatchesRouteMap(%q) returned %q but expected %q as the value", test.input, resultValue, test.expectedValue) - } - if resultIsNegative != test.expectedIsNegative { - t.Errorf("generateValueForMatchesRouteMap(%q) returned %v but expected %v as the isNegative", test.input, resultIsNegative, test.expectedIsNegative) + result := generateHealthCheck(test.upstream, test.upstreamName, baseCfgParams) + if !reflect.DeepEqual(result, test.expected) { + t.Errorf("generateHealthCheck returned \n%v but expected \n%v \n for case: %v", result, test.expected, test.msg) } } } -func TestGenerateParametersForMatchesRouteMap(t *testing.T) { +func TestGenerateEndpointsForUpstream(t *testing.T) { t.Parallel() + name := "test" + namespace := "test-namespace" + tests := []struct { - inputMatchedValue string - inputSuccessfulResult string - expected []version2.Parameter + upstream conf_v1.Upstream + vsEx *VirtualServerEx + isPlus bool + isResolverConfigured bool + expected []string + warningsExpected bool + msg string }{ { - inputMatchedValue: "abc", - inputSuccessfulResult: "1", - expected: []version2.Parameter{ - { - Value: `"abc"`, - Result: "1", + upstream: conf_v1.Upstream{ + Service: name, + Port: 80, + }, + vsEx: &VirtualServerEx{ + VirtualServer: &conf_v1.VirtualServer{ + ObjectMeta: meta_v1.ObjectMeta{ + Name: name, + Namespace: namespace, + }, + }, + Endpoints: map[string][]string{ + "test-namespace/test:80": {"example.com:80"}, + }, + ExternalNameSvcs: map[string]bool{ + "test-namespace/test": true, + }, + }, + isPlus: true, + isResolverConfigured: true, + expected: []string{"example.com:80"}, + msg: "ExternalName service", + }, + { + upstream: conf_v1.Upstream{ + Service: name, + Port: 80, + }, + vsEx: &VirtualServerEx{ + VirtualServer: &conf_v1.VirtualServer{ + ObjectMeta: meta_v1.ObjectMeta{ + Name: name, + Namespace: namespace, + }, }, - { - Value: "default", - Result: "0", + Endpoints: map[string][]string{ + "test-namespace/test:80": {"example.com:80"}, + }, + ExternalNameSvcs: map[string]bool{ + "test-namespace/test": true, }, }, + isPlus: true, + isResolverConfigured: false, + warningsExpected: true, + expected: []string{}, + msg: "ExternalName service without resolver configured", }, { - inputMatchedValue: "!abc", - inputSuccessfulResult: "1", - expected: []version2.Parameter{ - { - Value: `"abc"`, - Result: "0", + upstream: conf_v1.Upstream{ + Service: name, + Port: 8080, + }, + vsEx: &VirtualServerEx{ + VirtualServer: &conf_v1.VirtualServer{ + ObjectMeta: meta_v1.ObjectMeta{ + Name: name, + Namespace: namespace, + }, }, - { - Value: "default", - Result: "1", + Endpoints: map[string][]string{ + "test-namespace/test:8080": {"192.168.10.10:8080"}, }, }, + isPlus: false, + isResolverConfigured: false, + expected: []string{"192.168.10.10:8080"}, + msg: "Service with endpoints", }, - } - - for _, test := range tests { - result := generateParametersForMatchesRouteMap(test.inputMatchedValue, test.inputSuccessfulResult) - if !reflect.DeepEqual(result, test.expected) { - t.Errorf("generateParametersForMatchesRouteMap(%q, %q) returned %v but expected %v", test.inputMatchedValue, test.inputSuccessfulResult, result, test.expected) - } - } -} - -func TestGetNameForSourceForMatchesRouteMapFromCondition(t *testing.T) { - t.Parallel() - tests := []struct { - input conf_v1.Condition - expected string - }{ { - input: conf_v1.Condition{ - Header: "x-version", + upstream: conf_v1.Upstream{ + Service: name, + Port: 8080, }, - expected: "$http_x_version", + vsEx: &VirtualServerEx{ + VirtualServer: &conf_v1.VirtualServer{ + ObjectMeta: meta_v1.ObjectMeta{ + Name: name, + Namespace: namespace, + }, + }, + Endpoints: map[string][]string{}, + }, + isPlus: false, + isResolverConfigured: false, + expected: []string{nginx502Server}, + msg: "Service with no endpoints", }, { - input: conf_v1.Condition{ - Cookie: "mycookie", + upstream: conf_v1.Upstream{ + Service: name, + Port: 8080, }, - expected: "$cookie_mycookie", + vsEx: &VirtualServerEx{ + VirtualServer: &conf_v1.VirtualServer{ + ObjectMeta: meta_v1.ObjectMeta{ + Name: name, + Namespace: namespace, + }, + }, + Endpoints: map[string][]string{}, + }, + isPlus: true, + isResolverConfigured: false, + expected: nil, + msg: "Service with no endpoints", }, { - input: conf_v1.Condition{ - Argument: "arg", + upstream: conf_v1.Upstream{ + Service: name, + Port: 8080, + Subselector: map[string]string{"version": "test"}, }, - expected: "$arg_arg", + vsEx: &VirtualServerEx{ + VirtualServer: &conf_v1.VirtualServer{ + ObjectMeta: meta_v1.ObjectMeta{ + Name: name, + Namespace: namespace, + }, + }, + Endpoints: map[string][]string{ + "test-namespace/test_version=test:8080": {"192.168.10.10:8080"}, + }, + }, + isPlus: false, + isResolverConfigured: false, + expected: []string{"192.168.10.10:8080"}, + msg: "Upstream with subselector, with a matching endpoint", }, { - input: conf_v1.Condition{ - Variable: "$request_method", + upstream: conf_v1.Upstream{ + Service: name, + Port: 8080, + Subselector: map[string]string{"version": "test"}, }, - expected: "$request_method", + vsEx: &VirtualServerEx{ + VirtualServer: &conf_v1.VirtualServer{ + ObjectMeta: meta_v1.ObjectMeta{ + Name: name, + Namespace: namespace, + }, + }, + Endpoints: map[string][]string{ + "test-namespace/test:8080": {"192.168.10.10:8080"}, + }, + }, + isPlus: false, + isResolverConfigured: false, + expected: []string{nginx502Server}, + msg: "Upstream with subselector, without a matching endpoint", }, } for _, test := range tests { - result := getNameForSourceForMatchesRouteMapFromCondition(test.input) - if result != test.expected { - t.Errorf("getNameForSourceForMatchesRouteMapFromCondition() returned %q but expected %q for input %v", result, test.expected, test.input) + isWildcardEnabled := false + vsc := newVirtualServerConfigurator( + &ConfigParams{}, + test.isPlus, + test.isResolverConfigured, + &StaticConfigParams{}, + isWildcardEnabled, + ) + result := vsc.generateEndpointsForUpstream(test.vsEx.VirtualServer, namespace, test.upstream, test.vsEx) + if !reflect.DeepEqual(result, test.expected) { + t.Errorf("generateEndpointsForUpstream(isPlus=%v, isResolverConfigured=%v) returned %v, but expected %v for case: %v", + test.isPlus, test.isResolverConfigured, result, test.expected, test.msg) + } + + if len(vsc.warnings) == 0 && test.warningsExpected { + t.Errorf( + "generateEndpointsForUpstream(isPlus=%v, isResolverConfigured=%v) didn't return any warnings for %v but warnings expected", + test.isPlus, + test.isResolverConfigured, + test.upstream, + ) + } + + if len(vsc.warnings) != 0 && !test.warningsExpected { + t.Errorf("generateEndpointsForUpstream(isPlus=%v, isResolverConfigured=%v) returned warnings for %v", + test.isPlus, test.isResolverConfigured, test.upstream) } } } -func TestGenerateLBMethod(t *testing.T) { +func TestGenerateSlowStartForPlusWithInCompatibleLBMethods(t *testing.T) { t.Parallel() - defaultMethod := "random two least_conn" + serviceName := "test-slowstart-with-incompatible-LBMethods" + upstream := conf_v1.Upstream{Service: serviceName, Port: 80, SlowStart: "10s"} + expected := "" - tests := []struct { - input string - expected string - }{ - { - input: "", - expected: defaultMethod, - }, - { - input: "round_robin", - expected: "", - }, - { - input: "random", - expected: "random", - }, + tests := []string{ + "random", + "ip_hash", + "hash 123", + "random two", + "random two least_conn", + "random two least_time=header", + "random two least_time=last_byte", } - for _, test := range tests { - result := generateLBMethod(test.input, defaultMethod) - if result != test.expected { - t.Errorf("generateLBMethod() returned %q but expected %q for input '%v'", result, test.expected, test.input) + + for _, lbMethod := range tests { + vsc := newVirtualServerConfigurator(&ConfigParams{}, true, false, &StaticConfigParams{}, false) + result := vsc.generateSlowStartForPlus(&conf_v1.VirtualServer{}, upstream, lbMethod) + + if !reflect.DeepEqual(result, expected) { + t.Errorf("generateSlowStartForPlus returned %v, but expected %v for lbMethod %v", result, expected, lbMethod) + } + + if len(vsc.warnings) == 0 { + t.Errorf("generateSlowStartForPlus returned no warnings for %v but warnings expected", upstream) } } } -func TestUpstreamHasKeepalive(t *testing.T) { - t.Parallel() - noKeepalive := 0 - keepalive := 32 +func TestGenerateSlowStartForPlus(t *testing.T) { + serviceName := "test-slowstart" tests := []struct { - upstream conf_v1.Upstream - cfgParams *ConfigParams - expected bool - msg string - }{ - { - conf_v1.Upstream{}, - &ConfigParams{Keepalive: keepalive}, - true, - "upstream keepalive not set, configparam keepalive set", - }, + upstream conf_v1.Upstream + lbMethod string + expected string + }{ { - conf_v1.Upstream{Keepalive: &noKeepalive}, - &ConfigParams{Keepalive: keepalive}, - false, - "upstream keepalive set to 0, configparam keepalive set", + upstream: conf_v1.Upstream{Service: serviceName, Port: 80, SlowStart: "", LBMethod: "least_conn"}, + lbMethod: "least_conn", + expected: "", }, { - conf_v1.Upstream{Keepalive: &keepalive}, - &ConfigParams{Keepalive: noKeepalive}, - true, - "upstream keepalive set, configparam keepalive set to 0", + upstream: conf_v1.Upstream{Service: serviceName, Port: 80, SlowStart: "10s", LBMethod: "least_conn"}, + lbMethod: "least_conn", + expected: "10s", }, } for _, test := range tests { - result := upstreamHasKeepalive(test.upstream, test.cfgParams) - if result != test.expected { - t.Errorf("upstreamHasKeepalive() returned %v, but expected %v for the case of %v", result, test.expected, test.msg) + vsc := newVirtualServerConfigurator(&ConfigParams{}, true, false, &StaticConfigParams{}, false) + result := vsc.generateSlowStartForPlus(&conf_v1.VirtualServer{}, test.upstream, test.lbMethod) + if !reflect.DeepEqual(result, test.expected) { + t.Errorf("generateSlowStartForPlus returned %v, but expected %v", result, test.expected) + } + + if len(vsc.warnings) != 0 { + t.Errorf("generateSlowStartForPlus returned warnings for %v", test.upstream) } } } -func TestNewHealthCheckWithDefaults(t *testing.T) { +func TestCreateEndpointsFromUpstream(t *testing.T) { t.Parallel() - upstreamName := "test-upstream" - baseCfgParams := &ConfigParams{ - ProxySendTimeout: "5s", - ProxyReadTimeout: "5s", - ProxyConnectTimeout: "5s", - } - expected := &version2.HealthCheck{ - Name: upstreamName, - ProxySendTimeout: "5s", - ProxyReadTimeout: "5s", - ProxyConnectTimeout: "5s", - ProxyPass: fmt.Sprintf("http://%v", upstreamName), - URI: "/", - Interval: "5s", - Jitter: "0s", - KeepaliveTime: "60s", - Fails: 1, - Passes: 1, - Headers: make(map[string]string), + ups := version2.Upstream{ + Servers: []version2.UpstreamServer{ + { + Address: "10.0.0.20:80", + }, + { + Address: "10.0.0.30:80", + }, + }, } - result := newHealthCheckWithDefaults(conf_v1.Upstream{}, upstreamName, baseCfgParams) + expected := []string{ + "10.0.0.20:80", + "10.0.0.30:80", + } - if !reflect.DeepEqual(result, expected) { - t.Errorf("newHealthCheckWithDefaults returned \n%v but expected \n%v", result, expected) + endpoints := createEndpointsFromUpstream(ups) + if !reflect.DeepEqual(endpoints, expected) { + t.Errorf("createEndpointsFromUpstream returned %v, but expected %v", endpoints, expected) } } -func TestGenerateHealthCheck(t *testing.T) { +func TestGenerateUpstreamWithQueue(t *testing.T) { t.Parallel() - upstreamName := "test-upstream" + serviceName := "test-queue" + tests := []struct { - upstream conf_v1.Upstream - upstreamName string - expected *version2.HealthCheck - msg string + name string + upstream conf_v1.Upstream + isPlus bool + expected version2.Upstream + msg string }{ { - upstream: conf_v1.Upstream{ - HealthCheck: &conf_v1.HealthCheck{ - Enable: true, - Path: "/healthz", - Interval: "5s", - Jitter: "2s", - KeepaliveTime: "120s", - Fails: 3, - Passes: 2, - Port: 8080, - ConnectTimeout: "20s", - SendTimeout: "20s", - ReadTimeout: "20s", - Headers: []conf_v1.Header{ - { - Name: "Host", - Value: "my.service", - }, - { - Name: "User-Agent", - Value: "nginx", - }, - }, - StatusMatch: "! 500", + name: "test-upstream-queue", + upstream: conf_v1.Upstream{Service: serviceName, Port: 80, Queue: &conf_v1.UpstreamQueue{ + Size: 10, + Timeout: "10s", + }}, + isPlus: true, + expected: version2.Upstream{ + UpstreamLabels: version2.UpstreamLabels{ + Service: "test-queue", }, - }, - upstreamName: upstreamName, - expected: &version2.HealthCheck{ - Name: upstreamName, - ProxyConnectTimeout: "20s", - ProxySendTimeout: "20s", - ProxyReadTimeout: "20s", - ProxyPass: fmt.Sprintf("http://%v", upstreamName), - URI: "/healthz", - Interval: "5s", - Jitter: "2s", - KeepaliveTime: "120s", - Fails: 3, - Passes: 2, - Port: 8080, - Headers: map[string]string{ - "Host": "my.service", - "User-Agent": "nginx", + Name: "test-upstream-queue", + Queue: &version2.Queue{ + Size: 10, + Timeout: "10s", }, - Match: fmt.Sprintf("%v_match", upstreamName), }, - msg: "HealthCheck with changed parameters", + msg: "upstream queue with size and timeout", }, { + name: "test-upstream-queue-with-default-timeout", upstream: conf_v1.Upstream{ - HealthCheck: &conf_v1.HealthCheck{ - Enable: true, - }, - ProxyConnectTimeout: "30s", - ProxyReadTimeout: "30s", - ProxySendTimeout: "30s", - }, - upstreamName: upstreamName, - expected: &version2.HealthCheck{ - Name: upstreamName, - ProxyConnectTimeout: "30s", - ProxyReadTimeout: "30s", - ProxySendTimeout: "30s", - ProxyPass: fmt.Sprintf("http://%v", upstreamName), - URI: "/", - Interval: "5s", - Jitter: "0s", - KeepaliveTime: "60s", - Fails: 1, - Passes: 1, - Headers: make(map[string]string), + Service: serviceName, + Port: 80, + Queue: &conf_v1.UpstreamQueue{Size: 10, Timeout: ""}, }, - msg: "HealthCheck with default parameters from Upstream", - }, - { - upstream: conf_v1.Upstream{ - HealthCheck: &conf_v1.HealthCheck{ - Enable: true, + isPlus: true, + expected: version2.Upstream{ + UpstreamLabels: version2.UpstreamLabels{ + Service: "test-queue", }, - }, - upstreamName: upstreamName, - expected: &version2.HealthCheck{ - Name: upstreamName, - ProxyConnectTimeout: "5s", - ProxyReadTimeout: "5s", - ProxySendTimeout: "5s", - ProxyPass: fmt.Sprintf("http://%v", upstreamName), - URI: "/", - Interval: "5s", - Jitter: "0s", - KeepaliveTime: "60s", - Fails: 1, - Passes: 1, - Headers: make(map[string]string), - }, - msg: "HealthCheck with default parameters from ConfigMap (not defined in Upstream)", - }, - { - upstream: conf_v1.Upstream{}, - upstreamName: upstreamName, - expected: nil, - msg: "HealthCheck not enabled", - }, - { - upstream: conf_v1.Upstream{ - HealthCheck: &conf_v1.HealthCheck{ - Enable: true, - Interval: "1m 5s", - Jitter: "2m 3s", - KeepaliveTime: "1m 6s", - ConnectTimeout: "1m 10s", - SendTimeout: "1m 20s", - ReadTimeout: "1m 30s", + Name: "test-upstream-queue-with-default-timeout", + Queue: &version2.Queue{ + Size: 10, + Timeout: "60s", }, }, - upstreamName: upstreamName, - expected: &version2.HealthCheck{ - Name: upstreamName, - ProxyConnectTimeout: "1m10s", - ProxySendTimeout: "1m20s", - ProxyReadTimeout: "1m30s", - ProxyPass: fmt.Sprintf("http://%v", upstreamName), - URI: "/", - Interval: "1m5s", - Jitter: "2m3s", - KeepaliveTime: "1m6s", - Fails: 1, - Passes: 1, - Headers: make(map[string]string), - }, - msg: "HealthCheck with time parameters have correct format", + msg: "upstream queue with only size", }, { - upstream: conf_v1.Upstream{ - HealthCheck: &conf_v1.HealthCheck{ - Enable: true, - Mandatory: true, - Persistent: true, - }, - ProxyConnectTimeout: "30s", - ProxyReadTimeout: "30s", - ProxySendTimeout: "30s", - }, - upstreamName: upstreamName, - expected: &version2.HealthCheck{ - Name: upstreamName, - ProxyConnectTimeout: "30s", - ProxyReadTimeout: "30s", - ProxySendTimeout: "30s", - ProxyPass: fmt.Sprintf("http://%v", upstreamName), - URI: "/", - Interval: "5s", - Jitter: "0s", - KeepaliveTime: "60s", - Fails: 1, - Passes: 1, - Headers: make(map[string]string), - Mandatory: true, - Persistent: true, + name: "test-upstream-queue-nil", + upstream: conf_v1.Upstream{Service: serviceName, Port: 80, Queue: nil}, + isPlus: false, + expected: version2.Upstream{ + UpstreamLabels: version2.UpstreamLabels{ + Service: "test-queue", + }, + Name: "test-upstream-queue-nil", }, - msg: "HealthCheck with mandatory and persistent set", + msg: "upstream queue with nil for OSS", }, } - baseCfgParams := &ConfigParams{ - ProxySendTimeout: "5s", - ProxyReadTimeout: "5s", - ProxyConnectTimeout: "5s", - } - for _, test := range tests { - result := generateHealthCheck(test.upstream, test.upstreamName, baseCfgParams) + vsc := newVirtualServerConfigurator(&ConfigParams{}, test.isPlus, false, &StaticConfigParams{}, false) + result := vsc.generateUpstream(nil, test.name, test.upstream, false, []string{}) if !reflect.DeepEqual(result, test.expected) { - t.Errorf("generateHealthCheck returned \n%v but expected \n%v \n for case: %v", result, test.expected, test.msg) + t.Errorf("generateUpstream() returned %v but expected %v for the case of %v", result, test.expected, test.msg) } } } -func TestGenerateGrpcHealthCheck(t *testing.T) { +func TestGenerateQueueForPlus(t *testing.T) { t.Parallel() - upstreamName := "test-upstream" tests := []struct { - upstream conf_v1.Upstream - upstreamName string - expected *version2.HealthCheck - msg string + upstreamQueue *conf_v1.UpstreamQueue + expected *version2.Queue + msg string }{ { - upstream: conf_v1.Upstream{ - HealthCheck: &conf_v1.HealthCheck{ - Enable: true, - Interval: "5s", - Jitter: "2s", - KeepaliveTime: "120s", - Fails: 3, - Passes: 2, - Port: 50051, - ConnectTimeout: "20s", - SendTimeout: "20s", - ReadTimeout: "20s", - GRPCStatus: createPointerFromInt(12), - GRPCService: "grpc-service", - Headers: []conf_v1.Header{ - { - Name: "Host", - Value: "my.service", - }, - { - Name: "User-Agent", - Value: "nginx", - }, - }, - }, - Type: "grpc", - }, - upstreamName: upstreamName, - expected: &version2.HealthCheck{ - Name: upstreamName, - ProxyConnectTimeout: "20s", - ProxySendTimeout: "20s", - ProxyReadTimeout: "20s", - ProxyPass: fmt.Sprintf("http://%v", upstreamName), - GRPCPass: fmt.Sprintf("grpc://%v", upstreamName), - Interval: "5s", - Jitter: "2s", - KeepaliveTime: "120s", - Fails: 3, - Passes: 2, - Port: 50051, - GRPCStatus: createPointerFromInt(12), - GRPCService: "grpc-service", - Headers: map[string]string{ - "Host": "my.service", - "User-Agent": "nginx", - }, - }, - msg: "HealthCheck with changed parameters", + upstreamQueue: &conf_v1.UpstreamQueue{Size: 10, Timeout: "10s"}, + expected: &version2.Queue{Size: 10, Timeout: "10s"}, + msg: "upstream queue with size and timeout", }, { - upstream: conf_v1.Upstream{ - HealthCheck: &conf_v1.HealthCheck{ - Enable: true, - }, - ProxyConnectTimeout: "30s", - ProxyReadTimeout: "30s", - ProxySendTimeout: "30s", - Type: "grpc", - }, - upstreamName: upstreamName, - expected: &version2.HealthCheck{ - Name: upstreamName, - ProxyConnectTimeout: "30s", - ProxyReadTimeout: "30s", - ProxySendTimeout: "30s", - ProxyPass: fmt.Sprintf("http://%v", upstreamName), - GRPCPass: fmt.Sprintf("grpc://%v", upstreamName), - Interval: "5s", - Jitter: "0s", - KeepaliveTime: "60s", - Fails: 1, - Passes: 1, - Headers: make(map[string]string), - }, - msg: "HealthCheck with default parameters from Upstream", + upstreamQueue: nil, + expected: nil, + msg: "upstream queue with nil", + }, + { + upstreamQueue: &conf_v1.UpstreamQueue{Size: 10}, + expected: &version2.Queue{Size: 10, Timeout: "60s"}, + msg: "upstream queue with only size", }, } - baseCfgParams := &ConfigParams{ - ProxySendTimeout: "5s", - ProxyReadTimeout: "5s", - ProxyConnectTimeout: "5s", + for _, test := range tests { + result := generateQueueForPlus(test.upstreamQueue, "60s") + if !reflect.DeepEqual(result, test.expected) { + t.Errorf("generateQueueForPlus() returned %v but expected %v for the case of %v", result, test.expected, test.msg) + } } +} +func TestGenerateSessionCookie(t *testing.T) { + t.Parallel() + tests := []struct { + sc *conf_v1.SessionCookie + expected *version2.SessionCookie + msg string + }{ + { + sc: &conf_v1.SessionCookie{Enable: true, Name: "test"}, + expected: &version2.SessionCookie{Enable: true, Name: "test"}, + msg: "session cookie with name", + }, + { + sc: nil, + expected: nil, + msg: "session cookie with nil", + }, + { + sc: &conf_v1.SessionCookie{Name: "test"}, + expected: nil, + msg: "session cookie not enabled", + }, + } for _, test := range tests { - result := generateHealthCheck(test.upstream, test.upstreamName, baseCfgParams) + result := generateSessionCookie(test.sc) if !reflect.DeepEqual(result, test.expected) { - t.Errorf("generateHealthCheck returned \n%v but expected \n%v \n for case: %v", result, test.expected, test.msg) + t.Errorf("generateSessionCookie() returned %v, but expected %v for the case of: %v", result, test.expected, test.msg) } } } -func TestGenerateEndpointsForUpstream(t *testing.T) { +func TestGeneratePath(t *testing.T) { t.Parallel() - name := "test" - namespace := "test-namespace" - tests := []struct { - upstream conf_v1.Upstream - vsEx *VirtualServerEx - isPlus bool - isResolverConfigured bool - expected []string - warningsExpected bool - msg string + path string + expected string }{ { - upstream: conf_v1.Upstream{ - Service: name, - Port: 80, - }, - vsEx: &VirtualServerEx{ - VirtualServer: &conf_v1.VirtualServer{ - ObjectMeta: meta_v1.ObjectMeta{ - Name: name, - Namespace: namespace, - }, - }, - Endpoints: map[string][]string{ - "test-namespace/test:80": {"example.com:80"}, - }, - ExternalNameSvcs: map[string]bool{ - "test-namespace/test": true, - }, - }, - isPlus: true, - isResolverConfigured: true, - expected: []string{"example.com:80"}, - msg: "ExternalName service", + path: "/", + expected: "/", }, { - upstream: conf_v1.Upstream{ - Service: name, - Port: 80, - }, - vsEx: &VirtualServerEx{ - VirtualServer: &conf_v1.VirtualServer{ - ObjectMeta: meta_v1.ObjectMeta{ - Name: name, - Namespace: namespace, - }, - }, - Endpoints: map[string][]string{ - "test-namespace/test:80": {"example.com:80"}, - }, - ExternalNameSvcs: map[string]bool{ - "test-namespace/test": true, - }, - }, - isPlus: true, - isResolverConfigured: false, - warningsExpected: true, - expected: []string{}, - msg: "ExternalName service without resolver configured", + path: "=/exact/match", + expected: "=/exact/match", }, { - upstream: conf_v1.Upstream{ - Service: name, - Port: 8080, - }, - vsEx: &VirtualServerEx{ - VirtualServer: &conf_v1.VirtualServer{ - ObjectMeta: meta_v1.ObjectMeta{ - Name: name, - Namespace: namespace, - }, - }, - Endpoints: map[string][]string{ - "test-namespace/test:8080": {"192.168.10.10:8080"}, - }, - }, - isPlus: false, - isResolverConfigured: false, - expected: []string{"192.168.10.10:8080"}, - msg: "Service with endpoints", + path: `~ *\\.jpg`, + expected: `~ "*\\.jpg"`, + }, + { + path: `~* *\\.PNG`, + expected: `~* "*\\.PNG"`, + }, + } + + for _, test := range tests { + result := generatePath(test.path) + if result != test.expected { + t.Errorf("generatePath() returned %v, but expected %v.", result, test.expected) + } + } +} + +func TestGenerateErrorPageName(t *testing.T) { + t.Parallel() + tests := []struct { + routeIndex int + index int + expected string + }{ + { + 0, + 0, + "@error_page_0_0", }, { - upstream: conf_v1.Upstream{ - Service: name, - Port: 8080, - }, - vsEx: &VirtualServerEx{ - VirtualServer: &conf_v1.VirtualServer{ - ObjectMeta: meta_v1.ObjectMeta{ - Name: name, - Namespace: namespace, - }, - }, - Endpoints: map[string][]string{}, - }, - isPlus: false, - isResolverConfigured: false, - expected: []string{nginx502Server}, - msg: "Service with no endpoints", + 0, + 1, + "@error_page_0_1", }, { - upstream: conf_v1.Upstream{ - Service: name, - Port: 8080, - }, - vsEx: &VirtualServerEx{ - VirtualServer: &conf_v1.VirtualServer{ - ObjectMeta: meta_v1.ObjectMeta{ - Name: name, - Namespace: namespace, - }, - }, - Endpoints: map[string][]string{}, - }, - isPlus: true, - isResolverConfigured: false, - expected: nil, - msg: "Service with no endpoints", + 1, + 0, + "@error_page_1_0", }, + } + + for _, test := range tests { + result := generateErrorPageName(test.routeIndex, test.index) + if result != test.expected { + t.Errorf("generateErrorPageName(%v, %v) returned %v but expected %v", test.routeIndex, test.index, result, test.expected) + } + } +} + +func TestGenerateErrorPageCodes(t *testing.T) { + t.Parallel() + tests := []struct { + codes []int + expected string + }{ { - upstream: conf_v1.Upstream{ - Service: name, - Port: 8080, - Subselector: map[string]string{"version": "test"}, - }, - vsEx: &VirtualServerEx{ - VirtualServer: &conf_v1.VirtualServer{ - ObjectMeta: meta_v1.ObjectMeta{ - Name: name, - Namespace: namespace, + codes: []int{400}, + expected: "400", + }, + { + codes: []int{404, 405, 502}, + expected: "404 405 502", + }, + } + + for _, test := range tests { + result := generateErrorPageCodes(test.codes) + if result != test.expected { + t.Errorf("generateErrorPageCodes(%v) returned %v but expected %v", test.codes, result, test.expected) + } + } +} + +func TestGenerateErrorPages(t *testing.T) { + t.Parallel() + tests := []struct { + upstreamName string + errorPages []conf_v1.ErrorPage + expected []version2.ErrorPage + }{ + {}, // empty errorPages + { + "vs_test_test", + []conf_v1.ErrorPage{ + { + Codes: []int{404, 405, 500, 502}, + Return: &conf_v1.ErrorPageReturn{ + ActionReturn: conf_v1.ActionReturn{ + Code: 200, + }, + Headers: nil, }, + Redirect: nil, }, - Endpoints: map[string][]string{ - "test-namespace/test_version=test:8080": {"192.168.10.10:8080"}, + }, + []version2.ErrorPage{ + { + Name: "@error_page_1_0", + Codes: "404 405 500 502", + ResponseCode: 200, }, }, - isPlus: false, - isResolverConfigured: false, - expected: []string{"192.168.10.10:8080"}, - msg: "Upstream with subselector, with a matching endpoint", }, { - upstream: conf_v1.Upstream{ - Service: name, - Port: 8080, - Subselector: map[string]string{"version": "test"}, - }, - vsEx: &VirtualServerEx{ - VirtualServer: &conf_v1.VirtualServer{ - ObjectMeta: meta_v1.ObjectMeta{ - Name: name, - Namespace: namespace, + "vs_test_test", + []conf_v1.ErrorPage{ + { + Codes: []int{404, 405, 500, 502}, + Return: nil, + Redirect: &conf_v1.ErrorPageRedirect{ + ActionRedirect: conf_v1.ActionRedirect{ + URL: "http://nginx.org", + Code: 302, + }, }, }, - Endpoints: map[string][]string{ - "test-namespace/test:8080": {"192.168.10.10:8080"}, + }, + []version2.ErrorPage{ + { + Name: "http://nginx.org", + Codes: "404 405 500 502", + ResponseCode: 302, }, }, - isPlus: false, - isResolverConfigured: false, - expected: []string{nginx502Server}, - msg: "Upstream with subselector, without a matching endpoint", }, } - for _, test := range tests { - isWildcardEnabled := false - vsc := newVirtualServerConfigurator( - &ConfigParams{}, - test.isPlus, - test.isResolverConfigured, - &StaticConfigParams{}, - isWildcardEnabled, - ) - result := vsc.generateEndpointsForUpstream(test.vsEx.VirtualServer, namespace, test.upstream, test.vsEx) + for i, test := range tests { + result := generateErrorPages(i, test.errorPages) if !reflect.DeepEqual(result, test.expected) { - t.Errorf("generateEndpointsForUpstream(isPlus=%v, isResolverConfigured=%v) returned %v, but expected %v for case: %v", - test.isPlus, test.isResolverConfigured, result, test.expected, test.msg) - } - - if len(vsc.warnings) == 0 && test.warningsExpected { - t.Errorf( - "generateEndpointsForUpstream(isPlus=%v, isResolverConfigured=%v) didn't return any warnings for %v but warnings expected", - test.isPlus, - test.isResolverConfigured, - test.upstream, - ) - } - - if len(vsc.warnings) != 0 && !test.warningsExpected { - t.Errorf("generateEndpointsForUpstream(isPlus=%v, isResolverConfigured=%v) returned warnings for %v", - test.isPlus, test.isResolverConfigured, test.upstream) + t.Errorf("generateErrorPages(%v, %v) returned %v but expected %v", test.upstreamName, test.errorPages, result, test.expected) } } } -func TestGenerateSlowStartForPlusWithInCompatibleLBMethods(t *testing.T) { +func TestGenerateErrorPageLocations(t *testing.T) { t.Parallel() - serviceName := "test-slowstart-with-incompatible-LBMethods" - upstream := conf_v1.Upstream{Service: serviceName, Port: 80, SlowStart: "10s"} - expected := "" - - tests := []string{ - "random", - "ip_hash", - "hash 123", - "random two", - "random two least_conn", - "random two least_time=header", - "random two least_time=last_byte", - } - - for _, lbMethod := range tests { - vsc := newVirtualServerConfigurator(&ConfigParams{}, true, false, &StaticConfigParams{}, false) - result := vsc.generateSlowStartForPlus(&conf_v1.VirtualServer{}, upstream, lbMethod) - - if !reflect.DeepEqual(result, expected) { - t.Errorf("generateSlowStartForPlus returned %v, but expected %v for lbMethod %v", result, expected, lbMethod) - } - - if len(vsc.warnings) == 0 { - t.Errorf("generateSlowStartForPlus returned no warnings for %v but warnings expected", upstream) - } - } -} - -func TestGenerateSlowStartForPlus(t *testing.T) { - serviceName := "test-slowstart" - tests := []struct { - upstream conf_v1.Upstream - lbMethod string - expected string + upstreamName string + errorPages []conf_v1.ErrorPage + expected []version2.ErrorPageLocation }{ + {}, { - upstream: conf_v1.Upstream{Service: serviceName, Port: 80, SlowStart: "", LBMethod: "least_conn"}, - lbMethod: "least_conn", - expected: "", + "vs_test_test", + []conf_v1.ErrorPage{ + { + Codes: []int{404, 405, 500, 502}, + Return: nil, + Redirect: &conf_v1.ErrorPageRedirect{ + ActionRedirect: conf_v1.ActionRedirect{ + URL: "http://nginx.org", + Code: 302, + }, + }, + }, + }, + nil, }, { - upstream: conf_v1.Upstream{Service: serviceName, Port: 80, SlowStart: "10s", LBMethod: "least_conn"}, - lbMethod: "least_conn", - expected: "10s", + "vs_test_test", + []conf_v1.ErrorPage{ + { + Codes: []int{404, 405, 500, 502}, + Return: &conf_v1.ErrorPageReturn{ + ActionReturn: conf_v1.ActionReturn{ + Code: 200, + Type: "application/json", + Body: "Hello World", + }, + Headers: []conf_v1.Header{ + { + Name: "HeaderName", + Value: "HeaderValue", + }, + }, + }, + Redirect: nil, + }, + }, + []version2.ErrorPageLocation{ + { + Name: "@error_page_2_0", + DefaultType: "application/json", + Return: &version2.Return{ + Code: 0, + Text: "Hello World", + }, + Headers: []version2.Header{ + { + Name: "HeaderName", + Value: "HeaderValue", + }, + }, + }, + }, }, } - for _, test := range tests { - vsc := newVirtualServerConfigurator(&ConfigParams{}, true, false, &StaticConfigParams{}, false) - result := vsc.generateSlowStartForPlus(&conf_v1.VirtualServer{}, test.upstream, test.lbMethod) + for i, test := range tests { + result := generateErrorPageLocations(i, test.errorPages) if !reflect.DeepEqual(result, test.expected) { - t.Errorf("generateSlowStartForPlus returned %v, but expected %v", result, test.expected) - } - - if len(vsc.warnings) != 0 { - t.Errorf("generateSlowStartForPlus returned warnings for %v", test.upstream) + t.Errorf("generateErrorPageLocations(%v, %v) returned %v but expected %v", test.upstreamName, test.errorPages, result, test.expected) } } } -func TestCreateEndpointsFromUpstream(t *testing.T) { +func TestGenerateProxySSLName(t *testing.T) { t.Parallel() - ups := version2.Upstream{ - Servers: []version2.UpstreamServer{ - { - Address: "10.0.0.20:80", - }, - { - Address: "10.0.0.30:80", - }, - }, - } - - expected := []string{ - "10.0.0.20:80", - "10.0.0.30:80", - } - - endpoints := createEndpointsFromUpstream(ups) - if !reflect.DeepEqual(endpoints, expected) { - t.Errorf("createEndpointsFromUpstream returned %v, but expected %v", endpoints, expected) + result := generateProxySSLName("coffee-v1", "default") + if result != "coffee-v1.default.svc" { + t.Errorf("generateProxySSLName(coffee-v1, default) returned %v but expected coffee-v1.default.svc", result) } } -func TestGenerateUpstreamWithQueue(t *testing.T) { +func TestIsTLSEnabled(t *testing.T) { t.Parallel() - serviceName := "test-queue" - tests := []struct { - name string - upstream conf_v1.Upstream - isPlus bool - expected version2.Upstream - msg string + upstream conf_v1.Upstream + spiffeCert bool + nsmEgress bool + expected bool }{ { - name: "test-upstream-queue", - upstream: conf_v1.Upstream{Service: serviceName, Port: 80, Queue: &conf_v1.UpstreamQueue{ - Size: 10, - Timeout: "10s", - }}, - isPlus: true, - expected: version2.Upstream{ - UpstreamLabels: version2.UpstreamLabels{ - Service: "test-queue", - }, - Name: "test-upstream-queue", - Queue: &version2.Queue{ - Size: 10, - Timeout: "10s", + upstream: conf_v1.Upstream{ + TLS: conf_v1.UpstreamTLS{ + Enable: false, }, }, - msg: "upstream queue with size and timeout", + spiffeCert: false, + expected: false, }, { - name: "test-upstream-queue-with-default-timeout", upstream: conf_v1.Upstream{ - Service: serviceName, - Port: 80, - Queue: &conf_v1.UpstreamQueue{Size: 10, Timeout: ""}, - }, - isPlus: true, - expected: version2.Upstream{ - UpstreamLabels: version2.UpstreamLabels{ - Service: "test-queue", - }, - Name: "test-upstream-queue-with-default-timeout", - Queue: &version2.Queue{ - Size: 10, - Timeout: "60s", + TLS: conf_v1.UpstreamTLS{ + Enable: false, }, }, - msg: "upstream queue with only size", + spiffeCert: true, + expected: true, }, { - name: "test-upstream-queue-nil", - upstream: conf_v1.Upstream{Service: serviceName, Port: 80, Queue: nil}, - isPlus: false, - expected: version2.Upstream{ - UpstreamLabels: version2.UpstreamLabels{ - Service: "test-queue", + upstream: conf_v1.Upstream{ + TLS: conf_v1.UpstreamTLS{ + Enable: true, }, - Name: "test-upstream-queue-nil", }, - msg: "upstream queue with nil for OSS", - }, - } - - for _, test := range tests { - vsc := newVirtualServerConfigurator(&ConfigParams{}, test.isPlus, false, &StaticConfigParams{}, false) - result := vsc.generateUpstream(nil, test.name, test.upstream, false, []string{}) - if !reflect.DeepEqual(result, test.expected) { - t.Errorf("generateUpstream() returned %v but expected %v for the case of %v", result, test.expected, test.msg) - } - } -} - -func TestGenerateQueueForPlus(t *testing.T) { - t.Parallel() - tests := []struct { - upstreamQueue *conf_v1.UpstreamQueue - expected *version2.Queue - msg string - }{ - { - upstreamQueue: &conf_v1.UpstreamQueue{Size: 10, Timeout: "10s"}, - expected: &version2.Queue{Size: 10, Timeout: "10s"}, - msg: "upstream queue with size and timeout", + spiffeCert: true, + expected: true, }, { - upstreamQueue: nil, - expected: nil, - msg: "upstream queue with nil", + upstream: conf_v1.Upstream{ + TLS: conf_v1.UpstreamTLS{ + Enable: true, + }, + }, + spiffeCert: false, + expected: true, }, { - upstreamQueue: &conf_v1.UpstreamQueue{Size: 10}, - expected: &version2.Queue{Size: 10, Timeout: "60s"}, - msg: "upstream queue with only size", + upstream: conf_v1.Upstream{ + TLS: conf_v1.UpstreamTLS{ + Enable: true, + }, + }, + nsmEgress: true, + spiffeCert: false, + expected: false, }, } for _, test := range tests { - result := generateQueueForPlus(test.upstreamQueue, "60s") - if !reflect.DeepEqual(result, test.expected) { - t.Errorf("generateQueueForPlus() returned %v but expected %v for the case of %v", result, test.expected, test.msg) + result := isTLSEnabled(test.upstream, test.spiffeCert, test.nsmEgress) + if result != test.expected { + t.Errorf("isTLSEnabled(%v, %v) returned %v but expected %v", test.upstream, test.spiffeCert, result, test.expected) } } } -func TestGenerateSessionCookie(t *testing.T) { +func TestGenerateRewrites(t *testing.T) { t.Parallel() tests := []struct { - sc *conf_v1.SessionCookie - expected *version2.SessionCookie - msg string + path string + proxy *conf_v1.ActionProxy + internal bool + originalPath string + grpcEnabled bool + expected []string + msg string }{ { - sc: &conf_v1.SessionCookie{Enable: true, Name: "test"}, - expected: &version2.SessionCookie{Enable: true, Name: "test"}, - msg: "session cookie with name", + proxy: nil, + expected: nil, + msg: "action isn't proxy", }, { - sc: nil, + proxy: &conf_v1.ActionProxy{ + RewritePath: "", + }, expected: nil, - msg: "session cookie with nil", + msg: "no rewrite is configured", }, { - sc: &conf_v1.SessionCookie{Name: "test"}, + path: "/path", + proxy: &conf_v1.ActionProxy{ + RewritePath: "/rewrite", + }, expected: nil, - msg: "session cookie not enabled", + msg: "non-regex rewrite for non-internal location is not needed", }, - } - for _, test := range tests { - result := generateSessionCookie(test.sc) - if !reflect.DeepEqual(result, test.expected) { - t.Errorf("generateSessionCookie() returned %v, but expected %v for the case of: %v", result, test.expected, test.msg) - } - } -} - -func TestGeneratePath(t *testing.T) { - t.Parallel() - tests := []struct { - path string - expected string - }{ { - path: "/", - expected: "/", + path: "/_internal_path", + internal: true, + proxy: &conf_v1.ActionProxy{ + RewritePath: "/rewrite", + }, + originalPath: "/path", + expected: []string{`^ $request_uri_no_args`, `"^/path(.*)$" "/rewrite$1" break`}, + msg: "non-regex rewrite for internal location", }, { - path: "=/exact/match", - expected: "=/exact/match", + path: "~/regex", + internal: true, + proxy: &conf_v1.ActionProxy{ + RewritePath: "/rewrite", + }, + originalPath: "/path", + expected: []string{`^ $request_uri_no_args`, `"^/path(.*)$" "/rewrite$1" break`}, + msg: "regex rewrite for internal location", }, { - path: `~ *\\.jpg`, - expected: `~ "*\\.jpg"`, + path: "~/regex", + internal: false, + proxy: &conf_v1.ActionProxy{ + RewritePath: "/rewrite", + }, + expected: []string{`"^/regex" "/rewrite" break`}, + msg: "regex rewrite for non-internal location", + }, + { + path: "/_internal_path", + internal: true, + proxy: &conf_v1.ActionProxy{ + RewritePath: "/rewrite", + }, + originalPath: "/path", + grpcEnabled: true, + expected: []string{`^ $request_uri_no_args`, `"^/path(.*)$" "/rewrite$1" break`}, + msg: "non-regex rewrite for internal location with grpc enabled", }, { - path: `~* *\\.PNG`, - expected: `~* "*\\.PNG"`, + path: "/_internal_path", + internal: true, + originalPath: "/path", + grpcEnabled: true, + expected: []string{`^ $request_uri break`}, + msg: "empty rewrite for internal location with grpc enabled", }, } for _, test := range tests { - result := generatePath(test.path) - if result != test.expected { - t.Errorf("generatePath() returned %v, but expected %v.", result, test.expected) + result := generateRewrites(test.path, test.proxy, test.internal, test.originalPath, test.grpcEnabled) + if diff := cmp.Diff(test.expected, result); diff != "" { + t.Errorf("generateRewrites() '%v' mismatch (-want +got):\n%s", test.msg, diff) } } } -func TestGenerateErrorPageName(t *testing.T) { +func TestGenerateProxyPassRewrite(t *testing.T) { t.Parallel() tests := []struct { - routeIndex int - index int - expected string + path string + proxy *conf_v1.ActionProxy + internal bool + expected string }{ { - 0, - 0, - "@error_page_0_0", + expected: "", }, { - 0, - 1, - "@error_page_0_1", + internal: true, + proxy: &conf_v1.ActionProxy{ + RewritePath: "/rewrite", + }, + expected: "", }, { - 1, - 0, - "@error_page_1_0", + path: "/path", + proxy: &conf_v1.ActionProxy{ + RewritePath: "/rewrite", + }, + expected: "/rewrite", }, - } - - for _, test := range tests { - result := generateErrorPageName(test.routeIndex, test.index) - if result != test.expected { - t.Errorf("generateErrorPageName(%v, %v) returned %v but expected %v", test.routeIndex, test.index, result, test.expected) - } - } -} - -func TestGenerateErrorPageCodes(t *testing.T) { - t.Parallel() - tests := []struct { - codes []int - expected string - }{ { - codes: []int{400}, - expected: "400", + path: "=/path", + proxy: &conf_v1.ActionProxy{ + RewritePath: "/rewrite", + }, + expected: "/rewrite", }, { - codes: []int{404, 405, 502}, - expected: "404 405 502", + path: "~/path", + proxy: &conf_v1.ActionProxy{ + RewritePath: "/rewrite", + }, + expected: "", }, } for _, test := range tests { - result := generateErrorPageCodes(test.codes) + result := generateProxyPassRewrite(test.path, test.proxy, test.internal) if result != test.expected { - t.Errorf("generateErrorPageCodes(%v) returned %v but expected %v", test.codes, result, test.expected) + t.Errorf("generateProxyPassRewrite(%v, %v, %v) returned %v but expected %v", + test.path, test.proxy, test.internal, result, test.expected) } } } -func TestGenerateErrorPages(t *testing.T) { +func TestGenerateProxySetHeaders(t *testing.T) { t.Parallel() tests := []struct { - upstreamName string - errorPages []conf_v1.ErrorPage - expected []version2.ErrorPage + proxy *conf_v1.ActionProxy + expected []version2.Header + msg string }{ - {}, // empty errorPages { - "vs_test_test", - []conf_v1.ErrorPage{ - { - Codes: []int{404, 405, 500, 502}, - Return: &conf_v1.ErrorPageReturn{ - ActionReturn: conf_v1.ActionReturn{ - Code: 200, + proxy: nil, + expected: []version2.Header{{Name: "Host", Value: "$host"}}, + msg: "no action proxy", + }, + { + proxy: &conf_v1.ActionProxy{}, + expected: []version2.Header{{Name: "Host", Value: "$host"}}, + msg: "empty action proxy", + }, + { + proxy: &conf_v1.ActionProxy{ + RequestHeaders: &conf_v1.ProxyRequestHeaders{ + Set: []conf_v1.Header{ + { + Name: "Header-Name", + Value: "HeaderValue", }, - Headers: nil, }, - Redirect: nil, }, }, - []version2.ErrorPage{ + expected: []version2.Header{ { - Name: "@error_page_1_0", - Codes: "404 405 500 502", - ResponseCode: 200, + Name: "Header-Name", + Value: "HeaderValue", + }, + { + Name: "Host", + Value: "$host", }, }, + msg: "set headers without host", }, { - "vs_test_test", - []conf_v1.ErrorPage{ - { - Codes: []int{404, 405, 500, 502}, - Return: nil, - Redirect: &conf_v1.ErrorPageRedirect{ - ActionRedirect: conf_v1.ActionRedirect{ - URL: "http://nginx.org", - Code: 302, + proxy: &conf_v1.ActionProxy{ + RequestHeaders: &conf_v1.ProxyRequestHeaders{ + Set: []conf_v1.Header{ + { + Name: "Header-Name", + Value: "HeaderValue", + }, + { + Name: "Host", + Value: "example.com", }, }, }, }, - []version2.ErrorPage{ + expected: []version2.Header{ { - Name: "http://nginx.org", - Codes: "404 405 500 502", - ResponseCode: 302, + Name: "Header-Name", + Value: "HeaderValue", }, - }, - }, - } - - for i, test := range tests { - result := generateErrorPages(i, test.errorPages) - if !reflect.DeepEqual(result, test.expected) { - t.Errorf("generateErrorPages(%v, %v) returned %v but expected %v", test.upstreamName, test.errorPages, result, test.expected) - } - } -} - -func TestGenerateErrorPageLocations(t *testing.T) { - t.Parallel() - tests := []struct { - upstreamName string - errorPages []conf_v1.ErrorPage - expected []version2.ErrorPageLocation - }{ - {}, - { - "vs_test_test", - []conf_v1.ErrorPage{ { - Codes: []int{404, 405, 500, 502}, - Return: nil, - Redirect: &conf_v1.ErrorPageRedirect{ - ActionRedirect: conf_v1.ActionRedirect{ - URL: "http://nginx.org", - Code: 302, - }, - }, + Name: "Host", + Value: "example.com", }, }, - nil, + msg: "set headers with host capitalized", }, { - "vs_test_test", - []conf_v1.ErrorPage{ - { - Codes: []int{404, 405, 500, 502}, - Return: &conf_v1.ErrorPageReturn{ - ActionReturn: conf_v1.ActionReturn{ - Code: 200, - Type: "application/json", - Body: "Hello World", + proxy: &conf_v1.ActionProxy{ + RequestHeaders: &conf_v1.ProxyRequestHeaders{ + Set: []conf_v1.Header{ + { + Name: "Header-Name", + Value: "HeaderValue", }, - Headers: []conf_v1.Header{ - { - Name: "HeaderName", - Value: "HeaderValue", - }, + { + Name: "hoST", + Value: "example.com", }, }, - Redirect: nil, }, }, - []version2.ErrorPageLocation{ + expected: []version2.Header{ { - Name: "@error_page_2_0", - DefaultType: "application/json", - Return: &version2.Return{ - Code: 0, - Text: "Hello World", - }, - Headers: []version2.Header{ + Name: "Header-Name", + Value: "HeaderValue", + }, + { + Name: "hoST", + Value: "example.com", + }, + }, + msg: "set headers with host in mixed case", + }, + { + proxy: &conf_v1.ActionProxy{ + RequestHeaders: &conf_v1.ProxyRequestHeaders{ + Set: []conf_v1.Header{ { - Name: "HeaderName", + Name: "Header-Name", Value: "HeaderValue", }, + { + Name: "Host", + Value: "one.example.com", + }, + { + Name: "Host", + Value: "two.example.com", + }, }, }, }, + expected: []version2.Header{ + { + Name: "Header-Name", + Value: "HeaderValue", + }, + { + Name: "Host", + Value: "one.example.com", + }, + { + Name: "Host", + Value: "two.example.com", + }, + }, + msg: "set headers with multiple hosts", }, } - for i, test := range tests { - result := generateErrorPageLocations(i, test.errorPages) - if !reflect.DeepEqual(result, test.expected) { - t.Errorf("generateErrorPageLocations(%v, %v) returned %v but expected %v", test.upstreamName, test.errorPages, result, test.expected) + for _, test := range tests { + result := generateProxySetHeaders(test.proxy) + if diff := cmp.Diff(test.expected, result); diff != "" { + t.Errorf("generateProxySetHeaders() '%v' mismatch (-want +got):\n%s", test.msg, diff) } } } -func TestGenerateProxySSLName(t *testing.T) { - t.Parallel() - result := generateProxySSLName("coffee-v1", "default") - if result != "coffee-v1.default.svc" { - t.Errorf("generateProxySSLName(coffee-v1, default) returned %v but expected coffee-v1.default.svc", result) - } -} - -func TestIsTLSEnabled(t *testing.T) { +func TestGenerateProxyPassRequestHeaders(t *testing.T) { t.Parallel() + passTrue := true + passFalse := false tests := []struct { - upstream conf_v1.Upstream - spiffeCert bool - nsmEgress bool - expected bool + proxy *conf_v1.ActionProxy + expected bool }{ { - upstream: conf_v1.Upstream{ - TLS: conf_v1.UpstreamTLS{ - Enable: false, - }, - }, - spiffeCert: false, - expected: false, + proxy: nil, + expected: true, }, { - upstream: conf_v1.Upstream{ - TLS: conf_v1.UpstreamTLS{ - Enable: false, - }, - }, - spiffeCert: true, - expected: true, + proxy: &conf_v1.ActionProxy{}, + expected: true, }, { - upstream: conf_v1.Upstream{ - TLS: conf_v1.UpstreamTLS{ - Enable: true, + proxy: &conf_v1.ActionProxy{ + RequestHeaders: &conf_v1.ProxyRequestHeaders{ + Pass: nil, }, }, - spiffeCert: true, - expected: true, + expected: true, }, { - upstream: conf_v1.Upstream{ - TLS: conf_v1.UpstreamTLS{ - Enable: true, + proxy: &conf_v1.ActionProxy{ + RequestHeaders: &conf_v1.ProxyRequestHeaders{ + Pass: &passTrue, }, }, - spiffeCert: false, - expected: true, + expected: true, }, { - upstream: conf_v1.Upstream{ - TLS: conf_v1.UpstreamTLS{ - Enable: true, + proxy: &conf_v1.ActionProxy{ + RequestHeaders: &conf_v1.ProxyRequestHeaders{ + Pass: &passFalse, }, }, - nsmEgress: true, - spiffeCert: false, - expected: false, + expected: false, }, } for _, test := range tests { - result := isTLSEnabled(test.upstream, test.spiffeCert, test.nsmEgress) + result := generateProxyPassRequestHeaders(test.proxy) if result != test.expected { - t.Errorf("isTLSEnabled(%v, %v) returned %v but expected %v", test.upstream, test.spiffeCert, result, test.expected) + t.Errorf("generateProxyPassRequestHeaders(%v) returned %v but expected %v", test.proxy, result, test.expected) } } } -func TestGenerateRewrites(t *testing.T) { +func TestGenerateProxyHideHeaders(t *testing.T) { t.Parallel() tests := []struct { - path string - proxy *conf_v1.ActionProxy - internal bool - originalPath string - grpcEnabled bool - expected []string - msg string + proxy *conf_v1.ActionProxy + expected []string }{ { proxy: nil, expected: nil, - msg: "action isn't proxy", }, { proxy: &conf_v1.ActionProxy{ - RewritePath: "", + ResponseHeaders: nil, }, - expected: nil, - msg: "no rewrite is configured", }, { - path: "/path", proxy: &conf_v1.ActionProxy{ - RewritePath: "/rewrite", + ResponseHeaders: &conf_v1.ProxyResponseHeaders{ + Hide: []string{"Header", "Header-2"}, + }, }, + expected: []string{"Header", "Header-2"}, + }, + } + + for _, test := range tests { + result := generateProxyHideHeaders(test.proxy) + if !reflect.DeepEqual(result, test.expected) { + t.Errorf("generateProxyHideHeaders(%v) returned %v but expected %v", test.proxy, result, test.expected) + } + } +} + +func TestGenerateProxyPassHeaders(t *testing.T) { + t.Parallel() + tests := []struct { + proxy *conf_v1.ActionProxy + expected []string + }{ + { + proxy: nil, expected: nil, - msg: "non-regex rewrite for non-internal location is not needed", }, { - path: "/_internal_path", - internal: true, proxy: &conf_v1.ActionProxy{ - RewritePath: "/rewrite", + ResponseHeaders: nil, }, - originalPath: "/path", - expected: []string{`^ $request_uri_no_args`, `"^/path(.*)$" "/rewrite$1" break`}, - msg: "non-regex rewrite for internal location", }, { - path: "~/regex", - internal: true, proxy: &conf_v1.ActionProxy{ - RewritePath: "/rewrite", + ResponseHeaders: &conf_v1.ProxyResponseHeaders{ + Pass: []string{"Header", "Header-2"}, + }, }, - originalPath: "/path", - expected: []string{`^ $request_uri_no_args`, `"^/path(.*)$" "/rewrite$1" break`}, - msg: "regex rewrite for internal location", + expected: []string{"Header", "Header-2"}, }, + } + + for _, test := range tests { + result := generateProxyPassHeaders(test.proxy) + if !reflect.DeepEqual(result, test.expected) { + t.Errorf("generateProxyPassHeaders(%v) returned %v but expected %v", test.proxy, result, test.expected) + } + } +} + +func TestGenerateProxyIgnoreHeaders(t *testing.T) { + t.Parallel() + tests := []struct { + proxy *conf_v1.ActionProxy + expected string + }{ { - path: "~/regex", - internal: false, - proxy: &conf_v1.ActionProxy{ - RewritePath: "/rewrite", - }, - expected: []string{`"^/regex" "/rewrite" break`}, - msg: "regex rewrite for non-internal location", + proxy: nil, + expected: "", }, { - path: "/_internal_path", - internal: true, proxy: &conf_v1.ActionProxy{ - RewritePath: "/rewrite", + ResponseHeaders: nil, }, - originalPath: "/path", - grpcEnabled: true, - expected: []string{`^ $request_uri_no_args`, `"^/path(.*)$" "/rewrite$1" break`}, - msg: "non-regex rewrite for internal location with grpc enabled", + expected: "", }, { - path: "/_internal_path", - internal: true, - originalPath: "/path", - grpcEnabled: true, - expected: []string{`^ $request_uri break`}, - msg: "empty rewrite for internal location with grpc enabled", + proxy: &conf_v1.ActionProxy{ + ResponseHeaders: &conf_v1.ProxyResponseHeaders{ + Ignore: []string{"Header", "Header-2"}, + }, + }, + expected: "Header Header-2", }, } for _, test := range tests { - result := generateRewrites(test.path, test.proxy, test.internal, test.originalPath, test.grpcEnabled) - if diff := cmp.Diff(test.expected, result); diff != "" { - t.Errorf("generateRewrites() '%v' mismatch (-want +got):\n%s", test.msg, diff) + result := generateProxyIgnoreHeaders(test.proxy) + if result != test.expected { + t.Errorf("generateProxyIgnoreHeaders(%v) returned %v but expected %v", test.proxy, result, test.expected) } } } -func TestGenerateProxyPassRewrite(t *testing.T) { +func TestGenerateProxyAddHeaders(t *testing.T) { t.Parallel() tests := []struct { - path string proxy *conf_v1.ActionProxy - internal bool - expected string + expected []version2.AddHeader }{ { - expected: "", + proxy: nil, + expected: nil, + }, + { + proxy: &conf_v1.ActionProxy{}, + expected: nil, + }, + { + proxy: &conf_v1.ActionProxy{ + ResponseHeaders: &conf_v1.ProxyResponseHeaders{ + Add: []conf_v1.AddHeader{ + { + Header: conf_v1.Header{ + Name: "Header-Name", + Value: "HeaderValue", + }, + Always: true, + }, + { + Header: conf_v1.Header{ + Name: "Server", + Value: "myServer", + }, + Always: false, + }, + }, + }, + }, + expected: []version2.AddHeader{ + { + Header: version2.Header{ + Name: "Header-Name", + Value: "HeaderValue", + }, + Always: true, + }, + { + Header: version2.Header{ + Name: "Server", + Value: "myServer", + }, + Always: false, + }, + }, }, + } + + for _, test := range tests { + result := generateProxyAddHeaders(test.proxy) + if !reflect.DeepEqual(result, test.expected) { + t.Errorf("generateProxyAddHeaders(%v) returned %v but expected %v", test.proxy, result, test.expected) + } + } +} + +func TestGetUpstreamResourceLabels(t *testing.T) { + t.Parallel() + tests := []struct { + owner runtime.Object + expected version2.UpstreamLabels + }{ { - internal: true, - proxy: &conf_v1.ActionProxy{ - RewritePath: "/rewrite", - }, - expected: "", + owner: nil, + expected: version2.UpstreamLabels{}, }, { - path: "/path", - proxy: &conf_v1.ActionProxy{ - RewritePath: "/rewrite", + owner: &conf_v1.VirtualServer{ + ObjectMeta: meta_v1.ObjectMeta{ + Namespace: "namespace", + Name: "name", + }, }, - expected: "/rewrite", - }, - { - path: "=/path", - proxy: &conf_v1.ActionProxy{ - RewritePath: "/rewrite", + expected: version2.UpstreamLabels{ + ResourceNamespace: "namespace", + ResourceName: "name", + ResourceType: "virtualserver", }, - expected: "/rewrite", }, { - path: "~/path", - proxy: &conf_v1.ActionProxy{ - RewritePath: "/rewrite", + owner: &conf_v1.VirtualServerRoute{ + ObjectMeta: meta_v1.ObjectMeta{ + Namespace: "namespace", + Name: "name", + }, + }, + expected: version2.UpstreamLabels{ + ResourceNamespace: "namespace", + ResourceName: "name", + ResourceType: "virtualserverroute", }, - expected: "", }, } - for _, test := range tests { - result := generateProxyPassRewrite(test.path, test.proxy, test.internal) - if result != test.expected { - t.Errorf("generateProxyPassRewrite(%v, %v, %v) returned %v but expected %v", - test.path, test.proxy, test.internal, result, test.expected) + result := getUpstreamResourceLabels(test.owner) + if !reflect.DeepEqual(result, test.expected) { + t.Errorf("getUpstreamResourceLabels(%+v) returned %+v but expected %+v", test.owner, result, test.expected) } } } -func TestGenerateProxySetHeaders(t *testing.T) { +func TestAddWafConfig(t *testing.T) { t.Parallel() tests := []struct { - proxy *conf_v1.ActionProxy - expected []version2.Header - msg string + wafInput *conf_v1.WAF + polKey string + polNamespace string + apResources *appProtectResourcesForVS + wafConfig *version2.WAF + expected *validationResults + msg string }{ { - proxy: nil, - expected: []version2.Header{{Name: "Host", Value: "$host"}}, - msg: "no action proxy", - }, - { - proxy: &conf_v1.ActionProxy{}, - expected: []version2.Header{{Name: "Host", Value: "$host"}}, - msg: "empty action proxy", - }, - { - proxy: &conf_v1.ActionProxy{ - RequestHeaders: &conf_v1.ProxyRequestHeaders{ - Set: []conf_v1.Header{ - { - Name: "Header-Name", - Value: "HeaderValue", - }, - }, - }, + wafInput: &conf_v1.WAF{ + Enable: true, }, - expected: []version2.Header{ - { - Name: "Header-Name", - Value: "HeaderValue", - }, - { - Name: "Host", - Value: "$host", - }, + polKey: "default/waf-policy", + polNamespace: "default", + apResources: &appProtectResourcesForVS{ + Policies: map[string]string{}, + LogConfs: map[string]string{}, }, - msg: "set headers without host", + wafConfig: &version2.WAF{ + Enable: "on", + }, + expected: &validationResults{isError: false}, + msg: "valid waf config, default App Protect config", }, { - proxy: &conf_v1.ActionProxy{ - RequestHeaders: &conf_v1.ProxyRequestHeaders{ - Set: []conf_v1.Header{ - { - Name: "Header-Name", - Value: "HeaderValue", - }, - { - Name: "Host", - Value: "example.com", - }, - }, + wafInput: &conf_v1.WAF{ + Enable: true, + ApPolicy: "dataguard-alarm", + SecurityLog: &conf_v1.SecurityLog{ + Enable: true, + ApLogConf: "logconf", + LogDest: "syslog:server=127.0.0.1:514", }, }, - expected: []version2.Header{ - { - Name: "Header-Name", - Value: "HeaderValue", + polKey: "default/waf-policy", + polNamespace: "default", + apResources: &appProtectResourcesForVS{ + Policies: map[string]string{ + "default/dataguard-alarm": "/etc/nginx/waf/nac-policies/default-dataguard-alarm", }, - { - Name: "Host", - Value: "example.com", + LogConfs: map[string]string{ + "default/logconf": "/etc/nginx/waf/nac-logconfs/default-logconf", }, }, - msg: "set headers with host capitalized", + wafConfig: &version2.WAF{ + ApPolicy: "/etc/nginx/waf/nac-policies/default-dataguard-alarm", + ApSecurityLogEnable: true, + ApLogConf: []string{"/etc/nginx/waf/nac-logconfs/default-logconf"}, + }, + expected: &validationResults{isError: false}, + msg: "valid waf config", }, { - proxy: &conf_v1.ActionProxy{ - RequestHeaders: &conf_v1.ProxyRequestHeaders{ - Set: []conf_v1.Header{ - { - Name: "Header-Name", - Value: "HeaderValue", - }, - { - Name: "hoST", - Value: "example.com", - }, + wafInput: &conf_v1.WAF{ + Enable: true, + ApPolicy: "dataguard-alarm", + SecurityLogs: []*conf_v1.SecurityLog{ + { + Enable: true, + ApLogConf: "logconf", + LogDest: "syslog:server=127.0.0.1:514", }, }, }, - expected: []version2.Header{ - { - Name: "Header-Name", - Value: "HeaderValue", + polKey: "default/waf-policy", + polNamespace: "default", + apResources: &appProtectResourcesForVS{ + Policies: map[string]string{ + "default/dataguard-alarm": "/etc/nginx/waf/nac-policies/default-dataguard-alarm", }, - { - Name: "hoST", - Value: "example.com", + LogConfs: map[string]string{ + "default/logconf": "/etc/nginx/waf/nac-logconfs/default-logconf", }, }, - msg: "set headers with host in mixed case", + wafConfig: &version2.WAF{ + ApPolicy: "/etc/nginx/waf/nac-policies/default-dataguard-alarm", + ApSecurityLogEnable: true, + ApLogConf: []string{"/etc/nginx/waf/nac-logconfs/default-logconf"}, + }, + expected: &validationResults{isError: false}, + msg: "valid waf config", }, { - proxy: &conf_v1.ActionProxy{ - RequestHeaders: &conf_v1.ProxyRequestHeaders{ - Set: []conf_v1.Header{ - { - Name: "Header-Name", - Value: "HeaderValue", - }, - { - Name: "Host", - Value: "one.example.com", - }, - { - Name: "Host", - Value: "two.example.com", - }, - }, + wafInput: &conf_v1.WAF{ + Enable: true, + ApPolicy: "default/dataguard-alarm", + SecurityLog: &conf_v1.SecurityLog{ + Enable: true, + ApLogConf: "default/logconf", + LogDest: "syslog:server=127.0.0.1:514", }, }, - expected: []version2.Header{ - { - Name: "Header-Name", - Value: "HeaderValue", - }, - { - Name: "Host", - Value: "one.example.com", - }, - { - Name: "Host", - Value: "two.example.com", + polKey: "default/waf-policy", + polNamespace: "", + apResources: &appProtectResourcesForVS{ + Policies: map[string]string{ + "default/dataguard-alarm": "/etc/nginx/waf/nac-policies/default-dataguard-alarm", }, + LogConfs: map[string]string{}, }, - msg: "set headers with multiple hosts", - }, - } - - for _, test := range tests { - result := generateProxySetHeaders(test.proxy) - if diff := cmp.Diff(test.expected, result); diff != "" { - t.Errorf("generateProxySetHeaders() '%v' mismatch (-want +got):\n%s", test.msg, diff) - } - } -} - -func TestGenerateProxyPassRequestHeaders(t *testing.T) { - t.Parallel() - passTrue := true - passFalse := false - tests := []struct { - proxy *conf_v1.ActionProxy - expected bool - }{ - { - proxy: nil, - expected: true, - }, - { - proxy: &conf_v1.ActionProxy{}, - expected: true, + wafConfig: &version2.WAF{ + ApPolicy: "/etc/nginx/waf/nac-policies/default-dataguard-alarm", + ApSecurityLogEnable: true, + ApLogConf: []string{"/etc/nginx/waf/nac-logconfs/default-logconf"}, + }, + expected: &validationResults{ + isError: true, + warnings: []string{ + `WAF policy default/waf-policy references an invalid or non-existing log config default/logconf`, + }, + }, + msg: "invalid waf config, apLogConf references non-existing log conf", }, { - proxy: &conf_v1.ActionProxy{ - RequestHeaders: &conf_v1.ProxyRequestHeaders{ - Pass: nil, + wafInput: &conf_v1.WAF{ + Enable: true, + ApPolicy: "default/dataguard-alarm", + SecurityLog: &conf_v1.SecurityLog{ + Enable: true, + LogDest: "syslog:server=127.0.0.1:514", }, }, - expected: true, + polKey: "default/waf-policy", + polNamespace: "", + apResources: &appProtectResourcesForVS{ + Policies: map[string]string{}, + LogConfs: map[string]string{ + "default/logconf": "/etc/nginx/waf/nac-logconfs/default-logconf", + }, + }, + wafConfig: &version2.WAF{ + ApPolicy: "/etc/nginx/waf/nac-policies/default-dataguard-alarm", + ApSecurityLogEnable: true, + ApLogConf: []string{"/etc/nginx/waf/nac-logconfs/default-logconf"}, + }, + expected: &validationResults{ + isError: true, + warnings: []string{ + `WAF policy default/waf-policy references an invalid or non-existing App Protect policy default/dataguard-alarm`, + }, + }, + msg: "invalid waf config, apLogConf references non-existing ap conf", }, { - proxy: &conf_v1.ActionProxy{ - RequestHeaders: &conf_v1.ProxyRequestHeaders{ - Pass: &passTrue, + wafInput: &conf_v1.WAF{ + Enable: true, + ApPolicy: "ns1/dataguard-alarm", + SecurityLog: &conf_v1.SecurityLog{ + Enable: true, + ApLogConf: "ns2/logconf", + LogDest: "syslog:server=127.0.0.1:514", }, }, - expected: true, + polKey: "default/waf-policy", + polNamespace: "", + apResources: &appProtectResourcesForVS{ + Policies: map[string]string{ + "ns1/dataguard-alarm": "/etc/nginx/waf/nac-policies/ns1-dataguard-alarm", + }, + LogConfs: map[string]string{ + "ns2/logconf": "/etc/nginx/waf/nac-logconfs/ns2-logconf", + }, + }, + wafConfig: &version2.WAF{ + ApPolicy: "/etc/nginx/waf/nac-policies/ns1-dataguard-alarm", + ApSecurityLogEnable: true, + ApLogConf: []string{"/etc/nginx/waf/nac-logconfs/ns2-logconf"}, + }, + expected: &validationResults{}, + msg: "valid waf config, cross ns reference", }, { - proxy: &conf_v1.ActionProxy{ - RequestHeaders: &conf_v1.ProxyRequestHeaders{ - Pass: &passFalse, + wafInput: &conf_v1.WAF{ + Enable: false, + ApPolicy: "dataguard-alarm", + }, + polKey: "default/waf-policy", + polNamespace: "default", + apResources: &appProtectResourcesForVS{ + Policies: map[string]string{ + "default/dataguard-alarm": "/etc/nginx/waf/nac-policies/ns1-dataguard-alarm", + }, + LogConfs: map[string]string{ + "default/logconf": "/etc/nginx/waf/nac-logconfs/ns2-logconf", }, }, - expected: false, + wafConfig: &version2.WAF{ + Enable: "off", + ApPolicy: "/etc/nginx/waf/nac-policies/ns1-dataguard-alarm", + }, + expected: &validationResults{}, + msg: "valid waf config, disable waf", }, } for _, test := range tests { - result := generateProxyPassRequestHeaders(test.proxy) - if result != test.expected { - t.Errorf("generateProxyPassRequestHeaders(%v) returned %v but expected %v", test.proxy, result, test.expected) + polCfg := newPoliciesConfig() + result := polCfg.addWAFConfig(test.wafInput, test.polKey, test.polNamespace, test.apResources) + if diff := cmp.Diff(test.expected.warnings, result.warnings); diff != "" { + t.Errorf("policiesCfg.addWAFConfig() '%v' mismatch (-want +got):\n%s", test.msg, diff) } } } -func TestGenerateProxyHideHeaders(t *testing.T) { +func TestGenerateTime(t *testing.T) { t.Parallel() tests := []struct { - proxy *conf_v1.ActionProxy - expected []string + value, expected string }{ { - proxy: nil, - expected: nil, + value: "0s", + expected: "0s", }, { - proxy: &conf_v1.ActionProxy{ - ResponseHeaders: nil, - }, + value: "0", + expected: "0s", }, { - proxy: &conf_v1.ActionProxy{ - ResponseHeaders: &conf_v1.ProxyResponseHeaders{ - Hide: []string{"Header", "Header-2"}, - }, - }, - expected: []string{"Header", "Header-2"}, + value: "1h", + expected: "1h", + }, + { + value: "1h 30m", + expected: "1h30m", }, } for _, test := range tests { - result := generateProxyHideHeaders(test.proxy) - if !reflect.DeepEqual(result, test.expected) { - t.Errorf("generateProxyHideHeaders(%v) returned %v but expected %v", test.proxy, result, test.expected) + result := generateTime(test.value) + if result != test.expected { + t.Errorf("generateTime(%q) returned %q but expected %q", test.value, result, test.expected) } } } -func TestGenerateProxyPassHeaders(t *testing.T) { +func TestGenerateTimeWithDefault(t *testing.T) { t.Parallel() tests := []struct { - proxy *conf_v1.ActionProxy - expected []string + value, defaultValue, expected string }{ { - proxy: nil, - expected: nil, + value: "1h 30m", + defaultValue: "", + expected: "1h30m", }, { - proxy: &conf_v1.ActionProxy{ - ResponseHeaders: nil, - }, + value: "", + defaultValue: "60s", + expected: "60s", }, { - proxy: &conf_v1.ActionProxy{ - ResponseHeaders: &conf_v1.ProxyResponseHeaders{ - Pass: []string{"Header", "Header-2"}, - }, - }, - expected: []string{"Header", "Header-2"}, + value: "", + defaultValue: "test", + expected: "test", }, } for _, test := range tests { - result := generateProxyPassHeaders(test.proxy) - if !reflect.DeepEqual(result, test.expected) { - t.Errorf("generateProxyPassHeaders(%v) returned %v but expected %v", test.proxy, result, test.expected) + result := generateTimeWithDefault(test.value, test.defaultValue) + if result != test.expected { + t.Errorf("generateTimeWithDefault(%q, %q) returned %q but expected %q", test.value, test.defaultValue, result, test.expected) } } } -func TestGenerateProxyIgnoreHeaders(t *testing.T) { - t.Parallel() - tests := []struct { - proxy *conf_v1.ActionProxy - expected string - }{ - { - proxy: nil, - expected: "", - }, - { - proxy: &conf_v1.ActionProxy{ - ResponseHeaders: nil, +var ( + baseCfgParams = ConfigParams{ + ServerTokens: "off", + Keepalive: 16, + ServerSnippets: []string{"# server snippet"}, + ProxyProtocol: true, + SetRealIPFrom: []string{"0.0.0.0/0"}, + RealIPHeader: "X-Real-IP", + RealIPRecursive: true, + } + + virtualServerExWithGunzipOn = VirtualServerEx{ + VirtualServer: &conf_v1.VirtualServer{ + ObjectMeta: meta_v1.ObjectMeta{ + Name: "cafe", + Namespace: "default", }, - expected: "", - }, - { - proxy: &conf_v1.ActionProxy{ - ResponseHeaders: &conf_v1.ProxyResponseHeaders{ - Ignore: []string{"Header", "Header-2"}, + Spec: conf_v1.VirtualServerSpec{ + Host: "cafe.example.com", + Gunzip: true, + Upstreams: []conf_v1.Upstream{ + { + Name: "tea", + Service: "tea-svc", + Port: 80, + }, + { + Name: "tea-latest", + Service: "tea-svc", + Subselector: map[string]string{"version": "v1"}, + Port: 80, + }, + { + Name: "coffee", + Service: "coffee-svc", + Port: 80, + }, + }, + Routes: []conf_v1.Route{ + { + Path: "/tea", + Action: &conf_v1.Action{ + Pass: "tea", + }, + }, + { + Path: "/tea-latest", + Action: &conf_v1.Action{ + Pass: "tea-latest", + }, + }, + { + Path: "/coffee", + Route: "default/coffee", + }, + { + Path: "/subtea", + Route: "default/subtea", + }, + { + Path: "/coffee-errorpage", + Action: &conf_v1.Action{ + Pass: "coffee", + }, + ErrorPages: []conf_v1.ErrorPage{ + { + Codes: []int{401, 403}, + Redirect: &conf_v1.ErrorPageRedirect{ + ActionRedirect: conf_v1.ActionRedirect{ + URL: "http://nginx.com", + Code: 301, + }, + }, + }, + }, + }, + { + Path: "/coffee-errorpage-subroute", + Route: "default/subcoffee", + ErrorPages: []conf_v1.ErrorPage{ + { + Codes: []int{401, 403}, + Redirect: &conf_v1.ErrorPageRedirect{ + ActionRedirect: conf_v1.ActionRedirect{ + URL: "http://nginx.com", + Code: 301, + }, + }, + }, + }, + }, }, }, - expected: "Header Header-2", - }, - } - - for _, test := range tests { - result := generateProxyIgnoreHeaders(test.proxy) - if result != test.expected { - t.Errorf("generateProxyIgnoreHeaders(%v) returned %v but expected %v", test.proxy, result, test.expected) - } - } -} - -func TestGenerateProxyAddHeaders(t *testing.T) { - t.Parallel() - tests := []struct { - proxy *conf_v1.ActionProxy - expected []version2.AddHeader - }{ - { - proxy: nil, - expected: nil, }, - { - proxy: &conf_v1.ActionProxy{}, - expected: nil, + Endpoints: map[string][]string{ + "default/tea-svc:80": { + "10.0.0.20:80", + }, + "default/tea-svc_version=v1:80": { + "10.0.0.30:80", + }, + "default/coffee-svc:80": { + "10.0.0.40:80", + }, + "default/sub-tea-svc_version=v1:80": { + "10.0.0.50:80", + }, }, - { - proxy: &conf_v1.ActionProxy{ - ResponseHeaders: &conf_v1.ProxyResponseHeaders{ - Add: []conf_v1.AddHeader{ + VirtualServerRoutes: []*conf_v1.VirtualServerRoute{ + { + ObjectMeta: meta_v1.ObjectMeta{ + Name: "coffee", + Namespace: "default", + }, + Spec: conf_v1.VirtualServerRouteSpec{ + Host: "cafe.example.com", + Upstreams: []conf_v1.Upstream{ { - Header: conf_v1.Header{ - Name: "Header-Name", - Value: "HeaderValue", - }, - Always: true, + Name: "coffee", + Service: "coffee-svc", + Port: 80, }, + }, + Subroutes: []conf_v1.Route{ { - Header: conf_v1.Header{ - Name: "Server", - Value: "myServer", + Path: "/coffee", + Action: &conf_v1.Action{ + Pass: "coffee", }, - Always: false, }, }, }, }, - expected: []version2.AddHeader{ - { - Header: version2.Header{ - Name: "Header-Name", - Value: "HeaderValue", + { + ObjectMeta: meta_v1.ObjectMeta{ + Name: "subtea", + Namespace: "default", + }, + Spec: conf_v1.VirtualServerRouteSpec{ + Host: "cafe.example.com", + Upstreams: []conf_v1.Upstream{ + { + Name: "subtea", + Service: "sub-tea-svc", + Port: 80, + Subselector: map[string]string{"version": "v1"}, + }, + }, + Subroutes: []conf_v1.Route{ + { + Path: "/subtea", + Action: &conf_v1.Action{ + Pass: "subtea", + }, + }, }, - Always: true, }, - { - Header: version2.Header{ - Name: "Server", - Value: "myServer", + }, + { + ObjectMeta: meta_v1.ObjectMeta{ + Name: "subcoffee", + Namespace: "default", + }, + Spec: conf_v1.VirtualServerRouteSpec{ + Host: "cafe.example.com", + Upstreams: []conf_v1.Upstream{ + { + Name: "coffee", + Service: "coffee-svc", + Port: 80, + }, + }, + Subroutes: []conf_v1.Route{ + { + Path: "/coffee-errorpage-subroute", + Action: &conf_v1.Action{ + Pass: "coffee", + }, + }, + { + Path: "/coffee-errorpage-subroute-defined", + Action: &conf_v1.Action{ + Pass: "coffee", + }, + ErrorPages: []conf_v1.ErrorPage{ + { + Codes: []int{502, 503}, + Return: &conf_v1.ErrorPageReturn{ + ActionReturn: conf_v1.ActionReturn{ + Code: 200, + Type: "text/plain", + Body: "All Good", + }, + }, + }, + }, + }, }, - Always: false, }, }, }, } - for _, test := range tests { - result := generateProxyAddHeaders(test.proxy) - if !reflect.DeepEqual(result, test.expected) { - t.Errorf("generateProxyAddHeaders(%v) returned %v but expected %v", test.proxy, result, test.expected) - } - } -} - -func TestGetUpstreamResourceLabels(t *testing.T) { - t.Parallel() - tests := []struct { - owner runtime.Object - expected version2.UpstreamLabels - }{ - { - owner: nil, - expected: version2.UpstreamLabels{}, - }, - { - owner: &conf_v1.VirtualServer{ - ObjectMeta: meta_v1.ObjectMeta{ - Namespace: "namespace", - Name: "name", - }, - }, - expected: version2.UpstreamLabels{ - ResourceNamespace: "namespace", - ResourceName: "name", - ResourceType: "virtualserver", + virtualServerExWithGunzipOff = VirtualServerEx{ + VirtualServer: &conf_v1.VirtualServer{ + ObjectMeta: meta_v1.ObjectMeta{ + Name: "cafe", + Namespace: "default", }, - }, - { - owner: &conf_v1.VirtualServerRoute{ - ObjectMeta: meta_v1.ObjectMeta{ - Namespace: "namespace", - Name: "name", + Spec: conf_v1.VirtualServerSpec{ + Host: "cafe.example.com", + Gunzip: false, + Upstreams: []conf_v1.Upstream{ + { + Name: "tea", + Service: "tea-svc", + Port: 80, + }, + { + Name: "tea-latest", + Service: "tea-svc", + Subselector: map[string]string{"version": "v1"}, + Port: 80, + }, + { + Name: "coffee", + Service: "coffee-svc", + Port: 80, + }, + }, + Routes: []conf_v1.Route{ + { + Path: "/tea", + Action: &conf_v1.Action{ + Pass: "tea", + }, + }, + { + Path: "/tea-latest", + Action: &conf_v1.Action{ + Pass: "tea-latest", + }, + }, + { + Path: "/coffee", + Route: "default/coffee", + }, + { + Path: "/subtea", + Route: "default/subtea", + }, + { + Path: "/coffee-errorpage", + Action: &conf_v1.Action{ + Pass: "coffee", + }, + ErrorPages: []conf_v1.ErrorPage{ + { + Codes: []int{401, 403}, + Redirect: &conf_v1.ErrorPageRedirect{ + ActionRedirect: conf_v1.ActionRedirect{ + URL: "http://nginx.com", + Code: 301, + }, + }, + }, + }, + }, + { + Path: "/coffee-errorpage-subroute", + Route: "default/subcoffee", + ErrorPages: []conf_v1.ErrorPage{ + { + Codes: []int{401, 403}, + Redirect: &conf_v1.ErrorPageRedirect{ + ActionRedirect: conf_v1.ActionRedirect{ + URL: "http://nginx.com", + Code: 301, + }, + }, + }, + }, + }, }, }, - expected: version2.UpstreamLabels{ - ResourceNamespace: "namespace", - ResourceName: "name", - ResourceType: "virtualserverroute", - }, }, - } - for _, test := range tests { - result := getUpstreamResourceLabels(test.owner) - if !reflect.DeepEqual(result, test.expected) { - t.Errorf("getUpstreamResourceLabels(%+v) returned %+v but expected %+v", test.owner, result, test.expected) - } - } -} - -func TestAddWafConfig(t *testing.T) { - t.Parallel() - tests := []struct { - wafInput *conf_v1.WAF - polKey string - polNamespace string - apResources *appProtectResourcesForVS - wafConfig *version2.WAF - expected *validationResults - msg string - }{ - { - wafInput: &conf_v1.WAF{ - Enable: true, + Endpoints: map[string][]string{ + "default/tea-svc:80": { + "10.0.0.20:80", }, - polKey: "default/waf-policy", - polNamespace: "default", - apResources: &appProtectResourcesForVS{ - Policies: map[string]string{}, - LogConfs: map[string]string{}, + "default/tea-svc_version=v1:80": { + "10.0.0.30:80", }, - wafConfig: &version2.WAF{ - Enable: "on", + "default/coffee-svc:80": { + "10.0.0.40:80", }, - expected: &validationResults{isError: false}, - msg: "valid waf config, default App Protect config", - }, - { - wafInput: &conf_v1.WAF{ - Enable: true, - ApPolicy: "dataguard-alarm", - SecurityLog: &conf_v1.SecurityLog{ - Enable: true, - ApLogConf: "logconf", - LogDest: "syslog:server=127.0.0.1:514", - }, + "default/sub-tea-svc_version=v1:80": { + "10.0.0.50:80", }, - polKey: "default/waf-policy", - polNamespace: "default", - apResources: &appProtectResourcesForVS{ - Policies: map[string]string{ - "default/dataguard-alarm": "/etc/nginx/waf/nac-policies/default-dataguard-alarm", + }, + VirtualServerRoutes: []*conf_v1.VirtualServerRoute{ + { + ObjectMeta: meta_v1.ObjectMeta{ + Name: "coffee", + Namespace: "default", }, - LogConfs: map[string]string{ - "default/logconf": "/etc/nginx/waf/nac-logconfs/default-logconf", + Spec: conf_v1.VirtualServerRouteSpec{ + Host: "cafe.example.com", + Upstreams: []conf_v1.Upstream{ + { + Name: "coffee", + Service: "coffee-svc", + Port: 80, + }, + }, + Subroutes: []conf_v1.Route{ + { + Path: "/coffee", + Action: &conf_v1.Action{ + Pass: "coffee", + }, + }, + }, }, }, - wafConfig: &version2.WAF{ - ApPolicy: "/etc/nginx/waf/nac-policies/default-dataguard-alarm", - ApSecurityLogEnable: true, - ApLogConf: []string{"/etc/nginx/waf/nac-logconfs/default-logconf"}, - }, - expected: &validationResults{isError: false}, - msg: "valid waf config", - }, - { - wafInput: &conf_v1.WAF{ - Enable: true, - ApPolicy: "dataguard-alarm", - SecurityLogs: []*conf_v1.SecurityLog{ - { - Enable: true, - ApLogConf: "logconf", - LogDest: "syslog:server=127.0.0.1:514", + { + ObjectMeta: meta_v1.ObjectMeta{ + Name: "subtea", + Namespace: "default", + }, + Spec: conf_v1.VirtualServerRouteSpec{ + Host: "cafe.example.com", + Upstreams: []conf_v1.Upstream{ + { + Name: "subtea", + Service: "sub-tea-svc", + Port: 80, + Subselector: map[string]string{"version": "v1"}, + }, + }, + Subroutes: []conf_v1.Route{ + { + Path: "/subtea", + Action: &conf_v1.Action{ + Pass: "subtea", + }, + }, }, }, }, - polKey: "default/waf-policy", - polNamespace: "default", - apResources: &appProtectResourcesForVS{ - Policies: map[string]string{ - "default/dataguard-alarm": "/etc/nginx/waf/nac-policies/default-dataguard-alarm", + { + ObjectMeta: meta_v1.ObjectMeta{ + Name: "subcoffee", + Namespace: "default", }, - LogConfs: map[string]string{ - "default/logconf": "/etc/nginx/waf/nac-logconfs/default-logconf", + Spec: conf_v1.VirtualServerRouteSpec{ + Host: "cafe.example.com", + Upstreams: []conf_v1.Upstream{ + { + Name: "coffee", + Service: "coffee-svc", + Port: 80, + }, + }, + Subroutes: []conf_v1.Route{ + { + Path: "/coffee-errorpage-subroute", + Action: &conf_v1.Action{ + Pass: "coffee", + }, + }, + { + Path: "/coffee-errorpage-subroute-defined", + Action: &conf_v1.Action{ + Pass: "coffee", + }, + ErrorPages: []conf_v1.ErrorPage{ + { + Codes: []int{502, 503}, + Return: &conf_v1.ErrorPageReturn{ + ActionReturn: conf_v1.ActionReturn{ + Code: 200, + Type: "text/plain", + Body: "All Good", + }, + }, + }, + }, + }, + }, }, }, - wafConfig: &version2.WAF{ - ApPolicy: "/etc/nginx/waf/nac-policies/default-dataguard-alarm", - ApSecurityLogEnable: true, - ApLogConf: []string{"/etc/nginx/waf/nac-logconfs/default-logconf"}, - }, - expected: &validationResults{isError: false}, - msg: "valid waf config", }, - { - wafInput: &conf_v1.WAF{ - Enable: true, - ApPolicy: "default/dataguard-alarm", - SecurityLog: &conf_v1.SecurityLog{ - Enable: true, - ApLogConf: "default/logconf", - LogDest: "syslog:server=127.0.0.1:514", - }, + } + + virtualServerExWithNoGunzip = VirtualServerEx{ + VirtualServer: &conf_v1.VirtualServer{ + ObjectMeta: meta_v1.ObjectMeta{ + Name: "cafe", + Namespace: "default", }, - polKey: "default/waf-policy", - polNamespace: "", - apResources: &appProtectResourcesForVS{ - Policies: map[string]string{ - "default/dataguard-alarm": "/etc/nginx/waf/nac-policies/default-dataguard-alarm", + Spec: conf_v1.VirtualServerSpec{ + Host: "cafe.example.com", + Upstreams: []conf_v1.Upstream{ + { + Name: "tea", + Service: "tea-svc", + Port: 80, + }, + { + Name: "tea-latest", + Service: "tea-svc", + Subselector: map[string]string{"version": "v1"}, + Port: 80, + }, + { + Name: "coffee", + Service: "coffee-svc", + Port: 80, + }, }, - LogConfs: map[string]string{}, - }, - wafConfig: &version2.WAF{ - ApPolicy: "/etc/nginx/waf/nac-policies/default-dataguard-alarm", - ApSecurityLogEnable: true, - ApLogConf: []string{"/etc/nginx/waf/nac-logconfs/default-logconf"}, - }, - expected: &validationResults{ - isError: true, - warnings: []string{ - `WAF policy default/waf-policy references an invalid or non-existing log config default/logconf`, + Routes: []conf_v1.Route{ + { + Path: "/tea", + Action: &conf_v1.Action{ + Pass: "tea", + }, + }, + { + Path: "/tea-latest", + Action: &conf_v1.Action{ + Pass: "tea-latest", + }, + }, + { + Path: "/coffee", + Route: "default/coffee", + }, + { + Path: "/subtea", + Route: "default/subtea", + }, + { + Path: "/coffee-errorpage", + Action: &conf_v1.Action{ + Pass: "coffee", + }, + ErrorPages: []conf_v1.ErrorPage{ + { + Codes: []int{401, 403}, + Redirect: &conf_v1.ErrorPageRedirect{ + ActionRedirect: conf_v1.ActionRedirect{ + URL: "http://nginx.com", + Code: 301, + }, + }, + }, + }, + }, + { + Path: "/coffee-errorpage-subroute", + Route: "default/subcoffee", + ErrorPages: []conf_v1.ErrorPage{ + { + Codes: []int{401, 403}, + Redirect: &conf_v1.ErrorPageRedirect{ + ActionRedirect: conf_v1.ActionRedirect{ + URL: "http://nginx.com", + Code: 301, + }, + }, + }, + }, + }, }, }, - msg: "invalid waf config, apLogConf references non-existing log conf", }, - { - wafInput: &conf_v1.WAF{ - Enable: true, - ApPolicy: "default/dataguard-alarm", - SecurityLog: &conf_v1.SecurityLog{ - Enable: true, - LogDest: "syslog:server=127.0.0.1:514", - }, + Endpoints: map[string][]string{ + "default/tea-svc:80": { + "10.0.0.20:80", }, - polKey: "default/waf-policy", - polNamespace: "", - apResources: &appProtectResourcesForVS{ - Policies: map[string]string{}, - LogConfs: map[string]string{ - "default/logconf": "/etc/nginx/waf/nac-logconfs/default-logconf", - }, + "default/tea-svc_version=v1:80": { + "10.0.0.30:80", }, - wafConfig: &version2.WAF{ - ApPolicy: "/etc/nginx/waf/nac-policies/default-dataguard-alarm", - ApSecurityLogEnable: true, - ApLogConf: []string{"/etc/nginx/waf/nac-logconfs/default-logconf"}, + "default/coffee-svc:80": { + "10.0.0.40:80", }, - expected: &validationResults{ - isError: true, - warnings: []string{ - `WAF policy default/waf-policy references an invalid or non-existing App Protect policy default/dataguard-alarm`, - }, + "default/sub-tea-svc_version=v1:80": { + "10.0.0.50:80", }, - msg: "invalid waf config, apLogConf references non-existing ap conf", }, - { - wafInput: &conf_v1.WAF{ - Enable: true, - ApPolicy: "ns1/dataguard-alarm", - SecurityLog: &conf_v1.SecurityLog{ - Enable: true, - ApLogConf: "ns2/logconf", - LogDest: "syslog:server=127.0.0.1:514", + VirtualServerRoutes: []*conf_v1.VirtualServerRoute{ + { + ObjectMeta: meta_v1.ObjectMeta{ + Name: "coffee", + Namespace: "default", + }, + Spec: conf_v1.VirtualServerRouteSpec{ + Host: "cafe.example.com", + Upstreams: []conf_v1.Upstream{ + { + Name: "coffee", + Service: "coffee-svc", + Port: 80, + }, + }, + Subroutes: []conf_v1.Route{ + { + Path: "/coffee", + Action: &conf_v1.Action{ + Pass: "coffee", + }, + }, + }, }, }, - polKey: "default/waf-policy", - polNamespace: "", - apResources: &appProtectResourcesForVS{ - Policies: map[string]string{ - "ns1/dataguard-alarm": "/etc/nginx/waf/nac-policies/ns1-dataguard-alarm", + { + ObjectMeta: meta_v1.ObjectMeta{ + Name: "subtea", + Namespace: "default", }, - LogConfs: map[string]string{ - "ns2/logconf": "/etc/nginx/waf/nac-logconfs/ns2-logconf", + Spec: conf_v1.VirtualServerRouteSpec{ + Host: "cafe.example.com", + Upstreams: []conf_v1.Upstream{ + { + Name: "subtea", + Service: "sub-tea-svc", + Port: 80, + Subselector: map[string]string{"version": "v1"}, + }, + }, + Subroutes: []conf_v1.Route{ + { + Path: "/subtea", + Action: &conf_v1.Action{ + Pass: "subtea", + }, + }, + }, }, }, - wafConfig: &version2.WAF{ - ApPolicy: "/etc/nginx/waf/nac-policies/ns1-dataguard-alarm", - ApSecurityLogEnable: true, - ApLogConf: []string{"/etc/nginx/waf/nac-logconfs/ns2-logconf"}, - }, - expected: &validationResults{}, - msg: "valid waf config, cross ns reference", - }, - { - wafInput: &conf_v1.WAF{ - Enable: false, - ApPolicy: "dataguard-alarm", - }, - polKey: "default/waf-policy", - polNamespace: "default", - apResources: &appProtectResourcesForVS{ - Policies: map[string]string{ - "default/dataguard-alarm": "/etc/nginx/waf/nac-policies/ns1-dataguard-alarm", + { + ObjectMeta: meta_v1.ObjectMeta{ + Name: "subcoffee", + Namespace: "default", }, - LogConfs: map[string]string{ - "default/logconf": "/etc/nginx/waf/nac-logconfs/ns2-logconf", + Spec: conf_v1.VirtualServerRouteSpec{ + Host: "cafe.example.com", + Upstreams: []conf_v1.Upstream{ + { + Name: "coffee", + Service: "coffee-svc", + Port: 80, + }, + }, + Subroutes: []conf_v1.Route{ + { + Path: "/coffee-errorpage-subroute", + Action: &conf_v1.Action{ + Pass: "coffee", + }, + }, + { + Path: "/coffee-errorpage-subroute-defined", + Action: &conf_v1.Action{ + Pass: "coffee", + }, + ErrorPages: []conf_v1.ErrorPage{ + { + Codes: []int{502, 503}, + Return: &conf_v1.ErrorPageReturn{ + ActionReturn: conf_v1.ActionReturn{ + Code: 200, + Type: "text/plain", + Body: "All Good", + }, + }, + }, + }, + }, + }, }, }, - wafConfig: &version2.WAF{ - Enable: "off", - ApPolicy: "/etc/nginx/waf/nac-policies/ns1-dataguard-alarm", - }, - expected: &validationResults{}, - msg: "valid waf config, disable waf", - }, - } - - for _, test := range tests { - polCfg := newPoliciesConfig() - result := polCfg.addWAFConfig(test.wafInput, test.polKey, test.polNamespace, test.apResources) - if diff := cmp.Diff(test.expected.warnings, result.warnings); diff != "" { - t.Errorf("policiesCfg.addWAFConfig() '%v' mismatch (-want +got):\n%s", test.msg, diff) - } - } -} - -func TestGenerateTime(t *testing.T) { - t.Parallel() - tests := []struct { - value, expected string - }{ - { - value: "0s", - expected: "0s", - }, - { - value: "0", - expected: "0s", - }, - { - value: "1h", - expected: "1h", - }, - { - value: "1h 30m", - expected: "1h30m", - }, - } - - for _, test := range tests { - result := generateTime(test.value) - if result != test.expected { - t.Errorf("generateTime(%q) returned %q but expected %q", test.value, result, test.expected) - } - } -} - -func TestGenerateTimeWithDefault(t *testing.T) { - t.Parallel() - tests := []struct { - value, defaultValue, expected string - }{ - { - value: "1h 30m", - defaultValue: "", - expected: "1h30m", - }, - { - value: "", - defaultValue: "60s", - expected: "60s", - }, - { - value: "", - defaultValue: "test", - expected: "test", }, } - - for _, test := range tests { - result := generateTimeWithDefault(test.value, test.defaultValue) - if result != test.expected { - t.Errorf("generateTimeWithDefault(%q, %q) returned %q but expected %q", test.value, test.defaultValue, result, test.expected) - } - } -} +) diff --git a/pkg/apis/configuration/v1/types.go b/pkg/apis/configuration/v1/types.go index d6d55e5e77..adc519df13 100644 --- a/pkg/apis/configuration/v1/types.go +++ b/pkg/apis/configuration/v1/types.go @@ -39,7 +39,7 @@ type VirtualServerSpec struct { IngressClass string `json:"ingressClassName"` Host string `json:"host"` TLS *TLS `json:"tls"` - Gunzip string `json:"gunzip"` + Gunzip bool `json:"gunzip"` Policies []PolicyReference `json:"policies"` Upstreams []Upstream `json:"upstreams"` Routes []Route `json:"routes"` diff --git a/pkg/apis/configuration/validation/virtualserver.go b/pkg/apis/configuration/validation/virtualserver.go index ee036d9cf3..1ac7be51e8 100644 --- a/pkg/apis/configuration/validation/virtualserver.go +++ b/pkg/apis/configuration/validation/virtualserver.go @@ -77,7 +77,6 @@ func (vsv *VirtualServerValidator) validateVirtualServerSpec(spec *v1.VirtualSer allErrs := field.ErrorList{} allErrs = append(allErrs, validateHost(spec.Host, fieldPath.Child("host"))...) - allErrs = append(allErrs, validateGunzip(spec.Gunzip, fieldPath.Child("gunzip"))...) allErrs = append(allErrs, vsv.validateTLS(spec.TLS, fieldPath.Child("tls"))...) allErrs = append(allErrs, validatePolicies(spec.Policies, fieldPath.Child("policies"), namespace)...) @@ -115,15 +114,6 @@ func validateHost(host string, fieldPath *field.Path) field.ErrorList { return allErrs } -func validateGunzip(fieldValue string, fl *field.Path) field.ErrorList { - switch fieldValue { - case "on", "off", "": - return nil - default: - return field.ErrorList{field.NotSupported(fl, fieldValue, []string{"on", "off"})} - } -} - func validatePolicies(policies []v1.PolicyReference, fieldPath *field.Path, namespace string) field.ErrorList { allErrs := field.ErrorList{} policyKeys := sets.Set[string]{} diff --git a/pkg/apis/configuration/validation/virtualserver_test.go b/pkg/apis/configuration/validation/virtualserver_test.go index 6eae75a179..a694fa76d2 100644 --- a/pkg/apis/configuration/validation/virtualserver_test.go +++ b/pkg/apis/configuration/validation/virtualserver_test.go @@ -69,215 +69,6 @@ func TestValidateVirtualServer(t *testing.T) { } } -func TestValidateVirtualServer_PassesOnValidGunzipOn(t *testing.T) { - t.Parallel() - - vsv := &VirtualServerValidator{isPlus: false, isDosEnabled: false} - err := vsv.ValidateVirtualServer(&virtualServerWithValidGunzipOn) - if err != nil { - t.Errorf("ValidateVirtualServer() returned error %v for valid input %+v", err, virtualServerWithValidGunzipOn) - } -} - -func TestValidateVirtualServer_PassesOnValidGunzipOff(t *testing.T) { - t.Parallel() - - vsv := &VirtualServerValidator{isPlus: false, isDosEnabled: false} - err := vsv.ValidateVirtualServer(&virtualServerWithValidGunzipOff) - if err != nil { - t.Errorf("ValidateVirtualServer() returned error %v for valid input %+v", err, virtualServerWithValidGunzipOff) - } -} - -func TestValidateVirtualServer_PassesOnNoGunzip(t *testing.T) { - t.Parallel() - - vsv := &VirtualServerValidator{isPlus: false, isDosEnabled: false} - err := vsv.ValidateVirtualServer(&virtualServerWithNoGunzip) - if err != nil { - t.Errorf("ValidateVirtualServer() returned error %v for valid input %+v", err, virtualServerWithNoGunzip) - } -} - -func TestValidateVirtualServer_FailsOnBogusGunzipValue(t *testing.T) { - t.Parallel() - - vsv := &VirtualServerValidator{isPlus: false, isDosEnabled: false} - err := vsv.ValidateVirtualServer(&virtualServerWithBogusGunzipValue) - if err == nil { - t.Error("ValidateVirtualServer() returned no error on bogus gunzip value") - } -} - -var ( - virtualServerWithValidGunzipOn = v1.VirtualServer{ - ObjectMeta: meta_v1.ObjectMeta{ - Name: "cafe", - Namespace: "default", - }, - Spec: v1.VirtualServerSpec{ - Host: "example.com", - Gunzip: "on", - Upstreams: []v1.Upstream{ - { - Name: "first", - Service: "service-1", - LBMethod: "random", - Port: 80, - MaxFails: createPointerFromInt(8), - MaxConns: createPointerFromInt(16), - Keepalive: createPointerFromInt(32), - Type: "grpc", - }, - { - Name: "second", - Service: "service-2", - Port: 80, - }, - }, - Routes: []v1.Route{ - { - Path: "/first", - Action: &v1.Action{ - Pass: "first", - }, - }, - { - Path: "/second", - Action: &v1.Action{ - Pass: "second", - }, - }, - }, - }, - } - - virtualServerWithValidGunzipOff = v1.VirtualServer{ - ObjectMeta: meta_v1.ObjectMeta{ - Name: "cafe", - Namespace: "default", - }, - Spec: v1.VirtualServerSpec{ - Host: "example.com", - Gunzip: "off", - Upstreams: []v1.Upstream{ - { - Name: "first", - Service: "service-1", - LBMethod: "random", - Port: 80, - MaxFails: createPointerFromInt(8), - MaxConns: createPointerFromInt(16), - Keepalive: createPointerFromInt(32), - Type: "grpc", - }, - { - Name: "second", - Service: "service-2", - Port: 80, - }, - }, - Routes: []v1.Route{ - { - Path: "/first", - Action: &v1.Action{ - Pass: "first", - }, - }, - { - Path: "/second", - Action: &v1.Action{ - Pass: "second", - }, - }, - }, - }, - } - - virtualServerWithNoGunzip = v1.VirtualServer{ - ObjectMeta: meta_v1.ObjectMeta{ - Name: "cafe", - Namespace: "default", - }, - Spec: v1.VirtualServerSpec{ - Host: "example.com", - Upstreams: []v1.Upstream{ - { - Name: "first", - Service: "service-1", - LBMethod: "random", - Port: 80, - MaxFails: createPointerFromInt(8), - MaxConns: createPointerFromInt(16), - Keepalive: createPointerFromInt(32), - Type: "grpc", - }, - { - Name: "second", - Service: "service-2", - Port: 80, - }, - }, - Routes: []v1.Route{ - { - Path: "/first", - Action: &v1.Action{ - Pass: "first", - }, - }, - { - Path: "/second", - Action: &v1.Action{ - Pass: "second", - }, - }, - }, - }, - } - - virtualServerWithBogusGunzipValue = v1.VirtualServer{ - ObjectMeta: meta_v1.ObjectMeta{ - Name: "cafe", - Namespace: "default", - }, - Spec: v1.VirtualServerSpec{ - Host: "example.com", - Gunzip: "bogus", - Upstreams: []v1.Upstream{ - { - Name: "first", - Service: "service-1", - LBMethod: "random", - Port: 80, - MaxFails: createPointerFromInt(8), - MaxConns: createPointerFromInt(16), - Keepalive: createPointerFromInt(32), - Type: "grpc", - }, - { - Name: "second", - Service: "service-2", - Port: 80, - }, - }, - Routes: []v1.Route{ - { - Path: "/first", - Action: &v1.Action{ - Pass: "first", - }, - }, - { - Path: "/second", - Action: &v1.Action{ - Pass: "second", - }, - }, - }, - }, - } -) - func TestValidateHost(t *testing.T) { t.Parallel() validHosts := []string{ @@ -311,26 +102,6 @@ func TestValidateHost(t *testing.T) { } } -func TestValidateGunzip_FailsOnBogusGunzipValue(t *testing.T) { - t.Parallel() - bogusGunzipValue := "bogus" - allErr := validateGunzip(bogusGunzipValue, field.NewPath("gunzip")) - if len(allErr) == 0 { - t.Errorf("validateGunzip(%q) did not return error on invalid input.", bogusGunzipValue) - } -} - -func TestValidateGunzip_PassesOnValidGunzipValues(t *testing.T) { - t.Parallel() - tt := []string{"on", "off", ""} - for _, v := range tt { - allErr := validateGunzip(v, field.NewPath("gunzip")) - if len(allErr) > 0 { - t.Errorf("validateGunzip(%q) returned errors %v for valid input", v, allErr) - } - } -} - func TestValidateDos(t *testing.T) { t.Parallel() validDosResources := []string{ diff --git a/tests/data/virtual-server/virtual-server-gunzip.yaml b/tests/data/virtual-server/virtual-server-gunzip.yaml new file mode 100644 index 0000000000..41a0f0154c --- /dev/null +++ b/tests/data/virtual-server/virtual-server-gunzip.yaml @@ -0,0 +1,21 @@ +apiVersion: k8s.nginx.org/v1 +kind: VirtualServer +metadata: + name: virtual-server +spec: + host: virtual-server.example.com + gunzip: on + upstreams: + - name: backend2 + service: backend2-svc + port: 80 + - name: backend1 + service: backend1-svc + port: 80 + routes: + - path: "/backend1" + action: + pass: backend1 + - path: "/backend2" + action: + pass: backend2 diff --git a/tests/suite/test_virtual_server.py b/tests/suite/test_virtual_server.py index b13c46838a..1048162159 100644 --- a/tests/suite/test_virtual_server.py +++ b/tests/suite/test_virtual_server.py @@ -5,6 +5,7 @@ from suite.utils.resources_utils import ( create_service_from_yaml, delete_service, + get_first_pod_name, patch_rbac, read_service, replace_service, @@ -13,6 +14,7 @@ from suite.utils.vs_vsr_resources_utils import ( create_virtual_server_from_yaml, delete_virtual_server, + get_vs_nginx_template_conf, patch_virtual_server_from_yaml, ) from suite.utils.yaml_utils import get_first_host_from_yaml, get_name_from_yaml, get_paths_from_vs_yaml @@ -166,6 +168,46 @@ def test_responses_after_crd_removal_on_the_fly(self, kube_apis, crd_ingress_con wait_and_assert_status_code(200, virtual_server_setup.backend_1_url, virtual_server_setup.vs_host) wait_and_assert_status_code(200, virtual_server_setup.backend_2_url, virtual_server_setup.vs_host) + def test_responses_after_virtual_server_update_with_gunzip( + self, kube_apis, ingress_controller_prerequisites, crd_ingress_controller, virtual_server_setup + ): + print("Step 1: update gunzip in the VS and check") + patch_virtual_server_from_yaml( + kube_apis.custom_objects, + virtual_server_setup.vs_name, + f"{TEST_DATA}/virtual-server/virtual-server-gunzip.yaml", + virtual_server_setup.namespace, + ) + wait_before_test(1) + wait_and_assert_status_code(200, virtual_server_setup.backend_1_url, virtual_server_setup.vs_host) + wait_and_assert_status_code(200, virtual_server_setup.backend_2_url, virtual_server_setup.vs_host) + + print("Step 2: verify gunzip directive is present") + + pod_name = get_first_pod_name(kube_apis.v1, ingress_controller_prerequisites.namespace) + + confFile = get_vs_nginx_template_conf( + kube_apis.v1, + virtual_server_setup.namespace, + virtual_server_setup.vs_name, + pod_name, + ingress_controller_prerequisites.namespace, + ) + + assert "gunzip on;" in confFile + + print("Step 3: restore VS and check") + patch_virtual_server_from_yaml( + kube_apis.custom_objects, + virtual_server_setup.vs_name, + f"{TEST_DATA}/virtual-server/standard/virtual-server.yaml", + virtual_server_setup.namespace, + ) + wait_before_test(1) + wait_and_assert_status_code(200, virtual_server_setup.backend_1_url, virtual_server_setup.vs_host) + wait_and_assert_status_code(200, virtual_server_setup.backend_2_url, virtual_server_setup.vs_host) + + @pytest.mark.vs @pytest.mark.parametrize( From 34103c64f8a09e7dd93e459ffefdc4e83637a222 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 3 May 2023 16:59:42 +0000 Subject: [PATCH 2/3] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- tests/suite/test_virtual_server.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/suite/test_virtual_server.py b/tests/suite/test_virtual_server.py index 1048162159..8e254d804c 100644 --- a/tests/suite/test_virtual_server.py +++ b/tests/suite/test_virtual_server.py @@ -208,7 +208,6 @@ def test_responses_after_virtual_server_update_with_gunzip( wait_and_assert_status_code(200, virtual_server_setup.backend_2_url, virtual_server_setup.vs_host) - @pytest.mark.vs @pytest.mark.parametrize( "crd_ingress_controller, virtual_server_setup", From 94cb84175dfca0dee644bc8e97d3ae92571764e7 Mon Sep 17 00:00:00 2001 From: Jakub Jarosz Date: Thu, 4 May 2023 12:34:13 +0100 Subject: [PATCH 3/3] Update docs --- .../virtualserver-and-virtualserverroute-resources.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/content/configuration/virtualserver-and-virtualserverroute-resources.md b/docs/content/configuration/virtualserver-and-virtualserverroute-resources.md index 2bbbfd0473..3240d64794 100644 --- a/docs/content/configuration/virtualserver-and-virtualserverroute-resources.md +++ b/docs/content/configuration/virtualserver-and-virtualserverroute-resources.md @@ -54,7 +54,7 @@ spec: | ---| ---| ---| --- | |``host`` | The host (domain name) of the server. Must be a valid subdomain as defined in RFC 1123, such as ``my-app`` or ``hello.example.com``. When using a wildcard domain like ``*.example.com`` the domain must be contained in double quotes. The ``host`` value needs to be unique among all Ingress and VirtualServer resources. See also [Handling Host and Listener Collisions](/nginx-ingress-controller/configuration/handling-host-and-listener-collisions). | ``string`` | Yes | |``tls`` | The TLS termination configuration. | [tls](#virtualservertls) | No | -|``gunzip`` | Enables or disables [decompression](https://docs.nginx.com/nginx/admin-guide/web-server/compression/) of gzipped responses for clients. Allowed values are: "on" or "off". If the ``gunzip`` value is not set, it defaults to ``off``. | ``boolean`` | No | +|``gunzip`` | Enables or disables [decompression](https://docs.nginx.com/nginx/admin-guide/web-server/compression/) of gzipped responses for clients. Allowed values “on”/“off”, “true”/“false” or “yes”/“no”. If the ``gunzip`` value is not set, it defaults to ``off``. | ``boolean`` | No | |``externalDNS`` | The externalDNS configuration for a VirtualServer. | [externalDNS](#virtualserverexternaldns) | No | |``dos`` | A reference to a DosProtectedResource, setting this enables DOS protection of the VirtualServer. | ``string`` | No | |``policies`` | A list of policies. | [[]policy](#virtualserverpolicy) | No |