1 // Copyright (C) The Arvados Authors. All rights reserved.
3 // SPDX-License-Identifier: Apache-2.0
17 type Vocabulary struct {
18 reservedTagKeys map[string]bool `json:"-"`
19 StrictTags bool `json:"strict_tags"`
20 Tags map[string]VocabularyTag `json:"tags"`
23 type VocabularyTag struct {
24 Strict bool `json:"strict"`
25 Labels []VocabularyLabel `json:"labels"`
26 Values map[string]VocabularyTagValue `json:"values"`
29 // Cannot have a constant map in Go, so we have to use a function.
30 // If you are adding a new system property, it SHOULD start with `arv:`,
31 // and Check will allow it. This map is for historical exceptions that
32 // predate standardizing on this prefix.
33 func (v *Vocabulary) systemTagKeys() map[string]bool {
34 return map[string]bool{
35 // Collection keys - set by arvados-cwl-runner
36 "container_request": true,
37 "container_uuid": true,
39 // Collection keys - set by arv-keepdocker (on the way out)
40 "docker-image-repo-tag": true,
41 // Container request keys - set by arvados-cwl-runner
44 "template_uuid": true,
49 "image_timestamp": true,
54 type VocabularyLabel struct {
55 Label string `json:"label"`
58 type VocabularyTagValue struct {
59 Labels []VocabularyLabel `json:"labels"`
62 // NewVocabulary creates a new Vocabulary from a JSON definition and a list
63 // of reserved tag keys that will get special treatment when strict mode is
65 func NewVocabulary(data []byte, managedTagKeys []string) (voc *Vocabulary, err error) {
66 if r := bytes.Compare(data, []byte("")); r == 0 {
67 return &Vocabulary{}, nil
69 err = json.Unmarshal(data, &voc)
71 var serr *json.SyntaxError
72 if errors.As(err, &serr) {
74 errorMsg := string(data[:offset])
75 line := 1 + strings.Count(errorMsg, "\n")
76 column := offset - int64(strings.LastIndex(errorMsg, "\n")+len("\n"))
77 return nil, fmt.Errorf("invalid JSON format: %q (line %d, column %d)", err, line, column)
79 return nil, fmt.Errorf("invalid JSON format: %q", err)
81 if reflect.DeepEqual(voc, &Vocabulary{}) {
82 return nil, fmt.Errorf("JSON data provided doesn't match Vocabulary format: %q", data)
85 shouldReportErrors := false
88 // json.Unmarshal() doesn't error out on duplicate keys.
89 dupedKeys := []string{}
90 err = checkJSONDupedKeys(json.NewDecoder(bytes.NewReader(data)), nil, &dupedKeys)
92 shouldReportErrors = true
93 for _, dk := range dupedKeys {
94 errors = append(errors, fmt.Sprintf("duplicate JSON key %q", dk))
97 voc.reservedTagKeys = make(map[string]bool)
98 for _, managedKey := range managedTagKeys {
99 voc.reservedTagKeys[managedKey] = true
101 for systemKey := range voc.systemTagKeys() {
102 voc.reservedTagKeys[systemKey] = true
104 validationErrs, err := voc.validate()
106 shouldReportErrors = true
107 errors = append(errors, validationErrs...)
109 if shouldReportErrors {
110 return nil, fmt.Errorf("%s", strings.Join(errors, "\n"))
115 func checkJSONDupedKeys(d *json.Decoder, path []string, errors *[]string) error {
120 delim, ok := t.(json.Delim)
126 keys := make(map[string]bool)
135 *errors = append(*errors, strings.Join(append(path, key), "."))
139 if err := checkJSONDupedKeys(d, append(path, key), errors); err != nil {
143 // consume closing '}'
144 if _, err := d.Token(); err != nil {
150 if err := checkJSONDupedKeys(d, append(path, strconv.Itoa(i)), errors); err != nil {
155 // consume closing ']'
156 if _, err := d.Token(); err != nil {
160 if len(path) == 0 && len(*errors) > 0 {
161 return fmt.Errorf("duplicate JSON key(s) found")
166 func (v *Vocabulary) validate() ([]string, error) {
170 tagKeys := map[string]string{}
171 // Checks for Vocabulary strictness
172 if v.StrictTags && len(v.Tags) == 0 {
173 return nil, fmt.Errorf("vocabulary is strict but no tags are defined")
175 // Checks for collisions between tag keys, reserved tag keys
176 // and tag key labels.
178 for key := range v.Tags {
179 if v.reservedTagKeys[key] {
180 errors = append(errors, fmt.Sprintf("tag key %q is reserved", key))
182 lcKey := strings.ToLower(key)
183 if tagKeys[lcKey] != "" {
184 errors = append(errors, fmt.Sprintf("duplicate tag key %q", key))
187 for _, lbl := range v.Tags[key].Labels {
188 label := strings.ToLower(lbl.Label)
189 if tagKeys[label] != "" {
190 errors = append(errors, fmt.Sprintf("tag label %q for key %q already seen as a tag key or label", lbl.Label, key))
192 tagKeys[label] = lbl.Label
194 // Checks for value strictness
195 if v.Tags[key].Strict && len(v.Tags[key].Values) == 0 {
196 errors = append(errors, fmt.Sprintf("tag key %q is configured as strict but doesn't provide values", key))
198 // Checks for collisions between tag values and tag value labels.
199 tagValues := map[string]string{}
200 for val := range v.Tags[key].Values {
201 lcVal := strings.ToLower(val)
202 if tagValues[lcVal] != "" {
203 errors = append(errors, fmt.Sprintf("duplicate tag value %q for tag %q", val, key))
205 // Checks for collisions between labels from different values.
206 tagValues[lcVal] = val
207 for _, tagLbl := range v.Tags[key].Values[val].Labels {
208 label := strings.ToLower(tagLbl.Label)
209 if tagValues[label] != "" && tagValues[label] != val {
210 errors = append(errors, fmt.Sprintf("tag value label %q for pair (%q:%q) already seen on value %q", tagLbl.Label, key, val, tagValues[label]))
212 tagValues[label] = val
217 return errors, fmt.Errorf("invalid vocabulary")
222 func (v *Vocabulary) getLabelsToKeys() (labels map[string]string) {
226 labels = make(map[string]string)
227 for key, val := range v.Tags {
228 for _, lbl := range val.Labels {
229 label := strings.ToLower(lbl.Label)
236 func (v *Vocabulary) getLabelsToValues(key string) (labels map[string]string) {
240 labels = make(map[string]string)
241 if _, ok := v.Tags[key]; ok {
242 for val := range v.Tags[key].Values {
243 labels[strings.ToLower(val)] = val
244 for _, tagLbl := range v.Tags[key].Values[val].Labels {
245 label := strings.ToLower(tagLbl.Label)
253 func (v *Vocabulary) checkValue(key, val string) error {
254 if _, ok := v.Tags[key].Values[val]; !ok {
255 lcVal := strings.ToLower(val)
256 correctValue, ok := v.getLabelsToValues(key)[lcVal]
258 return fmt.Errorf("tag value %q for key %q is an alias, must be provided as %q", val, key, correctValue)
259 } else if v.Tags[key].Strict {
260 return fmt.Errorf("tag value %q is not valid for key %q", val, key)
266 // Check validates the given data against the vocabulary.
267 func (v *Vocabulary) Check(data map[string]interface{}) error {
271 for key, val := range data {
272 // Checks for key validity
273 if strings.HasPrefix(key, "arv:") || v.reservedTagKeys[key] {
274 // Allow reserved keys to be used even if they are not defined in
275 // the vocabulary no matter its strictness.
278 if _, ok := v.Tags[key]; !ok {
279 lcKey := strings.ToLower(key)
280 correctKey, ok := v.getLabelsToKeys()[lcKey]
282 return fmt.Errorf("tag key %q is an alias, must be provided as %q", key, correctKey)
283 } else if v.StrictTags {
284 return fmt.Errorf("tag key %q is not defined in the vocabulary", key)
286 // If the key is not defined, we don't need to check the value
289 // Checks for value validity -- key is defined
290 switch val := val.(type) {
292 err := v.checkValue(key, val)
297 for _, singleVal := range val {
298 switch singleVal := singleVal.(type) {
300 err := v.checkValue(key, singleVal)
305 return fmt.Errorf("value list element type for tag key %q was %T, but expected a string", key, singleVal)
309 return fmt.Errorf("value type for tag key %q was %T, but expected a string or list of strings", key, val)