~nskaggs/+junk/xenial-test

« back to all changes in this revision

Viewing changes to src/gopkg.in/juju/charmstore.v5-unstable/internal/v4/api.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 2015 Canonical Ltd.
 
2
// Licensed under the AGPLv3, see LICENCE file for details.
 
3
 
 
4
package v4 // import "gopkg.in/juju/charmstore.v5-unstable/internal/v4"
 
5
 
 
6
import (
 
7
        "net/http"
 
8
        "net/url"
 
9
 
 
10
        "github.com/juju/httprequest"
 
11
        "github.com/juju/loggo"
 
12
        "github.com/juju/mempool"
 
13
        "gopkg.in/errgo.v1"
 
14
        "gopkg.in/juju/charm.v6-unstable"
 
15
        "gopkg.in/juju/charmrepo.v2-unstable/csclient/params"
 
16
        "gopkg.in/mgo.v2"
 
17
        "gopkg.in/mgo.v2/bson"
 
18
 
 
19
        "gopkg.in/juju/charmstore.v5-unstable/internal/charmstore"
 
20
        "gopkg.in/juju/charmstore.v5-unstable/internal/entitycache"
 
21
        "gopkg.in/juju/charmstore.v5-unstable/internal/mongodoc"
 
22
        "gopkg.in/juju/charmstore.v5-unstable/internal/router"
 
23
        "gopkg.in/juju/charmstore.v5-unstable/internal/v5"
 
24
)
 
25
 
 
26
var logger = loggo.GetLogger("charmstore.internal.v4")
 
27
 
 
28
const (
 
29
        PromulgatorsGroup         = v5.PromulgatorsGroup
 
30
        UsernameAttr              = v5.UsernameAttr
 
31
        DelegatableMacaroonExpiry = v5.DelegatableMacaroonExpiry
 
32
        DefaultIcon               = v5.DefaultIcon
 
33
        ArchiveCachePublicMaxAge  = v5.ArchiveCachePublicMaxAge
 
34
)
 
35
 
 
36
// reqHandlerPool holds a cache of ReqHandlers to save
 
37
// on allocation time. When a handler is done with,
 
38
// it is put back into the pool.
 
39
var reqHandlerPool = mempool.Pool{
 
40
        New: func() interface{} {
 
41
                return newReqHandler()
 
42
        },
 
43
}
 
44
 
 
45
type Handler struct {
 
46
        *v5.Handler
 
47
}
 
48
 
 
49
type ReqHandler struct {
 
50
        *v5.ReqHandler
 
51
}
 
52
 
 
53
func New(pool *charmstore.Pool, config charmstore.ServerParams, rootPath string) Handler {
 
54
        return Handler{
 
55
                Handler: v5.New(pool, config, rootPath),
 
56
        }
 
57
}
 
58
 
 
59
func (h Handler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
 
60
        rh, err := h.NewReqHandler(req)
 
61
        if err != nil {
 
62
                router.WriteError(w, err)
 
63
                return
 
64
        }
 
65
        defer rh.Close()
 
66
        rh.ServeHTTP(w, req)
 
67
}
 
68
 
 
69
func NewAPIHandler(pool *charmstore.Pool, config charmstore.ServerParams, rootPath string) charmstore.HTTPCloseHandler {
 
70
        return New(pool, config, rootPath)
 
71
}
 
72
 
 
73
// The v4 resolvedURL function also requires SupportedSeries.
 
74
var requiredEntityFields = func() map[string]int {
 
75
        fields := make(map[string]int)
 
76
        for f := range v5.RequiredEntityFields {
 
77
                fields[f] = 1
 
78
        }
 
79
        fields["supportedseries"] = 1
 
80
        return fields
 
81
}()
 
82
 
 
83
// NewReqHandler returns an instance of a *ReqHandler
 
84
// suitable for handling the given HTTP request. After use, the ReqHandler.Close
 
85
// method should be called to close it.
 
86
//
 
87
// If no handlers are available, it returns an error with
 
88
// a charmstore.ErrTooManySessions cause.
 
