Skip to content

Commit 2b71c68

Browse files
committed
Filling in gaps in GRPCRoute validation, sharing alpha and beta
validation where possible
1 parent 027075b commit 2b71c68

File tree

10 files changed

+400
-320
lines changed

10 files changed

+400
-320
lines changed

apis/v1alpha2/grpcroute_types.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -322,7 +322,7 @@ type GRPCMethodMatch struct {
322322
// At least one of Service and Method MUST be a non-empty string.
323323
// +optional
324324
// +kubebuilder:validation:MaxLength=1024
325-
// +kubebuilder:validation:Pattern=`^[^\/]*$`
325+
// +kubebuilder:validation:Pattern=`^(?i)[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$`
326326
Service *string `json:"service,omitempty"`
327327

328328
// Value of the method to match against. If left empty or omitted, will
@@ -331,7 +331,7 @@ type GRPCMethodMatch struct {
331331
// At least one of Service and Method MUST be a non-empty string.
332332
// +optional
333333
// +kubebuilder:validation:MaxLength=1024
334-
// +kubebuilder:validation:Pattern=`^[^\/]*$`
334+
// +kubebuilder:validation:Pattern=`^[A-Za-z_][A-Za-z_0-9]*$`
335335
Method *string `json:"method,omitempty"`
336336
}
337337

apis/v1alpha2/validation/gateway.go

Lines changed: 2 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -17,30 +17,10 @@ limitations under the License.
1717
package validation
1818

1919
import (
20-
"fmt"
21-
2220
"k8s.io/apimachinery/pkg/util/validation/field"
2321

2422
gatewayv1a2 "sigs.k8s.io/gateway-api/apis/v1alpha2"
25-
gatewayv1b1 "sigs.k8s.io/gateway-api/apis/v1beta1"
26-
gatewayvalidationv1b1 "sigs.k8s.io/gateway-api/apis/v1beta1/validation"
27-
)
28-
29-
var (
30-
// set of protocols for which we need to validate that hostname is empty
31-
protocolsHostnameInvalid = map[gatewayv1a2.ProtocolType]struct{}{
32-
gatewayv1b1.TCPProtocolType: {},
33-
gatewayv1b1.UDPProtocolType: {},
34-
}
35-
36-
// ValidateTLSCertificateRefs validates the certificateRefs
37-
// must be set and not empty when tls config is set and
38-
// TLSModeType is terminate
39-
validateTLSCertificateRefs = gatewayvalidationv1b1.ValidateTLSCertificateRefs
40-
41-
// validateListenerTLSConfig validates TLS config must be set when protocol is HTTPS or TLS,
42-
// and TLS config shall not be present when protocol is HTTP, TCP or UDP
43-
validateListenerTLSConfig = gatewayvalidationv1b1.ValidateListenerTLSConfig
23+
gatewayv1b1validation "sigs.k8s.io/gateway-api/apis/v1beta1/validation"
4424
)
4525

4626
// ValidateGateway validates gw according to the Gateway API specification.
@@ -51,40 +31,5 @@ var (
5131
// Validation that is not possible with CRD annotations may be added here in the future.
5232
// See https://github.com/kubernetes-sigs/gateway-api/issues/868 for more information.
5333
func ValidateGateway(gw *gatewayv1a2.Gateway) field.ErrorList {
54-
return validateGatewaySpec(&gw.Spec, field.NewPath("spec"))
55-
}
56-
57-
// validateGatewaySpec validates whether required fields of spec are set according to the
58-
// Gateway API specification.
59-
func validateGatewaySpec(spec *gatewayv1a2.GatewaySpec, path *field.Path) field.ErrorList {
60-
var errs field.ErrorList
61-
errs = append(errs, validateGatewayListeners(spec.Listeners, path.Child("listeners"))...)
62-
return errs
63-
}
64-
65-
// validateGatewayListeners validates whether required fields of listeners are set according
66-
// to the Gateway API specification.
67-
func validateGatewayListeners(listeners []gatewayv1a2.Listener, path *field.Path) field.ErrorList {
68-
var errs field.ErrorList
69-
errs = append(errs, validateListenerTLSConfig(listeners, path)...)
70-
errs = append(errs, validateListenerHostname(listeners, path)...)
71-
errs = append(errs, validateTLSCertificateRefs(listeners, path)...)
72-
return errs
73-
}
74-
75-
func isProtocolInSubset(protocol gatewayv1a2.ProtocolType, set map[gatewayv1a2.ProtocolType]struct{}) bool {
76-
_, ok := set[protocol]
77-
return ok
78-
}
79-
80-
// validateListenerHostname validates each listener hostname
81-
// should be empty in case protocol is TCP or UDP
82-
func validateListenerHostname(listeners []gatewayv1a2.Listener, path *field.Path) field.ErrorList {
83-
var errs field.ErrorList
84-
for i, h := range listeners {
85-
if isProtocolInSubset(h.Protocol, protocolsHostnameInvalid) && h.Hostname != nil {
86-
errs = append(errs, field.Forbidden(path.Index(i).Child("hostname"), fmt.Sprintf("should be empty for protocol %v", h.Protocol)))
87-
}
88-
}
89-
return errs
34+
return gatewayv1b1validation.ValidateGatewaySpec(&gw.Spec, field.NewPath("spec"))
9035
}

apis/v1alpha2/validation/grpcroute.go

Lines changed: 138 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,22 @@ limitations under the License.
1717
package validation
1818

1919
import (
20+
"net/http"
21+
"strings"
22+
2023
"k8s.io/apimachinery/pkg/util/validation/field"
2124

2225
gatewayv1a2 "sigs.k8s.io/gateway-api/apis/v1alpha2"
2326
)
2427

28+
var (
29+
// repeatableGRPCRouteFilters are filter types that are allowed to be
30+
// repeated multiple times in a rule.
31+
repeatableGRPCRouteFilters = []gatewayv1a2.GRPCRouteFilterType{
32+
gatewayv1a2.GRPCRouteFilterExtensionRef,
33+
}
34+
)
35+
2536
// ValidateGRPCRoute validates GRPCRoute according to the Gateway API specification.
2637
// For additional details of the GRPCRoute spec, refer to:
2738
// https://gateway-api.sigs.k8s.io/v1alpha2/references/spec/#gateway.networking.k8s.io/v1alpha2.GRPCRoute
@@ -44,6 +55,10 @@ func validateGRPCRouteRules(rules []gatewayv1a2.GRPCRouteRule, path *field.Path)
4455
var errs field.ErrorList
4556
for i, rule := range rules {
4657
errs = append(errs, validateRuleMatches(rule.Matches, path.Index(i).Child("matches"))...)
58+
errs = append(errs, validateGRPCRouteFilters(rule.Filters, path.Index(i).Child(("filters")))...)
59+
for j, backendRef := range rule.BackendRefs {
60+
errs = append(errs, validateGRPCRouteFilters(backendRef.Filters, path.Child("rules").Index(i).Child("backendRefs").Index(j))...)
61+
}
4762
}
4863
return errs
4964
}
@@ -54,8 +69,129 @@ func validateRuleMatches(matches []gatewayv1a2.GRPCRouteMatch, path *field.Path)
5469
var errs field.ErrorList
5570
for i, m := range matches {
5671
if m.Method != nil && m.Method.Service == nil && m.Method.Method == nil {
57-
errs = append(errs, field.Required(path.Index(i).Child("methods"), "one or both of `service` or `method` must be specified"))
58-
return errs
72+
errs = append(errs, field.Required(path.Index(i).Child("method"), "one or both of `service` or `method` must be specified"))
73+
}
74+
if m.Headers != nil {
75+
errs = append(errs, validateGRPCHeaderMatches(m.Headers, path.Index(i).Child("headers"))...)
76+
}
77+
}
78+
return errs
79+
}
80+
81+
// validateGRPCHeaderMatches validates that no header name is matched more than
82+
// once (case-insensitive), and that at least one of service or method was
83+
// provided.
84+
func validateGRPCHeaderMatches(matches []gatewayv1a2.GRPCHeaderMatch, path *field.Path) field.ErrorList {
85+
var errs field.ErrorList
86+
counts := map[string]int{}
87+
88+
for _, match := range matches {
89+
// Header names are case-insensitive.
90+
counts[strings.ToLower(string(match.Name))]++
91+
}
92+
93+
for name, count := range counts {
94+
if count > 1 {
95+
errs = append(errs, field.Invalid(path, http.CanonicalHeaderKey(name), "cannot match the same header multiple times in the same rule"))
96+
}
97+
}
98+
99+
return errs
100+
}
101+
102+
// validateGRPCRouteFilterType validates that only the expected fields are
103+
// set for the specified filter type.
104+
func validateGRPCRouteFilterType(filter gatewayv1a2.GRPCRouteFilter, path *field.Path) field.ErrorList {
105+
var errs field.ErrorList
106+
if filter.ExtensionRef != nil && filter.Type != gatewayv1a2.GRPCRouteFilterExtensionRef {
107+
errs = append(errs, field.Invalid(path, filter.ExtensionRef, "must be nil if the GRPCRouteFilter.Type is not ExtensionRef"))
108+
}
109+
if filter.ExtensionRef == nil && filter.Type == gatewayv1a2.GRPCRouteFilterExtensionRef {
110+
errs = append(errs, field.Required(path, "filter.ExtensionRef must be specified for ExtensionRef GRPCRouteFilter.Type"))
111+
}
112+
if filter.RequestHeaderModifier != nil && filter.Type != gatewayv1a2.GRPCRouteFilterRequestHeaderModifier {
113+
errs = append(errs, field.Invalid(path, filter.RequestHeaderModifier, "must be nil if the GRPCRouteFilter.Type is not RequestHeaderModifier"))
114+
}
115+
if filter.RequestHeaderModifier == nil && filter.Type == gatewayv1a2.GRPCRouteFilterRequestHeaderModifier {
116+
errs = append(errs, field.Required(path, "filter.RequestHeaderModifier must be specified for RequestHeaderModifier GRPCRouteFilter.Type"))
117+
}
118+
if filter.ResponseHeaderModifier != nil && filter.Type != gatewayv1a2.GRPCRouteFilterResponseHeaderModifier {
119+
errs = append(errs, field.Invalid(path, filter.ResponseHeaderModifier, "must be nil if the GRPCRouteFilter.Type is not ResponseHeaderModifier"))
120+
}
121+
if filter.ResponseHeaderModifier == nil && filter.Type == gatewayv1a2.GRPCRouteFilterResponseHeaderModifier {
122+
errs = append(errs, field.Required(path, "filter.ResponseHeaderModifier must be specified for ResponseHeaderModifier GRPCRouteFilter.Type"))
123+
}
124+
if filter.RequestMirror != nil && filter.Type != gatewayv1a2.GRPCRouteFilterRequestMirror {
125+
errs = append(errs, field.Invalid(path, filter.RequestMirror, "must be nil if the GRPCRouteFilter.Type is not RequestMirror"))
126+
}
127+
if filter.RequestMirror == nil && filter.Type == gatewayv1a2.GRPCRouteFilterRequestMirror {
128+
errs = append(errs, field.Required(path, "filter.RequestMirror must be specified for RequestMirror GRPCRouteFilter.Type"))
129+
}
130+
return errs
131+
}
132+
133+
// validateGRPCRouteFilters validates that a list of core and extended filters
134+
// is used at most once and that the filter type matches its value
135+
func validateGRPCRouteFilters(filters []gatewayv1a2.GRPCRouteFilter, path *field.Path) field.ErrorList {
136+
var errs field.ErrorList
137+
counts := map[gatewayv1a2.GRPCRouteFilterType]int{}
138+
139+
for i, filter := range filters {
140+
counts[filter.Type]++
141+
if filter.RequestHeaderModifier != nil {
142+
errs = append(errs, validateGRPCHeaderModifier(*filter.RequestHeaderModifier, path.Index(i).Child("requestHeaderModifier"))...)
143+
}
144+
if filter.ResponseHeaderModifier != nil {
145+
errs = append(errs, validateGRPCHeaderModifier(*filter.ResponseHeaderModifier, path.Index(i).Child("responseHeaderModifier"))...)
146+
}
147+
errs = append(errs, validateGRPCRouteFilterType(filter, path.Index(i))...)
148+
}
149+
// custom filters don't have any validation
150+
for _, key := range repeatableGRPCRouteFilters {
151+
delete(counts, key)
152+
}
153+
154+
for filterType, count := range counts {
155+
if count > 1 {
156+
errs = append(errs, field.Invalid(path, filterType, "cannot be used multiple times in the same rule"))
157+
}
158+
}
159+
return errs
160+
}
161+
162+
// validateGRPCHeaderModifier ensures that multiple actions cannot be set for
163+
// the same header.
164+
func validateGRPCHeaderModifier(filter gatewayv1a2.HTTPHeaderFilter, path *field.Path) field.ErrorList {
165+
var errs field.ErrorList
166+
singleAction := make(map[string]bool)
167+
for i, action := range filter.Add {
168+
if needsErr, ok := singleAction[strings.ToLower(string(action.Name))]; ok {
169+
if needsErr {
170+
errs = append(errs, field.Invalid(path.Child("add"), filter.Add[i], "cannot specify multiple actions for header"))
171+
}
172+
singleAction[strings.ToLower(string(action.Name))] = false
173+
} else {
174+
singleAction[strings.ToLower(string(action.Name))] = true
175+
}
176+
}
177+
for i, action := range filter.Set {
178+
if needsErr, ok := singleAction[strings.ToLower(string(action.Name))]; ok {
179+
if needsErr {
180+
errs = append(errs, field.Invalid(path.Child("set"), filter.Set[i], "cannot specify multiple actions for header"))
181+
}
182+
singleAction[strings.ToLower(string(action.Name))] = false
183+
} else {
184+
singleAction[strings.ToLower(string(action.Name))] = true
185+
}
186+
}
187+
for i, action := range filter.Remove {
188+
if needsErr, ok := singleAction[strings.ToLower(action)]; ok {
189+
if needsErr {
190+
errs = append(errs, field.Invalid(path.Child("remove"), filter.Remove[i], "cannot specify multiple actions for header"))
191+
}
192+
singleAction[strings.ToLower(action)] = false
193+
} else {
194+
singleAction[strings.ToLower(action)] = true
59195
}
60196
}
61197
return errs

0 commit comments

Comments
 (0)