Merge branch '21448-menu-reorder' into 21224-project-details
[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 // 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,
38                 "type":              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
42                 "cwl_input":     true,
43                 "cwl_output":    true,
44                 "template_uuid": true,
45                 // Group keys
46                 "filters": true,
47                 // Link keys
48                 "groups":          true,
49                 "image_timestamp": true,
50                 "username":        true,
51         }
52 }
53
54 type VocabularyLabel struct {
55         Label string `json:"label"`
56 }
57
58 type VocabularyTagValue struct {
59         Labels []VocabularyLabel `json:"labels"`
60 }
61
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
64 // enabled.
65 func NewVocabulary(data []byte, managedTagKeys []string) (voc *Vocabulary, err error) {
66         if r := bytes.Compare(data, []byte("")); r == 0 {
67                 return &Vocabulary{}, nil
68         }
69         err = json.Unmarshal(data, &voc)
70         if err != nil {
71                 var serr *json.SyntaxError
72                 if errors.As(err, &serr) {
73                         offset := serr.Offset
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)
78                 }
79                 return nil, fmt.Errorf("invalid JSON format: %q", err)
80         }
81         if reflect.DeepEqual(voc, &Vocabulary{}) {
82                 return nil, fmt.Errorf("JSON data provided doesn't match Vocabulary format: %q", data)
83         }
84
85         shouldReportErrors := false
86         errors := []string{}
87
88         // json.Unmarshal() doesn't error out on duplicate keys.
89         dupedKeys := []string{}
90         err = checkJSONDupedKeys(json.NewDecoder(bytes.NewReader(data)), nil, &dupedKeys)
91         if err != nil {
92                 shouldReportErrors = true
93                 for _, dk := range dupedKeys {
94                         errors = append(errors, fmt.Sprintf("duplicate JSON key %q", dk))
95                 }
96         }
97         voc.reservedTagKeys = make(map[string]bool)
98         for _, managedKey := range managedTagKeys {
99                 voc.reservedTagKeys[managedKey] = true
100         }
101         for systemKey := range voc.systemTagKeys() {
102                 voc.reservedTagKeys[systemKey] = true
103         }
104         validationErrs, err := voc.validate()
105         if err != nil {
106                 shouldReportErrors = true
107                 errors = append(errors, validationErrs...)
108         }
109         if shouldReportErrors {
110                 return nil, fmt.Errorf("%s", strings.Join(errors, "\n"))
111         }
112         return voc, nil
113 }
114
115 func checkJSONDupedKeys(d *json.Decoder, path []string, errors *[]string) error {
116         t, err := d.Token()
117         if err != nil {
118                 return err
119         }
120         delim, ok := t.(json.Delim)
121         if !ok {
122                 return nil
123         }
124         switch delim {
125         case '{':
126                 keys := make(map[string]bool)
127                 for d.More() {
128                         t, err := d.Token()
129                         if err != nil {
130                                 return err
131                         }
132                         key := t.(string)
133
134                         if keys[key] {
135                                 *errors = append(*errors, strings.Join(append(path, key), "."))
136                         }
137                         keys[key] = true
138
139                         if err := checkJSONDupedKeys(d, append(path, key), errors); err != nil {
140                                 return err
141                         }
142                 }
143                 // consume closing '}'
144                 if _, err := d.Token(); err != nil {
145                         return err
146                 }
147         case '[':
148                 i := 0
149                 for d.More() {
150                         if err := checkJSONDupedKeys(d, append(path, strconv.Itoa(i)), errors); err != nil {
151                                 return err
152                         }
153                         i++
154                 }
155                 // consume closing ']'
156                 if _, err := d.Token(); err != nil {
157                         return err
158                 }
159         }
160         if len(path) == 0 && len(*errors) > 0 {
161                 return fmt.Errorf("duplicate JSON key(s) found")
162         }
163         return nil
164 }
165
166 func (v *Vocabulary) validate() ([]string, error) {
167         if v == nil {
168                 return nil, nil
169         }
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")
174         }
175         // Checks for collisions between tag keys, reserved tag keys
176         // and tag key labels.
177         errors := []string{}
178         for key := range v.Tags {
179                 if v.reservedTagKeys[key] {
180                         errors = append(errors, fmt.Sprintf("tag key %q is reserved", key))
181                 }
182                 lcKey := strings.ToLower(key)
183                 if tagKeys[lcKey] != "" {
184                         errors = append(errors, fmt.Sprintf("duplicate tag key %q", key))
185                 }
186                 tagKeys[lcKey] = 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))
191                         }
192                         tagKeys[label] = lbl.Label
193                 }
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))
197                 }
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))
204                         }
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]))
211                                 }
212                                 tagValues[label] = val
213                         }
214                 }
215         }
216         if len(errors) > 0 {
217                 return errors, fmt.Errorf("invalid vocabulary")
218         }
219         return nil, nil
220 }
221
222 func (v *Vocabulary) getLabelsToKeys() (labels map[string]string) {
223         if v == nil {
224                 return
225         }
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)
230                         labels[label] = key
231                 }
232         }
233         return labels
234 }
235
236 func (v *Vocabulary) getLabelsToValues(key string) (labels map[string]string) {
237         if v == nil {
238                 return
239         }
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)
246                                 labels[label] = val
247                         }
248                 }
249         }
250         return labels
251 }
252
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]
257                 if ok {
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)
261                 }
262         }
263         return nil
264 }
265
266 // Check validates the given data against the vocabulary.
267 func (v *Vocabulary) Check(data map[string]interface{}) error {
268         if v == nil {
269                 return nil
270         }
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.
276                         continue
277                 }
278                 if _, ok := v.Tags[key]; !ok {
279                         lcKey := strings.ToLower(key)
280                         correctKey, ok := v.getLabelsToKeys()[lcKey]
281                         if ok {
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)
285                         }
286                         // If the key is not defined, we don't need to check the value
287                         continue
288                 }
289                 // Checks for value validity -- key is defined
290                 switch val := val.(type) {
291                 case string:
292                         err := v.checkValue(key, val)
293                         if err != nil {
294                                 return err
295                         }
296                 case []interface{}:
297                         for _, singleVal := range val {
298                                 switch singleVal := singleVal.(type) {
299                                 case string:
300                                         err := v.checkValue(key, singleVal)
301                                         if err != nil {
302                                                 return err
303                                         }
304                                 default:
305                                         return fmt.Errorf("value list element type for tag key %q was %T, but expected a string", key, singleVal)
306                                 }
307                         }
308                 default:
309                         return fmt.Errorf("value type for tag key %q was %T, but expected a string or list of strings", key, val)
310                 }
311         }
312         return nil
313 }