diff --git a/apis/v1alpha2/validation/gateway.go b/apis/v1alpha2/validation/gateway.go index 5c6c27a4a9..2341171e72 100644 --- a/apis/v1alpha2/validation/gateway.go +++ b/apis/v1alpha2/validation/gateway.go @@ -17,30 +17,10 @@ limitations under the License. package validation import ( - "fmt" - "k8s.io/apimachinery/pkg/util/validation/field" gatewayv1a2 "sigs.k8s.io/gateway-api/apis/v1alpha2" - gatewayv1b1 "sigs.k8s.io/gateway-api/apis/v1beta1" - gatewayvalidationv1b1 "sigs.k8s.io/gateway-api/apis/v1beta1/validation" -) - -var ( - // set of protocols for which we need to validate that hostname is empty - protocolsHostnameInvalid = map[gatewayv1a2.ProtocolType]struct{}{ - gatewayv1b1.TCPProtocolType: {}, - gatewayv1b1.UDPProtocolType: {}, - } - - // ValidateTLSCertificateRefs validates the certificateRefs - // must be set and not empty when tls config is set and - // TLSModeType is terminate - validateTLSCertificateRefs = gatewayvalidationv1b1.ValidateTLSCertificateRefs - - // validateListenerTLSConfig validates TLS config must be set when protocol is HTTPS or TLS, - // and TLS config shall not be present when protocol is HTTP, TCP or UDP - validateListenerTLSConfig = gatewayvalidationv1b1.ValidateListenerTLSConfig + gatewayv1b1validation "sigs.k8s.io/gateway-api/apis/v1beta1/validation" ) // ValidateGateway validates gw according to the Gateway API specification. @@ -51,40 +31,5 @@ var ( // Validation that is not possible with CRD annotations may be added here in the future. // See https://github.com/kubernetes-sigs/gateway-api/issues/868 for more information. func ValidateGateway(gw *gatewayv1a2.Gateway) field.ErrorList { - return validateGatewaySpec(&gw.Spec, field.NewPath("spec")) -} - -// validateGatewaySpec validates whether required fields of spec are set according to the -// Gateway API specification. -func validateGatewaySpec(spec *gatewayv1a2.GatewaySpec, path *field.Path) field.ErrorList { - var errs field.ErrorList - errs = append(errs, validateGatewayListeners(spec.Listeners, path.Child("listeners"))...) - return errs -} - -// validateGatewayListeners validates whether required fields of listeners are set according -// to the Gateway API specification. -func validateGatewayListeners(listeners []gatewayv1a2.Listener, path *field.Path) field.ErrorList { - var errs field.ErrorList - errs = append(errs, validateListenerTLSConfig(listeners, path)...) - errs = append(errs, validateListenerHostname(listeners, path)...) - errs = append(errs, validateTLSCertificateRefs(listeners, path)...) - return errs -} - -func isProtocolInSubset(protocol gatewayv1a2.ProtocolType, set map[gatewayv1a2.ProtocolType]struct{}) bool { - _, ok := set[protocol] - return ok -} - -// validateListenerHostname validates each listener hostname -// should be empty in case protocol is TCP or UDP -func validateListenerHostname(listeners []gatewayv1a2.Listener, path *field.Path) field.ErrorList { - var errs field.ErrorList - for i, h := range listeners { - if isProtocolInSubset(h.Protocol, protocolsHostnameInvalid) && h.Hostname != nil { - errs = append(errs, field.Forbidden(path.Index(i).Child("hostname"), fmt.Sprintf("should be empty for protocol %v", h.Protocol))) - } - } - return errs + return gatewayv1b1validation.ValidateGatewaySpec(&gw.Spec, field.NewPath("spec")) } diff --git a/apis/v1alpha2/validation/grpcroute.go b/apis/v1alpha2/validation/grpcroute.go index 9c32ea8114..9a78d64a7a 100644 --- a/apis/v1alpha2/validation/grpcroute.go +++ b/apis/v1alpha2/validation/grpcroute.go @@ -17,11 +17,26 @@ limitations under the License. package validation import ( + "fmt" + "net/http" + "strings" + "k8s.io/apimachinery/pkg/util/validation/field" gatewayv1a2 "sigs.k8s.io/gateway-api/apis/v1alpha2" ) +var ( + // repeatableGRPCRouteFilters are filter types that can are allowed to be + // repeated multiple times in a rule. + repeatableGRPCRouteFilters = []gatewayv1a2.GRPCRouteFilterType{ + gatewayv1a2.GRPCRouteFilterExtensionRef, + } + + invalidPathSequences = []string{"//", "/./", "/../", "%2f", "%2F", "#"} + invalidPathSuffixes = []string{"/..", "/."} +) + // ValidateGRPCRoute validates GRPCRoute according to the Gateway API specification. // For additional details of the GRPCRoute spec, refer to: // https://gateway-api.sigs.k8s.io/v1alpha2/references/spec/#gateway.networking.k8s.io/v1alpha2.GRPCRoute @@ -44,6 +59,10 @@ func validateGRPCRouteRules(rules []gatewayv1a2.GRPCRouteRule, path *field.Path) var errs field.ErrorList for i, rule := range rules { errs = append(errs, validateRuleMatches(rule.Matches, path.Index(i).Child("matches"))...) + errs = append(errs, validateGRPCRouteFilters(rule.Filters, path.Index(i).Child(("filters")))...) + for j, backendRef := range rule.BackendRefs { + errs = append(errs, validateGRPCRouteFilters(backendRef.Filters, path.Child("rules").Index(i).Child("backendRefs").Index(j))...) + } } return errs } @@ -53,9 +72,166 @@ func validateGRPCRouteRules(rules []gatewayv1a2.GRPCRouteRule, path *field.Path) func validateRuleMatches(matches []gatewayv1a2.GRPCRouteMatch, path *field.Path) field.ErrorList { var errs field.ErrorList for i, m := range matches { - if m.Method != nil && m.Method.Service == nil && m.Method.Method == nil { - errs = append(errs, field.Required(path.Index(i).Child("methods"), "one or both of `service` or `method` must be specified")) - return errs + if m.Method != nil { + errs = append(errs, validateGRPCMethod(*m.Method, path.Index(i).Child("method"))...) + } + if m.Headers != nil { + errs = append(errs, validateGRPCHeaderMatches(m.Headers, path.Index(i).Child("headers"))...) + } + } + return errs +} + +// validateGRPCMethod validates that no header name +// is matched more than once (case-insensitive). +func validateGRPCMethod(m gatewayv1a2.GRPCMethodMatch, path *field.Path) field.ErrorList { + var errs field.ErrorList + if m.Service == nil && m.Method == nil { + return append(errs, field.Required(path, "one or both of `service` or `method` must be specified")) + } + + if m.Service != nil { + for _, invalidSeq := range invalidPathSequences { + if strings.Contains(*m.Service, invalidSeq) { + errs = append(errs, field.Invalid(path.Child("service"), *m.Service, fmt.Sprintf("must not contain %q", invalidSeq))) + } + } + + for _, invalidSuff := range invalidPathSuffixes { + if strings.HasSuffix(*m.Service, invalidSuff) { + errs = append(errs, field.Invalid(path.Child("service"), *m.Service, fmt.Sprintf("cannot end with '%s'", invalidSuff))) + } + } + } + + if m.Method != nil { + for _, invalidSeq := range invalidPathSequences { + if strings.Contains(*m.Method, invalidSeq) { + errs = append(errs, field.Invalid(path.Child("method"), *m.Method, fmt.Sprintf("must not contain %q", invalidSeq))) + } + } + + for _, invalidSuff := range invalidPathSuffixes { + if strings.HasSuffix(*m.Method, invalidSuff) { + errs = append(errs, field.Invalid(path.Child("method"), *m.Method, fmt.Sprintf("cannot end with '%s'", invalidSuff))) + } + } + } + + return errs +} + +// validateGRPCHeaderMatches validates that no header name +// is matched more than once (case-insensitive). +func validateGRPCHeaderMatches(matches []gatewayv1a2.GRPCHeaderMatch, path *field.Path) field.ErrorList { + var errs field.ErrorList + counts := map[string]int{} + + for _, match := range matches { + // Header names are case-insensitive. + counts[strings.ToLower(string(match.Name))]++ + } + + for name, count := range counts { + if count > 1 { + errs = append(errs, field.Invalid(path, http.CanonicalHeaderKey(name), "cannot match the same header multiple times in the same rule")) + } + } + + return errs +} + +// validateGRPCRouteFilterType validates that only the expected fields are +// set for the specified filter type. +func validateGRPCRouteFilterType(filter gatewayv1a2.GRPCRouteFilter, path *field.Path) field.ErrorList { + var errs field.ErrorList + if filter.ExtensionRef != nil && filter.Type != gatewayv1a2.GRPCRouteFilterExtensionRef { + errs = append(errs, field.Invalid(path, filter.ExtensionRef, "must be nil if the GRPCRouteFilter.Type is not ExtensionRef")) + } + if filter.ExtensionRef == nil && filter.Type == gatewayv1a2.GRPCRouteFilterExtensionRef { + errs = append(errs, field.Required(path, "filter.ExtensionRef must be specified for ExtensionRef GRPCRouteFilter.Type")) + } + if filter.RequestHeaderModifier != nil && filter.Type != gatewayv1a2.GRPCRouteFilterRequestHeaderModifier { + errs = append(errs, field.Invalid(path, filter.RequestHeaderModifier, "must be nil if the GRPCRouteFilter.Type is not RequestHeaderModifier")) + } + if filter.RequestHeaderModifier == nil && filter.Type == gatewayv1a2.GRPCRouteFilterRequestHeaderModifier { + errs = append(errs, field.Required(path, "filter.RequestHeaderModifier must be specified for RequestHeaderModifier GRPCRouteFilter.Type")) + } + if filter.ResponseHeaderModifier != nil && filter.Type != gatewayv1a2.GRPCRouteFilterResponseHeaderModifier { + errs = append(errs, field.Invalid(path, filter.ResponseHeaderModifier, "must be nil if the GRPCRouteFilter.Type is not ResponseHeaderModifier")) + } + if filter.ResponseHeaderModifier == nil && filter.Type == gatewayv1a2.GRPCRouteFilterResponseHeaderModifier { + errs = append(errs, field.Required(path, "filter.ResponseHeaderModifier must be specified for ResponseHeaderModifier GRPCRouteFilter.Type")) + } + if filter.RequestMirror != nil && filter.Type != gatewayv1a2.GRPCRouteFilterRequestMirror { + errs = append(errs, field.Invalid(path, filter.RequestMirror, "must be nil if the GRPCRouteFilter.Type is not RequestMirror")) + } + if filter.RequestMirror == nil && filter.Type == gatewayv1a2.GRPCRouteFilterRequestMirror { + errs = append(errs, field.Required(path, "filter.RequestMirror must be specified for RequestMirror GRPCRouteFilter.Type")) + } + return errs +} + +// validateGRPCRouteFilters validates that a list of core and extended filters +// is used at most once and that the filter type matches its value +func validateGRPCRouteFilters(filters []gatewayv1a2.GRPCRouteFilter, path *field.Path) field.ErrorList { + var errs field.ErrorList + counts := map[gatewayv1a2.GRPCRouteFilterType]int{} + + for i, filter := range filters { + counts[filter.Type]++ + if filter.RequestHeaderModifier != nil { + errs = append(errs, validateGRPCHeaderModifier(*filter.RequestHeaderModifier, path.Index(i).Child("requestHeaderModifier"))...) + } + if filter.ResponseHeaderModifier != nil { + errs = append(errs, validateGRPCHeaderModifier(*filter.ResponseHeaderModifier, path.Index(i).Child("responseHeaderModifier"))...) + } + errs = append(errs, validateGRPCRouteFilterType(filter, path.Index(i))...) + } + // custom filters don't have any validation + for _, key := range repeatableGRPCRouteFilters { + delete(counts, key) + } + + for filterType, count := range counts { + if count > 1 { + errs = append(errs, field.Invalid(path.Child("filters"), filterType, "cannot be used multiple times in the same rule")) + } + } + return errs +} + +func validateGRPCHeaderModifier(filter gatewayv1a2.HTTPHeaderFilter, path *field.Path) field.ErrorList { + var errs field.ErrorList + singleAction := make(map[string]bool) + for i, action := range filter.Add { + if needsErr, ok := singleAction[strings.ToLower(string(action.Name))]; ok { + if needsErr { + errs = append(errs, field.Invalid(path.Child("add"), filter.Add[i], "cannot specify multiple actions for header")) + } + singleAction[strings.ToLower(string(action.Name))] = false + } else { + singleAction[strings.ToLower(string(action.Name))] = true + } + } + for i, action := range filter.Set { + if needsErr, ok := singleAction[strings.ToLower(string(action.Name))]; ok { + if needsErr { + errs = append(errs, field.Invalid(path.Child("set"), filter.Set[i], "cannot specify multiple actions for header")) + } + singleAction[strings.ToLower(string(action.Name))] = false + } else { + singleAction[strings.ToLower(string(action.Name))] = true + } + } + for i, action := range filter.Remove { + if needsErr, ok := singleAction[strings.ToLower(action)]; ok { + if needsErr { + errs = append(errs, field.Invalid(path.Child("remove"), filter.Remove[i], "cannot specify multiple actions for header")) + } + singleAction[strings.ToLower(action)] = false + } else { + singleAction[strings.ToLower(action)] = true } } return errs diff --git a/apis/v1alpha2/validation/grpcroute_test.go b/apis/v1alpha2/validation/grpcroute_test.go index cc9005924f..28fdd53c93 100644 --- a/apis/v1alpha2/validation/grpcroute_test.go +++ b/apis/v1alpha2/validation/grpcroute_test.go @@ -19,6 +19,8 @@ package validation import ( "testing" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "k8s.io/apimachinery/pkg/util/validation/field" gatewayv1a2 "sigs.k8s.io/gateway-api/apis/v1alpha2" @@ -80,7 +82,7 @@ func TestValidateGRPCRoute(t *testing.T) { errs: field.ErrorList{ { Type: field.ErrorTypeRequired, - Field: "spec.rules[0].matches[0].methods", + Field: "spec.rules[0].matches[0].method", Detail: "one or both of `service` or `method` must be specified", }, }, @@ -109,3 +111,149 @@ func TestValidateGRPCRoute(t *testing.T) { }) } } + +func TestValidateGRPCBackendUniqueFilters(t *testing.T) { + var testService gatewayv1a2.ObjectName = "testService" + var specialService gatewayv1a2.ObjectName = "specialService" + tests := []struct { + name string + rules []gatewayv1a2.GRPCRouteRule + errCount int + }{{ + name: "valid grpcRoute Rules backendref filters", + errCount: 0, + rules: []gatewayv1a2.GRPCRouteRule{{ + BackendRefs: []gatewayv1a2.GRPCBackendRef{ + { + BackendRef: gatewayv1a2.BackendRef{ + BackendObjectReference: gatewayv1a2.BackendObjectReference{ + Name: testService, + Port: ptrTo(gatewayv1a2.PortNumber(8080)), + }, + Weight: ptrTo(int32(100)), + }, + Filters: []gatewayv1a2.GRPCRouteFilter{ + { + Type: gatewayv1a2.GRPCRouteFilterRequestMirror, + RequestMirror: &gatewayv1a2.HTTPRequestMirrorFilter{ + BackendRef: gatewayv1a2.BackendObjectReference{ + Name: testService, + Port: ptrTo(gatewayv1a2.PortNumber(8080)), + }, + }, + }, + }, + }, + }, + }}, + }, { + name: "invalid grpcRoute Rules duplicate mirror filter", + errCount: 1, + rules: []gatewayv1a2.GRPCRouteRule{{ + BackendRefs: []gatewayv1a2.GRPCBackendRef{ + { + BackendRef: gatewayv1a2.BackendRef{ + BackendObjectReference: gatewayv1a2.BackendObjectReference{ + Name: testService, + Port: ptrTo(gatewayv1a2.PortNumber(8080)), + }, + }, + Filters: []gatewayv1a2.GRPCRouteFilter{ + { + Type: gatewayv1a2.GRPCRouteFilterRequestMirror, + RequestMirror: &gatewayv1a2.HTTPRequestMirrorFilter{ + BackendRef: gatewayv1a2.BackendObjectReference{ + Name: testService, + Port: ptrTo(gatewayv1a2.PortNumber(8080)), + }, + }, + }, + { + Type: gatewayv1a2.GRPCRouteFilterRequestMirror, + RequestMirror: &gatewayv1a2.HTTPRequestMirrorFilter{ + BackendRef: gatewayv1a2.BackendObjectReference{ + Name: specialService, + Port: ptrTo(gatewayv1a2.PortNumber(8080)), + }, + }, + }, + }, + }, + }, + }}, + }} + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + route := gatewayv1a2.GRPCRoute{Spec: gatewayv1a2.GRPCRouteSpec{Rules: tc.rules}} + errs := ValidateGRPCRoute(&route) + if len(errs) != tc.errCount { + t.Errorf("got %d errors, want %d errors: %s", len(errs), tc.errCount, errs) + } + }) + } +} + +func TestValidateGRPCHeaderMatches(t *testing.T) { + tests := []struct { + name string + headerMatches []gatewayv1a2.GRPCHeaderMatch + expectErr string + }{{ + name: "no header matches", + headerMatches: nil, + expectErr: "", + }, { + name: "no header matched more than once", + headerMatches: []gatewayv1a2.GRPCHeaderMatch{ + {Name: "Header-Name-1", Value: "val-1"}, + {Name: "Header-Name-2", Value: "val-2"}, + {Name: "Header-Name-3", Value: "val-3"}, + }, + expectErr: "", + }, { + name: "header matched more than once (same case)", + headerMatches: []gatewayv1a2.GRPCHeaderMatch{ + {Name: "Header-Name-1", Value: "val-1"}, + {Name: "Header-Name-2", Value: "val-2"}, + {Name: "Header-Name-1", Value: "val-3"}, + }, + expectErr: "spec.rules[0].matches[0].headers: Invalid value: \"Header-Name-1\": cannot match the same header multiple times in the same rule", + }, { + name: "header matched more than once (different case)", + headerMatches: []gatewayv1a2.GRPCHeaderMatch{ + {Name: "Header-Name-1", Value: "val-1"}, + {Name: "Header-Name-2", Value: "val-2"}, + {Name: "HEADER-NAME-2", Value: "val-3"}, + }, + expectErr: "spec.rules[0].matches[0].headers: Invalid value: \"Header-Name-2\": cannot match the same header multiple times in the same rule", + }} + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + route := gatewayv1a2.GRPCRoute{Spec: gatewayv1a2.GRPCRouteSpec{ + Rules: []gatewayv1a2.GRPCRouteRule{{ + Matches: []gatewayv1a2.GRPCRouteMatch{{ + Headers: tc.headerMatches, + }}, + BackendRefs: []gatewayv1a2.GRPCBackendRef{{ + BackendRef: gatewayv1a2.BackendRef{ + BackendObjectReference: gatewayv1a2.BackendObjectReference{ + Name: gatewayv1a2.ObjectName("test"), + Port: ptrTo(gatewayv1a2.PortNumber(8080)), + }, + }, + }}, + }}, + }} + + errs := ValidateGRPCRoute(&route) + if len(tc.expectErr) == 0 { + assert.Emptyf(t, errs, "expected no errors, got %d errors: %s", len(errs), errs) + } else { + require.Lenf(t, errs, 1, "expected one error, got %d errors: %s", len(errs), errs) + assert.Equal(t, tc.expectErr, errs[0].Error()) + } + }) + } +} diff --git a/apis/v1alpha2/validation/httproute.go b/apis/v1alpha2/validation/httproute.go index 844aa5f4db..cc2f748b75 100644 --- a/apis/v1alpha2/validation/httproute.go +++ b/apis/v1alpha2/validation/httproute.go @@ -17,257 +17,15 @@ limitations under the License. package validation import ( - "fmt" - "net/http" - "strings" - "k8s.io/apimachinery/pkg/util/validation/field" gatewayv1a2 "sigs.k8s.io/gateway-api/apis/v1alpha2" - gatewayv1b1 "sigs.k8s.io/gateway-api/apis/v1beta1" -) - -var ( - // repeatableHTTPRouteFilters are filter types that can are allowed to be - // repeated multiple times in a rule. - repeatableHTTPRouteFilters = []gatewayv1a2.HTTPRouteFilterType{ - gatewayv1b1.HTTPRouteFilterExtensionRef, - } - - invalidPathSequences = []string{"//", "/./", "/../", "%2f", "%2F", "#"} - invalidPathSuffixes = []string{"/..", "/."} + gatewayv1b1validation "sigs.k8s.io/gateway-api/apis/v1beta1/validation" ) // ValidateHTTPRoute validates HTTPRoute according to the Gateway API specification. // For additional details of the HTTPRoute spec, refer to: -// https://gateway-api.sigs.k8s.io/v1alpha2/references/spec/#gateway.networking.k8s.io/v1alpha2.HTTPRoute +// https://gateway-api.sigs.k8s.io/v1beta1/references/spec/#gateway.networking.k8s.io/v1beta1.HTTPRoute func ValidateHTTPRoute(route *gatewayv1a2.HTTPRoute) field.ErrorList { - return validateHTTPRouteSpec(&route.Spec, field.NewPath("spec")) -} - -// validateHTTPRouteSpec validates that required fields of spec are set according to the -// HTTPRoute specification. -func validateHTTPRouteSpec(spec *gatewayv1a2.HTTPRouteSpec, path *field.Path) field.ErrorList { - var errs field.ErrorList - for i, rule := range spec.Rules { - errs = append(errs, validateHTTPRouteFilters(rule.Filters, rule.Matches, path.Child("rules").Index(i))...) - for j, backendRef := range rule.BackendRefs { - errs = append(errs, validateHTTPRouteFilters(backendRef.Filters, rule.Matches, path.Child("rules").Index(i).Child("backendsrefs").Index(j))...) - } - for j, m := range rule.Matches { - matchPath := path.Child("rules").Index(i).Child("matches").Index(j) - - if m.Path != nil { - errs = append(errs, validateHTTPPathMatch(m.Path, matchPath.Child("path"))...) - } - if len(m.Headers) > 0 { - errs = append(errs, validateHTTPHeaderMatches(m.Headers, matchPath.Child("headers"))...) - } - if len(m.QueryParams) > 0 { - errs = append(errs, validateHTTPQueryParamMatches(m.QueryParams, matchPath.Child("queryParams"))...) - } - } - } - errs = append(errs, validateHTTPRouteBackendServicePorts(spec.Rules, path.Child("rules"))...) - errs = append(errs, validateParentRefs(spec.ParentRefs, path.Child("spec"))...) - return errs -} - -// validateHTTPRouteBackendServicePorts validates that v1.Service backends always have a port. -func validateHTTPRouteBackendServicePorts(rules []gatewayv1a2.HTTPRouteRule, path *field.Path) field.ErrorList { - var errs field.ErrorList - path = path.Child("rules") - - for i, rule := range rules { - for j, ref := range rule.BackendRefs { - errs = append(errs, validateBackendRefServicePort(&ref.BackendRef, path.Index(i).Child("backendRefs").Index(j))...) - } - } - - return errs -} - -// validateHTTPRouteFilters validates that a list of core and extended filters -// is used at most once and that the filter type matches its value -func validateHTTPRouteFilters(filters []gatewayv1a2.HTTPRouteFilter, matches []gatewayv1a2.HTTPRouteMatch, path *field.Path) field.ErrorList { - var errs field.ErrorList - counts := map[gatewayv1a2.HTTPRouteFilterType]int{} - - for i, filter := range filters { - counts[filter.Type]++ - if filter.RequestRedirect != nil && filter.RequestRedirect.Path != nil { - errs = append(errs, validateHTTPPathModifier(*filter.RequestRedirect.Path, matches, path.Index(i).Child("requestRedirect", "path"))...) - } - if filter.URLRewrite != nil && filter.URLRewrite.Path != nil { - errs = append(errs, validateHTTPPathModifier(*filter.URLRewrite.Path, matches, path.Index(i).Child("urlRewrite", "path"))...) - } - errs = append(errs, validateHTTPRouteFilterTypeMatchesValue(filter, path.Index(i))...) - } - // custom filters don't have any validation - for _, key := range repeatableHTTPRouteFilters { - delete(counts, key) - } - - if counts[gatewayv1b1.HTTPRouteFilterRequestRedirect] > 0 && counts[gatewayv1b1.HTTPRouteFilterURLRewrite] > 0 { - errs = append(errs, field.Invalid(path.Child("filters"), gatewayv1b1.HTTPRouteFilterRequestRedirect, "may specify either httpRouteFilterRequestRedirect or httpRouteFilterRequestRewrite, but not both")) - } - - for filterType, count := range counts { - if count > 1 { - errs = append(errs, field.Invalid(path.Child("filters"), filterType, "cannot be used multiple times in the same rule")) - } - } - return errs -} - -// webhook validation of HTTPPathMatch -func validateHTTPPathMatch(path *gatewayv1a2.HTTPPathMatch, fldPath *field.Path) field.ErrorList { - allErrs := field.ErrorList{} - - if path.Type == nil { - return append(allErrs, field.Required(fldPath.Child("type"), "must be specified")) - } - - if path.Value == nil { - return append(allErrs, field.Required(fldPath.Child("value"), "must be specified")) - } - - switch *path.Type { - case gatewayv1b1.PathMatchExact, gatewayv1b1.PathMatchPathPrefix: - if !strings.HasPrefix(*path.Value, "/") { - allErrs = append(allErrs, field.Invalid(fldPath.Child("value"), *path.Value, "must be an absolute path")) - } - if len(*path.Value) > 0 { - for _, invalidSeq := range invalidPathSequences { - if strings.Contains(*path.Value, invalidSeq) { - allErrs = append(allErrs, field.Invalid(fldPath.Child("value"), *path.Value, fmt.Sprintf("must not contain %q", invalidSeq))) - } - } - - for _, invalidSuff := range invalidPathSuffixes { - if strings.HasSuffix(*path.Value, invalidSuff) { - allErrs = append(allErrs, field.Invalid(fldPath.Child("value"), *path.Value, fmt.Sprintf("cannot end with '%s'", invalidSuff))) - } - } - } - case gatewayv1b1.PathMatchRegularExpression: - default: - pathTypes := []string{string(gatewayv1b1.PathMatchExact), string(gatewayv1b1.PathMatchPathPrefix), string(gatewayv1b1.PathMatchRegularExpression)} - allErrs = append(allErrs, field.NotSupported(fldPath.Child("type"), *path.Type, pathTypes)) - } - return allErrs -} - -// validateHTTPHeaderMatches validates that no header name -// is matched more than once (case-insensitive). -func validateHTTPHeaderMatches(matches []gatewayv1a2.HTTPHeaderMatch, path *field.Path) field.ErrorList { - var errs field.ErrorList - counts := map[string]int{} - - for _, match := range matches { - // Header names are case-insensitive. - counts[strings.ToLower(string(match.Name))]++ - } - - for name, count := range counts { - if count > 1 { - errs = append(errs, field.Invalid(path, http.CanonicalHeaderKey(name), "cannot match the same header multiple times in the same rule")) - } - } - - return errs -} - -// validateHTTPQueryParamMatches validates that no query param name -// is matched more than once (case-sensitive). -func validateHTTPQueryParamMatches(matches []gatewayv1a2.HTTPQueryParamMatch, path *field.Path) field.ErrorList { - var errs field.ErrorList - counts := map[string]int{} - - for _, match := range matches { - // Query param names are case-sensitive. - counts[string(match.Name)]++ - } - - for name, count := range counts { - if count > 1 { - errs = append(errs, field.Invalid(path, name, "cannot match the same query parameter multiple times in the same rule")) - } - } - - return errs -} - -// validateHTTPRouteFilterTypeMatchesValue validates that only the expected fields are -// set for the specified filter type. -func validateHTTPRouteFilterTypeMatchesValue(filter gatewayv1a2.HTTPRouteFilter, path *field.Path) field.ErrorList { - var errs field.ErrorList - if filter.ExtensionRef != nil && filter.Type != gatewayv1b1.HTTPRouteFilterExtensionRef { - errs = append(errs, field.Invalid(path, filter.ExtensionRef, "must be nil if the HTTPRouteFilter.Type is not ExtensionRef")) - } - if filter.ExtensionRef == nil && filter.Type == gatewayv1b1.HTTPRouteFilterExtensionRef { - errs = append(errs, field.Required(path, "filter.ExtensionRef must be specified for ExtensionRef HTTPRouteFilter.Type")) - } - if filter.RequestHeaderModifier != nil && filter.Type != gatewayv1b1.HTTPRouteFilterRequestHeaderModifier { - errs = append(errs, field.Invalid(path, filter.RequestHeaderModifier, "must be nil if the HTTPRouteFilter.Type is not RequestHeaderModifier")) - } - if filter.RequestHeaderModifier == nil && filter.Type == gatewayv1b1.HTTPRouteFilterRequestHeaderModifier { - errs = append(errs, field.Required(path, "filter.RequestHeaderModifier must be specified for RequestHeaderModifier HTTPRouteFilter.Type")) - } - if filter.RequestMirror != nil && filter.Type != gatewayv1b1.HTTPRouteFilterRequestMirror { - errs = append(errs, field.Invalid(path, filter.RequestMirror, "must be nil if the HTTPRouteFilter.Type is not RequestMirror")) - } - if filter.RequestMirror == nil && filter.Type == gatewayv1b1.HTTPRouteFilterRequestMirror { - errs = append(errs, field.Required(path, "filter.RequestMirror must be specified for RequestMirror HTTPRouteFilter.Type")) - } - if filter.RequestRedirect != nil && filter.Type != gatewayv1b1.HTTPRouteFilterRequestRedirect { - errs = append(errs, field.Invalid(path, filter.RequestRedirect, "must be nil if the HTTPRouteFilter.Type is not RequestRedirect")) - } - if filter.RequestRedirect == nil && filter.Type == gatewayv1b1.HTTPRouteFilterRequestRedirect { - errs = append(errs, field.Required(path, "filter.RequestRedirect must be specified for RequestRedirect HTTPRouteFilter.Type")) - } - if filter.URLRewrite != nil && filter.Type != gatewayv1b1.HTTPRouteFilterURLRewrite { - errs = append(errs, field.Invalid(path, filter.URLRewrite, "must be nil if the HTTPRouteFilter.Type is not URLRewrite")) - } - if filter.URLRewrite == nil && filter.Type == gatewayv1b1.HTTPRouteFilterURLRewrite { - errs = append(errs, field.Required(path, "filter.URLRewrite must be specified for URLRewrite HTTPRouteFilter.Type")) - } - return errs -} - -// validateHTTPPathModifier validates that only the expected fields are set in a -// path modifier. -func validateHTTPPathModifier(modifier gatewayv1a2.HTTPPathModifier, matches []gatewayv1a2.HTTPRouteMatch, path *field.Path) field.ErrorList { - var errs field.ErrorList - if modifier.ReplaceFullPath != nil && modifier.Type != gatewayv1b1.FullPathHTTPPathModifier { - errs = append(errs, field.Invalid(path, modifier.ReplaceFullPath, "must be nil if the HTTPRouteFilter.Type is not ReplaceFullPath")) - } - if modifier.ReplaceFullPath == nil && modifier.Type == gatewayv1b1.FullPathHTTPPathModifier { - errs = append(errs, field.Invalid(path, modifier.ReplaceFullPath, "must not be nil if the HTTPRouteFilter.Type is ReplaceFullPath")) - } - if modifier.ReplacePrefixMatch != nil && modifier.Type != gatewayv1b1.PrefixMatchHTTPPathModifier { - errs = append(errs, field.Invalid(path, modifier.ReplacePrefixMatch, "must be nil if the HTTPRouteFilter.Type is not ReplacePrefixMatch")) - } - if modifier.ReplacePrefixMatch == nil && modifier.Type == gatewayv1b1.PrefixMatchHTTPPathModifier { - errs = append(errs, field.Invalid(path, modifier.ReplacePrefixMatch, "must not be nil if the HTTPRouteFilter.Type is ReplacePrefixMatch")) - } - - if modifier.Type == gatewayv1b1.PrefixMatchHTTPPathModifier && modifier.ReplacePrefixMatch != nil { - if !hasExactlyOnePrefixMatch(matches) { - errs = append(errs, field.Invalid(path, modifier.ReplacePrefixMatch, "exactly one PathPrefix match must be specified to use this path modifier")) - } - } - return errs -} - -func hasExactlyOnePrefixMatch(matches []gatewayv1a2.HTTPRouteMatch) bool { - if len(matches) != 1 || matches[0].Path == nil { - return false - } - pathMatchType := matches[0].Path.Type - if *pathMatchType != gatewayv1b1.PathMatchPathPrefix { - return false - } - - return true + return gatewayv1b1validation.ValidateHTTPRouteSpec(&route.Spec, field.NewPath("spec")) } diff --git a/apis/v1beta1/validation/gateway.go b/apis/v1beta1/validation/gateway.go index 5fd41974da..60d7f55305 100644 --- a/apis/v1beta1/validation/gateway.go +++ b/apis/v1beta1/validation/gateway.go @@ -51,12 +51,12 @@ var ( // Validation that is not possible with CRD annotations may be added here in the future. // See https://github.com/kubernetes-sigs/gateway-api/issues/868 for more information. func ValidateGateway(gw *gatewayv1b1.Gateway) field.ErrorList { - return validateGatewaySpec(&gw.Spec, field.NewPath("spec")) + return ValidateGatewaySpec(&gw.Spec, field.NewPath("spec")) } -// validateGatewaySpec validates whether required fields of spec are set according to the +// ValidateGatewaySpec validates whether required fields of spec are set according to the // Gateway API specification. -func validateGatewaySpec(spec *gatewayv1b1.GatewaySpec, path *field.Path) field.ErrorList { +func ValidateGatewaySpec(spec *gatewayv1b1.GatewaySpec, path *field.Path) field.ErrorList { var errs field.ErrorList errs = append(errs, validateGatewayListeners(spec.Listeners, path.Child("listeners"))...) return errs diff --git a/apis/v1beta1/validation/httproute.go b/apis/v1beta1/validation/httproute.go index 97a59ac1fd..58cf029c71 100644 --- a/apis/v1beta1/validation/httproute.go +++ b/apis/v1beta1/validation/httproute.go @@ -41,17 +41,17 @@ var ( // For additional details of the HTTPRoute spec, refer to: // https://gateway-api.sigs.k8s.io/v1beta1/references/spec/#gateway.networking.k8s.io/v1beta1.HTTPRoute func ValidateHTTPRoute(route *gatewayv1b1.HTTPRoute) field.ErrorList { - return validateHTTPRouteSpec(&route.Spec, field.NewPath("spec")) + return ValidateHTTPRouteSpec(&route.Spec, field.NewPath("spec")) } -// validateHTTPRouteSpec validates that required fields of spec are set according to the +// ValidateHTTPRouteSpec validates that required fields of spec are set according to the // HTTPRoute specification. -func validateHTTPRouteSpec(spec *gatewayv1b1.HTTPRouteSpec, path *field.Path) field.ErrorList { +func ValidateHTTPRouteSpec(spec *gatewayv1b1.HTTPRouteSpec, path *field.Path) field.ErrorList { var errs field.ErrorList for i, rule := range spec.Rules { errs = append(errs, validateHTTPRouteFilters(rule.Filters, rule.Matches, path.Child("rules").Index(i))...) for j, backendRef := range rule.BackendRefs { - errs = append(errs, validateHTTPRouteFilters(backendRef.Filters, rule.Matches, path.Child("rules").Index(i).Child("backendsrefs").Index(j))...) + errs = append(errs, validateHTTPRouteFilters(backendRef.Filters, rule.Matches, path.Child("rules").Index(i).Child("backendRefs").Index(j))...) } for j, m := range rule.Matches { matchPath := path.Child("rules").Index(i).Child("matches").Index(j)