github.com/juju/juju@v0.0.0-20240430160146-1752b71fcf00/core/constraints/validation.go (about)

     1  // Copyright 2014 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package constraints
     5  
     6  import (
     7  	"fmt"
     8  	"reflect"
     9  	"sort"
    10  	"strings"
    11  
    12  	"github.com/juju/collections/set"
    13  )
    14  
    15  // Validator defines operations on constraints attributes which are
    16  // used to ensure a constraints value is valid, as well as being able
    17  // to handle overridden attributes.
    18  type Validator interface {
    19  
    20  	// RegisterConflicts is used to define cross-constraint override behaviour.
    21  	// The red and blue attribute lists contain attribute names which conflict
    22  	// with those in the other list.
    23  	// When two constraints conflict:
    24  	//  it is an error to set both constraints in the same constraints Value.
    25  	//  when a constraints Value overrides another which specifies a conflicting
    26  	//   attribute, the attribute in the overridden Value is cleared.
    27  	RegisterConflicts(reds, blues []string)
    28  
    29  	// RegisterConflictResolver defines a resolver between two conflicting constraints.
    30  	// When there is a registered conflict between two contraints, it can be resolved by
    31  	// calling the resolver, if it returns a nil error, the conflict is considered resolved.
    32  	RegisterConflictResolver(red, blue string, resolver ConflictResolver)
    33  
    34  	// RegisterUnsupported records attributes which are not supported by a constraints Value.
    35  	RegisterUnsupported(unsupported []string)
    36  
    37  	// RegisterVocabulary records allowed values for the specified constraint attribute.
    38  	// allowedValues is expected to be a slice/array but is declared as interface{} so
    39  	// that vocabs of different types can be passed in.
    40  	RegisterVocabulary(attributeName string, allowedValues interface{})
    41  
    42  	// Validate returns an error if the given constraints are not valid, and also
    43  	// any unsupported attributes.
    44  	Validate(cons Value) ([]string, error)
    45  
    46  	// Merge merges cons into consFallback, with any conflicting attributes from cons
    47  	// overriding those from consFallback.
    48  	Merge(consFallback, cons Value) (Value, error)
    49  
    50  	// UpdateVocabulary merges new attribute values with existing values.
    51  	// This method does not overwrite or delete values, i.e.
    52  	//     if existing values are {a, b}
    53  	//     and new values are {c, d},
    54  	//     then the merge result would be {a, b, c, d}.
    55  	UpdateVocabulary(attributeName string, newValues interface{})
    56  }
    57  
    58  type ConflictResolver func(attrValues map[string]interface{}) error
    59  
    60  // NewValidator returns a new constraints Validator instance.
    61  func NewValidator() Validator {
    62  	return &validator{
    63  		conflicts:         make(map[string]set.Strings),
    64  		conflictResolvers: make(map[string]ConflictResolver),
    65  		vocab:             make(map[string][]interface{}),
    66  	}
    67  }
    68  
    69  type validator struct {
    70  	unsupported       set.Strings
    71  	conflicts         map[string]set.Strings
    72  	conflictResolvers map[string]ConflictResolver
    73  	vocab             map[string][]interface{}
    74  }
    75  
    76  // RegisterConflicts is defined on Validator.
    77  func (v *validator) RegisterConflicts(reds, blues []string) {
    78  	for _, red := range reds {
    79  		v.conflicts[red] = set.NewStrings(blues...)
    80  	}
    81  	for _, blue := range blues {
    82  		v.conflicts[blue] = set.NewStrings(reds...)
    83  	}
    84  }
    85  
    86  func conflictResolverId(red, blue string) string {
    87  	idx := []string{red, blue}
    88  	sort.Strings(idx)
    89  	return strings.Join(idx, " ")
    90  }
    91  
    92  func (v *validator) RegisterConflictResolver(red, blue string, resolver ConflictResolver) {
    93  	id := conflictResolverId(red, blue)
    94  	v.conflictResolvers[id] = resolver
    95  }
    96  
    97  // RegisterUnsupported is defined on Validator.
    98  func (v *validator) RegisterUnsupported(unsupported []string) {
    99  	v.unsupported = set.NewStrings(unsupported...)
   100  }
   101  
   102  // RegisterVocabulary is defined on Validator.
   103  func (v *validator) RegisterVocabulary(attributeName string, allowedValues interface{}) {
   104  	v.vocab[resolveAlias(attributeName)] = convertToSlice(allowedValues)
   105  }
   106  
   107  var checkIsCollection = func(coll interface{}) {
   108  	k := reflect.TypeOf(coll).Kind()
   109  	if k != reflect.Slice && k != reflect.Array {
   110  		panic(fmt.Errorf("invalid vocab: %v of type %T is not a slice", coll, coll))
   111  	}
   112  }
   113  
   114  var convertToSlice = func(coll interface{}) []interface{} {
   115  	checkIsCollection(coll)
   116  	var slice []interface{}
   117  	val := reflect.ValueOf(coll)
   118  	for i := 0; i < val.Len(); i++ {
   119  		slice = append(slice, val.Index(i).Interface())
   120  	}
   121  	return slice
   122  }
   123  
   124  // UpdateVocabulary is defined on Validator.
   125  func (v *validator) UpdateVocabulary(attributeName string, allowedValues interface{}) {
   126  	attributeName = resolveAlias(attributeName)
   127  	// If this attribute is not registered, delegate to RegisterVocabulary()
   128  	currentValues, ok := v.vocab[attributeName]
   129  	if !ok {
   130  		v.RegisterVocabulary(attributeName, allowedValues)
   131  	}
   132  
   133  	unique := map[interface{}]bool{}
   134  	writeUnique := func(all []interface{}) {
   135  		for _, one := range all {
   136  			unique[one] = true
   137  		}
   138  	}
   139  
   140  	// merge existing values with new, ensuring uniqueness
   141  	writeUnique(currentValues)
   142  	newValues := convertToSlice(allowedValues)
   143  	writeUnique(newValues)
   144  
   145  	v.updateVocabularyFromMap(attributeName, unique)
   146  }
   147  
   148  func (v *validator) updateVocabularyFromMap(attributeName string, valuesMap map[interface{}]bool) {
   149  	attributeName = resolveAlias(attributeName)
   150  	var merged []interface{}
   151  	for one := range valuesMap {
   152  		// TODO (anastasiamac) Because it's coming from the map, the order maybe affected
   153  		// and can be unreliable. Not sure how to fix it yet...
   154  		// How can we guarantee the order here?
   155  		merged = append(merged, one)
   156  	}
   157  	v.RegisterVocabulary(attributeName, merged)
   158  }
   159  
   160  // checkConflicts returns an error if the constraints Value contains conflicting attributes.
   161  func (v *validator) checkConflicts(cons Value) error {
   162  	attrValues := cons.attributesWithValues()
   163  	attrSet := make(set.Strings)
   164  	for attrTag := range attrValues {
   165  		attrSet.Add(attrTag)
   166  	}
   167  	for _, attrTag := range attrSet.SortedValues() {
   168  		conflicts, ok := v.conflicts[attrTag]
   169  		if !ok {
   170  			continue
   171  		}
   172  		for _, conflict := range conflicts.SortedValues() {
   173  			if !attrSet.Contains(conflict) {
   174  				continue
   175  			}
   176  			id := conflictResolverId(attrTag, conflict)
   177  			if resolver, ok := v.conflictResolvers[id]; ok {
   178  				err := resolver(attrValues)
   179  				if err != nil {
   180  					return fmt.Errorf("ambiguous constraints: %q overlaps with %q: %w", attrTag, conflict, err)
   181  				}
   182  				continue
   183  			}
   184  			return fmt.Errorf("ambiguous constraints: %q overlaps with %q", attrTag, conflict)
   185  		}
   186  	}
   187  	return nil
   188  }
   189  
   190  // checkUnsupported returns any unsupported attributes.
   191  func (v *validator) checkUnsupported(cons Value) []string {
   192  	return cons.hasAny(v.unsupported.Values()...)
   193  }
   194  
   195  // checkValidValues returns an error if the constraints value contains an
   196  // attribute value which is not allowed by the vocab which may have been
   197  // registered for it.
   198  func (v *validator) checkValidValues(cons Value) error {
   199  	for attrTag, attrValue := range cons.attributesWithValues() {
   200  		k := reflect.TypeOf(attrValue).Kind()
   201  		if k == reflect.Slice || k == reflect.Array {
   202  			// For slices we check that all values are valid.
   203  			val := reflect.ValueOf(attrValue)
   204  			for i := 0; i < val.Len(); i++ {
   205  				if err := v.checkInVocab(attrTag, val.Index(i).Interface()); err != nil {
   206  					return err
   207  				}
   208  			}
   209  		} else {
   210  			if err := v.checkInVocab(attrTag, attrValue); err != nil {
   211  				return err
   212  			}
   213  		}
   214  	}
   215  	return nil
   216  }
   217  
   218  // checkInVocab returns an error if the attribute value is not allowed by the
   219  // vocab which may have been registered for it.
   220  func (v *validator) checkInVocab(attributeName string, attributeValue interface{}) error {
   221  	validValues, ok := v.vocab[resolveAlias(attributeName)]
   222  	if !ok {
   223  		return nil
   224  	}
   225  	for _, validValue := range validValues {
   226  		if coerce(validValue) == coerce(attributeValue) {
   227  			return nil
   228  		}
   229  	}
   230  	return fmt.Errorf(
   231  		"invalid constraint value: %v=%v\nvalid values are: %v", attributeName, attributeValue, validValues)
   232  }
   233  
   234  // coerce returns v in a format that allows constraint values to be easily
   235  // compared. Its main purpose is to cast all numeric values to float64 (since
   236  // the numbers we compare are generated from json serialization).
   237  func coerce(v interface{}) interface{} {
   238  	switch val := v.(type) {
   239  	case string:
   240  		return v
   241  	// Yes, these are all the same, however we can't put them into a single
   242  	// case, or the value becomes interface{}, which can't be converted to a
   243  	// float64.
   244  	case int:
   245  		return float64(val)
   246  	case int8:
   247  		return float64(val)
   248  	case int16:
   249  		return float64(val)
   250  	case int32:
   251  		return float64(val)
   252  	case int64:
   253  		return float64(val)
   254  	case uint:
   255  		return float64(val)
   256  	case uint8:
   257  		return float64(val)
   258  	case uint16:
   259  		return float64(val)
   260  	case uint32:
   261  		return float64(val)
   262  	case uint64:
   263  		return float64(val)
   264  	case float32:
   265  		return float64(val)
   266  	case float64:
   267  		return val
   268  	}
   269  	return v
   270  }
   271  
   272  // withFallbacks returns a copy of v with nil values taken from vFallback.
   273  func withFallbacks(v Value, vFallback Value) Value {
   274  	vAttr := v.attributesWithValues()
   275  	fbAttr := vFallback.attributesWithValues()
   276  	for k, v := range fbAttr {
   277  		if _, ok := vAttr[k]; !ok {
   278  			vAttr[k] = v
   279  		}
   280  	}
   281  	return fromAttributes(vAttr)
   282  }
   283  
   284  // Validate is defined on Validator.
   285  func (v *validator) Validate(cons Value) ([]string, error) {
   286  	unsupported := v.checkUnsupported(cons)
   287  	if err := v.checkValidValues(cons); err != nil {
   288  		return unsupported, err
   289  	}
   290  	// Conflicts are validated after values because normally conflicting
   291  	// constraints may be valid based on those constraint values.
   292  	if err := v.checkConflicts(cons); err != nil {
   293  		return unsupported, err
   294  	}
   295  	return unsupported, nil
   296  }
   297  
   298  // Merge is defined on Validator.
   299  func (v *validator) Merge(consFallback, cons Value) (Value, error) {
   300  	// First ensure both constraints are valid. We don't care if there
   301  	// are constraint attributes that are unsupported.
   302  	if _, err := v.Validate(consFallback); err != nil {
   303  		return Value{}, err
   304  	}
   305  	if _, err := v.Validate(cons); err != nil {
   306  		return Value{}, err
   307  	}
   308  	// Gather any attributes from consFallback which conflict with those on cons.
   309  	attrValues := cons.attributesWithValues()
   310  	var fallbackConflicts []string
   311  	for attrTag := range attrValues {
   312  		fallbackConflicts = append(fallbackConflicts, v.conflicts[attrTag].Values()...)
   313  	}
   314  	// Null out the conflicting consFallback attribute values because
   315  	// cons takes priority. We can't error here because we
   316  	// know that aConflicts contains valid attr names.
   317  	consFallbackMinusConflicts := consFallback.without(fallbackConflicts...)
   318  	// The result is cons with fallbacks coming from any
   319  	// non conflicting consFallback attributes.
   320  	return withFallbacks(cons, consFallbackMinusConflicts), nil
   321  }