mirror of
https://github.com/ceph/ceph-csi.git
synced 2024-12-01 02:30:18 +00:00
f080b9e0c9
Signed-off-by: Niels de Vos <ndevos@ibm.com>
722 lines
20 KiB
Go
722 lines
20 KiB
Go
/*
|
|
Copyright 2021 The Kubernetes Authors.
|
|
|
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
you may not use this file except in compliance with the License.
|
|
You may obtain a copy of the License at
|
|
|
|
http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
Unless required by applicable law or agreed to in writing, software
|
|
distributed under the License is distributed on an "AS IS" BASIS,
|
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
See the License for the specific language governing permissions and
|
|
limitations under the License.
|
|
*/
|
|
|
|
package common
|
|
|
|
import (
|
|
"fmt"
|
|
"reflect"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/google/cel-go/common/types"
|
|
"github.com/google/cel-go/common/types/ref"
|
|
"github.com/google/cel-go/common/types/traits"
|
|
|
|
"k8s.io/kube-openapi/pkg/validation/strfmt"
|
|
|
|
"k8s.io/apimachinery/pkg/api/equality"
|
|
"k8s.io/apiserver/pkg/cel"
|
|
)
|
|
|
|
// UnstructuredToVal converts a Kubernetes unstructured data element to a CEL Val.
|
|
// The root schema of custom resource schema is expected contain type meta and object meta schemas.
|
|
// If Embedded resources do not contain type meta and object meta schemas, they will be added automatically.
|
|
func UnstructuredToVal(unstructured interface{}, schema Schema) ref.Val {
|
|
if unstructured == nil {
|
|
if schema.Nullable() {
|
|
return types.NullValue
|
|
}
|
|
return types.NewErr("invalid data, got null for schema with nullable=false")
|
|
}
|
|
if schema.IsXIntOrString() {
|
|
switch v := unstructured.(type) {
|
|
case string:
|
|
return types.String(v)
|
|
case int:
|
|
return types.Int(v)
|
|
case int32:
|
|
return types.Int(v)
|
|
case int64:
|
|
return types.Int(v)
|
|
}
|
|
return types.NewErr("invalid data, expected XIntOrString value to be either a string or integer")
|
|
}
|
|
if schema.Type() == "object" {
|
|
m, ok := unstructured.(map[string]interface{})
|
|
if !ok {
|
|
return types.NewErr("invalid data, expected a map for the provided schema with type=object")
|
|
}
|
|
if schema.IsXEmbeddedResource() || schema.Properties() != nil {
|
|
if schema.IsXEmbeddedResource() {
|
|
schema = schema.WithTypeAndObjectMeta()
|
|
}
|
|
return &unstructuredMap{
|
|
value: m,
|
|
schema: schema,
|
|
propSchema: func(key string) (Schema, bool) {
|
|
if schema, ok := schema.Properties()[key]; ok {
|
|
return schema, true
|
|
}
|
|
return nil, false
|
|
},
|
|
}
|
|
}
|
|
if schema.AdditionalProperties() != nil && schema.AdditionalProperties().Schema() != nil {
|
|
return &unstructuredMap{
|
|
value: m,
|
|
schema: schema,
|
|
propSchema: func(key string) (Schema, bool) {
|
|
return schema.AdditionalProperties().Schema(), true
|
|
},
|
|
}
|
|
}
|
|
|
|
// properties and additionalProperties are mutual exclusive, but nothing prevents the situation
|
|
// where both are missing.
|
|
// An object that (1) has no properties (2) has no additionalProperties or additionalProperties == false
|
|
// is treated as an empty object.
|
|
// An object that has additionalProperties == true is treated as an unstructured map.
|
|
// An object that has x-kubernetes-preserve-unknown-field extension set is treated as an unstructured map.
|
|
// Empty object vs unstructured map is differentiated by unstructuredMap implementation with the set schema.
|
|
// The resulting result remains the same.
|
|
return &unstructuredMap{
|
|
value: m,
|
|
schema: schema,
|
|
propSchema: func(key string) (Schema, bool) {
|
|
return nil, false
|
|
},
|
|
}
|
|
}
|
|
|
|
if schema.Type() == "array" {
|
|
l, ok := unstructured.([]interface{})
|
|
if !ok {
|
|
return types.NewErr("invalid data, expected an array for the provided schema with type=array")
|
|
}
|
|
if schema.Items() == nil {
|
|
return types.NewErr("invalid array type, expected Items with a non-empty Schema")
|
|
}
|
|
typedList := unstructuredList{elements: l, itemsSchema: schema.Items()}
|
|
listType := schema.XListType()
|
|
if listType != "" {
|
|
switch listType {
|
|
case "map":
|
|
mapKeys := schema.XListMapKeys()
|
|
return &unstructuredMapList{unstructuredList: typedList, escapedKeyProps: escapeKeyProps(mapKeys)}
|
|
case "set":
|
|
return &unstructuredSetList{unstructuredList: typedList}
|
|
case "atomic":
|
|
return &typedList
|
|
default:
|
|
return types.NewErr("invalid x-kubernetes-list-type, expected 'map', 'set' or 'atomic' but got %s", listType)
|
|
}
|
|
}
|
|
return &typedList
|
|
}
|
|
|
|
if schema.Type() == "string" {
|
|
str, ok := unstructured.(string)
|
|
if !ok {
|
|
return types.NewErr("invalid data, expected string, got %T", unstructured)
|
|
}
|
|
switch schema.Format() {
|
|
case "duration":
|
|
d, err := strfmt.ParseDuration(str)
|
|
if err != nil {
|
|
return types.NewErr("Invalid duration %s: %v", str, err)
|
|
}
|
|
return types.Duration{Duration: d}
|
|
case "date":
|
|
d, err := time.Parse(strfmt.RFC3339FullDate, str) // strfmt uses this format for OpenAPIv3 value validation
|
|
if err != nil {
|
|
return types.NewErr("Invalid date formatted string %s: %v", str, err)
|
|
}
|
|
return types.Timestamp{Time: d}
|
|
case "date-time":
|
|
d, err := strfmt.ParseDateTime(str)
|
|
if err != nil {
|
|
return types.NewErr("Invalid date-time formatted string %s: %v", str, err)
|
|
}
|
|
return types.Timestamp{Time: time.Time(d)}
|
|
case "byte":
|
|
base64 := strfmt.Base64{}
|
|
err := base64.UnmarshalText([]byte(str))
|
|
if err != nil {
|
|
return types.NewErr("Invalid byte formatted string %s: %v", str, err)
|
|
}
|
|
return types.Bytes(base64)
|
|
}
|
|
|
|
return types.String(str)
|
|
}
|
|
if schema.Type() == "number" {
|
|
switch v := unstructured.(type) {
|
|
// float representations of whole numbers (e.g. 1.0, 0.0) can convert to int representations (e.g. 1, 0) in yaml
|
|
// to json translation, and then get parsed as int64s
|
|
case int:
|
|
return types.Double(v)
|
|
case int32:
|
|
return types.Double(v)
|
|
case int64:
|
|
return types.Double(v)
|
|
|
|
case float32:
|
|
return types.Double(v)
|
|
case float64:
|
|
return types.Double(v)
|
|
default:
|
|
return types.NewErr("invalid data, expected float, got %T", unstructured)
|
|
}
|
|
}
|
|
if schema.Type() == "integer" {
|
|
switch v := unstructured.(type) {
|
|
case int:
|
|
return types.Int(v)
|
|
case int32:
|
|
return types.Int(v)
|
|
case int64:
|
|
return types.Int(v)
|
|
default:
|
|
return types.NewErr("invalid data, expected int, got %T", unstructured)
|
|
}
|
|
}
|
|
if schema.Type() == "boolean" {
|
|
b, ok := unstructured.(bool)
|
|
if !ok {
|
|
return types.NewErr("invalid data, expected bool, got %T", unstructured)
|
|
}
|
|
return types.Bool(b)
|
|
}
|
|
|
|
if schema.IsXPreserveUnknownFields() {
|
|
return &unknownPreserved{u: unstructured}
|
|
}
|
|
|
|
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())
|
|
}
|
|
|
|
// unknownPreserved represents unknown data preserved in custom resources via x-kubernetes-preserve-unknown-fields.
|
|
// It preserves the data at runtime without assuming it is of any particular type and supports only equality checking.
|
|
// unknownPreserved should be used only for values are not directly accessible in CEL expressions, i.e. for data
|
|
// where there is no corresponding CEL type declaration.
|
|
type unknownPreserved struct {
|
|
u interface{}
|
|
}
|
|
|
|
func (t *unknownPreserved) ConvertToNative(refType reflect.Type) (interface{}, error) {
|
|
return nil, fmt.Errorf("type conversion to '%s' not supported for values preserved by x-kubernetes-preserve-unknown-fields", refType)
|
|
}
|
|
|
|
func (t *unknownPreserved) ConvertToType(typeValue ref.Type) ref.Val {
|
|
return types.NewErr("type conversion to '%s' not supported for values preserved by x-kubernetes-preserve-unknown-fields", typeValue.TypeName())
|
|
}
|
|
|
|
func (t *unknownPreserved) Equal(other ref.Val) ref.Val {
|
|
return types.Bool(equality.Semantic.DeepEqual(t.u, other.Value()))
|
|
}
|
|
|
|
func (t *unknownPreserved) Type() ref.Type {
|
|
return types.UnknownType
|
|
}
|
|
|
|
func (t *unknownPreserved) Value() interface{} {
|
|
return t.u // used by Equal checks
|
|
}
|
|
|
|
// unstructuredMapList represents an unstructured data instance of an OpenAPI array with x-kubernetes-list-type=map.
|
|
type unstructuredMapList struct {
|
|
unstructuredList
|
|
escapedKeyProps []string
|
|
|
|
sync.Once // for for lazy load of mapOfList since it is only needed if Equals is called
|
|
mapOfList map[interface{}]interface{}
|
|
}
|
|
|
|
func (t *unstructuredMapList) getMap() map[interface{}]interface{} {
|
|
t.Do(func() {
|
|
t.mapOfList = make(map[interface{}]interface{}, len(t.elements))
|
|
for _, e := range t.elements {
|
|
t.mapOfList[t.toMapKey(e)] = e
|
|
}
|
|
})
|
|
return t.mapOfList
|
|
}
|
|
|
|
// toMapKey returns a valid golang map key for the given element of the map list.
|
|
// element must be a valid map list entry where all map key props are scalar types (which are comparable in go
|
|
// and valid for use in a golang map key).
|
|
func (t *unstructuredMapList) toMapKey(element interface{}) interface{} {
|
|
eObj, ok := element.(map[string]interface{})
|
|
if !ok {
|
|
return types.NewErr("unexpected data format for element of array with x-kubernetes-list-type=map: %T", element)
|
|
}
|
|
// Arrays are comparable in go and may be used as map keys, but maps and slices are not.
|
|
// So we can special case small numbers of key props as arrays and fall back to serialization
|
|
// for larger numbers of key props
|
|
if len(t.escapedKeyProps) == 1 {
|
|
return eObj[t.escapedKeyProps[0]]
|
|
}
|
|
if len(t.escapedKeyProps) == 2 {
|
|
return [2]interface{}{eObj[t.escapedKeyProps[0]], eObj[t.escapedKeyProps[1]]}
|
|
}
|
|
if len(t.escapedKeyProps) == 3 {
|
|
return [3]interface{}{eObj[t.escapedKeyProps[0]], eObj[t.escapedKeyProps[1]], eObj[t.escapedKeyProps[2]]}
|
|
}
|
|
|
|
key := make([]interface{}, len(t.escapedKeyProps))
|
|
for i, kf := range t.escapedKeyProps {
|
|
key[i] = eObj[kf]
|
|
}
|
|
return fmt.Sprintf("%v", key)
|
|
}
|
|
|
|
// Equal on a map list ignores list element order.
|
|
func (t *unstructuredMapList) Equal(other ref.Val) ref.Val {
|
|
oMapList, ok := other.(traits.Lister)
|
|
if !ok {
|
|
return types.MaybeNoSuchOverloadErr(other)
|
|
}
|
|
sz := types.Int(len(t.elements))
|
|
if sz != oMapList.Size() {
|
|
return types.False
|
|
}
|
|
tMap := t.getMap()
|
|
for it := oMapList.Iterator(); it.HasNext() == types.True; {
|
|
v := it.Next()
|
|
k := t.toMapKey(v.Value())
|
|
tVal, ok := tMap[k]
|
|
if !ok {
|
|
return types.False
|
|
}
|
|
eq := UnstructuredToVal(tVal, t.itemsSchema).Equal(v)
|
|
if eq != types.True {
|
|
return eq // either false or error
|
|
}
|
|
}
|
|
return types.True
|
|
}
|
|
|
|
// Add for a map list `X + Y` performs a merge where the array positions of all keys in `X` are preserved but the values
|
|
// are overwritten by values in `Y` when the key sets of `X` and `Y` intersect. Elements in `Y` with
|
|
// non-intersecting keys are appended, retaining their partial order.
|
|
func (t *unstructuredMapList) Add(other ref.Val) ref.Val {
|
|
oMapList, ok := other.(traits.Lister)
|
|
if !ok {
|
|
return types.MaybeNoSuchOverloadErr(other)
|
|
}
|
|
elements := make([]interface{}, len(t.elements))
|
|
keyToIdx := map[interface{}]int{}
|
|
for i, e := range t.elements {
|
|
k := t.toMapKey(e)
|
|
keyToIdx[k] = i
|
|
elements[i] = e
|
|
}
|
|
for it := oMapList.Iterator(); it.HasNext() == types.True; {
|
|
v := it.Next().Value()
|
|
k := t.toMapKey(v)
|
|
if overwritePosition, ok := keyToIdx[k]; ok {
|
|
elements[overwritePosition] = v
|
|
} else {
|
|
elements = append(elements, v)
|
|
}
|
|
}
|
|
return &unstructuredMapList{
|
|
unstructuredList: unstructuredList{elements: elements, itemsSchema: t.itemsSchema},
|
|
escapedKeyProps: t.escapedKeyProps,
|
|
}
|
|
}
|
|
|
|
// escapeKeyProps returns identifiers with Escape applied to each.
|
|
// Identifiers that cannot be escaped are left as-is. They are inaccessible to CEL programs but are
|
|
// are still needed internally to perform equality checks.
|
|
func escapeKeyProps(idents []string) []string {
|
|
result := make([]string, len(idents))
|
|
for i, prop := range idents {
|
|
if escaped, ok := cel.Escape(prop); ok {
|
|
result[i] = escaped
|
|
} else {
|
|
result[i] = prop
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
|
|
// unstructuredSetList represents an unstructured data instance of an OpenAPI array with x-kubernetes-list-type=set.
|
|
type unstructuredSetList struct {
|
|
unstructuredList
|
|
escapedKeyProps []string
|
|
|
|
sync.Once // for for lazy load of setOfList since it is only needed if Equals is called
|
|
set map[interface{}]struct{}
|
|
}
|
|
|
|
func (t *unstructuredSetList) getSet() map[interface{}]struct{} {
|
|
// sets are only allowed to contain scalar elements, which are comparable in go, and can safely be used as
|
|
// golang map keys
|
|
t.Do(func() {
|
|
t.set = make(map[interface{}]struct{}, len(t.elements))
|
|
for _, e := range t.elements {
|
|
t.set[e] = struct{}{}
|
|
}
|
|
})
|
|
return t.set
|
|
}
|
|
|
|
// Equal on a map list ignores list element order.
|
|
func (t *unstructuredSetList) Equal(other ref.Val) ref.Val {
|
|
oSetList, ok := other.(traits.Lister)
|
|
if !ok {
|
|
return types.MaybeNoSuchOverloadErr(other)
|
|
}
|
|
sz := types.Int(len(t.elements))
|
|
if sz != oSetList.Size() {
|
|
return types.False
|
|
}
|
|
tSet := t.getSet()
|
|
for it := oSetList.Iterator(); it.HasNext() == types.True; {
|
|
next := it.Next().Value()
|
|
_, ok := tSet[next]
|
|
if !ok {
|
|
return types.False
|
|
}
|
|
}
|
|
return types.True
|
|
}
|
|
|
|
// Add for a set list `X + Y` performs a union where the array positions of all elements in `X` are preserved and
|
|
// non-intersecting elements in `Y` are appended, retaining their partial order.
|
|
func (t *unstructuredSetList) Add(other ref.Val) ref.Val {
|
|
oSetList, ok := other.(traits.Lister)
|
|
if !ok {
|
|
return types.MaybeNoSuchOverloadErr(other)
|
|
}
|
|
elements := t.elements
|
|
set := t.getSet()
|
|
for it := oSetList.Iterator(); it.HasNext() == types.True; {
|
|
next := it.Next().Value()
|
|
if _, ok := set[next]; !ok {
|
|
set[next] = struct{}{}
|
|
elements = append(elements, next)
|
|
}
|
|
}
|
|
return &unstructuredSetList{
|
|
unstructuredList: unstructuredList{elements: elements, itemsSchema: t.itemsSchema},
|
|
escapedKeyProps: t.escapedKeyProps,
|
|
}
|
|
}
|
|
|
|
// unstructuredList represents an unstructured data instance of an OpenAPI array with x-kubernetes-list-type=atomic (the default).
|
|
type unstructuredList struct {
|
|
elements []interface{}
|
|
itemsSchema Schema
|
|
}
|
|
|
|
var _ = traits.Lister(&unstructuredList{})
|
|
|
|
func (t *unstructuredList) ConvertToNative(typeDesc reflect.Type) (interface{}, error) {
|
|
switch typeDesc.Kind() {
|
|
case reflect.Slice:
|
|
switch t.itemsSchema.Type() {
|
|
// Workaround for https://github.com/kubernetes/kubernetes/issues/117590 until we
|
|
// resolve the desired behavior in cel-go via https://github.com/google/cel-go/issues/688
|
|
case "string":
|
|
var result []string
|
|
for _, e := range t.elements {
|
|
s, ok := e.(string)
|
|
if !ok {
|
|
return nil, fmt.Errorf("expected all elements to be of type string, but got %T", e)
|
|
}
|
|
result = append(result, s)
|
|
}
|
|
return result, nil
|
|
default:
|
|
return t.elements, nil
|
|
}
|
|
}
|
|
return nil, fmt.Errorf("type conversion error from '%s' to '%s'", t.Type(), typeDesc)
|
|
}
|
|
|
|
func (t *unstructuredList) ConvertToType(typeValue ref.Type) ref.Val {
|
|
switch typeValue {
|
|
case types.ListType:
|
|
return t
|
|
case types.TypeType:
|
|
return types.ListType
|
|
}
|
|
return types.NewErr("type conversion error from '%s' to '%s'", t.Type(), typeValue.TypeName())
|
|
}
|
|
|
|
func (t *unstructuredList) Equal(other ref.Val) ref.Val {
|
|
oList, ok := other.(traits.Lister)
|
|
if !ok {
|
|
return types.MaybeNoSuchOverloadErr(other)
|
|
}
|
|
sz := types.Int(len(t.elements))
|
|
if sz != oList.Size() {
|
|
return types.False
|
|
}
|
|
for i := types.Int(0); i < sz; i++ {
|
|
eq := t.Get(i).Equal(oList.Get(i))
|
|
if eq != types.True {
|
|
return eq // either false or error
|
|
}
|
|
}
|
|
return types.True
|
|
}
|
|
|
|
func (t *unstructuredList) Type() ref.Type {
|
|
return types.ListType
|
|
}
|
|
|
|
func (t *unstructuredList) Value() interface{} {
|
|
return t.elements
|
|
}
|
|
|
|
func (t *unstructuredList) Add(other ref.Val) ref.Val {
|
|
oList, ok := other.(traits.Lister)
|
|
if !ok {
|
|
return types.MaybeNoSuchOverloadErr(other)
|
|
}
|
|
elements := t.elements
|
|
for it := oList.Iterator(); it.HasNext() == types.True; {
|
|
next := it.Next().Value()
|
|
elements = append(elements, next)
|
|
}
|
|
|
|
return &unstructuredList{elements: elements, itemsSchema: t.itemsSchema}
|
|
}
|
|
|
|
func (t *unstructuredList) Contains(val ref.Val) ref.Val {
|
|
if types.IsUnknownOrError(val) {
|
|
return val
|
|
}
|
|
var err ref.Val
|
|
sz := len(t.elements)
|
|
for i := 0; i < sz; i++ {
|
|
elem := UnstructuredToVal(t.elements[i], t.itemsSchema)
|
|
cmp := elem.Equal(val)
|
|
b, ok := cmp.(types.Bool)
|
|
if !ok && err == nil {
|
|
err = types.MaybeNoSuchOverloadErr(cmp)
|
|
}
|
|
if b == types.True {
|
|
return types.True
|
|
}
|
|
}
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return types.False
|
|
}
|
|
|
|
func (t *unstructuredList) Get(idx ref.Val) ref.Val {
|
|
iv, isInt := idx.(types.Int)
|
|
if !isInt {
|
|
return types.ValOrErr(idx, "unsupported index: %v", idx)
|
|
}
|
|
i := int(iv)
|
|
if i < 0 || i >= len(t.elements) {
|
|
return types.NewErr("index out of bounds: %v", idx)
|
|
}
|
|
return UnstructuredToVal(t.elements[i], t.itemsSchema)
|
|
}
|
|
|
|
func (t *unstructuredList) Iterator() traits.Iterator {
|
|
items := make([]ref.Val, len(t.elements))
|
|
for i, item := range t.elements {
|
|
itemCopy := item
|
|
items[i] = UnstructuredToVal(itemCopy, t.itemsSchema)
|
|
}
|
|
return &listIterator{unstructuredList: t, items: items}
|
|
}
|
|
|
|
type listIterator struct {
|
|
*unstructuredList
|
|
items []ref.Val
|
|
idx int
|
|
}
|
|
|
|
func (it *listIterator) HasNext() ref.Val {
|
|
return types.Bool(it.idx < len(it.items))
|
|
}
|
|
|
|
func (it *listIterator) Next() ref.Val {
|
|
item := it.items[it.idx]
|
|
it.idx++
|
|
return item
|
|
}
|
|
|
|
func (t *unstructuredList) Size() ref.Val {
|
|
return types.Int(len(t.elements))
|
|
}
|
|
|
|
// unstructuredMap represented an unstructured data instance of an OpenAPI object.
|
|
type unstructuredMap struct {
|
|
value map[string]interface{}
|
|
schema Schema
|
|
// propSchema finds the schema to use for a particular map key.
|
|
propSchema func(key string) (Schema, bool)
|
|
}
|
|
|
|
var _ = traits.Mapper(&unstructuredMap{})
|
|
|
|
func (t *unstructuredMap) ConvertToNative(typeDesc reflect.Type) (interface{}, error) {
|
|
switch typeDesc.Kind() {
|
|
case reflect.Map:
|
|
return t.value, nil
|
|
}
|
|
return nil, fmt.Errorf("type conversion error from '%s' to '%s'", t.Type(), typeDesc)
|
|
}
|
|
|
|
func (t *unstructuredMap) ConvertToType(typeValue ref.Type) ref.Val {
|
|
switch typeValue {
|
|
case types.MapType:
|
|
return t
|
|
case types.TypeType:
|
|
return types.MapType
|
|
}
|
|
return types.NewErr("type conversion error from '%s' to '%s'", t.Type(), typeValue.TypeName())
|
|
}
|
|
|
|
func (t *unstructuredMap) Equal(other ref.Val) ref.Val {
|
|
oMap, isMap := other.(traits.Mapper)
|
|
if !isMap {
|
|
return types.MaybeNoSuchOverloadErr(other)
|
|
}
|
|
if t.Size() != oMap.Size() {
|
|
return types.False
|
|
}
|
|
for key, value := range t.value {
|
|
if propSchema, ok := t.propSchema(key); ok {
|
|
ov, found := oMap.Find(types.String(key))
|
|
if !found {
|
|
return types.False
|
|
}
|
|
v := UnstructuredToVal(value, propSchema)
|
|
vEq := v.Equal(ov)
|
|
if vEq != types.True {
|
|
return vEq // either false or error
|
|
}
|
|
} else {
|
|
// Must be an object with properties.
|
|
// Since we've encountered an unknown field, fallback to unstructured equality checking.
|
|
ouMap, ok := other.(*unstructuredMap)
|
|
if !ok {
|
|
// The compiler ensures equality is against the same type of object, so this should be unreachable
|
|
return types.MaybeNoSuchOverloadErr(other)
|
|
}
|
|
if oValue, ok := ouMap.value[key]; ok {
|
|
if !equality.Semantic.DeepEqual(value, oValue) {
|
|
return types.False
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return types.True
|
|
}
|
|
|
|
func (t *unstructuredMap) Type() ref.Type {
|
|
return types.MapType
|
|
}
|
|
|
|
func (t *unstructuredMap) Value() interface{} {
|
|
return t.value
|
|
}
|
|
|
|
func (t *unstructuredMap) Contains(key ref.Val) ref.Val {
|
|
v, found := t.Find(key)
|
|
if v != nil && types.IsUnknownOrError(v) {
|
|
return v
|
|
}
|
|
|
|
return types.Bool(found)
|
|
}
|
|
|
|
func (t *unstructuredMap) Get(key ref.Val) ref.Val {
|
|
v, found := t.Find(key)
|
|
if found {
|
|
return v
|
|
}
|
|
return types.ValOrErr(key, "no such key: %v", key)
|
|
}
|
|
|
|
func (t *unstructuredMap) Iterator() traits.Iterator {
|
|
isObject := t.schema.Properties() != nil
|
|
keys := make([]ref.Val, len(t.value))
|
|
i := 0
|
|
for k := range t.value {
|
|
if _, ok := t.propSchema(k); ok {
|
|
mapKey := k
|
|
if isObject {
|
|
if escaped, ok := cel.Escape(k); ok {
|
|
mapKey = escaped
|
|
}
|
|
}
|
|
keys[i] = types.String(mapKey)
|
|
i++
|
|
}
|
|
}
|
|
return &mapIterator{unstructuredMap: t, keys: keys}
|
|
}
|
|
|
|
type mapIterator struct {
|
|
*unstructuredMap
|
|
keys []ref.Val
|
|
idx int
|
|
}
|
|
|
|
func (it *mapIterator) HasNext() ref.Val {
|
|
return types.Bool(it.idx < len(it.keys))
|
|
}
|
|
|
|
func (it *mapIterator) Next() ref.Val {
|
|
key := it.keys[it.idx]
|
|
it.idx++
|
|
return key
|
|
}
|
|
|
|
func (t *unstructuredMap) Size() ref.Val {
|
|
return types.Int(len(t.value))
|
|
}
|
|
|
|
func (t *unstructuredMap) Find(key ref.Val) (ref.Val, bool) {
|
|
isObject := t.schema.Properties() != nil
|
|
keyStr, ok := key.(types.String)
|
|
if !ok {
|
|
return types.MaybeNoSuchOverloadErr(key), true
|
|
}
|
|
k := keyStr.Value().(string)
|
|
if isObject {
|
|
k, ok = cel.Unescape(k)
|
|
if !ok {
|
|
return nil, false
|
|
}
|
|
}
|
|
if v, ok := t.value[k]; ok {
|
|
// If this is an object with properties, not an object with additionalProperties,
|
|
// then null valued nullable fields are treated the same as absent optional fields.
|
|
if isObject && v == nil {
|
|
return nil, false
|
|
}
|
|
if propSchema, ok := t.propSchema(k); ok {
|
|
return UnstructuredToVal(v, propSchema), true
|
|
}
|
|
}
|
|
|
|
return nil, false
|
|
}
|