github.com/altoros/juju-vmware@v0.0.0-20150312064031-f19ae857ccca/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/names" 12 "gopkg.in/mgo.v2" 13 "gopkg.in/mgo.v2/bson" 14 "gopkg.in/mgo.v2/txn" 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 EnvUUID string `bson:"env-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 (st *State) 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["annotations."+key] = true 45 } else { 46 toInsert[key] = value 47 toUpdate["annotations."+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 := st.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(st, entity, toInsert) 67 } 68 return updateAnnotations(st, entity, toUpdate, toRemove), nil 69 } 70 return st.run(buildTxn) 71 } 72 73 // Annotations returns all the annotations corresponding to an entity. 74 func (st *State) Annotations(entity GlobalEntity) (map[string]string, error) { 75 doc := new(annotatorDoc) 76 annotations, closer := st.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 (st *State) Annotation(entity GlobalEntity, key string) (string, error) { 92 ann, err := st.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 switch tag.(type) { 113 case names.EnvironTag: 114 return ops, nil 115 } 116 // If the entity is not the environment, add a DocExists check on the 117 // entity document, in order to avoid possible races between entity 118 // removal and annotation creation. 119 coll, id, err := st.tagToCollectionAndId(tag) 120 if err != nil { 121 return nil, errors.Trace(err) 122 } 123 return append(ops, txn.Op{ 124 C: coll, 125 Id: id, 126 Assert: txn.DocExists, 127 }), nil 128 } 129 130 // updateAnnotations returns the operations required to update or remove annotations in MongoDB. 131 func updateAnnotations(st *State, entity GlobalEntity, toUpdate, toRemove bson.M) []txn.Op { 132 return []txn.Op{{ 133 C: annotationsC, 134 Id: st.docID(entity.globalKey()), 135 Assert: txn.DocExists, 136 Update: setUnsetUpdate(toUpdate, toRemove), 137 }} 138 } 139 140 // annotationRemoveOp returns an operation to remove a given annotation 141 // document from MongoDB. 142 func annotationRemoveOp(st *State, id string) txn.Op { 143 return txn.Op{ 144 C: annotationsC, 145 Id: st.docID(id), 146 Remove: true, 147 } 148 }