// Copyright (C) The Arvados Authors. All rights reserved. // // SPDX-License-Identifier: Apache-2.0 package arvados import ( "bytes" "encoding/json" "fmt" "reflect" "strings" ) type Vocabulary struct { reservedTagKeys map[string]bool `json:"-"` StrictTags bool `json:"strict_tags"` Tags map[string]VocabularyTag `json:"tags"` } type VocabularyTag struct { Strict bool `json:"strict"` Labels []VocabularyLabel `json:"labels"` Values map[string]VocabularyTagValue `json:"values"` } // Cannot have a constant map in Go, so we have to use a function func (v *Vocabulary) systemTagKeys() map[string]bool { return map[string]bool{ "type": true, "template_uuid": true, "groups": true, "username": true, "image_timestamp": true, "docker-image-repo-tag": true, "filters": true, "container_request": true, } } type VocabularyLabel struct { Label string `json:"label"` } type VocabularyTagValue struct { Labels []VocabularyLabel `json:"labels"` } func NewVocabulary(data []byte, managedTagKeys []string) (voc *Vocabulary, err error) { if r := bytes.Compare(data, []byte("")); r == 0 { return &Vocabulary{}, nil } err = json.Unmarshal(data, &voc) if err != nil { return nil, fmt.Errorf("invalid JSON format error: %q", err) } if reflect.DeepEqual(voc, &Vocabulary{}) { return nil, fmt.Errorf("JSON data provided doesn't match Vocabulary format: %q", data) } voc.reservedTagKeys = make(map[string]bool) for _, managedKey := range managedTagKeys { voc.reservedTagKeys[managedKey] = true } for systemKey := range voc.systemTagKeys() { voc.reservedTagKeys[systemKey] = true } err = voc.Validate() if err != nil { return nil, err } return voc, nil } func (v *Vocabulary) Validate() error { if v == nil { return nil } tagKeys := map[string]bool{} // Checks for Vocabulary strictness if v.StrictTags && len(v.Tags) == 0 { return fmt.Errorf("vocabulary is strict but no tags are defined") } // Checks for duplicate tag keys for key := range v.Tags { if v.reservedTagKeys[key] { return fmt.Errorf("tag key %q is reserved", key) } if tagKeys[key] { return fmt.Errorf("duplicate tag key %q", key) } tagKeys[key] = true for _, lbl := range v.Tags[key].Labels { label := strings.ToLower(lbl.Label) if tagKeys[label] { return fmt.Errorf("tag label %q for key %q already seen as a tag key or label", label, key) } tagKeys[label] = true } // Checks for value strictness if v.Tags[key].Strict && len(v.Tags[key].Values) == 0 { return fmt.Errorf("tag key %q is configured as strict but doesn't provide values", key) } // Checks for value duplication within a key tagValues := map[string]bool{} for val := range v.Tags[key].Values { if tagValues[val] { return fmt.Errorf("duplicate tag value %q for tag %q", val, key) } tagValues[val] = true for _, tagLbl := range v.Tags[key].Values[val].Labels { label := strings.ToLower(tagLbl.Label) if tagValues[label] { return fmt.Errorf("tag value label %q for pair (%q:%q) already seen as a value key or label", label, key, val) } tagValues[label] = true } } } return nil } func (v *Vocabulary) getLabelsToKeys() (labels map[string]string) { if v == nil { return } labels = make(map[string]string) for key, val := range v.Tags { for _, lbl := range val.Labels { label := strings.ToLower(lbl.Label) labels[label] = key } } return labels } func (v *Vocabulary) getLabelsToValues(key string) (labels map[string]string) { if v == nil { return } labels = make(map[string]string) if _, ok := v.Tags[key]; ok { for val := range v.Tags[key].Values { for _, tagLbl := range v.Tags[key].Values[val].Labels { label := strings.ToLower(tagLbl.Label) labels[label] = val } } } return labels } func (v *Vocabulary) checkValue(key, val string) error { if _, ok := v.Tags[key].Values[val]; !ok { lcVal := strings.ToLower(val) alias, ok := v.getLabelsToValues(key)[lcVal] if ok { return fmt.Errorf("tag value %q for key %q is not defined but is an alias for %q", val, key, alias) } else if v.Tags[key].Strict { return fmt.Errorf("tag value %q for key %q is not listed as valid", val, key) } } return nil } // Check validates the given data against the vocabulary. func (v *Vocabulary) Check(data map[string]interface{}) error { if v == nil { return nil } for key, val := range data { // Checks for key validity if v.reservedTagKeys[key] { // Allow reserved keys to be used even if they are not defined in // the vocabulary no matter its strictness. continue } if _, ok := v.Tags[key]; !ok { lcKey := strings.ToLower(key) alias, ok := v.getLabelsToKeys()[lcKey] if ok { return fmt.Errorf("tag key %q is not defined but is an alias for %q", key, alias) } else if v.StrictTags { return fmt.Errorf("tag key %q is not defined", key) } // If the key is not defined, we don't need to check the value continue } // Checks for value validity -- key is defined switch val := val.(type) { case string: err := v.checkValue(key, val) if err != nil { return err } case []interface{}: for _, singleVal := range val { switch singleVal := singleVal.(type) { case string: err := v.checkValue(key, singleVal) if err != nil { return err } default: return fmt.Errorf("tag value %q for key %q is not a valid type (%T)", singleVal, key, singleVal) } } default: return fmt.Errorf("tag value %q for key %q is not a valid type (%T)", val, key, val) } } return nil }