k8s.io/kubernetes@v1.31.0-alpha.0.0.20240520171757-56147500dadc/pkg/apis/resource/validation/validation_resourceclaimparameters_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 testResourceClaimParameters(name, namespace string, requests []resource.DriverRequests) *resource.ResourceClaimParameters {
    31  	return &resource.ResourceClaimParameters{
    32  		ObjectMeta: metav1.ObjectMeta{
    33  			Name:      name,
    34  			Namespace: namespace,
    35  		},
    36  		DriverRequests: requests,
    37  	}
    38  }
    39  
    40  var goodRequests []resource.DriverRequests
    41  
    42  func TestValidateResourceClaimParameters(t *testing.T) {
    43  	goodName := "foo"
    44  	badName := "!@#$%^"
    45  	badValue := "spaces not allowed"
    46  	now := metav1.Now()
    47  
    48  	scenarios := map[string]struct {
    49  		parameters   *resource.ResourceClaimParameters
    50  		wantFailures field.ErrorList
    51  	}{
    52  		"good": {
    53  			parameters: testResourceClaimParameters(goodName, goodName, goodRequests),
    54  		},
    55  		"missing-name": {
    56  			wantFailures: field.ErrorList{field.Required(field.NewPath("metadata", "name"), "name or generateName is required")},
    57  			parameters:   testResourceClaimParameters("", goodName, goodRequests),
    58  		},
    59  		"missing-namespace": {
    60  			wantFailures: field.ErrorList{field.Required(field.NewPath("metadata", "namespace"), "")},
    61  			parameters:   testResourceClaimParameters(goodName, "", goodRequests),
    62  		},
    63  		"bad-name": {
    64  			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])?)*')")},
    65  			parameters:   testResourceClaimParameters(badName, goodName, goodRequests),
    66  		},
    67  		"bad-namespace": {
    68  			wantFailures: field.ErrorList{field.Invalid(field.NewPath("metadata", "namespace"), badName, "a lowercase RFC 1123 label must consist of lower case alphanumeric characters or '-', and must start and end with an alphanumeric character (e.g. 'my-name',  or '123-abc', regex used for validation is '[a-z0-9]([-a-z0-9]*[a-z0-9])?')")},
    69  			parameters:   testResourceClaimParameters(goodName, badName, goodRequests),
    70  		},
    71  		"generate-name": {
    72  			parameters: func() *resource.ResourceClaimParameters {
    73  				parameters := testResourceClaimParameters(goodName, goodName, goodRequests)
    74  				parameters.GenerateName = "prefix-"
    75  				return parameters
    76  			}(),
    77  		},
    78  		"uid": {
    79  			parameters: func() *resource.ResourceClaimParameters {
    80  				parameters := testResourceClaimParameters(goodName, goodName, goodRequests)
    81  				parameters.UID = "ac051fac-2ead-46d9-b8b4-4e0fbeb7455d"
    82  				return parameters
    83  			}(),
    84  		},
    85  		"resource-version": {
    86  			parameters: func() *resource.ResourceClaimParameters {
    87  				parameters := testResourceClaimParameters(goodName, goodName, goodRequests)
    88  				parameters.ResourceVersion = "1"
    89  				return parameters
    90  			}(),
    91  		},
    92  		"generation": {
    93  			parameters: func() *resource.ResourceClaimParameters {
    94  				parameters := testResourceClaimParameters(goodName, goodName, goodRequests)
    95  				parameters.Generation = 100
    96  				return parameters
    97  			}(),
    98  		},
    99  		"creation-timestamp": {
   100  			parameters: func() *resource.ResourceClaimParameters {
   101  				parameters := testResourceClaimParameters(goodName, goodName, goodRequests)
   102  				parameters.CreationTimestamp = now
   103  				return parameters
   104  			}(),
   105  		},
   106  		"deletion-grace-period-seconds": {
   107  			parameters: func() *resource.ResourceClaimParameters {
   108  				parameters := testResourceClaimParameters(goodName, goodName, goodRequests)
   109  				parameters.DeletionGracePeriodSeconds = ptr.To[int64](10)
   110  				return parameters
   111  			}(),
   112  		},
   113  		"owner-references": {
   114  			parameters: func() *resource.ResourceClaimParameters {
   115  				parameters := testResourceClaimParameters(goodName, goodName, goodRequests)
   116  				parameters.OwnerReferences = []metav1.OwnerReference{
   117  					{
   118  						APIVersion: "v1",
   119  						Kind:       "pod",
   120  						Name:       "foo",
   121  						UID:        "ac051fac-2ead-46d9-b8b4-4e0fbeb7455d",
   122  					},
   123  				}
   124  				return parameters
   125  			}(),
   126  		},
   127  		"finalizers": {
   128  			parameters: func() *resource.ResourceClaimParameters {
   129  				parameters := testResourceClaimParameters(goodName, goodName, goodRequests)
   130  				parameters.Finalizers = []string{
   131  					"example.com/foo",
   132  				}
   133  				return parameters
   134  			}(),
   135  		},
   136  		"managed-fields": {
   137  			parameters: func() *resource.ResourceClaimParameters {
   138  				parameters := testResourceClaimParameters(goodName, goodName, goodRequests)
   139  				parameters.ManagedFields = []metav1.ManagedFieldsEntry{
   140  					{
   141  						FieldsType: "FieldsV1",
   142  						Operation:  "Apply",
   143  						APIVersion: "apps/v1",
   144  						Manager:    "foo",
   145  					},
   146  				}
   147  				return parameters
   148  			}(),
   149  		},
   150  		"good-labels": {
   151  			parameters: func() *resource.ResourceClaimParameters {
   152  				parameters := testResourceClaimParameters(goodName, goodName, goodRequests)
   153  				parameters.Labels = map[string]string{
   154  					"apps.kubernetes.io/name": "test",
   155  				}
   156  				return parameters
   157  			}(),
   158  		},
   159  		"bad-labels": {
   160  			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])?')")},
   161  			parameters: func() *resource.ResourceClaimParameters {
   162  				parameters := testResourceClaimParameters(goodName, goodName, goodRequests)
   163  				parameters.Labels = map[string]string{
   164  					"hello-world": badValue,
   165  				}
   166  				return parameters
   167  			}(),
   168  		},
   169  		"good-annotations": {
   170  			parameters: func() *resource.ResourceClaimParameters {
   171  				parameters := testResourceClaimParameters(goodName, goodName, goodRequests)
   172  				parameters.Annotations = map[string]string{
   173  					"foo": "bar",
   174  				}
   175  				return parameters
   176  			}(),
   177  		},
   178  		"bad-annotations": {
   179  			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]')")},
   180  			parameters: func() *resource.ResourceClaimParameters {
   181  				parameters := testResourceClaimParameters(goodName, goodName, goodRequests)
   182  				parameters.Annotations = map[string]string{
   183  					badName: "hello world",
   184  				}
   185  				return parameters
   186  			}(),
   187  		},
   188  
   189  		"empty-model": {
   190  			wantFailures: field.ErrorList{field.Required(field.NewPath("driverRequests").Index(0).Child("requests").Index(0), "exactly one structured model field must be set")},
   191  			parameters: func() *resource.ResourceClaimParameters {
   192  				parameters := testResourceClaimParameters(goodName, goodName, goodRequests)
   193  				parameters.DriverRequests = []resource.DriverRequests{{DriverName: goodName, Requests: []resource.ResourceRequest{{}}}}
   194  				return parameters
   195  			}(),
   196  		},
   197  
   198  		"empty-requests": {
   199  			wantFailures: field.ErrorList{field.Required(field.NewPath("driverRequests").Index(0).Child("requests"), "empty entries with no requests are not allowed")},
   200  			parameters: func() *resource.ResourceClaimParameters {
   201  				parameters := testResourceClaimParameters(goodName, goodName, goodRequests)
   202  				parameters.DriverRequests = []resource.DriverRequests{{DriverName: goodName}}
   203  				return parameters
   204  			}(),
   205  		},
   206  
   207  		"invalid-driver": {
   208  			wantFailures: field.ErrorList{field.Invalid(field.NewPath("driverRequests").Index(1).Child("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])?)*')")},
   209  			parameters: func() *resource.ResourceClaimParameters {
   210  				parameters := testResourceClaimParameters(goodName, goodName, goodRequests)
   211  				parameters.DriverRequests = []resource.DriverRequests{
   212  					{
   213  						DriverName: goodName,
   214  						Requests: []resource.ResourceRequest{
   215  							{
   216  								ResourceRequestModel: resource.ResourceRequestModel{
   217  									NamedResources: &resource.NamedResourcesRequest{Selector: "true"},
   218  								},
   219  							},
   220  						},
   221  					},
   222  					{
   223  						DriverName: badName,
   224  						Requests: []resource.ResourceRequest{
   225  							{
   226  								ResourceRequestModel: resource.ResourceRequestModel{
   227  									NamedResources: &resource.NamedResourcesRequest{Selector: "true"},
   228  								},
   229  							},
   230  						},
   231  					},
   232  				}
   233  				return parameters
   234  			}(),
   235  		},
   236  
   237  		"duplicate-driver": {
   238  			wantFailures: field.ErrorList{field.Duplicate(field.NewPath("driverRequests").Index(1).Child("driverName"), goodName)},
   239  			parameters: func() *resource.ResourceClaimParameters {
   240  				parameters := testResourceClaimParameters(goodName, goodName, goodRequests)
   241  				parameters.DriverRequests = []resource.DriverRequests{
   242  					{
   243  						DriverName: goodName,
   244  						Requests: []resource.ResourceRequest{
   245  							{
   246  								ResourceRequestModel: resource.ResourceRequestModel{
   247  									NamedResources: &resource.NamedResourcesRequest{Selector: "true"},
   248  								},
   249  							},
   250  						},
   251  					},
   252  					{
   253  						DriverName: goodName,
   254  						Requests: []resource.ResourceRequest{
   255  							{
   256  								ResourceRequestModel: resource.ResourceRequestModel{
   257  									NamedResources: &resource.NamedResourcesRequest{Selector: "true"},
   258  								},
   259  							},
   260  						},
   261  					},
   262  				}
   263  				return parameters
   264  			}(),
   265  		},
   266  	}
   267  
   268  	for name, scenario := range scenarios {
   269  		t.Run(name, func(t *testing.T) {
   270  			errs := ValidateResourceClaimParameters(scenario.parameters)
   271  			assert.Equal(t, scenario.wantFailures, errs)
   272  		})
   273  	}
   274  }
   275  
   276  func TestValidateResourceClaimParametersUpdate(t *testing.T) {
   277  	name := "valid"
   278  	validResourceClaimParameters := testResourceClaimParameters(name, name, nil)
   279  
   280  	scenarios := map[string]struct {
   281  		oldResourceClaimParameters *resource.ResourceClaimParameters
   282  		update                     func(claim *resource.ResourceClaimParameters) *resource.ResourceClaimParameters
   283  		wantFailures               field.ErrorList
   284  	}{
   285  		"valid-no-op-update": {
   286  			oldResourceClaimParameters: validResourceClaimParameters,
   287  			update:                     func(claim *resource.ResourceClaimParameters) *resource.ResourceClaimParameters { return claim },
   288  		},
   289  		"invalid-name-update": {
   290  			oldResourceClaimParameters: validResourceClaimParameters,
   291  			update: func(claim *resource.ResourceClaimParameters) *resource.ResourceClaimParameters {
   292  				claim.Name += "-update"
   293  				return claim
   294  			},
   295  			wantFailures: field.ErrorList{field.Invalid(field.NewPath("metadata", "name"), name+"-update", "field is immutable")},
   296  		},
   297  	}
   298  
   299  	for name, scenario := range scenarios {
   300  		t.Run(name, func(t *testing.T) {
   301  			scenario.oldResourceClaimParameters.ResourceVersion = "1"
   302  			errs := ValidateResourceClaimParametersUpdate(scenario.update(scenario.oldResourceClaimParameters.DeepCopy()), scenario.oldResourceClaimParameters)
   303  			assert.Equal(t, scenario.wantFailures, errs)
   304  		})
   305  	}
   306  }