github.com/wallyworld/juju@v0.0.0-20161013125918-6cf1bc9d917a/apiserver/application/application.go (about) 1 // Copyright 2014 Canonical Ltd. 2 // Licensed under the AGPLv3, see LICENCE file for details. 3 4 // Package application contains api calls for functionality 5 // related to deploying and managing applications and their 6 // related charms. 7 package application 8 9 import ( 10 "github.com/juju/errors" 11 "github.com/juju/loggo" 12 "gopkg.in/juju/charm.v6-unstable" 13 csparams "gopkg.in/juju/charmrepo.v2-unstable/csclient/params" 14 goyaml "gopkg.in/yaml.v2" 15 16 "github.com/juju/juju/apiserver/common" 17 "github.com/juju/juju/apiserver/facade" 18 "github.com/juju/juju/apiserver/params" 19 "github.com/juju/juju/instance" 20 jjj "github.com/juju/juju/juju" 21 "github.com/juju/juju/permission" 22 "github.com/juju/juju/state" 23 statestorage "github.com/juju/juju/state/storage" 24 ) 25 26 var ( 27 logger = loggo.GetLogger("juju.apiserver.application") 28 29 newStateStorage = statestorage.NewStorage 30 ) 31 32 func init() { 33 common.RegisterStandardFacade("Application", 1, newAPI) 34 35 // Facade version 2 adds support for the ConfigSettings 36 // and StorageConstraints fields in SetCharm. 37 common.RegisterStandardFacade("Application", 2, newAPI) 38 } 39 40 // API implements the application interface and is the concrete 41 // implementation of the api end point. 42 type API struct { 43 backend Backend 44 authorizer facade.Authorizer 45 check BlockChecker 46 47 // TODO(axw) stateCharm only exists because I ran out 48 // of time unwinding all of the tendrils of state. We 49 // should pass a charm.Charm and charm.URL back into 50 // state wherever we pass in a state.Charm currently. 51 stateCharm func(Charm) *state.Charm 52 } 53 54 func newAPI( 55 st *state.State, 56 resources facade.Resources, 57 authorizer facade.Authorizer, 58 ) (*API, error) { 59 backend := NewStateBackend(st) 60 blockChecker := common.NewBlockChecker(st) 61 stateCharm := CharmToStateCharm 62 return NewAPI( 63 backend, 64 authorizer, 65 blockChecker, 66 stateCharm, 67 ) 68 } 69 70 // NewAPI returns a new application API facade. 71 func NewAPI( 72 backend Backend, 73 authorizer facade.Authorizer, 74 blockChecker BlockChecker, 75 stateCharm func(Charm) *state.Charm, 76 ) (*API, error) { 77 if !authorizer.AuthClient() { 78 return nil, common.ErrPerm 79 } 80 return &API{ 81 backend: backend, 82 authorizer: authorizer, 83 check: blockChecker, 84 stateCharm: stateCharm, 85 }, nil 86 } 87 88 func (api *API) checkCanRead() error { 89 canRead, err := api.authorizer.HasPermission(permission.ReadAccess, api.backend.ModelTag()) 90 if err != nil { 91 return errors.Trace(err) 92 } 93 if !canRead { 94 return common.ErrPerm 95 } 96 return nil 97 } 98 99 func (api *API) checkCanWrite() error { 100 canWrite, err := api.authorizer.HasPermission(permission.WriteAccess, api.backend.ModelTag()) 101 if err != nil { 102 return errors.Trace(err) 103 } 104 if !canWrite { 105 return common.ErrPerm 106 } 107 return nil 108 } 109 110 // SetMetricCredentials sets credentials on the application. 111 func (api *API) SetMetricCredentials(args params.ApplicationMetricCredentials) (params.ErrorResults, error) { 112 if err := api.checkCanWrite(); err != nil { 113 return params.ErrorResults{}, errors.Trace(err) 114 } 115 result := params.ErrorResults{ 116 Results: make([]params.ErrorResult, len(args.Creds)), 117 } 118 if len(args.Creds) == 0 { 119 return result, nil 120 } 121 for i, a := range args.Creds { 122 application, err := api.backend.Application(a.ApplicationName) 123 if err != nil { 124 result.Results[i].Error = common.ServerError(err) 125 continue 126 } 127 err = application.SetMetricCredentials(a.MetricCredentials) 128 if err != nil { 129 result.Results[i].Error = common.ServerError(err) 130 } 131 } 132 return result, nil 133 } 134 135 // Deploy fetches the charms from the charm store and deploys them 136 // using the specified placement directives. 137 func (api *API) Deploy(args params.ApplicationsDeploy) (params.ErrorResults, error) { 138 if err := api.checkCanWrite(); err != nil { 139 return params.ErrorResults{}, errors.Trace(err) 140 } 141 result := params.ErrorResults{ 142 Results: make([]params.ErrorResult, len(args.Applications)), 143 } 144 if err := api.check.ChangeAllowed(); err != nil { 145 return result, errors.Trace(err) 146 } 147 for i, arg := range args.Applications { 148 err := deployApplication(api.backend, api.stateCharm, arg) 149 result.Results[i].Error = common.ServerError(err) 150 } 151 return result, nil 152 } 153 154 // deployApplication fetches the charm from the charm store and deploys it. 155 // The logic has been factored out into a common function which is called by 156 // both the legacy API on the client facade, as well as the new application facade. 157 func deployApplication( 158 backend Backend, 159 stateCharm func(Charm) *state.Charm, 160 args params.ApplicationDeploy, 161 ) error { 162 curl, err := charm.ParseURL(args.CharmURL) 163 if err != nil { 164 return errors.Trace(err) 165 } 166 if curl.Revision < 0 { 167 return errors.Errorf("charm url must include revision") 168 } 169 170 // Do a quick but not complete validation check before going any further. 171 for _, p := range args.Placement { 172 if p.Scope != instance.MachineScope { 173 continue 174 } 175 _, err = backend.Machine(p.Directive) 176 if err != nil { 177 return errors.Annotatef(err, `cannot deploy "%v" to machine %v`, args.ApplicationName, p.Directive) 178 } 179 } 180 181 // Try to find the charm URL in state first. 182 ch, err := backend.Charm(curl) 183 if err != nil { 184 return errors.Trace(err) 185 } 186 187 if err := checkMinVersion(ch); err != nil { 188 return errors.Trace(err) 189 } 190 191 var settings charm.Settings 192 if len(args.ConfigYAML) > 0 { 193 settings, err = ch.Config().ParseSettingsYAML([]byte(args.ConfigYAML), args.ApplicationName) 194 } else if len(args.Config) > 0 { 195 // Parse config in a compatible way (see function comment). 196 settings, err = parseSettingsCompatible(ch.Config(), args.Config) 197 } 198 if err != nil { 199 return errors.Trace(err) 200 } 201 202 channel := csparams.Channel(args.Channel) 203 204 _, err = jjj.DeployApplication(backend, 205 jjj.DeployApplicationParams{ 206 ApplicationName: args.ApplicationName, 207 Series: args.Series, 208 Charm: stateCharm(ch), 209 Channel: channel, 210 NumUnits: args.NumUnits, 211 ConfigSettings: settings, 212 Constraints: args.Constraints, 213 Placement: args.Placement, 214 Storage: args.Storage, 215 EndpointBindings: args.EndpointBindings, 216 Resources: args.Resources, 217 }) 218 return errors.Trace(err) 219 } 220 221 // ApplicationSetSettingsStrings updates the settings for the given application, 222 // taking the configuration from a map of strings. 223 func ApplicationSetSettingsStrings(application Application, settings map[string]string) error { 224 ch, _, err := application.Charm() 225 if err != nil { 226 return errors.Trace(err) 227 } 228 // Parse config in a compatible way (see function comment). 229 changes, err := parseSettingsCompatible(ch.Config(), settings) 230 if err != nil { 231 return errors.Trace(err) 232 } 233 return application.UpdateConfigSettings(changes) 234 } 235 236 // parseSettingsCompatible parses setting strings in a way that is 237 // compatible with the behavior before this CL based on the issue 238 // http://pad.lv/1194945. Until then setting an option to an empty 239 // string caused it to reset to the default value. We now allow 240 // empty strings as actual values, but we want to preserve the API 241 // behavior. 242 func parseSettingsCompatible(charmConfig *charm.Config, settings map[string]string) (charm.Settings, error) { 243 setSettings := map[string]string{} 244 unsetSettings := charm.Settings{} 245 // Split settings into those which set and those which unset a value. 246 for name, value := range settings { 247 if value == "" { 248 unsetSettings[name] = nil 249 continue 250 } 251 setSettings[name] = value 252 } 253 // Validate the settings. 254 changes, err := charmConfig.ParseSettingsStrings(setSettings) 255 if err != nil { 256 return nil, errors.Trace(err) 257 } 258 // Validate the unsettings and merge them into the changes. 259 unsetSettings, err = charmConfig.ValidateSettings(unsetSettings) 260 if err != nil { 261 return nil, errors.Trace(err) 262 } 263 for name := range unsetSettings { 264 changes[name] = nil 265 } 266 return changes, nil 267 } 268 269 // Update updates the application attributes, including charm URL, 270 // minimum number of units, settings and constraints. 271 // All parameters in params.ApplicationUpdate except the application name are optional. 272 func (api *API) Update(args params.ApplicationUpdate) error { 273 if err := api.checkCanWrite(); err != nil { 274 return err 275 } 276 if !args.ForceCharmURL { 277 if err := api.check.ChangeAllowed(); err != nil { 278 return errors.Trace(err) 279 } 280 } 281 app, err := api.backend.Application(args.ApplicationName) 282 if err != nil { 283 return errors.Trace(err) 284 } 285 // Set the charm for the given application. 286 if args.CharmURL != "" { 287 // For now we do not support changing the channel through Update(). 288 // TODO(ericsnow) Support it? 289 channel := app.Channel() 290 if err = api.applicationSetCharm( 291 args.ApplicationName, 292 app, 293 args.CharmURL, 294 channel, 295 nil, // charm settings (strings map) 296 "", // charm settings (YAML) 297 args.ForceSeries, 298 args.ForceCharmURL, 299 nil, // resource IDs 300 nil, // storage constraints 301 ); err != nil { 302 return errors.Trace(err) 303 } 304 } 305 // Set the minimum number of units for the given application. 306 if args.MinUnits != nil { 307 if err = app.SetMinUnits(*args.MinUnits); err != nil { 308 return errors.Trace(err) 309 } 310 } 311 // Set up application's settings. 312 if args.SettingsYAML != "" { 313 if err = applicationSetSettingsYAML(args.ApplicationName, app, args.SettingsYAML); err != nil { 314 return errors.Annotate(err, "setting configuration from YAML") 315 } 316 } else if len(args.SettingsStrings) > 0 { 317 if err = ApplicationSetSettingsStrings(app, args.SettingsStrings); err != nil { 318 return errors.Trace(err) 319 } 320 } 321 // Update application's constraints. 322 if args.Constraints != nil { 323 return app.SetConstraints(*args.Constraints) 324 } 325 return nil 326 } 327 328 // SetCharm sets the charm for a given for the application. 329 func (api *API) SetCharm(args params.ApplicationSetCharm) error { 330 if err := api.checkCanWrite(); err != nil { 331 return err 332 } 333 // when forced units in error, don't block 334 if !args.ForceUnits { 335 if err := api.check.ChangeAllowed(); err != nil { 336 return errors.Trace(err) 337 } 338 } 339 application, err := api.backend.Application(args.ApplicationName) 340 if err != nil { 341 return errors.Trace(err) 342 } 343 channel := csparams.Channel(args.Channel) 344 return api.applicationSetCharm( 345 args.ApplicationName, 346 application, 347 args.CharmURL, 348 channel, 349 args.ConfigSettings, 350 args.ConfigSettingsYAML, 351 args.ForceSeries, 352 args.ForceUnits, 353 args.ResourceIDs, 354 args.StorageConstraints, 355 ) 356 } 357 358 // applicationSetCharm sets the charm for the given for the application. 359 func (api *API) applicationSetCharm( 360 appName string, 361 application Application, 362 url string, 363 channel csparams.Channel, 364 configSettingsStrings map[string]string, 365 configSettingsYAML string, 366 forceSeries, 367 forceUnits bool, 368 resourceIDs map[string]string, 369 storageConstraints map[string]params.StorageConstraints, 370 ) error { 371 curl, err := charm.ParseURL(url) 372 if err != nil { 373 return errors.Trace(err) 374 } 375 sch, err := api.backend.Charm(curl) 376 if err != nil { 377 return errors.Trace(err) 378 } 379 var settings charm.Settings 380 if configSettingsYAML != "" { 381 settings, err = sch.Config().ParseSettingsYAML([]byte(configSettingsYAML), appName) 382 } else if len(configSettingsStrings) > 0 { 383 settings, err = parseSettingsCompatible(sch.Config(), configSettingsStrings) 384 } 385 if err != nil { 386 return errors.Annotate(err, "parsing config settings") 387 } 388 var stateStorageConstraints map[string]state.StorageConstraints 389 if len(storageConstraints) > 0 { 390 stateStorageConstraints = make(map[string]state.StorageConstraints) 391 for name, cons := range storageConstraints { 392 stateCons := state.StorageConstraints{Pool: cons.Pool} 393 if cons.Size != nil { 394 stateCons.Size = *cons.Size 395 } 396 if cons.Count != nil { 397 stateCons.Count = *cons.Count 398 } 399 stateStorageConstraints[name] = stateCons 400 } 401 } 402 cfg := state.SetCharmConfig{ 403 Charm: api.stateCharm(sch), 404 Channel: channel, 405 ConfigSettings: settings, 406 ForceSeries: forceSeries, 407 ForceUnits: forceUnits, 408 ResourceIDs: resourceIDs, 409 StorageConstraints: stateStorageConstraints, 410 } 411 return application.SetCharm(cfg) 412 } 413 414 // settingsYamlFromGetYaml will parse a yaml produced by juju get and generate 415 // charm.Settings from it that can then be sent to the application. 416 func settingsFromGetYaml(yamlContents map[string]interface{}) (charm.Settings, error) { 417 onlySettings := charm.Settings{} 418 settingsMap, ok := yamlContents["settings"].(map[interface{}]interface{}) 419 if !ok { 420 return nil, errors.New("unknown format for settings") 421 } 422 423 for setting := range settingsMap { 424 s, ok := settingsMap[setting].(map[interface{}]interface{}) 425 if !ok { 426 return nil, errors.Errorf("unknown format for settings section %v", setting) 427 } 428 // some keys might not have a value, we don't care about those. 429 v, ok := s["value"] 430 if !ok { 431 continue 432 } 433 stringSetting, ok := setting.(string) 434 if !ok { 435 return nil, errors.Errorf("unexpected setting key, expected string got %T", setting) 436 } 437 onlySettings[stringSetting] = v 438 } 439 return onlySettings, nil 440 } 441 442 // applicationSetSettingsYAML updates the settings for the given application, 443 // taking the configuration from a YAML string. 444 func applicationSetSettingsYAML(appName string, application Application, settings string) error { 445 b := []byte(settings) 446 var all map[string]interface{} 447 if err := goyaml.Unmarshal(b, &all); err != nil { 448 return errors.Annotate(err, "parsing settings data") 449 } 450 // The file is already in the right format. 451 if _, ok := all[appName]; !ok { 452 changes, err := settingsFromGetYaml(all) 453 if err != nil { 454 return errors.Annotate(err, "processing YAML generated by get") 455 } 456 return errors.Annotate(application.UpdateConfigSettings(changes), "updating settings with application YAML") 457 } 458 459 ch, _, err := application.Charm() 460 if err != nil { 461 return errors.Annotate(err, "obtaining charm for this application") 462 } 463 464 changes, err := ch.Config().ParseSettingsYAML(b, appName) 465 if err != nil { 466 return errors.Annotate(err, "creating config from YAML") 467 } 468 return errors.Annotate(application.UpdateConfigSettings(changes), "updating settings") 469 } 470 471 // GetCharmURL returns the charm URL the given application is 472 // running at present. 473 func (api *API) GetCharmURL(args params.ApplicationGet) (params.StringResult, error) { 474 if err := api.checkCanWrite(); err != nil { 475 return params.StringResult{}, errors.Trace(err) 476 } 477 application, err := api.backend.Application(args.ApplicationName) 478 if err != nil { 479 return params.StringResult{}, errors.Trace(err) 480 } 481 charmURL, _ := application.CharmURL() 482 return params.StringResult{Result: charmURL.String()}, nil 483 } 484 485 // Set implements the server side of Application.Set. 486 // It does not unset values that are set to an empty string. 487 // Unset should be used for that. 488 func (api *API) Set(p params.ApplicationSet) error { 489 if err := api.checkCanWrite(); err != nil { 490 return err 491 } 492 if err := api.check.ChangeAllowed(); err != nil { 493 return errors.Trace(err) 494 } 495 app, err := api.backend.Application(p.ApplicationName) 496 if err != nil { 497 return err 498 } 499 ch, _, err := app.Charm() 500 if err != nil { 501 return err 502 } 503 // Validate the settings. 504 changes, err := ch.Config().ParseSettingsStrings(p.Options) 505 if err != nil { 506 return err 507 } 508 509 return app.UpdateConfigSettings(changes) 510 511 } 512 513 // Unset implements the server side of Client.Unset. 514 func (api *API) Unset(p params.ApplicationUnset) error { 515 if err := api.checkCanWrite(); err != nil { 516 return err 517 } 518 if err := api.check.ChangeAllowed(); err != nil { 519 return errors.Trace(err) 520 } 521 app, err := api.backend.Application(p.ApplicationName) 522 if err != nil { 523 return err 524 } 525 settings := make(charm.Settings) 526 for _, option := range p.Options { 527 settings[option] = nil 528 } 529 return app.UpdateConfigSettings(settings) 530 } 531 532 // CharmRelations implements the server side of Application.CharmRelations. 533 func (api *API) CharmRelations(p params.ApplicationCharmRelations) (params.ApplicationCharmRelationsResults, error) { 534 var results params.ApplicationCharmRelationsResults 535 if err := api.checkCanRead(); err != nil { 536 return results, errors.Trace(err) 537 } 538 539 application, err := api.backend.Application(p.ApplicationName) 540 if err != nil { 541 return results, errors.Trace(err) 542 } 543 endpoints, err := application.Endpoints() 544 if err != nil { 545 return results, errors.Trace(err) 546 } 547 results.CharmRelations = make([]string, len(endpoints)) 548 for i, endpoint := range endpoints { 549 results.CharmRelations[i] = endpoint.Relation.Name 550 } 551 return results, nil 552 } 553 554 // Expose changes the juju-managed firewall to expose any ports that 555 // were also explicitly marked by units as open. 556 func (api *API) Expose(args params.ApplicationExpose) error { 557 if err := api.checkCanWrite(); err != nil { 558 return err 559 } 560 if err := api.check.ChangeAllowed(); err != nil { 561 return errors.Trace(err) 562 } 563 app, err := api.backend.Application(args.ApplicationName) 564 if err != nil { 565 return err 566 } 567 return app.SetExposed() 568 } 569 570 // Unexpose changes the juju-managed firewall to unexpose any ports that 571 // were also explicitly marked by units as open. 572 func (api *API) Unexpose(args params.ApplicationUnexpose) error { 573 if err := api.checkCanWrite(); err != nil { 574 return err 575 } 576 if err := api.check.ChangeAllowed(); err != nil { 577 return errors.Trace(err) 578 } 579 app, err := api.backend.Application(args.ApplicationName) 580 if err != nil { 581 return err 582 } 583 return app.ClearExposed() 584 } 585 586 // addApplicationUnits adds a given number of units to an application. 587 func addApplicationUnits(backend Backend, args params.AddApplicationUnits) ([]*state.Unit, error) { 588 application, err := backend.Application(args.ApplicationName) 589 if err != nil { 590 return nil, errors.Trace(err) 591 } 592 if args.NumUnits < 1 { 593 return nil, errors.New("must add at least one unit") 594 } 595 return jjj.AddUnits(backend, application, args.ApplicationName, args.NumUnits, args.Placement) 596 } 597 598 // AddUnits adds a given number of units to an application. 599 func (api *API) AddUnits(args params.AddApplicationUnits) (params.AddApplicationUnitsResults, error) { 600 if err := api.checkCanWrite(); err != nil { 601 return params.AddApplicationUnitsResults{}, errors.Trace(err) 602 } 603 if err := api.check.ChangeAllowed(); err != nil { 604 return params.AddApplicationUnitsResults{}, errors.Trace(err) 605 } 606 units, err := addApplicationUnits(api.backend, args) 607 if err != nil { 608 return params.AddApplicationUnitsResults{}, errors.Trace(err) 609 } 610 unitNames := make([]string, len(units)) 611 for i, unit := range units { 612 unitNames[i] = unit.String() 613 } 614 return params.AddApplicationUnitsResults{Units: unitNames}, nil 615 } 616 617 // DestroyUnits removes a given set of application units. 618 func (api *API) DestroyUnits(args params.DestroyApplicationUnits) error { 619 if err := api.checkCanWrite(); err != nil { 620 return err 621 } 622 if err := api.check.RemoveAllowed(); err != nil { 623 return errors.Trace(err) 624 } 625 var errs []string 626 for _, name := range args.UnitNames { 627 unit, err := api.backend.Unit(name) 628 switch { 629 case errors.IsNotFound(err): 630 err = errors.Errorf("unit %q does not exist", name) 631 case err != nil: 632 case unit.Life() != state.Alive: 633 continue 634 case unit.IsPrincipal(): 635 err = unit.Destroy() 636 default: 637 err = errors.Errorf("unit %q is a subordinate", name) 638 } 639 if err != nil { 640 errs = append(errs, err.Error()) 641 } 642 } 643 return common.DestroyErr("units", args.UnitNames, errs) 644 } 645 646 // Destroy destroys a given application. 647 func (api *API) Destroy(args params.ApplicationDestroy) error { 648 if err := api.checkCanWrite(); err != nil { 649 return err 650 } 651 if err := api.check.RemoveAllowed(); err != nil { 652 return errors.Trace(err) 653 } 654 app, err := api.backend.Application(args.ApplicationName) 655 if err != nil { 656 return err 657 } 658 return app.Destroy() 659 } 660 661 // GetConstraints returns the constraints for a given application. 662 func (api *API) GetConstraints(args params.GetApplicationConstraints) (params.GetConstraintsResults, error) { 663 if err := api.checkCanRead(); err != nil { 664 return params.GetConstraintsResults{}, errors.Trace(err) 665 } 666 app, err := api.backend.Application(args.ApplicationName) 667 if err != nil { 668 return params.GetConstraintsResults{}, errors.Trace(err) 669 } 670 cons, err := app.Constraints() 671 return params.GetConstraintsResults{cons}, errors.Trace(err) 672 } 673 674 // SetConstraints sets the constraints for a given application. 675 func (api *API) SetConstraints(args params.SetConstraints) error { 676 if err := api.checkCanWrite(); err != nil { 677 return err 678 } 679 if err := api.check.ChangeAllowed(); err != nil { 680 return errors.Trace(err) 681 } 682 app, err := api.backend.Application(args.ApplicationName) 683 if err != nil { 684 return err 685 } 686 return app.SetConstraints(args.Constraints) 687 } 688 689 // AddRelation adds a relation between the specified endpoints and returns the relation info. 690 func (api *API) AddRelation(args params.AddRelation) (params.AddRelationResults, error) { 691 if err := api.checkCanWrite(); err != nil { 692 return params.AddRelationResults{}, errors.Trace(err) 693 } 694 if err := api.check.ChangeAllowed(); err != nil { 695 return params.AddRelationResults{}, errors.Trace(err) 696 } 697 inEps, err := api.backend.InferEndpoints(args.Endpoints...) 698 if err != nil { 699 return params.AddRelationResults{}, errors.Trace(err) 700 } 701 rel, err := api.backend.AddRelation(inEps...) 702 if err != nil { 703 return params.AddRelationResults{}, errors.Trace(err) 704 } 705 outEps := make(map[string]params.CharmRelation) 706 for _, inEp := range inEps { 707 outEp, err := rel.Endpoint(inEp.ApplicationName) 708 if err != nil { 709 return params.AddRelationResults{}, errors.Trace(err) 710 } 711 outEps[inEp.ApplicationName] = params.CharmRelation{ 712 Name: outEp.Relation.Name, 713 Role: string(outEp.Relation.Role), 714 Interface: outEp.Relation.Interface, 715 Optional: outEp.Relation.Optional, 716 Limit: outEp.Relation.Limit, 717 Scope: string(outEp.Relation.Scope), 718 } 719 } 720 return params.AddRelationResults{Endpoints: outEps}, nil 721 } 722 723 // DestroyRelation removes the relation between the specified endpoints. 724 func (api *API) DestroyRelation(args params.DestroyRelation) error { 725 if err := api.checkCanWrite(); err != nil { 726 return err 727 } 728 if err := api.check.RemoveAllowed(); err != nil { 729 return errors.Trace(err) 730 } 731 eps, err := api.backend.InferEndpoints(args.Endpoints...) 732 if err != nil { 733 return err 734 } 735 rel, err := api.backend.EndpointsRelation(eps...) 736 if err != nil { 737 return err 738 } 739 return rel.Destroy() 740 }