...

Source file src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema/defaulting/validation_test.go

Documentation: k8s.io/apiextensions-apiserver/pkg/apiserver/schema/defaulting

     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 defaulting
    18  
    19  import (
    20  	"context"
    21  	"strings"
    22  	"testing"
    23  
    24  	"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions"
    25  	structuralschema "k8s.io/apiextensions-apiserver/pkg/apiserver/schema"
    26  	apiextensionsfeatures "k8s.io/apiextensions-apiserver/pkg/features"
    27  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    28  	"k8s.io/apimachinery/pkg/util/validation/field"
    29  	utilfeature "k8s.io/apiserver/pkg/util/feature"
    30  	"k8s.io/component-base/featuregate"
    31  	featuregatetesting "k8s.io/component-base/featuregate/testing"
    32  	"k8s.io/utils/ptr"
    33  )
    34  
    35  func jsonPtr(x interface{}) *apiextensions.JSON {
    36  	ret := apiextensions.JSON(x)
    37  	return &ret
    38  }
    39  
    40  func TestDefaultValidationWithCostBudget(t *testing.T) {
    41  	tests := []struct {
    42  		name     string
    43  		input    apiextensions.CustomResourceValidation
    44  		features []featuregate.Feature
    45  	}{
    46  		{
    47  			name: "default cel validation",
    48  			input: apiextensions.CustomResourceValidation{
    49  				OpenAPIV3Schema: &apiextensions.JSONSchemaProps{
    50  					Type: "object",
    51  					Properties: map[string]apiextensions.JSONSchemaProps{
    52  						"embedded": {
    53  							Type: "object",
    54  							Properties: map[string]apiextensions.JSONSchemaProps{
    55  								"metadata": {
    56  									Type:              "object",
    57  									XEmbeddedResource: true,
    58  									Properties: map[string]apiextensions.JSONSchemaProps{
    59  										"name": {
    60  											Type: "string",
    61  											XValidations: apiextensions.ValidationRules{
    62  												{
    63  													Rule: "self == 'singleton'",
    64  												},
    65  											},
    66  											Default: jsonPtr("singleton"),
    67  										},
    68  									},
    69  								},
    70  							},
    71  						},
    72  						"value": {
    73  							Type: "string",
    74  							XValidations: apiextensions.ValidationRules{
    75  								{
    76  									Rule: "self.startsWith('kube')",
    77  								},
    78  							},
    79  							Default: jsonPtr("kube-everything"),
    80  						},
    81  						"object": {
    82  							Type: "object",
    83  							Properties: map[string]apiextensions.JSONSchemaProps{
    84  								"field1": {
    85  									Type: "integer",
    86  								},
    87  								"field2": {
    88  									Type: "integer",
    89  								},
    90  							},
    91  							XValidations: apiextensions.ValidationRules{
    92  								{
    93  									Rule: "self.field1 < self.field2",
    94  								},
    95  							},
    96  							Default: jsonPtr(map[string]interface{}{"field1": 1, "field2": 2}),
    97  						},
    98  					},
    99  				},
   100  			},
   101  		},
   102  	}
   103  
   104  	for _, tt := range tests {
   105  		ctx := context.TODO()
   106  		t.Run(tt.name, func(t *testing.T) {
   107  			for _, f := range tt.features {
   108  				defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, f, true)()
   109  			}
   110  
   111  			schema := tt.input.OpenAPIV3Schema
   112  			ss, err := structuralschema.NewStructural(schema)
   113  			if err != nil {
   114  				t.Errorf("unexpected error: %v", err)
   115  			}
   116  
   117  			f := NewRootObjectFunc().WithTypeMeta(metav1.TypeMeta{APIVersion: "validation/v1", Kind: "Validation"})
   118  
   119  			// cost budget is large enough to pass all validation rules
   120  			allErrs, err, _ := validate(ctx, field.NewPath("test"), ss, ss, f, false, false, 10)
   121  			if err != nil {
   122  				t.Errorf("unexpected error: %v", err)
   123  			}
   124  
   125  			for _, valErr := range allErrs {
   126  				t.Errorf("unexpected error: %v", valErr)
   127  			}
   128  
   129  			// cost budget exceeded for the first validation rule
   130  			allErrs, err, _ = validate(ctx, field.NewPath("test"), ss, ss, f, false, false, 0)
   131  			meet := 0
   132  			for _, er := range allErrs {
   133  				if er.Type == field.ErrorTypeInvalid && strings.Contains(er.Error(), "validation failed due to running out of cost budget, no further validation rules will be run") {
   134  					meet += 1
   135  				}
   136  			}
   137  			if meet != 1 {
   138  				t.Errorf("expected to get cost budget exceed error once but got %v cost budget exceed error", meet)
   139  			}
   140  			if err != nil {
   141  				t.Errorf("unexpected error: %v", err)
   142  			}
   143  
   144  			// cost budget exceeded for the last validation rule
   145  			allErrs, err, _ = validate(ctx, field.NewPath("test"), ss, ss, f, false, false, 9)
   146  			meet = 0
   147  			for _, er := range allErrs {
   148  				if er.Type == field.ErrorTypeInvalid && strings.Contains(er.Error(), "validation failed due to running out of cost budget, no further validation rules will be run") {
   149  					meet += 1
   150  				}
   151  			}
   152  			if meet != 1 {
   153  				t.Errorf("expected to get cost budget exceed error once but got %v cost budget exceed error", meet)
   154  			}
   155  			if err != nil {
   156  				t.Errorf("unexpected error: %v", err)
   157  			}
   158  		})
   159  	}
   160  }
   161  
   162  func TestDefaultValidationWithOptionalOldSelf(t *testing.T) {
   163  	tests := []struct {
   164  		name   string
   165  		input  apiextensions.CustomResourceValidation
   166  		errors []string
   167  	}{
   168  		{
   169  			name: "invalid default",
   170  			input: apiextensions.CustomResourceValidation{
   171  				OpenAPIV3Schema: &apiextensions.JSONSchemaProps{
   172  					Type: "object",
   173  					Properties: map[string]apiextensions.JSONSchemaProps{
   174  						"defaultFailsRatcheting": {
   175  							Type:    "string",
   176  							Default: jsonPtr("default"),
   177  							XValidations: apiextensions.ValidationRules{
   178  								{
   179  									Rule:            "oldSelf.hasValue()",
   180  									OptionalOldSelf: ptr.To(true),
   181  									Message:         "foobarErrorMessage",
   182  								},
   183  							},
   184  						},
   185  					},
   186  				},
   187  			},
   188  			errors: []string{"foobarErrorMessage"},
   189  		},
   190  		{
   191  			name: "valid default",
   192  			input: apiextensions.CustomResourceValidation{
   193  				OpenAPIV3Schema: &apiextensions.JSONSchemaProps{
   194  					Type: "object",
   195  					Properties: map[string]apiextensions.JSONSchemaProps{
   196  						"defaultFailsRatcheting": {
   197  							Type:    "string",
   198  							Default: jsonPtr("default"),
   199  							XValidations: apiextensions.ValidationRules{
   200  								{
   201  									Rule:            "oldSelf.orValue(self) == self",
   202  									OptionalOldSelf: ptr.To(true),
   203  									Message:         "foobarErrorMessage",
   204  								},
   205  							},
   206  						},
   207  					},
   208  				},
   209  			},
   210  			errors: []string{},
   211  		},
   212  	}
   213  
   214  	for _, tt := range tests {
   215  		ctx := context.TODO()
   216  		t.Run(tt.name, func(t *testing.T) {
   217  			defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, apiextensionsfeatures.CRDValidationRatcheting, true)()
   218  			schema := tt.input.OpenAPIV3Schema
   219  			ss, err := structuralschema.NewStructural(schema)
   220  			if err != nil {
   221  				t.Errorf("unexpected error: %v", err)
   222  			}
   223  
   224  			f := NewRootObjectFunc().WithTypeMeta(metav1.TypeMeta{APIVersion: "validation/v1", Kind: "Validation"})
   225  
   226  			// cost budget is large enough to pass all validation rules
   227  			allErrs, err, _ := validate(ctx, field.NewPath("test"), ss, ss, f, false, false, 10)
   228  			if err != nil {
   229  				t.Errorf("unexpected error: %v", err)
   230  			}
   231  
   232  			for _, err := range allErrs {
   233  				found := false
   234  				for _, expected := range tt.errors {
   235  					if strings.Contains(err.Error(), expected) {
   236  						found = true
   237  						break
   238  					}
   239  				}
   240  				if !found {
   241  					t.Errorf("unexpected error: %v", err)
   242  				}
   243  			}
   244  
   245  			for _, expected := range tt.errors {
   246  				found := false
   247  				for _, err := range allErrs {
   248  					if strings.Contains(err.Error(), expected) {
   249  						found = true
   250  						break
   251  					}
   252  				}
   253  				if !found {
   254  					t.Errorf("expected error: %v", expected)
   255  				}
   256  			}
   257  
   258  		})
   259  	}
   260  }
   261  

View as plain text