Support HTTPRoute redirect port and scheme

Co-authored-by: Kevin Pollet <pollet.kevin@gmail.com>
This commit is contained in:
Romain 2024-06-13 11:16:04 +02:00 committed by GitHub
parent 27af1fb478
commit 3ca667a3d4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
31 changed files with 1063 additions and 997 deletions

View file

@ -204,12 +204,12 @@ func (s *K8sConformanceSuite) TestK8sGatewayAPIConformance() {
ksuite.SupportHTTPRouteSchemeRedirect,
ksuite.SupportHTTPRouteHostRewrite,
ksuite.SupportHTTPRoutePathRewrite,
ksuite.SupportHTTPRoutePathRedirect,
),
ExemptFeatures: sets.New(
ksuite.SupportHTTPRouteRequestTimeout,
ksuite.SupportHTTPRouteBackendTimeout,
ksuite.SupportHTTPRouteResponseHeaderModification,
ksuite.SupportHTTPRoutePathRedirect,
ksuite.SupportHTTPRouteRequestMirror,
ksuite.SupportHTTPRouteRequestMultipleMirrors,
),

View file

@ -30,27 +30,27 @@
"traefik"
]
},
"default-http-app-1-my-gateway-web-af4b9876d1fe36359e27@kubernetesgateway": {
"default-http-app-1-my-gateway-web-0-1c0cf64bde37d9d0df06@kubernetesgateway": {
"entryPoints": [
"web"
],
"service": "default-http-app-1-my-gateway-web-af4b9876d1fe36359e27-wrr",
"rule": "Host(`foo.com`) \u0026\u0026 (Path(`/bar`))",
"service": "default-http-app-1-my-gateway-web-0-wrr",
"rule": "Host(`foo.com`) \u0026\u0026 Path(`/bar`)",
"ruleSyntax": "v3",
"priority": 99997,
"priority": 100008,
"status": "enabled",
"using": [
"web"
]
},
"default-http-app-1-my-https-gateway-websecure-af4b9876d1fe36359e27@kubernetesgateway": {
"default-http-app-1-my-https-gateway-websecure-0-1c0cf64bde37d9d0df06@kubernetesgateway": {
"entryPoints": [
"websecure"
],
"service": "default-http-app-1-my-https-gateway-websecure-af4b9876d1fe36359e27-wrr",
"rule": "Host(`foo.com`) \u0026\u0026 (Path(`/bar`))",
"service": "default-http-app-1-my-https-gateway-websecure-0-wrr",
"rule": "Host(`foo.com`) \u0026\u0026 Path(`/bar`)",
"ruleSyntax": "v3",
"priority": 99997,
"priority": 100008,
"tls": {},
"status": "enabled",
"using": [
@ -96,7 +96,7 @@
"dashboard@internal"
]
},
"default-http-app-1-my-gateway-web-af4b9876d1fe36359e27-wrr@kubernetesgateway": {
"default-http-app-1-my-gateway-web-0-wrr@kubernetesgateway": {
"weighted": {
"services": [
{
@ -107,10 +107,10 @@
},
"status": "enabled",
"usedBy": [
"default-http-app-1-my-gateway-web-af4b9876d1fe36359e27@kubernetesgateway"
"default-http-app-1-my-gateway-web-0-1c0cf64bde37d9d0df06@kubernetesgateway"
]
},
"default-http-app-1-my-https-gateway-websecure-af4b9876d1fe36359e27-wrr@kubernetesgateway": {
"default-http-app-1-my-https-gateway-websecure-0-wrr@kubernetesgateway": {
"weighted": {
"services": [
{
@ -121,7 +121,7 @@
},
"status": "enabled",
"usedBy": [
"default-http-app-1-my-https-gateway-websecure-af4b9876d1fe36359e27@kubernetesgateway"
"default-http-app-1-my-https-gateway-websecure-0-1c0cf64bde37d9d0df06@kubernetesgateway"
]
},
"default-whoami-80@kubernetesgateway": {
@ -150,11 +150,11 @@
}
},
"tcpRouters": {
"default-tcp-app-1-my-tcp-gateway-footcp-e3b0c44298fc1c149afb@kubernetesgateway": {
"default-tcp-app-1-my-tcp-gateway-footcp@kubernetesgateway": {
"entryPoints": [
"footcp"
],
"service": "default-tcp-app-1-my-tcp-gateway-footcp-e3b0c44298fc1c149afb-wrr-0",
"service": "default-tcp-app-1-my-tcp-gateway-footcp-wrr-0",
"rule": "HostSNI(`*`)",
"ruleSyntax": "v3",
"priority": -1,
@ -163,11 +163,11 @@
"footcp"
]
},
"default-tcp-app-1-my-tls-gateway-footlsterminate-e3b0c44298fc1c149afb@kubernetesgateway": {
"default-tcp-app-1-my-tls-gateway-footlsterminate@kubernetesgateway": {
"entryPoints": [
"footlsterminate"
],
"service": "default-tcp-app-1-my-tls-gateway-footlsterminate-e3b0c44298fc1c149afb-wrr-0",
"service": "default-tcp-app-1-my-tls-gateway-footlsterminate-wrr-0",
"rule": "HostSNI(`*`)",
"ruleSyntax": "v3",
"priority": -1,
@ -197,7 +197,7 @@
}
},
"tcpServices": {
"default-tcp-app-1-my-tcp-gateway-footcp-e3b0c44298fc1c149afb-wrr-0@kubernetesgateway": {
"default-tcp-app-1-my-tcp-gateway-footcp-wrr-0@kubernetesgateway": {
"weighted": {
"services": [
{
@ -208,10 +208,10 @@
},
"status": "enabled",
"usedBy": [
"default-tcp-app-1-my-tcp-gateway-footcp-e3b0c44298fc1c149afb@kubernetesgateway"
"default-tcp-app-1-my-tcp-gateway-footcp@kubernetesgateway"
]
},
"default-tcp-app-1-my-tls-gateway-footlsterminate-e3b0c44298fc1c149afb-wrr-0@kubernetesgateway": {
"default-tcp-app-1-my-tls-gateway-footlsterminate-wrr-0@kubernetesgateway": {
"weighted": {
"services": [
{
@ -222,7 +222,7 @@
},
"status": "enabled",
"usedBy": [
"default-tcp-app-1-my-tls-gateway-footlsterminate-e3b0c44298fc1c149afb@kubernetesgateway"
"default-tcp-app-1-my-tls-gateway-footlsterminate@kubernetesgateway"
]
},
"default-tls-app-1-my-tls-gateway-footlspassthrough-2279fe75c5156dc5eb26-wrr-0@kubernetesgateway": {

View file

@ -696,7 +696,10 @@ type RequestHeaderModifier struct {
// RequestRedirect holds the request redirect middleware configuration.
type RequestRedirect struct {
Regex string `json:"regex,omitempty"`
Replacement string `json:"replacement,omitempty"`
Permanent bool `json:"permanent,omitempty"`
Scheme *string `json:"scheme,omitempty"`
Hostname *string `json:"hostname,omitempty"`
Port *string `json:"port,omitempty"`
Path *string `json:"path,omitempty"`
PathPrefix *string `json:"pathPrefix,omitempty"`
StatusCode int `json:"statusCode,omitempty"`
}

View file

@ -867,7 +867,7 @@ func (in *Middleware) DeepCopyInto(out *Middleware) {
if in.RequestRedirect != nil {
in, out := &in.RequestRedirect, &out.RequestRedirect
*out = new(RequestRedirect)
**out = **in
(*in).DeepCopyInto(*out)
}
return
}
@ -1115,6 +1115,31 @@ func (in *RequestHeaderModifier) DeepCopy() *RequestHeaderModifier {
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *RequestRedirect) DeepCopyInto(out *RequestRedirect) {
*out = *in
if in.Scheme != nil {
in, out := &in.Scheme, &out.Scheme
*out = new(string)
**out = **in
}
if in.Hostname != nil {
in, out := &in.Hostname, &out.Hostname
*out = new(string)
**out = **in
}
if in.Port != nil {
in, out := &in.Port, &out.Port
*out = new(string)
**out = **in
}
if in.Path != nil {
in, out := &in.Path, &out.Path
*out = new(string)
**out = **in
}
if in.PathPrefix != nil {
in, out := &in.PathPrefix, &out.PathPrefix
*out = new(string)
**out = **in
}
return
}

View file

@ -2,133 +2,101 @@ package redirect
import (
"context"
"fmt"
"net"
"net/http"
"net/url"
"regexp"
"path"
"strings"
"github.com/traefik/traefik/v3/pkg/config/dynamic"
"github.com/traefik/traefik/v3/pkg/middlewares"
"github.com/vulcand/oxy/v2/utils"
"go.opentelemetry.io/otel/trace"
)
const (
schemeHTTP = "http"
schemeHTTPS = "https"
typeName = "RequestRedirect"
typeName = "RequestRedirect"
)
var uriRegexp = regexp.MustCompile(`^(https?):\/\/(\[[\w:.]+\]|[\w\._-]+)?(:\d+)?(.*)$`)
type redirect struct {
name string
next http.Handler
scheme *string
hostname *string
port *string
path *string
pathPrefix *string
statusCode int
}
// NewRequestRedirect creates a redirect middleware.
func NewRequestRedirect(ctx context.Context, next http.Handler, conf dynamic.RequestRedirect, name string) (http.Handler, error) {
logger := middlewares.GetLogger(ctx, name, typeName)
logger.Debug().Msg("Creating middleware")
logger.Debug().Msgf("Setting up redirection from %s to %s", conf.Regex, conf.Replacement)
re, err := regexp.Compile(conf.Regex)
if err != nil {
return nil, err
statusCode := conf.StatusCode
if statusCode == 0 {
statusCode = http.StatusFound
}
if statusCode != http.StatusMovedPermanently && statusCode != http.StatusFound {
return nil, fmt.Errorf("unsupported status code: %d", statusCode)
}
return &redirect{
regex: re,
replacement: conf.Replacement,
permanent: conf.Permanent,
errHandler: utils.DefaultHandler,
next: next,
name: name,
rawURL: rawURL,
return redirect{
name: name,
next: next,
scheme: conf.Scheme,
hostname: conf.Hostname,
port: conf.Port,
path: conf.Path,
pathPrefix: conf.PathPrefix,
statusCode: statusCode,
}, nil
}
type redirect struct {
next http.Handler
regex *regexp.Regexp
replacement string
permanent bool
errHandler utils.ErrorHandler
name string
rawURL func(*http.Request) string
}
func rawURL(req *http.Request) string {
scheme := schemeHTTP
host := req.Host
port := ""
uri := req.RequestURI
if match := uriRegexp.FindStringSubmatch(req.RequestURI); len(match) > 0 {
scheme = match[1]
if len(match[2]) > 0 {
host = match[2]
}
if len(match[3]) > 0 {
port = match[3]
}
uri = match[4]
}
if req.TLS != nil {
scheme = schemeHTTPS
}
return strings.Join([]string{scheme, "://", host, port, uri}, "")
}
func (r *redirect) GetTracingInformation() (string, string, trace.SpanKind) {
func (r redirect) GetTracingInformation() (string, string, trace.SpanKind) {
return r.name, typeName, trace.SpanKindInternal
}
func (r *redirect) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
oldURL := r.rawURL(req)
func (r redirect) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
redirectURL := *req.URL
redirectURL.Host = req.Host
// If the Regexp doesn't match, skip to the next handler.
if !r.regex.MatchString(oldURL) {
r.next.ServeHTTP(rw, req)
return
if r.scheme != nil {
redirectURL.Scheme = *r.scheme
}
// Apply a rewrite regexp to the URL.
newURL := r.regex.ReplaceAllString(oldURL, r.replacement)
// Parse the rewritten URL and replace request URL with it.
parsedURL, err := url.Parse(newURL)
if err != nil {
r.errHandler.ServeHTTP(rw, req, err)
return
host := redirectURL.Hostname()
if r.hostname != nil {
host = *r.hostname
}
handler := &moveHandler{location: parsedURL, permanent: r.permanent}
handler.ServeHTTP(rw, req)
}
type moveHandler struct {
location *url.URL
permanent bool
}
func (m *moveHandler) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
rw.Header().Set("Location", m.location.String())
status := http.StatusFound
if req.Method != http.MethodGet {
status = http.StatusTemporaryRedirect
port := redirectURL.Port()
if r.port != nil {
port = *r.port
}
if m.permanent {
status = http.StatusMovedPermanently
if req.Method != http.MethodGet {
status = http.StatusPermanentRedirect
if port != "" {
host = net.JoinHostPort(host, port)
}
redirectURL.Host = host
if r.path != nil && r.pathPrefix == nil {
redirectURL.Path = *r.path
}
if r.path != nil && r.pathPrefix != nil {
redirectURL.Path = path.Join(*r.path, strings.TrimPrefix(req.URL.Path, *r.pathPrefix))
// add the trailing slash if needed, as path.Join removes trailing slashes.
if strings.HasSuffix(req.URL.Path, "/") && !strings.HasSuffix(redirectURL.Path, "/") {
redirectURL.Path += "/"
}
}
rw.WriteHeader(status)
_, err := rw.Write([]byte(http.StatusText(status)))
if err != nil {
rw.Header().Set("Location", redirectURL.String())
rw.WriteHeader(r.statusCode)
if _, err := rw.Write([]byte(http.StatusText(r.statusCode))); err != nil {
http.Error(rw, err.Error(), http.StatusInternalServerError)
}
}

View file

@ -10,6 +10,7 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/traefik/traefik/v3/pkg/config/dynamic"
"k8s.io/utils/ptr"
)
func TestRequestRedirectHandler(t *testing.T) {
@ -24,49 +25,107 @@ func TestRequestRedirectHandler(t *testing.T) {
expectedStatus int
errorExpected bool
}{
{
desc: "wrong status code",
config: dynamic.RequestRedirect{
Path: ptr.To("/baz"),
StatusCode: http.StatusOK,
},
url: "http://foo.com:80/foo/bar",
errorExpected: true,
},
{
desc: "replace path",
config: dynamic.RequestRedirect{
Path: ptr.To("/baz"),
},
url: "http://foo.com:80/foo/bar",
expectedURL: "http://foo.com:80/baz",
expectedStatus: http.StatusFound,
},
{
desc: "replace path without trailing slash",
config: dynamic.RequestRedirect{
Path: ptr.To("/baz"),
},
url: "http://foo.com:80/foo/bar/",
expectedURL: "http://foo.com:80/baz",
expectedStatus: http.StatusFound,
},
{
desc: "replace path with trailing slash",
config: dynamic.RequestRedirect{
Path: ptr.To("/baz/"),
},
url: "http://foo.com:80/foo/bar",
expectedURL: "http://foo.com:80/baz/",
expectedStatus: http.StatusFound,
},
{
desc: "only hostname",
config: dynamic.RequestRedirect{
Hostname: ptr.To("bar.com"),
},
url: "http://foo.com:8080/foo/",
expectedURL: "http://bar.com:8080/foo/",
expectedStatus: http.StatusFound,
},
{
desc: "replace prefix path",
config: dynamic.RequestRedirect{
Path: ptr.To("/baz"),
PathPrefix: ptr.To("/foo"),
},
url: "http://foo.com:80/foo/bar",
expectedURL: "http://foo.com:80/baz/bar",
expectedStatus: http.StatusFound,
},
{
desc: "replace prefix path with trailing slash",
config: dynamic.RequestRedirect{
Path: ptr.To("/baz"),
PathPrefix: ptr.To("/foo"),
},
url: "http://foo.com:80/foo/bar/",
expectedURL: "http://foo.com:80/baz/bar/",
expectedStatus: http.StatusFound,
},
{
desc: "replace prefix path without slash prefix",
config: dynamic.RequestRedirect{
Path: ptr.To("baz"),
PathPrefix: ptr.To("/foo"),
},
url: "http://foo.com:80/foo/bar",
expectedURL: "http://foo.com:80/baz/bar",
expectedStatus: http.StatusFound,
},
{
desc: "replace prefix path without slash prefix",
config: dynamic.RequestRedirect{
Path: ptr.To("/baz"),
PathPrefix: ptr.To("/foo/"),
},
url: "http://foo.com:80/foo/bar",
expectedURL: "http://foo.com:80/baz/bar",
expectedStatus: http.StatusFound,
},
{
desc: "simple redirection",
config: dynamic.RequestRedirect{
Regex: `^(?:http?:\/\/)(foo)(\.com)(:\d+)(.*)$`,
Replacement: "https://${1}bar$2:443$4",
Scheme: ptr.To("https"),
Hostname: ptr.To("foobar.com"),
Port: ptr.To("443"),
},
url: "http://foo.com:80",
expectedURL: "https://foobar.com:443",
expectedStatus: http.StatusFound,
},
{
desc: "URL doesn't match regex",
config: dynamic.RequestRedirect{
Regex: `^(?:http?:\/\/)(foo)(\.com)(:\d+)(.*)$`,
Replacement: "https://${1}bar$2:443$4",
},
url: "http://bar.com:80",
expectedStatus: http.StatusOK,
},
{
desc: "invalid rewritten URL",
config: dynamic.RequestRedirect{
Regex: `^(.*)$`,
Replacement: "http://192.168.0.%31/",
},
url: "http://foo.com:80",
expectedStatus: http.StatusBadGateway,
},
{
desc: "invalid regex",
config: dynamic.RequestRedirect{
Regex: `^(.*`,
Replacement: "$1",
},
url: "http://foo.com:80",
errorExpected: true,
},
{
desc: "HTTP to HTTPS permanent",
config: dynamic.RequestRedirect{
Regex: `^http://`,
Replacement: "https://$1",
Permanent: true,
Scheme: ptr.To("https"),
StatusCode: http.StatusMovedPermanently,
},
url: "http://foo",
expectedURL: "https://foo",
@ -75,9 +134,8 @@ func TestRequestRedirectHandler(t *testing.T) {
{
desc: "HTTPS to HTTP permanent",
config: dynamic.RequestRedirect{
Regex: `https://foo`,
Replacement: "http://foo",
Permanent: true,
Scheme: ptr.To("http"),
StatusCode: http.StatusMovedPermanently,
},
secured: true,
url: "https://foo",
@ -87,8 +145,8 @@ func TestRequestRedirectHandler(t *testing.T) {
{
desc: "HTTP to HTTPS",
config: dynamic.RequestRedirect{
Regex: `http://foo:80`,
Replacement: "https://foo:443",
Scheme: ptr.To("https"),
Port: ptr.To("443"),
},
url: "http://foo:80",
expectedURL: "https://foo:443",
@ -97,8 +155,8 @@ func TestRequestRedirectHandler(t *testing.T) {
{
desc: "HTTP to HTTPS, with X-Forwarded-Proto",
config: dynamic.RequestRedirect{
Regex: `http://foo:80`,
Replacement: "https://foo:443",
Scheme: ptr.To("https"),
Port: ptr.To("443"),
},
url: "http://foo:80",
headers: map[string]string{
@ -110,8 +168,8 @@ func TestRequestRedirectHandler(t *testing.T) {
{
desc: "HTTPS to HTTP",
config: dynamic.RequestRedirect{
Regex: `https://foo:443`,
Replacement: "http://foo:80",
Scheme: ptr.To("http"),
Port: ptr.To("80"),
},
secured: true,
url: "https://foo:443",
@ -121,36 +179,13 @@ func TestRequestRedirectHandler(t *testing.T) {
{
desc: "HTTP to HTTP",
config: dynamic.RequestRedirect{
Regex: `http://foo:80`,
Replacement: "http://foo:88",
Scheme: ptr.To("http"),
Port: ptr.To("88"),
},
url: "http://foo:80",
expectedURL: "http://foo:88",
expectedStatus: http.StatusFound,
},
{
desc: "HTTP to HTTP POST",
config: dynamic.RequestRedirect{
Regex: `^http://`,
Replacement: "https://$1",
},
url: "http://foo",
method: http.MethodPost,
expectedURL: "https://foo",
expectedStatus: http.StatusTemporaryRedirect,
},
{
desc: "HTTP to HTTP POST permanent",
config: dynamic.RequestRedirect{
Regex: `^http://`,
Replacement: "https://$1",
Permanent: true,
},
url: "http://foo",
method: http.MethodPost,
expectedURL: "https://foo",
expectedStatus: http.StatusPermanentRedirect,
},
}
for _, test := range testCases {
@ -188,7 +223,7 @@ func TestRequestRedirectHandler(t *testing.T) {
assert.Equal(t, test.expectedStatus, recorder.Code)
switch test.expectedStatus {
case http.StatusMovedPermanently, http.StatusFound, http.StatusTemporaryRedirect, http.StatusPermanentRedirect:
case http.StatusMovedPermanently, http.StatusFound:
location, err := recorder.Result().Location()
require.NoError(t, err)

View file

@ -39,7 +39,11 @@ spec:
hostnames:
- "example.org"
rules:
- filters:
- matches:
- path:
type: PathPrefix
value: /
filters:
- type: RequestRedirect
requestRedirect:
scheme: https

View file

@ -39,7 +39,11 @@ spec:
hostnames:
- "example.org"
rules:
- filters:
- matches:
- path:
type: PathPrefix
value: /
filters:
- type: RequestRedirect
requestRedirect:
hostname: example.com

View file

@ -39,7 +39,11 @@ spec:
hostnames:
- "example.org"
rules:
- backendRefs:
- matches:
- path:
type: PathPrefix
value: /
backendRefs:
- name: whoami
port: 80
weight: 1

View file

@ -37,7 +37,11 @@ spec:
- "foo.com"
- "bar.com"
rules:
- backendRefs:
- matches:
- path:
type: PathPrefix
value: /
backendRefs:
- name: whoami
port: 80
weight: 1

View file

@ -107,7 +107,11 @@ spec:
kind: Gateway
group: gateway.networking.k8s.io
rules:
- backendRefs:
- matches:
- path:
type: PathPrefix
value: /
backendRefs:
- name: whoami
port: 80
weight: 1
@ -131,7 +135,11 @@ spec:
kind: Gateway
group: gateway.networking.k8s.io
rules:
- backendRefs:
- matches:
- path:
type: PathPrefix
value: /
backendRefs:
- name: whoamitcp
port: 9000
weight: 1
@ -151,7 +159,11 @@ spec:
kind: Gateway
group: gateway.networking.k8s.io
rules:
- backendRefs:
- matches:
- path:
type: PathPrefix
value: /
backendRefs:
- name: whoamitcp
port: 9000
weight: 1

View file

@ -33,7 +33,11 @@ metadata:
namespace: default
spec:
rules:
- backendRefs:
- matches:
- path:
type: PathPrefix
value: /
backendRefs:
- name: whoami
port: 80
weight: 1

View file

@ -33,7 +33,11 @@ metadata:
namespace: default
spec:
rules:
- backendRefs:
- matches:
- path:
type: PathPrefix
value: /
backendRefs:
- name: whoami
port: 80
weight: 1

View file

@ -89,7 +89,11 @@ spec:
kind: Gateway
group: gateway.networking.k8s.io
rules:
- backendRefs:
- matches:
- path:
type: PathPrefix
value: /
backendRefs:
- name: whoami
port: 80
weight: 1

View file

@ -33,7 +33,11 @@ metadata:
namespace: default
spec:
rules:
- backendRefs:
- matches:
- path:
type: PathPrefix
value: /
backendRefs:
- name: whoami
port: 80
weight: 1

View file

@ -119,7 +119,11 @@ metadata:
namespace: default
spec:
rules:
- backendRefs:
- matches:
- path:
type: PathPrefix
value: /
backendRefs:
- name: whoami
port: 80
weight: 1
@ -134,7 +138,11 @@ metadata:
namespace: default
spec:
rules:
- backendRefs:
- matches:
- path:
type: PathPrefix
value: /
backendRefs:
- name: whoami
port: 80
weight: 1

View file

@ -107,7 +107,11 @@ spec:
kind: Gateway
group: gateway.networking.k8s.io
rules:
- backendRefs:
- matches:
- path:
type: PathPrefix
value: /
backendRefs:
- name: whoami
port: 80
weight: 1
@ -177,7 +181,11 @@ spec:
kind: Gateway
group: gateway.networking.k8s.io
rules:
- backendRefs:
- matches:
- path:
type: PathPrefix
value: /
backendRefs:
- name: whoami-bar
port: 80
weight: 1

View file

@ -107,7 +107,11 @@ spec:
kind: Gateway
group: gateway.networking.k8s.io
rules:
- backendRefs:
- matches:
- path:
type: PathPrefix
value: /
backendRefs:
- name: whoami
port: 80
weight: 1
@ -177,7 +181,11 @@ spec:
kind: Gateway
group: gateway.networking.k8s.io
rules:
- backendRefs:
- matches:
- path:
type: PathPrefix
value: /
backendRefs:
- name: whoami-bar
port: 80
weight: 1

View file

@ -130,7 +130,11 @@ spec:
kind: Gateway
group: gateway.networking.k8s.io
rules:
- backendRefs:
- matches:
- path:
type: PathPrefix
value: /
backendRefs:
- name: whoami
port: 80
weight: 1
@ -200,7 +204,11 @@ spec:
kind: Gateway
group: gateway.networking.k8s.io
rules:
- backendRefs:
- matches:
- path:
type: PathPrefix
value: /
backendRefs:
- name: whoami-bar
port: 80
weight: 1

View file

@ -53,7 +53,11 @@ spec:
kind: Gateway
group: gateway.networking.k8s.io
rules:
- backendRefs:
- matches:
- path:
type: PathPrefix
value: /
backendRefs:
- name: whoami-bar
port: 80
weight: 1

View file

@ -38,7 +38,11 @@ spec:
kind: Gateway
group: gateway.networking.k8s.io
rules:
- backendRefs:
- matches:
- path:
type: PathPrefix
value: /
backendRefs:
- name: whoami-bar
port: 80
weight: 1

View file

@ -53,7 +53,11 @@ spec:
kind: Gateway
group: gateway.networking.k8s.io
rules:
- backendRefs:
- matches:
- path:
type: PathPrefix
value: /
backendRefs:
- name: whoami-bar
port: 80
weight: 1

View file

@ -54,7 +54,11 @@ spec:
kind: Gateway
group: gateway.networking.k8s.io
rules:
- backendRefs:
- matches:
- path:
type: PathPrefix
value: /
backendRefs:
- name: whoami-bar
port: 80
weight: 1

View file

@ -37,7 +37,11 @@ spec:
- "foo.com"
- "*.bar.com"
rules:
- backendRefs:
- matches:
- path:
type: PathPrefix
value: /
backendRefs:
- name: whoami
port: 80
weight: 1

View file

@ -37,7 +37,11 @@ spec:
- "foo.com"
- "*.foo.com"
rules:
- backendRefs:
- matches:
- path:
type: PathPrefix
value: /
backendRefs:
- name: whoami
port: 80
weight: 1

View file

@ -20,8 +20,8 @@ import (
gatev1 "sigs.k8s.io/gateway-api/apis/v1"
)
func (p *Provider) loadHTTPRoutes(ctx context.Context, client Client, gatewayListeners []gatewayListener, conf *dynamic.Configuration) {
routes, err := client.ListHTTPRoutes()
func (p *Provider) loadHTTPRoutes(ctx context.Context, gatewayListeners []gatewayListener, conf *dynamic.Configuration) {
routes, err := p.client.ListHTTPRoutes()
if err != nil {
log.Ctx(ctx).Error().Err(err).Msg("Unable to list HTTPRoutes")
return
@ -74,7 +74,7 @@ func (p *Provider) loadHTTPRoutes(ctx context.Context, client Client, gatewayLis
}
}
routeConf, resolveRefCondition := p.loadHTTPRoute(logger.WithContext(ctx), client, listener, route, hostnames)
routeConf, resolveRefCondition := p.loadHTTPRoute(logger.WithContext(ctx), listener, route, hostnames)
if accepted && listener.Attached {
mergeHTTPConfiguration(routeConf, conf)
}
@ -90,7 +90,7 @@ func (p *Provider) loadHTTPRoutes(ctx context.Context, client Client, gatewayLis
Parents: parentStatuses,
},
}
if err := client.UpdateHTTPRouteStatus(ctx, ktypes.NamespacedName{Namespace: route.Namespace, Name: route.Name}, status); err != nil {
if err := p.client.UpdateHTTPRouteStatus(ctx, ktypes.NamespacedName{Namespace: route.Namespace, Name: route.Name}, status); err != nil {
logger.Error().
Err(err).
Msg("Unable to update HTTPRoute status")
@ -98,8 +98,8 @@ func (p *Provider) loadHTTPRoutes(ctx context.Context, client Client, gatewayLis
}
}
func (p *Provider) loadHTTPRoute(ctx context.Context, client Client, listener gatewayListener, route *gatev1.HTTPRoute, hostnames []gatev1.Hostname) (*dynamic.Configuration, metav1.Condition) {
routeConf := &dynamic.Configuration{
func (p *Provider) loadHTTPRoute(ctx context.Context, listener gatewayListener, route *gatev1.HTTPRoute, hostnames []gatev1.Hostname) (*dynamic.Configuration, metav1.Condition) {
conf := &dynamic.Configuration{
HTTP: &dynamic.HTTPConfiguration{
Routers: make(map[string]*dynamic.Router),
Middlewares: make(map[string]*dynamic.Middleware),
@ -108,7 +108,7 @@ func (p *Provider) loadHTTPRoute(ctx context.Context, client Client, listener ga
},
}
routeCondition := metav1.Condition{
condition := metav1.Condition{
Type: string(gatev1.RouteConditionResolvedRefs),
Status: metav1.ConditionTrue,
ObservedGeneration: route.Generation,
@ -116,95 +116,101 @@ func (p *Provider) loadHTTPRoute(ctx context.Context, client Client, listener ga
Reason: string(gatev1.RouteConditionResolvedRefs),
}
for _, routeRule := range route.Spec.Rules {
rule, priority := buildRouterRule(hostnames, routeRule.Matches)
router := dynamic.Router{
RuleSyntax: "v3",
Rule: rule,
Priority: priority,
EntryPoints: []string{listener.EPName},
}
if listener.Protocol == gatev1.HTTPSProtocolType {
router.TLS = &dynamic.RouterTLSConfig{}
}
// Adding the gateway desc and the entryPoint desc prevents overlapping of routers build from the same routes.
routerName := route.Name + "-" + listener.GWName + "-" + listener.EPName
routerKey := makeRouterKey(router.Rule, makeID(route.Namespace, routerName))
var wrr dynamic.WeightedRoundRobin
wrrName := provider.Normalize(routerKey + "-wrr")
middlewares, err := p.loadMiddlewares(route.Namespace, routerKey, routeRule.Filters)
if err != nil {
log.Ctx(ctx).Error().
Err(err).
Msg("Unable to load HTTPRoute filters")
wrr.Services = append(wrr.Services, dynamic.WRRService{
errWrr := dynamic.WeightedRoundRobin{
Services: []dynamic.WRRService{
{
Name: "invalid-httproute-filter",
Status: ptr.To(500),
Weight: ptr.To(1),
})
routeConf.HTTP.Services[wrrName] = &dynamic.Service{Weighted: &wrr}
router.Service = wrrName
} else {
for name, middleware := range middlewares {
// If the middleware config is nil in the return of the loadMiddlewares function,
// it means that we just need a reference to that middleware.
if middleware != nil {
routeConf.HTTP.Middlewares[name] = middleware
}
router.Middlewares = append(router.Middlewares, name)
}
// Traefik internal service can be used only if there is only one BackendRef service reference.
if len(routeRule.BackendRefs) == 1 && isInternalService(routeRule.BackendRefs[0].BackendRef) {
router.Service = string(routeRule.BackendRefs[0].Name)
} else {
for _, backendRef := range routeRule.BackendRefs {
name, svc, errCondition := p.loadHTTPService(client, route, backendRef)
weight := ptr.To(int(ptr.Deref(backendRef.Weight, 1)))
if errCondition != nil {
routeCondition = *errCondition
wrr.Services = append(wrr.Services, dynamic.WRRService{
Name: name,
Status: ptr.To(500),
Weight: weight,
})
continue
}
if svc != nil {
routeConf.HTTP.Services[name] = svc
}
wrr.Services = append(wrr.Services, dynamic.WRRService{
Name: name,
Weight: weight,
})
}
routeConf.HTTP.Services[wrrName] = &dynamic.Service{Weighted: &wrr}
router.Service = wrrName
}
}
rt := &router
p.applyRouterTransform(ctx, rt, route)
routerKey = provider.Normalize(routerKey)
routeConf.HTTP.Routers[routerKey] = rt
},
},
}
return routeConf, routeCondition
for ri, routeRule := range route.Spec.Rules {
// Adding the gateway desc and the entryPoint desc prevents overlapping of routers build from the same routes.
routeKey := provider.Normalize(fmt.Sprintf("%s-%s-%s-%s-%d", route.Namespace, route.Name, listener.GWName, listener.EPName, ri))
for _, match := range routeRule.Matches {
rule, priority := buildMatchRule(hostnames, match)
router := dynamic.Router{
RuleSyntax: "v3",
Rule: rule,
Priority: priority + len(route.Spec.Rules) - ri,
EntryPoints: []string{listener.EPName},
}
if listener.Protocol == gatev1.HTTPSProtocolType {
router.TLS = &dynamic.RouterTLSConfig{}
}
var err error
routerName := makeRouterName(rule, routeKey)
router.Middlewares, err = p.loadMiddlewares(conf, route.Namespace, routerName, routeRule.Filters, match.Path)
switch {
case err != nil:
log.Ctx(ctx).Error().Err(err).Msg("Unable to load HTTPRoute filters")
errWrrName := routerName + "-err-wrr"
conf.HTTP.Services[errWrrName] = &dynamic.Service{Weighted: &errWrr}
router.Service = errWrrName
case len(routeRule.BackendRefs) == 1 && isInternalService(routeRule.BackendRefs[0].BackendRef):
router.Service = string(routeRule.BackendRefs[0].Name)
default:
var serviceCondition *metav1.Condition
router.Service, serviceCondition = p.loadService(conf, routeKey, routeRule, route)
if serviceCondition != nil {
condition = *serviceCondition
}
}
p.applyRouterTransform(ctx, &router, route)
conf.HTTP.Routers[routerName] = &router
}
}
return conf, condition
}
func (p *Provider) loadService(conf *dynamic.Configuration, routeKey string, routeRule gatev1.HTTPRouteRule, route *gatev1.HTTPRoute) (string, *metav1.Condition) {
name := routeKey + "-wrr"
if _, ok := conf.HTTP.Services[name]; ok {
return name, nil
}
var wrr dynamic.WeightedRoundRobin
var condition *metav1.Condition
for _, backendRef := range routeRule.BackendRefs {
svcName, svc, errCondition := p.loadHTTPService(route, backendRef)
weight := ptr.To(int(ptr.Deref(backendRef.Weight, 1)))
if errCondition != nil {
condition = errCondition
wrr.Services = append(wrr.Services, dynamic.WRRService{
Name: svcName,
Status: ptr.To(500),
Weight: weight,
})
continue
}
if svc != nil {
conf.HTTP.Services[svcName] = svc
}
wrr.Services = append(wrr.Services, dynamic.WRRService{
Name: svcName,
Weight: weight,
})
}
conf.HTTP.Services[name] = &dynamic.Service{Weighted: &wrr}
return name, condition
}
// loadHTTPService returns a dynamic.Service config corresponding to the given gatev1.HTTPBackendRef.
// Note that the returned dynamic.Service config can be nil (for cross-provider, internal services, and backendFunc).
func (p *Provider) loadHTTPService(client Client, route *gatev1.HTTPRoute, backendRef gatev1.HTTPBackendRef) (string, *dynamic.Service, *metav1.Condition) {
func (p *Provider) loadHTTPService(route *gatev1.HTTPRoute, backendRef gatev1.HTTPBackendRef) (string, *dynamic.Service, *metav1.Condition) {
kind := ptr.Deref(backendRef.Kind, "Service")
group := groupCore
@ -217,9 +223,9 @@ func (p *Provider) loadHTTPService(client Client, route *gatev1.HTTPRoute, backe
namespace = string(*backendRef.Namespace)
}
serviceName := provider.Normalize(makeID(namespace, string(backendRef.Name)))
serviceName := provider.Normalize(namespace + "-" + string(backendRef.Name))
if err := isReferenceGranted(client, groupGateway, kindHTTPRoute, route.Namespace, group, string(kind), string(backendRef.Name), namespace); err != nil {
if err := p.isReferenceGranted(groupGateway, kindHTTPRoute, route.Namespace, group, string(kind), string(backendRef.Name), namespace); err != nil {
return serviceName, nil, &metav1.Condition{
Type: string(gatev1.RouteConditionResolvedRefs),
Status: metav1.ConditionFalse,
@ -261,7 +267,7 @@ func (p *Provider) loadHTTPService(client Client, route *gatev1.HTTPRoute, backe
portStr := strconv.FormatInt(int64(port), 10)
serviceName = provider.Normalize(serviceName + "-" + portStr)
lb, err := loadHTTPServers(client, namespace, backendRef)
lb, err := p.loadHTTPServers(namespace, backendRef)
if err != nil {
return serviceName, nil, &metav1.Condition{
Type: string(gatev1.RouteConditionResolvedRefs),
@ -294,18 +300,17 @@ func (p *Provider) loadHTTPBackendRef(namespace string, backendRef gatev1.HTTPBa
return backendFunc(string(backendRef.Name), namespace)
}
func (p *Provider) loadMiddlewares(namespace, prefix string, filters []gatev1.HTTPRouteFilter) (map[string]*dynamic.Middleware, error) {
func (p *Provider) loadMiddlewares(conf *dynamic.Configuration, namespace, routerName string, filters []gatev1.HTTPRouteFilter, pathMatch *gatev1.HTTPPathMatch) ([]string, error) {
middlewares := make(map[string]*dynamic.Middleware)
for i, filter := range filters {
switch filter.Type {
case gatev1.HTTPRouteFilterRequestRedirect:
middlewareName := provider.Normalize(fmt.Sprintf("%s-%s-%d", prefix, strings.ToLower(string(filter.Type)), i))
middlewares[middlewareName] = createRedirectMiddleware(filter.RequestRedirect)
name := fmt.Sprintf("%s-%s-%d", routerName, strings.ToLower(string(filter.Type)), i)
middlewares[name] = createRequestRedirect(filter.RequestRedirect, pathMatch)
case gatev1.HTTPRouteFilterRequestHeaderModifier:
middlewareName := provider.Normalize(fmt.Sprintf("%s-%s-%d", prefix, strings.ToLower(string(filter.Type)), i))
middlewares[middlewareName] = createRequestHeaderModifier(filter.RequestHeaderModifier)
name := fmt.Sprintf("%s-%s-%d", routerName, strings.ToLower(string(filter.Type)), i)
middlewares[name] = createRequestHeaderModifier(filter.RequestHeaderModifier)
case gatev1.HTTPRouteFilterExtensionRef:
name, middleware, err := p.loadHTTPRouteFilterExtensionRef(namespace, filter.ExtensionRef)
@ -324,7 +329,16 @@ func (p *Provider) loadMiddlewares(namespace, prefix string, filters []gatev1.HT
}
}
return middlewares, nil
var middlewareNames []string
for name, middleware := range middlewares {
if middleware != nil {
conf.HTTP.Middlewares[name] = middleware
}
middlewareNames = append(middlewareNames, name)
}
return middlewareNames, nil
}
func (p *Provider) loadHTTPRouteFilterExtensionRef(namespace string, extensionRef *gatev1.LocalObjectReference) (string, *dynamic.Middleware, error) {
@ -343,9 +357,8 @@ func (p *Provider) loadHTTPRouteFilterExtensionRef(namespace string, extensionRe
return filterFunc(string(extensionRef.Name), namespace)
}
// TODO support cross namespace through ReferencePolicy.
func loadHTTPServers(client Client, namespace string, backendRef gatev1.HTTPBackendRef) (*dynamic.ServersLoadBalancer, error) {
service, exists, err := client.GetService(namespace, string(backendRef.Name))
func (p *Provider) loadHTTPServers(namespace string, backendRef gatev1.HTTPBackendRef) (*dynamic.ServersLoadBalancer, error) {
service, exists, err := p.client.GetService(namespace, string(backendRef.Name))
if err != nil {
return nil, fmt.Errorf("getting service: %w", err)
}
@ -367,7 +380,7 @@ func loadHTTPServers(client Client, namespace string, backendRef gatev1.HTTPBack
return nil, errors.New("service port not found")
}
endpoints, endpointsExists, err := client.GetEndpoints(namespace, string(backendRef.Name))
endpoints, endpointsExists, err := p.client.GetEndpoints(namespace, string(backendRef.Name))
if err != nil {
return nil, fmt.Errorf("getting endpoints: %w", err)
}
@ -440,7 +453,7 @@ func buildHostRule(hostnames []gatev1.Hostname) (string, int) {
}
}
// buildRouterRule builds the route rule and computes its priority.
// buildMatchRule builds the route rule and computes its priority.
// The current priority computing is rather naive but aims to fulfill Conformance tests suite requirement.
// The priority is computed to match the following precedence order:
//
@ -451,60 +464,34 @@ func buildHostRule(hostnames []gatev1.Hostname) (string, int) {
// * Largest number of query param matches. (not implemented)
//
// In case of multiple matches for a route, the maximum priority among all matches is retain.
func buildRouterRule(hostnames []gatev1.Hostname, routeMatches []gatev1.HTTPRouteMatch) (string, int) {
var matchesRules []string
var maxPriority int
func buildMatchRule(hostnames []gatev1.Hostname, match gatev1.HTTPRouteMatch) (string, int) {
path := ptr.Deref(match.Path, gatev1.HTTPPathMatch{
Type: ptr.To(gatev1.PathMatchPathPrefix),
Value: ptr.To("/"),
})
for _, match := range routeMatches {
path := ptr.Deref(match.Path, gatev1.HTTPPathMatch{
Type: ptr.To(gatev1.PathMatchPathPrefix),
Value: ptr.To("/"),
})
var priority int
var matchRules []string
var priority int
var matchRules []string
pathRule, pathPriority := buildPathRule(path)
matchRules = append(matchRules, pathRule)
priority += pathPriority
pathRule, pathPriority := buildPathRule(path)
matchRules = append(matchRules, pathRule)
priority += pathPriority
headerRules, headersPriority := buildHeaderRules(match.Headers)
matchRules = append(matchRules, headerRules...)
priority += headersPriority
headerRules, headersPriority := buildHeaderRules(match.Headers)
matchRules = append(matchRules, headerRules...)
priority += headersPriority
matchesRules = append(matchesRules, strings.Join(matchRules, " && "))
if priority > maxPriority {
maxPriority = priority
}
}
matchRulesStr := strings.Join(matchRules, " && ")
hostRule, hostPriority := buildHostRule(hostnames)
matchesRulesStr := strings.Join(matchesRules, " || ")
if hostRule == "" && matchesRulesStr == "" {
return "PathPrefix(`/`)", 1
}
if hostRule != "" && matchesRulesStr == "" {
return hostRule, hostPriority
}
// Enforce that, at the same priority,
// the route with fewer matches (more specific) matches first.
maxPriority -= len(matchesRules) * 10
if maxPriority < 1 {
maxPriority = 1
}
if hostRule == "" {
return matchesRulesStr, maxPriority
return matchRulesStr, priority
}
// A route with a host should match over the same route with no host.
maxPriority += hostPriority
return hostRule + " && " + "(" + matchesRulesStr + ")", maxPriority
priority += hostPriority
return hostRule + " && " + matchRulesStr, priority
}
func buildPathRule(pathMatch gatev1.HTTPPathMatch) (string, int) {
@ -573,29 +560,41 @@ func createRequestHeaderModifier(filter *gatev1.HTTPHeaderFilter) *dynamic.Middl
}
}
func createRedirectMiddleware(filter *gatev1.HTTPRequestRedirectFilter) *dynamic.Middleware {
filterScheme := ptr.Deref(filter.Scheme, "${scheme}")
func createRequestRedirect(filter *gatev1.HTTPRequestRedirectFilter, pathMatch *gatev1.HTTPPathMatch) *dynamic.Middleware {
var hostname *string
if filter.Hostname != nil {
hostname = ptr.To(string(*filter.Hostname))
}
port := "${port}"
var port *string
filterScheme := ptr.Deref(filter.Scheme, "")
if filterScheme == "http" || filterScheme == "https" {
port = ""
port = ptr.To("")
}
if filter.Port != nil {
port = fmt.Sprintf(":%d", *filter.Port)
port = ptr.To(fmt.Sprintf("%d", *filter.Port))
}
statusCode := ptr.Deref(filter.StatusCode, http.StatusFound)
hostname := "${hostname}"
if filter.Hostname != nil && *filter.Hostname != "" {
hostname = string(*filter.Hostname)
var path *string
var pathPrefix *string
if filter.Path != nil {
switch filter.Path.Type {
case gatev1.FullPathHTTPPathModifier:
path = filter.Path.ReplaceFullPath
case gatev1.PrefixMatchHTTPPathModifier:
path = filter.Path.ReplacePrefixMatch
pathPrefix = pathMatch.Value
}
}
return &dynamic.Middleware{
RequestRedirect: &dynamic.RequestRedirect{
Regex: `^(?P<scheme>[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,
Scheme: filter.Scheme,
Hostname: hostname,
Port: port,
Path: path,
PathPrefix: pathPrefix,
StatusCode: ptr.Deref(filter.StatusCode, http.StatusFound),
},
}
}

View file

@ -69,10 +69,10 @@ func Test_buildHostRule(t *testing.T) {
}
}
func Test_buildRouterRule(t *testing.T) {
func Test_buildMatchRule(t *testing.T) {
testCases := []struct {
desc string
routeMatches []gatev1.HTTPRouteMatch
routeMatch gatev1.HTTPRouteMatch
hostnames []gatev1.Hostname
expectedRule string
expectedPriority int
@ -84,187 +84,112 @@ func Test_buildRouterRule(t *testing.T) {
expectedPriority: 1,
},
{
desc: "One Host rule without matches",
desc: "One Host rule without match",
hostnames: []gatev1.Hostname{"foo.com"},
expectedRule: "Host(`foo.com`)",
expectedPriority: 7,
expectedRule: "Host(`foo.com`) && PathPrefix(`/`)",
expectedPriority: 8,
},
{
desc: "One HTTPRouteMatch with nil HTTPHeaderMatch",
routeMatches: []gatev1.HTTPRouteMatch{
{
Path: ptr.To(gatev1.HTTPPathMatch{
Type: ptr.To(gatev1.PathMatchPathPrefix),
Value: ptr.To("/"),
}),
Headers: nil,
},
routeMatch: gatev1.HTTPRouteMatch{
Path: ptr.To(gatev1.HTTPPathMatch{
Type: ptr.To(gatev1.PathMatchPathPrefix),
Value: ptr.To("/"),
}),
Headers: nil,
},
expectedRule: "PathPrefix(`/`)",
expectedPriority: 1,
},
{
desc: "One HTTPRouteMatch with nil HTTPHeaderMatch Type",
routeMatches: []gatev1.HTTPRouteMatch{
{
Path: ptr.To(gatev1.HTTPPathMatch{
Type: ptr.To(gatev1.PathMatchPathPrefix),
Value: ptr.To("/"),
}),
Headers: []gatev1.HTTPHeaderMatch{
{Name: "foo", Value: "bar"},
},
routeMatch: gatev1.HTTPRouteMatch{
Path: ptr.To(gatev1.HTTPPathMatch{
Type: ptr.To(gatev1.PathMatchPathPrefix),
Value: ptr.To("/"),
}),
Headers: []gatev1.HTTPHeaderMatch{
{Name: "foo", Value: "bar"},
},
},
expectedRule: "PathPrefix(`/`) && Header(`foo`,`bar`)",
expectedPriority: 91,
expectedPriority: 101,
},
{
desc: "One HTTPRouteMatch with nil HTTPPathMatch",
routeMatches: []gatev1.HTTPRouteMatch{
{Path: nil},
},
desc: "One HTTPRouteMatch with nil HTTPPathMatch",
routeMatch: gatev1.HTTPRouteMatch{Path: nil},
expectedRule: "PathPrefix(`/`)",
expectedPriority: 1,
},
{
desc: "One HTTPRouteMatch with nil HTTPPathMatch Type",
routeMatches: []gatev1.HTTPRouteMatch{
{
Path: &gatev1.HTTPPathMatch{
Type: nil,
Value: ptr.To("/foo/"),
},
routeMatch: gatev1.HTTPRouteMatch{
Path: &gatev1.HTTPPathMatch{
Type: nil,
Value: ptr.To("/foo/"),
},
},
expectedRule: "(Path(`/foo`) || PathPrefix(`/foo/`))",
expectedPriority: 10490,
expectedPriority: 10500,
},
{
desc: "One HTTPRouteMatch with nil HTTPPathMatch Values",
routeMatches: []gatev1.HTTPRouteMatch{
{
Path: &gatev1.HTTPPathMatch{
Type: ptr.To(gatev1.PathMatchExact),
Value: nil,
},
routeMatch: gatev1.HTTPRouteMatch{
Path: &gatev1.HTTPPathMatch{
Type: ptr.To(gatev1.PathMatchExact),
Value: nil,
},
},
expectedRule: "Path(`/`)",
expectedPriority: 99990,
expectedPriority: 100000,
},
{
desc: "One Path in matches",
routeMatches: []gatev1.HTTPRouteMatch{
{
Path: &gatev1.HTTPPathMatch{
Type: ptr.To(gatev1.PathMatchExact),
Value: ptr.To("/foo/"),
},
desc: "One Path",
routeMatch: gatev1.HTTPRouteMatch{
Path: &gatev1.HTTPPathMatch{
Type: ptr.To(gatev1.PathMatchExact),
Value: ptr.To("/foo/"),
},
},
expectedRule: "Path(`/foo/`)",
expectedPriority: 99990,
expectedPriority: 100000,
},
{
desc: "One Path in matches and another empty",
routeMatches: []gatev1.HTTPRouteMatch{
{
Path: &gatev1.HTTPPathMatch{
Type: ptr.To(gatev1.PathMatchExact),
Value: ptr.To("/foo/"),
},
desc: "Path && Header",
routeMatch: gatev1.HTTPRouteMatch{
Path: &gatev1.HTTPPathMatch{
Type: ptr.To(gatev1.PathMatchExact),
Value: ptr.To("/foo/"),
},
{},
},
expectedRule: "Path(`/foo/`) || PathPrefix(`/`)",
expectedPriority: 99980,
},
{
desc: "Path OR Header rules",
routeMatches: []gatev1.HTTPRouteMatch{
{
Path: &gatev1.HTTPPathMatch{
Type: ptr.To(gatev1.PathMatchExact),
Value: ptr.To("/foo/"),
},
},
{
Headers: []gatev1.HTTPHeaderMatch{
{
Type: ptr.To(gatev1.HeaderMatchExact),
Name: "my-header",
Value: "foo",
},
},
},
},
expectedRule: "Path(`/foo/`) || PathPrefix(`/`) && Header(`my-header`,`foo`)",
expectedPriority: 99980,
},
{
desc: "Path && Header rules",
routeMatches: []gatev1.HTTPRouteMatch{
{
Path: &gatev1.HTTPPathMatch{
Type: ptr.To(gatev1.PathMatchExact),
Value: ptr.To("/foo/"),
},
Headers: []gatev1.HTTPHeaderMatch{
{
Type: ptr.To(gatev1.HeaderMatchExact),
Name: "my-header",
Value: "foo",
},
Headers: []gatev1.HTTPHeaderMatch{
{
Type: ptr.To(gatev1.HeaderMatchExact),
Name: "my-header",
Value: "foo",
},
},
},
expectedRule: "Path(`/foo/`) && Header(`my-header`,`foo`)",
expectedPriority: 100090,
expectedPriority: 100100,
},
{
desc: "Host && Path && Header rules",
desc: "Host && Path && Header",
hostnames: []gatev1.Hostname{"foo.com"},
routeMatches: []gatev1.HTTPRouteMatch{
{
Path: &gatev1.HTTPPathMatch{
Type: ptr.To(gatev1.PathMatchExact),
Value: ptr.To("/foo/"),
},
Headers: []gatev1.HTTPHeaderMatch{
{
Type: ptr.To(gatev1.HeaderMatchExact),
Name: "my-header",
Value: "foo",
},
routeMatch: gatev1.HTTPRouteMatch{
Path: &gatev1.HTTPPathMatch{
Type: ptr.To(gatev1.PathMatchExact),
Value: ptr.To("/foo/"),
},
Headers: []gatev1.HTTPHeaderMatch{
{
Type: ptr.To(gatev1.HeaderMatchExact),
Name: "my-header",
Value: "foo",
},
},
},
expectedRule: "Host(`foo.com`) && (Path(`/foo/`) && Header(`my-header`,`foo`))",
expectedPriority: 100097,
},
{
desc: "Host && (Path || Header) rules",
hostnames: []gatev1.Hostname{"foo.com"},
routeMatches: []gatev1.HTTPRouteMatch{
{
Path: &gatev1.HTTPPathMatch{
Type: ptr.To(gatev1.PathMatchExact),
Value: ptr.To("/foo/"),
},
},
{
Headers: []gatev1.HTTPHeaderMatch{
{
Type: ptr.To(gatev1.HeaderMatchExact),
Name: "my-header",
Value: "foo",
},
},
},
},
expectedRule: "Host(`foo.com`) && (Path(`/foo/`) || PathPrefix(`/`) && Header(`my-header`,`foo`))",
expectedPriority: 99987,
expectedRule: "Host(`foo.com`) && Path(`/foo/`) && Header(`my-header`,`foo`)",
expectedPriority: 100107,
},
}
@ -272,7 +197,7 @@ func Test_buildRouterRule(t *testing.T) {
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
rule, priority := buildRouterRule(test.hostnames, test.routeMatches)
rule, priority := buildMatchRule(test.hostnames, test.routeMatch)
assert.Equal(t, test.expectedRule, rule)
assert.Equal(t, test.expectedPriority, priority)
})

View file

@ -68,6 +68,7 @@ type Provider struct {
lastConfiguration safe.Safe
routerTransform k8s.RouterTransform
client *clientWrapper
}
// Entrypoint defines the available entry points.
@ -194,6 +195,14 @@ func (p *Provider) newK8sClient(ctx context.Context) (*clientWrapper, error) {
// Init the provider.
func (p *Provider) Init() error {
logger := log.With().Str(logs.ProviderName, providerName).Logger()
var err error
p.client, err = p.newK8sClient(logger.WithContext(context.Background()))
if err != nil {
return fmt.Errorf("creating k8s client: %w", err)
}
return nil
}
@ -202,14 +211,9 @@ func (p *Provider) Provide(configurationChan chan<- dynamic.Message, pool *safe.
logger := log.With().Str(logs.ProviderName, providerName).Logger()
ctxLog := logger.WithContext(context.Background())
k8sClient, err := p.newK8sClient(ctxLog)
if err != nil {
return err
}
pool.GoCtx(func(ctxPool context.Context) {
operation := func() error {
eventsChan, err := k8sClient.WatchAll(p.Namespaces, ctxPool.Done())
eventsChan, err := p.client.WatchAll(p.Namespaces, ctxPool.Done())
if err != nil {
logger.Error().Err(err).Msg("Error watching kubernetes events")
timer := time.NewTimer(1 * time.Second)
@ -235,7 +239,7 @@ func (p *Provider) Provide(configurationChan chan<- dynamic.Message, pool *safe.
// Note that event is the *first* event that came in during this throttling interval -- if we're hitting our throttle, we may have dropped events.
// This is fine, because we don't treat different event types differently.
// But if we do in the future, we'll need to track more information about the dropped events.
conf := p.loadConfigurationFromGateways(ctxLog, k8sClient)
conf := p.loadConfigurationFromGateways(ctxLog)
confHash, err := hashstructure.Hash(conf, nil)
switch {
@ -272,7 +276,7 @@ func (p *Provider) Provide(configurationChan chan<- dynamic.Message, pool *safe.
}
// TODO Handle errors and update resources statuses (gatewayClass, gateway).
func (p *Provider) loadConfigurationFromGateways(ctx context.Context, client Client) *dynamic.Configuration {
func (p *Provider) loadConfigurationFromGateways(ctx context.Context) *dynamic.Configuration {
conf := &dynamic.Configuration{
HTTP: &dynamic.HTTPConfiguration{
Routers: map[string]*dynamic.Router{},
@ -293,13 +297,13 @@ func (p *Provider) loadConfigurationFromGateways(ctx context.Context, client Cli
TLS: &dynamic.TLSConfiguration{},
}
addresses, err := p.gatewayAddresses(client)
addresses, err := p.gatewayAddresses()
if err != nil {
log.Ctx(ctx).Error().Err(err).Msg("Unable to get Gateway status addresses")
return nil
}
gatewayClasses, err := client.ListGatewayClasses()
gatewayClasses, err := p.client.ListGatewayClasses()
if err != nil {
log.Ctx(ctx).Error().Err(err).Msg("Unable to list GatewayClasses")
return nil
@ -313,7 +317,7 @@ func (p *Provider) loadConfigurationFromGateways(ctx context.Context, client Cli
gatewayClassNames[gatewayClass.Name] = struct{}{}
err := client.UpdateGatewayClassStatus(gatewayClass, metav1.Condition{
err := p.client.UpdateGatewayClassStatus(gatewayClass, metav1.Condition{
Type: string(gatev1.GatewayClassConditionStatusAccepted),
Status: metav1.ConditionTrue,
ObservedGeneration: gatewayClass.Generation,
@ -330,7 +334,7 @@ func (p *Provider) loadConfigurationFromGateways(ctx context.Context, client Cli
}
}
gateways := client.ListGateways()
gateways := p.client.ListGateways()
var gatewayListeners []gatewayListener
for _, gateway := range gateways {
@ -343,14 +347,14 @@ func (p *Provider) loadConfigurationFromGateways(ctx context.Context, client Cli
continue
}
gatewayListeners = append(gatewayListeners, p.loadGatewayListeners(logger.WithContext(ctx), client, gateway, conf)...)
gatewayListeners = append(gatewayListeners, p.loadGatewayListeners(logger.WithContext(ctx), gateway, conf)...)
}
p.loadHTTPRoutes(ctx, client, gatewayListeners, conf)
p.loadHTTPRoutes(ctx, gatewayListeners, conf)
if p.ExperimentalChannel {
p.loadTCPRoutes(ctx, client, gatewayListeners, conf)
p.loadTLSRoutes(ctx, client, gatewayListeners, conf)
p.loadTCPRoutes(ctx, gatewayListeners, conf)
p.loadTLSRoutes(ctx, gatewayListeners, conf)
}
for _, gateway := range gateways {
@ -367,7 +371,7 @@ func (p *Provider) loadConfigurationFromGateways(ctx context.Context, client Cli
}
gatewayStatus, errG := p.makeGatewayStatus(gateway, listeners, addresses)
if err = client.UpdateGatewayStatus(gateway, gatewayStatus); err != nil {
if err = p.client.UpdateGatewayStatus(gateway, gatewayStatus); err != nil {
logger.Error().
Err(err).
Msg("Unable to update Gateway status")
@ -382,7 +386,7 @@ func (p *Provider) loadConfigurationFromGateways(ctx context.Context, client Cli
return conf
}
func (p *Provider) loadGatewayListeners(ctx context.Context, client Client, gateway *gatev1.Gateway, conf *dynamic.Configuration) []gatewayListener {
func (p *Provider) loadGatewayListeners(ctx context.Context, gateway *gatev1.Gateway, conf *dynamic.Configuration) []gatewayListener {
tlsConfigs := make(map[string]*tls.CertAndStores)
allocatedListeners := make(map[string]struct{})
gatewayListeners := make([]gatewayListener, len(gateway.Spec.Listeners))
@ -420,7 +424,7 @@ func (p *Provider) loadGatewayListeners(ctx context.Context, client Client, gate
gatewayListeners[i].EPName = ep
allowedRoutes := ptr.Deref(listener.AllowedRoutes, gatev1.AllowedRoutes{Namespaces: &gatev1.RouteNamespaces{From: ptr.To(gatev1.NamespacesFromSame)}})
gatewayListeners[i].AllowedNamespaces, err = allowedNamespaces(client, gateway.Namespace, allowedRoutes.Namespaces)
gatewayListeners[i].AllowedNamespaces, err = p.allowedNamespaces(gateway.Namespace, allowedRoutes.Namespaces)
if err != nil {
// update "ResolvedRefs" status true with "InvalidRoutesRef" reason
gatewayListeners[i].Status.Conditions = append(gatewayListeners[i].Status.Conditions, metav1.Condition{
@ -565,7 +569,7 @@ func (p *Provider) loadGatewayListeners(ctx context.Context, client Client, gate
certificateNamespace = string(*certificateRef.Namespace)
}
if err := isReferenceGranted(client, groupGateway, kindGateway, gateway.Namespace, groupCore, "Secret", string(certificateRef.Name), certificateNamespace); err != nil {
if err := p.isReferenceGranted(groupGateway, kindGateway, gateway.Namespace, groupCore, "Secret", string(certificateRef.Name), certificateNamespace); err != nil {
gatewayListeners[i].Status.Conditions = append(gatewayListeners[i].Status.Conditions, metav1.Condition{
Type: string(gatev1.ListenerConditionResolvedRefs),
Status: metav1.ConditionFalse,
@ -580,7 +584,7 @@ func (p *Provider) loadGatewayListeners(ctx context.Context, client Client, gate
configKey := certificateNamespace + "/" + string(certificateRef.Name)
if _, tlsExists := tlsConfigs[configKey]; !tlsExists {
tlsConf, err := getTLS(client, certificateRef.Name, certificateNamespace)
tlsConf, err := p.getTLS(certificateRef.Name, certificateNamespace)
if err != nil {
// update "ResolvedRefs" status false with "InvalidCertificateRef" reason
// update "Programmed" status false with "Invalid" reason
@ -703,7 +707,7 @@ func (p *Provider) makeGatewayStatus(gateway *gatev1.Gateway, listeners []gatewa
return gatewayStatus, nil
}
func (p *Provider) gatewayAddresses(client Client) ([]gatev1.GatewayStatusAddress, error) {
func (p *Provider) gatewayAddresses() ([]gatev1.GatewayStatusAddress, error) {
if p.StatusAddress == nil {
return nil, nil
}
@ -724,7 +728,7 @@ func (p *Provider) gatewayAddresses(client Client) ([]gatev1.GatewayStatusAddres
svcRef := p.StatusAddress.Service
if svcRef.Name != "" && svcRef.Namespace != "" {
svc, exists, err := client.GetService(svcRef.Namespace, svcRef.Name)
svc, exists, err := p.client.GetService(svcRef.Namespace, svcRef.Name)
if err != nil {
return nil, fmt.Errorf("unable to get service: %w", err)
}
@ -772,6 +776,71 @@ func (p *Provider) entryPointName(port gatev1.PortNumber, protocol gatev1.Protoc
return "", fmt.Errorf("no matching entryPoint for port %d and protocol %q", port, protocol)
}
func (p *Provider) isReferenceGranted(fromGroup, fromKind, fromNamespace, toGroup, toKind, toName, toNamespace string) error {
if toNamespace == fromNamespace {
return nil
}
refGrants, err := p.client.ListReferenceGrants(toNamespace)
if err != nil {
return fmt.Errorf("listing ReferenceGrant: %w", err)
}
refGrants = filterReferenceGrantsFrom(refGrants, fromGroup, fromKind, fromNamespace)
refGrants = filterReferenceGrantsTo(refGrants, toGroup, toKind, toName)
if len(refGrants) == 0 {
return errors.New("missing ReferenceGrant")
}
return nil
}
func (p *Provider) getTLS(secretName gatev1.ObjectName, namespace string) (*tls.CertAndStores, error) {
secret, exists, err := p.client.GetSecret(namespace, string(secretName))
if err != nil {
return nil, fmt.Errorf("failed to fetch secret %s/%s: %w", namespace, secretName, err)
}
if !exists {
return nil, fmt.Errorf("secret %s/%s does not exist", namespace, secretName)
}
cert, key, err := getCertificateBlocks(secret, namespace, string(secretName))
if err != nil {
return nil, err
}
return &tls.CertAndStores{
Certificate: tls.Certificate{
CertFile: types.FileOrContent(cert),
KeyFile: types.FileOrContent(key),
},
}, nil
}
func (p *Provider) allowedNamespaces(gatewayNamespace string, routeNamespaces *gatev1.RouteNamespaces) ([]string, error) {
if routeNamespaces == nil || routeNamespaces.From == nil {
return []string{gatewayNamespace}, nil
}
switch *routeNamespaces.From {
case gatev1.NamespacesFromAll:
return []string{metav1.NamespaceAll}, nil
case gatev1.NamespacesFromSame:
return []string{gatewayNamespace}, nil
case gatev1.NamespacesFromSelector:
selector, err := metav1.LabelSelectorAsSelector(routeNamespaces.Selector)
if err != nil {
return nil, fmt.Errorf("malformed selector: %w", err)
}
return p.client.ListNamespaces(selector)
}
return nil, fmt.Errorf("unsupported RouteSelectType: %q", *routeNamespaces.From)
}
func supportedRouteKinds(protocol gatev1.ProtocolType, experimentalChannel bool) ([]gatev1.RouteGroupKind, []metav1.Condition) {
group := gatev1.Group(gatev1.GroupName)
@ -856,30 +925,6 @@ func allowedRouteKinds(gateway *gatev1.Gateway, listener gatev1.Listener, suppor
return routeKinds, conditions
}
func allowedNamespaces(client Client, gatewayNamespace string, routeNamespaces *gatev1.RouteNamespaces) ([]string, error) {
if routeNamespaces == nil || routeNamespaces.From == nil {
return []string{gatewayNamespace}, nil
}
switch *routeNamespaces.From {
case gatev1.NamespacesFromAll:
return []string{metav1.NamespaceAll}, nil
case gatev1.NamespacesFromSame:
return []string{gatewayNamespace}, nil
case gatev1.NamespacesFromSelector:
selector, err := metav1.LabelSelectorAsSelector(routeNamespaces.Selector)
if err != nil {
return nil, fmt.Errorf("malformed selector: %w", err)
}
return client.ListNamespaces(selector)
}
return nil, fmt.Errorf("unsupported RouteSelectType: %q", *routeNamespaces.From)
}
func findMatchingHostnames(listenerHostname *gatev1.Hostname, routeHostnames []gatev1.Hostname) ([]gatev1.Hostname, bool) {
if listenerHostname == nil {
return routeHostnames, true
@ -971,7 +1016,7 @@ func matchListener(listener gatewayListener, routeNamespace string, parentRef ga
return true
}
func makeRouterKey(rule, name string) string {
func makeRouterName(rule, name string) string {
h := sha256.New()
// As explained in https://pkg.go.dev/hash#Hash,
@ -981,36 +1026,6 @@ func makeRouterKey(rule, name string) string {
return fmt.Sprintf("%s-%.10x", name, h.Sum(nil))
}
func makeID(namespace, name string) string {
if namespace == "" {
return name
}
return namespace + "-" + name
}
func getTLS(client Client, secretName gatev1.ObjectName, namespace string) (*tls.CertAndStores, error) {
secret, exists, err := client.GetSecret(namespace, string(secretName))
if err != nil {
return nil, fmt.Errorf("failed to fetch secret %s/%s: %w", namespace, secretName, err)
}
if !exists {
return nil, fmt.Errorf("secret %s/%s does not exist", namespace, secretName)
}
cert, key, err := getCertificateBlocks(secret, namespace, string(secretName))
if err != nil {
return nil, err
}
return &tls.CertAndStores{
Certificate: tls.Certificate{
CertFile: types.FileOrContent(cert),
KeyFile: types.FileOrContent(key),
},
}, nil
}
func getTLSConfig(tlsConfigs map[string]*tls.CertAndStores) []*tls.CertAndStores {
var secretNames []string
for secretName := range tlsConfigs {
@ -1114,25 +1129,6 @@ func makeListenerKey(l gatev1.Listener) string {
return fmt.Sprintf("%s|%s|%d", l.Protocol, hostname, l.Port)
}
func isReferenceGranted(client Client, fromGroup, fromKind, fromNamespace, toGroup, toKind, toName, toNamespace string) error {
if toNamespace == fromNamespace {
return nil
}
refGrants, err := client.ListReferenceGrants(toNamespace)
if err != nil {
return fmt.Errorf("listing ReferenceGrant: %w", err)
}
refGrants = filterReferenceGrantsFrom(refGrants, fromGroup, fromKind, fromNamespace)
refGrants = filterReferenceGrantsTo(refGrants, toGroup, toKind, toName)
if len(refGrants) == 0 {
return errors.New("missing ReferenceGrant")
}
return nil
}
func filterReferenceGrantsFrom(referenceGrants []*gatev1beta1.ReferenceGrant, group, kind, namespace string) []*gatev1beta1.ReferenceGrant {
var matchingReferenceGrants []*gatev1beta1.ReferenceGrant
for _, referenceGrant := range referenceGrants {

File diff suppressed because it is too large Load diff

View file

@ -18,9 +18,9 @@ import (
gatev1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2"
)
func (p *Provider) loadTCPRoutes(ctx context.Context, client Client, gatewayListeners []gatewayListener, conf *dynamic.Configuration) {
func (p *Provider) loadTCPRoutes(ctx context.Context, gatewayListeners []gatewayListener, conf *dynamic.Configuration) {
logger := log.Ctx(ctx)
routes, err := client.ListTCPRoutes()
routes, err := p.client.ListTCPRoutes()
if err != nil {
logger.Error().Err(err).Msgf("Unable to list TCPRoutes")
return
@ -64,7 +64,7 @@ func (p *Provider) loadTCPRoutes(ctx context.Context, client Client, gatewayList
}
}
routeConf, resolveRefCondition := p.loadTCPRoute(client, listener, route)
routeConf, resolveRefCondition := p.loadTCPRoute(listener, route)
if accepted && listener.Attached {
mergeTCPConfiguration(routeConf, conf)
}
@ -79,7 +79,7 @@ func (p *Provider) loadTCPRoutes(ctx context.Context, client Client, gatewayList
Parents: parentStatuses,
},
}
if err := client.UpdateTCPRouteStatus(ctx, ktypes.NamespacedName{Namespace: route.Namespace, Name: route.Name}, routeStatus); err != nil {
if err := p.client.UpdateTCPRouteStatus(ctx, ktypes.NamespacedName{Namespace: route.Namespace, Name: route.Name}, routeStatus); err != nil {
logger.Error().
Err(err).
Msg("Unable to update TCPRoute status")
@ -87,8 +87,8 @@ func (p *Provider) loadTCPRoutes(ctx context.Context, client Client, gatewayList
}
}
func (p *Provider) loadTCPRoute(client Client, listener gatewayListener, route *gatev1alpha2.TCPRoute) (*dynamic.Configuration, metav1.Condition) {
routeConf := &dynamic.Configuration{
func (p *Provider) loadTCPRoute(listener gatewayListener, route *gatev1alpha2.TCPRoute) (*dynamic.Configuration, metav1.Condition) {
conf := &dynamic.Configuration{
TCP: &dynamic.TCPConfiguration{
Routers: make(map[string]*dynamic.TCPRouter),
Middlewares: make(map[string]*dynamic.TCPMiddleware),
@ -97,7 +97,7 @@ func (p *Provider) loadTCPRoute(client Client, listener gatewayListener, route *
},
}
routeCondition := metav1.Condition{
condition := metav1.Condition{
Type: string(gatev1.RouteConditionResolvedRefs),
Status: metav1.ConditionTrue,
ObservedGeneration: route.Generation,
@ -119,8 +119,7 @@ func (p *Provider) loadTCPRoute(client Client, listener gatewayListener, route *
}
// Adding the gateway desc and the entryPoint desc prevents overlapping of routers build from the same routes.
routerName := route.Name + "-" + listener.GWName + "-" + listener.EPName
routerKey := provider.Normalize(makeRouterKey("", makeID(route.Namespace, routerName)))
routerName := provider.Normalize(route.Namespace + "-" + route.Name + "-" + listener.GWName + "-" + listener.EPName)
var ruleServiceNames []string
for i, rule := range route.Spec.Rules {
@ -130,9 +129,9 @@ func (p *Provider) loadTCPRoute(client Client, listener gatewayListener, route *
continue
}
wrrService, subServices, err := loadTCPServices(client, route.Namespace, rule.BackendRefs)
wrrService, subServices, err := p.loadTCPServices(route.Namespace, rule.BackendRefs)
if err != nil {
return routeConf, metav1.Condition{
return conf, metav1.Condition{
Type: string(gatev1.RouteConditionResolvedRefs),
Status: metav1.ConditionFalse,
ObservedGeneration: route.Generation,
@ -143,22 +142,22 @@ func (p *Provider) loadTCPRoute(client Client, listener gatewayListener, route *
}
for svcName, svc := range subServices {
routeConf.TCP.Services[svcName] = svc
conf.TCP.Services[svcName] = svc
}
serviceName := fmt.Sprintf("%s-wrr-%d", routerKey, i)
routeConf.TCP.Services[serviceName] = wrrService
serviceName := fmt.Sprintf("%s-wrr-%d", routerName, i)
conf.TCP.Services[serviceName] = wrrService
ruleServiceNames = append(ruleServiceNames, serviceName)
}
if len(ruleServiceNames) == 1 {
router.Service = ruleServiceNames[0]
routeConf.TCP.Routers[routerKey] = &router
return routeConf, routeCondition
conf.TCP.Routers[routerName] = &router
return conf, condition
}
routeServiceKey := routerKey + "-wrr"
routeServiceKey := routerName + "-wrr"
routeService := &dynamic.TCPService{Weighted: &dynamic.TCPWeightedRoundRobin{}}
for _, name := range ruleServiceNames {
@ -168,16 +167,16 @@ func (p *Provider) loadTCPRoute(client Client, listener gatewayListener, route *
routeService.Weighted.Services = append(routeService.Weighted.Services, service)
}
routeConf.TCP.Services[routeServiceKey] = routeService
conf.TCP.Services[routeServiceKey] = routeService
router.Service = routeServiceKey
routeConf.TCP.Routers[routerKey] = &router
conf.TCP.Routers[routerName] = &router
return routeConf, routeCondition
return conf, condition
}
// loadTCPServices is generating a WRR service, even when there is only one target.
func loadTCPServices(client Client, namespace string, backendRefs []gatev1.BackendRef) (*dynamic.TCPService, map[string]*dynamic.TCPService, error) {
func (p *Provider) loadTCPServices(namespace string, backendRefs []gatev1.BackendRef) (*dynamic.TCPService, map[string]*dynamic.TCPService, error) {
services := map[string]*dynamic.TCPService{}
wrrSvc := &dynamic.TCPService{
@ -211,7 +210,7 @@ func loadTCPServices(client Client, namespace string, backendRefs []gatev1.Backe
LoadBalancer: &dynamic.TCPServersLoadBalancer{},
}
service, exists, err := client.GetService(namespace, string(backendRef.Name))
service, exists, err := p.client.GetService(namespace, string(backendRef.Name))
if err != nil {
return nil, nil, err
}
@ -247,7 +246,7 @@ func loadTCPServices(client Client, namespace string, backendRefs []gatev1.Backe
return nil, nil, errors.New("service port not found")
}
endpoints, endpointsExists, endpointsErr := client.GetEndpoints(namespace, string(backendRef.Name))
endpoints, endpointsExists, endpointsErr := p.client.GetEndpoints(namespace, string(backendRef.Name))
if endpointsErr != nil {
return nil, nil, endpointsErr
}
@ -282,7 +281,7 @@ func loadTCPServices(client Client, namespace string, backendRefs []gatev1.Backe
}
}
serviceName := provider.Normalize(makeID(service.Namespace, service.Name) + "-" + portStr)
serviceName := provider.Normalize(service.Namespace + "-" + service.Name + "-" + portStr)
services[serviceName] = &svc
wrrSvc.Weighted.Services = append(wrrSvc.Weighted.Services, dynamic.TCPWRRService{Name: serviceName, Weight: &weight})

View file

@ -15,9 +15,9 @@ import (
gatev1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2"
)
func (p *Provider) loadTLSRoutes(ctx context.Context, client Client, gatewayListeners []gatewayListener, conf *dynamic.Configuration) {
func (p *Provider) loadTLSRoutes(ctx context.Context, gatewayListeners []gatewayListener, conf *dynamic.Configuration) {
logger := log.Ctx(ctx)
routes, err := client.ListTLSRoutes()
routes, err := p.client.ListTLSRoutes()
if err != nil {
logger.Error().Err(err).Msgf("Unable to list TLSRoute")
return
@ -66,7 +66,7 @@ func (p *Provider) loadTLSRoutes(ctx context.Context, client Client, gatewayList
}
}
routeConf, resolveRefCondition := p.loadTLSRoute(client, listener, route, hostnames)
routeConf, resolveRefCondition := p.loadTLSRoute(listener, route, hostnames)
if accepted && listener.Attached {
mergeTCPConfiguration(routeConf, conf)
}
@ -81,7 +81,7 @@ func (p *Provider) loadTLSRoutes(ctx context.Context, client Client, gatewayList
Parents: parentStatuses,
},
}
if err := client.UpdateTLSRouteStatus(ctx, ktypes.NamespacedName{Namespace: route.Namespace, Name: route.Name}, routeStatus); err != nil {
if err := p.client.UpdateTLSRouteStatus(ctx, ktypes.NamespacedName{Namespace: route.Namespace, Name: route.Name}, routeStatus); err != nil {
logger.Error().
Err(err).
Msg("Unable to update TLSRoute status")
@ -89,8 +89,8 @@ func (p *Provider) loadTLSRoutes(ctx context.Context, client Client, gatewayList
}
}
func (p *Provider) loadTLSRoute(client Client, listener gatewayListener, route *gatev1alpha2.TLSRoute, hostnames []gatev1.Hostname) (*dynamic.Configuration, metav1.Condition) {
routeConf := &dynamic.Configuration{
func (p *Provider) loadTLSRoute(listener gatewayListener, route *gatev1alpha2.TLSRoute, hostnames []gatev1.Hostname) (*dynamic.Configuration, metav1.Condition) {
conf := &dynamic.Configuration{
TCP: &dynamic.TCPConfiguration{
Routers: make(map[string]*dynamic.TCPRouter),
Middlewares: make(map[string]*dynamic.TCPMiddleware),
@ -99,7 +99,7 @@ func (p *Provider) loadTLSRoute(client Client, listener gatewayListener, route *
},
}
routeCondition := metav1.Condition{
condition := metav1.Condition{
Type: string(gatev1.RouteConditionResolvedRefs),
Status: metav1.ConditionTrue,
ObservedGeneration: route.Generation,
@ -117,8 +117,8 @@ func (p *Provider) loadTLSRoute(client Client, listener gatewayListener, route *
}
// Adding the gateway desc and the entryPoint desc prevents overlapping of routers build from the same routes.
routerName := route.Name + "-" + listener.GWName + "-" + listener.EPName
routerKey := provider.Normalize(makeRouterKey(router.Rule, makeID(route.Namespace, routerName)))
routeKey := provider.Normalize(route.Namespace + "-" + route.Name + "-" + listener.GWName + "-" + listener.EPName)
routerName := makeRouterName(router.Rule, routeKey)
var ruleServiceNames []string
for i, routeRule := range route.Spec.Rules {
@ -128,10 +128,10 @@ func (p *Provider) loadTLSRoute(client Client, listener gatewayListener, route *
continue
}
wrrService, subServices, err := loadTCPServices(client, route.Namespace, routeRule.BackendRefs)
wrrService, subServices, err := p.loadTCPServices(route.Namespace, routeRule.BackendRefs)
if err != nil {
// update "ResolvedRefs" status true with "InvalidBackendRefs" reason
routeCondition = metav1.Condition{
condition = metav1.Condition{
Type: string(gatev1.RouteConditionResolvedRefs),
Status: metav1.ConditionFalse,
ObservedGeneration: route.Generation,
@ -143,23 +143,23 @@ func (p *Provider) loadTLSRoute(client Client, listener gatewayListener, route *
}
for svcName, svc := range subServices {
routeConf.TCP.Services[svcName] = svc
conf.TCP.Services[svcName] = svc
}
serviceName := fmt.Sprintf("%s-wrr-%d", routerKey, i)
routeConf.TCP.Services[serviceName] = wrrService
serviceName := fmt.Sprintf("%s-wrr-%d", routerName, i)
conf.TCP.Services[serviceName] = wrrService
ruleServiceNames = append(ruleServiceNames, serviceName)
}
if len(ruleServiceNames) == 1 {
router.Service = ruleServiceNames[0]
routeConf.TCP.Routers[routerKey] = &router
conf.TCP.Routers[routerName] = &router
return routeConf, routeCondition
return conf, condition
}
routeServiceKey := routerKey + "-wrr"
routeServiceKey := routerName + "-wrr"
routeService := &dynamic.TCPService{Weighted: &dynamic.TCPWeightedRoundRobin{}}
for _, name := range ruleServiceNames {
@ -169,12 +169,12 @@ func (p *Provider) loadTLSRoute(client Client, listener gatewayListener, route *
routeService.Weighted.Services = append(routeService.Weighted.Services, service)
}
routeConf.TCP.Services[routeServiceKey] = routeService
conf.TCP.Services[routeServiceKey] = routeService
router.Service = routeServiceKey
routeConf.TCP.Routers[routerKey] = &router
conf.TCP.Routers[routerName] = &router
return routeConf, routeCondition
return conf, condition
}
func hostSNIRule(hostnames []gatev1.Hostname) string {