github.com/axw/juju@v0.0.0-20161005053422-4bd6544d08d4/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  	"gopkg.in/juju/names.v2"
    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  	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 (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[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 := 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  
   113  	switch tag := tag.(type) {
   114  	case names.ModelTag:
   115  		model, err := st.GetModel(tag)
   116  		if err != nil {
   117  			return nil, errors.Annotatef(err, "inserting annotations")
   118  		}
   119  		if model.UUID() == model.ControllerUUID() {
   120  			// This is the controller model, and cannot be removed.
   121  			// Ergo, we can skip the existence check below.
   122  			return ops, nil
   123  		}
   124  	}
   125  	// If the entity is not the controller model, add a DocExists check on the
   126  	// entity document, in order to avoid possible races between entity
   127  	// removal and annotation creation.
   128  	coll, id, err := st.tagToCollectionAndId(tag)
   129  	if err != nil {
   130  		return nil, errors.Trace(err)
   131  	}
   132  	return append(ops, txn.Op{
   133  		C:      coll,
   134  		Id:     id,
   135  		Assert: txn.DocExists,
   136  	}), nil
   137  }
   138  
   139  // updateAnnotations returns the operations required to update or remove annotations in MongoDB.
   140  func updateAnnotations(st *State, entity GlobalEntity, toUpdate, toRemove bson.M) []txn.Op {
   141  	return []txn.Op{{
   142  		C:      annotationsC,
   143  		Id:     st.docID(entity.globalKey()),
   144  		Assert: txn.DocExists,
   145  		Update: setUnsetUpdateAnnotations(toUpdate, toRemove),
   146  	}}
   147  }
   148  
   149  // annotationRemoveOp returns an operation to remove a given annotation
   150  // document from MongoDB.
   151  func annotationRemoveOp(st *State, id string) txn.Op {
   152  	return txn.Op{
   153  		C:      annotationsC,
   154  		Id:     st.docID(id),
   155  		Remove: true,
   156  	}
   157  }
   158  
   159  // setUnsetUpdateAnnotations returns a bson.D for use
   160  // in an annotationsC txn.Op's Update field, containing $set and
   161  // $unset operators if the corresponding operands
   162  // are non-empty.
   163  func setUnsetUpdateAnnotations(set, unset bson.M) bson.D {
   164  	var update bson.D
   165  	replace := inSubdocReplacer("annotations")
   166  	if len(set) > 0 {
   167  		set = bson.M(copyMap(map[string]interface{}(set), replace))
   168  		update = append(update, bson.DocElem{"$set", set})
   169  	}
   170  	if len(unset) > 0 {
   171  		unset = bson.M(copyMap(map[string]interface{}(unset), replace))
   172  		update = append(update, bson.DocElem{"$unset", unset})
   173  	}
   174  	return update
   175  }