k8s.io/kubernetes@v1.31.0-alpha.0.0.20240520171757-56147500dadc/pkg/apis/resource/structured/namedresources/validation/validation_test.go (about) 1 /* 2 Copyright 2022 The Kubernetes Authors. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 package validation 18 19 import ( 20 "testing" 21 22 "github.com/stretchr/testify/assert" 23 24 "k8s.io/apimachinery/pkg/api/resource" 25 "k8s.io/apimachinery/pkg/util/validation/field" 26 resourceapi "k8s.io/kubernetes/pkg/apis/resource" 27 "k8s.io/utils/ptr" 28 ) 29 30 func testResources(instances []resourceapi.NamedResourcesInstance) *resourceapi.NamedResourcesResources { 31 resources := &resourceapi.NamedResourcesResources{ 32 Instances: instances, 33 } 34 return resources 35 } 36 37 func TestValidateResources(t *testing.T) { 38 goodName := "foo" 39 badName := "!@#$%^" 40 quantity := resource.MustParse("1") 41 42 scenarios := map[string]struct { 43 resources *resourceapi.NamedResourcesResources 44 wantFailures field.ErrorList 45 }{ 46 "empty": { 47 resources: testResources(nil), 48 }, 49 "good": { 50 resources: testResources([]resourceapi.NamedResourcesInstance{{Name: goodName}}), 51 }, 52 "bad-name": { 53 wantFailures: field.ErrorList{field.Invalid(field.NewPath("instances").Index(0).Child("name"), badName, "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])?)*')")}, 54 resources: testResources([]resourceapi.NamedResourcesInstance{{Name: badName}}), 55 }, 56 "duplicate-name": { 57 wantFailures: field.ErrorList{field.Duplicate(field.NewPath("instances").Index(1).Child("name"), goodName)}, 58 resources: testResources([]resourceapi.NamedResourcesInstance{{Name: goodName}, {Name: goodName}}), 59 }, 60 "quantity": { 61 resources: testResources([]resourceapi.NamedResourcesInstance{{Name: goodName, Attributes: []resourceapi.NamedResourcesAttribute{{Name: goodName, NamedResourcesAttributeValue: resourceapi.NamedResourcesAttributeValue{QuantityValue: &quantity}}}}}), 62 }, 63 "bool": { 64 resources: testResources([]resourceapi.NamedResourcesInstance{{Name: goodName, Attributes: []resourceapi.NamedResourcesAttribute{{Name: goodName, NamedResourcesAttributeValue: resourceapi.NamedResourcesAttributeValue{BoolValue: ptr.To(true)}}}}}), 65 }, 66 "int": { 67 resources: testResources([]resourceapi.NamedResourcesInstance{{Name: goodName, Attributes: []resourceapi.NamedResourcesAttribute{{Name: goodName, NamedResourcesAttributeValue: resourceapi.NamedResourcesAttributeValue{IntValue: ptr.To(int64(1))}}}}}), 68 }, 69 "int-slice": { 70 resources: testResources([]resourceapi.NamedResourcesInstance{{Name: goodName, Attributes: []resourceapi.NamedResourcesAttribute{{Name: goodName, NamedResourcesAttributeValue: resourceapi.NamedResourcesAttributeValue{IntSliceValue: &resourceapi.NamedResourcesIntSlice{Ints: []int64{1, 2, 3}}}}}}}), 71 }, 72 "string": { 73 resources: testResources([]resourceapi.NamedResourcesInstance{{Name: goodName, Attributes: []resourceapi.NamedResourcesAttribute{{Name: goodName, NamedResourcesAttributeValue: resourceapi.NamedResourcesAttributeValue{StringValue: ptr.To("hello")}}}}}), 74 }, 75 "string-slice": { 76 resources: testResources([]resourceapi.NamedResourcesInstance{{Name: goodName, Attributes: []resourceapi.NamedResourcesAttribute{{Name: goodName, NamedResourcesAttributeValue: resourceapi.NamedResourcesAttributeValue{StringSliceValue: &resourceapi.NamedResourcesStringSlice{Strings: []string{"hello"}}}}}}}), 77 }, 78 "version-okay": { 79 resources: testResources([]resourceapi.NamedResourcesInstance{{Name: goodName, Attributes: []resourceapi.NamedResourcesAttribute{{Name: goodName, NamedResourcesAttributeValue: resourceapi.NamedResourcesAttributeValue{VersionValue: ptr.To("1.0.0")}}}}}), 80 }, 81 "version-beta": { 82 resources: testResources([]resourceapi.NamedResourcesInstance{{Name: goodName, Attributes: []resourceapi.NamedResourcesAttribute{{Name: goodName, NamedResourcesAttributeValue: resourceapi.NamedResourcesAttributeValue{VersionValue: ptr.To("1.0.0-beta")}}}}}), 83 }, 84 "version-beta-1": { 85 resources: testResources([]resourceapi.NamedResourcesInstance{{Name: goodName, Attributes: []resourceapi.NamedResourcesAttribute{{Name: goodName, NamedResourcesAttributeValue: resourceapi.NamedResourcesAttributeValue{VersionValue: ptr.To("1.0.0-beta.1")}}}}}), 86 }, 87 "version-build": { 88 resources: testResources([]resourceapi.NamedResourcesInstance{{Name: goodName, Attributes: []resourceapi.NamedResourcesAttribute{{Name: goodName, NamedResourcesAttributeValue: resourceapi.NamedResourcesAttributeValue{VersionValue: ptr.To("1.0.0+build")}}}}}), 89 }, 90 "version-build-1": { 91 resources: testResources([]resourceapi.NamedResourcesInstance{{Name: goodName, Attributes: []resourceapi.NamedResourcesAttribute{{Name: goodName, NamedResourcesAttributeValue: resourceapi.NamedResourcesAttributeValue{VersionValue: ptr.To("1.0.0+build.1")}}}}}), 92 }, 93 "version-beta-1-build-1": { 94 resources: testResources([]resourceapi.NamedResourcesInstance{{Name: goodName, Attributes: []resourceapi.NamedResourcesAttribute{{Name: goodName, NamedResourcesAttributeValue: resourceapi.NamedResourcesAttributeValue{VersionValue: ptr.To("1.0.0-beta.1+build.1")}}}}}), 95 }, 96 "version-bad": { 97 wantFailures: field.ErrorList{field.Invalid(field.NewPath("instances").Index(0).Child("attributes").Index(0).Child("version"), "1.0", "must be a string compatible with semver.org spec 2.0.0")}, 98 resources: testResources([]resourceapi.NamedResourcesInstance{{Name: goodName, Attributes: []resourceapi.NamedResourcesAttribute{{Name: goodName, NamedResourcesAttributeValue: resourceapi.NamedResourcesAttributeValue{VersionValue: ptr.To("1.0")}}}}}), 99 }, 100 "version-bad-leading-zeros": { 101 wantFailures: field.ErrorList{field.Invalid(field.NewPath("instances").Index(0).Child("attributes").Index(0).Child("version"), "01.0.0", "must be a string compatible with semver.org spec 2.0.0")}, 102 resources: testResources([]resourceapi.NamedResourcesInstance{{Name: goodName, Attributes: []resourceapi.NamedResourcesAttribute{{Name: goodName, NamedResourcesAttributeValue: resourceapi.NamedResourcesAttributeValue{VersionValue: ptr.To("01.0.0")}}}}}), 103 }, 104 "version-bad-leading-zeros-middle": { 105 wantFailures: field.ErrorList{field.Invalid(field.NewPath("instances").Index(0).Child("attributes").Index(0).Child("version"), "1.00.0", "must be a string compatible with semver.org spec 2.0.0")}, 106 resources: testResources([]resourceapi.NamedResourcesInstance{{Name: goodName, Attributes: []resourceapi.NamedResourcesAttribute{{Name: goodName, NamedResourcesAttributeValue: resourceapi.NamedResourcesAttributeValue{VersionValue: ptr.To("1.00.0")}}}}}), 107 }, 108 "version-bad-leading-zeros-end": { 109 wantFailures: field.ErrorList{field.Invalid(field.NewPath("instances").Index(0).Child("attributes").Index(0).Child("version"), "1.0.00", "must be a string compatible with semver.org spec 2.0.0")}, 110 resources: testResources([]resourceapi.NamedResourcesInstance{{Name: goodName, Attributes: []resourceapi.NamedResourcesAttribute{{Name: goodName, NamedResourcesAttributeValue: resourceapi.NamedResourcesAttributeValue{VersionValue: ptr.To("1.0.00")}}}}}), 111 }, 112 "version-bad-spaces": { 113 wantFailures: field.ErrorList{field.Invalid(field.NewPath("instances").Index(0).Child("attributes").Index(0).Child("version"), " 1.0.0 ", "must be a string compatible with semver.org spec 2.0.0")}, 114 resources: testResources([]resourceapi.NamedResourcesInstance{{Name: goodName, Attributes: []resourceapi.NamedResourcesAttribute{{Name: goodName, NamedResourcesAttributeValue: resourceapi.NamedResourcesAttributeValue{VersionValue: ptr.To(" 1.0.0 ")}}}}}), 115 }, 116 "empty-attribute": { 117 wantFailures: field.ErrorList{field.Required(field.NewPath("instances").Index(0).Child("attributes").Index(0), "exactly one value must be set")}, 118 resources: testResources([]resourceapi.NamedResourcesInstance{{Name: goodName, Attributes: []resourceapi.NamedResourcesAttribute{{Name: goodName}}}}), 119 }, 120 "duplicate-value": { 121 wantFailures: field.ErrorList{field.Invalid(field.NewPath("instances").Index(0).Child("attributes").Index(0), []string{"bool", "int"}, "exactly one field must be set, not several")}, 122 resources: testResources([]resourceapi.NamedResourcesInstance{{Name: goodName, Attributes: []resourceapi.NamedResourcesAttribute{{Name: goodName, NamedResourcesAttributeValue: resourceapi.NamedResourcesAttributeValue{BoolValue: ptr.To(true), IntValue: ptr.To(int64(1))}}}}}), 123 }, 124 } 125 126 for name, scenario := range scenarios { 127 t.Run(name, func(t *testing.T) { 128 errs := ValidateResources(scenario.resources, nil) 129 assert.Equal(t, scenario.wantFailures, errs) 130 }) 131 } 132 } 133 134 func TestValidateSelector(t *testing.T) { 135 scenarios := map[string]struct { 136 selector string 137 wantFailures field.ErrorList 138 }{ 139 "okay": { 140 selector: "true", 141 }, 142 "empty": { 143 selector: "", 144 wantFailures: field.ErrorList{field.Required(nil, "")}, 145 }, 146 "undefined": { 147 selector: "nosuchvar", 148 wantFailures: field.ErrorList{field.Invalid(nil, "nosuchvar", "compilation failed: ERROR: <input>:1:1: undeclared reference to 'nosuchvar' (in container '')\n | nosuchvar\n | ^")}, 149 }, 150 "wrong-type": { 151 selector: "1", 152 wantFailures: field.ErrorList{field.Invalid(nil, "1", "must evaluate to bool")}, 153 }, 154 "quantity": { 155 selector: `attributes.quantity["name"].isGreaterThan(quantity("0"))`, 156 }, 157 "bool": { 158 selector: `attributes.bool["name"]`, 159 }, 160 "int": { 161 selector: `attributes.int["name"] > 0`, 162 }, 163 "intslice": { 164 selector: `attributes.intslice["name"].isSorted()`, 165 }, 166 "string": { 167 selector: `attributes.string["name"] == "fish"`, 168 }, 169 "stringslice": { 170 selector: `attributes.stringslice["name"].isSorted()`, 171 }, 172 "version": { 173 selector: `attributes.version["name"].isGreaterThan(semver("1.0.0"))`, 174 }, 175 } 176 177 for name, scenario := range scenarios { 178 t.Run(name, func(t *testing.T) { 179 // At the moment, there's no difference between stored and new expressions. 180 // This uses the stricter validation. 181 opts := Options{ 182 StoredExpressions: false, 183 } 184 errs := validateSelector(opts, scenario.selector, nil) 185 assert.Equal(t, scenario.wantFailures, errs) 186 }) 187 } 188 }