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 func (v *Vocabulary) systemTagKeys() map[string]bool {
31 return map[string]bool{
32 // Collection keys - set by arvados-cwl-runner
33 "container_request": true,
34 "container_uuid": true,
36 // Collection keys - set by arv-keepdocker (on the way out)
37 "docker-image-repo-tag": true,
38 // Container request keys - set by arvados-cwl-runner
41 "template_uuid": true,
46 "image_timestamp": true,
51 type VocabularyLabel struct {
52 Label string `json:"label"`
55 type VocabularyTagValue struct {
56 Labels []VocabularyLabel `json:"labels"`
59 // NewVocabulary creates a new Vocabulary from a JSON definition and a list
60 // of reserved tag keys that will get special treatment when strict mode is
62 func NewVocabulary(data []byte, managedTagKeys []string) (voc *Vocabulary, err error) {
63 if r := bytes.Compare(data, []byte("")); r == 0 {
64 return &Vocabulary{}, nil
66 err = json.Unmarshal(data, &voc)
68 var serr *json.SyntaxError
69 if errors.As(err, &serr) {
71 errorMsg := string(data[:offset])
72 line := 1 + strings.Count(errorMsg, "\n")
73 column := offset - int64(strings.LastIndex(errorMsg, "\n")+len("\n"))
74 return nil, fmt.Errorf("invalid JSON format: %q (line %d, column %d)", err, line, column)
76 return nil, fmt.Errorf("invalid JSON format: %q", err)
78 if reflect.DeepEqual(voc, &Vocabulary{}) {
79 return nil, fmt.Errorf("JSON data provided doesn't match Vocabulary format: %q", data)
82 shouldReportErrors := false
85 // json.Unmarshal() doesn't error out on duplicate keys.
86 dupedKeys := []string{}
87 err = checkJSONDupedKeys(json.NewDecoder(bytes.NewReader(data)), nil, &dupedKeys)
89 shouldReportErrors = true
90 for _, dk := range dupedKeys {
91 errors = append(errors, fmt.Sprintf("duplicate JSON key %q", dk))
94 voc.reservedTagKeys = make(map[string]bool)
95 for _, managedKey := range managedTagKeys {
96 voc.reservedTagKeys[managedKey] = true
98 for systemKey := range voc.systemTagKeys() {
99 voc.reservedTagKeys[systemKey] = true
101 validationErrs, err := voc.validate()
103 shouldReportErrors = true
104 errors = append(errors, validationErrs...)
106 if shouldReportErrors {
107 return nil, fmt.Errorf("%s", strings.Join(errors, "\n"))
112 func checkJSONDupedKeys(d *json.Decoder, path []string, errors *[]string) error {
117 delim, ok := t.(json.Delim)
123 keys := make(map[string]bool)
132 *errors = append(*errors, strings.Join(append(path, key), "."))
136 if err := checkJSONDupedKeys(d, append(path, key), errors); err != nil {
140 // consume closing '}'
141 if _, err := d.Token(); err != nil {
147 if err := checkJSONDupedKeys(d, append(path, strconv.Itoa(i)), errors); err != nil {
152 // consume closing ']'
153 if _, err := d.Token(); err != nil {
157 if len(path) == 0 && len(*errors) > 0 {
158 return fmt.Errorf("duplicate JSON key(s) found")
163 func (v *Vocabulary) validate() ([]string, error) {
167 tagKeys := map[string]string{}
168 // Checks for Vocabulary strictness
169 if v.StrictTags && len(v.Tags) == 0 {
170 return nil, fmt.Errorf("vocabulary is strict but no tags are defined")
172 // Checks for collisions between tag keys, reserved tag keys
173 // and tag key labels.
175 for key := range v.Tags {
176 if v.reservedTagKeys[key] {
177 errors = append(errors, fmt.Sprintf("tag key %q is reserved", key))
179 lcKey := strings.ToLower(key)
180 if tagKeys[lcKey] != "" {
181 errors = append(errors, fmt.Sprintf("duplicate tag key %q", key))
184 for _, lbl := range v.Tags[key].Labels {
185 label := strings.ToLower(lbl.Label)
186 if tagKeys[label] != "" {
187 errors = append(errors, fmt.Sprintf("tag label %q for key %q already seen as a tag key or label", lbl.Label, key))
189 tagKeys[label] = lbl.Label
191 // Checks for value strictness
192 if v.Tags[key].Strict && len(v.Tags[key].Values) == 0 {
193 errors = append(errors, fmt.Sprintf("tag key %q is configured as strict but doesn't provide values", key))
195 // Checks for collisions between tag values and tag value labels.
196 tagValues := map[string]string{}
197 for val := range v.Tags[key].Values {
198 lcVal := strings.ToLower(val)
199 if tagValues[lcVal] != "" {
200 errors = append(errors, fmt.Sprintf("duplicate tag value %q for tag %q", val, key))
202 // Checks for collisions between labels from different values.
203 tagValues[lcVal] = val
204 for _, tagLbl := range v.Tags[key].Values[val].Labels {
205 label := strings.ToLower(tagLbl.Label)
206 if tagValues[label] != "" && tagValues[label] != val {
207 errors = append(errors, fmt.Sprintf("tag value label %q for pair (%q:%q) already seen on value %q", tagLbl.Label, key, val, tagValues[label]))
209 tagValues[label] = val
214 return errors, fmt.Errorf("invalid vocabulary")
219 func (v *Vocabulary) getLabelsToKeys() (labels map[string]string) {
223 labels = make(map[string]string)
224 for key, val := range v.Tags {
225 for _, lbl := range val.Labels {
226 label := strings.ToLower(lbl.Label)
233 func (v *Vocabulary) getLabelsToValues(key string) (labels map[string]string) {
237 labels = make(map[string]string)
238 if _, ok := v.Tags[key]; ok {
239 for val := range v.Tags[key].Values {
240 labels[strings.ToLower(val)] = val
241 for _, tagLbl := range v.Tags[key].Values[val].Labels {
242 label := strings.ToLower(tagLbl.Label)
250 func (v *Vocabulary) checkValue(key, val string) error {
251 if _, ok := v.Tags[key].Values[val]; !ok {
252 lcVal := strings.ToLower(val)
253 correctValue, ok := v.getLabelsToValues(key)[lcVal]
255 return fmt.Errorf("tag value %q for key %q is an alias, must be provided as %q", val, key, correctValue)
256 } else if v.Tags[key].Strict {
257 return fmt.Errorf("tag value %q is not valid for key %q", val, key)
263 // Check validates the given data against the vocabulary.
264 func (v *Vocabulary) Check(data map[string]interface{}) error {
268 for key, val := range data {
269 // Checks for key validity
270 if v.reservedTagKeys[key] {
271 // Allow reserved keys to be used even if they are not defined in
272 // the vocabulary no matter its strictness.
275 if _, ok := v.Tags[key]; !ok {
276 lcKey := strings.ToLower(key)
277 correctKey, ok := v.getLabelsToKeys()[lcKey]
279 return fmt.Errorf("tag key %q is an alias, must be provided as %q", key, correctKey)
280 } else if v.StrictTags {
281 return fmt.Errorf("tag key %q is not defined in the vocabulary", key)
283 // If the key is not defined, we don't need to check the value
286 // Checks for value validity -- key is defined
287 switch val := val.(type) {
289 err := v.checkValue(key, val)
294 for _, singleVal := range val {
295 switch singleVal := singleVal.(type) {
297 err := v.checkValue(key, singleVal)
302 return fmt.Errorf("value list element type for tag key %q was %T, but expected a string", key, singleVal)
306 return fmt.Errorf("value type for tag key %q was %T, but expected a string or list of strings", key, val)