17944: Vocabulary loading, monitoring and checking on several object types.
[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 func NewVocabulary(data []byte, managedTagKeys []string) (voc *Vocabulary, err error) {
50         if r := bytes.Compare(data, []byte("")); r == 0 {
51                 return &Vocabulary{}, nil
52         }
53         err = json.Unmarshal(data, &voc)
54         if err != nil {
55                 return nil, fmt.Errorf("invalid JSON format error: %q", err)
56         }
57         if reflect.DeepEqual(voc, &Vocabulary{}) {
58                 return nil, fmt.Errorf("JSON data provided doesn't match Vocabulary format: %q", data)
59         }
60         voc.reservedTagKeys = make(map[string]bool)
61         for _, managedKey := range managedTagKeys {
62                 voc.reservedTagKeys[managedKey] = true
63         }
64         for systemKey := range voc.systemTagKeys() {
65                 voc.reservedTagKeys[systemKey] = true
66         }
67         err = voc.Validate()
68         if err != nil {
69                 return nil, err
70         }
71         return voc, nil
72 }
73
74 func (v *Vocabulary) Validate() error {
75         if v == nil {
76                 return nil
77         }
78         tagKeys := map[string]bool{}
79         // Checks for Vocabulary strictness
80         if v.StrictTags && len(v.Tags) == 0 {
81                 return fmt.Errorf("vocabulary is strict but no tags are defined")
82         }
83         // Checks for duplicate tag keys
84         for key := range v.Tags {
85                 if v.reservedTagKeys[key] {
86                         return fmt.Errorf("tag key %q is reserved", key)
87                 }
88                 if tagKeys[key] {
89                         return fmt.Errorf("duplicate tag key %q", key)
90                 }
91                 tagKeys[key] = true
92                 for _, lbl := range v.Tags[key].Labels {
93                         label := strings.ToLower(lbl.Label)
94                         if tagKeys[label] {
95                                 return fmt.Errorf("tag label %q for key %q already seen as a tag key or label", label, key)
96                         }
97                         tagKeys[label] = true
98                 }
99                 // Checks for value strictness
100                 if v.Tags[key].Strict && len(v.Tags[key].Values) == 0 {
101                         return fmt.Errorf("tag key %q is configured as strict but doesn't provide values", key)
102                 }
103                 // Checks for value duplication within a key
104                 tagValues := map[string]bool{}
105                 for val := range v.Tags[key].Values {
106                         if tagValues[val] {
107                                 return fmt.Errorf("duplicate tag value %q for tag %q", val, key)
108                         }
109                         tagValues[val] = true
110                         for _, tagLbl := range v.Tags[key].Values[val].Labels {
111                                 label := strings.ToLower(tagLbl.Label)
112                                 if tagValues[label] {
113                                         return fmt.Errorf("tag value label %q for pair (%q:%q) already seen as a value key or label", label, key, val)
114                                 }
115                                 tagValues[label] = true
116                         }
117                 }
118         }
119         return nil
120 }
121
122 func (v *Vocabulary) getLabelsToKeys() (labels map[string]string) {
123         if v == nil {
124                 return
125         }
126         labels = make(map[string]string)
127         for key, val := range v.Tags {
128                 for _, lbl := range val.Labels {
129                         label := strings.ToLower(lbl.Label)
130                         labels[label] = key
131                 }
132         }
133         return labels
134 }
135
136 func (v *Vocabulary) getLabelsToValues(key string) (labels map[string]string) {
137         if v == nil {
138                 return
139         }
140         labels = make(map[string]string)
141         if _, ok := v.Tags[key]; ok {
142                 for val := range v.Tags[key].Values {
143                         for _, tagLbl := range v.Tags[key].Values[val].Labels {
144                                 label := strings.ToLower(tagLbl.Label)
145                                 labels[label] = val
146                         }
147                 }
148         }
149         return labels
150 }
151
152 func (v *Vocabulary) checkValue(key, val string) error {
153         if _, ok := v.Tags[key].Values[val]; !ok {
154                 lcVal := strings.ToLower(val)
155                 alias, ok := v.getLabelsToValues(key)[lcVal]
156                 if ok {
157                         return fmt.Errorf("tag value %q for key %q is not defined but is an alias for %q", val, key, alias)
158                 } else if v.Tags[key].Strict {
159                         return fmt.Errorf("tag value %q for key %q is not listed as valid", val, key)
160                 }
161         }
162         return nil
163 }
164
165 // Check validates the given data against the vocabulary.
166 func (v *Vocabulary) Check(data map[string]interface{}) error {
167         if v == nil {
168                 return nil
169         }
170         for key, val := range data {
171                 // Checks for key validity
172                 if v.reservedTagKeys[key] {
173                         // Allow reserved keys to be used even if they are not defined in
174                         // the vocabulary no matter its strictness.
175                         continue
176                 }
177                 if _, ok := v.Tags[key]; !ok {
178                         lcKey := strings.ToLower(key)
179                         alias, ok := v.getLabelsToKeys()[lcKey]
180                         if ok {
181                                 return fmt.Errorf("tag key %q is not defined but is an alias for %q", key, alias)
182                         } else if v.StrictTags {
183                                 return fmt.Errorf("tag key %q is not defined", key)
184                         }
185                         // If the key is not defined, we don't need to check the value
186                         continue
187                 }
188                 // Checks for value validity -- key is defined
189                 switch val := val.(type) {
190                 case string:
191                         return v.checkValue(key, val)
192                 case []interface{}:
193                         for _, singleVal := range val {
194                                 switch singleVal := singleVal.(type) {
195                                 case string:
196                                         err := v.checkValue(key, singleVal)
197                                         if err != nil {
198                                                 return err
199                                         }
200                                 default:
201                                         return fmt.Errorf("tag value %q for key %q is not a valid type (%T)", singleVal, key, singleVal)
202                                 }
203                         }
204                 default:
205                         return fmt.Errorf("tag value %q for key %q is not a valid type (%T)", val, key, val)
206                 }
207         }
208         return nil
209 }