diff --git a/apis/v1beta1/httproute_types.go b/apis/v1beta1/httproute_types.go index ff9e6a041b..32af274c35 100644 --- a/apis/v1beta1/httproute_types.go +++ b/apis/v1beta1/httproute_types.go @@ -125,6 +125,12 @@ type HTTPRouteSpec struct { // HTTPRouteRule defines semantics for matching an HTTP request based on // conditions (matches), processing it (filters), and forwarding the request to // an API object (backendRefs). +// +// +kubebuilder:validation:XValidation:message="RequestRedirect filter must not be used together with backendRefs",rule="(has(self.backendRefs) && size(self.backendRefs) > 0) ? (!has(self.filters) || self.filters.all(f, !has(f.requestRedirect))): true" +// +kubebuilder:validation:XValidation:message="When using RequestRedirect filter with Path.ReplacePrefixMatch, exactly one PathPrefix match must be specified",rule="(has(self.filters) && self.filters.exists_one(f, has(f.requestRedirect) && has(f.requestRedirect.path) && f.requestRedirect.path.type == 'ReplacePrefixMatch' && has(f.requestRedirect.path.replacePrefixMatch))) ? ((size(self.matches) != 1 || !has(self.matches[0].path) || self.matches[0].path.type != 'PathPrefix') ? false : true) : true" +// +kubebuilder:validation:XValidation:message="When using URLRewrite filter with Path.ReplacePrefixMatch, exactly one PathPrefix match must be specified",rule="(has(self.filters) && self.filters.exists_one(f, has(f.urlRewrite) && has(f.urlRewrite.path) && f.urlRewrite.path.type == 'ReplacePrefixMatch' && has(f.urlRewrite.path.replacePrefixMatch))) ? ((size(self.matches) != 1 || !has(self.matches[0].path) || self.matches[0].path.type != 'PathPrefix') ? false : true) : true" +// +kubebuilder:validation:XValidation:message="Within BackendRefs, when using RequestRedirect filter with Path.ReplacePrefixMatch, exactly one PathPrefix match must be specified",rule="(has(self.backendRefs) && self.backendRefs.exists_one(b, (has(b.filters) && b.filters.exists_one(f, has(f.requestRedirect) && has(f.requestRedirect.path) && f.requestRedirect.path.type == 'ReplacePrefixMatch' && has(f.requestRedirect.path.replacePrefixMatch))) )) ? ((size(self.matches) != 1 || !has(self.matches[0].path) || self.matches[0].path.type != 'PathPrefix') ? false : true) : true" +// +kubebuilder:validation:XValidation:message="Within BackendRefs, When using URLRewrite filter with Path.ReplacePrefixMatch, exactly one PathPrefix match must be specified",rule="(has(self.backendRefs) && self.backendRefs.exists_one(b, (has(b.filters) && b.filters.exists_one(f, has(f.urlRewrite) && has(f.urlRewrite.path) && f.urlRewrite.path.type == 'ReplacePrefixMatch' && has(f.urlRewrite.path.replacePrefixMatch))) )) ? ((size(self.matches) != 1 || !has(self.matches[0].path) || self.matches[0].path.type != 'PathPrefix') ? false : true) : true" type HTTPRouteRule struct { // Matches define conditions used for matching the rule against incoming // HTTP requests. Each match is independent, i.e. this rule will be matched @@ -216,6 +222,11 @@ type HTTPRouteRule struct { // // +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="RequestHeaderModifier filter cannot be repeated",rule="self.exists(f, f.type == 'RequestHeaderModifier') ? self.exists_one(f, f.type == 'RequestHeaderModifier') : true" + // +kubebuilder:validation:XValidation:message="ResponseHeaderModifier filter cannot be repeated",rule="self.exists(f, f.type == 'ResponseHeaderModifier') ? self.exists_one(f, f.type == 'ResponseHeaderModifier') : true" + // +kubebuilder:validation:XValidation:message="RequestRedirect filter cannot be repeated",rule="self.exists(f, f.type == 'RequestRedirect') ? self.exists_one(f, f.type == 'RequestRedirect') : true" + // +kubebuilder:validation:XValidation:message="URLRewrite filter cannot be repeated",rule="self.exists(f, f.type == 'URLRewrite') ? self.exists_one(f, f.type == 'URLRewrite') : true" Filters []HTTPRouteFilter `json:"filters,omitempty"` // BackendRefs defines the backend(s) where matching requests should be @@ -306,6 +317,18 @@ const ( ) // HTTPPathMatch describes how to select a HTTP route by matching the HTTP request path. +// +// +kubebuilder:validation:XValidation:message="value must be an absolute path and start with '/' when type one of ['Exact', 'PathPrefix']",rule="(self.type == 'Exact' || self.type == 'PathPrefix') ? self.value.startsWith('/') : true" +// +kubebuilder:validation:XValidation:message="must not contain '//' when type one of ['Exact', 'PathPrefix']",rule="(self.type == 'Exact' || self.type == 'PathPrefix') ? !self.value.contains('//') : true" +// +kubebuilder:validation:XValidation:message="must not contain '/./' when type one of ['Exact', 'PathPrefix']",rule="(self.type == 'Exact' || self.type == 'PathPrefix') ? !self.value.contains('/./') : true" +// +kubebuilder:validation:XValidation:message="must not contain '/../' when type one of ['Exact', 'PathPrefix']",rule="(self.type == 'Exact' || self.type == 'PathPrefix') ? !self.value.contains('/../') : true" +// +kubebuilder:validation:XValidation:message="must not contain '%2f' when type one of ['Exact', 'PathPrefix']",rule="(self.type == 'Exact' || self.type == 'PathPrefix') ? !self.value.contains('%2f') : true" +// +kubebuilder:validation:XValidation:message="must not contain '%2F' when type one of ['Exact', 'PathPrefix']",rule="(self.type == 'Exact' || self.type == 'PathPrefix') ? !self.value.contains('%2F') : true" +// +kubebuilder:validation:XValidation:message="must not contain '#' when type one of ['Exact', 'PathPrefix']",rule="(self.type == 'Exact' || self.type == 'PathPrefix') ? !self.value.contains('#') : true" +// +kubebuilder:validation:XValidation:message="must not end with '/..' when type one of ['Exact', 'PathPrefix']",rule="(self.type == 'Exact' || self.type == 'PathPrefix') ? !self.value.endsWith('/..') : true" +// +kubebuilder:validation:XValidation:message="must not end with '/.' when type one of ['Exact', 'PathPrefix']",rule="(self.type == 'Exact' || self.type == 'PathPrefix') ? !self.value.endsWith('/.') : true" +// +kubebuilder:validation:XValidation:message="type must be one of ['Exact', 'PathPrefix', 'RegularExpression']",rule="self.type == 'Exact' || self.type == 'PathPrefix' || self.type == 'RegularExpression'" +// +kubebuilder:validation:XValidation:message="must only contain valid characters (matching ^(?:[-A-Za-z0-9/._~!$&'()*+,;=:@]|[%][0-9a-fA-F]{2})+$) for types ['Exact', 'PathPrefix']",rule="(self.type == 'Exact' || self.type == 'PathPrefix') ? self.value.matches(r\"\"\"^(?:[-A-Za-z0-9/._~!$&'()*+,;=:@]|[%][0-9a-fA-F]{2})+$\"\"\") : true" type HTTPPathMatch struct { // Type specifies how to match against the path Value. // @@ -560,6 +583,19 @@ type HTTPRouteMatch struct { // examples include request or response modification, implementing // authentication strategies, rate-limiting, and traffic shaping. API // guarantee/conformance is defined based on the type of the filter. +// +// +kubebuilder:validation:XValidation:message="filter.RequestHeaderModifier must be nil if the HTTPRouteFilter.Type is not RequestHeaderModifier",rule="!(has(self.requestHeaderModifier) && self.type != 'RequestHeaderModifier')" +// +kubebuilder:validation:XValidation:message="filter.RequestHeaderModifier must be specified for RequestHeaderModifier HTTPRouteFilter.Type",rule="!(!has(self.requestHeaderModifier) && self.type == 'RequestHeaderModifier')" +// +kubebuilder:validation:XValidation:message="filter.ResponseHeaderModifier must be nil if the HTTPRouteFilter.Type is not ResponseHeaderModifier",rule="!(has(self.responseHeaderModifier) && self.type != 'ResponseHeaderModifier')" +// +kubebuilder:validation:XValidation:message="filter.ResponseHeaderModifier must be specified for ResponseHeaderModifier HTTPRouteFilter.Type",rule="!(!has(self.responseHeaderModifier) && self.type == 'ResponseHeaderModifier')" +// +kubebuilder:validation:XValidation:message="filter.RequestMirror must be nil if the HTTPRouteFilter.Type is not RequestMirror",rule="!(has(self.requestMirror) && self.type != 'RequestMirror')" +// +kubebuilder:validation:XValidation:message="filter.RequestMirror must be specified for RequestMirror HTTPRouteFilter.Type",rule="!(!has(self.requestMirror) && self.type == 'RequestMirror')" +// +kubebuilder:validation:XValidation:message="filter.RequestRedirect must be nil if the HTTPRouteFilter.Type is not RequestRedirect",rule="!(has(self.requestRedirect) && self.type != 'RequestRedirect')" +// +kubebuilder:validation:XValidation:message="filter.RequestRedirect must be specified for RequestRedirect HTTPRouteFilter.Type",rule="!(!has(self.requestRedirect) && self.type == 'RequestRedirect')" +// +kubebuilder:validation:XValidation:message="filter.URLRewrite must be nil if the HTTPRouteFilter.Type is not URLRewrite",rule="!(has(self.urlRewrite) && self.type != 'URLRewrite')" +// +kubebuilder:validation:XValidation:message="filter.URLRewrite must be specified for URLRewrite HTTPRouteFilter.Type",rule="!(!has(self.urlRewrite) && self.type == 'URLRewrite')" +// +kubebuilder:validation:XValidation:message="filter.ExtensionRef must be nil if the HTTPRouteFilter.Type is not ExtensionRef",rule="!(has(self.extensionRef) && self.type != 'ExtensionRef')" +// +kubebuilder:validation:XValidation:message="filter.ExtensionRef must be specified for ExtensionRef HTTPRouteFilter.Type",rule="!(!has(self.extensionRef) && self.type == 'ExtensionRef')" type HTTPRouteFilter struct { // Type identifies the type of filter to apply. As with other API fields, // types are classified into three conformance levels: @@ -829,6 +865,11 @@ const ( ) // HTTPPathModifier defines configuration for path modifiers. +// +// +kubebuilder:validation:XValidation:message="replaceFullPath must be set when type is set to 'ReplaceFullPath'",rule="self.type == 'ReplaceFullPath' ? has(self.replaceFullPath) : true" +// +kubebuilder:validation:XValidation:message="type must be 'ReplaceFullPath' when replaceFullPath is set",rule="has(self.replaceFullPath) ? self.type == 'ReplaceFullPath' : true" +// +kubebuilder:validation:XValidation:message="replacePrefixMatch must be set when type is set to 'ReplacePrefixMatch'",rule="self.type == 'ReplacePrefixMatch' ? has(self.replacePrefixMatch) : true" +// +kubebuilder:validation:XValidation:message="type must be 'ReplacePrefixMatch' when replacePrefixMatch is set",rule="has(self.replacePrefixMatch) ? self.type == 'ReplacePrefixMatch' : true" type HTTPPathModifier struct { // Type defines the type of path modifier. Additional types may be // added in a future release of the API. @@ -1054,6 +1095,12 @@ type HTTPBackendRef struct { // // +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.exists(f, f.type == 'RequestHeaderModifier') ? self.exists_one(f, f.type == 'RequestHeaderModifier') : true" + // +kubebuilder:validation:XValidation:message="ResponseHeaderModifier filter cannot be repeated",rule="self.exists(f, f.type == 'ResponseHeaderModifier') ? self.exists_one(f, f.type == 'ResponseHeaderModifier') : true" + // +kubebuilder:validation:XValidation:message="RequestRedirect filter cannot be repeated",rule="self.exists(f, f.type == 'RequestRedirect') ? self.exists_one(f, f.type == 'RequestRedirect') : true" + // +kubebuilder:validation:XValidation:message="URLRewrite filter cannot be repeated",rule="self.exists(f, f.type == 'URLRewrite') ? self.exists_one(f, f.type == 'URLRewrite') : true" Filters []HTTPRouteFilter `json:"filters,omitempty"` } diff --git a/apis/v1beta1/object_reference_types.go b/apis/v1beta1/object_reference_types.go index 229b27f38f..fd5a199332 100644 --- a/apis/v1beta1/object_reference_types.go +++ b/apis/v1beta1/object_reference_types.go @@ -91,6 +91,8 @@ type SecretObjectReference struct { // References to objects with invalid Group and Kind are not valid, and must // be rejected by the implementation, with appropriate Conditions set // on the containing object. +// +// +kubebuilder:validation:XValidation:message="Must have port for Service reference",rule="((!has(self.group) || size(self.group) == 0) && (!has(self.kind) || self.kind == 'Service')) ? has(self.port) : true" type BackendObjectReference struct { // Group is the group of the referent. For example, "gateway.networking.k8s.io". // When unspecified or empty string, core API group is inferred. diff --git a/config/crd/experimental/gateway.networking.k8s.io_grpcroutes.yaml b/config/crd/experimental/gateway.networking.k8s.io_grpcroutes.yaml index bc42220b70..4a6e76abf9 100644 --- a/config/crd/experimental/gateway.networking.k8s.io_grpcroutes.yaml +++ b/config/crd/experimental/gateway.networking.k8s.io_grpcroutes.yaml @@ -558,6 +558,11 @@ spec: required: - name type: object + x-kubernetes-validations: + - message: Must have port for Service reference + rule: '((!has(self.group) || size(self.group) + == 0) && (!has(self.kind) || self.kind == + ''Service'')) ? has(self.port) : true' required: - backendRef type: object @@ -781,6 +786,10 @@ spec: required: - name type: object + x-kubernetes-validations: + - message: Must have port for Service reference + rule: '((!has(self.group) || size(self.group) == 0) && (!has(self.kind) + || self.kind == ''Service'')) ? has(self.port) : true' maxItems: 16 type: array filters: @@ -1027,6 +1036,11 @@ spec: required: - name type: object + x-kubernetes-validations: + - message: Must have port for Service reference + rule: '((!has(self.group) || size(self.group) == + 0) && (!has(self.kind) || self.kind == ''Service'')) + ? has(self.port) : true' required: - backendRef type: object diff --git a/config/crd/experimental/gateway.networking.k8s.io_httproutes.yaml b/config/crd/experimental/gateway.networking.k8s.io_httproutes.yaml index 2d71ee1bee..1c71104596 100644 --- a/config/crd/experimental/gateway.networking.k8s.io_httproutes.yaml +++ b/config/crd/experimental/gateway.networking.k8s.io_httproutes.yaml @@ -550,6 +550,11 @@ spec: required: - name type: object + x-kubernetes-validations: + - message: Must have port for Service reference + rule: '((!has(self.group) || size(self.group) + == 0) && (!has(self.kind) || self.kind == + ''Service'')) ? has(self.port) : true' required: - backendRef type: object @@ -639,6 +644,23 @@ spec: required: - type type: object + x-kubernetes-validations: + - message: replaceFullPath must be set when + type is set to 'ReplaceFullPath' + rule: 'self.type == ''ReplaceFullPath'' ? + has(self.replaceFullPath) : true' + - message: type must be 'ReplaceFullPath' when + replaceFullPath is set + rule: 'has(self.replaceFullPath) ? self.type + == ''ReplaceFullPath'' : true' + - message: replacePrefixMatch must be set when + type is set to 'ReplacePrefixMatch' + rule: 'self.type == ''ReplacePrefixMatch'' + ? has(self.replacePrefixMatch) : true' + - message: type must be 'ReplacePrefixMatch' + when replacePrefixMatch is set + rule: 'has(self.replacePrefixMatch) ? self.type + == ''ReplacePrefixMatch'' : true' port: description: "Port is the port to be used in the value of the `Location` header in the @@ -927,12 +949,97 @@ spec: required: - type type: object + x-kubernetes-validations: + - message: replaceFullPath must be set when + type is set to 'ReplaceFullPath' + rule: 'self.type == ''ReplaceFullPath'' ? + has(self.replaceFullPath) : true' + - message: type must be 'ReplaceFullPath' when + replaceFullPath is set + rule: 'has(self.replaceFullPath) ? self.type + == ''ReplaceFullPath'' : true' + - message: replacePrefixMatch must be set when + type is set to 'ReplacePrefixMatch' + rule: 'self.type == ''ReplacePrefixMatch'' + ? has(self.replacePrefixMatch) : true' + - message: type must be 'ReplacePrefixMatch' + when replacePrefixMatch is set + rule: 'has(self.replacePrefixMatch) ? self.type + == ''ReplacePrefixMatch'' : true' type: object required: - type type: object + x-kubernetes-validations: + - message: filter.RequestHeaderModifier must be nil + if the HTTPRouteFilter.Type is not RequestHeaderModifier + rule: '!(has(self.requestHeaderModifier) && self.type + != ''RequestHeaderModifier'')' + - message: filter.RequestHeaderModifier must be specified + for RequestHeaderModifier HTTPRouteFilter.Type + rule: '!(!has(self.requestHeaderModifier) && self.type + == ''RequestHeaderModifier'')' + - message: filter.ResponseHeaderModifier must be nil + if the HTTPRouteFilter.Type is not ResponseHeaderModifier + rule: '!(has(self.responseHeaderModifier) && self.type + != ''ResponseHeaderModifier'')' + - message: filter.ResponseHeaderModifier must be specified + for ResponseHeaderModifier HTTPRouteFilter.Type + rule: '!(!has(self.responseHeaderModifier) && self.type + == ''ResponseHeaderModifier'')' + - message: filter.RequestMirror must be nil if the HTTPRouteFilter.Type + is not RequestMirror + rule: '!(has(self.requestMirror) && self.type != ''RequestMirror'')' + - message: filter.RequestMirror must be specified for + RequestMirror HTTPRouteFilter.Type + rule: '!(!has(self.requestMirror) && self.type == + ''RequestMirror'')' + - message: filter.RequestRedirect must be nil if the + HTTPRouteFilter.Type is not RequestRedirect + rule: '!(has(self.requestRedirect) && self.type != + ''RequestRedirect'')' + - message: filter.RequestRedirect must be specified + for RequestRedirect HTTPRouteFilter.Type + rule: '!(!has(self.requestRedirect) && self.type == + ''RequestRedirect'')' + - message: filter.URLRewrite must be nil if the HTTPRouteFilter.Type + is not URLRewrite + rule: '!(has(self.urlRewrite) && self.type != ''URLRewrite'')' + - message: filter.URLRewrite must be specified for URLRewrite + HTTPRouteFilter.Type + rule: '!(!has(self.urlRewrite) && self.type == ''URLRewrite'')' + - message: filter.ExtensionRef must be nil if the HTTPRouteFilter.Type + is not ExtensionRef + rule: '!(has(self.extensionRef) && self.type != ''ExtensionRef'')' + - message: filter.ExtensionRef must be specified for + ExtensionRef HTTPRouteFilter.Type + rule: '!(!has(self.extensionRef) && self.type == ''ExtensionRef'')' maxItems: 16 type: array + x-kubernetes-validations: + - message: May specify either httpRouteFilterRequestRedirect + or httpRouteFilterRequestRewrite, but not both + rule: '!(self.exists(f, f.type == ''RequestRedirect'') + && self.exists(f, f.type == ''URLRewrite''))' + - message: May specify either httpRouteFilterRequestRedirect + or httpRouteFilterRequestRewrite, but not both + rule: '!(self.exists(f, f.type == ''RequestRedirect'') + && self.exists(f, f.type == ''URLRewrite''))' + - message: RequestHeaderModifier filter cannot be repeated + rule: 'self.exists(f, f.type == ''RequestHeaderModifier'') + ? self.exists_one(f, f.type == ''RequestHeaderModifier'') + : true' + - message: ResponseHeaderModifier filter cannot be repeated + rule: 'self.exists(f, f.type == ''ResponseHeaderModifier'') + ? self.exists_one(f, f.type == ''ResponseHeaderModifier'') + : true' + - message: RequestRedirect filter cannot be repeated + rule: 'self.exists(f, f.type == ''RequestRedirect'') + ? self.exists_one(f, f.type == ''RequestRedirect'') + : true' + - message: URLRewrite filter cannot be repeated + rule: 'self.exists(f, f.type == ''URLRewrite'') ? self.exists_one(f, + f.type == ''URLRewrite'') : true' group: default: "" description: Group is the group of the referent. For example, @@ -1008,6 +1115,10 @@ spec: required: - name type: object + x-kubernetes-validations: + - message: Must have port for Service reference + rule: '((!has(self.group) || size(self.group) == 0) && (!has(self.kind) + || self.kind == ''Service'')) ? has(self.port) : true' maxItems: 16 type: array filters: @@ -1260,6 +1371,11 @@ spec: required: - name type: object + x-kubernetes-validations: + - message: Must have port for Service reference + rule: '((!has(self.group) || size(self.group) == + 0) && (!has(self.kind) || self.kind == ''Service'')) + ? has(self.port) : true' required: - backendRef type: object @@ -1342,6 +1458,23 @@ spec: required: - type type: object + x-kubernetes-validations: + - message: replaceFullPath must be set when type is + set to 'ReplaceFullPath' + rule: 'self.type == ''ReplaceFullPath'' ? has(self.replaceFullPath) + : true' + - message: type must be 'ReplaceFullPath' when replaceFullPath + is set + rule: 'has(self.replaceFullPath) ? self.type == + ''ReplaceFullPath'' : true' + - message: replacePrefixMatch must be set when type + is set to 'ReplacePrefixMatch' + rule: 'self.type == ''ReplacePrefixMatch'' ? has(self.replacePrefixMatch) + : true' + - message: type must be 'ReplacePrefixMatch' when + replacePrefixMatch is set + rule: 'has(self.replacePrefixMatch) ? self.type + == ''ReplacePrefixMatch'' : true' port: description: "Port is the port to be used in the value of the `Location` header in the response. \n If @@ -1609,12 +1742,89 @@ spec: required: - type type: object + x-kubernetes-validations: + - message: replaceFullPath must be set when type is + set to 'ReplaceFullPath' + rule: 'self.type == ''ReplaceFullPath'' ? has(self.replaceFullPath) + : true' + - message: type must be 'ReplaceFullPath' when replaceFullPath + is set + rule: 'has(self.replaceFullPath) ? self.type == + ''ReplaceFullPath'' : true' + - message: replacePrefixMatch must be set when type + is set to 'ReplacePrefixMatch' + rule: 'self.type == ''ReplacePrefixMatch'' ? has(self.replacePrefixMatch) + : true' + - message: type must be 'ReplacePrefixMatch' when + replacePrefixMatch is set + rule: 'has(self.replacePrefixMatch) ? self.type + == ''ReplacePrefixMatch'' : true' type: object required: - type type: object + x-kubernetes-validations: + - message: filter.RequestHeaderModifier must be nil if the + HTTPRouteFilter.Type is not RequestHeaderModifier + rule: '!(has(self.requestHeaderModifier) && self.type != + ''RequestHeaderModifier'')' + - message: filter.RequestHeaderModifier must be specified + for RequestHeaderModifier HTTPRouteFilter.Type + rule: '!(!has(self.requestHeaderModifier) && self.type == + ''RequestHeaderModifier'')' + - message: filter.ResponseHeaderModifier must be nil if the + HTTPRouteFilter.Type is not ResponseHeaderModifier + rule: '!(has(self.responseHeaderModifier) && self.type != + ''ResponseHeaderModifier'')' + - message: filter.ResponseHeaderModifier must be specified + for ResponseHeaderModifier HTTPRouteFilter.Type + rule: '!(!has(self.responseHeaderModifier) && self.type + == ''ResponseHeaderModifier'')' + - message: filter.RequestMirror must be nil if the HTTPRouteFilter.Type + is not RequestMirror + rule: '!(has(self.requestMirror) && self.type != ''RequestMirror'')' + - message: filter.RequestMirror must be specified for RequestMirror + HTTPRouteFilter.Type + rule: '!(!has(self.requestMirror) && self.type == ''RequestMirror'')' + - message: filter.RequestRedirect must be nil if the HTTPRouteFilter.Type + is not RequestRedirect + rule: '!(has(self.requestRedirect) && self.type != ''RequestRedirect'')' + - message: filter.RequestRedirect must be specified for RequestRedirect + HTTPRouteFilter.Type + rule: '!(!has(self.requestRedirect) && self.type == ''RequestRedirect'')' + - message: filter.URLRewrite must be nil if the HTTPRouteFilter.Type + is not URLRewrite + rule: '!(has(self.urlRewrite) && self.type != ''URLRewrite'')' + - message: filter.URLRewrite must be specified for URLRewrite + HTTPRouteFilter.Type + rule: '!(!has(self.urlRewrite) && self.type == ''URLRewrite'')' + - message: filter.ExtensionRef must be nil if the HTTPRouteFilter.Type + is not ExtensionRef + rule: '!(has(self.extensionRef) && self.type != ''ExtensionRef'')' + - message: filter.ExtensionRef must be specified for ExtensionRef + HTTPRouteFilter.Type + rule: '!(!has(self.extensionRef) && self.type == ''ExtensionRef'')' maxItems: 16 type: array + x-kubernetes-validations: + - message: May specify either httpRouteFilterRequestRedirect + or httpRouteFilterRequestRewrite, but not both + rule: '!(self.exists(f, f.type == ''RequestRedirect'') && + self.exists(f, f.type == ''URLRewrite''))' + - message: RequestHeaderModifier filter cannot be repeated + rule: 'self.exists(f, f.type == ''RequestHeaderModifier'') + ? self.exists_one(f, f.type == ''RequestHeaderModifier'') + : true' + - message: ResponseHeaderModifier filter cannot be repeated + rule: 'self.exists(f, f.type == ''ResponseHeaderModifier'') + ? self.exists_one(f, f.type == ''ResponseHeaderModifier'') + : true' + - message: RequestRedirect filter cannot be repeated + rule: 'self.exists(f, f.type == ''RequestRedirect'') ? self.exists_one(f, + f.type == ''RequestRedirect'') : true' + - message: URLRewrite filter cannot be repeated + rule: 'self.exists(f, f.type == ''URLRewrite'') ? self.exists_one(f, + f.type == ''URLRewrite'') : true' matches: default: - path: @@ -1758,6 +1968,53 @@ spec: maxLength: 1024 type: string type: object + x-kubernetes-validations: + - message: value must be an absolute path and start with + '/' when type one of ['Exact', 'PathPrefix'] + rule: '(self.type == ''Exact'' || self.type == ''PathPrefix'') + ? self.value.startsWith(''/'') : true' + - message: must not contain '//' when type one of ['Exact', + 'PathPrefix'] + rule: '(self.type == ''Exact'' || self.type == ''PathPrefix'') + ? !self.value.contains(''//'') : true' + - message: must not contain '/./' when type one of ['Exact', + 'PathPrefix'] + rule: '(self.type == ''Exact'' || self.type == ''PathPrefix'') + ? !self.value.contains(''/./'') : true' + - message: must not contain '/../' when type one of ['Exact', + 'PathPrefix'] + rule: '(self.type == ''Exact'' || self.type == ''PathPrefix'') + ? !self.value.contains(''/../'') : true' + - message: must not contain '%2f' when type one of ['Exact', + 'PathPrefix'] + rule: '(self.type == ''Exact'' || self.type == ''PathPrefix'') + ? !self.value.contains(''%2f'') : true' + - message: must not contain '%2F' when type one of ['Exact', + 'PathPrefix'] + rule: '(self.type == ''Exact'' || self.type == ''PathPrefix'') + ? !self.value.contains(''%2F'') : true' + - message: must not contain '#' when type one of ['Exact', + 'PathPrefix'] + rule: '(self.type == ''Exact'' || self.type == ''PathPrefix'') + ? !self.value.contains(''#'') : true' + - message: must not end with '/..' when type one of ['Exact', + 'PathPrefix'] + rule: '(self.type == ''Exact'' || self.type == ''PathPrefix'') + ? !self.value.endsWith(''/..'') : true' + - message: must not end with '/.' when type one of ['Exact', + 'PathPrefix'] + rule: '(self.type == ''Exact'' || self.type == ''PathPrefix'') + ? !self.value.endsWith(''/.'') : true' + - message: type must be one of ['Exact', 'PathPrefix', + 'RegularExpression'] + rule: self.type == 'Exact' || self.type == 'PathPrefix' + || self.type == 'RegularExpression' + - message: must only contain valid characters (matching + ^(?:[-A-Za-z0-9/._~!$&'()*+,;=:@]|[%][0-9a-fA-F]{2})+$) + for types ['Exact', 'PathPrefix'] + rule: '(self.type == ''Exact'' || self.type == ''PathPrefix'') + ? self.value.matches(r"""^(?:[-A-Za-z0-9/._~!$&''()*+,;=:@]|[%][0-9a-fA-F]{2})+$""") + : true' queryParams: description: "QueryParams specifies HTTP query parameter matchers. Multiple match values are ANDed together, @@ -1824,6 +2081,46 @@ spec: maxItems: 8 type: array type: object + x-kubernetes-validations: + - message: RequestRedirect filter must not be used together with + backendRefs + rule: '(has(self.backendRefs) && size(self.backendRefs) > 0) ? + (!has(self.filters) || self.filters.all(f, !has(f.requestRedirect))): + true' + - message: When using RequestRedirect filter with Path.ReplacePrefixMatch, + exactly one PathPrefix match must be specified + rule: '(has(self.filters) && self.filters.exists_one(f, has(f.requestRedirect) + && has(f.requestRedirect.path) && f.requestRedirect.path.type + == ''ReplacePrefixMatch'' && has(f.requestRedirect.path.replacePrefixMatch))) + ? ((size(self.matches) != 1 || !has(self.matches[0].path) || + self.matches[0].path.type != ''PathPrefix'') ? false : true) + : true' + - message: When using URLRewrite filter with Path.ReplacePrefixMatch, + exactly one PathPrefix match must be specified + rule: '(has(self.filters) && self.filters.exists_one(f, has(f.urlRewrite) + && has(f.urlRewrite.path) && f.urlRewrite.path.type == ''ReplacePrefixMatch'' + && has(f.urlRewrite.path.replacePrefixMatch))) ? ((size(self.matches) + != 1 || !has(self.matches[0].path) || self.matches[0].path.type + != ''PathPrefix'') ? false : true) : true' + - message: Within BackendRefs, when using RequestRedirect filter + with Path.ReplacePrefixMatch, exactly one PathPrefix match must + be specified + rule: '(has(self.backendRefs) && self.backendRefs.exists_one(b, + (has(b.filters) && b.filters.exists_one(f, has(f.requestRedirect) + && has(f.requestRedirect.path) && f.requestRedirect.path.type + == ''ReplacePrefixMatch'' && has(f.requestRedirect.path.replacePrefixMatch))) + )) ? ((size(self.matches) != 1 || !has(self.matches[0].path) + || self.matches[0].path.type != ''PathPrefix'') ? false : true) + : true' + - message: Within BackendRefs, When using URLRewrite filter with + Path.ReplacePrefixMatch, exactly one PathPrefix match must be + specified + rule: '(has(self.backendRefs) && self.backendRefs.exists_one(b, + (has(b.filters) && b.filters.exists_one(f, has(f.urlRewrite) + && has(f.urlRewrite.path) && f.urlRewrite.path.type == ''ReplacePrefixMatch'' + && has(f.urlRewrite.path.replacePrefixMatch))) )) ? ((size(self.matches) + != 1 || !has(self.matches[0].path) || self.matches[0].path.type + != ''PathPrefix'') ? false : true) : true' maxItems: 16 type: array type: object @@ -2614,6 +2911,11 @@ spec: required: - name type: object + x-kubernetes-validations: + - message: Must have port for Service reference + rule: '((!has(self.group) || size(self.group) + == 0) && (!has(self.kind) || self.kind == + ''Service'')) ? has(self.port) : true' required: - backendRef type: object @@ -2703,6 +3005,23 @@ spec: required: - type type: object + x-kubernetes-validations: + - message: replaceFullPath must be set when + type is set to 'ReplaceFullPath' + rule: 'self.type == ''ReplaceFullPath'' ? + has(self.replaceFullPath) : true' + - message: type must be 'ReplaceFullPath' when + replaceFullPath is set + rule: 'has(self.replaceFullPath) ? self.type + == ''ReplaceFullPath'' : true' + - message: replacePrefixMatch must be set when + type is set to 'ReplacePrefixMatch' + rule: 'self.type == ''ReplacePrefixMatch'' + ? has(self.replacePrefixMatch) : true' + - message: type must be 'ReplacePrefixMatch' + when replacePrefixMatch is set + rule: 'has(self.replacePrefixMatch) ? self.type + == ''ReplacePrefixMatch'' : true' port: description: "Port is the port to be used in the value of the `Location` header in the @@ -2991,12 +3310,97 @@ spec: required: - type type: object + x-kubernetes-validations: + - message: replaceFullPath must be set when + type is set to 'ReplaceFullPath' + rule: 'self.type == ''ReplaceFullPath'' ? + has(self.replaceFullPath) : true' + - message: type must be 'ReplaceFullPath' when + replaceFullPath is set + rule: 'has(self.replaceFullPath) ? self.type + == ''ReplaceFullPath'' : true' + - message: replacePrefixMatch must be set when + type is set to 'ReplacePrefixMatch' + rule: 'self.type == ''ReplacePrefixMatch'' + ? has(self.replacePrefixMatch) : true' + - message: type must be 'ReplacePrefixMatch' + when replacePrefixMatch is set + rule: 'has(self.replacePrefixMatch) ? self.type + == ''ReplacePrefixMatch'' : true' type: object required: - type type: object + x-kubernetes-validations: + - message: filter.RequestHeaderModifier must be nil + if the HTTPRouteFilter.Type is not RequestHeaderModifier + rule: '!(has(self.requestHeaderModifier) && self.type + != ''RequestHeaderModifier'')' + - message: filter.RequestHeaderModifier must be specified + for RequestHeaderModifier HTTPRouteFilter.Type + rule: '!(!has(self.requestHeaderModifier) && self.type + == ''RequestHeaderModifier'')' + - message: filter.ResponseHeaderModifier must be nil + if the HTTPRouteFilter.Type is not ResponseHeaderModifier + rule: '!(has(self.responseHeaderModifier) && self.type + != ''ResponseHeaderModifier'')' + - message: filter.ResponseHeaderModifier must be specified + for ResponseHeaderModifier HTTPRouteFilter.Type + rule: '!(!has(self.responseHeaderModifier) && self.type + == ''ResponseHeaderModifier'')' + - message: filter.RequestMirror must be nil if the HTTPRouteFilter.Type + is not RequestMirror + rule: '!(has(self.requestMirror) && self.type != ''RequestMirror'')' + - message: filter.RequestMirror must be specified for + RequestMirror HTTPRouteFilter.Type + rule: '!(!has(self.requestMirror) && self.type == + ''RequestMirror'')' + - message: filter.RequestRedirect must be nil if the + HTTPRouteFilter.Type is not RequestRedirect + rule: '!(has(self.requestRedirect) && self.type != + ''RequestRedirect'')' + - message: filter.RequestRedirect must be specified + for RequestRedirect HTTPRouteFilter.Type + rule: '!(!has(self.requestRedirect) && self.type == + ''RequestRedirect'')' + - message: filter.URLRewrite must be nil if the HTTPRouteFilter.Type + is not URLRewrite + rule: '!(has(self.urlRewrite) && self.type != ''URLRewrite'')' + - message: filter.URLRewrite must be specified for URLRewrite + HTTPRouteFilter.Type + rule: '!(!has(self.urlRewrite) && self.type == ''URLRewrite'')' + - message: filter.ExtensionRef must be nil if the HTTPRouteFilter.Type + is not ExtensionRef + rule: '!(has(self.extensionRef) && self.type != ''ExtensionRef'')' + - message: filter.ExtensionRef must be specified for + ExtensionRef HTTPRouteFilter.Type + rule: '!(!has(self.extensionRef) && self.type == ''ExtensionRef'')' maxItems: 16 type: array + x-kubernetes-validations: + - message: May specify either httpRouteFilterRequestRedirect + or httpRouteFilterRequestRewrite, but not both + rule: '!(self.exists(f, f.type == ''RequestRedirect'') + && self.exists(f, f.type == ''URLRewrite''))' + - message: May specify either httpRouteFilterRequestRedirect + or httpRouteFilterRequestRewrite, but not both + rule: '!(self.exists(f, f.type == ''RequestRedirect'') + && self.exists(f, f.type == ''URLRewrite''))' + - message: RequestHeaderModifier filter cannot be repeated + rule: 'self.exists(f, f.type == ''RequestHeaderModifier'') + ? self.exists_one(f, f.type == ''RequestHeaderModifier'') + : true' + - message: ResponseHeaderModifier filter cannot be repeated + rule: 'self.exists(f, f.type == ''ResponseHeaderModifier'') + ? self.exists_one(f, f.type == ''ResponseHeaderModifier'') + : true' + - message: RequestRedirect filter cannot be repeated + rule: 'self.exists(f, f.type == ''RequestRedirect'') + ? self.exists_one(f, f.type == ''RequestRedirect'') + : true' + - message: URLRewrite filter cannot be repeated + rule: 'self.exists(f, f.type == ''URLRewrite'') ? self.exists_one(f, + f.type == ''URLRewrite'') : true' group: default: "" description: Group is the group of the referent. For example, @@ -3072,6 +3476,10 @@ spec: required: - name type: object + x-kubernetes-validations: + - message: Must have port for Service reference + rule: '((!has(self.group) || size(self.group) == 0) && (!has(self.kind) + || self.kind == ''Service'')) ? has(self.port) : true' maxItems: 16 type: array filters: @@ -3324,6 +3732,11 @@ spec: required: - name type: object + x-kubernetes-validations: + - message: Must have port for Service reference + rule: '((!has(self.group) || size(self.group) == + 0) && (!has(self.kind) || self.kind == ''Service'')) + ? has(self.port) : true' required: - backendRef type: object @@ -3406,6 +3819,23 @@ spec: required: - type type: object + x-kubernetes-validations: + - message: replaceFullPath must be set when type is + set to 'ReplaceFullPath' + rule: 'self.type == ''ReplaceFullPath'' ? has(self.replaceFullPath) + : true' + - message: type must be 'ReplaceFullPath' when replaceFullPath + is set + rule: 'has(self.replaceFullPath) ? self.type == + ''ReplaceFullPath'' : true' + - message: replacePrefixMatch must be set when type + is set to 'ReplacePrefixMatch' + rule: 'self.type == ''ReplacePrefixMatch'' ? has(self.replacePrefixMatch) + : true' + - message: type must be 'ReplacePrefixMatch' when + replacePrefixMatch is set + rule: 'has(self.replacePrefixMatch) ? self.type + == ''ReplacePrefixMatch'' : true' port: description: "Port is the port to be used in the value of the `Location` header in the response. \n If @@ -3673,12 +4103,89 @@ spec: required: - type type: object + x-kubernetes-validations: + - message: replaceFullPath must be set when type is + set to 'ReplaceFullPath' + rule: 'self.type == ''ReplaceFullPath'' ? has(self.replaceFullPath) + : true' + - message: type must be 'ReplaceFullPath' when replaceFullPath + is set + rule: 'has(self.replaceFullPath) ? self.type == + ''ReplaceFullPath'' : true' + - message: replacePrefixMatch must be set when type + is set to 'ReplacePrefixMatch' + rule: 'self.type == ''ReplacePrefixMatch'' ? has(self.replacePrefixMatch) + : true' + - message: type must be 'ReplacePrefixMatch' when + replacePrefixMatch is set + rule: 'has(self.replacePrefixMatch) ? self.type + == ''ReplacePrefixMatch'' : true' type: object required: - type type: object + x-kubernetes-validations: + - message: filter.RequestHeaderModifier must be nil if the + HTTPRouteFilter.Type is not RequestHeaderModifier + rule: '!(has(self.requestHeaderModifier) && self.type != + ''RequestHeaderModifier'')' + - message: filter.RequestHeaderModifier must be specified + for RequestHeaderModifier HTTPRouteFilter.Type + rule: '!(!has(self.requestHeaderModifier) && self.type == + ''RequestHeaderModifier'')' + - message: filter.ResponseHeaderModifier must be nil if the + HTTPRouteFilter.Type is not ResponseHeaderModifier + rule: '!(has(self.responseHeaderModifier) && self.type != + ''ResponseHeaderModifier'')' + - message: filter.ResponseHeaderModifier must be specified + for ResponseHeaderModifier HTTPRouteFilter.Type + rule: '!(!has(self.responseHeaderModifier) && self.type + == ''ResponseHeaderModifier'')' + - message: filter.RequestMirror must be nil if the HTTPRouteFilter.Type + is not RequestMirror + rule: '!(has(self.requestMirror) && self.type != ''RequestMirror'')' + - message: filter.RequestMirror must be specified for RequestMirror + HTTPRouteFilter.Type + rule: '!(!has(self.requestMirror) && self.type == ''RequestMirror'')' + - message: filter.RequestRedirect must be nil if the HTTPRouteFilter.Type + is not RequestRedirect + rule: '!(has(self.requestRedirect) && self.type != ''RequestRedirect'')' + - message: filter.RequestRedirect must be specified for RequestRedirect + HTTPRouteFilter.Type + rule: '!(!has(self.requestRedirect) && self.type == ''RequestRedirect'')' + - message: filter.URLRewrite must be nil if the HTTPRouteFilter.Type + is not URLRewrite + rule: '!(has(self.urlRewrite) && self.type != ''URLRewrite'')' + - message: filter.URLRewrite must be specified for URLRewrite + HTTPRouteFilter.Type + rule: '!(!has(self.urlRewrite) && self.type == ''URLRewrite'')' + - message: filter.ExtensionRef must be nil if the HTTPRouteFilter.Type + is not ExtensionRef + rule: '!(has(self.extensionRef) && self.type != ''ExtensionRef'')' + - message: filter.ExtensionRef must be specified for ExtensionRef + HTTPRouteFilter.Type + rule: '!(!has(self.extensionRef) && self.type == ''ExtensionRef'')' maxItems: 16 type: array + x-kubernetes-validations: + - message: May specify either httpRouteFilterRequestRedirect + or httpRouteFilterRequestRewrite, but not both + rule: '!(self.exists(f, f.type == ''RequestRedirect'') && + self.exists(f, f.type == ''URLRewrite''))' + - message: RequestHeaderModifier filter cannot be repeated + rule: 'self.exists(f, f.type == ''RequestHeaderModifier'') + ? self.exists_one(f, f.type == ''RequestHeaderModifier'') + : true' + - message: ResponseHeaderModifier filter cannot be repeated + rule: 'self.exists(f, f.type == ''ResponseHeaderModifier'') + ? self.exists_one(f, f.type == ''ResponseHeaderModifier'') + : true' + - message: RequestRedirect filter cannot be repeated + rule: 'self.exists(f, f.type == ''RequestRedirect'') ? self.exists_one(f, + f.type == ''RequestRedirect'') : true' + - message: URLRewrite filter cannot be repeated + rule: 'self.exists(f, f.type == ''URLRewrite'') ? self.exists_one(f, + f.type == ''URLRewrite'') : true' matches: default: - path: @@ -3822,6 +4329,53 @@ spec: maxLength: 1024 type: string type: object + x-kubernetes-validations: + - message: value must be an absolute path and start with + '/' when type one of ['Exact', 'PathPrefix'] + rule: '(self.type == ''Exact'' || self.type == ''PathPrefix'') + ? self.value.startsWith(''/'') : true' + - message: must not contain '//' when type one of ['Exact', + 'PathPrefix'] + rule: '(self.type == ''Exact'' || self.type == ''PathPrefix'') + ? !self.value.contains(''//'') : true' + - message: must not contain '/./' when type one of ['Exact', + 'PathPrefix'] + rule: '(self.type == ''Exact'' || self.type == ''PathPrefix'') + ? !self.value.contains(''/./'') : true' + - message: must not contain '/../' when type one of ['Exact', + 'PathPrefix'] + rule: '(self.type == ''Exact'' || self.type == ''PathPrefix'') + ? !self.value.contains(''/../'') : true' + - message: must not contain '%2f' when type one of ['Exact', + 'PathPrefix'] + rule: '(self.type == ''Exact'' || self.type == ''PathPrefix'') + ? !self.value.contains(''%2f'') : true' + - message: must not contain '%2F' when type one of ['Exact', + 'PathPrefix'] + rule: '(self.type == ''Exact'' || self.type == ''PathPrefix'') + ? !self.value.contains(''%2F'') : true' + - message: must not contain '#' when type one of ['Exact', + 'PathPrefix'] + rule: '(self.type == ''Exact'' || self.type == ''PathPrefix'') + ? !self.value.contains(''#'') : true' + - message: must not end with '/..' when type one of ['Exact', + 'PathPrefix'] + rule: '(self.type == ''Exact'' || self.type == ''PathPrefix'') + ? !self.value.endsWith(''/..'') : true' + - message: must not end with '/.' when type one of ['Exact', + 'PathPrefix'] + rule: '(self.type == ''Exact'' || self.type == ''PathPrefix'') + ? !self.value.endsWith(''/.'') : true' + - message: type must be one of ['Exact', 'PathPrefix', + 'RegularExpression'] + rule: self.type == 'Exact' || self.type == 'PathPrefix' + || self.type == 'RegularExpression' + - message: must only contain valid characters (matching + ^(?:[-A-Za-z0-9/._~!$&'()*+,;=:@]|[%][0-9a-fA-F]{2})+$) + for types ['Exact', 'PathPrefix'] + rule: '(self.type == ''Exact'' || self.type == ''PathPrefix'') + ? self.value.matches(r"""^(?:[-A-Za-z0-9/._~!$&''()*+,;=:@]|[%][0-9a-fA-F]{2})+$""") + : true' queryParams: description: "QueryParams specifies HTTP query parameter matchers. Multiple match values are ANDed together, @@ -3888,6 +4442,46 @@ spec: maxItems: 8 type: array type: object + x-kubernetes-validations: + - message: RequestRedirect filter must not be used together with + backendRefs + rule: '(has(self.backendRefs) && size(self.backendRefs) > 0) ? + (!has(self.filters) || self.filters.all(f, !has(f.requestRedirect))): + true' + - message: When using RequestRedirect filter with Path.ReplacePrefixMatch, + exactly one PathPrefix match must be specified + rule: '(has(self.filters) && self.filters.exists_one(f, has(f.requestRedirect) + && has(f.requestRedirect.path) && f.requestRedirect.path.type + == ''ReplacePrefixMatch'' && has(f.requestRedirect.path.replacePrefixMatch))) + ? ((size(self.matches) != 1 || !has(self.matches[0].path) || + self.matches[0].path.type != ''PathPrefix'') ? false : true) + : true' + - message: When using URLRewrite filter with Path.ReplacePrefixMatch, + exactly one PathPrefix match must be specified + rule: '(has(self.filters) && self.filters.exists_one(f, has(f.urlRewrite) + && has(f.urlRewrite.path) && f.urlRewrite.path.type == ''ReplacePrefixMatch'' + && has(f.urlRewrite.path.replacePrefixMatch))) ? ((size(self.matches) + != 1 || !has(self.matches[0].path) || self.matches[0].path.type + != ''PathPrefix'') ? false : true) : true' + - message: Within BackendRefs, when using RequestRedirect filter + with Path.ReplacePrefixMatch, exactly one PathPrefix match must + be specified + rule: '(has(self.backendRefs) && self.backendRefs.exists_one(b, + (has(b.filters) && b.filters.exists_one(f, has(f.requestRedirect) + && has(f.requestRedirect.path) && f.requestRedirect.path.type + == ''ReplacePrefixMatch'' && has(f.requestRedirect.path.replacePrefixMatch))) + )) ? ((size(self.matches) != 1 || !has(self.matches[0].path) + || self.matches[0].path.type != ''PathPrefix'') ? false : true) + : true' + - message: Within BackendRefs, When using URLRewrite filter with + Path.ReplacePrefixMatch, exactly one PathPrefix match must be + specified + rule: '(has(self.backendRefs) && self.backendRefs.exists_one(b, + (has(b.filters) && b.filters.exists_one(f, has(f.urlRewrite) + && has(f.urlRewrite.path) && f.urlRewrite.path.type == ''ReplacePrefixMatch'' + && has(f.urlRewrite.path.replacePrefixMatch))) )) ? ((size(self.matches) + != 1 || !has(self.matches[0].path) || self.matches[0].path.type + != ''PathPrefix'') ? false : true) : true' maxItems: 16 type: array type: object diff --git a/config/crd/experimental/gateway.networking.k8s.io_tcproutes.yaml b/config/crd/experimental/gateway.networking.k8s.io_tcproutes.yaml index c8f925e61d..0e6e806f23 100644 --- a/config/crd/experimental/gateway.networking.k8s.io_tcproutes.yaml +++ b/config/crd/experimental/gateway.networking.k8s.io_tcproutes.yaml @@ -295,6 +295,10 @@ spec: required: - name type: object + x-kubernetes-validations: + - message: Must have port for Service reference + rule: '((!has(self.group) || size(self.group) == 0) && (!has(self.kind) + || self.kind == ''Service'')) ? has(self.port) : true' maxItems: 16 minItems: 1 type: array diff --git a/config/crd/experimental/gateway.networking.k8s.io_tlsroutes.yaml b/config/crd/experimental/gateway.networking.k8s.io_tlsroutes.yaml index e35b549dc2..b9586e458c 100644 --- a/config/crd/experimental/gateway.networking.k8s.io_tlsroutes.yaml +++ b/config/crd/experimental/gateway.networking.k8s.io_tlsroutes.yaml @@ -344,6 +344,10 @@ spec: required: - name type: object + x-kubernetes-validations: + - message: Must have port for Service reference + rule: '((!has(self.group) || size(self.group) == 0) && (!has(self.kind) + || self.kind == ''Service'')) ? has(self.port) : true' maxItems: 16 minItems: 1 type: array diff --git a/config/crd/experimental/gateway.networking.k8s.io_udproutes.yaml b/config/crd/experimental/gateway.networking.k8s.io_udproutes.yaml index 3af15f3892..24fc2e7478 100644 --- a/config/crd/experimental/gateway.networking.k8s.io_udproutes.yaml +++ b/config/crd/experimental/gateway.networking.k8s.io_udproutes.yaml @@ -295,6 +295,10 @@ spec: required: - name type: object + x-kubernetes-validations: + - message: Must have port for Service reference + rule: '((!has(self.group) || size(self.group) == 0) && (!has(self.kind) + || self.kind == ''Service'')) ? has(self.port) : true' maxItems: 16 minItems: 1 type: array diff --git a/config/crd/standard/gateway.networking.k8s.io_httproutes.yaml b/config/crd/standard/gateway.networking.k8s.io_httproutes.yaml index bd77feae15..4e4484be2b 100644 --- a/config/crd/standard/gateway.networking.k8s.io_httproutes.yaml +++ b/config/crd/standard/gateway.networking.k8s.io_httproutes.yaml @@ -520,6 +520,11 @@ spec: required: - name type: object + x-kubernetes-validations: + - message: Must have port for Service reference + rule: '((!has(self.group) || size(self.group) + == 0) && (!has(self.kind) || self.kind == + ''Service'')) ? has(self.port) : true' required: - backendRef type: object @@ -609,6 +614,23 @@ spec: required: - type type: object + x-kubernetes-validations: + - message: replaceFullPath must be set when + type is set to 'ReplaceFullPath' + rule: 'self.type == ''ReplaceFullPath'' ? + has(self.replaceFullPath) : true' + - message: type must be 'ReplaceFullPath' when + replaceFullPath is set + rule: 'has(self.replaceFullPath) ? self.type + == ''ReplaceFullPath'' : true' + - message: replacePrefixMatch must be set when + type is set to 'ReplacePrefixMatch' + rule: 'self.type == ''ReplacePrefixMatch'' + ? has(self.replacePrefixMatch) : true' + - message: type must be 'ReplacePrefixMatch' + when replacePrefixMatch is set + rule: 'has(self.replacePrefixMatch) ? self.type + == ''ReplacePrefixMatch'' : true' port: description: "Port is the port to be used in the value of the `Location` header in the @@ -897,12 +919,97 @@ spec: required: - type type: object + x-kubernetes-validations: + - message: replaceFullPath must be set when + type is set to 'ReplaceFullPath' + rule: 'self.type == ''ReplaceFullPath'' ? + has(self.replaceFullPath) : true' + - message: type must be 'ReplaceFullPath' when + replaceFullPath is set + rule: 'has(self.replaceFullPath) ? self.type + == ''ReplaceFullPath'' : true' + - message: replacePrefixMatch must be set when + type is set to 'ReplacePrefixMatch' + rule: 'self.type == ''ReplacePrefixMatch'' + ? has(self.replacePrefixMatch) : true' + - message: type must be 'ReplacePrefixMatch' + when replacePrefixMatch is set + rule: 'has(self.replacePrefixMatch) ? self.type + == ''ReplacePrefixMatch'' : true' type: object required: - type type: object + x-kubernetes-validations: + - message: filter.RequestHeaderModifier must be nil + if the HTTPRouteFilter.Type is not RequestHeaderModifier + rule: '!(has(self.requestHeaderModifier) && self.type + != ''RequestHeaderModifier'')' + - message: filter.RequestHeaderModifier must be specified + for RequestHeaderModifier HTTPRouteFilter.Type + rule: '!(!has(self.requestHeaderModifier) && self.type + == ''RequestHeaderModifier'')' + - message: filter.ResponseHeaderModifier must be nil + if the HTTPRouteFilter.Type is not ResponseHeaderModifier + rule: '!(has(self.responseHeaderModifier) && self.type + != ''ResponseHeaderModifier'')' + - message: filter.ResponseHeaderModifier must be specified + for ResponseHeaderModifier HTTPRouteFilter.Type + rule: '!(!has(self.responseHeaderModifier) && self.type + == ''ResponseHeaderModifier'')' + - message: filter.RequestMirror must be nil if the HTTPRouteFilter.Type + is not RequestMirror + rule: '!(has(self.requestMirror) && self.type != ''RequestMirror'')' + - message: filter.RequestMirror must be specified for + RequestMirror HTTPRouteFilter.Type + rule: '!(!has(self.requestMirror) && self.type == + ''RequestMirror'')' + - message: filter.RequestRedirect must be nil if the + HTTPRouteFilter.Type is not RequestRedirect + rule: '!(has(self.requestRedirect) && self.type != + ''RequestRedirect'')' + - message: filter.RequestRedirect must be specified + for RequestRedirect HTTPRouteFilter.Type + rule: '!(!has(self.requestRedirect) && self.type == + ''RequestRedirect'')' + - message: filter.URLRewrite must be nil if the HTTPRouteFilter.Type + is not URLRewrite + rule: '!(has(self.urlRewrite) && self.type != ''URLRewrite'')' + - message: filter.URLRewrite must be specified for URLRewrite + HTTPRouteFilter.Type + rule: '!(!has(self.urlRewrite) && self.type == ''URLRewrite'')' + - message: filter.ExtensionRef must be nil if the HTTPRouteFilter.Type + is not ExtensionRef + rule: '!(has(self.extensionRef) && self.type != ''ExtensionRef'')' + - message: filter.ExtensionRef must be specified for + ExtensionRef HTTPRouteFilter.Type + rule: '!(!has(self.extensionRef) && self.type == ''ExtensionRef'')' maxItems: 16 type: array + x-kubernetes-validations: + - message: May specify either httpRouteFilterRequestRedirect + or httpRouteFilterRequestRewrite, but not both + rule: '!(self.exists(f, f.type == ''RequestRedirect'') + && self.exists(f, f.type == ''URLRewrite''))' + - message: May specify either httpRouteFilterRequestRedirect + or httpRouteFilterRequestRewrite, but not both + rule: '!(self.exists(f, f.type == ''RequestRedirect'') + && self.exists(f, f.type == ''URLRewrite''))' + - message: RequestHeaderModifier filter cannot be repeated + rule: 'self.exists(f, f.type == ''RequestHeaderModifier'') + ? self.exists_one(f, f.type == ''RequestHeaderModifier'') + : true' + - message: ResponseHeaderModifier filter cannot be repeated + rule: 'self.exists(f, f.type == ''ResponseHeaderModifier'') + ? self.exists_one(f, f.type == ''ResponseHeaderModifier'') + : true' + - message: RequestRedirect filter cannot be repeated + rule: 'self.exists(f, f.type == ''RequestRedirect'') + ? self.exists_one(f, f.type == ''RequestRedirect'') + : true' + - message: URLRewrite filter cannot be repeated + rule: 'self.exists(f, f.type == ''URLRewrite'') ? self.exists_one(f, + f.type == ''URLRewrite'') : true' group: default: "" description: Group is the group of the referent. For example, @@ -978,6 +1085,10 @@ spec: required: - name type: object + x-kubernetes-validations: + - message: Must have port for Service reference + rule: '((!has(self.group) || size(self.group) == 0) && (!has(self.kind) + || self.kind == ''Service'')) ? has(self.port) : true' maxItems: 16 type: array filters: @@ -1230,6 +1341,11 @@ spec: required: - name type: object + x-kubernetes-validations: + - message: Must have port for Service reference + rule: '((!has(self.group) || size(self.group) == + 0) && (!has(self.kind) || self.kind == ''Service'')) + ? has(self.port) : true' required: - backendRef type: object @@ -1312,6 +1428,23 @@ spec: required: - type type: object + x-kubernetes-validations: + - message: replaceFullPath must be set when type is + set to 'ReplaceFullPath' + rule: 'self.type == ''ReplaceFullPath'' ? has(self.replaceFullPath) + : true' + - message: type must be 'ReplaceFullPath' when replaceFullPath + is set + rule: 'has(self.replaceFullPath) ? self.type == + ''ReplaceFullPath'' : true' + - message: replacePrefixMatch must be set when type + is set to 'ReplacePrefixMatch' + rule: 'self.type == ''ReplacePrefixMatch'' ? has(self.replacePrefixMatch) + : true' + - message: type must be 'ReplacePrefixMatch' when + replacePrefixMatch is set + rule: 'has(self.replacePrefixMatch) ? self.type + == ''ReplacePrefixMatch'' : true' port: description: "Port is the port to be used in the value of the `Location` header in the response. \n If @@ -1579,12 +1712,89 @@ spec: required: - type type: object + x-kubernetes-validations: + - message: replaceFullPath must be set when type is + set to 'ReplaceFullPath' + rule: 'self.type == ''ReplaceFullPath'' ? has(self.replaceFullPath) + : true' + - message: type must be 'ReplaceFullPath' when replaceFullPath + is set + rule: 'has(self.replaceFullPath) ? self.type == + ''ReplaceFullPath'' : true' + - message: replacePrefixMatch must be set when type + is set to 'ReplacePrefixMatch' + rule: 'self.type == ''ReplacePrefixMatch'' ? has(self.replacePrefixMatch) + : true' + - message: type must be 'ReplacePrefixMatch' when + replacePrefixMatch is set + rule: 'has(self.replacePrefixMatch) ? self.type + == ''ReplacePrefixMatch'' : true' type: object required: - type type: object + x-kubernetes-validations: + - message: filter.RequestHeaderModifier must be nil if the + HTTPRouteFilter.Type is not RequestHeaderModifier + rule: '!(has(self.requestHeaderModifier) && self.type != + ''RequestHeaderModifier'')' + - message: filter.RequestHeaderModifier must be specified + for RequestHeaderModifier HTTPRouteFilter.Type + rule: '!(!has(self.requestHeaderModifier) && self.type == + ''RequestHeaderModifier'')' + - message: filter.ResponseHeaderModifier must be nil if the + HTTPRouteFilter.Type is not ResponseHeaderModifier + rule: '!(has(self.responseHeaderModifier) && self.type != + ''ResponseHeaderModifier'')' + - message: filter.ResponseHeaderModifier must be specified + for ResponseHeaderModifier HTTPRouteFilter.Type + rule: '!(!has(self.responseHeaderModifier) && self.type + == ''ResponseHeaderModifier'')' + - message: filter.RequestMirror must be nil if the HTTPRouteFilter.Type + is not RequestMirror + rule: '!(has(self.requestMirror) && self.type != ''RequestMirror'')' + - message: filter.RequestMirror must be specified for RequestMirror + HTTPRouteFilter.Type + rule: '!(!has(self.requestMirror) && self.type == ''RequestMirror'')' + - message: filter.RequestRedirect must be nil if the HTTPRouteFilter.Type + is not RequestRedirect + rule: '!(has(self.requestRedirect) && self.type != ''RequestRedirect'')' + - message: filter.RequestRedirect must be specified for RequestRedirect + HTTPRouteFilter.Type + rule: '!(!has(self.requestRedirect) && self.type == ''RequestRedirect'')' + - message: filter.URLRewrite must be nil if the HTTPRouteFilter.Type + is not URLRewrite + rule: '!(has(self.urlRewrite) && self.type != ''URLRewrite'')' + - message: filter.URLRewrite must be specified for URLRewrite + HTTPRouteFilter.Type + rule: '!(!has(self.urlRewrite) && self.type == ''URLRewrite'')' + - message: filter.ExtensionRef must be nil if the HTTPRouteFilter.Type + is not ExtensionRef + rule: '!(has(self.extensionRef) && self.type != ''ExtensionRef'')' + - message: filter.ExtensionRef must be specified for ExtensionRef + HTTPRouteFilter.Type + rule: '!(!has(self.extensionRef) && self.type == ''ExtensionRef'')' maxItems: 16 type: array + x-kubernetes-validations: + - message: May specify either httpRouteFilterRequestRedirect + or httpRouteFilterRequestRewrite, but not both + rule: '!(self.exists(f, f.type == ''RequestRedirect'') && + self.exists(f, f.type == ''URLRewrite''))' + - message: RequestHeaderModifier filter cannot be repeated + rule: 'self.exists(f, f.type == ''RequestHeaderModifier'') + ? self.exists_one(f, f.type == ''RequestHeaderModifier'') + : true' + - message: ResponseHeaderModifier filter cannot be repeated + rule: 'self.exists(f, f.type == ''ResponseHeaderModifier'') + ? self.exists_one(f, f.type == ''ResponseHeaderModifier'') + : true' + - message: RequestRedirect filter cannot be repeated + rule: 'self.exists(f, f.type == ''RequestRedirect'') ? self.exists_one(f, + f.type == ''RequestRedirect'') : true' + - message: URLRewrite filter cannot be repeated + rule: 'self.exists(f, f.type == ''URLRewrite'') ? self.exists_one(f, + f.type == ''URLRewrite'') : true' matches: default: - path: @@ -1728,6 +1938,53 @@ spec: maxLength: 1024 type: string type: object + x-kubernetes-validations: + - message: value must be an absolute path and start with + '/' when type one of ['Exact', 'PathPrefix'] + rule: '(self.type == ''Exact'' || self.type == ''PathPrefix'') + ? self.value.startsWith(''/'') : true' + - message: must not contain '//' when type one of ['Exact', + 'PathPrefix'] + rule: '(self.type == ''Exact'' || self.type == ''PathPrefix'') + ? !self.value.contains(''//'') : true' + - message: must not contain '/./' when type one of ['Exact', + 'PathPrefix'] + rule: '(self.type == ''Exact'' || self.type == ''PathPrefix'') + ? !self.value.contains(''/./'') : true' + - message: must not contain '/../' when type one of ['Exact', + 'PathPrefix'] + rule: '(self.type == ''Exact'' || self.type == ''PathPrefix'') + ? !self.value.contains(''/../'') : true' + - message: must not contain '%2f' when type one of ['Exact', + 'PathPrefix'] + rule: '(self.type == ''Exact'' || self.type == ''PathPrefix'') + ? !self.value.contains(''%2f'') : true' + - message: must not contain '%2F' when type one of ['Exact', + 'PathPrefix'] + rule: '(self.type == ''Exact'' || self.type == ''PathPrefix'') + ? !self.value.contains(''%2F'') : true' + - message: must not contain '#' when type one of ['Exact', + 'PathPrefix'] + rule: '(self.type == ''Exact'' || self.type == ''PathPrefix'') + ? !self.value.contains(''#'') : true' + - message: must not end with '/..' when type one of ['Exact', + 'PathPrefix'] + rule: '(self.type == ''Exact'' || self.type == ''PathPrefix'') + ? !self.value.endsWith(''/..'') : true' + - message: must not end with '/.' when type one of ['Exact', + 'PathPrefix'] + rule: '(self.type == ''Exact'' || self.type == ''PathPrefix'') + ? !self.value.endsWith(''/.'') : true' + - message: type must be one of ['Exact', 'PathPrefix', + 'RegularExpression'] + rule: self.type == 'Exact' || self.type == 'PathPrefix' + || self.type == 'RegularExpression' + - message: must only contain valid characters (matching + ^(?:[-A-Za-z0-9/._~!$&'()*+,;=:@]|[%][0-9a-fA-F]{2})+$) + for types ['Exact', 'PathPrefix'] + rule: '(self.type == ''Exact'' || self.type == ''PathPrefix'') + ? self.value.matches(r"""^(?:[-A-Za-z0-9/._~!$&''()*+,;=:@]|[%][0-9a-fA-F]{2})+$""") + : true' queryParams: description: "QueryParams specifies HTTP query parameter matchers. Multiple match values are ANDed together, @@ -1794,6 +2051,46 @@ spec: maxItems: 8 type: array type: object + x-kubernetes-validations: + - message: RequestRedirect filter must not be used together with + backendRefs + rule: '(has(self.backendRefs) && size(self.backendRefs) > 0) ? + (!has(self.filters) || self.filters.all(f, !has(f.requestRedirect))): + true' + - message: When using RequestRedirect filter with Path.ReplacePrefixMatch, + exactly one PathPrefix match must be specified + rule: '(has(self.filters) && self.filters.exists_one(f, has(f.requestRedirect) + && has(f.requestRedirect.path) && f.requestRedirect.path.type + == ''ReplacePrefixMatch'' && has(f.requestRedirect.path.replacePrefixMatch))) + ? ((size(self.matches) != 1 || !has(self.matches[0].path) || + self.matches[0].path.type != ''PathPrefix'') ? false : true) + : true' + - message: When using URLRewrite filter with Path.ReplacePrefixMatch, + exactly one PathPrefix match must be specified + rule: '(has(self.filters) && self.filters.exists_one(f, has(f.urlRewrite) + && has(f.urlRewrite.path) && f.urlRewrite.path.type == ''ReplacePrefixMatch'' + && has(f.urlRewrite.path.replacePrefixMatch))) ? ((size(self.matches) + != 1 || !has(self.matches[0].path) || self.matches[0].path.type + != ''PathPrefix'') ? false : true) : true' + - message: Within BackendRefs, when using RequestRedirect filter + with Path.ReplacePrefixMatch, exactly one PathPrefix match must + be specified + rule: '(has(self.backendRefs) && self.backendRefs.exists_one(b, + (has(b.filters) && b.filters.exists_one(f, has(f.requestRedirect) + && has(f.requestRedirect.path) && f.requestRedirect.path.type + == ''ReplacePrefixMatch'' && has(f.requestRedirect.path.replacePrefixMatch))) + )) ? ((size(self.matches) != 1 || !has(self.matches[0].path) + || self.matches[0].path.type != ''PathPrefix'') ? false : true) + : true' + - message: Within BackendRefs, When using URLRewrite filter with + Path.ReplacePrefixMatch, exactly one PathPrefix match must be + specified + rule: '(has(self.backendRefs) && self.backendRefs.exists_one(b, + (has(b.filters) && b.filters.exists_one(f, has(f.urlRewrite) + && has(f.urlRewrite.path) && f.urlRewrite.path.type == ''ReplacePrefixMatch'' + && has(f.urlRewrite.path.replacePrefixMatch))) )) ? ((size(self.matches) + != 1 || !has(self.matches[0].path) || self.matches[0].path.type + != ''PathPrefix'') ? false : true) : true' maxItems: 16 type: array type: object @@ -2522,6 +2819,11 @@ spec: required: - name type: object + x-kubernetes-validations: + - message: Must have port for Service reference + rule: '((!has(self.group) || size(self.group) + == 0) && (!has(self.kind) || self.kind == + ''Service'')) ? has(self.port) : true' required: - backendRef type: object @@ -2611,6 +2913,23 @@ spec: required: - type type: object + x-kubernetes-validations: + - message: replaceFullPath must be set when + type is set to 'ReplaceFullPath' + rule: 'self.type == ''ReplaceFullPath'' ? + has(self.replaceFullPath) : true' + - message: type must be 'ReplaceFullPath' when + replaceFullPath is set + rule: 'has(self.replaceFullPath) ? self.type + == ''ReplaceFullPath'' : true' + - message: replacePrefixMatch must be set when + type is set to 'ReplacePrefixMatch' + rule: 'self.type == ''ReplacePrefixMatch'' + ? has(self.replacePrefixMatch) : true' + - message: type must be 'ReplacePrefixMatch' + when replacePrefixMatch is set + rule: 'has(self.replacePrefixMatch) ? self.type + == ''ReplacePrefixMatch'' : true' port: description: "Port is the port to be used in the value of the `Location` header in the @@ -2899,12 +3218,97 @@ spec: required: - type type: object + x-kubernetes-validations: + - message: replaceFullPath must be set when + type is set to 'ReplaceFullPath' + rule: 'self.type == ''ReplaceFullPath'' ? + has(self.replaceFullPath) : true' + - message: type must be 'ReplaceFullPath' when + replaceFullPath is set + rule: 'has(self.replaceFullPath) ? self.type + == ''ReplaceFullPath'' : true' + - message: replacePrefixMatch must be set when + type is set to 'ReplacePrefixMatch' + rule: 'self.type == ''ReplacePrefixMatch'' + ? has(self.replacePrefixMatch) : true' + - message: type must be 'ReplacePrefixMatch' + when replacePrefixMatch is set + rule: 'has(self.replacePrefixMatch) ? self.type + == ''ReplacePrefixMatch'' : true' type: object required: - type type: object + x-kubernetes-validations: + - message: filter.RequestHeaderModifier must be nil + if the HTTPRouteFilter.Type is not RequestHeaderModifier + rule: '!(has(self.requestHeaderModifier) && self.type + != ''RequestHeaderModifier'')' + - message: filter.RequestHeaderModifier must be specified + for RequestHeaderModifier HTTPRouteFilter.Type + rule: '!(!has(self.requestHeaderModifier) && self.type + == ''RequestHeaderModifier'')' + - message: filter.ResponseHeaderModifier must be nil + if the HTTPRouteFilter.Type is not ResponseHeaderModifier + rule: '!(has(self.responseHeaderModifier) && self.type + != ''ResponseHeaderModifier'')' + - message: filter.ResponseHeaderModifier must be specified + for ResponseHeaderModifier HTTPRouteFilter.Type + rule: '!(!has(self.responseHeaderModifier) && self.type + == ''ResponseHeaderModifier'')' + - message: filter.RequestMirror must be nil if the HTTPRouteFilter.Type + is not RequestMirror + rule: '!(has(self.requestMirror) && self.type != ''RequestMirror'')' + - message: filter.RequestMirror must be specified for + RequestMirror HTTPRouteFilter.Type + rule: '!(!has(self.requestMirror) && self.type == + ''RequestMirror'')' + - message: filter.RequestRedirect must be nil if the + HTTPRouteFilter.Type is not RequestRedirect + rule: '!(has(self.requestRedirect) && self.type != + ''RequestRedirect'')' + - message: filter.RequestRedirect must be specified + for RequestRedirect HTTPRouteFilter.Type + rule: '!(!has(self.requestRedirect) && self.type == + ''RequestRedirect'')' + - message: filter.URLRewrite must be nil if the HTTPRouteFilter.Type + is not URLRewrite + rule: '!(has(self.urlRewrite) && self.type != ''URLRewrite'')' + - message: filter.URLRewrite must be specified for URLRewrite + HTTPRouteFilter.Type + rule: '!(!has(self.urlRewrite) && self.type == ''URLRewrite'')' + - message: filter.ExtensionRef must be nil if the HTTPRouteFilter.Type + is not ExtensionRef + rule: '!(has(self.extensionRef) && self.type != ''ExtensionRef'')' + - message: filter.ExtensionRef must be specified for + ExtensionRef HTTPRouteFilter.Type + rule: '!(!has(self.extensionRef) && self.type == ''ExtensionRef'')' maxItems: 16 type: array + x-kubernetes-validations: + - message: May specify either httpRouteFilterRequestRedirect + or httpRouteFilterRequestRewrite, but not both + rule: '!(self.exists(f, f.type == ''RequestRedirect'') + && self.exists(f, f.type == ''URLRewrite''))' + - message: May specify either httpRouteFilterRequestRedirect + or httpRouteFilterRequestRewrite, but not both + rule: '!(self.exists(f, f.type == ''RequestRedirect'') + && self.exists(f, f.type == ''URLRewrite''))' + - message: RequestHeaderModifier filter cannot be repeated + rule: 'self.exists(f, f.type == ''RequestHeaderModifier'') + ? self.exists_one(f, f.type == ''RequestHeaderModifier'') + : true' + - message: ResponseHeaderModifier filter cannot be repeated + rule: 'self.exists(f, f.type == ''ResponseHeaderModifier'') + ? self.exists_one(f, f.type == ''ResponseHeaderModifier'') + : true' + - message: RequestRedirect filter cannot be repeated + rule: 'self.exists(f, f.type == ''RequestRedirect'') + ? self.exists_one(f, f.type == ''RequestRedirect'') + : true' + - message: URLRewrite filter cannot be repeated + rule: 'self.exists(f, f.type == ''URLRewrite'') ? self.exists_one(f, + f.type == ''URLRewrite'') : true' group: default: "" description: Group is the group of the referent. For example, @@ -2980,6 +3384,10 @@ spec: required: - name type: object + x-kubernetes-validations: + - message: Must have port for Service reference + rule: '((!has(self.group) || size(self.group) == 0) && (!has(self.kind) + || self.kind == ''Service'')) ? has(self.port) : true' maxItems: 16 type: array filters: @@ -3232,6 +3640,11 @@ spec: required: - name type: object + x-kubernetes-validations: + - message: Must have port for Service reference + rule: '((!has(self.group) || size(self.group) == + 0) && (!has(self.kind) || self.kind == ''Service'')) + ? has(self.port) : true' required: - backendRef type: object @@ -3314,6 +3727,23 @@ spec: required: - type type: object + x-kubernetes-validations: + - message: replaceFullPath must be set when type is + set to 'ReplaceFullPath' + rule: 'self.type == ''ReplaceFullPath'' ? has(self.replaceFullPath) + : true' + - message: type must be 'ReplaceFullPath' when replaceFullPath + is set + rule: 'has(self.replaceFullPath) ? self.type == + ''ReplaceFullPath'' : true' + - message: replacePrefixMatch must be set when type + is set to 'ReplacePrefixMatch' + rule: 'self.type == ''ReplacePrefixMatch'' ? has(self.replacePrefixMatch) + : true' + - message: type must be 'ReplacePrefixMatch' when + replacePrefixMatch is set + rule: 'has(self.replacePrefixMatch) ? self.type + == ''ReplacePrefixMatch'' : true' port: description: "Port is the port to be used in the value of the `Location` header in the response. \n If @@ -3581,12 +4011,89 @@ spec: required: - type type: object + x-kubernetes-validations: + - message: replaceFullPath must be set when type is + set to 'ReplaceFullPath' + rule: 'self.type == ''ReplaceFullPath'' ? has(self.replaceFullPath) + : true' + - message: type must be 'ReplaceFullPath' when replaceFullPath + is set + rule: 'has(self.replaceFullPath) ? self.type == + ''ReplaceFullPath'' : true' + - message: replacePrefixMatch must be set when type + is set to 'ReplacePrefixMatch' + rule: 'self.type == ''ReplacePrefixMatch'' ? has(self.replacePrefixMatch) + : true' + - message: type must be 'ReplacePrefixMatch' when + replacePrefixMatch is set + rule: 'has(self.replacePrefixMatch) ? self.type + == ''ReplacePrefixMatch'' : true' type: object required: - type type: object + x-kubernetes-validations: + - message: filter.RequestHeaderModifier must be nil if the + HTTPRouteFilter.Type is not RequestHeaderModifier + rule: '!(has(self.requestHeaderModifier) && self.type != + ''RequestHeaderModifier'')' + - message: filter.RequestHeaderModifier must be specified + for RequestHeaderModifier HTTPRouteFilter.Type + rule: '!(!has(self.requestHeaderModifier) && self.type == + ''RequestHeaderModifier'')' + - message: filter.ResponseHeaderModifier must be nil if the + HTTPRouteFilter.Type is not ResponseHeaderModifier + rule: '!(has(self.responseHeaderModifier) && self.type != + ''ResponseHeaderModifier'')' + - message: filter.ResponseHeaderModifier must be specified + for ResponseHeaderModifier HTTPRouteFilter.Type + rule: '!(!has(self.responseHeaderModifier) && self.type + == ''ResponseHeaderModifier'')' + - message: filter.RequestMirror must be nil if the HTTPRouteFilter.Type + is not RequestMirror + rule: '!(has(self.requestMirror) && self.type != ''RequestMirror'')' + - message: filter.RequestMirror must be specified for RequestMirror + HTTPRouteFilter.Type + rule: '!(!has(self.requestMirror) && self.type == ''RequestMirror'')' + - message: filter.RequestRedirect must be nil if the HTTPRouteFilter.Type + is not RequestRedirect + rule: '!(has(self.requestRedirect) && self.type != ''RequestRedirect'')' + - message: filter.RequestRedirect must be specified for RequestRedirect + HTTPRouteFilter.Type + rule: '!(!has(self.requestRedirect) && self.type == ''RequestRedirect'')' + - message: filter.URLRewrite must be nil if the HTTPRouteFilter.Type + is not URLRewrite + rule: '!(has(self.urlRewrite) && self.type != ''URLRewrite'')' + - message: filter.URLRewrite must be specified for URLRewrite + HTTPRouteFilter.Type + rule: '!(!has(self.urlRewrite) && self.type == ''URLRewrite'')' + - message: filter.ExtensionRef must be nil if the HTTPRouteFilter.Type + is not ExtensionRef + rule: '!(has(self.extensionRef) && self.type != ''ExtensionRef'')' + - message: filter.ExtensionRef must be specified for ExtensionRef + HTTPRouteFilter.Type + rule: '!(!has(self.extensionRef) && self.type == ''ExtensionRef'')' maxItems: 16 type: array + x-kubernetes-validations: + - message: May specify either httpRouteFilterRequestRedirect + or httpRouteFilterRequestRewrite, but not both + rule: '!(self.exists(f, f.type == ''RequestRedirect'') && + self.exists(f, f.type == ''URLRewrite''))' + - message: RequestHeaderModifier filter cannot be repeated + rule: 'self.exists(f, f.type == ''RequestHeaderModifier'') + ? self.exists_one(f, f.type == ''RequestHeaderModifier'') + : true' + - message: ResponseHeaderModifier filter cannot be repeated + rule: 'self.exists(f, f.type == ''ResponseHeaderModifier'') + ? self.exists_one(f, f.type == ''ResponseHeaderModifier'') + : true' + - message: RequestRedirect filter cannot be repeated + rule: 'self.exists(f, f.type == ''RequestRedirect'') ? self.exists_one(f, + f.type == ''RequestRedirect'') : true' + - message: URLRewrite filter cannot be repeated + rule: 'self.exists(f, f.type == ''URLRewrite'') ? self.exists_one(f, + f.type == ''URLRewrite'') : true' matches: default: - path: @@ -3730,6 +4237,53 @@ spec: maxLength: 1024 type: string type: object + x-kubernetes-validations: + - message: value must be an absolute path and start with + '/' when type one of ['Exact', 'PathPrefix'] + rule: '(self.type == ''Exact'' || self.type == ''PathPrefix'') + ? self.value.startsWith(''/'') : true' + - message: must not contain '//' when type one of ['Exact', + 'PathPrefix'] + rule: '(self.type == ''Exact'' || self.type == ''PathPrefix'') + ? !self.value.contains(''//'') : true' + - message: must not contain '/./' when type one of ['Exact', + 'PathPrefix'] + rule: '(self.type == ''Exact'' || self.type == ''PathPrefix'') + ? !self.value.contains(''/./'') : true' + - message: must not contain '/../' when type one of ['Exact', + 'PathPrefix'] + rule: '(self.type == ''Exact'' || self.type == ''PathPrefix'') + ? !self.value.contains(''/../'') : true' + - message: must not contain '%2f' when type one of ['Exact', + 'PathPrefix'] + rule: '(self.type == ''Exact'' || self.type == ''PathPrefix'') + ? !self.value.contains(''%2f'') : true' + - message: must not contain '%2F' when type one of ['Exact', + 'PathPrefix'] + rule: '(self.type == ''Exact'' || self.type == ''PathPrefix'') + ? !self.value.contains(''%2F'') : true' + - message: must not contain '#' when type one of ['Exact', + 'PathPrefix'] + rule: '(self.type == ''Exact'' || self.type == ''PathPrefix'') + ? !self.value.contains(''#'') : true' + - message: must not end with '/..' when type one of ['Exact', + 'PathPrefix'] + rule: '(self.type == ''Exact'' || self.type == ''PathPrefix'') + ? !self.value.endsWith(''/..'') : true' + - message: must not end with '/.' when type one of ['Exact', + 'PathPrefix'] + rule: '(self.type == ''Exact'' || self.type == ''PathPrefix'') + ? !self.value.endsWith(''/.'') : true' + - message: type must be one of ['Exact', 'PathPrefix', + 'RegularExpression'] + rule: self.type == 'Exact' || self.type == 'PathPrefix' + || self.type == 'RegularExpression' + - message: must only contain valid characters (matching + ^(?:[-A-Za-z0-9/._~!$&'()*+,;=:@]|[%][0-9a-fA-F]{2})+$) + for types ['Exact', 'PathPrefix'] + rule: '(self.type == ''Exact'' || self.type == ''PathPrefix'') + ? self.value.matches(r"""^(?:[-A-Za-z0-9/._~!$&''()*+,;=:@]|[%][0-9a-fA-F]{2})+$""") + : true' queryParams: description: "QueryParams specifies HTTP query parameter matchers. Multiple match values are ANDed together, @@ -3796,6 +4350,46 @@ spec: maxItems: 8 type: array type: object + x-kubernetes-validations: + - message: RequestRedirect filter must not be used together with + backendRefs + rule: '(has(self.backendRefs) && size(self.backendRefs) > 0) ? + (!has(self.filters) || self.filters.all(f, !has(f.requestRedirect))): + true' + - message: When using RequestRedirect filter with Path.ReplacePrefixMatch, + exactly one PathPrefix match must be specified + rule: '(has(self.filters) && self.filters.exists_one(f, has(f.requestRedirect) + && has(f.requestRedirect.path) && f.requestRedirect.path.type + == ''ReplacePrefixMatch'' && has(f.requestRedirect.path.replacePrefixMatch))) + ? ((size(self.matches) != 1 || !has(self.matches[0].path) || + self.matches[0].path.type != ''PathPrefix'') ? false : true) + : true' + - message: When using URLRewrite filter with Path.ReplacePrefixMatch, + exactly one PathPrefix match must be specified + rule: '(has(self.filters) && self.filters.exists_one(f, has(f.urlRewrite) + && has(f.urlRewrite.path) && f.urlRewrite.path.type == ''ReplacePrefixMatch'' + && has(f.urlRewrite.path.replacePrefixMatch))) ? ((size(self.matches) + != 1 || !has(self.matches[0].path) || self.matches[0].path.type + != ''PathPrefix'') ? false : true) : true' + - message: Within BackendRefs, when using RequestRedirect filter + with Path.ReplacePrefixMatch, exactly one PathPrefix match must + be specified + rule: '(has(self.backendRefs) && self.backendRefs.exists_one(b, + (has(b.filters) && b.filters.exists_one(f, has(f.requestRedirect) + && has(f.requestRedirect.path) && f.requestRedirect.path.type + == ''ReplacePrefixMatch'' && has(f.requestRedirect.path.replacePrefixMatch))) + )) ? ((size(self.matches) != 1 || !has(self.matches[0].path) + || self.matches[0].path.type != ''PathPrefix'') ? false : true) + : true' + - message: Within BackendRefs, When using URLRewrite filter with + Path.ReplacePrefixMatch, exactly one PathPrefix match must be + specified + rule: '(has(self.backendRefs) && self.backendRefs.exists_one(b, + (has(b.filters) && b.filters.exists_one(f, has(f.urlRewrite) + && has(f.urlRewrite.path) && f.urlRewrite.path.type == ''ReplacePrefixMatch'' + && has(f.urlRewrite.path.replacePrefixMatch))) )) ? ((size(self.matches) + != 1 || !has(self.matches[0].path) || self.matches[0].path.type + != ''PathPrefix'') ? false : true) : true' maxItems: 16 type: array type: object diff --git a/hack/cel-validation/httproute_test.go b/hack/cel-validation/httproute_test.go new file mode 100644 index 0000000000..5f08d36b8b --- /dev/null +++ b/hack/cel-validation/httproute_test.go @@ -0,0 +1,1393 @@ +/* +Copyright 2023 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 main + +import ( + "context" + "fmt" + "strings" + "testing" + "time" + + gatewayv1b1 "sigs.k8s.io/gateway-api/apis/v1beta1" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +//////////////////////////////////////////////////////////////////////////////// +//////////////////////////////////////////////////////////////////////////////// +// +// How are tests named? Where to add new tests? +// +// Ensure that tests for newly added CEL validations are added in the correctly +// named test function. For example, if you added a test at the +// `HTTPRouteFilter` hierarchy (i.e. either at the struct level, or on one of +// the immediate descendent fields), then the test will go in the +// TestHTTPRouteFilter function. If the appropriate test function does not +// exist, please create one. +// +//////////////////////////////////////////////////////////////////////////////// +//////////////////////////////////////////////////////////////////////////////// + +func TestHTTPPathMatch(t *testing.T) { + tests := []struct { + name string + wantErrors []string + path *gatewayv1b1.HTTPPathMatch + }{ + { + name: "invalid because path does not start with '/'", + wantErrors: []string{"value must be an absolute path and start with '/' when type one of ['Exact', 'PathPrefix']"}, + path: &gatewayv1b1.HTTPPathMatch{ + Type: ptrTo(gatewayv1b1.PathMatchType("PathPrefix")), + Value: ptrTo("foo"), + }, + }, + { + name: "invalid httpRoute prefix (/.)", + wantErrors: []string{"must not end with '/.' when type one of ['Exact', 'PathPrefix']"}, + path: &gatewayv1b1.HTTPPathMatch{ + Type: ptrTo(gatewayv1b1.PathMatchType("PathPrefix")), + Value: ptrTo("/."), + }, + }, + { + name: "invalid exact (/./)", + wantErrors: []string{"must not contain '/./' when type one of ['Exact', 'PathPrefix']"}, + path: &gatewayv1b1.HTTPPathMatch{ + Type: ptrTo(gatewayv1b1.PathMatchType("Exact")), + Value: ptrTo("/foo/./bar"), + }, + }, + { + name: "invalid type", + wantErrors: []string{"type must be one of ['Exact', 'PathPrefix', 'RegularExpression']"}, + path: &gatewayv1b1.HTTPPathMatch{ + Type: ptrTo(gatewayv1b1.PathMatchType("FooBar")), + Value: ptrTo("/path"), + }, + }, + { + name: "valid because type is RegularExpression but would not be valid for Exact", + path: &gatewayv1b1.HTTPPathMatch{ + Type: ptrTo(gatewayv1b1.PathMatchType("RegularExpression")), + Value: ptrTo("/foo/./bar"), + }, + }, + { + name: "valid httpRoute prefix", + path: &gatewayv1b1.HTTPPathMatch{ + Type: ptrTo(gatewayv1b1.PathMatchType("PathPrefix")), + Value: ptrTo("/path"), + }, + }, + { + name: "valid path with some special characters", + path: &gatewayv1b1.HTTPPathMatch{ + Type: ptrTo(gatewayv1b1.PathMatchType("Exact")), + Value: ptrTo("/abc/123'/a-b-c/d@gmail/%0A"), + }, + }, + { + name: "invalid prefix path (/[])", + path: &gatewayv1b1.HTTPPathMatch{ + Type: ptrTo(gatewayv1b1.PathMatchType("PathPrefix")), + Value: ptrTo("/[]"), + }, + wantErrors: []string{"must only contain valid characters (matching ^(?:[-A-Za-z0-9/._~!$&'()*+,;=:@]|[%][0-9a-fA-F]{2})+$) for types ['Exact', 'PathPrefix']"}, + }, + { + name: "invalid exact path (/^)", + path: &gatewayv1b1.HTTPPathMatch{ + Type: ptrTo(gatewayv1b1.PathMatchType("Exact")), + Value: ptrTo("/^"), + }, + wantErrors: []string{"must only contain valid characters (matching ^(?:[-A-Za-z0-9/._~!$&'()*+,;=:@]|[%][0-9a-fA-F]{2})+$) for types ['Exact', 'PathPrefix']"}, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + route := &gatewayv1b1.HTTPRoute{ + ObjectMeta: metav1.ObjectMeta{ + Name: fmt.Sprintf("foo-%v", time.Now().UnixNano()), + Namespace: metav1.NamespaceDefault, + }, + Spec: gatewayv1b1.HTTPRouteSpec{ + Rules: []gatewayv1b1.HTTPRouteRule{{ + Matches: []gatewayv1b1.HTTPRouteMatch{{ + Path: tc.path, + }}, + BackendRefs: []gatewayv1b1.HTTPBackendRef{{ + BackendRef: gatewayv1b1.BackendRef{ + BackendObjectReference: gatewayv1b1.BackendObjectReference{ + Name: gatewayv1b1.ObjectName("test"), + Port: ptrTo(gatewayv1b1.PortNumber(8080)), + }, + }, + }}, + }}, + }, + } + validateHTTPRoute(t, route, tc.wantErrors) + }) + } +} + +func TestBackendObjectReference(t *testing.T) { + portPtr := func(n int) *gatewayv1b1.PortNumber { + p := gatewayv1b1.PortNumber(n) + return &p + } + + groupPtr := func(g string) *gatewayv1b1.Group { + p := gatewayv1b1.Group(g) + return &p + } + + kindPtr := func(k string) *gatewayv1b1.Kind { + p := gatewayv1b1.Kind(k) + return &p + } + + tests := []struct { + name string + wantErrors []string + rules []gatewayv1b1.HTTPRouteRule + backendRef gatewayv1b1.BackendObjectReference + }{ + { + name: "default groupkind with port", + backendRef: gatewayv1b1.BackendObjectReference{ + Name: "backend", + Port: portPtr(99), + }, + }, + { + name: "default groupkind with no port", + wantErrors: []string{"Must have port for Service reference"}, + backendRef: gatewayv1b1.BackendObjectReference{ + Name: "backend", + }, + }, + { + name: "explicit service with port", + backendRef: gatewayv1b1.BackendObjectReference{ + Group: groupPtr(""), + Kind: kindPtr("Service"), + Name: "backend", + Port: portPtr(99), + }, + }, + { + name: "explicit service with no port", + wantErrors: []string{"Must have port for Service reference"}, + backendRef: gatewayv1b1.BackendObjectReference{ + Group: groupPtr(""), + Kind: kindPtr("Service"), + Name: "backend", + }, + }, + { + name: "explicit ref with no port", + backendRef: gatewayv1b1.BackendObjectReference{ + Group: groupPtr("foo.example.com"), + Kind: kindPtr("Foo"), + Name: "backend", + }, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + route := &gatewayv1b1.HTTPRoute{ + ObjectMeta: metav1.ObjectMeta{ + Name: fmt.Sprintf("foo-%v", time.Now().UnixNano()), + Namespace: metav1.NamespaceDefault, + }, + Spec: gatewayv1b1.HTTPRouteSpec{ + Rules: []gatewayv1b1.HTTPRouteRule{{ + BackendRefs: []gatewayv1b1.HTTPBackendRef{{ + BackendRef: gatewayv1b1.BackendRef{ + BackendObjectReference: tc.backendRef, + }, + }}, + }}, + }, + } + validateHTTPRoute(t, route, tc.wantErrors) + }) + } +} + +func TestHTTPRouteFilter(t *testing.T) { + tests := []struct { + name string + wantErrors []string + routeFilter gatewayv1b1.HTTPRouteFilter + }{ + { + name: "valid HTTPRouteFilterRequestHeaderModifier route filter", + routeFilter: gatewayv1b1.HTTPRouteFilter{ + Type: gatewayv1b1.HTTPRouteFilterRequestHeaderModifier, + RequestHeaderModifier: &gatewayv1b1.HTTPHeaderFilter{ + Set: []gatewayv1b1.HTTPHeader{{Name: "name", Value: "foo"}}, + Add: []gatewayv1b1.HTTPHeader{{Name: "add", Value: "foo"}}, + Remove: []string{"remove"}, + }, + }, + }, + { + name: "invalid HTTPRouteFilterRequestHeaderModifier type filter with non-matching field", + routeFilter: gatewayv1b1.HTTPRouteFilter{ + Type: gatewayv1b1.HTTPRouteFilterRequestHeaderModifier, + RequestMirror: &gatewayv1b1.HTTPRequestMirrorFilter{}, + }, + wantErrors: []string{"filter.RequestHeaderModifier must be specified for RequestHeaderModifier HTTPRouteFilter.Type", "filter.RequestMirror must be nil if the HTTPRouteFilter.Type is not RequestMirror"}, + }, + { + name: "invalid HTTPRouteFilterRequestHeaderModifier type filter with empty value field", + routeFilter: gatewayv1b1.HTTPRouteFilter{ + Type: gatewayv1b1.HTTPRouteFilterRequestHeaderModifier, + }, + wantErrors: []string{"filter.RequestHeaderModifier must be specified for RequestHeaderModifier HTTPRouteFilter.Type"}, + }, + { + name: "valid HTTPRouteFilterRequestMirror route filter", + routeFilter: gatewayv1b1.HTTPRouteFilter{ + Type: gatewayv1b1.HTTPRouteFilterRequestMirror, + RequestMirror: &gatewayv1b1.HTTPRequestMirrorFilter{BackendRef: gatewayv1b1.BackendObjectReference{ + Group: ptrTo(gatewayv1b1.Group("group")), + Kind: ptrTo(gatewayv1b1.Kind("kind")), + Name: "name", + Namespace: ptrTo(gatewayv1b1.Namespace("ns")), + Port: ptrTo(gatewayv1b1.PortNumber(22)), + }}, + }, + }, + { + name: "invalid HTTPRouteFilterRequestMirror type filter with non-matching field", + routeFilter: gatewayv1b1.HTTPRouteFilter{ + Type: gatewayv1b1.HTTPRouteFilterRequestMirror, + RequestHeaderModifier: &gatewayv1b1.HTTPHeaderFilter{}, + }, + wantErrors: []string{"filter.RequestHeaderModifier must be nil if the HTTPRouteFilter.Type is not RequestHeaderModifier", "filter.RequestMirror must be specified for RequestMirror HTTPRouteFilter.Type"}, + }, + { + name: "invalid HTTPRouteFilterRequestMirror type filter with empty value field", + routeFilter: gatewayv1b1.HTTPRouteFilter{ + Type: gatewayv1b1.HTTPRouteFilterRequestMirror, + }, + wantErrors: []string{"filter.RequestMirror must be specified for RequestMirror HTTPRouteFilter.Type"}, + }, + { + name: "valid HTTPRouteFilterRequestRedirect route filter", + routeFilter: gatewayv1b1.HTTPRouteFilter{ + Type: gatewayv1b1.HTTPRouteFilterRequestRedirect, + RequestRedirect: &gatewayv1b1.HTTPRequestRedirectFilter{ + Scheme: ptrTo("http"), + Hostname: ptrTo(gatewayv1b1.PreciseHostname("hostname")), + Path: &gatewayv1b1.HTTPPathModifier{ + Type: gatewayv1b1.FullPathHTTPPathModifier, + ReplaceFullPath: ptrTo("path"), + }, + Port: ptrTo(gatewayv1b1.PortNumber(8080)), + StatusCode: ptrTo(302), + }, + }, + }, + { + name: "invalid HTTPRouteFilterRequestRedirect type filter with non-matching field", + routeFilter: gatewayv1b1.HTTPRouteFilter{ + Type: gatewayv1b1.HTTPRouteFilterRequestRedirect, + RequestMirror: &gatewayv1b1.HTTPRequestMirrorFilter{}, + }, + wantErrors: []string{"filter.RequestMirror must be nil if the HTTPRouteFilter.Type is not RequestMirror", "filter.RequestRedirect must be specified for RequestRedirect HTTPRouteFilter.Type"}, + }, + { + name: "invalid HTTPRouteFilterRequestRedirect type filter with empty value field", + routeFilter: gatewayv1b1.HTTPRouteFilter{ + Type: gatewayv1b1.HTTPRouteFilterRequestRedirect, + }, + wantErrors: []string{"filter.RequestRedirect must be specified for RequestRedirect HTTPRouteFilter.Type"}, + }, + { + name: "valid HTTPRouteFilterExtensionRef filter", + routeFilter: gatewayv1b1.HTTPRouteFilter{ + Type: gatewayv1b1.HTTPRouteFilterExtensionRef, + ExtensionRef: &gatewayv1b1.LocalObjectReference{ + Group: "group", + Kind: "kind", + Name: "name", + }, + }, + }, + { + name: "invalid HTTPRouteFilterExtensionRef type filter with non-matching field", + routeFilter: gatewayv1b1.HTTPRouteFilter{ + Type: gatewayv1b1.HTTPRouteFilterExtensionRef, + RequestMirror: &gatewayv1b1.HTTPRequestMirrorFilter{}, + }, + wantErrors: []string{"filter.RequestMirror must be nil if the HTTPRouteFilter.Type is not RequestMirror", "filter.ExtensionRef must be specified for ExtensionRef HTTPRouteFilter.Type"}, + }, + { + name: "invalid HTTPRouteFilterExtensionRef type filter with empty value field", + routeFilter: gatewayv1b1.HTTPRouteFilter{ + Type: gatewayv1b1.HTTPRouteFilterExtensionRef, + }, + wantErrors: []string{"filter.ExtensionRef must be specified for ExtensionRef HTTPRouteFilter.Type"}, + }, + { + name: "valid HTTPRouteFilterURLRewrite route filter", + routeFilter: gatewayv1b1.HTTPRouteFilter{ + Type: gatewayv1b1.HTTPRouteFilterURLRewrite, + URLRewrite: &gatewayv1b1.HTTPURLRewriteFilter{ + Hostname: ptrTo(gatewayv1b1.PreciseHostname("hostname")), + Path: &gatewayv1b1.HTTPPathModifier{ + Type: gatewayv1b1.FullPathHTTPPathModifier, + ReplaceFullPath: ptrTo("path"), + }, + }, + }, + }, + { + name: "invalid HTTPRouteFilterURLRewrite type filter with non-matching field", + routeFilter: gatewayv1b1.HTTPRouteFilter{ + Type: gatewayv1b1.HTTPRouteFilterURLRewrite, + RequestMirror: &gatewayv1b1.HTTPRequestMirrorFilter{}, + }, + wantErrors: []string{"filter.RequestMirror must be nil if the HTTPRouteFilter.Type is not RequestMirror", "filter.URLRewrite must be specified for URLRewrite HTTPRouteFilter.Type"}, + }, + { + name: "invalid HTTPRouteFilterURLRewrite type filter with empty value field", + routeFilter: gatewayv1b1.HTTPRouteFilter{ + Type: gatewayv1b1.HTTPRouteFilterURLRewrite, + }, + wantErrors: []string{"filter.URLRewrite must be specified for URLRewrite HTTPRouteFilter.Type"}, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + route := &gatewayv1b1.HTTPRoute{ + ObjectMeta: metav1.ObjectMeta{ + Name: fmt.Sprintf("foo-%v", time.Now().UnixNano()), + Namespace: metav1.NamespaceDefault, + }, + Spec: gatewayv1b1.HTTPRouteSpec{ + Rules: []gatewayv1b1.HTTPRouteRule{{ + Filters: []gatewayv1b1.HTTPRouteFilter{tc.routeFilter}, + }}, + }, + } + validateHTTPRoute(t, route, tc.wantErrors) + }) + } +} + +func TestHTTPRouteRule(t *testing.T) { + testService := gatewayv1b1.ObjectName("test-service") + tests := []struct { + name string + wantErrors []string + rules []gatewayv1b1.HTTPRouteRule + }{ + { + name: "valid httpRoute with no filters", + rules: []gatewayv1b1.HTTPRouteRule{ + { + Matches: []gatewayv1b1.HTTPRouteMatch{ + { + Path: &gatewayv1b1.HTTPPathMatch{ + Type: ptrTo(gatewayv1b1.PathMatchType("PathPrefix")), + Value: ptrTo("/"), + }, + }, + }, + BackendRefs: []gatewayv1b1.HTTPBackendRef{ + { + BackendRef: gatewayv1b1.BackendRef{ + BackendObjectReference: gatewayv1b1.BackendObjectReference{ + Name: testService, + Port: ptrTo(gatewayv1b1.PortNumber(8080)), + }, + Weight: ptrTo(int32(100)), + }, + }, + }, + }, + }, + }, + { + name: "valid httpRoute with 1 filter", + rules: []gatewayv1b1.HTTPRouteRule{ + { + Matches: []gatewayv1b1.HTTPRouteMatch{ + { + Path: &gatewayv1b1.HTTPPathMatch{ + Type: ptrTo(gatewayv1b1.PathMatchType("PathPrefix")), + Value: ptrTo("/"), + }, + }, + }, + Filters: []gatewayv1b1.HTTPRouteFilter{ + { + Type: gatewayv1b1.HTTPRouteFilterRequestMirror, + RequestMirror: &gatewayv1b1.HTTPRequestMirrorFilter{ + BackendRef: gatewayv1b1.BackendObjectReference{ + Name: testService, + Port: ptrTo(gatewayv1b1.PortNumber(8081)), + }, + }, + }, + }, + }, + }, + }, + { + name: "valid httpRoute with duplicate ExtensionRef filters", + rules: []gatewayv1b1.HTTPRouteRule{ + { + Matches: []gatewayv1b1.HTTPRouteMatch{ + { + Path: &gatewayv1b1.HTTPPathMatch{ + Type: ptrTo(gatewayv1b1.PathMatchType("PathPrefix")), + Value: ptrTo("/"), + }, + }, + }, + Filters: []gatewayv1b1.HTTPRouteFilter{ + { + Type: gatewayv1b1.HTTPRouteFilterRequestHeaderModifier, + RequestHeaderModifier: &gatewayv1b1.HTTPHeaderFilter{ + Set: []gatewayv1b1.HTTPHeader{ + { + Name: "special-header", + Value: "foo", + }, + }, + }, + }, + { + Type: gatewayv1b1.HTTPRouteFilterRequestMirror, + RequestMirror: &gatewayv1b1.HTTPRequestMirrorFilter{ + BackendRef: gatewayv1b1.BackendObjectReference{ + Name: testService, + Port: ptrTo(gatewayv1b1.PortNumber(8080)), + }, + }, + }, + { + Type: "ExtensionRef", + ExtensionRef: &gatewayv1b1.LocalObjectReference{ + Kind: "Service", + Name: "test", + }, + }, + { + Type: "ExtensionRef", + ExtensionRef: &gatewayv1b1.LocalObjectReference{ + Kind: "Service", + Name: "test", + }, + }, + { + Type: "ExtensionRef", + ExtensionRef: &gatewayv1b1.LocalObjectReference{ + Kind: "Service", + Name: "test", + }, + }, + }, + }, + }, + }, + { + name: "valid redirect path modifier", + rules: []gatewayv1b1.HTTPRouteRule{ + { + Filters: []gatewayv1b1.HTTPRouteFilter{ + { + Type: gatewayv1b1.HTTPRouteFilterRequestRedirect, + RequestRedirect: &gatewayv1b1.HTTPRequestRedirectFilter{ + Path: &gatewayv1b1.HTTPPathModifier{ + Type: gatewayv1b1.FullPathHTTPPathModifier, + ReplaceFullPath: ptrTo("foo"), + }, + }, + }, + }, + }, + }, + }, + { + name: "valid rewrite path modifier", + rules: []gatewayv1b1.HTTPRouteRule{{ + Matches: []gatewayv1b1.HTTPRouteMatch{{ + Path: &gatewayv1b1.HTTPPathMatch{ + Type: ptrTo(gatewayv1b1.PathMatchPathPrefix), + Value: ptrTo("/bar"), + }, + }}, + Filters: []gatewayv1b1.HTTPRouteFilter{{ + Type: gatewayv1b1.HTTPRouteFilterURLRewrite, + URLRewrite: &gatewayv1b1.HTTPURLRewriteFilter{ + Path: &gatewayv1b1.HTTPPathModifier{ + Type: gatewayv1b1.PrefixMatchHTTPPathModifier, + ReplacePrefixMatch: ptrTo("foo"), + }, + }, + }}, + }}, + }, + { + name: "multiple actions for different request headers", + rules: []gatewayv1b1.HTTPRouteRule{{ + Filters: []gatewayv1b1.HTTPRouteFilter{{ + Type: gatewayv1b1.HTTPRouteFilterRequestHeaderModifier, + RequestHeaderModifier: &gatewayv1b1.HTTPHeaderFilter{ + Add: []gatewayv1b1.HTTPHeader{ + { + Name: gatewayv1b1.HTTPHeaderName("x-vegetable"), + Value: "carrot", + }, + { + Name: gatewayv1b1.HTTPHeaderName("x-grain"), + Value: "rye", + }, + }, + Set: []gatewayv1b1.HTTPHeader{ + { + Name: gatewayv1b1.HTTPHeaderName("x-fruit"), + Value: "watermelon", + }, + { + Name: gatewayv1b1.HTTPHeaderName("x-spice"), + Value: "coriander", + }, + }, + }, + }}, + }}, + }, + { + name: "multiple actions for different response headers", + rules: []gatewayv1b1.HTTPRouteRule{{ + Filters: []gatewayv1b1.HTTPRouteFilter{{ + Type: gatewayv1b1.HTTPRouteFilterResponseHeaderModifier, + ResponseHeaderModifier: &gatewayv1b1.HTTPHeaderFilter{ + Add: []gatewayv1b1.HTTPHeader{{ + Name: gatewayv1b1.HTTPHeaderName("x-example"), + Value: "blueberry", + }}, + Set: []gatewayv1b1.HTTPHeader{{ + Name: gatewayv1b1.HTTPHeaderName("x-different"), + Value: "turnip", + }}, + }, + }}, + }}, + }, + { + name: "backendref with request redirect httpRoute filter", + wantErrors: []string{"RequestRedirect filter must not be used together with backendRefs"}, + rules: []gatewayv1b1.HTTPRouteRule{ + { + Filters: []gatewayv1b1.HTTPRouteFilter{ + { + Type: gatewayv1b1.HTTPRouteFilterRequestRedirect, + RequestRedirect: &gatewayv1b1.HTTPRequestRedirectFilter{ + Scheme: ptrTo("https"), + StatusCode: ptrTo(301), + }, + }, + }, + BackendRefs: []gatewayv1b1.HTTPBackendRef{ + { + BackendRef: gatewayv1b1.BackendRef{ + BackendObjectReference: gatewayv1b1.BackendObjectReference{ + Name: testService, + Port: ptrTo(gatewayv1b1.PortNumber(80)), + }, + }, + }, + }, + }, + }, + }, + { + name: "request redirect without backendref in httpRoute filter", + rules: []gatewayv1b1.HTTPRouteRule{ + { + Filters: []gatewayv1b1.HTTPRouteFilter{ + { + Type: gatewayv1b1.HTTPRouteFilterRequestRedirect, + RequestRedirect: &gatewayv1b1.HTTPRequestRedirectFilter{ + Scheme: ptrTo("https"), + StatusCode: ptrTo(301), + }, + }, + }, + }, + }, + }, + { + name: "backendref without request redirect filter", + rules: []gatewayv1b1.HTTPRouteRule{ + { + Filters: []gatewayv1b1.HTTPRouteFilter{ + { + Type: gatewayv1b1.HTTPRouteFilterRequestHeaderModifier, + RequestHeaderModifier: &gatewayv1b1.HTTPHeaderFilter{ + Set: []gatewayv1b1.HTTPHeader{{Name: "name", Value: "foo"}}, + }, + }, + }, + BackendRefs: []gatewayv1b1.HTTPBackendRef{ + { + BackendRef: gatewayv1b1.BackendRef{ + BackendObjectReference: gatewayv1b1.BackendObjectReference{ + Name: testService, + Port: ptrTo(gatewayv1b1.PortNumber(80)), + }, + }, + }, + }, + }, + }, + }, + { + name: "backendref without any filter", + rules: []gatewayv1b1.HTTPRouteRule{ + { + BackendRefs: []gatewayv1b1.HTTPBackendRef{ + { + BackendRef: gatewayv1b1.BackendRef{ + BackendObjectReference: gatewayv1b1.BackendObjectReference{ + Name: testService, + Port: ptrTo(gatewayv1b1.PortNumber(80)), + }, + }, + }, + }, + }, + }, + }, + { + name: "valid use of URLRewrite filter", + rules: []gatewayv1b1.HTTPRouteRule{{ + Matches: []gatewayv1b1.HTTPRouteMatch{ + { + Path: &gatewayv1b1.HTTPPathMatch{ + Type: ptrTo(gatewayv1b1.PathMatchPathPrefix), + Value: ptrTo("/foo"), + }, + }, + }, + Filters: []gatewayv1b1.HTTPRouteFilter{{ + Type: gatewayv1b1.HTTPRouteFilterURLRewrite, + URLRewrite: &gatewayv1b1.HTTPURLRewriteFilter{ + Path: &gatewayv1b1.HTTPPathModifier{ + Type: gatewayv1b1.PrefixMatchHTTPPathModifier, + ReplacePrefixMatch: ptrTo("foo"), + }, + }, + }}, + }}, + }, + { + name: "invalid URLRewrite filter because too many path matches", + wantErrors: []string{"When using URLRewrite filter with Path.ReplacePrefixMatch, exactly one PathPrefix match must be specified"}, + rules: []gatewayv1b1.HTTPRouteRule{{ + Matches: []gatewayv1b1.HTTPRouteMatch{ + { + Path: &gatewayv1b1.HTTPPathMatch{ + Type: ptrTo(gatewayv1b1.PathMatchPathPrefix), + Value: ptrTo("/foo"), + }, + }, + { // Cannot have multiple path matches. + Path: &gatewayv1b1.HTTPPathMatch{ + Type: ptrTo(gatewayv1b1.PathMatchPathPrefix), + Value: ptrTo("/bar"), + }, + }, + }, + Filters: []gatewayv1b1.HTTPRouteFilter{{ + Type: gatewayv1b1.HTTPRouteFilterURLRewrite, + URLRewrite: &gatewayv1b1.HTTPURLRewriteFilter{ + Path: &gatewayv1b1.HTTPPathModifier{ + Type: gatewayv1b1.PrefixMatchHTTPPathModifier, + ReplacePrefixMatch: ptrTo("foo"), + }, + }, + }}, + }}, + }, + { + name: "invalid URLRewrite filter because too many path matches", + wantErrors: []string{"When using URLRewrite filter with Path.ReplacePrefixMatch, exactly one PathPrefix match must be specified"}, + rules: []gatewayv1b1.HTTPRouteRule{{ + Matches: []gatewayv1b1.HTTPRouteMatch{ + { + Path: &gatewayv1b1.HTTPPathMatch{ + Type: ptrTo(gatewayv1b1.PathMatchType(gatewayv1b1.FullPathHTTPPathModifier)), // Incorrect Patch match Type for URLRewrite filter with ReplacePrefixMatch. + Value: ptrTo("/foo"), + }, + }, + }, + Filters: []gatewayv1b1.HTTPRouteFilter{{ + Type: gatewayv1b1.HTTPRouteFilterURLRewrite, + URLRewrite: &gatewayv1b1.HTTPURLRewriteFilter{ + Path: &gatewayv1b1.HTTPPathModifier{ + Type: gatewayv1b1.PrefixMatchHTTPPathModifier, + ReplacePrefixMatch: ptrTo("foo"), + }, + }, + }}, + }}, + }, + { + name: "valid use of RequestRedirect filter", + rules: []gatewayv1b1.HTTPRouteRule{{ + Matches: []gatewayv1b1.HTTPRouteMatch{ + { + Path: &gatewayv1b1.HTTPPathMatch{ + Type: ptrTo(gatewayv1b1.PathMatchPathPrefix), + Value: ptrTo("/foo"), + }, + }, + }, + Filters: []gatewayv1b1.HTTPRouteFilter{{ + Type: gatewayv1b1.HTTPRouteFilterRequestRedirect, + RequestRedirect: &gatewayv1b1.HTTPRequestRedirectFilter{ + Path: &gatewayv1b1.HTTPPathModifier{ + Type: gatewayv1b1.PrefixMatchHTTPPathModifier, + ReplacePrefixMatch: ptrTo("foo"), + }, + }, + }}, + }}, + }, + { + name: "invalid RequestRedirect filter because too many path matches", + wantErrors: []string{"When using RequestRedirect filter with Path.ReplacePrefixMatch, exactly one PathPrefix match must be specified"}, + rules: []gatewayv1b1.HTTPRouteRule{{ + Matches: []gatewayv1b1.HTTPRouteMatch{ + { + Path: &gatewayv1b1.HTTPPathMatch{ + Type: ptrTo(gatewayv1b1.PathMatchPathPrefix), + Value: ptrTo("/foo"), + }, + }, + { // Cannot have multiple path matches. + Path: &gatewayv1b1.HTTPPathMatch{ + Type: ptrTo(gatewayv1b1.PathMatchPathPrefix), + Value: ptrTo("/bar"), + }, + }, + }, + Filters: []gatewayv1b1.HTTPRouteFilter{{ + Type: gatewayv1b1.HTTPRouteFilterRequestRedirect, + RequestRedirect: &gatewayv1b1.HTTPRequestRedirectFilter{ + Path: &gatewayv1b1.HTTPPathModifier{ + Type: gatewayv1b1.PrefixMatchHTTPPathModifier, + ReplacePrefixMatch: ptrTo("foo"), + }, + }, + }}, + }}, + }, + { + name: "invalid RequestRedirect filter because path match has type ReplaceFullPath", + wantErrors: []string{"When using RequestRedirect filter with Path.ReplacePrefixMatch, exactly one PathPrefix match must be specified"}, + rules: []gatewayv1b1.HTTPRouteRule{{ + Matches: []gatewayv1b1.HTTPRouteMatch{ + { + Path: &gatewayv1b1.HTTPPathMatch{ + Type: ptrTo(gatewayv1b1.PathMatchType(gatewayv1b1.FullPathHTTPPathModifier)), // Incorrect Patch match Type for RequestRedirect filter with ReplacePrefixMatch. + Value: ptrTo("/foo"), + }, + }, + }, + Filters: []gatewayv1b1.HTTPRouteFilter{{ + Type: gatewayv1b1.HTTPRouteFilterRequestRedirect, + RequestRedirect: &gatewayv1b1.HTTPRequestRedirectFilter{ + Path: &gatewayv1b1.HTTPPathModifier{ + Type: gatewayv1b1.PrefixMatchHTTPPathModifier, + ReplacePrefixMatch: ptrTo("foo"), + }, + }, + }}, + }}, + }, + { + name: "valid use of URLRewrite filter (within backendRefs)", + rules: []gatewayv1b1.HTTPRouteRule{{ + Matches: []gatewayv1b1.HTTPRouteMatch{ + { + Path: &gatewayv1b1.HTTPPathMatch{ + Type: ptrTo(gatewayv1b1.PathMatchPathPrefix), + Value: ptrTo("/foo"), + }, + }, + }, + BackendRefs: []gatewayv1b1.HTTPBackendRef{ + { + BackendRef: gatewayv1b1.BackendRef{ + BackendObjectReference: gatewayv1b1.BackendObjectReference{ + Name: testService, + Port: ptrTo(gatewayv1b1.PortNumber(80)), + }, + }, + Filters: []gatewayv1b1.HTTPRouteFilter{{ + Type: gatewayv1b1.HTTPRouteFilterURLRewrite, + URLRewrite: &gatewayv1b1.HTTPURLRewriteFilter{ + Path: &gatewayv1b1.HTTPPathModifier{ + Type: gatewayv1b1.PrefixMatchHTTPPathModifier, + ReplacePrefixMatch: ptrTo("foo"), + }, + }, + }}, + }, + }, + }}, + }, + { + name: "invalid URLRewrite filter (within backendRefs) because too many path matches", + wantErrors: []string{"Within BackendRefs, when using URLRewrite filter with Path.ReplacePrefixMatch, exactly one PathPrefix match must be specified"}, + rules: []gatewayv1b1.HTTPRouteRule{{ + Matches: []gatewayv1b1.HTTPRouteMatch{ + { + Path: &gatewayv1b1.HTTPPathMatch{ + Type: ptrTo(gatewayv1b1.PathMatchPathPrefix), + Value: ptrTo("/foo"), + }, + }, + { // Cannot have multiple path matches. + Path: &gatewayv1b1.HTTPPathMatch{ + Type: ptrTo(gatewayv1b1.PathMatchPathPrefix), + Value: ptrTo("/bar"), + }, + }, + }, + BackendRefs: []gatewayv1b1.HTTPBackendRef{ + { + BackendRef: gatewayv1b1.BackendRef{ + BackendObjectReference: gatewayv1b1.BackendObjectReference{ + Name: testService, + Port: ptrTo(gatewayv1b1.PortNumber(80)), + }, + }, + Filters: []gatewayv1b1.HTTPRouteFilter{{ + Type: gatewayv1b1.HTTPRouteFilterURLRewrite, + URLRewrite: &gatewayv1b1.HTTPURLRewriteFilter{ + Path: &gatewayv1b1.HTTPPathModifier{ + Type: gatewayv1b1.PrefixMatchHTTPPathModifier, + ReplacePrefixMatch: ptrTo("foo"), + }, + }, + }}, + }, + }, + }}, + }, + { + name: "invalid URLRewrite filter (within backendRefs) because path match has type ReplaceFullPath", + wantErrors: []string{"Within BackendRefs, when using URLRewrite filter with Path.ReplacePrefixMatch, exactly one PathPrefix match must be specified"}, + rules: []gatewayv1b1.HTTPRouteRule{{ + Matches: []gatewayv1b1.HTTPRouteMatch{ + { + Path: &gatewayv1b1.HTTPPathMatch{ + Type: ptrTo(gatewayv1b1.PathMatchType(gatewayv1b1.FullPathHTTPPathModifier)), // Incorrect Patch match Type for URLRewrite filter with ReplacePrefixMatch. + Value: ptrTo("/foo"), + }, + }, + }, + BackendRefs: []gatewayv1b1.HTTPBackendRef{ + { + BackendRef: gatewayv1b1.BackendRef{ + BackendObjectReference: gatewayv1b1.BackendObjectReference{ + Name: testService, + Port: ptrTo(gatewayv1b1.PortNumber(80)), + }, + }, + Filters: []gatewayv1b1.HTTPRouteFilter{{ + Type: gatewayv1b1.HTTPRouteFilterURLRewrite, + URLRewrite: &gatewayv1b1.HTTPURLRewriteFilter{ + Path: &gatewayv1b1.HTTPPathModifier{ + Type: gatewayv1b1.PrefixMatchHTTPPathModifier, + ReplacePrefixMatch: ptrTo("foo"), + }, + }, + }}, + }, + }, + }}, + }, + { + name: "valid use of RequestRedirect filter (within backendRefs)", + rules: []gatewayv1b1.HTTPRouteRule{{ + Matches: []gatewayv1b1.HTTPRouteMatch{ + { + Path: &gatewayv1b1.HTTPPathMatch{ + Type: ptrTo(gatewayv1b1.PathMatchPathPrefix), + Value: ptrTo("/foo"), + }, + }, + }, + BackendRefs: []gatewayv1b1.HTTPBackendRef{ + { + BackendRef: gatewayv1b1.BackendRef{ + BackendObjectReference: gatewayv1b1.BackendObjectReference{ + Name: testService, + Port: ptrTo(gatewayv1b1.PortNumber(80)), + }, + }, + Filters: []gatewayv1b1.HTTPRouteFilter{{ + Type: gatewayv1b1.HTTPRouteFilterRequestRedirect, + RequestRedirect: &gatewayv1b1.HTTPRequestRedirectFilter{ + Path: &gatewayv1b1.HTTPPathModifier{ + Type: gatewayv1b1.PrefixMatchHTTPPathModifier, + ReplacePrefixMatch: ptrTo("foo"), + }, + }, + }}, + }, + }, + }}, + }, + { + name: "invalid RequestRedirect filter (within backendRefs) because too many path matches", + wantErrors: []string{"Within BackendRefs, when using RequestRedirect filter with Path.ReplacePrefixMatch, exactly one PathPrefix match must be specified"}, + rules: []gatewayv1b1.HTTPRouteRule{{ + Matches: []gatewayv1b1.HTTPRouteMatch{ + { + Path: &gatewayv1b1.HTTPPathMatch{ + Type: ptrTo(gatewayv1b1.PathMatchPathPrefix), + Value: ptrTo("/foo"), + }, + }, + { // Cannot have multiple path matches. + Path: &gatewayv1b1.HTTPPathMatch{ + Type: ptrTo(gatewayv1b1.PathMatchPathPrefix), + Value: ptrTo("/bar"), + }, + }, + }, + BackendRefs: []gatewayv1b1.HTTPBackendRef{ + { + BackendRef: gatewayv1b1.BackendRef{ + BackendObjectReference: gatewayv1b1.BackendObjectReference{ + Name: testService, + Port: ptrTo(gatewayv1b1.PortNumber(80)), + }, + }, + Filters: []gatewayv1b1.HTTPRouteFilter{{ + Type: gatewayv1b1.HTTPRouteFilterRequestRedirect, + RequestRedirect: &gatewayv1b1.HTTPRequestRedirectFilter{ + Path: &gatewayv1b1.HTTPPathModifier{ + Type: gatewayv1b1.PrefixMatchHTTPPathModifier, + ReplacePrefixMatch: ptrTo("foo"), + }, + }, + }}, + }, + }, + }}, + }, + { + name: "invalid RequestRedirect filter (within backendRefs) because path match has type ReplaceFullPath", + wantErrors: []string{"Within BackendRefs, when using RequestRedirect filter with Path.ReplacePrefixMatch, exactly one PathPrefix match must be specified"}, + rules: []gatewayv1b1.HTTPRouteRule{{ + Matches: []gatewayv1b1.HTTPRouteMatch{ + { + Path: &gatewayv1b1.HTTPPathMatch{ + Type: ptrTo(gatewayv1b1.PathMatchType(gatewayv1b1.FullPathHTTPPathModifier)), // Incorrect Patch match Type for RequestRedirect filter with ReplacePrefixMatch. + Value: ptrTo("/foo"), + }, + }, + }, + BackendRefs: []gatewayv1b1.HTTPBackendRef{ + { + BackendRef: gatewayv1b1.BackendRef{ + BackendObjectReference: gatewayv1b1.BackendObjectReference{ + Name: testService, + Port: ptrTo(gatewayv1b1.PortNumber(80)), + }, + }, + Filters: []gatewayv1b1.HTTPRouteFilter{{ + Type: gatewayv1b1.HTTPRouteFilterRequestRedirect, + RequestRedirect: &gatewayv1b1.HTTPRequestRedirectFilter{ + Path: &gatewayv1b1.HTTPPathModifier{ + Type: gatewayv1b1.PrefixMatchHTTPPathModifier, + ReplacePrefixMatch: ptrTo("foo"), + }, + }, + }}, + }, + }, + }}, + }, + { + name: "rewrite and redirect filters combined (invalid)", + wantErrors: []string{"May specify either httpRouteFilterRequestRedirect or httpRouteFilterRequestRewrite, but not both"}, // errCount: 3, + rules: []gatewayv1b1.HTTPRouteRule{{ + Filters: []gatewayv1b1.HTTPRouteFilter{{ + Type: gatewayv1b1.HTTPRouteFilterURLRewrite, + URLRewrite: &gatewayv1b1.HTTPURLRewriteFilter{ + Path: &gatewayv1b1.HTTPPathModifier{ + Type: gatewayv1b1.PrefixMatchHTTPPathModifier, + ReplacePrefixMatch: ptrTo("foo"), + }, + }, + }, { + Type: gatewayv1b1.HTTPRouteFilterRequestRedirect, + RequestRedirect: &gatewayv1b1.HTTPRequestRedirectFilter{ + Path: &gatewayv1b1.HTTPPathModifier{ + Type: gatewayv1b1.PrefixMatchHTTPPathModifier, + ReplacePrefixMatch: ptrTo("foo"), + }, + }, + }}, + }}, + }, + { + name: "invalid because repeated URLRewrite filter", + wantErrors: []string{"URLRewrite filter cannot be repeated"}, + rules: []gatewayv1b1.HTTPRouteRule{ + { + Matches: []gatewayv1b1.HTTPRouteMatch{ + { + Path: &gatewayv1b1.HTTPPathMatch{ + Type: ptrTo(gatewayv1b1.PathMatchType("PathPrefix")), + Value: ptrTo("/"), + }, + }, + }, + Filters: []gatewayv1b1.HTTPRouteFilter{ + { + Type: gatewayv1b1.HTTPRouteFilterURLRewrite, + URLRewrite: &gatewayv1b1.HTTPURLRewriteFilter{ + Path: &gatewayv1b1.HTTPPathModifier{ + Type: gatewayv1b1.PrefixMatchHTTPPathModifier, + ReplacePrefixMatch: ptrTo("foo"), + }, + }, + }, + { + Type: gatewayv1b1.HTTPRouteFilterURLRewrite, + URLRewrite: &gatewayv1b1.HTTPURLRewriteFilter{ + Path: &gatewayv1b1.HTTPPathModifier{ + Type: gatewayv1b1.PrefixMatchHTTPPathModifier, + ReplacePrefixMatch: ptrTo("bar"), + }, + }, + }, + }, + }, + }, + }, + { + name: "invalid because repeated RequestHeaderModifier filter among mix of filters", + wantErrors: []string{"RequestHeaderModifier filter cannot be repeated"}, + rules: []gatewayv1b1.HTTPRouteRule{ + { + Matches: []gatewayv1b1.HTTPRouteMatch{ + { + Path: &gatewayv1b1.HTTPPathMatch{ + Type: ptrTo(gatewayv1b1.PathMatchType("PathPrefix")), + Value: ptrTo("/"), + }, + }, + }, + Filters: []gatewayv1b1.HTTPRouteFilter{ + { + Type: gatewayv1b1.HTTPRouteFilterRequestHeaderModifier, + RequestHeaderModifier: &gatewayv1b1.HTTPHeaderFilter{ + Set: []gatewayv1b1.HTTPHeader{ + { + Name: "special-header", + Value: "foo", + }, + }, + }, + }, + { + Type: gatewayv1b1.HTTPRouteFilterRequestMirror, + RequestMirror: &gatewayv1b1.HTTPRequestMirrorFilter{ + BackendRef: gatewayv1b1.BackendObjectReference{ + Name: testService, + Port: ptrTo(gatewayv1b1.PortNumber(8080)), + }, + }, + }, + { + Type: gatewayv1b1.HTTPRouteFilterRequestHeaderModifier, + RequestHeaderModifier: &gatewayv1b1.HTTPHeaderFilter{ + Add: []gatewayv1b1.HTTPHeader{ + { + Name: "my-header", + Value: "bar", + }, + }, + }, + }, + }, + }, + }, + }, + { + name: "invalid because multipler filters are repeated", + wantErrors: []string{"ResponseHeaderModifier filter cannot be repeated", "RequestRedirect filter cannot be repeated"}, + rules: []gatewayv1b1.HTTPRouteRule{ + { + Matches: []gatewayv1b1.HTTPRouteMatch{ + { + Path: &gatewayv1b1.HTTPPathMatch{ + Type: ptrTo(gatewayv1b1.PathMatchType("PathPrefix")), + Value: ptrTo("/"), + }, + }, + }, + Filters: []gatewayv1b1.HTTPRouteFilter{ + { + Type: gatewayv1b1.HTTPRouteFilterResponseHeaderModifier, + ResponseHeaderModifier: &gatewayv1b1.HTTPHeaderFilter{ + Set: []gatewayv1b1.HTTPHeader{ + { + Name: "special-header", + Value: "foo", + }, + }, + }, + }, + { + Type: gatewayv1b1.HTTPRouteFilterResponseHeaderModifier, + ResponseHeaderModifier: &gatewayv1b1.HTTPHeaderFilter{ + Add: []gatewayv1b1.HTTPHeader{ + { + Name: "my-header", + Value: "bar", + }, + }, + }, + }, + { + Type: gatewayv1b1.HTTPRouteFilterRequestRedirect, + RequestRedirect: &gatewayv1b1.HTTPRequestRedirectFilter{ + Path: &gatewayv1b1.HTTPPathModifier{ + Type: gatewayv1b1.FullPathHTTPPathModifier, + ReplaceFullPath: ptrTo("foo"), + }, + }, + }, + { + Type: gatewayv1b1.HTTPRouteFilterRequestRedirect, + RequestRedirect: &gatewayv1b1.HTTPRequestRedirectFilter{ + Path: &gatewayv1b1.HTTPPathModifier{ + Type: gatewayv1b1.FullPathHTTPPathModifier, + ReplaceFullPath: ptrTo("bar"), + }, + }, + }, + }, + }, + }, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + route := &gatewayv1b1.HTTPRoute{ + ObjectMeta: metav1.ObjectMeta{ + Name: fmt.Sprintf("foo-%v", time.Now().UnixNano()), + Namespace: metav1.NamespaceDefault, + }, + Spec: gatewayv1b1.HTTPRouteSpec{Rules: tc.rules}, + } + validateHTTPRoute(t, route, tc.wantErrors) + }) + } +} + +func TestHTTPBackendRef(t *testing.T) { + testService := gatewayv1b1.ObjectName("test-service") + tests := []struct { + name string + wantErrors []string + rules []gatewayv1b1.HTTPRouteRule + }{ + { + name: "invalid because repeated URLRewrite filter within backendRefs", + wantErrors: []string{"URLRewrite filter cannot be repeated"}, + rules: []gatewayv1b1.HTTPRouteRule{ + { + Matches: []gatewayv1b1.HTTPRouteMatch{ + { + Path: &gatewayv1b1.HTTPPathMatch{ + Type: ptrTo(gatewayv1b1.PathMatchType("PathPrefix")), + Value: ptrTo("/"), + }, + }, + }, + BackendRefs: []gatewayv1b1.HTTPBackendRef{ + { + BackendRef: gatewayv1b1.BackendRef{ + BackendObjectReference: gatewayv1b1.BackendObjectReference{ + Name: testService, + Port: ptrTo(gatewayv1b1.PortNumber(80)), + }, + }, + Filters: []gatewayv1b1.HTTPRouteFilter{ + { + Type: gatewayv1b1.HTTPRouteFilterURLRewrite, + URLRewrite: &gatewayv1b1.HTTPURLRewriteFilter{ + Path: &gatewayv1b1.HTTPPathModifier{ + Type: gatewayv1b1.PrefixMatchHTTPPathModifier, + ReplacePrefixMatch: ptrTo("foo"), + }, + }, + }, + { + Type: gatewayv1b1.HTTPRouteFilterURLRewrite, + URLRewrite: &gatewayv1b1.HTTPURLRewriteFilter{ + Path: &gatewayv1b1.HTTPPathModifier{ + Type: gatewayv1b1.PrefixMatchHTTPPathModifier, + ReplacePrefixMatch: ptrTo("bar"), + }, + }, + }, + }, + }, + }, + }, + }, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + route := &gatewayv1b1.HTTPRoute{ + ObjectMeta: metav1.ObjectMeta{ + Name: fmt.Sprintf("foo-%v", time.Now().UnixNano()), + Namespace: metav1.NamespaceDefault, + }, + Spec: gatewayv1b1.HTTPRouteSpec{Rules: tc.rules}, + } + validateHTTPRoute(t, route, tc.wantErrors) + }) + } +} + +func TestHTTPPathModifier(t *testing.T) { + tests := []struct { + name string + wantErrors []string + pathModifier gatewayv1b1.HTTPPathModifier + }{ + { + name: "valid ReplaceFullPath", + pathModifier: gatewayv1b1.HTTPPathModifier{ + Type: gatewayv1b1.FullPathHTTPPathModifier, + ReplaceFullPath: ptrTo("foo"), + }, + }, + { + name: "replaceFullPath must be set when type is set to 'ReplaceFullPath'", + wantErrors: []string{"replaceFullPath must be set when type is set to 'ReplaceFullPath'"}, + pathModifier: gatewayv1b1.HTTPPathModifier{ + Type: gatewayv1b1.FullPathHTTPPathModifier, + }, + }, + { + name: "type must be 'ReplaceFullPath' when replaceFullPath is set", + wantErrors: []string{"type must be 'ReplaceFullPath' when replaceFullPath is set"}, + pathModifier: gatewayv1b1.HTTPPathModifier{ + ReplaceFullPath: ptrTo("foo"), + }, + }, + { + name: "valie ReplacePrefixMatch", + pathModifier: gatewayv1b1.HTTPPathModifier{ + Type: gatewayv1b1.PrefixMatchHTTPPathModifier, + ReplacePrefixMatch: ptrTo("/foo"), + }, + }, + { + name: "replacePrefixMatch must be set when type is set to 'ReplacePrefixMatch'", + wantErrors: []string{"replacePrefixMatch must be set when type is set to 'ReplacePrefixMatch'"}, + pathModifier: gatewayv1b1.HTTPPathModifier{ + Type: gatewayv1b1.PrefixMatchHTTPPathModifier, + }, + }, + { + name: "type must be 'ReplacePrefixMatch' when replacePrefixMatch is set", + wantErrors: []string{"type must be 'ReplacePrefixMatch' when replacePrefixMatch is set"}, + pathModifier: gatewayv1b1.HTTPPathModifier{ + ReplacePrefixMatch: ptrTo("/foo"), + }, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + route := &gatewayv1b1.HTTPRoute{ + ObjectMeta: metav1.ObjectMeta{ + Name: fmt.Sprintf("foo-%v", time.Now().UnixNano()), + Namespace: metav1.NamespaceDefault, + }, + Spec: gatewayv1b1.HTTPRouteSpec{ + Rules: []gatewayv1b1.HTTPRouteRule{ + { + Filters: []gatewayv1b1.HTTPRouteFilter{ + { + Type: gatewayv1b1.HTTPRouteFilterRequestRedirect, + RequestRedirect: &gatewayv1b1.HTTPRequestRedirectFilter{ + Path: &tc.pathModifier, + }, + }, + }, + }, + }, + }, + } + validateHTTPRoute(t, route, tc.wantErrors) + }) + } +} + +func validateHTTPRoute(t *testing.T, route *gatewayv1b1.HTTPRoute, wantErrors []string) { + t.Helper() + + ctx := context.Background() + err := k8sClient.Create(ctx, route) + + if (len(wantErrors) != 0) != (err != nil) { + t.Fatalf("Unexpected response while creating HTTPRoute %q; got err=\n%v\n;want error=%v", fmt.Sprintf("%v/%v", route.Namespace, route.Name), err, wantErrors != nil) + } + + var missingErrorStrings []string + for _, wantError := range wantErrors { + if !strings.Contains(strings.ToLower(err.Error()), strings.ToLower(wantError)) { + missingErrorStrings = append(missingErrorStrings, wantError) + } + } + if len(missingErrorStrings) != 0 { + t.Errorf("Unexpected response while creating HTTPRoute %q; got err=\n%v\n;missing strings within error=%q", fmt.Sprintf("%v/%v", route.Namespace, route.Name), err, missingErrorStrings) + } +}