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  }