diff --git a/apis/v1alpha2/grpcroute_types.go b/apis/v1alpha2/grpcroute_types.go index a5f2110501..a1c589105f 100644 --- a/apis/v1alpha2/grpcroute_types.go +++ b/apis/v1alpha2/grpcroute_types.go @@ -427,7 +427,7 @@ type GRPCRouteFilter struct { // Support: Core // // +optional - RequestHeaderModifier *HTTPRequestHeaderFilter `json:"requestHeaderModifier,omitempty"` + RequestHeaderModifier *HTTPHeaderFilter `json:"requestHeaderModifier,omitempty"` // RequestMirror defines a schema for a filter that mirrors requests. // Requests are sent to the specified destination, but responses from diff --git a/apis/v1alpha2/httproute_types.go b/apis/v1alpha2/httproute_types.go index dfbabce7ef..62ff375d8c 100644 --- a/apis/v1alpha2/httproute_types.go +++ b/apis/v1alpha2/httproute_types.go @@ -292,10 +292,10 @@ const ( // +k8s:deepcopy-gen=false type HTTPHeader = v1beta1.HTTPHeader -// HTTPRequestHeaderFilter defines configuration for the RequestHeaderModifier -// filter. +// HTTPHeaderFilter defines a filter that modifies the headers of an HTTP request +// or response. // +k8s:deepcopy-gen=false -type HTTPRequestHeaderFilter = v1beta1.HTTPRequestHeaderFilter +type HTTPHeaderFilter = v1beta1.HTTPHeaderFilter // HTTPPathModifierType defines the type of path redirect or rewrite. // +k8s:deepcopy-gen=false diff --git a/apis/v1alpha2/validation/httproute_test.go b/apis/v1alpha2/validation/httproute_test.go index d5af02fcb0..24454e87e3 100644 --- a/apis/v1alpha2/validation/httproute_test.go +++ b/apis/v1alpha2/validation/httproute_test.go @@ -140,7 +140,7 @@ func TestValidateHTTPRoute(t *testing.T) { Filters: []gatewayv1a2.HTTPRouteFilter{ { Type: gatewayv1a2.HTTPRouteFilterRequestHeaderModifier, - RequestHeaderModifier: &gatewayv1a2.HTTPRequestHeaderFilter{ + RequestHeaderModifier: &gatewayv1a2.HTTPHeaderFilter{ Set: []gatewayv1a2.HTTPHeader{ { Name: "special-header", @@ -160,7 +160,7 @@ func TestValidateHTTPRoute(t *testing.T) { }, { Type: gatewayv1a2.HTTPRouteFilterRequestHeaderModifier, - RequestHeaderModifier: &gatewayv1a2.HTTPRequestHeaderFilter{ + RequestHeaderModifier: &gatewayv1a2.HTTPHeaderFilter{ Add: []gatewayv1a2.HTTPHeader{ { Name: "my-header", @@ -197,7 +197,7 @@ func TestValidateHTTPRoute(t *testing.T) { }, { Type: gatewayv1a2.HTTPRouteFilterRequestHeaderModifier, - RequestHeaderModifier: &gatewayv1a2.HTTPRequestHeaderFilter{ + RequestHeaderModifier: &gatewayv1a2.HTTPHeaderFilter{ Set: []gatewayv1a2.HTTPHeader{ { Name: "special-header", @@ -217,7 +217,7 @@ func TestValidateHTTPRoute(t *testing.T) { }, { Type: gatewayv1a2.HTTPRouteFilterRequestHeaderModifier, - RequestHeaderModifier: &gatewayv1a2.HTTPRequestHeaderFilter{ + RequestHeaderModifier: &gatewayv1a2.HTTPHeaderFilter{ Add: []gatewayv1a2.HTTPHeader{ { Name: "my-header", @@ -254,7 +254,7 @@ func TestValidateHTTPRoute(t *testing.T) { Filters: []gatewayv1a2.HTTPRouteFilter{ { Type: gatewayv1a2.HTTPRouteFilterRequestHeaderModifier, - RequestHeaderModifier: &gatewayv1a2.HTTPRequestHeaderFilter{ + RequestHeaderModifier: &gatewayv1a2.HTTPHeaderFilter{ Set: []gatewayv1a2.HTTPHeader{ { Name: "special-header", @@ -811,7 +811,7 @@ func TestValidateHTTPRouteTypeMatchesField(t *testing.T) { name: "valid HTTPRouteFilterRequestHeaderModifier route filter", routeFilter: gatewayv1a2.HTTPRouteFilter{ Type: gatewayv1a2.HTTPRouteFilterRequestHeaderModifier, - RequestHeaderModifier: &gatewayv1a2.HTTPRequestHeaderFilter{ + RequestHeaderModifier: &gatewayv1a2.HTTPHeaderFilter{ Set: []gatewayv1a2.HTTPHeader{{Name: "name"}}, Add: []gatewayv1a2.HTTPHeader{{Name: "add"}}, Remove: []string{"remove"}, @@ -848,7 +848,7 @@ func TestValidateHTTPRouteTypeMatchesField(t *testing.T) { name: "invalid HTTPRouteFilterRequestMirror type filter with non-matching field", routeFilter: gatewayv1a2.HTTPRouteFilter{ Type: gatewayv1a2.HTTPRouteFilterRequestMirror, - RequestHeaderModifier: &gatewayv1a2.HTTPRequestHeaderFilter{}, + RequestHeaderModifier: &gatewayv1a2.HTTPHeaderFilter{}, }, errCount: 2, }, { diff --git a/apis/v1alpha2/zz_generated.deepcopy.go b/apis/v1alpha2/zz_generated.deepcopy.go index f1e2a85f69..a7f5fa9bbe 100644 --- a/apis/v1alpha2/zz_generated.deepcopy.go +++ b/apis/v1alpha2/zz_generated.deepcopy.go @@ -131,7 +131,7 @@ func (in *GRPCRouteFilter) DeepCopyInto(out *GRPCRouteFilter) { *out = *in if in.RequestHeaderModifier != nil { in, out := &in.RequestHeaderModifier, &out.RequestHeaderModifier - *out = new(v1beta1.HTTPRequestHeaderFilter) + *out = new(v1beta1.HTTPHeaderFilter) (*in).DeepCopyInto(*out) } if in.RequestMirror != nil { diff --git a/apis/v1beta1/httproute_types.go b/apis/v1beta1/httproute_types.go index ff1d0dec09..2520c8b49a 100644 --- a/apis/v1beta1/httproute_types.go +++ b/apis/v1beta1/httproute_types.go @@ -572,7 +572,7 @@ type HTTPRouteFilter struct { // // +unionDiscriminator // +kubebuilder:validation:Enum=RequestHeaderModifier;RequestMirror;RequestRedirect;ExtensionRef - // + // Type HTTPRouteFilterType `json:"type"` // RequestHeaderModifier defines a schema for a filter that modifies request @@ -581,7 +581,16 @@ type HTTPRouteFilter struct { // Support: Core // // +optional - RequestHeaderModifier *HTTPRequestHeaderFilter `json:"requestHeaderModifier,omitempty"` + RequestHeaderModifier *HTTPHeaderFilter `json:"requestHeaderModifier,omitempty"` + + // ResponseHeaderModifier defines a schema for a filter that modifies response + // headers. + // + // Support: Extended + // + // +optional + // + ResponseHeaderModifier *HTTPHeaderFilter `json:"responseHeaderModifier,omitempty"` // RequestMirror defines a schema for a filter that mirrors requests. // Requests are sent to the specified destination, but responses from @@ -631,6 +640,15 @@ const ( // Support in HTTPBackendRef: Extended HTTPRouteFilterRequestHeaderModifier HTTPRouteFilterType = "RequestHeaderModifier" + // HTTPRouteFilterResponseHeaderModifier can be used to add or remove an HTTP + // header from an HTTP response before it is sent to the client. + // + // Support in HTTPRouteRule: Extended + // + // Support in HTTPBackendRef: Extended + // + HTTPRouteFilterResponseHeaderModifier HTTPRouteFilterType = "ResponseHeaderModifier" + // HTTPRouteFilterRequestRedirect can be used to redirect a request to // another location. This filter can also be used for HTTP to HTTPS // redirects. This may not be used on the same Route rule or BackendRef as a @@ -690,9 +708,9 @@ type HTTPHeader struct { Value string `json:"value"` } -// HTTPRequestHeaderFilter defines configuration for the RequestHeaderModifier -// filter. -type HTTPRequestHeaderFilter struct { +// HTTPHeaderFilter defines a filter that modifies the headers of an HTTP request +// or response. +type HTTPHeaderFilter struct { // Set overwrites the request with the given header (name, value) // before the action. // diff --git a/apis/v1beta1/validation/httproute.go b/apis/v1beta1/validation/httproute.go index 5425004045..bb50f91d8b 100644 --- a/apis/v1beta1/validation/httproute.go +++ b/apis/v1beta1/validation/httproute.go @@ -224,6 +224,12 @@ func validateHTTPRouteFilterTypeMatchesValue(filter gatewayv1b1.HTTPRouteFilter, if filter.RequestHeaderModifier == nil && filter.Type == gatewayv1b1.HTTPRouteFilterRequestHeaderModifier { errs = append(errs, field.Required(path, "filter.RequestHeaderModifier must be specified for RequestHeaderModifier HTTPRouteFilter.Type")) } + if filter.ResponseHeaderModifier != nil && filter.Type != gatewayv1b1.HTTPRouteFilterResponseHeaderModifier { + errs = append(errs, field.Invalid(path, filter.ResponseHeaderModifier, "must be nil if the HTTPRouteFilter.Type is not ResponseHeaderModifier")) + } + if filter.ResponseHeaderModifier == nil && filter.Type == gatewayv1b1.HTTPRouteFilterResponseHeaderModifier { + errs = append(errs, field.Required(path, "filter.ResponseHeaderModifier must be specified for ResponseHeaderModifier HTTPRouteFilter.Type")) + } if filter.RequestMirror != nil && filter.Type != gatewayv1b1.HTTPRouteFilterRequestMirror { errs = append(errs, field.Invalid(path, filter.RequestMirror, "must be nil if the HTTPRouteFilter.Type is not RequestMirror")) } diff --git a/apis/v1beta1/validation/httproute_test.go b/apis/v1beta1/validation/httproute_test.go index 89ada2ab46..e12d44a28b 100644 --- a/apis/v1beta1/validation/httproute_test.go +++ b/apis/v1beta1/validation/httproute_test.go @@ -140,7 +140,7 @@ func TestValidateHTTPRoute(t *testing.T) { Filters: []gatewayv1b1.HTTPRouteFilter{ { Type: gatewayv1b1.HTTPRouteFilterRequestHeaderModifier, - RequestHeaderModifier: &gatewayv1b1.HTTPRequestHeaderFilter{ + RequestHeaderModifier: &gatewayv1b1.HTTPHeaderFilter{ Set: []gatewayv1b1.HTTPHeader{ { Name: "special-header", @@ -160,7 +160,7 @@ func TestValidateHTTPRoute(t *testing.T) { }, { Type: gatewayv1b1.HTTPRouteFilterRequestHeaderModifier, - RequestHeaderModifier: &gatewayv1b1.HTTPRequestHeaderFilter{ + RequestHeaderModifier: &gatewayv1b1.HTTPHeaderFilter{ Add: []gatewayv1b1.HTTPHeader{ { Name: "my-header", @@ -174,7 +174,7 @@ func TestValidateHTTPRoute(t *testing.T) { }, }, { name: "invalid httpRoute with multiple duplicate filters", - errCount: 2, + errCount: 3, rules: []gatewayv1b1.HTTPRouteRule{ { Matches: []gatewayv1b1.HTTPRouteMatch{ @@ -197,7 +197,7 @@ func TestValidateHTTPRoute(t *testing.T) { }, { Type: gatewayv1b1.HTTPRouteFilterRequestHeaderModifier, - RequestHeaderModifier: &gatewayv1b1.HTTPRequestHeaderFilter{ + RequestHeaderModifier: &gatewayv1b1.HTTPHeaderFilter{ Set: []gatewayv1b1.HTTPHeader{ { Name: "special-header", @@ -217,7 +217,7 @@ func TestValidateHTTPRoute(t *testing.T) { }, { Type: gatewayv1b1.HTTPRouteFilterRequestHeaderModifier, - RequestHeaderModifier: &gatewayv1b1.HTTPRequestHeaderFilter{ + RequestHeaderModifier: &gatewayv1b1.HTTPHeaderFilter{ Add: []gatewayv1b1.HTTPHeader{ { Name: "my-header", @@ -226,6 +226,17 @@ func TestValidateHTTPRoute(t *testing.T) { }, }, }, + { + Type: gatewayv1b1.HTTPRouteFilterResponseHeaderModifier, + ResponseHeaderModifier: &gatewayv1b1.HTTPHeaderFilter{ + Add: []gatewayv1b1.HTTPHeader{ + { + Name: "extra-header", + Value: "foo", + }, + }, + }, + }, { Type: gatewayv1b1.HTTPRouteFilterRequestMirror, RequestMirror: &gatewayv1b1.HTTPRequestMirrorFilter{ @@ -235,6 +246,17 @@ func TestValidateHTTPRoute(t *testing.T) { }, }, }, + { + Type: gatewayv1b1.HTTPRouteFilterResponseHeaderModifier, + ResponseHeaderModifier: &gatewayv1b1.HTTPHeaderFilter{ + Set: []gatewayv1b1.HTTPHeader{ + { + Name: "other-header", + Value: "bat", + }, + }, + }, + }, }, }, }, @@ -254,7 +276,7 @@ func TestValidateHTTPRoute(t *testing.T) { Filters: []gatewayv1b1.HTTPRouteFilter{ { Type: gatewayv1b1.HTTPRouteFilterRequestHeaderModifier, - RequestHeaderModifier: &gatewayv1b1.HTTPRequestHeaderFilter{ + RequestHeaderModifier: &gatewayv1b1.HTTPHeaderFilter{ Set: []gatewayv1b1.HTTPHeader{ { Name: "special-header", @@ -811,7 +833,7 @@ func TestValidateHTTPRouteTypeMatchesField(t *testing.T) { name: "valid HTTPRouteFilterRequestHeaderModifier route filter", routeFilter: gatewayv1b1.HTTPRouteFilter{ Type: gatewayv1b1.HTTPRouteFilterRequestHeaderModifier, - RequestHeaderModifier: &gatewayv1b1.HTTPRequestHeaderFilter{ + RequestHeaderModifier: &gatewayv1b1.HTTPHeaderFilter{ Set: []gatewayv1b1.HTTPHeader{{Name: "name"}}, Add: []gatewayv1b1.HTTPHeader{{Name: "add"}}, Remove: []string{"remove"}, @@ -848,7 +870,7 @@ func TestValidateHTTPRouteTypeMatchesField(t *testing.T) { name: "invalid HTTPRouteFilterRequestMirror type filter with non-matching field", routeFilter: gatewayv1b1.HTTPRouteFilter{ Type: gatewayv1b1.HTTPRouteFilterRequestMirror, - RequestHeaderModifier: &gatewayv1b1.HTTPRequestHeaderFilter{}, + RequestHeaderModifier: &gatewayv1b1.HTTPHeaderFilter{}, }, errCount: 2, }, { diff --git a/apis/v1beta1/zz_generated.deepcopy.go b/apis/v1beta1/zz_generated.deepcopy.go index cbea355eae..c09f5a5578 100644 --- a/apis/v1beta1/zz_generated.deepcopy.go +++ b/apis/v1beta1/zz_generated.deepcopy.go @@ -453,6 +453,36 @@ func (in *HTTPHeader) DeepCopy() *HTTPHeader { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *HTTPHeaderFilter) DeepCopyInto(out *HTTPHeaderFilter) { + *out = *in + if in.Set != nil { + in, out := &in.Set, &out.Set + *out = make([]HTTPHeader, len(*in)) + copy(*out, *in) + } + if in.Add != nil { + in, out := &in.Add, &out.Add + *out = make([]HTTPHeader, len(*in)) + copy(*out, *in) + } + if in.Remove != nil { + in, out := &in.Remove, &out.Remove + *out = make([]string, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HTTPHeaderFilter. +func (in *HTTPHeaderFilter) DeepCopy() *HTTPHeaderFilter { + if in == nil { + return nil + } + out := new(HTTPHeaderFilter) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *HTTPHeaderMatch) DeepCopyInto(out *HTTPHeaderMatch) { *out = *in @@ -543,36 +573,6 @@ func (in *HTTPQueryParamMatch) DeepCopy() *HTTPQueryParamMatch { return out } -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *HTTPRequestHeaderFilter) DeepCopyInto(out *HTTPRequestHeaderFilter) { - *out = *in - if in.Set != nil { - in, out := &in.Set, &out.Set - *out = make([]HTTPHeader, len(*in)) - copy(*out, *in) - } - if in.Add != nil { - in, out := &in.Add, &out.Add - *out = make([]HTTPHeader, len(*in)) - copy(*out, *in) - } - if in.Remove != nil { - in, out := &in.Remove, &out.Remove - *out = make([]string, len(*in)) - copy(*out, *in) - } -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HTTPRequestHeaderFilter. -func (in *HTTPRequestHeaderFilter) DeepCopy() *HTTPRequestHeaderFilter { - if in == nil { - return nil - } - out := new(HTTPRequestHeaderFilter) - in.DeepCopyInto(out) - return out -} - // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *HTTPRequestMirrorFilter) DeepCopyInto(out *HTTPRequestMirrorFilter) { *out = *in @@ -661,7 +661,12 @@ func (in *HTTPRouteFilter) DeepCopyInto(out *HTTPRouteFilter) { *out = *in if in.RequestHeaderModifier != nil { in, out := &in.RequestHeaderModifier, &out.RequestHeaderModifier - *out = new(HTTPRequestHeaderFilter) + *out = new(HTTPHeaderFilter) + (*in).DeepCopyInto(*out) + } + if in.ResponseHeaderModifier != nil { + in, out := &in.ResponseHeaderModifier, &out.ResponseHeaderModifier + *out = new(HTTPHeaderFilter) (*in).DeepCopyInto(*out) } if in.RequestMirror != nil { diff --git a/config/crd/experimental/gateway.networking.k8s.io_httproutes.yaml b/config/crd/experimental/gateway.networking.k8s.io_httproutes.yaml index 67bebbc91b..93f34846be 100644 --- a/config/crd/experimental/gateway.networking.k8s.io_httproutes.yaml +++ b/config/crd/experimental/gateway.networking.k8s.io_httproutes.yaml @@ -583,6 +583,115 @@ spec: - 302 type: integer type: object + responseHeaderModifier: + description: "ResponseHeaderModifier defines a schema + for a filter that modifies response headers. \n + Support: Extended \n " + 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 type: description: "Type identifies the type of filter to apply. As with other API fields, types are @@ -617,6 +726,7 @@ spec: of `UnsupportedValue`. \n " enum: - RequestHeaderModifier + - ResponseHeaderModifier - RequestMirror - RequestRedirect - URLRewrite @@ -1091,6 +1201,107 @@ spec: - 302 type: integer type: object + responseHeaderModifier: + description: "ResponseHeaderModifier defines a schema + for a filter that modifies response headers. \n Support: + Extended \n " + 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 type: description: "Type identifies the type of filter to apply. As with other API fields, types are classified into @@ -1122,6 +1333,7 @@ spec: False`, with a Reason of `UnsupportedValue`. \n " enum: - RequestHeaderModifier + - ResponseHeaderModifier - RequestMirror - RequestRedirect - URLRewrite @@ -2196,6 +2408,115 @@ spec: - 302 type: integer type: object + responseHeaderModifier: + description: "ResponseHeaderModifier defines a schema + for a filter that modifies response headers. \n + Support: Extended \n " + 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 type: description: "Type identifies the type of filter to apply. As with other API fields, types are @@ -2230,6 +2551,7 @@ spec: of `UnsupportedValue`. \n " enum: - RequestHeaderModifier + - ResponseHeaderModifier - RequestMirror - RequestRedirect - URLRewrite @@ -2704,6 +3026,107 @@ spec: - 302 type: integer type: object + responseHeaderModifier: + description: "ResponseHeaderModifier defines a schema + for a filter that modifies response headers. \n Support: + Extended \n " + 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 type: description: "Type identifies the type of filter to apply. As with other API fields, types are classified into @@ -2735,6 +3158,7 @@ spec: False`, with a Reason of `UnsupportedValue`. \n " enum: - RequestHeaderModifier + - ResponseHeaderModifier - RequestMirror - RequestRedirect - URLRewrite diff --git a/conformance/tests/httproute-response-header-modifier.go b/conformance/tests/httproute-response-header-modifier.go new file mode 100644 index 0000000000..4f8ae227a2 --- /dev/null +++ b/conformance/tests/httproute-response-header-modifier.go @@ -0,0 +1,162 @@ +/* +Copyright 2022 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package tests + +import ( + "testing" + + "k8s.io/apimachinery/pkg/types" + + "sigs.k8s.io/gateway-api/conformance/utils/http" + "sigs.k8s.io/gateway-api/conformance/utils/kubernetes" + "sigs.k8s.io/gateway-api/conformance/utils/suite" +) + +func init() { + ConformanceTests = append(ConformanceTests, HTTPRouteResponseHeaderModifier) +} + +var HTTPRouteResponseHeaderModifier = suite.ConformanceTest{ + ShortName: "HTTPRouteResponseHeaderModifier", + Description: "An HTTPRoute has response header modifier filters applied correctly", + Features: []suite.SupportedFeature{suite.SupportHTTPResponseHeaderModification}, + Manifests: []string{"tests/httproute-response-header-modifier.yaml"}, + Test: func(t *testing.T, suite *suite.ConformanceTestSuite) { + ns := "gateway-conformance-infra" + routeNN := types.NamespacedName{Name: "response-header-modifier", Namespace: ns} + gwNN := types.NamespacedName{Name: "same-namespace", Namespace: ns} + gwAddr := kubernetes.GatewayAndHTTPRoutesMustBeReady(t, suite.Client, suite.TimeoutConfig, suite.ControllerName, kubernetes.NewGatewayRef(gwNN), routeNN) + + testCases := []http.ExpectedResponse{{ + Request: http.Request{ + Path: "/set", + Headers: map[string]string{ + "Some-Other-Header": "val", + }, + }, + Headers: map[string]string{ + "Some-Other-Header": "val", + "X-Header-Set": "set-overwrites-values", + }, + Backend: "infra-backend-v1", + Namespace: ns, + }, { + Request: http.Request{ + Path: "/set", + Headers: map[string]string{ + "Some-Other-Header": "val", + "X-Header-Set": "some-other-value", + }, + }, + Headers: map[string]string{ + "Some-Other-Header": "val", + "X-Header-Set": "set-overwrites-values", + }, + Backend: "infra-backend-v1", + Namespace: ns, + }, { + Request: http.Request{ + Path: "/add", + Headers: map[string]string{ + "Some-Other-Header": "val", + }, + }, + Headers: map[string]string{ + "Some-Other-Header": "val", + "X-Header-Add": "add-appends-values", + }, + Backend: "infra-backend-v1", + Namespace: ns, + }, { + Request: http.Request{ + Path: "/add", + Headers: map[string]string{ + "Some-Other-Header": "val", + "X-Header-Add": "some-other-value", + }, + }, + Headers: map[string]string{ + "Some-Other-Header": "val", + "X-Header-Add": "some-other-value,add-appends-values", + }, + Backend: "infra-backend-v1", + Namespace: ns, + }, { + Request: http.Request{ + Path: "/remove", + Headers: map[string]string{ + "X-Header-Remove": "val", + }, + }, + Headers: map[string]string{}, + AbsentHeaders: []string{"X-Header-Remove"}, + Backend: "infra-backend-v1", + Namespace: ns, + }, { + Request: http.Request{ + Path: "/multiple", + Headers: map[string]string{ + "X-Header-Set-2": "set-val-2", + "X-Header-Add-2": "add-val-2", + "X-Header-Remove-2": "remove-val-2", + "Another-Header": "another-header-val", + }, + }, + Headers: map[string]string{ + "X-Header-Set-1": "header-set-1", + "X-Header-Set-2": "header-set-2", + "X-Header-Add-1": "header-add-1", + "X-Header-Add-2": "add-val-2,header-add-2", + "X-Header-Add-3": "header-add-3", + "Another-Header": "another-header-val", + }, + AbsentHeaders: []string{"X-Header-Remove-1", "X-Header-Remove-2"}, + Backend: "infra-backend-v1", + Namespace: ns, + }, { + Request: http.Request{ + Path: "/case-insensitivity", + // The filter uses canonicalized header names, + // the request uses lowercase names. + Headers: map[string]string{ + "x-header-set": "original-val-set", + "x-header-add": "original-val-add", + "x-header-remove": "original-val-remove", + "Another-Header": "another-header-val", + }, + }, + Headers: map[string]string{ + "X-Header-Set": "header-set", + "X-Header-Add": "original-val-add,header-add", + "Another-Header": "another-header-val", + }, + AbsentHeaders: []string{"x-header-remove", "X-Header-Remove"}, + Backend: "infra-backend-v1", + Namespace: ns, + }} + + for i := range testCases { + // Declare tc here to avoid loop variable + // reuse issues across parallel tests. + tc := testCases[i] + t.Run(tc.GetTestCaseName(i), func(t *testing.T) { + t.Parallel() + http.MakeRequestAndExpectEventuallyConsistentResponse(t, suite.RoundTripper, suite.TimeoutConfig, gwAddr, tc) + }) + } + }, +} diff --git a/conformance/tests/httproute-response-header-modifier.yaml b/conformance/tests/httproute-response-header-modifier.yaml new file mode 100644 index 0000000000..94d05fb0b5 --- /dev/null +++ b/conformance/tests/httproute-response-header-modifier.yaml @@ -0,0 +1,90 @@ +apiVersion: gateway.networking.k8s.io/v1beta1 +kind: HTTPRoute +metadata: + name: request-header-modifier + namespace: gateway-conformance-infra +spec: + parentRefs: + - name: same-namespace + rules: + - matches: + - path: + type: PathPrefix + value: /set + filters: + - type: ResponseHeaderModifier + responseHeaderModifier: + set: + - name: X-Header-Set + value: set-overwrites-values + backendRefs: + - name: infra-backend-v1 + port: 8080 + - matches: + - path: + type: PathPrefix + value: /add + filters: + - type: ResponseHeaderModifier + responseHeaderModifier: + add: + - name: X-Header-Add + value: add-appends-values + backendRefs: + - name: infra-backend-v1 + port: 8080 + - matches: + - path: + type: PathPrefix + value: /remove + filters: + - type: ResponseHeaderModifier + responseHeaderModifier: + remove: + - X-Header-Remove + backendRefs: + - name: infra-backend-v1 + port: 8080 + - matches: + - path: + type: PathPrefix + value: /multiple + filters: + - type: ResponseHeaderModifier + responseHeaderModifier: + set: + - name: X-Header-Set-1 + value: header-set-1 + - name: X-Header-Set-2 + value: header-set-2 + add: + - name: X-Header-Add-1 + value: header-add-1 + - name: X-Header-Add-2 + value: header-add-2 + - name: X-Header-Add-3 + value: header-add-3 + remove: + - X-Header-Remove-1 + - X-Header-Remove-2 + backendRefs: + - name: infra-backend-v1 + port: 8080 + - matches: + - path: + type: PathPrefix + value: /case-insensitivity + filters: + - type: ResponseHeaderModifier + responseHeaderModifier: + set: + - name: X-Header-Set + value: header-set + add: + - name: X-Header-Add + value: header-add + remove: + - X-Header-Remove + backendRefs: + - name: infra-backend-v1 + port: 8080 diff --git a/conformance/utils/http/http.go b/conformance/utils/http/http.go index a4d818a400..157600a104 100644 --- a/conformance/utils/http/http.go +++ b/conformance/utils/http/http.go @@ -38,9 +38,14 @@ type ExpectedResponse struct { // expected to match Request. ExpectedRequest *ExpectedRequest - StatusCode int - Backend string - Namespace string + // TODO: move these into a dedicated type named Response + // ref: https://github.com/kubernetes-sigs/gateway-api/issues/1384 + StatusCode int + Headers map[string]string + AbsentHeaders []string + + Backend string + Namespace string // User Given TestCase name TestCaseName string @@ -209,6 +214,37 @@ func CompareRequest(cReq *roundtripper.CapturedRequest, cRes *roundtripper.Captu } } + if expected.Headers != nil { + if cRes.Headers == nil { + return fmt.Errorf("no headers captured, expected %v", len(expected.ExpectedRequest.Headers)) + } + for name, val := range cRes.Headers { + cRes.Headers[strings.ToLower(name)] = val + } + + for name, expectedVal := range expected.Headers { + actualVal, ok := cRes.Headers[strings.ToLower(name)] + if !ok { + return fmt.Errorf("expected %s header to be set, actual headers: %v", name, cRes.Headers) + } else if strings.Join(actualVal, ",") != expectedVal { + return fmt.Errorf("expected %s header to be set to %s, got %s", name, expectedVal, strings.Join(actualVal, ",")) + } + } + } + + if len(expected.AbsentHeaders) > 0 { + for name, val := range cRes.Headers { + cRes.Headers[strings.ToLower(name)] = val + } + + for _, name := range expected.AbsentHeaders { + val, ok := cRes.Headers[strings.ToLower(name)] + if ok { + return fmt.Errorf("expected %s header to not be set, got %s", name, val) + } + } + } + // Verify that headers expected *not* to be present on the // request are actually not present. if len(expected.ExpectedRequest.AbsentHeaders) > 0 { diff --git a/conformance/utils/suite/suite.go b/conformance/utils/suite/suite.go index 4f2b2e69a8..8c34d4bed2 100644 --- a/conformance/utils/suite/suite.go +++ b/conformance/utils/suite/suite.go @@ -52,6 +52,9 @@ const ( // This option indicates support for HTTPRoute method matching (extended conformance). SupportHTTPRouteMethodMatching SupportedFeature = "HTTPRouteMethodMatching" + + // This option indicates support for HTTPRoute response header modification (extended conformance). + SupportHTTPResponseHeaderModification SupportedFeature = "HTTPResponseHeaderModification" ) // ConformanceTestSuite defines the test suite used to run Gateway API