k8s.io/kubernetes@v1.31.0-alpha.0.0.20240520171757-56147500dadc/pkg/apis/resource/validation/validation_resourceslice_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  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    25  	"k8s.io/apimachinery/pkg/util/validation/field"
    26  	"k8s.io/kubernetes/pkg/apis/resource"
    27  	"k8s.io/utils/ptr"
    28  )
    29  
    30  func testResourceSlice(name, nodeName, driverName string) *resource.ResourceSlice {
    31  	return &resource.ResourceSlice{
    32  		ObjectMeta: metav1.ObjectMeta{
    33  			Name: name,
    34  		},
    35  		NodeName:   nodeName,
    36  		DriverName: driverName,
    37  		ResourceModel: resource.ResourceModel{
    38  			NamedResources: &resource.NamedResourcesResources{},
    39  		},
    40  	}
    41  }
    42  
    43  func TestValidateResourceSlice(t *testing.T) {
    44  	goodName := "foo"
    45  	badName := "!@#$%^"
    46  	driverName := "test.example.com"
    47  	now := metav1.Now()
    48  	badValue := "spaces not allowed"
    49  
    50  	scenarios := map[string]struct {
    51  		slice        *resource.ResourceSlice
    52  		wantFailures field.ErrorList
    53  	}{
    54  		"good": {
    55  			slice: testResourceSlice(goodName, goodName, driverName),
    56  		},
    57  		"missing-name": {
    58  			wantFailures: field.ErrorList{field.Required(field.NewPath("metadata", "name"), "name or generateName is required")},
    59  			slice:        testResourceSlice("", goodName, driverName),
    60  		},
    61  		"bad-name": {
    62  			wantFailures: field.ErrorList{field.Invalid(field.NewPath("metadata", "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])?)*')")},
    63  			slice:        testResourceSlice(badName, goodName, driverName),
    64  		},
    65  		"generate-name": {
    66  			slice: func() *resource.ResourceSlice {
    67  				slice := testResourceSlice(goodName, goodName, driverName)
    68  				slice.GenerateName = "prefix-"
    69  				return slice
    70  			}(),
    71  		},
    72  		"uid": {
    73  			slice: func() *resource.ResourceSlice {
    74  				slice := testResourceSlice(goodName, goodName, driverName)
    75  				slice.UID = "ac051fac-2ead-46d9-b8b4-4e0fbeb7455d"
    76  				return slice
    77  			}(),
    78  		},
    79  		"resource-version": {
    80  			slice: func() *resource.ResourceSlice {
    81  				slice := testResourceSlice(goodName, goodName, driverName)
    82  				slice.ResourceVersion = "1"
    83  				return slice
    84  			}(),
    85  		},
    86  		"generation": {
    87  			slice: func() *resource.ResourceSlice {
    88  				slice := testResourceSlice(goodName, goodName, driverName)
    89  				slice.Generation = 100
    90  				return slice
    91  			}(),
    92  		},
    93  		"creation-timestamp": {
    94  			slice: func() *resource.ResourceSlice {
    95  				slice := testResourceSlice(goodName, goodName, driverName)
    96  				slice.CreationTimestamp = now
    97  				return slice
    98  			}(),
    99  		},
   100  		"deletion-grace-period-seconds": {
   101  			slice: func() *resource.ResourceSlice {
   102  				slice := testResourceSlice(goodName, goodName, driverName)
   103  				slice.DeletionGracePeriodSeconds = ptr.To[int64](10)
   104  				return slice
   105  			}(),
   106  		},
   107  		"owner-references": {
   108  			slice: func() *resource.ResourceSlice {
   109  				slice := testResourceSlice(goodName, goodName, driverName)
   110  				slice.OwnerReferences = []metav1.OwnerReference{
   111  					{
   112  						APIVersion: "v1",
   113  						Kind:       "pod",
   114  						Name:       "foo",
   115  						UID:        "ac051fac-2ead-46d9-b8b4-4e0fbeb7455d",
   116  					},
   117  				}
   118  				return slice
   119  			}(),
   120  		},
   121  		"finalizers": {
   122  			slice: func() *resource.ResourceSlice {
   123  				slice := testResourceSlice(goodName, goodName, driverName)
   124  				slice.Finalizers = []string{
   125  					"example.com/foo",
   126  				}
   127  				return slice
   128  			}(),
   129  		},
   130  		"managed-fields": {
   131  			slice: func() *resource.ResourceSlice {
   132  				slice := testResourceSlice(goodName, goodName, driverName)
   133  				slice.ManagedFields = []metav1.ManagedFieldsEntry{
   134  					{
   135  						FieldsType: "FieldsV1",
   136  						Operation:  "Apply",
   137  						APIVersion: "apps/v1",
   138  						Manager:    "foo",
   139  					},
   140  				}
   141  				return slice
   142  			}(),
   143  		},
   144  		"good-labels": {
   145  			slice: func() *resource.ResourceSlice {
   146  				slice := testResourceSlice(goodName, goodName, driverName)
   147  				slice.Labels = map[string]string{
   148  					"apps.kubernetes.io/name": "test",
   149  				}
   150  				return slice
   151  			}(),
   152  		},
   153  		"bad-labels": {
   154  			wantFailures: field.ErrorList{field.Invalid(field.NewPath("metadata", "labels"), badValue, "a valid label must be an empty string or consist of alphanumeric characters, '-', '_' or '.', and must start and end with an alphanumeric character (e.g. 'MyValue',  or 'my_value',  or '12345', regex used for validation is '(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])?')")},
   155  			slice: func() *resource.ResourceSlice {
   156  				slice := testResourceSlice(goodName, goodName, driverName)
   157  				slice.Labels = map[string]string{
   158  					"hello-world": badValue,
   159  				}
   160  				return slice
   161  			}(),
   162  		},
   163  		"good-annotations": {
   164  			slice: func() *resource.ResourceSlice {
   165  				slice := testResourceSlice(goodName, goodName, driverName)
   166  				slice.Annotations = map[string]string{
   167  					"foo": "bar",
   168  				}
   169  				return slice
   170  			}(),
   171  		},
   172  		"bad-annotations": {
   173  			wantFailures: field.ErrorList{field.Invalid(field.NewPath("metadata", "annotations"), badName, "name part must consist of alphanumeric characters, '-', '_' or '.', and must start and end with an alphanumeric character (e.g. 'MyName',  or 'my.name',  or '123-abc', regex used for validation is '([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9]')")},
   174  			slice: func() *resource.ResourceSlice {
   175  				slice := testResourceSlice(goodName, goodName, driverName)
   176  				slice.Annotations = map[string]string{
   177  					badName: "hello world",
   178  				}
   179  				return slice
   180  			}(),
   181  		},
   182  		"bad-nodename": {
   183  			wantFailures: field.ErrorList{field.Invalid(field.NewPath("nodeName"), 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])?)*')")},
   184  			slice:        testResourceSlice(goodName, badName, driverName),
   185  		},
   186  		"bad-drivername": {
   187  			wantFailures: field.ErrorList{field.Invalid(field.NewPath("driverName"), 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])?)*')")},
   188  			slice:        testResourceSlice(goodName, goodName, badName),
   189  		},
   190  
   191  		"empty-model": {
   192  			wantFailures: field.ErrorList{field.Required(nil, "exactly one structured model field must be set")},
   193  			slice: func() *resource.ResourceSlice {
   194  				slice := testResourceSlice(goodName, goodName, driverName)
   195  				slice.ResourceModel = resource.ResourceModel{}
   196  				return slice
   197  			}(),
   198  		},
   199  	}
   200  
   201  	for name, scenario := range scenarios {
   202  		t.Run(name, func(t *testing.T) {
   203  			errs := ValidateResourceSlice(scenario.slice)
   204  			assert.Equal(t, scenario.wantFailures, errs)
   205  		})
   206  	}
   207  }
   208  
   209  func TestValidateResourceSliceUpdate(t *testing.T) {
   210  	name := "valid"
   211  	validResourceSlice := testResourceSlice(name, name, name)
   212  
   213  	scenarios := map[string]struct {
   214  		oldResourceSlice *resource.ResourceSlice
   215  		update           func(slice *resource.ResourceSlice) *resource.ResourceSlice
   216  		wantFailures     field.ErrorList
   217  	}{
   218  		"valid-no-op-update": {
   219  			oldResourceSlice: validResourceSlice,
   220  			update:           func(slice *resource.ResourceSlice) *resource.ResourceSlice { return slice },
   221  		},
   222  		"invalid-name-update": {
   223  			oldResourceSlice: validResourceSlice,
   224  			update: func(slice *resource.ResourceSlice) *resource.ResourceSlice {
   225  				slice.Name += "-update"
   226  				return slice
   227  			},
   228  			wantFailures: field.ErrorList{field.Invalid(field.NewPath("metadata", "name"), name+"-update", "field is immutable")},
   229  		},
   230  		"invalid-update-nodename": {
   231  			wantFailures:     field.ErrorList{field.Invalid(field.NewPath("nodeName"), name+"-updated", "field is immutable")},
   232  			oldResourceSlice: validResourceSlice,
   233  			update: func(slice *resource.ResourceSlice) *resource.ResourceSlice {
   234  				slice.NodeName += "-updated"
   235  				return slice
   236  			},
   237  		},
   238  		"invalid-update-drivername": {
   239  			wantFailures:     field.ErrorList{field.Invalid(field.NewPath("driverName"), name+"-updated", "field is immutable")},
   240  			oldResourceSlice: validResourceSlice,
   241  			update: func(slice *resource.ResourceSlice) *resource.ResourceSlice {
   242  				slice.DriverName += "-updated"
   243  				return slice
   244  			},
   245  		},
   246  	}
   247  
   248  	for name, scenario := range scenarios {
   249  		t.Run(name, func(t *testing.T) {
   250  			scenario.oldResourceSlice.ResourceVersion = "1"
   251  			errs := ValidateResourceSliceUpdate(scenario.update(scenario.oldResourceSlice.DeepCopy()), scenario.oldResourceSlice)
   252  			assert.Equal(t, scenario.wantFailures, errs)
   253  		})
   254  	}
   255  }