~nskaggs/+junk/xenial-test

« back to all changes in this revision

Viewing changes to src/gopkg.in/juju/charmrepo.v2-unstable/legacy.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 2012, 2013 Canonical Ltd.
 
2
// Licensed under the AGPLv3, see LICENCE file for details.
 
3
 
 
4
package charmrepo // import "gopkg.in/juju/charmrepo.v2-unstable"
 
5
 
 
6
import (
 
7
        "encoding/json"
 
8
        "errors"
 
9
        "fmt"
 
10
        "io"
 
11
        "io/ioutil"
 
12
        "net"
 
13
        "net/http"
 
14
        "net/url"
 
15
        "os"
 
16
        "path/filepath"
 
17
        "strings"
 
18
 
 
19
        "github.com/juju/utils"
 
20
        "gopkg.in/juju/charm.v6-unstable"
 
21
)
 
22
 
 
23
// LegacyCharmStore is a repository Interface that provides access to the
 
24
// legacy Juju charm store.
 
25
type LegacyCharmStore struct {
 
26
        BaseURL   string
 
27
        authAttrs string // a list of attr=value pairs, comma separated
 
28
        jujuAttrs string // a list of attr=value pairs, comma separated
 
29
        testMode  bool
 
30
}
 
31
 
 
32
var _ Interface = (*LegacyCharmStore)(nil)
 
33
 
 
34
var LegacyStore = &LegacyCharmStore{BaseURL: "https://store.juju.ubuntu.com"}
 
35
 
 
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 {
 
39
        authCS := *s
 
40
        authCS.authAttrs = authAttrs
 
41
        return &authCS
 
42
}
 
43
 
 
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 {
 
47
        newRepo := *s
 
48
        newRepo.testMode = testMode
 
49
        return &newRepo
 
50
}
 
51
 
 
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 {
 
55
        jujuCS := *s
 
56
        jujuCS.jujuAttrs = jujuAttrs
 
57
        return &jujuCS
 
58
}
 
59
 
 
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)
 
63
        if err != nil {
 
64
                return nil, err
 
65
        }
 
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)
 
71
        }
 
72
        if s.jujuAttrs != "" {
 
73
                // The use of "X-" to prefix custom header values is deprecated.
 
74
                req.Header.Add("Juju-Metadata", s.jujuAttrs)
 
75
        }
 
76
        return http.DefaultClient.Do(req)
 
77
}
 
78
 
 
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)
 
82
        if err != nil {
 
83
                return nil, nil, err
 
84
        }
 
85
        if len(infos) == 0 {
 
86
                return nil, nil, fmt.Errorf("missing response when resolving charm URL: %q", ref)
 
87
        }
 
88
        if infos[0].CanonicalURL == "" {
 
89
                return nil, nil, fmt.Errorf("cannot resolve charm URL: %q", ref)
 
90
        }
 
91
        curl, err := charm.ParseURL(infos[0].CanonicalURL)
 
92
        if err != nil {
 
93
                return nil, nil, err
 
94
        }
 
95
        // Legacy store does not support returning the supported series.
 
96
        return curl, nil, nil
 
97
}
 
98
 
 
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())
 
105
        }
 
106
        if s.testMode {
 
107
                queryParams = append(queryParams, "stats=0")
 
108
        }
 
109
        resp, err := s.get(baseURL + strings.Join(queryParams, "&"))
 
110
        if err != nil {
 
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)
 
115
                        }
 
116
                }
 
117
                return nil, err
 
118
        }
 
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)
 
123
                if err != nil {
 
124
                        return nil, readErr
 
125
                }
 
126
                logger.Errorf("%v Response body: %s", errMsg, body)
 
127
                return nil, errMsg
 
128
        }
 
129
        body, err := ioutil.ReadAll(resp.Body)
 
130
        if err != nil {
 
131
                return nil, err
 
132
        }
 
133
        infos := make(map[string]*InfoResponse)
 
