1
// Copyright 2015 Canonical Ltd.
2
// Licensed under the AGPLv3, see LICENCE file for details.
4
package v4 // import "gopkg.in/juju/charmstore.v5-unstable/internal/v4"
10
"github.com/juju/httprequest"
11
"github.com/juju/loggo"
12
"github.com/juju/mempool"
14
"gopkg.in/juju/charm.v6-unstable"
15
"gopkg.in/juju/charmrepo.v2-unstable/csclient/params"
17
"gopkg.in/mgo.v2/bson"
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"
26
var logger = loggo.GetLogger("charmstore.internal.v4")
29
PromulgatorsGroup = v5.PromulgatorsGroup
30
UsernameAttr = v5.UsernameAttr
31
DelegatableMacaroonExpiry = v5.DelegatableMacaroonExpiry
32
DefaultIcon = v5.DefaultIcon
33
ArchiveCachePublicMaxAge = v5.ArchiveCachePublicMaxAge
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()
49
type ReqHandler struct {
53
func New(pool *charmstore.Pool, config charmstore.ServerParams, rootPath string) Handler {
55
Handler: v5.New(pool, config, rootPath),
59
func (h Handler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
60
rh, err := h.NewReqHandler(req)
62
router.WriteError(w, err)
69
func NewAPIHandler(pool *charmstore.Pool, config charmstore.ServerParams, rootPath string) charmstore.HTTPCloseHandler {
70
return New(pool, config, rootPath)
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 {
79
fields["supportedseries"] = 1
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.
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) {
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)
101
store, err := h.Pool.RequestStore()
103
if errgo.Cause(err) == charmstore.ErrTooManySessions {
104
return ReqHandler{}, errgo.WithCausef(err, params.ErrServiceUnavailable, "")
106
return ReqHandler{}, errgo.Mask(err)
108
rh := reqHandlerPool.Get().(ReqHandler)
109
rh.Handler = h.Handler
110
rh.Store = &v5.StoreWithChannel{
112
Channel: params.Channel(req.Form.Get("channel")),
114
rh.Cache = entitycache.New(rh.Store)
115
rh.Cache.AddEntityFields(requiredEntityFields)
116
rh.Cache.AddBaseEntityFields(v5.RequiredBaseEntityFields)
120
func newReqHandler() ReqHandler {
122
ReqHandler: new(v5.ReqHandler),
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))
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")
146
h.Router = router.New(handlers, h)
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)
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 {
162
rurls[i], err = resolveURL(h.Cache, url)
163
if err != nil && errgo.Cause(err) != params.ErrNotFound {
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"))
176
return nil, errgo.Mask(err, errgo.Is(params.ErrNotFound))
178
rurl := &router.ResolvedURL{
180
PromulgatedRevision: -1,
183
rurl.PromulgatedRevision = entity.PromulgatedRevision
185
if rurl.URL.Series != "" {
188
if url.Series != "" {
189
rurl.PreferredSeries = url.Series
192
if len(entity.SupportedSeries) == 0 {
193
return nil, errgo.Newf("entity %q has no supported series", &rurl.URL)
195
rurl.PreferredSeries = entity.SupportedSeries[0]
199
// Close closes the ReqHandler. This should always be called when the
200
// ReqHandler is done with.
201
func (h ReqHandler) Close() {
205
reqHandlerPool.Put(h)
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)
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
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
230
q := h.Store.EntitiesQuery(searchURL)
231
if id.PromulgatedRevision != -1 {
232
q = q.Sort("-promulgated-revision")
234
q = q.Sort("-revision")
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")
242
return "", errgo.WithCausef(nil, params.ErrNotFound, "no matching charm or bundle for %s", id)
244
specifiedSeries := id.URL.Series
245
if specifiedSeries == "" {
246
specifiedSeries = id.PreferredSeries
248
var response params.RevisionInfoResponse
249
expandMultiSeries(docs, func(series string, doc *mongodoc.Entity) error {
250
if specifiedSeries != series {
253
url := doc.PreferredURL(id.PromulgatedRevision != -1)
255
response.Revisions = append(response.Revisions, url)
258
return &response, nil
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 ¶ms.ArchiveSizeResponse{
265
Size: entity.PreV5BlobSize,
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 ¶ms.HashResponse{
273
Sum: entity.PreV5BlobHash,
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 ¶ms.HashResponse{
281
Sum: entity.PreV5BlobHash256,
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
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.
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")
302
q = q.Sort("-series", "-revision")
304
var docs []*mongodoc.Entity
306
if err != nil && errgo.Cause(err) != mgo.ErrNotFound {
307
return errgo.Mask(err)
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 {
316
url := doc.PreferredURL(id.PromulgatedRevision != -1)
318
response = append(response, params.ExpandedId{Id: url.String()})
322
// Write the response in JSON format.
323
return httprequest.WriteJSON(w, http.StatusOK, response)
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.
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)
342
for _, series := range entity.SupportedSeries {
343
if err := append(series, entity); err != nil {
344
return errgo.Mask(err, errgo.Any)
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)