Add Gateway API HTTPRoute CORS filter
Some checks failed
CodeQL / Analyze (push) Has been cancelled
Build and Publish Documentation / Doc Process (push) Has been cancelled
Build experimental image on branch / build-webui (push) Has been cancelled
Build experimental image on branch / Build experimental image on branch (push) Has been cancelled
Test Integration / build (push) Has been cancelled
Test Unit / Unit tests (push) Has been cancelled
Test Unit / test-ui-unit (push) Has been cancelled
Test Integration / test-integration (0, 12) (push) Has been cancelled
Test Integration / test-integration (1, 12) (push) Has been cancelled
Test Integration / test-integration (10, 12) (push) Has been cancelled
Test Integration / test-integration (11, 12) (push) Has been cancelled
Test Integration / test-integration (2, 12) (push) Has been cancelled
Test Integration / test-integration (3, 12) (push) Has been cancelled
Test Integration / test-integration (4, 12) (push) Has been cancelled
Test Integration / test-integration (5, 12) (push) Has been cancelled
Test Integration / test-integration (6, 12) (push) Has been cancelled
Test Integration / test-integration (7, 12) (push) Has been cancelled
Test Integration / test-integration (8, 12) (push) Has been cancelled
Test Integration / test-integration (9, 12) (push) Has been cancelled
Test Integration / merge-test-results (push) Has been cancelled

This commit is contained in:
Anatole Lucet 2026-07-07 09:08:05 +02:00 committed by GitHub
parent 1fbe532c64
commit db21cbae1a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 188 additions and 2 deletions

View file

@ -320,6 +320,17 @@ There are three types of filters:
- **Extended:** Optional filters for Gateway controllers, such as `ResponseHeaderModifier` and `RequestMirror`.
- **ExtensionRef:** Additional filters provided by the Gateway controller. In Traefik, these are the [HTTP middlewares](../http/middlewares/overview.md) supported through the [Middleware CRD](../kubernetes/crd/http/middleware.md).
!!! info "Supported Filter Types"
Traefik supports the following route-level filter types:
- `RequestHeaderModifier`: Add, set, or remove HTTP request headers before forwarding to the backend.
- `ResponseHeaderModifier`: Add, set, or remove HTTP response headers.
- `RequestRedirect`: Redirect the request to a different URL.
- `URLRewrite`: Rewrite the request URL path and/or hostname.
- `CORS`: Configure Cross-Origin Resource Sharing (CORS) response headers.
- `ExtensionRef`: Reference a Traefik [Middleware](../kubernetes/crd/http/middleware.md) resource.
!!! info "ExtensionRef Filters"
To use Traefik middlewares as `ExtensionRef` filters, the Kubernetes CRD provider must be enabled in the static configuration, as detailed in the [documentation](../../install-configuration/providers/kubernetes/kubernetes-crd.md).
@ -442,6 +453,7 @@ This allows request modifications to be applied to specific backends, enabling t
- `ResponseHeaderModifier`: Add, set, or remove HTTP response headers.
- `RequestRedirect`: Redirect the request to a different URL.
- `URLRewrite`: Rewrite the request URL path and/or hostname.
- `CORS`: Configure Cross-Origin Resource Sharing (CORS) response headers.
- `ExtensionRef`: Reference a Traefik [Middleware](../kubernetes/crd/http/middleware.md) resource.
!!! info "Middlewares Execution Order"

View file

@ -30,7 +30,7 @@ profiles:
result: success
statistics:
Failed: 0
Passed: 27
Passed: 29
Skipped: 0
supportedFeatures:
- BackendTLSPolicy
@ -42,6 +42,7 @@ profiles:
- HTTPRouteBackendProtocolH2C
- HTTPRouteBackendProtocolWebSocket
- HTTPRouteBackendRequestHeaderModification
- HTTPRouteCORS
- HTTPRouteDestinationPortMatching
- HTTPRouteHostRewrite
- HTTPRouteMethodMatching
@ -63,7 +64,6 @@ profiles:
- GatewayInfrastructurePropagation
- GatewayStaticAddresses
- HTTPRouteBackendTimeout
- HTTPRouteCORS
- HTTPRouteRequestMirror
- HTTPRouteRequestMultipleMirrors
- HTTPRouteRequestPercentageMirror

View file

@ -61,6 +61,7 @@ func extendedHTTPRouteFeatures() sets.Set[features.Feature] {
features.HTTPRouteBackendRequestHeaderModificationFeature,
features.HTTPRouteNamedRouteRule,
features.HTTPRouteParentRefPortFeature,
features.HTTPRouteCORS,
)
}

View file

