diff --git a/apis/v1alpha2/validation/gateway.go b/apis/v1alpha2/validation/gateway.go index 1524c89c78..7e7bc27695 100644 --- a/apis/v1alpha2/validation/gateway.go +++ b/apis/v1alpha2/validation/gateway.go @@ -17,10 +17,6 @@ limitations under the License. package validation import ( - "net" - "strings" - - "k8s.io/apimachinery/pkg/util/validation" "k8s.io/apimachinery/pkg/util/validation/field" gatewayv1a2 "sigs.k8s.io/gateway-api/apis/v1alpha2" @@ -29,49 +25,9 @@ import ( // 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/#gateway.networking.k8s.io/v1alpha2.Gateway +// +// 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 { - // 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 []gatewayv1a2.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 []gatewayv1a2.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 - } - 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 + return nil } diff --git a/apis/v1alpha2/validation/gateway_test.go b/apis/v1alpha2/validation/gateway_test.go deleted file mode 100644 index dd7dd0c173..0000000000 --- a/apis/v1alpha2/validation/gateway_test.go +++ /dev/null @@ -1,162 +0,0 @@ -/* -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" - - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - - gatewayv1a2 "sigs.k8s.io/gateway-api/apis/v1alpha2" -) - -func TestValidateGateway(t *testing.T) { - listeners := []gatewayv1a2.Listener{ - { - Hostname: nil, - }, - } - baseGateway := gatewayv1a2.Gateway{ - ObjectMeta: metav1.ObjectMeta{ - Name: "foo", - Namespace: metav1.NamespaceDefault, - }, - Spec: gatewayv1a2.GatewaySpec{ - GatewayClassName: "foo", - Listeners: listeners, - }, - } - - testCases := map[string]struct { - mutate func(gw *gatewayv1a2.Gateway) - expectErrsOnFields []string - }{ - "nil hostname": { - mutate: func(gw *gatewayv1a2.Gateway) {}, - expectErrsOnFields: []string{}, - }, - "empty string hostname": { - mutate: func(gw *gatewayv1a2.Gateway) { - hostname := gatewayv1a2.Hostname("") - gw.Spec.Listeners[0].Hostname = &hostname - }, - expectErrsOnFields: []string{}, - }, - "wildcard hostname": { - mutate: func(gw *gatewayv1a2.Gateway) { - hostname := gatewayv1a2.Hostname("*") - gw.Spec.Listeners[0].Hostname = &hostname - }, - expectErrsOnFields: []string{}, - }, - "wildcard-prefixed hostname": { - mutate: func(gw *gatewayv1a2.Gateway) { - hostname := gatewayv1a2.Hostname("*.example.com") - gw.Spec.Listeners[0].Hostname = &hostname - }, - expectErrsOnFields: []string{}, - }, - "valid dns subdomain": { - mutate: func(gw *gatewayv1a2.Gateway) { - hostname := gatewayv1a2.Hostname("foo.example.com") - gw.Spec.Listeners[0].Hostname = &hostname - }, - expectErrsOnFields: []string{}, - }, - // Invalid use cases - "IPv4 address hostname": { - mutate: func(gw *gatewayv1a2.Gateway) { - hostname := gatewayv1a2.Hostname("1.2.3.4") - gw.Spec.Listeners[0].Hostname = &hostname - }, - expectErrsOnFields: []string{"spec.listeners[0].hostname"}, - }, - "Invalid IPv4 address hostname": { - mutate: func(gw *gatewayv1a2.Gateway) { - hostname := gatewayv1a2.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 *gatewayv1a2.Gateway) { - hostname := gatewayv1a2.Hostname("1.2.3.4:8080") - gw.Spec.Listeners[0].Hostname = &hostname - }, - expectErrsOnFields: []string{"spec.listeners[0].hostname"}, - }, - "IPv6 address hostname": { - mutate: func(gw *gatewayv1a2.Gateway) { - hostname := gatewayv1a2.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 *gatewayv1a2.Gateway) { - hostname := gatewayv1a2.Hostname("fe80::/10") - gw.Spec.Listeners[0].Hostname = &hostname - }, - expectErrsOnFields: []string{"spec.listeners[0].hostname"}, - }, - "dns subdomain with port": { - mutate: func(gw *gatewayv1a2.Gateway) { - hostname := gatewayv1a2.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 *gatewayv1a2.Gateway) { - hostname := gatewayv1a2.Hostname("*.*.com") - gw.Spec.Listeners[0].Hostname = &hostname - }, - expectErrsOnFields: []string{"spec.listeners[0].hostname"}, - }, - "dns subdomain with multiple wildcards": { - mutate: func(gw *gatewayv1a2.Gateway) { - hostname := gatewayv1a2.Hostname("*.foo.*.com") - gw.Spec.Listeners[0].Hostname = &hostname - }, - expectErrsOnFields: []string{"spec.listeners[0].hostname"}, - }, - "dns subdomain with wildcard root label": { - mutate: func(gw *gatewayv1a2.Gateway) { - hostname := gatewayv1a2.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()) - } - } - }) - } -} diff --git a/pkg/admission/server_test.go b/pkg/admission/server_test.go index 659b0829dc..428665ea42 100644 --- a/pkg/admission/server_test.go +++ b/pkg/admission/server_test.go @@ -59,10 +59,6 @@ func TestServeHTTPInvalidMethod(t *testing.T) { } func TestServeHTTPSubmissions(t *testing.T) { - - const invalidHostname = "!@!.foo.com" - const lowercaseRFC1123ErrorMessage = "a lowercase RFC 1123 subdomain must consist of lower case alphanumeric characters, '-' or '.', and must start and end with an alphanumeric character (e.g. 'example.com', regex used for validation is '[a-z0-9]([-a-z0-9]*[a-z0-9])?(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*')" - for _, apiVersion := range []string{ "admission.k8s.io/v1", "admission.k8s.io/v1", @@ -173,58 +169,6 @@ func TestServeHTTPSubmissions(t *testing.T) { Result: &metav1.Status{}, }, }, - { - name: "invalid v1alpha1 Gateway resource with bad listener hostname", - reqBody: dedent.Dedent(`{ - "kind": "AdmissionReview", - "apiVersion": "` + apiVersion + `", - "request": { - "uid": "7313cd05-eddc-4150-b88c-971a0d53b2ab", - "resource": { - "group": "networking.x-k8s.io", - "version": "v1alpha1", - "resource": "gateways" - }, - "object": { - "kind": "Gateway", - "apiVersion": "networking.x-k8s.io/v1alpha1", - "metadata": { - "name": "gateway-1", - "labels": { - "app": "foo" - } - }, - "spec": { - "gatewayClassName": "contour-class", - "listeners": [ - { - "port": 80, - "protocol": "HTTP", - "hostname": "` + invalidHostname + `", - "routes": { - "group": "networking.x-k8s.io", - "kind": "HTTPRoute", - "namespaces": { - "from": "All" - } - } - } - ] - } - }, - "operation": "CREATE" - } - }`), - wantRespCode: http.StatusOK, - wantSuccessResponse: admission.AdmissionResponse{ - UID: "7313cd05-eddc-4150-b88c-971a0d53b2ab", - Allowed: false, - Result: &metav1.Status{ - Code: 400, - Message: fmt.Sprintf("spec.listeners[0].hostname: Invalid value: %q: %s", invalidHostname, lowercaseRFC1123ErrorMessage), - }, - }, - }, { name: "valid v1alpha2 Gateway resource", reqBody: dedent.Dedent(`{ @@ -274,58 +218,6 @@ func TestServeHTTPSubmissions(t *testing.T) { Result: &metav1.Status{}, }, }, - { - name: "invalid v1alpha2 Gateway resource with bad listener hostname", - reqBody: dedent.Dedent(`{ - "kind": "AdmissionReview", - "apiVersion": "` + apiVersion + `", - "request": { - "uid": "7313cd05-eddc-4150-b88c-971a0d53b2ab", - "resource": { - "group": "gateway.networking.k8s.io", - "version": "v1alpha2", - "resource": "gateways" - }, - "object": { - "kind": "Gateway", - "apiVersion": "gateway.networking.k8s.io/v1alpha2", - "metadata": { - "name": "gateway-1", - "labels": { - "app": "foo" - } - }, - "spec": { - "gatewayClassName": "contour-class", - "listeners": [ - { - "port": 80, - "protocol": "HTTP", - "hostname": "` + invalidHostname + `", - "routes": { - "group": "gateway.networking.k8s.io", - "kind": "HTTPRoute", - "namespaces": { - "from": "All" - } - } - } - ] - } - }, - "operation": "CREATE" - } - }`), - wantRespCode: http.StatusOK, - wantSuccessResponse: admission.AdmissionResponse{ - UID: "7313cd05-eddc-4150-b88c-971a0d53b2ab", - Allowed: false, - Result: &metav1.Status{ - Code: 400, - Message: fmt.Sprintf("spec.listeners[0].hostname: Invalid value: %q: %s", invalidHostname, lowercaseRFC1123ErrorMessage), - }, - }, - }, { name: "valid HTTPRoute resource", reqBody: dedent.Dedent(`{