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  }