17840: Merge branch 'main'
[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         "fmt"
11         "reflect"
12         "strings"
13 )
14
15 type Vocabulary struct {
16         reservedTagKeys map[string]bool          `json:"-"`
17         StrictTags      bool                     `json:"strict_tags"`
18         Tags            map[string]VocabularyTag `json:"tags"`
19 }
20
21 type VocabularyTag struct {
22         Strict bool                          `json:"strict"`
23         Labels []VocabularyLabel             `json:"labels"`
24         Values map[string]VocabularyTagValue `json:"values"`
25 }
26
27 // Cannot have a constant map in Go, so we have to use a function
28 func (v *Vocabulary) systemTagKeys() map[string]bool {
29         return map[string]bool{
30                 "type":                  true,
31                 "template_uuid":         true,
32                 "groups":                true,
33                 "username":              true,
34                 "image_timestamp":       true,
35                 "docker-image-repo-tag": true,
36                 "filters":               true,
37                 "container_request":     true,
38         }
39 }
40
41 type VocabularyLabel struct {
42         Label string `json:"label"`
43 }
44
45 type VocabularyTagValue struct {
46         Labels []VocabularyLabel `json:"labels"`
47 }
48
49 // NewVocabulary creates a new Vocabulary from a JSON definition and a list
50 // of reserved tag keys that will get special treatment when strict mode is
51 // enabled.
52 func NewVocabulary(data []byte, managedTagKeys []string) (voc *Vocabulary, err error) {
53         if r := bytes.Compare(data, []byte("")); r == 0 {
54                 return &Vocabulary{}, nil
55         }
56         err = json.Unmarshal(data, &voc)
57         if err != nil {
58                 return nil, fmt.Errorf("invalid JSON format error: %q", err)
59         }
60         if reflect.DeepEqual(voc, &Vocabulary{}) {
61                 return nil, fmt.Errorf("JSON data provided doesn't match Vocabulary format: %q", data)
62         }
63         voc.reservedTagKeys = make(map[string]bool)
64         for _, managedKey := range managedTagKeys {
65                 voc.reservedTagKeys[managedKey] = true
66         }
67         for systemKey := range voc.systemTagKeys() {
68                 voc.reservedTagKeys[systemKey] = true
69         }
70         err = voc.validate()
71         if err != nil {
72                 return nil, err
73         }
74         return voc, nil
75 }
76
77 func (v *Vocabulary) validate() error {
78         if v == nil {
79                 return nil
80         }
81         tagKeys := map[string]string{}
82         // Checks for Vocabulary strictness
83         if v.StrictTags && len(v.Tags) == 0 {
84                 return fmt.Errorf("vocabulary is strict but no tags are defined")
85         }
86         // Checks for collisions between tag keys, reserved tag keys
87         // and tag key labels.
88         for key := range v.Tags {
89                 if v.reservedTagKeys[key] {
90                         return fmt.Errorf("tag key %q is reserved", key)
91                 }
92                 lcKey := strings.ToLower(key)
93                 if tagKeys[lcKey] != "" {
94                         return fmt.Errorf("duplicate tag key %q", key)
95                 }
96                 tagKeys[lcKey] = key
97                 for _, lbl := range v.Tags[key].Labels {
98                         label := strings.ToLower(lbl.Label)
99                         if tagKeys[label] != "" {
100                                 return fmt.Errorf("tag label %q for key %q already seen as a tag key or label", lbl.Label, key)
101                         }
102                         tagKeys[label] = lbl.Label
103                 }
104                 // Checks for value strictness
105                 if v.Tags[key].Strict && len(v.Tags[key].Values) == 0 {
106                         return fmt.Errorf("tag key %q is configured as strict but doesn't provide values", key)
107                 }
108                 // Checks for collisions between tag values and tag value labels.
109                 tagValues := map[string]string{}
110                 for val := range v.Tags[key].Values {
111                         lcVal := strings.ToLower(val)
112                         if tagValues[lcVal] != "" {
113                                 return fmt.Errorf("duplicate tag value %q for tag %q", val, key)
114                         }
115                         // Checks for collisions between labels from different values.
116                         tagValues[lcVal] = val
117                         for _, tagLbl := range v.Tags[key].Values[val].Labels {
118                                 label := strings.ToLower(tagLbl.Label)
119                                 if tagValues[label] != "" && tagValues[label] != val {
120                                         return fmt.Errorf("tag value label %q for pair (%q:%q) already seen on value %q", tagLbl.Label, key, val, tagValues[label])
121                                 }
122                                 tagValues[label] = val
123                         }
124                 }
125         }
126         return nil
127 }
128
129 func (v *Vocabulary) getLabelsToKeys() (labels map[string]string) {
130         if v == nil {
131                 return
132         }
133         labels = make(map[string]string)
134         for key, val := range v.Tags {
135                 for _, lbl := range val.Labels {
136                         label := strings.ToLower(lbl.Label)
137                         labels[label] = key
138                 }
139         }
140         return labels
141 }
142
143 func (v *Vocabulary) getLabelsToValues(key string) (labels map[string]string) {
144         if v == nil {
145                 return
146         }
147         labels = make(map[string]string)
148         if _, ok := v.Tags[key]; ok {
149                 for val := range v.Tags[key].Values {
150                         labels[strings.ToLower(val)] = val
151                         for _, tagLbl := range v.Tags[key].Values[val].Labels {
152                                 label := strings.ToLower(tagLbl.Label)
153                                 labels[label] = val
154                         }
155                 }
156         }
157         return labels
158 }
159
160 func (v *Vocabulary) checkValue(key, val string) error {
161         if _, ok := v.Tags[key].Values[val]; !ok {
162                 lcVal := strings.ToLower(val)
163                 correctValue, ok := v.getLabelsToValues(key)[lcVal]
164                 if ok {
165                         return fmt.Errorf("tag value %q for key %q is an alias, must be provided as %q", val, key, correctValue)
166                 } else if v.Tags[key].Strict {
167                         return fmt.Errorf("tag value %q is not valid for key %q", val, key)
168                 }
169         }
170         return nil
171 }
172
173 // Check validates the given data against the vocabulary.
174 func (v *Vocabulary) Check(data map[string]interface{}) error {
175         if v == nil {
176                 return nil
177         }
178         for key, val := range data {
179                 // Checks for key validity
180                 if v.reservedTagKeys[key] {
181                         // Allow reserved keys to be used even if they are not defined in
182                         // the vocabulary no matter its strictness.
183                         continue
184                 }
185                 if _, ok := v.Tags[key]; !ok {
186                         lcKey := strings.ToLower(key)
187                         correctKey, ok := v.getLabelsToKeys()[lcKey]
188                         if ok {
189                                 return fmt.Errorf("tag key %q is an alias, must be provided as %q", key, correctKey)
190                         } else if v.StrictTags {
191                                 return fmt.Errorf("tag key %q is not defined in the vocabulary", key)
192                         }
193                         // If the key is not defined, we don't need to check the value
194                         continue
195                 }
196                 // Checks for value validity -- key is defined
197                 switch val := val.(type) {
198                 case string:
199                         err := v.checkValue(key, val)
200                         if err != nil {
201                                 return err
202                         }
203                 case []interface{}:
204                         for _, singleVal := range val {
205                                 switch singleVal := singleVal.(type) {
206                                 case string:
207                                         err := v.checkValue(key, singleVal)
208                                         if err != nil {
209                                                 return err
210                                         }
211                                 default:
212                                         return fmt.Errorf("value list element type for tag key %q was %T, but expected a string", key, singleVal)
213                                 }
214                         }
215                 default:
216                         return fmt.Errorf("value type for tag key %q was %T, but expected a string or list of strings", key, val)
217                 }
218         }
219         return nil
220 }