1
// Copyright 2012, 2013 Canonical Ltd.
2
// Licensed under the AGPLv3, see LICENCE file for details.
4
package charmrepo // import "gopkg.in/juju/charmrepo.v2-unstable"
19
"github.com/juju/utils"
20
"gopkg.in/juju/charm.v6-unstable"
23
// LegacyCharmStore is a repository Interface that provides access to the
24
// legacy Juju charm store.
25
type LegacyCharmStore struct {
27
authAttrs string // a list of attr=value pairs, comma separated
28
jujuAttrs string // a list of attr=value pairs, comma separated
32
var _ Interface = (*LegacyCharmStore)(nil)
34
var LegacyStore = &LegacyCharmStore{BaseURL: "https://store.juju.ubuntu.com"}
36
// WithAuthAttrs return a repository Interface with the authentication token
37
// list set. authAttrs is a list of attr=value pairs.
38
func (s *LegacyCharmStore) WithAuthAttrs(authAttrs string) Interface {
40
authCS.authAttrs = authAttrs
44
// WithTestMode returns a repository Interface where testMode is set to value
45
// passed to this method.
46
func (s *LegacyCharmStore) WithTestMode(testMode bool) Interface {
48
newRepo.testMode = testMode
52
// WithJujuAttrs returns a repository Interface with the Juju metadata
53
// attributes set. jujuAttrs is a list of attr=value pairs.
54
func (s *LegacyCharmStore) WithJujuAttrs(jujuAttrs string) Interface {
56
jujuCS.jujuAttrs = jujuAttrs
60
// Perform an http get, adding custom auth header if necessary.
61
func (s *LegacyCharmStore) get(url string) (resp *http.Response, err error) {
62
req, err := http.NewRequest("GET", url, nil)
66
if s.authAttrs != "" {
67
// To comply with RFC 2617, we send the authentication data in
68
// the Authorization header with a custom auth scheme
69
// and the authentication attributes.
70
req.Header.Add("Authorization", "charmstore "+s.authAttrs)
72
if s.jujuAttrs != "" {
73
// The use of "X-" to prefix custom header values is deprecated.
74
req.Header.Add("Juju-Metadata", s.jujuAttrs)
76
return http.DefaultClient.Do(req)
79
// Resolve canonicalizes charm URLs any implied series in the reference.
80
func (s *LegacyCharmStore) Resolve(ref *charm.URL) (*charm.URL, []string, error) {
81
infos, err := s.Info(ref)
86
return nil, nil, fmt.Errorf("missing response when resolving charm URL: %q", ref)
88
if infos[0].CanonicalURL == "" {
89
return nil, nil, fmt.Errorf("cannot resolve charm URL: %q", ref)
91
curl, err := charm.ParseURL(infos[0].CanonicalURL)
95
// Legacy store does not support returning the supported series.
99
// Info returns details for all the specified charms in the charm store.
100
func (s *LegacyCharmStore) Info(curls ...charm.Location) ([]*InfoResponse, error) {
101
baseURL := s.BaseURL + "/charm-info?"
102
queryParams := make([]string, len(curls), len(curls)+1)
103
for i, curl := range curls {
104
queryParams[i] = "charms=" + url.QueryEscape(curl.String())
107
queryParams = append(queryParams, "stats=0")
109
resp, err := s.get(baseURL + strings.Join(queryParams, "&"))
111
if url_error, ok := err.(*url.Error); ok {
112
switch url_error.Err.(type) {
113
case *net.DNSError, *net.OpError:
114
return nil, fmt.Errorf("Cannot access the charm store. Are you connected to the internet? Error details: %v", err)
119
defer resp.Body.Close()
120
if resp.StatusCode != 200 {
121
errMsg := fmt.Errorf("Cannot access the charm store. Invalid response code: %q", resp.Status)
122
body, readErr := ioutil.ReadAll(resp.Body)
126
logger.Errorf("%v Response body: %s", errMsg, body)
129
body, err := ioutil.ReadAll(resp.Body)
133
infos := make(map[string]*InfoResponse)
134
if err = json.Unmarshal(body, &infos); err != nil {
137
result := make([]*InfoResponse, len(curls))
138
for i, curl := range curls {
140
info, found := infos[key]
142
return nil, fmt.Errorf("charm store returned response without charm %q", key)
144
if len(info.Errors) == 1 && info.Errors[0] == "entry not found" {
145
info.Errors[0] = fmt.Sprintf("charm not found: %s", curl)
152
// Event returns details for a charm event in the charm store.
154
// If digest is empty, the latest event is returned.
155
func (s *LegacyCharmStore) Event(curl *charm.URL, digest string) (*EventResponse, error) {
159
query += "@" + digest
161
resp, err := s.get(s.BaseURL + "/charm-event?charms=" + url.QueryEscape(query))
165
defer resp.Body.Close()
166
body, err := ioutil.ReadAll(resp.Body)
170
events := make(map[string]*EventResponse)
171
if err = json.Unmarshal(body, &events); err != nil {
174
event, found := events[key]
176
return nil, fmt.Errorf("charm store returned response without charm %q", key)
178
if len(event.Errors) == 1 && event.Errors[0] == "entry not found" {
180
return nil, &NotFoundError{fmt.Sprintf("charm event not found for %q", curl)}
182
return nil, &NotFoundError{fmt.Sprintf("charm event not found for %q with digest %q", curl, digest)}
188
// CharmRevision holds the revision number of a charm and any error
189
// encountered in retrieving it.
190
type CharmRevision struct {
196
// revisions returns the revisions of the charms referenced by curls.
197
func (s *LegacyCharmStore) revisions(curls ...charm.Location) (revisions []CharmRevision, err error) {
198
infos, err := s.Info(curls...)
202
revisions = make([]CharmRevision, len(infos))
203
for i, info := range infos {
204
for _, w := range info.Warnings {
205
logger.Warningf("charm store reports for %q: %s", curls[i], w)
207
if info.Errors == nil {
208
revisions[i].Revision = info.Revision
209
revisions[i].Sha256 = info.Sha256
211
// If a charm is not found, we are more concise with the error message.
212
if len(info.Errors) == 1 && strings.HasPrefix(info.Errors[0], "charm not found") {
213
revisions[i].Err = fmt.Errorf(info.Errors[0])
215
revisions[i].Err = fmt.Errorf("charm info errors for %q: %s", curls[i], strings.Join(info.Errors, "; "))
219
return revisions, nil
222
// Latest returns the latest revision of the charms referenced by curls, regardless
223
// of the revision set on each curl.
224
func (s *LegacyCharmStore) Latest(curls ...*charm.URL) ([]CharmRevision, error) {
225
baseCurls := make([]charm.Location, len(curls))
226
for i, curl := range curls {
227
baseCurls[i] = curl.WithRevision(-1)
229
return s.revisions(baseCurls...)
232
// BranchLocation returns the location for the branch holding the charm at curl.
233
func (s *LegacyCharmStore) BranchLocation(curl *charm.URL) string {
235
return fmt.Sprintf("lp:~%s/charms/%s/%s/trunk", curl.User, curl.Series, curl.Name)
237
return fmt.Sprintf("lp:charms/%s/%s", curl.Series, curl.Name)
240
var branchPrefixes = []string{
242
"bzr+ssh://bazaar.launchpad.net/+branch/",
243
"bzr+ssh://bazaar.launchpad.net/",
244
"http://launchpad.net/+branch/",
245
"http://launchpad.net/",
246
"https://launchpad.net/+branch/",
247
"https://launchpad.net/",
248
"http://code.launchpad.net/+branch/",
249
"http://code.launchpad.net/",
250
"https://code.launchpad.net/+branch/",
251
"https://code.launchpad.net/",
254
// CharmURL returns the charm URL for the branch at location.
255
func (s *LegacyCharmStore) CharmURL(location string) (*charm.URL, error) {
257
if len(location) > 0 && location[0] == '~' {
260
for _, prefix := range branchPrefixes {
261
if strings.HasPrefix(location, prefix) {
262
l = location[len(prefix):]
268
for len(l) > 0 && l[len(l)-1] == '/' {
271
u := strings.Split(l, "/")
272
if len(u) == 3 && u[0] == "charms" {
273
return charm.ParseURL(fmt.Sprintf("cs:%s/%s", u[1], u[2]))
275
if len(u) == 4 && u[0] == "charms" && u[3] == "trunk" {
276
return charm.ParseURL(fmt.Sprintf("cs:%s/%s", u[1], u[2]))
278
if len(u) == 5 && u[1] == "charms" && u[4] == "trunk" && len(u[0]) > 0 && u[0][0] == '~' {
279
return charm.ParseURL(fmt.Sprintf("cs:%s/%s/%s", u[0], u[2], u[3]))
282
return nil, fmt.Errorf("unknown branch location: %q", location)
285
// verify returns an error unless a file exists at path with a hex-encoded
286
// SHA256 matching digest.
287
func verify(path, digest string) error {
288
hash, _, err := utils.ReadFileSHA256(path)
293
return fmt.Errorf("bad SHA256 of %q", path)
298
// Get returns the charm referenced by curl.
299
// CacheDir must have been set, otherwise Get will panic.
300
func (s *LegacyCharmStore) Get(curl *charm.URL) (charm.Charm, error) {
301
// The cache location must have been previously set.
303
panic("charm cache directory path is empty")
305
if err := os.MkdirAll(CacheDir, os.FileMode(0755)); err != nil {
308
revInfo, err := s.revisions(curl)
312
if len(revInfo) != 1 {
313
return nil, fmt.Errorf("expected 1 result, got %d", len(revInfo))
315
if revInfo[0].Err != nil {
316
return nil, revInfo[0].Err
318
rev, digest := revInfo[0].Revision, revInfo[0].Sha256
319
if curl.Revision == -1 {
320
curl = curl.WithRevision(rev)
321
} else if curl.Revision != rev {
322
return nil, fmt.Errorf("store returned charm with wrong revision %d for %q", rev, curl.String())
324
path := filepath.Join(CacheDir, charm.Quote(curl.String())+".charm")
325
if verify(path, digest) != nil {
326
store_url := s.BaseURL + "/charm/" + curl.Path()
328
store_url = store_url + "?stats=0"
330
resp, err := s.get(store_url)
334
defer resp.Body.Close()
335
if resp.StatusCode != http.StatusOK {
336
return nil, fmt.Errorf("bad status from request for %q: %q", store_url, resp.Status)
338
f, err := ioutil.TempFile(CacheDir, "charm-download")
343
_, err = io.Copy(f, resp.Body)
344
if cerr := f.Close(); err == nil {
351
if err := utils.ReplaceFile(dlPath, path); err != nil {
355
if err := verify(path, digest); err != nil {
358
return charm.ReadCharmArchive(path)
361
// GetBundle is only defined for implementing Interface.
362
func (s *LegacyCharmStore) GetBundle(curl *charm.URL) (charm.Bundle, error) {
363
return nil, errors.New("not implemented: legacy API does not support bundles")
366
// LegacyInferRepository returns a charm repository inferred from the provided
367
// charm or bundle reference. Local references will use the provided path.
368
func LegacyInferRepository(ref *charm.URL, localRepoPath string) (repo Interface, err error) {
373
if localRepoPath == "" {
374
return nil, errors.New("path to local repository not specified")
376
repo = &LocalRepository{Path: localRepoPath}
378
return nil, fmt.Errorf("unknown schema for charm reference %q", ref)