35
// CharmStore exposes the functionality of the charm store as needed here.
36
type CharmStore interface {
37
// ListResources composes, for each of the identified charms, the
38
// list of details for each of the charm's resources. Those details
39
// are those associated with the specific charm revision. They
40
// include the resource's metadata and revision.
41
ListResources(charmURLs []*charm.URL) ([][]charmresource.Resource, error)
43
// GetResource returns a reader for the resource's data. That data
44
// is streamed from the charm store. The charm's revision, if any,
45
// is ignored. If the identified resource is not in the charm store
46
// then errors.NotFound is returned.
47
GetResource(cURL *charm.URL, resourceName string, revision int) (charmresource.Resource, io.ReadCloser, error)
33
50
// Facade is the public API facade for resources.
34
51
type Facade struct {
35
52
// store is the data source for the facade.
36
53
store resourceInfoStore
55
newCharmstoreClient func(*charm.URL, *macaroon.Macaroon) (CharmStore, error)
39
58
// NewFacade returns a new resoures facade for the given Juju state.
40
func NewFacade(store DataStore) *Facade {
59
func NewFacade(store DataStore, newClient func(*charm.URL, *macaroon.Macaroon) (CharmStore, error)) (*Facade, error) {
61
return nil, errors.Errorf("missing data store")
64
// Technically this only matters for one code path through
65
// AddPendingResources(). However, that functionality should be
66
// provided. So we indicate the problem here instead of later
67
// in the specific place where it actually matters.
68
return nil, errors.Errorf("missing factory for new charm store clients")
73
newCharmstoreClient: newClient,
46
78
// resourceInfoStore is the portion of Juju's "state" needed
107
130
serviceID := tag.Id()
132
ids, err := f.addPendingResources(serviceID, args.URL, args.CharmStoreMacaroon, args.Resources)
134
result.Error = common.ServerError(err)
137
result.PendingIDs = ids
141
func (f Facade) addPendingResources(serviceID, chRef string, 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)
156
// TODO(ericsnow) Do something else for local charms.
157
client, err := f.newCharmstoreClient(cURL, csMac)
159
return nil, errors.Trace(err)
161
storeResources, err := f.resourcesFromCharmstore(cURL, client)
165
resolved, err := resolveResources(resources, storeResources, cURL, client)
169
// TODO(ericsnow) Ensure that the non-upload resource revisions
170
// match a previously published revision set?
110
for _, apiRes := range args.Resources {
111
pendingID, err := f.addPendingResource(serviceID, apiRes)
175
for _, res := range resources {
176
pendingID, err := f.addPendingResource(serviceID, res)
113
result.Error = common.ServerError(err)
114
178
// We don't bother aggregating errors since a partial
115
179
// completion is disruptive and a retry of this endpoint
116
180
// is not expensive.
119
183
ids = append(ids, pendingID)
121
result.PendingIDs = ids
125
func (f Facade) addPendingResource(serviceID string, apiRes api.CharmResource) (pendingID string, err error) {
126
chRes, err := api.API2CharmResource(apiRes)
188
// resourcesFromCharmstore gets the info for the charm's resources in
189
// the charm store. If the charm URL has a revision then that revision's
190
// resources are returned. Otherwise the latest info for each of the
191
// resources is returned.
192
func (f Facade) resourcesFromCharmstore(cURL *charm.URL, client CharmStore) (map[string]charmresource.Resource, error) {
193
results, err := client.ListResources([]*charm.URL{cURL})
128
return "", errors.Annotatef(err, "bad resource info for %q", chRes.Name)
195
return nil, errors.Trace(err)
197
storeResources := make(map[string]charmresource.Resource)
198
if len(results) != 0 {
199
for _, res := range results[0] {
200
storeResources[res.Name] = res
203
return storeResources, nil
206
// resolveResources determines the resource info that should actually
207
// be stored on the controller. That decision is based on the provided
208
// resources along with those in the charm store (if any).
209
func resolveResources(resources []charmresource.Resource, storeResources map[string]charmresource.Resource, cURL *charm.URL, client CharmStore) ([]charmresource.Resource, error) {
210
allResolved := make([]charmresource.Resource, len(resources))
211
copy(allResolved, resources)
212
for i, res := range resources {
213
// Note that incoming "upload" resources take precedence over
214
// ones already known to the controller, regardless of their
216
if res.Origin != charmresource.OriginStore {
220
resolved, err := resolveStoreResource(res, storeResources, cURL, client)
222
return nil, errors.Trace(err)
224
allResolved[i] = resolved
226
return allResolved, nil
229
// resolveStoreResource selects the resource info to use. It decides
230
// between the provided and latest info based on the revision.
231
func resolveStoreResource(res charmresource.Resource, storeResources map[string]charmresource.Resource, cURL *charm.URL, client CharmStore) (charmresource.Resource, error) {
232
storeRes, ok := storeResources[res.Name]
234
// This indicates that AddPendingResources() was called for
235
// a resource the charm store doesn't know about (for the
236
// relevant charm revision).
237
// TODO(ericsnow) Do the following once the charm store supports
238
// the necessary endpoints:
239
// return res, errors.NotFoundf("charm store resource %q", res.Name)
243
if res.Revision < 0 {
244
// The caller wants to use the charm store info.
247
if res.Revision == storeRes.Revision {
248
// We don't worry about if they otherwise match. Only the
249
// revision is significant here. So we use the info from the
250
// charm store since it is authoritative.
253
if res.Fingerprint.IsZero() {
254
// The caller wants resource info from the charm store, but with
255
// a different resource revision than the one associated with
256
// the charm in the store.
257
storeRes, r, err := client.GetResource(cURL, res.Name, res.Revision)
259
return storeRes, errors.Trace(err)
261
r.Close() // We don't care about the file.
264
// The caller fully-specified a resource with a different resource
265
// revision than the one associated with the charm in the store. So
266
// we use the provided info as-is.
270
func (f Facade) addPendingResource(serviceID string, chRes charmresource.Resource) (pendingID string, err error) {
132
272
var reader io.Reader
133
273
pendingID, err = f.store.AddPendingResource(serviceID, userID, chRes, reader)