github.com/axw/juju@v0.0.0-20161005053422-4bd6544d08d4/charmstore/client.go (about)

     1  // Copyright 2016 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package charmstore
     5  
     6  import (
     7  	"io"
     8  	"net/http"
     9  	"net/url"
    10  
    11  	"github.com/juju/errors"
    12  	"github.com/juju/loggo"
    13  	"gopkg.in/juju/charm.v6-unstable"
    14  	charmresource "gopkg.in/juju/charm.v6-unstable/resource"
    15  	"gopkg.in/juju/charmrepo.v2-unstable/csclient"
    16  	csparams "gopkg.in/juju/charmrepo.v2-unstable/csclient/params"
    17  	"gopkg.in/macaroon-bakery.v1/httpbakery"
    18  	"gopkg.in/macaroon.v1"
    19  )
    20  
    21  var logger = loggo.GetLogger("juju.charmstore")
    22  
    23  // TODO(natefinch): Ideally, this whole package would live in the
    24  // charmstore-client repo, so as to keep it near the API it wraps (and make it
    25  // more available to tools outside juju-core).
    26  
    27  // MacaroonCache represents a value that can store and retrieve macaroons for
    28  // charms.  It is used when we are requesting data from the charmstore for
    29  // private charms.
    30  type MacaroonCache interface {
    31  	Set(*charm.URL, macaroon.Slice) error
    32  	Get(*charm.URL) (macaroon.Slice, error)
    33  }
    34  
    35  // NewCachingClient returns a Juju charm store client that stores and retrieves
    36  // macaroons for calls in the given cache. If not nil, the client will use server
    37  // as the charmstore url, otherwise it will default to the standard juju
    38  // charmstore url.
    39  func NewCachingClient(cache MacaroonCache, server *url.URL) (Client, error) {
    40  	return newCachingClient(cache, server, makeWrapper)
    41  }
    42  
    43  func newCachingClient(
    44  	cache MacaroonCache,
    45  	server *url.URL,
    46  	makeWrapper func(*httpbakery.Client, *url.URL) csWrapper,
    47  ) (Client, error) {
    48  	bakeryClient := &httpbakery.Client{
    49  		Client: httpbakery.NewHTTPClient(),
    50  	}
    51  	client := makeWrapper(bakeryClient, server)
    52  	server, err := url.Parse(client.ServerURL())
    53  	if err != nil {
    54  		return Client{}, errors.Trace(err)
    55  	}
    56  	jar, err := newMacaroonJar(cache, server)
    57  	if err != nil {
    58  		return Client{}, errors.Trace(err)
    59  	}
    60  	bakeryClient.Jar = jar
    61  	return Client{client, jar}, nil
    62  }
    63  
    64  // TODO(natefinch): we really shouldn't let something like a bakeryclient
    65  // leak out of our abstraction like this. Instead, pass more salient details.
    66  
    67  // NewCustomClient returns a juju charmstore client that relies on the passed-in
    68  // httpbakery.Client to store and retrieve macaroons.  If not nil, the client
    69  // will use server as the charmstore url, otherwise it will default to the
    70  // standard juju charmstore url.
    71  func NewCustomClient(bakeryClient *httpbakery.Client, server *url.URL) (Client, error) {
    72  	return newCustomClient(bakeryClient, server, makeWrapper)
    73  }
    74  
    75  func newCustomClient(
    76  	bakeryClient *httpbakery.Client,
    77  	server *url.URL,
    78  	makeWrapper func(*httpbakery.Client, *url.URL) csWrapper,
    79  ) (Client, error) {
    80  	client := makeWrapper(bakeryClient, server)
    81  	return Client{csWrapper: client}, nil
    82  }
    83  
    84  func makeWrapper(bakeryClient *httpbakery.Client, server *url.URL) csWrapper {
    85  	p := csclient.Params{
    86  		BakeryClient: bakeryClient,
    87  	}
    88  	if server != nil {
    89  		p.URL = server.String()
    90  	}
    91  	return csclientImpl{csclient.New(p)}
    92  }
    93  
    94  // Client wraps charmrepo/csclient (the charm store's API client
    95  // library) in a higher level API.
    96  type Client struct {
    97  	csWrapper
    98  	jar *macaroonJar
    99  }
   100  
   101  // CharmRevision holds the data returned from the charmstore about the latest
   102  // revision of a charm.  Notet hat this may be different per channel.
   103  type CharmRevision struct {
   104  	// Revision is newest revision for the charm.
   105  	Revision int
   106  
   107  	// Err holds any error that occurred while making the request.
   108  	Err error
   109  }
   110  
   111  // LatestRevisions returns the latest revisions of the given charms, using the given metadata.
   112  func (c Client) LatestRevisions(charms []CharmID, metadata map[string][]string) ([]CharmRevision, error) {
   113  	// Due to the fact that we cannot use multiple macaroons per API call,
   114  	// we need to perform one call at a time, rather than making bulk calls.
   115  	// We could bulk the calls that use non-private charms, but we'd still need
   116  	// to do one bulk call per channel, due to how channels are used by the
   117  	// underlying csclient.
   118  	results := make([]CharmRevision, len(charms))
   119  	for i, cid := range charms {
   120  		revisions, err := c.csWrapper.Latest(cid.Channel, []*charm.URL{cid.URL}, metadata)
   121  		if err != nil {
   122  			return nil, errors.Trace(err)
   123  		}
   124  		rev := revisions[0]
   125  		results[i] = CharmRevision{Revision: rev.Revision, Err: rev.Err}
   126  	}
   127  	return results, nil
   128  }
   129  
   130  func (c Client) latestRevisions(channel csparams.Channel, cid CharmID, metadata map[string][]string) (CharmRevision, error) {
   131  	if err := c.jar.Activate(cid.URL); err != nil {
   132  		return CharmRevision{}, errors.Trace(err)
   133  	}
   134  	defer c.jar.Deactivate()
   135  	revisions, err := c.csWrapper.Latest(cid.Channel, []*charm.URL{cid.URL}, metadata)
   136  	if err != nil {
   137  		return CharmRevision{}, errors.Trace(err)
   138  	}
   139  	rev := revisions[0]
   140  	return CharmRevision{Revision: rev.Revision, Err: rev.Err}, nil
   141  }
   142  
   143  // ResourceRequest is the data needed to request a resource from the charmstore.
   144  type ResourceRequest struct {
   145  	// Charm is the URL of the charm for which we're requesting a resource.
   146  	Charm *charm.URL
   147  
   148  	// Channel is the channel from which to request the resource info.
   149  	Channel csparams.Channel
   150  
   151  	// Name is the name of the resource we're asking about.
   152  	Name string
   153  
   154  	// Revision is the specific revision of the resource we're asking about.
   155  	Revision int
   156  }
   157  
   158  // ResourceData represents the response from the charmstore about a request for
   159  // resource bytes.
   160  type ResourceData struct {
   161  	// ReadCloser holds the bytes for the resource.
   162  	io.ReadCloser
   163  
   164  	// Resource holds the metadata for the resource.
   165  	Resource charmresource.Resource
   166  }
   167  
   168  // GetResource returns the data (bytes) and metadata for a resource from the charmstore.
   169  func (c Client) GetResource(req ResourceRequest) (data ResourceData, err error) {
   170  	if err := c.jar.Activate(req.Charm); err != nil {
   171  		return ResourceData{}, errors.Trace(err)
   172  	}
   173  	defer c.jar.Deactivate()
   174  	meta, err := c.csWrapper.ResourceMeta(req.Channel, req.Charm, req.Name, req.Revision)
   175  
   176  	if err != nil {
   177  		return ResourceData{}, errors.Trace(err)
   178  	}
   179  	data.Resource, err = csparams.API2Resource(meta)
   180  	if err != nil {
   181  		return ResourceData{}, errors.Trace(err)
   182  	}
   183  	resData, err := c.csWrapper.GetResource(req.Channel, req.Charm, req.Name, req.Revision)
   184  	if err != nil {
   185  		return ResourceData{}, errors.Trace(err)
   186  	}
   187  	defer func() {
   188  		if err != nil {
   189  			resData.Close()
   190  		}
   191  	}()
   192  	data.ReadCloser = resData.ReadCloser
   193  	fpHash := data.Resource.Fingerprint.String()
   194  	if resData.Hash != fpHash {
   195  		return ResourceData{},
   196  			errors.Errorf("fingerprint for data (%s) does not match fingerprint in metadata (%s)", resData.Hash, fpHash)
   197  	}
   198  	if resData.Size != data.Resource.Size {
   199  		return ResourceData{},
   200  			errors.Errorf("size for data (%d) does not match size in metadata (%d)", resData.Size, data.Resource.Size)
   201  	}
   202  	return data, nil
   203  }
   204  
   205  // ResourceInfo returns the metadata for the given resource from the charmstore.
   206  func (c Client) ResourceInfo(req ResourceRequest) (charmresource.Resource, error) {
   207  	if err := c.jar.Activate(req.Charm); err != nil {
   208  		return charmresource.Resource{}, errors.Trace(err)
   209  	}
   210  	defer c.jar.Deactivate()
   211  	meta, err := c.csWrapper.ResourceMeta(req.Channel, req.Charm, req.Name, req.Revision)
   212  	if err != nil {
   213  		return charmresource.Resource{}, errors.Trace(err)
   214  	}
   215  	res, err := csparams.API2Resource(meta)
   216  	if err != nil {
   217  		return charmresource.Resource{}, errors.Trace(err)
   218  	}
   219  	return res, nil
   220  }
   221  
   222  // ListResources returns a list of resources for each of the given charms.
   223  func (c Client) ListResources(charms []CharmID) ([][]charmresource.Resource, error) {
   224  	results := make([][]charmresource.Resource, len(charms))
   225  	for i, ch := range charms {
   226  		res, err := c.listResources(ch)
   227  		if err != nil {
   228  			if csclient.IsAuthorizationError(err) || errors.Cause(err) == csparams.ErrNotFound {
   229  				// Ignore authorization errors and not-found errors so we get some results
   230  				// even if others fail.
   231  				continue
   232  			}
   233  			return nil, errors.Trace(err)
   234  		}
   235  		results[i] = res
   236  	}
   237  	return results, nil
   238  }
   239  
   240  func (c Client) listResources(ch CharmID) ([]charmresource.Resource, error) {
   241  	if err := c.jar.Activate(ch.URL); err != nil {
   242  		return nil, errors.Trace(err)
   243  	}
   244  	defer c.jar.Deactivate()
   245  	resources, err := c.csWrapper.ListResources(ch.Channel, ch.URL)
   246  	if err != nil {
   247  		return nil, errors.Trace(err)
   248  	}
   249  	return api2resources(resources)
   250  }
   251  
   252  // csWrapper is a type that abstracts away the low-level implementation details
   253  // of the charmstore client.
   254  type csWrapper interface {
   255  	Latest(channel csparams.Channel, ids []*charm.URL, headers map[string][]string) ([]csparams.CharmRevision, error)
   256  	ListResources(channel csparams.Channel, id *charm.URL) ([]csparams.Resource, error)
   257  	GetResource(channel csparams.Channel, id *charm.URL, name string, revision int) (csclient.ResourceData, error)
   258  	ResourceMeta(channel csparams.Channel, id *charm.URL, name string, revision int) (csparams.Resource, error)
   259  	ServerURL() string
   260  }
   261  
   262  // csclientImpl is an implementation of csWrapper that uses csclient.Client.
   263  // It exists for testing purposes to hide away the hard-to-mock parts of
   264  // csclient.Client.
   265  type csclientImpl struct {
   266  	*csclient.Client
   267  }
   268  
   269  // Latest gets the latest CharmRevisions for the charm URLs on the channel.
   270  func (c csclientImpl) Latest(channel csparams.Channel, ids []*charm.URL, metadata map[string][]string) ([]csparams.CharmRevision, error) {
   271  	client := c.WithChannel(channel)
   272  	client.SetHTTPHeader(http.Header(metadata))
   273  	return client.Latest(ids)
   274  }
   275  
   276  // ListResources gets the latest resources for the charm URL on the channel.
   277  func (c csclientImpl) ListResources(channel csparams.Channel, id *charm.URL) ([]csparams.Resource, error) {
   278  	client := c.WithChannel(channel)
   279  	return client.ListResources(id)
   280  }
   281  
   282  // Getresource downloads the bytes and some metadata about the bytes for the revisioned resource.
   283  func (c csclientImpl) GetResource(channel csparams.Channel, id *charm.URL, name string, revision int) (csclient.ResourceData, error) {
   284  	client := c.WithChannel(channel)
   285  	return client.GetResource(id, name, revision)
   286  }
   287  
   288  // ResourceInfo gets the full metadata for the revisioned resource.
   289  func (c csclientImpl) ResourceMeta(channel csparams.Channel, id *charm.URL, name string, revision int) (csparams.Resource, error) {
   290  	client := c.WithChannel(channel)
   291  	return client.ResourceMeta(id, name, revision)
   292  }
   293  
   294  func api2resources(res []csparams.Resource) ([]charmresource.Resource, error) {
   295  	result := make([]charmresource.Resource, len(res))
   296  	for i, r := range res {
   297  		var err error
   298  		result[i], err = csparams.API2Resource(r)
   299  		if err != nil {
   300  			return nil, errors.Trace(err)
   301  		}
   302  	}
   303  	return result, nil
   304  }