89
func (h *Handler) NewReqHandler(req *http.Request) (ReqHandler, error) {
 
90
        req.ParseForm()
 
91
        // Validate all the values for channel, even though
 
92
        // most endpoints will only ever use the first one.
 
93
        // PUT to an archive is the notable exception.
 
94
        // TODO Why is the v4 API accepting a channel parameter anyway? We
 
95
        // should probably always use "stable".
 
96
        for _, ch := range req.Form["channel"] {
 
97
                if !v5.ValidChannels[params.Channel(ch)] {
 
98
                        return ReqHandler{}, badRequestf(nil, "invalid channel %q specified in request", ch)
 
99
                }
 
100
        }
 
101
        store, err := h.Pool.RequestStore()
 
102
        if err != nil {
 
103
                if errgo.Cause(err) == charmstore.ErrTooManySessions {
 
104
                        return ReqHandler{}, errgo.WithCausef(err, params.ErrServiceUnavailable, "")
 
105
                }
 
106
                return ReqHandler{}, errgo.Mask(err)
 
107
        }
 
108
        rh := reqHandlerPool.Get().(ReqHandler)
 
109
        rh.Handler = h.Handler
 
110
        rh.Store = &v5.StoreWithChannel{
 
111
                Store:   store,
 
112
                Channel: params.Channel(req.Form.Get("channel")),
 
113
        }
 
114
        rh.Cache = entitycache.New(rh.Store)
 
115
        rh.Cache.AddEntityFields(requiredEntityFields)
 
116
        rh.Cache.AddBaseEntityFields(v5.RequiredBaseEntityFields)
 
117
        return rh, nil
 
118
}
 
119
 
 
120
func newReqHandler() ReqHandler {
 
121
        h := ReqHandler{
 
122
                ReqHandler: new(v5.ReqHandler),
 
123
        }
 
124
        resolveId := h.ResolvedIdHandler
 
125
        authId := h.AuthIdHandler
 
126
        handlers := v5.RouterHandlers(h.ReqHandler)
 
127
        handlers.Global["search"] = router.HandleJSON(h.serveSearch)
 
128
        handlers.Meta["charm-related"] = h.EntityHandler(h.metaCharmRelated, "charmprovidedinterfaces", "charmrequiredinterfaces")
 
129
        handlers.Meta["charm-metadata"] = h.EntityHandler(h.metaCharmMetadata, "charmmeta")
 
130
        handlers.Meta["revision-info"] = router.SingleIncludeHandler(h.metaRevisionInfo)
 
131
        handlers.Meta["archive-size"] = h.EntityHandler(h.metaArchiveSize, "prev5blobsize")
 
132
        handlers.Meta["hash"] = h.EntityHandler(h.metaHash, "prev5blobhash")
 
133
        handlers.Meta["hash256"] = h.EntityHandler(h.metaHash256, "prev5blobhash256")
 
134
        handlers.Id["expand-id"] = resolveId(authId(h.serveExpandId))
 
135
        handlers.Id["archive"] = h.serveArchive(handlers.Id["archive"])
 
136
        handlers.Id["archive/"] = resolveId(authId(h.serveArchiveFile))
 
137
 
 
138
        // Delete new endpoints that we don't want to provide in v4.
 
139
        delete(handlers.Id, "publish")
 
140
        delete(handlers.Meta, "published")
 
141
        delete(handlers.Id, "resource")
 
142
        delete(handlers.Meta, "resources")
 
143
        delete(handlers.Meta, "resources/")
 
144
        delete(handlers.Meta, "can-ingest")
 
145
 
 
146
        h.Router = router.New(handlers, h)
 
147
        return h
 
148
}
 
149
 
 
150
// ResolveURL implements router.Context.ResolveURL,
 
151
// ensuring that any resulting ResolvedURL always
 
152
// has a non-empty PreferredSeries field.
 
153
func (h ReqHandler) ResolveURL(url *charm.URL) (*router.ResolvedURL, error) {
 
154
        return resolveURL(h.Cache, url)
 
155
}
 
156
 
 
157
func (h ReqHandler) ResolveURLs(urls []*charm.URL) ([]*router.ResolvedURL, error) {
 
158
        h.Cache.StartFetch(urls)
 
159
        rurls := make([]*router.ResolvedURL, len(urls))
 
160
        for i, url := range urls {
 
161
                var err error
 
162
                rurls[i], err = resolveURL(h.Cache, url)
 
163
                if err != nil && errgo.Cause(err) != params.ErrNotFound {
 
164
                        return nil, err
 
165
                }
 
166
        }
 
167
        return rurls, nil
 
168
}
 
169
 
 
170
// resolveURL implements URL resolving for the ReqHandler.
 
171
// It's defined as a separate function so it can be more
 
172
// easily unit-tested.
 
