Merge branch 'main' into 18842-arv-mount-disk-config
[arvados.git] / sdk / go / arvados / vocabulary.go
index 5804d4c408a291cac3998be11d3972ea12b940c9..bf60a770267e437f7551bfb59fc60e62920fea9c 100644 (file)
@@ -7,8 +7,10 @@ package arvados
 import (
        "bytes"
        "encoding/json"
+       "errors"
        "fmt"
        "reflect"
+       "strconv"
        "strings"
 )
 
@@ -35,6 +37,8 @@ func (v *Vocabulary) systemTagKeys() map[string]bool {
                "docker-image-repo-tag": true,
                "filters":               true,
                "container_request":     true,
+               "cwl_input":             true,
+               "cwl_output":            true,
        }
 }
 
@@ -46,17 +50,41 @@ type VocabularyTagValue struct {
        Labels []VocabularyLabel `json:"labels"`
 }
 
+// NewVocabulary creates a new Vocabulary from a JSON definition and a list
+// of reserved tag keys that will get special treatment when strict mode is
+// enabled.
 func NewVocabulary(data []byte, managedTagKeys []string) (voc *Vocabulary, err error) {
        if r := bytes.Compare(data, []byte("")); r == 0 {
                return &Vocabulary{}, nil
        }
        err = json.Unmarshal(data, &voc)
        if err != nil {
-               return nil, fmt.Errorf("invalid JSON format error: %q", err)
+               var serr *json.SyntaxError
+               if errors.As(err, &serr) {
+                       offset := serr.Offset
+                       errorMsg := string(data[:offset])
+                       line := 1 + strings.Count(errorMsg, "\n")
+                       column := offset - int64(strings.LastIndex(errorMsg, "\n")+len("\n"))
+                       return nil, fmt.Errorf("invalid JSON format: %q (line %d, column %d)", err, line, column)
+               }
+               return nil, fmt.Errorf("invalid JSON format: %q", err)
        }
        if reflect.DeepEqual(voc, &Vocabulary{}) {
                return nil, fmt.Errorf("JSON data provided doesn't match Vocabulary format: %q", data)
        }
+
+       shouldReportErrors := false
+       errors := []string{}
+
+       // json.Unmarshal() doesn't error out on duplicate keys.
+       dupedKeys := []string{}
+       err = checkJSONDupedKeys(json.NewDecoder(bytes.NewReader(data)), nil, &dupedKeys)
+       if err != nil {
+               shouldReportErrors = true
+               for _, dk := range dupedKeys {
+                       errors = append(errors, fmt.Sprintf("duplicate JSON key %q", dk))
+               }
+       }
        voc.reservedTagKeys = make(map[string]bool)
        for _, managedKey := range managedTagKeys {
                voc.reservedTagKeys[managedKey] = true
@@ -64,59 +92,122 @@ func NewVocabulary(data []byte, managedTagKeys []string) (voc *Vocabulary, err e
        for systemKey := range voc.systemTagKeys() {
                voc.reservedTagKeys[systemKey] = true
        }
-       err = voc.Validate()
+       validationErrs, err := voc.validate()
        if err != nil {
-               return nil, err
+               shouldReportErrors = true
+               errors = append(errors, validationErrs...)
+       }
+       if shouldReportErrors {
+               return nil, fmt.Errorf("%s", strings.Join(errors, "\n"))
        }
        return voc, nil
 }
 
-func (v *Vocabulary) Validate() error {
-       if v == nil {
+func checkJSONDupedKeys(d *json.Decoder, path []string, errors *[]string) error {
+       t, err := d.Token()
+       if err != nil {
+               return err
+       }
+       delim, ok := t.(json.Delim)
+       if !ok {
                return nil
        }
-       tagKeys := map[string]bool{}
+       switch delim {
+       case '{':
+               keys := make(map[string]bool)
+               for d.More() {
+                       t, err := d.Token()
+                       if err != nil {
+                               return err
+                       }
+                       key := t.(string)
+
+                       if keys[key] {
+                               *errors = append(*errors, strings.Join(append(path, key), "."))
+                       }
+                       keys[key] = true
+
+                       if err := checkJSONDupedKeys(d, append(path, key), errors); err != nil {
+                               return err
+                       }
+               }
+               // consume closing '}'
+               if _, err := d.Token(); err != nil {
+                       return err
+               }
+       case '[':
+               i := 0
+               for d.More() {
+                       if err := checkJSONDupedKeys(d, append(path, strconv.Itoa(i)), errors); err != nil {
+                               return err
+                       }
+                       i++
+               }
+               // consume closing ']'
+               if _, err := d.Token(); err != nil {
+                       return err
+               }
+       }
+       if len(path) == 0 && len(*errors) > 0 {
+               return fmt.Errorf("duplicate JSON key(s) found")
+       }
+       return nil
+}
+
+func (v *Vocabulary) validate() ([]string, error) {
+       if v == nil {
+               return nil, nil
+       }
+       tagKeys := map[string]string{}
        // Checks for Vocabulary strictness
        if v.StrictTags && len(v.Tags) == 0 {
-               return fmt.Errorf("vocabulary is strict but no tags are defined")
+               return nil, fmt.Errorf("vocabulary is strict but no tags are defined")
        }
-       // Checks for duplicate tag keys
+       // Checks for collisions between tag keys, reserved tag keys
+       // and tag key labels.
+       errors := []string{}
        for key := range v.Tags {
                if v.reservedTagKeys[key] {
-                       return fmt.Errorf("tag key %q is reserved", key)
+                       errors = append(errors, fmt.Sprintf("tag key %q is reserved", key))
                }
-               if tagKeys[key] {
-                       return fmt.Errorf("duplicate tag key %q", key)
+               lcKey := strings.ToLower(key)
+               if tagKeys[lcKey] != "" {
+                       errors = append(errors, fmt.Sprintf("duplicate tag key %q", key))
                }
-               tagKeys[key] = true
+               tagKeys[lcKey] = key
                for _, lbl := range v.Tags[key].Labels {
                        label := strings.ToLower(lbl.Label)
-                       if tagKeys[label] {
-                               return fmt.Errorf("tag label %q for key %q already seen as a tag key or label", label, key)
+                       if tagKeys[label] != "" {
+                               errors = append(errors, fmt.Sprintf("tag label %q for key %q already seen as a tag key or label", lbl.Label, key))
                        }
-                       tagKeys[label] = true
+                       tagKeys[label] = lbl.Label
                }
                // Checks for value strictness
                if v.Tags[key].Strict && len(v.Tags[key].Values) == 0 {
-                       return fmt.Errorf("tag key %q is configured as strict but doesn't provide values", key)
+                       errors = append(errors, fmt.Sprintf("tag key %q is configured as strict but doesn't provide values", key))
                }
-               // Checks for value duplication within a key
-               tagValues := map[string]bool{}
+               // Checks for collisions between tag values and tag value labels.
+               tagValues := map[string]string{}
                for val := range v.Tags[key].Values {
-                       if tagValues[val] {
-                               return fmt.Errorf("duplicate tag value %q for tag %q", val, key)
+                       lcVal := strings.ToLower(val)
+                       if tagValues[lcVal] != "" {
+                               errors = append(errors, fmt.Sprintf("duplicate tag value %q for tag %q", val, key))
                        }
-                       tagValues[val] = true
+                       // Checks for collisions between labels from different values.
+                       tagValues[lcVal] = val
                        for _, tagLbl := range v.Tags[key].Values[val].Labels {
                                label := strings.ToLower(tagLbl.Label)
-                               if tagValues[label] {
-                                       return fmt.Errorf("tag value label %q for pair (%q:%q) already seen as a value key or label", label, key, val)
+                               if tagValues[label] != "" && tagValues[label] != val {
+                                       errors = append(errors, fmt.Sprintf("tag value label %q for pair (%q:%q) already seen on value %q", tagLbl.Label, key, val, tagValues[label]))
                                }
-                               tagValues[label] = true
+                               tagValues[label] = val
                        }
                }
        }
-       return nil
+       if len(errors) > 0 {
+               return errors, fmt.Errorf("invalid vocabulary")
+       }
+       return nil, nil
 }
 
 func (v *Vocabulary) getLabelsToKeys() (labels map[string]string) {
@@ -140,6 +231,7 @@ func (v *Vocabulary) getLabelsToValues(key string) (labels map[string]string) {
        labels = make(map[string]string)
        if _, ok := v.Tags[key]; ok {
                for val := range v.Tags[key].Values {
+                       labels[strings.ToLower(val)] = val
                        for _, tagLbl := range v.Tags[key].Values[val].Labels {
                                label := strings.ToLower(tagLbl.Label)
                                labels[label] = val
@@ -152,11 +244,11 @@ func (v *Vocabulary) getLabelsToValues(key string) (labels map[string]string) {
 func (v *Vocabulary) checkValue(key, val string) error {
        if _, ok := v.Tags[key].Values[val]; !ok {
                lcVal := strings.ToLower(val)
-               alias, ok := v.getLabelsToValues(key)[lcVal]
+               correctValue, ok := v.getLabelsToValues(key)[lcVal]
                if ok {
-                       return fmt.Errorf("tag value %q for key %q is not defined but is an alias for %q", val, key, alias)
+                       return fmt.Errorf("tag value %q for key %q is an alias, must be provided as %q", val, key, correctValue)
                } else if v.Tags[key].Strict {
-                       return fmt.Errorf("tag value %q for key %q is not listed as valid", val, key)
+                       return fmt.Errorf("tag value %q is not valid for key %q", val, key)
                }
        }
        return nil
@@ -176,11 +268,11 @@ func (v *Vocabulary) Check(data map[string]interface{}) error {
                }
                if _, ok := v.Tags[key]; !ok {
                        lcKey := strings.ToLower(key)
-                       alias, ok := v.getLabelsToKeys()[lcKey]
+                       correctKey, ok := v.getLabelsToKeys()[lcKey]
                        if ok {
-                               return fmt.Errorf("tag key %q is not defined but is an alias for %q", key, alias)
+                               return fmt.Errorf("tag key %q is an alias, must be provided as %q", key, correctKey)
                        } else if v.StrictTags {
-                               return fmt.Errorf("tag key %q is not defined", key)
+                               return fmt.Errorf("tag key %q is not defined in the vocabulary", key)
                        }
                        // If the key is not defined, we don't need to check the value
                        continue
@@ -201,11 +293,11 @@ func (v *Vocabulary) Check(data map[string]interface{}) error {
                                                return err
                                        }
                                default:
-                                       return fmt.Errorf("tag value %q for key %q is not a valid type (%T)", singleVal, key, singleVal)
+                                       return fmt.Errorf("value list element type for tag key %q was %T, but expected a string", key, singleVal)
                                }
                        }
                default:
-                       return fmt.Errorf("tag value %q for key %q is not a valid type (%T)", val, key, val)
+                       return fmt.Errorf("value type for tag key %q was %T, but expected a string or list of strings", key, val)
                }
        }
        return nil