20032: Fix unnecessary race in test.
[arvados.git] / sdk / go / arvados / vocabulary.go
1 // Copyright (C) The Arvados Authors. All rights reserved.
2 //
3 // SPDX-License-Identifier: Apache-2.0
4
5 package arvados
6
7 import (
8         "bytes"
9         "encoding/json"
10         "errors"
11         "fmt"
12         "reflect"
13         "strconv"
14         "strings"
15 )
16
17 type Vocabulary struct {
18         reservedTagKeys map[string]bool          `json:"-"`
19         StrictTags      bool                     `json:"strict_tags"`
20         Tags            map[string]VocabularyTag `json:"tags"`
21 }
22
23 type VocabularyTag struct {
24         Strict bool                          `json:"strict"`
25         Labels []VocabularyLabel             `json:"labels"`
26         Values map[string]VocabularyTagValue `json:"values"`
27 }
28
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,
35                 "type":              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
39                 "cwl_input":     true,
40                 "cwl_output":    true,
41                 "template_uuid": true,
42                 // Group keys
43                 "filters": true,
44                 // Link keys
45                 "groups":          true,
46                 "image_timestamp": true,
47                 "username":        true,
48         }
49 }
50
51 type VocabularyLabel struct {
52         Label string `json:"label"`
53 }
54
55 type VocabularyTagValue struct {
56         Labels []VocabularyLabel `json:"labels"`
57 }
58
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
61 // enabled.
62 func NewVocabulary(data []byte, managedTagKeys []string) (voc *Vocabulary, err error) {
63         if r := bytes.Compare(data, []byte("")); r == 0 {
64                 return &Vocabulary{}, nil
65         }
66         err = json.Unmarshal(data, &voc)
67         if err != nil {
68                 var serr *json.SyntaxError
69                 if errors.As(err, &serr) {
70                         offset := serr.Offset
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)
75                 }
76                 return nil, fmt.Errorf("invalid JSON format: %q", err)
77         }
78         if reflect.DeepEqual(voc, &Vocabulary{}) {
79                 return nil, fmt.Errorf("JSON data provided doesn't match Vocabulary format: %q", data)
80         }
81
82         shouldReportErrors := false
83         errors := []string{}
84
85         // json.Unmarshal() doesn't error out on duplicate keys.
86         dupedKeys := []string{}
87         err = checkJSONDupedKeys(json.NewDecoder(bytes.NewReader(data)), nil, &dupedKeys)
88         if err != nil {
89                 shouldReportErrors = true
90                 for _, dk := range dupedKeys {
91                         errors = append(errors, fmt.Sprintf("duplicate JSON key %q", dk))
92                 }
93         }
94         voc.reservedTagKeys = make(map[string]bool)
95         for _, managedKey := range managedTagKeys {
96                 voc.reservedTagKeys[managedKey] = true
97         }
98         for systemKey := range voc.systemTagKeys() {
99                 voc.reservedTagKeys[systemKey] = true
100         }
101         validationErrs, err := voc.validate()
102         if err != nil {
103                 shouldReportErrors = true
104                 errors = append(errors, validationErrs...)
105         }
106         if shouldReportErrors {
107                 return nil, fmt.Errorf("%s", strings.Join(errors, "\n"))
108         }
109         return voc, nil
110 }
111
112 func checkJSONDupedKeys(d *json.Decoder, path []string, errors *[]string) error {
113         t, err := d.Token()
114         if err != nil {
115                 return err
116         }
117         delim, ok := t.(json.Delim)
118         if !ok {
119                 return nil
120         }
121         switch delim {
122         case '{':
123                 keys := make(map[string]bool)
124                 for d.More() {
125                         t, err := d.Token()
126                         if err != nil {
127                                 return err
128                         }
129                         key := t.(string)
130
131                         if keys[key] {
132                                 *errors = append(*errors, strings.Join(append(path, key), "."))
133                         }
134                         keys[key] = true
135
136                         if err := checkJSONDupedKeys(d, append(path, key), errors); err != nil {
137                                 return err
138                         }
139                 }
140                 // consume closing '}'
141                 if _, err := d.Token(); err != nil {
142                         return err
143                 }
144         case '[':
145                 i := 0
146                 for d.More() {
147                         if err := checkJSONDupedKeys(d, append(path, strconv.Itoa(i)), errors); err != nil {
148                                 return err
149                         }
150                         i++
151                 }
152                 // consume closing ']'
153                 if _, err := d.Token(); err != nil {
154                         return err
155                 }
156         }
157         if len(path) == 0 && len(*errors) > 0 {
158                 return fmt.Errorf("duplicate JSON key(s) found")
159         }
160         return nil
161 }
162
163 func (v *Vocabulary) validate() ([]string, error) {
164         if v == nil {
165                 return nil, nil
166         }
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")
171         }
172         // Checks for collisions between tag keys, reserved tag keys
173         // and tag key labels.
174         errors := []string{}
175         for key := range v.Tags {
176                 if v.reservedTagKeys[key] {
177                         errors = append(errors, fmt.Sprintf("tag key %q is reserved", key))
178                 }
179                 lcKey := strings.ToLower(key)
180                 if tagKeys[lcKey] != "" {
181                         errors = append(errors, fmt.Sprintf("duplicate tag key %q", key))
182                 }
183                 tagKeys[lcKey] = 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))
188                         }
189                         tagKeys[label] = lbl.Label
190                 }
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))
194                 }
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))
201                         }
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]))
208                                 }
209                                 tagValues[label] = val
210                         }
211                 }
212         }
213         if len(errors) > 0 {
214                 return errors, fmt.Errorf("invalid vocabulary")
215         }
216         return nil, nil
217 }
218
219 func (v *Vocabulary) getLabelsToKeys() (labels map[string]string) {
220         if v == nil {
221                 return
222         }
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)
227                         labels[label] = key
228                 }
229         }
230         return labels
231 }
232
233 func (v *Vocabulary) getLabelsToValues(key string) (labels map[string]string) {
234         if v == nil {
235                 return
236         }
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)
243                                 labels[label] = val
244                         }
245                 }
246         }
247         return labels
248 }
249
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]
254                 if ok {
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)
258                 }
259         }
260         return nil
261 }
262
263 // Check validates the given data against the vocabulary.
264 func (v *Vocabulary) Check(data map[string]interface{}) error {
265         if v == nil {
266                 return nil
267         }
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.
273                         continue
274                 }
275                 if _, ok := v.Tags[key]; !ok {
276                         lcKey := strings.ToLower(key)
277                         correctKey, ok := v.getLabelsToKeys()[lcKey]
278                         if ok {
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)
282                         }
283                         // If the key is not defined, we don't need to check the value
284                         continue
285                 }
286                 // Checks for value validity -- key is defined
287                 switch val := val.(type) {
288                 case string:
289                         err := v.checkValue(key, val)
290                         if err != nil {
291                                 return err
292                         }
293                 case []interface{}:
294                         for _, singleVal := range val {
295                                 switch singleVal := singleVal.(type) {
296                                 case string:
297                                         err := v.checkValue(key, singleVal)
298                                         if err != nil {
299                                                 return err
300                                         }
301                                 default:
302                                         return fmt.Errorf("value list element type for tag key %q was %T, but expected a string", key, singleVal)
303                                 }
304                         }
305                 default:
306                         return fmt.Errorf("value type for tag key %q was %T, but expected a string or list of strings", key, val)
307                 }
308         }
309         return nil
310 }