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