...

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

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

     1  /*
     2  Copyright 2021 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 cel
    18  
    19  import (
    20  	"bytes"
    21  	"context"
    22  	"flag"
    23  	"fmt"
    24  	"math"
    25  	"strings"
    26  	"sync"
    27  	"testing"
    28  	"time"
    29  
    30  	"github.com/stretchr/testify/assert"
    31  	"github.com/stretchr/testify/require"
    32  
    33  	"k8s.io/klog/v2"
    34  	"k8s.io/kube-openapi/pkg/validation/strfmt"
    35  	"k8s.io/utils/ptr"
    36  
    37  	apiextensionsinternal "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions"
    38  	apiextensions "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
    39  	"k8s.io/apiextensions-apiserver/pkg/apiserver/schema"
    40  	"k8s.io/apiextensions-apiserver/pkg/apiserver/schema/cel/model"
    41  	apiextensionsfeatures "k8s.io/apiextensions-apiserver/pkg/features"
    42  	"k8s.io/apimachinery/pkg/util/validation/field"
    43  	"k8s.io/apimachinery/pkg/util/yaml"
    44  	celconfig "k8s.io/apiserver/pkg/apis/cel"
    45  	"k8s.io/apiserver/pkg/cel/common"
    46  	utilfeature "k8s.io/apiserver/pkg/util/feature"
    47  	"k8s.io/apiserver/pkg/warning"
    48  	featuregatetesting "k8s.io/component-base/featuregate/testing"
    49  )
    50  
    51  // TestValidationExpressions tests CEL integration with custom resource values and OpenAPIv3.
    52  func TestValidationExpressions(t *testing.T) {
    53  	tests := []struct {
    54  		name          string
    55  		schema        *schema.Structural
    56  		oldSchema     *schema.Structural
    57  		obj           interface{}
    58  		oldObj        interface{}
    59  		valid         []string
    60  		errors        map[string]string // rule -> string that error message must contain
    61  		costBudget    int64
    62  		isRoot        bool
    63  		expectSkipped bool
    64  	}{
    65  		// tests where val1 and val2 are equal but val3 is different
    66  		// equality, comparisons and type specific functions
    67  		{name: "integers",
    68  			// 1st obj and schema args are for "self.val1" field, 2nd for "self.val2" and so on.
    69  			obj:    objs(math.MaxInt64, math.MaxInt64, math.MaxInt32, math.MaxInt32, math.MaxInt64, math.MaxInt64),
    70  			schema: schemas(integerType, integerType, int32Type, int32Type, int64Type, int64Type),
    71  			valid: []string{
    72  				ValsEqualThemselvesAndDataLiteral("self.val1", "self.val2", fmt.Sprintf("%d", math.MaxInt64)),
    73  				ValsEqualThemselvesAndDataLiteral("self.val3", "self.val4", fmt.Sprintf("%d", math.MaxInt32)),
    74  				ValsEqualThemselvesAndDataLiteral("self.val5", "self.val6", fmt.Sprintf("%d", math.MaxInt64)),
    75  				"self.val1 == self.val6", // integer with no format is the same as int64
    76  				"type(self.val1) == int",
    77  				fmt.Sprintf("self.val3 + 1 == %d + 1", math.MaxInt32), // CEL integers are 64 bit
    78  				"type(self.val3) == int",
    79  				"type(self.val5) == int",
    80  			},
    81  			errors: map[string]string{
    82  				"self.val1 + 1 == 0": "integer overflow",
    83  				"self.val5 + 1 == 0": "integer overflow",
    84  				"1 / 0 == 1 / 0":     "division by zero",
    85  			},
    86  		},
    87  		{name: "numbers",
    88  			obj:    objs(math.MaxFloat64, math.MaxFloat64, math.MaxFloat32, math.MaxFloat32, math.MaxFloat64, math.MaxFloat64, int64(1)),
    89  			schema: schemas(numberType, numberType, floatType, floatType, doubleType, doubleType, doubleType),
    90  			valid: []string{
    91  				ValsEqualThemselvesAndDataLiteral("self.val1", "self.val2", fmt.Sprintf("%f", math.MaxFloat64)),
    92  				ValsEqualThemselvesAndDataLiteral("self.val3", "self.val4", fmt.Sprintf("%f", math.MaxFloat32)),
    93  				ValsEqualThemselvesAndDataLiteral("self.val5", "self.val6", fmt.Sprintf("%f", math.MaxFloat64)),
    94  				"self.val1 == self.val6", // number with no format is the same as float64
    95  				"type(self.val1) == double",
    96  				"type(self.val3) == double",
    97  				"type(self.val5) == double",
    98  
    99  				// Use a int64 value with a number openAPI schema type since float representations of whole numbers
   100  				// (e.g. 1.0, 0.0) can convert to int representations (e.g. 1, 0) in yaml to json translation, and
   101  				// then get parsed as int64s.
   102  				"type(self.val7) == double",
   103  				"self.val7 == 1.0",
   104  			},
   105  		},
   106  		{name: "numeric comparisons",
   107  			obj: objs(
   108  				int64(5),      // val1, integer type, integer value
   109  				int64(10),     // val2, integer type, integer value
   110  				int64(15),     // val3, integer type, integer value
   111  				float64(10.0), // val4, number type, parsed from decimal literal
   112  				float64(10.0), // val5, float type, parsed from decimal literal
   113  				float64(10.0), // val6, double type, parsed from decimal literal
   114  				int64(10),     // val7, number type, parsed from integer literal
   115  				int64(10),     // val8, float type, parsed from integer literal
   116  				int64(10),     // val9, double type, parsed from integer literal
   117  			),
   118  			schema: schemas(integerType, integerType, integerType, numberType, floatType, doubleType, numberType, floatType, doubleType),
   119  			valid: []string{
   120  				// xref: https://github.com/google/cel-spec/wiki/proposal-210
   121  
   122  				// compare integers with all float types
   123  				"double(self.val1) < self.val4",
   124  				"double(self.val1) <= self.val4",
   125  				"double(self.val2) <= self.val4",
   126  				"double(self.val2) == self.val4",
   127  				"double(self.val2) >= self.val4",
   128  				"double(self.val3) > self.val4",
   129  				"double(self.val3) >= self.val4",
   130  
   131  				"self.val1 < int(self.val4)",
   132  				"self.val2 == int(self.val4)",
   133  				"self.val3 > int(self.val4)",
   134  
   135  				"double(self.val1) < self.val5",
   136  				"double(self.val2) == self.val5",
   137  				"double(self.val3) > self.val5",
   138  
   139  				"self.val1 < int(self.val5)",
   140  				"self.val2 == int(self.val5)",
   141  				"self.val3 > int(self.val5)",
   142  
   143  				"double(self.val1) < self.val6",
   144  				"double(self.val2) == self.val6",
   145  				"double(self.val3) > self.val6",
   146  
   147  				"self.val1 < int(self.val6)",
   148  				"self.val2 == int(self.val6)",
   149  				"self.val3 > int(self.val6)",
   150  
   151  				// Also compare with float types backed by integer values,
   152  				// which is how integer literals are parsed from JSON for custom resources.
   153  				"double(self.val1) < self.val7",
   154  				"double(self.val2) == self.val7",
   155  				"double(self.val3) > self.val7",
   156  
   157  				"self.val1 < int(self.val7)",
   158  				"self.val2 == int(self.val7)",
   159  				"self.val3 > int(self.val7)",
   160  
   161  				"double(self.val1) < self.val8",
   162  				"double(self.val2) == self.val8",
   163  				"double(self.val3) > self.val8",
   164  
   165  				"self.val1 < int(self.val8)",
   166  				"self.val2 == int(self.val8)",
   167  				"self.val3 > int(self.val8)",
   168  
   169  				"double(self.val1) < self.val9",
   170  				"double(self.val2) == self.val9",
   171  				"double(self.val3) > self.val9",
   172  
   173  				"self.val1 < int(self.val9)",
   174  				"self.val2 == int(self.val9)",
   175  				"self.val3 > int(self.val9)",
   176  
   177  				// compare literal integers and floats
   178  				"double(5) < 10.0",
   179  				"double(10) == 10.0",
   180  				"double(15) > 10.0",
   181  
   182  				"5 < int(10.0)",
   183  				"10 == int(10.0)",
   184  				"15 > int(10.0)",
   185  
   186  				// compare integers with literal floats
   187  				"double(self.val1) < 10.0",
   188  				"double(self.val2) == 10.0",
   189  				"double(self.val3) > 10.0",
   190  
   191  				// Cross Type Numeric Comparisons: integers with all float types
   192  				"self.val1 < self.val4",
   193  				"self.val1 <= self.val4",
   194  				"self.val2 <= self.val4",
   195  				"self.val2 >= self.val4",
   196  				"self.val3 > self.val4",
   197  				"self.val3 >= self.val4",
   198  
   199  				"self.val1 < self.val4",
   200  				"self.val3 > self.val4",
   201  
   202  				"self.val1 < self.val5",
   203  				"self.val3 > self.val5",
   204  
   205  				"self.val1 < self.val5",
   206  				"self.val3 > self.val5",
   207  
   208  				"self.val1 < self.val6",
   209  				"self.val3 > self.val6",
   210  
   211  				"self.val1 < self.val6",
   212  				"self.val3 > self.val6",
   213  
   214  				// Cross Type Numeric Comparisons: float types backed by integer values,
   215  				// which is how integer literals are parsed from JSON for custom resources.
   216  				"self.val1 < self.val7",
   217  				"self.val3 > self.val7",
   218  
   219  				"self.val1 < int(self.val7)",
   220  				"self.val3 > int(self.val7)",
   221  
   222  				"self.val1 < self.val8",
   223  				"self.val3 > self.val8",
   224  
   225  				"self.val1 < self.val8",
   226  				"self.val3 > self.val8",
   227  
   228  				"self.val1 < self.val9",
   229  				"self.val3 > self.val9",
   230  
   231  				"self.val1 < self.val9",
   232  				"self.val3 > self.val9",
   233  
   234  				// Cross Type Numeric Comparisons: literal integers and floats
   235  				"5 < 10.0",
   236  				"15 > 10.0",
   237  
   238  				"5 < 10.0",
   239  				"15 > 10.0",
   240  
   241  				// Cross Type Numeric Comparisons: integers with literal floats
   242  				"self.val1 < 10.0",
   243  				"self.val3 > 10.0",
   244  			},
   245  		},
   246  		{name: "unicode strings",
   247  			obj:    objs("Rook takes πŸ‘‘", "Rook takes πŸ‘‘"),
   248  			schema: schemas(stringType, stringType),
   249  			valid: []string{
   250  				ValsEqualThemselvesAndDataLiteral("self.val1", "self.val2", "'Rook takes πŸ‘‘'"),
   251  				"self.val1.startsWith('Rook')",
   252  				"!self.val1.startsWith('knight')",
   253  				"self.val1.contains('takes')",
   254  				"!self.val1.contains('gives')",
   255  				"self.val1.endsWith('πŸ‘‘')",
   256  				"!self.val1.endsWith('pawn')",
   257  				"self.val1.matches('^[^0-9]*$')",
   258  				"!self.val1.matches('^[0-9]*$')",
   259  				"type(self.val1) == string",
   260  				"size(self.val1) == 12",
   261  
   262  				// string functions (https://github.com/google/cel-go/blob/v0.9.0/ext/strings.go)
   263  				"self.val1.charAt(3) == 'k'",
   264  				"self.val1.indexOf('o') == 1",
   265  				"self.val1.indexOf('o', 2) == 2",
   266  				"self.val1.replace(' ', 'x') == 'RookxtakesxπŸ‘‘'",
   267  				"self.val1.replace(' ', 'x', 1) == 'Rookxtakes πŸ‘‘'",
   268  				"self.val1.split(' ') == ['Rook', 'takes', 'πŸ‘‘']",
   269  				"self.val1.split(' ', 2) == ['Rook', 'takes πŸ‘‘']",
   270  				"self.val1.substring(5) == 'takes πŸ‘‘'",
   271  				"self.val1.substring(0, 4) == 'Rook'",
   272  				"self.val1.substring(4, 10).trim() == 'takes'",
   273  				"self.val1.upperAscii() == 'ROOK TAKES πŸ‘‘'",
   274  				"self.val1.lowerAscii() == 'rook takes πŸ‘‘'",
   275  
   276  				"'%d %s %f %s %s'.format([1, 'abc', 1.0, duration('1m'), timestamp('2000-01-01T00:00:00.000Z')]) == '1 abc 1.000000 60s 2000-01-01T00:00:00Z'",
   277  				"'%e'.format([3.14]) == '3.140000β€―Γ—β€―10⁰⁰'",
   278  				"'%o %o %o'.format([7, 8, 9]) == '7 10 11'",
   279  				"'%b %b %b'.format([7, 8, 9]) == '111 1000 1001'",
   280  			},
   281  			errors: map[string]string{
   282  				// Invalid regex with a string constant regex pattern is compile time error
   283  				"self.val1.matches(')')": "compile error: compilation failed: ERROR: <input>:1:19: invalid matches argument",
   284  			},
   285  		},
   286  		{name: "escaped strings",
   287  			obj:    objs("l1\nl2", "l1\nl2"),
   288  			schema: schemas(stringType, stringType),
   289  			valid: []string{
   290  				ValsEqualThemselvesAndDataLiteral("self.val1", "self.val2", "'l1\\nl2'"),
   291  				"self.val1 == '''l1\nl2'''",
   292  			},
   293  		},
   294  		{name: "bytes",
   295  			obj:    objs("QUI=", "QUI="),
   296  			schema: schemas(byteType, byteType),
   297  			valid: []string{
   298  				"self.val1 == self.val2",
   299  				"self.val1 == b'AB'",
   300  				"self.val1 == bytes('AB')",
   301  				"self.val1 == b'\\x41\\x42'",
   302  				"type(self.val1) == bytes",
   303  				"size(self.val1) == 2",
   304  			},
   305  		},
   306  		{name: "booleans",
   307  			obj:    objs(true, true, false, false),
   308  			schema: schemas(booleanType, booleanType, booleanType, booleanType),
   309  			valid: []string{
   310  				ValsEqualThemselvesAndDataLiteral("self.val1", "self.val2", "true"),
   311  				ValsEqualThemselvesAndDataLiteral("self.val3", "self.val4", "false"),
   312  				"self.val1 != self.val4",
   313  				"type(self.val1) == bool",
   314  			},
   315  		},
   316  		{name: "duration format",
   317  			obj:    objs("1h2m3s4ms", "1h2m3s4ms"),
   318  			schema: schemas(durationFormat, durationFormat),
   319  			valid: []string{
   320  				ValsEqualThemselvesAndDataLiteral("self.val1", "self.val2", "duration('1h2m3s4ms')"),
   321  				"self.val1 == duration('1h2m') + duration('3s4ms')",
   322  				"self.val1.getHours() == 1",
   323  				"self.val1.getMinutes() == 62",
   324  				"self.val1.getSeconds() == 3723",
   325  				"self.val1.getMilliseconds() == 3723004",
   326  				"type(self.val1) == google.protobuf.Duration",
   327  			},
   328  			errors: map[string]string{
   329  				"duration('1')":                      "compilation failed: ERROR: <input>:1:10: invalid duration argument",
   330  				"duration('1d')":                     "compilation failed: ERROR: <input>:1:10: invalid duration argument",
   331  				"duration('1us') < duration('1nns')": "compilation failed: ERROR: <input>:1:28: invalid duration argument",
   332  			},
   333  		},
   334  		{name: "date format",
   335  			obj:    objs("1997-07-16", "1997-07-16"),
   336  			schema: schemas(dateFormat, dateFormat),
   337  			valid: []string{
   338  				ValsEqualThemselvesAndDataLiteral("self.val1", "self.val2", "timestamp('1997-07-16T00:00:00.000Z')"),
   339  				"self.val1.getDate() == 16",
   340  				"self.val1.getMonth() == 06", // zero based indexing
   341  				"self.val1.getFullYear() == 1997",
   342  				"type(self.val1) == google.protobuf.Timestamp",
   343  			},
   344  		},
   345  		{name: "date-time format",
   346  			obj:    objs("2011-08-18T19:03:37.010000000+01:00", "2011-08-18T19:03:37.010000000+01:00"),
   347  			schema: schemas(dateTimeFormat, dateTimeFormat),
   348  			valid: []string{
   349  				ValsEqualThemselvesAndDataLiteral("self.val1", "self.val2", "timestamp('2011-08-18T19:03:37.010+01:00')"),
   350  				"self.val1 == timestamp('2011-08-18T00:00:00.000+01:00') + duration('19h3m37s10ms')",
   351  				"self.val1.getDate('01:00') == 18",
   352  				"self.val1.getMonth('01:00') == 7", // zero based indexing
   353  				"self.val1.getFullYear('01:00') == 2011",
   354  				"self.val1.getHours('01:00') == 19",
   355  				"self.val1.getMinutes('01:00') == 03",
   356  				"self.val1.getSeconds('01:00') == 37",
   357  				"self.val1.getMilliseconds('01:00') == 10",
   358  				"self.val1.getHours('UTC') == 18", // TZ in string is 1hr off of UTC
   359  				"type(self.val1) == google.protobuf.Timestamp",
   360  			},
   361  			errors: map[string]string{
   362  				"timestamp('1000-00-00T00:00:00Z')":  "compilation failed: ERROR: <input>:1:11: invalid timestamp",
   363  				"timestamp('1000-01-01T00:00:00ZZ')": "compilation failed: ERROR: <input>:1:11: invalid timestamp",
   364  				"timestamp(-62135596801)":            "compilation failed: ERROR: <input>:1:11: invalid timestamp",
   365  			},
   366  		},
   367  		{name: "enums",
   368  			obj: map[string]interface{}{"enumStr": "Pending"},
   369  			schema: objectTypePtr(map[string]schema.Structural{"enumStr": {
   370  				Generic: schema.Generic{
   371  					Type: "string",
   372  				},
   373  				ValueValidation: &schema.ValueValidation{
   374  					Enum: []schema.JSON{
   375  						{Object: "Pending"},
   376  						{Object: "Available"},
   377  						{Object: "Bound"},
   378  						{Object: "Released"},
   379  						{Object: "Failed"},
   380  					},
   381  				},
   382  			}}),
   383  			valid: []string{
   384  				"self.enumStr == 'Pending'",
   385  				"self.enumStr in ['Pending', 'Available']",
   386  			},
   387  		},
   388  		{name: "conversions",
   389  			obj:    objs(int64(10), 10.0, 10.49, 10.5, true, "10", "MTA=", "3723.004s", "1h2m3s4ms", "2011-08-18T19:03:37.01+01:00", "2011-08-18T19:03:37.01+01:00", "2011-08-18T00:00:00Z", "2011-08-18"),
   390  			schema: schemas(integerType, numberType, numberType, numberType, booleanType, stringType, byteType, stringType, durationFormat, stringType, dateTimeFormat, stringType, dateFormat),
   391  			valid: []string{
   392  				"int(self.val2) == self.val1",
   393  				"int(self.val3) == self.val1",
   394  				"int(self.val4) == self.val1",
   395  				"int(self.val6) == self.val1",
   396  				"double(self.val1) == self.val2",
   397  				"double(self.val6) == self.val2",
   398  				"bytes(self.val6) == self.val7",
   399  				"string(self.val1) == self.val6",
   400  				"string(self.val2) == '10'",
   401  				"string(self.val3) == '10.49'",
   402  				"string(self.val4) == '10.5'",
   403  				"string(self.val5) == 'true'",
   404  				"string(self.val7) == self.val6",
   405  				"duration(self.val8) == self.val9",
   406  				"string(self.val9) == self.val8",
   407  				"timestamp(self.val10) == self.val11",
   408  				"string(self.val11) == self.val10",
   409  				"timestamp(self.val12) == self.val13",
   410  				"string(self.val13) == self.val12",
   411  			},
   412  		},
   413  		{name: "lists",
   414  			obj:    objs([]interface{}{1, 2, 3}, []interface{}{1, 2, 3}),
   415  			schema: schemas(listType(&integerType), listType(&integerType)),
   416  			valid: []string{
   417  				ValsEqualThemselvesAndDataLiteral("self.val1", "self.val2", "[1, 2, 3]"),
   418  				"1 in self.val1",
   419  				"self.val2[0] in self.val1",
   420  				"!(0 in self.val1)",
   421  				"self.val1 + self.val2 == [1, 2, 3, 1, 2, 3]",
   422  				"self.val1 + [4, 5] == [1, 2, 3, 4, 5]",
   423  			},
   424  			errors: map[string]string{
   425  				// Mixed type lists are not allowed since we have HomogeneousAggregateLiterals enabled
   426  				"[1, 'a', false].filter(x, string(x) == 'a')": "compilation failed: ERROR: <input>:1:5: expected type 'int' but found 'string'",
   427  			},
   428  		},
   429  		{name: "string lists",
   430  			obj:    objs([]interface{}{"a", "b", "c"}),
   431  			schema: schemas(listType(&stringType)),
   432  			valid: []string{
   433  				// Join function
   434  				"self.val1.join('-') == 'a-b-c'",
   435  				"['a', 'b', 'c'].join('-') == 'a-b-c'",
   436  				"self.val1.join() == 'abc'",
   437  				"['a', 'b', 'c'].join() == 'abc'",
   438  			},
   439  		},
   440  		{name: "listSets",
   441  			obj:    objs([]interface{}{"a", "b", "c"}, []interface{}{"a", "c", "b"}),
   442  			schema: schemas(listSetType(&stringType), listSetType(&stringType)),
   443  			valid: []string{
   444  				// equal even though order is different
   445  				"self.val1 == ['c', 'b', 'a']",
   446  				"self.val1 == self.val2",
   447  				"'a' in self.val1",
   448  				"self.val2[0] in self.val1",
   449  				"!('x' in self.val1)",
   450  				"self.val1 + self.val2 == ['a', 'b', 'c']",
   451  				"self.val1 + ['c', 'd'] == ['a', 'b', 'c', 'd']",
   452  
   453  				// Join function
   454  				"self.val1.join('-') == 'a-b-c'",
   455  				"['a', 'b', 'c'].join('-') == 'a-b-c'",
   456  				"self.val1.join() == 'abc'",
   457  				"['a', 'b', 'c'].join() == 'abc'",
   458  
   459  				// CEL sets functions
   460  				"sets.contains(['a', 'b'], [])",
   461  				"sets.contains(['a', 'b'], ['b'])",
   462  				"!sets.contains(['a', 'b'], ['c'])",
   463  				"sets.equivalent([], [])",
   464  				"sets.equivalent(['c', 'b', 'a'], ['b', 'c', 'a'])",
   465  				"!sets.equivalent(['a', 'b'], ['b', 'c'])",
   466  				"sets.intersects(['a', 'b'], ['b', 'c'])",
   467  				"!sets.intersects([], [])",
   468  				"!sets.intersects(['a', 'b'], [])",
   469  				"!sets.intersects(['a', 'b'], ['c', 'd'])",
   470  
   471  				"sets.contains([1, 2], [2])",
   472  				"sets.contains([true, false], [false])",
   473  				"sets.contains([1.25, 1.5], [1.5])",
   474  				"sets.contains([{'a': 1}, {'b': 2}], [{'b': 2}])",
   475  				"sets.contains([[1, 2], [3, 4]], [[3, 4]])",
   476  				"sets.contains([timestamp('2000-01-01T00:00:00.000+01:00'), timestamp('2012-01-01T00:00:00.000+01:00')], [timestamp('2012-01-01T00:00:00.000+01:00')])",
   477  				"sets.contains([duration('1h'), duration('2h')], [duration('2h')])",
   478  
   479  				"sets.equivalent([1, 2], [1, 2])",
   480  				"sets.equivalent([true, false], [true, false])",
   481  				"sets.equivalent([1.25, 1.5], [1.25, 1.5])",
   482  				"sets.equivalent([{'a': 1}, {'b': 2}], [{'a': 1}, {'b': 2}])",
   483  				"sets.equivalent([[1, 2], [3, 4]], [[1, 2], [3, 4]])",
   484  				"sets.equivalent([timestamp('2012-01-01T00:00:00.000+01:00')], [timestamp('2012-01-01T00:00:00.000+01:00')])",
   485  				"sets.equivalent([duration('1h'), duration('2h')], [duration('1h'), duration('2h')])",
   486  
   487  				"sets.intersects([1, 2], [2])",
   488  				"sets.intersects([true, false], [false])",
   489  				"sets.intersects([1.25, 1.5], [1.5])",
   490  				"sets.intersects([{'a': 1}, {'b': 2}], [{'b': 2}])",
   491  				"sets.intersects([[1, 2], [3, 4]], [[3, 4]])",
   492  				"sets.intersects([timestamp('2000-01-01T00:00:00.000+01:00'), timestamp('2012-01-01T00:00:00.000+01:00')], [timestamp('2012-01-01T00:00:00.000+01:00')])",
   493  				"sets.intersects([duration('1h'), duration('2h')], [duration('2h')])",
   494  			},
   495  		},
   496  		{name: "listMaps",
   497  			obj: map[string]interface{}{
   498  				"objs": []interface{}{
   499  					[]interface{}{
   500  						map[string]interface{}{"k": "a", "v": "1"},
   501  						map[string]interface{}{"k": "b", "v": "2"},
   502  					},
   503  					[]interface{}{
   504  						map[string]interface{}{"k": "b", "v": "2"},
   505  						map[string]interface{}{"k": "a", "v": "1"},
   506  					},
   507  					[]interface{}{
   508  						map[string]interface{}{"k": "b", "v": "3"},
   509  						map[string]interface{}{"k": "a", "v": "1"},
   510  					},
   511  					[]interface{}{
   512  						map[string]interface{}{"k": "c", "v": "4"},
   513  					},
   514  				},
   515  			},
   516  			schema: objectTypePtr(map[string]schema.Structural{
   517  				"objs": listType(listMapTypePtr([]string{"k"}, objectTypePtr(map[string]schema.Structural{
   518  					"k": stringType,
   519  					"v": stringType,
   520  				}))),
   521  			}),
   522  			valid: []string{
   523  				"self.objs[0] == self.objs[1]",                // equal even though order is different
   524  				"self.objs[0] + self.objs[2] == self.objs[2]", // rhs overwrites lhs values
   525  				"self.objs[2] + self.objs[0] == self.objs[0]",
   526  
   527  				"self.objs[0] == [self.objs[0][0], self.objs[0][1]]", // equal against a declared list
   528  				"self.objs[0] == [self.objs[0][1], self.objs[0][0]]",
   529  
   530  				"self.objs[2] + [self.objs[0][0], self.objs[0][1]] == self.objs[0]", // concat against a declared list
   531  				"size(self.objs[0] + [self.objs[3][0]]) == 3",
   532  			},
   533  			errors: map[string]string{
   534  				"self.objs[0] == {'k': 'a', 'v': '1'}": "no matching overload for '_==_'", // objects cannot be compared against a data literal map
   535  			},
   536  		},
   537  		{name: "maps",
   538  			obj:    objs(map[string]interface{}{"k1": "a", "k2": "b"}, map[string]interface{}{"k2": "b", "k1": "a"}),
   539  			schema: schemas(mapType(&stringType), mapType(&stringType)),
   540  			valid: []string{
   541  				"self.val1 == self.val2", // equal even though order is different
   542  				"'k1' in self.val1",
   543  				"!('k3' in self.val1)",
   544  				"self.val1 == {'k1': 'a', 'k2': 'b'}",
   545  			},
   546  			errors: map[string]string{
   547  				// Mixed type maps are not allowed since we have HomogeneousAggregateLiterals enabled
   548  				"{'k1': 'a', 'k2': 1, 'k2': false}": "expected type 'string' but found 'int'",
   549  			},
   550  		},
   551  		{name: "objects",
   552  			obj: map[string]interface{}{
   553  				"objs": []interface{}{
   554  					map[string]interface{}{"f1": "a", "f2": "b"},
   555  					map[string]interface{}{"f1": "a", "f2": "b"},
   556  				},
   557  			},
   558  			schema: objectTypePtr(map[string]schema.Structural{
   559  				"objs": listType(objectTypePtr(map[string]schema.Structural{
   560  					"f1": stringType,
   561  					"f2": stringType,
   562  				})),
   563  			}),
   564  			valid: []string{
   565  				"self.objs[0] == self.objs[1]",
   566  			},
   567  			errors: map[string]string{
   568  				"self.objs[0] == {'f1': 'a', 'f2': 'b'}": "found no matching overload for '_==_'", // objects cannot be compared against a data literal map
   569  			},
   570  		},
   571  		{name: "object access",
   572  			obj: map[string]interface{}{
   573  				"a": map[string]interface{}{
   574  					"b": 1,
   575  					"d": nil,
   576  				},
   577  				"a1": map[string]interface{}{
   578  					"b1": map[string]interface{}{
   579  						"c1": 4,
   580  					},
   581  				},
   582  				"a3": map[string]interface{}{},
   583  			},
   584  			schema: objectTypePtr(map[string]schema.Structural{
   585  				"a": objectType(map[string]schema.Structural{
   586  					"b": integerType,
   587  					"c": integerType,
   588  					"d": withNullable(true, integerType),
   589  				}),
   590  				"a1": objectType(map[string]schema.Structural{
   591  					"b1": objectType(map[string]schema.Structural{
   592  						"c1": integerType,
   593  					}),
   594  					"d2": objectType(map[string]schema.Structural{
   595  						"e2": integerType,
   596  					}),
   597  				}),
   598  			}),
   599  			// https://github.com/google/cel-spec/blob/master/doc/langdef.md#field-selection
   600  			valid: []string{
   601  				"has(self.a.b)",
   602  				"!has(self.a.c)",
   603  				"!has(self.a.d)", // We treat null value fields the same as absent fields in CEL
   604  				"has(self.a1.b1.c1)",
   605  				"!(has(self.a1.d2) && has(self.a1.d2.e2))", // must check intermediate optional fields (see below no such key error for d2)
   606  				"!has(self.a1.d2)",
   607  			},
   608  			errors: map[string]string{
   609  				"has(self.a.z)":      "undefined field 'z'",             // may not reference undefined fields, not even with has
   610  				"self.a['b'] == 1":   "no matching overload for '_[_]'", // only allowed on maps, not objects
   611  				"has(self.a1.d2.e2)": "no such key: d2",                 // has only checks last element in path, when d2 is absent in value, this is an error
   612  			},
   613  		},
   614  		{name: "map access",
   615  			obj: map[string]interface{}{
   616  				"val": map[string]interface{}{
   617  					"b": 1,
   618  					"d": 2,
   619  				},
   620  			},
   621  			schema: objectTypePtr(map[string]schema.Structural{
   622  				"val": mapType(&integerType),
   623  			}),
   624  			valid: []string{
   625  				// idiomatic map access
   626  				"!('a' in self.val)",
   627  				"'b' in self.val",
   628  				"!('c' in self.val)",
   629  				"'d' in self.val",
   630  				// field selection also possible if map key is a valid CEL identifier
   631  				"!has(self.val.a)",
   632  				"has(self.val.b)",
   633  				"!has(self.val.c)",
   634  				"has(self.val.d)",
   635  				"self.val.all(k, self.val[k] > 0)",
   636  				"self.val.exists(k, self.val[k] > 1)",
   637  				"self.val.exists_one(k, self.val[k] == 2)",
   638  				"!self.val.exists(k, self.val[k] > 2)",
   639  				"!self.val.exists_one(k, self.val[k] > 0)",
   640  				"size(self.val) == 2",
   641  				"self.val.map(k, self.val[k]).exists(v, v == 1)",
   642  				"size(self.val.filter(k, self.val[k] > 1)) == 1",
   643  			},
   644  			errors: map[string]string{
   645  				"self.val['c'] == 1": "no such key: c",
   646  			},
   647  		},
   648  		{name: "listMap access",
   649  			obj: map[string]interface{}{
   650  				"listMap": []interface{}{
   651  					map[string]interface{}{"k": "a1", "v": "b1"},
   652  					map[string]interface{}{"k": "a2", "v": "b2"},
   653  					map[string]interface{}{"k": "a3", "v": "b3", "v2": "z"},
   654  				},
   655  			},
   656  			schema: objectTypePtr(map[string]schema.Structural{
   657  				"listMap": listMapType([]string{"k"}, objectTypePtr(map[string]schema.Structural{
   658  					"k":  stringType,
   659  					"v":  stringType,
   660  					"v2": stringType,
   661  				})),
   662  			}),
   663  			valid: []string{
   664  				"has(self.listMap[0].v)",
   665  				"self.listMap.all(m, m.k.startsWith('a'))",
   666  				"self.listMap.all(m, !has(m.v2) || m.v2 == 'z')",
   667  				"self.listMap.exists(m, m.k.endsWith('1'))",
   668  				"self.listMap.exists_one(m, m.k == 'a3')",
   669  				"!self.listMap.all(m, m.k.endsWith('1'))",
   670  				"!self.listMap.exists(m, m.v == 'x')",
   671  				"!self.listMap.exists_one(m, m.k.startsWith('a'))",
   672  				"size(self.listMap.filter(m, m.k == 'a1')) == 1",
   673  				"self.listMap.exists(m, m.k == 'a1' && m.v == 'b1')",
   674  				"self.listMap.map(m, m.v).exists(v, v == 'b1')",
   675  
   676  				// test comprehensions where the field used in predicates is unset on all but one of the elements:
   677  				// - with has checks:
   678  
   679  				"self.listMap.exists(m, has(m.v2) && m.v2 == 'z')",
   680  				"!self.listMap.all(m, has(m.v2) && m.v2 != 'z')",
   681  				"self.listMap.exists_one(m, has(m.v2) && m.v2 == 'z')",
   682  				"self.listMap.filter(m, has(m.v2) && m.v2 == 'z').size() == 1",
   683  				// undocumented overload of map that takes a filter argument. This is the same as .filter().map()
   684  				"self.listMap.map(m, has(m.v2) && m.v2 == 'z', m.v2).size() == 1",
   685  				"self.listMap.filter(m, has(m.v2) && m.v2 == 'z').map(m, m.v2).size() == 1",
   686  				// - without has checks:
   687  
   688  				// all() and exists() macros ignore errors from predicates so long as the condition holds for at least one element
   689  				"self.listMap.exists(m, m.v2 == 'z')",
   690  				"!self.listMap.all(m, m.v2 != 'z')",
   691  			},
   692  			errors: map[string]string{
   693  				// test comprehensions where the field used in predicates is unset on all but one of the elements: (error cases)
   694  				// - without has checks:
   695  
   696  				// if all() predicate evaluates to false or error for all elements, any error encountered is raised
   697  				"self.listMap.all(m, m.v2 == 'z')": "no such key: v2",
   698  				// exists one() is stricter than map() or exists(), it requires exactly one predicate evaluate to true and the rest
   699  				// evaluate to false, any error encountered is raised
   700  				"self.listMap.exists_one(m, m.v2 == 'z')": "no such key: v2",
   701  				// filter and map raise any error encountered
   702  				"self.listMap.filter(m, m.v2 == 'z').size() == 1": "no such key: v2",
   703  				"self.listMap.map(m, m.v2).size() == 1":           "no such key: v2",
   704  				// undocumented overload of map that takes a filter argument. This is the same as .filter().map()
   705  				"self.listMap.map(m, m.v2 == 'z', m.v2).size() == 1": "no such key: v2",
   706  			},
   707  		},
   708  		{name: "list access",
   709  			obj: map[string]interface{}{
   710  				"array": []interface{}{1, 1, 2, 2, 3, 3, 4, 5},
   711  			},
   712  			schema: objectTypePtr(map[string]schema.Structural{
   713  				"array": listType(&integerType),
   714  			}),
   715  			valid: []string{
   716  				"2 in self.array",
   717  				"self.array.all(e, e > 0)",
   718  				"self.array.exists(e, e > 2)",
   719  				"self.array.exists_one(e, e > 4)",
   720  				"!self.array.all(e, e < 2)",
   721  				"!self.array.exists(e, e < 0)",
   722  				"!self.array.exists_one(e, e == 2)",
   723  				"self.array.all(e, e < 100)",
   724  				"size(self.array.filter(e, e%2 == 0)) == 3",
   725  				"self.array.map(e, e * 20).filter(e, e > 50).exists(e, e == 60)",
   726  				"size(self.array) == 8",
   727  			},
   728  			errors: map[string]string{
   729  				"self.array[100] == 0": "index out of bounds: 100",
   730  			},
   731  		},
   732  		{name: "listSet access",
   733  			obj: map[string]interface{}{
   734  				"set": []interface{}{1, 2, 3, 4, 5},
   735  			},
   736  			schema: objectTypePtr(map[string]schema.Structural{
   737  				"set": listType(&integerType),
   738  			}),
   739  			valid: []string{
   740  				"3 in self.set",
   741  				"self.set.all(e, e > 0)",
   742  				"self.set.exists(e, e > 3)",
   743  				"self.set.exists_one(e, e == 3)",
   744  				"!self.set.all(e, e < 3)",
   745  				"!self.set.exists(e, e < 0)",
   746  				"!self.set.exists_one(e, e > 3)",
   747  				"self.set.all(e, e < 10)",
   748  				"size(self.set.filter(e, e%2 == 0)) == 2",
   749  				"self.set.map(e, e * 20).filter(e, e > 50).exists_one(e, e == 60)",
   750  				"size(self.set) == 5",
   751  			},
   752  		},
   753  		{name: "typemeta and objectmeta access specified",
   754  			obj: map[string]interface{}{
   755  				"apiVersion": "v1",
   756  				"kind":       "Pod",
   757  				"metadata": map[string]interface{}{
   758  					"name":         "foo",
   759  					"generateName": "pickItForMe",
   760  					"namespace":    "xyz",
   761  				},
   762  			},
   763  			schema: objectTypePtr(map[string]schema.Structural{
   764  				"kind":       stringType,
   765  				"apiVersion": stringType,
   766  				"metadata": objectType(map[string]schema.Structural{
   767  					"name":         stringType,
   768  					"generateName": stringType,
   769  				}),
   770  			}),
   771  			valid: []string{
   772  				"self.kind == 'Pod'",
   773  				"self.apiVersion == 'v1'",
   774  				"self.metadata.name == 'foo'",
   775  				"self.metadata.generateName == 'pickItForMe'",
   776  			},
   777  			errors: map[string]string{
   778  				"has(self.metadata.namespace)": "undefined field 'namespace'",
   779  			},
   780  		},
   781  		{name: "typemeta and objectmeta access not specified",
   782  			isRoot: true,
   783  			obj: map[string]interface{}{
   784  				"apiVersion": "v1",
   785  				"kind":       "Pod",
   786  				"metadata": map[string]interface{}{
   787  					"name":         "foo",
   788  					"generateName": "pickItForMe",
   789  					"namespace":    "xyz",
   790  				},
   791  				"spec": map[string]interface{}{
   792  					"field1": "a",
   793  				},
   794  			},
   795  			schema: objectTypePtr(map[string]schema.Structural{
   796  				"spec": objectType(map[string]schema.Structural{
   797  					"field1": stringType,
   798  				}),
   799  			}),
   800  			valid: []string{
   801  				"self.kind == 'Pod'",
   802  				"self.apiVersion == 'v1'",
   803  				"self.metadata.name == 'foo'",
   804  				"self.metadata.generateName == 'pickItForMe'",
   805  				"self.spec.field1 == 'a'",
   806  			},
   807  			errors: map[string]string{
   808  				"has(self.metadata.namespace)": "undefined field 'namespace'",
   809  			},
   810  		},
   811  
   812  		// Kubernetes special types
   813  		{name: "embedded object",
   814  			obj: map[string]interface{}{
   815  				"embedded": map[string]interface{}{
   816  					"apiVersion": "v1",
   817  					"kind":       "Pod",
   818  					"metadata": map[string]interface{}{
   819  						"name":         "foo",
   820  						"generateName": "pickItForMe",
   821  						"namespace":    "xyz",
   822  					},
   823  					"spec": map[string]interface{}{
   824  						"field1": "a",
   825  					},
   826  				},
   827  			},
   828  			schema: objectTypePtr(map[string]schema.Structural{
   829  				"embedded": {
   830  					Generic: schema.Generic{Type: "object"},
   831  					Extensions: schema.Extensions{
   832  						XEmbeddedResource: true,
   833  					},
   834  				},
   835  			}),
   836  			valid: []string{
   837  				// 'kind', 'apiVersion', 'metadata.name' and 'metadata.generateName' are always accessible
   838  				// even if not specified in the schema.
   839  				"self.embedded.kind == 'Pod'",
   840  				"self.embedded.apiVersion == 'v1'",
   841  				"self.embedded.metadata.name == 'foo'",
   842  				"self.embedded.metadata.generateName == 'pickItForMe'",
   843  			},
   844  			// only field declared in the schema can be field selected in CEL expressions
   845  			errors: map[string]string{
   846  				"has(self.embedded.metadata.namespace)": "undefined field 'namespace'",
   847  				"has(self.embedded.spec)":               "undefined field 'spec'",
   848  			},
   849  		},
   850  		{name: "embedded object with properties",
   851  			obj: map[string]interface{}{
   852  				"embedded": map[string]interface{}{
   853  					"apiVersion": "v1",
   854  					"kind":       "Pod",
   855  					"metadata": map[string]interface{}{
   856  						"name":         "foo",
   857  						"generateName": "pickItForMe",
   858  						"namespace":    "xyz",
   859  					},
   860  					"spec": map[string]interface{}{
   861  						"field1": "a",
   862  					},
   863  				},
   864  			},
   865  			schema: objectTypePtr(map[string]schema.Structural{
   866  				"embedded": {
   867  					Generic: schema.Generic{Type: "object"},
   868  					Extensions: schema.Extensions{
   869  						XEmbeddedResource: true,
   870  					},
   871  					Properties: map[string]schema.Structural{
   872  						"kind":       stringType,
   873  						"apiVersion": stringType,
   874  						"metadata": objectType(map[string]schema.Structural{
   875  							"name":         stringType,
   876  							"generateName": stringType,
   877  						}),
   878  						"spec": objectType(map[string]schema.Structural{
   879  							"field1": stringType,
   880  						}),
   881  					},
   882  				},
   883  			}),
   884  			valid: []string{
   885  				// in this case 'kind', 'apiVersion', 'metadata.name' and 'metadata.generateName' are specified in the
   886  				// schema, but they would be accessible even if they were not
   887  				"self.embedded.kind == 'Pod'",
   888  				"self.embedded.apiVersion == 'v1'",
   889  				"self.embedded.metadata.name == 'foo'",
   890  				"self.embedded.metadata.generateName == 'pickItForMe'",
   891  				// the specified embedded fields are accessible
   892  				"self.embedded.spec.field1 == 'a'",
   893  			},
   894  			errors: map[string]string{
   895  				// only name and generateName are accessible on metadata
   896  				"has(self.embedded.metadata.namespace)": "undefined field 'namespace'",
   897  			},
   898  		},
   899  		{name: "embedded object with preserve unknown",
   900  			obj: map[string]interface{}{
   901  				"embedded": map[string]interface{}{
   902  					"apiVersion": "v1",
   903  					"kind":       "Pod",
   904  					"metadata": map[string]interface{}{
   905  						"name":         "foo",
   906  						"generateName": "pickItForMe",
   907  						"namespace":    "xyz",
   908  					},
   909  					"spec": map[string]interface{}{
   910  						"field1": "a",
   911  					},
   912  				},
   913  			},
   914  			schema: objectTypePtr(map[string]schema.Structural{
   915  				"embedded": {
   916  					Generic: schema.Generic{Type: "object"},
   917  					Extensions: schema.Extensions{
   918  						XPreserveUnknownFields: true,
   919  						XEmbeddedResource:      true,
   920  					},
   921  				},
   922  			}),
   923  			valid: []string{
   924  				// 'kind', 'apiVersion', 'metadata.name' and 'metadata.generateName' are always accessible
   925  				// even if not specified in the schema, regardless of if x-kubernetes-preserve-unknown-fields is set.
   926  				"self.embedded.kind == 'Pod'",
   927  				"self.embedded.apiVersion == 'v1'",
   928  				"self.embedded.metadata.name == 'foo'",
   929  				"self.embedded.metadata.generateName == 'pickItForMe'",
   930  
   931  				// the object exists
   932  				"has(self.embedded)",
   933  			},
   934  			// only field declared in the schema can be field selected in CEL expressions, regardless of if
   935  			// x-kubernetes-preserve-unknown-fields is set.
   936  			errors: map[string]string{
   937  				"has(self.embedded.spec)": "undefined field 'spec'",
   938  			},
   939  		},
   940  		{name: "string in intOrString",
   941  			obj: map[string]interface{}{
   942  				"something": "25%",
   943  			},
   944  			schema: objectTypePtr(map[string]schema.Structural{
   945  				"something": intOrStringType(),
   946  			}),
   947  			valid: []string{
   948  				// In Kubernetes 1.24 and later, the CEL type returns false for an int-or-string comparison against the
   949  				// other type, making it safe to write validation rules like:
   950  				"self.something == '25%'",
   951  				"self.something != 1",
   952  				"self.something == 1 || self.something == '25%'",
   953  				"self.something == '25%' || self.something == 1",
   954  
   955  				// In Kubernetes 1.23 and earlier, all int-or-string access must be guarded by a type check to prevent
   956  				// a runtime error attempting an equality check between string and int types.
   957  				"type(self.something) == string && self.something == '25%'",
   958  				"type(self.something) == int ? self.something == 1 : self.something == '25%'",
   959  
   960  				// Because the type is dynamic it receives no type checking, and evaluates to false when compared to
   961  				// other types at runtime.
   962  				"self.something != ['anything']",
   963  			},
   964  		},
   965  		{name: "int in intOrString",
   966  			obj: map[string]interface{}{
   967  				"something": int64(1),
   968  			},
   969  			schema: objectTypePtr(map[string]schema.Structural{
   970  				"something": intOrStringType(),
   971  			}),
   972  			valid: []string{
   973  				// In Kubernetes 1.24 and later, the CEL type returns false for an int-or-string comparison against the
   974  				// other type, making it safe to write validation rules like:
   975  				"self.something == 1",
   976  				"self.something != 'some string'",
   977  				"self.something == 1 || self.something == '25%'",
   978  				"self.something == '25%' || self.something == 1",
   979  
   980  				// In Kubernetes 1.23 and earlier, all int-or-string access must be guarded by a type check to prevent
   981  				// a runtime error attempting an equality check between string and int types.
   982  				"type(self.something) == int && self.something == 1",
   983  				"type(self.something) == int ? self.something == 1 : self.something == '25%'",
   984  
   985  				// Because the type is dynamic it receives no type checking, and evaluates to false when compared to
   986  				// other types at runtime.
   987  				"self.something != ['anything']",
   988  			},
   989  		},
   990  		{name: "null in intOrString",
   991  			obj: map[string]interface{}{
   992  				"something": nil,
   993  			},
   994  			schema: objectTypePtr(map[string]schema.Structural{
   995  				"something": withNullable(true, intOrStringType()),
   996  			}),
   997  			valid: []string{
   998  				"!has(self.something)",
   999  			},
  1000  			errors: map[string]string{
  1001  				"type(self.something) == int": "no such key",
  1002  			},
  1003  		},
  1004  		{name: "percent comparison using intOrString",
  1005  			obj: map[string]interface{}{
  1006  				"min":       "50%",
  1007  				"current":   5,
  1008  				"available": 10,
  1009  			},
  1010  			schema: objectTypePtr(map[string]schema.Structural{
  1011  				"min":       intOrStringType(),
  1012  				"current":   integerType,
  1013  				"available": integerType,
  1014  			}),
  1015  			valid: []string{
  1016  				// validate that if 'min' is a string that it is a percentage
  1017  				`type(self.min) == string && self.min.matches(r'(\d+(\.\d+)?%)')`,
  1018  				// validate that 'min' can be either a exact value minimum, or a minimum as a percentage of 'available'
  1019  				"type(self.min) == int ? self.current <= self.min : double(self.current) / double(self.available) >= double(self.min.replace('%', '')) / 100.0",
  1020  			},
  1021  		},
  1022  		{name: "preserve unknown fields",
  1023  			obj: map[string]interface{}{
  1024  				"withUnknown": map[string]interface{}{
  1025  					"field1": "a",
  1026  					"field2": "b",
  1027  				},
  1028  				"withUnknownList": []interface{}{
  1029  					map[string]interface{}{
  1030  						"field1": "a",
  1031  						"field2": "b",
  1032  					},
  1033  					map[string]interface{}{
  1034  						"field1": "x",
  1035  						"field2": "y",
  1036  					},
  1037  					map[string]interface{}{
  1038  						"field1": "x",
  1039  						"field2": "y",
  1040  					},
  1041  					map[string]interface{}{},
  1042  					map[string]interface{}{},
  1043  				},
  1044  				"withUnknownFieldList": []interface{}{
  1045  					map[string]interface{}{
  1046  						"fieldOfUnknownType": "a",
  1047  					},
  1048  					map[string]interface{}{
  1049  						"fieldOfUnknownType": 1,
  1050  					},
  1051  					map[string]interface{}{
  1052  						"fieldOfUnknownType": 1,
  1053  					},
  1054  				},
  1055  				"anyvalList":   []interface{}{"a", 2},
  1056  				"anyvalMap":    map[string]interface{}{"k": "1"},
  1057  				"anyvalField1": 1,
  1058  				"anyvalField2": "a",
  1059  			},
  1060  			schema: objectTypePtr(map[string]schema.Structural{
  1061  				"withUnknown": {
  1062  					Generic: schema.Generic{Type: "object"},
  1063  					Extensions: schema.Extensions{
  1064  						XPreserveUnknownFields: true,
  1065  					},
  1066  				},
  1067  				"withUnknownList": listType(&schema.Structural{
  1068  					Generic: schema.Generic{Type: "object"},
  1069  					Extensions: schema.Extensions{
  1070  						XPreserveUnknownFields: true,
  1071  					},
  1072  				}),
  1073  				"withUnknownFieldList": listType(&schema.Structural{
  1074  					Generic: schema.Generic{Type: "object"},
  1075  					Properties: map[string]schema.Structural{
  1076  						"fieldOfUnknownType": {
  1077  							Extensions: schema.Extensions{
  1078  								XPreserveUnknownFields: true,
  1079  							},
  1080  						},
  1081  					},
  1082  				}),
  1083  				"anyvalList": listType(&schema.Structural{
  1084  					Extensions: schema.Extensions{
  1085  						XPreserveUnknownFields: true,
  1086  					},
  1087  				}),
  1088  				"anyvalMap": mapType(&schema.Structural{
  1089  					Extensions: schema.Extensions{
  1090  						XPreserveUnknownFields: true,
  1091  					},
  1092  				}),
  1093  				"anyvalField1": {
  1094  					Extensions: schema.Extensions{
  1095  						XPreserveUnknownFields: true,
  1096  					},
  1097  				},
  1098  				"anyvalField2": {
  1099  					Extensions: schema.Extensions{
  1100  						XPreserveUnknownFields: true,
  1101  					},
  1102  				},
  1103  			}),
  1104  			valid: []string{
  1105  				"has(self.withUnknown)",
  1106  				"self.withUnknownList.size() == 5",
  1107  				// fields that are unknown because they were not specified on the object schema are included in equality checks
  1108  				"self.withUnknownList[0] != self.withUnknownList[1]",
  1109  				"self.withUnknownList[1] == self.withUnknownList[2]",
  1110  				"self.withUnknownList[3] == self.withUnknownList[4]",
  1111  
  1112  				// fields specified on the object schema that are unknown because the field's schema is unknown are also included equality checks
  1113  				"self.withUnknownFieldList[0] != self.withUnknownFieldList[1]",
  1114  				"self.withUnknownFieldList[1] == self.withUnknownFieldList[2]",
  1115  			},
  1116  			errors: map[string]string{
  1117  				// unknown fields cannot be field selected
  1118  				"has(self.withUnknown.field1)": "undefined field 'field1'",
  1119  				// if items type of a list, or additionalProperties type of a map, is unknown we treat entire list or map as unknown
  1120  				// If the type of a property is unknown it is not accessible in CEL
  1121  				"has(self.anyvalList)":   "undefined field 'anyvalList'",
  1122  				"has(self.anyvalMap)":    "undefined field 'anyvalMap'",
  1123  				"has(self.anyvalField1)": "undefined field 'anyvalField1'",
  1124  				"has(self.anyvalField2)": "undefined field 'anyvalField2'",
  1125  			},
  1126  		},
  1127  		{name: "known and unknown fields",
  1128  			obj: map[string]interface{}{
  1129  				"withUnknown": map[string]interface{}{
  1130  					"known":   1,
  1131  					"unknown": "a",
  1132  				},
  1133  				"withUnknownList": []interface{}{
  1134  					map[string]interface{}{
  1135  						"known":   1,
  1136  						"unknown": "a",
  1137  					},
  1138  					map[string]interface{}{
  1139  						"known":   1,
  1140  						"unknown": "b",
  1141  					},
  1142  					map[string]interface{}{
  1143  						"known":   1,
  1144  						"unknown": "b",
  1145  					},
  1146  					map[string]interface{}{
  1147  						"known": 1,
  1148  					},
  1149  					map[string]interface{}{
  1150  						"known": 1,
  1151  					},
  1152  					map[string]interface{}{
  1153  						"known": 2,
  1154  					},
  1155  				},
  1156  			},
  1157  			schema: &schema.Structural{
  1158  				Generic: schema.Generic{
  1159  					Type: "object",
  1160  				},
  1161  				Properties: map[string]schema.Structural{
  1162  					"withUnknown": {
  1163  						Generic: schema.Generic{Type: "object"},
  1164  						Extensions: schema.Extensions{
  1165  							XPreserveUnknownFields: true,
  1166  						},
  1167  						Properties: map[string]schema.Structural{
  1168  							"known": integerType,
  1169  						},
  1170  					},
  1171  					"withUnknownList": listType(&schema.Structural{
  1172  						Generic: schema.Generic{Type: "object"},
  1173  						Extensions: schema.Extensions{
  1174  							XPreserveUnknownFields: true,
  1175  						},
  1176  						Properties: map[string]schema.Structural{
  1177  							"known": integerType,
  1178  						},
  1179  					}),
  1180  				},
  1181  			},
  1182  			valid: []string{
  1183  				"self.withUnknown.known == 1",
  1184  				"self.withUnknownList[0] != self.withUnknownList[1]",
  1185  				// if the unknown fields are the same, they are equal
  1186  				"self.withUnknownList[1] == self.withUnknownList[2]",
  1187  
  1188  				// if unknown fields are different, they are not equal
  1189  				"self.withUnknownList[0] != self.withUnknownList[1]",
  1190  				"self.withUnknownList[0] != self.withUnknownList[3]",
  1191  				"self.withUnknownList[0] != self.withUnknownList[5]",
  1192  
  1193  				// if all fields are known, equality works as usual
  1194  				"self.withUnknownList[3] == self.withUnknownList[4]",
  1195  				"self.withUnknownList[4] != self.withUnknownList[5]",
  1196  			},
  1197  			// only field declared in the schema can be field selected in CEL expressions
  1198  			errors: map[string]string{
  1199  				"has(self.withUnknown.unknown)": "undefined field 'unknown'",
  1200  			},
  1201  		},
  1202  		{name: "field nullability",
  1203  			obj: map[string]interface{}{
  1204  				"setPlainStr":          "v1",
  1205  				"setDefaultedStr":      "v2",
  1206  				"setNullableStr":       "v3",
  1207  				"setToNullNullableStr": nil,
  1208  
  1209  				// we don't run the defaulter in this test suite (depending on it would introduce a cycle)
  1210  				// so we fake it :(
  1211  				"unsetDefaultedStr": "default value",
  1212  			},
  1213  			schema: objectTypePtr(map[string]schema.Structural{
  1214  				"unsetPlainStr":     stringType,
  1215  				"unsetDefaultedStr": withDefault("default value", stringType),
  1216  				"unsetNullableStr":  withNullable(true, stringType),
  1217  
  1218  				"setPlainStr":          stringType,
  1219  				"setDefaultedStr":      withDefault("default value", stringType),
  1220  				"setNullableStr":       withNullable(true, stringType),
  1221  				"setToNullNullableStr": withNullable(true, stringType),
  1222  			}),
  1223  			valid: []string{
  1224  				"!has(self.unsetPlainStr)",
  1225  				"has(self.unsetDefaultedStr) && self.unsetDefaultedStr == 'default value'",
  1226  				"!has(self.unsetNullableStr)",
  1227  
  1228  				"has(self.setPlainStr) && self.setPlainStr == 'v1'",
  1229  				"has(self.setDefaultedStr) && self.setDefaultedStr == 'v2'",
  1230  				"has(self.setNullableStr) && self.setNullableStr == 'v3'",
  1231  				// We treat null fields as absent fields, not as null valued fields.
  1232  				// Note that this is different than how we treat nullable list items or map values.
  1233  				"type(self.setNullableStr) != null_type",
  1234  
  1235  				// a field that is set to null is treated the same as an absent field in validation rules
  1236  				"!has(self.setToNullNullableStr)",
  1237  			},
  1238  			errors: map[string]string{
  1239  				// the types used in validation rules don't integrate with CEL's Null type, so
  1240  				// all attempts to compare values with null are caught by the type checker at compilation time
  1241  				"self.unsetPlainStr == null":     "no matching overload for '_==_' applied to '(string, null)",
  1242  				"self.unsetDefaultedStr != null": "no matching overload for '_!=_' applied to '(string, null)",
  1243  				"self.unsetNullableStr == null":  "no matching overload for '_==_' applied to '(string, null)",
  1244  				"self.setPlainStr != null":       "no matching overload for '_!=_' applied to '(string, null)",
  1245  				"self.setDefaultedStr != null":   "no matching overload for '_!=_' applied to '(string, null)",
  1246  				"self.setNullableStr != null":    "no matching overload for '_!=_' applied to '(string, null)",
  1247  			},
  1248  		},
  1249  		{name: "null values in container types",
  1250  			obj: map[string]interface{}{
  1251  				"m": map[string]interface{}{
  1252  					"a": nil,
  1253  					"b": "not-nil",
  1254  				},
  1255  				"l": []interface{}{
  1256  					nil, "not-nil",
  1257  				},
  1258  				"s": []interface{}{
  1259  					nil, "not-nil",
  1260  				},
  1261  			},
  1262  			schema: objectTypePtr(map[string]schema.Structural{
  1263  				"m": mapType(withNullablePtr(true, stringType)),
  1264  				"l": listType(withNullablePtr(true, stringType)),
  1265  				"s": listSetType(withNullablePtr(true, stringType)),
  1266  			}),
  1267  			valid: []string{
  1268  				"self.m.size() == 2",
  1269  				"'a' in self.m",
  1270  				"type(self.m['a']) == null_type", // null check using runtime type checking
  1271  				//"self.m['a'] == null",
  1272  
  1273  				"self.l.size() == 2",
  1274  				"type(self.l[0]) == null_type",
  1275  				//"self.l[0] == null",
  1276  
  1277  				"self.s.size() == 2",
  1278  				"type(self.s[0]) == null_type",
  1279  				//"self.s[0] == null",
  1280  			},
  1281  			errors: map[string]string{
  1282  				// TODO(jpbetz): Type checker does not support unions of (<type>, Null).
  1283  				// This will be available when "heterogeneous type" supported is added to cel-go.
  1284  				// In the meantime, the only other option would be to use dynamic types for nullable types, which
  1285  				// would disable type checking. We plan to wait for "heterogeneous type" support.
  1286  				//"self.m['a'] == null": "found no matching overload for '_==_' applied to '(string, null)",
  1287  				//"self.l[0] == null": "found no matching overload for '_==_' applied to '(string, null)",
  1288  				//"self.s[0] == null": "found no matching overload for '_==_' applied to '(string, null)",
  1289  			},
  1290  		},
  1291  		{name: "escaping",
  1292  			obj: map[string]interface{}{
  1293  				// RESERVED symbols defined in the CEL lexer
  1294  				"true": 1, "false": 2, "null": 3, "in": 4, "as": 5,
  1295  				"break": 6, "const": 7, "continue": 8, "else": 9,
  1296  				"for": 10, "function": 11, "if": 12, "import": 13,
  1297  				"let": 14, "loop": 15, "package": 16, "namespace": 17,
  1298  				"return": 18, "var": 19, "void": 20, "while": 21,
  1299  				// identifiers that are part of the CEL language
  1300  				"int": 101, "uint": 102, "double": 103, "bool": 104,
  1301  				"string": 105, "bytes": 106, "list": 107, "map": 108,
  1302  				"null_type": 109, "type": 110,
  1303  				// validation expression reserved identifiers
  1304  				"self": 201,
  1305  				// identifiers of CEL builtin function and macro names
  1306  				"getDate": 202,
  1307  				"all":     203,
  1308  				"size":    "204",
  1309  				// identifiers that have _s
  1310  				"_true": 301,
  1311  				// identifiers that have the characters we escape
  1312  				"dot.dot":                            401,
  1313  				"dash-dash":                          402,
  1314  				"slash/slash":                        403,
  1315  				"underscore_underscore":              404, // ok, this is not so weird, but it's one we'd like a test case for
  1316  				"doubleunderscore__doubleunderscore": 405,
  1317  			},
  1318  			schema: objectTypePtr(map[string]schema.Structural{
  1319  				"true": integerType, "false": integerType, "null": integerType, "in": integerType, "as": integerType,
  1320  				"break": integerType, "const": integerType, "continue": integerType, "else": integerType,
  1321  				"for": integerType, "function": integerType, "if": integerType, "import": integerType,
  1322  				"let": integerType, "loop": integerType, "package": integerType, "namespace": integerType,
  1323  				"return": integerType, "var": integerType, "void": integerType, "while": integerType,
  1324  
  1325  				"int": integerType, "uint": integerType, "double": integerType, "bool": integerType,
  1326  				"string": integerType, "bytes": integerType, "list": integerType, "map": integerType,
  1327  				"null_type": integerType, "type": integerType,
  1328  
  1329  				"self": integerType,
  1330  
  1331  				"getDate": integerType,
  1332  				"all":     integerType,
  1333  				"size":    stringType,
  1334  
  1335  				"_true": integerType,
  1336  
  1337  				"dot.dot":                            integerType,
  1338  				"dash-dash":                          integerType,
  1339  				"slash/slash":                        integerType,
  1340  				"underscore_underscore":              integerType, // ok, this is not so weird, but it's one we'd like a test case for
  1341  				"doubleunderscore__doubleunderscore": integerType,
  1342  			}),
  1343  			valid: []string{
  1344  				// CEL lexer RESERVED keywords must be escaped
  1345  				"self.__true__ == 1", "self.__false__ == 2", "self.__null__ == 3", "self.__in__ == 4", "self.__as__ == 5",
  1346  				"self.__break__ == 6", "self.__const__ == 7", "self.__continue__ == 8", "self.__else__ == 9",
  1347  				"self.__for__ == 10", "self.__function__ == 11", "self.__if__ == 12", "self.__import__ == 13",
  1348  				"self.__let__ == 14", "self.__loop__ == 15", "self.__package__ == 16", "self.__namespace__ == 17",
  1349  				"self.__return__ == 18", "self.__var__ == 19", "self.__void__ == 20", "self.__while__ == 21", "self.__true__ == 1",
  1350  
  1351  				// CEL language identifiers do not need to be escaped, but would collide with builtin language identifier if bound as
  1352  				// root variable names, so are only field selectable as a field of 'self'.
  1353  				"self.int == 101", "self.uint == 102", "self.double == 103", "self.bool == 104",
  1354  				"self.string == 105", "self.bytes == 106", "self.list == 107", "self.map == 108",
  1355  				"self.null_type == 109", "self.type == 110",
  1356  
  1357  				// if a property name is 'self', it can be field selected as 'self.self' (but not as just 'self' because we bind that
  1358  				// variable name to the locally scoped expression value.
  1359  				"self.self == 201",
  1360  				// CEL macro and function names do not need to be escaped because the parser can disambiguate them from the function and
  1361  				// macro identifiers.
  1362  				"self.getDate == 202",
  1363  				"self.all == 203",
  1364  				"self.size == '204'",
  1365  				// _ is not escaped
  1366  				"self._true == 301",
  1367  				"self.__true__ != 301",
  1368  
  1369  				"self.dot__dot__dot == 401",
  1370  				"self.dash__dash__dash == 402",
  1371  				"self.slash__slash__slash == 403",
  1372  				"self.underscore_underscore == 404",
  1373  				"self.doubleunderscore__underscores__doubleunderscore == 405",
  1374  			},
  1375  			errors: map[string]string{
  1376  				// 'true' is a boolean literal, not a field name
  1377  				"self.true == 1": "mismatched input 'true' expecting IDENTIFIER",
  1378  				// 'self' is the locally scoped expression value
  1379  				"self == 201": "found no matching overload for '_==_'",
  1380  				// attempts to use identifiers that are not escapable are caught by the compiler since
  1381  				// we don't register declarations for them
  1382  				"self.__illegal__ == 301": "undefined field '__illegal__'",
  1383  			},
  1384  		},
  1385  		{name: "map keys are not escaped",
  1386  			obj: map[string]interface{}{
  1387  				"m": map[string]interface{}{
  1388  					"@":   1,
  1389  					"9":   2,
  1390  					"int": 3,
  1391  					"./-": 4,
  1392  					"πŸ‘‘":   5,
  1393  				},
  1394  			},
  1395  			schema: objectTypePtr(map[string]schema.Structural{"m": mapType(&integerType)}),
  1396  			valid: []string{
  1397  				"self.m['@'] == 1",
  1398  				"self.m['9'] == 2",
  1399  				"self.m['int'] == 3",
  1400  				"self.m['./-'] == 4",
  1401  				"self.m['πŸ‘‘'] == 5",
  1402  			},
  1403  		},
  1404  		{name: "object types are not accessible",
  1405  			obj: map[string]interface{}{
  1406  				"nestedInMap": map[string]interface{}{
  1407  					"k1": map[string]interface{}{
  1408  						"inMapField": 1,
  1409  					},
  1410  					"k2": map[string]interface{}{
  1411  						"inMapField": 2,
  1412  					},
  1413  				},
  1414  				"nestedInList": []interface{}{
  1415  					map[string]interface{}{
  1416  						"inListField": 1,
  1417  					},
  1418  					map[string]interface{}{
  1419  						"inListField": 2,
  1420  					},
  1421  				},
  1422  			},
  1423  			schema: objectTypePtr(map[string]schema.Structural{
  1424  				"nestedInMap": mapType(objectTypePtr(map[string]schema.Structural{
  1425  					"inMapField": integerType,
  1426  				})),
  1427  				"nestedInList": listType(objectTypePtr(map[string]schema.Structural{
  1428  					"inListField": integerType,
  1429  				})),
  1430  			}),
  1431  			valid: []string{
  1432  				// we do not expose a stable type for the self variable, even when it is an object that CEL
  1433  				// considers a named type. The only operation developers should be able to perform on the type is
  1434  				// equality checking.
  1435  				"type(self) == type(self)",
  1436  				"type(self.nestedInMap['k1']) == type(self.nestedInMap['k2'])",
  1437  				"type(self.nestedInList[0]) == type(self.nestedInList[1])",
  1438  			},
  1439  			errors: map[string]string{
  1440  				// Note that errors, like the below, do print the type name, but it changes each time a CRD is updated.
  1441  				// Type name printed in the below error will be of the form "<uuid>.nestedInList.@idx".
  1442  
  1443  				// Developers may not cast the type of variables as a string:
  1444  				"string(type(self.nestedInList[0])).endsWith('.nestedInList.@idx')": "found no matching overload for 'string' applied to '(type",
  1445  			},
  1446  		},
  1447  		{name: "listMaps with unsupported identity characters in property names",
  1448  			obj: map[string]interface{}{
  1449  				"objs": []interface{}{
  1450  					[]interface{}{
  1451  						map[string]interface{}{"k!": "a", "k.": "1"},
  1452  						map[string]interface{}{"k!": "b", "k.": "2"},
  1453  					},
  1454  					[]interface{}{
  1455  						map[string]interface{}{"k!": "b", "k.": "2"},
  1456  						map[string]interface{}{"k!": "a", "k.": "1"},
  1457  					},
  1458  					[]interface{}{
  1459  						map[string]interface{}{"k!": "b", "k.": "2"},
  1460  						map[string]interface{}{"k!": "c", "k.": "1"},
  1461  					},
  1462  					[]interface{}{
  1463  						map[string]interface{}{"k!": "b", "k.": "2"},
  1464  						map[string]interface{}{"k!": "a", "k.": "3"},
  1465  					},
  1466  				},
  1467  			},
  1468  			schema: objectTypePtr(map[string]schema.Structural{
  1469  				"objs": listType(listMapTypePtr([]string{"k!", "k."}, objectTypePtr(map[string]schema.Structural{
  1470  					"k!": stringType,
  1471  					"k.": stringType,
  1472  				}))),
  1473  			}),
  1474  			valid: []string{
  1475  				"self.objs[0] == self.objs[1]",    // equal even though order is different
  1476  				"self.objs[0][0].k__dot__ == '1'", // '.' is a supported character in identifiers, but it is escaped
  1477  				"self.objs[0] != self.objs[2]",    // not equal even though difference is in unsupported char
  1478  				"self.objs[0] != self.objs[3]",    // not equal even though difference is in escaped char
  1479  			},
  1480  			errors: map[string]string{
  1481  				// '!' is not a supported character in identifiers, there is no way to access the field
  1482  				"self.objs[0][0].k! == '1'": "Syntax error: mismatched input '!' expecting",
  1483  			},
  1484  		},
  1485  		{name: "container type composition",
  1486  			obj: map[string]interface{}{
  1487  				"obj": map[string]interface{}{
  1488  					"field": "a",
  1489  				},
  1490  				"mapOfMap": map[string]interface{}{
  1491  					"x": map[string]interface{}{
  1492  						"y": "b",
  1493  					},
  1494  				},
  1495  				"mapOfObj": map[string]interface{}{
  1496  					"k": map[string]interface{}{
  1497  						"field2": "c",
  1498  					},
  1499  				},
  1500  				"mapOfListMap": map[string]interface{}{
  1501  					"o": []interface{}{
  1502  						map[string]interface{}{
  1503  							"k": "1",
  1504  							"v": "d",
  1505  						},
  1506  					},
  1507  				},
  1508  				"mapOfList": map[string]interface{}{
  1509  					"l": []interface{}{"e"},
  1510  				},
  1511  				"listMapOfObj": []interface{}{
  1512  					map[string]interface{}{
  1513  						"k2": "2",
  1514  						"v2": "f",
  1515  					},
  1516  				},
  1517  				"listOfMap": []interface{}{
  1518  					map[string]interface{}{
  1519  						"z": "g",
  1520  					},
  1521  				},
  1522  				"listOfObj": []interface{}{
  1523  					map[string]interface{}{
  1524  						"field3": "h",
  1525  					},
  1526  				},
  1527  				"listOfListMap": []interface{}{
  1528  					[]interface{}{
  1529  						map[string]interface{}{
  1530  							"k3": "3",
  1531  							"v3": "i",
  1532  						},
  1533  					},
  1534  				},
  1535  			},
  1536  			schema: objectTypePtr(map[string]schema.Structural{
  1537  				"obj": objectType(map[string]schema.Structural{
  1538  					"field": stringType,
  1539  				}),
  1540  				"mapOfMap": mapType(mapTypePtr(&stringType)),
  1541  				"mapOfObj": mapType(objectTypePtr(map[string]schema.Structural{
  1542  					"field2": stringType,
  1543  				})),
  1544  				"mapOfListMap": mapType(listMapTypePtr([]string{"k"}, objectTypePtr(map[string]schema.Structural{
  1545  					"k": stringType,
  1546  					"v": stringType,
  1547  				}))),
  1548  				"mapOfList": mapType(listTypePtr(&stringType)),
  1549  				"listMapOfObj": listMapType([]string{"k2"}, objectTypePtr(map[string]schema.Structural{
  1550  					"k2": stringType,
  1551  					"v2": stringType,
  1552  				})),
  1553  				"listOfMap": listType(mapTypePtr(&stringType)),
  1554  				"listOfObj": listType(objectTypePtr(map[string]schema.Structural{
  1555  					"field3": stringType,
  1556  				})),
  1557  				"listOfListMap": listType(listMapTypePtr([]string{"k3"}, objectTypePtr(map[string]schema.Structural{
  1558  					"k3": stringType,
  1559  					"v3": stringType,
  1560  				}))),
  1561  			}),
  1562  			valid: []string{
  1563  				"self.obj.field == 'a'",
  1564  				"self.mapOfMap['x']['y'] == 'b'",
  1565  				"self.mapOfObj['k'].field2 == 'c'",
  1566  				"self.mapOfListMap['o'].exists(e, e.k == '1' && e.v == 'd')",
  1567  				"self.mapOfList['l'][0] == 'e'",
  1568  				"self.listMapOfObj.exists(e, e.k2 == '2' && e.v2 == 'f')",
  1569  				"self.listOfMap[0]['z'] == 'g'",
  1570  				"self.listOfObj[0].field3 == 'h'",
  1571  				"self.listOfListMap[0].exists(e, e.k3 == '3' && e.v3 == 'i')",
  1572  			},
  1573  			errors: map[string]string{},
  1574  		},
  1575  		{name: "invalid data",
  1576  			obj: map[string]interface{}{
  1577  				"o":           []interface{}{},
  1578  				"m":           []interface{}{},
  1579  				"l":           map[string]interface{}{},
  1580  				"s":           map[string]interface{}{},
  1581  				"lm":          map[string]interface{}{},
  1582  				"intorstring": true,
  1583  				"nullable":    []interface{}{nil},
  1584  			},
  1585  			schema: objectTypePtr(map[string]schema.Structural{
  1586  				"o": objectType(map[string]schema.Structural{
  1587  					"field": stringType,
  1588  				}),
  1589  				"m": mapType(&stringType),
  1590  				"l": listType(&stringType),
  1591  				"s": listSetType(&stringType),
  1592  				"lm": listMapType([]string{"k"}, objectTypePtr(map[string]schema.Structural{
  1593  					"k": stringType,
  1594  					"v": stringType,
  1595  				})),
  1596  				"intorstring": intOrStringType(),
  1597  				"nullable":    listType(&stringType),
  1598  			}),
  1599  			errors: map[string]string{
  1600  				// data is validated before CEL evaluation, so these errors should not be surfaced to end users
  1601  				"has(self.o)":             "invalid data, expected a map for the provided schema with type=object",
  1602  				"has(self.m)":             "invalid data, expected a map for the provided schema with type=object",
  1603  				"has(self.l)":             "invalid data, expected an array for the provided schema with type=array",
  1604  				"has(self.s)":             "invalid data, expected an array for the provided schema with type=array",
  1605  				"has(self.lm)":            "invalid data, expected an array for the provided schema with type=array",
  1606  				"has(self.intorstring)":   "invalid data, expected XIntOrString value to be either a string or integer",
  1607  				"self.nullable[0] == 'x'": "invalid data, got null for schema with nullable=false",
  1608  				// TODO: also find a way to test the errors returned for: array with no items, object with no properties or additionalProperties, invalid listType and invalid type.
  1609  			},
  1610  		},
  1611  		{name: "stdlib list functions",
  1612  			obj: map[string]interface{}{
  1613  				"ints":         []interface{}{int64(1), int64(2), int64(2), int64(3)},
  1614  				"unsortedInts": []interface{}{int64(2), int64(1)},
  1615  				"emptyInts":    []interface{}{},
  1616  
  1617  				"doubles":         []interface{}{float64(1), float64(2), float64(2), float64(3)},
  1618  				"unsortedDoubles": []interface{}{float64(2), float64(1)},
  1619  				"emptyDoubles":    []interface{}{},
  1620  
  1621  				"intBackedDoubles":          []interface{}{int64(1), int64(2), int64(2), int64(3)},
  1622  				"unsortedIntBackedDDoubles": []interface{}{int64(2), int64(1)},
  1623  				"emptyIntBackedDDoubles":    []interface{}{},
  1624  
  1625  				"durations":         []interface{}{"1s", "1m", "1m", "1h"},
  1626  				"unsortedDurations": []interface{}{"1m", "1s"},
  1627  				"emptyDurations":    []interface{}{},
  1628  
  1629  				"strings":         []interface{}{"a", "b", "b", "c"},
  1630  				"unsortedStrings": []interface{}{"b", "a"},
  1631  				"emptyStrings":    []interface{}{},
  1632  
  1633  				"dates":         []interface{}{"2000-01-01", "2000-02-01", "2000-02-01", "2010-01-01"},
  1634  				"unsortedDates": []interface{}{"2000-02-01", "2000-01-01"},
  1635  				"emptyDates":    []interface{}{},
  1636  
  1637  				"objs": []interface{}{
  1638  					map[string]interface{}{"f1": "a", "f2": "a"},
  1639  					map[string]interface{}{"f1": "a", "f2": "b"},
  1640  					map[string]interface{}{"f1": "a", "f2": "b"},
  1641  					map[string]interface{}{"f1": "a", "f2": "c"},
  1642  				},
  1643  			},
  1644  			schema: objectTypePtr(map[string]schema.Structural{
  1645  				"ints":         listType(&integerType),
  1646  				"unsortedInts": listType(&integerType),
  1647  				"emptyInts":    listType(&integerType),
  1648  
  1649  				"doubles":         listType(&doubleType),
  1650  				"unsortedDoubles": listType(&doubleType),
  1651  				"emptyDoubles":    listType(&doubleType),
  1652  
  1653  				"intBackedDoubles":          listType(&doubleType),
  1654  				"unsortedIntBackedDDoubles": listType(&doubleType),
  1655  				"emptyIntBackedDDoubles":    listType(&doubleType),
  1656  
  1657  				"durations":         listType(&durationFormat),
  1658  				"unsortedDurations": listType(&durationFormat),
  1659  				"emptyDurations":    listType(&durationFormat),
  1660  
  1661  				"strings":         listType(&stringType),
  1662  				"unsortedStrings": listType(&stringType),
  1663  				"emptyStrings":    listType(&stringType),
  1664  
  1665  				"dates":         listType(&dateFormat),
  1666  				"unsortedDates": listType(&dateFormat),
  1667  				"emptyDates":    listType(&dateFormat),
  1668  
  1669  				"objs": listType(objectTypePtr(map[string]schema.Structural{
  1670  					"f1": stringType,
  1671  					"f2": stringType,
  1672  				})),
  1673  			}),
  1674  			valid: []string{
  1675  				"self.ints.sum() == 8",
  1676  				"self.ints.min() == 1",
  1677  				"self.ints.max() == 3",
  1678  				"self.emptyInts.sum() == 0",
  1679  				"self.ints.isSorted()",
  1680  				"self.emptyInts.isSorted()",
  1681  				"self.unsortedInts.isSorted() == false",
  1682  				"self.ints.indexOf(2) == 1",
  1683  				"self.ints.lastIndexOf(2) == 2",
  1684  				"self.ints.indexOf(10) == -1",
  1685  				"self.ints.lastIndexOf(10) == -1",
  1686  
  1687  				"self.doubles.sum() == 8.0",
  1688  				"self.doubles.min() == 1.0",
  1689  				"self.doubles.max() == 3.0",
  1690  				"self.emptyDoubles.sum() == 0.0",
  1691  				"self.doubles.isSorted()",
  1692  				"self.emptyDoubles.isSorted()",
  1693  				"self.unsortedDoubles.isSorted() == false",
  1694  				"self.doubles.indexOf(2.0) == 1",
  1695  				"self.doubles.lastIndexOf(2.0) == 2",
  1696  				"self.doubles.indexOf(10.0) == -1",
  1697  				"self.doubles.lastIndexOf(10.0) == -1",
  1698  
  1699  				"self.intBackedDoubles.sum() == 8.0",
  1700  				"self.intBackedDoubles.min() == 1.0",
  1701  				"self.intBackedDoubles.max() == 3.0",
  1702  				"self.emptyIntBackedDDoubles.sum() == 0.0",
  1703  				"self.intBackedDoubles.isSorted()",
  1704  				"self.emptyDoubles.isSorted()",
  1705  				"self.unsortedIntBackedDDoubles.isSorted() == false",
  1706  				"self.intBackedDoubles.indexOf(2.0) == 1",
  1707  				"self.intBackedDoubles.lastIndexOf(2.0) == 2",
  1708  				"self.intBackedDoubles.indexOf(10.0) == -1",
  1709  				"self.intBackedDoubles.lastIndexOf(10.0) == -1",
  1710  
  1711  				"self.durations.sum() == duration('1h2m1s')",
  1712  				"self.durations.min() == duration('1s')",
  1713  				"self.durations.max() == duration('1h')",
  1714  				"self.emptyDurations.sum() == duration('0')",
  1715  				"self.durations.isSorted()",
  1716  				"self.emptyDurations.isSorted()",
  1717  				"self.unsortedDurations.isSorted() == false",
  1718  				"self.durations.indexOf(duration('1m')) == 1",
  1719  				"self.durations.lastIndexOf(duration('1m')) == 2",
  1720  				"self.durations.indexOf(duration('2m')) == -1",
  1721  				"self.durations.lastIndexOf(duration('2m')) == -1",
  1722  
  1723  				"self.strings.min() == 'a'",
  1724  				"self.strings.max() == 'c'",
  1725  				"self.strings.isSorted()",
  1726  				"self.emptyStrings.isSorted()",
  1727  				"self.unsortedStrings.isSorted() == false",
  1728  				"self.strings.indexOf('b') == 1",
  1729  				"self.strings.lastIndexOf('b') == 2",
  1730  				"self.strings.indexOf('x') == -1",
  1731  				"self.strings.lastIndexOf('x') == -1",
  1732  
  1733  				"self.dates.min() == timestamp('2000-01-01T00:00:00.000Z')",
  1734  				"self.dates.max() == timestamp('2010-01-01T00:00:00.000Z')",
  1735  				"self.dates.isSorted()",
  1736  				"self.emptyDates.isSorted()",
  1737  				"self.unsortedDates.isSorted() == false",
  1738  				"self.dates.indexOf(timestamp('2000-02-01T00:00:00.000Z')) == 1",
  1739  				"self.dates.lastIndexOf(timestamp('2000-02-01T00:00:00.000Z')) == 2",
  1740  				"self.dates.indexOf(timestamp('2005-02-01T00:00:00.000Z')) == -1",
  1741  				"self.dates.lastIndexOf(timestamp('2005-02-01T00:00:00.000Z')) == -1",
  1742  
  1743  				// array, map and object types use structural equality (aka "deep equals")
  1744  				"[[1], [2]].indexOf([1]) == 0",
  1745  				"[{'a': 1}, {'b': 2}].lastIndexOf({'b': 2}) == 1",
  1746  				"self.objs.indexOf(self.objs[1]) == 1",
  1747  				"self.objs.lastIndexOf(self.objs[1]) == 2",
  1748  
  1749  				// avoiding empty list error with min and max by appending an acceptable default minimum value
  1750  				"([0] + self.emptyInts).min() == 0",
  1751  
  1752  				// handle CEL's dynamic dispatch appropriately (special cases to handle an empty list)
  1753  				"dyn([]).sum() == 0",
  1754  				"dyn([1, 2]).sum() == 3",
  1755  				"dyn([1.0, 2.0]).sum() == 3.0",
  1756  
  1757  				"[].sum() == 0", // An empty list returns an 0 int
  1758  			},
  1759  			errors: map[string]string{
  1760  				// return an error for min/max on empty list
  1761  				"self.emptyInts.min() == 1":      "min called on empty list",
  1762  				"self.emptyInts.max() == 3":      "max called on empty list",
  1763  				"self.emptyDoubles.min() == 1.0": "min called on empty list",
  1764  				"self.emptyDoubles.max() == 3.0": "max called on empty list",
  1765  				"self.emptyStrings.min() == 'a'": "min called on empty list",
  1766  				"self.emptyStrings.max() == 'c'": "max called on empty list",
  1767  
  1768  				// only allow sum on numeric types and duration
  1769  				"['a', 'b'].sum() == 'c'": "found no matching overload for 'sum' applied to 'list(string).()", // compiler type checking error
  1770  
  1771  				// only allow min/max/indexOf/lastIndexOf on comparable types
  1772  				"[[1], [2]].min() == [1]":                "found no matching overload for 'min' applied to 'list(list(int)).()",        // compiler type checking error
  1773  				"[{'a': 1}, {'b': 2}].max() == {'b': 2}": "found no matching overload for 'max' applied to 'list(map(string, int)).()", // compiler type checking error
  1774  			},
  1775  		},
  1776  		{name: "stdlib regex functions",
  1777  			obj: map[string]interface{}{
  1778  				"str": "this is a 123 string 456",
  1779  			},
  1780  			schema: objectTypePtr(map[string]schema.Structural{
  1781  				"str": stringType,
  1782  			}),
  1783  			valid: []string{
  1784  				"self.str.find('[0-9]+') == '123'",
  1785  				"self.str.find('[0-9]+') != '456'",
  1786  				"self.str.find('xyz') == ''",
  1787  
  1788  				"self.str.findAll('[0-9]+') == ['123', '456']",
  1789  				"self.str.findAll('[0-9]+', 0) == []",
  1790  				"self.str.findAll('[0-9]+', 1) == ['123']",
  1791  				"self.str.findAll('[0-9]+', 2) == ['123', '456']",
  1792  				"self.str.findAll('[0-9]+', 3) == ['123', '456']",
  1793  				"self.str.findAll('[0-9]+', -1) == ['123', '456']",
  1794  				"self.str.findAll('xyz') == []",
  1795  				"self.str.findAll('xyz', 1) == []",
  1796  			},
  1797  			errors: map[string]string{
  1798  				// Invalid regex with a string constant regex pattern is compile time error
  1799  				"self.str.find(')') == ''":       "compile error: program instantiation failed: error parsing regexp: unexpected ): `)`",
  1800  				"self.str.findAll(')') == []":    "compile error: program instantiation failed: error parsing regexp: unexpected ): `)`",
  1801  				"self.str.findAll(')', 1) == []": "compile error: program instantiation failed: error parsing regexp: unexpected ): `)`",
  1802  				"self.str.matches('x++')":        "invalid matches argument",
  1803  			},
  1804  		},
  1805  		{name: "URL parsing",
  1806  			obj: map[string]interface{}{
  1807  				"url": "https://user:pass@kubernetes.io:80/docs/home?k1=a&k2=b&k2=c#anchor",
  1808  			},
  1809  			schema: objectTypePtr(map[string]schema.Structural{
  1810  				"url": stringType,
  1811  			}),
  1812  			valid: []string{
  1813  				"url('/path').getScheme() == ''",
  1814  				"url('https://example.com/').getScheme() == 'https'",
  1815  				"url('https://example.com:80/').getHost() == 'example.com:80'",
  1816  				"url('https://example.com/').getHost() == 'example.com'",
  1817  				"url('https://[::1]:80/').getHost() == '[::1]:80'",
  1818  				"url('https://[::1]/').getHost() == '[::1]'",
  1819  				"url('/path').getHost() == ''",
  1820  				"url('https://example.com:80/').getHostname() == 'example.com'",
  1821  				"url('https://127.0.0.1/').getHostname() == '127.0.0.1'",
  1822  				"url('https://[::1]/').getHostname() == '::1'",
  1823  				"url('/path').getHostname() == ''",
  1824  				"url('https://example.com:80/').getPort() == '80'",
  1825  				"url('https://example.com/').getPort() == ''",
  1826  				"url('/path').getPort() == ''",
  1827  				"url('https://example.com/path').getEscapedPath() == '/path'",
  1828  				"url('https://example.com/with space/').getEscapedPath() == '/with%20space/'",
  1829  				"url('https://example.com').getEscapedPath() == ''",
  1830  				"url('https://example.com/path?k1=a&k2=b&k2=c').getQuery() == { 'k1': ['a'], 'k2': ['b', 'c']}",
  1831  				"url('https://example.com/path?key with spaces=value with spaces').getQuery() == { 'key with spaces': ['value with spaces']}",
  1832  				"url('https://example.com/path?').getQuery() == {}",
  1833  				"url('https://example.com/path').getQuery() == {}",
  1834  
  1835  				// test with string input
  1836  				"url(self.url).getScheme() == 'https'",
  1837  				"url(self.url).getHost() == 'kubernetes.io:80'",
  1838  				"url(self.url).getHostname() == 'kubernetes.io'",
  1839  				"url(self.url).getPort() == '80'",
  1840  				"url(self.url).getEscapedPath() == '/docs/home'",
  1841  				"url(self.url).getQuery() == {'k1': ['a'], 'k2': ['b', 'c']}",
  1842  
  1843  				"isURL('https://user:pass@example.com:80/path?query=val#fragment')",
  1844  				"isURL('/path') == true",
  1845  				"isURL('https://a:b:c/') == false",
  1846  				"isURL('../relative-path') == false",
  1847  			},
  1848  		},
  1849  		{name: "transition rules",
  1850  			obj: map[string]interface{}{
  1851  				"v": "new",
  1852  			},
  1853  			oldObj: map[string]interface{}{
  1854  				"v": "old",
  1855  			},
  1856  			schema: objectTypePtr(map[string]schema.Structural{
  1857  				"v": stringType,
  1858  			}),
  1859  			valid: []string{
  1860  				"oldSelf.v != self.v",
  1861  				"oldSelf.v == 'old' && self.v == 'new'",
  1862  			},
  1863  		},
  1864  		{name: "skipped transition rule for nil old primitive",
  1865  			expectSkipped: true,
  1866  			obj:           "exists",
  1867  			oldObj:        nil,
  1868  			schema:        &stringType,
  1869  			valid: []string{
  1870  				"oldSelf == self",
  1871  			},
  1872  		},
  1873  		{name: "skipped transition rule for nil old array",
  1874  			expectSkipped: true,
  1875  			obj:           []interface{}{},
  1876  			oldObj:        nil,
  1877  			schema:        listTypePtr(&stringType),
  1878  			valid: []string{
  1879  				"oldSelf == self",
  1880  			},
  1881  		},
  1882  		{name: "skipped transition rule for nil old object",
  1883  			expectSkipped: true,
  1884  			obj:           map[string]interface{}{"f": "exists"},
  1885  			oldObj:        nil,
  1886  			schema: objectTypePtr(map[string]schema.Structural{
  1887  				"f": stringType,
  1888  			}),
  1889  			valid: []string{
  1890  				"oldSelf.f == self.f",
  1891  			},
  1892  		},
  1893  		{name: "skipped transition rule for old with non-nil interface but nil value",
  1894  			expectSkipped: true,
  1895  			obj:           []interface{}{},
  1896  			oldObj:        nilInterfaceOfStringSlice(),
  1897  			schema:        listTypePtr(&stringType),
  1898  			valid: []string{
  1899  				"oldSelf == self",
  1900  			},
  1901  		},
  1902  		{name: "authorizer is not supported for CRD Validation Rules",
  1903  			obj:    []interface{}{},
  1904  			oldObj: []interface{}{},
  1905  			schema: objectTypePtr(map[string]schema.Structural{}),
  1906  			errors: map[string]string{
  1907  				"authorizer.path('/healthz').check('get').allowed()": "undeclared reference to 'authorizer'",
  1908  			},
  1909  		},
  1910  		{name: "optionals", // https://github.com/google/cel-spec/wiki/proposal-246
  1911  			obj: map[string]interface{}{
  1912  				"presentObj": map[string]interface{}{
  1913  					"presentStr": "value",
  1914  				},
  1915  				"m": map[string]interface{}{"k": "v"},
  1916  				"l": []interface{}{"a"},
  1917  			},
  1918  			schema: objectTypePtr(map[string]schema.Structural{
  1919  				"presentObj": objectType(map[string]schema.Structural{
  1920  					"presentStr": stringType,
  1921  				}),
  1922  				"absentObj": objectType(map[string]schema.Structural{
  1923  					"absentStr": stringType,
  1924  				}),
  1925  				"m": mapType(&stringType),
  1926  				"l": listType(&stringType),
  1927  			}),
  1928  			valid: []string{
  1929  				"self.?presentObj.?presentStr == optional.of('value')",
  1930  				"self.presentObj.?presentStr == optional.of('value')",
  1931  				"self.presentObj.?presentStr.or(optional.of('nope')) == optional.of('value')",
  1932  				"self.presentObj.?presentStr.orValue('') == 'value'",
  1933  				"self.presentObj.?presentStr.hasValue() == true",
  1934  				"self.presentObj.?presentStr.optMap(v, v == 'value').hasValue()",
  1935  				"self.?absentObj.?absentStr == optional.none()",
  1936  				"self.?absentObj.?absentStr.or(optional.of('nope')) == optional.of('nope')",
  1937  				"self.?absentObj.?absentStr.orValue('nope') == 'nope'",
  1938  				"self.?absentObj.?absentStr.hasValue() == false",
  1939  				"self.?absentObj.?absentStr.optMap(v, v == 'value').hasValue() == false",
  1940  
  1941  				"self.m[?'k'] == optional.of('v')",
  1942  				"self.m[?'k'].or(optional.of('nope')) == optional.of('v')",
  1943  				"self.m[?'k'].orValue('') == 'v'",
  1944  				"self.m[?'k'].hasValue() == true",
  1945  				"self.m[?'k'].optMap(v, v == 'v').hasValue()",
  1946  				"self.m[?'x'] == optional.none()",
  1947  				"self.m[?'x'].or(optional.of('nope')) == optional.of('nope')",
  1948  				"self.m[?'x'].orValue('nope') == 'nope'",
  1949  				"self.m[?'x'].hasValue() == false",
  1950  				"self.m[?'x'].hasValue() == false",
  1951  
  1952  				"self.l[?0] == optional.of('a')",
  1953  				"self.l[?1] == optional.none()",
  1954  				"self.l[?0].orValue('') == 'a'",
  1955  				"self.l[?0].hasValue() == true",
  1956  				"self.l[?0].optMap(v, v == 'a').hasValue()",
  1957  				"self.l[?1] == optional.none()",
  1958  				"self.l[?1].or(optional.of('nope')) == optional.of('nope')",
  1959  				"self.l[?1].orValue('nope') == 'nope'",
  1960  				"self.l[?1].hasValue() == false",
  1961  				"self.l[?1].hasValue() == false",
  1962  
  1963  				"optional.ofNonZeroValue(1).hasValue()",
  1964  				"optional.ofNonZeroValue(uint(1)).hasValue()",
  1965  				"optional.ofNonZeroValue(1.1).hasValue()",
  1966  				"optional.ofNonZeroValue('a').hasValue()",
  1967  				"optional.ofNonZeroValue(true).hasValue()",
  1968  				"optional.ofNonZeroValue(['a']).hasValue()",
  1969  				"optional.ofNonZeroValue({'k': 'v'}).hasValue()",
  1970  				"optional.ofNonZeroValue(timestamp('2011-08-18T00:00:00.000+01:00')).hasValue()",
  1971  				"optional.ofNonZeroValue(duration('19h3m37s10ms')).hasValue()",
  1972  				"optional.ofNonZeroValue(null) == optional.none()",
  1973  				"optional.ofNonZeroValue(0) == optional.none()",
  1974  				"optional.ofNonZeroValue(uint(0)) == optional.none()",
  1975  				"optional.ofNonZeroValue(0.0) == optional.none()",
  1976  				"optional.ofNonZeroValue('') == optional.none()",
  1977  				"optional.ofNonZeroValue(false) == optional.none()",
  1978  				"optional.ofNonZeroValue([]) == optional.none()",
  1979  				"optional.ofNonZeroValue({}) == optional.none()",
  1980  				"optional.ofNonZeroValue(timestamp('0001-01-01T00:00:00.000+00:00')) == optional.none()",
  1981  				"optional.ofNonZeroValue(duration('0s')) == optional.none()",
  1982  
  1983  				"{?'k': optional.none(), 'k2': 'v2'} == {'k2': 'v2'}",
  1984  				"{?'k': optional.of('v'), 'k2': 'v2'} == {'k': 'v', 'k2': 'v2'}",
  1985  				"['a', ?optional.none(), 'c'] == ['a', 'c']",
  1986  				"['a', ?optional.of('v'), 'c'] == ['a', 'v', 'c']",
  1987  			},
  1988  			errors: map[string]string{
  1989  				"self.absentObj.?absentStr == optional.none()": "no such key: absentObj", // missing ?. operator on first deref is an error
  1990  			},
  1991  		},
  1992  		{name: "quantity",
  1993  			obj:    objs("20", "200M"),
  1994  			schema: schemas(stringType, stringType),
  1995  			valid: []string{
  1996  				"isQuantity(self.val1)",
  1997  				"isQuantity(self.val2)",
  1998  				`isQuantity("20Mi")`,
  1999  				`quantity(self.val2) == quantity("0.2G") && quantity("0.2G") == quantity("200M")`,
  2000  				`quantity("2M") == quantity("0.002G") && quantity("2000k") == quantity("2M") && quantity("0.002G") == quantity("2000k")`,
  2001  				`quantity(self.val1).isLessThan(quantity("100M"))`,
  2002  				`quantity(self.val2).isGreaterThan(quantity("50M"))`,
  2003  				`quantity(self.val2).compareTo(quantity("0.2G")) == 0`,
  2004  				`quantity("50k").add(quantity(self.val1)) == quantity("50.02k")`,
  2005  				`quantity("50k").sub(quantity(self.val1)) == quantity("49980")`,
  2006  				`quantity(self.val1).isInteger()`,
  2007  			},
  2008  		},
  2009  	}
  2010  
  2011  	for i := range tests {
  2012  		i := i
  2013  		t.Run(tests[i].name, func(t *testing.T) {
  2014  			t.Parallel()
  2015  			tt := tests[i]
  2016  			tt.costBudget = celconfig.RuntimeCELCostBudget
  2017  			ctx := context.TODO()
  2018  			for j := range tt.valid {
  2019  				validRule := tt.valid[j]
  2020  				testName := validRule
  2021  				if len(testName) > 127 {
  2022  					testName = testName[:127]
  2023  				}
  2024  				t.Run(testName, func(t *testing.T) {
  2025  					t.Parallel()
  2026  					s := withRule(*tt.schema, validRule)
  2027  					celValidator := validator(&s, tt.isRoot, model.SchemaDeclType(&s, tt.isRoot), celconfig.PerCallLimit)
  2028  					if celValidator == nil {
  2029  						t.Fatal("expected non nil validator")
  2030  					}
  2031  					errs, remainingBudget := celValidator.Validate(ctx, field.NewPath("root"), &s, tt.obj, tt.oldObj, tt.costBudget)
  2032  					for _, err := range errs {
  2033  						t.Errorf("unexpected error: %v", err)
  2034  					}
  2035  					if tt.expectSkipped {
  2036  						// Skipped validations should have no cost. The only possible false positive here would be the CEL expression 'true'.
  2037  						if remainingBudget != tt.costBudget {
  2038  							t.Errorf("expected no cost expended for skipped validation, but got %d remaining from %d budget", remainingBudget, tt.costBudget)
  2039  						}
  2040  						return
  2041  					}
  2042  				})
  2043  			}
  2044  			for rule, expectErrToContain := range tt.errors {
  2045  				testName := rule
  2046  				if len(testName) > 127 {
  2047  					testName = testName[:127]
  2048  				}
  2049  				t.Run(testName, func(t *testing.T) {
  2050  					s := withRule(*tt.schema, rule)
  2051  					celValidator := NewValidator(&s, true, celconfig.PerCallLimit)
  2052  					if celValidator == nil {
  2053  						t.Fatal("expected non nil validator")
  2054  					}
  2055  					errs, _ := celValidator.Validate(ctx, field.NewPath("root"), &s, tt.obj, tt.oldObj, tt.costBudget)
  2056  					if len(errs) == 0 {
  2057  						t.Error("expected validation errors but got none")
  2058  					}
  2059  					for _, err := range errs {
  2060  						if err.Type != field.ErrorTypeInvalid || !strings.Contains(err.Error(), expectErrToContain) {
  2061  							t.Errorf("expected error to contain '%s', but got: %v", expectErrToContain, err)
  2062  						}
  2063  					}
  2064  				})
  2065  			}
  2066  		})
  2067  	}
  2068  }
  2069  
  2070  // TestValidationExpressionsInSchema tests CEL integration with custom resource values and OpenAPIv3 for cases
  2071  // where the validation rules are defined at any level within the schema.
  2072  func TestValidationExpressionsAtSchemaLevels(t *testing.T) {
  2073  	tests := []struct {
  2074  		name   string
  2075  		schema *schema.Structural
  2076  		obj    interface{}
  2077  		oldObj interface{}
  2078  		errors []string // strings that error message must contain
  2079  	}{
  2080  		{name: "invalid rule under array items",
  2081  			obj: map[string]interface{}{
  2082  				"f": []interface{}{1},
  2083  			},
  2084  			schema: objectTypePtr(map[string]schema.Structural{
  2085  				"f": listType(cloneWithRule(&integerType, "self == 'abc'")),
  2086  			}),
  2087  			errors: []string{"found no matching overload for '_==_' applied to '(int, string)"},
  2088  		},
  2089  		{name: "invalid rule under array items, parent has rule",
  2090  			obj: map[string]interface{}{
  2091  				"f": []interface{}{1},
  2092  			},
  2093  			schema: objectTypePtr(map[string]schema.Structural{
  2094  				"f": withRule(listType(cloneWithRule(&integerType, "self == 'abc'")), "1 == 1"),
  2095  			}),
  2096  			errors: []string{"found no matching overload for '_==_' applied to '(int, string)"},
  2097  		},
  2098  		{name: "invalid rule under additionalProperties",
  2099  			obj: map[string]interface{}{
  2100  				"f": map[string]interface{}{"k": 1},
  2101  			},
  2102  			schema: objectTypePtr(map[string]schema.Structural{
  2103  				"f": mapType(cloneWithRule(&integerType, "self == 'abc'")),
  2104  			}),
  2105  			errors: []string{"found no matching overload for '_==_' applied to '(int, string)"},
  2106  		},
  2107  		{name: "invalid rule under additionalProperties, parent has rule",
  2108  			obj: map[string]interface{}{
  2109  				"f": map[string]interface{}{"k": 1},
  2110  			},
  2111  			schema: objectTypePtr(map[string]schema.Structural{
  2112  				"f": withRule(mapType(cloneWithRule(&integerType, "self == 'abc'")), "1 == 1"),
  2113  			}),
  2114  			errors: []string{"found no matching overload for '_==_' applied to '(int, string)"},
  2115  		},
  2116  		{name: "invalid rule under unescaped field name",
  2117  			obj: map[string]interface{}{
  2118  				"f": map[string]interface{}{
  2119  					"m": 1,
  2120  				},
  2121  			},
  2122  			schema: objectTypePtr(map[string]schema.Structural{
  2123  				"f": withRule(objectType(map[string]schema.Structural{"m": integerType}), "self.m == 'abc'"),
  2124  			}),
  2125  			errors: []string{"found no matching overload for '_==_' applied to '(int, string)"},
  2126  		},
  2127  		{name: "invalid rule under unescaped field name, parent has rule",
  2128  			obj: map[string]interface{}{
  2129  				"f": map[string]interface{}{
  2130  					"m": 1,
  2131  				},
  2132  			},
  2133  			schema: withRulePtr(objectTypePtr(map[string]schema.Structural{
  2134  				"f": withRule(objectType(map[string]schema.Structural{"m": integerType}), "self.m == 'abc'"),
  2135  			}), "1 == 1"),
  2136  			errors: []string{"found no matching overload for '_==_' applied to '(int, string)"},
  2137  		},
  2138  		// check that escaped field names do not impact CEL rule validation
  2139  		{name: "invalid rule under escaped field name",
  2140  			obj: map[string]interface{}{
  2141  				"f/2": map[string]interface{}{
  2142  					"m": 1,
  2143  				},
  2144  			},
  2145  			schema: objectTypePtr(map[string]schema.Structural{
  2146  				"f/2": withRule(objectType(map[string]schema.Structural{"m": integerType}), "self.m == 'abc'"),
  2147  			}),
  2148  			errors: []string{"found no matching overload for '_==_' applied to '(int, string)"},
  2149  		},
  2150  		{name: "invalid rule under escaped field name, parent has rule",
  2151  			obj: map[string]interface{}{
  2152  				"f/2": map[string]interface{}{
  2153  					"m": 1,
  2154  				},
  2155  			},
  2156  			schema: withRulePtr(objectTypePtr(map[string]schema.Structural{
  2157  				"f/2": withRule(objectType(map[string]schema.Structural{"m": integerType}), "self.m == 'abc'"),
  2158  			}), "1 == 1"),
  2159  			errors: []string{"found no matching overload for '_==_' applied to '(int, string)"},
  2160  		},
  2161  		{name: "failing rule under escaped field name",
  2162  			obj: map[string]interface{}{
  2163  				"f/2": map[string]interface{}{
  2164  					"m": 1,
  2165  				},
  2166  			},
  2167  			schema: objectTypePtr(map[string]schema.Structural{
  2168  				"f/2": withRule(objectType(map[string]schema.Structural{"m": integerType}), "self.m == 2"),
  2169  			}),
  2170  			errors: []string{"Invalid value: \"object\": failed rule: self.m == 2"},
  2171  		},
  2172  		// unescapable field names that are not accessed by the CEL rule are allowed and should not impact CEL rule validation
  2173  		{name: "invalid rule under unescapable field name",
  2174  			obj: map[string]interface{}{
  2175  				"a@b": map[string]interface{}{
  2176  					"m": 1,
  2177  				},
  2178  			},
  2179  			schema: objectTypePtr(map[string]schema.Structural{
  2180  				"a@b": withRule(objectType(map[string]schema.Structural{"m": integerType}), "self.m == 'abc'"),
  2181  			}),
  2182  			errors: []string{"found no matching overload for '_==_' applied to '(int, string)"},
  2183  		},
  2184  		{name: "invalid rule under unescapable field name, parent has rule",
  2185  			obj: map[string]interface{}{
  2186  				"f@2": map[string]interface{}{
  2187  					"m": 1,
  2188  				},
  2189  			},
  2190  			schema: withRulePtr(objectTypePtr(map[string]schema.Structural{
  2191  				"f@2": withRule(objectType(map[string]schema.Structural{"m": integerType}), "self.m == 'abc'"),
  2192  			}), "1 == 1"),
  2193  			errors: []string{"found no matching overload for '_==_' applied to '(int, string)"},
  2194  		},
  2195  		{name: "failing rule under unescapable field name",
  2196  			obj: map[string]interface{}{
  2197  				"a@b": map[string]interface{}{
  2198  					"m": 1,
  2199  				},
  2200  			},
  2201  			schema: objectTypePtr(map[string]schema.Structural{
  2202  				"a@b": withRule(objectType(map[string]schema.Structural{"m": integerType}), "self.m == 2"),
  2203  			}),
  2204  			errors: []string{"Invalid value: \"object\": failed rule: self.m == 2"},
  2205  		},
  2206  		{name: "matchExpressions - 'values' must be specified when 'operator' is 'In' or 'NotIn'",
  2207  			obj: map[string]interface{}{
  2208  				"matchExpressions": []interface{}{
  2209  					map[string]interface{}{
  2210  						"key":      "tier",
  2211  						"operator": "In",
  2212  						"values":   []interface{}{},
  2213  					},
  2214  				},
  2215  			},
  2216  			schema: genMatchSelectorSchema(`self.matchExpressions.all(rule, (rule.operator != "In" && rule.operator != "NotIn") || ((has(rule.values) && size(rule.values) > 0)))`),
  2217  			errors: []string{"failed rule"},
  2218  		},
  2219  		{name: "matchExpressions - 'values' may not be specified when 'operator' is 'Exists' or 'DoesNotExist'",
  2220  			obj: map[string]interface{}{
  2221  				"matchExpressions": []interface{}{
  2222  					map[string]interface{}{
  2223  						"key":      "tier",
  2224  						"operator": "Exists",
  2225  						"values":   []interface{}{"somevalue"},
  2226  					},
  2227  				},
  2228  			},
  2229  			schema: genMatchSelectorSchema(`self.matchExpressions.all(rule, (rule.operator != "Exists" && rule.operator != "DoesNotExist") || ((!has(rule.values) || size(rule.values) == 0)))`),
  2230  			errors: []string{"failed rule"},
  2231  		},
  2232  		{name: "matchExpressions - invalid selector operator",
  2233  			obj: map[string]interface{}{
  2234  				"matchExpressions": []interface{}{
  2235  					map[string]interface{}{
  2236  						"key":      "tier",
  2237  						"operator": "badop",
  2238  						"values":   []interface{}{},
  2239  					},
  2240  				},
  2241  			},
  2242  			schema: genMatchSelectorSchema(`self.matchExpressions.all(rule, rule.operator == "In" || rule.operator == "NotIn" || rule.operator == "DoesNotExist")`),
  2243  			errors: []string{"failed rule"},
  2244  		},
  2245  		{name: "matchExpressions - invalid label value",
  2246  			obj: map[string]interface{}{
  2247  				"matchExpressions": []interface{}{
  2248  					map[string]interface{}{
  2249  						"key":      "badkey!",
  2250  						"operator": "Exists",
  2251  						"values":   []interface{}{},
  2252  					},
  2253  				},
  2254  			},
  2255  			schema: genMatchSelectorSchema(`self.matchExpressions.all(rule, size(rule.key) <= 63 && rule.key.matches("^(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])?$"))`),
  2256  			errors: []string{"failed rule"},
  2257  		},
  2258  	}
  2259  
  2260  	for _, tt := range tests {
  2261  		tt := tt
  2262  		t.Run(tt.name, func(t *testing.T) {
  2263  			t.Parallel()
  2264  			ctx := context.TODO()
  2265  			celValidator := validator(tt.schema, true, model.SchemaDeclType(tt.schema, true), celconfig.PerCallLimit)
  2266  			if celValidator == nil {
  2267  				t.Fatal("expected non nil validator")
  2268  			}
  2269  			errs, _ := celValidator.Validate(ctx, field.NewPath("root"), tt.schema, tt.obj, tt.oldObj, math.MaxInt)
  2270  			unmatched := map[string]struct{}{}
  2271  			for _, e := range tt.errors {
  2272  				unmatched[e] = struct{}{}
  2273  			}
  2274  			for _, err := range errs {
  2275  				if err.Type != field.ErrorTypeInvalid {
  2276  					t.Errorf("expected only ErrorTypeInvalid errors, but got: %v", err)
  2277  					continue
  2278  				}
  2279  				matched := false
  2280  				for expected := range unmatched {
  2281  					if strings.Contains(err.Error(), expected) {
  2282  						delete(unmatched, expected)
  2283  						matched = true
  2284  						break
  2285  					}
  2286  				}
  2287  				if !matched {
  2288  					t.Errorf("expected error to contain one of %v, but got: %v", unmatched, err)
  2289  				}
  2290  			}
  2291  			if len(unmatched) > 0 {
  2292  				t.Errorf("expected errors %v", unmatched)
  2293  			}
  2294  		})
  2295  	}
  2296  }
  2297  
  2298  func genMatchSelectorSchema(rule string) *schema.Structural {
  2299  	s := withRule(objectType(map[string]schema.Structural{
  2300  		"matchExpressions": listType(objectTypePtr(map[string]schema.Structural{
  2301  			"key":      stringType,
  2302  			"operator": stringType,
  2303  			"values":   listType(&stringType),
  2304  		})),
  2305  	}), rule)
  2306  	return &s
  2307  }
  2308  
  2309  func TestCELValidationLimit(t *testing.T) {
  2310  	tests := []struct {
  2311  		name   string
  2312  		schema *schema.Structural
  2313  		obj    interface{}
  2314  		valid  []string
  2315  	}{
  2316  		{
  2317  			name:   "test limit",
  2318  			obj:    objs(math.MaxInt64),
  2319  			schema: schemas(integerType),
  2320  			valid: []string{
  2321  				"self.val1 > 0",
  2322  			}},
  2323  	}
  2324  	for _, tt := range tests {
  2325  		t.Run(tt.name, func(t *testing.T) {
  2326  			ctx := context.TODO()
  2327  			for j := range tt.valid {
  2328  				validRule := tt.valid[j]
  2329  				t.Run(validRule, func(t *testing.T) {
  2330  					t.Parallel()
  2331  					s := withRule(*tt.schema, validRule)
  2332  					celValidator := validator(&s, false, model.SchemaDeclType(&s, false), celconfig.PerCallLimit)
  2333  
  2334  					// test with cost budget exceeded
  2335  					errs, _ := celValidator.Validate(ctx, field.NewPath("root"), &s, tt.obj, nil, 0)
  2336  					var found bool
  2337  					for _, err := range errs {
  2338  						if err.Type == field.ErrorTypeInvalid && strings.Contains(err.Error(), "validation failed due to running out of cost budget, no further validation rules will be run") {
  2339  							found = true
  2340  						} else {
  2341  							t.Errorf("unexpected err: %v", err)
  2342  						}
  2343  					}
  2344  					if !found {
  2345  						t.Errorf("expect cost limit exceed err but did not find")
  2346  					}
  2347  					if len(errs) > 1 {
  2348  						t.Errorf("expect to only return cost budget exceed err once but got: %v", len(errs))
  2349  					}
  2350  
  2351  					// test with PerCallLimit exceeded
  2352  					found = false
  2353  					celValidator = NewValidator(&s, true, 0)
  2354  					if celValidator == nil {
  2355  						t.Fatal("expected non nil validator")
  2356  					}
  2357  					errs, _ = celValidator.Validate(ctx, field.NewPath("root"), &s, tt.obj, nil, celconfig.RuntimeCELCostBudget)
  2358  					for _, err := range errs {
  2359  						if err.Type == field.ErrorTypeInvalid && strings.Contains(err.Error(), "no further validation rules will be run due to call cost exceeds limit for rule") {
  2360  							found = true
  2361  							break
  2362  						}
  2363  					}
  2364  					if !found {
  2365  						t.Errorf("expect PerCostLimit exceed err but did not find")
  2366  					}
  2367  				})
  2368  			}
  2369  		})
  2370  	}
  2371  
  2372  }
  2373  
  2374  func TestCELValidationContextCancellation(t *testing.T) {
  2375  	items := make([]interface{}, 1000)
  2376  	for i := int64(0); i < 1000; i++ {
  2377  		items[i] = i
  2378  	}
  2379  	tests := []struct {
  2380  		name   string
  2381  		schema *schema.Structural
  2382  		obj    map[string]interface{}
  2383  		rule   string
  2384  	}{
  2385  		{name: "test cel validation with context cancellation",
  2386  			obj: map[string]interface{}{
  2387  				"array": items,
  2388  			},
  2389  			schema: objectTypePtr(map[string]schema.Structural{
  2390  				"array": listType(&integerType),
  2391  			}),
  2392  			rule: "self.array.map(e, e * 20).filter(e, e > 50).exists(e, e == 60)",
  2393  		},
  2394  	}
  2395  
  2396  	for _, tt := range tests {
  2397  		t.Run(tt.name, func(t *testing.T) {
  2398  			ctx := context.TODO()
  2399  			s := withRule(*tt.schema, tt.rule)
  2400  			celValidator := NewValidator(&s, true, celconfig.PerCallLimit)
  2401  			if celValidator == nil {
  2402  				t.Fatal("expected non nil validator")
  2403  			}
  2404  			errs, _ := celValidator.Validate(ctx, field.NewPath("root"), &s, tt.obj, nil, celconfig.RuntimeCELCostBudget)
  2405  			for _, err := range errs {
  2406  				t.Errorf("unexpected error: %v", err)
  2407  			}
  2408  
  2409  			// test context cancellation
  2410  			found := false
  2411  			evalCtx, cancel := context.WithTimeout(ctx, time.Microsecond)
  2412  			cancel()
  2413  			errs, _ = celValidator.Validate(evalCtx, field.NewPath("root"), &s, tt.obj, nil, celconfig.RuntimeCELCostBudget)
  2414  			for _, err := range errs {
  2415  				if err.Type == field.ErrorTypeInvalid && strings.Contains(err.Error(), "operation interrupted") {
  2416  					found = true
  2417  					break
  2418  				}
  2419  			}
  2420  			if !found {
  2421  				t.Errorf("expect operation interrupted err but did not find")
  2422  			}
  2423  		})
  2424  	}
  2425  }
  2426  
  2427  // This is the most recursive operations we expect to be able to include in an expression.
  2428  // This number could get larger with more improvements in the grammar or ANTLR stack, but should *never* decrease or previously valid expressions could be treated as invalid.
  2429  const maxValidDepth = 250
  2430  
  2431  // TestCELMaxRecursionDepth tests CEL setting for maxRecursionDepth.
  2432  func TestCELMaxRecursionDepth(t *testing.T) {
  2433  	tests := []struct {
  2434  		name          string
  2435  		schema        *schema.Structural
  2436  		obj           interface{}
  2437  		oldObj        interface{}
  2438  		valid         []string
  2439  		errors        map[string]string // rule -> string that error message must contain
  2440  		costBudget    int64
  2441  		isRoot        bool
  2442  		expectSkipped bool
  2443  	}{
  2444  		{name: "test CEL maxRecursionDepth",
  2445  			obj:    objs(true),
  2446  			schema: schemas(booleanType),
  2447  			valid: []string{
  2448  				strings.Repeat("self.val1"+" == ", maxValidDepth-1) + "self.val1",
  2449  			},
  2450  			errors: map[string]string{
  2451  				strings.Repeat("self.val1"+" == ", maxValidDepth) + "self.val1": "max recursion depth exceeded",
  2452  			},
  2453  		},
  2454  	}
  2455  
  2456  	for _, tt := range tests {
  2457  		t.Run(tt.name, func(t *testing.T) {
  2458  			tt.costBudget = celconfig.RuntimeCELCostBudget
  2459  			ctx := context.TODO()
  2460  			for j := range tt.valid {
  2461  				validRule := tt.valid[j]
  2462  				testName := validRule
  2463  				t.Run(testName, func(t *testing.T) {
  2464  					t.Parallel()
  2465  					s := withRule(*tt.schema, validRule)
  2466  					celValidator := validator(&s, tt.isRoot, model.SchemaDeclType(&s, tt.isRoot), celconfig.PerCallLimit)
  2467  					if celValidator == nil {
  2468  						t.Fatal("expected non nil validator")
  2469  					}
  2470  					errs, remainingBudget := celValidator.Validate(ctx, field.NewPath("root"), &s, tt.obj, tt.oldObj, tt.costBudget)
  2471  					for _, err := range errs {
  2472  						t.Errorf("unexpected error: %v", err)
  2473  					}
  2474  					if tt.expectSkipped {
  2475  						// Skipped validations should have no cost. The only possible false positive here would be the CEL expression 'true'.
  2476  						if remainingBudget != tt.costBudget {
  2477  							t.Errorf("expected no cost expended for skipped validation, but got %d remaining from %d budget", remainingBudget, tt.costBudget)
  2478  						}
  2479  						return
  2480  					}
  2481  				})
  2482  			}
  2483  			for rule, expectErrToContain := range tt.errors {
  2484  				testName := rule
  2485  				if len(testName) > 127 {
  2486  					testName = testName[:127]
  2487  				}
  2488  				t.Run(testName, func(t *testing.T) {
  2489  					s := withRule(*tt.schema, rule)
  2490  					celValidator := NewValidator(&s, true, celconfig.PerCallLimit)
  2491  					if celValidator == nil {
  2492  						t.Fatal("expected non nil validator")
  2493  					}
  2494  					errs, _ := celValidator.Validate(ctx, field.NewPath("root"), &s, tt.obj, tt.oldObj, tt.costBudget)
  2495  					if len(errs) == 0 {
  2496  						t.Error("expected validation errors but got none")
  2497  					}
  2498  					for _, err := range errs {
  2499  						if err.Type != field.ErrorTypeInvalid || !strings.Contains(err.Error(), expectErrToContain) {
  2500  							t.Errorf("expected error to contain '%s', but got: %v", expectErrToContain, err)
  2501  						}
  2502  					}
  2503  				})
  2504  			}
  2505  		})
  2506  	}
  2507  }
  2508  
  2509  func TestMessageExpression(t *testing.T) {
  2510  	klog.LogToStderr(false)
  2511  	klog.InitFlags(nil)
  2512  	setDefaultVerbosity(2)
  2513  	defer klog.LogToStderr(true)
  2514  	tests := []struct {
  2515  		name                    string
  2516  		costBudget              int64
  2517  		perCallLimit            uint64
  2518  		message                 string
  2519  		messageExpression       string
  2520  		expectedLogErr          string
  2521  		expectedValidationErr   string
  2522  		expectedRemainingBudget int64
  2523  	}{
  2524  		{
  2525  			name:                    "no cost error expected",
  2526  			messageExpression:       `"static string"`,
  2527  			expectedValidationErr:   "static string",
  2528  			costBudget:              300,
  2529  			expectedRemainingBudget: 300,
  2530  		},
  2531  		{
  2532  			name:                  "messageExpression takes precedence over message",
  2533  			message:               "invisible",
  2534  			messageExpression:     `"this is messageExpression"`,
  2535  			costBudget:            celconfig.RuntimeCELCostBudget,
  2536  			expectedValidationErr: "this is messageExpression",
  2537  		},
  2538  		{
  2539  			name:                    "default rule message used if messageExpression does not eval to string",
  2540  			messageExpression:       `true`,
  2541  			costBudget:              celconfig.RuntimeCELCostBudget,
  2542  			expectedValidationErr:   "failed rule",
  2543  			expectedRemainingBudget: celconfig.RuntimeCELCostBudget,
  2544  		},
  2545  		{
  2546  			name:                    "limit exceeded",
  2547  			messageExpression:       `"string 1" + "string 2" + "string 3"`,
  2548  			costBudget:              1,
  2549  			expectedValidationErr:   "messageExpression evaluation failed due to running out of cost budget",
  2550  			expectedRemainingBudget: -1,
  2551  		},
  2552  		{
  2553  			name:                    "messageExpression budget (str concat)",
  2554  			messageExpression:       `"str1 " + self.str`,
  2555  			costBudget:              50,
  2556  			expectedValidationErr:   "str1 a string",
  2557  			expectedRemainingBudget: 46,
  2558  		},
  2559  		{
  2560  			name:                    "runtime cost preserved if messageExpression fails during evaluation",
  2561  			message:                 "message not messageExpression",
  2562  			messageExpression:       `"str1 " + ["a", "b", "c", "d"][4]`,
  2563  			costBudget:              50,
  2564  			expectedLogErr:          "messageExpression evaluation failed due to: index out of bounds: 4",
  2565  			expectedValidationErr:   "message not messageExpression",
  2566  			expectedRemainingBudget: 47,
  2567  		},
  2568  		{
  2569  			name:                    "runtime cost preserved if messageExpression fails during evaluation (no message set)",
  2570  			messageExpression:       `"str1 " + ["a", "b", "c", "d"][4]`,
  2571  			costBudget:              50,
  2572  			expectedLogErr:          "messageExpression evaluation failed due to: index out of bounds: 4",
  2573  			expectedValidationErr:   "failed rule",
  2574  			expectedRemainingBudget: 47,
  2575  		},
  2576  		{
  2577  			name:                    "per-call limit exceeded during messageExpression execution",
  2578  			messageExpression:       `"string 1" + "string 2" + "string 3"`,
  2579  			costBudget:              celconfig.RuntimeCELCostBudget,
  2580  			perCallLimit:            1,
  2581  			expectedValidationErr:   "call cost exceeds limit for messageExpression",
  2582  			expectedRemainingBudget: -1,
  2583  		},
  2584  		{
  2585  			name:                  "messageExpression is not allowed to generate a string with newlines",
  2586  			message:               "message not messageExpression",
  2587  			messageExpression:     `"str with \na newline"`,
  2588  			costBudget:            celconfig.RuntimeCELCostBudget,
  2589  			expectedLogErr:        "messageExpression should not contain line breaks",
  2590  			expectedValidationErr: "message not messageExpression",
  2591  		},
  2592  		{
  2593  			name:                  "messageExpression is not allowed to generate messages >5000 characters",
  2594  			message:               "message not messageExpression",
  2595  			messageExpression:     fmt.Sprintf(`"%s"`, genString(5121, 'a')),
  2596  			costBudget:            celconfig.RuntimeCELCostBudget,
  2597  			expectedLogErr:        "messageExpression beyond allowable length of 5120",
  2598  			expectedValidationErr: "message not messageExpression",
  2599  		},
  2600  		{
  2601  			name:                  "messageExpression is not allowed to generate an empty string",
  2602  			message:               "message not messageExpression",
  2603  			messageExpression:     `string("")`,
  2604  			costBudget:            celconfig.RuntimeCELCostBudget,
  2605  			expectedLogErr:        "messageExpression should evaluate to a non-empty string",
  2606  			expectedValidationErr: "message not messageExpression",
  2607  		},
  2608  		{
  2609  			name:                  "messageExpression is not allowed to generate a string with only spaces",
  2610  			message:               "message not messageExpression",
  2611  			messageExpression:     `string("     ")`,
  2612  			costBudget:            celconfig.RuntimeCELCostBudget,
  2613  			expectedLogErr:        "messageExpression should evaluate to a non-empty string",
  2614  			expectedValidationErr: "message not messageExpression",
  2615  		},
  2616  	}
  2617  	for _, tt := range tests {
  2618  		t.Run(tt.name, func(t *testing.T) {
  2619  			outputBuffer := strings.Builder{}
  2620  			klog.SetOutput(&outputBuffer)
  2621  
  2622  			ctx := context.TODO()
  2623  			var s schema.Structural
  2624  			if tt.message != "" {
  2625  				s = withRuleMessageAndMessageExpression(objectType(map[string]schema.Structural{
  2626  					"str": stringType}), "false", tt.message, tt.messageExpression)
  2627  			} else {
  2628  				s = withRuleAndMessageExpression(objectType(map[string]schema.Structural{
  2629  					"str": stringType}), "false", tt.messageExpression)
  2630  			}
  2631  			obj := map[string]interface{}{
  2632  				"str": "a string",
  2633  			}
  2634  
  2635  			callLimit := uint64(celconfig.PerCallLimit)
  2636  			if tt.perCallLimit != 0 {
  2637  				callLimit = tt.perCallLimit
  2638  			}
  2639  			celValidator := NewValidator(&s, false, callLimit)
  2640  			if celValidator == nil {
  2641  				t.Fatal("expected non nil validator")
  2642  			}
  2643  			errs, remainingBudget := celValidator.Validate(ctx, field.NewPath("root"), &s, obj, nil, tt.costBudget)
  2644  			klog.Flush()
  2645  
  2646  			if len(errs) != 1 {
  2647  				t.Fatalf("expected 1 error, got %d", len(errs))
  2648  			}
  2649  
  2650  			if tt.expectedLogErr != "" {
  2651  				if !strings.Contains(outputBuffer.String(), tt.expectedLogErr) {
  2652  					t.Fatalf("did not find expected log error message: %q\n%q", tt.expectedLogErr, outputBuffer.String())
  2653  				}
  2654  			} else if tt.expectedLogErr == "" && outputBuffer.String() != "" {
  2655  				t.Fatalf("expected no log output, got: %q", outputBuffer.String())
  2656  			}
  2657  
  2658  			if tt.expectedValidationErr != "" {
  2659  				if !strings.Contains(errs[0].Error(), tt.expectedValidationErr) {
  2660  					t.Fatalf("did not find expected validation error message: %q", tt.expectedValidationErr)
  2661  				}
  2662  			}
  2663  
  2664  			if tt.expectedRemainingBudget != 0 {
  2665  				if tt.expectedRemainingBudget != remainingBudget {
  2666  					t.Fatalf("expected %d cost left, got %d", tt.expectedRemainingBudget, remainingBudget)
  2667  				}
  2668  			}
  2669  		})
  2670  	}
  2671  }
  2672  
  2673  func TestReasonAndFldPath(t *testing.T) {
  2674  	forbiddenReason := func() *apiextensions.FieldValueErrorReason {
  2675  		r := apiextensions.FieldValueForbidden
  2676  		return &r
  2677  	}()
  2678  	tests := []struct {
  2679  		name   string
  2680  		schema *schema.Structural
  2681  		obj    interface{}
  2682  		errors field.ErrorList
  2683  	}{
  2684  		{name: "Return error based on input reason",
  2685  			obj: map[string]interface{}{
  2686  				"f": map[string]interface{}{
  2687  					"m": 1,
  2688  				},
  2689  			},
  2690  			schema: withRulePtr(objectTypePtr(map[string]schema.Structural{
  2691  				"f": withReasonAndFldPath(objectType(map[string]schema.Structural{"m": integerType}), "self.m == 2", "", forbiddenReason),
  2692  			}), "1 == 1"),
  2693  			errors: field.ErrorList{
  2694  				{
  2695  					Type:  field.ErrorTypeForbidden,
  2696  					Field: "root.f",
  2697  				},
  2698  			},
  2699  		},
  2700  		{name: "Return error default is invalid",
  2701  			obj: map[string]interface{}{
  2702  				"f": map[string]interface{}{
  2703  					"m": 1,
  2704  				},
  2705  			},
  2706  			schema: withRulePtr(objectTypePtr(map[string]schema.Structural{
  2707  				"f": withReasonAndFldPath(objectType(map[string]schema.Structural{"m": integerType}), "self.m == 2", "", nil),
  2708  			}), "1 == 1"),
  2709  			errors: field.ErrorList{
  2710  				{
  2711  					Type:  field.ErrorTypeInvalid,
  2712  					Field: "root.f",
  2713  				},
  2714  			},
  2715  		},
  2716  		{name: "Return error based on input fieldPath",
  2717  			obj: map[string]interface{}{
  2718  				"f": map[string]interface{}{
  2719  					"m": 1,
  2720  				},
  2721  			},
  2722  			schema: withRulePtr(objectTypePtr(map[string]schema.Structural{
  2723  				"f": withReasonAndFldPath(objectType(map[string]schema.Structural{"m": integerType}), "self.m == 2", ".m", forbiddenReason),
  2724  			}), "1 == 1"),
  2725  			errors: field.ErrorList{
  2726  				{
  2727  					Type:  field.ErrorTypeForbidden,
  2728  					Field: "root.f.m",
  2729  				},
  2730  			},
  2731  		},
  2732  		{
  2733  			name: "multiple rules with custom reason and field path",
  2734  			obj: map[string]interface{}{
  2735  				"field1": "value1",
  2736  				"field2": "value2",
  2737  				"field3": "value3",
  2738  			},
  2739  			schema: &schema.Structural{
  2740  				Generic: schema.Generic{
  2741  					Type: "object",
  2742  				},
  2743  				Properties: map[string]schema.Structural{
  2744  					"field1": stringType,
  2745  					"field2": stringType,
  2746  					"field3": stringType,
  2747  				},
  2748  				Extensions: schema.Extensions{
  2749  					XValidations: apiextensions.ValidationRules{
  2750  						{
  2751  							Rule:      `self.field2 != "value2"`,
  2752  							Reason:    ptr.To(apiextensions.FieldValueDuplicate),
  2753  							FieldPath: ".field2",
  2754  						},
  2755  						{
  2756  							Rule:      `self.field3 != "value3"`,
  2757  							Reason:    ptr.To(apiextensions.FieldValueRequired),
  2758  							FieldPath: ".field3",
  2759  						},
  2760  						{
  2761  							Rule:      `self.field1 != "value1"`,
  2762  							Reason:    ptr.To(apiextensions.FieldValueForbidden),
  2763  							FieldPath: ".field1",
  2764  						},
  2765  					},
  2766  				},
  2767  			},
  2768  			errors: field.ErrorList{
  2769  				{
  2770  					Type:  field.ErrorTypeDuplicate,
  2771  					Field: "root.field2",
  2772  				},
  2773  				{
  2774  					Type:  field.ErrorTypeRequired,
  2775  					Field: "root.field3",
  2776  				},
  2777  				{
  2778  					Type:  field.ErrorTypeForbidden,
  2779  					Field: "root.field1",
  2780  				},
  2781  			},
  2782  		},
  2783  	}
  2784  	for _, tt := range tests {
  2785  		t.Run(tt.name, func(t *testing.T) {
  2786  			ctx := context.TODO()
  2787  			celValidator := validator(tt.schema, true, model.SchemaDeclType(tt.schema, true), celconfig.PerCallLimit)
  2788  			if celValidator == nil {
  2789  				t.Fatal("expected non nil validator")
  2790  			}
  2791  			errs, _ := celValidator.Validate(ctx, field.NewPath("root"), tt.schema, tt.obj, nil, celconfig.RuntimeCELCostBudget)
  2792  
  2793  			for i := range errs {
  2794  				// Ignore unchecked fields for this test
  2795  				errs[i].Detail = ""
  2796  				errs[i].BadValue = nil
  2797  			}
  2798  
  2799  			require.ElementsMatch(t, tt.errors, errs)
  2800  		})
  2801  	}
  2802  }
  2803  
  2804  func TestValidateFieldPath(t *testing.T) {
  2805  	sts := schema.Structural{
  2806  		Generic: schema.Generic{
  2807  			Type: "object",
  2808  		},
  2809  		Properties: map[string]schema.Structural{
  2810  			"foo": {
  2811  				Generic: schema.Generic{
  2812  					Type: "object",
  2813  					AdditionalProperties: &schema.StructuralOrBool{
  2814  						Structural: &schema.Structural{
  2815  							Generic: schema.Generic{
  2816  								Type: "object",
  2817  							},
  2818  							Properties: map[string]schema.Structural{
  2819  								"subAdd": {
  2820  									Generic: schema.Generic{
  2821  										Type: "number",
  2822  									},
  2823  								},
  2824  							},
  2825  						},
  2826  					},
  2827  				},
  2828  			},
  2829  			"white space": {
  2830  				Generic: schema.Generic{
  2831  					Type: "number",
  2832  				},
  2833  			},
  2834  			"'foo'bar": {
  2835  				Generic: schema.Generic{
  2836  					Type: "number",
  2837  				},
  2838  			},
  2839  			"a": {
  2840  				Generic: schema.Generic{
  2841  					Type: "object",
  2842  				},
  2843  				Properties: map[string]schema.Structural{
  2844  					"foo's": {
  2845  						Generic: schema.Generic{
  2846  							Type: "number",
  2847  						},
  2848  					},
  2849  					"test\a": {
  2850  						Generic: schema.Generic{
  2851  							Type: "number",
  2852  						},
  2853  					},
  2854  					"bb[b": {
  2855  						Generic: schema.Generic{
  2856  							Type: "number",
  2857  						},
  2858  					},
  2859  					"bbb": {
  2860  						Generic: schema.Generic{
  2861  							Type: "object",
  2862  						},
  2863  						Properties: map[string]schema.Structural{
  2864  							"c": {
  2865  								Generic: schema.Generic{
  2866  									Type: "number",
  2867  								},
  2868  							},
  2869  							"34": {
  2870  								Generic: schema.Generic{
  2871  									Type: "number",
  2872  								},
  2873  							},
  2874  						},
  2875  					},
  2876  					"bbb.c": {
  2877  						Generic: schema.Generic{
  2878  							Type: "object",
  2879  						},
  2880  						Properties: map[string]schema.Structural{
  2881  							"a-b.3'4": {
  2882  								Generic: schema.Generic{
  2883  									Type: "number",
  2884  								},
  2885  							},
  2886  						},
  2887  					},
  2888  				},
  2889  			},
  2890  			"list": {
  2891  				Generic: schema.Generic{
  2892  					Type: "array",
  2893  				},
  2894  				Items: &schema.Structural{
  2895  					Generic: schema.Generic{
  2896  						Type: "object",
  2897  					},
  2898  					Properties: map[string]schema.Structural{
  2899  						"a": {
  2900  							Generic: schema.Generic{
  2901  								Type: "number",
  2902  							},
  2903  						},
  2904  						"a-b.3'4": {
  2905  							Generic: schema.Generic{
  2906  								Type: "number",
  2907  							},
  2908  						},
  2909  					},
  2910  				},
  2911  			},
  2912  		},
  2913  	}
  2914  
  2915  	path := field.NewPath("")
  2916  
  2917  	tests := []struct {
  2918  		name            string
  2919  		fieldPath       string
  2920  		pathOfFieldPath *field.Path
  2921  		schema          *schema.Structural
  2922  		errDetail       string
  2923  		validFieldPath  *field.Path
  2924  	}{
  2925  		{
  2926  			name:            "Valid .a",
  2927  			fieldPath:       ".a",
  2928  			pathOfFieldPath: path,
  2929  			schema:          &sts,
  2930  			validFieldPath:  path.Child("a"),
  2931  		},
  2932  		{
  2933  			name:            "Valid 'foo'bar",
  2934  			fieldPath:       "['\\'foo\\'bar']",
  2935  			pathOfFieldPath: path,
  2936  			schema:          &sts,
  2937  			validFieldPath:  path.Child("'foo'bar"),
  2938  		},
  2939  		{
  2940  			name:            "Invalid 'foo'bar",
  2941  			fieldPath:       ".\\'foo\\'bar",
  2942  			pathOfFieldPath: path,
  2943  			schema:          &sts,
  2944  			errDetail:       "does not refer to a valid field",
  2945  		},
  2946  		{
  2947  			name:            "Invalid with whitespace",
  2948  			fieldPath:       ". a",
  2949  			pathOfFieldPath: path,
  2950  			schema:          &sts,
  2951  			errDetail:       "does not refer to a valid field",
  2952  		},
  2953  		{
  2954  			name:            "Valid with whitespace inside field",
  2955  			fieldPath:       ".white space",
  2956  			pathOfFieldPath: path,
  2957  			schema:          &sts,
  2958  		},
  2959  		{
  2960  			name:            "Valid with whitespace inside field",
  2961  			fieldPath:       "['white space']",
  2962  			pathOfFieldPath: path,
  2963  			schema:          &sts,
  2964  		},
  2965  		{
  2966  			name:            "invalid dot annotation",
  2967  			fieldPath:       ".a.bb[b",
  2968  			pathOfFieldPath: path,
  2969  			schema:          &sts,
  2970  			errDetail:       "does not refer to a valid field",
  2971  		},
  2972  		{
  2973  			name:            "valid with .",
  2974  			fieldPath:       ".a['bbb.c']",
  2975  			pathOfFieldPath: path,
  2976  			schema:          &sts,
  2977  			validFieldPath:  path.Child("a", "bbb.c"),
  2978  		},
  2979  		{
  2980  			name:            "Unclosed ]",
  2981  			fieldPath:       ".a['bbb.c'",
  2982  			pathOfFieldPath: path,
  2983  			schema:          &sts,
  2984  			errDetail:       "unexpected end of JSON path",
  2985  		},
  2986  		{
  2987  			name:            "Unexpected end of JSON path",
  2988  			fieldPath:       ".",
  2989  			pathOfFieldPath: path,
  2990  			schema:          &sts,
  2991  			errDetail:       "unexpected end of JSON path",
  2992  		},
  2993  		{
  2994  			name:            "Valid map syntax .a.bbb",
  2995  			fieldPath:       ".a['bbb.c']",
  2996  			pathOfFieldPath: path,
  2997  			schema:          &sts,
  2998  			validFieldPath:  path.Child("a").Child("bbb.c"),
  2999  		},
  3000  		{
  3001  			name:            "Valid map key",
  3002  			fieldPath:       ".foo.subAdd",
  3003  			pathOfFieldPath: path,
  3004  			schema:          &sts,
  3005  			validFieldPath:  path.Child("foo").Key("subAdd"),
  3006  		},
  3007  		{
  3008  			name:            "Valid map key",
  3009  			fieldPath:       ".foo['subAdd']",
  3010  			pathOfFieldPath: path,
  3011  			schema:          &sts,
  3012  			validFieldPath:  path.Child("foo").Key("subAdd"),
  3013  		},
  3014  		{
  3015  			name:            "Invalid",
  3016  			fieldPath:       ".a.foo's",
  3017  			pathOfFieldPath: path,
  3018  			schema:          &sts,
  3019  		},
  3020  		{
  3021  			name:            "Escaping",
  3022  			fieldPath:       ".a['foo\\'s']",
  3023  			pathOfFieldPath: path,
  3024  			schema:          &sts,
  3025  			validFieldPath:  path.Child("a").Child("foo's"),
  3026  		},
  3027  		{
  3028  			name:            "Escaping",
  3029  			fieldPath:       ".a['test\\a']",
  3030  			pathOfFieldPath: path,
  3031  			schema:          &sts,
  3032  			validFieldPath:  path.Child("a").Child("test\a"),
  3033  		},
  3034  
  3035  		{
  3036  			name:            "Invalid map key",
  3037  			fieldPath:       ".a.foo",
  3038  			pathOfFieldPath: path,
  3039  			schema:          &sts,
  3040  			errDetail:       "does not refer to a valid field",
  3041  		},
  3042  		{
  3043  			name:            "Malformed map key",
  3044  			fieldPath:       ".a.bbb[0]",
  3045  			pathOfFieldPath: path,
  3046  			schema:          &sts,
  3047  			errDetail:       "expected single quoted string but got 0",
  3048  		},
  3049  		{
  3050  			name:            "Valid refer for name has number",
  3051  			fieldPath:       ".a.bbb.34",
  3052  			pathOfFieldPath: path,
  3053  			schema:          &sts,
  3054  		},
  3055  		{
  3056  			name:            "Map syntax for special field names",
  3057  			fieldPath:       ".a.bbb['34']",
  3058  			pathOfFieldPath: path,
  3059  			schema:          &sts,
  3060  			//errDetail:       "does not refer to a valid field",
  3061  		},
  3062  		{
  3063  			name:            "Valid .list",
  3064  			fieldPath:       ".list",
  3065  			pathOfFieldPath: path,
  3066  			schema:          &sts,
  3067  			validFieldPath:  path.Child("list"),
  3068  		},
  3069  		{
  3070  			name:            "Invalid list index",
  3071  			fieldPath:       ".list[0]",
  3072  			pathOfFieldPath: path,
  3073  			schema:          &sts,
  3074  			errDetail:       "expected single quoted string but got 0",
  3075  		},
  3076  		{
  3077  			name:            "Invalid list reference",
  3078  			fieldPath:       ".list. a",
  3079  			pathOfFieldPath: path,
  3080  			schema:          &sts,
  3081  			errDetail:       "does not refer to a valid field",
  3082  		},
  3083  		{
  3084  			name:            "Invalid .list.a",
  3085  			fieldPath:       ".list['a']",
  3086  			pathOfFieldPath: path,
  3087  			schema:          &sts,
  3088  			errDetail:       "does not refer to a valid field",
  3089  		},
  3090  		{
  3091  			name:            "Missing leading dot",
  3092  			fieldPath:       "a",
  3093  			pathOfFieldPath: path,
  3094  			schema:          &sts,
  3095  			errDetail:       "expected [ or . but got: a",
  3096  		},
  3097  		{
  3098  			name:            "Nonexistent field",
  3099  			fieldPath:       ".c",
  3100  			pathOfFieldPath: path,
  3101  			schema:          &sts,
  3102  			errDetail:       "does not refer to a valid field",
  3103  		},
  3104  		{
  3105  			name:            "Duplicate dots",
  3106  			fieldPath:       ".a..b",
  3107  			pathOfFieldPath: path,
  3108  			schema:          &sts,
  3109  			errDetail:       "does not refer to a valid field",
  3110  		},
  3111  		{
  3112  			name:            "object of array",
  3113  			fieldPath:       ".list.a-b.34",
  3114  			pathOfFieldPath: path,
  3115  			schema:          &sts,
  3116  			errDetail:       "does not refer to a valid field",
  3117  		},
  3118  	}
  3119  
  3120  	for _, tc := range tests {
  3121  		t.Run(tc.name, func(t *testing.T) {
  3122  			validField, _, err := ValidFieldPath(tc.fieldPath, tc.schema)
  3123  
  3124  			if err == nil && tc.errDetail != "" {
  3125  				t.Errorf("expected err contains: %v but get nil", tc.errDetail)
  3126  			}
  3127  			if err != nil && tc.errDetail == "" {
  3128  				t.Errorf("unexpected error: %v", err)
  3129  			}
  3130  			if err != nil && !strings.Contains(err.Error(), tc.errDetail) {
  3131  				t.Errorf("expected error to contain: %v, but get: %v", tc.errDetail, err)
  3132  			}
  3133  			if tc.validFieldPath != nil && tc.validFieldPath.String() != path.Child(validField.String()).String() {
  3134  				t.Errorf("expected %v, got %v", tc.validFieldPath, path.Child(validField.String()))
  3135  			}
  3136  		})
  3137  	}
  3138  }
  3139  
  3140  // FixTabsOrDie counts the number of tab characters preceding the first
  3141  // line in the given yaml object. It removes that many tabs from every
  3142  // line. It panics (it's a test funvion) if some line has fewer tabs
  3143  // than the first line.
  3144  //
  3145  // The purpose of this is to make it easier to read tests.
  3146  func FixTabsOrDie(in string) string {
  3147  	lines := bytes.Split([]byte(in), []byte{'\n'})
  3148  	if len(lines[0]) == 0 && len(lines) > 1 {
  3149  		lines = lines[1:]
  3150  	}
  3151  	// Create prefix made of tabs that we want to remove.
  3152  	var prefix []byte
  3153  	for _, c := range lines[0] {
  3154  		if c != '\t' {
  3155  			break
  3156  		}
  3157  		prefix = append(prefix, byte('\t'))
  3158  	}
  3159  	// Remove prefix from all tabs, fail otherwise.
  3160  	for i := range lines {
  3161  		line := lines[i]
  3162  		// It's OK for the last line to be blank (trailing \n)
  3163  		if i == len(lines)-1 && len(line) <= len(prefix) && bytes.TrimSpace(line) == nil {
  3164  			lines[i] = []byte{}
  3165  			break
  3166  		}
  3167  		if !bytes.HasPrefix(line, prefix) {
  3168  			minRange := i - 5
  3169  			maxRange := i + 5
  3170  			if minRange < 0 {
  3171  				minRange = 0
  3172  			}
  3173  			if maxRange > len(lines) {
  3174  				maxRange = len(lines)
  3175  			}
  3176  			panic(fmt.Errorf("line %d doesn't start with expected number (%d) of tabs (%v-%v):\n%v", i, len(prefix), minRange, maxRange, string(bytes.Join(lines[minRange:maxRange], []byte{'\n'}))))
  3177  		}
  3178  		lines[i] = line[len(prefix):]
  3179  	}
  3180  
  3181  	joined := string(bytes.Join(lines, []byte{'\n'}))
  3182  
  3183  	// Convert rest of tabs to spaces since yaml doesnt like yabs
  3184  	// (assuming 2 space alignment)
  3185  	return strings.ReplaceAll(joined, "\t", "  ")
  3186  }
  3187  
  3188  // Creates a *spec.Schema Schema by decoding the given YAML. Panics on error
  3189  func mustSchema(source string) *schema.Structural {
  3190  	source = FixTabsOrDie(source)
  3191  	d := yaml.NewYAMLOrJSONDecoder(strings.NewReader(source), 4096)
  3192  	props := &apiextensions.JSONSchemaProps{}
  3193  	if err := d.Decode(props); err != nil {
  3194  		panic(err)
  3195  	}
  3196  	convertedProps := &apiextensionsinternal.JSONSchemaProps{}
  3197  	if err := apiextensions.Convert_v1_JSONSchemaProps_To_apiextensions_JSONSchemaProps(props, convertedProps, nil); err != nil {
  3198  		panic(err)
  3199  	}
  3200  
  3201  	res, err := schema.NewStructural(convertedProps)
  3202  	if err != nil {
  3203  		panic(err)
  3204  	}
  3205  	return res
  3206  }
  3207  
  3208  // Creates an *unstructured by decoding the given YAML. Panics on error
  3209  func mustUnstructured(source string) interface{} {
  3210  	source = FixTabsOrDie(source)
  3211  	d := yaml.NewYAMLOrJSONDecoder(strings.NewReader(source), 4096)
  3212  	var res interface{}
  3213  	if err := d.Decode(&res); err != nil {
  3214  		panic(err)
  3215  	}
  3216  	return res
  3217  }
  3218  
  3219  type warningRecorder struct {
  3220  	mu       sync.Mutex
  3221  	warnings []string
  3222  }
  3223  
  3224  // AddWarning adds a warning to recorder.
  3225  func (r *warningRecorder) AddWarning(agent, text string) {
  3226  	r.mu.Lock()
  3227  	defer r.mu.Unlock()
  3228  	r.warnings = append(r.warnings, text)
  3229  }
  3230  
  3231  func (r *warningRecorder) Warnings() []string {
  3232  	r.mu.Lock()
  3233  	defer r.mu.Unlock()
  3234  
  3235  	warnings := make([]string, len(r.warnings))
  3236  	copy(warnings, r.warnings)
  3237  	return warnings
  3238  }
  3239  
  3240  func TestRatcheting(t *testing.T) {
  3241  	cases := []struct {
  3242  		name   string
  3243  		schema *schema.Structural
  3244  		oldObj interface{}
  3245  		newObj interface{}
  3246  
  3247  		// Errors that should occur when evaluating this operation with
  3248  		// ratcheting feature enabled
  3249  		errors []string
  3250  
  3251  		// Errors that should occur when evaluating this operation with
  3252  		// ratcheting feature disabled
  3253  		// These errors should be raised as warnings when ratcheting is enabled
  3254  		warnings []string
  3255  
  3256  		runtimeCostBudget int64
  3257  	}{
  3258  		{
  3259  			name: "normal CEL expression",
  3260  			schema: mustSchema(`
  3261  				type: object
  3262  				properties:
  3263  					foo:
  3264  						type: string
  3265  						x-kubernetes-validations:
  3266  						- rule: self == "bar"
  3267  						  message: "gotta be baz"
  3268  				`),
  3269  			oldObj: mustUnstructured(`
  3270  				foo: baz
  3271  			`),
  3272  			newObj: mustUnstructured(`
  3273  				foo: baz
  3274  			`),
  3275  			warnings: []string{
  3276  				`root.foo: Invalid value: "string": gotta be baz`,
  3277  			},
  3278  		},
  3279  		{
  3280  			name: "normal CEL expression thats a descendent of an atomic array whose parent is totally unchanged",
  3281  			schema: mustSchema(`
  3282  				type: array
  3283  				x-kubernetes-list-type: atomic
  3284  				items:
  3285  					type: object
  3286  					properties:
  3287  						bar:
  3288  							type: string
  3289  							x-kubernetes-validations:
  3290  							- rule: self == "baz"
  3291  							  message: "gotta be baz"
  3292  				`),
  3293  			// CEL error comes from uncorrelatable portion of the schema,
  3294  			// but it should be ratcheted anyway because it is the descendent
  3295  			// of an unchanged correlatable node
  3296  			oldObj: mustUnstructured(`
  3297  				- bar: bar
  3298  			`),
  3299  			newObj: mustUnstructured(`
  3300  				- bar: bar
  3301  			`),
  3302  			warnings: []string{
  3303  				`root[0].bar: Invalid value: "string": gotta be baz`,
  3304  			},
  3305  		},
  3306  		{
  3307  			name: "normal CEL expression thats a descendent of a set whose parent is totally unchanged",
  3308  			schema: mustSchema(`
  3309  				type: array
  3310  				x-kubernetes-list-type: set
  3311  				items:
  3312  					type: number
  3313  					x-kubernetes-validations:
  3314  					- rule: int(self) % 2 == 1
  3315  					  message: "gotta be odd"
  3316  				`),
  3317  			// CEL error comes from uncorrelatable portion of the schema,
  3318  			// but it should be ratcheted anyway because it is the descendent
  3319  			// of an unchanged correlatable node
  3320  			oldObj: mustUnstructured(`
  3321  				- 1
  3322  				- 2
  3323  			`),
  3324  			newObj: mustUnstructured(`
  3325  				- 1
  3326  				- 2
  3327  			`),
  3328  			warnings: []string{
  3329  				`root[1]: Invalid value: "number": gotta be odd`,
  3330  			},
  3331  		},
  3332  		{
  3333  			name: "normal CEL expression thats a descendent of a set and one of its siblings has changed",
  3334  			schema: mustSchema(`
  3335  				type: object
  3336  				properties:
  3337  					stringField:
  3338  						type: string
  3339  					setArray:
  3340  						type: array
  3341  						x-kubernetes-list-type: set
  3342  						items:
  3343  							type: number
  3344  							x-kubernetes-validations:
  3345  							- rule: int(self) % 2 == 1
  3346  							  message: "gotta be odd"
  3347  				`),
  3348  			oldObj: mustUnstructured(`
  3349  				stringField: foo
  3350  				setArray:
  3351  				- 1
  3352  				- 3
  3353  				- 2
  3354  			`),
  3355  			newObj: mustUnstructured(`
  3356  				stringField: changed but ratcheted
  3357  				setArray:
  3358  				- 1
  3359  				- 3
  3360  				- 2
  3361  			`),
  3362  			warnings: []string{
  3363  				`root.setArray[2]: Invalid value: "number": gotta be odd`,
  3364  			},
  3365  		},
  3366  		{
  3367  			name: "descendent of a map list whose parent is unchanged",
  3368  			schema: mustSchema(`
  3369  				type: array
  3370  				x-kubernetes-list-type: map
  3371  				x-kubernetes-list-map-keys: ["key"]
  3372  				items:
  3373  					type: object
  3374  					properties:
  3375  						key:
  3376  							type: string
  3377  						value:
  3378  							type: string
  3379  							x-kubernetes-validations:
  3380  							- rule: self == "baz"
  3381  							  message: "gotta be baz"
  3382  			`),
  3383  			oldObj: mustUnstructured(`
  3384  				- key: foo
  3385  				  value: notbaz
  3386  				- key: bar
  3387  				  value: notbaz
  3388  			`),
  3389  			newObj: mustUnstructured(`
  3390  				- key: foo
  3391  				  value: notbaz
  3392  				- key: bar
  3393  				  value: notbaz
  3394  				- key: baz
  3395  				  value: baz
  3396  			`),
  3397  			warnings: []string{
  3398  				`root[0].value: Invalid value: "string": gotta be baz`,
  3399  				`root[1].value: Invalid value: "string": gotta be baz`,
  3400  			},
  3401  		},
  3402  		{
  3403  			name: "descendent of a map list whose siblings have changed",
  3404  			schema: mustSchema(`
  3405  				type: array
  3406  				x-kubernetes-list-type: map
  3407  				x-kubernetes-list-map-keys: ["key"]
  3408  				items:
  3409  					type: object
  3410  					properties:
  3411  						key:
  3412  							type: string
  3413  						value:
  3414  							type: string
  3415  							x-kubernetes-validations:
  3416  							- rule: self == "baz"
  3417  							  message: "gotta be baz"
  3418  			`),
  3419  			oldObj: mustUnstructured(`
  3420  				- key: foo
  3421  				  value: notbaz
  3422  				- key: bar
  3423  				  value: notbaz
  3424  			`),
  3425  			newObj: mustUnstructured(`
  3426  				- key: foo
  3427  				  value: baz
  3428  				- key: bar
  3429  				  value: notbaz
  3430  			`),
  3431  			warnings: []string{
  3432  				`root[1].value: Invalid value: "string": gotta be baz`,
  3433  			},
  3434  		},
  3435  		{
  3436  			name: "descendent of a map whose parent is totally unchanged",
  3437  			schema: mustSchema(`
  3438  				type: object
  3439  				properties:
  3440  					stringField:
  3441  						type: string
  3442  					mapField:
  3443  						type: object
  3444  						properties:
  3445  							foo:
  3446  								type: string
  3447  								x-kubernetes-validations:
  3448  								- rule: self == "baz"
  3449  								  message: "gotta be baz"
  3450  							mapField:
  3451  								type: object
  3452  								properties:
  3453  									bar:
  3454  										type: string
  3455  										x-kubernetes-validations:
  3456  										- rule: self == "baz"
  3457  										  message: "gotta be nested baz"
  3458  				`),
  3459  			oldObj: mustUnstructured(`
  3460  				stringField: foo
  3461  				mapField:
  3462  					foo: notbaz
  3463  					mapField:
  3464  						bar: notbaz
  3465  			`),
  3466  			newObj: mustUnstructured(`
  3467  				stringField: foo
  3468  				mapField:
  3469  					foo: notbaz
  3470  					mapField:
  3471  						bar: notbaz
  3472  			`),
  3473  			warnings: []string{
  3474  				`root.mapField.foo: Invalid value: "string": gotta be baz`,
  3475  				`root.mapField.mapField.bar: Invalid value: "string": gotta be nested baz`,
  3476  			},
  3477  		},
  3478  		{
  3479  			name: "descendent of a map whose siblings have changed",
  3480  			schema: mustSchema(`
  3481  				type: object
  3482  				properties:
  3483  					stringField:
  3484  						type: string
  3485  					mapField:
  3486  						type: object
  3487  						properties:
  3488  							foo:
  3489  								type: string
  3490  								x-kubernetes-validations:
  3491  								- rule: self == "baz"
  3492  								  message: "gotta be baz"
  3493  							mapField:
  3494  								type: object
  3495  								properties:
  3496  									bar:
  3497  										type: string
  3498  										x-kubernetes-validations:
  3499  										- rule: self == "baz"
  3500  										  message: "gotta be baz"
  3501  									otherBar:
  3502  										type: string
  3503  										x-kubernetes-validations:
  3504  										- rule: self == "otherBaz"
  3505  										  message: "gotta be otherBaz"
  3506  				`),
  3507  			oldObj: mustUnstructured(`
  3508  				stringField: foo
  3509  				mapField:
  3510  					foo: baz
  3511  					mapField:
  3512  						bar: notbaz
  3513  						otherBar: nototherBaz
  3514  			`),
  3515  			newObj: mustUnstructured(`
  3516  				stringField: foo
  3517  				mapField:
  3518  					foo: notbaz
  3519  					mapField:
  3520  						bar: notbaz
  3521  						otherBar: otherBaz
  3522  			`),
  3523  			errors: []string{
  3524  				// Didn't get ratcheted because we changed its value from baz to notbaz
  3525  				`root.mapField.foo: Invalid value: "string": gotta be baz`,
  3526  			},
  3527  			warnings: []string{
  3528  				// Ratcheted because its value remained the same, even though it is invalid
  3529  				`root.mapField.mapField.bar: Invalid value: "string": gotta be baz`,
  3530  			},
  3531  		},
  3532  		{
  3533  			name: "normal CEL expression thats a descendent of an atomic array whose siblings has changed",
  3534  			schema: mustSchema(`
  3535  				type: object
  3536  				properties:
  3537  					stringField:
  3538  						type: string
  3539  					atomicArray:
  3540  						type: array
  3541  						x-kubernetes-list-type: atomic
  3542  						items:
  3543  							type: object
  3544  							properties:
  3545  								bar:
  3546  									type: string
  3547  									x-kubernetes-validations:
  3548  									- rule: self == "baz"
  3549  									  message: "gotta be baz"
  3550  				`),
  3551  			oldObj: mustUnstructured(`
  3552  				stringField: foo
  3553  				atomicArray:
  3554  				- bar: bar
  3555  			`),
  3556  			newObj: mustUnstructured(`
  3557  				stringField: changed but ratcheted
  3558  				atomicArray:
  3559  				- bar: bar
  3560  			`),
  3561  			warnings: []string{
  3562  				`root.atomicArray[0].bar: Invalid value: "string": gotta be baz`,
  3563  			},
  3564  		},
  3565  		{
  3566  			name: "we can't ratchet a normal CEL expression from an uncorrelatable part of the schema whose parent nodes has changed",
  3567  			schema: mustSchema(`
  3568  				type: array
  3569  				x-kubernetes-list-type: atomic
  3570  				items:
  3571  					type: object
  3572  					properties:
  3573  						bar:
  3574  							type: string
  3575  							x-kubernetes-validations:
  3576  							- rule: self == "baz"
  3577  							  message: "gotta be baz"
  3578  			`),
  3579  			// CEL error comes from uncorrelatable portion of the schema,
  3580  			// but it should be ratcheted anyway because it is the descendent
  3581  			// or an unchanged correlatable node
  3582  			oldObj: mustUnstructured(`
  3583  				- bar: bar
  3584  			`),
  3585  			newObj: mustUnstructured(`
  3586  				- bar: bar
  3587  				- bar: baz
  3588  			`),
  3589  			errors: []string{
  3590  				`root[0].bar: Invalid value: "string": gotta be baz`,
  3591  			},
  3592  		},
  3593  		{
  3594  			name: "transition rules never ratchet for correlatable schemas",
  3595  			schema: mustSchema(`
  3596  				type: object
  3597  				properties:
  3598  					foo:
  3599  						type: string
  3600  						x-kubernetes-validations:
  3601  						- rule: oldSelf != "bar" && self == "baz"
  3602  						  message: gotta be baz
  3603  			`),
  3604  			oldObj: mustUnstructured(`
  3605  				foo: bar
  3606  			`),
  3607  			newObj: mustUnstructured(`
  3608  				foo: bar
  3609  			`),
  3610  			errors: []string{
  3611  				`root.foo: Invalid value: "string": gotta be baz`,
  3612  			},
  3613  		},
  3614  		{
  3615  			name: "changing field path does not change ratcheting logic",
  3616  			schema: mustSchema(`
  3617  				type: object
  3618  				x-kubernetes-validations:
  3619  				- rule: self.foo == "baz"
  3620  				  message: gotta be baz
  3621  				  fieldPath: ".foo"
  3622  				properties:
  3623  					bar:
  3624  						type: string
  3625  					foo:
  3626  						type: string
  3627  			`),
  3628  			oldObj: mustUnstructured(`
  3629  				foo: bar
  3630  			`),
  3631  			// Fieldpath is on unchanged field `foo`, but since rule is on the
  3632  			// changed parent object we still get an error
  3633  			newObj: mustUnstructured(`
  3634  				foo: bar
  3635  				bar: invalid
  3636  			`),
  3637  			errors: []string{
  3638  				`root.foo: Invalid value: "object": gotta be baz`,
  3639  			},
  3640  		},
  3641  		{
  3642  			name: "cost budget errors are not ratcheted",
  3643  			schema: mustSchema(`
  3644  				type: string
  3645  				minLength: 5
  3646  				x-kubernetes-validations:
  3647  				- rule: self == "baz"
  3648  				  message: gotta be baz
  3649  			`),
  3650  			oldObj:            "unchanged",
  3651  			newObj:            "unchanged",
  3652  			runtimeCostBudget: 1,
  3653  			errors: []string{
  3654  				`validation failed due to running out of cost budget, no further validation rules will be run`,
  3655  			},
  3656  		},
  3657  		{
  3658  			name: "compile errors are not ratcheted",
  3659  			schema: mustSchema(`
  3660  				type: string
  3661  				x-kubernetes-validations:
  3662  				- rule: asdausidyhASDNJm
  3663  				  message: gotta be baz
  3664  			`),
  3665  			oldObj: "unchanged",
  3666  			newObj: "unchanged",
  3667  			errors: []string{
  3668  				`rule compile error: compilation failed: ERROR: <input>:1:1: undeclared reference to 'asdausidyhASDNJm'`,
  3669  			},
  3670  		},
  3671  		{
  3672  			name: "typemeta fields are not ratcheted",
  3673  			schema: mustSchema(`
  3674  				type: object
  3675  				properties:
  3676  					apiVersion:
  3677  						type: string
  3678  						x-kubernetes-validations:
  3679  						- rule: self == "v1"
  3680  					kind:
  3681  						type: string
  3682  						x-kubernetes-validations:
  3683  						- rule: self == "Pod"
  3684  			`),
  3685  			oldObj: mustUnstructured(`
  3686  				apiVersion: v2
  3687  				kind: Baz
  3688  			`),
  3689  			newObj: mustUnstructured(`
  3690  				apiVersion: v2
  3691  				kind: Baz
  3692  			`),
  3693  			errors: []string{
  3694  				`root.apiVersion: Invalid value: "string": failed rule: self == "v1"`,
  3695  				`root.kind: Invalid value: "string": failed rule: self == "Pod"`,
  3696  			},
  3697  		},
  3698  		{
  3699  			name: "nested typemeta fields may still be ratcheted",
  3700  			schema: mustSchema(`
  3701  				type: object
  3702  				properties:
  3703  					list:
  3704  						type: array
  3705  						x-kubernetes-list-type: map
  3706  						x-kubernetes-list-map-keys: ["name"]
  3707  						maxItems: 2
  3708  						items:
  3709  							type: object
  3710  							properties:
  3711  								name:
  3712  									type: string
  3713  								apiVersion:
  3714  									type: string
  3715  									x-kubernetes-validations:
  3716  									- rule: self == "v1"
  3717  								kind:
  3718  									type: string
  3719  									x-kubernetes-validations:
  3720  									- rule: self == "Pod"
  3721  					subField:
  3722  						type: object
  3723  						properties:
  3724  							apiVersion:
  3725  								type: string
  3726  								x-kubernetes-validations:
  3727  								- rule: self == "v1"
  3728  							kind:
  3729  								type: string
  3730  								x-kubernetes-validations:
  3731  								- rule: self == "Pod"
  3732  							otherField:
  3733  								type: string
  3734  			`),
  3735  			oldObj: mustUnstructured(`
  3736  				subField:
  3737  					apiVersion: v2
  3738  					kind: Baz
  3739  				list:	
  3740  				- name: entry1
  3741  				  apiVersion: v2
  3742  				  kind: Baz
  3743  				- name: entry2
  3744  				  apiVersion: v3
  3745  				  kind: Bar
  3746  			`),
  3747  			newObj: mustUnstructured(`
  3748  				subField:
  3749  					apiVersion: v2
  3750  					kind: Baz
  3751  					otherField: newValue
  3752  				list:	
  3753  				- name: entry1
  3754  				  apiVersion: v2
  3755  				  kind: Baz
  3756  				  otherField: newValue2
  3757  				- name: entry2
  3758  				  apiVersion: v3
  3759  				  kind: Bar
  3760  				  otherField: newValue3
  3761  			`),
  3762  			warnings: []string{
  3763  				`root.subField.apiVersion: Invalid value: "string": failed rule: self == "v1"`,
  3764  				`root.subField.kind: Invalid value: "string": failed rule: self == "Pod"`,
  3765  				`root.list[0].apiVersion: Invalid value: "string": failed rule: self == "v1"`,
  3766  				`root.list[0].kind: Invalid value: "string": failed rule: self == "Pod"`,
  3767  				`root.list[1].apiVersion: Invalid value: "string": failed rule: self == "v1"`,
  3768  				`root.list[1].kind: Invalid value: "string": failed rule: self == "Pod"`,
  3769  			},
  3770  		},
  3771  	}
  3772  
  3773  	for _, c := range cases {
  3774  		t.Run(c.name, func(t *testing.T) {
  3775  			validator := NewValidator(c.schema, false, celconfig.PerCallLimit)
  3776  			require.NotNil(t, validator)
  3777  			recorder := &warningRecorder{}
  3778  			ctx := warning.WithWarningRecorder(context.TODO(), recorder)
  3779  			budget := c.runtimeCostBudget
  3780  			if budget == 0 {
  3781  				budget = celconfig.RuntimeCELCostBudget
  3782  			}
  3783  			errs, _ := validator.Validate(
  3784  				ctx,
  3785  				field.NewPath("root"),
  3786  				c.schema,
  3787  				c.newObj,
  3788  				c.oldObj,
  3789  				budget,
  3790  				WithRatcheting(common.NewCorrelatedObject(c.newObj, c.oldObj, &model.Structural{Structural: c.schema})),
  3791  			)
  3792  
  3793  			require.Len(t, errs, len(c.errors), "must have expected number of errors")
  3794  			require.Len(t, recorder.Warnings(), len(c.warnings), "must have expected number of warnings")
  3795  
  3796  			// Check that the expected errors were raised
  3797  			for _, expectedErr := range c.errors {
  3798  				found := false
  3799  				for _, err := range errs {
  3800  					if strings.Contains(err.Error(), expectedErr) {
  3801  						found = true
  3802  						break
  3803  					}
  3804  				}
  3805  
  3806  				assert.True(t, found, "expected error %q not found", expectedErr)
  3807  			}
  3808  
  3809  			// Check that the ratcheting disabled errors were raised as warnings
  3810  			for _, expectedWarning := range c.warnings {
  3811  				found := false
  3812  				for _, warning := range recorder.Warnings() {
  3813  					if warning == expectedWarning {
  3814  						found = true
  3815  						break
  3816  					}
  3817  				}
  3818  				assert.True(t, found, "expected warning %q not found", expectedWarning)
  3819  			}
  3820  
  3821  		})
  3822  	}
  3823  }
  3824  
  3825  // Runs transition rule cases with OptionalOldSelf set to true on the schema
  3826  func TestOptionalOldSelf(t *testing.T) {
  3827  	defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, apiextensionsfeatures.CRDValidationRatcheting, true)()
  3828  
  3829  	tests := []struct {
  3830  		name   string
  3831  		schema *schema.Structural
  3832  		obj    interface{}
  3833  		oldObj interface{}
  3834  		errors []string // strings that error message must contain
  3835  	}{
  3836  		{
  3837  			name: "allow new value if old value is null",
  3838  			obj: map[string]interface{}{
  3839  				"foo": "bar",
  3840  			},
  3841  			schema: withRulePtr(objectTypePtr(map[string]schema.Structural{
  3842  				"foo": stringType,
  3843  			}), "self.foo == 'not bar' || !oldSelf.hasValue()"),
  3844  		},
  3845  		{
  3846  			name: "block new value if old value is not null",
  3847  			obj: map[string]interface{}{
  3848  				"foo": "invalid",
  3849  			},
  3850  			oldObj: map[string]interface{}{
  3851  				"foo": "bar",
  3852  			},
  3853  			schema: withRulePtr(objectTypePtr(map[string]schema.Structural{
  3854  				"foo": stringType,
  3855  			}), "self.foo == 'valid' || !oldSelf.hasValue()"),
  3856  			errors: []string{"failed rule"},
  3857  		},
  3858  		{
  3859  			name: "allow invalid new value if old value is also invalid",
  3860  			obj: map[string]interface{}{
  3861  				"foo": "invalid again",
  3862  			},
  3863  			oldObj: map[string]interface{}{
  3864  				"foo": "invalid",
  3865  			},
  3866  			schema: withRulePtr(objectTypePtr(map[string]schema.Structural{
  3867  				"foo": stringType,
  3868  			}), "self.foo == 'valid' || (oldSelf.hasValue() && oldSelf.value().foo != 'valid')"),
  3869  		},
  3870  		{
  3871  			name: "allow invalid new value if old value is also invalid with chained optionals",
  3872  			obj: map[string]interface{}{
  3873  				"foo": "invalid again",
  3874  			},
  3875  			oldObj: map[string]interface{}{
  3876  				"foo": "invalid",
  3877  			},
  3878  			schema: withRulePtr(objectTypePtr(map[string]schema.Structural{
  3879  				"foo": stringType,
  3880  			}), "self.foo == 'valid' || oldSelf.foo.orValue('') != 'valid'"),
  3881  		},
  3882  		{
  3883  			name: "block invalid new value if old value is valid",
  3884  			obj: map[string]interface{}{
  3885  				"foo": "invalid",
  3886  			},
  3887  			oldObj: map[string]interface{}{
  3888  				"foo": "valid",
  3889  			},
  3890  			schema: withRulePtr(objectTypePtr(map[string]schema.Structural{
  3891  				"foo": stringType,
  3892  			}), "self.foo == 'valid' || (oldSelf.hasValue() && oldSelf.value().foo != 'valid')"),
  3893  			errors: []string{"failed rule"},
  3894  		},
  3895  		{
  3896  			name:   "create: new min or allow higher than oldValue",
  3897  			obj:    10,
  3898  			schema: cloneWithRule(&integerType, "self >= 10 || (oldSelf.hasValue() && oldSelf.value() <= self)"),
  3899  		},
  3900  		{
  3901  			name: "block create: new min or allow higher than oldValue",
  3902  			obj:  9,
  3903  			// Can't use != null because type is integer and no overload
  3904  			// workaround by comparing type, but kinda hacky
  3905  			schema: cloneWithRule(&integerType, "self >= 10 || (oldSelf.hasValue() && oldSelf.value() <= self)"),
  3906  			errors: []string{"failed rule"},
  3907  		},
  3908  		{
  3909  			name:   "update: new min or allow higher than oldValue",
  3910  			obj:    10,
  3911  			oldObj: 5,
  3912  			schema: cloneWithRule(&integerType, "self >= 10 || (oldSelf.hasValue() && oldSelf.value() <= self)"),
  3913  		},
  3914  		{
  3915  			name:   "ratchet update: new min or allow higher than oldValue",
  3916  			obj:    9,
  3917  			oldObj: 5,
  3918  			schema: cloneWithRule(&integerType, "self >= 10 || (oldSelf.hasValue() && oldSelf.value() <= self)"),
  3919  		},
  3920  		{
  3921  			name:   "ratchet noop update: new min or allow higher than oldValue",
  3922  			obj:    5,
  3923  			oldObj: 5,
  3924  			schema: cloneWithRule(&integerType, "self >= 10 || (oldSelf.hasValue() && oldSelf.value() <= self)"),
  3925  		},
  3926  		{
  3927  			name:   "block update: new min or allow higher than oldValue",
  3928  			obj:    4,
  3929  			oldObj: 5,
  3930  			schema: cloneWithRule(&integerType, "self >= 10 || (oldSelf.hasValue() && oldSelf.value() <= self)"),
  3931  			errors: []string{"failed rule"},
  3932  		},
  3933  	}
  3934  
  3935  	for _, tt := range tests {
  3936  		tt := tt
  3937  		tp := true
  3938  		for i := range tt.schema.XValidations {
  3939  			tt.schema.XValidations[i].OptionalOldSelf = &tp
  3940  		}
  3941  
  3942  		t.Run(tt.name, func(t *testing.T) {
  3943  			// t.Parallel()
  3944  
  3945  			ctx := context.TODO()
  3946  			celValidator := validator(tt.schema, true, model.SchemaDeclType(tt.schema, false), celconfig.PerCallLimit)
  3947  			if celValidator == nil {
  3948  				t.Fatal("expected non nil validator")
  3949  			}
  3950  			errs, _ := celValidator.Validate(ctx, field.NewPath("root"), tt.schema, tt.obj, tt.oldObj, math.MaxInt)
  3951  			unmatched := map[string]struct{}{}
  3952  			for _, e := range tt.errors {
  3953  				unmatched[e] = struct{}{}
  3954  			}
  3955  			for _, err := range errs {
  3956  				if err.Type != field.ErrorTypeInvalid {
  3957  					t.Errorf("expected only ErrorTypeInvalid errors, but got: %v", err)
  3958  					continue
  3959  				}
  3960  				matched := false
  3961  				for expected := range unmatched {
  3962  					if strings.Contains(err.Error(), expected) {
  3963  						delete(unmatched, expected)
  3964  						matched = true
  3965  						break
  3966  					}
  3967  				}
  3968  				if !matched {
  3969  					t.Errorf("expected error to contain one of %v, but got: %v", unmatched, err)
  3970  				}
  3971  			}
  3972  			if len(unmatched) > 0 {
  3973  				t.Errorf("expected errors %v", unmatched)
  3974  			}
  3975  		})
  3976  	}
  3977  }
  3978  
  3979  // Shows that type(oldSelf) == null_type works for all supported OpenAPI types
  3980  // both when oldSelf is null and when it is not null
  3981  func TestOptionalOldSelfCheckForNull(t *testing.T) {
  3982  	defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, apiextensionsfeatures.CRDValidationRatcheting, true)()
  3983  
  3984  	tests := []struct {
  3985  		name   string
  3986  		schema schema.Structural
  3987  		obj    interface{}
  3988  		oldObj interface{}
  3989  	}{
  3990  		{
  3991  			name: "object",
  3992  			obj: map[string]interface{}{
  3993  				"foo": "bar",
  3994  			},
  3995  			oldObj: map[string]interface{}{
  3996  				"foo": "baz",
  3997  			},
  3998  			schema: withRule(objectType(map[string]schema.Structural{
  3999  				"foo": stringType,
  4000  			}), `!oldSelf.hasValue() || self.foo == "bar"`),
  4001  		},
  4002  		{
  4003  			name: "object - conditional field",
  4004  			obj: map[string]interface{}{
  4005  				"foo": "bar",
  4006  			},
  4007  			oldObj: map[string]interface{}{
  4008  				"foo": "baz",
  4009  			},
  4010  			schema: withRule(objectType(map[string]schema.Structural{
  4011  				"foo": stringType,
  4012  			}), `self.foo != "bar" || oldSelf.?foo.orValue("baz") == "baz"`),
  4013  		},
  4014  		{
  4015  			name:   "string",
  4016  			obj:    "bar",
  4017  			oldObj: "baz",
  4018  			schema: withRule(stringType, `
  4019  				!oldSelf.hasValue() || self == "bar"
  4020  			`),
  4021  		},
  4022  		{
  4023  			name:   "integer",
  4024  			obj:    1,
  4025  			oldObj: 2,
  4026  			schema: withRule(integerType, `
  4027  				!oldSelf.hasValue() || self == 1
  4028  			`),
  4029  		},
  4030  		{
  4031  			name:   "number",
  4032  			obj:    1.1,
  4033  			oldObj: 2.2,
  4034  			schema: withRule(numberType, `
  4035  				!oldSelf.hasValue() || self == 1.1
  4036  			`),
  4037  		},
  4038  		{
  4039  			name:   "boolean",
  4040  			obj:    true,
  4041  			oldObj: false,
  4042  			schema: withRule(booleanType, `
  4043  				!oldSelf.hasValue() || self == true
  4044  			`),
  4045  		},
  4046  		{
  4047  			name:   "array",
  4048  			obj:    []interface{}{"bar"},
  4049  			oldObj: []interface{}{"baz"},
  4050  			schema: withRule(arrayType("", nil, &stringSchema), `
  4051  				!oldSelf.hasValue() || self[0] == "bar"
  4052  			`),
  4053  		},
  4054  		{
  4055  			name: "array - conditional index",
  4056  			obj:  []interface{}{},
  4057  			oldObj: []interface{}{
  4058  				"baz",
  4059  			},
  4060  			schema: withRule(arrayType("", nil, &stringSchema), `
  4061  				self.size() > 0 || oldSelf[?0].orValue("baz") == "baz"
  4062  			`),
  4063  		},
  4064  		{
  4065  			name:   "set-array",
  4066  			obj:    []interface{}{"bar"},
  4067  			oldObj: []interface{}{"baz"},
  4068  			schema: withRule(arrayType("set", nil, &stringSchema), `
  4069  				!oldSelf.hasValue() || self[0] == "bar"
  4070  			`),
  4071  		},
  4072  		{
  4073  			name: "map-array",
  4074  			obj: []interface{}{map[string]interface{}{
  4075  				"key":   "foo",
  4076  				"value": "bar",
  4077  			}},
  4078  			oldObj: []interface{}{map[string]interface{}{
  4079  				"key":   "foo",
  4080  				"value": "baz",
  4081  			}},
  4082  			schema: withRule(arrayType("map", []string{"key"}, objectTypePtr(map[string]schema.Structural{
  4083  				"key":   stringType,
  4084  				"value": stringType,
  4085  			})), `
  4086  				!oldSelf.hasValue() || self[0].value == "bar"
  4087  			`),
  4088  		},
  4089  	}
  4090  
  4091  	for _, tt := range tests {
  4092  		tt := tt
  4093  		tp := true
  4094  		for i := range tt.schema.XValidations {
  4095  			tt.schema.XValidations[i].OptionalOldSelf = &tp
  4096  		}
  4097  
  4098  		t.Run(tt.name, func(t *testing.T) {
  4099  			ctx := context.TODO()
  4100  			celValidator := validator(&tt.schema, false, model.SchemaDeclType(&tt.schema, false), celconfig.PerCallLimit)
  4101  			if celValidator == nil {
  4102  				t.Fatal("expected non nil validator")
  4103  			}
  4104  
  4105  			t.Run("null old", func(t *testing.T) {
  4106  				errs, _ := celValidator.Validate(ctx, field.NewPath("root"), &tt.schema, tt.obj, nil, math.MaxInt)
  4107  				if len(errs) != 0 {
  4108  					t.Errorf("expected no errors, but got: %v", errs)
  4109  				}
  4110  			})
  4111  
  4112  			t.Run("non-null old", func(t *testing.T) {
  4113  				errs, _ := celValidator.Validate(ctx, field.NewPath("root"), &tt.schema, tt.obj, tt.oldObj, math.MaxInt)
  4114  				if len(errs) != 0 {
  4115  					t.Errorf("expected no errors, but got: %v", errs)
  4116  				}
  4117  			})
  4118  		})
  4119  	}
  4120  }
  4121  
  4122  // Show that we cant just use oldSelf as if it was unwrapped
  4123  func TestOptionalOldSelfIsOptionalType(t *testing.T) {
  4124  	defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, apiextensionsfeatures.CRDValidationRatcheting, true)()
  4125  
  4126  	cases := []struct {
  4127  		name   string
  4128  		schema schema.Structural
  4129  		obj    interface{}
  4130  		errors []string
  4131  	}{
  4132  		{
  4133  			name: "forbid direct usage of optional integer",
  4134  			schema: withRule(integerType, `
  4135  				oldSelf + self > 5
  4136  			`),
  4137  			obj:    5,
  4138  			errors: []string{"no matching overload for '_+_' applied to '(optional(int), int)"},
  4139  		},
  4140  		{
  4141  			name: "forbid direct usage of optional string",
  4142  			schema: withRule(stringType, `
  4143  				oldSelf == "foo"
  4144  			`),
  4145  			obj:    "bar",
  4146  			errors: []string{"no matching overload for '_==_' applied to '(optional(string), string)"},
  4147  		},
  4148  		{
  4149  			name: "forbid direct usage of optional array",
  4150  			schema: withRule(arrayType("", nil, &stringSchema), `
  4151  				oldSelf.all(x, x == x)
  4152  			`),
  4153  			obj:    []interface{}{"bar"},
  4154  			errors: []string{"expression of type 'optional(list(string))' cannot be range of a comprehension"},
  4155  		},
  4156  		{
  4157  			name: "forbid direct usage of optional array element",
  4158  			schema: withRule(arrayType("", nil, &stringSchema), `
  4159  				oldSelf[0] == "foo"
  4160  			`),
  4161  			obj:    []interface{}{"bar"},
  4162  			errors: []string{"found no matching overload for '_==_' applied to '(optional(string), string)"},
  4163  		},
  4164  		{
  4165  			name: "forbid direct usage of optional struct",
  4166  			schema: withRule(arrayType("map", []string{"key"}, objectTypePtr(map[string]schema.Structural{
  4167  				"key":   stringType,
  4168  				"value": stringType,
  4169  			})), `oldSelf.key == "foo"`),
  4170  			obj: []interface{}{map[string]interface{}{
  4171  				"key":   "bar",
  4172  				"value": "baz",
  4173  			}},
  4174  			errors: []string{"does not support field selection"},
  4175  		},
  4176  	}
  4177  
  4178  	for _, tt := range cases {
  4179  		t.Run(tt.name, func(t *testing.T) {
  4180  			ctx := context.TODO()
  4181  
  4182  			for i := range tt.schema.XValidations {
  4183  				tt.schema.XValidations[i].OptionalOldSelf = ptr.To(true)
  4184  			}
  4185  
  4186  			celValidator := validator(&tt.schema, false, model.SchemaDeclType(&tt.schema, false), celconfig.PerCallLimit)
  4187  			if celValidator == nil {
  4188  				t.Fatal("expected non nil validator")
  4189  			}
  4190  			errs, _ := celValidator.Validate(ctx, field.NewPath("root"), &tt.schema, tt.obj, tt.obj, math.MaxInt)
  4191  			unmatched := map[string]struct{}{}
  4192  			for _, e := range tt.errors {
  4193  				unmatched[e] = struct{}{}
  4194  			}
  4195  			for _, err := range errs {
  4196  				if err.Type != field.ErrorTypeInvalid {
  4197  					t.Errorf("expected only ErrorTypeInvalid errors, but got: %v", err)
  4198  					continue
  4199  				}
  4200  				matched := false
  4201  				for expected := range unmatched {
  4202  					if strings.Contains(err.Error(), expected) {
  4203  						delete(unmatched, expected)
  4204  						matched = true
  4205  						break
  4206  					}
  4207  				}
  4208  				if !matched {
  4209  					t.Errorf("expected error to contain one of %v, but got: %v", unmatched, err)
  4210  				}
  4211  			}
  4212  
  4213  			if len(unmatched) > 0 {
  4214  				t.Errorf("expected errors %v", unmatched)
  4215  			}
  4216  		})
  4217  	}
  4218  }
  4219  
  4220  func genString(n int, c rune) string {
  4221  	b := strings.Builder{}
  4222  	for i := 0; i < n; i++ {
  4223  		_, err := b.WriteRune(c)
  4224  		if err != nil {
  4225  			panic(err)
  4226  		}
  4227  	}
  4228  	return b.String()
  4229  }
  4230  
  4231  func setDefaultVerbosity(v int) {
  4232  	f := flag.CommandLine.Lookup("v")
  4233  	_ = f.Value.Set(fmt.Sprintf("%d", v))
  4234  }
  4235  
  4236  func BenchmarkCELValidationWithContext(b *testing.B) {
  4237  	items := make([]interface{}, 1000)
  4238  	for i := int64(0); i < 1000; i++ {
  4239  		items[i] = i
  4240  	}
  4241  	tests := []struct {
  4242  		name   string
  4243  		schema *schema.Structural
  4244  		obj    map[string]interface{}
  4245  		rule   string
  4246  	}{
  4247  		{name: "benchmark for cel validation with context",
  4248  			obj: map[string]interface{}{
  4249  				"array": items,
  4250  			},
  4251  			schema: objectTypePtr(map[string]schema.Structural{
  4252  				"array": listType(&integerType),
  4253  			}),
  4254  			rule: "self.array.map(e, e * 20).filter(e, e > 50).exists(e, e == 60)",
  4255  		},
  4256  	}
  4257  
  4258  	for _, tt := range tests {
  4259  		b.Run(tt.name, func(b *testing.B) {
  4260  			ctx := context.TODO()
  4261  			s := withRule(*tt.schema, tt.rule)
  4262  			celValidator := NewValidator(&s, true, celconfig.PerCallLimit)
  4263  			if celValidator == nil {
  4264  				b.Fatal("expected non nil validator")
  4265  			}
  4266  			for i := 0; i < b.N; i++ {
  4267  				errs, _ := celValidator.Validate(ctx, field.NewPath("root"), &s, tt.obj, nil, celconfig.RuntimeCELCostBudget)
  4268  				for _, err := range errs {
  4269  					b.Fatalf("validation failed: %v", err)
  4270  				}
  4271  			}
  4272  		})
  4273  	}
  4274  }
  4275  
  4276  func BenchmarkCELValidationWithCancelledContext(b *testing.B) {
  4277  	items := make([]interface{}, 1000)
  4278  	for i := int64(0); i < 1000; i++ {
  4279  		items[i] = i
  4280  	}
  4281  	tests := []struct {
  4282  		name   string
  4283  		schema *schema.Structural
  4284  		obj    map[string]interface{}
  4285  		rule   string
  4286  	}{
  4287  		{name: "benchmark for cel validation with context",
  4288  			obj: map[string]interface{}{
  4289  				"array": items,
  4290  			},
  4291  			schema: objectTypePtr(map[string]schema.Structural{
  4292  				"array": listType(&integerType),
  4293  			}),
  4294  			rule: "self.array.map(e, e * 20).filter(e, e > 50).exists(e, e == 60)",
  4295  		},
  4296  	}
  4297  
  4298  	for _, tt := range tests {
  4299  		b.Run(tt.name, func(b *testing.B) {
  4300  			ctx := context.TODO()
  4301  			s := withRule(*tt.schema, tt.rule)
  4302  			celValidator := NewValidator(&s, true, celconfig.PerCallLimit)
  4303  			if celValidator == nil {
  4304  				b.Fatal("expected non nil validator")
  4305  			}
  4306  			for i := 0; i < b.N; i++ {
  4307  				evalCtx, cancel := context.WithTimeout(ctx, time.Microsecond)
  4308  				cancel()
  4309  				errs, _ := celValidator.Validate(evalCtx, field.NewPath("root"), &s, tt.obj, nil, celconfig.RuntimeCELCostBudget)
  4310  				//found := false
  4311  				//for _, err := range errs {
  4312  				//	if err.Type == field.ErrorTypeInvalid && strings.Contains(err.Error(), "operation interrupted") {
  4313  				//		found = true
  4314  				//		break
  4315  				//	}
  4316  				//}
  4317  				if len(errs) == 0 {
  4318  					b.Errorf("expect operation interrupted err but did not find")
  4319  				}
  4320  			}
  4321  		})
  4322  	}
  4323  }
  4324  
  4325  // BenchmarkCELValidationWithAndWithoutOldSelfReference measures the additional cost of evaluating
  4326  // validation rules that reference "oldSelf".
  4327  func BenchmarkCELValidationWithAndWithoutOldSelfReference(b *testing.B) {
  4328  	for _, rule := range []string{
  4329  		"self.getMonth() >= 0",
  4330  		"oldSelf.getMonth() >= 0",
  4331  	} {
  4332  		b.Run(rule, func(b *testing.B) {
  4333  			obj := map[string]interface{}{
  4334  				"datetime": time.Time{}.Format(strfmt.ISO8601LocalTime),
  4335  			}
  4336  			s := &schema.Structural{
  4337  				Generic: schema.Generic{
  4338  					Type: "object",
  4339  				},
  4340  				Properties: map[string]schema.Structural{
  4341  					"datetime": {
  4342  						Generic: schema.Generic{
  4343  							Type: "string",
  4344  						},
  4345  						ValueValidation: &schema.ValueValidation{
  4346  							Format: "date-time",
  4347  						},
  4348  						Extensions: schema.Extensions{
  4349  							XValidations: []apiextensions.ValidationRule{
  4350  								{Rule: rule},
  4351  							},
  4352  						},
  4353  					},
  4354  				},
  4355  			}
  4356  			validator := NewValidator(s, true, celconfig.PerCallLimit)
  4357  			if validator == nil {
  4358  				b.Fatal("expected non nil validator")
  4359  			}
  4360  
  4361  			ctx := context.TODO()
  4362  			root := field.NewPath("root")
  4363  
  4364  			b.ReportAllocs()
  4365  			b.ResetTimer()
  4366  			for i := 0; i < b.N; i++ {
  4367  				errs, _ := validator.Validate(ctx, root, s, obj, obj, celconfig.RuntimeCELCostBudget)
  4368  				for _, err := range errs {
  4369  					b.Errorf("unexpected error: %v", err)
  4370  				}
  4371  			}
  4372  		})
  4373  	}
  4374  }
  4375  
  4376  func primitiveType(typ, format string) schema.Structural {
  4377  	result := schema.Structural{
  4378  		Generic: schema.Generic{
  4379  			Type: typ,
  4380  		},
  4381  	}
  4382  	if len(format) != 0 {
  4383  		result.ValueValidation = &schema.ValueValidation{
  4384  			Format: format,
  4385  		}
  4386  	}
  4387  	return result
  4388  }
  4389  
  4390  var (
  4391  	integerType = primitiveType("integer", "")
  4392  	int32Type   = primitiveType("integer", "int32")
  4393  	int64Type   = primitiveType("integer", "int64")
  4394  	numberType  = primitiveType("number", "")
  4395  	floatType   = primitiveType("number", "float")
  4396  	doubleType  = primitiveType("number", "double")
  4397  	stringType  = primitiveType("string", "")
  4398  	byteType    = primitiveType("string", "byte")
  4399  	booleanType = primitiveType("boolean", "")
  4400  
  4401  	durationFormat = primitiveType("string", "duration")
  4402  	dateFormat     = primitiveType("string", "date")
  4403  	dateTimeFormat = primitiveType("string", "date-time")
  4404  )
  4405  
  4406  func listType(items *schema.Structural) schema.Structural {
  4407  	return arrayType("atomic", nil, items)
  4408  }
  4409  
  4410  func listTypePtr(items *schema.Structural) *schema.Structural {
  4411  	l := listType(items)
  4412  	return &l
  4413  }
  4414  
  4415  func listSetType(items *schema.Structural) schema.Structural {
  4416  	return arrayType("set", nil, items)
  4417  }
  4418  
  4419  func listMapType(keys []string, items *schema.Structural) schema.Structural {
  4420  	return arrayType("map", keys, items)
  4421  }
  4422  
  4423  func listMapTypePtr(keys []string, items *schema.Structural) *schema.Structural {
  4424  	l := listMapType(keys, items)
  4425  	return &l
  4426  }
  4427  
  4428  func arrayType(listType string, keys []string, items *schema.Structural) schema.Structural {
  4429  	result := schema.Structural{
  4430  		Generic: schema.Generic{
  4431  			Type: "array",
  4432  		},
  4433  		Extensions: schema.Extensions{
  4434  			XListType: &listType,
  4435  		},
  4436  		Items: items,
  4437  	}
  4438  	if len(keys) > 0 && listType == "map" {
  4439  		result.Extensions.XListMapKeys = keys
  4440  	}
  4441  	return result
  4442  }
  4443  
  4444  func ValsEqualThemselvesAndDataLiteral(val1, val2 string, dataLiteral string) string {
  4445  	return fmt.Sprintf("%s == %s && %s == %s && %s == %s", val1, dataLiteral, dataLiteral, val1, val1, val2)
  4446  }
  4447  
  4448  func objs(val ...interface{}) map[string]interface{} {
  4449  	result := make(map[string]interface{}, len(val))
  4450  	for i, v := range val {
  4451  		result[fmt.Sprintf("val%d", i+1)] = v
  4452  	}
  4453  	return result
  4454  }
  4455  
  4456  func schemas(valSchema ...schema.Structural) *schema.Structural {
  4457  	result := make(map[string]schema.Structural, len(valSchema))
  4458  	for i, v := range valSchema {
  4459  		result[fmt.Sprintf("val%d", i+1)] = v
  4460  	}
  4461  	return objectTypePtr(result)
  4462  }
  4463  
  4464  func objectType(props map[string]schema.Structural) schema.Structural {
  4465  	return schema.Structural{
  4466  		Generic: schema.Generic{
  4467  			Type: "object",
  4468  		},
  4469  		Properties: props,
  4470  	}
  4471  }
  4472  
  4473  func objectTypePtr(props map[string]schema.Structural) *schema.Structural {
  4474  	o := objectType(props)
  4475  	return &o
  4476  }
  4477  
  4478  func mapType(valSchema *schema.Structural) schema.Structural {
  4479  	result := schema.Structural{
  4480  		Generic: schema.Generic{
  4481  			Type:                 "object",
  4482  			AdditionalProperties: &schema.StructuralOrBool{Bool: true, Structural: valSchema},
  4483  		},
  4484  	}
  4485  	return result
  4486  }
  4487  
  4488  func mapTypePtr(valSchema *schema.Structural) *schema.Structural {
  4489  	m := mapType(valSchema)
  4490  	return &m
  4491  }
  4492  
  4493  func intOrStringType() schema.Structural {
  4494  	return schema.Structural{
  4495  		Extensions: schema.Extensions{
  4496  			XIntOrString: true,
  4497  		},
  4498  	}
  4499  }
  4500  
  4501  func withRule(s schema.Structural, rule string) schema.Structural {
  4502  	s.Extensions.XValidations = apiextensions.ValidationRules{
  4503  		{
  4504  			Rule: rule,
  4505  		},
  4506  	}
  4507  	return s
  4508  }
  4509  
  4510  func withRuleMessageAndMessageExpression(s schema.Structural, rule, message, messageExpression string) schema.Structural {
  4511  	s.Extensions.XValidations = apiextensions.ValidationRules{
  4512  		{
  4513  			Rule:              rule,
  4514  			Message:           message,
  4515  			MessageExpression: messageExpression,
  4516  		},
  4517  	}
  4518  	return s
  4519  }
  4520  
  4521  func withReasonAndFldPath(s schema.Structural, rule, jsonPath string, reason *apiextensions.FieldValueErrorReason) schema.Structural {
  4522  	s.Extensions.XValidations = apiextensions.ValidationRules{
  4523  		{
  4524  			Rule:      rule,
  4525  			FieldPath: jsonPath,
  4526  			Reason:    reason,
  4527  		},
  4528  	}
  4529  	return s
  4530  }
  4531  
  4532  func withRuleAndMessageExpression(s schema.Structural, rule, messageExpression string) schema.Structural {
  4533  	s.Extensions.XValidations = apiextensions.ValidationRules{
  4534  		{
  4535  			Rule:              rule,
  4536  			MessageExpression: messageExpression,
  4537  		},
  4538  	}
  4539  	return s
  4540  }
  4541  
  4542  func withRulePtr(s *schema.Structural, rule string) *schema.Structural {
  4543  	s.Extensions.XValidations = apiextensions.ValidationRules{
  4544  		{
  4545  			Rule: rule,
  4546  		},
  4547  	}
  4548  	return s
  4549  }
  4550  
  4551  func cloneWithRule(s *schema.Structural, rule string) *schema.Structural {
  4552  	s = s.DeepCopy()
  4553  	return withRulePtr(s, rule)
  4554  }
  4555  
  4556  func withMaxLength(s schema.Structural, maxLength *int64) schema.Structural {
  4557  	if s.ValueValidation == nil {
  4558  		s.ValueValidation = &schema.ValueValidation{}
  4559  	}
  4560  	s.ValueValidation.MaxLength = maxLength
  4561  	return s
  4562  }
  4563  
  4564  func withMaxItems(s schema.Structural, maxItems *int64) schema.Structural {
  4565  	if s.ValueValidation == nil {
  4566  		s.ValueValidation = &schema.ValueValidation{}
  4567  	}
  4568  	s.ValueValidation.MaxItems = maxItems
  4569  	return s
  4570  }
  4571  
  4572  func withMaxProperties(s schema.Structural, maxProperties *int64) schema.Structural {
  4573  	if s.ValueValidation == nil {
  4574  		s.ValueValidation = &schema.ValueValidation{}
  4575  	}
  4576  	s.ValueValidation.MaxProperties = maxProperties
  4577  	return s
  4578  }
  4579  
  4580  func withDefault(dflt interface{}, s schema.Structural) schema.Structural {
  4581  	s.Generic.Default = schema.JSON{Object: dflt}
  4582  	return s
  4583  }
  4584  
  4585  func withNullable(nullable bool, s schema.Structural) schema.Structural {
  4586  	s.Generic.Nullable = nullable
  4587  	return s
  4588  }
  4589  
  4590  func withNullablePtr(nullable bool, s schema.Structural) *schema.Structural {
  4591  	s.Generic.Nullable = nullable
  4592  	return &s
  4593  }
  4594  
  4595  func nilInterfaceOfStringSlice() []interface{} {
  4596  	var slice []interface{} = nil
  4597  	return slice
  4598  }
  4599  

View as plain text