Add support for HTTPRequestRedirectFilter in k8s Gateway API

This commit is contained in:
Roman Tomjak 2022-12-22 14:02:05 +00:00 committed by GitHub
parent 943238faba
commit d046af2e91
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 339 additions and 0 deletions

View file

@ -0,0 +1,52 @@
---
kind: GatewayClass
apiVersion: gateway.networking.k8s.io/v1alpha2
metadata:
name: my-gateway-class
spec:
controllerName: traefik.io/gateway-controller
---
kind: Gateway
apiVersion: gateway.networking.k8s.io/v1alpha2
metadata:
name: my-gateway
namespace: default
spec:
gatewayClassName: my-gateway-class
listeners: # Use GatewayClass defaults for listener definition.
- name: http
protocol: HTTP
port: 80
allowedRoutes:
kinds:
- kind: HTTPRoute
group: gateway.networking.k8s.io
namespaces:
from: Same
---
kind: HTTPRoute
apiVersion: gateway.networking.k8s.io/v1alpha2
metadata:
name: http-app-1
namespace: default
spec:
parentRefs:
- name: my-gateway
kind: Gateway
group: gateway.networking.k8s.io
hostnames:
- "example.org"
rules:
- backendRefs:
- name: whoami
port: 80
weight: 1
kind: Service
group: ""
filters:
- type: RequestRedirect
requestRedirect:
scheme: https
statusCode: 301

View file

@ -0,0 +1,52 @@
---
kind: GatewayClass
apiVersion: gateway.networking.k8s.io/v1alpha2
metadata:
name: my-gateway-class
spec:
controllerName: traefik.io/gateway-controller
---
kind: Gateway
apiVersion: gateway.networking.k8s.io/v1alpha2
metadata:
name: my-gateway
namespace: default
spec:
gatewayClassName: my-gateway-class
listeners: # Use GatewayClass defaults for listener definition.
- name: http
protocol: HTTP
port: 80
allowedRoutes:
kinds:
- kind: HTTPRoute
group: gateway.networking.k8s.io
namespaces:
from: Same
---
kind: HTTPRoute
apiVersion: gateway.networking.k8s.io/v1alpha2
metadata:
name: http-app-1
namespace: default
spec:
parentRefs:
- name: my-gateway
kind: Gateway
group: gateway.networking.k8s.io
hostnames:
- "example.org"
rules:
- backendRefs:
- name: whoami
port: 80
weight: 1
kind: Service
group: ""
filters:
- type: RequestRedirect
requestRedirect:
hostname: example.com
port: 443

View file

