github.com/niedbalski/juju@v0.0.0-20190215020005-8ff100488e47/resource/resourceadapters/charmstore.go (about)

     1  // Copyright 2016 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package resourceadapters
     5  
     6  import (
     7  	"io"
     8  	"time"
     9  
    10  	"github.com/juju/clock"
    11  	"github.com/juju/errors"
    12  	"github.com/juju/retry"
    13  	charmresource "gopkg.in/juju/charm.v6/resource"
    14  	"gopkg.in/juju/names.v2"
    15  
    16  	"github.com/juju/juju/charmstore"
    17  	"github.com/juju/juju/resource"
    18  	"github.com/juju/juju/state"
    19  )
    20  
    21  // charmstoreEntityCache adapts between resource state and charmstore.EntityCache.
    22  type charmstoreEntityCache struct {
    23  	st            state.Resources
    24  	userID        names.Tag
    25  	unit          resource.Unit
    26  	applicationID string
    27  }
    28  
    29  // GetResource implements charmstore.EntityCache.
    30  func (cache *charmstoreEntityCache) GetResource(name string) (resource.Resource, error) {
    31  	return cache.st.GetResource(cache.applicationID, name)
    32  }
    33  
    34  // SetResource implements charmstore.EntityCache.
    35  func (cache *charmstoreEntityCache) SetResource(chRes charmresource.Resource, reader io.Reader) (resource.Resource, error) {
    36  	return cache.st.SetResource(cache.applicationID, cache.userID.Id(), chRes, reader)
    37  }
    38  
    39  // OpenResource implements charmstore.EntityCache.
    40  func (cache *charmstoreEntityCache) OpenResource(name string) (resource.Resource, io.ReadCloser, error) {
    41  	if cache.unit == nil {
    42  		return resource.Resource{}, nil, errors.NotImplementedf("")
    43  	}
    44  	return cache.st.OpenResourceForUniter(cache.unit, name)
    45  }
    46  
    47  type charmstoreOpener struct {
    48  	st *state.State
    49  }
    50  
    51  func newCharmstoreOpener(st *state.State) *charmstoreOpener {
    52  	return &charmstoreOpener{st}
    53  }
    54  
    55  func newCharmStoreClient(st *state.State) (charmstore.Client, error) {
    56  	controllerCfg, err := st.ControllerConfig()
    57  	if err != nil {
    58  		return charmstore.Client{}, errors.Trace(err)
    59  	}
    60  	return charmstore.NewCachingClient(state.MacaroonCache{st}, controllerCfg.CharmStoreURL())
    61  }
    62  
    63  // NewClient opens a new charm store client.
    64  func (cs *charmstoreOpener) NewClient() (*CSRetryClient, error) {
    65  	// TODO(ericsnow) Use a valid charm store client.
    66  	client, err := newCharmStoreClient(cs.st)
    67  	if err != nil {
    68  		return nil, errors.Trace(err)
    69  	}
    70  	return newCSRetryClient(client), nil
    71  }
    72  
    73  // ResourceClient defines a set of functionality that a client
    74  // needs to define to support resources.
    75  type ResourceClient interface {
    76  	GetResource(req charmstore.ResourceRequest) (data charmstore.ResourceData, err error)
    77  }
    78  
    79  // CSRetryClient is a wrapper around a Juju charm store client that
    80  // retries GetResource() calls.
    81  type CSRetryClient struct {
    82  	ResourceClient
    83  	retryArgs retry.CallArgs
    84  }
    85  
    86  func newCSRetryClient(client ResourceClient) *CSRetryClient {
    87  	retryArgs := retry.CallArgs{
    88  		// (anastasiamac 2017-05-25) This might not work as the error types
    89  		// may be lost after a call to some clients.
    90  		IsFatalError: func(err error) bool {
    91  			return errors.IsNotFound(err) || errors.IsNotValid(err)
    92  		},
    93  		// We don't want to retry for ever.
    94  		// If we cannot get a resource after trying a few times,
    95  		// most likely user intervention is needed.
    96  		Attempts: 3,
    97  		// A one minute gives enough time for potential connection
    98  		// issues to sort themselves out without making the caller wait
    99  		// for an exceptional amount of time.
   100  		Delay: 1 * time.Minute,
   101  		Clock: clock.WallClock,
   102  	}
   103  	return &CSRetryClient{
   104  		ResourceClient: client,
   105  		retryArgs:      retryArgs,
   106  	}
   107  }
   108  
   109  // GetResource returns a reader for the resource's data.
   110  func (client CSRetryClient) GetResource(req charmstore.ResourceRequest) (charmstore.ResourceData, error) {
   111  	args := client.retryArgs // a copy
   112  
   113  	var data charmstore.ResourceData
   114  	args.Func = func() error {
   115  		var err error
   116  		data, err = client.ResourceClient.GetResource(req)
   117  		if err != nil {
   118  			return errors.Trace(err)
   119  		}
   120  		return nil
   121  	}
   122  
   123  	var lastErr error
   124  	args.NotifyFunc = func(err error, i int) {
   125  		// Remember the error we're hiding and then retry!
   126  		logger.Warningf("attempt %d/%d to download resource %q from charm store [channel (%v), charm (%v), resource revision (%v)] failed with error (will retry): %v",
   127  			i, client.retryArgs.Attempts, req.Name, req.Channel, req.Charm, req.Revision, err)
   128  		logger.Tracef("resource get error stack: %v", errors.ErrorStack(err))
   129  		lastErr = err
   130  	}
   131  
   132  	err := retry.Call(args)
   133  	if retry.IsAttemptsExceeded(err) {
   134  		return data, errors.Annotate(lastErr, "failed after retrying")
   135  	}
   136  	if err != nil {
   137  		return data, errors.Trace(err)
   138  	}
   139  
   140  	return data, nil
   141  }