github.com/axw/juju@v0.0.0-20161005053422-4bd6544d08d4/resource/api/server/server.go (about) 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, ¶ms.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 }