@ -6,6 +6,7 @@ import (
"errors"
"fmt"
"net"
"net/http"
"os"
"regexp"
"sort"
@ -756,6 +757,26 @@ func gatewayHTTPRouteToHTTPConf(ctx context.Context, ep string, listener v1alpha
continue
}
middlewares, err := loadMiddlewares(listener, routerKey, routeRule.Filters)
if err != nil {
// update "ResolvedRefs" status true with "InvalidFilters" reason
conditions = append(conditions, metav1.Condition{
Type: string(v1alpha2.ListenerConditionResolvedRefs),
Status: metav1.ConditionFalse,
LastTransitionTime: metav1.Now(),
Reason: "InvalidFilters", // TODO check the spec if a proper reason is introduced at some point
Message: fmt.Sprintf("Cannot load HTTPRoute filter %s/%s: %v", route.Namespace, route.Name, err),
})
// TODO update the RouteStatus condition / deduplicate conditions on listener
continue
}
for middlewareName, middleware := range middlewares {
conf.HTTP.Middlewares[middlewareName] = middleware
router.Middlewares = append(router.Middlewares, middlewareName)
}
if len(routeRule.BackendRefs) == 0 {
continue
}
@ -1663,6 +1684,85 @@ func loadTCPServices(client Client, namespace string, backendRefs []v1alpha2.Bac
return wrrSvc, services, nil
}
func loadMiddlewares(listener v1alpha2.Listener, prefix string, filters []v1alpha2.HTTPRouteFilter) (map[string]*dynamic.Middleware, error) {
middlewares := make(map[string]*dynamic.Middleware)
// The spec allows for an empty string in which case we should use the
// scheme of the request which in this case is the listener scheme.
var listenerScheme string
switch listener.Protocol {
case v1alpha2.HTTPProtocolType:
listenerScheme = "http"
case v1alpha2.HTTPSProtocolType:
listenerScheme = "https"
default:
return nil, fmt.Errorf("invalid listener protocol %s", listener.Protocol)
}
for i, filter := range filters {
var middleware *dynamic.Middleware
switch filter.Type {
case v1alpha2.HTTPRouteFilterRequestRedirect:
var err error
middleware, err = createRedirectRegexMiddleware(listenerScheme, filter.RequestRedirect)
if err != nil {
return nil, fmt.Errorf("creating RedirectRegex middleware: %w", err)
}
default:
// As per the spec:
// https://gateway-api.sigs.k8s.io/api-types/httproute/#filters-optional
// In all cases where incompatible or unsupported filters are
// specified, implementations MUST add a warning condition to
// status.
return nil, fmt.Errorf("unsupported filter %s", filter.Type)
}
middlewareName := provider.Normalize(fmt.Sprintf("%s-%s-%d", prefix, strings.ToLower(string(filter.Type)), i))
middlewares[middlewareName] = middleware
}
return middlewares, nil
}
func createRedirectRegexMiddleware(scheme string, filter *v1alpha2.HTTPRequestRedirectFilter) (*dynamic.Middleware, error) {
// Use the HTTPRequestRedirectFilter scheme if defined.
filterScheme := scheme
if filter.Scheme != nil {
filterScheme = *filter.Scheme
}
if filterScheme != "http" && filterScheme != "https" {
return nil, fmt.Errorf("invalid scheme %s", filterScheme)
}
statusCode := http.StatusFound
if filter.StatusCode != nil {
statusCode = *filter.StatusCode
}
if statusCode != http.StatusMovedPermanently && statusCode != http.StatusFound {
return nil, fmt.Errorf("invalid status code %d", statusCode)
}
port := "${port}"
if filter.Port != nil {
port = fmt.Sprintf(":%d", *filter.Port)
}
hostname := "${hostname}"
if filter.Hostname != nil && *filter.Hostname != "" {
hostname = string(*filter.Hostname)
}
return &dynamic.Middleware{
RedirectRegex: &dynamic.RedirectRegex{
Regex: `^[a-z]+:\/\/(?P<userInfo>.+@)?(?P<hostname>\[[\w:\.]+\]|[\w\._-]+)(?P<port>:\d+)?\/(?P<path>.*)`,
Replacement: fmt.Sprintf("%s://${userinfo}%s%s/${path}", filterScheme, hostname, port),
Permanent: statusCode == http.StatusMovedPermanently,
},
}, nil
}
func getProtocol(portSpec corev1.ServicePort) string {
protocol := "http"
if portSpec.Port == 443 || strings.HasPrefix(portSpec.Name, "https") {

View file

@ -1555,6 +1555,141 @@ func TestLoadHTTPRoutes(t *testing.T) {
TLS: &dynamic.TLSConfiguration{},
},
},
{
desc: "Simple HTTPRoute, redirect HTTP to HTTPS",
paths: []string{"services.yml", "httproute/filter_http_to_https.yml"},
entryPoints: map[string]Entrypoint{"web": {
Address: ":80",
}},
expected: &dynamic.Configuration{
UDP: &dynamic.UDPConfiguration{
Routers: map[string]*dynamic.UDPRouter{},
Services: map[string]*dynamic.UDPService{},
},
TCP: &dynamic.TCPConfiguration{
Routers: map[string]*dynamic.TCPRouter{},
Middlewares: map[string]*dynamic.TCPMiddleware{},
Services: map[string]*dynamic.TCPService{},
ServersTransports: map[string]*dynamic.TCPServersTransport{},
},
HTTP: &dynamic.HTTPConfiguration{
Routers: map[string]*dynamic.Router{
"default-http-app-1-my-gateway-web-364ce6ec04c3d49b19c4": {
EntryPoints: []string{"web"},
Service: "default-http-app-1-my-gateway-web-364ce6ec04c3d49b19c4-wrr",
Rule: "Host(`example.org`) && PathPrefix(`/`)",
Middlewares: []string{"default-http-app-1-my-gateway-web-364ce6ec04c3d49b19c4-requestredirect-0"},
},
},
Middlewares: map[string]*dynamic.Middleware{
"default-http-app-1-my-gateway-web-364ce6ec04c3d49b19c4-requestredirect-0": {
RedirectRegex: &dynamic.RedirectRegex{
Regex: "^[a-z]+:\\/\\/(?P<userInfo>.+@)?(?P<hostname>\\[[\\w:\\.]+\\]|[\\w\\._-]+)(?P<port>:\\d+)?\\/(?P<path>.*)",
Replacement: "https://${userinfo}${hostname}${port}/${path}",
Permanent: true,
},
},
},
Services: map[string]*dynamic.Service{
"default-http-app-1-my-gateway-web-364ce6ec04c3d49b19c4-wrr": {
Weighted: &dynamic.WeightedRoundRobin{
Services: []dynamic.WRRService{
{
Name: "default-whoami-80",
Weight: func(i int) *int { return &i }(1),
},
},
},
},
"default-whoami-80": {
LoadBalancer: &dynamic.ServersLoadBalancer{
Servers: []dynamic.Server{
{
URL: "http://10.10.0.1:80",
},
{
URL: "http://10.10.0.2:80",
},
},
PassHostHeader: pointer.Bool(true),
ResponseForwarding: &dynamic.ResponseForwarding{
FlushInterval: ptypes.Duration(100 * time.Millisecond),
},
},
},
},
ServersTransports: map[string]*dynamic.ServersTransport{},
},
TLS: &dynamic.TLSConfiguration{},
},
},
{
desc: "Simple HTTPRoute, redirect HTTP to HTTPS with hostname",
paths: []string{"services.yml", "httproute/filter_http_to_https_with_hostname_and_port.yml"},
entryPoints: map[string]Entrypoint{"web": {
Address: ":80",
}},
expected: &dynamic.Configuration{
UDP: &dynamic.UDPConfiguration{
Routers: map[string]*dynamic.UDPRouter{},
Services: map[string]*dynamic.UDPService{},
},
TCP: &dynamic.TCPConfiguration{
Routers: map[string]*dynamic.TCPRouter{},
Middlewares: map[string]*dynamic.TCPMiddleware{},
Services: map[string]*dynamic.TCPService{},
ServersTransports: map[string]*dynamic.TCPServersTransport{},
},
HTTP: &dynamic.HTTPConfiguration{
Routers: map[string]*dynamic.Router{
"default-http-app-1-my-gateway-web-364ce6ec04c3d49b19c4": {
EntryPoints: []string{"web"},
Service: "default-http-app-1-my-gateway-web-364ce6ec04c3d49b19c4-wrr",
Rule: "Host(`example.org`) && PathPrefix(`/`)",
Middlewares: []string{"default-http-app-1-my-gateway-web-364ce6ec04c3d49b19c4-requestredirect-0"},
},
},
Middlewares: map[string]*dynamic.Middleware{
"default-http-app-1-my-gateway-web-364ce6ec04c3d49b19c4-requestredirect-0": {
RedirectRegex: &dynamic.RedirectRegex{
Regex: "^[a-z]+:\\/\\/(?P<userInfo>.+@)?(?P<hostname>\\[[\\w:\\.]+\\]|[\\w\\._-]+)(?P<port>:\\d+)?\\/(?P<path>.*)",
Replacement: "http://${userinfo}example.com:443/${path}",
},
},
},
Services: map[string]*dynamic.Service{
"default-http-app-1-my-gateway-web-364ce6ec04c3d49b19c4-wrr": {
Weighted: &dynamic.WeightedRoundRobin{
Services: []dynamic.WRRService{
{
Name: "default-whoami-80",
Weight: func(i int) *int { return &i }(1),
},
},
},
},
"default-whoami-80": {
LoadBalancer: &dynamic.ServersLoadBalancer{
Servers: []dynamic.Server{
{
URL: "http://10.10.0.1:80",
},
{
URL: "http://10.10.0.2:80",
},
},
PassHostHeader: pointer.Bool(true),
ResponseForwarding: &dynamic.ResponseForwarding{
FlushInterval: ptypes.Duration(100 * time.Millisecond),
},
},
},
},
ServersTransports: map[string]*dynamic.ServersTransport{},
},
TLS: &dynamic.TLSConfiguration{},
},
},
}
for _, test := range testCases {