Skip to content

Commit

Permalink
Filling in gaps in GRPCRoute validation, sharing alpha and beta
Browse files Browse the repository at this point in the history
validation where possible
  • Loading branch information
robscott committed Dec 14, 2022
1 parent 027075b commit a341f9c
Show file tree
Hide file tree
Showing 6 changed files with 340 additions and 313 deletions.
59 changes: 2 additions & 57 deletions apis/v1alpha2/validation/gateway.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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"))
}
182 changes: 179 additions & 3 deletions apis/v1alpha2/validation/grpcroute.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
}
Expand All @@ -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
Expand Down
Loading

0 comments on commit a341f9c

Please sign in to comment.