diff --git a/pkg/apis/flagger/v1beta1/canary.go b/pkg/apis/flagger/v1beta1/canary.go index f5797e876..7940dde1f 100644 --- a/pkg/apis/flagger/v1beta1/canary.go +++ b/pkg/apis/flagger/v1beta1/canary.go @@ -20,6 +20,7 @@ import ( "fmt" "time" + v1 "github.com/fluxcd/flagger/pkg/apis/gatewayapi/v1" "github.com/fluxcd/flagger/pkg/apis/gatewayapi/v1beta1" istiov1beta1 "github.com/fluxcd/flagger/pkg/apis/istio/v1beta1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -213,11 +214,11 @@ type CanaryService struct { // Primary is the metadata to add to the primary service // +optional - Primary *CustomMetadata `json:"primary,omitempty"` + Primary *CustomBackend `json:"primary,omitempty"` // Canary is the metadata to add to the canary service // +optional - Canary *CustomMetadata `json:"canary,omitempty"` + Canary *CustomBackend `json:"canary,omitempty"` } // CanaryAnalysis is used to describe how the analysis should be done @@ -496,6 +497,38 @@ type CustomMetadata struct { Annotations map[string]string `json:"annotations,omitempty"` } +// CustomBackend holds labels, annotations, and proxyRef to set on generated objects. +type CustomBackend struct { + CustomMetadata + + // Backend is a reference to a backend to forward matched requests to. + // Defaults to the primary or canary service. + // +optional + Backend *HTTPBackendRefTemplate `json:"backendRef,omitempty"` +} + +// HTTPBackendRefTemplate is a reference to a backend to forward matched requests to. +type HTTPBackendRefTemplate struct { + // Ref references a Kubernetes object. + BackendObjectReference *v1.BackendObjectReference `json:"ref,omitempty"` + + // Filters defined at this level should be executed if and only if the + // request is being forwarded to the backend defined here. + // + // Support: Implementation-specific (For broader support of filters, use the + // Filters field in HTTPRouteRule.) + // + // +optional + // +kubebuilder:validation:MaxItems=16 + // +kubebuilder:validation:XValidation:message="May specify either httpRouteFilterRequestRedirect or httpRouteFilterRequestRewrite, but not both",rule="!(self.exists(f, f.type == 'RequestRedirect') && self.exists(f, f.type == 'URLRewrite'))" + // +kubebuilder:validation:XValidation:message="May specify either httpRouteFilterRequestRedirect or httpRouteFilterRequestRewrite, but not both",rule="!(self.exists(f, f.type == 'RequestRedirect') && self.exists(f, f.type == 'URLRewrite'))" + // +kubebuilder:validation:XValidation:message="RequestHeaderModifier filter cannot be repeated",rule="self.filter(f, f.type == 'RequestHeaderModifier').size() <= 1" + // +kubebuilder:validation:XValidation:message="ResponseHeaderModifier filter cannot be repeated",rule="self.filter(f, f.type == 'ResponseHeaderModifier').size() <= 1" + // +kubebuilder:validation:XValidation:message="RequestRedirect filter cannot be repeated",rule="self.filter(f, f.type == 'RequestRedirect').size() <= 1" + // +kubebuilder:validation:XValidation:message="URLRewrite filter cannot be repeated",rule="self.filter(f, f.type == 'URLRewrite').size() <= 1" + Filters []v1.HTTPRouteFilter `json:"filters,omitempty"` +} + // HTTPRewrite holds information about how to modify a request URI during // forwarding. type HTTPRewrite struct { diff --git a/pkg/apis/flagger/v1beta1/zz_generated.deepcopy.go b/pkg/apis/flagger/v1beta1/zz_generated.deepcopy.go index 340fc0439..24fd18ff3 100644 --- a/pkg/apis/flagger/v1beta1/zz_generated.deepcopy.go +++ b/pkg/apis/flagger/v1beta1/zz_generated.deepcopy.go @@ -22,6 +22,7 @@ limitations under the License. package v1beta1 import ( + gatewayapiv1 "github.com/fluxcd/flagger/pkg/apis/gatewayapi/v1" gatewayapiv1beta1 "github.com/fluxcd/flagger/pkg/apis/gatewayapi/v1beta1" istiov1beta1 "github.com/fluxcd/flagger/pkg/apis/istio/v1beta1" v1 "k8s.io/api/core/v1" @@ -442,12 +443,12 @@ func (in *CanaryService) DeepCopyInto(out *CanaryService) { } if in.Primary != nil { in, out := &in.Primary, &out.Primary - *out = new(CustomMetadata) + *out = new(CustomBackend) (*in).DeepCopyInto(*out) } if in.Canary != nil { in, out := &in.Canary, &out.Canary - *out = new(CustomMetadata) + *out = new(CustomBackend) (*in).DeepCopyInto(*out) } return @@ -643,6 +644,28 @@ func (in *CrossNamespaceObjectReference) DeepCopy() *CrossNamespaceObjectReferen return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *CustomBackend) DeepCopyInto(out *CustomBackend) { + *out = *in + in.CustomMetadata.DeepCopyInto(&out.CustomMetadata) + if in.Backend != nil { + in, out := &in.Backend, &out.Backend + *out = new(HTTPBackendRefTemplate) + (*in).DeepCopyInto(*out) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CustomBackend. +func (in *CustomBackend) DeepCopy() *CustomBackend { + if in == nil { + return nil + } + out := new(CustomBackend) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *CustomMetadata) DeepCopyInto(out *CustomMetadata) { *out = *in @@ -673,6 +696,34 @@ 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 *HTTPBackendRefTemplate) DeepCopyInto(out *HTTPBackendRefTemplate) { + *out = *in + if in.BackendObjectReference != nil { + in, out := &in.BackendObjectReference, &out.BackendObjectReference + *out = new(gatewayapiv1.BackendObjectReference) + (*in).DeepCopyInto(*out) + } + if in.Filters != nil { + in, out := &in.Filters, &out.Filters + *out = make([]gatewayapiv1.HTTPRouteFilter, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HTTPBackendRefTemplate. +func (in *HTTPBackendRefTemplate) DeepCopy() *HTTPBackendRefTemplate { + if in == nil { + return nil + } + out := new(HTTPBackendRefTemplate) + in.DeepCopyInto(out) + 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 diff --git a/pkg/router/gateway_api.go b/pkg/router/gateway_api.go index bc398afda..25b91210a 100644 --- a/pkg/router/gateway_api.go +++ b/pkg/router/gateway_api.go @@ -96,12 +96,8 @@ func (gwr *GatewayAPIRouter) Reconcile(canary *flaggerv1.Canary) error { Matches: matches, Filters: gwr.makeFilters(canary), BackendRefs: []v1.HTTPBackendRef{ - { - BackendRef: gwr.makeBackendRef(primarySvcName, initialPrimaryWeight, canary.Spec.Service.Port), - }, - { - BackendRef: gwr.makeBackendRef(canarySvcName, initialCanaryWeight, canary.Spec.Service.Port), - }, + gwr.makeHTTPBackendRef(primarySvcName, initialPrimaryWeight, canary.Spec.Service.Port, canary.Spec.Service.Primary), + gwr.makeHTTPBackendRef(canarySvcName, initialCanaryWeight, canary.Spec.Service.Port, canary.Spec.Service.Canary), }, }, }, @@ -122,9 +118,7 @@ func (gwr *GatewayAPIRouter) Reconcile(canary *flaggerv1.Canary) error { Matches: matches, Filters: gwr.makeFilters(canary), BackendRefs: []v1.HTTPBackendRef{ - { - BackendRef: gwr.makeBackendRef(primarySvcName, initialPrimaryWeight, canary.Spec.Service.Port), - }, + gwr.makeHTTPBackendRef(primarySvcName, initialPrimaryWeight, canary.Spec.Service.Port, canary.Spec.Service.Primary), }, }) if canary.Spec.Service.Timeout != "" { @@ -340,12 +334,8 @@ func (gwr *GatewayAPIRouter) SetRoutes( Matches: matches, Filters: gwr.makeFilters(canary), BackendRefs: []v1.HTTPBackendRef{ - { - BackendRef: gwr.makeBackendRef(primarySvcName, pWeight, canary.Spec.Service.Port), - }, - { - BackendRef: gwr.makeBackendRef(canarySvcName, cWeight, canary.Spec.Service.Port), - }, + gwr.makeHTTPBackendRef(primarySvcName, pWeight, canary.Spec.Service.Port, canary.Spec.Service.Primary), + gwr.makeHTTPBackendRef(canarySvcName, cWeight, canary.Spec.Service.Port, canary.Spec.Service.Canary), }, } if canary.Spec.Service.Timeout != "" { @@ -399,9 +389,7 @@ func (gwr *GatewayAPIRouter) SetRoutes( Matches: matches, Filters: gwr.makeFilters(canary), BackendRefs: []v1.HTTPBackendRef{ - { - BackendRef: gwr.makeBackendRef(primarySvcName, initialPrimaryWeight, canary.Spec.Service.Port), - }, + gwr.makeHTTPBackendRef(primarySvcName, initialPrimaryWeight, canary.Spec.Service.Port, canary.Spec.Service.Primary), }, Timeouts: &v1.HTTPRouteTimeouts{ Request: &timeout, @@ -484,12 +472,8 @@ func (gwr *GatewayAPIRouter) getSessionAffinityRouteRules(canary *flaggerv1.Cana mergedMatches := gwr.mergeMatchConditions([]v1.HTTPRouteMatch{cookieMatch}, svcMatches) stickyRouteRule.Matches = mergedMatches stickyRouteRule.BackendRefs = []v1.HTTPBackendRef{ - { - BackendRef: gwr.makeBackendRef(primarySvcName, 0, canary.Spec.Service.Port), - }, - { - BackendRef: gwr.makeBackendRef(canarySvcName, 100, canary.Spec.Service.Port), - }, + gwr.makeHTTPBackendRef(primarySvcName, 0, canary.Spec.Service.Port, canary.Spec.Service.Primary), + gwr.makeHTTPBackendRef(canarySvcName, 100, canary.Spec.Service.Port, canary.Spec.Service.Canary), } } else { // If canary weight is 0 and SessionAffinityCookie is non-blank, then it belongs to a previous canary run. @@ -612,16 +596,28 @@ func (gwr *GatewayAPIRouter) mapRouteMatches(requestMatches []istiov1beta1.HTTPM return matches, nil } -func (gwr *GatewayAPIRouter) makeBackendRef(svcName string, weight, port int32) v1.BackendRef { - return v1.BackendRef{ - BackendObjectReference: v1.BackendObjectReference{ - Group: (*v1.Group)(&backendRefGroup), - Kind: (*v1.Kind)(&backendRefKind), - Name: v1.ObjectName(svcName), - Port: (*v1.PortNumber)(&port), +func (gwr *GatewayAPIRouter) makeHTTPBackendRef(svcName string, weight, port int32, customBackend *flaggerv1.CustomBackend) v1.HTTPBackendRef { + httpBackendRef := v1.HTTPBackendRef{ + BackendRef: v1.BackendRef{ + BackendObjectReference: v1.BackendObjectReference{ + Group: (*v1.Group)(&backendRefGroup), + Kind: (*v1.Kind)(&backendRefKind), + Name: v1.ObjectName(svcName), + Port: (*v1.PortNumber)(&port), + }, + Weight: &weight, }, - Weight: &weight, } + if customBackend != nil && customBackend.Backend != nil { + if customBackend.Backend.BackendObjectReference != nil { + httpBackendRef.BackendObjectReference = *customBackend.Backend.BackendObjectReference + } + if customBackend.Backend.Filters != nil { + httpBackendRef.Filters = customBackend.Backend.Filters + } + } + + return httpBackendRef } func (gwr *GatewayAPIRouter) mergeMatchConditions(analysis, service []v1.HTTPRouteMatch) []v1.HTTPRouteMatch { diff --git a/pkg/router/gateway_api_test.go b/pkg/router/gateway_api_test.go index a7d6b6d89..fb621f215 100644 --- a/pkg/router/gateway_api_test.go +++ b/pkg/router/gateway_api_test.go @@ -295,12 +295,8 @@ func TestGatewayAPIRouter_getSessionAffinityRouteRules(t *testing.T) { _, pSvcName, cSvcName := canary.GetServiceNames() weightedRouteRule := &v1.HTTPRouteRule{ BackendRefs: []v1.HTTPBackendRef{ - { - BackendRef: router.makeBackendRef(pSvcName, initialPrimaryWeight, canary.Spec.Service.Port), - }, - { - BackendRef: router.makeBackendRef(cSvcName, initialCanaryWeight, canary.Spec.Service.Port), - }, + router.makeHTTPBackendRef(pSvcName, initialPrimaryWeight, canary.Spec.Service.Port, canary.Spec.Service.Primary), + router.makeHTTPBackendRef(cSvcName, initialCanaryWeight, canary.Spec.Service.Port, canary.Spec.Service.Canary), }, } rules, err := router.getSessionAffinityRouteRules(canary, 10, weightedRouteRule) diff --git a/pkg/router/kubernetes_default.go b/pkg/router/kubernetes_default.go index 840005672..f5623f15f 100644 --- a/pkg/router/kubernetes_default.go +++ b/pkg/router/kubernetes_default.go @@ -50,13 +50,21 @@ func (c *KubernetesDefaultRouter) Initialize(canary *flaggerv1.Canary) error { _, primaryName, canaryName := canary.GetServiceNames() // canary svc - err := c.reconcileService(canary, canaryName, c.labelValue, canary.Spec.Service.Canary) + canaryCustomMetadata := &flaggerv1.CustomMetadata{} + if canary.Spec.Service.Canary != nil { + canaryCustomMetadata = &canary.Spec.Service.Canary.CustomMetadata + } + err := c.reconcileService(canary, canaryName, c.labelValue, canaryCustomMetadata) if err != nil { return fmt.Errorf("reconcileService failed: %w", err) } // primary svc - err = c.reconcileService(canary, primaryName, fmt.Sprintf("%s-primary", c.labelValue), canary.Spec.Service.Primary) + primaryCustomMetadata := &flaggerv1.CustomMetadata{} + if canary.Spec.Service.Primary != nil { + primaryCustomMetadata = &canary.Spec.Service.Primary.CustomMetadata + } + err = c.reconcileService(canary, primaryName, fmt.Sprintf("%s-primary", c.labelValue), primaryCustomMetadata) if err != nil { return fmt.Errorf("reconcileService failed: %w", err) } diff --git a/pkg/router/kubernetes_default_test.go b/pkg/router/kubernetes_default_test.go index c0f969958..4d7ed4122 100644 --- a/pkg/router/kubernetes_default_test.go +++ b/pkg/router/kubernetes_default_test.go @@ -375,13 +375,13 @@ func TestServiceRouter_InitializeMetadata(t *testing.T) { labelSelector: "app", } - metadata := &flaggerv1.CustomMetadata{ - Labels: map[string]string{"test": "test"}, - Annotations: map[string]string{"test": "test"}, + mocks.canary.Spec.Service.Canary = &flaggerv1.CustomBackend{ + CustomMetadata: flaggerv1.CustomMetadata{ + Labels: map[string]string{"test": "test"}, + Annotations: map[string]string{"test": "test"}, + }, } - mocks.canary.Spec.Service.Canary = metadata - err := router.Initialize(mocks.canary) require.NoError(t, err)