gatewayapi: add support for route rule filters

Add support for [`Filters`](https://gateway-api.sigs.k8s.io/references/spec/#gateway.networking.k8s.io/v1beta1.HTTPRouteFilter)
in the HTTPRoute API. We reuse most of the existing fields used for
Istio to construct the appopriate filter. A new API
`.spec.service.mirror` is added to allow for request mirroring. The
`.spec.service.rewrite` API has been changed to a custom `HTTPRewrite`
API instead of importing it from Istio, to allow covering all features
that Gateway API provides.

Support for the [`RequestRedirect`](https://gateway-api.sigs.k8s.io/references/spec/#gateway.networking.k8s.io/v1beta1.HTTPRequestRedirectFilter)
Filter has been left out on purpose, since it's not possible to specify
it if the same rule also specifies `.backendRefs` (which Flagger does).

Signed-off-by: Sanskar Jaiswal <jaiswalsanskar078@gmail.com>
This commit is contained in:
Sanskar Jaiswal
2023-09-12 19:23:57 +05:30
parent 794fea8cc6
commit c0e2096f92
8 changed files with 310 additions and 9 deletions

View File

@@ -482,6 +482,54 @@ spec:
uri:
format: string
type: string
authority:
format: string
type: string
type:
format: string
type: string
mirror:
description: Mirror defines a schema for a filter that mirrors requests.
type: array
items:
type: object
properties:
backendRef:
properties:
group:
default: ""
maxLength: 253
pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$
type: string
kind:
default: Service
maxLength: 63
minLength: 1
pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$
type: string
name:
maxLength: 253
minLength: 1
type: string
namespace:
maxLength: 63
minLength: 1
pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$
type: string
port:
format: int32
maximum: 65535
minimum: 1
type: integer
required:
- name
type: object
x-kubernetes-validations:
- message: Must have port for Service reference
rule: '(size(self.group) == 0 && self.kind == ''Service'')
? has(self.port) : true'
required:
- backendRef
headers:
description: Headers operations
type: object

View File

@@ -482,6 +482,54 @@ spec:
uri:
format: string
type: string
authority:
format: string
type: string
type:
format: string
type: string
mirror:
description: Mirror defines a schema for a filter that mirrors requests.
type: array
items:
type: object
properties:
backendRef:
properties:
group:
default: ""
maxLength: 253
pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$
type: string
kind:
default: Service
maxLength: 63
minLength: 1
pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$
type: string
name:
maxLength: 253
minLength: 1
type: string
namespace:
maxLength: 63
minLength: 1
pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$
type: string
port:
format: int32
maximum: 65535
minimum: 1
type: integer
required:
- name
type: object
x-kubernetes-validations:
- message: Must have port for Service reference
rule: '(size(self.group) == 0 && self.kind == ''Service'')
? has(self.port) : true'
required:
- backendRef
headers:
description: Headers operations
type: object

View File

@@ -482,6 +482,54 @@ spec:
uri:
format: string
type: string
authority:
format: string
type: string
type:
format: string
type: string
mirror:
description: Mirror defines a schema for a filter that mirrors requests.
type: array
items:
type: object
properties:
backendRef:
properties:
group:
default: ""
maxLength: 253
pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$
type: string
kind:
default: Service
maxLength: 63
minLength: 1
pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$
type: string
name:
maxLength: 253
minLength: 1
type: string
namespace:
maxLength: 63
minLength: 1
pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$
type: string
port:
format: int32
maximum: 65535
minimum: 1
type: integer
required:
- name
type: object
x-kubernetes-validations:
- message: Must have port for Service reference
rule: '(size(self.group) == 0 && self.kind == ''Service'')
? has(self.port) : true'
required:
- backendRef
headers:
description: Headers operations
type: object

View File

@@ -181,7 +181,7 @@ type CanaryService struct {
// Rewrite HTTP URIs for the generated service
// +optional
Rewrite *istiov1alpha3.HTTPRewrite `json:"rewrite,omitempty"`
Rewrite *HTTPRewrite `json:"rewrite,omitempty"`
// Retries policy for the generated virtual service
// +optional
@@ -191,6 +191,10 @@ type CanaryService struct {
// +optional
Headers *istiov1alpha3.Headers `json:"headers,omitempty"`
// Mirror specifies the destination for request mirroring.
// Responses from this destination are dropped.
Mirror []v1beta1.HTTPRequestMirrorFilter `json:"mirror,omitempty"`
// Cross-Origin Resource Sharing policy for the generated Istio virtual service
// +optional
CorsPolicy *istiov1alpha3.CorsPolicy `json:"corsPolicy,omitempty"`
@@ -484,6 +488,41 @@ type CustomMetadata struct {
Annotations map[string]string `json:"annotations,omitempty"`
}
// HTTPRewrite holds information about how to modify a request URI during
// forwarding.
type HTTPRewrite struct {
// rewrite the path (or the prefix) portion of the URI with this
// value. If the original URI was matched based on prefix, the value
// provided in this field will replace the corresponding matched prefix.
Uri string `json:"uri,omitempty"`
// rewrite the Authority/Host header with this value.
Authority string `json:"authority,omitempty"`
// Type is the type of path modification to make.
// +optional
Type string `json:"type,omitempty"`
}
// GetType returns the type of HTTP path rewrite to be performed.
func (r *HTTPRewrite) GetType() string {
if r.Type == string(v1beta1.PrefixMatchHTTPPathModifier) {
return r.Type
}
return string(v1beta1.FullPathHTTPPathModifier)
}
// GetIstioRewrite returns a istiov1alpha3.HTTPRewrite object.
func (s *CanaryService) GetIstioRewrite() *istiov1alpha3.HTTPRewrite {
if s.Rewrite != nil {
return &istiov1alpha3.HTTPRewrite{
Authority: s.Rewrite.Authority,
Uri: s.Rewrite.Uri,
}
}
return nil
}
// GetMaxAge returns the max age of a cookie in seconds.
func (s *SessionAffinity) GetMaxAge() int {
if s.MaxAge == 0 {

View File

@@ -405,7 +405,7 @@ func (in *CanaryService) DeepCopyInto(out *CanaryService) {
}
if in.Rewrite != nil {
in, out := &in.Rewrite, &out.Rewrite
*out = new(v1alpha3.HTTPRewrite)
*out = new(HTTPRewrite)
**out = **in
}
if in.Retries != nil {
@@ -418,6 +418,13 @@ func (in *CanaryService) DeepCopyInto(out *CanaryService) {
*out = new(v1alpha3.Headers)
(*in).DeepCopyInto(*out)
}
if in.Mirror != nil {
in, out := &in.Mirror, &out.Mirror
*out = make([]gatewayapiv1beta1.HTTPRequestMirrorFilter, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
if in.CorsPolicy != nil {
in, out := &in.CorsPolicy, &out.CorsPolicy
*out = new(v1alpha3.CorsPolicy)
@@ -666,6 +673,22 @@ func (in *CustomMetadata) DeepCopy() *CustomMetadata {
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *HTTPRewrite) DeepCopyInto(out *HTTPRewrite) {
*out = *in
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HTTPRewrite.
func (in *HTTPRewrite) DeepCopy() *HTTPRewrite {
if in == nil {
return nil
}
out := new(HTTPRewrite)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *LocalObjectReference) DeepCopyInto(out *LocalObjectReference) {
*out = *in

View File

@@ -87,6 +87,7 @@ func (gwr *GatewayAPIV1Beta1Router) Reconcile(canary *flaggerv1.Canary) error {
Rules: []v1beta1.HTTPRouteRule{
{
Matches: matches,
Filters: gwr.makeFilters(canary),
BackendRefs: []v1beta1.HTTPBackendRef{
{
BackendRef: gwr.makeBackendRef(primarySvcName, initialPrimaryWeight, canary.Spec.Service.Port),
@@ -106,6 +107,7 @@ func (gwr *GatewayAPIV1Beta1Router) Reconcile(canary *flaggerv1.Canary) error {
httpRouteSpec.Rules[0].Matches = gwr.mergeMatchConditions(analysisMatches, matches)
httpRouteSpec.Rules = append(httpRouteSpec.Rules, v1beta1.HTTPRouteRule{
Matches: matches,
Filters: gwr.makeFilters(canary),
BackendRefs: []v1beta1.HTTPBackendRef{
{
BackendRef: gwr.makeBackendRef(primarySvcName, initialPrimaryWeight, canary.Spec.Service.Port),
@@ -295,6 +297,7 @@ func (gwr *GatewayAPIV1Beta1Router) SetRoutes(
}
weightedRouteRule := &v1beta1.HTTPRouteRule{
Matches: matches,
Filters: gwr.makeFilters(canary),
BackendRefs: []v1beta1.HTTPBackendRef{
{
BackendRef: gwr.makeBackendRef(primarySvcName, pWeight, canary.Spec.Service.Port),
@@ -330,6 +333,7 @@ func (gwr *GatewayAPIV1Beta1Router) SetRoutes(
hrClone.Spec.Rules[0].Matches = gwr.mergeMatchConditions(analysisMatches, matches)
hrClone.Spec.Rules = append(hrClone.Spec.Rules, v1beta1.HTTPRouteRule{
Matches: matches,
Filters: gwr.makeFilters(canary),
BackendRefs: []v1beta1.HTTPBackendRef{
{
BackendRef: gwr.makeBackendRef(primarySvcName, initialPrimaryWeight, canary.Spec.Service.Port),
@@ -570,3 +574,94 @@ func (gwr *GatewayAPIV1Beta1Router) mergeMatchConditions(analysis, service []v1b
}
return merged
}
func (gwr *GatewayAPIV1Beta1Router) makeFilters(canary *flaggerv1.Canary) []v1beta1.HTTPRouteFilter {
var filters []v1beta1.HTTPRouteFilter
if canary.Spec.Service.Headers != nil {
if canary.Spec.Service.Headers.Request != nil {
requestHeaderFilter := v1beta1.HTTPRouteFilter{
Type: v1beta1.HTTPRouteFilterRequestHeaderModifier,
RequestHeaderModifier: &v1beta1.HTTPHeaderFilter{},
}
for name, val := range canary.Spec.Service.Headers.Request.Add {
requestHeaderFilter.RequestHeaderModifier.Add = append(requestHeaderFilter.RequestHeaderModifier.Add, v1beta1.HTTPHeader{
Name: v1beta1.HTTPHeaderName(name),
Value: val,
})
}
for name, val := range canary.Spec.Service.Headers.Request.Set {
requestHeaderFilter.RequestHeaderModifier.Set = append(requestHeaderFilter.RequestHeaderModifier.Set, v1beta1.HTTPHeader{
Name: v1beta1.HTTPHeaderName(name),
Value: val,
})
}
for _, name := range canary.Spec.Service.Headers.Request.Remove {
requestHeaderFilter.RequestHeaderModifier.Remove = append(requestHeaderFilter.RequestHeaderModifier.Remove, name)
}
filters = append(filters, requestHeaderFilter)
}
if canary.Spec.Service.Headers.Response != nil {
responseHeaderFilter := v1beta1.HTTPRouteFilter{
Type: v1beta1.HTTPRouteFilterResponseHeaderModifier,
ResponseHeaderModifier: &v1beta1.HTTPHeaderFilter{},
}
for name, val := range canary.Spec.Service.Headers.Response.Add {
responseHeaderFilter.ResponseHeaderModifier.Add = append(responseHeaderFilter.ResponseHeaderModifier.Add, v1beta1.HTTPHeader{
Name: v1beta1.HTTPHeaderName(name),
Value: val,
})
}
for name, val := range canary.Spec.Service.Headers.Response.Set {
responseHeaderFilter.ResponseHeaderModifier.Set = append(responseHeaderFilter.ResponseHeaderModifier.Set, v1beta1.HTTPHeader{
Name: v1beta1.HTTPHeaderName(name),
Value: val,
})
}
for _, name := range canary.Spec.Service.Headers.Response.Remove {
responseHeaderFilter.ResponseHeaderModifier.Remove = append(responseHeaderFilter.ResponseHeaderModifier.Remove, name)
}
filters = append(filters, responseHeaderFilter)
}
}
if canary.Spec.Service.Rewrite != nil {
rewriteFilter := v1beta1.HTTPRouteFilter{
Type: v1beta1.HTTPRouteFilterURLRewrite,
URLRewrite: &v1beta1.HTTPURLRewriteFilter{},
}
if canary.Spec.Service.Rewrite.Authority != "" {
hostname := v1beta1.PreciseHostname(canary.Spec.Service.Rewrite.Authority)
rewriteFilter.URLRewrite.Hostname = &hostname
}
if canary.Spec.Service.Rewrite.Uri != "" {
rewriteFilter.URLRewrite.Path = &v1beta1.HTTPPathModifier{
Type: v1beta1.HTTPPathModifierType(canary.Spec.Service.Rewrite.GetType()),
}
if rewriteFilter.URLRewrite.Path.Type == v1beta1.FullPathHTTPPathModifier {
rewriteFilter.URLRewrite.Path.ReplaceFullPath = &canary.Spec.Service.Rewrite.Uri
} else {
rewriteFilter.URLRewrite.Path.ReplacePrefixMatch = &canary.Spec.Service.Rewrite.Uri
}
}
filters = append(filters, rewriteFilter)
}
for _, mirror := range canary.Spec.Service.Mirror {
mirror := mirror
mirrorFilter := v1beta1.HTTPRouteFilter{
Type: v1beta1.HTTPRouteFilterRequestMirror,
RequestMirror: &mirror,
}
filters = append(filters, mirrorFilter)
}
return filters
}

View File

@@ -181,7 +181,7 @@ func (ir *IstioRouter) reconcileVirtualService(canary *flaggerv1.Canary) error {
Http: []istiov1alpha3.HTTPRoute{
{
Match: canary.Spec.Service.Match,
Rewrite: canary.Spec.Service.Rewrite,
Rewrite: canary.Spec.Service.GetIstioRewrite(),
Timeout: canary.Spec.Service.Timeout,
Retries: canary.Spec.Service.Retries,
CorsPolicy: canary.Spec.Service.CorsPolicy,
@@ -208,7 +208,7 @@ func (ir *IstioRouter) reconcileVirtualService(canary *flaggerv1.Canary) error {
newSpec.Http = []istiov1alpha3.HTTPRoute{
{
Match: canaryMatch,
Rewrite: canary.Spec.Service.Rewrite,
Rewrite: canary.Spec.Service.GetIstioRewrite(),
Timeout: canary.Spec.Service.Timeout,
Retries: canary.Spec.Service.Retries,
CorsPolicy: canary.Spec.Service.CorsPolicy,
@@ -217,7 +217,7 @@ func (ir *IstioRouter) reconcileVirtualService(canary *flaggerv1.Canary) error {
},
{
Match: canary.Spec.Service.Match,
Rewrite: canary.Spec.Service.Rewrite,
Rewrite: canary.Spec.Service.GetIstioRewrite(),
Timeout: canary.Spec.Service.Timeout,
Retries: canary.Spec.Service.Retries,
CorsPolicy: canary.Spec.Service.CorsPolicy,
@@ -415,7 +415,7 @@ func (ir *IstioRouter) SetRoutes(
// weighted routing (progressive canary)
weightedRoute := istiov1alpha3.HTTPRoute{
Match: canary.Spec.Service.Match,
Rewrite: canary.Spec.Service.Rewrite,
Rewrite: canary.Spec.Service.GetIstioRewrite(),
Timeout: canary.Spec.Service.Timeout,
Retries: canary.Spec.Service.Retries,
CorsPolicy: canary.Spec.Service.CorsPolicy,
@@ -530,7 +530,7 @@ func (ir *IstioRouter) SetRoutes(
vsCopy.Spec.Http = []istiov1alpha3.HTTPRoute{
{
Match: canaryMatch,
Rewrite: canary.Spec.Service.Rewrite,
Rewrite: canary.Spec.Service.GetIstioRewrite(),
Timeout: canary.Spec.Service.Timeout,
Retries: canary.Spec.Service.Retries,
CorsPolicy: canary.Spec.Service.CorsPolicy,
@@ -542,7 +542,7 @@ func (ir *IstioRouter) SetRoutes(
},
{
Match: canary.Spec.Service.Match,
Rewrite: canary.Spec.Service.Rewrite,
Rewrite: canary.Spec.Service.GetIstioRewrite(),
Timeout: canary.Spec.Service.Timeout,
Retries: canary.Spec.Service.Retries,
CorsPolicy: canary.Spec.Service.CorsPolicy,

View File

@@ -573,7 +573,7 @@ func TestIstioRouter_Finalize(t *testing.T) {
Http: []istiov1alpha3.HTTPRoute{
{
Match: mocks.canary.Spec.Service.Match,
Rewrite: mocks.canary.Spec.Service.Rewrite,
Rewrite: mocks.canary.Spec.Service.GetIstioRewrite(),
Timeout: mocks.canary.Spec.Service.Timeout,
Retries: mocks.canary.Spec.Service.Retries,
CorsPolicy: mocks.canary.Spec.Service.CorsPolicy,