~nskaggs/+junk/xenial-test

« back to all changes in this revision

Viewing changes to src/github.com/juju/juju/resource/api/server/server.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 server
 
5
 
 
6
import (
 
7
        "io"
 
8
 
 
9
        "github.com/juju/errors"
 
10
        "github.com/juju/loggo"
 
11
        "gopkg.in/juju/charm.v6-unstable"
 
12
        charmresource "gopkg.in/juju/charm.v6-unstable/resource"
 
13
        csparams "gopkg.in/juju/charmrepo.v2-unstable/csclient/params"
 
14
        "gopkg.in/juju/names.v2"
 
15
        "gopkg.in/macaroon.v1"
 
16
 
 
17
        "github.com/juju/juju/apiserver/common"
 
18
        "github.com/juju/juju/apiserver/params"
 
19
        "github.com/juju/juju/charmstore"
 
20
        "github.com/juju/juju/resource"
 
21
        "github.com/juju/juju/resource/api"
 
22
)
 
23
 
 
24
var logger = loggo.GetLogger("juju.resource.api.server")
 
25
 
 
26
const (
 
27
        // Version is the version number of the current Facade.
 
28
        Version = 1
 
29
)
 
30
 
 
31
// DataStore is the functionality of Juju's state needed for the resources API.
 
32
type DataStore interface {
 
33
        resourceInfoStore
 
34
        UploadDataStore
 
35
}
 
36
 
 
37
// CharmStore exposes the functionality of the charm store as needed here.
 
38
type CharmStore interface {
 
39
        // ListResources composes, for each of the identified charms, the
 
40
        // list of details for each of the charm's resources. Those details
 
41
        // are those associated with the specific charm revision. They
 
42
        // include the resource's metadata and revision.
 
43
        ListResources([]charmstore.CharmID) ([][]charmresource.Resource, error)
 
44
 
 
45
        // ResourceInfo returns the metadata for the given resource.
 
46
        ResourceInfo(charmstore.ResourceRequest) (charmresource.Resource, error)
 
47
}
 
48
 
 
49
// Facade is the public API facade for resources.
 
50
type Facade struct {
 
51
        // store is the data source for the facade.
 
52
        store resourceInfoStore
 
53
 
 
54
        newCharmstoreClient func() (CharmStore, error)
 
55
}
 
56
 
 
57
// NewFacade returns a new resoures facade for the given Juju state.
 
58
func NewFacade(store DataStore, newClient func() (CharmStore, error)) (*Facade, error) {
 
59
        if store == nil {
 
60
                return nil, errors.Errorf("missing data store")
 
61
        }
 
62
        if newClient == nil {
 
63
                // Technically this only matters for one code path through
 
64
                // AddPendingResources(). However, that functionality should be
 
65
                // provided. So we indicate the problem here instead of later
 
66
                // in the specific place where it actually matters.
 
67
                return nil, errors.Errorf("missing factory for new charm store clients")
 
68
        }
 
69
 
 
70
        f := &Facade{
 
71
                store:               store,
 
72
                newCharmstoreClient: newClient,
 
73
        }
 
74
        return f, nil
 
75
}
 
76
 
 
77
// resourceInfoStore is the portion of Juju's "state" needed
 
78
// for the resources facade.
 
79
type resourceInfoStore interface {
 
80
        // ListResources returns the resources for the given application.
 
81
        ListResources(service string) (resource.ServiceResources, error)
 
82
 
 
83
        // AddPendingResource adds the resource to the data store in a
 
84
        // "pending" state. It will stay pending (and unavailable) until
 
85
        // it is resolved. The returned ID is used to identify the pending
 
86
        // resources when resolving it.
 
87
        AddPendingResource(applicationID, userID string, chRes charmresource.Resource, r io.Reader) (string, error)
 
88
}
 
89
 
 
90
// ListResources returns the list of resources for the given application.
 
91
func (f Facade) ListResources(args api.ListResourcesArgs) (api.ResourcesResults, error) {
 
92
        var r api.ResourcesResults
 
93
        r.Results = make([]api.ResourcesResult, len(args.Entities))
 
94
 
 
95
        for i, e := range args.Entities {
 
96
                logger.Tracef("Listing resources for %q", e.Tag)
 
97
                tag, apierr := parseApplicationTag(e.Tag)
 
98
                if apierr != nil {
 
99
                        r.Results[i] = api.ResourcesResult{
 
100
                                ErrorResult: params.ErrorResult{
 
101
                                        Error: apierr,
 
102
                                },
 
103
                        }
 
104
                        continue
 
105
                }
 
106
 
 
107
                svcRes, err := f.store.ListResources(tag.Id())
 
108
                if err != nil {
 
109
                        r.Results[i] = errorResult(err)
 
110
                        continue
 
111
                }
 
112
 
 
113
                r.Results[i] = api.ServiceResources2APIResult(svcRes)
 
114
        }
 
115
        return r, nil
 
116
}
 