134
        if err = json.Unmarshal(body, &infos); err != nil {
 
135
                return nil, err
 
136
        }
 
137
        result := make([]*InfoResponse, len(curls))
 
138
        for i, curl := range curls {
 
139
                key := curl.String()
 
140
                info, found := infos[key]
 
141
                if !found {
 
142
                        return nil, fmt.Errorf("charm store returned response without charm %q", key)
 
143
                }
 
144
                if len(info.Errors) == 1 && info.Errors[0] == "entry not found" {
 
145
                        info.Errors[0] = fmt.Sprintf("charm not found: %s", curl)
 
146
                }
 
147
                result[i] = info
 
148
        }
 
149
        return result, nil
 
150
}
 
151
 
 
152
// Event returns details for a charm event in the charm store.
 
153
//
 
154
// If digest is empty, the latest event is returned.
 
155
func (s *LegacyCharmStore) Event(curl *charm.URL, digest string) (*EventResponse, error) {
 
156
        key := curl.String()
 
157
        query := key
 
158
        if digest != "" {
 
159
                query += "@" + digest
 
160
        }
 
161
        resp, err := s.get(s.BaseURL + "/charm-event?charms=" + url.QueryEscape(query))
 
162
        if err != nil {
 
163
                return nil, err
 
164
        }
 
165
        defer resp.Body.Close()
 
166
        body, err := ioutil.ReadAll(resp.Body)
 
167
        if err != nil {
 
168
                return nil, err
 
169
        }
 
170
        events := make(map[string]*EventResponse)
 
171
        if err = json.Unmarshal(body, &events); err != nil {
 
172
                return nil, err
 
173
        }
 
174
        event, found := events[key]
 
175
        if !found {
 
176
                return nil, fmt.Errorf("charm store returned response without charm %q", key)
 
177
        }
 
178
        if len(event.Errors) == 1 && event.Errors[0] == "entry not found" {
 
179
                if digest == "" {
 
180
                        return nil, &NotFoundError{fmt.Sprintf("charm event not found for %q", curl)}
 
181
                } else {
 
182
                        return nil, &NotFoundError{fmt.Sprintf("charm event not found for %q with digest %q", curl, digest)}
 
183
                }
 
184
        }
 
185
        return event, nil
 
186
}
 
187
 
 
188
// CharmRevision holds the revision number of a charm and any error
 
189
// encountered in retrieving it.
 
190
type CharmRevision struct {
 
191
        Revision int
 
192
        Sha256   string
 
193
        Err      error
 
194
}
 
195
 
 
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...)
 
199
        if err != nil {
 
200
                return nil, err
 
201
        }
 
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)
 
206
                }
 
207
                if info.Errors == nil {
 
208
                        revisions[i].Revision = info.Revision
 
209
                        revisions[i].Sha256 = info.Sha256
 
210
                } else {
 
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])
 
214
                        } else {
 
215
                                revisions[i].Err = fmt.Errorf("charm info errors for %q: %s", curls[i], strings.Join(info.Errors, "; "))
 
216
                        }
 
217
                }
 
218
        }
 
219
        return revisions, nil
 
220
}
 
221
 
 
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)
 
228
        }
 
229
        return s.revisions(baseCurls...)
 
230
}
 
231
 
 
232
// BranchLocation returns the location for the branch holding the charm at curl.
 
233
func (s *LegacyCharmStore) BranchLocation(curl *charm.URL) string {
 
234
        if curl.User != "" {
 
235
                return fmt.Sprintf("lp:~%s/charms/%s/%s/trunk", curl.User, curl.Series, curl.Name)
 
236
        }
 
237
        return fmt.Sprintf("lp:charms/%s/%s", curl.Series, curl.Name)
 
238
}
 
239
 
 
240
var branchPrefixes = []string{
 
241
        "lp:",
 
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/",
 
252
}
 
