Skip to content

Commit cd3a100

Browse files
committed
Updating Gateway and HTTPRoute validation to share code across API
versions
1 parent d31691b commit cd3a100

File tree

5 files changed

+45
-313
lines changed

5 files changed

+45
-313
lines changed

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/httproute.go

Lines changed: 3 additions & 245 deletions
Original file line numberDiff line numberDiff line change
@@ -17,257 +17,15 @@ limitations under the License.
1717
package validation
1818

1919
import (
20-
"fmt"
21-
"net/http"
22-
"strings"
23-
2420
"k8s.io/apimachinery/pkg/util/validation/field"
2521

2622
gatewayv1a2 "sigs.k8s.io/gateway-api/apis/v1alpha2"
27-
gatewayv1b1 "sigs.k8s.io/gateway-api/apis/v1beta1"
28-
)
29-
30-
var (
31-
// repeatableHTTPRouteFilters are filter types that can are allowed to be
32-
// repeated multiple times in a rule.
33-
repeatableHTTPRouteFilters = []gatewayv1a2.HTTPRouteFilterType{
34-
gatewayv1b1.HTTPRouteFilterExtensionRef,
35-
}
36-
37-
invalidPathSequences = []string{"//", "/./", "/../", "%2f", "%2F", "#"}
38-
invalidPathSuffixes = []string{"/..", "/."}
23+
gatewayv1b1validation "sigs.k8s.io/gateway-api/apis/v1beta1/validation"
3924
)
4025

