1
// Copyright 2015 Canonical Ltd.
2
// Licensed under the AGPLv3, see LICENCE file for details.
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"
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"
24
var logger = loggo.GetLogger("juju.resource.api.server")
27
// Version is the version number of the current Facade.
31
// DataStore is the functionality of Juju's state needed for the resources API.
32
type DataStore interface {
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)
45
// ResourceInfo returns the metadata for the given resource.
46
ResourceInfo(charmstore.ResourceRequest) (charmresource.Resource, error)
49
// Facade is the public API facade for resources.
51
// store is the data source for the facade.
52
store resourceInfoStore
54
newCharmstoreClient func() (CharmStore, error)
57
// NewFacade returns a new resoures facade for the given Juju state.
58
func NewFacade(store DataStore, newClient func() (CharmStore, error)) (*Facade, error) {
60
return nil, errors.Errorf("missing data store")
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")
72
newCharmstoreClient: newClient,
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)
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)
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))
95
for i, e := range args.Entities {
96
logger.Tracef("Listing resources for %q", e.Tag)
97
tag, apierr := parseApplicationTag(e.Tag)
99
r.Results[i] = api.ResourcesResult{
100
ErrorResult: params.ErrorResult{
107
svcRes, err := f.store.ListResources(tag.Id())
109
r.Results[i] = errorResult(err)
113
r.Results[i] = api.ServiceResources2APIResult(svcRes)
118
// AddPendingResources adds the provided resources (info) to the Juju
119
// model in a pending state, meaning they are not available until
121
func (f Facade) AddPendingResources(args api.AddPendingResourcesArgs) (api.AddPendingResourcesResult, error) {
122
var result api.AddPendingResourcesResult
124
tag, apiErr := parseApplicationTag(args.Tag)
126
result.Error = apiErr
129
applicationID := tag.Id()
131
channel := csparams.Channel(args.Channel)
132
ids, err := f.addPendingResources(applicationID, args.URL, channel, args.CharmStoreMacaroon, args.Resources)
134
result.Error = common.ServerError(err)
137
result.PendingIDs = ids
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)
146
return nil, errors.Annotatef(err, "bad resource info for %q", apiRes.Name)
148
resources = append(resources, res)
152
cURL, err := charm.ParseURL(chRef)
159
id := charmstore.CharmID{
163
resources, err = f.resolveCharmstoreResources(id, csMac, resources)
165
return nil, errors.Trace(err)
168
resources, err = f.resolveLocalResources(resources)
170
return nil, errors.Trace(err)
173
return nil, errors.Errorf("unrecognized charm schema %q", cURL.Schema)
178
for _, res := range resources {
179
pendingID, err := f.addPendingResource(applicationID, res)
181
// We don't bother aggregating errors since a partial
182
// completion is disruptive and a retry of this endpoint
186
ids = append(ids, pendingID)
191
func (f Facade) resolveCharmstoreResources(id charmstore.CharmID, csMac *macaroon.Macaroon, resources []charmresource.Resource) ([]charmresource.Resource, error) {
192
client, err := f.newCharmstoreClient()
194
return nil, errors.Trace(err)
196
ids := []charmstore.CharmID{id}
197
storeResources, err := f.resourcesFromCharmstore(ids, client)
201
resolved, err := resolveResources(resources, storeResources, id, client)
205
// TODO(ericsnow) Ensure that the non-upload resource revisions
206
// match a previously published revision set?
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{
215
Origin: charmresource.OriginUpload,
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)
228
return nil, errors.Trace(err)
230
storeResources := make(map[string]charmresource.Resource)
231
if len(results) != 0 {
232
for _, res := range results[0] {
233
storeResources[res.Name] = res
236
return storeResources, nil
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
249
if res.Origin != charmresource.OriginStore {
253
resolved, err := resolveStoreResource(res, storeResources, id, client)
255
return nil, errors.Trace(err)
257
allResolved[i] = resolved
259
return allResolved, nil
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]
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)
276
if res.Revision < 0 {
277
// The caller wants to use the charm store info.
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.
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{
294
Revision: res.Revision,
296
storeRes, err := client.ResourceInfo(req)
298
return storeRes, errors.Trace(err)
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.
308
func (f Facade) addPendingResource(applicationID string, chRes charmresource.Resource) (pendingID string, err error) {
311
pendingID, err = f.store.AddPendingResource(applicationID, userID, chRes, reader)
313
return "", errors.Annotatef(err, "while adding pending resource info for %q", chRes.Name)
315
return pendingID, nil
318
func parseApplicationTag(tagStr string) (names.ApplicationTag, *params.Error) { // note the concrete error type
319
ApplicationTag, err := names.ParseApplicationTag(tagStr)
321
return ApplicationTag, ¶ms.Error{
322
Message: err.Error(),
323
Code: params.CodeBadRequest,
326
return ApplicationTag, nil
329
func errorResult(err error) api.ResourcesResult {
330
return api.ResourcesResult{
331
ErrorResult: params.ErrorResult{
332
Error: common.ServerError(err),