k8s.io/apiserver@v0.31.1/pkg/cel/common/equality_test.go (about)

     1  /*
     2  Copyright 2023 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 common_test
    18  
    19  import (
    20  	"errors"
    21  	"fmt"
    22  	"reflect"
    23  	"strings"
    24  	"testing"
    25  
    26  	"k8s.io/apimachinery/pkg/util/yaml"
    27  	"k8s.io/apiserver/pkg/cel/common"
    28  	"k8s.io/apiserver/pkg/cel/openapi"
    29  	"k8s.io/kube-openapi/pkg/validation/spec"
    30  )
    31  
    32  type TestCase struct {
    33  	Name string
    34  
    35  	// Expected old value after traversal. If nil, then the traversal should fail.
    36  	OldValue interface{}
    37  
    38  	// Expected value after traversal. If nil, then the traversal should fail.
    39  	NewValue interface{}
    40  
    41  	// Whether OldValue and NewValue are considered to be equal.
    42  	// Defaults to reflect.DeepEqual comparison of the two. Can be overridden to
    43  	// true here if the two values are not DeepEqual, but are considered equal
    44  	// for instance due to map-list reordering.
    45  	ExpectEqual bool
    46  
    47  	// Schema to provide to the correlated object
    48  	Schema common.Schema
    49  
    50  	// Array of field names and indexes to traverse to get to the value
    51  	KeyPath []interface{}
    52  
    53  	// Root object to traverse from
    54  	RootObject    interface{}
    55  	RootOldObject interface{}
    56  }
    57  
    58  func (c TestCase) Run() error {
    59  	// Create the correlated object
    60  	correlatedObject := common.NewCorrelatedObject(c.RootObject, c.RootOldObject, c.Schema)
    61  
    62  	// Traverse the correlated object
    63  	var err error
    64  	for _, key := range c.KeyPath {
    65  		if correlatedObject == nil {
    66  			break
    67  		}
    68  
    69  		switch k := key.(type) {
    70  		case string:
    71  			correlatedObject = correlatedObject.Key(k)
    72  		case int:
    73  			correlatedObject = correlatedObject.Index(k)
    74  		default:
    75  			return errors.New("key must be a string or int")
    76  		}
    77  		if err != nil {
    78  			return err
    79  		}
    80  	}
    81  
    82  	if correlatedObject == nil {
    83  		if c.OldValue != nil || c.NewValue != nil {
    84  			return fmt.Errorf("expected non-nil value, got nil")
    85  		}
    86  	} else {
    87  		// Check that the correlated object has the expected values
    88  		if !reflect.DeepEqual(correlatedObject.Value, c.NewValue) {
    89  			return fmt.Errorf("expected value %v, got %v", c.NewValue, correlatedObject.Value)
    90  		}
    91  		if !reflect.DeepEqual(correlatedObject.OldValue, c.OldValue) {
    92  			return fmt.Errorf("expected old value %v, got %v", c.OldValue, correlatedObject.OldValue)
    93  		}
    94  
    95  		// Check that the correlated object is considered equal to the expected value
    96  		if (c.ExpectEqual || reflect.DeepEqual(correlatedObject.Value, correlatedObject.OldValue)) != correlatedObject.CachedDeepEqual() {
    97  			return fmt.Errorf("expected equal, got not equal")
    98  		}
    99  	}
   100  
   101  	return nil
   102  }
   103  
   104  // Creates a *spec.Schema Schema by decoding the given YAML. Panics on error
   105  func mustSchema(source string) *openapi.Schema {
   106  	d := yaml.NewYAMLOrJSONDecoder(strings.NewReader(source), 4096)
   107  	res := &spec.Schema{}
   108  	if err := d.Decode(res); err != nil {
   109  		panic(err)
   110  	}
   111  	return &openapi.Schema{Schema: res}
   112  }
   113  
   114  // Creates an *unstructured by decoding the given YAML. Panics on error
   115  func mustUnstructured(source string) interface{} {
   116  	d := yaml.NewYAMLOrJSONDecoder(strings.NewReader(source), 4096)
   117  	var res interface{}
   118  	if err := d.Decode(&res); err != nil {
   119  		panic(err)
   120  	}
   121  	return res
   122  }
   123  
   124  func TestCorrelation(t *testing.T) {
   125  	// Tests ensure that the output of following keypath using the given
   126  	// schema and root objects yields the provided new value and old value.
   127  	// If new or old are nil, then ensures that the traversal failed due to
   128  	// uncorrelatable field path.
   129  	// Also confirms that CachedDeepEqual output is equal to expected result of
   130  	// reflect.DeepEqual of the new and old values.
   131  	cases := []TestCase{
   132  		{
   133  			Name:          "Basic Key",
   134  			RootObject:    mustUnstructured(`a: b`),
   135  			RootOldObject: mustUnstructured(`a: b`),
   136  			Schema: mustSchema(`
   137                  properties:
   138                    a: { type: string }
   139              `),
   140  			KeyPath:  []interface{}{"a"},
   141  			NewValue: "b",
   142  			OldValue: "b",
   143  		},
   144  		{
   145  			Name:          "Atomic Array not correlatable",
   146  			RootObject:    mustUnstructured(`[a, b]`),
   147  			RootOldObject: mustUnstructured(`[a, b]`),
   148  			Schema: mustSchema(`
   149                  items:
   150                    type: string
   151              `),
   152  			KeyPath: []interface{}{1},
   153  		},
   154  		{
   155  			Name: "Added Key Not In Old Object",
   156  			RootObject: mustUnstructured(`
   157                  a: b
   158                  c: d
   159              `),
   160  			RootOldObject: mustUnstructured(`
   161                  a: b
   162              `),
   163  			Schema: mustSchema(`
   164                  properties:
   165                    a: { type: string }
   166                    c: { type: string }
   167              `),
   168  			KeyPath: []interface{}{"c"},
   169  		},
   170  		{
   171  			Name: "Added Index Not In Old Object",
   172  			RootObject: mustUnstructured(`
   173                  - a
   174                  - b
   175                  - c
   176              `),
   177  			RootOldObject: mustUnstructured(`
   178                  - a
   179                  - b
   180              `),
   181  			Schema: mustSchema(`
   182                  items:
   183                      type: string
   184              `),
   185  			KeyPath: []interface{}{2},
   186  		},
   187  		{
   188  			Name: "Changed Index In Old Object not correlatable",
   189  			RootObject: []interface{}{
   190  				"a",
   191  				"b",
   192  			},
   193  			RootOldObject: []interface{}{
   194  				"a",
   195  				"oldB",
   196  			},
   197  			Schema: mustSchema(`
   198                  items:
   199                      type: string
   200              `),
   201  			KeyPath: []interface{}{1},
   202  		},
   203  		{
   204  			Name: "Changed Index In Nested Old Object",
   205  			RootObject: []interface{}{
   206  				"a",
   207  				"b",
   208  			},
   209  			RootOldObject: []interface{}{
   210  				"a",
   211  				"oldB",
   212  			},
   213  			Schema: mustSchema(`
   214                  items:
   215                      type: string
   216              `),
   217  			KeyPath:  []interface{}{},
   218  			NewValue: []interface{}{"a", "b"},
   219  			OldValue: []interface{}{"a", "oldB"},
   220  		},
   221  		{
   222  			Name: "Changed Key In Old Object",
   223  			RootObject: map[string]interface{}{
   224  				"a": "b",
   225  			},
   226  			RootOldObject: map[string]interface{}{
   227  				"a": "oldB",
   228  			},
   229  			Schema: mustSchema(`
   230                  properties:
   231                    a: { type: string }
   232              `),
   233  			KeyPath:  []interface{}{"a"},
   234  			NewValue: "b",
   235  			OldValue: "oldB",
   236  		},
   237  		{
   238  			Name: "Replaced Key In Old Object",
   239  			RootObject: map[string]interface{}{
   240  				"a": "b",
   241  			},
   242  			RootOldObject: map[string]interface{}{
   243  				"b": "a",
   244  			},
   245  			Schema: mustSchema(`
   246                  properties:
   247                    a: { type: string }
   248              `),
   249  			KeyPath:  []interface{}{},
   250  			NewValue: map[string]interface{}{"a": "b"},
   251  			OldValue: map[string]interface{}{"b": "a"},
   252  		},
   253  		{
   254  			Name: "Added Key In Old Object",
   255  			RootObject: map[string]interface{}{
   256  				"a": "b",
   257  			},
   258  			RootOldObject: map[string]interface{}{},
   259  			Schema: mustSchema(`
   260                  properties:
   261                    a: { type: string }
   262              `),
   263  			KeyPath:  []interface{}{},
   264  			NewValue: map[string]interface{}{"a": "b"},
   265  			OldValue: map[string]interface{}{},
   266  		},
   267  		{
   268  			Name: "Changed list to map",
   269  			RootObject: map[string]interface{}{
   270  				"a": "b",
   271  			},
   272  			RootOldObject: []interface{}{"a", "b"},
   273  			Schema: mustSchema(`
   274                  properties:
   275                    a: { type: string }
   276              `),
   277  			KeyPath:  []interface{}{},
   278  			NewValue: map[string]interface{}{"a": "b"},
   279  			OldValue: []interface{}{"a", "b"},
   280  		},
   281  		{
   282  			Name: "Changed string to map",
   283  			RootObject: map[string]interface{}{
   284  				"a": "b",
   285  			},
   286  			RootOldObject: "a string",
   287  			Schema: mustSchema(`
   288                  properties:
   289                    a: { type: string }
   290              `),
   291  			KeyPath:  []interface{}{},
   292  			NewValue: map[string]interface{}{"a": "b"},
   293  			OldValue: "a string",
   294  		},
   295  		{
   296  			Name: "Map list type",
   297  			RootObject: mustUnstructured(`
   298                  foo:
   299                  - bar: baz
   300                    val: newBazValue
   301              `),
   302  			RootOldObject: mustUnstructured(`
   303                  foo:
   304                  - bar: fizz
   305                    val: fizzValue
   306                  - bar: baz
   307                    val: bazValue
   308              `),
   309  			Schema: mustSchema(`
   310                  properties:
   311                    foo:
   312                      type: array
   313                      items:
   314                        type: object
   315                        properties:
   316                          bar:
   317                            type: string
   318                          val:
   319                            type: string
   320                      x-kubernetes-list-type: map
   321                      x-kubernetes-list-map-keys:
   322                        - bar
   323              `),
   324  			KeyPath:  []interface{}{"foo", 0, "val"},
   325  			NewValue: "newBazValue",
   326  			OldValue: "bazValue",
   327  		},
   328  		{
   329  			Name: "Atomic list item should not correlate",
   330  			RootObject: mustUnstructured(`
   331                  foo:
   332                  - bar: baz
   333                    val: newValue
   334              `),
   335  			RootOldObject: mustUnstructured(`
   336                  foo:
   337                  - bar: fizz
   338                    val: fizzValue
   339                  - bar: baz
   340                    val: barValue
   341              `),
   342  			Schema: mustSchema(`
   343                  properties:
   344                    foo:
   345                      type: array
   346                      items:
   347                        type: object
   348                        properties:
   349                          bar:
   350                            type: string
   351                          val:
   352                            type: string
   353                      x-kubernetes-list-type: atomic
   354              `),
   355  			KeyPath: []interface{}{"foo", 0, "val"},
   356  		},
   357  		{
   358  			Name: "Map used inside of map list type should correlate",
   359  			RootObject: mustUnstructured(`
   360                  foo:
   361                  - key: keyValue
   362                    bar:
   363                      baz: newValue
   364              `),
   365  			RootOldObject: mustUnstructured(`
   366                  foo:
   367                  - key: otherKeyValue
   368                    bar:
   369                      baz: otherOldValue
   370                  - key: altKeyValue
   371                    bar:
   372                      baz: altOldValue
   373                  - key: keyValue
   374                    bar:
   375                      baz: oldValue
   376              `),
   377  			Schema: mustSchema(`
   378                  properties:
   379                    foo:
   380                      type: array
   381                      items:
   382                        type: object
   383                        properties:
   384                          key:
   385                            type: string
   386                          bar:
   387                            type: object
   388                            properties:
   389                              baz:
   390                                type: string
   391                      x-kubernetes-list-type: map
   392                      x-kubernetes-list-map-keys:
   393                        - key
   394              `),
   395  			KeyPath:  []interface{}{"foo", 0, "bar", "baz"},
   396  			NewValue: "newValue",
   397  			OldValue: "oldValue",
   398  		},
   399  		{
   400  			Name: "Map used inside another map should correlate",
   401  			RootObject: mustUnstructured(`
   402                  foo:
   403                      key: keyValue
   404                      bar:
   405                          baz: newValue
   406              `),
   407  			RootOldObject: mustUnstructured(`
   408                  foo:
   409                      key: otherKeyValue
   410                      bar:
   411                          baz: otherOldValue
   412                  altFoo:
   413                      key: altKeyValue
   414                      bar:
   415                          baz: altOldValue
   416                  otherFoo:
   417                      key: keyValue
   418                      bar:
   419                          baz: oldValue
   420              `),
   421  			Schema: mustSchema(`
   422                  properties:
   423                    foo:
   424                      type: object
   425                      properties:
   426                        key:
   427                          type: string
   428                        bar:
   429                          type: object
   430                          properties:
   431                            baz:
   432                              type: string
   433              `),
   434  			KeyPath:  []interface{}{"foo", "bar"},
   435  			NewValue: map[string]interface{}{"baz": "newValue"},
   436  			OldValue: map[string]interface{}{"baz": "otherOldValue"},
   437  		},
   438  		{
   439  			Name: "Nested map equal to old",
   440  			RootObject: mustUnstructured(`
   441                  foo:
   442                      key: newKeyValue
   443                      bar:
   444                          baz: value
   445              `),
   446  			RootOldObject: mustUnstructured(`
   447                  foo:
   448                      key: keyValue
   449                      bar:
   450                          baz: value
   451              `),
   452  			Schema: mustSchema(`
   453                  properties:
   454                    foo:
   455                      type: object
   456                      properties:
   457                        key:
   458                          type: string
   459                        bar:
   460                          type: object
   461                          properties:
   462                            baz:
   463                              type: string
   464              `),
   465  			KeyPath:  []interface{}{"foo", "bar"},
   466  			NewValue: map[string]interface{}{"baz": "value"},
   467  			OldValue: map[string]interface{}{"baz": "value"},
   468  		},
   469  		{
   470  			Name: "Re-ordered list considered equal to old value due to map keys",
   471  			RootObject: mustUnstructured(`
   472                  foo:
   473                  - key: keyValue
   474                    bar:
   475                      baz: value
   476                  - key: altKeyValue
   477                    bar:
   478                      baz: altValue
   479              `),
   480  			RootOldObject: mustUnstructured(`
   481                  foo:
   482                  - key: altKeyValue
   483                    bar:
   484                      baz: altValue
   485                  - key: keyValue
   486                    bar:
   487                      baz: value
   488              `),
   489  			Schema: mustSchema(`
   490                  properties:
   491                    foo:
   492                      type: array
   493                      items:
   494                        type: object
   495                        properties:
   496                          key:
   497                            type: string
   498                          bar:
   499                            type: object
   500                            properties:
   501                              baz:
   502                                type: string
   503                      x-kubernetes-list-type: map
   504                      x-kubernetes-list-map-keys:
   505                        - key
   506              `),
   507  			KeyPath: []interface{}{"foo"},
   508  			NewValue: mustUnstructured(`
   509                  - key: keyValue
   510                    bar:
   511                      baz: value
   512                  - key: altKeyValue
   513                    bar:
   514                      baz: altValue
   515              `),
   516  			OldValue: mustUnstructured(`
   517                  - key: altKeyValue
   518                    bar:
   519                      baz: altValue
   520                  - key: keyValue
   521                    bar:
   522                      baz: value
   523              `),
   524  			ExpectEqual: true,
   525  		},
   526  		{
   527  			Name: "Correlate unknown string key via additional properties",
   528  			RootObject: mustUnstructured(`
   529                  foo:
   530                      key: keyValue
   531                      bar:
   532                          baz: newValue
   533              `),
   534  			RootOldObject: mustUnstructured(`
   535                  foo:
   536                      key: otherKeyValue
   537                      bar:
   538                          baz: otherOldValue
   539              `),
   540  			Schema: mustSchema(`
   541                  properties:
   542                      foo:
   543                          type: object
   544                          additionalProperties:
   545                              properties:
   546                                  baz:
   547                                      type: string
   548              `),
   549  			KeyPath:  []interface{}{"foo", "bar", "baz"},
   550  			NewValue: "newValue",
   551  			OldValue: "otherOldValue",
   552  		},
   553  		{
   554  			Name: "Changed map value",
   555  			RootObject: mustUnstructured(`
   556                  foo:
   557                      key: keyValue
   558                      bar:
   559                          baz: newValue
   560              `),
   561  			RootOldObject: mustUnstructured(`
   562                  foo:
   563                      key: keyValue
   564                      bar:
   565                          baz: oldValue
   566              `),
   567  			Schema: mustSchema(`
   568                  properties:
   569                      foo:
   570                          type: object
   571                          properties:
   572                              key:
   573                                  type: string
   574                              bar:
   575                                  type: object
   576                                  properties:
   577                                      baz:
   578                                          type: string
   579              `),
   580  			KeyPath: []interface{}{"foo", "bar"},
   581  			NewValue: mustUnstructured(`
   582                  baz: newValue
   583              `),
   584  			OldValue: mustUnstructured(`
   585                  baz: oldValue
   586              `),
   587  		},
   588  		{
   589  			Name: "Changed nested map value",
   590  			RootObject: mustUnstructured(`
   591                  foo:
   592                      key: keyValue
   593                      bar:
   594                          baz: newValue
   595              `),
   596  			RootOldObject: mustUnstructured(`
   597                  foo:
   598                      key: keyValue
   599                      bar:
   600                          baz: oldValue
   601              `),
   602  			Schema: mustSchema(`
   603                  properties:
   604                      foo:
   605                          type: object
   606                          properties:
   607                              key:
   608                                  type: string
   609                              bar:
   610                                  type: object
   611                                  properties:
   612                                      baz:
   613                                          type: string
   614              `),
   615  			KeyPath: []interface{}{"foo"},
   616  			NewValue: mustUnstructured(`
   617                  key: keyValue    
   618                  bar:
   619                    baz: newValue
   620              `),
   621  			OldValue: mustUnstructured(`
   622                  key: keyValue    
   623                  bar:
   624                    baz: oldValue
   625              `),
   626  		},
   627  		{
   628  			Name: "unchanged list type set with atomic map values",
   629  			Schema: mustSchema(`
   630                  properties:
   631                      foo:
   632                          type: array
   633                          items:
   634                              type: object
   635                              x-kubernetes-map-type: atomic
   636                              properties:
   637                                  key:
   638                                      type: string
   639                                  bar:
   640                                      type: string
   641                          x-kubernetes-list-type: set
   642              `),
   643  			RootObject: mustUnstructured(`
   644                  foo:
   645                  - key: key1
   646                    bar: value1
   647                  - key: key2
   648                    bar: value2
   649              `),
   650  			RootOldObject: mustUnstructured(`
   651                  foo:
   652                  - key: key1
   653                    bar: value1
   654                  - key: key2
   655                    bar: value2
   656              `),
   657  			KeyPath: []interface{}{"foo"},
   658  			NewValue: mustUnstructured(`
   659                  - key: key1
   660                    bar: value1
   661                  - key: key2
   662                    bar: value2
   663              `),
   664  			OldValue: mustUnstructured(`
   665                  - key: key1
   666                    bar: value1
   667                  - key: key2
   668                    bar: value2
   669              `),
   670  		},
   671  		{
   672  			Name: "changed list type set with atomic map values",
   673  			Schema: mustSchema(`
   674                  properties:
   675                      foo:
   676                          type: array
   677                          items:
   678                              type: object
   679                              x-kubernetes-map-type: atomic
   680                              properties:
   681                                  key:
   682                                      type: string
   683                                  bar:
   684                                      type: string
   685                          x-kubernetes-list-type: set
   686              `),
   687  			RootObject: mustUnstructured(`
   688                  foo:
   689                  - key: key1
   690                    bar: value1
   691                  - key: key2
   692                    bar: newValue2
   693              `),
   694  			RootOldObject: mustUnstructured(`
   695                  foo:
   696                  - key: key1
   697                    bar: value1
   698                  - key: key2
   699                    bar: value2
   700              `),
   701  			KeyPath: []interface{}{"foo"},
   702  			NewValue: mustUnstructured(`
   703                  - key: key1
   704                    bar: value1
   705                  - key: key2
   706                    bar: newValue2
   707              `),
   708  			OldValue: mustUnstructured(`
   709                  - key: key1
   710                    bar: value1
   711                  - key: key2
   712                    bar: value2
   713              `),
   714  		},
   715  		{
   716  			Name: "elements of list type set with atomic map values are not correlated",
   717  			Schema: mustSchema(`
   718                  properties:
   719                      foo:
   720                          type: array
   721                          items:
   722                              type: object
   723                              x-kubernetes-map-type: atomic
   724                              properties:
   725                                  key:
   726                                      type: string
   727                                  bar:
   728                                      type: string
   729                          x-kubernetes-list-type: set
   730              `),
   731  			RootObject: mustUnstructured(`
   732                  foo:
   733                  - key: key1
   734                    bar: value1
   735                  - key: key2
   736                    bar: newValue2
   737              `),
   738  			RootOldObject: mustUnstructured(`
   739                  foo:
   740                  - key: key1
   741                    bar: value1
   742                  - key: key2
   743                    bar: value2
   744              `),
   745  			KeyPath:  []interface{}{"foo", 0, "key"},
   746  			NewValue: nil,
   747  		},
   748  	}
   749  	for _, c := range cases {
   750  		t.Run(c.Name, func(t *testing.T) {
   751  			if err := c.Run(); err != nil {
   752  				t.Errorf("unexpected error: %v", err)
   753  			}
   754  		})
   755  	}
   756  }