@ -407,6 +407,12 @@ func (p *Provider) loadMiddlewares(conf *dynamic.Configuration, namespace, paren
middleware,
})
case gatev1.HTTPRouteFilterCORS:
middlewares = append(middlewares, namedMiddleware{
name,
createCORS(filter.CORS),
})
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
@ -962,6 +968,52 @@ func createURLRewrite(filter *gatev1.HTTPURLRewriteFilter, pathMatch gatev1.HTTP
}, nil
}
func createCORS(filter *gatev1.HTTPCORSFilter) *dynamic.Middleware {
var allowOrigins, allowOriginsRegex []string
for _, origin := range filter.AllowOrigins {
if prefix, suffix, found := strings.Cut(string(origin), "*"); found {
switch {
case prefix != "" || suffix != "":
allowOriginsRegex = append(allowOriginsRegex, "^"+regexp.QuoteMeta(prefix)+`.*`+regexp.QuoteMeta(suffix)+"$")
default:
// Convert to regex so the middleware echoes the specific request origin rather than the literal "*".
// The Gateway API conformance tests require the specific origin, even when AllowCredentials is false/unset.
allowOriginsRegex = append(allowOriginsRegex, ".*")
}
continue
}
allowOrigins = append(allowOrigins, string(origin))
}
var allowMethods []string
for _, m := range filter.AllowMethods {
allowMethods = append(allowMethods, string(m))
}
var allowHeaders []string
for _, h := range filter.AllowHeaders {
allowHeaders = append(allowHeaders, string(h))
}
var exposeHeaders []string
for _, h := range filter.ExposeHeaders {
exposeHeaders = append(exposeHeaders, string(h))
}
return &dynamic.Middleware{
Headers: &dynamic.Headers{
AccessControlAllowCredentials: ptr.Deref(filter.AllowCredentials, false),
AccessControlAllowOriginList: allowOrigins,
AccessControlAllowOriginListRegex: allowOriginsRegex,
AccessControlAllowMethods: allowMethods,
AccessControlAllowHeaders: allowHeaders,
AccessControlExposeHeaders: exposeHeaders,
AccessControlMaxAge: ptr.To(int64(filter.MaxAge)),
},
}
}
func getHTTPServiceProtocol(portSpec corev1.ServicePort) (string, error) {
if portSpec.Protocol != corev1.ProtocolTCP {
return "", errors.New("only TCP protocol is supported")

View file

@ -4,6 +4,7 @@ import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/traefik/traefik/v3/pkg/config/dynamic"
"k8s.io/utils/ptr"
gatev1 "sigs.k8s.io/gateway-api/apis/v1"
)
@ -203,3 +204,123 @@ func Test_buildMatchRule(t *testing.T) {
})
}
}
func Test_createCORS(t *testing.T) {
testCases := []struct {
desc string
filter *gatev1.HTTPCORSFilter
expected *dynamic.Middleware
}{
{
desc: "Empty filter",
filter: &gatev1.HTTPCORSFilter{},
expected: &dynamic.Middleware{
Headers: &dynamic.Headers{
AccessControlMaxAge: ptr.To(int64(0)),
},
},
},
{
desc: "Plain origins",
filter: &gatev1.HTTPCORSFilter{
AllowOrigins: []gatev1.CORSOrigin{"https://foo.example.com", "https://bar.example.com"},
},
expected: &dynamic.Middleware{
Headers: &dynamic.Headers{
AccessControlAllowOriginList: []string{"https://foo.example.com", "https://bar.example.com"},
AccessControlMaxAge: ptr.To(int64(0)),
},
},
},
{
desc: "Wildcard origin becomes catch-all regex",
filter: &gatev1.HTTPCORSFilter{
AllowOrigins: []gatev1.CORSOrigin{"*"},
},
expected: &dynamic.Middleware{
Headers: &dynamic.Headers{
AccessControlAllowOriginListRegex: []string{`.*`},
AccessControlMaxAge: ptr.To(int64(0)),
},
},
},
{
desc: "Origin with bare wildcard host",
filter: &gatev1.HTTPCORSFilter{
AllowOrigins: []gatev1.CORSOrigin{"https://*"},
},
expected: &dynamic.Middleware{
Headers: &dynamic.Headers{
AccessControlAllowOriginListRegex: []string{`^https://.*$`},
AccessControlMaxAge: ptr.To(int64(0)),
},
},
},
{
desc: "Origin with wildcard subdomain becomes regex",
filter: &gatev1.HTTPCORSFilter{
AllowOrigins: []gatev1.CORSOrigin{"https://*.example.com"},
},
expected: &dynamic.Middleware{
Headers: &dynamic.Headers{
AccessControlAllowOriginListRegex: []string{`^https://.*\.example\.com$`},
AccessControlMaxAge: ptr.To(int64(0)),
},
},
},
{
desc: "Mixed plain and wildcard origins",
filter: &gatev1.HTTPCORSFilter{
AllowOrigins: []gatev1.CORSOrigin{"https://foo.example.com", "https://*.example.com"},
},
expected: &dynamic.Middleware{
Headers: &dynamic.Headers{
AccessControlAllowOriginList: []string{"https://foo.example.com"},
AccessControlAllowOriginListRegex: []string{`^https://.*\.example\.com$`},
AccessControlMaxAge: ptr.To(int64(0)),
},
},
},
{
desc: "All fields set",
filter: &gatev1.HTTPCORSFilter{
AllowOrigins: []gatev1.CORSOrigin{"https://foo.example.com"},
AllowCredentials: ptr.To(true),
AllowMethods: []gatev1.HTTPMethodWithWildcard{"GET", "POST"},
AllowHeaders: []gatev1.HTTPHeaderName{"X-Foo", "X-Bar"},
ExposeHeaders: []gatev1.HTTPHeaderName{"X-Baz"},
MaxAge: 600,
},
expected: &dynamic.Middleware{
Headers: &dynamic.Headers{
AccessControlAllowCredentials: true,
AccessControlAllowOriginList: []string{"https://foo.example.com"},
AccessControlAllowMethods: []string{"GET", "POST"},
AccessControlAllowHeaders: []string{"X-Foo", "X-Bar"},
AccessControlExposeHeaders: []string{"X-Baz"},
AccessControlMaxAge: ptr.To(int64(600)),
},
},
},
{
desc: "AllowCredentials explicitly false",
filter: &gatev1.HTTPCORSFilter{
AllowCredentials: ptr.To(false),
},
expected: &dynamic.Middleware{
Headers: &dynamic.Headers{
AccessControlAllowCredentials: false,
AccessControlMaxAge: ptr.To(int64(0)),
},
},
},
}
for _, test := range testCases {
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
assert.Equal(t, test.expected, createCORS(test.filter))
})
}
}