From c0e2096f920d4c8b9dc4c7df59b6fbbfb5586c85 Mon Sep 17 00:00:00 2001 From: Sanskar Jaiswal Date: Tue, 12 Sep 2023 19:23:57 +0530 Subject: [PATCH] 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 --- artifacts/flagger/crd.yaml | 48 ++++++++++ charts/flagger/crds/crd.yaml | 48 ++++++++++ kustomize/base/flagger/crd.yaml | 48 ++++++++++ pkg/apis/flagger/v1beta1/canary.go | 41 +++++++- .../flagger/v1beta1/zz_generated.deepcopy.go | 25 ++++- pkg/router/gateway_api_v1beta1.go | 95 +++++++++++++++++++ pkg/router/istio.go | 12 +-- pkg/router/istio_test.go | 2 +- 8 files changed, 310 insertions(+), 9 deletions(-) diff --git a/artifacts/flagger/crd.yaml b/artifacts/flagger/crd.yaml index ad4686100..d95bfc246 100644 --- a/artifacts/flagger/crd.yaml +++ b/artifacts/flagger/crd.yaml @@ -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 diff --git a/charts/flagger/crds/crd.yaml b/charts/flagger/crds/crd.yaml index ad4686100..d95bfc246 100644 --- a/charts/flagger/crds/crd.yaml +++ b/charts/flagger/crds/crd.yaml @@ -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 diff --git a/kustomize/base/flagger/crd.yaml b/kustomize/base/flagger/crd.yaml index ad4686100..d95bfc246 100644 --- a/kustomize/base/flagger/crd.yaml +++ b/kustomize/base/flagger/crd.yaml @@ -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 diff --git a/pkg/apis/flagger/v1beta1/canary.go b/pkg/apis/flagger/v1beta1/canary.go index d80b5a54f..0fe99dc30 100644 --- a/pkg/apis/flagger/v1beta1/canary.go +++ b/pkg/apis/flagger/v1beta1/canary.go @@ -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 { diff --git a/pkg/apis/flagger/v1beta1/zz_generated.deepcopy.go b/pkg/apis/flagger/v1beta1/zz_generated.deepcopy.go index 123da753e..ea64edb9d 100644 --- a/pkg/apis/flagger/v1beta1/zz_generated.deepcopy.go +++ b/pkg/apis/flagger/v1beta1/zz_generated.deepcopy.go @@ -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 diff --git a/pkg/router/gateway_api_v1beta1.go b/pkg/router/gateway_api_v1beta1.go index b966dc1fc..ff023854e 100644 --- a/pkg/router/gateway_api_v1beta1.go +++ b/pkg/router/gateway_api_v1beta1.go @@ -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 +} diff --git a/pkg/router/istio.go b/pkg/router/istio.go index 572a13db8..ea08f7261 100644 --- a/pkg/router/istio.go +++ b/pkg/router/istio.go @@ -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, diff --git a/pkg/router/istio_test.go b/pkg/router/istio_test.go index 5a96b2038..b18caa7a6 100644 --- a/pkg/router/istio_test.go +++ b/pkg/router/istio_test.go @@ -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,