1
// Copyright 2016 Canonical Ltd.
2
// Licensed under the AGPLv3, see LICENCE file for details.
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"
21
var logger = loggo.GetLogger("juju.charmstore")
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).
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
30
type MacaroonCache interface {
31
Set(*charm.URL, macaroon.Slice) error
32
Get(*charm.URL) (macaroon.Slice, error)
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
39
func NewCachingClient(cache MacaroonCache, server *url.URL) (Client, error) {
40
return newCachingClient(cache, server, makeWrapper)
43
func newCachingClient(
46
makeWrapper func(*httpbakery.Client, *url.URL) csWrapper,
48
bakeryClient := &httpbakery.Client{
49
Client: httpbakery.NewHTTPClient(),
51
client := makeWrapper(bakeryClient, server)
52
server, err := url.Parse(client.ServerURL())
54
return Client{}, errors.Trace(err)
56
jar, err := newMacaroonJar(cache, server)
58
return Client{}, errors.Trace(err)
60
bakeryClient.Jar = jar
61
return Client{client, jar}, nil
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.
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)
76
bakeryClient *httpbakery.Client,
78
makeWrapper func(*httpbakery.Client, *url.URL) csWrapper,
80
client := makeWrapper(bakeryClient, server)
81
return Client{csWrapper: client}, nil
84
func makeWrapper(bakeryClient *httpbakery.Client, server *url.URL) csWrapper {
86
BakeryClient: bakeryClient,
89
p.URL = server.String()
91
return csclientImpl{csclient.New(p)}
94
// Client wraps charmrepo/csclient (the charm store's API client
95
// library) in a higher level API.
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.
107
// Err holds any error that occurred while making the request.
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)
122
return nil, errors.Trace(err)
125
results[i] = CharmRevision{Revision: rev.Revision, Err: rev.Err}
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)
134
defer c.jar.Deactivate()
135
revisions, err := c.csWrapper.Latest(cid.Channel, []*charm.URL{cid.URL}, metadata)
137
return CharmRevision{}, errors.Trace(err)
140
return CharmRevision{Revision: rev.Revision, Err: rev.Err}, nil
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.
148
// Channel is the channel from which to request the resource info.
149
Channel csparams.Channel
151
// Name is the name of the resource we're asking about.
154
// Revision is the specific revision of the resource we're asking about.
158
// ResourceData represents the response from the charmstore about a request for
160
type ResourceData struct {
161
// ReadCloser holds the bytes for the resource.
164
// Resource holds the metadata for the resource.
165
Resource charmresource.Resource
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)
173
defer c.jar.Deactivate()
174
meta, err := c.csWrapper.ResourceMeta(req.Channel, req.Charm, req.Name, req.Revision)
177
return ResourceData{}, errors.Trace(err)
179
data.Resource, err = csparams.API2Resource(meta)
181
return ResourceData{}, errors.Trace(err)
183
resData, err := c.csWrapper.GetResource(req.Channel, req.Charm, req.Name, req.Revision)
185
return ResourceData{}, errors.Trace(err)
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)
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)
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)
210
defer c.jar.Deactivate()
211
meta, err := c.csWrapper.ResourceMeta(req.Channel, req.Charm, req.Name, req.Revision)
213
return charmresource.Resource{}, errors.Trace(err)
215
res, err := csparams.API2Resource(meta)
217
return charmresource.Resource{}, errors.Trace(err)
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)
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.
233
return nil, errors.Trace(err)
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)
244
defer c.jar.Deactivate()
245
resources, err := c.csWrapper.ListResources(ch.Channel, ch.URL)
247
return nil, errors.Trace(err)
249
return api2resources(resources)
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)
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
265
type csclientImpl struct {
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)
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)
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)
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)
294
func api2resources(res []csparams.Resource) ([]charmresource.Resource, error) {
295
result := make([]charmresource.Resource, len(res))
296
for i, r := range res {
298
result[i], err = csparams.API2Resource(r)
300
return nil, errors.Trace(err)