1 // Copyright (C) The Arvados Authors. All rights reserved.
3 // SPDX-License-Identifier: Apache-2.0
15 type Vocabulary struct {
16 reservedTagKeys map[string]bool `json:"-"`
17 StrictTags bool `json:"strict_tags"`
18 Tags map[string]VocabularyTag `json:"tags"`
21 type VocabularyTag struct {
22 Strict bool `json:"strict"`
23 Labels []VocabularyLabel `json:"labels"`
24 Values map[string]VocabularyTagValue `json:"values"`
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{
31 "template_uuid": true,
34 "image_timestamp": true,
35 "docker-image-repo-tag": true,
37 "container_request": true,
41 type VocabularyLabel struct {
42 Label string `json:"label"`
45 type VocabularyTagValue struct {
46 Labels []VocabularyLabel `json:"labels"`
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
52 func NewVocabulary(data []byte, managedTagKeys []string) (voc *Vocabulary, err error) {
53 if r := bytes.Compare(data, []byte("")); r == 0 {
54 return &Vocabulary{}, nil
56 err = json.Unmarshal(data, &voc)
58 return nil, fmt.Errorf("invalid JSON format error: %q", err)
60 if reflect.DeepEqual(voc, &Vocabulary{}) {
61 return nil, fmt.Errorf("JSON data provided doesn't match Vocabulary format: %q", data)
63 voc.reservedTagKeys = make(map[string]bool)
64 for _, managedKey := range managedTagKeys {
65 voc.reservedTagKeys[managedKey] = true
67 for systemKey := range voc.systemTagKeys() {
68 voc.reservedTagKeys[systemKey] = true
77 func (v *Vocabulary) validate() error {
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")
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)
92 lcKey := strings.ToLower(key)
93 if tagKeys[lcKey] != "" {
94 return fmt.Errorf("duplicate tag key %q", 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)
102 tagKeys[label] = lbl.Label
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)
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)
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])
122 tagValues[label] = val
129 func (v *Vocabulary) getLabelsToKeys() (labels map[string]string) {
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)
143 func (v *Vocabulary) getLabelsToValues(key string) (labels map[string]string) {
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)
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]
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)
173 // Check validates the given data against the vocabulary.
174 func (v *Vocabulary) Check(data map[string]interface{}) error {
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.
185 if _, ok := v.Tags[key]; !ok {
186 lcKey := strings.ToLower(key)
187 correctKey, ok := v.getLabelsToKeys()[lcKey]
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)
193 // If the key is not defined, we don't need to check the value
196 // Checks for value validity -- key is defined
197 switch val := val.(type) {
199 err := v.checkValue(key, val)
204 for _, singleVal := range val {
205 switch singleVal := singleVal.(type) {
207 err := v.checkValue(key, singleVal)
212 return fmt.Errorf("value list element type for tag key %q was %T, but expected a string", key, singleVal)
216 return fmt.Errorf("value type for tag key %q was %T, but expected a string or list of strings", key, val)