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  }