From f364d08a0d9d6e740062001ea37d09a84222ede8 Mon Sep 17 00:00:00 2001 From: Daneyon Hansen Date: Fri, 26 Mar 2021 09:42:25 -0700 Subject: [PATCH] Introduces a Validation Package --- apis/v1alpha1/validation/doc.go | 19 +++ apis/v1alpha1/validation/validation.go | 78 ++++++++++ apis/v1alpha1/validation/validation_test.go | 162 ++++++++++++++++++++ 3 files changed, 259 insertions(+) create mode 100644 apis/v1alpha1/validation/doc.go create mode 100644 apis/v1alpha1/validation/validation.go create mode 100644 apis/v1alpha1/validation/validation_test.go diff --git a/apis/v1alpha1/validation/doc.go b/apis/v1alpha1/validation/doc.go new file mode 100644 index 0000000000..71ac0c4e6d --- /dev/null +++ b/apis/v1alpha1/validation/doc.go @@ -0,0 +1,19 @@ +/* +Copyright 2021 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 validation has functions for validating the correctness of api +// objects and explaining what's wrong with them when they're not valid. +package validation // import "sigs.k8s.io/gateway-api/apis/v1alpha1/validation" diff --git a/apis/v1alpha1/validation/validation.go b/apis/v1alpha1/validation/validation.go new file mode 100644 index 0000000000..abc2d5f94f --- /dev/null +++ b/apis/v1alpha1/validation/validation.go @@ -0,0 +1,78 @@ +/* +Copyright 2021 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 validation + +import ( + "net" + "strings" + + gatewayv1a1 "sigs.k8s.io/gateway-api/apis/v1alpha1" + + "k8s.io/apimachinery/pkg/util/validation" + "k8s.io/apimachinery/pkg/util/validation/field" +) + +// ValidateGateway validates gw according to the Gateway API specification. +// For additional details of the Gateway spec, refer to: +// https://gateway-api.sigs.k8s.io/spec/#networking.x-k8s.io/v1alpha1.Gateway +func ValidateGateway(gw *gatewayv1a1.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 *gatewayv1a1.GatewaySpec, path *field.Path) field.ErrorList { + // TODO [danehans]: Add additional validation of spec fields. + return validateGatewayListeners(spec.Listeners, path.Child("listeners")) +} + +// validateGatewayListeners validates whether required fields of listeners are set according +// to the Gateway API specification. +func validateGatewayListeners(listeners []gatewayv1a1.Listener, path *field.Path) field.ErrorList { + // TODO [danehans]: Add additional validation of listener fields. + return validateListenerHostname(listeners, path) +} + +// validateListenerHostname validates each listener hostname is not an IP address and is one +// of the following: +// - A fully qualified domain name of a network host, as defined by RFC 3986. +// - A DNS subdomain as defined by RFC 1123. +// - A wildcard DNS subdomain as defined by RFC 1034 (section 4.3.3). +func validateListenerHostname(listeners []gatewayv1a1.Listener, path *field.Path) field.ErrorList { + var errs field.ErrorList + for i, h := range listeners { + // When unspecified, “”, or *, all hostnames are matched. + if h.Hostname == nil || (*h.Hostname == "" || *h.Hostname == "*") { + continue + } else { + hostname := string(*h.Hostname) + if ip := net.ParseIP(hostname); ip != nil { + errs = append(errs, field.Invalid(path.Index(i).Child("hostname"), hostname, "must be a DNS hostname, not an IP address")) + } + if strings.Contains(hostname, "*") { + for _, msg := range validation.IsWildcardDNS1123Subdomain(hostname) { + errs = append(errs, field.Invalid(path.Index(i).Child("hostname"), hostname, msg)) + } + } else { + for _, msg := range validation.IsDNS1123Subdomain(hostname) { + errs = append(errs, field.Invalid(path.Index(i).Child("hostname"), hostname, msg)) + } + } + } + } + return errs +} diff --git a/apis/v1alpha1/validation/validation_test.go b/apis/v1alpha1/validation/validation_test.go new file mode 100644 index 0000000000..c5d99d1ae4 --- /dev/null +++ b/apis/v1alpha1/validation/validation_test.go @@ -0,0 +1,162 @@ +/* +Copyright 2021 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 validation + +import ( + "testing" + + gatewayv1a1 "sigs.k8s.io/gateway-api/apis/v1alpha1" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func TestValidateGateway(t *testing.T) { + listeners := []gatewayv1a1.Listener{ + { + Hostname: nil, + }, + } + baseGateway := gatewayv1a1.Gateway{ + ObjectMeta: metav1.ObjectMeta{ + Name: "foo", + Namespace: metav1.NamespaceDefault, + }, + Spec: gatewayv1a1.GatewaySpec{ + GatewayClassName: "foo", + Listeners: listeners, + }, + } + + testCases := map[string]struct { + mutate func(gw *gatewayv1a1.Gateway) + expectErrsOnFields []string + }{ + "nil hostname": { + mutate: func(gw *gatewayv1a1.Gateway) {}, + expectErrsOnFields: []string{}, + }, + "empty string hostname": { + mutate: func(gw *gatewayv1a1.Gateway) { + hostname := gatewayv1a1.Hostname("") + gw.Spec.Listeners[0].Hostname = &hostname + }, + expectErrsOnFields: []string{}, + }, + "wildcard hostname": { + mutate: func(gw *gatewayv1a1.Gateway) { + hostname := gatewayv1a1.Hostname("*") + gw.Spec.Listeners[0].Hostname = &hostname + }, + expectErrsOnFields: []string{}, + }, + "wildcard-prefixed hostname": { + mutate: func(gw *gatewayv1a1.Gateway) { + hostname := gatewayv1a1.Hostname("*.example.com") + gw.Spec.Listeners[0].Hostname = &hostname + }, + expectErrsOnFields: []string{}, + }, + "valid dns subdomain": { + mutate: func(gw *gatewayv1a1.Gateway) { + hostname := gatewayv1a1.Hostname("foo.example.com") + gw.Spec.Listeners[0].Hostname = &hostname + }, + expectErrsOnFields: []string{}, + }, + // Invalid use cases + "IPv4 address hostname": { + mutate: func(gw *gatewayv1a1.Gateway) { + hostname := gatewayv1a1.Hostname("1.2.3.4") + gw.Spec.Listeners[0].Hostname = &hostname + }, + expectErrsOnFields: []string{"spec.listeners[0].hostname"}, + }, + "Invalid IPv4 address hostname": { + mutate: func(gw *gatewayv1a1.Gateway) { + hostname := gatewayv1a1.Hostname("1.2.3..4") + gw.Spec.Listeners[0].Hostname = &hostname + }, + expectErrsOnFields: []string{"spec.listeners[0].hostname"}, + }, + "IPv4 address with port hostname": { + mutate: func(gw *gatewayv1a1.Gateway) { + hostname := gatewayv1a1.Hostname("1.2.3.4:8080") + gw.Spec.Listeners[0].Hostname = &hostname + }, + expectErrsOnFields: []string{"spec.listeners[0].hostname"}, + }, + "IPv6 address hostname": { + mutate: func(gw *gatewayv1a1.Gateway) { + hostname := gatewayv1a1.Hostname("2001:db8::68") + gw.Spec.Listeners[0].Hostname = &hostname + }, + expectErrsOnFields: []string{"spec.listeners[0].hostname", "spec.listeners[0].hostname"}, + }, + "IPv6 link-local address hostname": { + mutate: func(gw *gatewayv1a1.Gateway) { + hostname := gatewayv1a1.Hostname("fe80::/10") + gw.Spec.Listeners[0].Hostname = &hostname + }, + expectErrsOnFields: []string{"spec.listeners[0].hostname"}, + }, + "dns subdomain with port": { + mutate: func(gw *gatewayv1a1.Gateway) { + hostname := gatewayv1a1.Hostname("foo.example.com:8080") + gw.Spec.Listeners[0].Hostname = &hostname + }, + expectErrsOnFields: []string{"spec.listeners[0].hostname"}, + }, + "dns subdomain with invalid wildcard label": { + mutate: func(gw *gatewayv1a1.Gateway) { + hostname := gatewayv1a1.Hostname("*.*.com") + gw.Spec.Listeners[0].Hostname = &hostname + }, + expectErrsOnFields: []string{"spec.listeners[0].hostname"}, + }, + "dns subdomain with multiple wildcards": { + mutate: func(gw *gatewayv1a1.Gateway) { + hostname := gatewayv1a1.Hostname("*.foo.*.com") + gw.Spec.Listeners[0].Hostname = &hostname + }, + expectErrsOnFields: []string{"spec.listeners[0].hostname"}, + }, + "dns subdomain with wildcard root label": { + mutate: func(gw *gatewayv1a1.Gateway) { + hostname := gatewayv1a1.Hostname("*.foo.*.com") + gw.Spec.Listeners[0].Hostname = &hostname + }, + expectErrsOnFields: []string{"spec.listeners[0].hostname"}, + }, + } + + for name, tc := range testCases { + tc := tc + t.Run(name, func(t *testing.T) { + gw := baseGateway.DeepCopy() + tc.mutate(gw) + errs := ValidateGateway(gw) + if len(tc.expectErrsOnFields) != len(errs) { + t.Fatalf("Expected %d errors, got %d errors: %v", len(tc.expectErrsOnFields), len(errs), errs) + } + for i, err := range errs { + if err.Field != tc.expectErrsOnFields[i] { + t.Errorf("Expected error on field: %s, got: %s", tc.expectErrsOnFields[i], err.Error()) + } + } + }) + } +}