~nskaggs/+junk/xenial-test

« back to all changes in this revision

Viewing changes to src/github.com/juju/juju/charmstore/client.go

  • Committer: Nicholas Skaggs
  • Date: 2016-10-24 20:56:05 UTC
  • Revision ID: nicholas.skaggs@canonical.com-20161024205605-z8lta0uvuhtxwzwl
Initi with beta15

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
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
}