github.com/wallyworld/juju@v0.0.0-20161013125918-6cf1bc9d917a/resource/state/resource.go (about) 1 // Copyright 2015 Canonical Ltd. 2 // Licensed under the AGPLv3, see LICENCE file for details. 3 4 package state 5 6 // TODO(ericsnow) Figure out a way to drop the txn dependency here? 7 8 import ( 9 "fmt" 10 "io" 11 "path" 12 "time" 13 14 "github.com/juju/errors" 15 "github.com/juju/utils" 16 "github.com/juju/utils/clock" 17 charmresource "gopkg.in/juju/charm.v6-unstable/resource" 18 "gopkg.in/juju/names.v2" 19 "gopkg.in/mgo.v2/txn" 20 21 "github.com/juju/juju/resource" 22 ) 23 24 type resourcePersistence interface { 25 // ListResources returns the resource data for the given application ID. 26 // None of the resources will be pending. 27 ListResources(applicationID string) (resource.ServiceResources, error) 28 29 // ListPendingResources returns the resource data for the given 30 // application ID. 31 ListPendingResources(applicationID string) ([]resource.Resource, error) 32 33 // GetResource returns the extended, model-related info for the 34 // non-pending resource. 35 GetResource(id string) (res resource.Resource, storagePath string, _ error) 36 37 // StageResource adds the resource in a separate staging area 38 // if the resource isn't already staged. If the resource already 39 // exists then it is treated as unavailable as long as the new one 40 // is staged. 41 StageResource(res resource.Resource, storagePath string) (StagedResource, error) 42 43 // SetResource stores the info for the resource. 44 SetResource(args resource.Resource) error 45 46 // SetCharmStoreResource stores the resource info that was retrieved 47 // from the charm store. 48 SetCharmStoreResource(id, applicationID string, res charmresource.Resource, lastPolled time.Time) error 49 50 // SetUnitResource stores the resource info for a unit. 51 SetUnitResource(unitID string, args resource.Resource) error 52 53 // SetUnitResourceProgress stores the resource info and download 54 // progressfor a unit. 55 SetUnitResourceProgress(unitID string, args resource.Resource, progress int64) error 56 57 // NewResolvePendingResourceOps generates mongo transaction operations 58 // to set the identified resource as active. 59 NewResolvePendingResourceOps(resID, pendingID string) ([]txn.Op, error) 60 } 61 62 // StagedResource represents resource info that has been added to the 63 // "staging" area of the persistence layer. 64 // 65 // A separate staging area is necessary because we are dealing with 66 // the DB and storage at the same time for the same resource in some 67 // operations (e.g. SetResource). Resources are staged in the DB, 68 // added to storage, and then finalized in the DB. 69 type StagedResource interface { 70 // Unstage ensures that the resource is removed 71 // from the staging area. If it isn't in the staging area 72 // then this is a noop. 73 Unstage() error 74 75 // Activate makes the staged resource the active resource. 76 Activate() error 77 } 78 79 type rawState interface { 80 // VerifyService ensures that the application is in state. 81 VerifyService(id string) error 82 83 // Units returns the tags for all units in the application. 84 Units(applicationID string) ([]names.UnitTag, error) 85 } 86 87 type resourceStorage interface { 88 // PutAndCheckHash stores the content of the reader into the storage. 89 PutAndCheckHash(path string, r io.Reader, length int64, hash string) error 90 91 // Remove removes the identified data from the storage. 92 Remove(path string) error 93 94 // Get returns a reader for the resource at path. The size of the 95 // data is also returned. 96 Get(path string) (io.ReadCloser, int64, error) 97 } 98 99 type resourceState struct { 100 persist resourcePersistence 101 raw rawState 102 storage resourceStorage 103 104 newPendingID func() (string, error) 105 currentTimestamp func() time.Time 106 } 107 108 // ListResources returns the resource data for the given application ID. 109 func (st resourceState) ListResources(applicationID string) (resource.ServiceResources, error) { 110 resources, err := st.persist.ListResources(applicationID) 111 if err != nil { 112 if err := st.raw.VerifyService(applicationID); err != nil { 113 return resource.ServiceResources{}, errors.Trace(err) 114 } 115 return resource.ServiceResources{}, errors.Trace(err) 116 } 117 118 unitIDs, err := st.raw.Units(applicationID) 119 if err != nil { 120 return resource.ServiceResources{}, errors.Trace(err) 121 } 122 for _, unitID := range unitIDs { 123 found := false 124 for _, unitRes := range resources.UnitResources { 125 if unitID.String() == unitRes.Tag.String() { 126 found = true 127 break 128 } 129 } 130 if !found { 131 unitRes := resource.UnitResources{ 132 Tag: unitID, 133 } 134 resources.UnitResources = append(resources.UnitResources, unitRes) 135 } 136 } 137 138 return resources, nil 139 } 140 141 // GetResource returns the resource data for the identified resource. 142 func (st resourceState) GetResource(applicationID, name string) (resource.Resource, error) { 143 id := newResourceID(applicationID, name) 144 res, _, err := st.persist.GetResource(id) 145 if err != nil { 146 if err := st.raw.VerifyService(applicationID); err != nil { 147 return resource.Resource{}, errors.Trace(err) 148 } 149 return res, errors.Trace(err) 150 } 151 return res, nil 152 } 153 154 // GetPendingResource returns the resource data for the identified resource. 155 func (st resourceState) GetPendingResource(applicationID, name, pendingID string) (resource.Resource, error) { 156 var res resource.Resource 157 158 resources, err := st.persist.ListPendingResources(applicationID) 159 if err != nil { 160 // We do not call VerifyService() here because pending resources 161 // do not have to have an existing application. 162 return res, errors.Trace(err) 163 } 164 165 for _, res := range resources { 166 if res.Name == name && res.PendingID == pendingID { 167 return res, nil 168 } 169 } 170 return res, errors.NotFoundf("pending resource %q (%s)", name, pendingID) 171 } 172 173 // TODO(ericsnow) Separate setting the metadata from storing the blob? 174 175 // SetResource stores the resource in the Juju model. 176 func (st resourceState) SetResource(applicationID, userID string, chRes charmresource.Resource, r io.Reader) (resource.Resource, error) { 177 logger.Tracef("adding resource %q for application %q", chRes.Name, applicationID) 178 pendingID := "" 179 res, err := st.setResource(pendingID, applicationID, userID, chRes, r) 180 if err != nil { 181 return res, errors.Trace(err) 182 } 183 return res, nil 184 } 185 186 // AddPendingResource stores the resource in the Juju model. 187 func (st resourceState) AddPendingResource(applicationID, userID string, chRes charmresource.Resource, r io.Reader) (pendingID string, err error) { 188 pendingID, err = st.newPendingID() 189 if err != nil { 190 return "", errors.Annotate(err, "could not generate resource ID") 191 } 192 logger.Debugf("adding pending resource %q for application %q (ID: %s)", chRes.Name, applicationID, pendingID) 193 194 if _, err := st.setResource(pendingID, applicationID, userID, chRes, r); err != nil { 195 return "", errors.Trace(err) 196 } 197 198 return pendingID, nil 199 } 200 201 // UpdatePendingResource stores the resource in the Juju model. 202 func (st resourceState) UpdatePendingResource(applicationID, pendingID, userID string, chRes charmresource.Resource, r io.Reader) (resource.Resource, error) { 203 logger.Tracef("updating pending resource %q (%s) for application %q", chRes.Name, pendingID, applicationID) 204 res, err := st.setResource(pendingID, applicationID, userID, chRes, r) 205 if err != nil { 206 return res, errors.Trace(err) 207 } 208 return res, nil 209 } 210 211 // TODO(ericsnow) Add ResolvePendingResource(). 212 213 func (st resourceState) setResource(pendingID, applicationID, userID string, chRes charmresource.Resource, r io.Reader) (resource.Resource, error) { 214 id := newResourceID(applicationID, chRes.Name) 215 216 res := resource.Resource{ 217 Resource: chRes, 218 ID: id, 219 PendingID: pendingID, 220 ApplicationID: applicationID, 221 } 222 if r != nil { 223 // TODO(ericsnow) Validate the user ID (or use a tag). 224 res.Username = userID 225 res.Timestamp = st.currentTimestamp() 226 } 227 228 if err := res.Validate(); err != nil { 229 return res, errors.Annotate(err, "bad resource metadata") 230 } 231 232 if r == nil { 233 if err := st.persist.SetResource(res); err != nil { 234 return res, errors.Trace(err) 235 } 236 } else { 237 if err := st.storeResource(res, r); err != nil { 238 return res, errors.Trace(err) 239 } 240 } 241 242 return res, nil 243 } 244 245 func (st resourceState) storeResource(res resource.Resource, r io.Reader) error { 246 // We use a staging approach for adding the resource metadata 247 // to the model. This is necessary because the resource data 248 // is stored separately and adding to both should be an atomic 249 // operation. 250 251 storagePath := storagePath(res.Name, res.ApplicationID, res.PendingID) 252 staged, err := st.persist.StageResource(res, storagePath) 253 if err != nil { 254 return errors.Trace(err) 255 } 256 257 hash := res.Fingerprint.String() 258 if err := st.storage.PutAndCheckHash(storagePath, r, res.Size, hash); err != nil { 259 if err := staged.Unstage(); err != nil { 260 logger.Errorf("could not unstage resource %q (application %q): %v", res.Name, res.ApplicationID, err) 261 } 262 return errors.Trace(err) 263 } 264 265 if err := staged.Activate(); err != nil { 266 if err := st.storage.Remove(storagePath); err != nil { 267 logger.Errorf("could not remove resource %q (application %q) from storage: %v", res.Name, res.ApplicationID, err) 268 } 269 if err := staged.Unstage(); err != nil { 270 logger.Errorf("could not unstage resource %q (application %q): %v", res.Name, res.ApplicationID, err) 271 } 272 return errors.Trace(err) 273 } 274 275 return nil 276 } 277 278 // OpenResource returns metadata about the resource, and a reader for 279 // the resource. 280 func (st resourceState) OpenResource(applicationID, name string) (resource.Resource, io.ReadCloser, error) { 281 id := newResourceID(applicationID, name) 282 resourceInfo, storagePath, err := st.persist.GetResource(id) 283 if err != nil { 284 if err := st.raw.VerifyService(applicationID); err != nil { 285 return resource.Resource{}, nil, errors.Trace(err) 286 } 287 return resource.Resource{}, nil, errors.Annotate(err, "while getting resource info") 288 } 289 if resourceInfo.IsPlaceholder() { 290 logger.Tracef("placeholder resource %q treated as not found", name) 291 return resource.Resource{}, nil, errors.NotFoundf("resource %q", name) 292 } 293 294 resourceReader, resSize, err := st.storage.Get(storagePath) 295 if err != nil { 296 return resource.Resource{}, nil, errors.Annotate(err, "while retrieving resource data") 297 } 298 if resSize != resourceInfo.Size { 299 msg := "storage returned a size (%d) which doesn't match resource metadata (%d)" 300 return resource.Resource{}, nil, errors.Errorf(msg, resSize, resourceInfo.Size) 301 } 302 303 return resourceInfo, resourceReader, nil 304 } 305 306 // OpenResourceForUniter returns metadata about the resource and 307 // a reader for the resource. The resource is associated with 308 // the unit once the reader is completely exhausted. 309 func (st resourceState) OpenResourceForUniter(unit resource.Unit, name string) (resource.Resource, io.ReadCloser, error) { 310 applicationID := unit.ApplicationName() 311 312 pendingID, err := newPendingID() 313 if err != nil { 314 return resource.Resource{}, nil, errors.Trace(err) 315 } 316 317 resourceInfo, resourceReader, err := st.OpenResource(applicationID, name) 318 if err != nil { 319 return resource.Resource{}, nil, errors.Trace(err) 320 } 321 322 pending := resourceInfo // a copy 323 pending.PendingID = pendingID 324 325 if err := st.persist.SetUnitResourceProgress(unit.Name(), pending, 0); err != nil { 326 resourceReader.Close() 327 return resource.Resource{}, nil, errors.Trace(err) 328 } 329 330 resourceReader = &unitSetter{ 331 ReadCloser: resourceReader, 332 persist: st.persist, 333 unit: unit, 334 pending: pending, 335 resource: resourceInfo, 336 clock: clock.WallClock, 337 } 338 339 return resourceInfo, resourceReader, nil 340 } 341 342 // SetCharmStoreResources sets the "polled" resources for the 343 // application to the provided values. 344 func (st resourceState) SetCharmStoreResources(applicationID string, info []charmresource.Resource, lastPolled time.Time) error { 345 for _, chRes := range info { 346 id := newResourceID(applicationID, chRes.Name) 347 if err := st.persist.SetCharmStoreResource(id, applicationID, chRes, lastPolled); err != nil { 348 return errors.Trace(err) 349 } 350 // TODO(ericsnow) Worry about extras? missing? 351 } 352 353 return nil 354 } 355 356 // TODO(ericsnow) Rename NewResolvePendingResourcesOps to reflect that 357 // it has more meat to it? 358 359 // NewResolvePendingResourcesOps generates mongo transaction operations 360 // to set the identified resources as active. 361 // 362 // Leaking mongo details (transaction ops) is a necessary evil since we 363 // do not have any machinery to facilitate transactions between 364 // different components. 365 func (st resourceState) NewResolvePendingResourcesOps(applicationID string, pendingIDs map[string]string) ([]txn.Op, error) { 366 if len(pendingIDs) == 0 { 367 return nil, nil 368 } 369 370 // TODO(ericsnow) The resources need to be pulled in from the charm 371 // store before we get to this point. 372 373 var allOps []txn.Op 374 for name, pendingID := range pendingIDs { 375 ops, err := st.newResolvePendingResourceOps(applicationID, name, pendingID) 376 if err != nil { 377 return nil, errors.Trace(err) 378 } 379 allOps = append(allOps, ops...) 380 } 381 return allOps, nil 382 } 383 384 func (st resourceState) newResolvePendingResourceOps(applicationID, name, pendingID string) ([]txn.Op, error) { 385 resID := newResourceID(applicationID, name) 386 return st.persist.NewResolvePendingResourceOps(resID, pendingID) 387 } 388 389 // TODO(ericsnow) Incorporate the application and resource name into the ID 390 // instead of just using a UUID? 391 392 // newPendingID generates a new unique identifier for a resource. 393 func newPendingID() (string, error) { 394 uuid, err := utils.NewUUID() 395 if err != nil { 396 return "", errors.Annotate(err, "could not create new resource ID") 397 } 398 return uuid.String(), nil 399 } 400 401 // newResourceID produces a new ID to use for the resource in the model. 402 func newResourceID(applicationID, name string) string { 403 return fmt.Sprintf("%s/%s", applicationID, name) 404 } 405 406 // storagePath returns the path used as the location where the resource 407 // is stored in state storage. This requires that the returned string 408 // be unique and that it be organized in a structured way. In this case 409 // we start with a top-level (the application), then under that application use 410 // the "resources" section. The provided ID is located under there. 411 func storagePath(name, applicationID, pendingID string) string { 412 // TODO(ericsnow) Use applications/<application>/resources/<resource>? 413 id := name 414 if pendingID != "" { 415 // TODO(ericsnow) How to resolve this later? 416 id += "-" + pendingID 417 } 418 return path.Join("application-"+applicationID, "resources", id) 419 } 420 421 // unitSetter records the resource as in use by a unit when the wrapped 422 // reader has been fully read. 423 type unitSetter struct { 424 io.ReadCloser 425 persist resourcePersistence 426 unit resource.Unit 427 pending resource.Resource 428 resource resource.Resource 429 progress int64 430 lastProgressUpdate time.Time 431 clock clock.Clock 432 } 433 434 // Read implements io.Reader. 435 func (u *unitSetter) Read(p []byte) (n int, err error) { 436 n, err = u.ReadCloser.Read(p) 437 if err == io.EOF { 438 // record that the unit is now using this version of the resource 439 if err := u.persist.SetUnitResource(u.unit.Name(), u.resource); err != nil { 440 msg := "Failed to record that unit %q is using resource %q revision %v" 441 logger.Errorf(msg, u.unit.Name(), u.resource.Name, u.resource.RevisionString()) 442 } 443 } else { 444 u.progress += int64(n) 445 if time.Since(u.lastProgressUpdate) > time.Second { 446 u.lastProgressUpdate = u.clock.Now() 447 if err := u.persist.SetUnitResourceProgress(u.unit.Name(), u.pending, u.progress); err != nil { 448 logger.Errorf("failed to track progress: %v", err) 449 } 450 } 451 } 452 return n, err 453 }