github.com/juju/juju@v0.0.0-20240430160146-1752b71fcf00/state/annotations.go (about) 1 // Copyright 2015 Canonical Ltd. 2 // Licensed under the AGPLv3, see LICENCE file for details. 3 4 package state 5 6 import ( 7 "fmt" 8 "strings" 9 10 "github.com/juju/errors" 11 "github.com/juju/mgo/v3" 12 "github.com/juju/mgo/v3/bson" 13 "github.com/juju/mgo/v3/txn" 14 "github.com/juju/names/v5" 15 ) 16 17 // annotatorDoc represents the internal state of annotations for an Entity in 18 // MongoDB. Note that the annotations map is not maintained in local storage 19 // due to the fact that it is not accessed directly, but through 20 // Annotations/Annotation below. 21 // Note also the correspondence with AnnotationInfo in apiserver/params. 22 type annotatorDoc struct { 23 ModelUUID string `bson:"model-uuid"` 24 GlobalKey string `bson:"globalkey"` 25 Tag string `bson:"tag"` 26 Annotations map[string]string `bson:"annotations"` 27 } 28 29 // SetAnnotations adds key/value pairs to annotations in MongoDB. 30 func (m *Model) SetAnnotations(entity GlobalEntity, annotations map[string]string) (err error) { 31 defer errors.DeferredAnnotatef(&err, "cannot update annotations on %s", entity.Tag()) 32 if len(annotations) == 0 { 33 return nil 34 } 35 // Collect in separate maps pairs to be inserted/updated or removed. 36 toRemove := make(bson.M) 37 toInsert := make(map[string]string) 38 toUpdate := make(bson.M) 39 for key, value := range annotations { 40 if strings.Contains(key, ".") { 41 return fmt.Errorf("invalid key %q", key) 42 } 43 if value == "" { 44 toRemove[key] = true 45 } else { 46 toInsert[key] = value 47 toUpdate[key] = value 48 } 49 } 50 // Set up and call the necessary transactions - if the document does not 51 // already exist, one of the clients will create it and the others will 52 // fail, then all the rest of the clients should succeed on their second 53 // attempt. If the referred-to entity has disappeared, and removed its 54 // annotations in the meantime, we consider that worthy of an error 55 // (will be fixed when new entities can never share names with old ones). 56 buildTxn := func(attempt int) ([]txn.Op, error) { 57 annotations, closer := m.st.db().GetCollection(annotationsC) 58 defer closer() 59 if count, err := annotations.FindId(entity.globalKey()).Count(); err != nil { 60 return nil, err 61 } else if count == 0 { 62 // Check that the annotator entity was not previously destroyed. 63 if attempt != 0 { 64 return nil, fmt.Errorf("%s no longer exists", entity.Tag()) 65 } 66 return insertAnnotationsOps(m.st, entity, toInsert) 67 } 68 return updateAnnotations(m.st, entity, toUpdate, toRemove), nil 69 } 70 return m.st.db().Run(buildTxn) 71 } 72 73 // Annotations returns all the annotations corresponding to an entity. 74 func (m *Model) Annotations(entity GlobalEntity) (map[string]string, error) { 75 doc := new(annotatorDoc) 76 annotations, closer := m.st.db().GetCollection(annotationsC) 77 defer closer() 78 err := annotations.FindId(entity.globalKey()).One(doc) 79 if err == mgo.ErrNotFound { 80 // Returning an empty map if there are no annotations. 81 return make(map[string]string), nil 82 } 83 if err != nil { 84 return nil, errors.Trace(err) 85 } 86 return doc.Annotations, nil 87 } 88 89 // Annotation returns the annotation value corresponding to the given key. 90 // If the requested annotation is not found, an empty string is returned. 91 func (m *Model) Annotation(entity GlobalEntity, key string) (string, error) { 92 ann, err := m.Annotations(entity) 93 if err != nil { 94 return "", errors.Trace(err) 95 } 96 return ann[key], nil 97 } 98 99 // insertAnnotationsOps returns the operations required to insert annotations in MongoDB. 100 func insertAnnotationsOps(st *State, entity GlobalEntity, toInsert map[string]string) ([]txn.Op, error) { 101 tag := entity.Tag() 102 ops := []txn.Op{{ 103 C: annotationsC, 104 Id: st.docID(entity.globalKey()), 105 Assert: txn.DocMissing, 106 Insert: &annotatorDoc{ 107 GlobalKey: entity.globalKey(), 108 Tag: tag.String(), 109 Annotations: toInsert, 110 }, 111 }} 112 113 switch tag := tag.(type) { 114 case names.ModelTag: 115 if tag.Id() == st.ControllerModelUUID() { 116 // This is the controller model, and cannot be removed. 117 // Ergo, we can skip the existence check below. 118 return ops, nil 119 } 120 } 121 122 // If the entity is not the controller model, add a DocExists check on the 123 // entity document, in order to avoid possible races between entity 124 // removal and annotation creation. 125 coll, id, err := st.tagToCollectionAndId(tag) 126 if err != nil { 127 return nil, errors.Trace(err) 128 } 129 return append(ops, txn.Op{ 130 C: coll, 131 Id: id, 132 Assert: txn.DocExists, 133 }), nil 134 } 135 136 // updateAnnotations returns the operations required to update or remove annotations in MongoDB. 137 func updateAnnotations(mb modelBackend, entity GlobalEntity, toUpdate, toRemove bson.M) []txn.Op { 138 return []txn.Op{{ 139 C: annotationsC, 140 Id: mb.docID(entity.globalKey()), 141 Assert: txn.DocExists, 142 Update: setUnsetUpdateAnnotations(toUpdate, toRemove), 143 }} 144 } 145 146 // annotationRemoveOp returns an operation to remove a given annotation 147 // document from MongoDB. 148 func annotationRemoveOp(mb modelBackend, id string) txn.Op { 149 return txn.Op{ 150 C: annotationsC, 151 Id: mb.docID(id), 152 Remove: true, 153 } 154 } 155 156 // setUnsetUpdateAnnotations returns a bson.D for use 157 // in an annotationsC txn.Op's Update field, containing $set and 158 // $unset operators if the corresponding operands 159 // are non-empty. 160 func setUnsetUpdateAnnotations(set, unset bson.M) bson.D { 161 var update bson.D 162 if len(set) > 0 { 163 set = bson.M(subDocKeys(map[string]interface{}(set), "annotations")) 164 update = append(update, bson.DocElem{Name: "$set", Value: set}) 165 } 166 if len(unset) > 0 { 167 unset = bson.M(subDocKeys(map[string]interface{}(unset), "annotations")) 168 update = append(update, bson.DocElem{Name: "$unset", Value: unset}) 169 } 170 return update 171 }