github.com/makyo/juju@v0.0.0-20160425123129-2608902037e9/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 serviceResourceID(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 serviceResourceID(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 serviceResourceID(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 serviceID 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 csRes := charmStoreResource{ 212 Resource: newRes.Resource.Resource, 213 id: newRes.ID, 214 serviceID: newRes.ServiceID, 215 lastPolled: time.Now().UTC(), 216 } 217 218 if exists { 219 ops = append(ops, newUpdateResourceOps(newRes)...) 220 return append(ops, newUpdateCharmStoreResourceOps(csRes)...) 221 } else { 222 ops = append(ops, newInsertResourceOps(newRes)...) 223 return append(ops, newInsertCharmStoreResourceOps(csRes)...) 224 } 225 } 226 227 // newCharmStoreResourceDoc generates a doc that represents the given resource. 228 func newCharmStoreResourceDoc(res charmStoreResource) *resourceDoc { 229 fullID := charmStoreResourceID(res.id) 230 return charmStoreResource2Doc(fullID, res) 231 } 232 233 // newUnitResourceDoc generates a doc that represents the given resource. 234 func newUnitResourceDoc(unitID string, stored storedResource) *resourceDoc { 235 fullID := unitResourceID(stored.ID, unitID) 236 return unitResource2Doc(fullID, unitID, stored) 237 } 238 239 // newResourceDoc generates a doc that represents the given resource. 240 func newResourceDoc(stored storedResource) *resourceDoc { 241 fullID := serviceResourceID(stored.ID) 242 if stored.PendingID != "" { 243 fullID = pendingResourceID(stored.ID, stored.PendingID) 244 } 245 return resource2doc(fullID, stored) 246 } 247 248 // newStagedResourceDoc generates a staging doc that represents 249 // the given resource. 250 func newStagedResourceDoc(stored storedResource) *resourceDoc { 251 stagedID := stagedResourceID(stored.ID) 252 return resource2doc(stagedID, stored) 253 } 254 255 // resources returns the resource docs for the given service. 256 func (p ResourcePersistence) resources(serviceID string) ([]resourceDoc, error) { 257 logger.Tracef("querying db for resources for %q", serviceID) 258 var docs []resourceDoc 259 query := bson.D{{"service-id", serviceID}} 260 if err := p.base.All(resourcesC, query, &docs); err != nil { 261 return nil, errors.Trace(err) 262 } 263 logger.Tracef("found %d resources", len(docs)) 264 return docs, nil 265 } 266 267 func (p ResourcePersistence) unitResources(unitID string) ([]resourceDoc, error) { 268 var docs []resourceDoc 269 query := bson.D{{"unit-id", unitID}} 270 if err := p.base.All(resourcesC, query, &docs); err != nil { 271 return nil, errors.Trace(err) 272 } 273 return docs, nil 274 } 275 276 // getOne returns the resource that matches the provided model ID. 277 func (p ResourcePersistence) getOne(resID string) (resourceDoc, error) { 278 logger.Tracef("querying db for resource %q", resID) 279 id := serviceResourceID(resID) 280 var doc resourceDoc 281 if err := p.base.One(resourcesC, id, &doc); err != nil { 282 return doc, errors.Trace(err) 283 } 284 return doc, nil 285 } 286 287 // getOnePending returns the resource that matches the provided model ID. 288 func (p ResourcePersistence) getOnePending(resID, pendingID string) (resourceDoc, error) { 289 logger.Tracef("querying db for resource %q (pending %q)", resID, pendingID) 290 id := pendingResourceID(resID, pendingID) 291 var doc resourceDoc 292 if err := p.base.One(resourcesC, id, &doc); err != nil { 293 return doc, errors.Trace(err) 294 } 295 return doc, nil 296 } 297 298 // resourceDoc is the top-level document for resources. 299 type resourceDoc struct { 300 DocID string `bson:"_id"` 301 EnvUUID string `bson:"env-uuid"` 302 ID string `bson:"resource-id"` 303 PendingID string `bson:"pending-id"` 304 305 ServiceID string `bson:"service-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 ServiceID: res.serviceID, 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 ServiceID: res.ServiceID, 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 ServiceID: doc.ServiceID, 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 }