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 }