Merge branch '18487-vocab-config-check'. Closes #18487
[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                 "type":                  true,
33                 "template_uuid":         true,
34                 "groups":                true,
35                 "username":              true,
36                 "image_timestamp":       true,
37                 "docker-image-repo-tag": true,
38                 "filters":               true,
39                 "container_request":     true,
40         }
41 }
42
43 type VocabularyLabel struct {
44         Label string `json:"label"`
45 }
46
47 type VocabularyTagValue struct {
48         Labels []VocabularyLabel `json:"labels"`
49 }
50
51 // NewVocabulary creates a new Vocabulary from a JSON definition and a list
52 // of reserved tag keys that will get special treatment when strict mode is
53 // enabled.
54 func NewVocabulary(data []byte, managedTagKeys []string) (voc *Vocabulary, err error) {
55         if r := bytes.Compare(data, []byte("")); r == 0 {
56                 return &Vocabulary{}, nil
57         }
58         err = json.Unmarshal(data, &voc)
59         if err != nil {
60                 var serr *json.SyntaxError
61                 if errors.As(err, &serr) {
62                         offset := serr.Offset
63                         errorMsg := string(data[:offset])
64                         line := 1 + strings.Count(errorMsg, "\n")
65                         column := offset - int64(strings.LastIndex(errorMsg, "\n")+len("\n"))
66                         return nil, fmt.Errorf("invalid JSON format: %q (line %d, column %d)", err, line, column)
67                 }
68                 return nil, fmt.Errorf("invalid JSON format: %q", err)
69         }
70         if reflect.DeepEqual(voc, &Vocabulary{}) {
71                 return nil, fmt.Errorf("JSON data provided doesn't match Vocabulary format: %q", data)
72         }
73
74         shouldReportErrors := false
75         errors := []string{}
76
77         // json.Unmarshal() doesn't error out on duplicate keys.
78         dupedKeys := []string{}
79         err = checkJSONDupedKeys(json.NewDecoder(bytes.NewReader(data)), nil, &dupedKeys)
80         if err != nil {
81                 shouldReportErrors = true
82                 for _, dk := range dupedKeys {
83                         errors = append(errors, fmt.Sprintf("duplicate JSON key %q", dk))
84                 }
85         }
86         voc.reservedTagKeys = make(map[string]bool)
87         for _, managedKey := range managedTagKeys {
88                 voc.reservedTagKeys[managedKey] = true
89         }
90         for systemKey := range voc.systemTagKeys() {
91                 voc.reservedTagKeys[systemKey] = true
92         }
93         validationErrs, err := voc.validate()
94         if err != nil {
95                 shouldReportErrors = true
96                 errors = append(errors, validationErrs...)
97         }
98         if shouldReportErrors {
99                 return nil, fmt.Errorf("%s", strings.Join(errors, "\n"))
100         }
101         return voc, nil
102 }
103
104 func checkJSONDupedKeys(d *json.Decoder, path []string, errors *[]string) error {
105         t, err := d.Token()
106         if err != nil {
107                 return err
108         }
109         delim, ok := t.(json.Delim)
110         if !ok {
111                 return nil
112         }
113         switch delim {
114         case '{':
115                 keys := make(map[string]bool)
116                 for d.More() {
117                         t, err := d.Token()
118                         if err != nil {
119                                 return err
120                         }
121                         key := t.(string)
122
123                         if keys[key] {
124                                 *errors = append(*errors, strings.Join(append(path, key), "."))
125                         }
126                         keys[key] = true
127
128                         if err := checkJSONDupedKeys(d, append(path, key), errors); err != nil {
129                                 return err
130                         }
131                 }
132                 // consume closing '}'
133                 if _, err := d.Token(); err != nil {
134                         return err
135                 }
136         case '[':
137                 i := 0
138                 for d.More() {
139                         if err := checkJSONDupedKeys(d, append(path, strconv.Itoa(i)), errors); err != nil {
140                                 return err
141                         }
142                         i++
143                 }
144                 // consume closing ']'
145                 if _, err := d.Token(); err != nil {
146                         return err
147                 }
148         }
149         if len(path) == 0 && len(*errors) > 0 {
150                 return fmt.Errorf("duplicate JSON key(s) found")
151         }
152         return nil
153 }
154
155 func (v *Vocabulary) validate() ([]string, error) {
156         if v == nil {
157                 return nil, nil
158         }
159         tagKeys := map[string]string{}
160         // Checks for Vocabulary strictness
161         if v.StrictTags && len(v.Tags) == 0 {
162                 return nil, fmt.Errorf("vocabulary is strict but no tags are defined")
163         }
164         // Checks for collisions between tag keys, reserved tag keys
165         // and tag key labels.
166         errors := []string{}
167         for key := range v.Tags {
168                 if v.reservedTagKeys[key] {
169                         errors = append(errors, fmt.Sprintf("tag key %q is reserved", key))
170                 }
171                 lcKey := strings.ToLower(key)
172                 if tagKeys[lcKey] != "" {
173                         errors = append(errors, fmt.Sprintf("duplicate tag key %q", key))
174                 }
175                 tagKeys[lcKey] = key
176                 for _, lbl := range v.Tags[key].Labels {
177                         label := strings.ToLower(lbl.Label)
178                         if tagKeys[label] != "" {
179                                 errors = append(errors, fmt.Sprintf("tag label %q for key %q already seen as a tag key or label", lbl.Label, key))
180                         }
181                         tagKeys[label] = lbl.Label
182                 }
183                 // Checks for value strictness
184                 if v.Tags[key].Strict && len(v.Tags[key].Values) == 0 {
185                         errors = append(errors, fmt.Sprintf("tag key %q is configured as strict but doesn't provide values", key))
186                 }
187                 // Checks for collisions between tag values and tag value labels.
188                 tagValues := map[string]string{}
189                 for val := range v.Tags[key].Values {
190                         lcVal := strings.ToLower(val)
191                         if tagValues[lcVal] != "" {
192                                 errors = append(errors, fmt.Sprintf("duplicate tag value %q for tag %q", val, key))
193                         }
194                         // Checks for collisions between labels from different values.
195                         tagValues[lcVal] = val
196                         for _, tagLbl := range v.Tags[key].Values[val].Labels {
197                                 label := strings.ToLower(tagLbl.Label)
198                                 if tagValues[label] != "" && tagValues[label] != val {
199                                         errors = append(errors, fmt.Sprintf("tag value label %q for pair (%q:%q) already seen on value %q", tagLbl.Label, key, val, tagValues[label]))
200                                 }
201                                 tagValues[label] = val
202                         }
203                 }
204         }
205         if len(errors) > 0 {
206                 return errors, fmt.Errorf("invalid vocabulary")
207         }
208         return nil, nil
209 }
210
211 func (v *Vocabulary) getLabelsToKeys() (labels map[string]string) {
212         if v == nil {
213                 return
214         }
215         labels = make(map[string]string)
216         for key, val := range v.Tags {
217                 for _, lbl := range val.Labels {
218                         label := strings.ToLower(lbl.Label)
219                         labels[label] = key
220                 }
221         }
222         return labels
223 }
224
225 func (v *Vocabulary) getLabelsToValues(key string) (labels map[string]string) {
226         if v == nil {
227                 return
228         }
229         labels = make(map[string]string)
230         if _, ok := v.Tags[key]; ok {
231                 for val := range v.Tags[key].Values {
232                         labels[strings.ToLower(val)] = val
233                         for _, tagLbl := range v.Tags[key].Values[val].Labels {
234                                 label := strings.ToLower(tagLbl.Label)
235                                 labels[label] = val
236                         }
237                 }
238         }
239         return labels
240 }
241
242 func (v *Vocabulary) checkValue(key, val string) error {
243         if _, ok := v.Tags[key].Values[val]; !ok {
244                 lcVal := strings.ToLower(val)
245                 correctValue, ok := v.getLabelsToValues(key)[lcVal]
246                 if ok {
247                         return fmt.Errorf("tag value %q for key %q is an alias, must be provided as %q", val, key, correctValue)
248                 } else if v.Tags[key].Strict {
249                         return fmt.Errorf("tag value %q is not valid for key %q", val, key)
250                 }
251         }
252         return nil
253 }
254
255 // Check validates the given data against the vocabulary.
256 func (v *Vocabulary) Check(data map[string]interface{}) error {
257         if v == nil {
258                 return nil
259         }
260         for key, val := range data {
261                 // Checks for key validity
262                 if v.reservedTagKeys[key] {
263                         // Allow reserved keys to be used even if they are not defined in
264                         // the vocabulary no matter its strictness.
265                         continue
266                 }
267                 if _, ok := v.Tags[key]; !ok {
268                         lcKey := strings.ToLower(key)
269                         correctKey, ok := v.getLabelsToKeys()[lcKey]
270                         if ok {
271                                 return fmt.Errorf("tag key %q is an alias, must be provided as %q", key, correctKey)
272                         } else if v.StrictTags {
273                                 return fmt.Errorf("tag key %q is not defined in the vocabulary", key)
274                         }
275                         // If the key is not defined, we don't need to check the value
276                         continue
277                 }
278                 // Checks for value validity -- key is defined
279                 switch val := val.(type) {
280                 case string:
281                         err := v.checkValue(key, val)
282                         if err != nil {
283                                 return err
284                         }
285                 case []interface{}:
286                         for _, singleVal := range val {
287                                 switch singleVal := singleVal.(type) {
288                                 case string:
289                                         err := v.checkValue(key, singleVal)
290                                         if err != nil {
291                                                 return err
292                                         }
293                                 default:
294                                         return fmt.Errorf("value list element type for tag key %q was %T, but expected a string", key, singleVal)
295                                 }
296                         }
297                 default:
298                         return fmt.Errorf("value type for tag key %q was %T, but expected a string or list of strings", key, val)
299                 }
300         }
301         return nil
302 }