253
 
 
254
// CharmURL returns the charm URL for the branch at location.
 
255
func (s *LegacyCharmStore) CharmURL(location string) (*charm.URL, error) {
 
256
        var l string
 
257
        if len(location) > 0 && location[0] == '~' {
 
258
                l = location
 
259
        } else {
 
260
                for _, prefix := range branchPrefixes {
 
261
                        if strings.HasPrefix(location, prefix) {
 
262
                                l = location[len(prefix):]
 
263
                                break
 
264
                        }
 
265
                }
 
266
        }
 
267
        if l != "" {
 
268
                for len(l) > 0 && l[len(l)-1] == '/' {
 
269
                        l = l[:len(l)-1]
 
270
                }
 
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]))
 
274
                }
 
275
                if len(u) == 4 && u[0] == "charms" && u[3] == "trunk" {
 
276
                        return charm.ParseURL(fmt.Sprintf("cs:%s/%s", u[1], u[2]))
 
277
                }
 
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]))
 
280
                }
 
281
        }
 
282
        return nil, fmt.Errorf("unknown branch location: %q", location)
 
283
}
 
284
 
 
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)
 
289
        if err != nil {
 
290
                return err
 
291
        }
 
292
        if hash != digest {
 
293
                return fmt.Errorf("bad SHA256 of %q", path)
 
294
        }
 
295
        return nil
 
296
}
 
297
 
 
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.
 
302
        if CacheDir == "" {
 
303
                panic("charm cache directory path is empty")
 
304
        }
 
305
        if err := os.MkdirAll(CacheDir, os.FileMode(0755)); err != nil {
 
306
                return nil, err
 
307
        }
 
308
        revInfo, err := s.revisions(curl)
 
309
        if err != nil {
 
310
                return nil, err
 
311
        }
 
312
        if len(revInfo) != 1 {
 
313
                return nil, fmt.Errorf("expected 1 result, got %d", len(revInfo))
 
314
        }
 
315
        if revInfo[0].Err != nil {
 
316
                return nil, revInfo[0].Err
 
317
        }
 
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())
 
323
        }
 
324
        path := filepath.Join(CacheDir, charm.Quote(curl.String())+".charm")
 
325
        if verify(path, digest) != nil {
 
326
                store_url := s.BaseURL + "/charm/" + curl.Path()
 
327
                if s.testMode {
 
328
                        store_url = store_url + "?stats=0"
 
329
                }
 
330
                resp, err := s.get(store_url)
 
331
                if err != nil {
 
332
                        return nil, err
 
333
                }
 
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)
 
337
                }
 
338
                f, err := ioutil.TempFile(CacheDir, "charm-download")
 
339
                if err != nil {
 
340
                        return nil, err
 
341
                }
 
342
                dlPath := f.Name()
 
343
                _, err = io.Copy(f, resp.Body)
 
344
                if cerr := f.Close(); err == nil {
 
345
                        err = cerr
 
346
                }
 
347
                if err != nil {
 
348
                        os.Remove(dlPath)
 
349
                        return nil, err
 
350
                }
 
351
                if err := utils.ReplaceFile(dlPath, path); err != nil {
 
352
                        return nil, err
 
353
                }
 
354
        }
 
355
        if err := verify(path, digest); err != nil {
 
356
                return nil, err
 
357
        }
 
358
        return charm.ReadCharmArchive(path)
 
359
}
 
360
 
 
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")
 
364
}
 
365
 
 
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) {
 
369
        switch ref.Schema {
 
370
        case "cs":
 
371
                repo = LegacyStore
 
372
        case "local":
 
373
                if localRepoPath == "" {
 
374
                        return nil, errors.New("path to local repository not specified")
 
375
                }
 
376
                repo = &LocalRepository{Path: localRepoPath}
 
377
        default:
 
378
                return nil, fmt.Errorf("unknown schema for charm reference %q", ref)
 
379
        }
 
380
        return
 
381
}