117
 
 
118
// AddPendingResources adds the provided resources (info) to the Juju
 
119
// model in a pending state, meaning they are not available until
 
120
// resolved.
 
121
func (f Facade) AddPendingResources(args api.AddPendingResourcesArgs) (api.AddPendingResourcesResult, error) {
 
122
        var result api.AddPendingResourcesResult
 
123
 
 
124
        tag, apiErr := parseApplicationTag(args.Tag)
 
125
        if apiErr != nil {
 
126
                result.Error = apiErr
 
127
                return result, nil
 
128
        }
 
129
        applicationID := tag.Id()
 
130
 
 
131
        channel := csparams.Channel(args.Channel)
 
132
        ids, err := f.addPendingResources(applicationID, args.URL, channel, args.CharmStoreMacaroon, args.Resources)
 
133
        if err != nil {
 
134
                result.Error = common.ServerError(err)
 
135
                return result, nil
 
136
        }
 
137
        result.PendingIDs = ids
 
138
        return result, nil
 
139
}
 
140
 
 
141
func (f Facade) addPendingResources(applicationID, chRef string, channel csparams.Channel, csMac *macaroon.Macaroon, apiResources []api.CharmResource) ([]string, error) {
 
142
        var resources []charmresource.Resource
 
143
        for _, apiRes := range apiResources {
 
144
                res, err := api.API2CharmResource(apiRes)
 
145
                if err != nil {
 
146
                        return nil, errors.Annotatef(err, "bad resource info for %q", apiRes.Name)
 
147
                }
 
148
                resources = append(resources, res)
 
149
        }
 
150
 
 
151
        if chRef != "" {
 
152
                cURL, err := charm.ParseURL(chRef)
 
153
                if err != nil {
 
154
                        return nil, err
 
155
                }
 
156
 
 
157
                switch cURL.Schema {
 
158
                case "cs":
 
159
                        id := charmstore.CharmID{
 
160
                                URL:     cURL,
 
161
                                Channel: channel,
 
162
                        }
 
163
                        resources, err = f.resolveCharmstoreResources(id, csMac, resources)
 
164
                        if err != nil {
 
165
                                return nil, errors.Trace(err)
 
166
                        }
 
167
                case "local":
 
168
                        resources, err = f.resolveLocalResources(resources)
 
169
                        if err != nil {
 
170
                                return nil, errors.Trace(err)
 
171
                        }
 
172
                default:
 
173
                        return nil, errors.Errorf("unrecognized charm schema %q", cURL.Schema)
 
174
                }
 
175
        }
 
176
 
 
177
        var ids []string
 
178
        for _, res := range resources {
 
179
                pendingID, err := f.addPendingResource(applicationID, res)
 
180
                if err != nil {
 
181
                        // We don't bother aggregating errors since a partial
 
182
                        // completion is disruptive and a retry of this endpoint
 
183
                        // is not expensive.
 
184
                        return nil, err
 
185
                }
 
186
                ids = append(ids, pendingID)
 
187
        }
 
188
        return ids, nil
 
189
}
 
190
 
 
191
func (f Facade) resolveCharmstoreResources(id charmstore.CharmID, csMac *macaroon.Macaroon, resources []charmresource.Resource) ([]charmresource.Resource, error) {
 
192
        client, err := f.newCharmstoreClient()
 
193
        if err != nil {
 
194
                return nil, errors.Trace(err)
 
195
        }
 
196
        ids := []charmstore.CharmID{id}
 
197
        storeResources, err := f.resourcesFromCharmstore(ids, client)
 
198
        if err != nil {
 
199
                return nil, err
 
200
        }
 
201
        resolved, err := resolveResources(resources, storeResources, id, client)
 
202
        if err != nil {
 
203
                return nil, err
 
204
        }
 
205
        // TODO(ericsnow) Ensure that the non-upload resource revisions
 
206
        // match a previously published revision set?
 
207
        return resolved, nil
 
208
}
 
209
 
 
210
func (f Facade) resolveLocalResources(resources []charmresource.Resource) ([]charmresource.Resource, error) {
 
211
        var resolved []charmresource.Resource
 
212
        for _, res := range resources {
 
213
                resolved = append(resolved, charmresource.Resource{
 
214
                        Meta:   res.Meta,
 
215
                        Origin: charmresource.OriginUpload,
 
216
                })
 
217
        }
 
218
        return resolved, nil
 
219
}
 
220
 
 
221
// resourcesFromCharmstore gets the info for the charm's resources in
 
222
// the charm store. If the charm URL has a revision then that revision's
 
223
// resources are returned. Otherwise the latest info for each of the
 
224
// resources is returned.
 
