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 := &params.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 &params.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  }