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