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