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

     1  /*
     2  Copyright 2022 The Kubernetes Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package common
    18  
    19  import (
    20  	"fmt"
    21  	"strings"
    22  )
    23  
    24  // MapList provides a "lookup by key" operation for lists (arrays) with x-kubernetes-list-type=map.
    25  type MapList interface {
    26  	// Get returns the first element having given key, for all
    27  	// x-kubernetes-list-map-keys, to the provided object. If the provided object isn't itself a valid MapList element,
    28  	// get returns nil.
    29  	Get(interface{}) interface{}
    30  }
    31  
    32  type keyStrategy interface {
    33  	// CompositeKeyFor returns a composite key for the provided object, if possible, and a
    34  	// boolean that indicates whether or not a key could be generated for the provided object.
    35  	CompositeKeyFor(map[string]interface{}) (interface{}, bool)
    36  }
    37  
    38  // singleKeyStrategy is a cheaper strategy for associative lists that have exactly one key.
    39  type singleKeyStrategy struct {
    40  	key string
    41  }
    42  
    43  // CompositeKeyFor directly returns the value of the single key  to
    44  // use as a composite key.
    45  func (ks *singleKeyStrategy) CompositeKeyFor(obj map[string]interface{}) (interface{}, bool) {
    46  	v, ok := obj[ks.key]
    47  	if !ok {
    48  		return nil, false
    49  	}
    50  
    51  	switch v.(type) {
    52  	case bool, float64, int64, string:
    53  		return v, true
    54  	default:
    55  		return nil, false // non-scalar
    56  	}
    57  }
    58  
    59  // multiKeyStrategy computes a composite key of all key values.
    60  type multiKeyStrategy struct {
    61  	sts Schema
    62  }
    63  
    64  // CompositeKeyFor returns a composite key computed from the values of all
    65  // keys.
    66  func (ks *multiKeyStrategy) CompositeKeyFor(obj map[string]interface{}) (interface{}, bool) {
    67  	const keyDelimiter = "\x00" // 0 byte should never appear in the composite key except as delimiter
    68  
    69  	var delimited strings.Builder
    70  	for _, key := range ks.sts.XListMapKeys() {
    71  		v, ok := obj[key]
    72  		if !ok {
    73  			return nil, false
    74  		}
    75  
    76  		switch v.(type) {
    77  		case bool:
    78  			fmt.Fprintf(&delimited, keyDelimiter+"%t", v)
    79  		case float64:
    80  			fmt.Fprintf(&delimited, keyDelimiter+"%f", v)
    81  		case int64:
    82  			fmt.Fprintf(&delimited, keyDelimiter+"%d", v)
    83  		case string:
    84  			fmt.Fprintf(&delimited, keyDelimiter+"%q", v)
    85  		default:
    86  			return nil, false // values must be scalars
    87  		}
    88  	}
    89  	return delimited.String(), true
    90  }
    91  
    92  // emptyMapList is a MapList containing no elements.
    93  type emptyMapList struct{}
    94  
    95  func (emptyMapList) Get(interface{}) interface{} {
    96  	return nil
    97  }
    98  
    99  type mapListImpl struct {
   100  	sts Schema
   101  	ks  keyStrategy
   102  	// keyedItems contains all lazily keyed map items
   103  	keyedItems map[interface{}]interface{}
   104  	// unkeyedItems contains all map items that have not yet been keyed
   105  	unkeyedItems []interface{}
   106  }
   107  
   108  func (a *mapListImpl) Get(obj interface{}) interface{} {
   109  	mobj, ok := obj.(map[string]interface{})
   110  	if !ok {
   111  		return nil
   112  	}
   113  
   114  	key, ok := a.ks.CompositeKeyFor(mobj)
   115  	if !ok {
   116  		return nil
   117  	}
   118  	if match, ok := a.keyedItems[key]; ok {
   119  		return match
   120  	}
   121  	// keep keying items until we either find a match or run out of unkeyed items
   122  	for len(a.unkeyedItems) > 0 {
   123  		// dequeue an unkeyed item
   124  		item := a.unkeyedItems[0]
   125  		a.unkeyedItems = a.unkeyedItems[1:]
   126  
   127  		// key the item
   128  		mitem, ok := item.(map[string]interface{})
   129  		if !ok {
   130  			continue
   131  		}
   132  		itemKey, ok := a.ks.CompositeKeyFor(mitem)
   133  		if !ok {
   134  			continue
   135  		}
   136  		if _, exists := a.keyedItems[itemKey]; !exists {
   137  			a.keyedItems[itemKey] = mitem
   138  		}
   139  
   140  		// if it matches, short-circuit
   141  		if itemKey == key {
   142  			return mitem
   143  		}
   144  	}
   145  
   146  	return nil
   147  }
   148  
   149  func makeKeyStrategy(sts Schema) keyStrategy {
   150  	listMapKeys := sts.XListMapKeys()
   151  	if len(listMapKeys) == 1 {
   152  		key := listMapKeys[0]
   153  		return &singleKeyStrategy{
   154  			key: key,
   155  		}
   156  	}
   157  
   158  	return &multiKeyStrategy{
   159  		sts: sts,
   160  	}
   161  }
   162  
   163  // MakeMapList returns a queryable interface over the provided x-kubernetes-list-type=map
   164  // keyedItems. If the provided schema is _not_ an array with x-kubernetes-list-type=map, returns an
   165  // empty mapList.
   166  func MakeMapList(sts Schema, items []interface{}) (rv MapList) {
   167  	if sts.Type() != "array" || sts.XListType() != "map" || len(sts.XListMapKeys()) == 0 || len(items) == 0 {
   168  		return emptyMapList{}
   169  	}
   170  	ks := makeKeyStrategy(sts)
   171  	return &mapListImpl{
   172  		sts:          sts,
   173  		ks:           ks,
   174  		keyedItems:   map[interface{}]interface{}{},
   175  		unkeyedItems: items,
   176  	}
   177  }