Skip to content

Commit

Permalink
Implement request redirect filter in HTTPRoute rule (#218)
Browse files Browse the repository at this point in the history
This commit implements the request redirect filter as part of the routing
rule in the HTTPRoute.

A common use-case for a request redirect is redirecting HTTP requests
to HTTPS. The commit updates the HTTPS termination example to include
HTTPS redirect configuration.

Notes:
- The experimental 'path' field of 'requestRedirect' is out of scope.
- The validation of the fields of `requestRedirect` is not implemented.
It is left to be done in a separate component responsible for validation
with FIXMEs added to the relevant code locations.
- If multiple redirect filters are configured, NGINX Kubernetes Gateway
will choose the first one and ignore the rest.
- NGINX will always redirect a request even if the request has already
been redirected. Thus, any backendRefs defined in the routing rule will
be ignored. However, that "always redirect" behavior is not specified
by the Gateway API. As a result, we might need to change our
implementation if different behavior becomes specified by the Gateway API
in the future.
  • Loading branch information
pleshakov authored Sep 9, 2022
1 parent 2eec798 commit 6da92d0
Show file tree
Hide file tree
Showing 11 changed files with 530 additions and 121 deletions.
5 changes: 4 additions & 1 deletion docs/gateway-api-compatibility.md.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,10 @@ Fields:
* `headers` - partially supported. Only `Exact` type.
* `queryParams` - partially supported. Only `Exact` type.
* `method` - supported.
* `filters` - not supported.
* `filters`
* `type` - supported.
* `requestRedirect` - supported except for the experimental `path` field. If multiple filters with `requestRedirect` are configured, NGINX Kubernetes Gateway will choose the first one and ignore the rest.
* `requestHeaderModifier`, `requestMirror`, `urlRewrite`, `extensionRef` - not supported.
* `backendRefs` - partially supported. Only a single backend ref without support for `weight`. Backend ref `filters` are not supported. NGINX Kubernetes Gateway will use the IP of the Service as a backend, not the IPs of the corresponding Pods. Watching for Service updates is not supported.
* `status`
* `parents`
Expand Down
51 changes: 43 additions & 8 deletions examples/https-termination/README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# HTTPS Termination Example

In this example we expand on the simple [cafe-example](../cafe-example) by adding HTTPS termination to our routes.
In this example, we expand on the simple [cafe-example](../cafe-example) by adding HTTPS termination to our routes and an HTTPS redirect from port 80 to 443.

## Running the Example

Expand All @@ -14,10 +14,11 @@ In this example we expand on the simple [cafe-example](../cafe-example) by addin
GW_IP=XXX.YYY.ZZZ.III
```

1. Save the HTTPS port of NGINX Kubernetes Gateway:
1. Save the ports of NGINX Kubernetes Gateway:

```
GW_HTTPS_PORT=port
GW_HTTP_PORT=<http port number>
GW_HTTPS_PORT=<https port number>
```

## 2. Deploy the Cafe Application
Expand Down Expand Up @@ -52,26 +53,60 @@ In this example we expand on the simple [cafe-example](../cafe-example) by addin
kubectl apply -f gateway.yaml
```

This [gateway](./gateway.yaml) configures an `https` listener is to terminate TLS connections using the `cafe-secret` we created in the step 1.
This [Gateway](./gateway.yaml) configures:
* `http` listener for HTTP traffic
* `https` listener for HTTPS traffic. It terminates TLS connections using the `cafe-secret` we created in step 1.

1. Create the `HTTPRoute` resources:
```
kubectl apply -f cafe-routes.yaml
```

To configure HTTPS termination for our cafe application, we will bind the `https` listener to our `HTTPRoutes` in [cafe-routes.yaml](./cafe-routes.yaml) using the [`parentReference`](https://gateway-api.sigs.k8s.io/references/spec/#gateway.networking.k8s.io/v1beta1.ParentReference) field:
To configure HTTPS termination for our cafe application, we will bind our `coffee` and `tea` HTTPRoutes to the `https` listener in [cafe-routes.yaml](./cafe-routes.yaml) using the [`parentReference`](https://gateway-api.sigs.k8s.io/references/spec/#gateway.networking.k8s.io/v1beta1.ParentReference) field:

```yaml
parentRefs:
- name: gateway
namespace: default
sectionName: https
```
To configure an HTTPS redirect from port 80 to 443, we will bind the special `cafe-tls-redirect` HTTPRoute with a [`HTTPRequestRedirectFilter`](https://gateway-api.sigs.k8s.io/references/spec/#gateway.networking.k8s.io/v1beta1.HTTPRequestRedirectFilter) to the `http` listener:

```yaml
parentRefs:
- name: gateway
sectionName: http
```

## 4. Test the Application

To access the application, we will use `curl` to send requests to the `coffee` and `tea` Services.
Since our certificate is self-signed, we'll use curl's `--insecure` option to turn off certificate verification.
To access the application, we will use `curl` to send requests to the `coffee` and `tea` Services. First, we will access the application over HTTP to test that the HTTPS redirect works. Then we will use HTTPS.

### 4.1 Test HTTPS Redirect

To test that NGINX sends an HTTPS redirect, we will send requests to the `coffee` and `tea` Services on HTTP port. We will use curl's `--include` option to print the response headers (we are interested in the `Location` header).

To get a redirect for coffee:
```
curl --resolve cafe.example.com:$GW_HTTP_PORT:$GW_IP http://cafe.example.com:$GW_HTTP_PORT/coffee --include
HTTP/1.1 302 Moved Temporarily
...
Location: https://cafe.example.com:443/coffee
...
```

To get a redirect for tea:
```
curl --resolve cafe.example.com:$GW_HTTP_PORT:$GW_IP http://cafe.example.com:$GW_HTTP_PORT/tea --include
HTTP/1.1 302 Moved Temporarily
...
Location: https://cafe.example.com:443/tea
...
```

### 4.2 Access Coffee and Tea

Now we will access the application over HTTPS. Since our certificate is self-signed, we will use curl's `--insecure` option to turn off certificate verification.

To get coffee:

Expand Down
17 changes: 17 additions & 0 deletions examples/https-termination/cafe-routes.yaml
Original file line number Diff line number Diff line change
@@ -1,5 +1,22 @@
apiVersion: gateway.networking.k8s.io/v1beta1
kind: HTTPRoute
metadata:
name: cafe-tls-redirect
spec:
parentRefs:
- name: gateway
sectionName: http
hostnames:
- "cafe.example.com"
rules:
- filters:
- type: RequestRedirect
requestRedirect:
scheme: https
port: 443
---
apiVersion: gateway.networking.k8s.io/v1beta1
kind: HTTPRoute
metadata:
name: coffee
spec:
Expand Down
3 changes: 3 additions & 0 deletions examples/https-termination/gateway.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ metadata:
spec:
gatewayClassName: nginx
listeners:
- name: http
port: 80
protocol: HTTP
- name: https
port: 443
protocol: HTTPS
Expand Down
5 changes: 5 additions & 0 deletions internal/helpers/helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,11 @@ func GetStringPointer(s string) *string {
return &s
}

// GetIntPointer takes an int and returns a pointer to it. Useful in unit tests when initializing structs.
func GetIntPointer(i int) *int {
return &i
}

// GetInt32Pointer takes an int32 and returns a pointer to it. Useful in unit tests when initializing structs.
func GetInt32Pointer(i int32) *int32 {
return &i
Expand Down
83 changes: 68 additions & 15 deletions internal/nginx/config/generator.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,11 +82,15 @@ func generate(virtualServer state.VirtualServer, serviceStore state.ServiceStore

s := server{ServerName: virtualServer.Hostname}

listenerPort := 80

if virtualServer.SSL != nil {
s.SSL = &ssl{
Certificate: virtualServer.SSL.CertificatePath,
CertificateKey: virtualServer.SSL.CertificatePath,
}

listenerPort = 443
}

if len(virtualServer.PathRules) == 0 {
Expand All @@ -100,26 +104,41 @@ func generate(virtualServer state.VirtualServer, serviceStore state.ServiceStore
matches := make([]httpMatch, 0, len(rule.MatchRules))

for ruleIdx, r := range rule.MatchRules {

address, err := getBackendAddress(r.Source.Spec.Rules[r.RuleIdx].BackendRefs, r.Source.Namespace, serviceStore)
if err != nil {
warnings.AddWarning(r.Source, err.Error())
}

m := r.GetMatch()

var loc location

// handle case where the only route is a path-only match
// generate a standard location block without http_matches.
if len(rule.MatchRules) == 1 && isPathOnlyMatch(m) {
locs = append(locs, location{
Path: rule.Path,
ProxyPass: generateProxyPass(address),
})
loc = location{
Path: rule.Path,
}
} else {
path := createPathForMatch(rule.Path, ruleIdx)
locs = append(locs, generateMatchLocation(path, address))
loc = generateMatchLocation(path)
matches = append(matches, createHTTPMatch(m, path))
}

// FIXME(pleshakov): There could be a case when the filter has the type set but not the corresponding field.
// For example, type is v1beta1.HTTPRouteFilterRequestRedirect, but RequestRedirect field is nil.
// The validation webhook catches that.
// If it doesn't work as expected, such situation is silently handled below in findFirstFilters.
// Consider reporting an error. But that should be done in a separate validation layer.

// RequestRedirect and proxying are mutually exclusive.
if r.Filters.RequestRedirect != nil {
loc.Return = generateReturnValForRedirectFilter(r.Filters.RequestRedirect, listenerPort)
} else {
address, err := getBackendAddress(r.Source.Spec.Rules[r.RuleIdx].BackendRefs, r.Source.Namespace, serviceStore)
if err != nil {
warnings.AddWarning(r.Source, err.Error())
}

loc.ProxyPass = generateProxyPass(address)
}

locs = append(locs, loc)
}

if len(matches) > 0 {
Expand Down Expand Up @@ -150,6 +169,41 @@ func generateProxyPass(address string) string {
return "http://" + address
}

func generateReturnValForRedirectFilter(filter *v1beta1.HTTPRequestRedirectFilter, listenerPort int) *returnVal {
if filter == nil {
return nil
}

hostname := "$host"
if filter.Hostname != nil {
hostname = string(*filter.Hostname)
}

// FIXME(pleshakov): Unknown values here must result in the implementation setting the Attached Condition for
// the Route to `status: False`, with a Reason of `UnsupportedValue`. In that case, all routes of the Route will be
// ignored. NGINX will return 500. This should be implemented in the validation layer.
code := statusFound
if filter.StatusCode != nil {
code = statusCode(*filter.StatusCode)
}

port := listenerPort
if filter.Port != nil {
port = int(*filter.Port)
}

// FIXME(pleshakov): Same as the FIXME about StatusCode above.
scheme := "$scheme"
if filter.Scheme != nil {
scheme = *filter.Scheme
}

return &returnVal{
Code: code,
URL: fmt.Sprintf("%s://%s:%d$request_uri", scheme, hostname, port),
}
}

func getBackendAddress(
refs []v1beta1.HTTPBackendRef,
parentNS string,
Expand Down Expand Up @@ -183,11 +237,10 @@ func getBackendAddress(
return fmt.Sprintf("%s:%d", address, *ref.Port), nil
}

func generateMatchLocation(path, address string) location {
func generateMatchLocation(path string) location {
return location{
Path: path,
ProxyPass: generateProxyPass(address),
Internal: true,
Path: path,
Internal: true,
}
}

Expand Down
Loading

0 comments on commit 6da92d0

Please sign in to comment.