1 // Copyright (C) The Arvados Authors. All rights reserved.
3 // SPDX-License-Identifier: Apache-2.0
17 type Vocabulary struct {
18 reservedTagKeys map[string]bool `json:"-"`
19 StrictTags bool `json:"strict_tags"`
20 Tags map[string]VocabularyTag `json:"tags"`
23 type VocabularyTag struct {
24 Strict bool `json:"strict"`
25 Labels []VocabularyLabel `json:"labels"`
26 Values map[string]VocabularyTagValue `json:"values"`
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{
33 "template_uuid": true,
36 "image_timestamp": true,
37 "docker-image-repo-tag": true,
39 "container_request": true,
43 type VocabularyLabel struct {
44 Label string `json:"label"`
47 type VocabularyTagValue struct {
48 Labels []VocabularyLabel `json:"labels"`
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
54 func NewVocabulary(data []byte, managedTagKeys []string) (voc *Vocabulary, err error) {
55 if r := bytes.Compare(data, []byte("")); r == 0 {
56 return &Vocabulary{}, nil
58 err = json.Unmarshal(data, &voc)
60 var serr *json.SyntaxError
61 if errors.As(err, &serr) {
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)
68 return nil, fmt.Errorf("invalid JSON format: %q", err)
70 if reflect.DeepEqual(voc, &Vocabulary{}) {
71 return nil, fmt.Errorf("JSON data provided doesn't match Vocabulary format: %q", data)
74 shouldReportErrors := false
77 // json.Unmarshal() doesn't error out on duplicate keys.
78 dupedKeys := []string{}
79 err = checkJSONDupedKeys(json.NewDecoder(bytes.NewReader(data)), nil, &dupedKeys)
81 shouldReportErrors = true
82 for _, dk := range dupedKeys {
83 errors = append(errors, fmt.Sprintf("duplicate JSON key %q", dk))
86 voc.reservedTagKeys = make(map[string]bool)
87 for _, managedKey := range managedTagKeys {
88 voc.reservedTagKeys[managedKey] = true
90 for systemKey := range voc.systemTagKeys() {
91 voc.reservedTagKeys[systemKey] = true
93 validationErrs, err := voc.validate()
95 shouldReportErrors = true
96 errors = append(errors, validationErrs...)
98 if shouldReportErrors {
99 return nil, fmt.Errorf("%s", strings.Join(errors, "\n"))
104 func checkJSONDupedKeys(d *json.Decoder, path []string, errors *[]string) error {
109 delim, ok := t.(json.Delim)
115 keys := make(map[string]bool)
124 *errors = append(*errors, strings.Join(append(path, key), "."))
128 if err := checkJSONDupedKeys(d, append(path, key), errors); err != nil {
132 // consume closing '}'
133 if _, err := d.Token(); err != nil {
139 if err := checkJSONDupedKeys(d, append(path, strconv.Itoa(i)), errors); err != nil {
144 // consume closing ']'
145 if _, err := d.Token(); err != nil {
149 if len(path) == 0 && len(*errors) > 0 {
150 return fmt.Errorf("duplicate JSON key(s) found")
155 func (v *Vocabulary) validate() ([]string, error) {
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")
164 // Checks for collisions between tag keys, reserved tag keys
165 // and tag key labels.
167 for key := range v.Tags {
168 if v.reservedTagKeys[key] {
169 errors = append(errors, fmt.Sprintf("tag key %q is reserved", key))
171 lcKey := strings.ToLower(key)
172 if tagKeys[lcKey] != "" {
173 errors = append(errors, fmt.Sprintf("duplicate tag key %q", 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))
181 tagKeys[label] = lbl.Label
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))
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))
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]))
201 tagValues[label] = val
206 return errors, fmt.Errorf("invalid vocabulary")
211 func (v *Vocabulary) getLabelsToKeys() (labels map[string]string) {
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)
225 func (v *Vocabulary) getLabelsToValues(key string) (labels map[string]string) {
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)
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]
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)
255 // Check validates the given data against the vocabulary.
256 func (v *Vocabulary) Check(data map[string]interface{}) error {
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.
267 if _, ok := v.Tags[key]; !ok {
268 lcKey := strings.ToLower(key)
269 correctKey, ok := v.getLabelsToKeys()[lcKey]
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)
275 // If the key is not defined, we don't need to check the value
278 // Checks for value validity -- key is defined
279 switch val := val.(type) {
281 err := v.checkValue(key, val)
286 for _, singleVal := range val {
287 switch singleVal := singleVal.(type) {
289 err := v.checkValue(key, singleVal)
294 return fmt.Errorf("value list element type for tag key %q was %T, but expected a string", key, singleVal)
298 return fmt.Errorf("value type for tag key %q was %T, but expected a string or list of strings", key, val)