173
func resolveURL(cache *entitycache.Cache, url *charm.URL) (*router.ResolvedURL, error) {
 
174
        entity, err := cache.Entity(url, charmstore.FieldSelector("supportedseries"))
 
175
        if err != nil {
 
176
                return nil, errgo.Mask(err, errgo.Is(params.ErrNotFound))
 
177
        }
 
178
        rurl := &router.ResolvedURL{
 
179
                URL:                 *entity.URL,
 
180
                PromulgatedRevision: -1,
 
181
        }
 
182
        if url.User == "" {
 
183
                rurl.PromulgatedRevision = entity.PromulgatedRevision
 
184
        }
 
185
        if rurl.URL.Series != "" {
 
186
                return rurl, nil
 
187
        }
 
188
        if url.Series != "" {
 
189
                rurl.PreferredSeries = url.Series
 
190
                return rurl, nil
 
191
        }
 
192
        if len(entity.SupportedSeries) == 0 {
 
193
                return nil, errgo.Newf("entity %q has no supported series", &rurl.URL)
 
194
        }
 
195
        rurl.PreferredSeries = entity.SupportedSeries[0]
 
196
        return rurl, nil
 
197
}
 
198
 
 
199
// Close closes the ReqHandler. This should always be called when the
 
200
// ReqHandler is done with.
 
201
func (h ReqHandler) Close() {
 
202
        h.Store.Close()
 
203
        h.Cache.Close()
 
204
        h.Reset()
 
205
        reqHandlerPool.Put(h)
 
206
}
 
207
 
 
208
// StatsEnabled reports whether statistics should be gathered for
 
209
// the given HTTP request.
 
210
func StatsEnabled(req *http.Request) bool {
 
211
        return v5.StatsEnabled(req)
 
212
}
 
213
 
 
214
// GET id/meta/charm-metadata
 
215
// https://github.com/juju/charmstore/blob/v4/docs/API.md#get-idmetacharm-metadata
 
216
func (h ReqHandler) metaCharmMetadata(entity *mongodoc.Entity, id *router.ResolvedURL, path string, flags url.Values, req *http.Request) (interface{}, error) {
 
217
        m := entity.CharmMeta
 
218
        if m != nil {
 
219
                m.Series = nil
 
220
        }
 
221
        return m, nil
 
222
}
 
223
 
 
224
// GET id/meta/revision-info
 
225
// https://github.com/juju/charmstore/blob/v4/docs/API.md#get-idmetarevision-info
 
226
func (h ReqHandler) metaRevisionInfo(id *router.ResolvedURL, path string, flags url.Values, req *http.Request) (interface{}, error) {
 
227
        searchURL := id.PreferredURL()
 
228
        searchURL.Revision = -1
 
229
 
 
230
        q := h.Store.EntitiesQuery(searchURL)
 
231
        if id.PromulgatedRevision != -1 {
 
232
                q = q.Sort("-promulgated-revision")
 
233
        } else {
 
234
                q = q.Sort("-revision")
 
235
        }
 
236
        var docs []*mongodoc.Entity
 
237
        if err := q.Select(bson.D{{"_id", 1}, {"promulgated-url", 1}, {"supportedseries", 1}}).All(&docs); err != nil {
 
238
                return "", errgo.Notef(err, "cannot get ids")
 
239
        }
 
240
 
 
241
        if len(docs) == 0 {
 
242
                return "", errgo.WithCausef(nil, params.ErrNotFound, "no matching charm or bundle for %s", id)
 
243
        }
 
244
        specifiedSeries := id.URL.Series
 
245
        if specifiedSeries == "" {
 
246
                specifiedSeries = id.PreferredSeries
 
247
        }
 
248
        var response params.RevisionInfoResponse
 
249
        expandMultiSeries(docs, func(series string, doc *mongodoc.Entity) error {
 
250
                if specifiedSeries != series {
 
251
                        return nil
 
252
                }
 
253
                url := doc.PreferredURL(id.PromulgatedRevision != -1)
 
254
                url.Series = series
 
255
                response.Revisions = append(response.Revisions, url)
 
256
                return nil
 
257
        })
 
258
        return &response, nil
 
259
}
 
260
 
 
261
// GET id/meta/archive-size
 
262
// https://github.com/juju/charmstore/blob/v4/docs/API.md#get-idmetaarchive-size
 
263
func (h ReqHandler) metaArchiveSize(entity *mongodoc.Entity, id *router.ResolvedURL, path string, flags url.Values, req *http.Request) (interface{}, error) {
 
264
        return &params.ArchiveSizeResponse{
 
265
                Size: entity.PreV5BlobSize,
 
266
        }, nil
 
267
}
 
268
 
 
269
// GET id/meta/hash
 