4126
// ValidateHTTPRoute validates HTTPRoute according to the Gateway API specification.
4227
// For additional details of the HTTPRoute spec, refer to:
43-
// https://gateway-api.sigs.k8s.io/v1alpha2/references/spec/#gateway.networking.k8s.io/v1alpha2.HTTPRoute
28+
// https://gateway-api.sigs.k8s.io/v1beta1/references/spec/#gateway.networking.k8s.io/v1beta1.HTTPRoute
4429
func ValidateHTTPRoute(route *gatewayv1a2.HTTPRoute) field.ErrorList {
45-
return validateHTTPRouteSpec(&route.Spec, field.NewPath("spec"))
46-
}
47-
48-
// validateHTTPRouteSpec validates that required fields of spec are set according to the
49-
// HTTPRoute specification.
50-
func validateHTTPRouteSpec(spec *gatewayv1a2.HTTPRouteSpec, path *field.Path) field.ErrorList {
51-
var errs field.ErrorList
52-
for i, rule := range spec.Rules {
53-
errs = append(errs, validateHTTPRouteFilters(rule.Filters, rule.Matches, path.Child("rules").Index(i))...)
54-
for j, backendRef := range rule.BackendRefs {
55-
errs = append(errs, validateHTTPRouteFilters(backendRef.Filters, rule.Matches, path.Child("rules").Index(i).Child("backendsrefs").Index(j))...)
56-
}
57-
for j, m := range rule.Matches {
58-
matchPath := path.Child("rules").Index(i).Child("matches").Index(j)
59-
60-
if m.Path != nil {
61-
errs = append(errs, validateHTTPPathMatch(m.Path, matchPath.Child("path"))...)
62-
}
63-
if len(m.Headers) > 0 {
64-
errs = append(errs, validateHTTPHeaderMatches(m.Headers, matchPath.Child("headers"))...)
65-
}
66-
if len(m.QueryParams) > 0 {
67-
errs = append(errs, validateHTTPQueryParamMatches(m.QueryParams, matchPath.Child("queryParams"))...)
68-
}
69-
}
70-
}
71-
errs = append(errs, validateHTTPRouteBackendServicePorts(spec.Rules, path.Child("rules"))...)
72-
errs = append(errs, validateParentRefs(spec.ParentRefs, path.Child("spec"))...)
73-
return errs
74-
}
75-
76-
// validateHTTPRouteBackendServicePorts validates that v1.Service backends always have a port.
77-
func validateHTTPRouteBackendServicePorts(rules []gatewayv1a2.HTTPRouteRule, path *field.Path) field.ErrorList {
78-
var errs field.ErrorList
79-
path = path.Child("rules")
80-
81-
for i, rule := range rules {
82-
for j, ref := range rule.BackendRefs {
83-
errs = append(errs, validateBackendRefServicePort(&ref.BackendRef, path.Index(i).Child("backendRefs").Index(j))...)
84-
}
85-
}
86-
87-
return errs
88-
}
89-
90-
// validateHTTPRouteFilters validates that a list of core and extended filters
91-
// is used at most once and that the filter type matches its value
92-
func validateHTTPRouteFilters(filters []gatewayv1a2.HTTPRouteFilter, matches []gatewayv1a2.HTTPRouteMatch, path *field.Path) field.ErrorList {
93-
var errs field.ErrorList
94-
counts := map[gatewayv1a2.HTTPRouteFilterType]int{}
95-
96-
for i, filter := range filters {
97-
counts[filter.Type]++
98-
if filter.RequestRedirect != nil && filter.RequestRedirect.Path != nil {
99-
errs = append(errs, validateHTTPPathModifier(*filter.RequestRedirect.Path, matches, path.Index(i).Child("requestRedirect", "path"))...)
100-
}
101-
if filter.URLRewrite != nil && filter.URLRewrite.Path != nil {
102-
errs = append(errs, validateHTTPPathModifier(*filter.URLRewrite.Path, matches, path.Index(i).Child("urlRewrite", "path"))...)
103-
}
104-
errs = append(errs, validateHTTPRouteFilterTypeMatchesValue(filter, path.Index(i))...)
105-
}
106-
// custom filters don't have any validation
107-
for _, key := range repeatableHTTPRouteFilters {
108-
delete(counts, key)
109-
}
110-
111-
if counts[gatewayv1b1.HTTPRouteFilterRequestRedirect] > 0 && counts[gatewayv1b1.HTTPRouteFilterURLRewrite] > 0 {
112-
errs = append(errs, field.Invalid(path.Child("filters"), gatewayv1b1.HTTPRouteFilterRequestRedirect, "may specify either httpRouteFilterRequestRedirect or httpRouteFilterRequestRewrite, but not both"))
113-
}
114-
115-
for filterType, count := range counts {
116-
if count > 1 {
117-
errs = append(errs, field.Invalid(path.Child("filters"), filterType, "cannot be used multiple times in the same rule"))
118-
}
119-
}
120-
return errs
121-
}
122-
123-
// webhook validation of HTTPPathMatch
124-
func validateHTTPPathMatch(path *gatewayv1a2.HTTPPathMatch, fldPath *field.Path) field.ErrorList {
125-
allErrs := field.ErrorList{}
126-
127-
if path.Type == nil {
128-
return append(allErrs, field.Required(fldPath.Child("type"), "must be specified"))
129-
}
130-
131-
if path.Value == nil {
132-
return append(allErrs, field.Required(fldPath.Child("value"), "must be specified"))
133-
}
134-
135-
switch *path.Type {
136-
case gatewayv1b1.PathMatchExact, gatewayv1b1.PathMatchPathPrefix:
137-
if !strings.HasPrefix(*path.Value, "/") {
138-
allErrs = append(allErrs, field.Invalid(fldPath.Child("value"), *path.Value, "must be an absolute path"))
139-
}
140-
if len(*path.Value) > 0 {
141-
for _, invalidSeq := range invalidPathSequences {
142-
if strings.Contains(*path.Value, invalidSeq) {
143-
allErrs = append(allErrs, field.Invalid(fldPath.Child("value"), *path.Value, fmt.Sprintf("must not contain %q", invalidSeq)))
144-
}
145-
}
146-
147-
for _, invalidSuff := range invalidPathSuffixes {
148-
if strings.HasSuffix(*path.Value, invalidSuff) {
149-
allErrs = append(allErrs, field.Invalid(fldPath.Child("value"), *path.Value, fmt.Sprintf("cannot end with '%s'", invalidSuff)))
150-
}
151-
}
152-
}
153-
case gatewayv1b1.PathMatchRegularExpression:
154-
default:
155-
pathTypes := []string{string(gatewayv1b1.PathMatchExact), string(gatewayv1b1.PathMatchPathPrefix), string(gatewayv1b1.PathMatchRegularExpression)}
156-
allErrs = append(allErrs, field.NotSupported(fldPath.Child("type"), *path.Type, pathTypes))
157-
}
158-
return allErrs
159-
}
160-
161-
// validateHTTPHeaderMatches validates that no header name
162-
// is matched more than once (case-insensitive).
163-
func validateHTTPHeaderMatches(matches []gatewayv1a2.HTTPHeaderMatch, path *field.Path) field.ErrorList {
164-
var errs field.ErrorList
165-
counts := map[string]int{}
166-
167-
for _, match := range matches {
168-
// Header names are case-insensitive.
169-
counts[strings.ToLower(string(match.Name))]++
170-
}
171-
172-
for name, count := range counts {
173-
if count > 1 {
174-
errs = append(errs, field.Invalid(path, http.CanonicalHeaderKey(name), "cannot match the same header multiple times in the same rule"))
175-
}
176-
}
177-
178-
return errs
179-
}
180-
181-
// validateHTTPQueryParamMatches validates that no query param name
182-
// is matched more than once (case-sensitive).
183-
func validateHTTPQueryParamMatches(matches []gatewayv1a2.HTTPQueryParamMatch, path *field.Path) field.ErrorList {
184-
var errs field.ErrorList
185-
counts := map[string]int{}
186-
187-
for _, match := range matches {
188-
// Query param names are case-sensitive.
189-
counts[string(match.Name)]++
190-
}
191-
192-
for name, count := range counts {
193-
if count > 1 {
194-
errs = append(errs, field.Invalid(path, name, "cannot match the same query parameter multiple times in the same rule"))
195-
}
196-
}
197-
198-
return errs
199-
}
200-
201-
// validateHTTPRouteFilterTypeMatchesValue validates that only the expected fields are
202-
// set for the specified filter type.
203-
func validateHTTPRouteFilterTypeMatchesValue(filter gatewayv1a2.HTTPRouteFilter, path *field.Path) field.ErrorList {
204-
var errs field.ErrorList
205-
if filter.ExtensionRef != nil && filter.Type != gatewayv1b1.HTTPRouteFilterExtensionRef {
206-
errs = append(errs, field.Invalid(path, filter.ExtensionRef, "must be nil if the HTTPRouteFilter.Type is not ExtensionRef"))
207-
}
208-
if filter.ExtensionRef == nil && filter.Type == gatewayv1b1.HTTPRouteFilterExtensionRef {
209-
errs = append(errs, field.Required(path, "filter.ExtensionRef must be specified for ExtensionRef HTTPRouteFilter.Type"))
210-
}
211-
if filter.RequestHeaderModifier != nil && filter.Type != gatewayv1b1.HTTPRouteFilterRequestHeaderModifier {
212-
errs = append(errs, field.Invalid(path, filter.RequestHeaderModifier, "must be nil if the HTTPRouteFilter.Type is not RequestHeaderModifier"))
213-
}
214-
if filter.RequestHeaderModifier == nil && filter.Type == gatewayv1b1.HTTPRouteFilterRequestHeaderModifier {
215-
errs = append(errs, field.Required(path, "filter.RequestHeaderModifier must be specified for RequestHeaderModifier HTTPRouteFilter.Type"))
216-
}
217-
if filter.RequestMirror != nil && filter.Type != gatewayv1b1.HTTPRouteFilterRequestMirror {
218-
errs = append(errs, field.Invalid(path, filter.RequestMirror, "must be nil if the HTTPRouteFilter.Type is not RequestMirror"))
219-
}
220-
if filter.RequestMirror == nil && filter.Type == gatewayv1b1.HTTPRouteFilterRequestMirror {
221-
errs = append(errs, field.Required(path, "filter.RequestMirror must be specified for RequestMirror HTTPRouteFilter.Type"))
222-
}
223-
if filter.RequestRedirect != nil && filter.Type != gatewayv1b1.HTTPRouteFilterRequestRedirect {
224-
errs = append(errs, field.Invalid(path, filter.RequestRedirect, "must be nil if the HTTPRouteFilter.Type is not RequestRedirect"))
225-
}
226-
if filter.RequestRedirect == nil && filter.Type == gatewayv1b1.HTTPRouteFilterRequestRedirect {
227-
errs = append(errs, field.Required(path, "filter.RequestRedirect must be specified for RequestRedirect HTTPRouteFilter.Type"))
228-
}
229-
if filter.URLRewrite != nil && filter.Type != gatewayv1b1.HTTPRouteFilterURLRewrite {
230-
errs = append(errs, field.Invalid(path, filter.URLRewrite, "must be nil if the HTTPRouteFilter.Type is not URLRewrite"))
231-
}
232-
if filter.URLRewrite == nil && filter.Type == gatewayv1b1.HTTPRouteFilterURLRewrite {
233-
errs = append(errs, field.Required(path, "filter.URLRewrite must be specified for URLRewrite HTTPRouteFilter.Type"))
234-
}
235-
return errs
236-
}
237-
238-
// validateHTTPPathModifier validates that only the expected fields are set in a
239-
// path modifier.
240-
func validateHTTPPathModifier(modifier gatewayv1a2.HTTPPathModifier, matches []gatewayv1a2.HTTPRouteMatch, path *field.Path) field.ErrorList {
241-
var errs field.ErrorList
242-
if modifier.ReplaceFullPath != nil && modifier.Type != gatewayv1b1.FullPathHTTPPathModifier {
243-
errs = append(errs, field.Invalid(path, modifier.ReplaceFullPath, "must be nil if the HTTPRouteFilter.Type is not ReplaceFullPath"))
244-
}
245-
if modifier.ReplaceFullPath == nil && modifier.Type == gatewayv1b1.FullPathHTTPPathModifier {
246-
errs = append(errs, field.Invalid(path, modifier.ReplaceFullPath, "must not be nil if the HTTPRouteFilter.Type is ReplaceFullPath"))
247-
}
248-
if modifier.ReplacePrefixMatch != nil && modifier.Type != gatewayv1b1.PrefixMatchHTTPPathModifier {
249-
errs = append(errs, field.Invalid(path, modifier.ReplacePrefixMatch, "must be nil if the HTTPRouteFilter.Type is not ReplacePrefixMatch"))
250-
}
251-
if modifier.ReplacePrefixMatch == nil && modifier.Type == gatewayv1b1.PrefixMatchHTTPPathModifier {
252-
errs = append(errs, field.Invalid(path, modifier.ReplacePrefixMatch, "must not be nil if the HTTPRouteFilter.Type is ReplacePrefixMatch"))
253-
}
254-
255-
if modifier.Type == gatewayv1b1.PrefixMatchHTTPPathModifier && modifier.ReplacePrefixMatch != nil {
256-
if !hasExactlyOnePrefixMatch(matches) {
257-
errs = append(errs, field.Invalid(path, modifier.ReplacePrefixMatch, "exactly one PathPrefix match must be specified to use this path modifier"))
258-
}
259-
}
260-
return errs
261-
}
262-
263-
func hasExactlyOnePrefixMatch(matches []gatewayv1a2.HTTPRouteMatch) bool {
264-
if len(matches) != 1 || matches[0].Path == nil {
265-
return false
266-
}
267-
pathMatchType := matches[0].Path.Type
268-
if *pathMatchType != gatewayv1b1.PathMatchPathPrefix {
269-
return false
270-
}
271-
272-
return true
30+
return gatewayv1b1validation.ValidateHTTPRouteSpec(&route.Spec, field.NewPath("spec"))
27331
}

apis/v1beta1/validation/gateway.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -51,12 +51,12 @@ var (
5151
// Validation that is not possible with CRD annotations may be added here in the future.
5252
// See https://github.com/kubernetes-sigs/gateway-api/issues/868 for more information.
5353
func ValidateGateway(gw *gatewayv1b1.Gateway) field.ErrorList {
54-
return validateGatewaySpec(&gw.Spec, field.NewPath("spec"))
54+
return ValidateGatewaySpec(&gw.Spec, field.NewPath("spec"))
5555
}
5656

57-
// validateGatewaySpec validates whether required fields of spec are set according to the
57+
// ValidateGatewaySpec validates whether required fields of spec are set according to the
5858
// Gateway API specification.
59-
func validateGatewaySpec(spec *gatewayv1b1.GatewaySpec, path *field.Path) field.ErrorList {
59+
func ValidateGatewaySpec(spec *gatewayv1b1.GatewaySpec, path *field.Path) field.ErrorList {
6060
var errs field.ErrorList
6161
errs = append(errs, validateGatewayListeners(spec.Listeners, path.Child("listeners"))...)
6262
return errs

0 commit comments

Comments
 (0)