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,
45 type VocabularyLabel struct {
46 Label string `json:"label"`
49 type VocabularyTagValue struct {
50 Labels []VocabularyLabel `json:"labels"`
53 // NewVocabulary creates a new Vocabulary from a JSON definition and a list
54 // of reserved tag keys that will get special treatment when strict mode is
56 func NewVocabulary(data []byte, managedTagKeys []string) (voc *Vocabulary, err error) {
57 if r := bytes.Compare(data, []byte("")); r == 0 {
58 return &Vocabulary{}, nil
60 err = json.Unmarshal(data, &voc)
62 var serr *json.SyntaxError
63 if errors.As(err, &serr) {
65 errorMsg := string(data[:offset])
66 line := 1 + strings.Count(errorMsg, "\n")
67 column := offset - int64(strings.LastIndex(errorMsg, "\n")+len("\n"))
68 return nil, fmt.Errorf("invalid JSON format: %q (line %d, column %d)", err, line, column)
70 return nil, fmt.Errorf("invalid JSON format: %q", err)
72 if reflect.DeepEqual(voc, &Vocabulary{}) {
73 return nil, fmt.Errorf("JSON data provided doesn't match Vocabulary format: %q", data)
76 shouldReportErrors := false
79 // json.Unmarshal() doesn't error out on duplicate keys.
80 dupedKeys := []string{}
81 err = checkJSONDupedKeys(json.NewDecoder(bytes.NewReader(data)), nil, &dupedKeys)
83 shouldReportErrors = true
84 for _, dk := range dupedKeys {
85 errors = append(errors, fmt.Sprintf("duplicate JSON key %q", dk))
88 voc.reservedTagKeys = make(map[string]bool)
89 for _, managedKey := range managedTagKeys {
90 voc.reservedTagKeys[managedKey] = true
92 for systemKey := range voc.systemTagKeys() {
93 voc.reservedTagKeys[systemKey] = true
95 validationErrs, err := voc.validate()
97 shouldReportErrors = true
98 errors = append(errors, validationErrs...)
100 if shouldReportErrors {
101 return nil, fmt.Errorf("%s", strings.Join(errors, "\n"))
106 func checkJSONDupedKeys(d *json.Decoder, path []string, errors *[]string) error {
111 delim, ok := t.(json.Delim)
117 keys := make(map[string]bool)
126 *errors = append(*errors, strings.Join(append(path, key), "."))
130 if err := checkJSONDupedKeys(d, append(path, key), errors); err != nil {
134 // consume closing '}'
135 if _, err := d.Token(); err != nil {
141 if err := checkJSONDupedKeys(d, append(path, strconv.Itoa(i)), errors); err != nil {
146 // consume closing ']'
147 if _, err := d.Token(); err != nil {
151 if len(path) == 0 && len(*errors) > 0 {
152 return fmt.Errorf("duplicate JSON key(s) found")
157 func (v *Vocabulary) validate() ([]string, error) {
161 tagKeys := map[string]string{}
162 // Checks for Vocabulary strictness
163 if v.StrictTags && len(v.Tags) == 0 {
164 return nil, fmt.Errorf("vocabulary is strict but no tags are defined")
166 // Checks for collisions between tag keys, reserved tag keys
167 // and tag key labels.
169 for key := range v.Tags {
170 if v.reservedTagKeys[key] {
171 errors = append(errors, fmt.Sprintf("tag key %q is reserved", key))
173 lcKey := strings.ToLower(key)
174 if tagKeys[lcKey] != "" {
175 errors = append(errors, fmt.Sprintf("duplicate tag key %q", key))
178 for _, lbl := range v.Tags[key].Labels {
179 label := strings.ToLower(lbl.Label)
180 if tagKeys[label] != "" {
181 errors = append(errors, fmt.Sprintf("tag label %q for key %q already seen as a tag key or label", lbl.Label, key))
183 tagKeys[label] = lbl.Label
185 // Checks for value strictness
186 if v.Tags[key].Strict && len(v.Tags[key].Values) == 0 {
187 errors = append(errors, fmt.Sprintf("tag key %q is configured as strict but doesn't provide values", key))
189 // Checks for collisions between tag values and tag value labels.
190 tagValues := map[string]string{}
191 for val := range v.Tags[key].Values {
192 lcVal := strings.ToLower(val)
193 if tagValues[lcVal] != "" {
194 errors = append(errors, fmt.Sprintf("duplicate tag value %q for tag %q", val, key))
196 // Checks for collisions between labels from different values.
197 tagValues[lcVal] = val
198 for _, tagLbl := range v.Tags[key].Values[val].Labels {
199 label := strings.ToLower(tagLbl.Label)
200 if tagValues[label] != "" && tagValues[label] != val {
201 errors = append(errors, fmt.Sprintf("tag value label %q for pair (%q:%q) already seen on value %q", tagLbl.Label, key, val, tagValues[label]))
203 tagValues[label] = val
208 return errors, fmt.Errorf("invalid vocabulary")
213 func (v *Vocabulary) getLabelsToKeys() (labels map[string]string) {
217 labels = make(map[string]string)
218 for key, val := range v.Tags {
219 for _, lbl := range val.Labels {
220 label := strings.ToLower(lbl.Label)
227 func (v *Vocabulary) getLabelsToValues(key string) (labels map[string]string) {
231 labels = make(map[string]string)
232 if _, ok := v.Tags[key]; ok {
233 for val := range v.Tags[key].Values {
234 labels[strings.ToLower(val)] = val
235 for _, tagLbl := range v.Tags[key].Values[val].Labels {
236 label := strings.ToLower(tagLbl.Label)
244 func (v *Vocabulary) checkValue(key, val string) error {
245 if _, ok := v.Tags[key].Values[val]; !ok {
246 lcVal := strings.ToLower(val)
247 correctValue, ok := v.getLabelsToValues(key)[lcVal]
249 return fmt.Errorf("tag value %q for key %q is an alias, must be provided as %q", val, key, correctValue)
250 } else if v.Tags[key].Strict {
251 return fmt.Errorf("tag value %q is not valid for key %q", val, key)
257 // Check validates the given data against the vocabulary.
258 func (v *Vocabulary) Check(data map[string]interface{}) error {
262 for key, val := range data {
263 // Checks for key validity
264 if v.reservedTagKeys[key] {
265 // Allow reserved keys to be used even if they are not defined in
266 // the vocabulary no matter its strictness.
269 if _, ok := v.Tags[key]; !ok {
270 lcKey := strings.ToLower(key)
271 correctKey, ok := v.getLabelsToKeys()[lcKey]
273 return fmt.Errorf("tag key %q is an alias, must be provided as %q", key, correctKey)
274 } else if v.StrictTags {
275 return fmt.Errorf("tag key %q is not defined in the vocabulary", key)
277 // If the key is not defined, we don't need to check the value
280 // Checks for value validity -- key is defined
281 switch val := val.(type) {
283 err := v.checkValue(key, val)
288 for _, singleVal := range val {
289 switch singleVal := singleVal.(type) {
291 err := v.checkValue(key, singleVal)
296 return fmt.Errorf("value list element type for tag key %q was %T, but expected a string", key, singleVal)
300 return fmt.Errorf("value type for tag key %q was %T, but expected a string or list of strings", key, val)