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