k8s.io/kubernetes@v1.29.3/pkg/apis/resource/validation/validation_resourceclaimtemplate_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/pointer"
    28  )
    29  
    30  func testClaimTemplate(name, namespace string, spec resource.ResourceClaimSpec) *resource.ResourceClaimTemplate {
    31  	return &resource.ResourceClaimTemplate{
    32  		ObjectMeta: metav1.ObjectMeta{
    33  			Name:      name,
    34  			Namespace: namespace,
    35  		},
    36  		Spec: resource.ResourceClaimTemplateSpec{
    37  			Spec: spec,
    38  		},
    39  	}
    40  }
    41  
    42  func TestValidateClaimTemplate(t *testing.T) {
    43  	validMode := resource.AllocationModeImmediate
    44  	invalidMode := resource.AllocationMode("invalid")
    45  	goodName := "foo"
    46  	badName := "!@#$%^"
    47  	goodNS := "ns"
    48  	goodClaimSpec := resource.ResourceClaimSpec{
    49  		ResourceClassName: goodName,
    50  		AllocationMode:    validMode,
    51  	}
    52  	now := metav1.Now()
    53  	badValue := "spaces not allowed"
    54  
    55  	scenarios := map[string]struct {
    56  		template     *resource.ResourceClaimTemplate
    57  		wantFailures field.ErrorList
    58  	}{
    59  		"good-claim": {
    60  			template: testClaimTemplate(goodName, goodNS, goodClaimSpec),
    61  		},
    62  		"missing-name": {
    63  			wantFailures: field.ErrorList{field.Required(field.NewPath("metadata", "name"), "name or generateName is required")},
    64  			template:     testClaimTemplate("", goodNS, goodClaimSpec),
    65  		},
    66  		"bad-name": {
    67  			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])?)*')")},
    68  			template:     testClaimTemplate(badName, goodNS, goodClaimSpec),
    69  		},
    70  		"missing-namespace": {
    71  			wantFailures: field.ErrorList{field.Required(field.NewPath("metadata", "namespace"), "")},
    72  			template:     testClaimTemplate(goodName, "", goodClaimSpec),
    73  		},
    74  		"generate-name": {
    75  			template: func() *resource.ResourceClaimTemplate {
    76  				template := testClaimTemplate(goodName, goodNS, goodClaimSpec)
    77  				template.GenerateName = "pvc-"
    78  				return template
    79  			}(),
    80  		},
    81  		"uid": {
    82  			template: func() *resource.ResourceClaimTemplate {
    83  				template := testClaimTemplate(goodName, goodNS, goodClaimSpec)
    84  				template.UID = "ac051fac-2ead-46d9-b8b4-4e0fbeb7455d"
    85  				return template
    86  			}(),
    87  		},
    88  		"resource-version": {
    89  			template: func() *resource.ResourceClaimTemplate {
    90  				template := testClaimTemplate(goodName, goodNS, goodClaimSpec)
    91  				template.ResourceVersion = "1"
    92  				return template
    93  			}(),
    94  		},
    95  		"generation": {
    96  			template: func() *resource.ResourceClaimTemplate {
    97  				template := testClaimTemplate(goodName, goodNS, goodClaimSpec)
    98  				template.Generation = 100
    99  				return template
   100  			}(),
   101  		},
   102  		"creation-timestamp": {
   103  			template: func() *resource.ResourceClaimTemplate {
   104  				template := testClaimTemplate(goodName, goodNS, goodClaimSpec)
   105  				template.CreationTimestamp = now
   106  				return template
   107  			}(),
   108  		},
   109  		"deletion-grace-period-seconds": {
   110  			template: func() *resource.ResourceClaimTemplate {
   111  				template := testClaimTemplate(goodName, goodNS, goodClaimSpec)
   112  				template.DeletionGracePeriodSeconds = pointer.Int64(10)
   113  				return template
   114  			}(),
   115  		},
   116  		"owner-references": {
   117  			template: func() *resource.ResourceClaimTemplate {
   118  				template := testClaimTemplate(goodName, goodNS, goodClaimSpec)
   119  				template.OwnerReferences = []metav1.OwnerReference{
   120  					{
   121  						APIVersion: "v1",
   122  						Kind:       "pod",
   123  						Name:       "foo",
   124  						UID:        "ac051fac-2ead-46d9-b8b4-4e0fbeb7455d",
   125  					},
   126  				}
   127  				return template
   128  			}(),
   129  		},
   130  		"finalizers": {
   131  			template: func() *resource.ResourceClaimTemplate {
   132  				template := testClaimTemplate(goodName, goodNS, goodClaimSpec)
   133  				template.Finalizers = []string{
   134  					"example.com/foo",
   135  				}
   136  				return template
   137  			}(),
   138  		},
   139  		"managed-fields": {
   140  			template: func() *resource.ResourceClaimTemplate {
   141  				template := testClaimTemplate(goodName, goodNS, goodClaimSpec)
   142  				template.ManagedFields = []metav1.ManagedFieldsEntry{
   143  					{
   144  						FieldsType: "FieldsV1",
   145  						Operation:  "Apply",
   146  						APIVersion: "apps/v1",
   147  						Manager:    "foo",
   148  					},
   149  				}
   150  				return template
   151  			}(),
   152  		},
   153  		"good-labels": {
   154  			template: func() *resource.ResourceClaimTemplate {
   155  				template := testClaimTemplate(goodName, goodNS, goodClaimSpec)
   156  				template.Labels = map[string]string{
   157  					"apps.kubernetes.io/name": "test",
   158  				}
   159  				return template
   160  			}(),
   161  		},
   162  		"bad-labels": {
   163  			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])?')")},
   164  			template: func() *resource.ResourceClaimTemplate {
   165  				template := testClaimTemplate(goodName, goodNS, goodClaimSpec)
   166  				template.Labels = map[string]string{
   167  					"hello-world": badValue,
   168  				}
   169  				return template
   170  			}(),
   171  		},
   172  		"good-annotations": {
   173  			template: func() *resource.ResourceClaimTemplate {
   174  				template := testClaimTemplate(goodName, goodNS, goodClaimSpec)
   175  				template.Annotations = map[string]string{
   176  					"foo": "bar",
   177  				}
   178  				return template
   179  			}(),
   180  		},
   181  		"bad-annotations": {
   182  			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]')")},
   183  			template: func() *resource.ResourceClaimTemplate {
   184  				template := testClaimTemplate(goodName, goodNS, goodClaimSpec)
   185  				template.Annotations = map[string]string{
   186  					badName: "hello world",
   187  				}
   188  				return template
   189  			}(),
   190  		},
   191  		"bad-classname": {
   192  			wantFailures: field.ErrorList{field.Invalid(field.NewPath("spec", "spec", "resourceClassName"), 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])?)*')")},
   193  			template: func() *resource.ResourceClaimTemplate {
   194  				template := testClaimTemplate(goodName, goodNS, goodClaimSpec)
   195  				template.Spec.Spec.ResourceClassName = badName
   196  				return template
   197  			}(),
   198  		},
   199  		"bad-mode": {
   200  			wantFailures: field.ErrorList{field.NotSupported(field.NewPath("spec", "spec", "allocationMode"), invalidMode, supportedAllocationModes.List())},
   201  			template: func() *resource.ResourceClaimTemplate {
   202  				template := testClaimTemplate(goodName, goodNS, goodClaimSpec)
   203  				template.Spec.Spec.AllocationMode = invalidMode
   204  				return template
   205  			}(),
   206  		},
   207  		"good-parameters": {
   208  			template: func() *resource.ResourceClaimTemplate {
   209  				template := testClaimTemplate(goodName, goodNS, goodClaimSpec)
   210  				template.Spec.Spec.ParametersRef = &resource.ResourceClaimParametersReference{
   211  					Kind: "foo",
   212  					Name: "bar",
   213  				}
   214  				return template
   215  			}(),
   216  		},
   217  		"missing-parameters-kind": {
   218  			wantFailures: field.ErrorList{field.Required(field.NewPath("spec", "spec", "parametersRef", "kind"), "")},
   219  			template: func() *resource.ResourceClaimTemplate {
   220  				template := testClaimTemplate(goodName, goodNS, goodClaimSpec)
   221  				template.Spec.Spec.ParametersRef = &resource.ResourceClaimParametersReference{
   222  					Name: "bar",
   223  				}
   224  				return template
   225  			}(),
   226  		},
   227  		"missing-parameters-name": {
   228  			wantFailures: field.ErrorList{field.Required(field.NewPath("spec", "spec", "parametersRef", "name"), "")},
   229  			template: func() *resource.ResourceClaimTemplate {
   230  				template := testClaimTemplate(goodName, goodNS, goodClaimSpec)
   231  				template.Spec.Spec.ParametersRef = &resource.ResourceClaimParametersReference{
   232  					Kind: "foo",
   233  				}
   234  				return template
   235  			}(),
   236  		},
   237  	}
   238  
   239  	for name, scenario := range scenarios {
   240  		t.Run(name, func(t *testing.T) {
   241  			errs := ValidateClaimTemplate(scenario.template)
   242  			assert.Equal(t, scenario.wantFailures, errs)
   243  		})
   244  	}
   245  }
   246  
   247  func TestValidateClaimTemplateUpdate(t *testing.T) {
   248  	name := "valid"
   249  	parameters := &resource.ResourceClaimParametersReference{
   250  		Kind: "foo",
   251  		Name: "bar",
   252  	}
   253  	validClaimTemplate := testClaimTemplate("foo", "ns", resource.ResourceClaimSpec{
   254  		ResourceClassName: name,
   255  		AllocationMode:    resource.AllocationModeImmediate,
   256  		ParametersRef:     parameters,
   257  	})
   258  
   259  	scenarios := map[string]struct {
   260  		oldClaimTemplate *resource.ResourceClaimTemplate
   261  		update           func(claim *resource.ResourceClaimTemplate) *resource.ResourceClaimTemplate
   262  		wantFailures     field.ErrorList
   263  	}{
   264  		"valid-no-op-update": {
   265  			oldClaimTemplate: validClaimTemplate,
   266  			update:           func(claim *resource.ResourceClaimTemplate) *resource.ResourceClaimTemplate { return claim },
   267  		},
   268  		"invalid-update-class": {
   269  			wantFailures: field.ErrorList{field.Invalid(field.NewPath("spec"), func() resource.ResourceClaimTemplateSpec {
   270  				spec := validClaimTemplate.Spec.DeepCopy()
   271  				spec.Spec.ResourceClassName += "2"
   272  				return *spec
   273  			}(), "field is immutable")},
   274  			oldClaimTemplate: validClaimTemplate,
   275  			update: func(template *resource.ResourceClaimTemplate) *resource.ResourceClaimTemplate {
   276  				template.Spec.Spec.ResourceClassName += "2"
   277  				return template
   278  			},
   279  		},
   280  		"invalid-update-remove-parameters": {
   281  			wantFailures: field.ErrorList{field.Invalid(field.NewPath("spec"), func() resource.ResourceClaimTemplateSpec {
   282  				spec := validClaimTemplate.Spec.DeepCopy()
   283  				spec.Spec.ParametersRef = nil
   284  				return *spec
   285  			}(), "field is immutable")},
   286  			oldClaimTemplate: validClaimTemplate,
   287  			update: func(template *resource.ResourceClaimTemplate) *resource.ResourceClaimTemplate {
   288  				template.Spec.Spec.ParametersRef = nil
   289  				return template
   290  			},
   291  		},
   292  		"invalid-update-mode": {
   293  			wantFailures: field.ErrorList{field.Invalid(field.NewPath("spec"), func() resource.ResourceClaimTemplateSpec {
   294  				spec := validClaimTemplate.Spec.DeepCopy()
   295  				spec.Spec.AllocationMode = resource.AllocationModeWaitForFirstConsumer
   296  				return *spec
   297  			}(), "field is immutable")},
   298  			oldClaimTemplate: validClaimTemplate,
   299  			update: func(template *resource.ResourceClaimTemplate) *resource.ResourceClaimTemplate {
   300  				template.Spec.Spec.AllocationMode = resource.AllocationModeWaitForFirstConsumer
   301  				return template
   302  			},
   303  		},
   304  	}
   305  
   306  	for name, scenario := range scenarios {
   307  		t.Run(name, func(t *testing.T) {
   308  			scenario.oldClaimTemplate.ResourceVersion = "1"
   309  			errs := ValidateClaimTemplateUpdate(scenario.update(scenario.oldClaimTemplate.DeepCopy()), scenario.oldClaimTemplate)
   310  			assert.Equal(t, scenario.wantFailures, errs)
   311  		})
   312  	}
   313  }