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 }