225
func (f Facade) resourcesFromCharmstore(charms []charmstore.CharmID, client CharmStore) (map[string]charmresource.Resource, error) {
 
226
        results, err := client.ListResources(charms)
 
227
        if err != nil {
 
228
                return nil, errors.Trace(err)
 
229
        }
 
230
        storeResources := make(map[string]charmresource.Resource)
 
231
        if len(results) != 0 {
 
232
                for _, res := range results[0] {
 
233
                        storeResources[res.Name] = res
 
234
                }
 
235
        }
 
236
        return storeResources, nil
 
237
}
 
238
 
 
239
// resolveResources determines the resource info that should actually
 
240
// be stored on the controller. That decision is based on the provided
 
241
// resources along with those in the charm store (if any).
 
242
func resolveResources(resources []charmresource.Resource, storeResources map[string]charmresource.Resource, id charmstore.CharmID, client CharmStore) ([]charmresource.Resource, error) {
 
243
        allResolved := make([]charmresource.Resource, len(resources))
 
244
        copy(allResolved, resources)
 
245
        for i, res := range resources {
 
246
                // Note that incoming "upload" resources take precedence over
 
247
                // ones already known to the controller, regardless of their
 
248
                // origin.
 
249
                if res.Origin != charmresource.OriginStore {
 
250
                        continue
 
251
                }
 
252
 
 
253
                resolved, err := resolveStoreResource(res, storeResources, id, client)
 
254
                if err != nil {
 
255
                        return nil, errors.Trace(err)
 
256
                }
 
257
                allResolved[i] = resolved
 
258
        }
 
259
        return allResolved, nil
 
260
}
 
261
 
 
262
// resolveStoreResource selects the resource info to use. It decides
 
263
// between the provided and latest info based on the revision.
 
264
func resolveStoreResource(res charmresource.Resource, storeResources map[string]charmresource.Resource, id charmstore.CharmID, client CharmStore) (charmresource.Resource, error) {
 
265
        storeRes, ok := storeResources[res.Name]
 
266
        if !ok {
 
267
                // This indicates that AddPendingResources() was called for
 
268
                // a resource the charm store doesn't know about (for the
 
269
                // relevant charm revision).
 
270
                // TODO(ericsnow) Do the following once the charm store supports
 
271
                // the necessary endpoints:
 
272
                // return res, errors.NotFoundf("charm store resource %q", res.Name)
 
273
                return res, nil
 
274
        }
 
275
 
 
276
        if res.Revision < 0 {
 
277
                // The caller wants to use the charm store info.
 
278
                return storeRes, nil
 
279
        }
 
280
        if res.Revision == storeRes.Revision {
 
281
                // We don't worry about if they otherwise match. Only the
 
282
                // revision is significant here. So we use the info from the
 
283
                // charm store since it is authoritative.
 
284
                return storeRes, nil
 
285
        }
 
286
        if res.Fingerprint.IsZero() {
 
287
                // The caller wants resource info from the charm store, but with
 
288
                // a different resource revision than the one associated with
 
289
                // the charm in the store.
 
290
                req := charmstore.ResourceRequest{
 
291
                        Charm:    id.URL,
 
292
                        Channel:  id.Channel,
 
293
                        Name:     res.Name,
 
294
                        Revision: res.Revision,
 
295
                }
 
296
                storeRes, err := client.ResourceInfo(req)
 
297
                if err != nil {
 
298
                        return storeRes, errors.Trace(err)
 
299
                }
 
300
                return storeRes, nil
 
301
        }
 
302
        // The caller fully-specified a resource with a different resource
 
303
        // revision than the one associated with the charm in the store. So
 
304
        // we use the provided info as-is.
 
305
        return res, nil
 
306
}
 
307
 
 
308
func (f Facade) addPendingResource(applicationID string, chRes charmresource.Resource) (pendingID string, err error) {
 
309
        userID := ""
 
310
        var reader io.Reader
 
311
        pendingID, err = f.store.AddPendingResource(applicationID, userID, chRes, reader)
 
312
        if err != nil {
 
313
                return "", errors.Annotatef(err, "while adding pending resource info for %q", chRes.Name)
 
314
        }
 
315
        return pendingID, nil
 
316
}
 
317
 
 
318
func parseApplicationTag(tagStr string) (names.ApplicationTag, *params.Error) { // note the concrete error type
 
319
        ApplicationTag, err := names.ParseApplicationTag(tagStr)
 
320
        if err != nil {
 
321
                return ApplicationTag, &params.Error{
 
322
                        Message: err.Error(),
 
323
                        Code:    params.CodeBadRequest,
 
324
                }
 
325
        }
 
326
        return ApplicationTag, nil
 
327
}
 
328
 
 
329
func errorResult(err error) api.ResourcesResult {
 
330
        return api.ResourcesResult{
 
331
                ErrorResult: params.ErrorResult{
 
332
                        Error: common.ServerError(err),
 
333
                },
 
334
        }
 
335
}