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

     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 common
    18  
    19  import (
    20  	"fmt"
    21  	"reflect"
    22  	"sync"
    23  	"time"
    24  
    25  	"github.com/google/cel-go/common/types"
    26  	"github.com/google/cel-go/common/types/ref"
    27  	"github.com/google/cel-go/common/types/traits"
    28  
    29  	"k8s.io/kube-openapi/pkg/validation/strfmt"
    30  
    31  	"k8s.io/apimachinery/pkg/api/equality"
    32  	"k8s.io/apiserver/pkg/cel"
    33  )
    34  
    35  // UnstructuredToVal converts a Kubernetes unstructured data element to a CEL Val.
    36  // The root schema of custom resource schema is expected contain type meta and object meta schemas.
    37  // If Embedded resources do not contain type meta and object meta schemas, they will be added automatically.
    38  func UnstructuredToVal(unstructured interface{}, schema Schema) ref.Val {
    39  	if unstructured == nil {
    40  		if schema.Nullable() {
    41  			return types.NullValue
    42  		}
    43  		return types.NewErr("invalid data, got null for schema with nullable=false")
    44  	}
    45  	if schema.IsXIntOrString() {
    46  		switch v := unstructured.(type) {
    47  		case string:
    48  			return types.String(v)
    49  		case int:
    50  			return types.Int(v)
    51  		case int32:
    52  			return types.Int(v)
    53  		case int64:
    54  			return types.Int(v)
    55  		}
    56  		return types.NewErr("invalid data, expected XIntOrString value to be either a string or integer")
    57  	}
    58  	if schema.Type() == "object" {
    59  		m, ok := unstructured.(map[string]interface{})
    60  		if !ok {
    61  			return types.NewErr("invalid data, expected a map for the provided schema with type=object")
    62  		}
    63  		if schema.IsXEmbeddedResource() || schema.Properties() != nil {
    64  			if schema.IsXEmbeddedResource() {
    65  				schema = schema.WithTypeAndObjectMeta()
    66  			}
    67  			return &unstructuredMap{
    68  				value:  m,
    69  				schema: schema,
    70  				propSchema: func(key string) (Schema, bool) {
    71  					if schema, ok := schema.Properties()[key]; ok {
    72  						return schema, true
    73  					}
    74  					return nil, false
    75  				},
    76  			}
    77  		}
    78  		if schema.AdditionalProperties() != nil && schema.AdditionalProperties().Schema() != nil {
    79  			return &unstructuredMap{
    80  				value:  m,
    81  				schema: schema,
    82  				propSchema: func(key string) (Schema, bool) {
    83  					return schema.AdditionalProperties().Schema(), true
    84  				},
    85  			}
    86  		}
    87  
    88  		// properties and additionalProperties are mutual exclusive, but nothing prevents the situation
    89  		// where both are missing.
    90  		// An object that (1) has no properties (2) has no additionalProperties or additionalProperties == false
    91  		// is treated as an empty object.
    92  		// An object that has additionalProperties == true is treated as an unstructured map.
    93  		// An object that has x-kubernetes-preserve-unknown-field extension set is treated as an unstructured map.
    94  		// Empty object vs unstructured map is differentiated by unstructuredMap implementation with the set schema.
    95  		// The resulting result remains the same.
    96  		return &unstructuredMap{
    97  			value:  m,
    98  			schema: schema,
    99  			propSchema: func(key string) (Schema, bool) {
   100  				return nil, false
   101  			},
   102  		}
   103  	}
   104  
   105  	if schema.Type() == "array" {
   106  		l, ok := unstructured.([]interface{})
   107  		if !ok {
   108  			return types.NewErr("invalid data, expected an array for the provided schema with type=array")
   109  		}
   110  		if schema.Items() == nil {
   111  			return types.NewErr("invalid array type, expected Items with a non-empty Schema")
   112  		}
   113  		typedList := unstructuredList{elements: l, itemsSchema: schema.Items()}
   114  		listType := schema.XListType()
   115  		if listType != "" {
   116  			switch listType {
   117  			case "map":
   118  				mapKeys := schema.XListMapKeys()
   119  				return &unstructuredMapList{unstructuredList: typedList, escapedKeyProps: escapeKeyProps(mapKeys)}
   120  			case "set":
   121  				return &unstructuredSetList{unstructuredList: typedList}
   122  			case "atomic":
   123  				return &typedList
   124  			default:
   125  				return types.NewErr("invalid x-kubernetes-list-type, expected 'map', 'set' or 'atomic' but got %s", listType)
   126  			}
   127  		}
   128  		return &typedList
   129  	}
   130  
   131  	if schema.Type() == "string" {
   132  		str, ok := unstructured.(string)
   133  		if !ok {
   134  			return types.NewErr("invalid data, expected string, got %T", unstructured)
   135  		}
   136  		switch schema.Format() {
   137  		case "duration":
   138  			d, err := strfmt.ParseDuration(str)
   139  			if err != nil {
   140  				return types.NewErr("Invalid duration %s: %v", str, err)
   141  			}
   142  			return types.Duration{Duration: d}
   143  		case "date":
   144  			d, err := time.Parse(strfmt.RFC3339FullDate, str) // strfmt uses this format for OpenAPIv3 value validation
   145  			if err != nil {
   146  				return types.NewErr("Invalid date formatted string %s: %v", str, err)
   147  			}
   148  			return types.Timestamp{Time: d}
   149  		case "date-time":
   150  			d, err := strfmt.ParseDateTime(str)
   151  			if err != nil {
   152  				return types.NewErr("Invalid date-time formatted string %s: %v", str, err)
   153  			}
   154  			return types.Timestamp{Time: time.Time(d)}
   155  		case "byte":
   156  			base64 := strfmt.Base64{}
   157  			err := base64.UnmarshalText([]byte(str))
   158  			if err != nil {
   159  				return types.NewErr("Invalid byte formatted string %s: %v", str, err)
   160  			}
   161  			return types.Bytes(base64)
   162  		}
   163  
   164  		return types.String(str)
   165  	}
   166  	if schema.Type() == "number" {
   167  		switch v := unstructured.(type) {
   168  		// float representations of whole numbers (e.g. 1.0, 0.0) can convert to int representations (e.g. 1, 0) in yaml
   169  		// to json translation, and then get parsed as int64s
   170  		case int:
   171  			return types.Double(v)
   172  		case int32:
   173  			return types.Double(v)
   174  		case int64:
   175  			return types.Double(v)
   176  
   177  		case float32:
   178  			return types.Double(v)
   179  		case float64:
   180  			return types.Double(v)
   181  		default:
   182  			return types.NewErr("invalid data, expected float, got %T", unstructured)
   183  		}
   184  	}
   185  	if schema.Type() == "integer" {
   186  		switch v := unstructured.(type) {
   187  		case int:
   188  			return types.Int(v)
   189  		case int32:
   190  			return types.Int(v)
   191  		case int64:
   192  			return types.Int(v)
   193  		default:
   194  			return types.NewErr("invalid data, expected int, got %T", unstructured)
   195  		}
   196  	}
   197  	if schema.Type() == "boolean" {
   198  		b, ok := unstructured.(bool)
   199  		if !ok {
   200  			return types.NewErr("invalid data, expected bool, got %T", unstructured)
   201  		}
   202  		return types.Bool(b)
   203  	}
   204  
   205  	if schema.IsXPreserveUnknownFields() {
   206  		return &unknownPreserved{u: unstructured}
   207  	}
   208  
   209  	return types.NewErr("invalid type, expected object, array, number, integer, boolean or string, or no type with x-kubernetes-int-or-string or x-kubernetes-preserve-unknown-fields is true, got %s", schema.Type())
   210  }
   211  
   212  // unknownPreserved represents unknown data preserved in custom resources via x-kubernetes-preserve-unknown-fields.
   213  // It preserves the data at runtime without assuming it is of any particular type and supports only equality checking.
   214  // unknownPreserved should be used only for values are not directly accessible in CEL expressions, i.e. for data
   215  // where there is no corresponding CEL type declaration.
   216  type unknownPreserved struct {
   217  	u interface{}
   218  }
   219  
   220  func (t *unknownPreserved) ConvertToNative(refType reflect.Type) (interface{}, error) {
   221  	return nil, fmt.Errorf("type conversion to '%s' not supported for values preserved by x-kubernetes-preserve-unknown-fields", refType)
   222  }
   223  
   224  func (t *unknownPreserved) ConvertToType(typeValue ref.Type) ref.Val {
   225  	return types.NewErr("type conversion to '%s' not supported for values preserved by x-kubernetes-preserve-unknown-fields", typeValue.TypeName())
   226  }
   227  
   228  func (t *unknownPreserved) Equal(other ref.Val) ref.Val {
   229  	return types.Bool(equality.Semantic.DeepEqual(t.u, other.Value()))
   230  }
   231  
   232  func (t *unknownPreserved) Type() ref.Type {
   233  	return types.UnknownType
   234  }
   235  
   236  func (t *unknownPreserved) Value() interface{} {
   237  	return t.u // used by Equal checks
   238  }
   239  
   240  // unstructuredMapList represents an unstructured data instance of an OpenAPI array with x-kubernetes-list-type=map.
   241  type unstructuredMapList struct {
   242  	unstructuredList
   243  	escapedKeyProps []string
   244  
   245  	sync.Once // for for lazy load of mapOfList since it is only needed if Equals is called
   246  	mapOfList map[interface{}]interface{}
   247  }
   248  
   249  func (t *unstructuredMapList) getMap() map[interface{}]interface{} {
   250  	t.Do(func() {
   251  		t.mapOfList = make(map[interface{}]interface{}, len(t.elements))
   252  		for _, e := range t.elements {
   253  			t.mapOfList[t.toMapKey(e)] = e
   254  		}
   255  	})
   256  	return t.mapOfList
   257  }
   258  
   259  // toMapKey returns a valid golang map key for the given element of the map list.
   260  // element must be a valid map list entry where all map key props are scalar types (which are comparable in go
   261  // and valid for use in a golang map key).
   262  func (t *unstructuredMapList) toMapKey(element interface{}) interface{} {
   263  	eObj, ok := element.(map[string]interface{})
   264  	if !ok {
   265  		return types.NewErr("unexpected data format for element of array with x-kubernetes-list-type=map: %T", element)
   266  	}
   267  	// Arrays are comparable in go and may be used as map keys, but maps and slices are not.
   268  	// So we can special case small numbers of key props as arrays and fall back to serialization
   269  	// for larger numbers of key props
   270  	if len(t.escapedKeyProps) == 1 {
   271  		return eObj[t.escapedKeyProps[0]]
   272  	}
   273  	if len(t.escapedKeyProps) == 2 {
   274  		return [2]interface{}{eObj[t.escapedKeyProps[0]], eObj[t.escapedKeyProps[1]]}
   275  	}
   276  	if len(t.escapedKeyProps) == 3 {
   277  		return [3]interface{}{eObj[t.escapedKeyProps[0]], eObj[t.escapedKeyProps[1]], eObj[t.escapedKeyProps[2]]}
   278  	}
   279  
   280  	key := make([]interface{}, len(t.escapedKeyProps))
   281  	for i, kf := range t.escapedKeyProps {
   282  		key[i] = eObj[kf]
   283  	}
   284  	return fmt.Sprintf("%v", key)
   285  }
   286  
   287  // Equal on a map list ignores list element order.
   288  func (t *unstructuredMapList) Equal(other ref.Val) ref.Val {
   289  	oMapList, ok := other.(traits.Lister)
   290  	if !ok {
   291  		return types.MaybeNoSuchOverloadErr(other)
   292  	}
   293  	sz := types.Int(len(t.elements))
   294  	if sz != oMapList.Size() {
   295  		return types.False
   296  	}
   297  	tMap := t.getMap()
   298  	for it := oMapList.Iterator(); it.HasNext() == types.True; {
   299  		v := it.Next()
   300  		k := t.toMapKey(v.Value())
   301  		tVal, ok := tMap[k]
   302  		if !ok {
   303  			return types.False
   304  		}
   305  		eq := UnstructuredToVal(tVal, t.itemsSchema).Equal(v)
   306  		if eq != types.True {
   307  			return eq // either false or error
   308  		}
   309  	}
   310  	return types.True
   311  }
   312  
   313  // Add for a map list `X + Y` performs a merge where the array positions of all keys in `X` are preserved but the values
   314  // are overwritten by values in `Y` when the key sets of `X` and `Y` intersect. Elements in `Y` with
   315  // non-intersecting keys are appended, retaining their partial order.
   316  func (t *unstructuredMapList) Add(other ref.Val) ref.Val {
   317  	oMapList, ok := other.(traits.Lister)
   318  	if !ok {
   319  		return types.MaybeNoSuchOverloadErr(other)
   320  	}
   321  	elements := make([]interface{}, len(t.elements))
   322  	keyToIdx := map[interface{}]int{}
   323  	for i, e := range t.elements {
   324  		k := t.toMapKey(e)
   325  		keyToIdx[k] = i
   326  		elements[i] = e
   327  	}
   328  	for it := oMapList.Iterator(); it.HasNext() == types.True; {
   329  		v := it.Next().Value()
   330  		k := t.toMapKey(v)
   331  		if overwritePosition, ok := keyToIdx[k]; ok {
   332  			elements[overwritePosition] = v
   333  		} else {
   334  			elements = append(elements, v)
   335  		}
   336  	}
   337  	return &unstructuredMapList{
   338  		unstructuredList: unstructuredList{elements: elements, itemsSchema: t.itemsSchema},
   339  		escapedKeyProps:  t.escapedKeyProps,
   340  	}
   341  }
   342  
   343  // escapeKeyProps returns identifiers with Escape applied to each.
   344  // Identifiers that cannot be escaped are left as-is. They are inaccessible to CEL programs but are
   345  // are still needed internally to perform equality checks.
   346  func escapeKeyProps(idents []string) []string {
   347  	result := make([]string, len(idents))
   348  	for i, prop := range idents {
   349  		if escaped, ok := cel.Escape(prop); ok {
   350  			result[i] = escaped
   351  		} else {
   352  			result[i] = prop
   353  		}
   354  	}
   355  	return result
   356  }
   357  
   358  // unstructuredSetList represents an unstructured data instance of an OpenAPI array with x-kubernetes-list-type=set.
   359  type unstructuredSetList struct {
   360  	unstructuredList
   361  	escapedKeyProps []string
   362  
   363  	sync.Once // for for lazy load of setOfList since it is only needed if Equals is called
   364  	set       map[interface{}]struct{}
   365  }
   366  
   367  func (t *unstructuredSetList) getSet() map[interface{}]struct{} {
   368  	// sets are only allowed to contain scalar elements, which are comparable in go, and can safely be used as
   369  	// golang map keys
   370  	t.Do(func() {
   371  		t.set = make(map[interface{}]struct{}, len(t.elements))
   372  		for _, e := range t.elements {
   373  			t.set[e] = struct{}{}
   374  		}
   375  	})
   376  	return t.set
   377  }
   378  
   379  // Equal on a map list ignores list element order.
   380  func (t *unstructuredSetList) Equal(other ref.Val) ref.Val {
   381  	oSetList, ok := other.(traits.Lister)
   382  	if !ok {
   383  		return types.MaybeNoSuchOverloadErr(other)
   384  	}
   385  	sz := types.Int(len(t.elements))
   386  	if sz != oSetList.Size() {
   387  		return types.False
   388  	}
   389  	tSet := t.getSet()
   390  	for it := oSetList.Iterator(); it.HasNext() == types.True; {
   391  		next := it.Next().Value()
   392  		_, ok := tSet[next]
   393  		if !ok {
   394  			return types.False
   395  		}
   396  	}
   397  	return types.True
   398  }
   399  
   400  // Add for a set list `X + Y` performs a union where the array positions of all elements in `X` are preserved and
   401  // non-intersecting elements in `Y` are appended, retaining their partial order.
   402  func (t *unstructuredSetList) Add(other ref.Val) ref.Val {
   403  	oSetList, ok := other.(traits.Lister)
   404  	if !ok {
   405  		return types.MaybeNoSuchOverloadErr(other)
   406  	}
   407  	elements := t.elements
   408  	set := t.getSet()
   409  	for it := oSetList.Iterator(); it.HasNext() == types.True; {
   410  		next := it.Next().Value()
   411  		if _, ok := set[next]; !ok {
   412  			set[next] = struct{}{}
   413  			elements = append(elements, next)
   414  		}
   415  	}
   416  	return &unstructuredSetList{
   417  		unstructuredList: unstructuredList{elements: elements, itemsSchema: t.itemsSchema},
   418  		escapedKeyProps:  t.escapedKeyProps,
   419  	}
   420  }
   421  
   422  // unstructuredList represents an unstructured data instance of an OpenAPI array with x-kubernetes-list-type=atomic (the default).
   423  type unstructuredList struct {
   424  	elements    []interface{}
   425  	itemsSchema Schema
   426  }
   427  
   428  var _ = traits.Lister(&unstructuredList{})
   429  
   430  func (t *unstructuredList) ConvertToNative(typeDesc reflect.Type) (interface{}, error) {
   431  	switch typeDesc.Kind() {
   432  	case reflect.Slice:
   433  		switch t.itemsSchema.Type() {
   434  		// Workaround for https://github.com/kubernetes/kubernetes/issues/117590 until we
   435  		// resolve the desired behavior in cel-go via https://github.com/google/cel-go/issues/688
   436  		case "string":
   437  			var result []string
   438  			for _, e := range t.elements {
   439  				s, ok := e.(string)
   440  				if !ok {
   441  					return nil, fmt.Errorf("expected all elements to be of type string, but got %T", e)
   442  				}
   443  				result = append(result, s)
   444  			}
   445  			return result, nil
   446  		default:
   447  			return t.elements, nil
   448  		}
   449  	}
   450  	return nil, fmt.Errorf("type conversion error from '%s' to '%s'", t.Type(), typeDesc)
   451  }
   452  
   453  func (t *unstructuredList) ConvertToType(typeValue ref.Type) ref.Val {
   454  	switch typeValue {
   455  	case types.ListType:
   456  		return t
   457  	case types.TypeType:
   458  		return types.ListType
   459  	}
   460  	return types.NewErr("type conversion error from '%s' to '%s'", t.Type(), typeValue.TypeName())
   461  }
   462  
   463  func (t *unstructuredList) Equal(other ref.Val) ref.Val {
   464  	oList, ok := other.(traits.Lister)
   465  	if !ok {
   466  		return types.MaybeNoSuchOverloadErr(other)
   467  	}
   468  	sz := types.Int(len(t.elements))
   469  	if sz != oList.Size() {
   470  		return types.False
   471  	}
   472  	for i := types.Int(0); i < sz; i++ {
   473  		eq := t.Get(i).Equal(oList.Get(i))
   474  		if eq != types.True {
   475  			return eq // either false or error
   476  		}
   477  	}
   478  	return types.True
   479  }
   480  
   481  func (t *unstructuredList) Type() ref.Type {
   482  	return types.ListType
   483  }
   484  
   485  func (t *unstructuredList) Value() interface{} {
   486  	return t.elements
   487  }
   488  
   489  func (t *unstructuredList) Add(other ref.Val) ref.Val {
   490  	oList, ok := other.(traits.Lister)
   491  	if !ok {
   492  		return types.MaybeNoSuchOverloadErr(other)
   493  	}
   494  	elements := t.elements
   495  	for it := oList.Iterator(); it.HasNext() == types.True; {
   496  		next := it.Next().Value()
   497  		elements = append(elements, next)
   498  	}
   499  
   500  	return &unstructuredList{elements: elements, itemsSchema: t.itemsSchema}
   501  }
   502  
   503  func (t *unstructuredList) Contains(val ref.Val) ref.Val {
   504  	if types.IsUnknownOrError(val) {
   505  		return val
   506  	}
   507  	var err ref.Val
   508  	sz := len(t.elements)
   509  	for i := 0; i < sz; i++ {
   510  		elem := UnstructuredToVal(t.elements[i], t.itemsSchema)
   511  		cmp := elem.Equal(val)
   512  		b, ok := cmp.(types.Bool)
   513  		if !ok && err == nil {
   514  			err = types.MaybeNoSuchOverloadErr(cmp)
   515  		}
   516  		if b == types.True {
   517  			return types.True
   518  		}
   519  	}
   520  	if err != nil {
   521  		return err
   522  	}
   523  	return types.False
   524  }
   525  
   526  func (t *unstructuredList) Get(idx ref.Val) ref.Val {
   527  	iv, isInt := idx.(types.Int)
   528  	if !isInt {
   529  		return types.ValOrErr(idx, "unsupported index: %v", idx)
   530  	}
   531  	i := int(iv)
   532  	if i < 0 || i >= len(t.elements) {
   533  		return types.NewErr("index out of bounds: %v", idx)
   534  	}
   535  	return UnstructuredToVal(t.elements[i], t.itemsSchema)
   536  }
   537  
   538  func (t *unstructuredList) Iterator() traits.Iterator {
   539  	items := make([]ref.Val, len(t.elements))
   540  	for i, item := range t.elements {
   541  		itemCopy := item
   542  		items[i] = UnstructuredToVal(itemCopy, t.itemsSchema)
   543  	}
   544  	return &listIterator{unstructuredList: t, items: items}
   545  }
   546  
   547  type listIterator struct {
   548  	*unstructuredList
   549  	items []ref.Val
   550  	idx   int
   551  }
   552  
   553  func (it *listIterator) HasNext() ref.Val {
   554  	return types.Bool(it.idx < len(it.items))
   555  }
   556  
   557  func (it *listIterator) Next() ref.Val {
   558  	item := it.items[it.idx]
   559  	it.idx++
   560  	return item
   561  }
   562  
   563  func (t *unstructuredList) Size() ref.Val {
   564  	return types.Int(len(t.elements))
   565  }
   566  
   567  // unstructuredMap represented an unstructured data instance of an OpenAPI object.
   568  type unstructuredMap struct {
   569  	value  map[string]interface{}
   570  	schema Schema
   571  	// propSchema finds the schema to use for a particular map key.
   572  	propSchema func(key string) (Schema, bool)
   573  }
   574  
   575  var _ = traits.Mapper(&unstructuredMap{})
   576  
   577  func (t *unstructuredMap) ConvertToNative(typeDesc reflect.Type) (interface{}, error) {
   578  	switch typeDesc.Kind() {
   579  	case reflect.Map:
   580  		return t.value, nil
   581  	}
   582  	return nil, fmt.Errorf("type conversion error from '%s' to '%s'", t.Type(), typeDesc)
   583  }
   584  
   585  func (t *unstructuredMap) ConvertToType(typeValue ref.Type) ref.Val {
   586  	switch typeValue {
   587  	case types.MapType:
   588  		return t
   589  	case types.TypeType:
   590  		return types.MapType
   591  	}
   592  	return types.NewErr("type conversion error from '%s' to '%s'", t.Type(), typeValue.TypeName())
   593  }
   594  
   595  func (t *unstructuredMap) Equal(other ref.Val) ref.Val {
   596  	oMap, isMap := other.(traits.Mapper)
   597  	if !isMap {
   598  		return types.MaybeNoSuchOverloadErr(other)
   599  	}
   600  	if t.Size() != oMap.Size() {
   601  		return types.False
   602  	}
   603  	for key, value := range t.value {
   604  		if propSchema, ok := t.propSchema(key); ok {
   605  			ov, found := oMap.Find(types.String(key))
   606  			if !found {
   607  				return types.False
   608  			}
   609  			v := UnstructuredToVal(value, propSchema)
   610  			vEq := v.Equal(ov)
   611  			if vEq != types.True {
   612  				return vEq // either false or error
   613  			}
   614  		} else {
   615  			// Must be an object with properties.
   616  			// Since we've encountered an unknown field, fallback to unstructured equality checking.
   617  			ouMap, ok := other.(*unstructuredMap)
   618  			if !ok {
   619  				// The compiler ensures equality is against the same type of object, so this should be unreachable
   620  				return types.MaybeNoSuchOverloadErr(other)
   621  			}
   622  			if oValue, ok := ouMap.value[key]; ok {
   623  				if !equality.Semantic.DeepEqual(value, oValue) {
   624  					return types.False
   625  				}
   626  			}
   627  		}
   628  	}
   629  	return types.True
   630  }
   631  
   632  func (t *unstructuredMap) Type() ref.Type {
   633  	return types.MapType
   634  }
   635  
   636  func (t *unstructuredMap) Value() interface{} {
   637  	return t.value
   638  }
   639  
   640  func (t *unstructuredMap) Contains(key ref.Val) ref.Val {
   641  	v, found := t.Find(key)
   642  	if v != nil && types.IsUnknownOrError(v) {
   643  		return v
   644  	}
   645  
   646  	return types.Bool(found)
   647  }
   648  
   649  func (t *unstructuredMap) Get(key ref.Val) ref.Val {
   650  	v, found := t.Find(key)
   651  	if found {
   652  		return v
   653  	}
   654  	return types.ValOrErr(key, "no such key: %v", key)
   655  }
   656  
   657  func (t *unstructuredMap) Iterator() traits.Iterator {
   658  	isObject := t.schema.Properties() != nil
   659  	keys := make([]ref.Val, len(t.value))
   660  	i := 0
   661  	for k := range t.value {
   662  		if _, ok := t.propSchema(k); ok {
   663  			mapKey := k
   664  			if isObject {
   665  				if escaped, ok := cel.Escape(k); ok {
   666  					mapKey = escaped
   667  				}
   668  			}
   669  			keys[i] = types.String(mapKey)
   670  			i++
   671  		}
   672  	}
   673  	return &mapIterator{unstructuredMap: t, keys: keys}
   674  }
   675  
   676  type mapIterator struct {
   677  	*unstructuredMap
   678  	keys []ref.Val
   679  	idx  int
   680  }
   681  
   682  func (it *mapIterator) HasNext() ref.Val {
   683  	return types.Bool(it.idx < len(it.keys))
   684  }
   685  
   686  func (it *mapIterator) Next() ref.Val {
   687  	key := it.keys[it.idx]
   688  	it.idx++
   689  	return key
   690  }
   691  
   692  func (t *unstructuredMap) Size() ref.Val {
   693  	return types.Int(len(t.value))
   694  }
   695  
   696  func (t *unstructuredMap) Find(key ref.Val) (ref.Val, bool) {
   697  	isObject := t.schema.Properties() != nil
   698  	keyStr, ok := key.(types.String)
   699  	if !ok {
   700  		return types.MaybeNoSuchOverloadErr(key), true
   701  	}
   702  	k := keyStr.Value().(string)
   703  	if isObject {
   704  		k, ok = cel.Unescape(k)
   705  		if !ok {
   706  			return nil, false
   707  		}
   708  	}
   709  	if v, ok := t.value[k]; ok {
   710  		// If this is an object with properties, not an object with additionalProperties,
   711  		// then null valued nullable fields are treated the same as absent optional fields.
   712  		if isObject && v == nil {
   713  			return nil, false
   714  		}
   715  		if propSchema, ok := t.propSchema(k); ok {
   716  			return UnstructuredToVal(v, propSchema), true
   717  		}
   718  	}
   719  
   720  	return nil, false
   721  }