github.com/juju/juju@v0.0.0-20240430160146-1752b71fcf00/state/resources.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 "bytes" 8 "fmt" 9 "io" 10 "path" 11 "strings" 12 "time" 13 14 charmresource "github.com/juju/charm/v12/resource" 15 "github.com/juju/clock" 16 "github.com/juju/collections/set" 17 "github.com/juju/errors" 18 "github.com/juju/mgo/v3" 19 "github.com/juju/mgo/v3/bson" 20 "github.com/juju/mgo/v3/txn" 21 "github.com/juju/names/v5" 22 "github.com/juju/utils/v3" 23 "github.com/kr/pretty" 24 25 "github.com/juju/juju/core/resources" 26 "github.com/juju/juju/state/storage" 27 ) 28 29 // Resources describes the state functionality for resources. 30 type Resources interface { 31 // ListResources returns the list of resources for the given application. 32 ListResources(applicationID string) (resources.ApplicationResources, error) 33 34 // ListPendingResources returns the list of pending resources for 35 // the given application. 36 ListPendingResources(applicationID string) (resources.ApplicationResources, error) 37 38 // AddPendingResource adds the resource to the data store in a 39 // "pending" state. It will stay pending (and unavailable) until 40 // it is resolved. The returned ID is used to identify the pending 41 // resources when resolving it. 42 AddPendingResource(applicationID, userID string, chRes charmresource.Resource) (string, error) 43 44 // GetResource returns the identified resource. 45 GetResource(applicationID, name string) (resources.Resource, error) 46 47 // GetPendingResource returns the identified resource. 48 GetPendingResource(applicationID, name, pendingID string) (resources.Resource, error) 49 50 // SetResource adds the resource to blob storage and updates the metadata. 51 SetResource(applicationID, userID string, res charmresource.Resource, r io.Reader, _ IncrementCharmModifiedVersionType) (resources.Resource, error) 52 53 // SetUnitResource sets the resource metadata for a specific unit. 54 SetUnitResource(unitName, userID string, res charmresource.Resource) (resources.Resource, error) 55 56 // UpdatePendingResource adds the resource to blob storage and updates the metadata. 57 UpdatePendingResource(applicationID, pendingID, userID string, res charmresource.Resource, r io.Reader) (resources.Resource, error) 58 59 // OpenResource returns the metadata for a resource and a reader for the resource. 60 OpenResource(applicationID, name string) (resources.Resource, io.ReadCloser, error) 61 62 // OpenResourceForUniter returns the metadata for a resource and a reader for the resource. 63 OpenResourceForUniter(unitName, name string) (resources.Resource, io.ReadCloser, error) 64 65 // SetCharmStoreResources sets the "polled" resources for the 66 // application to the provided values. 67 SetCharmStoreResources(applicationID string, info []charmresource.Resource, lastPolled time.Time) error 68 69 // RemovePendingAppResources removes any pending application-level 70 // resources for the named application. This is used to clean up 71 // resources for a failed application deployment. 72 RemovePendingAppResources(applicationID string, pendingIDs map[string]string) error 73 } 74 75 // Resources returns the resources functionality for the current state. 76 func (st *State) Resources() Resources { 77 return st.resources() 78 } 79 80 // Resources returns the resources functionality for the current state. 81 func (st *State) resources() *resourcePersistence { 82 return &resourcePersistence{ 83 st: st, 84 storage: storage.NewStorage(st.ModelUUID(), st.MongoSession()), 85 dockerMetadataStorage: NewDockerMetadataStorage(st), 86 } 87 } 88 89 var rLogger = logger.Child("resource") 90 91 const ( 92 resourcesStagedIDSuffix = "#staged" 93 resourcesCharmstoreIDSuffix = "#charmstore" 94 ) 95 96 // A change in CharmModifiedVersion triggers the uniter to run the upgrade_charm 97 // hook (and config hook). Increment required for a running unit to pick up 98 // new resources from `attach` or when a charm is upgraded without a new charm 99 // revision. 100 // 101 // IncrementCharmModifiedVersionType is the argument type for incrementing CharmModifiedVersion or not. 102 type IncrementCharmModifiedVersionType bool 103 104 const ( 105 // IncrementCharmModifiedVersion means CharmModifiedVersion needs to be incremented. 106 IncrementCharmModifiedVersion IncrementCharmModifiedVersionType = true 107 // DoNotIncrementCharmModifiedVersion means CharmModifiedVersion should not be incremented. 108 DoNotIncrementCharmModifiedVersion IncrementCharmModifiedVersionType = false 109 ) 110 111 // resourceID converts an external resource ID into an internal one. 112 func resourceID(id, subType, subID string) string { 113 if subType == "" { 114 return fmt.Sprintf("resource#%s", id) 115 } 116 return fmt.Sprintf("resource#%s#%s-%s", id, subType, subID) 117 } 118 119 func applicationResourceID(id string) string { 120 return resourceID(id, "", "") 121 } 122 123 func pendingResourceID(id, pendingID string) string { 124 return resourceID(id, "pending", pendingID) 125 } 126 127 func charmStoreResourceID(id string) string { 128 return applicationResourceID(id) + resourcesCharmstoreIDSuffix 129 } 130 131 func unitResourceID(id, unitID string) string { 132 return resourceID(id, "unit", unitID) 133 } 134 135 // stagedResourceID converts an external resource ID into an internal 136 // staged one. 137 func stagedResourceID(id string) string { 138 return applicationResourceID(id) + resourcesStagedIDSuffix 139 } 140 141 // newPendingID generates a new unique identifier for a resource. 142 func newPendingID() (string, error) { 143 uuid, err := utils.NewUUID() 144 if err != nil { 145 return "", errors.Annotate(err, "could not create new resource ID") 146 } 147 return uuid.String(), nil 148 } 149 150 // newAppResourceID produces a new ID to use for the resource in the model. 151 func newAppResourceID(applicationID, name string) string { 152 return fmt.Sprintf("%s/%s", applicationID, name) 153 } 154 155 // resourceDoc is the top-level document for resources. 156 type resourceDoc struct { 157 DocID string `bson:"_id"` 158 ID string `bson:"resource-id"` 159 PendingID string `bson:"pending-id"` 160 161 ApplicationID string `bson:"application-id"` 162 UnitID string `bson:"unit-id"` 163 164 Name string `bson:"name"` 165 Type string `bson:"type"` 166 Path string `bson:"path"` 167 Description string `bson:"description"` 168 169 Origin string `bson:"origin"` 170 Revision int `bson:"revision"` 171 Fingerprint []byte `bson:"fingerprint"` 172 Size int64 `bson:"size"` 173 174 Username string `bson:"username"` 175 Timestamp time.Time `bson:"timestamp-when-added"` 176 177 StoragePath string `bson:"storage-path"` 178 179 DownloadProgress *int64 `bson:"download-progress,omitempty"` 180 181 LastPolled time.Time `bson:"timestamp-when-last-polled"` 182 } 183 184 // storedResource holds all model-stored information for a resource. 185 type storedResource struct { 186 resources.Resource 187 188 // storagePath is the path to where the resource content is stored. 189 storagePath string 190 } 191 192 // charmStoreResource holds the info for a resource as provided by the 193 // charm store at as specific point in time. 194 type charmStoreResource struct { 195 charmresource.Resource 196 id string 197 applicationID string 198 lastPolled time.Time 199 } 200 201 func charmStoreResource2Doc(id string, res charmStoreResource) *resourceDoc { 202 stored := storedResource{ 203 Resource: resources.Resource{ 204 Resource: res.Resource, 205 ID: res.id, 206 ApplicationID: res.applicationID, 207 }, 208 } 209 doc := resource2doc(id, stored) 210 doc.LastPolled = res.lastPolled 211 return doc 212 } 213 214 // newCharmStoreResourceDoc generates a doc that represents the given resource. 215 func newCharmStoreResourceDoc(res charmStoreResource) *resourceDoc { 216 fullID := charmStoreResourceID(res.id) 217 return charmStoreResource2Doc(fullID, res) 218 } 219 220 // newUnitResourceDoc generates a doc that represents the given resource. 221 func newUnitResourceDoc(unitID string, stored storedResource) *resourceDoc { 222 fullID := unitResourceID(stored.ID, unitID) 223 return unitResource2Doc(fullID, unitID, stored) 224 } 225 226 // newResourceDoc generates a doc that represents the given resource. 227 func newResourceDoc(stored storedResource) *resourceDoc { 228 fullID := applicationResourceID(stored.ID) 229 if stored.PendingID != "" { 230 fullID = pendingResourceID(stored.ID, stored.PendingID) 231 } 232 return resource2doc(fullID, stored) 233 } 234 235 // newStagedResourceDoc generates a staging doc that represents 236 // the given resource. 237 func newStagedResourceDoc(stored storedResource) *resourceDoc { 238 stagedID := stagedResourceID(stored.ID) 239 return resource2doc(stagedID, stored) 240 } 241 242 func unitResource2Doc(id, unitID string, stored storedResource) *resourceDoc { 243 doc := resource2doc(id, stored) 244 doc.UnitID = unitID 245 return doc 246 } 247 248 // resource2doc converts the resource into a DB doc. 249 func resource2doc(id string, stored storedResource) *resourceDoc { 250 res := stored.Resource 251 // TODO(ericsnow) We may need to limit the resolution of timestamps 252 // in order to avoid some conversion problems from Mongo. 253 return &resourceDoc{ 254 DocID: id, 255 ID: res.ID, 256 PendingID: res.PendingID, 257 258 ApplicationID: res.ApplicationID, 259 260 Name: res.Name, 261 Type: res.Type.String(), 262 Path: res.Path, 263 Description: res.Description, 264 265 Origin: res.Origin.String(), 266 Revision: res.Revision, 267 Fingerprint: res.Fingerprint.Bytes(), 268 Size: res.Size, 269 270 Username: res.Username, 271 Timestamp: res.Timestamp, 272 273 StoragePath: stored.storagePath, 274 } 275 } 276 277 // doc2resource returns the resource info represented by the doc. 278 func doc2resource(doc resourceDoc) (storedResource, error) { 279 res, err := doc2basicResource(doc) 280 if err != nil { 281 return storedResource{}, errors.Trace(err) 282 } 283 284 stored := storedResource{ 285 Resource: res, 286 storagePath: doc.StoragePath, 287 } 288 return stored, nil 289 } 290 291 // doc2basicResource returns the resource info represented by the doc. 292 func doc2basicResource(doc resourceDoc) (resources.Resource, error) { 293 var res resources.Resource 294 295 resType, err := charmresource.ParseType(doc.Type) 296 if err != nil { 297 return res, errors.Annotate(err, "got invalid data from DB") 298 } 299 300 origin, err := charmresource.ParseOrigin(doc.Origin) 301 if err != nil { 302 return res, errors.Annotate(err, "got invalid data from DB") 303 } 304 305 fp, err := resources.DeserializeFingerprint(doc.Fingerprint) 306 if err != nil { 307 return res, errors.Annotate(err, "got invalid data from DB") 308 } 309 310 res = resources.Resource{ 311 Resource: charmresource.Resource{ 312 Meta: charmresource.Meta{ 313 Name: doc.Name, 314 Type: resType, 315 Path: doc.Path, 316 Description: doc.Description, 317 }, 318 Origin: origin, 319 Revision: doc.Revision, 320 Fingerprint: fp, 321 Size: doc.Size, 322 }, 323 ID: doc.ID, 324 PendingID: doc.PendingID, 325 ApplicationID: doc.ApplicationID, 326 Username: doc.Username, 327 Timestamp: doc.Timestamp, 328 } 329 if err := res.Validate(); err != nil { 330 return res, errors.Annotate(err, "got invalid data from DB") 331 } 332 return res, nil 333 } 334 335 // storagePath returns the path used as the location where the resource 336 // is stored in state storage. This requires that the returned string 337 // be unique and that it be organized in a structured way. In this case 338 // we start with a top-level (the application), then under that application use 339 // the "resources" section. The provided ID is located under there. 340 func storagePath(name, applicationID, pendingID string) string { 341 // TODO(ericsnow) Use applications/<application>/resources/<resource>? 342 id := name 343 if pendingID != "" { 344 // TODO(ericsnow) How to resolve this later? 345 id += "-" + pendingID 346 } 347 return path.Join("application-"+applicationID, "resources", id) 348 } 349 350 // resourcePersistence provides the persistence 351 // functionality for resources. 352 type resourcePersistence struct { 353 st *State 354 storage storage.Storage 355 dockerMetadataStorage DockerMetadataStorage 356 } 357 358 // One gets the identified document from the collection. 359 func (sp *resourcePersistence) one(collName, id string, doc interface{}) error { 360 coll, closeColl := sp.st.db().GetCollection(collName) 361 defer closeColl() 362 363 err := coll.FindId(id).One(doc) 364 if err == mgo.ErrNotFound { 365 return errors.NotFoundf(id) 366 } 367 if err != nil { 368 return errors.Trace(err) 369 } 370 return nil 371 } 372 373 // All gets all documents from the collection matching the query. 374 func (p *resourcePersistence) all(collName string, query, docs interface{}) error { 375 coll, closeColl := p.st.db().GetCollection(collName) 376 defer closeColl() 377 378 if err := coll.Find(query).All(docs); err != nil { 379 return errors.Trace(err) 380 } 381 return nil 382 } 383 384 // resources returns the resource docs for the given application. 385 func (p *resourcePersistence) resources(applicationID string) ([]resourceDoc, error) { 386 rLogger.Tracef("querying db for resources for %q", applicationID) 387 var docs []resourceDoc 388 query := bson.D{{"application-id", applicationID}} 389 if err := p.all(resourcesC, query, &docs); err != nil { 390 return nil, errors.Trace(err) 391 } 392 rLogger.Tracef("found %d resources", len(docs)) 393 return docs, nil 394 } 395 396 func (p *resourcePersistence) unitResources(unitID string) ([]resourceDoc, error) { 397 var docs []resourceDoc 398 query := bson.D{{"unit-id", unitID}} 399 if err := p.all(resourcesC, query, &docs); err != nil { 400 return nil, errors.Trace(err) 401 } 402 return docs, nil 403 } 404 405 // getOne returns the resource that matches the provided model ID. 406 func (p *resourcePersistence) getOne(resID string) (resourceDoc, error) { 407 id := applicationResourceID(resID) 408 rLogger.Tracef("querying db for resource %q as %q", resID, id) 409 var doc resourceDoc 410 if err := p.one(resourcesC, id, &doc); err != nil { 411 return doc, errors.Trace(err) 412 } 413 return doc, nil 414 } 415 416 // getOnePending returns the resource that matches the provided model ID. 417 func (p *resourcePersistence) getOnePending(resID, pendingID string) (resourceDoc, error) { 418 id := pendingResourceID(resID, pendingID) 419 rLogger.Tracef("querying db for resource %q (pending %q) as %q", resID, pendingID, id) 420 var doc resourceDoc 421 if err := p.one(resourcesC, id, &doc); err != nil { 422 return doc, errors.Trace(err) 423 } 424 return doc, nil 425 } 426 427 func (p *resourcePersistence) verifyApplication(id string) error { 428 app, err := p.st.Application(id) 429 if err != nil { 430 return errors.Trace(err) 431 } 432 if app.Life() != Alive { 433 return errors.NewNotFound(nil, fmt.Sprintf("application %q dying or dead", id)) 434 } 435 return nil 436 } 437 438 // listResources returns the info for each non-pending resource of the 439 // identified application. 440 func (p *resourcePersistence) listResources(applicationID string, pending bool) (resources.ApplicationResources, error) { 441 rLogger.Tracef("listing all resources for application %q, pending=%v", applicationID, pending) 442 443 docs, err := p.resources(applicationID) 444 if err != nil { 445 return resources.ApplicationResources{}, errors.Trace(err) 446 } 447 448 store := map[string]charmresource.Resource{} 449 units := map[names.UnitTag][]resources.Resource{} 450 downloadProgress := make(map[names.UnitTag]map[string]int64) 451 452 var results resources.ApplicationResources 453 for _, doc := range docs { 454 if !pending && doc.PendingID != "" { 455 continue 456 } 457 if pending && doc.PendingID == "" { 458 continue 459 } 460 461 res, err := doc2basicResource(doc) 462 if err != nil { 463 return resources.ApplicationResources{}, errors.Trace(err) 464 } 465 if strings.HasSuffix(doc.DocID, resourcesCharmstoreIDSuffix) { 466 store[res.Name] = res.Resource 467 continue 468 } 469 if doc.UnitID == "" { 470 results.Resources = append(results.Resources, res) 471 continue 472 } 473 tag := names.NewUnitTag(doc.UnitID) 474 units[tag] = append(units[tag], res) 475 if doc.DownloadProgress != nil { 476 if downloadProgress[tag] == nil { 477 downloadProgress[tag] = make(map[string]int64) 478 } 479 downloadProgress[tag][doc.Name] = *doc.DownloadProgress 480 } 481 } 482 for _, res := range results.Resources { 483 storeRes, ok := store[res.Name] 484 if ok { 485 results.CharmStoreResources = append(results.CharmStoreResources, storeRes) 486 } 487 } 488 for tag, res := range units { 489 results.UnitResources = append(results.UnitResources, resources.UnitResources{ 490 Tag: tag, 491 Resources: res, 492 DownloadProgress: downloadProgress[tag], 493 }) 494 } 495 if rLogger.IsTraceEnabled() { 496 rLogger.Tracef("found %d docs: %q", len(docs), pretty.Sprint(results)) 497 } 498 return results, nil 499 } 500 501 // ListPendingResources returns the extended, model-related info for 502 // each pending resource of the identifies application. 503 func (p *resourcePersistence) ListPendingResources(applicationID string) (resources.ApplicationResources, error) { 504 rLogger.Tracef("listing all pending resources for application %q", applicationID) 505 res, err := p.listResources(applicationID, true) 506 if err != nil { 507 if err := p.verifyApplication(applicationID); err != nil { 508 return resources.ApplicationResources{}, errors.Trace(err) 509 } 510 return resources.ApplicationResources{}, errors.Trace(err) 511 } 512 return res, nil 513 } 514 515 // ListResources returns the non pending resource data for the given application ID. 516 func (p *resourcePersistence) ListResources(applicationID string) (resources.ApplicationResources, error) { 517 res, err := p.listResources(applicationID, false) 518 if err != nil { 519 if err := p.verifyApplication(applicationID); err != nil { 520 return resources.ApplicationResources{}, errors.Trace(err) 521 } 522 return resources.ApplicationResources{}, errors.Trace(err) 523 } 524 525 units, err := allUnits(p.st, applicationID) 526 if err != nil { 527 return resources.ApplicationResources{}, errors.Trace(err) 528 } 529 for _, u := range units { 530 found := false 531 for _, unitRes := range res.UnitResources { 532 if u.Tag().String() == unitRes.Tag.String() { 533 found = true 534 break 535 } 536 } 537 if !found { 538 unitRes := resources.UnitResources{ 539 Tag: u.UnitTag(), 540 } 541 res.UnitResources = append(res.UnitResources, unitRes) 542 } 543 } 544 545 return res, nil 546 } 547 548 // GetResource returns the resource data for the identified resource. 549 func (p *resourcePersistence) GetResource(applicationID, name string) (resources.Resource, error) { 550 id := newAppResourceID(applicationID, name) 551 res, _, err := p.getResource(id) 552 if err != nil { 553 if err := p.verifyApplication(applicationID); err != nil { 554 return resources.Resource{}, errors.Trace(err) 555 } 556 return res, errors.Trace(err) 557 } 558 return res, nil 559 } 560 561 // GetPendingResource returns the resource data for the identified resource. 562 func (p *resourcePersistence) GetPendingResource(applicationID, name, pendingID string) (resources.Resource, error) { 563 var res resources.Resource 564 565 resources, err := p.ListPendingResources(applicationID) 566 if err != nil { 567 // We do not call VerifyApplication() here because pending resources 568 // do not have to have an existing application. 569 return res, errors.Trace(err) 570 } 571 572 for _, res := range resources.Resources { 573 if res.Name == name && res.PendingID == pendingID { 574 return res, nil 575 } 576 } 577 return res, errors.NotFoundf("pending resource %q (%s)", name, pendingID) 578 } 579 580 // getResource returns the extended, model-related info for the non-pending 581 // resource. 582 func (p *resourcePersistence) getResource(id string) (res resources.Resource, storagePath string, _ error) { 583 rLogger.Tracef("get resource %q", id) 584 doc, err := p.getOne(id) 585 if err != nil { 586 return res, "", errors.Trace(err) 587 } 588 589 stored, err := doc2resource(doc) 590 if err != nil { 591 return res, "", errors.Trace(err) 592 } 593 594 return stored.Resource, stored.storagePath, nil 595 } 596 597 // SetResource stores the resource in the Juju model. 598 func (p *resourcePersistence) SetResource( 599 applicationID, userID string, 600 chRes charmresource.Resource, 601 r io.Reader, incrementCharmModifiedVersion IncrementCharmModifiedVersionType, 602 ) (resources.Resource, error) { 603 rLogger.Tracef("adding resource %q for application %q", chRes.Name, applicationID) 604 pendingID := "" 605 res, err := p.setResource(pendingID, applicationID, userID, chRes, r, incrementCharmModifiedVersion) 606 if err != nil { 607 return res, errors.Trace(err) 608 } 609 return res, nil 610 } 611 612 // SetUnitResource stores the resource info for a particular unit. The 613 // resource must already be set for the application. 614 func (p *resourcePersistence) SetUnitResource(unitName, userID string, chRes charmresource.Resource) (_ resources.Resource, outErr error) { 615 rLogger.Tracef("adding resource %q for unit %q", chRes.Name, unitName) 616 var empty resources.Resource 617 618 applicationID, err := names.UnitApplication(unitName) 619 if err != nil { 620 return empty, errors.Trace(err) 621 } 622 623 res := resources.Resource{ 624 Resource: chRes, 625 ID: newAppResourceID(applicationID, chRes.Name), 626 ApplicationID: applicationID, 627 } 628 res.Username = userID 629 res.Timestamp = p.st.clock().Now().UTC() 630 if err := res.Validate(); err != nil { 631 return empty, errors.Annotate(err, "bad resource metadata") 632 } 633 634 if err := p.setUnitResourceProgress(unitName, res, nil); err != nil { 635 return empty, errors.Trace(err) 636 } 637 638 return res, nil 639 } 640 641 // SetCharmStoreResources sets the "polled" resources for the 642 // application to the provided values. 643 func (p *resourcePersistence) SetCharmStoreResources(applicationID string, info []charmresource.Resource, lastPolled time.Time) error { 644 for _, chRes := range info { 645 id := newAppResourceID(applicationID, chRes.Name) 646 if err := p.setCharmStoreResource(id, applicationID, chRes, lastPolled); err != nil { 647 return errors.Trace(err) 648 } 649 // TODO(ericsnow) Worry about extras? missing? 650 } 651 652 return nil 653 } 654 655 // setCharmStoreResource stores the resource info that was retrieved 656 // from the charm store. 657 func (p *resourcePersistence) setCharmStoreResource(id, applicationID string, res charmresource.Resource, lastPolled time.Time) error { 658 rLogger.Tracef("set charmstore %q resource %q", applicationID, res.Name) 659 if err := res.Validate(); err != nil { 660 return errors.Annotate(err, "bad resource") 661 } 662 if lastPolled.IsZero() { 663 return errors.NotValidf("empty last polled timestamp for charm resource %s/%s", applicationID, id) 664 } 665 666 csRes := charmStoreResource{ 667 Resource: res, 668 id: id, 669 applicationID: applicationID, 670 lastPolled: lastPolled, 671 } 672 673 buildTxn := func(attempt int) ([]txn.Op, error) { 674 // This is an "upsert". 675 var ops []txn.Op 676 switch attempt { 677 case 0: 678 ops = newInsertCharmStoreResourceOps(csRes) 679 case 1: 680 ops = newUpdateCharmStoreResourceOps(csRes) 681 default: 682 // Either insert or update will work so we should not get here. 683 return nil, errors.New("setting the resource failed") 684 } 685 // No pending resources so we always do this here. 686 ops = append(ops, applicationExistsOps(applicationID)...) 687 return ops, nil 688 } 689 if err := p.st.db().Run(buildTxn); err != nil { 690 return errors.Trace(err) 691 } 692 return nil 693 } 694 695 // AddPendingResource stores the resource in the Juju model. 696 func (st resourcePersistence) AddPendingResource(applicationID, userID string, chRes charmresource.Resource) (pendingID string, err error) { 697 pendingID, err = newPendingID() 698 if err != nil { 699 return "", errors.Annotate(err, "could not generate resource ID") 700 } 701 rLogger.Debugf("adding pending resource %q for application %q (ID: %s)", chRes.Name, applicationID, pendingID) 702 703 if _, err := st.setResource(pendingID, applicationID, userID, chRes, nil, IncrementCharmModifiedVersion); err != nil { 704 return "", errors.Trace(err) 705 } 706 707 return pendingID, nil 708 } 709 710 // UpdatePendingResource stores the resource in the Juju model. 711 func (st resourcePersistence) UpdatePendingResource(applicationID, pendingID, userID string, chRes charmresource.Resource, r io.Reader) (resources.Resource, error) { 712 rLogger.Tracef("updating pending resource %q (%s) for application %q", chRes.Name, pendingID, applicationID) 713 res, err := st.setResource(pendingID, applicationID, userID, chRes, r, IncrementCharmModifiedVersion) 714 if err != nil { 715 return res, errors.Trace(err) 716 } 717 return res, nil 718 } 719 720 // OpenResource returns metadata about the resource, and a reader for 721 // the resource. 722 func (p *resourcePersistence) OpenResource(applicationID, name string) (resources.Resource, io.ReadCloser, error) { 723 rLogger.Tracef("open resource %q of %q", name, applicationID) 724 id := newAppResourceID(applicationID, name) 725 resourceInfo, storagePath, err := p.getResource(id) 726 if err != nil { 727 if err := p.verifyApplication(applicationID); err != nil { 728 return resources.Resource{}, nil, errors.Trace(err) 729 } 730 return resources.Resource{}, nil, errors.Annotate(err, "while getting resource info") 731 } 732 if resourceInfo.IsPlaceholder() { 733 rLogger.Tracef("placeholder resource %q treated as not found", name) 734 return resources.Resource{}, nil, errors.NotFoundf("resource %q", name) 735 } 736 737 var resourceReader io.ReadCloser 738 var resSize int64 739 switch resourceInfo.Type { 740 case charmresource.TypeContainerImage: 741 resourceReader, resSize, err = p.dockerMetadataStorage.Get(resourceInfo.ID) 742 case charmresource.TypeFile: 743 resourceReader, resSize, err = p.storage.Get(storagePath) 744 default: 745 return resources.Resource{}, nil, errors.New("unknown resource type") 746 } 747 if err != nil { 748 return resources.Resource{}, nil, errors.Annotate(err, "while retrieving resource data") 749 } 750 switch resourceInfo.Type { 751 case charmresource.TypeContainerImage: 752 // Resource size only found at this stage in time as it's a response from the charmstore, not a stored file. 753 // Store it as it's used later for verification (in a separate call than this one) 754 resourceInfo.Size = resSize 755 if err := p.storeResourceInfo(resourceInfo); err != nil { 756 return resources.Resource{}, nil, errors.Annotate(err, "failed to update resource details with docker detail size") 757 } 758 case charmresource.TypeFile: 759 if resSize != resourceInfo.Size { 760 msg := "storage returned a size (%d) which doesn't match resource metadata (%d)" 761 return resources.Resource{}, nil, errors.Errorf(msg, resSize, resourceInfo.Size) 762 } 763 } 764 765 return resourceInfo, resourceReader, nil 766 } 767 768 // OpenResourceForUniter returns metadata about the resource and 769 // a reader for the resource. The resource is associated with 770 // the unit once the reader is completely exhausted. 771 func (p *resourcePersistence) OpenResourceForUniter(unitName, resName string) (resources.Resource, io.ReadCloser, error) { 772 rLogger.Tracef("open resource %q for uniter %q", resName, unitName) 773 774 pendingID, err := newPendingID() 775 if err != nil { 776 return resources.Resource{}, nil, errors.Trace(err) 777 } 778 779 appName, err := names.UnitApplication(unitName) 780 if err != nil { 781 return resources.Resource{}, nil, errors.Trace(err) 782 } 783 resourceInfo, resourceReader, err := p.OpenResource(appName, resName) 784 if err != nil { 785 return resources.Resource{}, nil, errors.Trace(err) 786 } 787 788 pending := resourceInfo // a copy 789 pending.PendingID = pendingID 790 791 progress := int64(0) 792 if err := p.setUnitResourceProgress(unitName, pending, &progress); err != nil { 793 _ = resourceReader.Close() 794 return resources.Resource{}, nil, errors.Trace(err) 795 } 796 797 resourceReader = &unitSetter{ 798 ReadCloser: resourceReader, 799 persist: p, 800 unitName: unitName, 801 pending: pending, 802 resource: resourceInfo, 803 clock: clock.WallClock, 804 } 805 806 return resourceInfo, resourceReader, nil 807 } 808 809 // unitSetter records the resource as in use by a unit when the wrapped 810 // reader has been fully read. 811 type unitSetter struct { 812 io.ReadCloser 813 persist *resourcePersistence 814 unitName string 815 pending resources.Resource 816 resource resources.Resource 817 progress int64 818 lastProgressUpdate time.Time 819 clock clock.Clock 820 } 821 822 // Read implements io.Reader. 823 func (u *unitSetter) Read(p []byte) (n int, err error) { 824 n, err = u.ReadCloser.Read(p) 825 if err == io.EOF { 826 // record that the unit is now using this version of the resource 827 if err := u.persist.setUnitResourceProgress(u.unitName, u.resource, nil); err != nil { 828 msg := "Failed to record that unit %q is using resource %q revision %v" 829 rLogger.Errorf(msg, u.unitName, u.resource.Name, u.resource.RevisionString()) 830 } 831 } else { 832 u.progress += int64(n) 833 if time.Since(u.lastProgressUpdate) > time.Second { 834 u.lastProgressUpdate = u.clock.Now() 835 if err := u.persist.setUnitResourceProgress(u.unitName, u.pending, &u.progress); err != nil { 836 rLogger.Errorf("failed to track progress: %v", err) 837 } 838 } 839 } 840 return n, err 841 } 842 843 // stageResource adds the resource in a separate staging area 844 // if the resource isn't already staged. If it is then 845 // errors.AlreadyExists is returned. A wrapper around the staged 846 // resource is returned which supports both finalizing and removing 847 // the staged resource. 848 func (p *resourcePersistence) stageResource(res resources.Resource, storagePath string) (*StagedResource, error) { 849 rLogger.Tracef("stage resource %q for %q", res.Name, res.ApplicationID) 850 if storagePath == "" { 851 return nil, errors.Errorf("missing storage path") 852 } 853 854 if err := res.Validate(); err != nil { 855 return nil, errors.Annotate(err, "bad resource") 856 } 857 858 stored := storedResource{ 859 Resource: res, 860 storagePath: storagePath, 861 } 862 staged := &StagedResource{ 863 p: p, 864 id: res.ID, 865 stored: stored, 866 } 867 if err := staged.stage(); err != nil { 868 return nil, errors.Trace(err) 869 } 870 return staged, nil 871 } 872 873 func (p *resourcePersistence) setResource( 874 pendingID, applicationID, userID string, 875 chRes charmresource.Resource, r io.Reader, 876 incrementCharmModifiedVersion IncrementCharmModifiedVersionType, 877 ) (resources.Resource, error) { 878 id := newAppResourceID(applicationID, chRes.Name) 879 880 res := resources.Resource{ 881 Resource: chRes, 882 ID: id, 883 PendingID: pendingID, 884 ApplicationID: applicationID, 885 } 886 if r != nil { 887 // TODO(ericsnow) Validate the user ID (or use a tag). 888 res.Username = userID 889 res.Timestamp = p.st.clock().Now().UTC() 890 } 891 892 if err := res.Validate(); err != nil { 893 return res, errors.Annotate(err, "bad resource metadata") 894 } 895 896 if r == nil { 897 if err := p.storeResourceInfo(res); err != nil { 898 return res, errors.Trace(err) 899 } 900 } else { 901 if err := p.storeResource(res, r, incrementCharmModifiedVersion); err != nil { 902 return res, errors.Trace(err) 903 } 904 } 905 906 return res, nil 907 } 908 909 func (p *resourcePersistence) getStored(res resources.Resource) (storedResource, error) { 910 doc, err := p.getOne(res.ID) 911 if errors.IsNotFound(err) { 912 err = errors.NotFoundf("resource %q", res.Name) 913 } 914 if err != nil { 915 return storedResource{}, errors.Trace(err) 916 } 917 918 stored, err := doc2resource(doc) 919 if err != nil { 920 return stored, errors.Trace(err) 921 } 922 923 return stored, nil 924 } 925 926 // storeResource stores the info for the resource. 927 func (p *resourcePersistence) storeResourceInfo(res resources.Resource) error { 928 rLogger.Tracef("set resource %q for %q", res.Name, res.ApplicationID) 929 stored, err := p.getStored(res) 930 if errors.IsNotFound(err) { 931 stored = storedResource{Resource: res} 932 } else if err != nil { 933 return errors.Trace(err) 934 } 935 // TODO(ericsnow) Ensure that stored.Resource matches res? If we do 936 // so then the following line is unnecessary. 937 stored.Resource = res 938 939 if err := res.Validate(); err != nil { 940 return errors.Annotate(err, "bad resource") 941 } 942 943 buildTxn := func(attempt int) ([]txn.Op, error) { 944 // This is an "upsert". 945 var ops []txn.Op 946 switch attempt { 947 case 0: 948 ops = newInsertResourceOps(stored) 949 case 1: 950 ops = newUpdateResourceOps(stored) 951 default: 952 // Either insert or update will work so we should not get here. 953 return nil, errors.New("setting the resource failed") 954 } 955 if stored.PendingID == "" { 956 // Only non-pending resources must have an existing application. 957 ops = append(ops, applicationExistsOps(res.ApplicationID)...) 958 } 959 return ops, nil 960 } 961 if err := p.st.db().Run(buildTxn); err != nil { 962 return errors.Trace(err) 963 } 964 return nil 965 } 966 967 func (p *resourcePersistence) storeResource(res resources.Resource, r io.Reader, incrementCharmModifiedVersion IncrementCharmModifiedVersionType) (err error) { 968 // We use a staging approach for adding the resource metadata 969 // to the model. This is necessary because the resource data 970 // is stored separately and adding to both should be an atomic 971 // operation. 972 973 storagePath := storagePath(res.Name, res.ApplicationID, res.PendingID) 974 staged, err := p.stageResource(res, storagePath) 975 if err != nil { 976 return errors.Trace(err) 977 } 978 defer func() { 979 if err != nil { 980 if e := staged.Unstage(); e != nil { 981 rLogger.Errorf("could not unstage resource %q (application %q): %v", res.Name, res.ApplicationID, e) 982 } 983 } 984 }() 985 986 hash := res.Fingerprint.String() 987 switch res.Type { 988 case charmresource.TypeFile: 989 if err = p.storage.PutAndCheckHash(storagePath, r, res.Size, hash); err != nil { 990 return errors.Trace(err) 991 } 992 case charmresource.TypeContainerImage: 993 respBuf := new(bytes.Buffer) 994 _, err = respBuf.ReadFrom(r) 995 if err != nil { 996 return errors.Trace(err) 997 } 998 dockerDetails, err := resources.UnmarshalDockerResource(respBuf.Bytes()) 999 if err != nil { 1000 return errors.Trace(err) 1001 } 1002 err = p.dockerMetadataStorage.Save(res.ID, dockerDetails) 1003 if err != nil { 1004 return errors.Trace(err) 1005 } 1006 } 1007 1008 if err = staged.Activate(incrementCharmModifiedVersion); err != nil { 1009 if e := p.storage.Remove(storagePath); e != nil { 1010 rLogger.Errorf("could not remove resource %q (application %q) from storage: %v", res.Name, res.ApplicationID, e) 1011 } 1012 return errors.Trace(err) 1013 } 1014 return nil 1015 } 1016 1017 // setUnitResourceProgress stores the resource info for a particular unit. The 1018 // resource must already be set for the application. The provided progress 1019 // is stored in the DB. 1020 func (p *resourcePersistence) setUnitResourceProgress(unitID string, res resources.Resource, progress *int64) error { 1021 rLogger.Tracef("set unit %q resource %q progress", unitID, res.Name) 1022 if res.PendingID == "" && progress != nil { 1023 return errors.Errorf("only pending resources may track progress") 1024 } 1025 stored, err := p.getStored(res) 1026 if err != nil { 1027 return errors.Trace(err) 1028 } 1029 // TODO(ericsnow) Ensure that stored.Resource matches res? If we do 1030 // so then the following line is unnecessary. 1031 stored.Resource = res 1032 1033 if err := res.Validate(); err != nil { 1034 return errors.Annotate(err, "bad resource") 1035 } 1036 1037 buildTxn := func(attempt int) ([]txn.Op, error) { 1038 // This is an "upsert". 1039 var ops []txn.Op 1040 switch attempt { 1041 case 0: 1042 ops = newInsertUnitResourceOps(unitID, stored, progress) 1043 case 1: 1044 ops = newUpdateUnitResourceOps(unitID, stored, progress) 1045 default: 1046 // Either insert or update will work so we should not get here. 1047 return nil, errors.New("setting the resource failed") 1048 } 1049 // No pending resources so we always do this here. 1050 ops = append(ops, applicationExistsOps(res.ApplicationID)...) 1051 return ops, nil 1052 } 1053 if err := p.st.db().Run(buildTxn); err != nil { 1054 return errors.Trace(err) 1055 } 1056 return nil 1057 } 1058 1059 // RemovePendingAppResources removes the pending application-level 1060 // resources for a specific application, normally in the case that the 1061 // application couldn't be deployed. 1062 func (p *resourcePersistence) RemovePendingAppResources(applicationID string, pendingIDs map[string]string) error { 1063 buildTxn := func(int) ([]txn.Op, error) { 1064 return p.removePendingAppResourcesOps(applicationID, pendingIDs) 1065 } 1066 return errors.Trace(p.st.db().Run(buildTxn)) 1067 } 1068 1069 // resolveApplicationPendingResourcesOps generates mongo transaction operations 1070 // to set the identified resources as active. 1071 func (p *resourcePersistence) resolveApplicationPendingResourcesOps(applicationID string, pendingIDs map[string]string) ([]txn.Op, error) { 1072 rLogger.Tracef("resolve pending resource ops for %q", applicationID) 1073 if len(pendingIDs) == 0 { 1074 return nil, nil 1075 } 1076 1077 // TODO(ericsnow) The resources need to be pulled in from the charm 1078 // store before we get to this point. 1079 1080 var allOps []txn.Op 1081 for name, pendingID := range pendingIDs { 1082 resID := newAppResourceID(applicationID, name) 1083 ops, err := p.resolvePendingResourceOps(resID, pendingID) 1084 if err != nil { 1085 return nil, errors.Trace(err) 1086 } 1087 allOps = append(allOps, ops...) 1088 } 1089 return allOps, nil 1090 } 1091 1092 // resolvePendingResourceOps generates mongo transaction operations 1093 // to set the identified resource as active. 1094 func (p *resourcePersistence) resolvePendingResourceOps(resID, pendingID string) ([]txn.Op, error) { 1095 rLogger.Tracef("resolve pending resource ops %q, %q", resID, pendingID) 1096 if pendingID == "" { 1097 return nil, errors.New("missing pending ID") 1098 } 1099 1100 oldDoc, err := p.getOnePending(resID, pendingID) 1101 if errors.IsNotFound(err) { 1102 return nil, errors.NotFoundf("pending resource %q (%s)", resID, pendingID) 1103 } 1104 if err != nil { 1105 return nil, errors.Trace(err) 1106 } 1107 pending, err := doc2resource(oldDoc) 1108 if err != nil { 1109 return nil, errors.Trace(err) 1110 } 1111 1112 exists := true 1113 if _, err := p.getOne(resID); errors.IsNotFound(err) { 1114 exists = false 1115 } else if err != nil { 1116 return nil, errors.Trace(err) 1117 } 1118 1119 csExists := true 1120 csResID := resID + resourcesCharmstoreIDSuffix 1121 if _, err := p.getOne(csResID); errors.IsNotFound(err) { 1122 csExists = false 1123 } else if err != nil { 1124 return nil, errors.Trace(err) 1125 } 1126 1127 ops := newResolvePendingResourceOps(pending, exists, csExists) 1128 return ops, nil 1129 } 1130 1131 // removeUnitResourcesOps returns mgo transaction operations 1132 // that remove resource information specific to the unit from state. 1133 func (p *resourcePersistence) removeUnitResourcesOps(unitID string) ([]txn.Op, error) { 1134 docs, err := p.unitResources(unitID) 1135 if err != nil { 1136 return nil, errors.Trace(err) 1137 } 1138 1139 ops := newRemoveResourcesOps(docs) 1140 // We do not remove the resource from the blob store here. That is 1141 // an application-level matter. 1142 return ops, nil 1143 } 1144 1145 // removeResourcesOps returns mgo transaction operations that 1146 // remove all the application's resources from state. 1147 func (p *resourcePersistence) removeResourcesOps(applicationID string) ([]txn.Op, error) { 1148 docs, err := p.resources(applicationID) 1149 if err != nil { 1150 return nil, errors.Trace(err) 1151 } 1152 return removeResourcesAndStorageCleanupOps(docs), nil 1153 } 1154 1155 // removePendingAppResourcesOps returns mgo transaction operations to 1156 // clean up pending resources for the application from state. We pass 1157 // in the pending IDs to avoid removing the wrong resources if there's 1158 // a race to deploy the same application. 1159 func (p *resourcePersistence) removePendingAppResourcesOps(applicationID string, pendingIDs map[string]string) ([]txn.Op, error) { 1160 docs, err := p.resources(applicationID) 1161 if err != nil { 1162 return nil, errors.Trace(err) 1163 } 1164 pending := make([]resourceDoc, 0, len(docs)) 1165 for _, doc := range docs { 1166 if doc.UnitID != "" || doc.PendingID == "" { 1167 continue 1168 } 1169 if pendingIDs[doc.Name] != doc.PendingID { 1170 // This is a pending resource for a different deployment 1171 // of an application with the same name. 1172 continue 1173 } 1174 pending = append(pending, doc) 1175 } 1176 return removeResourcesAndStorageCleanupOps(pending), nil 1177 } 1178 1179 func applicationExistsOps(applicationID string) []txn.Op { 1180 return []txn.Op{{ 1181 C: applicationsC, 1182 Id: applicationID, 1183 Assert: isAliveDoc, 1184 }} 1185 } 1186 1187 func removeResourcesAndStorageCleanupOps(docs []resourceDoc) []txn.Op { 1188 ops := newRemoveResourcesOps(docs) 1189 seenPaths := set.NewStrings() 1190 for _, doc := range docs { 1191 // Don't schedule cleanups for placeholder resources, or multiple for a given path. 1192 if doc.StoragePath == "" || seenPaths.Contains(doc.StoragePath) { 1193 continue 1194 } 1195 ops = append(ops, newCleanupOp(cleanupResourceBlob, doc.StoragePath)) 1196 seenPaths.Add(doc.StoragePath) 1197 } 1198 return ops 1199 } 1200 1201 func newInsertStagedResourceOps(stored storedResource) []txn.Op { 1202 doc := newStagedResourceDoc(stored) 1203 1204 return []txn.Op{{ 1205 C: resourcesC, 1206 Id: doc.DocID, 1207 Assert: txn.DocMissing, 1208 Insert: doc, 1209 }} 1210 } 1211 1212 func newEnsureStagedResourceSameOps(stored storedResource) []txn.Op { 1213 doc := newStagedResourceDoc(stored) 1214 1215 // Other than cause the txn to abort, we don't do anything here. 1216 return []txn.Op{{ 1217 C: resourcesC, 1218 Id: doc.DocID, 1219 Assert: doc, // TODO(ericsnow) Is this okay? 1220 }} 1221 } 1222 1223 func newRemoveStagedResourceOps(id string) []txn.Op { 1224 fullID := stagedResourceID(id) 1225 1226 // We don't assert that it exists. We want "missing" to be a noop. 1227 return []txn.Op{{ 1228 C: resourcesC, 1229 Id: fullID, 1230 Remove: true, 1231 }} 1232 } 1233 1234 func newInsertResourceOps(stored storedResource) []txn.Op { 1235 doc := newResourceDoc(stored) 1236 1237 return []txn.Op{{ 1238 C: resourcesC, 1239 Id: doc.DocID, 1240 Assert: txn.DocMissing, 1241 Insert: doc, 1242 }} 1243 } 1244 1245 func resourceDocToUpdateOp(doc *resourceDoc) bson.M { 1246 // Note (jam 2018-07-12): What are we actually allowed to update? 1247 // The old code was trying to delete the doc and replace it entirely, so for now we'll just set everything except 1248 // the doc's own id, which is clearly not allowed to change. 1249 return bson.M{"$set": bson.M{ 1250 "resource-id": doc.ID, 1251 "pending-id": doc.PendingID, 1252 "application-id": doc.ApplicationID, 1253 "unit-id": doc.UnitID, 1254 "name": doc.Name, 1255 "type": doc.Type, 1256 "path": doc.Path, 1257 "description": doc.Description, 1258 "origin": doc.Origin, 1259 "revision": doc.Revision, 1260 "fingerprint": doc.Fingerprint, 1261 "size": doc.Size, 1262 "username": doc.Username, 1263 "timestamp-when-added": doc.Timestamp, 1264 "storage-path": doc.StoragePath, 1265 "download-progress": doc.DownloadProgress, 1266 "timestamp-when-last-polled": doc.LastPolled.Round(time.Second).UTC(), 1267 }} 1268 } 1269 1270 func newUpdateResourceOps(stored storedResource) []txn.Op { 1271 doc := newResourceDoc(stored) 1272 1273 rLogger.Tracef("updating resource %s to %# v", stored.ID, pretty.Formatter(doc)) 1274 return []txn.Op{{ 1275 C: resourcesC, 1276 Id: doc.DocID, 1277 Assert: txn.DocExists, 1278 Update: resourceDocToUpdateOp(doc), 1279 }} 1280 } 1281 1282 func newInsertCharmStoreResourceOps(res charmStoreResource) []txn.Op { 1283 doc := newCharmStoreResourceDoc(res) 1284 1285 return []txn.Op{{ 1286 C: resourcesC, 1287 Id: doc.DocID, 1288 Assert: txn.DocMissing, 1289 Insert: doc, 1290 }} 1291 } 1292 1293 func newUpdateCharmStoreResourceOps(res charmStoreResource) []txn.Op { 1294 doc := newCharmStoreResourceDoc(res) 1295 1296 if rLogger.IsTraceEnabled() { 1297 rLogger.Tracef("updating charm store resource %s to %# v", res.id, pretty.Formatter(doc)) 1298 } 1299 return []txn.Op{{ 1300 C: resourcesC, 1301 Id: doc.DocID, 1302 Assert: txn.DocExists, 1303 Update: resourceDocToUpdateOp(doc), 1304 }} 1305 } 1306 1307 func newInsertUnitResourceOps(unitID string, stored storedResource, progress *int64) []txn.Op { 1308 doc := newUnitResourceDoc(unitID, stored) 1309 doc.DownloadProgress = progress 1310 1311 return []txn.Op{{ 1312 C: resourcesC, 1313 Id: doc.DocID, 1314 Assert: txn.DocMissing, 1315 Insert: doc, 1316 }} 1317 } 1318 1319 func newUpdateUnitResourceOps(unitID string, stored storedResource, progress *int64) []txn.Op { 1320 doc := newUnitResourceDoc(unitID, stored) 1321 doc.DownloadProgress = progress 1322 1323 if rLogger.IsTraceEnabled() { 1324 rLogger.Tracef("updating unit resource %s to %# v", unitID, pretty.Formatter(doc)) 1325 } 1326 return []txn.Op{{ 1327 C: resourcesC, 1328 Id: doc.DocID, 1329 Assert: txn.DocExists, // feels like we need more 1330 Update: resourceDocToUpdateOp(doc), 1331 }} 1332 } 1333 1334 func newRemoveResourcesOps(docs []resourceDoc) []txn.Op { 1335 // The likelihood of a race is small and the consequences are minor, 1336 // so we don't worry about the corner case of missing a doc here. 1337 var ops []txn.Op 1338 for _, doc := range docs { 1339 // We do not bother to assert txn.DocExists since it will be 1340 // gone either way, which is the result we're after. 1341 ops = append(ops, txn.Op{ 1342 C: resourcesC, 1343 Id: doc.DocID, 1344 Remove: true, 1345 }) 1346 } 1347 return ops 1348 } 1349 1350 // newResolvePendingResourceOps generates transaction operations that 1351 // will resolve a pending resource doc and make it active. 1352 // 1353 // We trust that the provided resource really is pending 1354 // and that it matches the existing doc with the same ID. 1355 func newResolvePendingResourceOps(pending storedResource, exists, csExists bool) []txn.Op { 1356 oldID := pendingResourceID(pending.ID, pending.PendingID) 1357 newRes := pending 1358 newRes.PendingID = "" 1359 // TODO(ericsnow) Update newRes.StoragePath? Doing so would require 1360 // moving the resource in the blobstore to the correct path, which 1361 // we cannot do in the transaction... 1362 ops := []txn.Op{{ 1363 C: resourcesC, 1364 Id: oldID, 1365 Assert: txn.DocExists, 1366 Remove: true, 1367 }} 1368 1369 // TODO(perrito666) 2016-05-02 lp:1558657 1370 csRes := charmStoreResource{ 1371 Resource: newRes.Resource.Resource, 1372 id: newRes.ID, 1373 applicationID: newRes.ApplicationID, 1374 // Truncate the time to remove monotonic time for Go 1.9+ 1375 // to make it easier for tests to compare the time. 1376 lastPolled: time.Now().Truncate(1).UTC(), 1377 } 1378 1379 if exists { 1380 ops = append(ops, newUpdateResourceOps(newRes)...) 1381 1382 } else { 1383 ops = append(ops, newInsertResourceOps(newRes)...) 1384 } 1385 if csExists { 1386 return append(ops, newUpdateCharmStoreResourceOps(csRes)...) 1387 } else { 1388 return append(ops, newInsertCharmStoreResourceOps(csRes)...) 1389 } 1390 }