diff --git a/deployments/common/vs-definition.yaml b/deployments/common/vs-definition.yaml index 593767d84b..bc425cdd28 100644 --- a/deployments/common/vs-definition.yaml +++ b/deployments/common/vs-definition.yaml @@ -71,6 +71,62 @@ spec: properties: pass: type: string + proxy: + description: ActionProxy defines a proxy in an Action. + type: object + properties: + requestHeaders: + description: ProxyRequestHeaders defines the request headers + manipulation in an ActionProxy. + type: object + properties: + pass: + type: boolean + set: + type: array + items: + description: Header defines an HTTP Header. + type: object + properties: + name: + type: string + value: + type: string + responseHeaders: + description: ProxyRequestHeaders defines the response + headers manipulation in an ActionProxy. + type: object + properties: + add: + type: array + items: + description: Header defines an HTTP Header with + an optional Always field to use with the add_header + NGINX directive. + type: object + properties: + always: + type: boolean + name: + type: string + value: + type: string + hide: + type: array + items: + type: string + ignore: + type: array + items: + type: string + pass: + type: array + items: + type: string + rewritePath: + type: string + upstream: + type: string redirect: description: ActionRedirect defines a redirect in an Action. type: object @@ -140,6 +196,62 @@ spec: properties: pass: type: string + proxy: + description: ActionProxy defines a proxy in an Action. + type: object + properties: + requestHeaders: + description: ProxyRequestHeaders defines the request + headers manipulation in an ActionProxy. + type: object + properties: + pass: + type: boolean + set: + type: array + items: + description: Header defines an HTTP Header. + type: object + properties: + name: + type: string + value: + type: string + responseHeaders: + description: ProxyRequestHeaders defines the response + headers manipulation in an ActionProxy. + type: object + properties: + add: + type: array + items: + description: Header defines an HTTP Header + with an optional Always field to use with + the add_header NGINX directive. + type: object + properties: + always: + type: boolean + name: + type: string + value: + type: string + hide: + type: array + items: + type: string + ignore: + type: array + items: + type: string + pass: + type: array + items: + type: string + rewritePath: + type: string + upstream: + type: string redirect: description: ActionRedirect defines a redirect in an Action. @@ -187,6 +299,64 @@ spec: properties: pass: type: string + proxy: + description: ActionProxy defines a proxy in an + Action. + type: object + properties: + requestHeaders: + description: ProxyRequestHeaders defines the + request headers manipulation in an ActionProxy. + type: object + properties: + pass: + type: boolean + set: + type: array + items: + description: Header defines an HTTP + Header. + type: object + properties: + name: + type: string + value: + type: string + responseHeaders: + description: ProxyRequestHeaders defines the + response headers manipulation in an ActionProxy. + type: object + properties: + add: + type: array + items: + description: Header defines an HTTP + Header with an optional Always field + to use with the add_header NGINX directive. + type: object + properties: + always: + type: boolean + name: + type: string + value: + type: string + hide: + type: array + items: + type: string + ignore: + type: array + items: + type: string + pass: + type: array + items: + type: string + rewritePath: + type: string + upstream: + type: string redirect: description: ActionRedirect defines a redirect in an Action. @@ -225,6 +395,62 @@ spec: properties: pass: type: string + proxy: + description: ActionProxy defines a proxy in an Action. + type: object + properties: + requestHeaders: + description: ProxyRequestHeaders defines the request + headers manipulation in an ActionProxy. + type: object + properties: + pass: + type: boolean + set: + type: array + items: + description: Header defines an HTTP Header. + type: object + properties: + name: + type: string + value: + type: string + responseHeaders: + description: ProxyRequestHeaders defines the response + headers manipulation in an ActionProxy. + type: object + properties: + add: + type: array + items: + description: Header defines an HTTP Header + with an optional Always field to use with + the add_header NGINX directive. + type: object + properties: + always: + type: boolean + name: + type: string + value: + type: string + hide: + type: array + items: + type: string + ignore: + type: array + items: + type: string + pass: + type: array + items: + type: string + rewritePath: + type: string + upstream: + type: string redirect: description: ActionRedirect defines a redirect in an Action. diff --git a/deployments/common/vsr-definition.yaml b/deployments/common/vsr-definition.yaml index e960db4363..ec4e7f4488 100644 --- a/deployments/common/vsr-definition.yaml +++ b/deployments/common/vsr-definition.yaml @@ -69,6 +69,62 @@ spec: properties: pass: type: string + proxy: + description: ActionProxy defines a proxy in an Action. + type: object + properties: + requestHeaders: + description: ProxyRequestHeaders defines the request headers + manipulation in an ActionProxy. + type: object + properties: + pass: + type: boolean + set: + type: array + items: + description: Header defines an HTTP Header. + type: object + properties: + name: + type: string + value: + type: string + responseHeaders: + description: ProxyRequestHeaders defines the response + headers manipulation in an ActionProxy. + type: object + properties: + add: + type: array + items: + description: Header defines an HTTP Header with + an optional Always field to use with the add_header + NGINX directive. + type: object + properties: + always: + type: boolean + name: + type: string + value: + type: string + hide: + type: array + items: + type: string + ignore: + type: array + items: + type: string + pass: + type: array + items: + type: string + rewritePath: + type: string + upstream: + type: string redirect: description: ActionRedirect defines a redirect in an Action. type: object @@ -138,6 +194,62 @@ spec: properties: pass: type: string + proxy: + description: ActionProxy defines a proxy in an Action. + type: object + properties: + requestHeaders: + description: ProxyRequestHeaders defines the request + headers manipulation in an ActionProxy. + type: object + properties: + pass: + type: boolean + set: + type: array + items: + description: Header defines an HTTP Header. + type: object + properties: + name: + type: string + value: + type: string + responseHeaders: + description: ProxyRequestHeaders defines the response + headers manipulation in an ActionProxy. + type: object + properties: + add: + type: array + items: + description: Header defines an HTTP Header + with an optional Always field to use with + the add_header NGINX directive. + type: object + properties: + always: + type: boolean + name: + type: string + value: + type: string + hide: + type: array + items: + type: string + ignore: + type: array + items: + type: string + pass: + type: array + items: + type: string + rewritePath: + type: string + upstream: + type: string redirect: description: ActionRedirect defines a redirect in an Action. @@ -185,6 +297,64 @@ spec: properties: pass: type: string + proxy: + description: ActionProxy defines a proxy in an + Action. + type: object + properties: + requestHeaders: + description: ProxyRequestHeaders defines the + request headers manipulation in an ActionProxy. + type: object + properties: + pass: + type: boolean + set: + type: array + items: + description: Header defines an HTTP + Header. + type: object + properties: + name: + type: string + value: + type: string + responseHeaders: + description: ProxyRequestHeaders defines the + response headers manipulation in an ActionProxy. + type: object + properties: + add: + type: array + items: + description: Header defines an HTTP + Header with an optional Always field + to use with the add_header NGINX directive. + type: object + properties: + always: + type: boolean + name: + type: string + value: + type: string + hide: + type: array + items: + type: string + ignore: + type: array + items: + type: string + pass: + type: array + items: + type: string + rewritePath: + type: string + upstream: + type: string redirect: description: ActionRedirect defines a redirect in an Action. @@ -223,6 +393,62 @@ spec: properties: pass: type: string + proxy: + description: ActionProxy defines a proxy in an Action. + type: object + properties: + requestHeaders: + description: ProxyRequestHeaders defines the request + headers manipulation in an ActionProxy. + type: object + properties: + pass: + type: boolean + set: + type: array + items: + description: Header defines an HTTP Header. + type: object + properties: + name: + type: string + value: + type: string + responseHeaders: + description: ProxyRequestHeaders defines the response + headers manipulation in an ActionProxy. + type: object + properties: + add: + type: array + items: + description: Header defines an HTTP Header + with an optional Always field to use with + the add_header NGINX directive. + type: object + properties: + always: + type: boolean + name: + type: string + value: + type: string + hide: + type: array + items: + type: string + ignore: + type: array + items: + type: string + pass: + type: array + items: + type: string + rewritePath: + type: string + upstream: + type: string redirect: description: ActionRedirect defines a redirect in an Action. diff --git a/deployments/helm-chart/templates/controller-vs-definition.yaml b/deployments/helm-chart/templates/controller-vs-definition.yaml index 8936296b76..f8849537f6 100644 --- a/deployments/helm-chart/templates/controller-vs-definition.yaml +++ b/deployments/helm-chart/templates/controller-vs-definition.yaml @@ -74,6 +74,62 @@ spec: properties: pass: type: string + proxy: + description: ActionProxy defines a proxy in an Action. + type: object + properties: + requestHeaders: + description: ProxyRequestHeaders defines the request headers + manipulation in an ActionProxy. + type: object + properties: + pass: + type: boolean + set: + type: array + items: + description: Header defines an HTTP Header. + type: object + properties: + name: + type: string + value: + type: string + responseHeaders: + description: ProxyRequestHeaders defines the response + headers manipulation in an ActionProxy. + type: object + properties: + add: + type: array + items: + description: Header defines an HTTP Header with + an optional Always field to use with the add_header + NGINX directive. + type: object + properties: + always: + type: boolean + name: + type: string + value: + type: string + hide: + type: array + items: + type: string + ignore: + type: array + items: + type: string + pass: + type: array + items: + type: string + rewritePath: + type: string + upstream: + type: string redirect: description: ActionRedirect defines a redirect in an Action. type: object @@ -143,6 +199,62 @@ spec: properties: pass: type: string + proxy: + description: ActionProxy defines a proxy in an Action. + type: object + properties: + requestHeaders: + description: ProxyRequestHeaders defines the request headers + manipulation in an ActionProxy. + type: object + properties: + pass: + type: boolean + set: + type: array + items: + description: Header defines an HTTP Header. + type: object + properties: + name: + type: string + value: + type: string + responseHeaders: + description: ProxyRequestHeaders defines the response + headers manipulation in an ActionProxy. + type: object + properties: + add: + type: array + items: + description: Header defines an HTTP Header with + an optional Always field to use with the add_header + NGINX directive. + type: object + properties: + always: + type: boolean + name: + type: string + value: + type: string + hide: + type: array + items: + type: string + ignore: + type: array + items: + type: string + pass: + type: array + items: + type: string + rewritePath: + type: string + upstream: + type: string redirect: description: ActionRedirect defines a redirect in an Action. @@ -190,6 +302,62 @@ spec: properties: pass: type: string + proxy: + description: ActionProxy defines a proxy in an Action. + type: object + properties: + requestHeaders: + description: ProxyRequestHeaders defines the request headers + manipulation in an ActionProxy. + type: object + properties: + pass: + type: boolean + set: + type: array + items: + description: Header defines an HTTP Header. + type: object + properties: + name: + type: string + value: + type: string + responseHeaders: + description: ProxyRequestHeaders defines the response + headers manipulation in an ActionProxy. + type: object + properties: + add: + type: array + items: + description: Header defines an HTTP Header with + an optional Always field to use with the add_header + NGINX directive. + type: object + properties: + always: + type: boolean + name: + type: string + value: + type: string + hide: + type: array + items: + type: string + ignore: + type: array + items: + type: string + pass: + type: array + items: + type: string + rewritePath: + type: string + upstream: + type: string redirect: description: ActionRedirect defines a redirect in an Action. @@ -228,6 +396,64 @@ spec: properties: pass: type: string + proxy: + description: ActionProxy defines a proxy in an + Action. + type: object + properties: + requestHeaders: + description: ProxyRequestHeaders defines the + request headers manipulation in an ActionProxy. + type: object + properties: + pass: + type: boolean + set: + type: array + items: + description: Header defines an HTTP + Header. + type: object + properties: + name: + type: string + value: + type: string + responseHeaders: + description: ProxyRequestHeaders defines the + response headers manipulation in an ActionProxy. + type: object + properties: + add: + type: array + items: + description: Header defines an HTTP + Header with an optional Always field + to use with the add_header NGINX directive. + type: object + properties: + always: + type: boolean + name: + type: string + value: + type: string + hide: + type: array + items: + type: string + ignore: + type: array + items: + type: string + pass: + type: array + items: + type: string + rewritePath: + type: string + upstream: + type: string redirect: description: ActionRedirect defines a redirect in an Action. diff --git a/deployments/helm-chart/templates/controller-vsr-definition.yaml b/deployments/helm-chart/templates/controller-vsr-definition.yaml index 3da6d71648..bb0fadb8b9 100644 --- a/deployments/helm-chart/templates/controller-vsr-definition.yaml +++ b/deployments/helm-chart/templates/controller-vsr-definition.yaml @@ -72,6 +72,62 @@ spec: properties: pass: type: string + proxy: + description: ActionProxy defines a proxy in an Action. + type: object + properties: + requestHeaders: + description: ProxyRequestHeaders defines the request + headers manipulation in an ActionProxy. + type: object + properties: + pass: + type: boolean + set: + type: array + items: + description: Header defines an HTTP Header. + type: object + properties: + name: + type: string + value: + type: string + responseHeaders: + description: ProxyRequestHeaders defines the response + headers manipulation in an ActionProxy. + type: object + properties: + add: + type: array + items: + description: Header defines an HTTP Header + with an optional Always field to use with + the add_header NGINX directive. + type: object + properties: + always: + type: boolean + name: + type: string + value: + type: string + hide: + type: array + items: + type: string + ignore: + type: array + items: + type: string + pass: + type: array + items: + type: string + rewritePath: + type: string + upstream: + type: string redirect: description: ActionRedirect defines a redirect in an Action. type: object @@ -141,6 +197,62 @@ spec: properties: pass: type: string + proxy: + description: ActionProxy defines a proxy in an Action. + type: object + properties: + requestHeaders: + description: ProxyRequestHeaders defines the request + headers manipulation in an ActionProxy. + type: object + properties: + pass: + type: boolean + set: + type: array + items: + description: Header defines an HTTP Header. + type: object + properties: + name: + type: string + value: + type: string + responseHeaders: + description: ProxyRequestHeaders defines the response + headers manipulation in an ActionProxy. + type: object + properties: + add: + type: array + items: + description: Header defines an HTTP Header + with an optional Always field to use with + the add_header NGINX directive. + type: object + properties: + always: + type: boolean + name: + type: string + value: + type: string + hide: + type: array + items: + type: string + ignore: + type: array + items: + type: string + pass: + type: array + items: + type: string + rewritePath: + type: string + upstream: + type: string redirect: description: ActionRedirect defines a redirect in an Action. @@ -188,6 +300,64 @@ spec: properties: pass: type: string + proxy: + description: ActionProxy defines a proxy in an + Action. + type: object + properties: + requestHeaders: + description: ProxyRequestHeaders defines the + request headers manipulation in an ActionProxy. + type: object + properties: + pass: + type: boolean + set: + type: array + items: + description: Header defines an HTTP + Header. + type: object + properties: + name: + type: string + value: + type: string + responseHeaders: + description: ProxyRequestHeaders defines the + response headers manipulation in an ActionProxy. + type: object + properties: + add: + type: array + items: + description: Header defines an HTTP + Header with an optional Always field + to use with the add_header NGINX directive. + type: object + properties: + always: + type: boolean + name: + type: string + value: + type: string + hide: + type: array + items: + type: string + ignore: + type: array + items: + type: string + pass: + type: array + items: + type: string + rewritePath: + type: string + upstream: + type: string redirect: description: ActionRedirect defines a redirect in an Action. @@ -226,6 +396,62 @@ spec: properties: pass: type: string + proxy: + description: ActionProxy defines a proxy in an Action. + type: object + properties: + requestHeaders: + description: ProxyRequestHeaders defines the request + headers manipulation in an ActionProxy. + type: object + properties: + pass: + type: boolean + set: + type: array + items: + description: Header defines an HTTP Header. + type: object + properties: + name: + type: string + value: + type: string + responseHeaders: + description: ProxyRequestHeaders defines the response + headers manipulation in an ActionProxy. + type: object + properties: + add: + type: array + items: + description: Header defines an HTTP Header + with an optional Always field to use with + the add_header NGINX directive. + type: object + properties: + always: + type: boolean + name: + type: string + value: + type: string + hide: + type: array + items: + type: string + ignore: + type: array + items: + type: string + pass: + type: array + items: + type: string + rewritePath: + type: string + upstream: + type: string redirect: description: ActionRedirect defines a redirect in an Action. diff --git a/docs-web/configuration/virtualserver-and-virtualserverroute-resources.md b/docs-web/configuration/virtualserver-and-virtualserverroute-resources.md index 385c4ffb91..1eaaafa53c 100644 --- a/docs-web/configuration/virtualserver-and-virtualserverroute-resources.md +++ b/docs-web/configuration/virtualserver-and-virtualserverroute-resources.md @@ -25,6 +25,7 @@ This document is the reference documentation for the resources. To see additiona - [Action](#action) - [Action.Redirect](#action-redirect) - [Action.Return](#action-return) + - [Action.Proxy](#action-proxy) - [Split](#split) - [Match](#match) - [Condition](#condition) @@ -726,9 +727,13 @@ In the example below, client requests are passed to an upstream `coffee`: - Returns a preconfigured response. - `action.return <#action-return>`_ - No* + * - ``proxy`` + - Passes requests to an upstream with the ability to modify the request/response (for example, rewrite the URI or modify the headers). + - `action.proxy <#action-proxy>`_ + - No* ``` -\* -- an action must include exactly one of the following: `pass`, `redirect` or `return`. +\* -- an action must include exactly one of the following: `pass`, `redirect`, `return` or `proxy`. ### Action.Redirect @@ -795,6 +800,149 @@ return: \* -- Supported NGINX variables: `$request_uri`, `$request_method`, `$request_body`, `$scheme`, `$http_`, `$args`, `$arg_`, `$cookie_`, `$host`, `$request_time`, `$request_length`, `$nginx_version`, `$pid`, `$connection`, `$remote_addr`, `$remote_port`, `$time_iso8601`, `$time_local`, `$server_addr`, `$server_port`, `$server_name`, `$server_protocol`, `$connections_active`, `$connections_reading`, `$connections_writing` and `$connections_waiting`. +### Action.Proxy + +The proxy action passes requests to an upstream with the ability to modify the request/response (for example, rewrite the URI or modify the headers). + +In the example below, the request URI is rewritten to `/`, and the request and the response headers are modified: +```yaml +proxy: + upstream: coffee + requestHeaders: + pass: true + set: + - name: My-Header + value: Value + responseHeaders: + add: + - name: My-Header + value: Value + always: true + hide: + - x-internal-version + ignore: + - Expires + - Set-Cookie + pass: + - Server + rewritePath: / +``` + +```eval_rst +.. list-table:: + :header-rows: 1 + + * - Field + - Description + - Type + - Required + * - ``upstream`` + - The name of the upstream which the requests will be proxied to. The upstream with that name must be defined in the resource. + - ``string`` + - Yes + * - ``requestHeaders`` + - The request headers modifications. + - `action.Proxy.RequestHeaders <#action-proxy-requestheaders>`_ + - No + * - ``responseHeaders`` + - The response headers modifications. + - `action.Proxy.ResponseHeaders <#action-proxy-responseheaders>`_ + - No + * - ``rewritePath`` + - The rewritten URI. If the route path is a regular expression (starts with ~), the rewritePath can include capture groups with ``$1-9``. For example `$1` for the first group, and so on. For more information, check the `rewrite `_ example. + - ``string`` + - No +``` + +### Action.Proxy.RequestHeaders + +The RequestHeaders field modifies the headers of the request to the proxied upstream server. + +```eval_rst +.. list-table:: + :header-rows: 1 + + * - Field + - Description + - Type + - Required + * - ``pass`` + - Passes the original request headers to the proxied upstream server. See the `proxy_pass_request_header `_ directive for more information. Default is true. + - ``bool`` + - No + * - ``set`` + - Allows redefining or appending fields to present request headers passed to the proxied upstream servers. See the `proxy_set_header `_ directive for more information. + - `[]header <#header>`_ + - No +``` + +### Action.Proxy.ResponseHeaders + +The ResponseHeaders field modifies the headers of the response to the client. + +```eval_rst +.. list-table:: + :header-rows: 1 + + * - Field + - Description + - Type + - Required + * - ``hide`` + - The headers that will not be passed* in the response to the client from a proxied upstream server. See the `proxy_hide_header `_ directive for more information. + - ``bool`` + - No + * - ``pass`` + - Allows passing the hidden header fields* to the client from a proxied upstream server. See the `proxy_pass_header `_ directive for more information. + - ``[]string`` + - No + * - ``ignore`` + - Disables processing of certain headers** to the client from a proxied upstream server. See the `proxy_ignore_headers `_ directive for more information. + - ``[]string`` + - No + * - ``add`` + - Adds headers to the response to the client. + - `[]addHeader <#addheader>`_ + - No +``` + +\* -- Default hidden headers are: `Date`, `Server`, `X-Pad` and `X-Accel-...`. + +\** -- The following fields can be ignored: `X-Accel-Redirect`, `X-Accel-Expires`, `X-Accel-Limit-Rate`, `X-Accel-Buffering`, `X-Accel-Charset`, `Expires`, `Cache-Control`, `Set-Cookie` and `Vary`. + +### AddHeader + +The addHeader defines an HTTP Header with an optional `always` field: +```yaml +name: Host +value: example.com +always: true +``` + +```eval_rst +.. list-table:: + :header-rows: 1 + + * - Field + - Description + - Type + - Required + * - ``name`` + - The name of the header. + - ``string`` + - Yes + * - ``value`` + - The value of the header. + - ``string`` + - No + * - ``always`` + - If set to true, add the header regardless of the response status code*. Default is false. See the `add_header `_ directive for more information. + - ``bool`` + - No +``` + +\* -- If `always` is false, the response header is added only if the response status code is any of `200`, `201`, `204`, `206`, `301`, `302`, `303`, `304`, `307` or `308`. + ### Split The split defines a weight for an action as part of the splits configuration. @@ -1055,7 +1203,7 @@ return: - Yes * - ``headers`` - The custom headers of the response. - - `errorPage.Redirect.Header <#errorpage-return-header>`_ + - `errorPage.Return.Header <#errorpage-return-header>`_ - No ``` diff --git a/examples-of-custom-resources/rewrites/README.md b/examples-of-custom-resources/rewrites/README.md new file mode 100644 index 0000000000..9b97ef4a42 --- /dev/null +++ b/examples-of-custom-resources/rewrites/README.md @@ -0,0 +1,75 @@ +# Rewrites Support + +You can configure NGINX to rewrite the URI of a request before sending it to the application. For example, `/tea/green` can be rewritten to `/green`. + +To configure URI rewriting you need to use the [ActionProxy](https://docs.nginx.com/nginx-ingress-controller/configuration/virtualserver-and-virtualserverroute-resources/#action-proxy) of the [VirtualServer or VirtualServerRoute](https://docs.nginx.com/nginx-ingress-controller/configuration/virtualserver-and-virtualserverroute-resources/). + +## Example with a Prefix Path + +In the following example we load balance two applications that require URI rewriting using prefix-based URI matching: + +```yaml +apiVersion: k8s.nginx.org/v1 +kind: VirtualServer +metadata: + name: cafe +spec: + host: cafe.example.com + upstreams: + - name: tea + service: tea-svc + port: 80 + - name: coffee + service: coffee-svc + port: 80 + routes: + - path: /tea/ + action: + proxy: + upstream: tea + rewritePath: / + - path: /coffee + action: + proxy: + upstream: coffee + rewritePath: /beans +``` + +Below are the examples of how the URI of requests to the *tea-svc* are rewritten (Note that the `/tea` requests are redirected to `/tea/`). +* `/tea/` -> `/` +* `/tea/abc` -> `/abc` + +Below are the examples of how the URI of requests to the *coffee-svc* are rewritten. +* `/coffee` -> `/beans` +* `/coffee/` -> `/beans/` +* `/coffee/abc` -> `/beans/abc` + +## Example with Regular Expressions + +If the route path is a regular expression instead of a prefix or an exact match, the `rewritePath` can include capture groups with `$1-9`, for example: + +```yaml +apiVersion: k8s.nginx.org/v1 +kind: VirtualServer +metadata: + name: cafe +spec: + host: cafe.example.com + upstreams: + - name: tea + service: tea-svc + port: 80 + routes: + - path: ~ /tea/?(.*) + action: + proxy: + upstream: tea + rewritePath: /$1 +``` + +Note the capture group in the path `(.*)` is used in the rewritePath `/$1`. This is needed in order to pass the rest of the request URI (after `/tea`). + +Below are the examples of how the URI of requests to the *tea-svc* are rewritten. +* `/tea` -> `/` +* `/tea/` -> `/` +* `/tea/abc` -> `/abc` \ No newline at end of file diff --git a/internal/configs/version2/http.go b/internal/configs/version2/http.go index a2ca3dac23..c2173ebc68 100644 --- a/internal/configs/version2/http.go +++ b/internal/configs/version2/http.go @@ -76,6 +76,14 @@ type Location struct { ProxyNextUpstreamTimeout string ProxyNextUpstreamTries int ProxyInterceptErrors bool + ProxyPassRequestHeaders bool + ProxySetHeaders []Header + ProxyHideHeaders []string + ProxyPassHeaders []string + ProxyIgnoreHeaders string + ProxyPassRewrite string + AddHeaders []AddHeader + Rewrites []string HasKeepalive bool DefaultType string Return *Return @@ -117,6 +125,12 @@ type Header struct { Value string } +// AddHeader defines a header to use with add_header directive with an optional Always field. +type AddHeader struct { + Header + Always bool +} + // HealthCheck defines a HealthCheck for an upstream in a Server. type HealthCheck struct { Name string diff --git a/internal/configs/version2/nginx-plus.virtualserver.tmpl b/internal/configs/version2/nginx-plus.virtualserver.tmpl index 5ae5b00518..355d213214 100644 --- a/internal/configs/version2/nginx-plus.virtualserver.tmpl +++ b/internal/configs/version2/nginx-plus.virtualserver.tmpl @@ -146,6 +146,11 @@ server { error_page {{ $e.Codes }} {{ if ne 0 $e.ResponseCode }}={{ $e.ResponseCode }}{{ end }} "{{ $e.Name }}"; {{ end }} + set $default_connection_header {{ if $l.HasKeepalive }}""{{ else }}close{{ end }}; + + {{ range $r := $l.Rewrites }} + rewrite {{ $r }}; + {{ end }} proxy_connect_timeout {{ $l.ProxyConnectTimeout }}; proxy_read_timeout {{ $l.ProxyReadTimeout }}; proxy_send_timeout {{ $l.ProxySendTimeout }}; @@ -167,7 +172,6 @@ server { {{ end }} proxy_http_version 1.1; - set $default_connection_header {{ if $l.HasKeepalive }}""{{ else }}close{{ end }}; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection $vs_connection_header; proxy_set_header Host $host; @@ -176,8 +180,22 @@ server { proxy_set_header X-Forwarded-Host $host; proxy_set_header X-Forwarded-Port $server_port; proxy_set_header X-Forwarded-Proto {{ with $s.TLSRedirect }}{{ .BasedOn }}{{ else }}$scheme{{ end }}; - - {{ if $.SpiffeCerts }} + {{ range $h := $l.ProxySetHeaders }} + proxy_set_header {{ $h.Name }} "{{ $h.Value }}"; + {{ end }} + {{ range $h := $l.ProxyHideHeaders }} + proxy_hide_header {{ $h }}; + {{ end }} + {{ range $h := $l.ProxyPassHeaders }} + proxy_pass_header {{ $h }}; + {{ end }} + {{ with $l.ProxyIgnoreHeaders }} + proxy_ignore_headers {{ $l.ProxyIgnoreHeaders }}; + {{ end }} + {{ range $h := $l.AddHeaders }} + add_header {{ $h.Name }} "{{ $h.Value }}" {{ if $h.Always }}always{{ end }}; + {{ end }} + {{ if $.SpiffeCerts }} proxy_ssl_certificate /etc/nginx/secrets/spiffe_cert.pem; proxy_ssl_certificate_key /etc/nginx/secrets/spiffe_key.pem; proxy_ssl_trusted_certificate /etc/nginx/secrets/spiffe_rootca.pem; @@ -185,11 +203,12 @@ server { proxy_ssl_verify on; proxy_ssl_verify_depth 25; proxy_ssl_name {{ $l.ProxySSLName }}; - {{ end }} - proxy_pass {{ $l.ProxyPass }}; + {{ end }} + proxy_pass {{ $l.ProxyPass }}{{ $l.ProxyPassRewrite }}; proxy_next_upstream {{ $l.ProxyNextUpstream }}; proxy_next_upstream_timeout {{ $l.ProxyNextUpstreamTimeout }}; proxy_next_upstream_tries {{ $l.ProxyNextUpstreamTries }}; + proxy_pass_request_headers {{ if $l.ProxyPassRequestHeaders }}on{{ else }}off{{ end }}; {{ end }} } {{ end }} diff --git a/internal/configs/version2/nginx.virtualserver.tmpl b/internal/configs/version2/nginx.virtualserver.tmpl index a7c58fd812..4a5f1282d2 100644 --- a/internal/configs/version2/nginx.virtualserver.tmpl +++ b/internal/configs/version2/nginx.virtualserver.tmpl @@ -115,6 +115,11 @@ server { error_page {{ $e.Codes }} {{ if ne 0 $e.ResponseCode }}={{ $e.ResponseCode }}{{ end }} "{{ $e.Name }}"; {{ end }} + set $default_connection_header {{ if $l.HasKeepalive }}""{{ else }}close{{ end }}; + + {{ range $r := $l.Rewrites }} + rewrite {{ $r }}; + {{ end }} proxy_connect_timeout {{ $l.ProxyConnectTimeout }}; proxy_read_timeout {{ $l.ProxyReadTimeout }}; proxy_send_timeout {{ $l.ProxySendTimeout }}; @@ -136,7 +141,6 @@ server { {{ end }} proxy_http_version 1.1; - set $default_connection_header {{ if $l.HasKeepalive }}""{{ else }}close{{ end }}; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection $vs_connection_header; proxy_set_header Host $host; @@ -145,7 +149,22 @@ server { proxy_set_header X-Forwarded-Host $host; proxy_set_header X-Forwarded-Port $server_port; proxy_set_header X-Forwarded-Proto {{ with $s.TLSRedirect }}{{ .BasedOn }}{{ else }}$scheme{{ end }}; - {{ if $.SpiffeCerts }} + {{ range $h := $l.ProxySetHeaders }} + proxy_set_header {{ $h.Name }} "{{ $h.Value }}"; + {{ end }} + {{ range $h := $l.ProxyHideHeaders }} + proxy_hide_header {{ $h }}; + {{ end }} + {{ range $h := $l.ProxyPassHeaders }} + proxy_pass_header {{ $h }}; + {{ end }} + {{ with $l.ProxyIgnoreHeaders }} + proxy_ignore_headers {{ $l.ProxyIgnoreHeaders }}; + {{ end }} + {{ range $h := $l.AddHeaders }} + add_header {{ $h.Name }} "{{ $h.Value }}" {{ if $h.Always }}always{{ end }}; + {{ end }} + {{ if $.SpiffeCerts }} proxy_ssl_certificate /etc/nginx/secrets/spiffe_cert.pem; proxy_ssl_certificate_key /etc/nginx/secrets/spiffe_key.pem; proxy_ssl_trusted_certificate /etc/nginx/secrets/spiffe_rootca.pem; @@ -153,11 +172,12 @@ server { proxy_ssl_verify on; proxy_ssl_verify_depth 25; proxy_ssl_name {{ $l.ProxySSLName }}; - {{ end }} - proxy_pass {{ $l.ProxyPass }}; + {{ end }} + proxy_pass {{ $l.ProxyPass }}{{ $l.ProxyPassRewrite }}; proxy_next_upstream {{ $l.ProxyNextUpstream }}; proxy_next_upstream_timeout {{ $l.ProxyNextUpstreamTimeout }}; proxy_next_upstream_tries {{ $l.ProxyNextUpstreamTries }}; + proxy_pass_request_headers {{ if $l.ProxyPassRequestHeaders }}on{{ else }}off{{ end }}; {{ end }} } {{ end }} diff --git a/internal/configs/version2/templates_test.go b/internal/configs/version2/templates_test.go index e5cc53b6e2..1f7dcd04ab 100644 --- a/internal/configs/version2/templates_test.go +++ b/internal/configs/version2/templates_test.go @@ -144,6 +144,21 @@ var virtualServerCfg = VirtualServerConfig{ ProxyNextUpstream: "error timeout", ProxyNextUpstreamTimeout: "5s", Internal: true, + ProxyPassRequestHeaders: false, + ProxyPassHeaders: []string{"Host"}, + ProxyPassRewrite: "$request_uri", + ProxyHideHeaders: []string{"Header"}, + ProxyIgnoreHeaders: "Cache", + Rewrites: []string{"$request_uri $request_uri", "$request_uri $request_uri"}, + AddHeaders: []AddHeader{ + { + Header: Header{ + Name: "Header-Name", + Value: "Header Value", + }, + Always: true, + }, + }, }, { Path: "@loc0", diff --git a/internal/configs/virtualserver.go b/internal/configs/virtualserver.go index 6ed2aac838..e7e412698d 100644 --- a/internal/configs/virtualserver.go +++ b/internal/configs/virtualserver.go @@ -82,6 +82,17 @@ func newUpstreamNamerForTransportServer(transportServer *conf_v1alpha1.Transport } } +func (namer *upstreamNamer) GetNameForUpstreamFromAction(action *conf_v1.Action) string { + var upstream string + if action.Proxy != nil && action.Proxy.Upstream != "" { + upstream = action.Proxy.Upstream + } else { + upstream = action.Pass + } + + return fmt.Sprintf("%s_%s", namer.prefix, upstream) +} + func (namer *upstreamNamer) GetNameForUpstream(upstream string) string { return fmt.Sprintf("%s_%s", namer.prefix, upstream) } @@ -273,16 +284,16 @@ func (vsc *virtualServerConfigurator) GenerateVirtualServerConfig(virtualServerE matchesRoutes++ } else if len(r.Splits) > 0 { - cfg := generateDefaultSplitsConfig(r, virtualServerUpstreamNamer, crUpstreams, variableNamer, len(splitClients), vsc.cfgParams, r.ErrorPages, errorPageIndex) + cfg := generateDefaultSplitsConfig(r, virtualServerUpstreamNamer, crUpstreams, variableNamer, len(splitClients), vsc.cfgParams, r.ErrorPages, errorPageIndex, r.Path) splitClients = append(splitClients, cfg.SplitClients...) locations = append(locations, cfg.Locations...) internalRedirectLocations = append(internalRedirectLocations, cfg.InternalRedirectLocation) } else { - upstreamName := virtualServerUpstreamNamer.GetNameForUpstream(r.Action.Pass) + upstreamName := virtualServerUpstreamNamer.GetNameForUpstreamFromAction(r.Action) upstream := crUpstreams[upstreamName] proxySSLName := generateProxySSLName(upstream.Service, virtualServerEx.VirtualServer.Namespace) - loc := generateLocation(r.Path, upstreamName, upstream, r.Action, vsc.cfgParams, r.ErrorPages, false, errorPageIndex, proxySSLName) + loc := generateLocation(r.Path, upstreamName, upstream, r.Action, vsc.cfgParams, r.ErrorPages, false, errorPageIndex, proxySSLName, r.Path) locations = append(locations, loc) } } @@ -313,16 +324,16 @@ func (vsc *virtualServerConfigurator) GenerateVirtualServerConfig(virtualServerE matchesRoutes++ } else if len(r.Splits) > 0 { - cfg := generateDefaultSplitsConfig(r, upstreamNamer, crUpstreams, variableNamer, len(splitClients), vsc.cfgParams, errorPages, errorPageIndex) + cfg := generateDefaultSplitsConfig(r, upstreamNamer, crUpstreams, variableNamer, len(splitClients), vsc.cfgParams, errorPages, errorPageIndex, r.Path) splitClients = append(splitClients, cfg.SplitClients...) locations = append(locations, cfg.Locations...) internalRedirectLocations = append(internalRedirectLocations, cfg.InternalRedirectLocation) } else { - upstreamName := upstreamNamer.GetNameForUpstream(r.Action.Pass) + upstreamName := upstreamNamer.GetNameForUpstreamFromAction(r.Action) upstream := crUpstreams[upstreamName] proxySSLName := generateProxySSLName(upstream.Service, vsr.Namespace) - loc := generateLocation(r.Path, upstreamName, upstream, r.Action, vsc.cfgParams, errorPages, false, errorPageIndex, proxySSLName) + loc := generateLocation(r.Path, upstreamName, upstream, r.Action, vsc.cfgParams, errorPages, false, errorPageIndex, proxySSLName, r.Path) locations = append(locations, loc) } } @@ -518,10 +529,55 @@ func upstreamHasKeepalive(upstream conf_v1.Upstream, cfgParams *ConfigParams) bo return cfgParams.Keepalive != 0 } -func generateProxyPass(tlsEnabled bool, upstreamName string, internal bool) string { - proxyPass := fmt.Sprintf("%v://%v", generateProxyPassProtocol(tlsEnabled), upstreamName) +func generateRewrites(path string, proxy *conf_v1.ActionProxy, internal bool, originalPath string) []string { + if proxy == nil || proxy.RewritePath == "" { + return nil + } + + if originalPath != "" { + path = originalPath + } + + isRegex := false + if strings.HasPrefix(path, "~") { + isRegex = true + } + + trimmedPath := strings.TrimPrefix(strings.TrimPrefix(path, "~"), "*") + trimmedPath = strings.TrimSpace(trimmedPath) + + var rewrites []string if internal { + // For internal locations (splits locations) only, recover the original request_uri. + rewrites = append(rewrites, "^ $request_uri") + } + + if isRegex { + rewrites = append(rewrites, fmt.Sprintf(`"^%v" "%v" break`, trimmedPath, proxy.RewritePath)) + } else if internal { + rewrites = append(rewrites, fmt.Sprintf(`"^%v(.*)$" "%v$1" break`, trimmedPath, proxy.RewritePath)) + } + + return rewrites +} + +func generateProxyPassRewrite(path string, proxy *conf_v1.ActionProxy, internal bool) string { + if proxy == nil || internal { + return "" + } + + if strings.HasPrefix(path, "/") || strings.HasPrefix(path, "=") { + return proxy.RewritePath + } + + return "" +} + +func generateProxyPass(tlsEnabled bool, upstreamName string, internal bool, proxy *conf_v1.ActionProxy) string { + proxyPass := fmt.Sprintf("%v://%v", generateProxyPassProtocol(tlsEnabled), upstreamName) + + if internal && (proxy == nil || proxy.RewritePath == "") { return fmt.Sprintf("%v$request_uri", proxyPass) } @@ -582,7 +638,7 @@ func generateReturnBlock(text string, code int, defaultCode int) *version2.Retur } func generateLocation(path string, upstreamName string, upstream conf_v1.Upstream, action *conf_v1.Action, - cfgParams *ConfigParams, errorPages []conf_v1.ErrorPage, internal bool, errPageIndex int, proxySSLName string) version2.Location { + cfgParams *ConfigParams, errorPages []conf_v1.ErrorPage, internal bool, errPageIndex int, proxySSLName string, originalPath string) version2.Location { if action.Redirect != nil { returnBlock := generateReturnBlock(action.Redirect.URL, action.Redirect.Code, 301) return generateLocationForReturnBlock(path, cfgParams.LocationSnippets, returnBlock, "") @@ -597,11 +653,82 @@ func generateLocation(path string, upstreamName string, upstream conf_v1.Upstrea return generateLocationForReturnBlock(path, cfgParams.LocationSnippets, returnBlock, defaultType) } - return generateLocationForProxying(path, upstreamName, upstream, cfgParams, errorPages, internal, errPageIndex, proxySSLName) + return generateLocationForProxying(path, upstreamName, upstream, cfgParams, errorPages, internal, errPageIndex, proxySSLName, action.Proxy, originalPath) +} + +func generateProxySetHeaders(proxy *conf_v1.ActionProxy) []version2.Header { + if proxy == nil || proxy.RequestHeaders == nil { + return nil + } + + var headers []version2.Header + for _, h := range proxy.RequestHeaders.Set { + headers = append(headers, version2.Header{ + Name: h.Name, + Value: h.Value, + }) + } + + return headers +} + +func generateProxyPassRequestHeaders(proxy *conf_v1.ActionProxy) bool { + if proxy == nil || proxy.RequestHeaders == nil { + return true + } + + if proxy.RequestHeaders.Pass != nil { + return *proxy.RequestHeaders.Pass + } + + return true +} + +func generateProxyHideHeaders(proxy *conf_v1.ActionProxy) []string { + if proxy == nil || proxy.ResponseHeaders == nil { + return nil + } + + return proxy.ResponseHeaders.Hide +} + +func generateProxyPassHeaders(proxy *conf_v1.ActionProxy) []string { + if proxy == nil || proxy.ResponseHeaders == nil { + return nil + } + + return proxy.ResponseHeaders.Pass +} + +func generateProxyIgnoreHeaders(proxy *conf_v1.ActionProxy) string { + if proxy == nil || proxy.ResponseHeaders == nil { + return "" + } + + return strings.Join(proxy.ResponseHeaders.Ignore, " ") +} + +func generateProxyAddHeaders(proxy *conf_v1.ActionProxy) []version2.AddHeader { + if proxy == nil || proxy.ResponseHeaders == nil { + return nil + } + + var addHeaders []version2.AddHeader + for _, h := range proxy.ResponseHeaders.Add { + addHeaders = append(addHeaders, version2.AddHeader{ + Header: version2.Header{ + Name: h.Name, + Value: h.Value, + }, + Always: h.Always, + }) + } + + return addHeaders } func generateLocationForProxying(path string, upstreamName string, upstream conf_v1.Upstream, - cfgParams *ConfigParams, errorPages []conf_v1.ErrorPage, internal bool, errPageIndex int, proxySSLName string) version2.Location { + cfgParams *ConfigParams, errorPages []conf_v1.ErrorPage, internal bool, errPageIndex int, proxySSLName string, proxy *conf_v1.ActionProxy, originalPath string) version2.Location { return version2.Location{ Path: generatePath(path), Internal: internal, @@ -614,11 +741,19 @@ func generateLocationForProxying(path string, upstreamName string, upstream conf ProxyBuffering: generateBool(upstream.ProxyBuffering, cfgParams.ProxyBuffering), ProxyBuffers: generateBuffers(upstream.ProxyBuffers, cfgParams.ProxyBuffers), ProxyBufferSize: generateString(upstream.ProxyBufferSize, cfgParams.ProxyBufferSize), - ProxyPass: generateProxyPass(upstream.TLS.Enable, upstreamName, internal), + ProxyPass: generateProxyPass(upstream.TLS.Enable, upstreamName, internal, proxy), ProxyNextUpstream: generateString(upstream.ProxyNextUpstream, "error timeout"), ProxyNextUpstreamTimeout: generateString(upstream.ProxyNextUpstreamTimeout, "0s"), ProxyNextUpstreamTries: upstream.ProxyNextUpstreamTries, ProxyInterceptErrors: generateProxyInterceptErrors(errorPages), + ProxyPassRequestHeaders: generateProxyPassRequestHeaders(proxy), + ProxySetHeaders: generateProxySetHeaders(proxy), + ProxyHideHeaders: generateProxyHideHeaders(proxy), + ProxyPassHeaders: generateProxyPassHeaders(proxy), + ProxyIgnoreHeaders: generateProxyIgnoreHeaders(proxy), + AddHeaders: generateProxyAddHeaders(proxy), + ProxyPassRewrite: generateProxyPassRewrite(path, proxy, internal), + Rewrites: generateRewrites(path, proxy, internal, originalPath), HasKeepalive: upstreamHasKeepalive(upstream, cfgParams), ErrorPages: generateErrorPages(errPageIndex, errorPages), ProxySSLName: proxySSLName, @@ -646,7 +781,7 @@ type routingCfg struct { } func generateSplits(splits []conf_v1.Split, upstreamNamer *upstreamNamer, crUpstreams map[string]conf_v1.Upstream, - variableNamer *variableNamer, scIndex int, cfgParams *ConfigParams, errorPages []conf_v1.ErrorPage, errPageIndex int) (version2.SplitClient, []version2.Location) { + variableNamer *variableNamer, scIndex int, cfgParams *ConfigParams, errorPages []conf_v1.ErrorPage, errPageIndex int, originalPath string) (version2.SplitClient, []version2.Location) { var distributions []version2.Distribution for i, s := range splits { @@ -667,10 +802,10 @@ func generateSplits(splits []conf_v1.Split, upstreamNamer *upstreamNamer, crUpst for i, s := range splits { path := fmt.Sprintf("/%vsplits_%d_split_%d", internalLocationPrefix, scIndex, i) - upstreamName := upstreamNamer.GetNameForUpstream(s.Action.Pass) + upstreamName := upstreamNamer.GetNameForUpstreamFromAction(s.Action) upstream := crUpstreams[upstreamName] proxySSLName := generateProxySSLName(upstream.Service, upstreamNamer.namespace) - loc := generateLocation(path, upstreamName, upstream, s.Action, cfgParams, errorPages, true, errPageIndex, proxySSLName) + loc := generateLocation(path, upstreamName, upstream, s.Action, cfgParams, errorPages, true, errPageIndex, proxySSLName, originalPath) locations = append(locations, loc) } @@ -678,8 +813,8 @@ func generateSplits(splits []conf_v1.Split, upstreamNamer *upstreamNamer, crUpst } func generateDefaultSplitsConfig(route conf_v1.Route, upstreamNamer *upstreamNamer, crUpstreams map[string]conf_v1.Upstream, - variableNamer *variableNamer, scIndex int, cfgParams *ConfigParams, errorPages []conf_v1.ErrorPage, errPageIndex int) routingCfg { - sc, locs := generateSplits(route.Splits, upstreamNamer, crUpstreams, variableNamer, scIndex, cfgParams, errorPages, errPageIndex) + variableNamer *variableNamer, scIndex int, cfgParams *ConfigParams, errorPages []conf_v1.ErrorPage, errPageIndex int, originalPath string) routingCfg { + sc, locs := generateSplits(route.Splits, upstreamNamer, crUpstreams, variableNamer, scIndex, cfgParams, errorPages, errPageIndex, originalPath) splitClientVarName := variableNamer.GetNameForSplitClientVariable(scIndex) @@ -769,32 +904,32 @@ func generateMatchesConfig(route conf_v1.Route, upstreamNamer *upstreamNamer, cr for i, m := range route.Matches { if len(m.Splits) > 0 { - sc, locs := generateSplits(m.Splits, upstreamNamer, crUpstreams, variableNamer, scIndex+scLocalIndex, cfgParams, errorPages, errPageIndex) + sc, locs := generateSplits(m.Splits, upstreamNamer, crUpstreams, variableNamer, scIndex+scLocalIndex, cfgParams, errorPages, errPageIndex, route.Path) scLocalIndex++ splitClients = append(splitClients, sc) locations = append(locations, locs...) } else { path := fmt.Sprintf("/%vmatches_%d_match_%d", internalLocationPrefix, index, i) - upstreamName := upstreamNamer.GetNameForUpstream(m.Action.Pass) + upstreamName := upstreamNamer.GetNameForUpstreamFromAction(m.Action) upstream := crUpstreams[upstreamName] proxySSLName := generateProxySSLName(upstream.Service, upstreamNamer.namespace) - loc := generateLocation(path, upstreamName, upstream, m.Action, cfgParams, errorPages, true, errPageIndex, proxySSLName) + loc := generateLocation(path, upstreamName, upstream, m.Action, cfgParams, errorPages, true, errPageIndex, proxySSLName, route.Path) locations = append(locations, loc) } } // Generate default splits or default action if len(route.Splits) > 0 { - sc, locs := generateSplits(route.Splits, upstreamNamer, crUpstreams, variableNamer, scIndex+scLocalIndex, cfgParams, errorPages, errPageIndex) + sc, locs := generateSplits(route.Splits, upstreamNamer, crUpstreams, variableNamer, scIndex+scLocalIndex, cfgParams, errorPages, errPageIndex, route.Path) splitClients = append(splitClients, sc) locations = append(locations, locs...) } else { path := fmt.Sprintf("/%vmatches_%d_default", internalLocationPrefix, index) - upstreamName := upstreamNamer.GetNameForUpstream(route.Action.Pass) + upstreamName := upstreamNamer.GetNameForUpstreamFromAction(route.Action) upstream := crUpstreams[upstreamName] proxySSLName := generateProxySSLName(upstream.Service, upstreamNamer.namespace) - loc := generateLocation(path, upstreamName, upstream, route.Action, cfgParams, errorPages, true, errPageIndex, proxySSLName) + loc := generateLocation(path, upstreamName, upstream, route.Action, cfgParams, errorPages, true, errPageIndex, proxySSLName, route.Path) locations = append(locations, loc) } diff --git a/internal/configs/virtualserver_test.go b/internal/configs/virtualserver_test.go index 87ea4c047d..1c9fe74c27 100644 --- a/internal/configs/virtualserver_test.go +++ b/internal/configs/virtualserver_test.go @@ -452,6 +452,7 @@ func TestGenerateVirtualServerConfig(t *testing.T) { ProxyNextUpstreamTries: 0, HasKeepalive: true, ProxySSLName: "tea-svc.default.svc", + ProxyPassRequestHeaders: true, }, { Path: "/tea-latest", @@ -461,6 +462,7 @@ func TestGenerateVirtualServerConfig(t *testing.T) { ProxyNextUpstreamTries: 0, HasKeepalive: true, ProxySSLName: "tea-svc.default.svc", + ProxyPassRequestHeaders: true, }, // Order changes here because we generate first all the VS Routes and then all the VSR Subroutes (separated for loops) { @@ -478,7 +480,8 @@ func TestGenerateVirtualServerConfig(t *testing.T) { ResponseCode: 301, }, }, - ProxySSLName: "coffee-svc.default.svc", + ProxySSLName: "coffee-svc.default.svc", + ProxyPassRequestHeaders: true, }, { Path: "/coffee", @@ -488,6 +491,7 @@ func TestGenerateVirtualServerConfig(t *testing.T) { ProxyNextUpstreamTries: 0, HasKeepalive: true, ProxySSLName: "coffee-svc.default.svc", + ProxyPassRequestHeaders: true, }, { Path: "/subtea", @@ -497,6 +501,7 @@ func TestGenerateVirtualServerConfig(t *testing.T) { ProxyNextUpstreamTries: 0, HasKeepalive: true, ProxySSLName: "sub-tea-svc.default.svc", + ProxyPassRequestHeaders: true, }, { @@ -514,7 +519,8 @@ func TestGenerateVirtualServerConfig(t *testing.T) { ResponseCode: 301, }, }, - ProxySSLName: "coffee-svc.default.svc", + ProxySSLName: "coffee-svc.default.svc", + ProxyPassRequestHeaders: true, }, { Path: "/coffee-errorpage-subroute-defined", @@ -531,7 +537,8 @@ func TestGenerateVirtualServerConfig(t *testing.T) { ResponseCode: 200, }, }, - ProxySSLName: "coffee-svc.default.svc", + ProxySSLName: "coffee-svc.default.svc", + ProxyPassRequestHeaders: true, }, }, ErrorPageLocations: []version2.ErrorPageLocation{ @@ -634,6 +641,7 @@ func TestGenerateVirtualServerConfigWithSpiffeCerts(t *testing.T) { ProxyNextUpstreamTries: 0, HasKeepalive: true, ProxySSLName: "tea-svc.default.svc", + ProxyPassRequestHeaders: true, }, }, }, @@ -847,6 +855,7 @@ func TestGenerateVirtualServerConfigForVirtualServerWithSplits(t *testing.T) { ProxyNextUpstreamTries: 0, Internal: true, ProxySSLName: "tea-svc-v1.default.svc", + ProxyPassRequestHeaders: true, }, { Path: "/internal_location_splits_0_split_1", @@ -856,6 +865,7 @@ func TestGenerateVirtualServerConfigForVirtualServerWithSplits(t *testing.T) { ProxyNextUpstreamTries: 0, Internal: true, ProxySSLName: "tea-svc-v2.default.svc", + ProxyPassRequestHeaders: true, }, { Path: "/internal_location_splits_1_split_0", @@ -865,6 +875,7 @@ func TestGenerateVirtualServerConfigForVirtualServerWithSplits(t *testing.T) { ProxyNextUpstreamTries: 0, Internal: true, ProxySSLName: "coffee-svc-v1.default.svc", + ProxyPassRequestHeaders: true, }, { Path: "/internal_location_splits_1_split_1", @@ -874,6 +885,7 @@ func TestGenerateVirtualServerConfigForVirtualServerWithSplits(t *testing.T) { ProxyNextUpstreamTries: 0, Internal: true, ProxySSLName: "coffee-svc-v2.default.svc", + ProxyPassRequestHeaders: true, }, }, }, @@ -1118,6 +1130,7 @@ func TestGenerateVirtualServerConfigForVirtualServerWithMatches(t *testing.T) { ProxyNextUpstreamTries: 0, Internal: true, ProxySSLName: "tea-svc-v2.default.svc", + ProxyPassRequestHeaders: true, }, { Path: "/internal_location_matches_0_default", @@ -1127,6 +1140,7 @@ func TestGenerateVirtualServerConfigForVirtualServerWithMatches(t *testing.T) { ProxyNextUpstreamTries: 0, Internal: true, ProxySSLName: "tea-svc-v1.default.svc", + ProxyPassRequestHeaders: true, }, { Path: "/internal_location_matches_1_match_0", @@ -1136,6 +1150,7 @@ func TestGenerateVirtualServerConfigForVirtualServerWithMatches(t *testing.T) { ProxyNextUpstreamTries: 0, Internal: true, ProxySSLName: "coffee-svc-v2.default.svc", + ProxyPassRequestHeaders: true, }, { Path: "/internal_location_matches_1_default", @@ -1145,6 +1160,7 @@ func TestGenerateVirtualServerConfigForVirtualServerWithMatches(t *testing.T) { ProxyNextUpstreamTries: 0, Internal: true, ProxySSLName: "coffee-svc-v1.default.svc", + ProxyPassRequestHeaders: true, }, }, }, @@ -1336,7 +1352,7 @@ func TestGenerateProxyPass(t *testing.T) { } for _, test := range tests { - result := generateProxyPass(test.tlsEnabled, test.upstreamName, test.internal) + result := generateProxyPass(test.tlsEnabled, test.upstreamName, test.internal, nil) if result != test.expected { t.Errorf("generateProxyPass(%v, %v, %v) returned %v but expected %v", test.tlsEnabled, test.upstreamName, test.internal, result, test.expected) } @@ -1446,11 +1462,12 @@ func TestGenerateLocationForProxying(t *testing.T) { ProxyNextUpstream: "error timeout", ProxyNextUpstreamTimeout: "0s", ProxyNextUpstreamTries: 0, + ProxyPassRequestHeaders: true, } - result := generateLocationForProxying(path, upstreamName, conf_v1.Upstream{}, &cfgParams, nil, false, 0, "") + result := generateLocationForProxying(path, upstreamName, conf_v1.Upstream{}, &cfgParams, nil, false, 0, "", nil, "") if !reflect.DeepEqual(result, expected) { - t.Errorf("generateLocationForProxying() returned %v but expected %v", result, expected) + t.Errorf("generateLocationForProxying() returned \n%v but expected \n%v", result, expected) } } @@ -1864,11 +1881,15 @@ func TestCreateUpstreamServersConfigForPlusNoUpstreams(t *testing.T) { } func TestGenerateSplits(t *testing.T) { + originalPath := "/path" splits := []conf_v1.Split{ { Weight: 90, Action: &conf_v1.Action{ - Pass: "coffee-v1", + Proxy: &conf_v1.ActionProxy{ + Upstream: "coffee-v1", + RewritePath: "/rewrite", + }, }, }, { @@ -1944,7 +1965,8 @@ func TestGenerateSplits(t *testing.T) { expectedLocations := []version2.Location{ { Path: "/internal_location_splits_1_split_0", - ProxyPass: "http://vs_default_cafe_coffee-v1$request_uri", + ProxyPass: "http://vs_default_cafe_coffee-v1", + Rewrites: []string{"^ $request_uri", fmt.Sprintf(`"^%v(.*)$" "/rewrite$1" break`, originalPath)}, ProxyNextUpstream: "error timeout", ProxyNextUpstreamTimeout: "0s", ProxyNextUpstreamTries: 0, @@ -1962,7 +1984,8 @@ func TestGenerateSplits(t *testing.T) { ResponseCode: 301, }, }, - ProxySSLName: "coffee-v1.default.svc", + ProxySSLName: "coffee-v1.default.svc", + ProxyPassRequestHeaders: true, }, { Path: "/internal_location_splits_1_split_1", @@ -1984,11 +2007,12 @@ func TestGenerateSplits(t *testing.T) { ResponseCode: 301, }, }, - ProxySSLName: "coffee-v2.default.svc", + ProxySSLName: "coffee-v2.default.svc", + ProxyPassRequestHeaders: true, }, } - resultSplitClient, resultLocations := generateSplits(splits, upstreamNamer, crUpstreams, variableNamer, scIndex, &cfgParams, errorPages, 0) + resultSplitClient, resultLocations := generateSplits(splits, upstreamNamer, crUpstreams, variableNamer, scIndex, &cfgParams, errorPages, 0, originalPath) if !reflect.DeepEqual(resultSplitClient, expectedSplitClient) { t.Errorf("generateSplits() returned \n%+v but expected \n%+v", resultSplitClient, expectedSplitClient) } @@ -2052,6 +2076,7 @@ func TestGenerateDefaultSplitsConfig(t *testing.T) { ProxyNextUpstreamTries: 0, Internal: true, ProxySSLName: "coffee-v1.default.svc", + ProxyPassRequestHeaders: true, }, { Path: "/internal_location_splits_1_split_1", @@ -2061,6 +2086,7 @@ func TestGenerateDefaultSplitsConfig(t *testing.T) { ProxyNextUpstreamTries: 0, Internal: true, ProxySSLName: "coffee-v2.default.svc", + ProxyPassRequestHeaders: true, }, }, InternalRedirectLocation: version2.InternalRedirectLocation{ @@ -2079,7 +2105,7 @@ func TestGenerateDefaultSplitsConfig(t *testing.T) { }, } - result := generateDefaultSplitsConfig(route, upstreamNamer, crUpstreams, variableNamer, index, &cfgParams, route.ErrorPages, 0) + result := generateDefaultSplitsConfig(route, upstreamNamer, crUpstreams, variableNamer, index, &cfgParams, route.ErrorPages, 0, "") if !reflect.DeepEqual(result, expected) { t.Errorf("generateDefaultSplitsConfig() returned \n%+v but expected \n%+v", result, expected) } @@ -2345,7 +2371,8 @@ func TestGenerateMatchesConfig(t *testing.T) { ResponseCode: 301, }, }, - ProxySSLName: "coffee-v1.default.svc", + ProxySSLName: "coffee-v1.default.svc", + ProxyPassRequestHeaders: true, }, { Path: "/internal_location_splits_2_split_0", @@ -2367,7 +2394,8 @@ func TestGenerateMatchesConfig(t *testing.T) { ResponseCode: 301, }, }, - ProxySSLName: "coffee-v1.default.svc", + ProxySSLName: "coffee-v1.default.svc", + ProxyPassRequestHeaders: true, }, { Path: "/internal_location_splits_2_split_1", @@ -2389,7 +2417,8 @@ func TestGenerateMatchesConfig(t *testing.T) { ResponseCode: 301, }, }, - ProxySSLName: "coffee-v2.default.svc", + ProxySSLName: "coffee-v2.default.svc", + ProxyPassRequestHeaders: true, }, { Path: "/internal_location_matches_1_default", @@ -2411,7 +2440,8 @@ func TestGenerateMatchesConfig(t *testing.T) { ResponseCode: 301, }, }, - ProxySSLName: "tea.default.svc", + ProxySSLName: "tea.default.svc", + ProxyPassRequestHeaders: true, }, }, InternalRedirectLocation: version2.InternalRedirectLocation{ @@ -2622,8 +2652,9 @@ func TestGenerateMatchesConfigWithMultipleSplits(t *testing.T) { ResponseCode: 301, }, }, - ProxyInterceptErrors: true, - ProxySSLName: "coffee-v1.default.svc", + ProxyInterceptErrors: true, + ProxySSLName: "coffee-v1.default.svc", + ProxyPassRequestHeaders: true, }, { Path: "/internal_location_splits_2_split_1", @@ -2644,8 +2675,9 @@ func TestGenerateMatchesConfigWithMultipleSplits(t *testing.T) { ResponseCode: 301, }, }, - ProxyInterceptErrors: true, - ProxySSLName: "coffee-v2.default.svc", + ProxyInterceptErrors: true, + ProxySSLName: "coffee-v2.default.svc", + ProxyPassRequestHeaders: true, }, { Path: "/internal_location_splits_3_split_0", @@ -2666,8 +2698,9 @@ func TestGenerateMatchesConfigWithMultipleSplits(t *testing.T) { ResponseCode: 301, }, }, - ProxyInterceptErrors: true, - ProxySSLName: "coffee-v2.default.svc", + ProxyInterceptErrors: true, + ProxySSLName: "coffee-v2.default.svc", + ProxyPassRequestHeaders: true, }, { Path: "/internal_location_splits_3_split_1", @@ -2688,8 +2721,9 @@ func TestGenerateMatchesConfigWithMultipleSplits(t *testing.T) { ResponseCode: 301, }, }, - ProxyInterceptErrors: true, - ProxySSLName: "coffee-v1.default.svc", + ProxyInterceptErrors: true, + ProxySSLName: "coffee-v1.default.svc", + ProxyPassRequestHeaders: true, }, { Path: "/internal_location_splits_4_split_0", @@ -2710,8 +2744,9 @@ func TestGenerateMatchesConfigWithMultipleSplits(t *testing.T) { ResponseCode: 301, }, }, - ProxyInterceptErrors: true, - ProxySSLName: "coffee-v1.default.svc", + ProxyInterceptErrors: true, + ProxySSLName: "coffee-v1.default.svc", + ProxyPassRequestHeaders: true, }, { Path: "/internal_location_splits_4_split_1", @@ -2732,8 +2767,9 @@ func TestGenerateMatchesConfigWithMultipleSplits(t *testing.T) { ResponseCode: 301, }, }, - ProxyInterceptErrors: true, - ProxySSLName: "coffee-v2.default.svc", + ProxyInterceptErrors: true, + ProxySSLName: "coffee-v2.default.svc", + ProxyPassRequestHeaders: true, }, }, InternalRedirectLocation: version2.InternalRedirectLocation{ @@ -3834,3 +3870,370 @@ func TestIsTLSEnabled(t *testing.T) { } } + +func TestGenerateRewrites(t *testing.T) { + tests := []struct { + path string + proxy *conf_v1.ActionProxy + internal bool + originalPath string + expected []string + }{ + { + proxy: nil, + expected: nil, + }, + { + proxy: &conf_v1.ActionProxy{ + RewritePath: "", + }, + expected: nil, + }, + { + path: "/path", + proxy: &conf_v1.ActionProxy{ + RewritePath: "/rewrite", + }, + expected: nil, + }, + { + path: "/_internal_path", + internal: true, + proxy: &conf_v1.ActionProxy{ + RewritePath: "/rewrite", + }, + originalPath: "/path", + expected: []string{`^ $request_uri`, `"^/path(.*)$" "/rewrite$1" break`}, + }, + { + path: "~/regex", + internal: true, + proxy: &conf_v1.ActionProxy{ + RewritePath: "/rewrite", + }, + originalPath: "/path", + expected: []string{`^ $request_uri`, `"^/path(.*)$" "/rewrite$1" break`}, + }, + { + path: "~/regex", + internal: false, + proxy: &conf_v1.ActionProxy{ + RewritePath: "/rewrite", + }, + expected: []string{`"^/regex" "/rewrite" break`}, + }, + } + + for _, test := range tests { + result := generateRewrites(test.path, test.proxy, test.internal, test.originalPath) + if !reflect.DeepEqual(result, test.expected) { + t.Errorf("generateRewrites(%v, %v, %v, %v) returned \n %v but expected \n %v", + test.path, test.proxy, test.internal, test.originalPath, result, test.expected) + } + } +} + +func TestGenerateProxyPassRewrite(t *testing.T) { + tests := []struct { + path string + proxy *conf_v1.ActionProxy + internal bool + expected string + }{ + { + expected: "", + }, + { + internal: true, + proxy: &conf_v1.ActionProxy{ + RewritePath: "/rewrite", + }, + expected: "", + }, + { + path: "/path", + proxy: &conf_v1.ActionProxy{ + RewritePath: "/rewrite", + }, + expected: "/rewrite", + }, + { + path: "=/path", + proxy: &conf_v1.ActionProxy{ + RewritePath: "/rewrite", + }, + expected: "/rewrite", + }, + { + path: "~/path", + proxy: &conf_v1.ActionProxy{ + RewritePath: "/rewrite", + }, + 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) + } + } +} + +func TestGenerateProxySetHeaders(t *testing.T) { + tests := []struct { + proxy *conf_v1.ActionProxy + expected []version2.Header + }{ + { + proxy: nil, + expected: nil, + }, + { + proxy: &conf_v1.ActionProxy{}, + expected: nil, + }, + { + proxy: &conf_v1.ActionProxy{ + RequestHeaders: &conf_v1.ProxyRequestHeaders{ + Set: []conf_v1.Header{ + { + Name: "Header-Name", + Value: "HeaderValue", + }, + { + Name: "Host", + Value: "nginx.org", + }, + }, + }, + }, + expected: []version2.Header{ + { + Name: "Header-Name", + Value: "HeaderValue", + }, + { + Name: "Host", + Value: "nginx.org", + }, + }, + }, + } + + for _, test := range tests { + result := generateProxySetHeaders(test.proxy) + if !reflect.DeepEqual(result, test.expected) { + t.Errorf("generateProxySetHeaders(%v) returned %v but expected %v", test.proxy, result, test.expected) + } + } +} + +func TestGenerateProxyPassRequestHeaders(t *testing.T) { + passTrue := true + passFalse := false + tests := []struct { + proxy *conf_v1.ActionProxy + expected bool + }{ + { + proxy: nil, + expected: true, + }, + { + proxy: &conf_v1.ActionProxy{}, + expected: true, + }, + { + proxy: &conf_v1.ActionProxy{ + RequestHeaders: &conf_v1.ProxyRequestHeaders{ + Pass: nil, + }, + }, + expected: true, + }, + { + proxy: &conf_v1.ActionProxy{ + RequestHeaders: &conf_v1.ProxyRequestHeaders{ + Pass: &passTrue, + }, + }, + expected: true, + }, + { + proxy: &conf_v1.ActionProxy{ + RequestHeaders: &conf_v1.ProxyRequestHeaders{ + Pass: &passFalse, + }, + }, + expected: false, + }, + } + + 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) + } + } +} + +func TestGenerateProxyHideHeaders(t *testing.T) { + tests := []struct { + proxy *conf_v1.ActionProxy + expected []string + }{ + { + proxy: nil, + expected: nil, + }, + { + proxy: &conf_v1.ActionProxy{ + ResponseHeaders: nil, + }, + }, + { + proxy: &conf_v1.ActionProxy{ + 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) { + tests := []struct { + proxy *conf_v1.ActionProxy + expected []string + }{ + { + proxy: nil, + expected: nil, + }, + { + proxy: &conf_v1.ActionProxy{ + ResponseHeaders: nil, + }, + }, + { + proxy: &conf_v1.ActionProxy{ + ResponseHeaders: &conf_v1.ProxyResponseHeaders{ + Pass: []string{"Header", "Header-2"}, + }, + }, + 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) { + tests := []struct { + proxy *conf_v1.ActionProxy + expected string + }{ + { + proxy: nil, + expected: "", + }, + { + proxy: &conf_v1.ActionProxy{ + ResponseHeaders: nil, + }, + expected: "", + }, + { + proxy: &conf_v1.ActionProxy{ + ResponseHeaders: &conf_v1.ProxyResponseHeaders{ + Ignore: []string{"Header", "Header-2"}, + }, + }, + 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) { + tests := []struct { + proxy *conf_v1.ActionProxy + expected []version2.AddHeader + }{ + { + 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) + } + } +} diff --git a/pkg/apis/configuration/v1/types.go b/pkg/apis/configuration/v1/types.go index 9ee54e19fd..17e0a5b0e6 100644 --- a/pkg/apis/configuration/v1/types.go +++ b/pkg/apis/configuration/v1/types.go @@ -122,6 +122,7 @@ type Action struct { Pass string `json:"pass"` Redirect *ActionRedirect `json:"redirect"` Return *ActionReturn `json:"return"` + Proxy *ActionProxy `json:"proxy"` } // ActionRedirect defines a redirect in an Action. @@ -137,6 +138,34 @@ type ActionReturn struct { Body string `json:"body"` } +// ActionProxy defines a proxy in an Action. +type ActionProxy struct { + Upstream string `json:"upstream"` + RewritePath string `json:"rewritePath"` + RequestHeaders *ProxyRequestHeaders `json:"requestHeaders"` + ResponseHeaders *ProxyResponseHeaders `json:"responseHeaders"` +} + +// ProxyRequestHeaders defines the request headers manipulation in an ActionProxy. +type ProxyRequestHeaders struct { + Pass *bool `json:"pass"` + Set []Header `json:"set"` +} + +// ProxyRequestHeaders defines the response headers manipulation in an ActionProxy. +type ProxyResponseHeaders struct { + Hide []string `json:"hide"` + Pass []string `json:"pass"` + Ignore []string `json:"ignore"` + Add []AddHeader `json:"add"` +} + +// Header defines an HTTP Header with an optional Always field to use with the add_header NGINX directive. +type AddHeader struct { + Header `json:",inline"` + Always bool `json:"always"` +} + // Split defines a split. type Split struct { Weight int `json:"weight"` diff --git a/pkg/apis/configuration/v1/zz_generated.deepcopy.go b/pkg/apis/configuration/v1/zz_generated.deepcopy.go index 731508732b..39eb13fdb8 100644 --- a/pkg/apis/configuration/v1/zz_generated.deepcopy.go +++ b/pkg/apis/configuration/v1/zz_generated.deepcopy.go @@ -21,6 +21,11 @@ func (in *Action) DeepCopyInto(out *Action) { *out = new(ActionReturn) **out = **in } + if in.Proxy != nil { + in, out := &in.Proxy, &out.Proxy + *out = new(ActionProxy) + (*in).DeepCopyInto(*out) + } return } @@ -34,6 +39,32 @@ func (in *Action) DeepCopy() *Action { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ActionProxy) DeepCopyInto(out *ActionProxy) { + *out = *in + if in.RequestHeaders != nil { + in, out := &in.RequestHeaders, &out.RequestHeaders + *out = new(ProxyRequestHeaders) + (*in).DeepCopyInto(*out) + } + if in.ResponseHeaders != nil { + in, out := &in.ResponseHeaders, &out.ResponseHeaders + *out = new(ProxyResponseHeaders) + (*in).DeepCopyInto(*out) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ActionProxy. +func (in *ActionProxy) DeepCopy() *ActionProxy { + if in == nil { + return nil + } + out := new(ActionProxy) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ActionRedirect) DeepCopyInto(out *ActionRedirect) { *out = *in @@ -66,6 +97,23 @@ func (in *ActionReturn) DeepCopy() *ActionReturn { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AddHeader) DeepCopyInto(out *AddHeader) { + *out = *in + out.Header = in.Header + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AddHeader. +func (in *AddHeader) DeepCopy() *AddHeader { + if in == nil { + return nil + } + out := new(AddHeader) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Condition) DeepCopyInto(out *Condition) { *out = *in @@ -243,6 +291,68 @@ func (in *Match) DeepCopy() *Match { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ProxyRequestHeaders) DeepCopyInto(out *ProxyRequestHeaders) { + *out = *in + if in.Pass != nil { + in, out := &in.Pass, &out.Pass + *out = new(bool) + **out = **in + } + if in.Set != nil { + in, out := &in.Set, &out.Set + *out = make([]Header, len(*in)) + copy(*out, *in) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ProxyRequestHeaders. +func (in *ProxyRequestHeaders) DeepCopy() *ProxyRequestHeaders { + if in == nil { + return nil + } + out := new(ProxyRequestHeaders) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ProxyResponseHeaders) DeepCopyInto(out *ProxyResponseHeaders) { + *out = *in + if in.Hide != nil { + in, out := &in.Hide, &out.Hide + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.Pass != nil { + in, out := &in.Pass, &out.Pass + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.Ignore != nil { + in, out := &in.Ignore, &out.Ignore + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.Add != nil { + in, out := &in.Add, &out.Add + *out = make([]AddHeader, len(*in)) + copy(*out, *in) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ProxyResponseHeaders. +func (in *ProxyResponseHeaders) DeepCopy() *ProxyResponseHeaders { + if in == nil { + return nil + } + out := new(ProxyResponseHeaders) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Route) DeepCopyInto(out *Route) { *out = *in diff --git a/pkg/apis/configuration/validation/virtualserver.go b/pkg/apis/configuration/validation/virtualserver.go index a9e3dd674e..157966392b 100644 --- a/pkg/apis/configuration/validation/virtualserver.go +++ b/pkg/apis/configuration/validation/virtualserver.go @@ -562,19 +562,19 @@ func validateRoute(route v1.Route, fieldPath *field.Path, upstreamNames sets.Str fieldCount := 0 if route.Action != nil { - allErrs = append(allErrs, validateAction(route.Action, fieldPath.Child("action"), upstreamNames)...) + allErrs = append(allErrs, validateAction(route.Action, fieldPath.Child("action"), upstreamNames, route.Path, false)...) fieldCount++ } if len(route.Splits) > 0 { - allErrs = append(allErrs, validateSplits(route.Splits, fieldPath.Child("splits"), upstreamNames)...) + allErrs = append(allErrs, validateSplits(route.Splits, fieldPath.Child("splits"), upstreamNames, route.Path)...) fieldCount++ } // Matches are optional. that's why we don't do fieldCount++ if len(route.Matches) > 0 { for i, m := range route.Matches { - allErrs = append(allErrs, validateMatch(m, fieldPath.Child("matches").Index(i), upstreamNames)...) + allErrs = append(allErrs, validateMatch(m, fieldPath.Child("matches").Index(i), upstreamNames, route.Path)...) } } @@ -706,6 +706,10 @@ func countActions(action *v1.Action) int { count++ } + if action.Proxy != nil { + count++ + } + return count } @@ -746,11 +750,11 @@ var validRedirectVariableNames = map[string]bool{ "host": true, } -func validateAction(action *v1.Action, fieldPath *field.Path, upstreamNames sets.String) field.ErrorList { +func validateAction(action *v1.Action, fieldPath *field.Path, upstreamNames sets.String, path string, internal bool) field.ErrorList { allErrs := field.ErrorList{} if countActions(action) != 1 { - return append(allErrs, field.Required(fieldPath, "action must specify exactly one of `pass`, `redirect` or `return`")) + return append(allErrs, field.Required(fieldPath, "action must specify exactly one of `pass`, `redirect`, `return` or `proxy`")) } if action.Pass != "" { @@ -765,6 +769,10 @@ func validateAction(action *v1.Action, fieldPath *field.Path, upstreamNames sets allErrs = append(allErrs, validateActionReturn(action.Return, fieldPath.Child("return"), returnBodySpecialVariables, returnBodyVariables)...) } + if action.Proxy != nil { + allErrs = append(allErrs, validateActionProxy(action.Proxy, fieldPath.Child("proxy"), upstreamNames, path, internal)...) + } + return allErrs } @@ -990,7 +998,137 @@ func validateReferencedUpstream(name string, fieldPath *field.Path, upstreamName return allErrs } -func validateSplits(splits []v1.Split, fieldPath *field.Path, upstreamNames sets.String) field.ErrorList { +func validateActionProxy(p *v1.ActionProxy, fieldPath *field.Path, upstreamNames sets.String, path string, internal bool) field.ErrorList { + allErrs := field.ErrorList{} + + allErrs = append(allErrs, validateReferencedUpstream(p.Upstream, fieldPath.Child("upstream"), upstreamNames)...) + allErrs = append(allErrs, validateActionProxyRequestHeaders(p.RequestHeaders, fieldPath.Child("requestHeaders"))...) + allErrs = append(allErrs, validateActionProxyResponseHeaders(p.ResponseHeaders, fieldPath.Child("responseHeaders"))...) + + if strings.HasPrefix(path, "~") || internal { + allErrs = append(allErrs, validateActionProxyRewritePathForRegexp(p.RewritePath, fieldPath.Child("rewritePath"))...) + } else { + allErrs = append(allErrs, validateActionProxyRewritePath(p.RewritePath, fieldPath.Child("rewritePath"))...) + } + + return allErrs +} + +func validateStringNoVariables(s string, fieldPath *field.Path) field.ErrorList { + allErrs := field.ErrorList{} + + for i, char := range s { + charLen := len(string(char)) + if string(char) == "$" && i+charLen < len(s) { + if _, err := strconv.Atoi(string(s[i+charLen])); err != nil { + return append(allErrs, field.Invalid(fieldPath, s, "`$` character can be only followed by a number")) + } + } + } + + return allErrs +} + +func validateActionProxyRewritePath(rewritePath string, fieldPath *field.Path) field.ErrorList { + allErrs := field.ErrorList{} + + if rewritePath == "" { + return allErrs + } + + allErrs = append(allErrs, validateStringNoVariables(rewritePath, fieldPath)...) + + return append(allErrs, validatePath(rewritePath, fieldPath)...) +} + +func validateActionProxyRewritePathForRegexp(rewritePath string, fieldPath *field.Path) field.ErrorList { + allErrs := field.ErrorList{} + + if rewritePath == "" { + return allErrs + } + + allErrs = append(allErrs, validateStringNoVariables(rewritePath, fieldPath)...) + + if !escapedStringsFmtRegexp.MatchString(rewritePath) { + msg := validation.RegexError(escapedStringsErrMsg, escapedStringsFmt, "/rewrite$1", "/images") + allErrs = append(allErrs, field.Invalid(fieldPath, rewritePath, msg)) + } + + return allErrs +} + +func validateActionProxyRequestHeaders(requestHeaders *v1.ProxyRequestHeaders, fieldPath *field.Path) field.ErrorList { + allErrs := field.ErrorList{} + + if requestHeaders == nil { + return allErrs + } + + for i, header := range requestHeaders.Set { + allErrs = append(allErrs, validateHeader(header, fieldPath.Index(i))...) + } + + return allErrs +} + +func validateActionProxyResponseHeaders(responseHeaders *v1.ProxyResponseHeaders, fieldPath *field.Path) field.ErrorList { + allErrs := field.ErrorList{} + + if responseHeaders == nil { + return allErrs + } + + for i, header := range responseHeaders.Hide { + for _, msg := range validation.IsHTTPHeaderName(header) { + allErrs = append(allErrs, field.Invalid(fieldPath.Child("hide").Index(i), header, msg)) + } + } + + for i, header := range responseHeaders.Pass { + for _, msg := range validation.IsHTTPHeaderName(header) { + allErrs = append(allErrs, field.Invalid(fieldPath.Child("pass").Index(i), header, msg)) + } + } + + for i, header := range responseHeaders.Add { + allErrs = append(allErrs, validateHeader(header.Header, fieldPath.Child("add").Index(i))...) + } + + allErrs = append(allErrs, validateIgnoreHeaders(responseHeaders.Ignore, fieldPath.Child("ignore"))...) + + return allErrs +} + +var validIgnoreHeaders = map[string]bool{ + "X-Accel-Redirect": true, + "X-Accel-Expires": true, + "X-Accel-Limit-Rate": true, + "X-Accel-Buffering": true, + "X-Accel-Charset": true, + "Expires": true, + "Cache-Control": true, + "Set-Cookie": true, + "Vary": true, +} + +func validateIgnoreHeaders(ignoreHeaders []string, fieldPath *field.Path) field.ErrorList { + allErrs := field.ErrorList{} + if len(ignoreHeaders) == 0 { + return allErrs + } + + for i, h := range ignoreHeaders { + if !validIgnoreHeaders[h] { + msg := fmt.Sprintf("not a valid ignore header name. Accepted headers are : %v", mapToPrettyString(validIgnoreHeaders)) + allErrs = append(allErrs, field.Invalid(fieldPath.Index(i), h, msg)) + } + } + + return allErrs +} + +func validateSplits(splits []v1.Split, fieldPath *field.Path, upstreamNames sets.String, path string) field.ErrorList { allErrs := field.ErrorList{} if len(splits) < 2 { @@ -1009,7 +1147,7 @@ func validateSplits(splits []v1.Split, fieldPath *field.Path, upstreamNames sets if s.Action == nil { allErrs = append(allErrs, field.Required(idxPath.Child("action"), "")) } else { - allErrs = append(allErrs, validateAction(s.Action, idxPath.Child("action"), upstreamNames)...) + allErrs = append(allErrs, validateAction(s.Action, idxPath.Child("action"), upstreamNames, path, true)...) } totalWeight += s.Weight @@ -1079,7 +1217,7 @@ func validatePath(path string, fieldPath *field.Path) field.ErrorList { return allErrs } -func validateMatch(match v1.Match, fieldPath *field.Path, upstreamNames sets.String) field.ErrorList { +func validateMatch(match v1.Match, fieldPath *field.Path, upstreamNames sets.String, path string) field.ErrorList { allErrs := field.ErrorList{} if len(match.Conditions) == 0 { @@ -1093,12 +1231,12 @@ func validateMatch(match v1.Match, fieldPath *field.Path, upstreamNames sets.Str fieldCount := 0 if match.Action != nil { - allErrs = append(allErrs, validateAction(match.Action, fieldPath.Child("action"), upstreamNames)...) + allErrs = append(allErrs, validateAction(match.Action, fieldPath.Child("action"), upstreamNames, path, true)...) fieldCount++ } if len(match.Splits) > 0 { - allErrs = append(allErrs, validateSplits(match.Splits, fieldPath.Child("splits"), upstreamNames)...) + allErrs = append(allErrs, validateSplits(match.Splits, fieldPath.Child("splits"), upstreamNames, path)...) fieldCount++ } diff --git a/pkg/apis/configuration/validation/virtualserver_test.go b/pkg/apis/configuration/validation/virtualserver_test.go index 379c415629..0d0cb5a219 100644 --- a/pkg/apis/configuration/validation/virtualserver_test.go +++ b/pkg/apis/configuration/validation/virtualserver_test.go @@ -843,13 +843,43 @@ func TestValidateAction(t *testing.T) { Code: 302, }, }, - msg: "redirect action with status code set", }, + { + action: &v1.Action{ + Proxy: &v1.ActionProxy{ + Upstream: "test", + RewritePath: "/rewrite", + RequestHeaders: &v1.ProxyRequestHeaders{ + Set: []v1.Header{ + { + Name: "Header-Name", + Value: "value", + }, + }, + }, + ResponseHeaders: &v1.ProxyResponseHeaders{ + Hide: []string{"header-name"}, + Pass: []string{"header-name"}, + Ignore: []string{"Expires"}, + Add: []v1.AddHeader{ + { + Header: v1.Header{ + Name: "Header-Name", + Value: "value", + }, + Always: true, + }, + }, + }, + }, + }, + msg: "proxy action with rewritePath, requestHeaders and responseHeaders", + }, } for _, test := range tests { - allErrs := validateAction(test.action, field.NewPath("action"), upstreamNames) + allErrs := validateAction(test.action, field.NewPath("action"), upstreamNames, "", false) if len(allErrs) > 0 { t.Errorf("validateAction() returned errors %v for valid input for the case of %s", allErrs, test.msg) } @@ -892,10 +922,18 @@ func TestValidateActionFails(t *testing.T) { }, msg: "redirect action with invalid status code set", }, + { + action: &v1.Action{ + Proxy: &v1.ActionProxy{ + Upstream: "", + }, + }, + msg: "proxy action with missing upstream field", + }, } for _, test := range tests { - allErrs := validateAction(test.action, field.NewPath("action"), upstreamNames) + allErrs := validateAction(test.action, field.NewPath("action"), upstreamNames, "", false) if len(allErrs) == 0 { t.Errorf("validateAction() returned no errors for invalid input for the case of %s", test.msg) } @@ -1241,7 +1279,9 @@ func TestValidateSplits(t *testing.T) { { Weight: 10, Action: &v1.Action{ - Pass: "test-2", + Proxy: &v1.ActionProxy{ + Upstream: "test-2", + }, }, }, } @@ -1250,7 +1290,7 @@ func TestValidateSplits(t *testing.T) { "test-2": {}, } - allErrs := validateSplits(splits, field.NewPath("splits"), upstreamNames) + allErrs := validateSplits(splits, field.NewPath("splits"), upstreamNames, "") if len(allErrs) > 0 { t.Errorf("validateSplits() returned errors %v for valid input", allErrs) } @@ -1363,7 +1403,7 @@ func TestValidateSplitsFails(t *testing.T) { } for _, test := range tests { - allErrs := validateSplits(test.splits, field.NewPath("splits"), test.upstreamNames) + allErrs := validateSplits(test.splits, field.NewPath("splits"), test.upstreamNames, "") if len(allErrs) == 0 { t.Errorf("validateSplits() returned no errors for invalid input for the case of %s", test.msg) } @@ -1600,7 +1640,7 @@ func TestValidateMatch(t *testing.T) { } for _, test := range tests { - allErrs := validateMatch(test.match, field.NewPath("match"), test.upstreamNames) + allErrs := validateMatch(test.match, field.NewPath("match"), test.upstreamNames, "") if len(allErrs) > 0 { t.Errorf("validateMatch() returned errors %v for valid input for the case of %s", allErrs, test.msg) } @@ -1690,7 +1730,7 @@ func TestValidateMatchFails(t *testing.T) { } for _, test := range tests { - allErrs := validateMatch(test.match, field.NewPath("match"), test.upstreamNames) + allErrs := validateMatch(test.match, field.NewPath("match"), test.upstreamNames, "") if len(allErrs) == 0 { t.Errorf("validateMatch() returned no errors for invalid input for the case of %s", test.msg) } @@ -2823,6 +2863,312 @@ func TestValidateActionReturnFails(t *testing.T) { } } +func TestValidateActionProxy(t *testing.T) { + upstreamNames := map[string]sets.Empty{ + "upstream1": {}, + } + path := "/path" + actionProxy := &v1.ActionProxy{ + Upstream: "upstream1", + RewritePath: "/test", + } + + allErrs := validateActionProxy(actionProxy, field.NewPath("proxy"), upstreamNames, path, false) + + if len(allErrs) != 0 { + t.Errorf("validateActionProxy(%+v, %v, %v) returned errors for valid input: %v", actionProxy, upstreamNames, path, allErrs) + } +} + +func TestValidateActionProxyFails(t *testing.T) { + upstreamNames := map[string]sets.Empty{ + "upstream1": {}, + } + path := "/path" + actionProxy := &v1.ActionProxy{ + Upstream: "", + } + + allErrs := validateActionProxy(actionProxy, field.NewPath("proxy"), upstreamNames, path, false) + + if len(allErrs) == 0 { + t.Errorf("validateActionProxy(%+v, %v, %v) returned no errors for invalid input", actionProxy, upstreamNames, path) + } +} + +func TestValidateActionProxyRewritePath(t *testing.T) { + tests := []string{"/rewrite", "/rewrite", `/$2`} + for _, test := range tests { + allErrs := validateActionProxyRewritePath(test, field.NewPath("rewritePath")) + if len(allErrs) != 0 { + t.Errorf("validateActionProxyRewritePath(%v) returned errors for valid input: %v", test, allErrs) + } + } +} + +func TestValidateActionProxyRewritePathFails(t *testing.T) { + tests := []string{`/\d{3}`, `(`, "$request_uri"} + for _, test := range tests { + allErrs := validateActionProxyRewritePath(test, field.NewPath("rewritePath")) + if len(allErrs) == 0 { + t.Errorf("validateActionProxyRewritePath(%v) returned no errors for invalid input", test) + } + } +} + +func TestValidateActionProxyRewritePathForRegexp(t *testing.T) { + tests := []string{"/rewrite$1", "test", `/$2`, `\"test\"`} + for _, test := range tests { + allErrs := validateActionProxyRewritePathForRegexp(test, field.NewPath("rewritePath")) + if len(allErrs) != 0 { + t.Errorf("validateActionProxyRewritePathForRegexp(%v) returned errors for valid input: %v", test, allErrs) + } + } +} + +func TestValidateActionProxyRewritePathForRegexpFails(t *testing.T) { + tests := []string{"$request_uri", `"test"`, `test\`} + for _, test := range tests { + allErrs := validateActionProxyRewritePathForRegexp(test, field.NewPath("rewritePath")) + if len(allErrs) == 0 { + t.Errorf("validateActionProxyRewritePathForRegexp(%v) returned no errors for invalid input", test) + } + } +} + +func TestValidateActionProxyRequestHeaders(t *testing.T) { + requestHeaders := &v1.ProxyRequestHeaders{ + Set: []v1.Header{ + { + Name: "Host", + Value: "nginx.org", + }, + }, + } + + allErrs := validateActionProxyRequestHeaders(requestHeaders, field.NewPath("requestHeaders")) + if len(allErrs) != 0 { + t.Errorf("validateActionProxyRequestHeaders(%v) returned errors for valid input: %v", requestHeaders, allErrs) + } +} + +func TestValidateActionProxyRequestHeadersFails(t *testing.T) { + requestHeaders := &v1.ProxyRequestHeaders{ + Set: []v1.Header{ + { + Name: "in va lid", + Value: "", + }, + { + Name: "Host", + Value: "$var", + }, + { + Name: "", + Value: "nginx.org", + }, + }, + } + + allErrs := validateActionProxyRequestHeaders(requestHeaders, field.NewPath("requestHeaders")) + if len(allErrs) == 0 { + t.Errorf("validateActionProxyRequestHeaders(%v) returned no errors for invalid input", requestHeaders) + } +} + +func TestValidateActionProxyResponseHeaders(t *testing.T) { + tests := []struct { + responseHeaders *v1.ProxyResponseHeaders + }{ + { + responseHeaders: &v1.ProxyResponseHeaders{ + Hide: []string{"Header"}, + Pass: []string{"Header"}, + Ignore: []string{"Expires"}, + Add: []v1.AddHeader{ + { + Header: v1.Header{ + Name: "Host", + Value: "nginx.org", + }, + Always: false, + }, + }, + }, + }, + { + responseHeaders: &v1.ProxyResponseHeaders{ + Hide: []string{"Header"}, + }, + }, + { + responseHeaders: &v1.ProxyResponseHeaders{ + Pass: []string{"Header"}, + }, + }, + { + responseHeaders: &v1.ProxyResponseHeaders{ + Ignore: []string{"Expires"}, + }, + }, + { + responseHeaders: &v1.ProxyResponseHeaders{ + Add: []v1.AddHeader{ + { + Header: v1.Header{ + Name: "Host", + Value: "nginx.org", + }, + Always: false, + }, + }, + }, + }, + } + + for _, test := range tests { + allErrs := validateActionProxyResponseHeaders(test.responseHeaders, field.NewPath("responseHeaders")) + if len(allErrs) != 0 { + t.Errorf("validateActionProxyResponseHeaders(%v) returned errors for valid input: %v", test.responseHeaders, allErrs) + } + } +} + +func TestValidateActionProxyResponseHeadersFails(t *testing.T) { + tests := []struct { + responseHeaders *v1.ProxyResponseHeaders + msg string + }{ + { + responseHeaders: &v1.ProxyResponseHeaders{ + Hide: []string{""}, + Pass: []string{""}, + Ignore: []string{""}, + Add: []v1.AddHeader{ + { + Header: v1.Header{ + Name: "", + Value: "nginx.org", + }, + }, + }, + }, + msg: "all fields invalid", + }, + { + responseHeaders: &v1.ProxyResponseHeaders{ + Hide: []string{"invalid header"}, + }, + msg: "invalid hide headers", + }, + { + responseHeaders: &v1.ProxyResponseHeaders{ + Pass: []string{"$invalid"}, + }, + msg: "invalid pass headers", + }, + { + responseHeaders: &v1.ProxyResponseHeaders{ + Ignore: []string{"1234 invalid"}, + }, + msg: "invalid ignore headers", + }, + { + responseHeaders: &v1.ProxyResponseHeaders{ + Add: []v1.AddHeader{ + { + Header: v1.Header{ + Name: "$invalid 123", + Value: "nginx.org", + }, + Always: false, + }, + }, + }, + msg: "invalid Add header name", + }, + { + responseHeaders: &v1.ProxyResponseHeaders{ + Add: []v1.AddHeader{ + { + Header: v1.Header{ + Name: "Host", + Value: "$invalid", + }, + Always: false, + }, + }, + }, + msg: "invalid Add header value", + }, + } + + for _, test := range tests { + allErrs := validateActionProxyResponseHeaders(test.responseHeaders, field.NewPath("responseHeaders")) + if len(allErrs) == 0 { + t.Errorf("validateActionProxyResponseHeaders(%v) returned no errors for invalid input for the case of %v", test.responseHeaders, test.msg) + } + } +} + +func TestValidateIgnoreHeaders(t *testing.T) { + var ignoreHeaders []string + + for header := range validIgnoreHeaders { + ignoreHeaders = append(ignoreHeaders, header) + } + + allErrs := validateIgnoreHeaders(ignoreHeaders, field.NewPath("ignoreHeaders")) + if len(allErrs) != 0 { + t.Errorf("validateIgnoreHeaders(%v) returned errors for valid input: %v", ignoreHeaders, allErrs) + } +} + +func TestValidateIgnoreHeadersFails(t *testing.T) { + ignoreHeaders := []string{ + "Host", + "Connection", + } + + allErrs := validateIgnoreHeaders(ignoreHeaders, field.NewPath("ignoreHeaders")) + if len(allErrs) == 0 { + t.Errorf("validateIgnoreHeaders(%v) returned no errors for invalid input", ignoreHeaders) + } +} + +func TestValidateStringNoVariables(t *testing.T) { + tests := []string{ + "string", + "endWith$", + "withNumber$1", + "abcййй", + "abcййй$1", + "", + } + + for _, test := range tests { + allErrs := validateStringNoVariables(test, field.NewPath("rewritePath")) + if len(allErrs) != 0 { + t.Errorf("validateStringNoVariables(%v) returned errors for valid input: %v", test, allErrs) + } + } +} + +func TestValidateStringNoVariablesFails(t *testing.T) { + tests := []string{ + "$var", + "abcйй$й", + "$$", + } + + for _, test := range tests { + allErrs := validateStringNoVariables(test, field.NewPath("rewritePath")) + if len(allErrs) == 0 { + t.Errorf("validateStringNoVariables(%v) returned no errors for invalid input", test) + } + } +} + func TestValidateStringWithVariables(t *testing.T) { testStrings := []string{ "",