diff --git a/api/v1alpha1/trafficrouting_types.go b/api/v1alpha1/trafficrouting_types.go index 4a9a15c7..0386a877 100644 --- a/api/v1alpha1/trafficrouting_types.go +++ b/api/v1alpha1/trafficrouting_types.go @@ -18,6 +18,7 @@ package v1alpha1 import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + gatewayv1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2" ) const ( @@ -86,7 +87,7 @@ type TrafficRoutingStrategy struct { // my-header: bar // // +optional - // RequestHeaderModifier *gatewayv1alpha2.HTTPRequestHeaderFilter `json:"requestHeaderModifier,omitempty"` + RequestHeaderModifier *gatewayv1alpha2.HTTPRequestHeaderFilter `json:"requestHeaderModifier,omitempty"` // Matches define conditions used for matching the incoming HTTP requests to canary service. // Each match is independent, i.e. this rule will be matched if **any** one of the matches is satisfied. // If Gateway API, current only support one match. diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index a5e87273..6940c32b 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -920,6 +920,11 @@ func (in *TrafficRoutingStrategy) DeepCopyInto(out *TrafficRoutingStrategy) { *out = new(int32) **out = **in } + if in.RequestHeaderModifier != nil { + in, out := &in.RequestHeaderModifier, &out.RequestHeaderModifier + *out = new(v1alpha2.HTTPRequestHeaderFilter) + (*in).DeepCopyInto(*out) + } if in.Matches != nil { in, out := &in.Matches, &out.Matches *out = make([]HttpRouteMatch, len(*in)) diff --git a/config/crd/bases/rollouts.kruise.io_rollouts.yaml b/config/crd/bases/rollouts.kruise.io_rollouts.yaml index 4d375139..ca0ab80e 100644 --- a/config/crd/bases/rollouts.kruise.io_rollouts.yaml +++ b/config/crd/bases/rollouts.kruise.io_rollouts.yaml @@ -111,20 +111,13 @@ spec: description: CanaryStep defines a step of a canary workload. properties: matches: - description: "Set overwrites the request with the given - header (name, value) before the action. \n Input: - \ GET /foo HTTP/1.1 my-header: foo \n requestHeaderModifier: - \ set: - name: \"my-header\" value: \"bar\" - \n Output: GET /foo HTTP/1.1 my-header: bar \n - RequestHeaderModifier *gatewayv1alpha2.HTTPRequestHeaderFilter - `json:\"requestHeaderModifier,omitempty\"` Matches - define conditions used for matching the incoming HTTP - requests to canary service. Each match is independent, - i.e. this rule will be matched if **any** one of the - matches is satisfied. If Gateway API, current only - support one match. And cannot support both weight - and matches, if both are configured, then matches - takes precedence." + description: Matches define conditions used for matching + the incoming HTTP requests to canary service. Each + match is independent, i.e. this rule will be matched + if **any** one of the matches is satisfied. If Gateway + API, current only support one match. And cannot support + both weight and matches, if both are configured, then + matches takes precedence. items: properties: headers: @@ -206,6 +199,110 @@ spec: pods in this batch it can be an absolute number (ex: 5) or a percentage of total pods.' x-kubernetes-int-or-string: true + requestHeaderModifier: + description: "Set overwrites the request with the given + header (name, value) before the action. \n Input: + \ GET /foo HTTP/1.1 my-header: foo \n requestHeaderModifier: + \ set: - name: \"my-header\" value: \"bar\" + \n Output: GET /foo HTTP/1.1 my-header: bar" + properties: + add: + description: "Add adds the given header(s) (name, + value) to the request before the action. It appends + to any existing values associated with the header + name. \n Input: GET /foo HTTP/1.1 my-header: + foo \n Config: add: - name: \"my-header\" + \ value: \"bar\" \n Output: GET /foo HTTP/1.1 + \ my-header: foo my-header: bar" + items: + description: HTTPHeader represents an HTTP Header + name and value as defined by RFC 7230. + properties: + name: + description: "Name is the name of the HTTP + Header to be matched. Name matching MUST + be case insensitive. (See https://tools.ietf.org/html/rfc7230#section-3.2). + \n If multiple entries specify equivalent + header names, the first entry with an equivalent + name MUST be considered for a match. Subsequent + entries with an equivalent header name MUST + be ignored. Due to the case-insensitivity + of header names, \"foo\" and \"Foo\" are + considered equivalent." + maxLength: 256 + minLength: 1 + pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$ + type: string + value: + description: Value is the value of HTTP Header + to be matched. + maxLength: 4096 + minLength: 1 + type: string + required: + - name + - value + type: object + maxItems: 16 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + remove: + description: "Remove the given header(s) from the + HTTP request before the action. The value of Remove + is a list of HTTP header names. Note that the + header names are case-insensitive (see https://datatracker.ietf.org/doc/html/rfc2616#section-4.2). + \n Input: GET /foo HTTP/1.1 my-header1: foo + \ my-header2: bar my-header3: baz \n Config: + \ remove: [\"my-header1\", \"my-header3\"] \n + Output: GET /foo HTTP/1.1 my-header2: bar" + items: + type: string + maxItems: 16 + type: array + set: + description: "Set overwrites the request with the + given header (name, value) before the action. + \n Input: GET /foo HTTP/1.1 my-header: foo + \n Config: set: - name: \"my-header\" value: + \"bar\" \n Output: GET /foo HTTP/1.1 my-header: + bar" + items: + description: HTTPHeader represents an HTTP Header + name and value as defined by RFC 7230. + properties: + name: + description: "Name is the name of the HTTP + Header to be matched. Name matching MUST + be case insensitive. (See https://tools.ietf.org/html/rfc7230#section-3.2). + \n If multiple entries specify equivalent + header names, the first entry with an equivalent + name MUST be considered for a match. Subsequent + entries with an equivalent header name MUST + be ignored. Due to the case-insensitivity + of header names, \"foo\" and \"Foo\" are + considered equivalent." + maxLength: 256 + minLength: 1 + pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$ + type: string + value: + description: Value is the value of HTTP Header + to be matched. + maxLength: 4096 + minLength: 1 + type: string + required: + - name + - value + type: object + maxItems: 16 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + type: object weight: description: Weight indicate how many percentage of traffic the canary pods should receive diff --git a/config/crd/bases/rollouts.kruise.io_trafficroutings.yaml b/config/crd/bases/rollouts.kruise.io_trafficroutings.yaml index f8ef5907..68775c5f 100644 --- a/config/crd/bases/rollouts.kruise.io_trafficroutings.yaml +++ b/config/crd/bases/rollouts.kruise.io_trafficroutings.yaml @@ -98,17 +98,12 @@ spec: description: trafficrouting strategy properties: matches: - description: "Set overwrites the request with the given header - (name, value) before the action. \n Input: GET /foo HTTP/1.1 - \ my-header: foo \n requestHeaderModifier: set: - name: - \"my-header\" value: \"bar\" \n Output: GET /foo HTTP/1.1 - \ my-header: bar \n RequestHeaderModifier *gatewayv1alpha2.HTTPRequestHeaderFilter - `json:\"requestHeaderModifier,omitempty\"` Matches define conditions - used for matching the incoming HTTP requests to canary service. - Each match is independent, i.e. this rule will be matched if - **any** one of the matches is satisfied. If Gateway API, current - only support one match. And cannot support both weight and matches, - if both are configured, then matches takes precedence." + description: Matches define conditions used for matching the incoming + HTTP requests to canary service. Each match is independent, + i.e. this rule will be matched if **any** one of the matches + is satisfied. If Gateway API, current only support one match. + And cannot support both weight and matches, if both are configured, + then matches takes precedence. items: properties: headers: @@ -167,6 +162,106 @@ spec: type: array type: object type: array + requestHeaderModifier: + description: "Set overwrites the request with the given header + (name, value) before the action. \n Input: GET /foo HTTP/1.1 + \ my-header: foo \n requestHeaderModifier: set: - name: + \"my-header\" value: \"bar\" \n Output: GET /foo HTTP/1.1 + \ my-header: bar" + properties: + add: + description: "Add adds the given header(s) (name, value) to + the request before the action. It appends to any existing + values associated with the header name. \n Input: GET + /foo HTTP/1.1 my-header: foo \n Config: add: - name: + \"my-header\" value: \"bar\" \n Output: GET /foo HTTP/1.1 + \ my-header: foo my-header: bar" + items: + description: HTTPHeader represents an HTTP Header name and + value as defined by RFC 7230. + properties: + name: + description: "Name is the name of the HTTP Header to + be matched. Name matching MUST be case insensitive. + (See https://tools.ietf.org/html/rfc7230#section-3.2). + \n If multiple entries specify equivalent header names, + the first entry with an equivalent name MUST be considered + for a match. Subsequent entries with an equivalent + header name MUST be ignored. Due to the case-insensitivity + of header names, \"foo\" and \"Foo\" are considered + equivalent." + maxLength: 256 + minLength: 1 + pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$ + type: string + value: + description: Value is the value of HTTP Header to be + matched. + maxLength: 4096 + minLength: 1 + type: string + required: + - name + - value + type: object + maxItems: 16 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + remove: + description: "Remove the given header(s) from the HTTP request + before the action. The value of Remove is a list of HTTP + header names. Note that the header names are case-insensitive + (see https://datatracker.ietf.org/doc/html/rfc2616#section-4.2). + \n Input: GET /foo HTTP/1.1 my-header1: foo my-header2: + bar my-header3: baz \n Config: remove: [\"my-header1\", + \"my-header3\"] \n Output: GET /foo HTTP/1.1 my-header2: + bar" + items: + type: string + maxItems: 16 + type: array + set: + description: "Set overwrites the request with the given header + (name, value) before the action. \n Input: GET /foo HTTP/1.1 + \ my-header: foo \n Config: set: - name: \"my-header\" + \ value: \"bar\" \n Output: GET /foo HTTP/1.1 my-header: + bar" + items: + description: HTTPHeader represents an HTTP Header name and + value as defined by RFC 7230. + properties: + name: + description: "Name is the name of the HTTP Header to + be matched. Name matching MUST be case insensitive. + (See https://tools.ietf.org/html/rfc7230#section-3.2). + \n If multiple entries specify equivalent header names, + the first entry with an equivalent name MUST be considered + for a match. Subsequent entries with an equivalent + header name MUST be ignored. Due to the case-insensitivity + of header names, \"foo\" and \"Foo\" are considered + equivalent." + maxLength: 256 + minLength: 1 + pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$ + type: string + value: + description: Value is the value of HTTP Header to be + matched. + maxLength: 4096 + minLength: 1 + type: string + required: + - name + - value + type: object + maxItems: 16 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + type: object weight: description: Weight indicate how many percentage of traffic the canary pods should receive diff --git a/pkg/trafficrouting/network/ingress/ingress.go b/pkg/trafficrouting/network/ingress/ingress.go index e54b0779..9531a765 100644 --- a/pkg/trafficrouting/network/ingress/ingress.go +++ b/pkg/trafficrouting/network/ingress/ingress.go @@ -39,6 +39,7 @@ import ( "k8s.io/klog/v2" utilpointer "k8s.io/utils/pointer" "sigs.k8s.io/controller-runtime/pkg/client" + gatewayv1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2" ) type ingressController struct { @@ -84,7 +85,7 @@ func (r *ingressController) Initialize(ctx context.Context) error { func (r *ingressController) EnsureRoutes(ctx context.Context, strategy *rolloutv1alpha1.TrafficRoutingStrategy) (bool, error) { weight := strategy.Weight matches := strategy.Matches - // headerModifier := strategy.RequestHeaderModifier + headerModifier := strategy.RequestHeaderModifier canaryIngress := &netv1.Ingress{} err := r.Get(ctx, types.NamespacedName{Namespace: r.conf.Namespace, Name: defaultCanaryIngressName(r.conf.TrafficConf.Name)}, canaryIngress) @@ -101,7 +102,7 @@ func (r *ingressController) EnsureRoutes(ctx context.Context, strategy *rolloutv } // build and create canary ingress canaryIngress = r.buildCanaryIngress(ingress) - canaryIngress.Annotations, err = r.executeLuaForCanary(canaryIngress.Annotations, utilpointer.Int32(0), nil) + canaryIngress.Annotations, err = r.executeLuaForCanary(canaryIngress.Annotations, utilpointer.Int32(0), nil, nil) if err != nil { klog.Errorf("%s execute lua failed: %s", r.conf.Key, err.Error()) return false, err @@ -116,7 +117,7 @@ func (r *ingressController) EnsureRoutes(ctx context.Context, strategy *rolloutv klog.Errorf("%s get canary ingress failed: %s", r.conf.Key, err.Error()) return false, err } - newAnnotations, err := r.executeLuaForCanary(canaryIngress.Annotations, weight, matches) + newAnnotations, err := r.executeLuaForCanary(canaryIngress.Annotations, weight, matches, headerModifier) if err != nil { klog.Errorf("%s execute lua failed: %s", r.conf.Key, err.Error()) return false, err @@ -216,7 +217,8 @@ func defaultCanaryIngressName(name string) string { return fmt.Sprintf("%s-canary", name) } -func (r *ingressController) executeLuaForCanary(annotations map[string]string, weight *int32, matches []rolloutv1alpha1.HttpRouteMatch) (map[string]string, error) { +func (r *ingressController) executeLuaForCanary(annotations map[string]string, weight *int32, matches []rolloutv1alpha1.HttpRouteMatch, + headerModifier *gatewayv1alpha2.HTTPRequestHeaderFilter) (map[string]string, error) { if weight == nil { // the lua script does not have a pointer type, @@ -224,18 +226,18 @@ func (r *ingressController) executeLuaForCanary(annotations map[string]string, w weight = utilpointer.Int32(-1) } type LuaData struct { - Annotations map[string]string - Weight string - Matches []rolloutv1alpha1.HttpRouteMatch - CanaryService string - //todo, RequestHeaderModifier *gatewayv1alpha2.HTTPRequestHeaderFilter + Annotations map[string]string + Weight string + Matches []rolloutv1alpha1.HttpRouteMatch + CanaryService string + RequestHeaderModifier *gatewayv1alpha2.HTTPRequestHeaderFilter } data := &LuaData{ - Annotations: annotations, - Weight: fmt.Sprintf("%d", *weight), - Matches: matches, - CanaryService: r.conf.CanaryService, - // RequestHeaderModifier: headerModifier, + Annotations: annotations, + Weight: fmt.Sprintf("%d", *weight), + Matches: matches, + CanaryService: r.conf.CanaryService, + RequestHeaderModifier: headerModifier, } unObj, err := runtime.DefaultUnstructuredConverter.ToUnstructured(data) if err != nil { diff --git a/pkg/trafficrouting/network/ingress/ingress_test.go b/pkg/trafficrouting/network/ingress/ingress_test.go index d7c9d77f..2cec56ca 100644 --- a/pkg/trafficrouting/network/ingress/ingress_test.go +++ b/pkg/trafficrouting/network/ingress/ingress_test.go @@ -346,7 +346,7 @@ func TestEnsureRoutes(t *testing.T) { }, }, }, - /*RequestHeaderModifier: &gatewayv1alpha2.HTTPRequestHeaderFilter{ + RequestHeaderModifier: &gatewayv1alpha2.HTTPRequestHeaderFilter{ Set: []gatewayv1alpha2.HTTPHeader{ { Name: "gray", @@ -357,7 +357,7 @@ func TestEnsureRoutes(t *testing.T) { Value: "green", }, }, - },*/ + }, }, } }, @@ -368,7 +368,7 @@ func TestEnsureRoutes(t *testing.T) { expect.Annotations["nginx.ingress.kubernetes.io/canary-by-cookie"] = "demo" expect.Annotations["nginx.ingress.kubernetes.io/canary-by-header"] = "user_id" expect.Annotations["nginx.ingress.kubernetes.io/canary-by-header-value"] = "123456" - //expect.Annotations["mse.ingress.kubernetes.io/request-header-control-update"] = "gray blue\ngray green\n" + expect.Annotations["mse.ingress.kubernetes.io/request-header-control-update"] = "gray blue\ngray green\n" expect.Spec.Rules[0].HTTP.Paths = expect.Spec.Rules[0].HTTP.Paths[:1] expect.Spec.Rules[0].HTTP.Paths[0].Backend.Service.Name = "echoserver-canary" expect.Spec.Rules[1].HTTP.Paths[0].Backend.Service.Name = "echoserver-canary"