Add support for HTTPRequestRedirectFilter in k8s Gateway API
This commit is contained in:
parent
943238faba
commit
d046af2e91
4 changed files with 339 additions and 0 deletions
|
@ -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
|
|
@ -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
|
|
@ -6,6 +6,7 @@ import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"regexp"
|
"regexp"
|
||||||
"sort"
|
"sort"
|
||||||
|
@ -756,6 +757,26 @@ func gatewayHTTPRouteToHTTPConf(ctx context.Context, ep string, listener v1alpha
|
||||||
continue
|
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 {
|
if len(routeRule.BackendRefs) == 0 {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
@ -1663,6 +1684,85 @@ func loadTCPServices(client Client, namespace string, backendRefs []v1alpha2.Bac
|
||||||
return wrrSvc, services, nil
|
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 {
|
func getProtocol(portSpec corev1.ServicePort) string {
|
||||||
protocol := "http"
|
protocol := "http"
|
||||||
if portSpec.Port == 443 || strings.HasPrefix(portSpec.Name, "https") {
|
if portSpec.Port == 443 || strings.HasPrefix(portSpec.Name, "https") {
|
||||||
|
|
|
@ -1555,6 +1555,141 @@ func TestLoadHTTPRoutes(t *testing.T) {
|
||||||
TLS: &dynamic.TLSConfiguration{},
|
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 {
|
for _, test := range testCases {
|
||||||
|
|
Loading…
Reference in a new issue