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