Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement request redirect filter in HTTPRoute rule #218

Merged
merged 4 commits into from
Sep 9, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
kate-osborn marked this conversation as resolved.
Show resolved Hide resolved
// 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)
kate-osborn marked this conversation as resolved.
Show resolved Hide resolved
}

// FIXME(pleshakov): Same as the FIXME about StatusCode above.
scheme := "$scheme"
if filter.Scheme != nil {
scheme = *filter.Scheme
kate-osborn marked this conversation as resolved.
Show resolved Hide resolved
}

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