270
// https://github.com/juju/charmstore/blob/v4/docs/API.md#get-idmetahash
 
271
func (h ReqHandler) metaHash(entity *mongodoc.Entity, id *router.ResolvedURL, path string, flags url.Values, req *http.Request) (interface{}, error) {
 
272
        return &params.HashResponse{
 
273
                Sum: entity.PreV5BlobHash,
 
274
        }, nil
 
275
}
 
276
 
 
277
// GET id/meta/hash256
 
278
// https://github.com/juju/charmstore/blob/v4/docs/API.md#get-idmetahash256
 
279
func (h ReqHandler) metaHash256(entity *mongodoc.Entity, id *router.ResolvedURL, path string, flags url.Values, req *http.Request) (interface{}, error) {
 
280
        return &params.HashResponse{
 
281
                Sum: entity.PreV5BlobHash256,
 
282
        }, nil
 
283
}
 
284
 
 
285
// GET id/expand-id
 
286
// https://docs.google.com/a/canonical.com/document/d/1TgRA7jW_mmXoKH3JiwBbtPvQu7WiM6XMrz1wSrhTMXw/edit#bookmark=id.4xdnvxphb2si
 
287
func (h ReqHandler) serveExpandId(id *router.ResolvedURL, w http.ResponseWriter, req *http.Request) error {
 
288
        baseURL := id.PreferredURL()
 
289
        baseURL.Revision = -1
 
290
        baseURL.Series = ""
 
291
 
 
292
        // baseURL now represents the base URL of the given id;
 
293
        // it will be a promulgated URL iff the original URL was
 
294
        // specified without a user, which will cause EntitiesQuery
 
295
        // to return entities that match appropriately.
 
296
 
 
297
        // Retrieve all the entities with the same base URL.
 
298
        q := h.Store.EntitiesQuery(baseURL).Select(bson.D{{"_id", 1}, {"promulgated-url", 1}, {"supportedseries", 1}})
 
299
        if id.PromulgatedRevision != -1 {
 
300
                q = q.Sort("-series", "-promulgated-revision")
 
301
        } else {
 
302
                q = q.Sort("-series", "-revision")
 
303
        }
 
304
        var docs []*mongodoc.Entity
 
305
        err := q.All(&docs)
 
306
        if err != nil && errgo.Cause(err) != mgo.ErrNotFound {
 
307
                return errgo.Mask(err)
 
308
        }
 
309
 
 
310
        // Collect all the expanded identifiers for each entity.
 
311
        response := make([]params.ExpandedId, 0, len(docs))
 
312
        expandMultiSeries(docs, func(series string, doc *mongodoc.Entity) error {
 
313
                if err := h.AuthorizeEntity(charmstore.EntityResolvedURL(doc), req); err != nil {
 
314
                        return nil
 
315
                }
 
316
                url := doc.PreferredURL(id.PromulgatedRevision != -1)
 
317
                url.Series = series
 
318
                response = append(response, params.ExpandedId{Id: url.String()})
 
319
                return nil
 
320
        })
 
321
 
 
322
        // Write the response in JSON format.
 
323
        return httprequest.WriteJSON(w, http.StatusOK, response)
 
324
}
 
325
 
 
326
// expandMultiSeries calls the provided append function once for every
 
327
// supported series of each entry in the given entities slice. The series
 
328
// argument will be passed as that series and the doc argument will point
 
329
// to the entity. This function will only return an error if the append
 
330
// function returns an error; such an error will be returned without
 
331
// masking the cause.
 
332
//
 
333
// Note that the SupportedSeries field of the entities must have
 
334
// been populated for this to work.
 
335
func expandMultiSeries(entities []*mongodoc.Entity, append func(series string, doc *mongodoc.Entity) error) error {
 
336
        // TODO(rog) make this concurrent.
 
337
        for _, entity := range entities {
 
338
                if entity.URL.Series != "" {
 
339
                        append(entity.URL.Series, entity)
 
340
                        continue
 
341
                }
 
342
                for _, series := range entity.SupportedSeries {
 
343
                        if err := append(series, entity); err != nil {
 
344
                                return errgo.Mask(err, errgo.Any)
 
345
                        }
 
346
                }
 
347
        }
 
348
        return nil
 
349
}
 
350
 
 
351
func badRequestf(underlying error, f string, a ...interface{}) error {
 
352
        err := errgo.WithCausef(underlying, params.ErrBadRequest, f, a...)
 
353
        err.(*errgo.Err).SetLocation(1)
 
354
        return err
 
355
}