github.com/axw/juju@v0.0.0-20161005053422-4bd6544d08d4/state/resources_mongo.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  	"time"
     9  
    10  	"github.com/juju/errors"
    11  	charmresource "gopkg.in/juju/charm.v6-unstable/resource"
    12  	"gopkg.in/mgo.v2/bson"
    13  	"gopkg.in/mgo.v2/txn"
    14  
    15  	"github.com/juju/juju/resource"
    16  )
    17  
    18  const (
    19  	resourcesC = "resources"
    20  
    21  	resourcesStagedIDSuffix     = "#staged"
    22  	resourcesCharmstoreIDSuffix = "#charmstore"
    23  )
    24  
    25  // resourceID converts an external resource ID into an internal one.
    26  func resourceID(id, subType, subID string) string {
    27  	if subType == "" {
    28  		return fmt.Sprintf("resource#%s", id)
    29  	}
    30  	return fmt.Sprintf("resource#%s#%s-%s", id, subType, subID)
    31  }
    32  
    33  func applicationResourceID(id string) string {
    34  	return resourceID(id, "", "")
    35  }
    36  
    37  func pendingResourceID(id, pendingID string) string {
    38  	return resourceID(id, "pending", pendingID)
    39  }
    40  
    41  func charmStoreResourceID(id string) string {
    42  	return applicationResourceID(id) + resourcesCharmstoreIDSuffix
    43  }
    44  
    45  func unitResourceID(id, unitID string) string {
    46  	return resourceID(id, "unit", unitID)
    47  }
    48  
    49  // stagedResourceID converts an external resource ID into an internal
    50  // staged one.
    51  func stagedResourceID(id string) string {
    52  	return applicationResourceID(id) + resourcesStagedIDSuffix
    53  }
    54  
    55  // storedResource holds all model-stored information for a resource.
    56  type storedResource struct {
    57  	resource.Resource
    58  
    59  	// storagePath is the path to where the resource content is stored.
    60  	storagePath string
    61  }
    62  
    63  // charmStoreResource holds the info for a resource as provided by the
    64  // charm store at as specific point in time.
    65  type charmStoreResource struct {
    66  	charmresource.Resource
    67  	id            string
    68  	applicationID string
    69  	lastPolled    time.Time
    70  }
    71  
    72  func newInsertStagedResourceOps(stored storedResource) []txn.Op {
    73  	doc := newStagedResourceDoc(stored)
    74  
    75  	return []txn.Op{{
    76  		C:      resourcesC,
    77  		Id:     doc.DocID,
    78  		Assert: txn.DocMissing,
    79  		Insert: doc,
    80  	}}
    81  }
    82  
    83  func newEnsureStagedResourceSameOps(stored storedResource) []txn.Op {
    84  	doc := newStagedResourceDoc(stored)
    85  
    86  	// Other than cause the txn to abort, we don't do anything here.
    87  	return []txn.Op{{
    88  		C:      resourcesC,
    89  		Id:     doc.DocID,
    90  		Assert: doc, // TODO(ericsnow) Is this okay?
    91  	}}
    92  }
    93  
    94  func newRemoveStagedResourceOps(id string) []txn.Op {
    95  	fullID := stagedResourceID(id)
    96  
    97  	// We don't assert that it exists. We want "missing" to be a noop.
    98  	return []txn.Op{{
    99  		C:      resourcesC,
   100  		Id:     fullID,
   101  		Remove: true,
   102  	}}
   103  }
   104  
   105  func newInsertResourceOps(stored storedResource) []txn.Op {
   106  	doc := newResourceDoc(stored)
   107  
   108  	return []txn.Op{{
   109  		C:      resourcesC,
   110  		Id:     doc.DocID,
   111  		Assert: txn.DocMissing,
   112  		Insert: doc,
   113  	}}
   114  }
   115  
   116  func newUpdateResourceOps(stored storedResource) []txn.Op {
   117  	doc := newResourceDoc(stored)
   118  
   119  	// TODO(ericsnow) Using "update" doesn't work right...
   120  	return append([]txn.Op{{
   121  		C:      resourcesC,
   122  		Id:     doc.DocID,
   123  		Assert: txn.DocExists,
   124  		Remove: true,
   125  	}}, newInsertResourceOps(stored)...)
   126  }
   127  
   128  func newInsertCharmStoreResourceOps(res charmStoreResource) []txn.Op {
   129  	doc := newCharmStoreResourceDoc(res)
   130  
   131  	return []txn.Op{{
   132  		C:      resourcesC,
   133  		Id:     doc.DocID,
   134  		Assert: txn.DocMissing,
   135  		Insert: doc,
   136  	}}
   137  }
   138  
   139  func newUpdateCharmStoreResourceOps(res charmStoreResource) []txn.Op {
   140  	doc := newCharmStoreResourceDoc(res)
   141  
   142  	// TODO(ericsnow) Using "update" doesn't work right...
   143  	return append([]txn.Op{{
   144  		C:      resourcesC,
   145  		Id:     doc.DocID,
   146  		Assert: txn.DocExists,
   147  		Remove: true,
   148  	}}, newInsertCharmStoreResourceOps(res)...)
   149  }
   150  
   151  func newInsertUnitResourceOps(unitID string, stored storedResource, progress *int64) []txn.Op {
   152  	doc := newUnitResourceDoc(unitID, stored)
   153  	doc.DownloadProgress = progress
   154  
   155  	return []txn.Op{{
   156  		C:      resourcesC,
   157  		Id:     doc.DocID,
   158  		Assert: txn.DocMissing,
   159  		Insert: doc,
   160  	}}
   161  }
   162  
   163  func newUpdateUnitResourceOps(unitID string, stored storedResource, progress *int64) []txn.Op {
   164  	doc := newUnitResourceDoc(unitID, stored)
   165  	doc.DownloadProgress = progress
   166  
   167  	// TODO(ericsnow) Using "update" doesn't work right...
   168  	return append([]txn.Op{{
   169  		C:      resourcesC,
   170  		Id:     doc.DocID,
   171  		Assert: txn.DocExists,
   172  		Remove: true,
   173  	}}, newInsertUnitResourceOps(unitID, stored, progress)...)
   174  }
   175  
   176  func newRemoveResourcesOps(docs []resourceDoc) []txn.Op {
   177  	// The likelihood of a race is small and the consequences are minor,
   178  	// so we don't worry about the corner case of missing a doc here.
   179  	var ops []txn.Op
   180  	for _, doc := range docs {
   181  		// We do not bother to assert txn.DocExists since it will be
   182  		// gone either way, which is the result we're after.
   183  		ops = append(ops, txn.Op{
   184  			C:      resourcesC,
   185  			Id:     doc.DocID,
   186  			Remove: true,
   187  		})
   188  	}
   189  	return ops
   190  }
   191  
   192  // newResolvePendingResourceOps generates transaction operations that
   193  // will resolve a pending resource doc and make it active.
   194  //
   195  // We trust that the provided resource really is pending
   196  // and that it matches the existing doc with the same ID.
   197  func newResolvePendingResourceOps(pending storedResource, exists bool) []txn.Op {
   198  	oldID := pendingResourceID(pending.ID, pending.PendingID)
   199  	newRes := pending
   200  	newRes.PendingID = ""
   201  	// TODO(ericsnow) Update newRes.StoragePath? Doing so would require
   202  	// moving the resource in the blobstore to the correct path, which
   203  	// we cannot do in the transaction...
   204  	ops := []txn.Op{{
   205  		C:      resourcesC,
   206  		Id:     oldID,
   207  		Assert: txn.DocExists,
   208  		Remove: true,
   209  	}}
   210  
   211  	// TODO(perrito666) 2016-05-02 lp:1558657
   212  	csRes := charmStoreResource{
   213  		Resource:      newRes.Resource.Resource,
   214  		id:            newRes.ID,
   215  		applicationID: newRes.ApplicationID,
   216  		lastPolled:    time.Now().UTC(),
   217  	}
   218  
   219  	if exists {
   220  		ops = append(ops, newUpdateResourceOps(newRes)...)
   221  		return append(ops, newUpdateCharmStoreResourceOps(csRes)...)
   222  	} else {
   223  		ops = append(ops, newInsertResourceOps(newRes)...)
   224  		return append(ops, newInsertCharmStoreResourceOps(csRes)...)
   225  	}
   226  }
   227  
   228  // newCharmStoreResourceDoc generates a doc that represents the given resource.
   229  func newCharmStoreResourceDoc(res charmStoreResource) *resourceDoc {
   230  	fullID := charmStoreResourceID(res.id)
   231  	return charmStoreResource2Doc(fullID, res)
   232  }
   233  
   234  // newUnitResourceDoc generates a doc that represents the given resource.
   235  func newUnitResourceDoc(unitID string, stored storedResource) *resourceDoc {
   236  	fullID := unitResourceID(stored.ID, unitID)
   237  	return unitResource2Doc(fullID, unitID, stored)
   238  }
   239  
   240  // newResourceDoc generates a doc that represents the given resource.
   241  func newResourceDoc(stored storedResource) *resourceDoc {
   242  	fullID := applicationResourceID(stored.ID)
   243  	if stored.PendingID != "" {
   244  		fullID = pendingResourceID(stored.ID, stored.PendingID)
   245  	}
   246  	return resource2doc(fullID, stored)
   247  }
   248  
   249  // newStagedResourceDoc generates a staging doc that represents
   250  // the given resource.
   251  func newStagedResourceDoc(stored storedResource) *resourceDoc {
   252  	stagedID := stagedResourceID(stored.ID)
   253  	return resource2doc(stagedID, stored)
   254  }
   255  
   256  // resources returns the resource docs for the given application.
   257  func (p ResourcePersistence) resources(applicationID string) ([]resourceDoc, error) {
   258  	logger.Tracef("querying db for resources for %q", applicationID)
   259  	var docs []resourceDoc
   260  	query := bson.D{{"application-id", applicationID}}
   261  	if err := p.base.All(resourcesC, query, &docs); err != nil {
   262  		return nil, errors.Trace(err)
   263  	}
   264  	logger.Tracef("found %d resources", len(docs))
   265  	return docs, nil
   266  }
   267  
   268  func (p ResourcePersistence) unitResources(unitID string) ([]resourceDoc, error) {
   269  	var docs []resourceDoc
   270  	query := bson.D{{"unit-id", unitID}}
   271  	if err := p.base.All(resourcesC, query, &docs); err != nil {
   272  		return nil, errors.Trace(err)
   273  	}
   274  	return docs, nil
   275  }
   276  
   277  // getOne returns the resource that matches the provided model ID.
   278  func (p ResourcePersistence) getOne(resID string) (resourceDoc, error) {
   279  	logger.Tracef("querying db for resource %q", resID)
   280  	id := applicationResourceID(resID)
   281  	var doc resourceDoc
   282  	if err := p.base.One(resourcesC, id, &doc); err != nil {
   283  		return doc, errors.Trace(err)
   284  	}
   285  	return doc, nil
   286  }
   287  
   288  // getOnePending returns the resource that matches the provided model ID.
   289  func (p ResourcePersistence) getOnePending(resID, pendingID string) (resourceDoc, error) {
   290  	logger.Tracef("querying db for resource %q (pending %q)", resID, pendingID)
   291  	id := pendingResourceID(resID, pendingID)
   292  	var doc resourceDoc
   293  	if err := p.base.One(resourcesC, id, &doc); err != nil {
   294  		return doc, errors.Trace(err)
   295  	}
   296  	return doc, nil
   297  }
   298  
   299  // resourceDoc is the top-level document for resources.
   300  type resourceDoc struct {
   301  	DocID     string `bson:"_id"`
   302  	ID        string `bson:"resource-id"`
   303  	PendingID string `bson:"pending-id"`
   304  
   305  	ApplicationID string `bson:"application-id"`
   306  	UnitID        string `bson:"unit-id"`
   307  
   308  	Name        string `bson:"name"`
   309  	Type        string `bson:"type"`
   310  	Path        string `bson:"path"`
   311  	Description string `bson:"description"`
   312  
   313  	Origin      string `bson:"origin"`
   314  	Revision    int    `bson:"revision"`
   315  	Fingerprint []byte `bson:"fingerprint"`
   316  	Size        int64  `bson:"size"`
   317  
   318  	Username  string    `bson:"username"`
   319  	Timestamp time.Time `bson:"timestamp-when-added"`
   320  
   321  	StoragePath string `bson:"storage-path"`
   322  
   323  	DownloadProgress *int64 `bson:"download-progress,omitempty"`
   324  
   325  	LastPolled time.Time `bson:"timestamp-when-last-polled"`
   326  }
   327  
   328  func charmStoreResource2Doc(id string, res charmStoreResource) *resourceDoc {
   329  	stored := storedResource{
   330  		Resource: resource.Resource{
   331  			Resource:      res.Resource,
   332  			ID:            res.id,
   333  			ApplicationID: res.applicationID,
   334  		},
   335  	}
   336  	doc := resource2doc(id, stored)
   337  	doc.LastPolled = res.lastPolled
   338  	return doc
   339  }
   340  
   341  func unitResource2Doc(id, unitID string, stored storedResource) *resourceDoc {
   342  	doc := resource2doc(id, stored)
   343  	doc.UnitID = unitID
   344  	return doc
   345  }
   346  
   347  // resource2doc converts the resource into a DB doc.
   348  func resource2doc(id string, stored storedResource) *resourceDoc {
   349  	res := stored.Resource
   350  	// TODO(ericsnow) We may need to limit the resolution of timestamps
   351  	// in order to avoid some conversion problems from Mongo.
   352  	return &resourceDoc{
   353  		DocID:     id,
   354  		ID:        res.ID,
   355  		PendingID: res.PendingID,
   356  
   357  		ApplicationID: res.ApplicationID,
   358  
   359  		Name:        res.Name,
   360  		Type:        res.Type.String(),
   361  		Path:        res.Path,
   362  		Description: res.Description,
   363  
   364  		Origin:      res.Origin.String(),
   365  		Revision:    res.Revision,
   366  		Fingerprint: res.Fingerprint.Bytes(),
   367  		Size:        res.Size,
   368  
   369  		Username:  res.Username,
   370  		Timestamp: res.Timestamp,
   371  
   372  		StoragePath: stored.storagePath,
   373  	}
   374  }
   375  
   376  // doc2resource returns the resource info represented by the doc.
   377  func doc2resource(doc resourceDoc) (storedResource, error) {
   378  	res, err := doc2basicResource(doc)
   379  	if err != nil {
   380  		return storedResource{}, errors.Trace(err)
   381  	}
   382  
   383  	stored := storedResource{
   384  		Resource:    res,
   385  		storagePath: doc.StoragePath,
   386  	}
   387  	return stored, nil
   388  }
   389  
   390  // doc2basicResource returns the resource info represented by the doc.
   391  func doc2basicResource(doc resourceDoc) (resource.Resource, error) {
   392  	var res resource.Resource
   393  
   394  	resType, err := charmresource.ParseType(doc.Type)
   395  	if err != nil {
   396  		return res, errors.Annotate(err, "got invalid data from DB")
   397  	}
   398  
   399  	origin, err := charmresource.ParseOrigin(doc.Origin)
   400  	if err != nil {
   401  		return res, errors.Annotate(err, "got invalid data from DB")
   402  	}
   403  
   404  	fp, err := resource.DeserializeFingerprint(doc.Fingerprint)
   405  	if err != nil {
   406  		return res, errors.Annotate(err, "got invalid data from DB")
   407  	}
   408  
   409  	res = resource.Resource{
   410  		Resource: charmresource.Resource{
   411  			Meta: charmresource.Meta{
   412  				Name:        doc.Name,
   413  				Type:        resType,
   414  				Path:        doc.Path,
   415  				Description: doc.Description,
   416  			},
   417  			Origin:      origin,
   418  			Revision:    doc.Revision,
   419  			Fingerprint: fp,
   420  			Size:        doc.Size,
   421  		},
   422  		ID:            doc.ID,
   423  		PendingID:     doc.PendingID,
   424  		ApplicationID: doc.ApplicationID,
   425  		Username:      doc.Username,
   426  		Timestamp:     doc.Timestamp,
   427  	}
   428  	if err := res.Validate(); err != nil {
   429  		return res, errors.Annotate(err, "got invalid data from DB")
   430  	}
   431  	return res, nil
   432  }