github.com/niedbalski/juju@v0.0.0-20190215020005-8ff100488e47/apiserver/facades/client/applicationoffers/applicationoffers.go (about) 1 // Copyright 2015 Canonical Ltd. 2 // Licensed under the AGPLv3, see LICENCE file for details. 3 4 package applicationoffers 5 6 import ( 7 "fmt" 8 9 "github.com/juju/errors" 10 "github.com/juju/loggo" 11 "github.com/juju/txn" 12 "gopkg.in/juju/names.v2" 13 14 "github.com/juju/juju/apiserver/common" 15 commoncrossmodel "github.com/juju/juju/apiserver/common/crossmodel" 16 "github.com/juju/juju/apiserver/facade" 17 "github.com/juju/juju/apiserver/params" 18 jujucrossmodel "github.com/juju/juju/core/crossmodel" 19 "github.com/juju/juju/environs" 20 "github.com/juju/juju/environs/context" 21 "github.com/juju/juju/permission" 22 "github.com/juju/juju/state" 23 "github.com/juju/juju/state/stateenvirons" 24 ) 25 26 var logger = loggo.GetLogger("juju.apiserver.applicationoffers") 27 28 type environFromModelFunc func(string) (environs.Environ, error) 29 30 // OffersAPI implements the cross model interface and is the concrete 31 // implementation of the api end point. 32 type OffersAPI struct { 33 BaseAPI 34 dataDir string 35 authContext *commoncrossmodel.AuthContext 36 } 37 38 // OffersAPIV2 implements the cross model interface V2. 39 type OffersAPIV2 struct { 40 *OffersAPI 41 } 42 43 // createAPI returns a new application offers OffersAPI facade. 44 func createOffersAPI( 45 getApplicationOffers func(interface{}) jujucrossmodel.ApplicationOffers, 46 getEnviron environFromModelFunc, 47 getControllerInfo func() ([]string, string, error), 48 backend Backend, 49 statePool StatePool, 50 authorizer facade.Authorizer, 51 resources facade.Resources, 52 authContext *commoncrossmodel.AuthContext, 53 callCtx context.ProviderCallContext, 54 ) (*OffersAPI, error) { 55 if !authorizer.AuthClient() { 56 return nil, common.ErrPerm 57 } 58 59 dataDir := resources.Get("dataDir").(common.StringResource) 60 api := &OffersAPI{ 61 dataDir: dataDir.String(), 62 authContext: authContext, 63 BaseAPI: BaseAPI{ 64 Authorizer: authorizer, 65 GetApplicationOffers: getApplicationOffers, 66 ControllerModel: backend, 67 StatePool: statePool, 68 getEnviron: getEnviron, 69 getControllerInfo: getControllerInfo, 70 callContext: callCtx, 71 }, 72 } 73 return api, nil 74 } 75 76 // NewOffersAPIV2 returns a new application offers OffersAPIV2 facade. 77 func NewOffersAPI(ctx facade.Context) (*OffersAPI, error) { 78 environFromModel := func(modelUUID string) (environs.Environ, error) { 79 st, err := ctx.StatePool().Get(modelUUID) 80 if err != nil { 81 return nil, errors.Trace(err) 82 } 83 defer st.Release() 84 model, err := st.Model() 85 if err != nil { 86 return nil, errors.Trace(err) 87 } 88 g := stateenvirons.EnvironConfigGetter{st.State, model} 89 env, err := environs.GetEnviron(g, environs.New) 90 if err != nil { 91 return nil, errors.Trace(err) 92 } 93 return env, nil 94 } 95 96 st := ctx.State() 97 getControllerInfo := func() ([]string, string, error) { 98 return common.StateControllerInfo(st) 99 } 100 101 callCtx := state.CallContext(st) 102 authContext := ctx.Resources().Get("offerAccessAuthContext").(common.ValueResource).Value 103 return createOffersAPI( 104 GetApplicationOffers, 105 environFromModel, 106 getControllerInfo, 107 GetStateAccess(st), 108 GetStatePool(ctx.StatePool()), 109 ctx.Auth(), 110 ctx.Resources(), 111 authContext.(*commoncrossmodel.AuthContext), 112 callCtx, 113 ) 114 } 115 116 // NewOffersAPIV2 returns a new application offers OffersAPIV2 facade. 117 func NewOffersAPIV2(ctx facade.Context) (*OffersAPIV2, error) { 118 apiV1, err := NewOffersAPI(ctx) 119 if err != nil { 120 return nil, errors.Trace(err) 121 } 122 return &OffersAPIV2{OffersAPI: apiV1}, nil 123 } 124 125 // Offer makes application endpoints available for consumption at a specified URL. 126 func (api *OffersAPI) Offer(all params.AddApplicationOffers) (params.ErrorResults, error) { 127 result := make([]params.ErrorResult, len(all.Offers)) 128 129 for i, one := range all.Offers { 130 modelTag, err := names.ParseModelTag(one.ModelTag) 131 if err != nil { 132 result[i].Error = common.ServerError(err) 133 continue 134 } 135 backend, releaser, err := api.StatePool.Get(modelTag.Id()) 136 if err != nil { 137 result[i].Error = common.ServerError(err) 138 continue 139 } 140 defer releaser() 141 142 if err := api.checkAdmin(backend); err != nil { 143 result[i].Error = common.ServerError(err) 144 continue 145 } 146 147 applicationOfferParams, err := api.makeAddOfferArgsFromParams(backend, one) 148 if err != nil { 149 result[i].Error = common.ServerError(err) 150 continue 151 } 152 _, err = api.GetApplicationOffers(backend).AddOffer(applicationOfferParams) 153 result[i].Error = common.ServerError(err) 154 } 155 return params.ErrorResults{Results: result}, nil 156 } 157 158 func (api *OffersAPI) makeAddOfferArgsFromParams(backend Backend, addOfferParams params.AddApplicationOffer) (jujucrossmodel.AddApplicationOfferArgs, error) { 159 result := jujucrossmodel.AddApplicationOfferArgs{ 160 OfferName: addOfferParams.OfferName, 161 ApplicationName: addOfferParams.ApplicationName, 162 ApplicationDescription: addOfferParams.ApplicationDescription, 163 Endpoints: addOfferParams.Endpoints, 164 Owner: api.Authorizer.GetAuthTag().Id(), 165 HasRead: []string{common.EveryoneTagName}, 166 } 167 if result.OfferName == "" { 168 result.OfferName = result.ApplicationName 169 } 170 application, err := backend.Application(addOfferParams.ApplicationName) 171 if err != nil { 172 return result, errors.Annotatef(err, "getting offered application %v", addOfferParams.ApplicationName) 173 } 174 175 if result.ApplicationDescription == "" { 176 ch, _, err := application.Charm() 177 if err != nil { 178 return result, 179 errors.Annotatef(err, "getting charm for application %v", addOfferParams.ApplicationName) 180 } 181 result.ApplicationDescription = ch.Meta().Description 182 } 183 return result, nil 184 } 185 186 // ListApplicationOffers gets deployed details about application offers that match given filter. 187 // The results contain details about the deployed applications such as connection count. 188 func (api *OffersAPI) ListApplicationOffers(filters params.OfferFilters) (params.QueryApplicationOffersResults, error) { 189 var result params.QueryApplicationOffersResults 190 offers, err := api.getApplicationOffersDetails(filters, permission.AdminAccess) 191 if err != nil { 192 return result, common.ServerError(err) 193 } 194 result.Results = offers 195 return result, nil 196 } 197 198 // ModifyOfferAccess changes the application offer access granted to users. 199 func (api *OffersAPI) ModifyOfferAccess(args params.ModifyOfferAccessRequest) (result params.ErrorResults, _ error) { 200 result = params.ErrorResults{ 201 Results: make([]params.ErrorResult, len(args.Changes)), 202 } 203 if len(args.Changes) == 0 { 204 return result, nil 205 } 206 207 isControllerAdmin, err := api.Authorizer.HasPermission(permission.SuperuserAccess, api.ControllerModel.ControllerTag()) 208 if err != nil { 209 return result, errors.Trace(err) 210 } 211 212 offerURLs := make([]string, len(args.Changes)) 213 for i, arg := range args.Changes { 214 offerURLs[i] = arg.OfferURL 215 } 216 models, err := api.getModelsFromOffers(offerURLs...) 217 if err != nil { 218 return result, errors.Trace(err) 219 } 220 221 for i, arg := range args.Changes { 222 if models[i].err != nil { 223 result.Results[i].Error = common.ServerError(models[i].err) 224 continue 225 } 226 err = api.modifyOneOfferAccess(models[i].model.UUID(), isControllerAdmin, arg) 227 result.Results[i].Error = common.ServerError(err) 228 } 229 return result, nil 230 } 231 232 func (api *OffersAPI) modifyOneOfferAccess(modelUUID string, isControllerAdmin bool, arg params.ModifyOfferAccess) error { 233 backend, releaser, err := api.StatePool.Get(modelUUID) 234 if err != nil { 235 return errors.Trace(err) 236 } 237 defer releaser() 238 239 offerAccess := permission.Access(arg.Access) 240 if err := permission.ValidateOfferAccess(offerAccess); err != nil { 241 return errors.Annotate(err, "could not modify offer access") 242 } 243 244 url, err := jujucrossmodel.ParseOfferURL(arg.OfferURL) 245 if err != nil { 246 return errors.Trace(err) 247 } 248 offerTag := names.NewApplicationOfferTag(url.ApplicationName) 249 250 canModifyOffer := isControllerAdmin 251 if !canModifyOffer { 252 if canModifyOffer, err = api.Authorizer.HasPermission(permission.AdminAccess, backend.ModelTag()); err != nil { 253 return errors.Trace(err) 254 } 255 } 256 257 if !canModifyOffer { 258 apiUser := api.Authorizer.GetAuthTag().(names.UserTag) 259 offer, err := backend.ApplicationOffer(offerTag.Id()) 260 if err != nil { 261 return common.ErrPerm 262 } 263 access, err := backend.GetOfferAccess(offer.OfferUUID, apiUser) 264 if err != nil && !errors.IsNotFound(err) { 265 return errors.Trace(err) 266 } else if err == nil { 267 canModifyOffer = access == permission.AdminAccess 268 } 269 } 270 if !canModifyOffer { 271 return common.ErrPerm 272 } 273 274 targetUserTag, err := names.ParseUserTag(arg.UserTag) 275 if err != nil { 276 return errors.Annotate(err, "could not modify offer access") 277 } 278 return api.changeOfferAccess(backend, offerTag, targetUserTag, arg.Action, offerAccess) 279 } 280 281 // changeOfferAccess performs the requested access grant or revoke action for the 282 // specified user on the specified application offer. 283 func (api *OffersAPI) changeOfferAccess( 284 backend Backend, 285 offerTag names.ApplicationOfferTag, 286 targetUserTag names.UserTag, 287 action params.OfferAction, 288 access permission.Access, 289 ) error { 290 _, err := backend.ApplicationOffer(offerTag.Name) 291 if err != nil { 292 return errors.Trace(err) 293 } 294 switch action { 295 case params.GrantOfferAccess: 296 return api.grantOfferAccess(backend, offerTag, targetUserTag, access) 297 case params.RevokeOfferAccess: 298 return api.revokeOfferAccess(backend, offerTag, targetUserTag, access) 299 default: 300 return errors.Errorf("unknown action %q", action) 301 } 302 } 303 304 func (api *OffersAPI) grantOfferAccess(backend Backend, offerTag names.ApplicationOfferTag, targetUserTag names.UserTag, access permission.Access) error { 305 err := backend.CreateOfferAccess(offerTag, targetUserTag, access) 306 if errors.IsAlreadyExists(err) { 307 offer, err := backend.ApplicationOffer(offerTag.Id()) 308 if err != nil { 309 return common.ErrPerm 310 } 311 offerAccess, err := backend.GetOfferAccess(offer.OfferUUID, targetUserTag) 312 if errors.IsNotFound(err) { 313 // Conflicts with prior check, must be inconsistent state. 314 err = txn.ErrExcessiveContention 315 } 316 if err != nil { 317 return errors.Annotate(err, "could not look up offer access for user") 318 } 319 320 // Only set access if greater access is being granted. 321 if offerAccess.EqualOrGreaterOfferAccessThan(access) { 322 return errors.Errorf("user already has %q access or greater", access) 323 } 324 if err = backend.UpdateOfferAccess(offerTag, targetUserTag, access); err != nil { 325 return errors.Annotate(err, "could not set offer access for user") 326 } 327 return nil 328 } 329 return errors.Annotate(err, "could not grant offer access") 330 } 331 332 func (api *OffersAPI) revokeOfferAccess(backend Backend, offerTag names.ApplicationOfferTag, targetUserTag names.UserTag, access permission.Access) error { 333 switch access { 334 case permission.ReadAccess: 335 // Revoking read access removes all access. 336 err := backend.RemoveOfferAccess(offerTag, targetUserTag) 337 return errors.Annotate(err, "could not revoke offer access") 338 case permission.ConsumeAccess: 339 // Revoking consume access sets read-only. 340 err := backend.UpdateOfferAccess(offerTag, targetUserTag, permission.ReadAccess) 341 return errors.Annotate(err, "could not set offer access to read-only") 342 case permission.AdminAccess: 343 // Revoking admin access sets read-consume. 344 err := backend.UpdateOfferAccess(offerTag, targetUserTag, permission.ConsumeAccess) 345 return errors.Annotate(err, "could not set offer access to read-consume") 346 347 default: 348 return errors.Errorf("don't know how to revoke %q access", access) 349 } 350 } 351 352 // ApplicationOffers gets details about remote applications that match given URLs. 353 func (api *OffersAPI) ApplicationOffers(urls params.OfferURLs) (params.ApplicationOffersResults, error) { 354 var results params.ApplicationOffersResults 355 results.Results = make([]params.ApplicationOfferResult, len(urls.OfferURLs)) 356 357 var ( 358 filters []params.OfferFilter 359 // fullURLs contains the URL strings from the url args, 360 // with any optional parts like model owner filled in. 361 // It is used to process the result offers. 362 fullURLs []string 363 ) 364 for i, urlStr := range urls.OfferURLs { 365 url, err := jujucrossmodel.ParseOfferURL(urlStr) 366 if err != nil { 367 results.Results[i].Error = common.ServerError(err) 368 continue 369 } 370 if url.User == "" { 371 url.User = api.Authorizer.GetAuthTag().Id() 372 } 373 if url.HasEndpoint() { 374 results.Results[i].Error = common.ServerError( 375 errors.Errorf("remote application %q shouldn't include endpoint", url)) 376 continue 377 } 378 if url.Source != "" { 379 results.Results[i].Error = common.ServerError( 380 errors.NotSupportedf("query for non-local application offers")) 381 continue 382 } 383 fullURLs = append(fullURLs, url.String()) 384 filters = append(filters, api.filterFromURL(url)) 385 } 386 if len(filters) == 0 { 387 return results, nil 388 } 389 offers, err := api.getApplicationOffersDetails(params.OfferFilters{filters}, permission.ReadAccess) 390 if err != nil { 391 return results, common.ServerError(err) 392 } 393 offersByURL := make(map[string]params.ApplicationOfferAdminDetails) 394 for _, offer := range offers { 395 offersByURL[offer.OfferURL] = offer 396 } 397 398 for i, urlStr := range fullURLs { 399 offer, ok := offersByURL[urlStr] 400 if !ok { 401 err = errors.NotFoundf("application offer %q", urlStr) 402 results.Results[i].Error = common.ServerError(err) 403 continue 404 } 405 results.Results[i].Result = &offer 406 } 407 return results, nil 408 } 409 410 // FindApplicationOffers gets details about remote applications that match given filter. 411 func (api *OffersAPI) FindApplicationOffers(filters params.OfferFilters) (params.QueryApplicationOffersResults, error) { 412 var result params.QueryApplicationOffersResults 413 var filtersToUse params.OfferFilters 414 415 // If there is only one filter term, and no model is specified, add in 416 // any models the user can see and query across those. 417 // If there's more than one filter term, each must specify a model. 418 if len(filters.Filters) == 1 && filters.Filters[0].ModelName == "" { 419 uuids, err := api.ControllerModel.AllModelUUIDs() 420 if err != nil { 421 return result, errors.Trace(err) 422 } 423 for _, uuid := range uuids { 424 m, release, err := api.StatePool.GetModel(uuid) 425 if err != nil { 426 return result, errors.Trace(err) 427 } 428 defer release() 429 modelFilter := filters.Filters[0] 430 modelFilter.ModelName = m.Name() 431 modelFilter.OwnerName = m.Owner().Name() 432 filtersToUse.Filters = append(filtersToUse.Filters, modelFilter) 433 } 434 } else { 435 filtersToUse = filters 436 } 437 offers, err := api.getApplicationOffersDetails(filtersToUse, permission.ReadAccess) 438 if err != nil { 439 return result, common.ServerError(err) 440 } 441 result.Results = offers 442 return result, nil 443 } 444 445 // GetConsumeDetails returns the details necessary to pass to another model to 446 // consume the specified offers represented by the urls. 447 func (api *OffersAPI) GetConsumeDetails(args params.OfferURLs) (params.ConsumeOfferDetailsResults, error) { 448 var consumeResults params.ConsumeOfferDetailsResults 449 results := make([]params.ConsumeOfferDetailsResult, len(args.OfferURLs)) 450 451 offers, err := api.ApplicationOffers(args) 452 if err != nil { 453 return consumeResults, common.ServerError(err) 454 } 455 456 addrs, caCert, err := api.getControllerInfo() 457 if err != nil { 458 return consumeResults, common.ServerError(err) 459 } 460 461 controllerInfo := ¶ms.ExternalControllerInfo{ 462 ControllerTag: api.ControllerModel.ControllerTag().String(), 463 Addrs: addrs, 464 CACert: caCert, 465 } 466 467 for i, result := range offers.Results { 468 results[i].Error = result.Error 469 if result.Error != nil { 470 continue 471 } 472 offer := result.Result 473 offerDetails := &offer.ApplicationOfferDetails 474 results[i].Offer = offerDetails 475 results[i].ControllerInfo = controllerInfo 476 offerMacaroon, err := api.authContext.CreateConsumeOfferMacaroon(offerDetails, api.Authorizer.GetAuthTag().Id()) 477 if err != nil { 478 results[i].Error = common.ServerError(err) 479 continue 480 } 481 results[i].Macaroon = offerMacaroon 482 } 483 consumeResults.Results = results 484 return consumeResults, nil 485 } 486 487 // RemoteApplicationInfo returns information about the requested remote application. 488 func (api *OffersAPI) RemoteApplicationInfo(args params.OfferURLs) (params.RemoteApplicationInfoResults, error) { 489 results := make([]params.RemoteApplicationInfoResult, len(args.OfferURLs)) 490 for i, url := range args.OfferURLs { 491 info, err := api.oneRemoteApplicationInfo(url) 492 results[i].Result = info 493 results[i].Error = common.ServerError(err) 494 } 495 return params.RemoteApplicationInfoResults{results}, nil 496 } 497 498 func (api *OffersAPI) filterFromURL(url *jujucrossmodel.OfferURL) params.OfferFilter { 499 f := params.OfferFilter{ 500 OwnerName: url.User, 501 ModelName: url.ModelName, 502 OfferName: url.ApplicationName, 503 } 504 return f 505 } 506 507 func (api *OffersAPI) oneRemoteApplicationInfo(urlStr string) (*params.RemoteApplicationInfo, error) { 508 url, err := jujucrossmodel.ParseOfferURL(urlStr) 509 if err != nil { 510 return nil, errors.Trace(err) 511 } 512 513 // We need at least read access to the model to see the application details. 514 // offer, err := api.offeredApplicationDetails(url, permission.ReadAccess) 515 offers, err := api.getApplicationOffersDetails( 516 params.OfferFilters{[]params.OfferFilter{api.filterFromURL(url)}}, permission.ConsumeAccess) 517 if err != nil { 518 return nil, errors.Trace(err) 519 } 520 521 // The offers query succeeded but there were no offers matching the required offer name. 522 if len(offers) == 0 { 523 return nil, errors.NotFoundf("application offer %q", url.ApplicationName) 524 } 525 // Sanity check - this should never happen. 526 if len(offers) > 1 { 527 return nil, errors.Errorf("unexpected: %d matching offers for %q", len(offers), url.ApplicationName) 528 } 529 offer := offers[0] 530 531 return ¶ms.RemoteApplicationInfo{ 532 ModelTag: offer.SourceModelTag, 533 Name: url.ApplicationName, 534 Description: offer.ApplicationDescription, 535 OfferURL: url.String(), 536 SourceModelLabel: url.ModelName, 537 Endpoints: offer.Endpoints, 538 IconURLPath: fmt.Sprintf("rest/1.0/remote-application/%s/icon", url.ApplicationName), 539 }, nil 540 } 541 542 // DestroyOffers removes the offers specified by the given URLs. 543 func (api *OffersAPI) DestroyOffers(args params.DestroyApplicationOffers) (params.ErrorResults, error) { 544 return destroyOffers(api, args.OfferURLs, false) 545 } 546 547 // DestroyOffers removes the offers specified by the given URLs, forcing if necessary. 548 func (api *OffersAPIV2) DestroyOffers(args params.DestroyApplicationOffers) (params.ErrorResults, error) { 549 return destroyOffers(api.OffersAPI, args.OfferURLs, args.Force) 550 } 551 552 func destroyOffers(api *OffersAPI, offerURLs []string, force bool) (params.ErrorResults, error) { 553 result := make([]params.ErrorResult, len(offerURLs)) 554 555 models, err := api.getModelsFromOffers(offerURLs...) 556 if err != nil { 557 return params.ErrorResults{}, errors.Trace(err) 558 } 559 560 for i, one := range offerURLs { 561 url, err := jujucrossmodel.ParseOfferURL(one) 562 if err != nil { 563 result[i].Error = common.ServerError(err) 564 continue 565 } 566 if models[i].err != nil { 567 result[i].Error = common.ServerError(models[i].err) 568 continue 569 } 570 backend, releaser, err := api.StatePool.Get(models[i].model.UUID()) 571 if err != nil { 572 result[i].Error = common.ServerError(err) 573 continue 574 } 575 defer releaser() 576 577 if err := api.checkAdmin(backend); err != nil { 578 result[i].Error = common.ServerError(err) 579 continue 580 } 581 err = api.GetApplicationOffers(backend).Remove(url.ApplicationName, force) 582 result[i].Error = common.ServerError(err) 583 } 584 return params.ErrorResults{Results: result}, nil 585 }